From cf91863bd604adeb1aea163ba2dddc862f8c416b Mon Sep 17 00:00:00 2001 From: Aditya Pachauri Date: Mon, 16 Mar 2026 16:04:57 +0530 Subject: [PATCH 01/15] fix(VP-1133/purge-flag): inversion added --- src/livePreview/editButton/editButton.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/livePreview/editButton/editButton.ts b/src/livePreview/editButton/editButton.ts index df88fc7d..d3cd0bfa 100644 --- a/src/livePreview/editButton/editButton.ts +++ b/src/livePreview/editButton/editButton.ts @@ -570,6 +570,12 @@ effect(function handleWindowTypeChange() { // we need to specify when to run this effect. // here, we run it when the value of windowType changes if (typeof window === "undefined") return; + if ( + typeof process !== "undefined" && + (process?.env?.PURGE_PREVIEW_SDK === "true" || + process?.env?.REACT_APP_PURGE_PREVIEW_SDK === "true") + ) + return; Config.get().windowType; if (LivePreviewEditButton && !isOpeningInTimeline()) { toggleEditButtonElement(); From cef602d1f2013017bf435f4a7a28fc72545ec89d Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Fri, 27 Mar 2026 11:00:03 +0530 Subject: [PATCH 02/15] feat: enhance field label wrapper with request edit access functionality --- .../components/fieldLabelWrapper.tsx | 85 ++++++++++- .../utils/__test__/isFieldDisabled.test.ts | 142 +++++++++++++++++- .../utils/getWorkflowStageDetails.ts | 12 ++ src/visualBuilder/utils/isFieldDisabled.ts | 56 ++++++- .../utils/types/postMessage.types.ts | 1 + 5 files changed, 285 insertions(+), 11 deletions(-) diff --git a/src/visualBuilder/components/fieldLabelWrapper.tsx b/src/visualBuilder/components/fieldLabelWrapper.tsx index af2ed986..e838fc9e 100644 --- a/src/visualBuilder/components/fieldLabelWrapper.tsx +++ b/src/visualBuilder/components/fieldLabelWrapper.tsx @@ -171,7 +171,11 @@ function FieldLabelWrapperComponent( variantUid: props.fieldMetadata.variant, fieldPathWithIndex: props.fieldMetadata.fieldPathWithIndex, }); - const { isDisabled: fieldDisabled, reason } = isFieldDisabled( + const { + isDisabled: fieldDisabled, + reason, + workflowRequestUi, + } = isFieldDisabled( fieldSchema, eventDetails, resolvedVariantPermissions, @@ -179,6 +183,26 @@ function FieldLabelWrapperComponent( entryWorkflowStageDetails, ); + const handleRequestEditAccess = async () => { + try { + await visualBuilderPostMessage?.send( + VisualBuilderPostMessageEvents.OPEN_REQUEST_EDIT_ACCESS, + { + entryUid: props.fieldMetadata.entry_uid, + contentTypeUid: + props.fieldMetadata.content_type_uid, + locale: props.fieldMetadata.locale, + variantUid: props.fieldMetadata.variant, + } + ); + } catch (error) { + console.error( + "Error opening request edit access flow:", + error + ); + } + }; + const handleLinkVariant = async () => { try { if (fieldSchema.field_metadata?.canLinkVariant) { @@ -218,6 +242,11 @@ function FieldLabelWrapperComponent( const hasParentPaths = !!props?.parentPaths?.length; const isVariant = props.fieldMetadata.variant ? true : false; + const usePlainDataTooltip = + reason && + !reason.includes(DisableReason.CanLinkVariant) && + workflowRequestUi == null; + setCurrentField({ text: currentFieldDisplayName, contentTypeName: contentTypeName ?? "", @@ -228,12 +257,11 @@ function FieldLabelWrapperComponent( "visual-builder__tooltip--persistent" ] )} - data-tooltip={!reason?.includes(DisableReason.CanLinkVariant) - ? reason - : undefined} + data-tooltip={ + usePlainDataTooltip ? reason : undefined + } > - {reason - .includes(DisableReason.CanLinkVariant) && ( + {reason?.includes(DisableReason.CanLinkVariant) && (
)} + {workflowRequestUi === "request" && reason ? ( +
+ {reason}{" "} + { + e.stopPropagation(); + void handleRequestEditAccess(); + }} + onKeyDown={(e) => { + if ( + e.key === "Enter" || + e.key === " " + ) { + e.preventDefault(); + void handleRequestEditAccess(); + } + }} + > + Request Edit Access + +
+ ) : null} + {workflowRequestUi === "pending" && reason ? ( +
+ {reason} +
+ ) : null}
) : hasParentPaths ? ( diff --git a/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts b/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts index dee18c8a..96884839 100644 --- a/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts +++ b/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts @@ -11,7 +11,6 @@ import { ResolvedVariantPermissions } from "../getResolvedVariantPermissions"; const resolvedVariantPermissions: ResolvedVariantPermissions = { update: true, }; -import { WORKFLOW_STAGES } from "../constants"; describe("isFieldDisabled", () => { it("should return disabled state due to read-only role", () => { @@ -563,5 +562,146 @@ describe("isFieldDisabled", () => { expect(result.isDisabled).toBe(false); expect(result.reason).toBe(DisableReason.None); }); + + it("should return request-edit workflow message when workflow denies update and requestEditAccess.canRequest", () => { + const fieldSchemaMap: ISchemaFieldMap = {}; + const eventFieldDetails: FieldDetails = { + editableElement: document.createElement("div"), + // @ts-expect-error mocking only required properties + fieldMetadata: { + locale: "en-us", + }, + }; + const entryPermissions: EntryPermissions = { + update: true, + create: true, + read: true, + delete: true, + publish: true, + }; + const workflowStageDetails = { + stage: { + name: "Review Stage", + }, + permissions: { + entry: { + update: false, + }, + }, + requestEditAccess: { + canRequest: true, + hasPending: false, + }, + }; + + const result = isFieldDisabled( + fieldSchemaMap, + eventFieldDetails, + resolvedVariantPermissions, + entryPermissions, + workflowStageDetails + ); + expect(result.isDisabled).toBe(true); + expect(result.reason).toBe( + DisableReason.WorkflowStageRequestEdit({ + stageName: WORKFLOW_STAGES.REVIEW, + }) + ); + expect(result.workflowRequestUi).toBe("request"); + }); + + it("should return pending workflow message when requestEditAccess.hasPending", () => { + const fieldSchemaMap: ISchemaFieldMap = {}; + const eventFieldDetails: FieldDetails = { + editableElement: document.createElement("div"), + // @ts-expect-error mocking only required properties + fieldMetadata: { + locale: "en-us", + }, + }; + const entryPermissions: EntryPermissions = { + update: true, + create: true, + read: true, + delete: true, + publish: true, + }; + const workflowStageDetails = { + stage: { + name: "Review Stage", + }, + permissions: { + entry: { + update: false, + }, + }, + requestEditAccess: { + canRequest: false, + hasPending: true, + }, + }; + + const result = isFieldDisabled( + fieldSchemaMap, + eventFieldDetails, + resolvedVariantPermissions, + entryPermissions, + workflowStageDetails + ); + expect(result.isDisabled).toBe(true); + expect(result.reason).toBe( + DisableReason.WorkflowStageRequestPending({ + stageName: WORKFLOW_STAGES.REVIEW, + }) + ); + expect(result.workflowRequestUi).toBe("pending"); + }); + + it("should fall back to workflow stage message when requestEditAccess is locked for all", () => { + const fieldSchemaMap: ISchemaFieldMap = {}; + const eventFieldDetails: FieldDetails = { + editableElement: document.createElement("div"), + // @ts-expect-error mocking only required properties + fieldMetadata: { + locale: "en-us", + }, + }; + const entryPermissions: EntryPermissions = { + update: true, + create: true, + read: true, + delete: true, + publish: true, + }; + const workflowStageDetails = { + stage: { + name: "Review Stage", + }, + permissions: { + entry: { + update: false, + }, + }, + requestEditAccess: { + canRequest: false, + hasPending: false, + }, + }; + + const result = isFieldDisabled( + fieldSchemaMap, + eventFieldDetails, + resolvedVariantPermissions, + entryPermissions, + workflowStageDetails + ); + expect(result.isDisabled).toBe(true); + expect(result.reason).toBe( + DisableReason.WorkflowStagePermission({ + stageName: WORKFLOW_STAGES.REVIEW, + }) + ); + expect(result.workflowRequestUi).toBeUndefined(); + }); }); }); diff --git a/src/visualBuilder/utils/getWorkflowStageDetails.ts b/src/visualBuilder/utils/getWorkflowStageDetails.ts index 98b63ac9..2bc744bb 100644 --- a/src/visualBuilder/utils/getWorkflowStageDetails.ts +++ b/src/visualBuilder/utils/getWorkflowStageDetails.ts @@ -42,9 +42,19 @@ export async function getWorkflowStageDetails({ update: true, }, }, + requestEditAccess: { + canRequest: false, + hasPending: false, + }, }; } +/** Mirrors visual-editor GET_WORKFLOW_STAGE_DETAILS payload (QuickForm / canvas alignment). */ +export interface WorkflowStageRequestEditAccess { + canRequest: boolean; + hasPending: boolean; +} + export interface WorkflowStageDetails { stage: | { @@ -56,4 +66,6 @@ export interface WorkflowStageDetails { update: boolean; }; }; + /** Present when returned by visual-editor; omitted in legacy SDK-only fallbacks. */ + requestEditAccess?: WorkflowStageRequestEditAccess; } diff --git a/src/visualBuilder/utils/isFieldDisabled.ts b/src/visualBuilder/utils/isFieldDisabled.ts index 86c61f27..c2683ecd 100644 --- a/src/visualBuilder/utils/isFieldDisabled.ts +++ b/src/visualBuilder/utils/isFieldDisabled.ts @@ -30,16 +30,26 @@ export const DisableReason = { stageName: string; }) => `Editing is restricted for your role or by the rules for the '${stageName}' stage. Contact your admin for edit access.`, + WorkflowStageRequestEdit: ({ stageName }: { stageName: string }) => + `You do not have the edit access to this entry on the '${stageName}' workflow stage.`, + WorkflowStageRequestPending: ({ stageName }: { stageName: string }) => + `You do not have the edit access to this entry on the '${stageName}' workflow stage. Your request has been sent and is awaiting approval.`, } as const; -interface FieldDisableState { +export interface FieldDisableState { isDisabled: boolean; reason: string; + /** Canvas: workflow stage lock with request-edit UX (see fieldLabelWrapper). */ + workflowRequestUi?: "request" | "pending"; } const getDisableReason = ( flags: Record, - params?: Record + params?: { + stageName?: string; + entryWorkflowStageDetails?: WorkflowStageDetails; + entryPermissions?: EntryPermissions; + }, ) => { if (flags.updateRestrictDueToRole) return DisableReason.ReadOnly; if (flags.updateRestrictDueToNonLocalizableFields) @@ -67,8 +77,29 @@ const getDisableReason = ( return DisableReason.EntryUpdateRestricted; } if (flags.updateRestrictDueToWorkflowStagePermission) { + const stageName = params?.stageName ? params.stageName : "Unknown"; + const req = params?.entryWorkflowStageDetails?.requestEditAccess; + const entryAllowsUpdate = + params?.entryPermissions == null || + params.entryPermissions.update === true; + if ( + entryAllowsUpdate && + !flags.updateRestrictDueToEntryUpdateRestriction && + req + ) { + if (req.hasPending) { + return DisableReason.WorkflowStageRequestPending({ + stageName, + }); + } + if (req.canRequest) { + return DisableReason.WorkflowStageRequestEdit({ + stageName, + }); + } + } return DisableReason.WorkflowStagePermission({ - stageName: params?.stageName ? params.stageName : "Unknown", + stageName, }); } if(flags.updateRestrictDueToResolvedVariantPermissions) { @@ -148,7 +179,24 @@ export const isFieldDisabled = ( const isDisabled = Object.values(flags).some(Boolean); const reason = getDisableReason(flags, { stageName: entryWorkflowStageDetails?.stage?.name, + entryWorkflowStageDetails, + entryPermissions, }); - return { isDisabled, reason }; + let workflowRequestUi: "request" | "pending" | undefined; + if ( + flags.updateRestrictDueToWorkflowStagePermission && + !flags.updateRestrictDueToEntryUpdateRestriction && + (entryPermissions == null || entryPermissions.update === true) && + entryWorkflowStageDetails?.requestEditAccess + ) { + const req = entryWorkflowStageDetails.requestEditAccess; + if (req.hasPending) { + workflowRequestUi = "pending"; + } else if (req.canRequest) { + workflowRequestUi = "request"; + } + } + + return { isDisabled, reason, workflowRequestUi }; }; diff --git a/src/visualBuilder/utils/types/postMessage.types.ts b/src/visualBuilder/utils/types/postMessage.types.ts index 54bda7d0..e26e7df2 100644 --- a/src/visualBuilder/utils/types/postMessage.types.ts +++ b/src/visualBuilder/utils/types/postMessage.types.ts @@ -32,6 +32,7 @@ export enum VisualBuilderPostMessageEvents { GET_PERMISSIONS = "get-permissions", GET_WORKFLOW_STAGE_DETAILS = "get-workflow-stage-details", GET_RESOLVED_VARIANT_PERMISSIONS = "get-resolved-variant-permissions", + OPEN_REQUEST_EDIT_ACCESS = "open-request-edit-access", // FROM visual builder GET_ALL_ENTRIES_IN_CURRENT_PAGE = "get-entries-in-current-page", From 4881789850fc9f92070859f16853cef8d104f8d1 Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Fri, 27 Mar 2026 15:42:23 +0530 Subject: [PATCH 03/15] feat: add tooltip positioning logic to field label wrapper for improved UX --- .../components/fieldLabelWrapper.tsx | 189 ++++++++++-------- src/visualBuilder/visualBuilder.style.ts | 26 +++ 2 files changed, 137 insertions(+), 78 deletions(-) diff --git a/src/visualBuilder/components/fieldLabelWrapper.tsx b/src/visualBuilder/components/fieldLabelWrapper.tsx index e838fc9e..b14c43da 100644 --- a/src/visualBuilder/components/fieldLabelWrapper.tsx +++ b/src/visualBuilder/components/fieldLabelWrapper.tsx @@ -1,5 +1,5 @@ import classNames from "classnames"; -import React, { useEffect, useState } from "preact/compat"; +import React, { useEffect, useRef, useState } from "preact/compat"; import { extractDetailsFromCslp, isValidCslp } from "../../cslp"; import { CslpData } from "../../cslp/types/cslp.types"; import { VisualBuilderCslpEventDetails } from "../types/visualBuilder.types"; @@ -79,6 +79,107 @@ interface ICurrentField { parentContentTypeName: string; } +/** Space needed above the icon for the default (above) tooltip before flipping below. */ +const TOOLTIP_VIEWPORT_TOP_CLEARANCE_PX = 148; + +interface FieldLabelDisabledIconProps { + reason: string; + workflowRequestUi?: "request" | "pending"; + usePlainDataTooltip: boolean; + onLinkVariant: () => void; + onRequestEditAccess: () => void | Promise; +} + +function FieldLabelDisabledIcon( + props: FieldLabelDisabledIconProps +): JSX.Element { + const { + reason, + workflowRequestUi, + usePlainDataTooltip, + onLinkVariant, + onRequestEditAccess, + } = props; + const wrapRef = useRef(null); + const [showTooltipBelow, setShowTooltipBelow] = useState(false); + + const updateTooltipPlacement = () => { + const el = wrapRef.current; + if (!el) return; + const { top } = el.getBoundingClientRect(); + setShowTooltipBelow(top < TOOLTIP_VIEWPORT_TOP_CLEARANCE_PX); + }; + + const customTooltipClass = classNames( + visualBuilderStyles()["visual-builder__custom-tooltip"], + showTooltipBelow && + visualBuilderStyles()["visual-builder__custom-tooltip--below"] + ); + + return ( +
+ {reason?.includes(DisableReason.CanLinkVariant) ? ( +
+ {(() => { + const [before, after] = reason.split( + DisableReason.UnderlinedAndClickableWord + ); + return ( + <> + {before} + + {DisableReason.UnderlinedAndClickableWord} + + {after} + + ); + })()} +
+ ) : null} + {workflowRequestUi === "request" && reason ? ( +
+ {reason}{" "} + { + e.stopPropagation(); + onRequestEditAccess(); + }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onRequestEditAccess(); + } + }} + > + Request Edit Access + +
+ ) : null} + {workflowRequestUi === "pending" && reason ? ( +
{reason}
+ ) : null} + +
+ ); +} + function FieldLabelWrapperComponent( props: FieldLabelWrapperProps ): JSX.Element { @@ -182,7 +283,6 @@ function FieldLabelWrapperComponent( entryAcl, entryWorkflowStageDetails, ); - const handleRequestEditAccess = async () => { try { await visualBuilderPostMessage?.send( @@ -251,82 +351,15 @@ function FieldLabelWrapperComponent( text: currentFieldDisplayName, contentTypeName: contentTypeName ?? "", icon: fieldDisabled ? ( -
- {reason?.includes(DisableReason.CanLinkVariant) && ( -
- {(() => { - const [before, after] = reason.split( - DisableReason.UnderlinedAndClickableWord - ); - return ( - <> - {before} - {DisableReason.UnderlinedAndClickableWord} - {after} - - ); - })()} -
- )} - {workflowRequestUi === "request" && reason ? ( -
- {reason}{" "} - { - e.stopPropagation(); - void handleRequestEditAccess(); - }} - onKeyDown={(e) => { - if ( - e.key === "Enter" || - e.key === " " - ) { - e.preventDefault(); - void handleRequestEditAccess(); - } - }} - > - Request Edit Access - -
- ) : null} - {workflowRequestUi === "pending" && reason ? ( -
- {reason} -
- ) : null} - -
+ ) : hasParentPaths ? ( ) : ( diff --git a/src/visualBuilder/visualBuilder.style.ts b/src/visualBuilder/visualBuilder.style.ts index bd6c9e11..49a86498 100644 --- a/src/visualBuilder/visualBuilder.style.ts +++ b/src/visualBuilder/visualBuilder.style.ts @@ -587,6 +587,22 @@ export function visualBuilderStyles() { display: none; } `, + /** When the field label is near the top of the viewport, show the tooltip below the icon. */ + "visual-builder__tooltip--persistent--below": css` + &:before { + bottom: -66px; + margin-bottom: 0; + margin-top: 0; + top: auto; + } + + &:after { + bottom: -13px; + margin-top: 0; + top: auto; + transform: rotate(180deg); + } + `, "visual-builder__custom-tooltip": css` position: absolute; bottom: 20px; @@ -613,6 +629,16 @@ export function visualBuilderStyles() { border-color: #767676 transparent transparent transparent; } `, + "visual-builder__custom-tooltip--below": css` + bottom: auto; + top: 100%; + margin-bottom: 0; + margin-top: 8px; + + &:after { + content: none; + } + `, "visual-builder__empty-block": css` width: 100%; height: 100%; From cbc9e0f9fddd5f777db8f8d3744d5a57173db4dc Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Fri, 27 Mar 2026 16:40:22 +0530 Subject: [PATCH 04/15] feat: add wider tooltip for workflow access in visual builder --- src/visualBuilder/components/fieldLabelWrapper.tsx | 13 +++++++++++-- src/visualBuilder/visualBuilder.style.ts | 4 ++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/visualBuilder/components/fieldLabelWrapper.tsx b/src/visualBuilder/components/fieldLabelWrapper.tsx index b14c43da..b92a2834 100644 --- a/src/visualBuilder/components/fieldLabelWrapper.tsx +++ b/src/visualBuilder/components/fieldLabelWrapper.tsx @@ -116,6 +116,15 @@ function FieldLabelDisabledIcon( visualBuilderStyles()["visual-builder__custom-tooltip--below"] ); + const workflowAccessTooltipClass = classNames( + visualBuilderStyles()["visual-builder__custom-tooltip"], + showTooltipBelow && + visualBuilderStyles()["visual-builder__custom-tooltip--below"], + visualBuilderStyles()[ + "visual-builder__custom-tooltip--workflow-access" + ] + ); + return (
) : null} {workflowRequestUi === "request" && reason ? ( -
+
{reason}{" "} ) : null} {workflowRequestUi === "pending" && reason ? ( -
{reason}
+
{reason}
) : null}
diff --git a/src/visualBuilder/visualBuilder.style.ts b/src/visualBuilder/visualBuilder.style.ts index 49a86498..96a5ce7f 100644 --- a/src/visualBuilder/visualBuilder.style.ts +++ b/src/visualBuilder/visualBuilder.style.ts @@ -639,6 +639,10 @@ export function visualBuilderStyles() { content: none; } `, + /** Wider cap for workflow request / pending copy — must follow base `.visual-builder__custom-tooltip` so max-width wins over 200px. */ + "visual-builder__custom-tooltip--workflow-access": css` + max-width: 325px; + `, "visual-builder__empty-block": css` width: 100%; height: 100%; From 01dc87b88c9907420b99864721f4e600500d2f93 Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Fri, 27 Mar 2026 17:10:42 +0530 Subject: [PATCH 05/15] test: add getWorkflowStageDetails and workflow request field label coverage - Unit-test getWorkflowStageDetails postMessage payload and fallbacks - Integration-style tests for request/pending copy, tooltip classes, and OPEN_REQUEST_EDIT_ACCESS - Extend fieldLabelWrapper mock styles for workflow tooltip variants - Fix visualBuilder.style mock path (../../../) so class assertions match mocked labels Made-with: Cursor --- .../fieldLabelWrapper.mocks.ts | 5 + ...fieldLabelWrapper.workflowRequest.test.tsx | 421 ++++++++++++++++++ .../__test__/getWorkflowStageDetails.test.ts | 79 ++++ 3 files changed, 505 insertions(+) create mode 100644 src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.workflowRequest.test.tsx create mode 100644 src/visualBuilder/utils/__test__/getWorkflowStageDetails.test.ts diff --git a/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.mocks.ts b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.mocks.ts index a59699ab..9a0bb1d9 100644 --- a/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.mocks.ts +++ b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.mocks.ts @@ -20,6 +20,11 @@ export const mockStyles = { "visual-builder__tooltip--persistent": "visual-builder__tooltip--persistent", "visual-builder__custom-tooltip": "visual-builder__custom-tooltip", + "visual-builder__custom-tooltip--below": "visual-builder__custom-tooltip--below", + "visual-builder__custom-tooltip--workflow-access": + "visual-builder__custom-tooltip--workflow-access", + "visual-builder__tooltip--persistent--below": + "visual-builder__tooltip--persistent--below", "visual-builder__focused-toolbar__field-label-wrapper": "visual-builder__focused-toolbar__field-label-wrapper", "visual-builder__focused-toolbar--field-disabled": diff --git a/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.workflowRequest.test.tsx b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.workflowRequest.test.tsx new file mode 100644 index 00000000..4bd3a49f --- /dev/null +++ b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.workflowRequest.test.tsx @@ -0,0 +1,421 @@ +import { render, waitFor, act, findByTestId, screen } from "@testing-library/preact"; +import { fireEvent } from "@testing-library/preact"; +import FieldLabelWrapperComponent from "../../fieldLabelWrapper"; +import { VisualBuilderCslpEventDetails } from "../../../types/visualBuilder.types"; +import { singleLineFieldSchema } from "../../../../__test__/data/fields"; +import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; +import visualBuilderPostMessage from "../../../utils/visualBuilderPostMessage"; +import { VisualBuilderPostMessageEvents } from "../../../utils/types/postMessage.types"; +import { fetchEntryPermissionsAndStageDetails } from "../../../utils/fetchEntryPermissionsAndStageDetails"; +import { WORKFLOW_STAGES } from "../../../utils/constants"; +import React from "preact/compat"; +import Config from "../../../../configManager/configManager"; +import { VisualBuilder } from "../../../index"; +import { mockFieldMetadata } from "./fieldLabelWrapper.mocks"; + +const testFieldSchemaCache: Record> = {}; + +vi.mock("../Tooltip", () => ({ + ToolbarTooltip: ({ children, data, disabled }: any) => ( +
+ {children} +
+ ), +})); + +vi.mock("../../../utils/fieldSchemaMap", async (importOriginal) => { + const actual = + await importOriginal(); + return { + FieldSchemaMap: { + ...actual.FieldSchemaMap, + getFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + if (testFieldSchemaCache[contentTypeUid]?.[fieldPath]) { + return Promise.resolve( + testFieldSchemaCache[contentTypeUid][fieldPath] + ); + } + const defaultSchema = { + display_name: "Field 0", + data_type: "text", + field_metadata: { + description: "", + default_value: "", + version: 3, + }, + uid: "test_field", + }; + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + testFieldSchemaCache[contentTypeUid][fieldPath] = + defaultSchema; + return Promise.resolve(defaultSchema); + } + ), + setFieldSchema: vi + .fn() + .mockImplementation( + ( + contentTypeUid: string, + schemaMap: Record + ) => { + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + Object.assign( + testFieldSchemaCache[contentTypeUid], + schemaMap + ); + } + ), + hasFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + return !!testFieldSchemaCache[contentTypeUid]?.[ + fieldPath + ]; + } + ), + clear: vi.fn().mockImplementation(() => { + Object.keys(testFieldSchemaCache).forEach( + (key) => delete testFieldSchemaCache[key] + ); + }), + }, + }; +}); + +vi.mock("../../../utils/visualBuilderPostMessage", () => ({ + default: { + send: vi.fn().mockImplementation((eventName: string, fields: any) => { + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + }), + }, +})); + +vi.mock("../../../cslp", () => ({ + extractDetailsFromCslp: vi.fn().mockImplementation((path) => { + return { + content_type_uid: "mockContentTypeUid", + fieldPath: path, + cslpValue: path, + }; + }), +})); + +vi.mock("../../../utils/fetchEntryPermissionsAndStageDetails", () => ({ + fetchEntryPermissionsAndStageDetails: vi.fn(), +})); + +vi.mock("../generators/generateCustomCursor", () => ({ + getFieldIcon: vi.fn().mockReturnValue("mock-icon"), + FieldTypeIconsMap: { + reference: "reference-icon", + }, +})); + +vi.mock("../../../visualBuilder.style", async () => { + const { mockStyles } = await import("./fieldLabelWrapper.mocks"); + return { + visualBuilderStyles: vi.fn(() => mockStyles), + }; +}); + +vi.mock("../../VariantIndicator", () => ({ + VariantIndicator: () =>
Variant
, +})); + +vi.mock("../../../utils/errorHandling", () => ({ + hasPostMessageError: vi.fn().mockReturnValue(false), +})); + +const workflowRequestEditAccessResponse = { + acl: { + update: true, + create: true, + read: true, + delete: true, + publish: true, + }, + workflowStage: { + stage: { name: WORKFLOW_STAGES.REVIEW }, + permissions: { + entry: { + update: false, + }, + }, + requestEditAccess: { canRequest: true, hasPending: false }, + }, + resolvedVariantPermissions: { + update: true, + }, +}; + +describe("FieldLabelWrapperComponent — workflow request edit access", () => { + const mockEventDetails: VisualBuilderCslpEventDetails = { + editableElement: document.createElement("div"), + cslpData: "", + fieldMetadata: mockFieldMetadata, + }; + + const mockGetParentEditable = () => document.createElement("div"); + + beforeEach(() => { + vi.clearAllMocks(); + + Config.get = () => + ({ + stackDetails: { masterLocale: "en-us" }, + editButton: { position: "bottom-right" }, + }) as ReturnType; + + VisualBuilder.VisualBuilderGlobalState = { + value: { + locale: "en-us", + variant: undefined, + audienceMode: false, + }, + } as typeof VisualBuilder.VisualBuilderGlobalState; + + vi.mocked(fetchEntryPermissionsAndStageDetails).mockResolvedValue( + workflowRequestEditAccessResponse + ); + + vi.mocked(visualBuilderPostMessage!.send).mockImplementation( + (eventName: string, fields: any) => { + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = + field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === + VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + } + ); + + FieldSchemaMap.setFieldSchema(mockFieldMetadata.content_type_uid, { + [mockFieldMetadata.fieldPath]: singleLineFieldSchema, + }); + }); + + afterEach(() => { + FieldSchemaMap.clear(); + document.body.innerHTML = ""; + }); + + it("renders Request Edit Access and workflow tooltip class when canRequest", async () => { + const { container } = render( + + ); + + await act(async () => { + await new Promise((resolve) => + queueMicrotask(() => resolve()) + ); + }); + + await findByTestId( + container as HTMLElement, + "visual-builder__focused-toolbar__field-label-wrapper", + {}, + { timeout: 10000 } + ); + + await waitFor( + () => { + expect(screen.getByText("Request Edit Access")).toBeTruthy(); + }, + { timeout: 10000 } + ); + + expect( + container.querySelector( + ".visual-builder__custom-tooltip--workflow-access" + ) + ).toBeTruthy(); + }); + + it("renders pending copy when hasPending", async () => { + vi.mocked(fetchEntryPermissionsAndStageDetails).mockResolvedValue({ + ...workflowRequestEditAccessResponse, + workflowStage: { + stage: { name: WORKFLOW_STAGES.REVIEW }, + permissions: { entry: { update: false } }, + requestEditAccess: { + canRequest: false, + hasPending: true, + }, + }, + }); + + const { container } = render( + + ); + + await act(async () => { + await new Promise((resolve) => + queueMicrotask(() => resolve()) + ); + }); + + await findByTestId( + container as HTMLElement, + "visual-builder__focused-toolbar__field-label-wrapper", + {}, + { timeout: 10000 } + ); + + await waitFor( + () => { + expect( + screen.getByText(/awaiting approval/i) + ).toBeTruthy(); + }, + { timeout: 10000 } + ); + + expect( + container.querySelector( + ".visual-builder__custom-tooltip--workflow-access" + ) + ).toBeTruthy(); + }); + + it("sends OPEN_REQUEST_EDIT_ACCESS when Request Edit Access is clicked", async () => { + render( + + ); + + await waitFor( + () => { + expect(screen.getByText("Request Edit Access")).toBeTruthy(); + }, + { timeout: 10000 } + ); + + fireEvent.click(screen.getByText("Request Edit Access")); + + expect(visualBuilderPostMessage!.send).toHaveBeenCalledWith( + VisualBuilderPostMessageEvents.OPEN_REQUEST_EDIT_ACCESS, + { + entryUid: mockFieldMetadata.entry_uid, + contentTypeUid: mockFieldMetadata.content_type_uid, + locale: mockFieldMetadata.locale, + variantUid: mockFieldMetadata.variant, + } + ); + }); +}); diff --git a/src/visualBuilder/utils/__test__/getWorkflowStageDetails.test.ts b/src/visualBuilder/utils/__test__/getWorkflowStageDetails.test.ts new file mode 100644 index 00000000..b14dbc29 --- /dev/null +++ b/src/visualBuilder/utils/__test__/getWorkflowStageDetails.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { getWorkflowStageDetails } from "../getWorkflowStageDetails"; +import { VisualBuilderPostMessageEvents } from "../types/postMessage.types"; + +vi.mock("../visualBuilderPostMessage", () => ({ + default: { + send: vi.fn(), + }, +})); + +import visualBuilderPostMessage from "../visualBuilderPostMessage"; + +describe("getWorkflowStageDetails", () => { + beforeEach(() => { + vi.mocked(visualBuilderPostMessage!.send).mockReset(); + }); + + it("returns payload from postMessage when present", async () => { + const payload = { + stage: { name: "Draft" }, + permissions: { entry: { update: true } }, + requestEditAccess: { canRequest: true, hasPending: false }, + }; + vi.mocked(visualBuilderPostMessage!.send).mockResolvedValue(payload); + + const result = await getWorkflowStageDetails({ + entryUid: "e1", + contentTypeUid: "ct1", + locale: "en-us", + variantUid: "v1", + }); + + expect(visualBuilderPostMessage!.send).toHaveBeenCalledWith( + VisualBuilderPostMessageEvents.GET_WORKFLOW_STAGE_DETAILS, + { + entryUid: "e1", + contentTypeUid: "ct1", + locale: "en-us", + variantUid: "v1", + } + ); + expect(result).toEqual(payload); + }); + + it("returns permissive fallback when send returns undefined", async () => { + vi.mocked(visualBuilderPostMessage!.send).mockResolvedValue(undefined); + + const result = await getWorkflowStageDetails({ + entryUid: "e1", + contentTypeUid: "ct1", + locale: "en-us", + }); + + expect(result).toEqual({ + stage: { name: "Unknown" }, + permissions: { entry: { update: true } }, + requestEditAccess: { canRequest: false, hasPending: false }, + }); + }); + + it("returns permissive fallback when send throws", async () => { + vi.mocked(visualBuilderPostMessage!.send).mockRejectedValue( + new Error("network") + ); + + const result = await getWorkflowStageDetails({ + entryUid: "e1", + contentTypeUid: "ct1", + locale: "en-us", + }); + + expect(result.stage?.name).toBe("Unknown"); + expect(result.permissions.entry.update).toBe(true); + expect(result.requestEditAccess).toEqual({ + canRequest: false, + hasPending: false, + }); + }); +}); From f414e8194e7e4f895a15da0725bfd9c0fefb7a27 Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Fri, 27 Mar 2026 18:10:08 +0530 Subject: [PATCH 06/15] refactor: improve getDisableReason parameter handling in isFieldDisabled function --- src/visualBuilder/utils/isFieldDisabled.ts | 27 +++++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/visualBuilder/utils/isFieldDisabled.ts b/src/visualBuilder/utils/isFieldDisabled.ts index c2683ecd..dbdf2c52 100644 --- a/src/visualBuilder/utils/isFieldDisabled.ts +++ b/src/visualBuilder/utils/isFieldDisabled.ts @@ -177,11 +177,24 @@ export const isFieldDisabled = ( } const isDisabled = Object.values(flags).some(Boolean); - const reason = getDisableReason(flags, { - stageName: entryWorkflowStageDetails?.stage?.name, - entryWorkflowStageDetails, - entryPermissions, - }); + + const getDisableReasonParams: { + stageName?: string; + entryWorkflowStageDetails?: WorkflowStageDetails; + entryPermissions?: EntryPermissions; + } = {}; + if (entryWorkflowStageDetails?.stage?.name !== undefined) { + getDisableReasonParams.stageName = entryWorkflowStageDetails.stage.name; + } + if (entryWorkflowStageDetails !== undefined) { + getDisableReasonParams.entryWorkflowStageDetails = + entryWorkflowStageDetails; + } + if (entryPermissions !== undefined) { + getDisableReasonParams.entryPermissions = entryPermissions; + } + + const reason = getDisableReason(flags, getDisableReasonParams); let workflowRequestUi: "request" | "pending" | undefined; if ( @@ -198,5 +211,7 @@ export const isFieldDisabled = ( } } - return { isDisabled, reason, workflowRequestUi }; + return workflowRequestUi !== undefined + ? { isDisabled, reason, workflowRequestUi } + : { isDisabled, reason }; }; From f5ec4fd90c460477e7e416151226aa3f4863a9b1 Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Mon, 30 Mar 2026 18:05:53 +0530 Subject: [PATCH 07/15] docs(readme): expand package README for preview, Timeline, Visual Editor, Studio (VB-665) - Add npm and license badges, async init quick start, config index links - Document Live Preview, Timeline, Visual Builder, and Composable Studio contexts - Link canonical Contentstack docs, Academy (including Understanding Timeline), and Studio Made-with: Cursor --- README.md | 125 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 94 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index c826f875..cbdaa212 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,123 @@ -# Contentstack Live Preview Utils SDK +# @contentstack/live-preview-utils -Contentstack is a headless CMS with an API-first approach. It is a CMS that developers can use to build powerful cross-platform applications in their favorite languages. Build your application frontend, and Contentstack will take care of the rest. +[![npm](https://img.shields.io/npm/v/@contentstack/live-preview-utils.svg)](https://www.npmjs.com/package/@contentstack/live-preview-utils) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -Contentstack provides the Live Preview Utils SDK to establish a communication channel between the various Contentstack SDKs and your website, transmitting live changes to the preview pane. [Read More](https://www.contentstack.com/docs/content-managers/live-preview/). +The **Live Preview Utils** package runs in your website and opens a communication channel between the page (often inside a Contentstack preview iframe) and the platform. It powers live content updates, edit controls, and Visual Builder UI in the preview surface via messaging—not a replacement for the Contentstack delivery SDKs, but the client bridge for preview and builder experiences. -# Installation +See how [Live Preview](https://www.contentstack.com/docs/content-managers/author-content/about-live-preview), [Timeline](https://www.contentstack.com/docs/content-managers/timeline/preview-content-across-a-timeline), and [Visual Editor](https://www.contentstack.com/docs/content-managers/visual-editor/about-visual-editor) work in Contentstack for editors and content managers. -To install the package via npm, use the following command: +## Where this SDK runs + +- **Live Preview** — Preview entries in the stack while your site loads inside the preview panel. +- **Timeline** — Time-based preview (how the site looks on future dates). Your integration pattern is the same as Live Preview from the app’s perspective; the parent context is still a preview iframe. In code, `config.windowType` can reflect a preview context that includes Timeline ([see `config` / `windowType`](docs/live-preview-configs.md#config)). +- **Visual Builder** — WYSIWYG editing with the site in an iframe. Use `mode: "builder"` so “Start Editing” targets Visual Builder; the SDK still works when the same site is opened in Live Preview ([`mode`](docs/live-preview-configs.md#mode)). +- **Composable Studio** — [Contentstack Studio](https://www.contentstack.com/docs/studio) is the visual composition experience for building experiences from components and content. Use this package when your front end needs the same live preview and stack-driven editing hooks as in Live Preview; follow Studio and Live Preview setup docs together for your stack. + +**Integrators:** In hosted preview, the platform loads your site in an iframe; this SDK talks to the parent window. When you open the site outside Contentstack (`windowType: independent`), behavior follows the [config](docs/live-preview-configs.md#config) rules in the reference doc. + +## Requirements + +- **Browser:** Initialize only on the client (`window` must exist). Server-only bundles should not call `init` during SSR. +- **SSR vs CSR:** Defaults assume SSR-friendly behavior. For **client-side rendering**, pass [`stackSdk`](docs/live-preview-configs.md#stacksdk) and set [`ssr: false`](docs/live-preview-configs.md#ssr) as described in the config reference. + +## Installation ```bash npm install @contentstack/live-preview-utils ``` -Alternatively, if you want to include the package directly in your website HTML code, use the following command: +### Load from a CDN (advanced) + +Pin the version to match your app (update `4.3.0` when you upgrade): ```html - ``` -> [!NOTE] -> This step involves incorporating the package into your HTML code and initializing it, eliminating the need for re-initialization in the subsequent step. - -# Initializing the SDK +> [!TIP] +> If you bootstrap with the snippet above, skip a second `init` from your app bundle on the same page. -### Live Preview Utils +## Quick start -Since the Live Preview Utils SDK is responsible for communication, you need to only initialize it. -Use the following command to initialize the SDK: +[`init`](docs/live-preview-configs.md#initconfig-iconfig) returns a `Promise` that resolves to `{ livePreview, visualBuilder }`. Calling `init` again returns the existing instance and logs a warning. ```javascript import ContentstackLivePreview from "@contentstack/live-preview-utils"; -ContentstackLivePreview.init({ - stackDetails: { - apiKey: "your-stack-api-key", - }, +const { livePreview, visualBuilder } = await ContentstackLivePreview.init({ + stackDetails: { + apiKey: "your-stack-api-key", + }, }); ``` +## Configuration + +Full tables and examples: **[docs/live-preview-configs.md](docs/live-preview-configs.md)**. -# License +**`init` options** -MIT License +- [`enable`](docs/live-preview-configs.md#enable) +- [`ssr`](docs/live-preview-configs.md#ssr) +- [`mode`](docs/live-preview-configs.md#mode) (`preview` vs `builder`) +- [`editButton`](docs/live-preview-configs.md#editbutton) +- [`editInVisualBuilderButton`](docs/live-preview-configs.md#editinvisualbuilderbutton) (Start Editing outside Visual Builder) +- [`cleanCslpOnProduction`](docs/live-preview-configs.md#cleancslponproduction) +- [`stackDetails`](docs/live-preview-configs.md#stackdetails) ([`apiKey`](docs/live-preview-configs.md#apikey), [`environment`](docs/live-preview-configs.md#environment)) +- [`clientUrlParams`](docs/live-preview-configs.md#clienturlparams) — [NA](docs/live-preview-configs.md#na-config) / [EU](docs/live-preview-configs.md#eu-config) +- [`stackSdk`](docs/live-preview-configs.md#stacksdk) -Copyright © 2021-2026 [Contentstack](https://www.contentstack.com/). All Rights Reserved +**Methods and properties** -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +- [`onLiveEdit`](docs/live-preview-configs.md#onliveeditcallback---void) +- [`onEntryChange`](docs/live-preview-configs.md#onentrychangecallback---void) +- [`hash`](docs/live-preview-configs.md#hash) +- [`config`](docs/live-preview-configs.md#config) (includes `windowType`: Live Preview / Timeline preview, Visual Builder, or independent) + +The [configs table of contents](docs/live-preview-configs.md#contentstack-live-preview-utils-sdk-configs) also lists `setConfigFromParams` and `getGatsbyDataFormat` for deeper workflows. + +## Documentation and learning + +**Developers** + +- [Set up Live Preview for your website](https://www.contentstack.com/docs/developers/set-up-live-preview/set-up-live-preview-for-your-website) +- [How Live Preview works](https://www.contentstack.com/docs/developers/set-up-live-preview/how-live-preview-works) +- [Preview API](https://www.contentstack.com/docs/developers/set-up-timeline/preview-api) +- [Set up Timeline for your website](https://www.contentstack.com/docs/developers/set-up-timeline/set-up-timeline-for-your-website) + +**Content managers** + +- [About Live Preview](https://www.contentstack.com/docs/content-managers/author-content/about-live-preview) +- [Preview content across a Timeline](https://www.contentstack.com/docs/content-managers/timeline/preview-content-across-a-timeline) +- [About Visual Editor](https://www.contentstack.com/docs/content-managers/visual-editor/about-visual-editor) + +**Studio** + +- [Studio](https://www.contentstack.com/docs/studio) +- [Get started with Studio](https://www.contentstack.com/docs/studio/get-started-with-studio) + +**Academy** + +- [Implementing Live Preview (course)](https://www.contentstack.com/academy/courses/implementing-live-preview) +- [Contentstack Live Preview under the hood](https://www.contentstack.com/academy/content/contentstack-live-preview-under-the-hood) +- [Understanding Timeline](https://www.contentstack.com/academy/content/understanding-timeline) +- [Understanding Visual Builder](https://www.contentstack.com/academy/content/understanding-visual-builder) + +## Advanced: stripping the SDK at build time + +> [!NOTE] +> Set `PURGE_PREVIEW_SDK` or `REACT_APP_PURGE_PREVIEW_SDK` to `"true"` at build time to swap the default export for a light stub so preview code is not bundled for production. -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +## Resources -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +- **Source:** [github.com/contentstack/live-preview-sdk](https://github.com/contentstack/live-preview-sdk) +- **Typed API (local):** `npm run docs` From de2e43e028e264f460b60b9d8b710f4524e138fa Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Mon, 30 Mar 2026 18:30:32 +0530 Subject: [PATCH 08/15] docs(readme): update terminology from Visual Builder to Visual Editor --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index cbdaa212..4b3c5227 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![npm](https://img.shields.io/npm/v/@contentstack/live-preview-utils.svg)](https://www.npmjs.com/package/@contentstack/live-preview-utils) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -The **Live Preview Utils** package runs in your website and opens a communication channel between the page (often inside a Contentstack preview iframe) and the platform. It powers live content updates, edit controls, and Visual Builder UI in the preview surface via messaging—not a replacement for the Contentstack delivery SDKs, but the client bridge for preview and builder experiences. +The **Live Preview Utils** package runs in your website and opens a communication channel between the page (often inside a Contentstack preview iframe) and the platform. It powers live content updates, edit controls, and Visual Editor UI in the preview surface via messaging—not a replacement for the Contentstack delivery SDKs, but the client bridge for preview and Visual Editor experiences. See how [Live Preview](https://www.contentstack.com/docs/content-managers/author-content/about-live-preview), [Timeline](https://www.contentstack.com/docs/content-managers/timeline/preview-content-across-a-timeline), and [Visual Editor](https://www.contentstack.com/docs/content-managers/visual-editor/about-visual-editor) work in Contentstack for editors and content managers. @@ -11,7 +11,7 @@ See how [Live Preview](https://www.contentstack.com/docs/content-managers/author - **Live Preview** — Preview entries in the stack while your site loads inside the preview panel. - **Timeline** — Time-based preview (how the site looks on future dates). Your integration pattern is the same as Live Preview from the app’s perspective; the parent context is still a preview iframe. In code, `config.windowType` can reflect a preview context that includes Timeline ([see `config` / `windowType`](docs/live-preview-configs.md#config)). -- **Visual Builder** — WYSIWYG editing with the site in an iframe. Use `mode: "builder"` so “Start Editing” targets Visual Builder; the SDK still works when the same site is opened in Live Preview ([`mode`](docs/live-preview-configs.md#mode)). +- **Visual Editor** — WYSIWYG editing with the site in an iframe. Use `mode: "builder"` so “Start Editing” targets Visual Editor; the SDK still works when the same site is opened in Live Preview ([`mode`](docs/live-preview-configs.md#mode)). - **Composable Studio** — [Contentstack Studio](https://www.contentstack.com/docs/studio) is the visual composition experience for building experiences from components and content. Use this package when your front end needs the same live preview and stack-driven editing hooks as in Live Preview; follow Studio and Live Preview setup docs together for your stack. **Integrators:** In hosted preview, the platform loads your site in an iframe; this SDK talks to the parent window. When you open the site outside Contentstack (`windowType: independent`), behavior follows the [config](docs/live-preview-configs.md#config) rules in the reference doc. @@ -70,7 +70,7 @@ Full tables and examples: **[docs/live-preview-configs.md](docs/live-preview-con - [`ssr`](docs/live-preview-configs.md#ssr) - [`mode`](docs/live-preview-configs.md#mode) (`preview` vs `builder`) - [`editButton`](docs/live-preview-configs.md#editbutton) -- [`editInVisualBuilderButton`](docs/live-preview-configs.md#editinvisualbuilderbutton) (Start Editing outside Visual Builder) +- [`editInVisualBuilderButton`](docs/live-preview-configs.md#editinvisualbuilderbutton) (Start Editing outside Visual Editor) - [`cleanCslpOnProduction`](docs/live-preview-configs.md#cleancslponproduction) - [`stackDetails`](docs/live-preview-configs.md#stackdetails) ([`apiKey`](docs/live-preview-configs.md#apikey), [`environment`](docs/live-preview-configs.md#environment)) - [`clientUrlParams`](docs/live-preview-configs.md#clienturlparams) — [NA](docs/live-preview-configs.md#na-config) / [EU](docs/live-preview-configs.md#eu-config) @@ -81,7 +81,7 @@ Full tables and examples: **[docs/live-preview-configs.md](docs/live-preview-con - [`onLiveEdit`](docs/live-preview-configs.md#onliveeditcallback---void) - [`onEntryChange`](docs/live-preview-configs.md#onentrychangecallback---void) - [`hash`](docs/live-preview-configs.md#hash) -- [`config`](docs/live-preview-configs.md#config) (includes `windowType`: Live Preview / Timeline preview, Visual Builder, or independent) +- [`config`](docs/live-preview-configs.md#config) (includes `windowType`: Live Preview / Timeline preview, Visual Editor, or independent) The [configs table of contents](docs/live-preview-configs.md#contentstack-live-preview-utils-sdk-configs) also lists `setConfigFromParams` and `getGatsbyDataFormat` for deeper workflows. @@ -110,7 +110,7 @@ The [configs table of contents](docs/live-preview-configs.md#contentstack-live-p - [Implementing Live Preview (course)](https://www.contentstack.com/academy/courses/implementing-live-preview) - [Contentstack Live Preview under the hood](https://www.contentstack.com/academy/content/contentstack-live-preview-under-the-hood) - [Understanding Timeline](https://www.contentstack.com/academy/content/understanding-timeline) -- [Understanding Visual Builder](https://www.contentstack.com/academy/content/understanding-visual-builder) +- [Understanding Visual Editor](https://www.contentstack.com/academy/content/understanding-visual-builder) ## Advanced: stripping the SDK at build time From 6e1572f79335097825997d3e3473f8dcd3b62fa7 Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Mon, 30 Mar 2026 18:41:17 +0530 Subject: [PATCH 09/15] docs(mustache): sync README template with published README - Expand main.mustache to match README (badges, contexts, docs links, Academy) - Use {{packageVersion}} in CDN snippet; pin wording matches README - Remove unused currentYear from mustache.ts DATA Made-with: Cursor --- main.mustache | 125 +++++++++++++++++++++++++++++++++++++------------- mustache.ts | 1 - 2 files changed, 94 insertions(+), 32 deletions(-) diff --git a/main.mustache b/main.mustache index 6fc64f64..07d59533 100644 --- a/main.mustache +++ b/main.mustache @@ -1,60 +1,123 @@ -# Contentstack Live Preview Utils SDK +# @contentstack/live-preview-utils -Contentstack is a headless CMS with an API-first approach. It is a CMS that developers can use to build powerful cross-platform applications in their favorite languages. Build your application frontend, and Contentstack will take care of the rest. +[![npm](https://img.shields.io/npm/v/@contentstack/live-preview-utils.svg)](https://www.npmjs.com/package/@contentstack/live-preview-utils) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -Contentstack provides the Live Preview Utils SDK to establish a communication channel between the various Contentstack SDKs and your website, transmitting live changes to the preview pane. [Read More](https://www.contentstack.com/docs/content-managers/live-preview/). +The **Live Preview Utils** package runs in your website and opens a communication channel between the page (often inside a Contentstack preview iframe) and the platform. It powers live content updates, edit controls, and Visual Editor UI in the preview surface via messaging—not a replacement for the Contentstack delivery SDKs, but the client bridge for preview and Visual Editor experiences. -# Installation +See how [Live Preview](https://www.contentstack.com/docs/content-managers/author-content/about-live-preview), [Timeline](https://www.contentstack.com/docs/content-managers/timeline/preview-content-across-a-timeline), and [Visual Editor](https://www.contentstack.com/docs/content-managers/visual-editor/about-visual-editor) work in Contentstack for editors and content managers. -To install the package via npm, use the following command: +## Where this SDK runs + +- **Live Preview** — Preview entries in the stack while your site loads inside the preview panel. +- **Timeline** — Time-based preview (how the site looks on future dates). Your integration pattern is the same as Live Preview from the app’s perspective; the parent context is still a preview iframe. In code, `config.windowType` can reflect a preview context that includes Timeline ([see `config` / `windowType`](docs/live-preview-configs.md#config)). +- **Visual Editor** — WYSIWYG editing with the site in an iframe. Use `mode: "builder"` so “Start Editing” targets Visual Editor; the SDK still works when the same site is opened in Live Preview ([`mode`](docs/live-preview-configs.md#mode)). +- **Composable Studio** — [Contentstack Studio](https://www.contentstack.com/docs/studio) is the visual composition experience for building experiences from components and content. Use this package when your front end needs the same live preview and stack-driven editing hooks as in Live Preview; follow Studio and Live Preview setup docs together for your stack. + +**Integrators:** In hosted preview, the platform loads your site in an iframe; this SDK talks to the parent window. When you open the site outside Contentstack (`windowType: independent`), behavior follows the [config](docs/live-preview-configs.md#config) rules in the reference doc. + +## Requirements + +- **Browser:** Initialize only on the client (`window` must exist). Server-only bundles should not call `init` during SSR. +- **SSR vs CSR:** Defaults assume SSR-friendly behavior. For **client-side rendering**, pass [`stackSdk`](docs/live-preview-configs.md#stacksdk) and set [`ssr: false`](docs/live-preview-configs.md#ssr) as described in the config reference. + +## Installation ```bash npm install @contentstack/live-preview-utils ``` -Alternatively, if you want to include the package directly in your website HTML code, use the following command: +### Load from a CDN (advanced) + +Pin the version to match your app (update `{{packageVersion}}` when you upgrade): ```html - ``` -> [!NOTE] -> This step involves incorporating the package into your HTML code and initializing it, eliminating the need for re-initialization in the subsequent step. - -# Initializing the SDK +> [!TIP] +> If you bootstrap with the snippet above, skip a second `init` from your app bundle on the same page. -### Live Preview Utils +## Quick start -Since the Live Preview Utils SDK is responsible for communication, you need to only initialize it. -Use the following command to initialize the SDK: +[`init`](docs/live-preview-configs.md#initconfig-iconfig) returns a `Promise` that resolves to `{ livePreview, visualBuilder }`. Calling `init` again returns the existing instance and logs a warning. ```javascript import ContentstackLivePreview from "@contentstack/live-preview-utils"; -ContentstackLivePreview.init({ - stackDetails: { - apiKey: "your-stack-api-key", - }, +const { livePreview, visualBuilder } = await ContentstackLivePreview.init({ + stackDetails: { + apiKey: "your-stack-api-key", + }, }); ``` +## Configuration + +Full tables and examples: **[docs/live-preview-configs.md](docs/live-preview-configs.md)**. -# License +**`init` options** -MIT License +- [`enable`](docs/live-preview-configs.md#enable) +- [`ssr`](docs/live-preview-configs.md#ssr) +- [`mode`](docs/live-preview-configs.md#mode) (`preview` vs `builder`) +- [`editButton`](docs/live-preview-configs.md#editbutton) +- [`editInVisualBuilderButton`](docs/live-preview-configs.md#editinvisualbuilderbutton) (Start Editing outside Visual Editor) +- [`cleanCslpOnProduction`](docs/live-preview-configs.md#cleancslponproduction) +- [`stackDetails`](docs/live-preview-configs.md#stackdetails) ([`apiKey`](docs/live-preview-configs.md#apikey), [`environment`](docs/live-preview-configs.md#environment)) +- [`clientUrlParams`](docs/live-preview-configs.md#clienturlparams) — [NA](docs/live-preview-configs.md#na-config) / [EU](docs/live-preview-configs.md#eu-config) +- [`stackSdk`](docs/live-preview-configs.md#stacksdk) -Copyright © 2021-{{currentYear}} [Contentstack](https://www.contentstack.com/). All Rights Reserved +**Methods and properties** -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +- [`onLiveEdit`](docs/live-preview-configs.md#onliveeditcallback---void) +- [`onEntryChange`](docs/live-preview-configs.md#onentrychangecallback---void) +- [`hash`](docs/live-preview-configs.md#hash) +- [`config`](docs/live-preview-configs.md#config) (includes `windowType`: Live Preview / Timeline preview, Visual Editor, or independent) + +The [configs table of contents](docs/live-preview-configs.md#contentstack-live-preview-utils-sdk-configs) also lists `setConfigFromParams` and `getGatsbyDataFormat` for deeper workflows. + +## Documentation and learning + +**Developers** + +- [Set up Live Preview for your website](https://www.contentstack.com/docs/developers/set-up-live-preview/set-up-live-preview-for-your-website) +- [How Live Preview works](https://www.contentstack.com/docs/developers/set-up-live-preview/how-live-preview-works) +- [Preview API](https://www.contentstack.com/docs/developers/set-up-timeline/preview-api) +- [Set up Timeline for your website](https://www.contentstack.com/docs/developers/set-up-timeline/set-up-timeline-for-your-website) + +**Content managers** + +- [About Live Preview](https://www.contentstack.com/docs/content-managers/author-content/about-live-preview) +- [Preview content across a Timeline](https://www.contentstack.com/docs/content-managers/timeline/preview-content-across-a-timeline) +- [About Visual Editor](https://www.contentstack.com/docs/content-managers/visual-editor/about-visual-editor) + +**Studio** + +- [Studio](https://www.contentstack.com/docs/studio) +- [Get started with Studio](https://www.contentstack.com/docs/studio/get-started-with-studio) + +**Academy** + +- [Implementing Live Preview (course)](https://www.contentstack.com/academy/courses/implementing-live-preview) +- [Contentstack Live Preview under the hood](https://www.contentstack.com/academy/content/contentstack-live-preview-under-the-hood) +- [Understanding Timeline](https://www.contentstack.com/academy/content/understanding-timeline) +- [Understanding Visual Editor](https://www.contentstack.com/academy/content/understanding-visual-builder) + +## Advanced: stripping the SDK at build time + +> [!NOTE] +> Set `PURGE_PREVIEW_SDK` or `REACT_APP_PURGE_PREVIEW_SDK` to `"true"` at build time to swap the default export for a light stub so preview code is not bundled for production. -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +## Resources -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +- **Source:** [github.com/contentstack/live-preview-sdk](https://github.com/contentstack/live-preview-sdk) +- **Typed API (local):** `npm run docs` diff --git a/mustache.ts b/mustache.ts index 2c34b574..9229f2bc 100644 --- a/mustache.ts +++ b/mustache.ts @@ -11,7 +11,6 @@ const MUSTACHE_MAIN_DIR = './main.mustache'; */ const DATA = { packageVersion: packageJson.version, - currentYear: new Date().getFullYear(), }; function generateReadMe() { From 82f879100c5f83ff8a01fded7a1a7d0412e51ba9 Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Mon, 30 Mar 2026 18:58:33 +0530 Subject: [PATCH 10/15] docs(readme): rename Composable Studio bullet to Studio Made-with: Cursor --- README.md | 2 +- main.mustache | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b3c5227..1fd90bd1 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ See how [Live Preview](https://www.contentstack.com/docs/content-managers/author - **Live Preview** — Preview entries in the stack while your site loads inside the preview panel. - **Timeline** — Time-based preview (how the site looks on future dates). Your integration pattern is the same as Live Preview from the app’s perspective; the parent context is still a preview iframe. In code, `config.windowType` can reflect a preview context that includes Timeline ([see `config` / `windowType`](docs/live-preview-configs.md#config)). - **Visual Editor** — WYSIWYG editing with the site in an iframe. Use `mode: "builder"` so “Start Editing” targets Visual Editor; the SDK still works when the same site is opened in Live Preview ([`mode`](docs/live-preview-configs.md#mode)). -- **Composable Studio** — [Contentstack Studio](https://www.contentstack.com/docs/studio) is the visual composition experience for building experiences from components and content. Use this package when your front end needs the same live preview and stack-driven editing hooks as in Live Preview; follow Studio and Live Preview setup docs together for your stack. +- **Studio** — [Contentstack Studio](https://www.contentstack.com/docs/studio) is the visual composition experience for building experiences from components and content. Use this package when your front end needs the same live preview and stack-driven editing hooks as in Live Preview; follow Studio and Live Preview setup docs together for your stack. **Integrators:** In hosted preview, the platform loads your site in an iframe; this SDK talks to the parent window. When you open the site outside Contentstack (`windowType: independent`), behavior follows the [config](docs/live-preview-configs.md#config) rules in the reference doc. diff --git a/main.mustache b/main.mustache index 07d59533..8f1a30c4 100644 --- a/main.mustache +++ b/main.mustache @@ -12,7 +12,7 @@ See how [Live Preview](https://www.contentstack.com/docs/content-managers/author - **Live Preview** — Preview entries in the stack while your site loads inside the preview panel. - **Timeline** — Time-based preview (how the site looks on future dates). Your integration pattern is the same as Live Preview from the app’s perspective; the parent context is still a preview iframe. In code, `config.windowType` can reflect a preview context that includes Timeline ([see `config` / `windowType`](docs/live-preview-configs.md#config)). - **Visual Editor** — WYSIWYG editing with the site in an iframe. Use `mode: "builder"` so “Start Editing” targets Visual Editor; the SDK still works when the same site is opened in Live Preview ([`mode`](docs/live-preview-configs.md#mode)). -- **Composable Studio** — [Contentstack Studio](https://www.contentstack.com/docs/studio) is the visual composition experience for building experiences from components and content. Use this package when your front end needs the same live preview and stack-driven editing hooks as in Live Preview; follow Studio and Live Preview setup docs together for your stack. +- **Studio** — [Contentstack Studio](https://www.contentstack.com/docs/studio) is the visual composition experience for building experiences from components and content. Use this package when your front end needs the same live preview and stack-driven editing hooks as in Live Preview; follow Studio and Live Preview setup docs together for your stack. **Integrators:** In hosted preview, the platform loads your site in an iframe; this SDK talks to the parent window. When you open the site outside Contentstack (`windowType: independent`), behavior follows the [config](docs/live-preview-configs.md#config) rules in the reference doc. From 52baa79dbd9345d4cc31cefce2714395ed111eb3 Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Tue, 31 Mar 2026 10:41:00 +0530 Subject: [PATCH 11/15] docs: update Timeline and Studio descriptions in README and main.mustache --- README.md | 6 ++---- main.mustache | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1fd90bd1..73905e11 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,9 @@ See how [Live Preview](https://www.contentstack.com/docs/content-managers/author ## Where this SDK runs - **Live Preview** — Preview entries in the stack while your site loads inside the preview panel. -- **Timeline** — Time-based preview (how the site looks on future dates). Your integration pattern is the same as Live Preview from the app’s perspective; the parent context is still a preview iframe. In code, `config.windowType` can reflect a preview context that includes Timeline ([see `config` / `windowType`](docs/live-preview-configs.md#config)). +- **Timeline** — Time-based preview (how the site looks on future dates and scheduled updates). Use the same Live Preview setup on your site; see the Timeline docs below for stack-side behavior. - **Visual Editor** — WYSIWYG editing with the site in an iframe. Use `mode: "builder"` so “Start Editing” targets Visual Editor; the SDK still works when the same site is opened in Live Preview ([`mode`](docs/live-preview-configs.md#mode)). -- **Studio** — [Contentstack Studio](https://www.contentstack.com/docs/studio) is the visual composition experience for building experiences from components and content. Use this package when your front end needs the same live preview and stack-driven editing hooks as in Live Preview; follow Studio and Live Preview setup docs together for your stack. - -**Integrators:** In hosted preview, the platform loads your site in an iframe; this SDK talks to the parent window. When you open the site outside Contentstack (`windowType: independent`), behavior follows the [config](docs/live-preview-configs.md#config) rules in the reference doc. +- **Studio** — [Contentstack Studio](https://www.contentstack.com/docs/studio) is Contentstack’s visual experience builder: you structure pages from reusable components, bind CMS data, and manage compositions. It is designed to work with [Live Preview and Visual Editor](https://www.contentstack.com/docs/studio/live-preview-and-visual-editing-with-studio) on your connected site (layout in Studio, real-time preview, then field-level edits in Visual Editor). ## Requirements diff --git a/main.mustache b/main.mustache index 8f1a30c4..6e0f440b 100644 --- a/main.mustache +++ b/main.mustache @@ -10,11 +10,9 @@ See how [Live Preview](https://www.contentstack.com/docs/content-managers/author ## Where this SDK runs - **Live Preview** — Preview entries in the stack while your site loads inside the preview panel. -- **Timeline** — Time-based preview (how the site looks on future dates). Your integration pattern is the same as Live Preview from the app’s perspective; the parent context is still a preview iframe. In code, `config.windowType` can reflect a preview context that includes Timeline ([see `config` / `windowType`](docs/live-preview-configs.md#config)). +- **Timeline** — Time-based preview (how the site looks on future dates and scheduled updates). Use the same Live Preview setup on your site; see the Timeline docs below for stack-side behavior. - **Visual Editor** — WYSIWYG editing with the site in an iframe. Use `mode: "builder"` so “Start Editing” targets Visual Editor; the SDK still works when the same site is opened in Live Preview ([`mode`](docs/live-preview-configs.md#mode)). -- **Studio** — [Contentstack Studio](https://www.contentstack.com/docs/studio) is the visual composition experience for building experiences from components and content. Use this package when your front end needs the same live preview and stack-driven editing hooks as in Live Preview; follow Studio and Live Preview setup docs together for your stack. - -**Integrators:** In hosted preview, the platform loads your site in an iframe; this SDK talks to the parent window. When you open the site outside Contentstack (`windowType: independent`), behavior follows the [config](docs/live-preview-configs.md#config) rules in the reference doc. +- **Studio** — [Contentstack Studio](https://www.contentstack.com/docs/studio) is Contentstack’s visual experience builder: you structure pages from reusable components, bind CMS data, and manage compositions. It is designed to work with [Live Preview and Visual Editor](https://www.contentstack.com/docs/studio/live-preview-and-visual-editing-with-studio) on your connected site (layout in Studio, real-time preview, then field-level edits in Visual Editor). ## Requirements From e8d6ba34ecd9ec3c58988e5d82c5efbd52f4fc96 Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Tue, 31 Mar 2026 18:12:15 +0530 Subject: [PATCH 12/15] docs: update README and main.mustache to include Studio in descriptions --- README.md | 6 +++--- main.mustache | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 73905e11..bf0d17b8 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ The **Live Preview Utils** package runs in your website and opens a communication channel between the page (often inside a Contentstack preview iframe) and the platform. It powers live content updates, edit controls, and Visual Editor UI in the preview surface via messaging—not a replacement for the Contentstack delivery SDKs, but the client bridge for preview and Visual Editor experiences. -See how [Live Preview](https://www.contentstack.com/docs/content-managers/author-content/about-live-preview), [Timeline](https://www.contentstack.com/docs/content-managers/timeline/preview-content-across-a-timeline), and [Visual Editor](https://www.contentstack.com/docs/content-managers/visual-editor/about-visual-editor) work in Contentstack for editors and content managers. +See how [Live Preview](https://www.contentstack.com/docs/content-managers/author-content/about-live-preview), [Timeline](https://www.contentstack.com/docs/content-managers/timeline/preview-content-across-a-timeline), [Visual Editor](https://www.contentstack.com/docs/content-managers/visual-editor/about-visual-editor), and [Studio](https://www.contentstack.com/docs/studio) work in Contentstack for editors and content managers. ## Where this SDK runs @@ -46,12 +46,12 @@ Pin the version to match your app (update `4.3.0` when you upgrade): ## Quick start -[`init`](docs/live-preview-configs.md#initconfig-iconfig) returns a `Promise` that resolves to `{ livePreview, visualBuilder }`. Calling `init` again returns the existing instance and logs a warning. +[`init`](docs/live-preview-configs.md#initconfig-iconfig) returns a `Promise` that resolves when the SDK is ready. Call it once; subsequent calls reuse the same in-memory instance and log a warning. **Use the static APIs** in [docs/live-preview-configs.md](docs/live-preview-configs.md) (for example [`onLiveEdit`](docs/live-preview-configs.md#onliveeditcallback---void), [`hash`](docs/live-preview-configs.md#hash), [`config`](docs/live-preview-configs.md#config))—we do not recommend destructuring or otherwise using the value returned from `init`. ```javascript import ContentstackLivePreview from "@contentstack/live-preview-utils"; -const { livePreview, visualBuilder } = await ContentstackLivePreview.init({ +await ContentstackLivePreview.init({ stackDetails: { apiKey: "your-stack-api-key", }, diff --git a/main.mustache b/main.mustache index 6e0f440b..2708829f 100644 --- a/main.mustache +++ b/main.mustache @@ -5,7 +5,7 @@ The **Live Preview Utils** package runs in your website and opens a communication channel between the page (often inside a Contentstack preview iframe) and the platform. It powers live content updates, edit controls, and Visual Editor UI in the preview surface via messaging—not a replacement for the Contentstack delivery SDKs, but the client bridge for preview and Visual Editor experiences. -See how [Live Preview](https://www.contentstack.com/docs/content-managers/author-content/about-live-preview), [Timeline](https://www.contentstack.com/docs/content-managers/timeline/preview-content-across-a-timeline), and [Visual Editor](https://www.contentstack.com/docs/content-managers/visual-editor/about-visual-editor) work in Contentstack for editors and content managers. +See how [Live Preview](https://www.contentstack.com/docs/content-managers/author-content/about-live-preview), [Timeline](https://www.contentstack.com/docs/content-managers/timeline/preview-content-across-a-timeline), [Visual Editor](https://www.contentstack.com/docs/content-managers/visual-editor/about-visual-editor), and [Studio](https://www.contentstack.com/docs/studio) work in Contentstack for editors and content managers. ## Where this SDK runs @@ -46,12 +46,12 @@ Pin the version to match your app (update `{{packageVersion}}` when you upgrade) ## Quick start -[`init`](docs/live-preview-configs.md#initconfig-iconfig) returns a `Promise` that resolves to `{ livePreview, visualBuilder }`. Calling `init` again returns the existing instance and logs a warning. +[`init`](docs/live-preview-configs.md#initconfig-iconfig) returns a `Promise` that resolves when the SDK is ready. Call it once; subsequent calls reuse the same in-memory instance and log a warning. **Use the static APIs** in [docs/live-preview-configs.md](docs/live-preview-configs.md) (for example [`onLiveEdit`](docs/live-preview-configs.md#onliveeditcallback---void), [`hash`](docs/live-preview-configs.md#hash), [`config`](docs/live-preview-configs.md#config))—we do not recommend destructuring or otherwise using the value returned from `init`. ```javascript import ContentstackLivePreview from "@contentstack/live-preview-utils"; -const { livePreview, visualBuilder } = await ContentstackLivePreview.init({ +await ContentstackLivePreview.init({ stackDetails: { apiKey: "your-stack-api-key", }, From f0b121166ad62aa17a0d2866682b1db54edecf9b Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Tue, 31 Mar 2026 18:22:17 +0530 Subject: [PATCH 13/15] docs: update initialization method in README and main.mustache to remove Promise usage --- README.md | 10 +++++++--- main.mustache | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bf0d17b8..1bf3e0cd 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Pin the version to match your app (update `4.3.0` when you upgrade):