diff --git a/package-lock.json b/package-lock.json index e288a0e9..0639d829 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "goober": "^2.1.16", "lodash-es": "^4.17.21", "mustache": "^4.2.0", - "preact": "^10.27.2", + "preact": "^10.28.2", "uuid": "^11.0.3" }, "devDependencies": { @@ -6738,9 +6738,9 @@ } }, "node_modules/preact": { - "version": "10.27.2", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz", - "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", + "version": "10.28.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz", + "integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==", "license": "MIT", "funding": { "type": "opencollective", diff --git a/package.json b/package.json index 6b1ee040..cd860cef 100644 --- a/package.json +++ b/package.json @@ -85,18 +85,18 @@ "url": "https://github.com/contentstack/live-preview-sdk.git" }, "dependencies": { - "@preact/signals": "^2.0.0", - "@preact/compat": "^18.3.1", "@floating-ui/dom": "^1.7.2", + "@preact/compat": "^18.3.1", + "@preact/signals": "^2.0.0", "classnames": "^2.5.1", "dayjs": "^1.11.13", "deepsignal": "^1.5.0", - "goober": "^2.1.16", "dompurify": "^3.2.3", "get-xpath": "^3.2.0", + "goober": "^2.1.16", "lodash-es": "^4.17.21", "mustache": "^4.2.0", - "preact": "^10.27.2", + "preact": "^10.28.2", "uuid": "^11.0.3" }, "optionalDependencies": { diff --git a/src/livePreview/editButton/__test__/editButtonAction.test.ts b/src/livePreview/editButton/__test__/editButtonAction.test.ts index 67b388cd..db2acca8 100644 --- a/src/livePreview/editButton/__test__/editButtonAction.test.ts +++ b/src/livePreview/editButton/__test__/editButtonAction.test.ts @@ -598,7 +598,7 @@ describe("cslp tooltip", () => { }); new LivePreview(); - let tooltip = document.querySelector( + const tooltip = document.querySelector( "[data-test-id='cs-cslp-tooltip']" ); const tooltipParent = tooltip?.parentNode; diff --git a/src/preview/__test__/contentstack-live-preview-HOC.test.ts b/src/preview/__test__/contentstack-live-preview-HOC.test.ts index 5c765332..fdb314e2 100644 --- a/src/preview/__test__/contentstack-live-preview-HOC.test.ts +++ b/src/preview/__test__/contentstack-live-preview-HOC.test.ts @@ -105,7 +105,8 @@ describe("Live Preview HOC init", () => { expect(livePreviewPostMessageSpy).toHaveBeenCalledTimes(1); expect(visualBuilderPostMessageSpy).toHaveBeenCalledWith('init', { isSSR: true, href: 'http://localhost:3000/' }); expect(visualBuilderPostMessageSpy).toHaveBeenCalledWith('send-variant-and-locale'); - expect(visualBuilderPostMessageSpy).toHaveBeenCalledTimes(2); + expect(visualBuilderPostMessageSpy).toHaveBeenCalledWith('get-highlight-variant-fields-status'); + expect(visualBuilderPostMessageSpy).toHaveBeenCalledTimes(3); }); test("should return the existing live preview instance if it is already initialized", async () => { diff --git a/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts b/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts index 412d1a40..f2528c65 100644 --- a/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts +++ b/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts @@ -7,6 +7,20 @@ import { afterEach, MockedObject, } from "vitest"; + +const { debounce } = vi.hoisted(() => { + return { + debounce: vi.fn((fn: any, _delay: number, _options?: any) => { + return fn; + }), + }; +}); +vi.mock("lodash-es", () => { + return { + ...vi.importActual("lodash-es"), + debounce: debounce, + }; +}); import { useVariantFieldsPostMessageEvent, addVariantFieldClass, @@ -14,6 +28,8 @@ import { setAudienceMode, setVariant, setLocale, + setHighlightVariantFields, + getHighlightVariantFieldsStatus, } from "../../../visualBuilder/eventManager/useVariantsPostMessageEvent"; import { VisualBuilderPostMessageEvents } from "../../../visualBuilder/utils/types/postMessage.types"; import { VisualBuilder } from "../../../visualBuilder"; @@ -21,6 +37,7 @@ import { FieldSchemaMap } from "../../../visualBuilder/utils/fieldSchemaMap"; import { visualBuilderStyles } from "../../../visualBuilder/visualBuilder.style"; import visualBuilderPostMessage from "../../../visualBuilder/utils/visualBuilderPostMessage"; import { EventManager } from "@contentstack/advanced-post-message"; +import { updateVariantClasses } from "../../../visualBuilder/eventManager/useRecalculateVariantDataCSLPValues"; import * as cslpdata from "../../../cslp/cslpdata"; const mockVisualBuilderPostMessage = @@ -44,6 +61,12 @@ vi.mock("../../../visualBuilder/utils/fieldSchemaMap", () => { }; }); +vi.mock("../../../visualBuilder/eventManager/useRecalculateVariantDataCSLPValues", () => { + return { + updateVariantClasses: vi.fn(), + }; +}); + vi.mock("../../../visualBuilder", () => { return { VisualBuilder: { @@ -52,6 +75,7 @@ vi.mock("../../../visualBuilder", () => { audienceMode: false, variant: null, locale: "en-us", + highlightVariantFields: false, }, }, }, @@ -60,11 +84,13 @@ vi.mock("../../../visualBuilder", () => { // Create a more realistic mock of the CSS modules const cssClassMock = "go109692693"; // Match the actual generated class name +const cssOutlineClassMock = "go109692694"; -vi.mock("../../../visualBuilder.style", () => { +vi.mock("../../visualBuilder.style", () => { return { visualBuilderStyles: () => ({ "visual-builder__variant-field": cssClassMock, + "visual-builder__variant-field-outline": cssOutlineClassMock, }), }; }); @@ -98,7 +124,7 @@ const mockQuerySelectorAll = vi.fn().mockImplementation((selector) => { // Return different mocks based on selector if (selector === "[data-cslp]") { return mockElements; - } else if (selector === `.${cssClassMock}`) { + } else if (selector === `.${cssOutlineClassMock}`) { return mockElements; // For onlyHighlighted=true case } else if ( selector === @@ -111,6 +137,12 @@ const mockQuerySelectorAll = vi.fn().mockImplementation((selector) => { return []; }); +describe("debounceAddVariantFieldClass", () => { + // Moved to the top of the file to ensure it is mocks are not cleared before the test is run + it("should debounce addVariantFieldClass calls", () => { + expect(debounce).toHaveBeenCalledWith(addVariantFieldClass, 1000, { trailing: true }); + }); +}); describe("useVariantFieldsPostMessageEvent", () => { // Store original document.querySelectorAll const originalQuerySelectorAll = document.querySelectorAll; @@ -130,7 +162,7 @@ describe("useVariantFieldsPostMessageEvent", () => { it("should register all event listeners", () => { // Call the function - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Verify event listeners are registered expect(mockVisualBuilderPostMessage.on).toHaveBeenCalledWith( @@ -161,7 +193,7 @@ describe("useVariantFieldsPostMessageEvent", () => { it("should handle GET_VARIANT_ID event", () => { // Register event handlers - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Extract the event handler function const call = mockVisualBuilderPostMessage.on.mock.calls.find( @@ -185,7 +217,7 @@ describe("useVariantFieldsPostMessageEvent", () => { it("should handle GET_LOCALE event", () => { // Register event handlers - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Extract the event handler function const call = mockVisualBuilderPostMessage.on.mock.calls.find( @@ -206,7 +238,7 @@ describe("useVariantFieldsPostMessageEvent", () => { it("should handle SET_AUDIENCE_MODE event", () => { // Register event handlers - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Extract the event handler function const call = mockVisualBuilderPostMessage.on.mock.calls.find( @@ -227,7 +259,7 @@ describe("useVariantFieldsPostMessageEvent", () => { it("should handle SHOW_VARIANT_FIELDS event", () => { // Register event handlers - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Extract the event handler function const call = mockVisualBuilderPostMessage.on.mock.calls.find( @@ -252,16 +284,16 @@ describe("useVariantFieldsPostMessageEvent", () => { // Verify that classes were added to elements correctly expect(mockElements[0].classList.add).toHaveBeenCalledWith( - visualBuilderStyles()["visual-builder__variant-field"] + "visual-builder__variant-field" ); expect(mockElements[0].classList.add).toHaveBeenCalledWith( - "visual-builder__variant-field" + visualBuilderStyles()["visual-builder__variant-field-outline"] ); }); it("should handle REMOVE_VARIANT_FIELDS event with onlyHighlighted=true", () => { // Register event handlers - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Extract the event handler function const call = mockVisualBuilderPostMessage.on.mock.calls.find( @@ -276,20 +308,20 @@ describe("useVariantFieldsPostMessageEvent", () => { // Verify querySelectorAll was called with the correct selector expect(mockQuerySelectorAll).toHaveBeenCalledWith( - `.${visualBuilderStyles()["visual-builder__variant-field"]}` + `.${visualBuilderStyles()["visual-builder__variant-field-outline"]}` ); // Verify that classes were removed from elements correctly mockElements.forEach((element) => { expect(element.classList.remove).toHaveBeenCalledWith( - visualBuilderStyles()["visual-builder__variant-field"] + visualBuilderStyles()["visual-builder__variant-field-outline"] ); }); }); it("should handle REMOVE_VARIANT_FIELDS event with onlyHighlighted=false", () => { // Register event handlers - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Extract the event handler function const call = mockVisualBuilderPostMessage.on.mock.calls.find( @@ -312,7 +344,7 @@ describe("useVariantFieldsPostMessageEvent", () => { expect(element.classList.remove).toHaveBeenCalledWith( "visual-builder__disabled-variant-field", "visual-builder__variant-field", - visualBuilderStyles()["visual-builder__variant-field"], + visualBuilderStyles()["visual-builder__variant-field-outline"], "visual-builder__base-field", "visual-builder__lower-order-variant-field" ); @@ -339,9 +371,9 @@ describe("addVariantFieldClass", () => { it("should add classes to elements correctly based on data-cslp attribute", () => { const variantUid = "variant-123"; - const highlightVariantFields = true; + VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields = true; - addVariantFieldClass(variantUid, highlightVariantFields, []); + addVariantFieldClass(variantUid); // Verify querySelectorAll was called with the correct selector expect(mockQuerySelectorAll).toHaveBeenCalledWith("[data-cslp]"); @@ -349,10 +381,10 @@ describe("addVariantFieldClass", () => { // First element has the variant ID expect(mockElements[0].getAttribute).toHaveBeenCalledWith("data-cslp"); expect(mockElements[0].classList.add).toHaveBeenCalledWith( - visualBuilderStyles()["visual-builder__variant-field"] + "visual-builder__variant-field" ); expect(mockElements[0].classList.add).toHaveBeenCalledWith( - "visual-builder__variant-field" + visualBuilderStyles()["visual-builder__variant-field-outline"] ); // Second element does not start with 'v2:' @@ -370,18 +402,18 @@ describe("addVariantFieldClass", () => { it("should not add highlight class when highlightVariantFields is false", () => { const variantUid = "variant-123"; - const highlightVariantFields = false; + VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields = false; - addVariantFieldClass(variantUid, highlightVariantFields, []); + addVariantFieldClass(variantUid); // First element has the variant ID but should not get highlight class expect(mockElements[0].getAttribute).toHaveBeenCalledWith("data-cslp"); - expect(mockElements[0].classList.add).not.toHaveBeenCalledWith( - visualBuilderStyles()["visual-builder__variant-field"] - ); expect(mockElements[0].classList.add).toHaveBeenCalledWith( "visual-builder__variant-field" ); + expect(mockElements[0].classList.add).not.toHaveBeenCalledWith( + visualBuilderStyles()["visual-builder__variant-field-outline"] + ); }); it("should handle lower order variant fields correctly", () => { @@ -392,10 +424,9 @@ describe("addVariantFieldClass", () => { } }); const variantUid = "variant-456"; - const highlightVariantFields = false; const variantOrder = ["variant-123", "variant-456"]; - - addVariantFieldClass(variantUid, highlightVariantFields, variantOrder); + VisualBuilder.VisualBuilderGlobalState.value.variantOrder = variantOrder; + addVariantFieldClass(variantUid); // Verify that classes were added to elements correctly expect(mockElements[0].classList.add).toHaveBeenCalledWith("visual-builder__variant-field", "visual-builder__lower-order-variant-field"); @@ -424,13 +455,13 @@ describe("removeVariantFieldClass", () => { // Verify querySelectorAll was called with the correct selector expect(mockQuerySelectorAll).toHaveBeenCalledWith( - `.${visualBuilderStyles()["visual-builder__variant-field"]}` + `.${visualBuilderStyles()["visual-builder__variant-field-outline"]}` ); // Verify classes were removed mockElements.forEach((element) => { expect(element.classList.remove).toHaveBeenCalledWith( - visualBuilderStyles()["visual-builder__variant-field"] + visualBuilderStyles()["visual-builder__variant-field-outline"] ); }); }); @@ -448,7 +479,7 @@ describe("removeVariantFieldClass", () => { expect(element.classList.remove).toHaveBeenCalledWith( "visual-builder__disabled-variant-field", "visual-builder__variant-field", - visualBuilderStyles()["visual-builder__variant-field"], + visualBuilderStyles()["visual-builder__variant-field-outline"], "visual-builder__base-field", "visual-builder__lower-order-variant-field" ); @@ -474,6 +505,7 @@ describe("State Management Functions", () => { VisualBuilder.VisualBuilderGlobalState.value.audienceMode = false; VisualBuilder.VisualBuilderGlobalState.value.variant = null; VisualBuilder.VisualBuilderGlobalState.value.locale = "en-us"; + VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields = false; }); it("setAudienceMode should update global state", () => { @@ -509,4 +541,125 @@ describe("State Management Functions", () => { "en-us" ); }); + + it("setHighlightVariantFields should update global state", () => { + setHighlightVariantFields(true); + expect(VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields).toBe( + true + ); + + setHighlightVariantFields(false); + expect(VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields).toBe( + false + ); + }); +}); + +describe("getHighlightVariantFieldsStatus", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should return highlight status when successful", async () => { + const mockResponse = { highlightVariantFields: true }; + (mockVisualBuilderPostMessage.send as any).mockResolvedValue(mockResponse); + + const result = await getHighlightVariantFieldsStatus(); + + expect(mockVisualBuilderPostMessage.send).toHaveBeenCalledWith( + VisualBuilderPostMessageEvents.GET_HIGHLIGHT_VARIANT_FIELDS_STATUS + ); + expect(result).toEqual(mockResponse); + }); + + it("should return default false when response is null", async () => { + (mockVisualBuilderPostMessage.send as any).mockResolvedValue(null); + + const result = await getHighlightVariantFieldsStatus(); + + expect(result).toEqual({ highlightVariantFields: false }); + }); + + it("should return default false when request fails", async () => { + (mockVisualBuilderPostMessage.send as any).mockRejectedValue( + new Error("Network error") + ); + + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + const result = await getHighlightVariantFieldsStatus(); + + expect(result).toEqual({ highlightVariantFields: false }); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Failed to get highlight variant fields status:", + expect.any(Error) + ); + + consoleErrorSpy.mockRestore(); + }); +}); + + +describe("useVariantFieldsPostMessageEvent SSR handling", () => { + const originalQuerySelectorAll = document.querySelectorAll; + + beforeEach(() => { + document.querySelectorAll = mockQuerySelectorAll; + vi.clearAllMocks(); + }); + + afterEach(() => { + document.querySelectorAll = originalQuerySelectorAll; + }); + + it("should call addVariantFieldClass directly when isSSR is true and variant is provided", () => { + useVariantFieldsPostMessageEvent({ isSSR: true }); + + const call = mockVisualBuilderPostMessage.on.mock.calls.find( + (call: any[]) => + call[0] === VisualBuilderPostMessageEvents.GET_VARIANT_ID + ); + const handler = call ? call[1] : null; + + vi.clearAllMocks(); + handler!({ data: { variant: "variant-123" } }); + + // Should call addVariantFieldClass directly (not updateVariantClasses) + expect(mockQuerySelectorAll).toHaveBeenCalledWith("[data-cslp]"); + expect(updateVariantClasses).not.toHaveBeenCalled(); + }); + + it("should call updateVariantClasses when isSSR is false", () => { + useVariantFieldsPostMessageEvent({ isSSR: false }); + + const call = mockVisualBuilderPostMessage.on.mock.calls.find( + (call: any[]) => + call[0] === VisualBuilderPostMessageEvents.GET_VARIANT_ID + ); + const handler = call ? call[1] : null; + + vi.clearAllMocks(); + handler!({ data: { variant: "variant-123" } }); + + // Should call updateVariantClasses (not addVariantFieldClass directly) + expect(updateVariantClasses).toHaveBeenCalled(); + expect(mockQuerySelectorAll).not.toHaveBeenCalled(); + }); + + it("should not call addVariantFieldClass when isSSR is true but variant is null", () => { + useVariantFieldsPostMessageEvent({ isSSR: true }); + + const call = mockVisualBuilderPostMessage.on.mock.calls.find( + (call: any[]) => + call[0] === VisualBuilderPostMessageEvents.GET_VARIANT_ID + ); + const handler = call ? call[1] : null; + + vi.clearAllMocks(); + handler!({ data: { variant: null } }); + + // Should not call addVariantFieldClass when variant is null + expect(mockQuerySelectorAll).not.toHaveBeenCalled(); + expect(updateVariantClasses).not.toHaveBeenCalled(); + }); }); diff --git a/src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts b/src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts index a577da4a..54142d82 100644 --- a/src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts +++ b/src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts @@ -3,6 +3,7 @@ import livePreviewPostMessage from "../../livePreview/eventManager/livePreviewEv import { LIVE_PREVIEW_POST_MESSAGE_EVENTS } from "../../livePreview/eventManager/livePreviewEventManager.constant"; import { DATA_CSLP_ATTR_SELECTOR } from "../utils/constants"; import { visualBuilderStyles } from "../visualBuilder.style"; +import { setHighlightVariantFields } from "./useVariantsPostMessageEvent"; const VARIANT_UPDATE_DELAY_MS: Readonly = 8000; @@ -19,15 +20,14 @@ export function useRecalculateVariantDataCSLPValues(): void { LIVE_PREVIEW_POST_MESSAGE_EVENTS.VARIANT_PATCH, (event) => { if (VisualBuilder.VisualBuilderGlobalState.value.audienceMode) { - updateVariantClasses(event.data); + setHighlightVariantFields(event.data.highlightVariantFields); + updateVariantClasses(); } } ); } -function updateVariantClasses({ - highlightVariantFields, - expectedCSLPValues, -}: OnAudienceModeVariantPatchUpdate): void { +export function updateVariantClasses(): void { + const highlightVariantFields = VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields; const variant = VisualBuilder.VisualBuilderGlobalState.value.variant; const observers: MutationObserver[] = []; @@ -46,20 +46,17 @@ function updateVariantClasses({ if (element.classList.contains("visual-builder__base-field")) { element.classList.remove("visual-builder__base-field"); } + const variantFieldClasses = ["visual-builder__variant-field"]; if (highlightVariantFields) { - element.classList.add( - visualBuilderStyles()["visual-builder__variant-field"], - "visual-builder__variant-field" - ); - } else { - element.classList.add("visual-builder__variant-field"); + variantFieldClasses.push(visualBuilderStyles()["visual-builder__variant-field-outline"]); } + element.classList.add(...variantFieldClasses); } else if ( !dataCslp.startsWith("v2:") && element.classList.contains("visual-builder__variant-field") ) { element.classList.remove( - visualBuilderStyles()["visual-builder__variant-field"], + visualBuilderStyles()["visual-builder__variant-field-outline"], "visual-builder__variant-field" ); element.classList.add("visual-builder__base-field"); @@ -70,7 +67,7 @@ function updateVariantClasses({ element.classList.contains("visual-builder__variant-field") ) { element.classList.remove( - visualBuilderStyles()["visual-builder__variant-field"], + visualBuilderStyles()["visual-builder__variant-field-outline"], "visual-builder__variant-field" ); element.classList.add("visual-builder__disabled-variant-field"); @@ -111,18 +108,15 @@ function updateVariantClasses({ if (element.classList.contains("visual-builder__base-field")) { element.classList.remove("visual-builder__base-field"); } + const variantFieldClasses = ["visual-builder__variant-field"]; if (highlightVariantFields) { - element.classList.add( - visualBuilderStyles()["visual-builder__variant-field"], - "visual-builder__variant-field" - ); - } else { - element.classList.add("visual-builder__variant-field"); + variantFieldClasses.push(visualBuilderStyles()["visual-builder__variant-field-outline"]); } + element.classList.add(...variantFieldClasses); } else if (!dataCslp.startsWith("v2:")) { if (element.classList.contains("visual-builder__variant-field")) { element.classList.remove( - visualBuilderStyles()["visual-builder__variant-field"], + visualBuilderStyles()["visual-builder__variant-field-outline"], "visual-builder__variant-field" ); } @@ -166,6 +160,7 @@ function updateVariantClasses({ }); observers.push(observer); + // TODO: Check if we could add attributeFilter to the observer to only observe the attribute changes for the data-cslp attribute. observer.observe(element, { attributes: true, childList: true, // Observe direct children diff --git a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts index b1d8a229..382a6976 100644 --- a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts @@ -3,6 +3,8 @@ import { visualBuilderStyles } from "../visualBuilder.style"; import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types"; import { FieldSchemaMap } from "../utils/fieldSchemaMap"; +import { updateVariantClasses } from "./useRecalculateVariantDataCSLPValues"; +import { debounce } from "lodash-es"; import { extractDetailsFromCslp } from "../../cslp/cslpdata"; interface VariantFieldsEvent { @@ -50,22 +52,24 @@ function isLowerOrderVariant(variant_uid: string, dataCslp: string, variantOrder return indexOfCslpVariant < indexOfCmsVariant; } + export function addVariantFieldClass( - variant_uid: string, - highlightVariantFields: boolean, - variantOrder: string[] + variant_uid: string ): void { + const variantOrder = VisualBuilder.VisualBuilderGlobalState.value.variantOrder; + const highlightVariantFields = VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields; const elements = document.querySelectorAll(`[data-cslp]`); elements.forEach((element) => { const dataCslp = element.getAttribute("data-cslp"); if (!dataCslp) return; if (dataCslp?.includes(variant_uid)) { - highlightVariantFields && + element.classList.add("visual-builder__variant-field"); + if (highlightVariantFields) { element.classList.add( - visualBuilderStyles()["visual-builder__variant-field"] + visualBuilderStyles()["visual-builder__variant-field-outline"] ); - element.classList.add("visual-builder__variant-field"); + } } else if (!dataCslp.startsWith("v2:")) { element.classList.add("visual-builder__base-field"); } @@ -78,16 +82,22 @@ export function addVariantFieldClass( }); } +export const debounceAddVariantFieldClass = debounce( + addVariantFieldClass, + 1000, + { trailing: true } +) as (variant_uid: string) => void; + export function removeVariantFieldClass( onlyHighlighted: boolean = false ): void { if (onlyHighlighted) { const variantElements = document.querySelectorAll( - `.${visualBuilderStyles()["visual-builder__variant-field"]}` + `.${visualBuilderStyles()["visual-builder__variant-field-outline"]}` ); variantElements.forEach((element) => { element.classList.remove( - visualBuilderStyles()["visual-builder__variant-field"] + visualBuilderStyles()["visual-builder__variant-field-outline"] ); }); } else { @@ -98,7 +108,7 @@ export function removeVariantFieldClass( element.classList.remove( "visual-builder__disabled-variant-field", "visual-builder__variant-field", - visualBuilderStyles()["visual-builder__variant-field"], + visualBuilderStyles()["visual-builder__variant-field-outline"], "visual-builder__base-field", "visual-builder__lower-order-variant-field" ); @@ -115,18 +125,52 @@ export function setVariant(uid: string | null): void { export function setLocale(locale: string): void { VisualBuilder.VisualBuilderGlobalState.value.locale = locale; } +export function setHighlightVariantFields(highlight: boolean): void { + VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields = highlight; +} +export function setVariantOrder(variantOrder: string[]): void { + VisualBuilder.VisualBuilderGlobalState.value.variantOrder = variantOrder; +} + +interface GetHighlightVariantFieldsStatusResponse { + highlightVariantFields: boolean; +} +export async function getHighlightVariantFieldsStatus(): Promise { + try { + const result = await visualBuilderPostMessage?.send( + VisualBuilderPostMessageEvents.GET_HIGHLIGHT_VARIANT_FIELDS_STATUS + ); + return result ?? { + highlightVariantFields: false, + }; + } catch (error) { + console.error("Failed to get highlight variant fields status:", error); + return { + highlightVariantFields: false, + }; + } +} -export function useVariantFieldsPostMessageEvent(): void { +export function useVariantFieldsPostMessageEvent({ isSSR }: { isSSR: boolean }): void { visualBuilderPostMessage?.on( VisualBuilderPostMessageEvents.GET_VARIANT_ID, (event: VariantEvent) => { - setVariant(event.data.variant); + const selectedVariant = event.data.variant; + setVariant(selectedVariant); // clear field schema when variant is changed. // this is required as we cache field schema // which contain a key isUnlinkedVariant. // This key can change when variant is changed, // so clear the field schema cache FieldSchemaMap.clear(); + if (isSSR) { + if (selectedVariant) { + addVariantFieldClass(selectedVariant); + } + } else { + // recalculate and apply classes + updateVariantClasses(); + } } ); visualBuilderPostMessage?.on( @@ -144,17 +188,18 @@ export function useVariantFieldsPostMessageEvent(): void { visualBuilderPostMessage?.on( VisualBuilderPostMessageEvents.SHOW_VARIANT_FIELDS, (event: VariantFieldsEvent) => { + setHighlightVariantFields(event.data.variant_data.highlightVariantFields); + setVariantOrder(event.data.variant_data.variantOrder || []); removeVariantFieldClass(); addVariantFieldClass( - event.data.variant_data.variant, - event.data.variant_data.highlightVariantFields, - event.data.variant_data.variantOrder + event.data.variant_data.variant ); } ); visualBuilderPostMessage?.on( VisualBuilderPostMessageEvents.REMOVE_VARIANT_FIELDS, (event: RemoveVariantFieldsEvent) => { + setHighlightVariantFields(false); removeVariantFieldClass(event?.data?.onlyHighlighted); } ); diff --git a/src/visualBuilder/index.ts b/src/visualBuilder/index.ts index da5992f3..8e24ba8c 100644 --- a/src/visualBuilder/index.ts +++ b/src/visualBuilder/index.ts @@ -26,7 +26,7 @@ import initUI from "./components"; import { useDraftFieldsPostMessageEvent } from "./eventManager/useDraftFieldsPostMessageEvent"; import { useHideFocusOverlayPostMessageEvent } from "./eventManager/useHideFocusOverlayPostMessageEvent"; import { useScrollToField } from "./eventManager/useScrollToField"; -import { useVariantFieldsPostMessageEvent } from "./eventManager/useVariantsPostMessageEvent"; +import { debounceAddVariantFieldClass, getHighlightVariantFieldsStatus, setHighlightVariantFields, useVariantFieldsPostMessageEvent } from "./eventManager/useVariantsPostMessageEvent"; import { generateEmptyBlocks, removeEmptyBlocks, @@ -66,6 +66,8 @@ interface VisualBuilderGlobalStateImpl { audienceMode: boolean; locale: string; variant: string | null; + highlightVariantFields: boolean; + variantOrder: string[]; focusElementObserver: MutationObserver | null; referenceParentMap: Record; isFocussed: boolean; @@ -89,6 +91,8 @@ export class VisualBuilder { audienceMode: false, locale: Config.get().stackDetails.masterLocale || "en-us", variant: null, + highlightVariantFields: false, + variantOrder: [], focusElementObserver: null, referenceParentMap: {}, isFocussed: false, @@ -238,6 +242,9 @@ export class VisualBuilder { previousEmptyBlockParents: emptyBlockParents, }; } + if (VisualBuilder.VisualBuilderGlobalState.value.variant && VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields) { + debounceAddVariantFieldClass(VisualBuilder.VisualBuilderGlobalState.value.variant); + } }, 100, { trailing: true } @@ -363,6 +370,9 @@ export class VisualBuilder { subtree: true, }); + getHighlightVariantFieldsStatus().then((result) => { + setHighlightVariantFields(result.highlightVariantFields); + }); visualBuilderPostMessage?.on( VisualBuilderPostMessageEvents.GET_ALL_ENTRIES_IN_CURRENT_PAGE, getEntryIdentifiersInCurrentPage @@ -398,7 +408,7 @@ export class VisualBuilder { useOnEntryUpdatePostMessageEvent(); useRecalculateVariantDataCSLPValues(); useDraftFieldsPostMessageEvent(); - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: config.ssr ?? false }); } }) .catch(() => { @@ -441,6 +451,8 @@ export class VisualBuilder { audienceMode: false, locale: "en-us", variant: null, + highlightVariantFields: false, + variantOrder: [], focusElementObserver: null, referenceParentMap: {}, isFocussed: false, diff --git a/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts b/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts index 0c4e3084..dee18c8a 100644 --- a/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts +++ b/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts @@ -5,6 +5,7 @@ import { FieldDetails } from "../../components/FieldToolbar"; import Config from "../../../configManager/configManager"; import { VisualBuilder } from "../.."; import { EntryPermissions } from "../getEntryPermissions"; +import { WORKFLOW_STAGES } from "../constants"; import { ResolvedVariantPermissions } from "../getResolvedVariantPermissions"; const resolvedVariantPermissions: ResolvedVariantPermissions = { diff --git a/src/visualBuilder/utils/__test__/updateFocussedState.test.ts b/src/visualBuilder/utils/__test__/updateFocussedState.test.ts index 56bdc09e..073db9e1 100644 --- a/src/visualBuilder/utils/__test__/updateFocussedState.test.ts +++ b/src/visualBuilder/utils/__test__/updateFocussedState.test.ts @@ -44,7 +44,6 @@ vi.mock("../../utils/fieldSchemaMap", () => { }; }); - describe("updateFocussedState", () => { beforeEach(() => { const previousSelectedEditableDOM = document.createElement("div"); @@ -293,12 +292,17 @@ describe("updateFocussedState", () => { } as unknown as ResizeObserver; const previousSelectedEditableDOM = document.createElement("div"); - previousSelectedEditableDOM.setAttribute("data-cslp", "content_type_uid.entry_uid.locale.field_path"); + 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); + document.querySelector = vi + .fn() + .mockReturnValue(previousSelectedEditableDOM); const result = await updateFocussedState({ editableElement: editableElementMock, diff --git a/src/visualBuilder/utils/types/postMessage.types.ts b/src/visualBuilder/utils/types/postMessage.types.ts index 06ea3f91..54bda7d0 100644 --- a/src/visualBuilder/utils/types/postMessage.types.ts +++ b/src/visualBuilder/utils/types/postMessage.types.ts @@ -47,6 +47,7 @@ export enum VisualBuilderPostMessageEvents { REMOVE_HIGHLIGHTED_COMMENTS = "remove-highlighted-comments", GET_VARIANT_ID = "get-variant-id", GET_LOCALE = "get-locale", + GET_HIGHLIGHT_VARIANT_FIELDS_STATUS = "get-highlight-variant-fields-status", SEND_VARIANT_AND_LOCALE = "send-variant-and-locale", GET_CONTENT_TYPE_NAME = "get-content-type-name", REFERENCE_MAP = "get-reference-map", diff --git a/src/visualBuilder/visualBuilder.style.ts b/src/visualBuilder/visualBuilder.style.ts index 87a669d9..bd6c9e11 100644 --- a/src/visualBuilder/visualBuilder.style.ts +++ b/src/visualBuilder/visualBuilder.style.ts @@ -673,7 +673,8 @@ export function visualBuilderStyles() { "visual-builder__draft-field": css` outline: 2px dashed #eb5646; `, - "visual-builder__variant-field": css` + "visual-builder__variant-field": css``, + "visual-builder__variant-field-outline": css` outline: 2px solid #bd59fa; outline-offset: -2px; `,