diff --git a/.chachalog/MphU2FML.md b/.chachalog/MphU2FML.md new file mode 100644 index 00000000..dcf2d862 --- /dev/null +++ b/.chachalog/MphU2FML.md @@ -0,0 +1,6 @@ +--- +# Allowed version bumps: patch, minor, major +javascript-modules: minor +--- + +Added an optional `parent` parameter to the `Area` component, allowing areas to be stored under any existing JCR node instead of only under the current page node. diff --git a/jahia-test-module/src/react/server/views/testAreas/TestAreas.tsx b/jahia-test-module/src/react/server/views/testAreas/TestAreas.tsx index 926b4e8d..d1011fb6 100644 --- a/jahia-test-module/src/react/server/views/testAreas/TestAreas.tsx +++ b/jahia-test-module/src/react/server/views/testAreas/TestAreas.tsx @@ -7,7 +7,7 @@ jahiaComponent( displayName: "test Areas (react)", componentType: "view", }, - () => ( + (_, { currentNode, renderContext }) => ( <>

React JArea test component

@@ -57,6 +57,21 @@ jahiaComponent( }} /> + +

Area with parent node

+
+ +
+ +

Area with home page as parent

+
+ +
+ +

Area with site root as parent

+
+ +
), ); diff --git a/javascript-modules-engine-java/src/main/java/org/jahia/modules/javascript/modules/engine/js/server/RenderHelper.java b/javascript-modules-engine-java/src/main/java/org/jahia/modules/javascript/modules/engine/js/server/RenderHelper.java index 08da5727..1d8d458e 100644 --- a/javascript-modules-engine-java/src/main/java/org/jahia/modules/javascript/modules/engine/js/server/RenderHelper.java +++ b/javascript-modules-engine-java/src/main/java/org/jahia/modules/javascript/modules/engine/js/server/RenderHelper.java @@ -62,7 +62,7 @@ public class RenderHelper { private static final Set ABSOLUTEAREA_ALLOWED_ATTRIBUTES = Set.of("name", "parent", "view", "allowedNodeTypes", "numberOfItems", "nodeType", "editable", "areaType", "limitedAbsoluteAreaEdit", "parameters"); - private static final Set AREA_ALLOWED_ATTRIBUTES = Set.of("name", "view", "allowedNodeTypes", + private static final Set AREA_ALLOWED_ATTRIBUTES = Set.of("name", "parent", "view", "allowedNodeTypes", "numberOfItems", "nodeType", "editable", "parameters"); private JCRSessionFactory jcrSessionFactory; @@ -409,15 +409,22 @@ public String renderAbsoluteArea(Map attr, RenderContext renderC public String renderArea(Map attr, RenderContext renderContext) throws IllegalAccessException, InvocationTargetException, JspException, IOException { checkAttributes(attr, AREA_ALLOWED_ATTRIBUTES); - // This is actually expected, the path of an area is relative by default, so the - // name is directly mapped to: path - // the AreaTag will resolve the area content using template inheritance - // hierarchy. - // Even if it's not used in the javascript engine, we need to respect this - // concept for compatibility with existing system relying on this behavior. - // (jExperience for example is using this behavior, for page perso/opti, by - // pushing the page variant node as parent template) - attr.put("path", readMandatoryAttribute(attr, "name")); + String name = readMandatoryAttribute(attr, "name"); + JCRNodeWrapper parent = (JCRNodeWrapper) attr.remove("parent"); + if (parent != null) { + // When a parent node is provided, compute an absolute JCR path so that AreaTag + // resolves the area via a direct node lookup (findNodeForAbsoluteAreaPath) rather + // than the default template-inheritance resolution. + attr.put("path", parent.getPath() + "/" + name); + } else { + // the AreaTag will resolve the area content using template inheritance + // hierarchy. + // Even if it's not used in the javascript engine, we need to respect this + // concept for compatibility with existing system relying on this behavior. + // (jExperience for example is using this behavior, for page perso/opti, by + // pushing the page variant node as parent template) + attr.put("path", name); + } return internalRenderArea(attr, "area", renderContext); } diff --git a/javascript-modules-library/src/components/Area.tsx b/javascript-modules-library/src/components/Area.tsx index 4ae0db3e..aaf4c061 100644 --- a/javascript-modules-library/src/components/Area.tsx +++ b/javascript-modules-library/src/components/Area.tsx @@ -1,3 +1,4 @@ +import type { JCRNodeWrapper } from "org.jahia.services.content"; import { createElement, type JSX } from "react"; import { useServerContext } from "../hooks/useServerContext.js"; @@ -8,6 +9,7 @@ import { useServerContext } from "../hooks/useServerContext.js"; */ export function Area({ name, + parent, view, allowedNodeTypes, numberOfItems, @@ -18,6 +20,16 @@ export function Area({ /** The name of the area. */ name: string; + /** + * Optional parent node where the area is stored in the JCR. When provided, the area is stored + * as a child of this node (i.e. at `/`), and the node is resolved directly + * by its absolute JCR path. The parent node must already exist. + * + * When omitted, the area is stored as a child of the current page node and resolved using the + * standard template-inheritance hierarchy (default behavior). + */ + parent?: JCRNodeWrapper; + /** The view to use for the area. */ view?: string; /** The allowed types for the area. */ @@ -47,6 +59,7 @@ export function Area({ __html: server.render.renderArea( { name, + parent, view, allowedNodeTypes, numberOfItems, diff --git a/tests/cypress/e2e/ui/areaParentTest.cy.ts b/tests/cypress/e2e/ui/areaParentTest.cy.ts new file mode 100644 index 00000000..92743412 --- /dev/null +++ b/tests/cypress/e2e/ui/areaParentTest.cy.ts @@ -0,0 +1,107 @@ +import { addNode, getNodeByPath } from "@jahia/cypress"; +import { addSimplePage } from "../../utils/helpers"; +import { GENERIC_SITE_KEY } from '../../support/constants'; + +/** + * This test verifies that the parent parameter on the Area component works correctly, + * ensuring that areas are stored as subnodes of the specified parent node rather than + * at the page level, and that two components with the same area name but different + * parent nodes produce distinct area nodes. + */ +describe("Area with parent parameter test", () => { + const pageName = "testAreaParent"; + const pagePath = `/sites/${GENERIC_SITE_KEY}/home/${pageName}`; + const componentPath = `${pagePath}/pagecontent`; + + before("Create test page and multiple components", () => { + addSimplePage(`/sites/${GENERIC_SITE_KEY}/home`, pageName, pageName, "en", "simple", [ + { + name: "pagecontent", + primaryNodeType: "jnt:contentList", + }, + ]).then(() => { + ['testArea', 'testArea2'].forEach((componentName) => { + addNode({ + parentPathOrId: componentPath, + name: componentName, + primaryNodeType: "javascriptExample:testAreas", + }); + }); + }); + }); + + beforeEach('Login and visit test page', () => { + cy.login(); + cy.visit(`/jahia/jcontent/${GENERIC_SITE_KEY}/en/pages/home/${pageName}`); + }); + + afterEach('Logout', () => cy.logout()); + + it(`${pageName}: Area with parent (current node) should render`, () => { + cy.iframe("#page-builder-frame-1").within(() => { + cy.get('div[data-testid="areaWithParent"]') + .find('div[type="area"]') + .should("be.visible"); + }); + }); + + it(`${pageName}: Area with home page as parent should render`, () => { + cy.iframe("#page-builder-frame-1").within(() => { + cy.get('div[data-testid="areaWithHomeParent"]') + .find('div[type="area"]') + .should("be.visible"); + }); + }); + + it(`${pageName}: Area with site root as parent should render`, () => { + cy.iframe("#page-builder-frame-1").within(() => { + cy.get('div[data-testid="areaWithSiteParent"]') + .find('div[type="area"]') + .should("be.visible"); + }); + }); + + it(`${pageName}: Area without parent should create area as subnode of the page`, () => { + const areaName = 'basicArea'; + getNodeByPath(`${pagePath}/${areaName}`).then((area) => { + const msg = `${areaName} should be created as a subnode of the page`; + expect(area?.data?.jcr?.nodeByPath?.name, msg).to.equal(areaName); + }); + }); + + it(`${pageName}: Area with parent should create distinct areas as subnodes of each component`, () => { + const areaName = 'parentArea'; + getNodeByPath(`${componentPath}/testArea/${areaName}`).then((area) => { + const msg = `${areaName} should be created as a subnode of the first test area component`; + expect(area?.data?.jcr?.nodeByPath?.name, msg).to.equal(areaName); + }); + getNodeByPath(`${componentPath}/testArea2/${areaName}`).then((area) => { + const msg = `${areaName} should be created as a subnode of the second test area component`; + expect(area?.data?.jcr?.nodeByPath?.name, msg).to.equal(areaName); + }); + }); + + it(`${pageName}: Area with parent should NOT create area at the page level`, () => { + const areaName = 'parentArea'; + getNodeByPath(`${pagePath}/${areaName}`).then((area) => { + const msg = `${areaName} should NOT exist at the page level when parent is specified`; + expect(area?.data?.jcr?.nodeByPath, msg).to.not.exist; + }); + }); + + it(`${pageName}: Area with home page as parent should store area under home`, () => { + const areaName = 'areaAtHomePage'; + getNodeByPath(`/sites/${GENERIC_SITE_KEY}/home/${areaName}`).then((area) => { + const msg = `${areaName} should be created as a subnode of the home page`; + expect(area?.data?.jcr?.nodeByPath?.name, msg).to.equal(areaName); + }); + }); + + it(`${pageName}: Area with site root as parent should store area under site root`, () => { + const areaName = 'areaAtSiteRoot'; + getNodeByPath(`/sites/${GENERIC_SITE_KEY}/${areaName}`).then((area) => { + const msg = `${areaName} should be created as a subnode of the site root`; + expect(area?.data?.jcr?.nodeByPath?.name, msg).to.equal(areaName); + }); + }); +});