├── .eslintignore ├── .gitignore ├── CHANGELOG_de-DE.md ├── CHANGELOG_en-GB.md ├── LICENSE ├── README.md ├── composer.json └── src ├── Api └── MacThemePreviewController.php ├── Framework └── Cookie │ └── CustomCookieProvider.php ├── MacThemePreview.php ├── Resources ├── app │ ├── administration │ │ └── src │ │ │ ├── main.js │ │ │ ├── modules │ │ │ └── sw-theme-manager │ │ │ │ ├── component │ │ │ │ └── sw-theme-modal │ │ │ │ │ ├── index.js │ │ │ │ │ └── sw-theme-modal.html.twig │ │ │ │ └── snippet │ │ │ │ ├── de-DE.json │ │ │ │ └── en-GB.json │ │ │ └── service │ │ │ └── mac-theme-preview.api.service.js │ └── storefront │ │ ├── dist │ │ └── storefront │ │ │ └── js │ │ │ └── mac-theme-preview.js │ │ └── src │ │ ├── main.js │ │ └── utility │ │ └── preview-theme │ │ └── preview-theme.util.js ├── config │ ├── routes.xml │ └── services.xml ├── public │ └── administration │ │ └── js │ │ └── mac-theme-preview.js └── snippet │ └── en_GB │ ├── SnippetFile_en_GB.php │ └── storefront.en-GB.json ├── Service └── MacThemePreviewService.php └── Subscriber └── MacThemePreviewSubscriber.php /.eslintignore: -------------------------------------------------------------------------------- 1 | src/Resources/public/administration/js/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .phpunit.result.cache 3 | phpstan.neon 4 | -------------------------------------------------------------------------------- /CHANGELOG_de-DE.md: -------------------------------------------------------------------------------- 1 | # 1.0.0 2 | 3 | * Button Copy Link & Share Theme hinzugefügt 4 | * Behobener Fehler kann keine Vorschau anderer Seiten außer der Homepage anzeigen 5 | 6 | # 0.30.4 7 | 8 | * Erste Veröffentlichung mit Feature-Vorschau-Thema im Detail des Vertriebskanals 9 | -------------------------------------------------------------------------------- /CHANGELOG_en-GB.md: -------------------------------------------------------------------------------- 1 | # 1.0.0 2 | 3 | * Added button copy link & share theme 4 | * Fixed bug can't preview other pages except the homepage 5 | 6 | # 0.30.4 7 | 8 | * First release with feature preview theme in sales channel detail 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Hung Mac 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Theme preview for Shopware 6 2 | 3 | [![Gitter](https://badges.gitter.im/mac/community.svg)](https://gitter.im/mac/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 4 | 5 | This plugin can support to preview a theme before you click to the button save. You can also share the link of the preview to someone 6 | 7 | [![](https://media.giphy.com/media/h4NFhO9hrtHpifxpS6/giphy.gif)](https://media.giphy.com/media/h4NFhO9hrtHpifxpS6/giphy.gif) 8 | ## Features 9 | 10 | * [x] Preview theme in storefront sales channel. 11 | * [ ] Preview theme before save in `Admin > content > Themes > Theme detail. 12 | 13 | ## Installation 14 | 15 | * Download latest release 16 | * Extract the zip file in `shopware_folder/custom/plugins/` 17 | 18 | ## Contributing 19 | 20 | Feel free to fork and send pull requests! 21 | 22 | ## Licence 23 | 24 | This project uses the [MIT License](LICENCE). -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mac/theme-preview", 3 | "description": "Theme preview plugin for Shopware 6", 4 | "version": "1.0.0", 5 | "type": "shopware-platform-plugin", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Hung Mac" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "MacThemePreview\\": "src/" 15 | } 16 | }, 17 | "extra": { 18 | "shopware-plugin-class": "MacThemePreview\\MacThemePreview", 19 | "label": { 20 | "de-DE": "Theme Preview Plugin für Shopware 6", 21 | "en-GB": "Theme Preview Plugin for Shopware 6" 22 | } 23 | }, 24 | "suggest": { 25 | "shopware/storefront": "Require '*'. Enables special feature for custom theme id", 26 | "shopware/administration": "Require '*'. Allows you to to set up the plugin easily" 27 | }, 28 | "conflict": { 29 | "shopware/storefront": "<6,>=7", 30 | "shopware/administration": "<6,>=7" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Api/MacThemePreviewController.php: -------------------------------------------------------------------------------- 1 | macThemePreviewService = $macThemePreviewService; 26 | } 27 | 28 | /** 29 | * @Route("/api/v{version}/_action/mac-theme-preview/compile", name="api.action.mac_theme_preview.compile", methods={"POST"}) 30 | * @throws \League\Flysystem\FileNotFoundException 31 | */ 32 | public function themeCompile(RequestDataBag $dataBag, Context $context): Response 33 | { 34 | $salesChannelId = $dataBag->get('sales_channel_id'); 35 | $themeId = $dataBag->get('theme_id'); 36 | 37 | $this->macThemePreviewService->themeCompile($context, $salesChannelId, $themeId); 38 | 39 | return new Response(null, Response::HTTP_NO_CONTENT); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Framework/Cookie/CustomCookieProvider.php: -------------------------------------------------------------------------------- 1 | originalService = $service; 14 | } 15 | 16 | public function getCookieGroups(): array 17 | { 18 | return array_merge( 19 | $this->originalService->getCookieGroups(), 20 | [ 21 | [ 22 | 'snippet_name' => 'cookie.theme', 23 | 'cookie' => 'preview-theme-id', 24 | ] 25 | ] 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/MacThemePreview.php: -------------------------------------------------------------------------------- 1 | { 9 | const initContainer = Shopware.Application.getContainer('init'); 10 | return new MacThemePreviewService(initContainer.httpClient, Shopware.Service('loginService')); 11 | }); 12 | -------------------------------------------------------------------------------- /src/Resources/app/administration/src/modules/sw-theme-manager/component/sw-theme-modal/index.js: -------------------------------------------------------------------------------- 1 | import template from './sw-theme-modal.html.twig'; 2 | 3 | const { Component, Mixin } = Shopware; 4 | const { Criteria } = Shopware.Data; 5 | const domUtils = Shopware.Utils.dom; 6 | 7 | Component.override('sw-theme-modal', { 8 | template, 9 | 10 | mixins: [ 11 | Mixin.getByName('notification') 12 | ], 13 | 14 | inject: [ 15 | 'macThemePreviewService' 16 | ], 17 | 18 | data() { 19 | return { 20 | selected: null, 21 | themes: [], 22 | isLoadingPreview: false, 23 | isLoadingCopy: false 24 | }; 25 | }, 26 | 27 | computed: { 28 | salesChannelRepository() { 29 | return this.repositoryFactory.create('sales_channel'); 30 | } 31 | }, 32 | 33 | created() { 34 | this.createdComponent(); 35 | }, 36 | 37 | methods: { 38 | createdComponent() { 39 | this.loadEntityData(); 40 | }, 41 | 42 | loadEntityData() { 43 | if (!this.$route.params.id) { 44 | return; 45 | } 46 | 47 | this.loadSalesChannel(); 48 | }, 49 | 50 | loadSalesChannel() { 51 | this.isLoadingPreview = true; 52 | const criteria = new Criteria(); 53 | criteria.addAssociation('domains'); 54 | 55 | this.salesChannelRepository 56 | .get(this.$route.params.id, Shopware.Context.api, criteria) 57 | .then((entity) => { 58 | this.salesChannel = entity; 59 | this.isLoadingPreview = false; 60 | }); 61 | }, 62 | 63 | selectPreview() { 64 | if (this.checkConditions()) { 65 | this.isLoadingPreview = true; 66 | this.macThemePreviewService.themeCompile(this.salesChannel.id, this.selected).then(() => { 67 | this.isLoadingPreview = false; 68 | window.open(this.renderPreviewUrl()); 69 | }); 70 | } 71 | }, 72 | 73 | copyToClipboard() { 74 | if (this.checkConditions()) { 75 | this.isLoadingCopy = true; 76 | this.macThemePreviewService.themeCompile(this.salesChannel.id, this.selected).then(() => { 77 | this.isLoadingCopy = false; 78 | domUtils.copyToClipboard(this.renderPreviewUrl()); 79 | this.createNotificationInfo({ 80 | title: this.$tc('global.default.info'), 81 | message: this.$tc('mac-theme-preview.notification.notificationCopyLinkSuccess') 82 | }); 83 | }); 84 | } 85 | }, 86 | 87 | renderPreviewUrl() { 88 | return this.salesChannel.domains.first().url + '/?preview-theme-id=' + this.selected; 89 | }, 90 | 91 | checkConditions() { 92 | const theme = this.themes.find((theme) => theme.id === this.selected); 93 | if (!theme) { 94 | this.createNotificationError({ 95 | title: this.$tc('global.default.error'), 96 | message: this.$tc('global.notification.unspecifiedSaveErrorMessage') 97 | }); 98 | return false; 99 | } 100 | 101 | if (!this.salesChannel.domains.first()) { 102 | this.createNotificationError({ 103 | title: this.$tc('global.default.error'), 104 | message: this.$tc('mac-theme-preview.notification.notificationEmptyDomain') 105 | }); 106 | return false; 107 | } 108 | 109 | return true; 110 | } 111 | } 112 | }); 113 | -------------------------------------------------------------------------------- /src/Resources/app/administration/src/modules/sw-theme-manager/component/sw-theme-modal/sw-theme-modal.html.twig: -------------------------------------------------------------------------------- 1 | {% block sw_theme_modal_footer %} 2 | 11 | 12 | {% parent %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /src/Resources/app/administration/src/modules/sw-theme-manager/snippet/de-DE.json: -------------------------------------------------------------------------------- 1 | { 2 | "mac-theme-preview": { 3 | "previewButton": "Vorschau", 4 | "buttonCopyLink": "Kopieren & teilen", 5 | "notificationCopyLinkSuccess": "URL in Zwischenablage kopiert.", 6 | "notificationEmptyDomain": "Dieser Vertriebskanal hat keine Domain." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Resources/app/administration/src/modules/sw-theme-manager/snippet/en-GB.json: -------------------------------------------------------------------------------- 1 | { 2 | "mac-theme-preview": { 3 | "previewButton": "Preview", 4 | "buttonCopyLink": "Copy & Share", 5 | "notification": { 6 | "notificationCopyLinkSuccess": "URL has been copied to clipboard.", 7 | "notificationEmptyDomain": "This sales channel have no domain." 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Resources/app/administration/src/service/mac-theme-preview.api.service.js: -------------------------------------------------------------------------------- 1 | import ApiService 2 | from 'src/core/service/api.service'; 3 | 4 | class MacThemePreviewService extends ApiService { 5 | constructor(httpClient, loginService, apiEndpoint = 'mac-theme-preview') { 6 | super(httpClient, loginService, apiEndpoint); 7 | this.name = 'MacThemePreviewService'; 8 | } 9 | 10 | themeCompile(salesChannelId, themeId) { 11 | const apiRoute = `/_action/${this.getApiBasePath()}/compile`; 12 | 13 | return this.httpClient.post( 14 | apiRoute, { 15 | sales_channel_id: salesChannelId, 16 | theme_id: themeId, 17 | }, 18 | { headers: this.getBasicHeaders() } 19 | ).then((response) => { 20 | return ApiService.handleResponse(response); 21 | }); 22 | } 23 | 24 | } 25 | 26 | export default MacThemePreviewService; 27 | -------------------------------------------------------------------------------- /src/Resources/app/storefront/dist/storefront/js/mac-theme-preview.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([["mac-theme-preview"],{"4owp":function(e,t,n){"use strict";n.r(t);var o=n("t8WJ"),i=n("prSB");function r(e,t){for(var n=0;n0}},{key:"_openOffcanvas",value:function(e,t){setTimeout((function(){r.c.create((function(){e.classList.add("is-open"),"function"==typeof t&&t()}))}),75)}},{key:"_registerEvents",value:function(e,t){var n=this,i=o.a.isTouchDevice()?"touchstart":"click";if(e){document.addEventListener(r.a.ON_CLICK,(function e(){n.close(t),document.removeEventListener(r.a.ON_CLICK,e)}))}var c=document.querySelectorAll(".".concat("js-offcanvas-close"));a.a.iterate(c,(function(e){return e.addEventListener(i,n.close.bind(n,t))}))}},{key:"_removeExistingOffCanvas",value:function(){var e=this.getOffCanvas();return a.a.iterate(e,(function(e){return e.remove()}))}},{key:"_getPositionClass",value:function(e){return"is-".concat(e)}},{key:"_createOffCanvas",value:function(e,t,n){var o=document.createElement("div");if(o.classList.add("offcanvas"),o.classList.add(this._getPositionClass(e)),!0===t&&o.classList.add("is-fullwidth"),n){var i=c(n);if("string"===i)o.classList.add(n);else{if(!Array.isArray(n))throw new Error('The type "'.concat(i,'" is not supported. Please pass an array or a string.'));n.forEach((function(e){o.classList.add(e)}))}}return document.body.appendChild(o),o}}]),e}(),v=Object.freeze(new l),h=function(){function e(){s(this,e)}return f(e,null,[{key:"open",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"left",o=!(arguments.length>3&&void 0!==arguments[3])||arguments[3],i=arguments.length>4&&void 0!==arguments[4]?arguments[4]:350,r=arguments.length>5&&void 0!==arguments[5]&&arguments[5],a=arguments.length>6&&void 0!==arguments[6]?arguments[6]:"";v.open(e,t,n,o,i,r,a)}},{key:"setContent",value:function(e){var t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:350;v.setContent(e,t,n)}},{key:"setAdditionalClassName",value:function(e){v.setAdditionalClassName(e)}},{key:"close",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:350;v.close(e)}},{key:"exists",value:function(){return v.exists()}},{key:"getOffCanvas",value:function(){return v.getOffCanvas()}},{key:"REMOVE_OFF_CANVAS_DELAY",value:function(){return 350}}]),e}()},lpb5:function(e,t,n){"use strict";n.d(t,"a",(function(){return h}));var o=n("bK22"),i=n("k8s9"),r=n("5lm9");function a(e){return(a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function c(e,t){for(var n=0;n0&&void 0!==arguments[0]&&arguments[0],t=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null,i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"left",r=!(arguments.length>4&&void 0!==arguments[4])||arguments[4],a=arguments.length>5&&void 0!==arguments[5]?arguments[5]:o.b.REMOVE_OFF_CANVAS_DELAY(),c=arguments.length>6&&void 0!==arguments[6]&&arguments[6],s=arguments.length>7&&void 0!==arguments[7]?arguments[7]:"";if(!e)throw new Error("A url must be given!");o.a._removeExistingOffCanvas();var u=o.a._createOffCanvas(i,c,s);this.setContent(e,t,n,r,a),o.a._openOffcanvas(u)}},{key:"setContent",value:function(e,n,o,a,c){var s=this,l=new i.a(window.accessKey,window.contextToken);u(f(t),"setContent",this).call(this,'
'.concat(r.a.getTemplate(),"
"),a,c),v&&v.abort();var h=function(e){u(f(t),"setContent",s).call(s,e,a,c),"function"==typeof o&&o(e)};v=n?l.post(e,n,t.executeCallback.bind(this,h)):l.get(e,t.executeCallback.bind(this,h))}},{key:"executeCallback",value:function(e,t){"function"==typeof e&&e(t),window.PluginManager.initializePlugins()}}],(a=null)&&c(n.prototype,a),h&&c(n,h),t}(o.b)},t8WJ:function(e,t,n){"use strict";n.d(t,"a",(function(){return b})),n.d(t,"b",(function(){return g}));var o=n("FGIj"),i=n("prSB"),r=n("41MI"),a=n("lpb5"),c=n("bK22"),s=n("DeZd");function u(e){return(u="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function f(e,t){for(var n=0;n2&&void 0!==arguments[2]?arguments[2]:null;e&&!e.classList.contains(n);){if(e.classList.contains(t))return e;e=e.parentElement}return null}},{key:"_isChecked",value:function(e){return!!e.checked}},{key:"_parentCheckboxEvent",value:function(e){var t=this.options.groupClass,n=this._isChecked(e),o=this._findParentEl(e,t);this._toggleWholeGroup(n,o)}},{key:"_childCheckboxEvent",value:function(e){var t=this.options.groupClass,n=this._isChecked(e),o=this._findParentEl(e,t);this._toggleParentCheckbox(n,o)}},{key:"_toggleWholeGroup",value:function(e,t){Array.from(t.querySelectorAll("input")).forEach((function(t){t.checked=e}))}},{key:"_toggleParentCheckbox",value:function(e,t){var n=this.options.parentInputSelector,o=Array.from(t.querySelectorAll("input:not(".concat(n,")"))),i=Array.from(t.querySelectorAll("input:not(".concat(n,"):checked")));if(o.length>0){var r=t.querySelector(n);if(r){var a=i.length>0,c=a&&i.length!==o.length;r.checked=a,r.indeterminate=c}}}},{key:"_handleSubmit",value:function(){var e=this._getCookies("active"),t=this._getCookies("inactive"),n=this.options.cookiePreference,o=[],r=[];t.forEach((function(e){var t=e.cookie;r.push(t),i.a.getItem(t)&&i.a.removeItem(t)})),e.forEach((function(e){var t=e.cookie,n=e.value,r=e.expiration;o.push(t),t&&n&&i.a.setItem(t,n,r)})),i.a.setItem(n,"1","30"),this._handleUpdateListener(o,r),this.closeOffCanvas()}},{key:"_getCookies",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"all",n=this.options.cookieSelector,o=this._getOffCanvas();return Array.from(o.querySelectorAll(n)).filter((function(n){switch(t){case"all":return!0;case"active":return e._isChecked(n);case"inactive":return!e._isChecked(n);default:return!1}})).map((function(e){var t=e.dataset;return{cookie:t.cookie,value:t.cookieValue,expiration:t.cookieExpiration,required:t.cookieRequired}}))}},{key:"_getOffCanvas",value:function(){var e=c.b?c.b.getOffCanvas():[];return!!(e&&e.length>0)&&e[0]}}])&&f(n.prototype,o),r&&f(n,r),t}(o.a);p=g,d="options",y={offCanvasPosition:"left",submitEvent:r.a.isTouchDevice()?"touchstart":"click",cookiePreference:"cookie-preference",cookieSelector:"[data-cookie]",buttonOpenSelector:".js-cookie-configuration-button button",buttonSubmitSelector:".js-offcanvas-cookie-submit",wrapperToggleSelector:".offcanvas-cookie-entries span",parentInputSelector:".offcanvas-cookie-parent-input",customLinkSelector:'[href="'.concat(window.router["frontend.cookie.offcanvas"],'"]'),entriesActiveClass:"offcanvas-cookie-entries--active",entriesClass:"offcanvas-cookie-entries",groupClass:"offcanvas-cookie-group",parentInputClass:"offcanvas-cookie-parent-input"},d in p?Object.defineProperty(p,d,{value:y,enumerable:!0,configurable:!0,writable:!0}):p[d]=y}},[["4owp","runtime","vendor-node","vendor-shared"]]]); -------------------------------------------------------------------------------- /src/Resources/app/storefront/src/main.js: -------------------------------------------------------------------------------- 1 | import PreviewThemeUtil from './utility/preview-theme/preview-theme.util'; 2 | 3 | new PreviewThemeUtil(); 4 | -------------------------------------------------------------------------------- /src/Resources/app/storefront/src/utility/preview-theme/preview-theme.util.js: -------------------------------------------------------------------------------- 1 | import { COOKIE_CONFIGURATION_UPDATE } from 'src/plugin/cookie/cookie-configuration.plugin'; 2 | import CookieStorageHelper from 'src/helper/storage/cookie-storage.helper'; 3 | 4 | const PREVIEW_THEME_COOKIE = 'preview-theme-id'; 5 | 6 | export default class PreviewThemeUtil{ 7 | constructor() { 8 | this._init(); 9 | } 10 | 11 | _init() { 12 | this._includePreviewTheme(); 13 | this._registerEvents(); 14 | } 15 | 16 | _registerEvents () { 17 | document.$emitter.subscribe(COOKIE_CONFIGURATION_UPDATE, this._includePreviewTheme); 18 | } 19 | 20 | _includePreviewTheme() { 21 | if (!CookieStorageHelper.isSupported()) { 22 | return; 23 | } 24 | 25 | const queryString = window.location.search; 26 | const urlParams = new URLSearchParams(queryString); 27 | const previewThemeId = urlParams.get(PREVIEW_THEME_COOKIE); 28 | if (!previewThemeId) { 29 | return; 30 | } 31 | 32 | if (previewThemeId == this._getCookie(PREVIEW_THEME_COOKIE)) { 33 | return; 34 | } 35 | 36 | CookieStorageHelper.setItem(PREVIEW_THEME_COOKIE, previewThemeId, 30); 37 | location.reload(); 38 | } 39 | 40 | _getCookie(cname) { 41 | var name = cname + '='; 42 | var ca = document.cookie.split(';'); 43 | for(var i = 0; i < ca.length; i++) { 44 | var c = ca[i]; 45 | while (c.charAt(0) == ' ') { 46 | c = c.substring(1); 47 | } 48 | if (c.indexOf(name) == 0) { 49 | return c.substring(name.length, c.length); 50 | } 51 | } 52 | return ''; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Resources/config/routes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Resources/config/services.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Resources/public/administration/js/mac-theme-preview.js: -------------------------------------------------------------------------------- 1 | (this.webpackJsonp=this.webpackJsonp||[]).push([["mac-theme-preview"],{"/p6D":function(e,t){e.exports='{% block sw_theme_modal_footer %}\n \n\n {% parent %}\n{% endblock %}\n'},CkOj:function(e,t,n){"use strict";n.d(t,"a",(function(){return u}));var i=n("lSNA"),r=n.n(i),a=n("lO2t"),s=n("lYO9");function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);t&&(i=i.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,i)}return n}function c(e){for(var t=1;t0&&(n=c({},n,{},h(e.attributes)));if(e.relationships){var i=function(e,t){var n={},i={};return Object.keys(e).forEach((function(r){var s=e[r];if(s.links&&Object.keys(s.links).length&&(i[r]=s.links.related),s.data){var o=s.data;a.a.isArray(o)?n[r]=o.map((function(e){return d(e,t)})):a.a.isObject(o)?n[r]=d(o,t):n[r]=null}})),{mappedRelations:n,associationLinks:i}}(e.relationships,t);n=c({},n,{},i.mappedRelations,{},{associationLinks:i.associationLinks})}return n}function h(e){var t={};return Object.keys(e).forEach((function(n){var i=e[n],r=n.replace(/-([a-z])/g,(function(e,t){return t.toUpperCase()}));t[r]=i})),t}function d(e,t){var n="".concat(e.type,"-").concat(e.id);return t.has(n)?l(t.get(n),t):e}},SwLI:function(e,t,n){"use strict";n.r(t);var i=n("lwsE"),r=n.n(i),a=n("W8MJ"),s=n.n(a),o=n("CkOj"),c=function(){function e(t,n,i){var a=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"application/vnd.api+json";r()(this,e),this.httpClient=t,this.loginService=n,this.apiEndpoint=i,this.contentType=a}return s()(e,[{key:"getList",value:function(t){var n=t.page,i=void 0===n?1:n,r=t.limit,a=void 0===r?25:r,s=t.sortBy,o=t.sortDirection,c=void 0===o?"asc":o,u=t.sortings,l=t.queries,h=t.term,d=t.criteria,p=t.aggregations,f=t.associations,g=t.headers,v=t.versionId,m=t.ids,y=this.getBasicHeaders(g),O={page:i,limit:a};return u?O.sort=u:s&&s.length&&(O.sort=("asc"===c.toLowerCase()?"":"-")+s),m&&(O.ids=m.join("|")),h&&(O.term=h),d&&(O.filter=[d.getQuery()]),p&&(O.aggregations=p),f&&(O.associations=f),v&&(y=Object.assign(y,e.getVersionHeader(v))),l&&(O.query=l),O.term&&O.term.length||O.filter&&O.filter.length||O.aggregations||O.sort||O.queries||O.associations?this.httpClient.post("".concat(this.getApiBasePath(null,"search")),O,{headers:y}).then((function(t){return e.handleResponse(t)})):this.httpClient.get(this.getApiBasePath(),{params:O,headers:y}).then((function(t){return e.handleResponse(t)}))}},{key:"getById",value:function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(!t)return Promise.reject(new Error("Missing required argument: id"));var r=n,a=this.getBasicHeaders(i);return this.httpClient.get(this.getApiBasePath(t),{params:r,headers:a}).then((function(t){return e.handleResponse(t)}))}},{key:"updateById",value:function(t,n){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};if(!t)return Promise.reject(new Error("Missing required argument: id"));var a=i,s=this.getBasicHeaders(r);return this.httpClient.patch(this.getApiBasePath(t),n,{params:a,headers:s}).then((function(t){return e.handleResponse(t)}))}},{key:"deleteAssociation",value:function(e,t,n,i){if(!e||!n||!n)return Promise.reject(new Error("Missing required arguments."));var r=this.getBasicHeaders(i);return this.httpClient.delete("".concat(this.getApiBasePath(e),"/").concat(t,"/").concat(n),{headers:r}).then((function(e){return e.status>=200&&e.status<300?Promise.resolve(e):Promise.reject(e)}))}},{key:"create",value:function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n,a=this.getBasicHeaders(i);return this.httpClient.post(this.getApiBasePath(),t,{params:r,headers:a}).then((function(t){return e.handleResponse(t)}))}},{key:"delete",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(!e)return Promise.reject(new Error("Missing required argument: id"));var i=Object.assign({},t),r=this.getBasicHeaders(n);return this.httpClient.delete(this.getApiBasePath(e),{params:i,headers:r})}},{key:"clone",value:function(t){return t?this.httpClient.post("/_action/clone/".concat(this.apiEndpoint,"/").concat(t),null,{headers:this.getBasicHeaders()}).then((function(t){return e.handleResponse(t)})):Promise.reject(new Error("Missing required argument: id"))}},{key:"versionize",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},i="/_action/version/".concat(this.apiEndpoint,"/").concat(e),r=Object.assign({},t),a=this.getBasicHeaders(n);return this.httpClient.post(i,{},{params:r,headers:a})}},{key:"mergeVersion",value:function(t,n,i,r){if(!t)return Promise.reject(new Error("Missing required argument: id"));if(!n)return Promise.reject(new Error("Missing required argument: versionId"));var a=Object.assign({},i),s=Object.assign(e.getVersionHeader(n),this.getBasicHeaders(r)),o="_action/version/merge/".concat(this.apiEndpoint,"/").concat(n);return this.httpClient.post(o,{},{params:a,headers:s})}},{key:"getApiBasePath",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n="";return t&&t.length&&(n+="".concat(t,"/")),e&&e.length>0?"".concat(n).concat(this.apiEndpoint,"/").concat(e):"".concat(n).concat(this.apiEndpoint)}},{key:"getBasicHeaders",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t={Accept:this.contentType,Authorization:"Bearer ".concat(this.loginService.getToken()),"Content-Type":"application/json"};return Object.assign({},t,e)}},{key:"apiEndpoint",get:function(){return this.endpoint},set:function(e){this.endpoint=e}},{key:"httpClient",get:function(){return this.client},set:function(e){this.client=e}},{key:"contentType",get:function(){return this.type},set:function(e){this.type=e}}],[{key:"handleResponse",value:function(t){if(null===t.data||void 0===t.data)return t;var n=t.data,i=t.headers;return i&&i["content-type"]&&"application/vnd.api+json"===i["content-type"]&&(n=e.parseJsonApiData(n)),n}},{key:"parseJsonApiData",value:function(e){return Object(o.a)(e)}},{key:"getVersionHeader",value:function(e){return{"sw-version-id":e}}}]),e}();t.default=c},VOfD:function(e){e.exports=JSON.parse("{}")},lCmU:function(e){e.exports=JSON.parse("{}")},lO2t:function(e,t,n){"use strict";n.d(t,"b",(function(){return C}));var i=n("GoyQ"),r=n.n(i),a=n("YO3V"),s=n.n(a),o=n("E+oP"),c=n.n(o),u=n("wAXd"),l=n.n(u),h=n("Z0cm"),d=n.n(h),p=n("lSCD"),f=n.n(p),g=n("YiAA"),v=n.n(g),m=n("4qC0"),y=n.n(m),O=n("Znm+"),b=n.n(O),w=n("Y+p1"),j=n.n(w),k=n("UB5X"),P=n.n(k);function C(e){return void 0===e}t.a={isObject:r.a,isPlainObject:s.a,isEmpty:c.a,isRegExp:l.a,isArray:d.a,isFunction:f.a,isDate:v.a,isString:y.a,isBoolean:b.a,isEqual:j.a,isNumber:P.a,isUndefined:C}},lYO9:function(e,t,n){"use strict";n.d(t,"g",(function(){return y})),n.d(t,"a",(function(){return O})),n.d(t,"c",(function(){return b})),n.d(t,"i",(function(){return w})),n.d(t,"h",(function(){return j})),n.d(t,"f",(function(){return k})),n.d(t,"b",(function(){return P})),n.d(t,"e",(function(){return C})),n.d(t,"d",(function(){return S}));var i=n("lSNA"),r=n.n(i),a=n("QkVN"),s=n.n(a),o=n("BkRI"),c=n.n(o),u=n("mwIZ"),l=n.n(u),h=n("D1y2"),d=n.n(h),p=n("JZM8"),f=n.n(p),g=n("lO2t");function v(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);t&&(i=i.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,i)}return n}function m(e){for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:{};return JSON.parse(JSON.stringify(e))}function C(e,t){return e===t?{}:g.a.isObject(e)&&g.a.isObject(t)?g.a.isDate(e)||g.a.isDate(t)?e.valueOf()===t.valueOf()?{}:t:Object.keys(t).reduce((function(n,i){if(!k(e,i))return m({},n,r()({},i,t[i]));if(g.a.isArray(t[i])){var a=S(e[i],t[i]);return Object.keys(a).length>0?m({},n,r()({},i,t[i])):n}if(g.a.isObject(t[i])){var s=C(e[i],t[i]);return!g.a.isObject(s)||Object.keys(s).length>0?m({},n,r()({},i,s)):n}return e[i]!==t[i]?m({},n,r()({},i,t[i])):n}),{}):t}function S(e,t){if(e===t)return[];if(!g.a.isArray(e)||!g.a.isArray(t))return t;if(e.length<=0&&t.length<=0)return[];if(e.length!==t.length)return t;if(!g.a.isObject(t[0]))return t.filter((function(t){return!e.includes(t)}));var n=[];return t.forEach((function(i,r){var a=C(e[r],t[r]);Object.keys(a).length>0&&n.push(t[r])})),n}},liwd:function(e,t,n){"use strict";n.r(t);var i=n("/p6D"),r=n.n(i);const{Component:a,Mixin:s}=Shopware,{Criteria:o}=Shopware.Data,c=Shopware.Utils.dom;a.override("sw-theme-modal",{template:r.a,mixins:[s.getByName("notification")],inject:["macThemePreviewService"],data:()=>({selected:null,themes:[],isLoadingPreview:!1,isLoadingCopy:!1}),computed:{salesChannelRepository(){return this.repositoryFactory.create("sales_channel")}},created(){this.createdComponent()},methods:{createdComponent(){this.loadEntityData()},loadEntityData(){this.$route.params.id&&this.loadSalesChannel()},loadSalesChannel(){this.isLoadingPreview=!0;const e=new o;e.addAssociation("domains"),this.salesChannelRepository.get(this.$route.params.id,Shopware.Context.api,e).then(e=>{this.salesChannel=e,this.isLoadingPreview=!1})},selectPreview(){this.checkConditions()&&(this.isLoadingPreview=!0,this.macThemePreviewService.themeCompile(this.salesChannel.id,this.selected).then(()=>{this.isLoadingPreview=!1,window.open(this.renderPreviewUrl())}))},copyToClipboard(){this.checkConditions()&&(this.isLoadingCopy=!0,this.macThemePreviewService.themeCompile(this.salesChannel.id,this.selected).then(()=>{this.isLoadingCopy=!1,c.copyToClipboard(this.renderPreviewUrl()),this.createNotificationInfo({title:this.$tc("global.default.info"),message:this.$tc("mac-theme-preview.notification.notificationCopyLinkSuccess")})}))},renderPreviewUrl(){return this.salesChannel.domains.first().url+"/?preview-theme-id="+this.selected},checkConditions(){return this.themes.find(e=>e.id===this.selected)?!!this.salesChannel.domains.first()||(this.createNotificationError({title:this.$tc("global.default.error"),message:this.$tc("mac-theme-preview.notification.notificationEmptyDomain")}),!1):(this.createNotificationError({title:this.$tc("global.default.error"),message:this.$tc("global.notification.unspecifiedSaveErrorMessage")}),!1)}}});var u=n("SwLI");class l extends u.default{constructor(e,t,n="mac-theme-preview"){super(e,t,n),this.name="MacThemePreviewService"}themeCompile(e,t){const n=`/_action/${this.getApiBasePath()}/compile`;return this.httpClient.post(n,{sales_channel_id:e,theme_id:t},{headers:this.getBasicHeaders()}).then(e=>u.default.handleResponse(e))}}var h=l;n("VOfD"),n("lCmU");Shopware.Application.addServiceProvider("macThemePreviewService",(function(){var e=Shopware.Application.getContainer("init");return new h(e.httpClient,Shopware.Service("loginService"))}))}},[["liwd","runtime","vendors-node"]]]); -------------------------------------------------------------------------------- /src/Resources/snippet/en_GB/SnippetFile_en_GB.php: -------------------------------------------------------------------------------- 1 | themeService = $themeService; 37 | $this->publicFilesystem = $publicFilesystem; 38 | $this->themeRepository = $themeRepository; 39 | } 40 | 41 | public function themeCompile(Context $context, string $salesChannelId, string $themeId): void 42 | { 43 | $criteria = new Criteria([$themeId]); 44 | $criteria->addAssociation('salesChannels'); 45 | 46 | /** @var ThemeEntity $theme */ 47 | $theme = $this->themeRepository->search($criteria, $context)->get($themeId); 48 | if (!$theme) { 49 | throw new InvalidThemeException($themeId); 50 | } 51 | 52 | if ($this->isNeedCompile($theme, $salesChannelId)) { 53 | $this->themeService->compileTheme($salesChannelId, $themeId, $context); 54 | } 55 | } 56 | 57 | private function isNeedCompile(ThemeEntity $theme, string $salesChannelId): bool 58 | { 59 | $themePrefix = md5($theme->getId() . $salesChannelId); 60 | $outputPath = 'theme' . DIRECTORY_SEPARATOR . $themePrefix; 61 | $scriptFilepath = $outputPath . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . 'all.js'; 62 | 63 | try { 64 | if (empty($theme->getUpdatedAt()) && !$this->publicFilesystem->has($scriptFilepath)) { 65 | return true; 66 | } 67 | 68 | if ($theme->getUpdatedAt()) { 69 | $compliedTime = $this->publicFilesystem->getTimestamp($scriptFilepath); 70 | if ($compliedTime < $theme->getUpdatedAt()->getTimestamp()) { 71 | return true; 72 | } 73 | } 74 | } catch (FileNotFoundException $e) { 75 | return true; 76 | } 77 | 78 | return false; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Subscriber/MacThemePreviewSubscriber.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 22 | } 23 | 24 | /** 25 | * @inheritDoc 26 | */ 27 | public static function getSubscribedEvents() 28 | { 29 | return [ 30 | KernelEvents::REQUEST => [ 31 | ['setTheme', 40] 32 | ] 33 | ]; 34 | } 35 | 36 | public function setTheme(RequestEvent $event) { 37 | $request = $event->getRequest(); 38 | 39 | if (!$request->cookies->has('preview-theme-id')) { 40 | return; 41 | } 42 | 43 | $themeId = $request->cookies->get('preview-theme-id'); 44 | $theme = $this->findTheme($themeId); 45 | if (empty($theme)) { 46 | return; 47 | } 48 | 49 | $request->attributes->set(SalesChannelRequest::ATTRIBUTE_THEME_ID, $themeId); 50 | $request->attributes->set(SalesChannelRequest::ATTRIBUTE_THEME_NAME, $theme['themeName']); 51 | } 52 | 53 | private function findTheme($themeId) { 54 | /** @var Statement $statement */ 55 | $statement = $this->connection->createQueryBuilder() 56 | ->select( 57 | [ 58 | 'LOWER(HEX(theme.id)) themeId', 59 | 'theme.technical_name as themeName' 60 | ] 61 | )->from('theme') 62 | ->where('theme.id = UNHEX(:themeId)') 63 | ->setParameter('themeId', $themeId) 64 | ->execute(); 65 | 66 | return $statement->fetch(); 67 | } 68 | } 69 | --------------------------------------------------------------------------------