diff --git a/src/visualBuilder/components/fieldLabelWrapper.tsx b/src/visualBuilder/components/fieldLabelWrapper.tsx
index 2ef77e6e..2380613b 100644
--- a/src/visualBuilder/components/fieldLabelWrapper.tsx
+++ b/src/visualBuilder/components/fieldLabelWrapper.tsx
@@ -111,9 +111,11 @@ function FieldLabelWrapperComponent(
const allPaths = uniqBy(
[
props.fieldMetadata,
- ...props.parentPaths.map((path) => {
- return extractDetailsFromCslp(path);
- }),
+ ...props.parentPaths
+ .filter((path) => path)
+ .map((path) => {
+ return extractDetailsFromCslp(path);
+ }),
],
"cslpValue"
);
@@ -140,12 +142,14 @@ function FieldLabelWrapperComponent(
const domAncestor = eventDetails.editableElement.closest(`[data-cslp]:not([data-cslp^="${props.fieldMetadata.content_type_uid}"])`);
if(domAncestor) {
const domAncestorCslp = domAncestor.getAttribute("data-cslp");
- const domAncestorDetails = extractDetailsFromCslp(domAncestorCslp!);
- const domAncestorContentTypeUid = domAncestorDetails.content_type_uid;
- const domAncestorContentParent = referenceData?.find(data => data.contentTypeUid === domAncestorContentTypeUid);
- if(domAncestorContentParent) {
- referenceFieldName = domAncestorContentParent.referenceFieldName;
- parentContentTypeName = domAncestorContentParent.contentTypeTitle;
+ if (domAncestorCslp) {
+ const domAncestorDetails = extractDetailsFromCslp(domAncestorCslp);
+ const domAncestorContentTypeUid = domAncestorDetails.content_type_uid;
+ const domAncestorContentParent = referenceData?.find(data => data.contentTypeUid === domAncestorContentTypeUid);
+ if(domAncestorContentParent) {
+ referenceFieldName = domAncestorContentParent.referenceFieldName;
+ parentContentTypeName = domAncestorContentParent.contentTypeTitle;
+ }
}
}
}
diff --git a/src/visualBuilder/generators/__test__/generateOverlay.test.ts b/src/visualBuilder/generators/__test__/generateOverlay.test.ts
new file mode 100644
index 00000000..02de3274
--- /dev/null
+++ b/src/visualBuilder/generators/__test__/generateOverlay.test.ts
@@ -0,0 +1,63 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { sendFieldEvent } from "../generateOverlay";
+import { VisualBuilder } from "../..";
+import { VisualBuilderPostMessageEvents } from "../../utils/types/postMessage.types";
+import visualBuilderPostMessage from "../../utils/visualBuilderPostMessage";
+import { FieldSchemaMap } from "../../utils/fieldSchemaMap";
+import { extractDetailsFromCslp } from "../../../cslp/cslpdata";
+
+vi.mock("../../utils/visualBuilderPostMessage", () => ({
+ default: {
+ send: vi.fn(),
+ },
+}));
+
+vi.mock("../../utils/fieldSchemaMap", () => ({
+ FieldSchemaMap: {
+ getFieldSchema: vi.fn().mockResolvedValue({
+ display_name: "Test Field",
+ data_type: "text",
+ }),
+ },
+}));
+
+vi.mock("../../../cslp/cslpdata", () => ({
+ extractDetailsFromCslp: vi.fn(),
+}));
+
+describe("sendFieldEvent", () => {
+ let previousSelectedEditableDOM: HTMLElement;
+ let visualBuilderContainer: HTMLElement;
+
+ beforeEach(() => {
+ previousSelectedEditableDOM = document.createElement("div");
+ previousSelectedEditableDOM.setAttribute("contenteditable", "true");
+ previousSelectedEditableDOM.innerText = "Test content";
+ document.body.appendChild(previousSelectedEditableDOM);
+
+ visualBuilderContainer = document.createElement("div");
+ document.body.appendChild(visualBuilderContainer);
+
+ VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM =
+ previousSelectedEditableDOM;
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = "";
+ vi.clearAllMocks();
+ });
+
+ it("should return early and not send event when data-cslp attribute is invalid", () => {
+ previousSelectedEditableDOM.setAttribute("data-cslp", "");
+
+ sendFieldEvent({
+ visualBuilderContainer,
+ eventType: VisualBuilderPostMessageEvents.UPDATE_FIELD,
+ });
+
+ expect(extractDetailsFromCslp).not.toHaveBeenCalled();
+ expect(FieldSchemaMap.getFieldSchema).not.toHaveBeenCalled();
+ expect(visualBuilderPostMessage?.send).not.toHaveBeenCalled();
+ });
+});
+
diff --git a/src/visualBuilder/generators/generateOverlay.tsx b/src/visualBuilder/generators/generateOverlay.tsx
index e09e9be9..595cefff 100644
--- a/src/visualBuilder/generators/generateOverlay.tsx
+++ b/src/visualBuilder/generators/generateOverlay.tsx
@@ -177,9 +177,12 @@ export function sendFieldEvent(options: ISendFieldEventParams): void {
? actualEditedElement.innerText
: actualEditedElement.textContent;
- const fieldMetadata = extractDetailsFromCslp(
- previousSelectedEditableDOM.getAttribute("data-cslp") as string
- );
+ const cslpData = previousSelectedEditableDOM.getAttribute("data-cslp");
+ if (!cslpData) {
+ return;
+ }
+
+ const fieldMetadata = extractDetailsFromCslp(cslpData);
FieldSchemaMap.getFieldSchema(
fieldMetadata.content_type_uid,
diff --git a/src/visualBuilder/utils/__test__/getCsDataOfElement.test.ts b/src/visualBuilder/utils/__test__/getCsDataOfElement.test.ts
index 307b52fe..ad387095 100644
--- a/src/visualBuilder/utils/__test__/getCsDataOfElement.test.ts
+++ b/src/visualBuilder/utils/__test__/getCsDataOfElement.test.ts
@@ -217,4 +217,46 @@ describe("getDOMEditStack", () => {
const editStack = getDOMEditStack(leafEl);
expect(editStack.length).toBe(2);
});
+
+ test("get dom edit stack should filter out elements with same prefix", () => {
+ const dom = new JSDOM(`
+
+ `).window.document;
+ const leafEl = dom.querySelector(
+ '[data-cslp="page.pageuid.en-us.group.field1.nested.leaf"]'
+ ) as HTMLElement;
+ const editStack = getDOMEditStack(leafEl);
+ // Should only have one entry since all have same prefix (page.pageuid.en-us)
+ expect(editStack.length).toBe(1);
+ expect(editStack[0].content_type_uid).toBe("page");
+ expect(editStack[0].entry_uid).toBe("pageuid");
+ });
+
+ test("get dom edit stack should filter out elements with invalid data-cslp attribute", () => {
+ const dom = new JSDOM(`
+
+ `).window.document;
+ const leafEl = dom.querySelector(
+ '[data-cslp="blog.bloguid.fr-fr.title.name"]'
+ ) as HTMLElement;
+ const editStack = getDOMEditStack(leafEl);
+ // Should have two entries (page, blog) - invalid cslp attributes (empty or no value) should be filtered out
+ expect(editStack.length).toBe(2);
+ expect(editStack[0].content_type_uid).toBe("page");
+ expect(editStack[1].content_type_uid).toBe("blog");
+ });
});
diff --git a/src/visualBuilder/utils/__test__/getEntryIdentifiersInCurrentPage.test.ts b/src/visualBuilder/utils/__test__/getEntryIdentifiersInCurrentPage.test.ts
index 2d057b31..7f50ce74 100644
--- a/src/visualBuilder/utils/__test__/getEntryIdentifiersInCurrentPage.test.ts
+++ b/src/visualBuilder/utils/__test__/getEntryIdentifiersInCurrentPage.test.ts
@@ -47,4 +47,17 @@ describe("getEntryIdentifiersInCurrentPage", () => {
expect(entriesInCurrentPage.length).toBe(0);
expect(entriesInCurrentPage).toEqual([]);
});
+
+ test("should filter out elements with empty data-cslp attribute and not break", () => {
+ document.body.innerHTML = `
+
+
Empty CSLP
+ Empty CSLP
+ Valid CSLP
+
+ `;
+ const { entriesInCurrentPage } = getEntryIdentifiersInCurrentPage();
+ expect(entriesInCurrentPage.length).toBe(1);
+ expect(entriesInCurrentPage[0].entryUid).toBe('bltf5bb5f8fb088a332');
+ });
});
diff --git a/src/visualBuilder/utils/__test__/getVisualBuilderRedirectionUrl.test.ts b/src/visualBuilder/utils/__test__/getVisualBuilderRedirectionUrl.test.ts
index e7232ad6..c09ec900 100644
--- a/src/visualBuilder/utils/__test__/getVisualBuilderRedirectionUrl.test.ts
+++ b/src/visualBuilder/utils/__test__/getVisualBuilderRedirectionUrl.test.ts
@@ -84,4 +84,25 @@ describe('getVisualBuilderRedirectionUrl', () => {
const result = getVisualBuilderRedirectionUrl();
expect(result.toString()).toBe('https://app.example.com/#!/stack/12345/visual-builder?branch=main&environment=production&target-url=https%3A%2F%2Fexample.com%2F');
});
+
+ it('should ignore invalid data-cslp attribute and use locale from config', () => {
+ document.body.innerHTML = '';
+ Config.get.mockReturnValue({
+ stackDetails: {
+ branch: 'main',
+ apiKey: '12345',
+ environment: 'production',
+ locale: 'en-US'
+ },
+ clientUrlParams: {
+ url: 'https://app.example.com'
+ }
+ });
+
+ const result = getVisualBuilderRedirectionUrl();
+ // Should use locale from config when data-cslp attribute is invalid (empty or no value)
+ expect(result.toString()).toBe('https://app.example.com/#!/stack/12345/visual-builder?branch=main&environment=production&target-url=https%3A%2F%2Fexample.com%2F&locale=en-US');
+ // Should not call extractDetailsFromCslp for invalid cslp
+ expect(extractDetailsFromCslp).not.toHaveBeenCalled();
+ });
});
\ No newline at end of file
diff --git a/src/visualBuilder/utils/__test__/updateFocussedState.test.ts b/src/visualBuilder/utils/__test__/updateFocussedState.test.ts
index f427eaf0..835114c4 100644
--- a/src/visualBuilder/utils/__test__/updateFocussedState.test.ts
+++ b/src/visualBuilder/utils/__test__/updateFocussedState.test.ts
@@ -55,11 +55,13 @@ describe("updateFocussedState", () => {
document.body.appendChild(previousSelectedEditableDOM);
VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM =
previousSelectedEditableDOM;
+ vi.clearAllMocks();
});
afterEach(() => {
document.body.innerHTML = "";
VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM =
null;
+ vi.clearAllMocks();
});
it("should return early if required elements are not provided", async () => {
const result = await updateFocussedState({
@@ -233,6 +235,38 @@ describe("updateFocussedState", () => {
expect.any(Boolean)
);
});
+
+ it("should return early if data-cslp attribute is invalid", async () => {
+ const editableElementMock = document.createElement("div");
+ editableElementMock.setAttribute("data-cslp", "");
+ const visualBuilderContainerMock = document.createElement("div");
+ const overlayWrapperMock = document.createElement("div");
+ const focusedToolbarMock = document.createElement("div");
+ const resizeObserverMock = {
+ disconnect: vi.fn(),
+ } as unknown as ResizeObserver;
+
+ const previousSelectedEditableDOM = document.createElement("div");
+ previousSelectedEditableDOM.setAttribute("data-cslp", "content_type_uid.entry_uid.locale.field_path");
+ document.body.appendChild(previousSelectedEditableDOM);
+ VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM =
+ previousSelectedEditableDOM;
+
+ document.querySelector = vi.fn().mockReturnValue(previousSelectedEditableDOM);
+
+ const result = await updateFocussedState({
+ editableElement: editableElementMock,
+ visualBuilderContainer: visualBuilderContainerMock,
+ overlayWrapper: overlayWrapperMock,
+ focusedToolbar: focusedToolbarMock,
+ resizeObserver: resizeObserverMock,
+ });
+
+ // Should return early without processing
+ expect(result).toBeUndefined();
+ expect(getEntryPermissionsCached).not.toHaveBeenCalled();
+ expect(addFocusOverlay).not.toHaveBeenCalled();
+ });
});
describe("updateFocussedStateOnMutation", () => {
diff --git a/src/visualBuilder/utils/getCsDataOfElement.ts b/src/visualBuilder/utils/getCsDataOfElement.ts
index 2cce0888..7139a50f 100644
--- a/src/visualBuilder/utils/getCsDataOfElement.ts
+++ b/src/visualBuilder/utils/getCsDataOfElement.ts
@@ -54,7 +54,11 @@ export function getDOMEditStack(ele: Element): CslpData[] {
const cslpSet: string[] = [];
let curr: any = ele.closest(`[${DATA_CSLP_ATTR_SELECTOR}]`);
while (curr) {
- const cslp = curr.getAttribute(DATA_CSLP_ATTR_SELECTOR)!;
+ const cslp = curr.getAttribute(DATA_CSLP_ATTR_SELECTOR);
+ if (!cslp) {
+ curr = curr.parentElement?.closest(`[${DATA_CSLP_ATTR_SELECTOR}]`);
+ continue;
+ }
const entryPrefix = getPrefix(cslp);
const hasSamePrevPrefix = getPrefix(cslpSet.at(0) || "").startsWith(
entryPrefix
@@ -64,5 +68,5 @@ export function getDOMEditStack(ele: Element): CslpData[] {
}
curr = curr.parentElement?.closest(`[${DATA_CSLP_ATTR_SELECTOR}]`);
}
- return cslpSet.map((cslp) => extractDetailsFromCslp(cslp));
+ return cslpSet.filter((cslp) => cslp).map((cslp) => extractDetailsFromCslp(cslp));
}
diff --git a/src/visualBuilder/utils/getEntryIdentifiersInCurrentPage.ts b/src/visualBuilder/utils/getEntryIdentifiersInCurrentPage.ts
index 7ea4f741..243bc4a1 100644
--- a/src/visualBuilder/utils/getEntryIdentifiersInCurrentPage.ts
+++ b/src/visualBuilder/utils/getEntryIdentifiersInCurrentPage.ts
@@ -14,9 +14,9 @@ export function getEntryIdentifiersInCurrentPage(): EntryIdentifiers {
);
const uniqueEntriesMap = new Map();
elementsWithCslp.forEach((element) => {
- const cslpData = extractDetailsFromCslp(
- element.getAttribute("data-cslp") as string
- );
+ const cslpValue = element.getAttribute("data-cslp");
+ if (!cslpValue) return;
+ const cslpData = extractDetailsFromCslp(cslpValue);
uniqueEntriesMap.set(cslpData.entry_uid,
{
entryUid: cslpData.entry_uid,
diff --git a/src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts b/src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts
index 955e466c..02e4659b 100644
--- a/src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts
+++ b/src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts
@@ -22,16 +22,18 @@ export default function getVisualBuilderRedirectionUrl(): URL {
// get the locale from the data cslp attribute
const elementWithDataCslp = document.querySelector(`[data-cslp]`);
+ let localeToUse = locale;
if (elementWithDataCslp) {
- const cslpData = elementWithDataCslp.getAttribute(
- "data-cslp"
- ) as string;
- const { locale } = extractDetailsFromCslp(cslpData);
+ const cslpData = elementWithDataCslp.getAttribute("data-cslp");
+ if (cslpData) {
+ const { locale: cslpLocale } = extractDetailsFromCslp(cslpData);
+ localeToUse = cslpLocale;
+ }
+ }
- searchParams.set("locale", locale);
- } else if (locale) {
- searchParams.set("locale", locale);
+ if (localeToUse) {
+ searchParams.set("locale", localeToUse);
}
const completeURL = new URL(
diff --git a/src/visualBuilder/utils/updateFocussedState.ts b/src/visualBuilder/utils/updateFocussedState.ts
index a5799fab..a6a33509 100644
--- a/src/visualBuilder/utils/updateFocussedState.ts
+++ b/src/visualBuilder/utils/updateFocussedState.ts
@@ -136,6 +136,9 @@ export async function updateFocussedState({
}
const cslp = editableElement?.getAttribute("data-cslp") || "";
+ if (!cslp) {
+ return;
+ }
const fieldMetadata = extractDetailsFromCslp(cslp);
hideHoverOutline(visualBuilderContainer);