From 47a6f2966c0aca90629fc5bcd5ec2f0579384525 Mon Sep 17 00:00:00 2001 From: csAyushDubey Date: Tue, 26 Aug 2025 10:23:11 +0530 Subject: [PATCH 1/5] feat: added variant order handling logic --- .../useVariantsPostMessageEvent.ts | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts index f3e543ac..c09fa67a 100644 --- a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts @@ -9,6 +9,7 @@ interface VariantFieldsEvent { variant_data: { variant: string; highlightVariantFields: boolean; + variantOrder: string[]; }; }; } @@ -34,9 +35,26 @@ interface LocaleEvent { locale: string; }; } + +function isLowerOrderVariant(variant_uid: string, dataCslp: string, variantOrder: string[]): boolean { + const indexOfVariant = variantOrder.indexOf(variant_uid); + let indexOfDataCslp = -1; + for (let i = variantOrder.length-1; i >= 0; i--) { + if (dataCslp.includes(variantOrder[i])) { + indexOfDataCslp = i; + break; + } + } + if(indexOfDataCslp < 0) { + return false; + } + return indexOfDataCslp < indexOfVariant; +} + export function addVariantFieldClass( variant_uid: string, - highlightVariantFields: boolean + highlightVariantFields: boolean, + variantOrder: string[] ): void { const elements = document.querySelectorAll(`[data-cslp]`); elements.forEach((element) => { @@ -51,7 +69,11 @@ export function addVariantFieldClass( element.classList.add("visual-builder__variant-field"); } else if (!dataCslp.startsWith("v2:")) { element.classList.add("visual-builder__base-field"); - } else { + } + else if (isLowerOrderVariant(variant_uid, dataCslp, variantOrder)) { + element.classList.add("visual-builder__variant-field"); + } + else { element.classList.add("visual-builder__disabled-variant-field"); } }); @@ -125,7 +147,8 @@ export function useVariantFieldsPostMessageEvent(): void { removeVariantFieldClass(); addVariantFieldClass( event.data.variant_data.variant, - event.data.variant_data.highlightVariantFields + event.data.variant_data.highlightVariantFields, + event.data.variant_data.variantOrder ); } ); From 15c8f04508cc072c846aebf7185b72f1bc179036 Mon Sep 17 00:00:00 2001 From: csAyushDubey Date: Fri, 12 Sep 2025 11:22:07 +0530 Subject: [PATCH 2/5] feat: added resolved variants permission handling logic --- src/visualBuilder/components/FieldToolbar.tsx | 6 ++++- .../__test__/fieldLabelWrapper.test.tsx | 1 + .../components/fieldLabelWrapper.tsx | 6 +++-- .../useVariantsPostMessageEvent.ts | 3 +++ .../generators/generateToolbar.tsx | 4 +++- src/visualBuilder/listeners/mouseClick.ts | 5 +++- src/visualBuilder/listeners/mouseHover.ts | 8 +++++-- .../__test__/handleIndividualFields.test.ts | 5 +++- .../utils/__test__/isFieldDisabled.test.ts | 14 ++++++++++- .../__test__/updateFocussedState.test.ts | 3 +++ .../fetchEntryPermissionsAndStageDetails.ts | 20 ++++++++++++++++ .../utils/getResolvedVariantPermissions.ts | 24 +++++++++++++++++++ .../utils/handleIndividualFields.ts | 6 ++++- src/visualBuilder/utils/isFieldDisabled.ts | 11 ++++++++- .../utils/types/postMessage.types.ts | 1 + .../utils/updateFocussedState.ts | 4 +++- 16 files changed, 109 insertions(+), 12 deletions(-) create mode 100644 src/visualBuilder/utils/getResolvedVariantPermissions.ts diff --git a/src/visualBuilder/components/FieldToolbar.tsx b/src/visualBuilder/components/FieldToolbar.tsx index f10a19fd..55fa5aba 100644 --- a/src/visualBuilder/components/FieldToolbar.tsx +++ b/src/visualBuilder/components/FieldToolbar.tsx @@ -43,6 +43,7 @@ import { EntryPermissions } from "../utils/getEntryPermissions"; import { FieldLocationAppList } from "./FieldLocationAppList"; import { FieldLocationIcon } from "./FieldLocationIcon"; import { WorkflowStageDetails } from "../utils/getWorkflowStageDetails"; +import { ResolvedVariantPermissions } from "../utils/getResolvedVariantPermissions"; export type FieldDetails = Pick< VisualBuilderCslpEventDetails, @@ -57,6 +58,7 @@ interface MultipleFieldToolbarProps { isVariant?: boolean; entryPermissions?: EntryPermissions | undefined; entryWorkflowStageDetails?: WorkflowStageDetails | undefined; + resolvedVariantPermissions?: ResolvedVariantPermissions | undefined; } function handleReplaceAsset(fieldMetadata: CslpData) { @@ -117,6 +119,7 @@ function FieldToolbarComponent( isVariant: isVariantOrParentOfVariant, entryPermissions, entryWorkflowStageDetails, + resolvedVariantPermissions, } = props; const { fieldMetadata, editableElement: targetElement } = eventDetails; const [isFormLoading, setIsFormLoading] = useState(false); @@ -157,8 +160,9 @@ function FieldToolbarComponent( editableElement: targetElement, fieldMetadata, }, + resolvedVariantPermissions, entryPermissions, - entryWorkflowStageDetails + entryWorkflowStageDetails, ); disableFieldActions = isDisabled; diff --git a/src/visualBuilder/components/__test__/fieldLabelWrapper.test.tsx b/src/visualBuilder/components/__test__/fieldLabelWrapper.test.tsx index 3a65abc5..c6f3b378 100644 --- a/src/visualBuilder/components/__test__/fieldLabelWrapper.test.tsx +++ b/src/visualBuilder/components/__test__/fieldLabelWrapper.test.tsx @@ -297,6 +297,7 @@ describe("FieldLabelWrapperComponent", () => { expect(isFieldDisabled).toHaveBeenCalledWith( mockFieldSchema, mockEventDetails, + undefined, { update: { create: true, diff --git a/src/visualBuilder/components/fieldLabelWrapper.tsx b/src/visualBuilder/components/fieldLabelWrapper.tsx index 9ac2c22b..1aed53c6 100644 --- a/src/visualBuilder/components/fieldLabelWrapper.tsx +++ b/src/visualBuilder/components/fieldLabelWrapper.tsx @@ -156,18 +156,20 @@ function FieldLabelWrapperComponent( return; } - const { acl: entryAcl, workflowStage: entryWorkflowStageDetails } = + const { acl: entryAcl, workflowStage: entryWorkflowStageDetails, resolvedVariantPermissions } = await fetchEntryPermissionsAndStageDetails({ entryUid: props.fieldMetadata.entry_uid, contentTypeUid: props.fieldMetadata.content_type_uid, locale: props.fieldMetadata.locale, variantUid: props.fieldMetadata.variant, + fieldPathWithIndex: props.fieldMetadata.fieldPathWithIndex, }); const { isDisabled: fieldDisabled, reason } = isFieldDisabled( fieldSchema, eventDetails, + resolvedVariantPermissions, entryAcl, - entryWorkflowStageDetails + entryWorkflowStageDetails, ); const currentFieldDisplayName = diff --git a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts index c09fa67a..9730a6f1 100644 --- a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts @@ -37,6 +37,9 @@ interface LocaleEvent { } function isLowerOrderVariant(variant_uid: string, dataCslp: string, variantOrder: string[]): boolean { + if(!variantOrder || variantOrder.length === 0) { + return false; + } const indexOfVariant = variantOrder.indexOf(variant_uid); let indexOfDataCslp = -1; for (let i = variantOrder.length-1; i >= 0; i--) { diff --git a/src/visualBuilder/generators/generateToolbar.tsx b/src/visualBuilder/generators/generateToolbar.tsx index d1ca133d..d9094de7 100644 --- a/src/visualBuilder/generators/generateToolbar.tsx +++ b/src/visualBuilder/generators/generateToolbar.tsx @@ -51,12 +51,13 @@ export async function appendFieldToolbar( ) && !isHover ) return; - const { acl: entryPermissions, workflowStage: entryWorkflowStageDetails } = + const { acl: entryPermissions, workflowStage: entryWorkflowStageDetails, resolvedVariantPermissions } = await fetchEntryPermissionsAndStageDetails({ entryUid: eventDetails.fieldMetadata.entry_uid, contentTypeUid: eventDetails.fieldMetadata.content_type_uid, locale: eventDetails.fieldMetadata.locale, variantUid: eventDetails.fieldMetadata.variant, + fieldPathWithIndex: eventDetails.fieldMetadata.fieldPathWithIndex, }); const wrapper = document.createDocumentFragment(); render( @@ -66,6 +67,7 @@ export async function appendFieldToolbar( isVariant={isVariant} entryPermissions={entryPermissions} entryWorkflowStageDetails={entryWorkflowStageDetails} + resolvedVariantPermissions={resolvedVariantPermissions} />, wrapper ); diff --git a/src/visualBuilder/listeners/mouseClick.ts b/src/visualBuilder/listeners/mouseClick.ts index d65ed0b3..76b2ca9e 100644 --- a/src/visualBuilder/listeners/mouseClick.ts +++ b/src/visualBuilder/listeners/mouseClick.ts @@ -310,23 +310,26 @@ async function handleFieldSchemaAndIndividualFields( fieldPath, locale, variant: variantUid, + fieldPathWithIndex, } = fieldMetadata; const fieldSchema = await FieldSchemaMap.getFieldSchema( content_type_uid, fieldPath ); - const { acl: entryAcl, workflowStage: entryWorkflowStageDetails } = + const { acl: entryAcl, workflowStage: entryWorkflowStageDetails, resolvedVariantPermissions } = await fetchEntryPermissionsAndStageDetails({ entryUid: entry_uid, contentTypeUid: content_type_uid, locale, variantUid, + fieldPathWithIndex, }); if (fieldSchema) { const { isDisabled } = isFieldDisabled( fieldSchema, eventDetails, + resolvedVariantPermissions, entryAcl, entryWorkflowStageDetails ); diff --git a/src/visualBuilder/listeners/mouseHover.ts b/src/visualBuilder/listeners/mouseHover.ts index fee58c28..563a7904 100644 --- a/src/visualBuilder/listeners/mouseHover.ts +++ b/src/visualBuilder/listeners/mouseHover.ts @@ -87,16 +87,18 @@ async function addOutline(params?: AddOutlineParams): Promise { fieldPath ); if (!fieldSchema) return; - const { acl: entryAcl, workflowStage: entryWorkflowStageDetails } = + const { acl: entryAcl, workflowStage: entryWorkflowStageDetails, resolvedVariantPermissions } = await fetchEntryPermissionsAndStageDetails({ entryUid: fieldMetadata.entry_uid, contentTypeUid: fieldMetadata.content_type_uid, locale: fieldMetadata.locale, variantUid: fieldMetadata.variant, + fieldPathWithIndex: fieldMetadata.fieldPathWithIndex, }); const { isDisabled } = isFieldDisabled( fieldSchema, eventDetails, + resolvedVariantPermissions, entryAcl, entryWorkflowStageDetails ); @@ -372,16 +374,18 @@ async function generateCursor({ if (!fieldSchema) { return; } - const { acl: entryAcl, workflowStage: entryWorkflowStageDetails } = + const { acl: entryAcl, workflowStage: entryWorkflowStageDetails, resolvedVariantPermissions } = await fetchEntryPermissionsAndStageDetails({ entryUid: fieldMetadata.entry_uid, contentTypeUid: fieldMetadata.content_type_uid, locale: fieldMetadata.locale, variantUid: fieldMetadata.variant, + fieldPathWithIndex: fieldMetadata.fieldPathWithIndex, }); const { isDisabled: fieldDisabled } = isFieldDisabled( fieldSchema, eventDetails, + resolvedVariantPermissions, entryAcl, entryWorkflowStageDetails ); diff --git a/src/visualBuilder/utils/__test__/handleIndividualFields.test.ts b/src/visualBuilder/utils/__test__/handleIndividualFields.test.ts index 85d364da..16b1afbf 100644 --- a/src/visualBuilder/utils/__test__/handleIndividualFields.test.ts +++ b/src/visualBuilder/utils/__test__/handleIndividualFields.test.ts @@ -74,7 +74,10 @@ describe("handleIndividualFields", () => { expect(getFieldType).toHaveBeenCalledWith(fieldSchema); expect(isFieldDisabled).toHaveBeenCalledWith( fieldSchema, - eventDetails, + eventDetails, + { + update: true, + }, { read: true, update: true, diff --git a/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts b/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts index 50efeb68..2e85da52 100644 --- a/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts +++ b/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts @@ -5,6 +5,11 @@ import { FieldDetails } from "../../components/FieldToolbar"; import Config from "../../../configManager/configManager"; import { VisualBuilder } from "../.."; import { EntryPermissions } from "../getEntryPermissions"; +import { ResolvedVariantPermissions } from "../getResolvedVariantPermissions"; + +const resolvedVariantPermissions: ResolvedVariantPermissions = { + update: true, +}; describe("isFieldDisabled", () => { it("should return disabled state due to read-only role", () => { @@ -247,7 +252,7 @@ describe("isFieldDisabled", () => { }, }; - const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails, { + const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails, resolvedVariantPermissions, { update: false, create: true, read: true, @@ -292,6 +297,7 @@ describe("isFieldDisabled", () => { const result = isFieldDisabled( fieldSchemaMap, eventFieldDetails, + resolvedVariantPermissions, entryPermissions, workflowStageDetails ); @@ -332,6 +338,7 @@ describe("isFieldDisabled", () => { const result = isFieldDisabled( fieldSchemaMap, eventFieldDetails, + resolvedVariantPermissions, entryPermissions, workflowStageDetails ); @@ -372,6 +379,7 @@ describe("isFieldDisabled", () => { const result = isFieldDisabled( fieldSchemaMap, eventFieldDetails, + resolvedVariantPermissions, entryPermissions, workflowStageDetails ); @@ -408,6 +416,7 @@ describe("isFieldDisabled", () => { const result = isFieldDisabled( fieldSchemaMap, eventFieldDetails, + resolvedVariantPermissions, entryPermissions, workflowStageDetails ); @@ -448,6 +457,7 @@ describe("isFieldDisabled", () => { const result = isFieldDisabled( fieldSchemaMap, eventFieldDetails, + resolvedVariantPermissions, entryPermissions, // @ts-expect-error testing missing name property workflowStageDetails @@ -493,6 +503,7 @@ describe("isFieldDisabled", () => { const result = isFieldDisabled( fieldSchemaMap, eventFieldDetails, + resolvedVariantPermissions, entryPermissions, workflowStageDetails ); @@ -524,6 +535,7 @@ describe("isFieldDisabled", () => { const result = isFieldDisabled( fieldSchemaMap, eventFieldDetails, + resolvedVariantPermissions, entryPermissions, undefined ); diff --git a/src/visualBuilder/utils/__test__/updateFocussedState.test.ts b/src/visualBuilder/utils/__test__/updateFocussedState.test.ts index f427eaf0..1f742a06 100644 --- a/src/visualBuilder/utils/__test__/updateFocussedState.test.ts +++ b/src/visualBuilder/utils/__test__/updateFocussedState.test.ts @@ -223,6 +223,9 @@ describe("updateFocussedState", () => { editableElement: editableElementMock, fieldMetadata: expect.any(Object), }, + { + update: true, + }, mockEntryPermissions, mockWorkflowStageDetails ); diff --git a/src/visualBuilder/utils/fetchEntryPermissionsAndStageDetails.ts b/src/visualBuilder/utils/fetchEntryPermissionsAndStageDetails.ts index f9ed2b24..6fe2b305 100644 --- a/src/visualBuilder/utils/fetchEntryPermissionsAndStageDetails.ts +++ b/src/visualBuilder/utils/fetchEntryPermissionsAndStageDetails.ts @@ -1,4 +1,5 @@ import { getEntryPermissionsCached } from "./getEntryPermissionsCached"; +import { getResolvedVariantPermissions } from "./getResolvedVariantPermissions"; import { getWorkflowStageDetails } from "./getWorkflowStageDetails"; export async function fetchEntryPermissionsAndStageDetails({ @@ -6,10 +7,12 @@ export async function fetchEntryPermissionsAndStageDetails({ contentTypeUid, locale, variantUid, + fieldPathWithIndex, }: { entryUid: string; contentTypeUid: string; locale: string; + fieldPathWithIndex: string; variantUid?: string | undefined; }) { const entryAclPromise = getEntryPermissionsCached({ @@ -17,6 +20,13 @@ export async function fetchEntryPermissionsAndStageDetails({ contentTypeUid, locale, }); + const resolvedVariantPermissionsPromise = getResolvedVariantPermissions({ + entry_uid: entryUid, + content_type_uid: contentTypeUid, + locale, + variant: variantUid, + fieldPathWithIndex, + }); const entryWorkflowStageDetailsPromise = getWorkflowStageDetails({ entryUid, contentTypeUid, @@ -26,6 +36,7 @@ export async function fetchEntryPermissionsAndStageDetails({ const results = await Promise.allSettled([ entryAclPromise, entryWorkflowStageDetailsPromise, + resolvedVariantPermissionsPromise ]); if (results[0].status === "rejected") { console.debug( @@ -39,12 +50,21 @@ export async function fetchEntryPermissionsAndStageDetails({ results[1].reason ); } + if (results[2].status === "rejected") { + console.debug( + "[Visual Builder] Error retrieving resolved variant permissions", + results[2].reason + ); + } const acl = results[0].status === "fulfilled" ? results[0].value : undefined; const workflowStage = results[1].status === "fulfilled" ? results[1].value : undefined; + const resolvedVariantPermissions = + results[2].status === "fulfilled" ? results[2].value : undefined; return { acl, workflowStage, + resolvedVariantPermissions, }; } diff --git a/src/visualBuilder/utils/getResolvedVariantPermissions.ts b/src/visualBuilder/utils/getResolvedVariantPermissions.ts new file mode 100644 index 00000000..130adb8b --- /dev/null +++ b/src/visualBuilder/utils/getResolvedVariantPermissions.ts @@ -0,0 +1,24 @@ +import { CslpData } from "../../cslp/types/cslp.types"; +import { VisualBuilderPostMessageEvents } from "./types/postMessage.types"; +import visualBuilderPostMessage from "./visualBuilderPostMessage"; + +export type FieldContext = Pick; + +export interface ResolvedVariantPermissions { + update: boolean; +} + +export async function getResolvedVariantPermissions(fieldContext: FieldContext) { + try { + const result = await visualBuilderPostMessage?.send(VisualBuilderPostMessageEvents.GET_RESOLVED_VARIANT_PERMISSIONS, fieldContext); + return result ?? { + update: true, + }; + } + catch(e) { + console.warn("Error retrieving resolved variant permissions", e); + return { + update: true, + }; + } +} \ No newline at end of file diff --git a/src/visualBuilder/utils/handleIndividualFields.ts b/src/visualBuilder/utils/handleIndividualFields.ts index 30f4bd5c..4ae9529b 100644 --- a/src/visualBuilder/utils/handleIndividualFields.ts +++ b/src/visualBuilder/utils/handleIndividualFields.ts @@ -33,6 +33,7 @@ export async function handleIndividualFields( content_type_uid, entry_uid, locale, + variant, fieldPath, fieldPathWithIndex, } = fieldMetadata; @@ -47,15 +48,18 @@ export async function handleIndividualFields( const fieldType = getFieldType(fieldSchema); - const { acl: entryAcl, workflowStage: entryWorkflowStageDetails } = + const { acl: entryAcl, workflowStage: entryWorkflowStageDetails, resolvedVariantPermissions } = await fetchEntryPermissionsAndStageDetails({ entryUid: entry_uid, contentTypeUid: content_type_uid, locale, + variantUid: variant, + fieldPathWithIndex, }); const { isDisabled: disabled } = isFieldDisabled( fieldSchema, eventDetails, + resolvedVariantPermissions, entryAcl, entryWorkflowStageDetails ); diff --git a/src/visualBuilder/utils/isFieldDisabled.ts b/src/visualBuilder/utils/isFieldDisabled.ts index 8d6c7118..cf557fb1 100644 --- a/src/visualBuilder/utils/isFieldDisabled.ts +++ b/src/visualBuilder/utils/isFieldDisabled.ts @@ -4,10 +4,12 @@ import { VisualBuilder } from ".."; import { FieldDetails } from "../components/FieldToolbar"; import { EntryPermissions } from "./getEntryPermissions"; import { WorkflowStageDetails } from "./getWorkflowStageDetails"; +import { ResolvedVariantPermissions } from "./getResolvedVariantPermissions"; const DisableReason = { ReadOnly: "You have only read access to this field", LocalizedEntry: "Editing this field is restricted in localized entries", + ResolvedVariantPermissions: "This field does not exist in the selected variant", UnlinkedVariant: "This field is not editable as it is not linked to the selected variant", AudienceMode: "Open an Experience from Audience widget to start editing", @@ -62,14 +64,18 @@ const getDisableReason = ( stageName: params?.stageName ? params.stageName : "Unknown", }); } + if(flags.updateRestrictDueToResolvedVariantPermissions) { + return DisableReason.ResolvedVariantPermissions; + } return DisableReason.None; }; export const isFieldDisabled = ( fieldSchemaMap: ISchemaFieldMap, eventFieldDetails: FieldDetails, + resolvedVariantPermissions?: ResolvedVariantPermissions, entryPermissions?: EntryPermissions, - entryWorkflowStageDetails?: WorkflowStageDetails + entryWorkflowStageDetails?: WorkflowStageDetails, ): FieldDisableState => { const { editableElement, fieldMetadata } = eventFieldDetails; const masterLocale = Config.get().stackDetails.masterLocale || "en-us"; @@ -90,6 +96,9 @@ export const isFieldDisabled = ( fieldSchemaMap?.non_localizable && masterLocale !== fieldMetadata.locale ), + updateRestrictDueToResolvedVariantPermissions: resolvedVariantPermissions ? Boolean( + !resolvedVariantPermissions.update + ) : false, updateRestrictDueToAudienceMode: false, updateRestrictDueToDisabledVariant: false, }; diff --git a/src/visualBuilder/utils/types/postMessage.types.ts b/src/visualBuilder/utils/types/postMessage.types.ts index f14c63f0..ed763222 100644 --- a/src/visualBuilder/utils/types/postMessage.types.ts +++ b/src/visualBuilder/utils/types/postMessage.types.ts @@ -30,6 +30,7 @@ export enum VisualBuilderPostMessageEvents { FIELD_LOCATION_SELECTED_APP = "field-location-selected-app", GET_PERMISSIONS = "get-permissions", GET_WORKFLOW_STAGE_DETAILS = "get-workflow-stage-details", + GET_RESOLVED_VARIANT_PERMISSIONS = "get-resolved-variant-permissions", // FROM visual builder GET_ALL_ENTRIES_IN_CURRENT_PAGE = "get-entries-in-current-page", diff --git a/src/visualBuilder/utils/updateFocussedState.ts b/src/visualBuilder/utils/updateFocussedState.ts index a5799fab..9ed5445a 100644 --- a/src/visualBuilder/utils/updateFocussedState.ts +++ b/src/visualBuilder/utils/updateFocussedState.ts @@ -146,16 +146,18 @@ export async function updateFocussedState({ fieldMetadata.content_type_uid, fieldMetadata.fieldPath ); - const { acl: entryAcl, workflowStage: entryWorkflowStageDetails } = + const { acl: entryAcl, workflowStage: entryWorkflowStageDetails, resolvedVariantPermissions } = await fetchEntryPermissionsAndStageDetails({ entryUid: fieldMetadata.entry_uid, contentTypeUid: fieldMetadata.content_type_uid, locale: fieldMetadata.locale, variantUid: fieldMetadata.variant, + fieldPathWithIndex: fieldMetadata.fieldPathWithIndex, }); const { isDisabled } = isFieldDisabled( fieldSchema, { editableElement, fieldMetadata }, + resolvedVariantPermissions, entryAcl, entryWorkflowStageDetails ); From 9ee8f8e8a3e27d3b3fd9aee0dbbfc5df0f8d9b57 Mon Sep 17 00:00:00 2001 From: csAyushDubey Date: Mon, 27 Oct 2025 00:11:14 +0530 Subject: [PATCH 3/5] fix: tests --- .../__test__/click/fields/multi-line.test.tsx | 8 ++++++++ src/visualBuilder/__test__/click/fields/number.test.tsx | 5 +++++ .../__test__/click/fields/single-line.test.tsx | 8 ++++++++ 3 files changed, 21 insertions(+) diff --git a/src/visualBuilder/__test__/click/fields/multi-line.test.tsx b/src/visualBuilder/__test__/click/fields/multi-line.test.tsx index 346eda0b..40adcc2b 100644 --- a/src/visualBuilder/__test__/click/fields/multi-line.test.tsx +++ b/src/visualBuilder/__test__/click/fields/multi-line.test.tsx @@ -115,6 +115,10 @@ describe("When an element is clicked in visual builder mode", () => { }, }, }); + case VisualBuilderPostMessageEvents.GET_RESOLVED_VARIANT_PERMISSIONS: + return Promise.resolve({ + update: true, + }); default: return Promise.resolve({}); } @@ -216,6 +220,10 @@ describe("When an element is clicked in visual builder mode", () => { }, }); } + case VisualBuilderPostMessageEvents.GET_RESOLVED_VARIANT_PERMISSIONS: + return Promise.resolve({ + update: true, + }); default: return Promise.resolve({}); } diff --git a/src/visualBuilder/__test__/click/fields/number.test.tsx b/src/visualBuilder/__test__/click/fields/number.test.tsx index 9190d8eb..d4fdacf4 100644 --- a/src/visualBuilder/__test__/click/fields/number.test.tsx +++ b/src/visualBuilder/__test__/click/fields/number.test.tsx @@ -223,6 +223,11 @@ describe("When an element is clicked in visual builder mode", () => { }, }); } + else if (eventName === VisualBuilderPostMessageEvents.GET_RESOLVED_VARIANT_PERMISSIONS) { + return Promise.resolve({ + update: true, + }); + } return Promise.resolve({}); } ); diff --git a/src/visualBuilder/__test__/click/fields/single-line.test.tsx b/src/visualBuilder/__test__/click/fields/single-line.test.tsx index 55a5899c..b9d113ee 100644 --- a/src/visualBuilder/__test__/click/fields/single-line.test.tsx +++ b/src/visualBuilder/__test__/click/fields/single-line.test.tsx @@ -119,6 +119,10 @@ describe("When an element is clicked in visual builder mode", () => { }, }, }); + case VisualBuilderPostMessageEvents.GET_RESOLVED_VARIANT_PERMISSIONS: + return Promise.resolve({ + update: true, + }); default: return Promise.resolve({}); } @@ -234,6 +238,10 @@ describe("When an element is clicked in visual builder mode", () => { }, }, }); + case VisualBuilderPostMessageEvents.GET_RESOLVED_VARIANT_PERMISSIONS: + return Promise.resolve({ + update: true, + }); default: return Promise.resolve({}); } From 1ba0b65d89c654507c0a704c9209245138dd7919 Mon Sep 17 00:00:00 2001 From: csAyushDubey Date: Tue, 28 Oct 2025 00:33:09 +0530 Subject: [PATCH 4/5] test: added test cases --- .../useVariantsPostMessageEvent.spec.ts | 22 +- .../useVariantsPostMessageEvent.ts | 18 +- .../getResolvedVariantPermissions.spec.ts | 267 ++++++++++++++++++ .../utils/getResolvedVariantPermissions.ts | 3 + src/visualBuilder/utils/isFieldDisabled.ts | 7 + 5 files changed, 304 insertions(+), 13 deletions(-) create mode 100644 src/visualBuilder/utils/__test__/getResolvedVariantPermissions.spec.ts diff --git a/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts b/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts index eae0fc12..2b4f7b3a 100644 --- a/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts +++ b/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts @@ -21,6 +21,7 @@ import { FieldSchemaMap } from "../../../visualBuilder/utils/fieldSchemaMap"; import { visualBuilderStyles } from "../../../visualBuilder/visualBuilder.style"; import visualBuilderPostMessage from "../../../visualBuilder/utils/visualBuilderPostMessage"; import { EventManager } from "@contentstack/advanced-post-message"; +import * as cslpdata from "../../../cslp/cslpdata"; const mockVisualBuilderPostMessage = visualBuilderPostMessage as MockedObject; @@ -339,7 +340,7 @@ describe("addVariantFieldClass", () => { const variantUid = "variant-123"; const highlightVariantFields = true; - addVariantFieldClass(variantUid, highlightVariantFields); + addVariantFieldClass(variantUid, highlightVariantFields, []); // Verify querySelectorAll was called with the correct selector expect(mockQuerySelectorAll).toHaveBeenCalledWith("[data-cslp]"); @@ -370,7 +371,7 @@ describe("addVariantFieldClass", () => { const variantUid = "variant-123"; const highlightVariantFields = false; - addVariantFieldClass(variantUid, highlightVariantFields); + addVariantFieldClass(variantUid, highlightVariantFields, []); // First element has the variant ID but should not get highlight class expect(mockElements[0].getAttribute).toHaveBeenCalledWith("data-cslp"); @@ -381,6 +382,23 @@ describe("addVariantFieldClass", () => { "visual-builder__variant-field" ); }); + + it("should handle lower order variant fields correctly", () => { + // @ts-expect-error mocking only required properties + vi.spyOn(cslpdata, "extractDetailsFromCslp").mockImplementation((cslpValue) => { + return { + variant: cslpValue.split(":")[1] + } + }); + const variantUid = "variant-456"; + const highlightVariantFields = false; + const variantOrder = ["variant-123", "variant-456"]; + + addVariantFieldClass(variantUid, highlightVariantFields, variantOrder); + + // Verify that classes were added to elements correctly + expect(mockElements[0].classList.add).toHaveBeenCalledWith("visual-builder__variant-field", "visual-builder__lower-order-variant-field"); + }); }); describe("removeVariantFieldClass", () => { diff --git a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts index 9730a6f1..ac36608b 100644 --- a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts @@ -3,6 +3,7 @@ import { visualBuilderStyles } from "../visualBuilder.style"; import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types"; import { FieldSchemaMap } from "../utils/fieldSchemaMap"; +import { extractDetailsFromCslp } from "../../cslp/cslpdata"; interface VariantFieldsEvent { data: { @@ -40,18 +41,13 @@ function isLowerOrderVariant(variant_uid: string, dataCslp: string, variantOrder if(!variantOrder || variantOrder.length === 0) { return false; } - const indexOfVariant = variantOrder.indexOf(variant_uid); - let indexOfDataCslp = -1; - for (let i = variantOrder.length-1; i >= 0; i--) { - if (dataCslp.includes(variantOrder[i])) { - indexOfDataCslp = i; - break; - } - } - if(indexOfDataCslp < 0) { + const {variant: cslpVariant} = extractDetailsFromCslp(dataCslp); + const indexOfCmsVariant = variantOrder.lastIndexOf(variant_uid); + const indexOfCslpVariant = variantOrder.lastIndexOf(cslpVariant || ""); + if(indexOfCslpVariant < 0) { return false; } - return indexOfDataCslp < indexOfVariant; + return indexOfCslpVariant < indexOfCmsVariant; } export function addVariantFieldClass( @@ -74,7 +70,7 @@ export function addVariantFieldClass( element.classList.add("visual-builder__base-field"); } else if (isLowerOrderVariant(variant_uid, dataCslp, variantOrder)) { - element.classList.add("visual-builder__variant-field"); + element.classList.add("visual-builder__variant-field", "visual-builder__lower-order-variant-field"); } else { element.classList.add("visual-builder__disabled-variant-field"); diff --git a/src/visualBuilder/utils/__test__/getResolvedVariantPermissions.spec.ts b/src/visualBuilder/utils/__test__/getResolvedVariantPermissions.spec.ts new file mode 100644 index 00000000..2ff08fce --- /dev/null +++ b/src/visualBuilder/utils/__test__/getResolvedVariantPermissions.spec.ts @@ -0,0 +1,267 @@ +import { describe, it, expect, vi, beforeEach, afterEach, MockedObject } from 'vitest'; +import { EventManager } from "@contentstack/advanced-post-message"; +import { getResolvedVariantPermissions, FieldContext, ResolvedVariantPermissions } from '../getResolvedVariantPermissions'; +import { VisualBuilderPostMessageEvents } from '../types/postMessage.types'; +import visualBuilderPostMessage from '../visualBuilderPostMessage'; + +// Mock the visualBuilderPostMessage module +vi.mock('../visualBuilderPostMessage', () => { + return { + default: { + send: vi.fn(), + }, + }; +}); + +const mockVisualBuilderPostMessage = visualBuilderPostMessage as MockedObject; + +describe('getResolvedVariantPermissions', () => { + const mockFieldContext: FieldContext = { + content_type_uid: 'content_type_123', + entry_uid: 'entry_456', + locale: 'en-us', + variant: 'variant_789', + fieldPathWithIndex: 'title', + }; + + const mockSuccessResponse: ResolvedVariantPermissions = { + update: true, + error: false, + }; + + const mockErrorResponse: ResolvedVariantPermissions = { + update: false, + error: true, + }; + + beforeEach(() => { + vi.clearAllMocks(); + // Mock console.warn to avoid noise in test output + vi.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('Success scenarios', () => { + it('should return resolved permissions when postMessage returns valid response', async () => { + + mockVisualBuilderPostMessage.send.mockResolvedValue(mockSuccessResponse); + + + const result = await getResolvedVariantPermissions(mockFieldContext); + + + expect(mockVisualBuilderPostMessage.send).toHaveBeenCalledWith( + VisualBuilderPostMessageEvents.GET_RESOLVED_VARIANT_PERMISSIONS, + mockFieldContext + ); + expect(result).toEqual(mockSuccessResponse); + }); + + it('should return resolved permissions with update: false when postMessage returns false', async () => { + + const responseWithUpdateFalse = { update: false, error: false }; + mockVisualBuilderPostMessage.send.mockResolvedValue(responseWithUpdateFalse); + + + const result = await getResolvedVariantPermissions(mockFieldContext); + + + expect(result).toEqual(responseWithUpdateFalse); + }); + + it('should handle response with only update property', async () => { + + const responseWithOnlyUpdate = { update: true }; + mockVisualBuilderPostMessage.send.mockResolvedValue(responseWithOnlyUpdate); + + + const result = await getResolvedVariantPermissions(mockFieldContext); + + + expect(result).toEqual(responseWithOnlyUpdate); + }); + }); + + describe('Null/undefined response handling', () => { + it('should return default error response when postMessage returns null', async () => { + + mockVisualBuilderPostMessage.send.mockResolvedValue(null); + + + const result = await getResolvedVariantPermissions(mockFieldContext); + + + expect(result).toEqual({ + update: true, + error: true, + }); + }); + + it('should return default error response when postMessage returns undefined', async () => { + + mockVisualBuilderPostMessage.send.mockResolvedValue(undefined); + + + const result = await getResolvedVariantPermissions(mockFieldContext); + + + expect(result).toEqual({ + update: true, + error: true, + }); + }); + }); + + describe('Error handling', () => { + it('should return default error response and log warning when postMessage throws error', async () => { + + const mockError = new Error('Network error'); + mockVisualBuilderPostMessage.send.mockRejectedValue(mockError); + + + const result = await getResolvedVariantPermissions(mockFieldContext); + + + expect(console.warn).toHaveBeenCalledWith( + 'Error retrieving resolved variant permissions', + mockError + ); + expect(result).toEqual({ + update: true, + error: true, + }); + }); + + it('should handle different types of errors', async () => { + + const mockError = 'String error'; + mockVisualBuilderPostMessage.send.mockRejectedValue(mockError); + + + const result = await getResolvedVariantPermissions(mockFieldContext); + + + expect(console.warn).toHaveBeenCalledWith( + 'Error retrieving resolved variant permissions', + mockError + ); + expect(result).toEqual({ + update: true, + error: true, + }); + }); + + it('should handle promise rejection without error object', async () => { + + mockVisualBuilderPostMessage.send.mockRejectedValue(null); + + + const result = await getResolvedVariantPermissions(mockFieldContext); + + + expect(console.warn).toHaveBeenCalledWith( + 'Error retrieving resolved variant permissions', + null + ); + expect(result).toEqual({ + update: true, + error: true, + }); + }); + }); + + describe('Edge cases', () => { + it('should handle empty field context object', async () => { + + const emptyFieldContext = {} as FieldContext; + mockVisualBuilderPostMessage.send.mockResolvedValue(mockSuccessResponse); + + + const result = await getResolvedVariantPermissions(emptyFieldContext); + + + expect(mockVisualBuilderPostMessage.send).toHaveBeenCalledWith( + VisualBuilderPostMessageEvents.GET_RESOLVED_VARIANT_PERMISSIONS, + emptyFieldContext + ); + expect(result).toEqual(mockSuccessResponse); + }); + + it('should handle field context with undefined variant', async () => { + + const fieldContextWithUndefinedVariant: FieldContext = { + ...mockFieldContext, + variant: undefined, + }; + mockVisualBuilderPostMessage.send.mockResolvedValue(mockSuccessResponse); + + + const result = await getResolvedVariantPermissions(fieldContextWithUndefinedVariant); + + + expect(mockVisualBuilderPostMessage.send).toHaveBeenCalledWith( + VisualBuilderPostMessageEvents.GET_RESOLVED_VARIANT_PERMISSIONS, + fieldContextWithUndefinedVariant + ); + expect(result).toEqual(mockSuccessResponse); + }); + + it('should handle field context with null variant', async () => { + + const fieldContextWithNullVariant: FieldContext = { + ...mockFieldContext, + variant: null as any, + }; + mockVisualBuilderPostMessage.send.mockResolvedValue(mockSuccessResponse); + + + const result = await getResolvedVariantPermissions(fieldContextWithNullVariant); + + + expect(mockVisualBuilderPostMessage.send).toHaveBeenCalledWith( + VisualBuilderPostMessageEvents.GET_RESOLVED_VARIANT_PERMISSIONS, + fieldContextWithNullVariant + ); + expect(result).toEqual(mockSuccessResponse); + }); + }); + + describe('Type safety', () => { + it('should maintain proper typing for ResolvedVariantPermissions interface', async () => { + + const response: ResolvedVariantPermissions = { + update: true, + error: false, + }; + mockVisualBuilderPostMessage.send.mockResolvedValue(response); + + + const result = await getResolvedVariantPermissions(mockFieldContext); + + + expect(typeof result.update).toBe('boolean'); + expect(typeof result.error).toBe('boolean'); + expect(result).toHaveProperty('update'); + expect(result).toHaveProperty('error'); + }); + + it('should handle response with additional properties', async () => { + + const responseWithExtraProps = { + update: true, + error: false, + extraProperty: 'should be ignored', + }; + mockVisualBuilderPostMessage.send.mockResolvedValue(responseWithExtraProps); + + + const result = await getResolvedVariantPermissions(mockFieldContext); + + + expect(result).toEqual(responseWithExtraProps); + }); + }); +}); diff --git a/src/visualBuilder/utils/getResolvedVariantPermissions.ts b/src/visualBuilder/utils/getResolvedVariantPermissions.ts index 130adb8b..2cc2a730 100644 --- a/src/visualBuilder/utils/getResolvedVariantPermissions.ts +++ b/src/visualBuilder/utils/getResolvedVariantPermissions.ts @@ -6,6 +6,7 @@ export type FieldContext = Pick(VisualBuilderPostMessageEvents.GET_RESOLVED_VARIANT_PERMISSIONS, fieldContext); return result ?? { update: true, + error: true, }; } catch(e) { console.warn("Error retrieving resolved variant permissions", e); return { update: true, + error: true, }; } } \ No newline at end of file diff --git a/src/visualBuilder/utils/isFieldDisabled.ts b/src/visualBuilder/utils/isFieldDisabled.ts index b7cdd4f8..4ad73466 100644 --- a/src/visualBuilder/utils/isFieldDisabled.ts +++ b/src/visualBuilder/utils/isFieldDisabled.ts @@ -114,6 +114,13 @@ export const isFieldDisabled = ( flags.updateRestrictDueToWorkflowStagePermission = true; } + if(VisualBuilder.VisualBuilderGlobalState.value.audienceMode + && editableElement.classList.contains("visual-builder__lower-order-variant-field")) { + // If resolvedVariantPermissions errors out for any reason, we need to disable editing + // for lower order (priority) variant fields with updateRestrictDueToDisabledVariant's message + flags.updateRestrictDueToDisabledVariant = resolvedVariantPermissions ? !!resolvedVariantPermissions.error : false; + } + if ( VisualBuilder.VisualBuilderGlobalState.value.audienceMode && !editableElement.classList.contains("visual-builder__variant-field") && From f3d83b7eea482fb111be858e1ffcdeee28ac65c4 Mon Sep 17 00:00:00 2001 From: csAyushDubey Date: Tue, 28 Oct 2025 10:50:08 +0530 Subject: [PATCH 5/5] fix: added removal for class --- .../eventManager/useVariantsPostMessageEvent.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts index ac36608b..bce526e5 100644 --- a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts @@ -92,14 +92,15 @@ export function removeVariantFieldClass( }); } else { const variantAndBaseFieldElements = document.querySelectorAll( - ".visual-builder__disabled-variant-field, .visual-builder__variant-field, .visual-builder__base-field" + ".visual-builder__disabled-variant-field, .visual-builder__variant-field, .visual-builder__base-field, visual-builder__lower-order-variant-field" ); variantAndBaseFieldElements.forEach((element) => { element.classList.remove( "visual-builder__disabled-variant-field", "visual-builder__variant-field", visualBuilderStyles()["visual-builder__variant-field"], - "visual-builder__base-field" + "visual-builder__base-field", + "visual-builder__lower-order-variant-field" ); }); }