From 5b149132e68f09b97e44105f8ba864280a89a97c Mon Sep 17 00:00:00 2001 From: Mridul Sharma Date: Fri, 11 Jul 2025 15:56:53 +0530 Subject: [PATCH 01/23] fix: multiple reloads in timeline --- .../eventManager/postMessageEvent.hooks.ts | 4 ++-- src/utils/addLivePreviewQueryTags.ts | 8 +++++++- src/utils/index.ts | 15 +++++++++++---- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/livePreview/eventManager/postMessageEvent.hooks.ts b/src/livePreview/eventManager/postMessageEvent.hooks.ts index 2cf60b01..75988942 100644 --- a/src/livePreview/eventManager/postMessageEvent.hooks.ts +++ b/src/livePreview/eventManager/postMessageEvent.hooks.ts @@ -1,6 +1,6 @@ import Config, { setConfigFromParams } from "../../configManager/configManager"; import { ILivePreviewWindowType } from "../../types/types"; -import { addParamsToUrl } from "../../utils"; +import { addParamsToUrl, isOpeningInTimeline } from "../../utils"; import livePreviewPostMessage from "./livePreviewEventManager"; import { LIVE_PREVIEW_POST_MESSAGE_EVENTS } from "./livePreviewEventManager.constant"; import { @@ -95,7 +95,7 @@ export function sendInitializeLivePreviewPostMessageEvent(): void { // "init message did not contain contentTypeUid or entryUid." // ); } - if (Config.get().ssr) { + if (Config.get().ssr || isOpeningInTimeline()) { addParamsToUrl(); } Config.set("windowType", windowType); diff --git a/src/utils/addLivePreviewQueryTags.ts b/src/utils/addLivePreviewQueryTags.ts index b99ead05..fc1014a7 100644 --- a/src/utils/addLivePreviewQueryTags.ts +++ b/src/utils/addLivePreviewQueryTags.ts @@ -8,11 +8,17 @@ export function addLivePreviewQueryTags(link: string): string { const ctUid: string | null = docUrl.searchParams.get("content_type_uid"); const entryUid: string | null = docUrl.searchParams.get("entry_uid"); - if (livePreviewHash && ctUid && entryUid) { + const previewTimestamp: string | null = docUrl.searchParams.get("preview_timestamp"); + if (livePreviewHash) { newUrl.searchParams.set("live_preview", livePreviewHash); + } + if(ctUid && entryUid){ newUrl.searchParams.set("content_type_uid", ctUid); newUrl.searchParams.set("entry_uid", entryUid); } + if (previewTimestamp) { + newUrl.searchParams.set("preview_timestamp", previewTimestamp); + } return newUrl.href; } catch (error) { PublicLogger.error("Error while adding live preview to URL"); diff --git a/src/utils/index.ts b/src/utils/index.ts index 4030ca0d..2e290153 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -7,16 +7,23 @@ export { addLivePreviewQueryTags }; export function addParamsToUrl() { // Setting the query params to all the click events related to current domain window.addEventListener("click", (event: any) => { - const target: any = event.target; - const targetHref: string | any = target.href; + const clickedElement = event.target; + const anchorElement = clickedElement.closest('a'); + + // Only proceed if the clicked element is either an anchor or a direct/indirect child of an anchor + if (!anchorElement || !anchorElement.contains(clickedElement)) { + return; + } + + const targetHref: string | any = anchorElement.href; const docOrigin: string = document.location.origin; if ( targetHref && targetHref.includes(docOrigin) && !targetHref.includes("live_preview") ) { - const newUrl = addLivePreviewQueryTags(target.href); - event.target.href = newUrl || target.href; + const newUrl = addLivePreviewQueryTags(targetHref); + anchorElement.href = newUrl || targetHref; } }); } From e335877c8f7d120f5f0549463f6ad1a1c81c9176 Mon Sep 17 00:00:00 2001 From: Mridul Sharma Date: Fri, 11 Jul 2025 16:43:28 +0530 Subject: [PATCH 02/23] chore: test fix from merge operation --- src/livePreview/__test__/live-preview.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/livePreview/__test__/live-preview.test.ts b/src/livePreview/__test__/live-preview.test.ts index dfdcce7d..023a19c8 100644 --- a/src/livePreview/__test__/live-preview.test.ts +++ b/src/livePreview/__test__/live-preview.test.ts @@ -432,7 +432,7 @@ describe("incoming postMessage", () => { describe("testing window event listeners", () => { let addEventListenerMock: any; - let sendInitEvent = vi.fn().mockImplementation(mockLivePreviewInitEventListener); + const sendInitEvent = vi.fn().mockImplementation(mockLivePreviewInitEventListener); let livePreviewInstance: LivePreview; beforeEach(() => { From e781891b9f65baae7860c11e09d0ac88b0c9b229 Mon Sep 17 00:00:00 2001 From: Mridul Sharma Date: Tue, 5 Aug 2025 15:38:32 +0530 Subject: [PATCH 03/23] test: added test cases for addLivePreviewQueryTags and addParamsToUrl --- .../__test__/addLivePreviewQueryTags.test.ts | 81 ++++++ src/utils/__test__/index.test.ts | 260 ++++++++++++++++-- 2 files changed, 312 insertions(+), 29 deletions(-) create mode 100644 src/utils/__test__/addLivePreviewQueryTags.test.ts diff --git a/src/utils/__test__/addLivePreviewQueryTags.test.ts b/src/utils/__test__/addLivePreviewQueryTags.test.ts new file mode 100644 index 00000000..c232abbb --- /dev/null +++ b/src/utils/__test__/addLivePreviewQueryTags.test.ts @@ -0,0 +1,81 @@ +import { describe, test, expect, vi, beforeEach } from "vitest"; +import { addLivePreviewQueryTags } from "../addLivePreviewQueryTags"; +import { PublicLogger } from "../../logger/logger"; + +// Mock the logger +vi.mock("../../logger/logger", () => ({ + PublicLogger: { + error: vi.fn(), + }, +})); + +describe("addLivePreviewQueryTags", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test("should return original URL when no live preview parameters in current location", () => { + // This test works with current document.location (likely has no live preview params) + const targetUrl = "http://example.com/target-page"; + + const result = addLivePreviewQueryTags(targetUrl); + + // Should return unchanged since no live preview params in current location + expect(result).toBe(targetUrl); + }); + + test("should log error and return original link when target URL is invalid", () => { + const targetUrl = "not-a-valid-url-at-all-invalid"; + + const result = addLivePreviewQueryTags(targetUrl); + + expect(PublicLogger.error).toHaveBeenCalledWith("Error while adding live preview to URL"); + expect(result).toBe(targetUrl); + }); + + test("should handle empty string input", () => { + const targetUrl = ""; + + const result = addLivePreviewQueryTags(targetUrl); + + expect(PublicLogger.error).toHaveBeenCalledWith("Error while adding live preview to URL"); + expect(result).toBe(targetUrl); + }); + + test("should handle malformed URLs gracefully", () => { + const targetUrl = "http://"; + + const result = addLivePreviewQueryTags(targetUrl); + + expect(PublicLogger.error).toHaveBeenCalledWith("Error while adding live preview to URL"); + expect(result).toBe(targetUrl); + }); + + test("should handle valid URLs without errors", () => { + const targetUrl = "https://example.com/valid-page"; + + const result = addLivePreviewQueryTags(targetUrl); + + // Should not throw errors and return some result + expect(typeof result).toBe("string"); + expect(result.length).toBeGreaterThan(0); + }); + + test("should add live preview query tags to URL with all required query parameters", () => { + const originalUrl = + "http://example.com?live_preview=hash&content_type_uid=ctuid&entry_uid=entryuid"; + const expectedUrl = + "http://example.com/?live_preview=hash&content_type_uid=ctuid&entry_uid=entryuid"; + + global.window = Object.create(window); + Object.defineProperty(window, "location", { + value: { + href: originalUrl, + }, + writable: true, + }); + const result = addLivePreviewQueryTags(originalUrl); + + expect(result).toEqual(expectedUrl); + }); +}); diff --git a/src/utils/__test__/index.test.ts b/src/utils/__test__/index.test.ts index fbf7ff9a..afb4cf83 100644 --- a/src/utils/__test__/index.test.ts +++ b/src/utils/__test__/index.test.ts @@ -1,52 +1,254 @@ -import { PublicLogger } from "../../logger/logger"; -import { addLivePreviewQueryTags, hasWindow } from "../index"; +import { hasWindow, addParamsToUrl } from "../index"; import { vi } from "vitest"; -vi.mock("../../logger/logger", () => ({ - PublicLogger: { - error: vi.fn(), - }, +// Mock addLivePreviewQueryTags function +vi.mock("../addLivePreviewQueryTags", () => ({ + addLivePreviewQueryTags: vi.fn() })); +// Import the mocked function after setting up the mock +import { addLivePreviewQueryTags } from "../addLivePreviewQueryTags"; + describe("hasWindow() function", () => { test("must check if window is available", () => { expect(hasWindow()).toBe(typeof window !== "undefined"); }); }); -describe("addLivePreviewQueryTags", () => { - test("should add live preview query tags to URL with all required query parameters", () => { - const originalUrl = - "http://example.com?live_preview=hash&content_type_uid=ctuid&entry_uid=entryuid"; - const expectedUrl = - "http://example.com/?live_preview=hash&content_type_uid=ctuid&entry_uid=entryuid"; - global.window = Object.create(window); - Object.defineProperty(window, "location", { - value: { - href: originalUrl, - }, - writable: true, +describe("addParamsToUrl", () => { + let mockAddEventListener: any; + let mockDocument: any; + let clickHandler: (event: any) => void; + + beforeEach(() => { + // Reset all mocks + vi.clearAllMocks(); + + // Mock window.addEventListener to capture the click handler + mockAddEventListener = vi.fn((event, handler) => { + if (event === "click") { + clickHandler = handler; + } }); + + // Setup mock return value for addLivePreviewQueryTags + vi.mocked(addLivePreviewQueryTags).mockImplementation((url) => `${url}?live_preview=test&content_type_uid=test&entry_uid=test`); + + // Mock document and window + mockDocument = { + location: { + origin: "https://example.com" + } + }; - const result = addLivePreviewQueryTags(originalUrl); + global.window = { + addEventListener: mockAddEventListener, + document: mockDocument + } as any; + + global.document = mockDocument as any; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); - expect(result).toEqual(expectedUrl); + test("should add event listener when function is called", () => { + addParamsToUrl(); + + expect(mockAddEventListener).toHaveBeenCalledWith("click", expect.any(Function)); }); - test("should log error and return original link if an error occurs while adding live preview query tags", () => { - const originalUrl = - "http://example.com?live_preview=hash&content_type_uid=ctuid&entry_uid=entryuid"; - const expectedLoggedError = "Error while adding live preview to URL"; + describe("when clicking on elements", () => { + beforeEach(() => { + addParamsToUrl(); + }); + + test("should handle click directly on anchor tag", () => { + // Create mock anchor element + const mockAnchor = { + href: "https://example.com/page", + closest: vi.fn().mockReturnValue(null), + contains: vi.fn().mockReturnValue(true) + }; + mockAnchor.closest.mockReturnValue(mockAnchor); // closest('a') returns self + + const mockEvent = { + target: mockAnchor + }; + + // Trigger the click event + clickHandler(mockEvent); - vi.spyOn(global, "URL").mockImplementation(() => { - throw new Error("Mock error"); + expect(mockAnchor.closest).toHaveBeenCalledWith('a'); + expect(mockAnchor.contains).toHaveBeenCalledWith(mockAnchor); + expect(addLivePreviewQueryTags).toHaveBeenCalledWith("https://example.com/page"); + expect(mockAnchor.href).toBe("https://example.com/page?live_preview=test&content_type_uid=test&entry_uid=test"); }); - const result = addLivePreviewQueryTags(originalUrl); + test("should handle click on child element of anchor tag", () => { + // Create mock child element and parent anchor + const mockChild = { + closest: vi.fn() + }; + + const mockAnchor = { + href: "https://example.com/child-page", + contains: vi.fn().mockReturnValue(true) + }; + + mockChild.closest.mockReturnValue(mockAnchor); // closest('a') returns parent anchor + + const mockEvent = { + target: mockChild + }; + + // Trigger the click event + clickHandler(mockEvent); + + expect(mockChild.closest).toHaveBeenCalledWith('a'); + expect(mockAnchor.contains).toHaveBeenCalledWith(mockChild); + expect(addLivePreviewQueryTags).toHaveBeenCalledWith("https://example.com/child-page"); + expect(mockAnchor.href).toBe("https://example.com/child-page?live_preview=test&content_type_uid=test&entry_uid=test"); + }); + + test("should not process click when no anchor element is found", () => { + const mockElement = { + closest: vi.fn().mockReturnValue(null) + }; + + const mockEvent = { + target: mockElement + }; + + clickHandler(mockEvent); + + expect(mockElement.closest).toHaveBeenCalledWith('a'); + expect(addLivePreviewQueryTags).not.toHaveBeenCalled(); + }); + + test("should not process click when anchor doesn't contain clicked element", () => { + const mockChild = { + closest: vi.fn() + }; + + const mockAnchor = { + href: "https://example.com/page", + contains: vi.fn().mockReturnValue(false) // Anchor doesn't contain the clicked element + }; + + mockChild.closest.mockReturnValue(mockAnchor); + + const mockEvent = { + target: mockChild + }; + + clickHandler(mockEvent); + + expect(mockChild.closest).toHaveBeenCalledWith('a'); + expect(mockAnchor.contains).toHaveBeenCalledWith(mockChild); + expect(addLivePreviewQueryTags).not.toHaveBeenCalled(); + }); - expect(PublicLogger.error).toHaveBeenCalledWith(expectedLoggedError); + test("should not process external links", () => { + const mockAnchor = { + href: "https://external-site.com/page", + closest: vi.fn().mockReturnValue(null), + contains: vi.fn().mockReturnValue(true) + }; + mockAnchor.closest.mockReturnValue(mockAnchor); + + const mockEvent = { + target: mockAnchor + }; - expect(result).toEqual(originalUrl); + clickHandler(mockEvent); + + expect(addLivePreviewQueryTags).not.toHaveBeenCalled(); + expect(mockAnchor.href).toBe("https://external-site.com/page"); // Unchanged + }); + + test("should not process links that already contain live_preview", () => { + const mockAnchor = { + href: "https://example.com/page?live_preview=existing", + closest: vi.fn().mockReturnValue(null), + contains: vi.fn().mockReturnValue(true) + }; + mockAnchor.closest.mockReturnValue(mockAnchor); + + const mockEvent = { + target: mockAnchor + }; + + clickHandler(mockEvent); + + expect(addLivePreviewQueryTags).not.toHaveBeenCalled(); + expect(mockAnchor.href).toBe("https://example.com/page?live_preview=existing"); // Unchanged + }); + + test("should not process links without href", () => { + const mockAnchor = { + href: "", + closest: vi.fn().mockReturnValue(null), + contains: vi.fn().mockReturnValue(true) + }; + mockAnchor.closest.mockReturnValue(mockAnchor); + + const mockEvent = { + target: mockAnchor + }; + + clickHandler(mockEvent); + + expect(addLivePreviewQueryTags).not.toHaveBeenCalled(); + expect(mockAnchor.href).toBe(""); // Unchanged + }); + + test("should handle case when addLivePreviewQueryTags returns empty string", () => { + vi.mocked(addLivePreviewQueryTags).mockReturnValue(""); + + const originalHref = "https://example.com/page"; + const mockAnchor = { + href: originalHref, + closest: vi.fn().mockReturnValue(null), + contains: vi.fn().mockReturnValue(true) + }; + mockAnchor.closest.mockReturnValue(mockAnchor); + + const mockEvent = { + target: mockAnchor + }; + + clickHandler(mockEvent); + + expect(addLivePreviewQueryTags).toHaveBeenCalledWith(originalHref); + expect(mockAnchor.href).toBe(originalHref); // Falls back to original href when empty string returned + }); + + test("should handle nested child elements", () => { + // Create deeply nested structure: span > button > a + const mockDeepChild = { + closest: vi.fn() + }; + + const mockAnchor = { + href: "https://example.com/nested-page", + contains: vi.fn().mockReturnValue(true) + }; + + mockDeepChild.closest.mockReturnValue(mockAnchor); + + const mockEvent = { + target: mockDeepChild + }; + + clickHandler(mockEvent); + + expect(mockDeepChild.closest).toHaveBeenCalledWith('a'); + expect(mockAnchor.contains).toHaveBeenCalledWith(mockDeepChild); + expect(addLivePreviewQueryTags).toHaveBeenCalledWith("https://example.com/nested-page"); + expect(mockAnchor.href).toBe("https://example.com/nested-page?live_preview=test&content_type_uid=test&entry_uid=test"); + }); }); }); From bab5a47953979af9cc5acc4fc05cbe6623caeb55 Mon Sep 17 00:00:00 2001 From: SahilCs15 Date: Sun, 29 Jun 2025 21:08:47 +0530 Subject: [PATCH 04/23] chore: fieldlocation fetch completed --- src/index.ts | 1 + .../components/FieldLocationAppList.tsx | 101 ++++++++++++++ src/visualBuilder/components/FieldToolbar.tsx | 131 +++++++++++++++--- .../components/icons/EmptyAppIcon.tsx | 40 ++++++ src/visualBuilder/components/icons/index.tsx | 33 +++-- .../utils/types/postMessage.types.ts | 2 + src/visualBuilder/visualBuilder.style.ts | 8 ++ 7 files changed, 282 insertions(+), 34 deletions(-) create mode 100644 src/visualBuilder/components/FieldLocationAppList.tsx create mode 100644 src/visualBuilder/components/icons/EmptyAppIcon.tsx diff --git a/src/index.ts b/src/index.ts index 45074abc..a4a5b462 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import LightLivePreviewHoC from "./light-sdk"; export type IStackSdk = ExternalStackSdkType; +console.log('initialised') const ContentstackLivePreview = typeof process !== "undefined" && diff --git a/src/visualBuilder/components/FieldLocationAppList.tsx b/src/visualBuilder/components/FieldLocationAppList.tsx new file mode 100644 index 00000000..69463865 --- /dev/null +++ b/src/visualBuilder/components/FieldLocationAppList.tsx @@ -0,0 +1,101 @@ +import React from "react"; +import { EmptyAppIcon } from "./icons/EmptyAppIcon"; + +interface App { + uid: string; + title: string; + name: string; + icon?: string; + visible: boolean; +} + +interface FieldLocationAppListProps { + apps: App[]; +} + +export const FieldLocationAppList: React.FC = ({ apps }) => { + if (!apps || apps.length === 0) { + return null; + } + + return ( +
+ {apps.map((app) => ( +
{ + console.log("App clicked:", app); + }} + > +
+ {app.icon ? ( + {app.title} + ) : ( + + )} +
+
+
+ {app.title} +
+
+ {app.name} +
+
+ +
+ ))} +
+ ); +}; \ No newline at end of file diff --git a/src/visualBuilder/components/FieldToolbar.tsx b/src/visualBuilder/components/FieldToolbar.tsx index c2ab9088..64ea688f 100644 --- a/src/visualBuilder/components/FieldToolbar.tsx +++ b/src/visualBuilder/components/FieldToolbar.tsx @@ -19,12 +19,13 @@ import { MoveLeftIcon, MoveRightIcon, ReplaceAssetIcon, + MoreIcon, } from "./icons"; import { fieldIcons } from "./icons/fields"; import classNames from "classnames"; import { visualBuilderStyles } from "../visualBuilder.style"; import CommentIcon from "./CommentIcon"; -import React, { useEffect, useState } from "preact/compat"; +import React, { useEffect, useState, useRef } from "preact/compat"; import { FieldSchemaMap } from "../utils/fieldSchemaMap"; import { isFieldDisabled } from "../utils/isFieldDisabled"; import { IReferenceContentTypeSchema } from "../../cms/types/contentTypeSchema.types"; @@ -40,6 +41,9 @@ import { } from "./FieldRevert/FieldRevertComponent"; import { LoadingIcon } from "./icons/loading"; import { EntryPermissions } from "../utils/getEntryPermissions"; +import { EmptyAppIcon } from "./icons/EmptyAppIcon"; +import { FieldLocationAppList } from "./FieldLocationAppList.tsx"; + export type FieldDetails = Pick< VisualBuilderCslpEventDetails, @@ -56,7 +60,6 @@ interface MultipleFieldToolbarProps { } function handleReplaceAsset(fieldMetadata: CslpData) { - // TODO avoid sending whole fieldMetadata visualBuilderPostMessage?.send( VisualBuilderPostMessageEvents.OPEN_ASSET_MODAL, { @@ -115,6 +118,9 @@ function FieldToolbarComponent( } = props; const { fieldMetadata, editableElement: targetElement } = eventDetails; const [isFormLoading, setIsFormLoading] = useState(false); + const [fieldLocationData, setFieldLocationData] = useState(null); + const [displayAllApps, setDisplayAllApps] = useState(false); + const moreButtonRef = useRef(null); const parentPath = fieldMetadata?.multipleFieldMetadata?.parentDetails?.parentCslpValue || @@ -158,13 +164,6 @@ function FieldToolbarComponent( isMultiple = (fieldSchema as IReferenceContentTypeSchema) .field_metadata.ref_multiple; - // field is multiple but an instance is not selected - // instead the whole field (all instances) is selected. - // Currently, when whole featured_blogs is selected in canvas, - // the fieldPathWithIndex and instance.fieldPathWithIndex are the same - // cannot rely on -1 index, as the non-negative index then refers to the index of - // the featured_blogs block in page_components - // It is not needed except taxanomy. isWholeMultipleField = isMultiple && (fieldMetadata.fieldPathWithIndex === @@ -173,18 +172,15 @@ function FieldToolbarComponent( isReplaceAllowed = ALLOWED_REPLACE_FIELDS.includes(fieldType) && !isWholeMultipleField; - // if ( - // DEFAULT_MULTIPLE_FIELDS.includes(fieldType) && - // isWholeMultipleField && - // !isVariant - // ) { - // return null; - // } } const invertTooltipPosition = targetElement.getBoundingClientRect().top <= TOOLTIP_TOP_EDGE_BUFFER; + const handleMoreIconClick = () => { + setDisplayAllApps(!displayAllApps); + }; + const editButton = Icon ? ( ); - // TODO sibling count is incorrect for this purpose + + + + + const totalElementCount = targetElement?.parentNode?.childElementCount ?? 1; const indexOfElement = fieldMetadata?.multipleFieldMetadata?.index; - const disableMoveLeft = indexOfElement === 0; // first element - const disableMoveRight = indexOfElement === totalElementCount - 1; // last element + const disableMoveLeft = indexOfElement === 0; + const disableMoveRight = indexOfElement === totalElementCount - 1; useEffect(() => { async function fetchFieldSchema() { @@ -353,6 +351,25 @@ function FieldToolbarComponent( }; }, []); + useEffect(() => { + const event = visualBuilderPostMessage?.on( + VisualBuilderPostMessageEvents.FIELD_LOCATION_DATA, + (data: { data: any }) => { + console.log('fieldLocationData received:', data.data); + setFieldLocationData(data.data.fieldLocationData || data.data); + } + ); + return () => { + event?.unregister(); + }; + }, [fieldMetadata]); + + // Debug logging + useEffect(() => { + console.log('fieldLocationData state:', fieldLocationData); + console.log('apps from fieldLocationData:', fieldLocationData?.apps); + }, [fieldLocationData, fieldMetadata]); + const multipleFieldToolbarButtonClasses = classNames( "visual-builder__button visual-builder__button--secondary", visualBuilderStyles()["visual-builder__button"], @@ -365,6 +382,9 @@ function FieldToolbarComponent( } ); + + + return (
)} + +
+ {fieldLocationData?.apps && + fieldLocationData.apps.length > 0 && ( +
+ + + +
+ )} +
+ {displayAllApps && ( + + )} ); } diff --git a/src/visualBuilder/components/icons/EmptyAppIcon.tsx b/src/visualBuilder/components/icons/EmptyAppIcon.tsx new file mode 100644 index 00000000..f0ba2d2b --- /dev/null +++ b/src/visualBuilder/components/icons/EmptyAppIcon.tsx @@ -0,0 +1,40 @@ +export const EmptyAppIcon = ({ + id = "", + ...props +}: React.SVGProps & { id?: string }) => { + return ( + + + + + ); +}; +export const sumOfAscii = (str: string): number => + [...str].reduce((a, b) => a + b.charCodeAt(0), 0); + +export const getColorFromString = (str: string = ""): string => { + const colorList: string[] = [ + "#99D8CE", + "#6BC3FE", + "#5060C1", + "#835EC3", + "#B16DBD", + "#FF85BC", + "#FF7E83", + "#A2D959", + "#59BA5E", + ]; + return colorList[sumOfAscii(str) % colorList.length]; +}; diff --git a/src/visualBuilder/components/icons/index.tsx b/src/visualBuilder/components/icons/index.tsx index d209df86..bb80aead 100644 --- a/src/visualBuilder/components/icons/index.tsx +++ b/src/visualBuilder/components/icons/index.tsx @@ -285,27 +285,18 @@ export function AddCommentIcon(): JSX.Element { export function WarningOctagonIcon(): JSX.Element { return ( - - ); @@ -341,4 +332,20 @@ export function CaretRightIcon(): JSX.Element { ) +} +export function MoreIcon(): JSX.Element { + return ( + + + + + + ); } \ No newline at end of file diff --git a/src/visualBuilder/utils/types/postMessage.types.ts b/src/visualBuilder/utils/types/postMessage.types.ts index e24bd4a5..050868b0 100644 --- a/src/visualBuilder/utils/types/postMessage.types.ts +++ b/src/visualBuilder/utils/types/postMessage.types.ts @@ -13,6 +13,7 @@ export enum VisualBuilderPostMessageEvents { GET_FIELD_DISPLAY_NAMES = "get-field-display-names", MOUSE_CLICK = "mouse-click", FOCUS_FIELD = "focus-field", + GET_FIELD_LOCATION_DETAILS = "get-field-location-details", OPEN_FIELD_EDIT_MODAL = "open-field-edit-modal", DELETE_INSTANCE = "delete-instance", MOVE_INSTANCE = "move-instance", @@ -26,6 +27,7 @@ export enum VisualBuilderPostMessageEvents { COLLAB_RESOLVE_THREAD = "collab-resolve-thread", COLLAB_DELETE_THREAD = "collab-delete-thread", COLLAB_MISSING_THREADS = "collab-missing-threads", + FIELD_LOCATION_DATA = "field-location-data", // FROM visual builder GET_ALL_ENTRIES_IN_CURRENT_PAGE = "get-entries-in-current-page", diff --git a/src/visualBuilder/visualBuilder.style.ts b/src/visualBuilder/visualBuilder.style.ts index dc4ea46a..118ac5d4 100644 --- a/src/visualBuilder/visualBuilder.style.ts +++ b/src/visualBuilder/visualBuilder.style.ts @@ -739,6 +739,14 @@ export function visualBuilderStyles() { fill: #475161; } `, + "visual-builder__field-location-icons-container": css` + display: flex; + gap: 0.25rem; + align-items: center; + justify-content: center; + margin-left: 0.25rem; + + `, }; } From 0c490390011da7a4c7b8a4b64e4e53cdab0cc9a3 Mon Sep 17 00:00:00 2001 From: SahilCs15 Date: Mon, 30 Jun 2025 21:02:00 +0530 Subject: [PATCH 05/23] chore: rendered the fieldmodifier apps added an event listner to send the selected app --- .../components/FieldLocationAppList.tsx | 190 +++++++++++------- .../components/FieldLocationIcon.tsx | 98 +++++++++ src/visualBuilder/components/FieldToolbar.tsx | 106 +++------- src/visualBuilder/components/icons/index.tsx | 36 ++-- .../utils/types/postMessage.types.ts | 2 +- src/visualBuilder/visualBuilder.style.ts | 95 +++++++++ 6 files changed, 360 insertions(+), 167 deletions(-) create mode 100644 src/visualBuilder/components/FieldLocationIcon.tsx diff --git a/src/visualBuilder/components/FieldLocationAppList.tsx b/src/visualBuilder/components/FieldLocationAppList.tsx index 69463865..4723884a 100644 --- a/src/visualBuilder/components/FieldLocationAppList.tsx +++ b/src/visualBuilder/components/FieldLocationAppList.tsx @@ -1,101 +1,135 @@ -import React from "react"; +import React, { useState, useEffect, useRef } from "preact/compat"; import { EmptyAppIcon } from "./icons/EmptyAppIcon"; +import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types"; +import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; +import { visualBuilderStyles } from "../visualBuilder.style"; +import classNames from "classnames"; interface App { - uid: string; - title: string; - name: string; + app_installation_uid: string; + app_uid: string; + contentTypeUid: string; + entryUid: string; + fieldDataType: string; + fieldDisplayName: string; + fieldPath: string; icon?: string; - visible: boolean; + locale: string; + manifest: { + uid: string; + name: string; + description: string; + icon: string; + visibility: string; + }; + title: string; + uid: string; } + interface FieldLocationAppListProps { apps: App[]; + position: "left" | "right"; } -export const FieldLocationAppList: React.FC = ({ apps }) => { - if (!apps || apps.length === 0) { - return null; +export const FieldLocationAppList = ({ apps, position }: FieldLocationAppListProps) => { + const [search, setSearch] = useState(""); + const [filteredApps, setFilteredApps] = useState(apps); + const appRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); + + useEffect(() => { + const filtered = apps.filter(app => + app.title.toLowerCase().includes(search.toLowerCase()) + ); + setFilteredApps(filtered); + }, [search, apps]); + + const handleAppClick = (app: App) => { + const appElement = appRefs.current[app.uid]; + const positionData = appElement?.getBoundingClientRect(); + visualBuilderPostMessage?.send(VisualBuilderPostMessageEvents.FIELD_LOCATION_SELECTED_APP, { + app: app, + position: positionData, + }); } return (
- {apps.map((app) => ( -
{ - console.log("App clicked:", app); - }} +
+ -
- {app.icon ? ( - {app.title} - ) : ( - - )} + + + + setSearch((e.target as HTMLInputElement).value) + } + placeholder="Search for Apps" + className={visualBuilderStyles()["visual-builder__field-location-app-list__search-input"]} + /> +
+
+ {filteredApps.length === 0 && ( +
+ + No matching results found! +
-
+ )} + {filteredApps + .filter((_, index) => index !== 0) + .map((app) => (
(appRefs.current[app.uid] = el)} + className={visualBuilderStyles()["visual-builder__field-location-app-list__item"]} + onClick={() => handleAppClick(app)} > - {app.title} +
+ {app.icon ? ( + {app.title} + ) : ( + + )} +
+ + {app.title} +
-
- {app.name} -
-
- -
- ))} + ))} +
); }; \ No newline at end of file diff --git a/src/visualBuilder/components/FieldLocationIcon.tsx b/src/visualBuilder/components/FieldLocationIcon.tsx new file mode 100644 index 00000000..c6a744d3 --- /dev/null +++ b/src/visualBuilder/components/FieldLocationIcon.tsx @@ -0,0 +1,98 @@ +import classNames from "classnames"; +import { visualBuilderStyles } from "../visualBuilder.style"; +import { EmptyAppIcon } from "./icons/EmptyAppIcon"; +import { MoreIcon } from "./icons"; +import React, { useRef } from "preact/compat"; +import { LoadingIcon } from "./icons/loading"; +import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types"; +import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; + +export const FieldLocationIcon = ({ + fieldLocationData, + multipleFieldToolbarButtonClasses, + handleMoreIconClick, + moreButtonRef, + isLoading, + toolbarRef, +}: { + fieldLocationData: any; + multipleFieldToolbarButtonClasses: any; + handleMoreIconClick: () => void; + moreButtonRef: any; + isLoading: boolean; + toolbarRef: any; +}) => { + // if (isLoading) { + // return ( + //
+ // + //
+ // ); + // } + + const currentAppRef = useRef(null); + + if (!fieldLocationData?.apps || fieldLocationData.apps.length === 0) { + return null; + } + + return ( +
+
+ + + +
+ ); +}; diff --git a/src/visualBuilder/components/FieldToolbar.tsx b/src/visualBuilder/components/FieldToolbar.tsx index 64ea688f..b5e47051 100644 --- a/src/visualBuilder/components/FieldToolbar.tsx +++ b/src/visualBuilder/components/FieldToolbar.tsx @@ -42,7 +42,8 @@ import { import { LoadingIcon } from "./icons/loading"; import { EntryPermissions } from "../utils/getEntryPermissions"; import { EmptyAppIcon } from "./icons/EmptyAppIcon"; -import { FieldLocationAppList } from "./FieldLocationAppList.tsx"; +import { FieldLocationAppList } from "./FieldLocationAppList"; +import { FieldLocationIcon } from "./FieldLocationIcon"; export type FieldDetails = Pick< @@ -121,6 +122,9 @@ function FieldToolbarComponent( const [fieldLocationData, setFieldLocationData] = useState(null); const [displayAllApps, setDisplayAllApps] = useState(false); const moreButtonRef = useRef(null); + const toolbarRef = useRef(null); + const [isFieldLocationLoading, setIsFieldLocationLoading] = useState(true); + const [appListPosition, setAppListPosition] = useState<"left" | "right">("right"); const parentPath = fieldMetadata?.multipleFieldMetadata?.parentDetails?.parentCslpValue || @@ -141,6 +145,7 @@ function FieldToolbarComponent( let Icon = null; let fieldType = null; let isWholeMultipleField = false; + const APP_LIST_MIN_WIDTH = 230; let disableFieldActions = false; if (fieldSchema) { @@ -178,6 +183,25 @@ function FieldToolbarComponent( targetElement.getBoundingClientRect().top <= TOOLTIP_TOP_EDGE_BUFFER; const handleMoreIconClick = () => { + + + if(toolbarRef.current){ + const rect=toolbarRef.current.getBoundingClientRect(); + const spaceRight=window.innerWidth-rect.right; + const spaceLeft=rect.left; + let position=''; + + if(spaceRightAPP_LIST_MIN_WIDTH){ + position="right"; + }else { + position=spaceRight>spaceLeft ? "right" : "left"; + } + setAppListPosition(position as "left" | "right"); + + } + setDisplayAllApps(!displayAllApps); }; @@ -352,11 +376,12 @@ function FieldToolbarComponent( }, []); useEffect(() => { + setIsFieldLocationLoading(true); const event = visualBuilderPostMessage?.on( VisualBuilderPostMessageEvents.FIELD_LOCATION_DATA, (data: { data: any }) => { - console.log('fieldLocationData received:', data.data); - setFieldLocationData(data.data.fieldLocationData || data.data); + setFieldLocationData(data.data.fieldLocationData); + setIsFieldLocationLoading(false); } ); return () => { @@ -364,11 +389,7 @@ function FieldToolbarComponent( }; }, [fieldMetadata]); - // Debug logging - useEffect(() => { - console.log('fieldLocationData state:', fieldLocationData); - console.log('apps from fieldLocationData:', fieldLocationData?.apps); - }, [fieldLocationData, fieldMetadata]); + const multipleFieldToolbarButtonClasses = classNames( "visual-builder__button visual-builder__button--secondary", @@ -535,76 +556,13 @@ function FieldToolbarComponent( )} -
- {fieldLocationData?.apps && - fieldLocationData.apps.length > 0 && ( -
- - - -
- )} -
+ +
{displayAllApps && ( - + )} ); diff --git a/src/visualBuilder/components/icons/index.tsx b/src/visualBuilder/components/icons/index.tsx index bb80aead..1793d6fc 100644 --- a/src/visualBuilder/components/icons/index.tsx +++ b/src/visualBuilder/components/icons/index.tsx @@ -161,8 +161,8 @@ export function EditIcon(): JSX.Element { return ( + + ); @@ -336,16 +345,15 @@ export function CaretRightIcon(): JSX.Element { export function MoreIcon(): JSX.Element { return ( - - - + + + ); } \ No newline at end of file diff --git a/src/visualBuilder/utils/types/postMessage.types.ts b/src/visualBuilder/utils/types/postMessage.types.ts index 050868b0..1c1e7dc5 100644 --- a/src/visualBuilder/utils/types/postMessage.types.ts +++ b/src/visualBuilder/utils/types/postMessage.types.ts @@ -13,7 +13,6 @@ export enum VisualBuilderPostMessageEvents { GET_FIELD_DISPLAY_NAMES = "get-field-display-names", MOUSE_CLICK = "mouse-click", FOCUS_FIELD = "focus-field", - GET_FIELD_LOCATION_DETAILS = "get-field-location-details", OPEN_FIELD_EDIT_MODAL = "open-field-edit-modal", DELETE_INSTANCE = "delete-instance", MOVE_INSTANCE = "move-instance", @@ -28,6 +27,7 @@ export enum VisualBuilderPostMessageEvents { COLLAB_DELETE_THREAD = "collab-delete-thread", COLLAB_MISSING_THREADS = "collab-missing-threads", FIELD_LOCATION_DATA = "field-location-data", + FIELD_LOCATION_SELECTED_APP = "field-location-selected-app", // FROM visual builder GET_ALL_ENTRIES_IN_CURRENT_PAGE = "get-entries-in-current-page", diff --git a/src/visualBuilder/visualBuilder.style.ts b/src/visualBuilder/visualBuilder.style.ts index 118ac5d4..8b9a8673 100644 --- a/src/visualBuilder/visualBuilder.style.ts +++ b/src/visualBuilder/visualBuilder.style.ts @@ -727,6 +727,7 @@ export function visualBuilderStyles() { display: flex; flex-direction: column-reverse; z-index: 2147483647 !important; + position: relative; `, "visual-builder__variant-button": css` display: flex; @@ -747,6 +748,100 @@ export function visualBuilderStyles() { margin-left: 0.25rem; `, + "visual-builder__field-location-icons-container__divider": css` + height: 32px !important; + width: 1px; + border-radius: 2px; + background-color: #8a8f99; + `, + "visual-builder__field-location-icons-container__app-icon": css` + width: 16px; + height: 16px; + object-fit: cover; + `, + "visual-builder__field-location-app-list": css` + position: absolute; + top: 0; + background: #fff; + border: 1px solid #e0e0e0; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0,0,0,0.15); + z-index: 1000; + min-width: 230px; + max-height: 250px; + min-height: 250px; + overflow-y: auto; + display: flex; + flex-direction: column; + `, + "visual-builder__field-location-app-list--left": css` + right: 100%; + margin-right: 8px; + `, + "visual-builder__field-location-app-list--right": css` + left: 100%; + margin-left: 8px; + `, + "visual-builder__field-location-app-list__search-container": css` + display: flex; + align-items: center; + padding: 10px 16px 0px 16px; + border: none; + border-bottom: 1px solid #f0f0f0; + `, + "visual-builder__field-location-app-list__search-input": css` + width: 100%; + padding: 10px 12px; + font-size: 14px; + outline: none; + box-sizing: border-box; + border: none; + `, + "visual-builder__field-location-app-list__search-icon": css` + width: 14px; + height: 14px; + `, + "visual-builder__field-location-app-list__content": css` + flex: 1; + overflow-y: auto; + `, + "visual-builder__field-location-app-list__no-results": css` + display: flex; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + text-align: center; + `, + "visual-builder__field-location-app-list__no-results-text": css` + color: #373b40; + font-weight: 400; + `, + "visual-builder__field-location-app-list__item": css` + display: flex; + align-items: center; + padding: 10px 16px; + cursor: pointer; + font-size: 14px; + `, + "visual-builder__field-location-app-list__item-icon-container": css` + width: 24px; + height: 24px; + margin-right: 12px; + display: flex; + align-items: center; + justify-content: center; + `, + "visual-builder__field-location-app-list__item-icon": css` + width: 24px; + height: 24px; + border-radius: 50%; + object-fit: cover; + `, + "visual-builder__field-location-app-list__item-title": css` + color: #373b40; + font-weight: 400; + `, }; } From 6337a6de2c41684d6cc70b62f600a4036264577b Mon Sep 17 00:00:00 2001 From: SahilCs15 Date: Tue, 1 Jul 2025 15:19:56 +0530 Subject: [PATCH 06/23] chore: added test cases for the field location data --- src/index.ts | 2 - .../components/FieldLocationAppList.tsx | 178 ++++++++++----- .../components/FieldLocationIcon.tsx | 39 ++-- src/visualBuilder/components/FieldToolbar.tsx | 12 +- .../__test__/FieldLocationAppList.test.tsx | 216 ++++++++++++++++++ .../__test__/FieldLocationIcon.test.tsx | 41 ++++ src/visualBuilder/components/icons/index.tsx | 4 +- 7 files changed, 399 insertions(+), 93 deletions(-) create mode 100644 src/visualBuilder/components/__test__/FieldLocationAppList.test.tsx create mode 100644 src/visualBuilder/components/__test__/FieldLocationIcon.test.tsx diff --git a/src/index.ts b/src/index.ts index a4a5b462..11164ffd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,8 +5,6 @@ import LightLivePreviewHoC from "./light-sdk"; export type IStackSdk = ExternalStackSdkType; -console.log('initialised') - const ContentstackLivePreview = typeof process !== "undefined" && (process?.env?.PURGE_PREVIEW_SDK === "true" || diff --git a/src/visualBuilder/components/FieldLocationAppList.tsx b/src/visualBuilder/components/FieldLocationAppList.tsx index 4723884a..b5bdba63 100644 --- a/src/visualBuilder/components/FieldLocationAppList.tsx +++ b/src/visualBuilder/components/FieldLocationAppList.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from "preact/compat"; +import React, { useState, useEffect, useMemo } from "preact/compat"; import { EmptyAppIcon } from "./icons/EmptyAppIcon"; import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types"; import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; @@ -26,44 +26,70 @@ interface App { uid: string; } - interface FieldLocationAppListProps { apps: App[]; position: "left" | "right"; + toolbarRef: React.RefObject; } -export const FieldLocationAppList = ({ apps, position }: FieldLocationAppListProps) => { +const normalize = (text: string) => + text + .toLowerCase() + .replace(/[^a-z0-9 ]/gi, "") + .trim(); + +export const FieldLocationAppList = ({ + apps, + position, + toolbarRef, +}: FieldLocationAppListProps) => { + const remainingApps = apps.filter((app, index) => index !== 0); const [search, setSearch] = useState(""); - const [filteredApps, setFilteredApps] = useState(apps); - const appRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); - useEffect(() => { - const filtered = apps.filter(app => - app.title.toLowerCase().includes(search.toLowerCase()) - ); - setFilteredApps(filtered); - }, [search, apps]); + const filteredApps = useMemo(() => { + if (!search.trim()) return remainingApps; - const handleAppClick = (app: App) => { - const appElement = appRefs.current[app.uid]; - const positionData = appElement?.getBoundingClientRect(); - visualBuilderPostMessage?.send(VisualBuilderPostMessageEvents.FIELD_LOCATION_SELECTED_APP, { - app: app, - position: positionData, + const normalizedSearch = normalize(search); + return remainingApps.filter((app) => { + return ( + normalize(app.title).includes(normalizedSearch) + ); }); - } + }, [search, remainingApps]); + + const handleAppClick = (app: App) => { + visualBuilderPostMessage?.send( + VisualBuilderPostMessageEvents.FIELD_LOCATION_SELECTED_APP, + { + app: app, + position: toolbarRef.current?.getBoundingClientRect(), + } + ); + }; return (
-
+
-
+ +
{filteredApps.length === 0 && ( -
- +
+ No matching results found!
)} - {filteredApps - .filter((_, index) => index !== 0) - .map((app) => ( + {filteredApps.map((app) => ( +
handleAppClick(app)} + >
(appRefs.current[app.uid] = el)} - className={visualBuilderStyles()["visual-builder__field-location-app-list__item"]} - onClick={() => handleAppClick(app)} + className={ + visualBuilderStyles()[ + "visual-builder__field-location-app-list__item-icon-container" + ] + } > -
- {app.icon ? ( - {app.title} - ) : ( - - )} -
- - {app.title} - + {app.icon ? ( + {app.title} + ) : ( + + )}
- ))} + + {app.title} + +
+ ))}
); -}; \ No newline at end of file +}; diff --git a/src/visualBuilder/components/FieldLocationIcon.tsx b/src/visualBuilder/components/FieldLocationIcon.tsx index c6a744d3..9f0081cf 100644 --- a/src/visualBuilder/components/FieldLocationIcon.tsx +++ b/src/visualBuilder/components/FieldLocationIcon.tsx @@ -12,38 +12,29 @@ export const FieldLocationIcon = ({ multipleFieldToolbarButtonClasses, handleMoreIconClick, moreButtonRef, - isLoading, toolbarRef, }: { fieldLocationData: any; multipleFieldToolbarButtonClasses: any; handleMoreIconClick: () => void; moreButtonRef: any; - isLoading: boolean; toolbarRef: any; }) => { - // if (isLoading) { - // return ( - //
- // - //
- // ); - // } - const currentAppRef = useRef(null); + if (!fieldLocationData?.apps || fieldLocationData.apps.length === 0) { return null; } + const handleAppClick = (app: any) => { + if(!toolbarRef.current) return + visualBuilderPostMessage?.send(VisualBuilderPostMessageEvents.FIELD_LOCATION_SELECTED_APP, { + app, + position: toolbarRef.current?.getBoundingClientRect(), + }); + }; + return (
{ - e.preventDefault(); - e.stopPropagation(); - const positionData = currentAppRef.current?.getBoundingClientRect(); - visualBuilderPostMessage?.send(VisualBuilderPostMessageEvents.FIELD_LOCATION_SELECTED_APP, { - app: fieldLocationData.apps[0], - position: positionData, - }); + e.preventDefault(); + e.stopPropagation(); + handleAppClick(fieldLocationData.apps[0]); }} + data-testid="field-location-icon" > {fieldLocationData.apps[0].icon ? ( (null); const toolbarRef = useRef(null); - const [isFieldLocationLoading, setIsFieldLocationLoading] = useState(true); const [appListPosition, setAppListPosition] = useState<"left" | "right">("right"); const parentPath = @@ -370,24 +369,23 @@ function FieldToolbarComponent( } } ); + return () => { event?.unregister(); }; }, []); useEffect(() => { - setIsFieldLocationLoading(true); const event = visualBuilderPostMessage?.on( VisualBuilderPostMessageEvents.FIELD_LOCATION_DATA, (data: { data: any }) => { - setFieldLocationData(data.data.fieldLocationData); - setIsFieldLocationLoading(false); + setFieldLocationData(data.data.fieldLocationData ); } ); return () => { event?.unregister(); }; - }, [fieldMetadata]); + }, []); @@ -556,13 +554,13 @@ function FieldToolbarComponent( )} - +
{displayAllApps && ( - + )}
); diff --git a/src/visualBuilder/components/__test__/FieldLocationAppList.test.tsx b/src/visualBuilder/components/__test__/FieldLocationAppList.test.tsx new file mode 100644 index 00000000..c2da1ef0 --- /dev/null +++ b/src/visualBuilder/components/__test__/FieldLocationAppList.test.tsx @@ -0,0 +1,216 @@ +import React from "preact/compat"; +import { render, fireEvent, screen } from "@testing-library/preact"; +import { FieldLocationAppList } from "../FieldLocationAppList"; +import visualBuilderPostMessage from "../../utils/visualBuilderPostMessage"; +import { vi } from "vitest"; + +vi.mock("../../utils/visualBuilderPostMessage", () => ({ + default: { + send: vi.fn(), + }, +})); + +vi.mock("../../visualBuilder.style", () => ({ + visualBuilderStyles: () => ({ + "visual-builder__field-location-app-list": "visual-builder__field-location-app-list", + "visual-builder__field-location-app-list--left": "visual-builder__field-location-app-list--left", + "visual-builder__field-location-app-list--right": "visual-builder__field-location-app-list--right", + "visual-builder__field-location-app-list__search-container": "visual-builder__field-location-app-list__search-container", + "visual-builder__field-location-app-list__search-input": "visual-builder__field-location-app-list__search-input", + "visual-builder__field-location-app-list__search-icon": "visual-builder__field-location-app-list__search-icon", + "visual-builder__field-location-app-list__content": "visual-builder__field-location-app-list__content", + "visual-builder__field-location-app-list__no-results": "visual-builder__field-location-app-list__no-results", + "visual-builder__field-location-app-list__no-results-text": "visual-builder__field-location-app-list__no-results-text", + "visual-builder__field-location-app-list__item": "visual-builder__field-location-app-list__item", + "visual-builder__field-location-app-list__item-icon-container": "visual-builder__field-location-app-list__item-icon-container", + "visual-builder__field-location-app-list__item-icon": "visual-builder__field-location-app-list__item-icon", + "visual-builder__field-location-app-list__item-title": "visual-builder__field-location-app-list__item-title", + }), +})); + +vi.mock("classnames", () => ({ + default: (...args: any[]) => args.filter(Boolean).join(" "), +})); + +vi.mock("../icons/EmptyAppIcon", () => ({ + EmptyAppIcon: ({ id }: { id: string }) =>
Empty Icon
, +})); + +const mockApps = [ + { + uid: "1", + title: "First App", + app_installation_uid: "install_1", + app_uid: "app_1", + contentTypeUid: "content_1", + entryUid: "entry_1", + fieldDataType: "text", + fieldDisplayName: "Title", + fieldPath: "title", + locale: "en-us", + manifest: { + uid: "manifest_1", + name: "First App", + description: "First app description", + icon: "icon1.png", + visibility: "public", + }, + }, + { + uid: "2", + title: "Second App", + app_installation_uid: "install_2", + app_uid: "app_2", + contentTypeUid: "content_2", + entryUid: "entry_2", + fieldDataType: "text", + fieldDisplayName: "Description", + fieldPath: "description", + locale: "en-us", + manifest: { + uid: "manifest_2", + name: "Second App", + description: "Second app description", + icon: "icon2.png", + visibility: "public", + }, + }, + { + uid: "3", + title: "Third App", + app_installation_uid: "install_3", + app_uid: "app_3", + contentTypeUid: "content_3", + entryUid: "entry_3", + fieldDataType: "text", + fieldDisplayName: "Content", + fieldPath: "content", + locale: "en-us", + manifest: { + uid: "manifest_3", + name: "Third App", + description: "Third app description", + icon: "icon3.png", + visibility: "public", + }, + }, +]; + +describe("FieldLocationAppList", () => { + const mockToolbarRef = { + current: { + getBoundingClientRect: () => ({ top: 0, left: 0, right: 100, bottom: 50, width: 100, height: 50, x: 0, y: 0 }), + } as HTMLDivElement, + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should render the app list with search input", () => { + render(); + + expect(screen.getByPlaceholderText("Search for Apps")).toBeInTheDocument(); + expect(screen.getByText("Second App")).toBeInTheDocument(); + expect(screen.getByText("Third App")).toBeInTheDocument(); + }); + + it("should not render the first app (index 0)", () => { + render(); + + expect(screen.queryByText("First App")).not.toBeInTheDocument(); + expect(screen.getByText("Second App")).toBeInTheDocument(); + expect(screen.getByText("Third App")).toBeInTheDocument(); + }); + + it("should filter apps when searching", () => { + render(); + + const searchInput = screen.getByPlaceholderText("Search for Apps"); + fireEvent.input(searchInput, { target: { value: "Second" } }); + + expect(screen.getByText("Second App")).toBeInTheDocument(); + expect(screen.queryByText("Third App")).not.toBeInTheDocument(); + }); + + it("should show no results message when search has no matches", () => { + render(); + + const searchInput = screen.getByPlaceholderText("Search for Apps"); + fireEvent.input(searchInput, { target: { value: "NonExistent" } }); + + expect(screen.getByText("No matching results found!")).toBeInTheDocument(); + expect(screen.queryByText("Second App")).not.toBeInTheDocument(); + expect(screen.queryByText("Third App")).not.toBeInTheDocument(); + }); + + it("should send event when app is clicked", () => { + render(); + + const secondApp = screen.getByText("Second App"); + fireEvent.click(secondApp); + + expect(visualBuilderPostMessage?.send).toHaveBeenCalledWith("field-location-selected-app", { + app: mockApps[1], + position: { top: 0, left: 0, right: 100, bottom: 50, width: 100, height: 50, x: 0, y: 0 }, + }); + }); + + + + + + it("should apply correct CSS classes for right position", () => { + const { container } = render(); + + const appList = container.firstChild as HTMLElement; + expect(appList).toHaveClass("visual-builder__field-location-app-list"); + }); + + it("should apply correct CSS classes for left position", () => { + const { container } = render(); + + const appList = container.firstChild as HTMLElement; + expect(appList).toHaveClass("visual-builder__field-location-app-list"); + }); + + it("should handle empty apps array", () => { + render(); + + expect(screen.getByText("No matching results found!")).toBeInTheDocument(); + }); + + it("should handle single app (which gets filtered out)", () => { + const singleApp = [mockApps[0]]; + render(); + + expect(screen.getByText("No matching results found!")).toBeInTheDocument(); + expect(screen.queryByText("First App")).not.toBeInTheDocument(); + }); + + it("should handle case-insensitive search", () => { + render(); + + const searchInput = screen.getByPlaceholderText("Search for Apps"); + fireEvent.input(searchInput, { target: { value: "second" } }); + + expect(screen.getByText("Second App")).toBeInTheDocument(); + expect(screen.queryByText("Third App")).not.toBeInTheDocument(); + }); + + it("should clear search results when search input is cleared", () => { + render(); + + const searchInput = screen.getByPlaceholderText("Search for Apps"); + + fireEvent.input(searchInput, { target: { value: "Second" } }); + expect(screen.getByText("Second App")).toBeInTheDocument(); + expect(screen.queryByText("Third App")).not.toBeInTheDocument(); + + fireEvent.input(searchInput, { target: { value: "" } }); + expect(screen.getByText("Second App")).toBeInTheDocument(); + expect(screen.getByText("Third App")).toBeInTheDocument(); + }); + + +}); diff --git a/src/visualBuilder/components/__test__/FieldLocationIcon.test.tsx b/src/visualBuilder/components/__test__/FieldLocationIcon.test.tsx new file mode 100644 index 00000000..b9fa0314 --- /dev/null +++ b/src/visualBuilder/components/__test__/FieldLocationIcon.test.tsx @@ -0,0 +1,41 @@ +import React from "preact/compat"; +import { render, fireEvent } from "@testing-library/preact"; +import { FieldLocationIcon } from "../FieldLocationIcon"; +import visualBuilderPostMessage from "../../utils/visualBuilderPostMessage"; +import { vi } from "vitest"; + + + +describe("FieldLocationIcon", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should render fieldlocation icon when we have data in fieldLocationData", () => { + const { getByTestId } = render( + {}} + moreButtonRef={null} + toolbarRef={null} + /> + ); + expect(getByTestId("field-location-icon")).toBeInTheDocument(); + }); + + it("should not render fieldlocation icon when we don't have data in fieldLocationData", () => { + const { queryByTestId } = render( + {}} + moreButtonRef={null} + toolbarRef={null} + /> + ); + expect(queryByTestId("field-location-icon")).not.toBeInTheDocument(); + }); + + +}); \ No newline at end of file diff --git a/src/visualBuilder/components/icons/index.tsx b/src/visualBuilder/components/icons/index.tsx index 1793d6fc..9e4901d1 100644 --- a/src/visualBuilder/components/icons/index.tsx +++ b/src/visualBuilder/components/icons/index.tsx @@ -161,8 +161,8 @@ export function EditIcon(): JSX.Element { return ( Date: Tue, 1 Jul 2025 17:35:11 +0530 Subject: [PATCH 07/23] fix: undo unneccsary changes --- src/index.ts | 1 + src/visualBuilder/components/FieldToolbar.tsx | 83 ++++++++++++------- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/src/index.ts b/src/index.ts index 11164ffd..45074abc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import LightLivePreviewHoC from "./light-sdk"; export type IStackSdk = ExternalStackSdkType; + const ContentstackLivePreview = typeof process !== "undefined" && (process?.env?.PURGE_PREVIEW_SDK === "true" || diff --git a/src/visualBuilder/components/FieldToolbar.tsx b/src/visualBuilder/components/FieldToolbar.tsx index 13cddad0..1ce33de6 100644 --- a/src/visualBuilder/components/FieldToolbar.tsx +++ b/src/visualBuilder/components/FieldToolbar.tsx @@ -61,6 +61,7 @@ interface MultipleFieldToolbarProps { } function handleReplaceAsset(fieldMetadata: CslpData) { + // TODO avoid sending whole fieldMetadata visualBuilderPostMessage?.send( VisualBuilderPostMessageEvents.OPEN_ASSET_MODAL, { @@ -123,7 +124,9 @@ function FieldToolbarComponent( const [displayAllApps, setDisplayAllApps] = useState(false); const moreButtonRef = useRef(null); const toolbarRef = useRef(null); - const [appListPosition, setAppListPosition] = useState<"left" | "right">("right"); + const [appListPosition, setAppListPosition] = useState<"left" | "right">( + "right" + ); const parentPath = fieldMetadata?.multipleFieldMetadata?.parentDetails?.parentCslpValue || @@ -168,6 +171,13 @@ function FieldToolbarComponent( isMultiple = (fieldSchema as IReferenceContentTypeSchema) .field_metadata.ref_multiple; + // field is multiple but an instance is not selected + // instead the whole field (all instances) is selected. + // Currently, when whole featured_blogs is selected in canvas, + // the fieldPathWithIndex and instance.fieldPathWithIndex are the same + // cannot rely on -1 index, as the non-negative index then refers to the index of + // the featured_blogs block in page_components + // It is not needed except taxanomy. isWholeMultipleField = isMultiple && (fieldMetadata.fieldPathWithIndex === @@ -176,29 +186,33 @@ function FieldToolbarComponent( isReplaceAllowed = ALLOWED_REPLACE_FIELDS.includes(fieldType) && !isWholeMultipleField; + // if ( + // DEFAULT_MULTIPLE_FIELDS.includes(fieldType) && + // isWholeMultipleField && + // !isVariant + // ) { + // return null; + // } } const invertTooltipPosition = targetElement.getBoundingClientRect().top <= TOOLTIP_TOP_EDGE_BUFFER; const handleMoreIconClick = () => { - - - if(toolbarRef.current){ - const rect=toolbarRef.current.getBoundingClientRect(); - const spaceRight=window.innerWidth-rect.right; - const spaceLeft=rect.left; - let position=''; - - if(spaceRightAPP_LIST_MIN_WIDTH){ - position="right"; - }else { - position=spaceRight>spaceLeft ? "right" : "left"; + if (toolbarRef.current) { + const rect = toolbarRef.current.getBoundingClientRect(); + const spaceRight = window.innerWidth - rect.right; + const spaceLeft = rect.left; + let position = ""; + + if (spaceRight < APP_LIST_MIN_WIDTH) { + position = "left"; + } else if (spaceRight > APP_LIST_MIN_WIDTH) { + position = "right"; + } else { + position = spaceRight > spaceLeft ? "right" : "left"; } setAppListPosition(position as "left" | "right"); - } setDisplayAllApps(!displayAllApps); @@ -221,6 +235,8 @@ function FieldToolbarComponent( )} data-tooltip={"Edit"} onClick={(e) => { + // TODO the listener for field path is attached to the common parent requiring + // propagation to be stopped, should ideally only attach onClick to fieldpath dropdown e.preventDefault(); e.stopPropagation(); handleEdit(fieldMetadata); @@ -331,16 +347,13 @@ function FieldToolbarComponent( ); - - - - + // TODO sibling count is incorrect for this purpose const totalElementCount = targetElement?.parentNode?.childElementCount ?? 1; const indexOfElement = fieldMetadata?.multipleFieldMetadata?.index; - const disableMoveLeft = indexOfElement === 0; - const disableMoveRight = indexOfElement === totalElementCount - 1; + const disableMoveLeft = indexOfElement === 0; // first element + const disableMoveRight = indexOfElement === totalElementCount - 1; // last element useEffect(() => { async function fetchFieldSchema() { @@ -377,9 +390,9 @@ function FieldToolbarComponent( useEffect(() => { const event = visualBuilderPostMessage?.on( - VisualBuilderPostMessageEvents.FIELD_LOCATION_DATA, + VisualBuilderPostMessageEvents.FIELD_LOCATION_DATA, (data: { data: any }) => { - setFieldLocationData(data.data.fieldLocationData ); + setFieldLocationData(data.data.fieldLocationData); } ); return () => { @@ -387,8 +400,6 @@ function FieldToolbarComponent( }; }, []); - - const multipleFieldToolbarButtonClasses = classNames( "visual-builder__button visual-builder__button--secondary", visualBuilderStyles()["visual-builder__button"], @@ -401,9 +412,6 @@ function FieldToolbarComponent( } ); - - - return (
)} - - +
{displayAllApps && ( - + )} ); From 3987e1c92daf17658c0b814e52f2ce8950334d5b Mon Sep 17 00:00:00 2001 From: SahilCs15 Date: Thu, 3 Jul 2025 18:07:42 +0530 Subject: [PATCH 08/23] fix: resolved the issues --- .../components/FieldLocationIcon.tsx | 22 +++-- .../components/__test__/fieldToolbar.test.tsx | 86 ++++++++++++++++++- src/visualBuilder/visualBuilder.style.ts | 4 +- 3 files changed, 101 insertions(+), 11 deletions(-) diff --git a/src/visualBuilder/components/FieldLocationIcon.tsx b/src/visualBuilder/components/FieldLocationIcon.tsx index 9f0081cf..c8719102 100644 --- a/src/visualBuilder/components/FieldLocationIcon.tsx +++ b/src/visualBuilder/components/FieldLocationIcon.tsx @@ -72,14 +72,20 @@ export const FieldLocationIcon = ({ )} - + + { + fieldLocationData.apps.length > 1 && ( + + ) + } ); }; diff --git a/src/visualBuilder/components/__test__/fieldToolbar.test.tsx b/src/visualBuilder/components/__test__/fieldToolbar.test.tsx index d31bf592..44d52537 100644 --- a/src/visualBuilder/components/__test__/fieldToolbar.test.tsx +++ b/src/visualBuilder/components/__test__/fieldToolbar.test.tsx @@ -12,6 +12,9 @@ import { asyncRender } from "../../../__test__/utils"; import { VisualBuilderCslpEventDetails } from "../../types/visualBuilder.types"; import { isFieldDisabled } from "../../utils/isFieldDisabled"; import React from "preact/compat"; +import visualBuilderPostMessage from "../../utils/visualBuilderPostMessage"; +import { FieldLocationIcon } from "../FieldLocationIcon"; +import { VisualBuilderPostMessageEvents } from "../../utils/types/postMessage.types"; vi.mock("../../utils/instanceHandlers", () => ({ handleMoveInstance: vi.fn(), @@ -182,7 +185,7 @@ describe("FieldToolbarComponent", () => { test("display variant icon instead of dropdown", async () => { mockEventDetails.fieldMetadata.variant = "variant"; const { findByTestId } = await asyncRender( - + ); const variantIcon = await findByTestId( @@ -301,4 +304,85 @@ describe("FieldToolbarComponent", () => { expect(replaceButton).toBeDisabled(); } }); + + describe("Field Location Dropdown", () => { + + + + test("FieldLocationIcon shows dropdown icon when multiple apps are available", async () => { + const mockFieldLocationData = { + apps: [ + { + uid: "app1", + title: "First App", + icon: "icon1.png", + app_installation_uid: "install1" + }, + { + uid: "app2", + title: "Second App", + icon: "icon2.png", + app_installation_uid: "install2" + } + ] + }; + + const mockHandleMoreIconClick = vi.fn(); + const mockButtonRef = { current: null }; + const mockToolbarRef = { current: null }; + + const { container } = await asyncRender( + + ); + + const appIcon = container.querySelector('[data-testid="field-location-icon"]'); + expect(appIcon).toBeInTheDocument(); + + const moreButton = container.querySelector('[data-testid="field-location-more-button"]'); + expect(moreButton).toBeInTheDocument(); + + fireEvent.click(moreButton!); + expect(mockHandleMoreIconClick).toHaveBeenCalledTimes(1); + }); + + test("FieldLocationIcon does not show dropdown icon when only one app is available", async () => { + const mockFieldLocationData = { + apps: [ + { + uid: "app1", + title: "First App", + icon: "icon1.png", + app_installation_uid: "install1" + } + ] + }; + + const mockHandleMoreIconClick = vi.fn(); + const mockButtonRef = { current: null }; + const mockToolbarRef = { current: null }; + + const { container } = await asyncRender( + + ); + + const appIcon = container.querySelector('[data-testid="field-location-icon"]'); + expect(appIcon).toBeInTheDocument(); + + const moreButton = container.querySelector('[data-testid="field-location-more-button"]'); + expect(moreButton).not.toBeInTheDocument(); + }); + + }); }); diff --git a/src/visualBuilder/visualBuilder.style.ts b/src/visualBuilder/visualBuilder.style.ts index 8b9a8673..c4a1f4f0 100644 --- a/src/visualBuilder/visualBuilder.style.ts +++ b/src/visualBuilder/visualBuilder.style.ts @@ -755,8 +755,8 @@ export function visualBuilderStyles() { background-color: #8a8f99; `, "visual-builder__field-location-icons-container__app-icon": css` - width: 16px; - height: 16px; + width: 24px; + height: 24px; object-fit: cover; `, "visual-builder__field-location-app-list": css` From bd5095a556930aeef55afc9de7f64d26f5e268d6 Mon Sep 17 00:00:00 2001 From: SahilCs15 Date: Wed, 9 Jul 2025 12:30:03 +0530 Subject: [PATCH 09/23] redner the app on dom --- .../components/FieldLocationAppList.tsx | 8 ++++++++ src/visualBuilder/components/FieldLocationIcon.tsx | 1 + src/visualBuilder/components/FieldToolbar.tsx | 3 +++ .../__test__/FieldLocationAppList.test.tsx | 14 +++++++++++++- .../__test__/multipleElementAddButton.test.ts | 12 ++++++++---- 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/visualBuilder/components/FieldLocationAppList.tsx b/src/visualBuilder/components/FieldLocationAppList.tsx index b5bdba63..c9630717 100644 --- a/src/visualBuilder/components/FieldLocationAppList.tsx +++ b/src/visualBuilder/components/FieldLocationAppList.tsx @@ -4,6 +4,7 @@ import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; import { visualBuilderStyles } from "../visualBuilder.style"; import classNames from "classnames"; +import { CslpData } from "../../utils/cslpdata"; interface App { app_installation_uid: string; @@ -30,6 +31,9 @@ interface FieldLocationAppListProps { apps: App[]; position: "left" | "right"; toolbarRef: React.RefObject; + domEditStack:CslpData[] + setDisplayAllApps: (displayAllApps: boolean) => void; + displayAllApps: boolean; } const normalize = (text: string) => @@ -42,6 +46,8 @@ export const FieldLocationAppList = ({ apps, position, toolbarRef, + domEditStack, + setDisplayAllApps, }: FieldLocationAppListProps) => { const remainingApps = apps.filter((app, index) => index !== 0); const [search, setSearch] = useState(""); @@ -63,8 +69,10 @@ export const FieldLocationAppList = ({ { app: app, position: toolbarRef.current?.getBoundingClientRect(), + DomEditStack:domEditStack } ); + setDisplayAllApps(false); }; return ( diff --git a/src/visualBuilder/components/FieldLocationIcon.tsx b/src/visualBuilder/components/FieldLocationIcon.tsx index c8719102..0c8b9fee 100644 --- a/src/visualBuilder/components/FieldLocationIcon.tsx +++ b/src/visualBuilder/components/FieldLocationIcon.tsx @@ -32,6 +32,7 @@ export const FieldLocationIcon = ({ visualBuilderPostMessage?.send(VisualBuilderPostMessageEvents.FIELD_LOCATION_SELECTED_APP, { app, position: toolbarRef.current?.getBoundingClientRect(), + DomEditStack:fieldLocationData.DomEditStack }); }; diff --git a/src/visualBuilder/components/FieldToolbar.tsx b/src/visualBuilder/components/FieldToolbar.tsx index 1ce33de6..b477320f 100644 --- a/src/visualBuilder/components/FieldToolbar.tsx +++ b/src/visualBuilder/components/FieldToolbar.tsx @@ -579,6 +579,9 @@ function FieldToolbarComponent( toolbarRef={toolbarRef} apps={fieldLocationData?.apps || ([] as any[])} position={appListPosition} + domEditStack={fieldLocationData.DomEditStack} + setDisplayAllApps={setDisplayAllApps} + displayAllApps={displayAllApps} /> )} diff --git a/src/visualBuilder/components/__test__/FieldLocationAppList.test.tsx b/src/visualBuilder/components/__test__/FieldLocationAppList.test.tsx index c2da1ef0..4c158351 100644 --- a/src/visualBuilder/components/__test__/FieldLocationAppList.test.tsx +++ b/src/visualBuilder/components/__test__/FieldLocationAppList.test.tsx @@ -145,7 +145,8 @@ describe("FieldLocationAppList", () => { }); it("should send event when app is clicked", () => { - render(); + const mockSetDisplayAllApps = vi.fn(); + render(); const secondApp = screen.getByText("Second App"); fireEvent.click(secondApp); @@ -153,9 +154,20 @@ describe("FieldLocationAppList", () => { expect(visualBuilderPostMessage?.send).toHaveBeenCalledWith("field-location-selected-app", { app: mockApps[1], position: { top: 0, left: 0, right: 100, bottom: 50, width: 100, height: 50, x: 0, y: 0 }, + DomEditStack: [] }); }); + it("should close the app list when an app is selected", () => { + const mockSetDisplayAllApps = vi.fn(); + render(); + + const secondApp = screen.getByText("Second App"); + fireEvent.click(secondApp); + + expect(mockSetDisplayAllApps).toHaveBeenCalledWith(false); + }); + diff --git a/src/visualBuilder/utils/__test__/multipleElementAddButton.test.ts b/src/visualBuilder/utils/__test__/multipleElementAddButton.test.ts index fd3e44ae..d4ec564a 100644 --- a/src/visualBuilder/utils/__test__/multipleElementAddButton.test.ts +++ b/src/visualBuilder/utils/__test__/multipleElementAddButton.test.ts @@ -92,7 +92,8 @@ describe("getChildrenDirection", () => { }); afterEach(() => { - document.getElementsByTagName("body")[0].innerHTML = ""; + vi.clearAllTimers(); + document.body.innerHTML = ''; vi.clearAllMocks(); }); @@ -232,7 +233,8 @@ describe("handleAddButtonsForMultiple", () => { }); afterEach(() => { - document.getElementsByTagName("body")[0].innerHTML = ""; + vi.clearAllTimers(); + document.body.innerHTML = ''; vi.clearAllMocks(); }); @@ -466,7 +468,8 @@ describe("handleAddButtonsForMultiple", () => { }); afterEach(() => { - document.getElementsByTagName("body")[0].innerHTML = ""; + vi.clearAllTimers(); + document.body.innerHTML = ''; vi.clearAllMocks(); }); @@ -590,7 +593,8 @@ describe("removeAddInstanceButtons", () => { }); afterEach(() => { - document.getElementsByTagName("body")[0].innerHTML = ""; + vi.clearAllTimers(); + document.body.innerHTML = ''; vi.clearAllMocks(); }); From 2cb9824dde552548a66d5a1ae79f1b98b089a535 Mon Sep 17 00:00:00 2001 From: SahilCs15 Date: Wed, 9 Jul 2025 15:58:43 +0530 Subject: [PATCH 10/23] fix: disable scroll when field modifer is active --- src/visualBuilder/index.ts | 16 ++++++++++++++++ .../utils/types/postMessage.types.ts | 3 ++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/visualBuilder/index.ts b/src/visualBuilder/index.ts index 6b6d005a..117f505f 100644 --- a/src/visualBuilder/index.ts +++ b/src/visualBuilder/index.ts @@ -371,6 +371,22 @@ export class VisualBuilder { VisualBuilderPostMessageEvents.SEND_VARIANT_AND_LOCALE ); + visualBuilderPostMessage?.on<{ + scroll: boolean + }>( + VisualBuilderPostMessageEvents.TOGGLE_SCROLL, + (event) => { + if (!event.data.scroll) { + document.body.style.overflow = 'hidden' + } else { + document.body.style.overflow = 'auto' + } + } + ); + + + + useHideFocusOverlayPostMessageEvent({ overlayWrapper: this.overlayWrapper, visualBuilderContainer: this.visualBuilderContainer, diff --git a/src/visualBuilder/utils/types/postMessage.types.ts b/src/visualBuilder/utils/types/postMessage.types.ts index 1c1e7dc5..ec960e3e 100644 --- a/src/visualBuilder/utils/types/postMessage.types.ts +++ b/src/visualBuilder/utils/types/postMessage.types.ts @@ -52,4 +52,5 @@ export enum VisualBuilderPostMessageEvents { COLLAB_THREADS_REMOVE = "collab-threads-remove", COLLAB_THREAD_REOPEN = "collab-thread-reopen", COLLAB_THREAD_HIGHLIGHT = "collab-thread-highlight", -} \ No newline at end of file + TOGGLE_SCROLL = "toggle-scroll", +} From a0803ad8aeab52fd298ef71a1dab754ae116aa27 Mon Sep 17 00:00:00 2001 From: SahilCs15 Date: Fri, 11 Jul 2025 16:15:04 +0530 Subject: [PATCH 11/23] fix:removed uncessary changes --- .../utils/__test__/multipleElementAddButton.test.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/visualBuilder/utils/__test__/multipleElementAddButton.test.ts b/src/visualBuilder/utils/__test__/multipleElementAddButton.test.ts index d4ec564a..fd3e44ae 100644 --- a/src/visualBuilder/utils/__test__/multipleElementAddButton.test.ts +++ b/src/visualBuilder/utils/__test__/multipleElementAddButton.test.ts @@ -92,8 +92,7 @@ describe("getChildrenDirection", () => { }); afterEach(() => { - vi.clearAllTimers(); - document.body.innerHTML = ''; + document.getElementsByTagName("body")[0].innerHTML = ""; vi.clearAllMocks(); }); @@ -233,8 +232,7 @@ describe("handleAddButtonsForMultiple", () => { }); afterEach(() => { - vi.clearAllTimers(); - document.body.innerHTML = ''; + document.getElementsByTagName("body")[0].innerHTML = ""; vi.clearAllMocks(); }); @@ -468,8 +466,7 @@ describe("handleAddButtonsForMultiple", () => { }); afterEach(() => { - vi.clearAllTimers(); - document.body.innerHTML = ''; + document.getElementsByTagName("body")[0].innerHTML = ""; vi.clearAllMocks(); }); @@ -593,8 +590,7 @@ describe("removeAddInstanceButtons", () => { }); afterEach(() => { - vi.clearAllTimers(); - document.body.innerHTML = ''; + document.getElementsByTagName("body")[0].innerHTML = ""; vi.clearAllMocks(); }); From 27ec2f3442f1fdc1b327e77fda1b8cb1b71b1822 Mon Sep 17 00:00:00 2001 From: SahilCs15 Date: Mon, 14 Jul 2025 17:55:08 +0530 Subject: [PATCH 12/23] fix: addresed the requested cahnges --- src/visualBuilder/components/FieldLocationIcon.tsx | 5 ++++- src/visualBuilder/components/FieldToolbar.tsx | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/visualBuilder/components/FieldLocationIcon.tsx b/src/visualBuilder/components/FieldLocationIcon.tsx index 0c8b9fee..e9b9cf8c 100644 --- a/src/visualBuilder/components/FieldLocationIcon.tsx +++ b/src/visualBuilder/components/FieldLocationIcon.tsx @@ -6,6 +6,7 @@ import React, { useRef } from "preact/compat"; import { LoadingIcon } from "./icons/loading"; import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types"; import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; +import { CslpData } from "../../utils/cslpdata"; export const FieldLocationIcon = ({ fieldLocationData, @@ -13,12 +14,14 @@ export const FieldLocationIcon = ({ handleMoreIconClick, moreButtonRef, toolbarRef, + domEditStack }: { fieldLocationData: any; multipleFieldToolbarButtonClasses: any; handleMoreIconClick: () => void; moreButtonRef: any; toolbarRef: any; + domEditStack:CslpData[] }) => { @@ -32,7 +35,7 @@ export const FieldLocationIcon = ({ visualBuilderPostMessage?.send(VisualBuilderPostMessageEvents.FIELD_LOCATION_SELECTED_APP, { app, position: toolbarRef.current?.getBoundingClientRect(), - DomEditStack:fieldLocationData.DomEditStack + DomEditStack:domEditStack }); }; diff --git a/src/visualBuilder/components/FieldToolbar.tsx b/src/visualBuilder/components/FieldToolbar.tsx index b477320f..c542c83a 100644 --- a/src/visualBuilder/components/FieldToolbar.tsx +++ b/src/visualBuilder/components/FieldToolbar.tsx @@ -1,4 +1,5 @@ import { CslpData } from "../../cslp/types/cslp.types"; +import { CslpData as CslpDataUtil } from "../../utils/cslpdata"; import getChildrenDirection from "../utils/getChildrenDirection"; import { ALLOWED_MODAL_EDITABLE_FIELD, @@ -195,6 +196,9 @@ function FieldToolbarComponent( // } } + const domEditStack=getDOMEditStack(eventDetails.editableElement) as CslpDataUtil[] + + const invertTooltipPosition = targetElement.getBoundingClientRect().top <= TOOLTIP_TOP_EDGE_BUFFER; @@ -570,6 +574,7 @@ function FieldToolbarComponent( handleMoreIconClick={handleMoreIconClick} moreButtonRef={moreButtonRef} toolbarRef={toolbarRef} + domEditStack={domEditStack} /> @@ -579,7 +584,7 @@ function FieldToolbarComponent( toolbarRef={toolbarRef} apps={fieldLocationData?.apps || ([] as any[])} position={appListPosition} - domEditStack={fieldLocationData.DomEditStack} + domEditStack={domEditStack} setDisplayAllApps={setDisplayAllApps} displayAllApps={displayAllApps} /> From 17423d5e34094d91c0840a49dbee913be3b4bc37 Mon Sep 17 00:00:00 2001 From: SahilCs15 Date: Wed, 16 Jul 2025 12:07:59 +0530 Subject: [PATCH 13/23] fix: fixed conficts --- src/visualBuilder/components/icons/index.tsx | 31 ++++++++++--------- .../generators/generateToolbar.tsx | 1 + 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/visualBuilder/components/icons/index.tsx b/src/visualBuilder/components/icons/index.tsx index 9e4901d1..0b51a27d 100644 --- a/src/visualBuilder/components/icons/index.tsx +++ b/src/visualBuilder/components/icons/index.tsx @@ -311,6 +311,22 @@ export function WarningOctagonIcon(): JSX.Element { ); } +export function MoreIcon(): JSX.Element { + return ( + + + + + + ); +} + export function ContentTypeIcon(): JSX.Element { return (
- - - - - ); -} \ No newline at end of file diff --git a/src/visualBuilder/generators/generateToolbar.tsx b/src/visualBuilder/generators/generateToolbar.tsx index 725b2c98..b15aa768 100644 --- a/src/visualBuilder/generators/generateToolbar.tsx +++ b/src/visualBuilder/generators/generateToolbar.tsx @@ -178,6 +178,7 @@ export function removeFieldToolbar(toolbar: Element) { const toolbarEvents = [ VisualBuilderPostMessageEvents.DELETE_INSTANCE, VisualBuilderPostMessageEvents.UPDATE_DISCUSSION_ID, + VisualBuilderPostMessageEvents.FIELD_LOCATION_DATA ]; toolbarEvents.forEach((event) => { //@ts-expect-error - We are accessing private method here, but it is necessary to clean up the event listeners. From 53c6e77c2d49718c748895d3bdecb7676461fe8d Mon Sep 17 00:00:00 2001 From: SahilCs15 Date: Wed, 16 Jul 2025 14:28:13 +0530 Subject: [PATCH 14/23] fix: fixed the required changes --- .../components/FieldLocationIcon.tsx | 2 +- .../__test__/FieldLocationAppList.test.tsx | 64 +++++++++++---- .../__test__/FieldLocationIcon.test.tsx | 82 +++++++++++++++++++ .../components/__test__/fieldToolbar.test.tsx | 81 ------------------ 4 files changed, 129 insertions(+), 100 deletions(-) diff --git a/src/visualBuilder/components/FieldLocationIcon.tsx b/src/visualBuilder/components/FieldLocationIcon.tsx index e9b9cf8c..a9396de9 100644 --- a/src/visualBuilder/components/FieldLocationIcon.tsx +++ b/src/visualBuilder/components/FieldLocationIcon.tsx @@ -26,7 +26,7 @@ export const FieldLocationIcon = ({ - if (!fieldLocationData?.apps || fieldLocationData.apps.length === 0) { + if (!fieldLocationData?.apps || fieldLocationData?.apps?.length === 0) { return null; } diff --git a/src/visualBuilder/components/__test__/FieldLocationAppList.test.tsx b/src/visualBuilder/components/__test__/FieldLocationAppList.test.tsx index 4c158351..ca4873c6 100644 --- a/src/visualBuilder/components/__test__/FieldLocationAppList.test.tsx +++ b/src/visualBuilder/components/__test__/FieldLocationAppList.test.tsx @@ -29,7 +29,21 @@ vi.mock("../../visualBuilder.style", () => ({ })); vi.mock("classnames", () => ({ - default: (...args: any[]) => args.filter(Boolean).join(" "), + default: (...args: any[]) => { + const classes: string[] = []; + args.forEach(arg => { + if (typeof arg === 'string') { + classes.push(arg); + } else if (typeof arg === 'object' && arg !== null) { + Object.entries(arg).forEach(([className, condition]) => { + if (condition) { + classes.push(className); + } + }); + } + }); + return classes.join(' '); + }, })); vi.mock("../icons/EmptyAppIcon", () => ({ @@ -108,7 +122,7 @@ describe("FieldLocationAppList", () => { }); it("should render the app list with search input", () => { - render(); + render( {}} displayAllApps={true} />); expect(screen.getByPlaceholderText("Search for Apps")).toBeInTheDocument(); expect(screen.getByText("Second App")).toBeInTheDocument(); @@ -116,7 +130,7 @@ describe("FieldLocationAppList", () => { }); it("should not render the first app (index 0)", () => { - render(); + render( {}} displayAllApps={true} />); expect(screen.queryByText("First App")).not.toBeInTheDocument(); expect(screen.getByText("Second App")).toBeInTheDocument(); @@ -124,7 +138,7 @@ describe("FieldLocationAppList", () => { }); it("should filter apps when searching", () => { - render(); + render( {}} displayAllApps={true} />); const searchInput = screen.getByPlaceholderText("Search for Apps"); fireEvent.input(searchInput, { target: { value: "Second" } }); @@ -134,7 +148,7 @@ describe("FieldLocationAppList", () => { }); it("should show no results message when search has no matches", () => { - render(); + render( {}} displayAllApps={true} />); const searchInput = screen.getByPlaceholderText("Search for Apps"); fireEvent.input(searchInput, { target: { value: "NonExistent" } }); @@ -168,40 +182,54 @@ describe("FieldLocationAppList", () => { expect(mockSetDisplayAllApps).toHaveBeenCalledWith(false); }); - - it("should apply correct CSS classes for right position", () => { - const { container } = render(); - + const { container } = render( + {}} + displayAllApps={true} + /> + ); const appList = container.firstChild as HTMLElement; - expect(appList).toHaveClass("visual-builder__field-location-app-list"); + expect(appList).toHaveClass("visual-builder__field-location-app-list--right"); }); - it("should apply correct CSS classes for left position", () => { - const { container } = render(); - + it("should apply correct CSS classes and left position style for left position", () => { + const { container } = render( + {}} + displayAllApps={true} + /> + ); const appList = container.firstChild as HTMLElement; - expect(appList).toHaveClass("visual-builder__field-location-app-list"); + expect(appList).toHaveClass("visual-builder__field-location-app-list--left"); }); it("should handle empty apps array", () => { - render(); + render( {}} displayAllApps={true} />); expect(screen.getByText("No matching results found!")).toBeInTheDocument(); }); it("should handle single app (which gets filtered out)", () => { const singleApp = [mockApps[0]]; - render(); + render( {}} displayAllApps={true} />); expect(screen.getByText("No matching results found!")).toBeInTheDocument(); expect(screen.queryByText("First App")).not.toBeInTheDocument(); }); it("should handle case-insensitive search", () => { - render(); + render( {}} displayAllApps={true} />); const searchInput = screen.getByPlaceholderText("Search for Apps"); fireEvent.input(searchInput, { target: { value: "second" } }); @@ -211,7 +239,7 @@ describe("FieldLocationAppList", () => { }); it("should clear search results when search input is cleared", () => { - render(); + render( {}} displayAllApps={true} />); const searchInput = screen.getByPlaceholderText("Search for Apps"); diff --git a/src/visualBuilder/components/__test__/FieldLocationIcon.test.tsx b/src/visualBuilder/components/__test__/FieldLocationIcon.test.tsx index b9fa0314..a2311068 100644 --- a/src/visualBuilder/components/__test__/FieldLocationIcon.test.tsx +++ b/src/visualBuilder/components/__test__/FieldLocationIcon.test.tsx @@ -3,6 +3,7 @@ import { render, fireEvent } from "@testing-library/preact"; import { FieldLocationIcon } from "../FieldLocationIcon"; import visualBuilderPostMessage from "../../utils/visualBuilderPostMessage"; import { vi } from "vitest"; +import { asyncRender } from "../../../__test__/utils"; @@ -19,6 +20,7 @@ describe("FieldLocationIcon", () => { handleMoreIconClick={() => {}} moreButtonRef={null} toolbarRef={null} + /> ); expect(getByTestId("field-location-icon")).toBeInTheDocument(); @@ -37,5 +39,85 @@ describe("FieldLocationIcon", () => { expect(queryByTestId("field-location-icon")).not.toBeInTheDocument(); }); + describe("Field Location Dropdown", () => { + + + + test("FieldLocationIcon shows dropdown icon when multiple apps are available", async () => { + const mockFieldLocationData = { + apps: [ + { + uid: "app1", + title: "First App", + icon: "icon1.png", + app_installation_uid: "install1" + }, + { + uid: "app2", + title: "Second App", + icon: "icon2.png", + app_installation_uid: "install2" + } + ] + }; + + const mockHandleMoreIconClick = vi.fn(); + const mockButtonRef = { current: null }; + const mockToolbarRef = { current: null }; + + const { container } = await asyncRender( + + ); + + const appIcon = container.querySelector('[data-testid="field-location-icon"]'); + expect(appIcon).toBeInTheDocument(); + + const moreButton = container.querySelector('[data-testid="field-location-more-button"]'); + expect(moreButton).toBeInTheDocument(); + + fireEvent.click(moreButton!); + expect(mockHandleMoreIconClick).toHaveBeenCalledTimes(1); + }); + + test("FieldLocationIcon does not show dropdown icon when only one app is available", async () => { + const mockFieldLocationData = { + apps: [ + { + uid: "app1", + title: "First App", + icon: "icon1.png", + app_installation_uid: "install1" + } + ] + }; + + const mockHandleMoreIconClick = vi.fn(); + const mockButtonRef = { current: null }; + const mockToolbarRef = { current: null }; + + const { container } = await asyncRender( + + ); + + const appIcon = container.querySelector('[data-testid="field-location-icon"]'); + expect(appIcon).toBeInTheDocument(); + + const moreButton = container.querySelector('[data-testid="field-location-more-button"]'); + expect(moreButton).not.toBeInTheDocument(); + }); + + }); }); \ No newline at end of file diff --git a/src/visualBuilder/components/__test__/fieldToolbar.test.tsx b/src/visualBuilder/components/__test__/fieldToolbar.test.tsx index 44d52537..59e43282 100644 --- a/src/visualBuilder/components/__test__/fieldToolbar.test.tsx +++ b/src/visualBuilder/components/__test__/fieldToolbar.test.tsx @@ -304,85 +304,4 @@ describe("FieldToolbarComponent", () => { expect(replaceButton).toBeDisabled(); } }); - - describe("Field Location Dropdown", () => { - - - - test("FieldLocationIcon shows dropdown icon when multiple apps are available", async () => { - const mockFieldLocationData = { - apps: [ - { - uid: "app1", - title: "First App", - icon: "icon1.png", - app_installation_uid: "install1" - }, - { - uid: "app2", - title: "Second App", - icon: "icon2.png", - app_installation_uid: "install2" - } - ] - }; - - const mockHandleMoreIconClick = vi.fn(); - const mockButtonRef = { current: null }; - const mockToolbarRef = { current: null }; - - const { container } = await asyncRender( - - ); - - const appIcon = container.querySelector('[data-testid="field-location-icon"]'); - expect(appIcon).toBeInTheDocument(); - - const moreButton = container.querySelector('[data-testid="field-location-more-button"]'); - expect(moreButton).toBeInTheDocument(); - - fireEvent.click(moreButton!); - expect(mockHandleMoreIconClick).toHaveBeenCalledTimes(1); - }); - - test("FieldLocationIcon does not show dropdown icon when only one app is available", async () => { - const mockFieldLocationData = { - apps: [ - { - uid: "app1", - title: "First App", - icon: "icon1.png", - app_installation_uid: "install1" - } - ] - }; - - const mockHandleMoreIconClick = vi.fn(); - const mockButtonRef = { current: null }; - const mockToolbarRef = { current: null }; - - const { container } = await asyncRender( - - ); - - const appIcon = container.querySelector('[data-testid="field-location-icon"]'); - expect(appIcon).toBeInTheDocument(); - - const moreButton = container.querySelector('[data-testid="field-location-more-button"]'); - expect(moreButton).not.toBeInTheDocument(); - }); - - }); }); From 6837e62f73fbb6cc46adf6d4454157181d428043 Mon Sep 17 00:00:00 2001 From: SahilCs15 Date: Wed, 16 Jul 2025 14:37:33 +0530 Subject: [PATCH 15/23] fix: removed not requied changes --- .../__test__/FieldLocationAppList.test.tsx | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/src/visualBuilder/components/__test__/FieldLocationAppList.test.tsx b/src/visualBuilder/components/__test__/FieldLocationAppList.test.tsx index ca4873c6..59f76e8a 100644 --- a/src/visualBuilder/components/__test__/FieldLocationAppList.test.tsx +++ b/src/visualBuilder/components/__test__/FieldLocationAppList.test.tsx @@ -28,24 +28,6 @@ vi.mock("../../visualBuilder.style", () => ({ }), })); -vi.mock("classnames", () => ({ - default: (...args: any[]) => { - const classes: string[] = []; - args.forEach(arg => { - if (typeof arg === 'string') { - classes.push(arg); - } else if (typeof arg === 'object' && arg !== null) { - Object.entries(arg).forEach(([className, condition]) => { - if (condition) { - classes.push(className); - } - }); - } - }); - return classes.join(' '); - }, -})); - vi.mock("../icons/EmptyAppIcon", () => ({ EmptyAppIcon: ({ id }: { id: string }) =>
Empty Icon
, })); @@ -214,12 +196,7 @@ describe("FieldLocationAppList", () => { expect(appList).toHaveClass("visual-builder__field-location-app-list--left"); }); - it("should handle empty apps array", () => { - render( {}} displayAllApps={true} />); - - expect(screen.getByText("No matching results found!")).toBeInTheDocument(); - }); - + it("should handle single app (which gets filtered out)", () => { const singleApp = [mockApps[0]]; render( {}} displayAllApps={true} />); From b7bba121cd3093916a0ca891bdc16c53b825c34a Mon Sep 17 00:00:00 2001 From: Sahil Chalke Date: Fri, 18 Jul 2025 18:35:48 +0530 Subject: [PATCH 16/23] fix: modified the way we were handling fieldlocation data fetch (#462) --- src/visualBuilder/components/FieldToolbar.tsx | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/visualBuilder/components/FieldToolbar.tsx b/src/visualBuilder/components/FieldToolbar.tsx index c542c83a..29967560 100644 --- a/src/visualBuilder/components/FieldToolbar.tsx +++ b/src/visualBuilder/components/FieldToolbar.tsx @@ -392,17 +392,23 @@ function FieldToolbarComponent( }; }, []); + + useEffect(() => { - const event = visualBuilderPostMessage?.on( - VisualBuilderPostMessageEvents.FIELD_LOCATION_DATA, - (data: { data: any }) => { - setFieldLocationData(data.data.fieldLocationData); + const fetchFieldLocationData = async () => { + try { + const event = await visualBuilderPostMessage?.send(VisualBuilderPostMessageEvents.FIELD_LOCATION_DATA, { + domEditStack: getDOMEditStack(eventDetails.editableElement) + }); + + setFieldLocationData(event) + } catch (error) { + console.error('Error fetching field location data:', error); } - ); - return () => { - event?.unregister(); }; - }, []); + + fetchFieldLocationData(); + }, [eventDetails.editableElement]); const multipleFieldToolbarButtonClasses = classNames( "visual-builder__button visual-builder__button--secondary", From 220588707fa2329926cc2a087f22e2ec3a9a7d2d Mon Sep 17 00:00:00 2001 From: Ayush Dubey <155638868+csAyushDubey@users.noreply.github.com> Date: Tue, 22 Jul 2025 13:10:20 +0530 Subject: [PATCH 17/23] Merge pull request #467 from contentstack/VE-7005-hover-toolbar-clickability Fix: Issue with hover toolbar click-ability --- src/visualBuilder/listeners/mouseHover.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/visualBuilder/listeners/mouseHover.ts b/src/visualBuilder/listeners/mouseHover.ts index e1cdc0e9..38adc4d8 100644 --- a/src/visualBuilder/listeners/mouseHover.ts +++ b/src/visualBuilder/listeners/mouseHover.ts @@ -200,6 +200,10 @@ function isFieldPathDropdown(target: HTMLElement): boolean { return target.classList.contains("visual-builder__focused-toolbar__field-label-wrapper") || target.classList.contains("visual-builder__focused-toolbar__field-label-wrapper__current-field"); } +function isFieldPathParent(target: HTMLElement): boolean { + return target.classList.contains("visual-builder__focused-toolbar__field-label-wrapper__parent-field"); +} + const throttledMouseHover = throttle(async (params: HandleMouseHoverParams) => { const eventDetails = getCsDataOfElement(params.event); const eventTarget = params.event.target as HTMLElement | null; @@ -221,7 +225,7 @@ const throttledMouseHover = throttle(async (params: HandleMouseHoverParams) => { } if( eventTarget && - isFieldPathDropdown(eventTarget) + (isFieldPathDropdown(eventTarget) || isFieldPathParent(eventTarget)) ) { params.customCursor && hideCustomCursor(params.customCursor); showOutline(); From 46ee07bcb195838d03aef6a0ceb6f4882b55fe37 Mon Sep 17 00:00:00 2001 From: Sahil Chalke Date: Mon, 4 Aug 2025 22:11:18 +0530 Subject: [PATCH 18/23] fix: removed edit icon when the click is on the container (#478) --- src/visualBuilder/components/FieldToolbar.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/visualBuilder/components/FieldToolbar.tsx b/src/visualBuilder/components/FieldToolbar.tsx index c2ab9088..0ad06fa2 100644 --- a/src/visualBuilder/components/FieldToolbar.tsx +++ b/src/visualBuilder/components/FieldToolbar.tsx @@ -149,7 +149,7 @@ function FieldToolbarComponent( disableFieldActions = isDisabled; fieldType = getFieldType(fieldSchema); - isModalEditable = ALLOWED_MODAL_EDITABLE_FIELD.includes(fieldType); + Icon = fieldIcons[fieldType]; @@ -171,6 +171,8 @@ function FieldToolbarComponent( fieldMetadata.instance.fieldPathWithIndex || fieldMetadata.multipleFieldMetadata?.index === -1); + isModalEditable = ALLOWED_MODAL_EDITABLE_FIELD.includes(fieldType) && !isWholeMultipleField; + isReplaceAllowed = ALLOWED_REPLACE_FIELDS.includes(fieldType) && !isWholeMultipleField; // if ( From 8122192f46a29579da04ea7e7c6e0fb102a9043a Mon Sep 17 00:00:00 2001 From: Aditya Pachauri Date: Tue, 15 Jul 2025 12:04:33 +0530 Subject: [PATCH 19/23] chore(VE-6918/error-text): warning message improved --- src/visualBuilder/components/emptyBlock.tsx | 26 ++++++++++++++++++--- src/visualBuilder/visualBuilder.style.ts | 16 +++++++++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/visualBuilder/components/emptyBlock.tsx b/src/visualBuilder/components/emptyBlock.tsx index 4de70fef..404c13f2 100644 --- a/src/visualBuilder/components/emptyBlock.tsx +++ b/src/visualBuilder/components/emptyBlock.tsx @@ -46,7 +46,18 @@ export function EmptyBlock(props: EmptyBlockProps): JSX.Element { visualBuilderStyles()["visual-builder__empty-block-title"] )} > - There are no {blockParentName.toLowerCase()} on this page yet. Click the button below to add one. + This page doesn’t have any{" "} + + {blockParentName.toLowerCase()} + {" "} + added. Click the button below to add one.
); diff --git a/src/visualBuilder/visualBuilder.style.ts b/src/visualBuilder/visualBuilder.style.ts index dc4ea46a..4e398deb 100644 --- a/src/visualBuilder/visualBuilder.style.ts +++ b/src/visualBuilder/visualBuilder.style.ts @@ -121,6 +121,13 @@ export function visualBuilderStyles() { visibility: visible; } `, + "visual-builder__empty-block-plus-icon": css` + font-size: 22px; + font-weight: 300; + display: flex; + align-items: center; + justify-content: center; + `, "visual-builder__overlay--outline": css` position: absolute; outline: 4px solid #715cdd; @@ -571,19 +578,24 @@ export function visualBuilderStyles() { line-height: 100%; color: #647696; `, + "visual-builder__empty-block-field-name": css` + font-weight: 700; + `, "visual-builder__empty-block-add-button": css` height: 32px; border-radius: 4px; background: #f9f8ff; border-color: #6c5ce7; border-width: 1px; - padding: 8px 16px 8px 16px; + padding: 0 16px; font-size: 0.9rem; font-family: Inter; font-weight: 600; color: #6c5ce7; - padding-block: 0px; letter-spacing: 0.01rem; + display: inline-flex; + align-items: center; + justify-content: center; `, "visual-builder__hover-outline": css` position: absolute; From dc3901fafe5560b8a58f57303f934e21bbb73d98 Mon Sep 17 00:00:00 2001 From: Aditya Pachauri Date: Wed, 23 Jul 2025 13:16:21 +0530 Subject: [PATCH 20/23] chore(VE-6918/error-text): component name changed to title case --- src/visualBuilder/components/emptyBlock.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/visualBuilder/components/emptyBlock.tsx b/src/visualBuilder/components/emptyBlock.tsx index 404c13f2..ad932e70 100644 --- a/src/visualBuilder/components/emptyBlock.tsx +++ b/src/visualBuilder/components/emptyBlock.tsx @@ -6,6 +6,7 @@ import { observeParentAndFocusNewInstance } from "../utils/multipleElementAddBut import { ISchemaFieldMap } from "../utils/types/index.types"; import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types"; import React from "preact/compat"; +import { startCase, toLower } from "lodash-es"; interface EmptyBlockProps { details: { @@ -55,7 +56,7 @@ export function EmptyBlock(props: EmptyBlockProps): JSX.Element { ] )} > - {blockParentName.toLowerCase()} + {startCase(toLower(blockParentName))} {" "} added. Click the button below to add one. From ed1c0bb03bc21a92f61c63e8db6120b6b35cb17e Mon Sep 17 00:00:00 2001 From: Aditya Pachauri Date: Fri, 25 Jul 2025 17:42:19 +0530 Subject: [PATCH 21/23] chore(VE-6918/error-text): unit test file added --- .../components/__test__/emptyBlock.test.tsx | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/visualBuilder/components/__test__/emptyBlock.test.tsx diff --git a/src/visualBuilder/components/__test__/emptyBlock.test.tsx b/src/visualBuilder/components/__test__/emptyBlock.test.tsx new file mode 100644 index 00000000..d7d5b2c8 --- /dev/null +++ b/src/visualBuilder/components/__test__/emptyBlock.test.tsx @@ -0,0 +1,73 @@ +import React from "preact/compat"; +import { render, fireEvent, waitFor } from "@testing-library/preact"; +import { EmptyBlock } from "../emptyBlock"; +import visualBuilderPostMessage from "../../utils/visualBuilderPostMessage"; +import { observeParentAndFocusNewInstance } from "../../utils/multipleElementAddButton"; +import { CslpData } from "../../../cslp/types/cslp.types"; +import { ISchemaFieldMap } from "../../utils/types/index.types"; +import { VisualBuilderPostMessageEvents } from "../../utils/types/postMessage.types"; + +vi.mock("../../utils/visualBuilderPostMessage", () => ({ + default: { + send: vi.fn(), + }, +})); + +vi.mock("../../utils/multipleElementAddButton", () => ({ + observeParentAndFocusNewInstance: vi.fn(), +})); + +describe("EmptyBlock", () => { + const mockDetails = { + fieldMetadata: { + cslpValue: "parent.cslp.value", + } as CslpData, + fieldSchema: { + display_name: "Test Block", + } as ISchemaFieldMap, + }; + + afterEach(() => { + vi.clearAllMocks(); + }); + + test("should render correctly", () => { + const { getByText, getByTestId } = render( + + ); + + expect( + getByText( + (_, element) => + element?.textContent === + "This page doesn’t have any Test Block added. Click the button below to add one." + ) + ).toBeTruthy(); + expect( + getByTestId("visual-builder__empty-block-add-button") + ).toBeTruthy(); + expect(getByText("Add Test Block")).toBeTruthy(); + }); + + test("should call sendAddInstanceEvent on button click", async () => { + const { getByTestId } = render(); + const addButton = getByTestId("visual-builder__empty-block-add-button"); + + fireEvent.click(addButton); + + await waitFor(() => { + expect((visualBuilderPostMessage as any).send).toHaveBeenCalledWith( + VisualBuilderPostMessageEvents.ADD_INSTANCE, + { + fieldMetadata: mockDetails.fieldMetadata, + index: 0, + } + ); + }); + + expect(observeParentAndFocusNewInstance).toHaveBeenCalledWith({ + parentCslp: mockDetails.fieldMetadata.cslpValue, + index: 0, + }); + }); +}); From 8ee321af17ce0c5854edebd9834f346536953d3c Mon Sep 17 00:00:00 2001 From: Faraaz Biyabani Date: Wed, 6 Aug 2025 14:17:15 +0530 Subject: [PATCH 22/23] 3.4.0 --- .talismanrc | 2 +- CHANGELOG.md | 57 ++++++++++++++++++++++++++++++++++++++++++++--- README.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 59 insertions(+), 8 deletions(-) diff --git a/.talismanrc b/.talismanrc index c7951c73..1d5f45cc 100644 --- a/.talismanrc +++ b/.talismanrc @@ -5,7 +5,7 @@ fileignoreconfig: - filename: README.md checksum: 568289bbe7c088967493db246dbf29e465382648ac574c1b1236be57d5662a38 - filename: CHANGELOG.md - checksum: ed794e2f5c5884f74af12e5f5bfbb117c08ba454104f929df3deb7627407317a + checksum: 8e2495db48ed58704b0883b052c5d44b9f1a96cff7556235f924d16a47442688 - filename: src/visualBuilder/components/__test__/fieldToolbar.test.tsx checksum: 3badd6a142456b6a361569e6fc546349a38ac6b366bef7fd5255d1e93220444e - filename: src/visualBuilder/components/Collab/ThreadPopup/__test__/CommentTextArea.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 03e0a92b..4dfda309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,51 @@ # Changelog +## [v3.4.0](https://github.com/contentstack/live-preview-sdk/compare/v3.3.0...v3.4.0) + +> 6 August 2025 + +### Fixes + +- fix: multiple reloads in timeline (Mridul Sharma - [#484](https://github.com/contentstack/live-preview-sdk/pull/484)) +- fix: removed edit icon when the click is on the container (#478) (Sahil Chalke - [#485](https://github.com/contentstack/live-preview-sdk/pull/485)) +- Fix: Issue with hover toolbar click-ability (Ayush Dubey - [#467](https://github.com/contentstack/live-preview-sdk/pull/467)) +- fix: modified the way we were handling fieldlocation data fetch (Sahil Chalke - [#462](https://github.com/contentstack/live-preview-sdk/pull/462)) + +### General Changes + +- VE-6918 (Aditya Pachauri - [#487](https://github.com/contentstack/live-preview-sdk/pull/487)) +- Hover Toolbar Click-ability Missed Commit (Ayush Dubey - [#482](https://github.com/contentstack/live-preview-sdk/pull/482)) +- Ve 5474 clean (Sahil Chalke - [#481](https://github.com/contentstack/live-preview-sdk/pull/481)) + +### Fixes + +- fix: fixed the required changes (SahilCs15 - [53c6e77](https://github.com/contentstack/live-preview-sdk/commit/53c6e77c2d49718c748895d3bdecb7676461fe8d)) +- fix: resolved the issues (SahilCs15 - [3987e1c](https://github.com/contentstack/live-preview-sdk/commit/3987e1c92daf17658c0b814e52f2ce8950334d5b)) +- fix: undo unneccsary changes (SahilCs15 - [d0dae47](https://github.com/contentstack/live-preview-sdk/commit/d0dae47dda1bc41fde6ff6205bcee9b046ac2c09)) +- fix: fixed conficts (SahilCs15 - [17423d5](https://github.com/contentstack/live-preview-sdk/commit/17423d5e34094d91c0840a49dbee913be3b4bc37)) +- fix: removed not requied changes (SahilCs15 - [6837e62](https://github.com/contentstack/live-preview-sdk/commit/6837e62f73fbb6cc46adf6d4454157181d428043)) +- fix: disable scroll when field modifer is active (SahilCs15 - [2cb9824](https://github.com/contentstack/live-preview-sdk/commit/2cb9824dde552548a66d5a1ae79f1b98b089a535)) +- fix: addresed the requested cahnges (SahilCs15 - [27ec2f3](https://github.com/contentstack/live-preview-sdk/commit/27ec2f3442f1fdc1b327e77fda1b8cb1b71b1822)) +- fix:removed uncessary changes (SahilCs15 - [a0803ad](https://github.com/contentstack/live-preview-sdk/commit/a0803ad8aeab52fd298ef71a1dab754ae116aa27)) + +### Chores And Housekeeping + +- chore: rendered the fieldmodifier apps added an event listner to send the selected app (SahilCs15 - [0c49039](https://github.com/contentstack/live-preview-sdk/commit/0c490390011da7a4c7b8a4b64e4e53cdab0cc9a3)) +- chore: added test cases for the field location data (SahilCs15 - [6337a6d](https://github.com/contentstack/live-preview-sdk/commit/6337a6de2c41684d6cc70b62f600a4036264577b)) +- chore: fieldlocation fetch completed (SahilCs15 - [bab5a47](https://github.com/contentstack/live-preview-sdk/commit/bab5a47953979af9cc5acc4fc05cbe6623caeb55)) +- chore(VE-6918/error-text): unit test file added (Aditya Pachauri - [ed1c0bb](https://github.com/contentstack/live-preview-sdk/commit/ed1c0bb03bc21a92f61c63e8db6120b6b35cb17e)) +- chore(VE-6918/error-text): warning message improved (Aditya Pachauri - [8122192](https://github.com/contentstack/live-preview-sdk/commit/8122192f46a29579da04ea7e7c6e0fb102a9043a)) +- chore(VE-6918/error-text): component name changed to title case (Aditya Pachauri - [dc3901f](https://github.com/contentstack/live-preview-sdk/commit/dc3901fafe5560b8a58f57303f934e21bbb73d98)) +- chore: test fix from merge operation (Mridul Sharma - [e335877](https://github.com/contentstack/live-preview-sdk/commit/e335877c8f7d120f5f0549463f6ad1a1c81c9176)) + +### Changes to Test Assests + +- test: added test cases for addLivePreviewQueryTags and addParamsToUrl (Mridul Sharma - [e781891](https://github.com/contentstack/live-preview-sdk/commit/e781891b9f65baae7860c11e09d0ac88b0c9b229)) + +### General Changes + +- redner the app on dom (SahilCs15 - [bd5095a](https://github.com/contentstack/live-preview-sdk/commit/bd5095a556930aeef55afc9de7f64d26f5e268d6)) + ## [v3.3.0](https://github.com/contentstack/live-preview-sdk/compare/v3.2.5...v3.3.0) > 24 July 2025 @@ -10,10 +56,15 @@ ### General Changes +- v3.3.0 (Sairaj - [#474](https://github.com/contentstack/live-preview-sdk/pull/474)) - Release 24 July to stage_v3 (Sairaj - [#473](https://github.com/contentstack/live-preview-sdk/pull/473)) - HoverToolbar: Requested Changes (Ayush Dubey - [#464](https://github.com/contentstack/live-preview-sdk/pull/464)) - [Feature] HoverToolbar (Ayush Dubey - [#455](https://github.com/contentstack/live-preview-sdk/pull/455)) +### Chores And Housekeeping + +- chore:increment package version (Aditya Pachauri - [950170c](https://github.com/contentstack/live-preview-sdk/commit/950170c321a8850d6854b9c09f27e6522a5c5798)) + ## [v3.2.5](https://github.com/contentstack/live-preview-sdk/compare/v3.2.4...v3.2.5) > 10 July 2025 @@ -121,6 +172,7 @@ - v3.2.2 (Hitesh Shetty - [#435](https://github.com/contentstack/live-preview-sdk/pull/435)) - Stage v3.2.2 (Hitesh Shetty - [#433](https://github.com/contentstack/live-preview-sdk/pull/433)) +- v3.2.1 (Faraaz Biyabani - [#430](https://github.com/contentstack/live-preview-sdk/pull/430)) ### Fixes @@ -150,6 +202,7 @@ ### General Changes - secrets-scan.yml (Aravind Kumar - [f9c5068](https://github.com/contentstack/live-preview-sdk/commit/f9c5068f3996d39aa73dcabac788950903513e32)) +- policy-scan.yml (Aravind Kumar - [7ae545b](https://github.com/contentstack/live-preview-sdk/commit/7ae545b3167869dc01874c8a75ffaff118d0d00a)) - talismanrc file updated (Aravind Kumar - [8a70888](https://github.com/contentstack/live-preview-sdk/commit/8a70888d24083ff4f5f9c2878d3b94661dabbd08)) - Updated codeowners (Aravind Kumar - [86bc538](https://github.com/contentstack/live-preview-sdk/commit/86bc538fc3f1bb42f2d86cf39ae8568be94880dd)) - issues-jira.yml (Aravind Kumar - [84a292c](https://github.com/contentstack/live-preview-sdk/commit/84a292cc28491bdab4ea9ab819594de320a99e8e)) @@ -157,7 +210,7 @@ ## [v3.2.1](https://github.com/contentstack/live-preview-sdk/compare/v3.2.0...v3.2.1) -> 24 April 2025 +> 23 April 2025 ### Fixes @@ -169,7 +222,6 @@ ### General Changes -- v3.2.1 (Faraaz Biyabani - [#430](https://github.com/contentstack/live-preview-sdk/pull/430)) - Stage-v3.2.1 (Faraaz Biyabani - [#428](https://github.com/contentstack/live-preview-sdk/pull/428)) ### Fixes @@ -195,7 +247,6 @@ - Delete jira.yml (Aravind Kumar - [80a496c](https://github.com/contentstack/live-preview-sdk/commit/80a496c805ce52542221f9bafa11116211f24f73)) - issues-jira.yml (Aravind Kumar - [a429983](https://github.com/contentstack/live-preview-sdk/commit/a4299833949c80c274abd8af8ae81062fd78556f)) - policy-scan.yml (Aravind Kumar - [a4b69a0](https://github.com/contentstack/live-preview-sdk/commit/a4b69a07a50b0d8145122bf99b330c3b22d91e8f)) -- policy-scan.yml (Aravind Kumar - [7ae545b](https://github.com/contentstack/live-preview-sdk/commit/7ae545b3167869dc01874c8a75ffaff118d0d00a)) - Delete sast-scan.yml (Aravind Kumar - [c0bffef](https://github.com/contentstack/live-preview-sdk/commit/c0bffefd3a100a96117b080bd5cd7d2f6302deea)) - Updated codeowners (Aravind Kumar - [b759727](https://github.com/contentstack/live-preview-sdk/commit/b759727225ca20ee5e4b5addb8783a87eb8bc385)) - codeql-analysis.yml (Aravind Kumar - [d90af9f](https://github.com/contentstack/live-preview-sdk/commit/d90af9f61ed6fca7f4fdfac6ad6993006ed771ea)) diff --git a/README.md b/README.md index dea08ca9..dddde365 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Alternatively, if you want to include the package directly in your website HTML ```html