Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .chachalog/MphU2FML.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jahiaComponent(
displayName: "test Areas (react)",
componentType: "view",
},
() => (
(_, { currentNode, renderContext }) => (
<>
<h2>React JArea test component</h2>

Expand Down Expand Up @@ -57,6 +57,21 @@ jahiaComponent(
}}
/>
</div>

<h2>Area with parent node</h2>
<div data-testid="areaWithParent">
<Area name="parentArea" parent={currentNode} />
</div>

<h2>Area with home page as parent</h2>
<div data-testid="areaWithHomeParent">
<Area name="areaAtHomePage" parent={renderContext.getSite().getHome()} />
</div>

<h2>Area with site root as parent</h2>
<div data-testid="areaWithSiteParent">
<Area name="areaAtSiteRoot" parent={renderContext.getSite()} />
</div>
</>
),
);
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public class RenderHelper {
private static final Set<String> ABSOLUTEAREA_ALLOWED_ATTRIBUTES = Set.of("name", "parent", "view",
"allowedNodeTypes", "numberOfItems", "nodeType", "editable", "areaType", "limitedAbsoluteAreaEdit",
"parameters");
private static final Set<String> AREA_ALLOWED_ATTRIBUTES = Set.of("name", "view", "allowedNodeTypes",
private static final Set<String> AREA_ALLOWED_ATTRIBUTES = Set.of("name", "parent", "view", "allowedNodeTypes",
"numberOfItems", "nodeType", "editable", "parameters");

private JCRSessionFactory jcrSessionFactory;
Expand Down Expand Up @@ -409,15 +409,22 @@ public String renderAbsoluteArea(Map<String, Object> attr, RenderContext renderC
public String renderArea(Map<String, Object> 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);
}

Expand Down
13 changes: 13 additions & 0 deletions javascript-modules-library/src/components/Area.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { JCRNodeWrapper } from "org.jahia.services.content";
import { createElement, type JSX } from "react";
import { useServerContext } from "../hooks/useServerContext.js";

Expand All @@ -8,6 +9,7 @@ import { useServerContext } from "../hooks/useServerContext.js";
*/
export function Area({
name,
parent,
view,
allowedNodeTypes,
numberOfItems,
Expand All @@ -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 `<parent path>/<name>`), 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. */
Expand Down Expand Up @@ -47,6 +59,7 @@ export function Area({
__html: server.render.renderArea(
{
name,
parent,
view,
allowedNodeTypes,
numberOfItems,
Expand Down
107 changes: 107 additions & 0 deletions tests/cypress/e2e/ui/areaParentTest.cy.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Loading