diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e2df92c4..d9bf82957 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0). +## 2026.3.3 + +### Added + +- Override module email templates in a selected project theme, including nested email paths, in PhpStorm and WebStorm [#1205](https://github.com/magento/magento2-phpstorm-plugin/issues/1205). + ## 2026.3.2 ### Added diff --git a/gradle.properties b/gradle.properties index 62b433758..2ea5db492 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ pluginGroup = com.magento.idea.magento2plugin pluginName = Magento 2 and Adobe Commerce pluginRepositoryUrl = https://github.com/magento/magento2-phpstorm-plugin -pluginVersion = 2026.3.2 +pluginVersion = 2026.3.3 pluginSinceBuild = 262.8665.265 pluginUntilBuild = 268.* platformType = PS diff --git a/src/main/java/com/magento/idea/magento2plugin/actions/generation/OverrideEmailTemplateInThemeAction.java b/src/main/java/com/magento/idea/magento2plugin/actions/generation/OverrideEmailTemplateInThemeAction.java new file mode 100644 index 000000000..91c455de3 --- /dev/null +++ b/src/main/java/com/magento/idea/magento2plugin/actions/generation/OverrideEmailTemplateInThemeAction.java @@ -0,0 +1,73 @@ +/* + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +package com.magento.idea.magento2plugin.actions.generation; + +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.psi.PsiFile; +import com.magento.idea.magento2plugin.MagentoIcons; +import com.magento.idea.magento2plugin.actions.generation.dialog.OverrideTemplateInThemeDialog; +import com.magento.idea.magento2plugin.magento.packages.Areas; +import com.magento.idea.magento2plugin.magento.packages.ComponentType; +import com.magento.idea.magento2plugin.util.magento.GetMagentoModuleUtil; +import org.jetbrains.annotations.NotNull; + +public class OverrideEmailTemplateInThemeAction extends OverrideFileInThemeAction { + + public static final String ACTION_NAME = "Override email template in a project theme"; + public static final String ACTION_DESCRIPTION = "Override email template file in project theme"; + public static final String EMAIL_DIRECTORY = "email"; + public static final String HTML = "html"; + + public OverrideEmailTemplateInThemeAction() { + super(ACTION_NAME, ACTION_DESCRIPTION, MagentoIcons.MODULE); + } + + @Override + public void actionPerformed(final @NotNull AnActionEvent event) { + final Project project = event.getProject(); + final PsiFile psiFile = getTargetFile(event); + + if (project == null || psiFile == null) { + return; + } + OverrideTemplateInThemeDialog.open(project, psiFile); + } + + @Override + protected boolean isOverrideAllowed( + final @NotNull PsiFile file, + final @NotNull Project project + ) { + final VirtualFile virtualFile = file.getVirtualFile(); + + if (virtualFile == null || !HTML.equals(virtualFile.getExtension())) { + return false; + } + final GetMagentoModuleUtil.MagentoModuleData moduleData = + GetMagentoModuleUtil.getByContext(file.getContainingDirectory(), project); + + if (moduleData == null || moduleData.getType() != ComponentType.module + || moduleData.getViewDir() == null) { + return false; + } + final String relativePath = VfsUtilCore.getRelativePath( + virtualFile, moduleData.getViewDir().getVirtualFile(), '/' + ); + + if (relativePath == null) { + return false; + } + final String[] path = relativePath.split("/"); + + return path.length >= 3 && EMAIL_DIRECTORY.equals(path[1]) + && (Areas.frontend.toString().equals(path[0]) + || Areas.adminhtml.toString().equals(path[0]) + || Areas.base.toString().equals(path[0])); + } +} diff --git a/src/main/java/com/magento/idea/magento2plugin/actions/generation/dialog/OverrideTemplateInThemeDialog.java b/src/main/java/com/magento/idea/magento2plugin/actions/generation/dialog/OverrideTemplateInThemeDialog.java index d5984ea3d..16385f42c 100644 --- a/src/main/java/com/magento/idea/magento2plugin/actions/generation/dialog/OverrideTemplateInThemeDialog.java +++ b/src/main/java/com/magento/idea/magento2plugin/actions/generation/dialog/OverrideTemplateInThemeDialog.java @@ -9,6 +9,7 @@ import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiFile; import com.magento.idea.magento2plugin.actions.generation.OverrideTemplateInThemeAction; +import com.magento.idea.magento2plugin.actions.generation.OverrideEmailTemplateInThemeAction; import com.magento.idea.magento2plugin.actions.generation.dialog.validator.annotation.FieldValidation; import com.magento.idea.magento2plugin.actions.generation.dialog.validator.annotation.RuleRegistry; import com.magento.idea.magento2plugin.actions.generation.dialog.validator.rule.NotEmptyRule; @@ -55,7 +56,9 @@ public OverrideTemplateInThemeDialog( this.psiFile = psiFile; final String fileType = psiFile.getVirtualFile().getExtension(); - if (OverridableFileType.isFilePhtml(fileType)) { + if (OverrideEmailTemplateInThemeAction.HTML.equals(fileType)) { + setTitle(OverrideEmailTemplateInThemeAction.ACTION_DESCRIPTION); + } else if (OverridableFileType.isFilePhtml(fileType)) { setTitle(OverrideTemplateInThemeAction.ACTION_TEMPLATE_DESCRIPTION); } else if (OverridableFileType.isFileJS(fileType)) { setTitle(OverrideTemplateInThemeAction.ACTION_JS_DESCRIPTION); diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 863cdb3c4..bd8c990e1 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -58,6 +58,10 @@ class="com.magento.idea.magento2plugin.actions.generation.OverrideLayoutInThemeAction"> + + + diff --git a/src/test/java/com/magento/idea/magento2plugin/actions/generation/OverrideEmailTemplateInThemeActionTest.java b/src/test/java/com/magento/idea/magento2plugin/actions/generation/OverrideEmailTemplateInThemeActionTest.java new file mode 100644 index 000000000..d8502a7ef --- /dev/null +++ b/src/test/java/com/magento/idea/magento2plugin/actions/generation/OverrideEmailTemplateInThemeActionTest.java @@ -0,0 +1,56 @@ +/* + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +package com.magento.idea.magento2plugin.actions.generation; + +import com.intellij.psi.PsiFile; +import com.magento.idea.magento2plugin.BaseProjectTestCase; + +public class OverrideEmailTemplateInThemeActionTest extends BaseProjectTestCase { + public void testFrontendEmailIsAllowed() { + assertAllowed("view/frontend/email/shipping.html", true); + } + + public void testNestedAdminEmailIsAllowed() { + assertAllowed("view/adminhtml/email/order/shipping.html", true); + } + + public void testBaseEmailIsAllowed() { + assertAllowed("view/base/email/shipping.html", true); + } + + public void testRegularHtmlTemplateIsNotAllowed() { + assertAllowed("view/frontend/web/template/shipping.html", false); + } + + public void testPhpTemplateIsNotAllowed() { + assertAllowed("view/frontend/email/shipping.phtml", false); + } + + public void testUnrelatedEmailDirectoryIsNotAllowed() { + assertAllowed("frontend/email/shipping.html", false); + } + + public void testUnsupportedAreaIsNotAllowed() { + assertAllowed("view/graphql/email/shipping.html", false); + } + + public void testHtmlOutsideModuleIsNotAllowed() { + final PsiFile file = myFixture.addFileToProject("misc/view/frontend/email/shipping.html", "

Hello

"); + assertFalse(new OverrideEmailTemplateInThemeAction().isOverrideAllowed(file, getProject())); + } + + private void assertAllowed(final String relativePath, final boolean expected) { + // Resolve the module through module.xml, without registration.php or PHP PSI. + myFixture.addFileToProject( + "vendor/example/email-module/etc/module.xml", + "" + ); + final PsiFile file = myFixture.addFileToProject( + "vendor/example/email-module/" + relativePath, "

Hello {{var name}}

" + ); + assertEquals(expected, new OverrideEmailTemplateInThemeAction().isOverrideAllowed(file, getProject())); + } +} diff --git a/src/uiTest/kotlin/com/magento/idea/magento2plugin/actions/OverrideEmailTemplateInThemeActionUiTest.kt b/src/uiTest/kotlin/com/magento/idea/magento2plugin/actions/OverrideEmailTemplateInThemeActionUiTest.kt new file mode 100644 index 000000000..2e669e74d --- /dev/null +++ b/src/uiTest/kotlin/com/magento/idea/magento2plugin/actions/OverrideEmailTemplateInThemeActionUiTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +package com.magento.idea.magento2plugin.actions + +import com.intellij.driver.sdk.ui.components.common.ideFrame +import com.intellij.driver.sdk.ui.components.elements.JComboBoxUiComponent +import com.intellij.driver.sdk.ui.components.elements.dialog +import com.magento.idea.magento2plugin.ui.assertGeneratedFile +import com.magento.idea.magento2plugin.ui.clickOkButton +import com.magento.idea.magento2plugin.ui.invokeProjectViewAction +import com.magento.idea.magento2plugin.ui.runWebStormUiTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.nio.file.Files + +class OverrideEmailTemplateInThemeActionUiTest { + @Test + fun `copies nested email into theme`() = + runWebStormUiTest( + FILE_GENERATORS_FIXTURE, + "app/design/frontend/Acme/storefront/Acme_Shipping/email/order/shipping.html", + ) { projectPath -> + val sourcePath = projectPath.resolve( + "app/code/Acme/Shipping/view/frontend/email/order/shipping.html", + ) + val originalText = Files.readString(sourcePath) + invokeProjectViewAction( + MODULE_PATH + listOf("view", "frontend", "email", "order", "shipping.html"), + "OverrideEmailTemplateInTheme.Menu", + waitForAction = false, + ) + + ideFrame { + dialog(title = "Override email template file in project theme") { + val theme = xx( + "//div[@class='JComboBox']", + JComboBoxUiComponent::class.java, + ).list().single() + theme.selectItem("frontend/Acme/storefront") + clickOkButton() + } + } + + assertGeneratedFile( + projectPath, + "app/design/frontend/Acme/storefront/Acme_Shipping/email/order/shipping.html", + "{{var order.increment_id}}", + ) + assertEquals( + originalText, + Files.readString(projectPath.resolve( + "app/design/frontend/Acme/storefront/Acme_Shipping/email/order/shipping.html", + )), + ) + assertEquals(originalText, Files.readString(sourcePath)) + } +} diff --git a/src/uiTest/resources/projects/file-generators/app/code/Acme/Shipping/view/frontend/email/order/shipping.html b/src/uiTest/resources/projects/file-generators/app/code/Acme/Shipping/view/frontend/email/order/shipping.html new file mode 100644 index 000000000..51cee0580 --- /dev/null +++ b/src/uiTest/resources/projects/file-generators/app/code/Acme/Shipping/view/frontend/email/order/shipping.html @@ -0,0 +1,2 @@ + +

Order {{var order.increment_id}} has shipped.