From 7eb49824a57a8b1570d5ef0442a89ce131c9000f Mon Sep 17 00:00:00 2001 From: erwinwalterddf Date: Thu, 24 Sep 2026 15:04:21 +0200 Subject: [PATCH 1/2] fix: open blog pages in the blog editor from every entry point --- .../src/init/cms-page-type.init.js | 2 + .../blog-module/extension/sw-cms/index.js | 1 + .../sw-cms/page/sw-cms-detail/index.js | 45 +++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 src/Resources/app/administration/src/module/blog-module/extension/sw-cms/page/sw-cms-detail/index.js diff --git a/src/Resources/app/administration/src/init/cms-page-type.init.js b/src/Resources/app/administration/src/init/cms-page-type.init.js index 27b47b6e..94ff1591 100644 --- a/src/Resources/app/administration/src/init/cms-page-type.init.js +++ b/src/Resources/app/administration/src/init/cms-page-type.init.js @@ -5,4 +5,6 @@ const pageTypeService = Shopware.Service('cmsPageTypeService'); pageTypeService.register({ name: BLOG.PAGE_TYPES.BLOG_DETAIL, icon: 'regular-file-text', + // blog pages are created in the blog module together with their blog entry + hideInList: true, }); diff --git a/src/Resources/app/administration/src/module/blog-module/extension/sw-cms/index.js b/src/Resources/app/administration/src/module/blog-module/extension/sw-cms/index.js index 4efbca7d..84a8802f 100644 --- a/src/Resources/app/administration/src/module/blog-module/extension/sw-cms/index.js +++ b/src/Resources/app/administration/src/module/blog-module/extension/sw-cms/index.js @@ -5,3 +5,4 @@ Component.override( () => import('./component/sw-cms-sidebar') ); Component.override('sw-cms-list', () => import('./page/sw-cms-list')); +Component.override('sw-cms-detail', () => import('./page/sw-cms-detail')); diff --git a/src/Resources/app/administration/src/module/blog-module/extension/sw-cms/page/sw-cms-detail/index.js b/src/Resources/app/administration/src/module/blog-module/extension/sw-cms/page/sw-cms-detail/index.js new file mode 100644 index 00000000..6d233df8 --- /dev/null +++ b/src/Resources/app/administration/src/module/blog-module/extension/sw-cms/page/sw-cms-detail/index.js @@ -0,0 +1,45 @@ +const { + Data: { Criteria }, +} = Shopware; + +export default { + methods: { + async createdComponent() { + const blogEntry = await this.findBlogEntryForPage( + this.$route.params.id + ); + + if (blogEntry) { + // blog pages belong to the blog module, the core editor has no blog sidebar + this.$router.replace({ + name: 'blog.module.detail', + params: { id: blogEntry.id }, + }); + + return; + } + + this.$super('createdComponent'); + }, + + async findBlogEntryForPage(pageId) { + if (!pageId) { + return null; + } + + const criteria = new Criteria(1, 1); + criteria.addFilter(Criteria.equals('cmsPageId', pageId)); + + try { + const blogEntries = await this.repositoryFactory + .create('werkl_blog_entry') + .search(criteria, Shopware.Context.api); + + return blogEntries.first(); + } catch { + // a failing lookup must not block the core editor + return null; + } + }, + }, +}; From 204c32b15ce223c3d2aafdae2a1da157581891df Mon Sep 17 00:00:00 2001 From: 7underlines <7underlines@gmail.com> Date: Wed, 7 Oct 2026 10:49:48 +0200 Subject: [PATCH 2/2] fix(administration): ignore stale blog page lookups --- .github/workflows/administration-tests.yml | 31 ++++ src/Resources/app/administration/package.json | 3 + .../sw-cms/page/sw-cms-detail/index.js | 20 ++- .../cms-detail-routing.test.mjs | 138 ++++++++++++++++++ 4 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/administration-tests.yml create mode 100644 tests/administration/cms-detail-routing.test.mjs diff --git a/.github/workflows/administration-tests.yml b/.github/workflows/administration-tests.yml new file mode 100644 index 00000000..f1831eef --- /dev/null +++ b/.github/workflows/administration-tests.yml @@ -0,0 +1,31 @@ +name: Administration Tests + +on: + pull_request: + paths: + - 'src/Resources/app/administration/**' + - 'tests/administration/**' + - '.github/workflows/administration-tests.yml' + push: + branches: + - main + paths: + - 'src/Resources/app/administration/**' + - 'tests/administration/**' + - '.github/workflows/administration-tests.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + administration: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Run administration tests + working-directory: src/Resources/app/administration + run: npm test diff --git a/src/Resources/app/administration/package.json b/src/Resources/app/administration/package.json index 681ba0eb..d80a1eb8 100755 --- a/src/Resources/app/administration/package.json +++ b/src/Resources/app/administration/package.json @@ -2,6 +2,9 @@ "name": "administration", "version": "1.0.0", "license": "MIT", + "scripts": { + "test": "node --test ../../../../tests/administration/*.test.mjs" + }, "dependencies": { "slugify": "^1.6.6", "vite": "^6.0.11" diff --git a/src/Resources/app/administration/src/module/blog-module/extension/sw-cms/page/sw-cms-detail/index.js b/src/Resources/app/administration/src/module/blog-module/extension/sw-cms/page/sw-cms-detail/index.js index 6d233df8..af19c89d 100644 --- a/src/Resources/app/administration/src/module/blog-module/extension/sw-cms/page/sw-cms-detail/index.js +++ b/src/Resources/app/administration/src/module/blog-module/extension/sw-cms/page/sw-cms-detail/index.js @@ -3,11 +3,25 @@ const { } = Shopware; export default { + data() { + return { + isBlogPageLookupCancelled: false, + }; + }, + + beforeUnmount() { + this.isBlogPageLookupCancelled = true; + }, + methods: { async createdComponent() { - const blogEntry = await this.findBlogEntryForPage( - this.$route.params.id - ); + const route = this.$route; + const blogEntry = await this.findBlogEntryForPage(route.params.id); + + // A late response must not redirect or reset stores after navigation. + if (this.isBlogPageLookupCancelled || this.$route !== route) { + return; + } if (blogEntry) { // blog pages belong to the blog module, the core editor has no blog sidebar diff --git a/tests/administration/cms-detail-routing.test.mjs b/tests/administration/cms-detail-routing.test.mjs new file mode 100644 index 00000000..ef4b79fa --- /dev/null +++ b/tests/administration/cms-detail-routing.test.mjs @@ -0,0 +1,138 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { test } from 'node:test'; + +class Criteria { + constructor(page, limit) { + this.page = page; + this.limit = limit; + this.filters = []; + } + + addFilter(filter) { + this.filters.push(filter); + } + + static equals(field, value) { + return { field, value }; + } +} + +const apiContext = {}; +globalThis.Shopware = { Data: { Criteria }, Context: { api: apiContext } }; + +// Load the actual administration override without a Vite build. +const source = await readFile(new URL( + '../../src/Resources/app/administration/src/module/blog-module/extension/sw-cms/page/sw-cms-detail/index.js', + import.meta.url +), 'utf8'); +const { default: override } = await import( + `data:text/javascript;base64,${Buffer.from(source).toString('base64')}` +); + +function createInstance(search, id = 'cms-page-a') { + const calls = []; + const instance = { + ...override.data?.(), + ...override.methods, + $route: { name: 'sw.cms.detail', params: { id } }, + $router: { replace: (route) => calls.push({ redirect: route }) }, + $super: (method) => calls.push({ core: method }), + repositoryFactory: { + create(entity) { + assert.equal(entity, 'werkl_blog_entry'); + return { search }; + }, + }, + }; + + return { instance, calls }; +} + +function deferred() { + let resolve; + let reject; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise; + reject = rejectPromise; + }); + return { promise, resolve, reject }; +} + +const result = (entry) => ({ first: () => entry }); + +test('a linked CMS page redirects to its blog entry', async () => { + const { instance, calls } = createInstance(async (criteria, context) => { + assert.equal(criteria.limit, 1); + assert.deepEqual(criteria.filters, [{ field: 'cmsPageId', value: 'cms-page-a' }]); + assert.equal(context, apiContext); + return result({ id: 'blog-entry-a' }); + }); + + await instance.createdComponent(); + assert.deepEqual(calls, [{ + redirect: { name: 'blog.module.detail', params: { id: 'blog-entry-a' } }, + }]); +}); + +test('an unlinked CMS page opens in the core editor', async () => { + const { instance, calls } = createInstance(async () => result(null)); + await instance.createdComponent(); + assert.deepEqual(calls, [{ core: 'createdComponent' }]); +}); + +test('an API failure falls back to the core editor', async () => { + const { instance, calls } = createInstance(async () => { throw new Error('API unavailable'); }); + await instance.createdComponent(); + assert.deepEqual(calls, [{ core: 'createdComponent' }]); +}); + +test('a route without a page ID never searches for a blog entry', async () => { + const { instance, calls } = createInstance(() => assert.fail('Unexpected lookup'), null); + await instance.createdComponent(); + assert.deepEqual(calls, [{ core: 'createdComponent' }]); +}); + +for (const entry of [{ id: 'blog-entry-a' }, null]) { + const kind = entry ? 'linked' : 'unlinked'; + + test(`a late ${kind} result cannot affect another route`, async () => { + const response = deferred(); + const { instance, calls } = createInstance(() => response.promise); + const pending = instance.createdComponent(); + instance.$route = { name: 'sw.product.detail', params: { id: 'product-b' } }; + response.resolve(result(entry)); + await pending; + assert.deepEqual(calls, []); + }); + + test(`a late ${kind} result cannot affect an unmounted component`, async () => { + const response = deferred(); + const { instance, calls } = createInstance(() => response.promise); + const pending = instance.createdComponent(); + override.beforeUnmount?.call(instance); + response.resolve(result(entry)); + await pending; + assert.deepEqual(calls, []); + }); +} + +test('a late API failure cannot initialize the editor after navigation', async () => { + const response = deferred(); + const { instance, calls } = createInstance(() => response.promise); + const pending = instance.createdComponent(); + instance.$route = { name: 'sw.product.index', params: {} }; + response.reject(new Error('API unavailable')); + await pending; + assert.deepEqual(calls, []); +}); + +test('a new route with the same ID cannot use the previous lookup', async () => { + const response = deferred(); + const { instance, calls } = createInstance(() => response.promise); + const pending = instance.createdComponent(); + instance.$route = { name: 'sw.cms.detail', params: { id: 'cms-page-a' } }; + response.resolve(result({ id: 'blog-entry-a' })); + await pending; + assert.deepEqual(calls, []); +});