From 40e4fac429a18de8ebd930105c2271c5b156f14f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:42:51 +0000 Subject: [PATCH 1/4] Initial plan From a093ce88eb2544b8864bbf659e9cdc9c12c9ade3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:49:05 +0000 Subject: [PATCH 2/4] Add blog entry duplication feature Co-authored-by: 7underlines <17121556+7underlines@users.noreply.github.com> --- .../blog-module/page/werkl-blog-list/index.js | 161 +++++++++++++++++- .../werkl-blog-list/werkl-blog-list.html.twig | 10 ++ .../src/module/blog-module/snippet/de-DE.json | 9 +- .../src/module/blog-module/snippet/en-GB.json | 9 +- 4 files changed, 186 insertions(+), 3 deletions(-) diff --git a/src/Resources/app/administration/src/module/blog-module/page/werkl-blog-list/index.js b/src/Resources/app/administration/src/module/blog-module/page/werkl-blog-list/index.js index 15a40dad..8c5f0012 100755 --- a/src/Resources/app/administration/src/module/blog-module/page/werkl-blog-list/index.js +++ b/src/Resources/app/administration/src/module/blog-module/page/werkl-blog-list/index.js @@ -1,8 +1,9 @@ import template from './werkl-blog-list.html.twig'; import './werkl-blog-list.scss'; -const { Mixin } = Shopware; +const { Mixin, Context } = Shopware; const Criteria = Shopware.Data.Criteria; +const { cloneDeep } = Shopware.Utils.object; export default { template, @@ -43,6 +44,10 @@ export default { return this.repositoryFactory.create('werkl_blog_category'); }, + pageRepository() { + return this.repositoryFactory.create('cms_page'); + }, + dateFilter() { return Shopware.Filter.getByName('date'); }, @@ -111,5 +116,159 @@ export default { openSponsorPage() { window.open('https://github.com/sponsors/7underlines', '_blank'); }, + + async onDuplicate(blogEntry) { + this.isLoading = true; + + try { + // Create criteria to load the full blog entry with all associations + const criteria = new Criteria(1, 1); + const sortCriteria = Criteria.sort('position', 'ASC', true); + + criteria + .addAssociation('blogCategories') + .addAssociation('tags') + .addAssociation('blogAuthor') + .getAssociation('cmsPage') + .getAssociation('sections') + .addSorting(sortCriteria) + .addAssociation('backgroundMedia') + .getAssociation('blocks') + .addSorting(sortCriteria) + .addAssociation('backgroundMedia') + .addAssociation('slots'); + + // Load the full blog entry + const fullBlogEntry = await this.blogEntryRepository.get( + blogEntry.id, + Context.api, + criteria + ); + + // Create a new CMS page (deep copy of the original) + let newCmsPage = null; + if (fullBlogEntry.cmsPage) { + newCmsPage = this.pageRepository.create(); + const originalPage = fullBlogEntry.cmsPage; + + // Copy basic page properties + newCmsPage.name = `${originalPage.name} (Copy)`; + newCmsPage.type = originalPage.type; + newCmsPage.locked = originalPage.locked; + newCmsPage.config = cloneDeep(originalPage.config); + + // Deep copy sections, blocks, and slots + if (originalPage.sections) { + newCmsPage.sections = originalPage.sections.map(section => { + const newSection = this.pageRepository.create().sections.create(); + newSection.type = section.type; + newSection.sizingMode = section.sizingMode; + newSection.mobileBehavior = section.mobileBehavior; + newSection.backgroundColor = section.backgroundColor; + newSection.backgroundMediaId = section.backgroundMediaId; + newSection.backgroundMediaMode = section.backgroundMediaMode; + newSection.cssClass = section.cssClass; + newSection.position = section.position; + newSection.visibility = cloneDeep(section.visibility); + + if (section.blocks) { + newSection.blocks = section.blocks.map(block => { + const newBlock = newSection.blocks.create(); + newBlock.type = block.type; + newBlock.position = block.position; + newBlock.locked = block.locked; + newBlock.name = block.name; + newBlock.sectionPosition = block.sectionPosition; + newBlock.marginTop = block.marginTop; + newBlock.marginBottom = block.marginBottom; + newBlock.marginLeft = block.marginLeft; + newBlock.marginRight = block.marginRight; + newBlock.backgroundColor = block.backgroundColor; + newBlock.backgroundMediaId = block.backgroundMediaId; + newBlock.backgroundMediaMode = block.backgroundMediaMode; + newBlock.cssClass = block.cssClass; + newBlock.visibility = cloneDeep(block.visibility); + + if (block.slots) { + newBlock.slots = block.slots.map(slot => { + const newSlot = newBlock.slots.create(); + newSlot.type = slot.type; + newSlot.slot = slot.slot; + newSlot.locked = slot.locked; + newSlot.config = cloneDeep(slot.config); + return newSlot; + }); + } + + return newBlock; + }); + } + + return newSection; + }); + } + + // Save the new CMS page first + await this.pageRepository.save(newCmsPage, Context.api); + } + + // Create the new blog entry + const newBlogEntry = this.blogEntryRepository.create(); + + // Copy properties from original blog entry + newBlogEntry.title = `${fullBlogEntry.title} (Copy)`; + newBlogEntry.slug = null; // Will be auto-generated from title + newBlogEntry.teaser = fullBlogEntry.teaser; + newBlogEntry.metaTitle = fullBlogEntry.metaTitle; + newBlogEntry.metaDescription = fullBlogEntry.metaDescription; + newBlogEntry.authorId = fullBlogEntry.authorId; + newBlogEntry.publishedAt = new Date(); // Set to current date + newBlogEntry.active = false; // Set as inactive by default + newBlogEntry.detailTeaserImage = fullBlogEntry.detailTeaserImage; + newBlogEntry.mediaId = fullBlogEntry.mediaId; + newBlogEntry.cmsPageId = newCmsPage ? newCmsPage.id : null; + + // Copy categories + if (fullBlogEntry.blogCategories && fullBlogEntry.blogCategories.length > 0) { + fullBlogEntry.blogCategories.forEach(category => { + newBlogEntry.blogCategories.add(category); + }); + } + + // Copy tags + if (fullBlogEntry.tags && fullBlogEntry.tags.length > 0) { + fullBlogEntry.tags.forEach(tag => { + newBlogEntry.tags.add(tag); + }); + } + + // Save the new blog entry + await this.blogEntryRepository.save(newBlogEntry, Context.api); + + // Show success notification + this.createNotificationSuccess({ + message: this.$tc('werkl-blog.list.notification.duplicateSuccess', 0, { + title: fullBlogEntry.title, + }), + }); + + // Refresh the list + await this.getList(); + + // Navigate to the duplicated entry + this.$router.push({ + name: 'blog.module.detail', + params: { id: newBlogEntry.id }, + }); + + } catch (error) { + this.createNotificationError({ + message: this.$tc('werkl-blog.list.notification.duplicateError'), + }); + console.error('Error duplicating blog entry:', error); + } finally { + this.isLoading = false; + } + }, }, }; diff --git a/src/Resources/app/administration/src/module/blog-module/page/werkl-blog-list/werkl-blog-list.html.twig b/src/Resources/app/administration/src/module/blog-module/page/werkl-blog-list/werkl-blog-list.html.twig index 29edf516..54b15f18 100644 --- a/src/Resources/app/administration/src/module/blog-module/page/werkl-blog-list/werkl-blog-list.html.twig +++ b/src/Resources/app/administration/src/module/blog-module/page/werkl-blog-list/werkl-blog-list.html.twig @@ -107,6 +107,16 @@ {% endblock %} + {% block werkl_blog_list_grid_actions %} + + {% endblock %} + {% endblock %} diff --git a/src/Resources/app/administration/src/module/blog-module/snippet/de-DE.json b/src/Resources/app/administration/src/module/blog-module/snippet/de-DE.json index fedca910..aee02040 100644 --- a/src/Resources/app/administration/src/module/blog-module/snippet/de-DE.json +++ b/src/Resources/app/administration/src/module/blog-module/snippet/de-DE.json @@ -13,11 +13,18 @@ "buttonAddBlogEntry": "Blog Eintrag erstellen", "buttonSponsor": "Entwicklung unterstützen", "messageEmpty": "Keine Einträge vorhanden", + "buttonDuplicate": "Duplizieren", + "contextMenuDuplicate": "Duplizieren", + "notification": { + "duplicateSuccess": "Blog Eintrag \"{title}\" wurde erfolgreich dupliziert.", + "duplicateError": "Fehler beim Duplizieren des Blog Eintrags." + }, "table": { "title": "Titel", "active": "Aktiv", "author": "Autor", - "publishedAt": "Veröffentlicht am" + "publishedAt": "Veröffentlicht am", + "actions": "Aktionen" } }, "detail": { diff --git a/src/Resources/app/administration/src/module/blog-module/snippet/en-GB.json b/src/Resources/app/administration/src/module/blog-module/snippet/en-GB.json index 55736748..f50f505d 100644 --- a/src/Resources/app/administration/src/module/blog-module/snippet/en-GB.json +++ b/src/Resources/app/administration/src/module/blog-module/snippet/en-GB.json @@ -13,11 +13,18 @@ "buttonAddBlogEntry": "Create blog entry", "buttonSponsor": "Support development", "messageEmpty": "No blog entries ...", + "buttonDuplicate": "Duplicate", + "contextMenuDuplicate": "Duplicate", + "notification": { + "duplicateSuccess": "Blog entry \"{title}\" has been successfully duplicated.", + "duplicateError": "Error duplicating blog entry." + }, "table": { "title": "Title", "active": "Active", "author": "Author", - "publishedAt": "Published at" + "publishedAt": "Published at", + "actions": "Actions" } }, "detail": { From c719acfc6683e8d3323f664bb40b9271e18a0379 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:52:04 +0000 Subject: [PATCH 3/4] Fix code review issues: simplify CMS duplication and set publishedAt to null Co-authored-by: 7underlines <17121556+7underlines@users.noreply.github.com> --- .../blog-module/page/werkl-blog-list/index.js | 67 +++++++------------ 1 file changed, 24 insertions(+), 43 deletions(-) diff --git a/src/Resources/app/administration/src/module/blog-module/page/werkl-blog-list/index.js b/src/Resources/app/administration/src/module/blog-module/page/werkl-blog-list/index.js index 8c5f0012..024cbf32 100755 --- a/src/Resources/app/administration/src/module/blog-module/page/werkl-blog-list/index.js +++ b/src/Resources/app/administration/src/module/blog-module/page/werkl-blog-list/index.js @@ -157,54 +157,35 @@ export default { newCmsPage.locked = originalPage.locked; newCmsPage.config = cloneDeep(originalPage.config); - // Deep copy sections, blocks, and slots - if (originalPage.sections) { - newCmsPage.sections = originalPage.sections.map(section => { - const newSection = this.pageRepository.create().sections.create(); - newSection.type = section.type; - newSection.sizingMode = section.sizingMode; - newSection.mobileBehavior = section.mobileBehavior; - newSection.backgroundColor = section.backgroundColor; - newSection.backgroundMediaId = section.backgroundMediaId; - newSection.backgroundMediaMode = section.backgroundMediaMode; - newSection.cssClass = section.cssClass; - newSection.position = section.position; - newSection.visibility = cloneDeep(section.visibility); - + // Deep copy sections - use cloneDeep and then assign new IDs + if (originalPage.sections && originalPage.sections.length > 0) { + const clonedSections = cloneDeep(originalPage.sections); + newCmsPage.sections = clonedSections.map(section => { + // Remove the ID so Shopware creates a new one + delete section.id; + delete section.cmsPageId; + delete section.versionId; + if (section.blocks) { - newSection.blocks = section.blocks.map(block => { - const newBlock = newSection.blocks.create(); - newBlock.type = block.type; - newBlock.position = block.position; - newBlock.locked = block.locked; - newBlock.name = block.name; - newBlock.sectionPosition = block.sectionPosition; - newBlock.marginTop = block.marginTop; - newBlock.marginBottom = block.marginBottom; - newBlock.marginLeft = block.marginLeft; - newBlock.marginRight = block.marginRight; - newBlock.backgroundColor = block.backgroundColor; - newBlock.backgroundMediaId = block.backgroundMediaId; - newBlock.backgroundMediaMode = block.backgroundMediaMode; - newBlock.cssClass = block.cssClass; - newBlock.visibility = cloneDeep(block.visibility); - + section.blocks = section.blocks.map(block => { + delete block.id; + delete block.sectionId; + delete block.versionId; + if (block.slots) { - newBlock.slots = block.slots.map(slot => { - const newSlot = newBlock.slots.create(); - newSlot.type = slot.type; - newSlot.slot = slot.slot; - newSlot.locked = slot.locked; - newSlot.config = cloneDeep(slot.config); - return newSlot; + block.slots = block.slots.map(slot => { + delete slot.id; + delete slot.blockId; + delete slot.versionId; + return slot; }); } - - return newBlock; + + return block; }); } - - return newSection; + + return section; }); } @@ -222,7 +203,7 @@ export default { newBlogEntry.metaTitle = fullBlogEntry.metaTitle; newBlogEntry.metaDescription = fullBlogEntry.metaDescription; newBlogEntry.authorId = fullBlogEntry.authorId; - newBlogEntry.publishedAt = new Date(); // Set to current date + newBlogEntry.publishedAt = null; // Set to null for inactive entry newBlogEntry.active = false; // Set as inactive by default newBlogEntry.detailTeaserImage = fullBlogEntry.detailTeaserImage; newBlogEntry.mediaId = fullBlogEntry.mediaId; From 02be1e3e3b25640da06979f85eb6a55fcc978c5f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:53:43 +0000 Subject: [PATCH 4/4] Optimize Criteria constructor call for single entity fetch Co-authored-by: 7underlines <17121556+7underlines@users.noreply.github.com> --- .../src/module/blog-module/page/werkl-blog-list/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Resources/app/administration/src/module/blog-module/page/werkl-blog-list/index.js b/src/Resources/app/administration/src/module/blog-module/page/werkl-blog-list/index.js index 024cbf32..866c87b5 100755 --- a/src/Resources/app/administration/src/module/blog-module/page/werkl-blog-list/index.js +++ b/src/Resources/app/administration/src/module/blog-module/page/werkl-blog-list/index.js @@ -122,7 +122,7 @@ export default { try { // Create criteria to load the full blog entry with all associations - const criteria = new Criteria(1, 1); + const criteria = new Criteria(); const sortCriteria = Criteria.sort('position', 'ASC', true); criteria