From 0e7c9ffb6c653c1e65aeef86beec5276a345c5c0 Mon Sep 17 00:00:00 2001 From: Giang Nguyen Date: Wed, 26 Nov 2025 15:19:52 +0700 Subject: [PATCH 1/3] =?UTF-8?q?=EF=BB=BFJS=20SDK=20-=20Add=20type=20Infer?= =?UTF-8?q?=20for=20displaySettings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feature: CMS-47083 --- packages/optimizely-cms-sdk/src/infer.ts | 19 ++++++-- .../__test__/parseDisplaySettings.test.ts | 4 +- .../src/model/displayTemplates.ts | 44 ++++++++++++------- .../optimizely-cms-sdk/src/model/index.ts | 2 +- .../src/components/Article.tsx | 25 ++++++++++- 5 files changed, 69 insertions(+), 25 deletions(-) diff --git a/packages/optimizely-cms-sdk/src/infer.ts b/packages/optimizely-cms-sdk/src/infer.ts index 1f0c8d13..34c8bb54 100644 --- a/packages/optimizely-cms-sdk/src/infer.ts +++ b/packages/optimizely-cms-sdk/src/infer.ts @@ -33,6 +33,7 @@ import { PublicRawFileAsset, PublicVideoAsset, } from './model/assets.js'; +import { DisplayTemplate } from './model/displayTemplates.js'; /** Forces Intellisense to resolve types */ export type Prettify = { @@ -200,9 +201,21 @@ type InferFromContentType = Prettify< InferredBase & InferProps & InferExperience & InferSection >; +/** Infers the TypeScript type for a display setting */ +type InferFromDisplayTemplate = + T extends { settings: infer S } + ? { + [K in keyof S]: + S[K] extends { choices: Record } + ? keyof S[K]['choices'] + : never; + } + : {}; + /** Infers the Graph response types of `T`. `T` can be a content type or a property */ // prettier-ignore export type Infer = - T extends AnyContentType ? InferFromContentType -: T extends AnyProperty ? InferFromProperty -: unknown; + T extends DisplayTemplate ? InferFromDisplayTemplate + : T extends AnyContentType ? InferFromContentType + : T extends AnyProperty ? InferFromProperty + : unknown; diff --git a/packages/optimizely-cms-sdk/src/model/__test__/parseDisplaySettings.test.ts b/packages/optimizely-cms-sdk/src/model/__test__/parseDisplaySettings.test.ts index 129c548d..8175be11 100644 --- a/packages/optimizely-cms-sdk/src/model/__test__/parseDisplaySettings.test.ts +++ b/packages/optimizely-cms-sdk/src/model/__test__/parseDisplaySettings.test.ts @@ -25,12 +25,12 @@ describe('parseDisplaySettings', () => { it('should return an empty object for null input', () => { const input = null; const result = parseDisplaySettings(input ?? undefined); - expect(result).toEqual(undefined); + expect(result).toEqual({}); }); it('should return an empty object for undefined input', () => { const input = undefined; const result = parseDisplaySettings(input); - expect(result).toEqual(undefined); + expect(result).toEqual({}); }); }); diff --git a/packages/optimizely-cms-sdk/src/model/displayTemplates.ts b/packages/optimizely-cms-sdk/src/model/displayTemplates.ts index b7126954..ea990143 100644 --- a/packages/optimizely-cms-sdk/src/model/displayTemplates.ts +++ b/packages/optimizely-cms-sdk/src/model/displayTemplates.ts @@ -1,4 +1,4 @@ -import { DisplaySettingsType } from '../infer.js'; +import { DisplaySettingsType, Infer } from '../infer.js'; import { BaseTypes } from './contentTypes.js'; // section node types @@ -62,20 +62,30 @@ export type DisplayTemplate = T & { __type: 'displayTemplate'; }; -export function parseDisplaySettings( - displaySettings?: DisplaySettingsType[] | null -): Record | undefined { - if (!displaySettings) { - return undefined; // Return undefined if displaySettings is not provided - } - - const result: Record = {}; // Initialize an empty object - - // Iterate over the input array - for (const item of displaySettings) { - // Assign the value to the key in the result object - result[item.key] = item.value; - } - +// export function parseDisplaySettings( +// displaySettings?: DisplaySettingsType[] | null +// ): Record | undefined { +// if (!displaySettings) { +// return undefined; // Return undefined if displaySettings is not provided +// } + +// const result: Record = {}; // Initialize an empty object + +// // Iterate over the input array +// for (const item of displaySettings) { +// // Assign the value to the key in the result object +// result[item.key] = item.value; +// } + +// return result; +// } + +export const parseDisplaySettings = ( + settings: DisplaySettingsType[] | null | undefined +): Infer => { + const result = {} as Infer; + settings?.forEach(s => { + result[s.key as keyof Infer] = s.value as Infer[keyof Infer]; + }); return result; -} +}; diff --git a/packages/optimizely-cms-sdk/src/model/index.ts b/packages/optimizely-cms-sdk/src/model/index.ts index a2a83944..2c2f86ab 100644 --- a/packages/optimizely-cms-sdk/src/model/index.ts +++ b/packages/optimizely-cms-sdk/src/model/index.ts @@ -12,7 +12,7 @@ export function contentType( /** Defines a Optimizely CMS display template */ export function displayTemplate( options: T -): T & { __type: 'displayTemplate' } { +): DisplayTemplate { return { ...options, __type: 'displayTemplate' }; } diff --git a/samples/nextjs-template/src/components/Article.tsx b/samples/nextjs-template/src/components/Article.tsx index 734136a0..c83cda72 100644 --- a/samples/nextjs-template/src/components/Article.tsx +++ b/samples/nextjs-template/src/components/Article.tsx @@ -1,4 +1,4 @@ -import { contentType, Infer } from '@optimizely/cms-sdk'; +import { contentType, displayTemplate, Infer } from '@optimizely/cms-sdk'; import { getPreviewUtils } from '@optimizely/cms-sdk/react/server'; export const ArticleContentType = contentType({ @@ -21,17 +21,37 @@ export const ArticleContentType = contentType({ }, }); +export const TeaserDisplayTemplate = displayTemplate({ + key: 'TeaserDisplayTemplate', + displayName: 'TeaserDisplayTemplate', + isDefault: false, + baseType: '_component', + settings: { + orientation: { + editor: 'select', + displayName: 'Teaser Orientation', + sortOrder: 0, + choices: { + vertical: { displayName: 'Vertical', sortOrder: 1 }, + horizontal: { displayName: 'Horizontal', sortOrder: 2 }, + }, + }, + }, +}); + type Props = { opti: Infer; + displaySettings?: Infer; }; -export default function Article({ opti }: Props) { +export default function Article({ opti, displaySettings }: Props) { const { pa } = getPreviewUtils(opti); return (

{opti.heading}

{opti.subtitle}

+

{displaySettings?.orientation}

); } + From 37d442f06c8e43ee7476264768b330fc85ff22e4 Mon Sep 17 00:00:00 2001 From: Giang Nguyen Date: Fri, 28 Nov 2025 09:45:01 +0700 Subject: [PATCH 2/3] =?UTF-8?q?=EF=BB=BFFix=20PR=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feature: CMS-47083 --- packages/optimizely-cms-sdk/src/infer.ts | 8 +++- .../__test__/parseDisplaySettings.test.ts | 40 +++++++++++----- .../src/model/displayTemplates.ts | 46 +++++++++---------- .../optimizely-cms-sdk/src/react/server.tsx | 39 ++++++++++++---- 4 files changed, 85 insertions(+), 48 deletions(-) diff --git a/packages/optimizely-cms-sdk/src/infer.ts b/packages/optimizely-cms-sdk/src/infer.ts index 34c8bb54..042b5109 100644 --- a/packages/optimizely-cms-sdk/src/infer.ts +++ b/packages/optimizely-cms-sdk/src/infer.ts @@ -206,8 +206,12 @@ type InferFromDisplayTemplate = T extends { settings: infer S } ? { [K in keyof S]: - S[K] extends { choices: Record } - ? keyof S[K]['choices'] + S[K] extends { choices: Record; editor: infer E } + ? E extends 'select' + ? keyof S[K]['choices'] + : E extends 'checkbox' + ? 'true' | 'false' + : never : never; } : {}; diff --git a/packages/optimizely-cms-sdk/src/model/__test__/parseDisplaySettings.test.ts b/packages/optimizely-cms-sdk/src/model/__test__/parseDisplaySettings.test.ts index 8175be11..38ddedd7 100644 --- a/packages/optimizely-cms-sdk/src/model/__test__/parseDisplaySettings.test.ts +++ b/packages/optimizely-cms-sdk/src/model/__test__/parseDisplaySettings.test.ts @@ -1,36 +1,54 @@ import { describe, it, expect } from 'vitest'; import { parseDisplaySettings } from '../displayTemplates.js'; + describe('parseDisplaySettings', () => { it('should parse valid display settings correctly', () => { + const template = { + settings: { + layout: { editor: 'select', choices: { grid: {}, list: {} } }, + theme: { editor: 'select', choices: { dark: {}, light: {} } } + } + } as any; + const input = [ { key: 'layout', value: 'grid' }, - { key: 'theme', value: 'dark' }, + { key: 'theme', value: 'dark' } ]; - const result = parseDisplaySettings(input); + + const result = parseDisplaySettings(input, template.settings); expect(result).toEqual({ layout: 'grid', - theme: 'dark', + theme: 'dark' }); }); - it('should handle missing properties gracefully', () => { - const input = [{ key: 'layout', value: 'grid' }]; - const result = parseDisplaySettings(input); + it('should handle checkbox correctly', () => { + const template = { + settings: { + showImage: { editor: 'checkbox', choices: {} } + } + } as any; + + const input = [{ key: 'showImage', value: 'true' }]; + const result = parseDisplaySettings(input, template.settings); expect(result).toEqual({ - layout: 'grid', + showImage: 'true' }); }); - it('should return an empty object for null input', () => { + it('should return undefined for null input', () => { + const template = { settings: {} } as any; const input = null; - const result = parseDisplaySettings(input ?? undefined); + const result = parseDisplaySettings(input, template.settings); expect(result).toEqual({}); }); - it('should return an empty object for undefined input', () => { + it('should return undefined for undefined input', () => { + const template = { settings: {} } as any; const input = undefined; - const result = parseDisplaySettings(input); + const result = parseDisplaySettings(input, template.settings); expect(result).toEqual({}); }); }); + diff --git a/packages/optimizely-cms-sdk/src/model/displayTemplates.ts b/packages/optimizely-cms-sdk/src/model/displayTemplates.ts index ea990143..784eb79f 100644 --- a/packages/optimizely-cms-sdk/src/model/displayTemplates.ts +++ b/packages/optimizely-cms-sdk/src/model/displayTemplates.ts @@ -62,30 +62,26 @@ export type DisplayTemplate = T & { __type: 'displayTemplate'; }; -// export function parseDisplaySettings( -// displaySettings?: DisplaySettingsType[] | null -// ): Record | undefined { -// if (!displaySettings) { -// return undefined; // Return undefined if displaySettings is not provided -// } - -// const result: Record = {}; // Initialize an empty object - -// // Iterate over the input array -// for (const item of displaySettings) { -// // Assign the value to the key in the result object -// result[item.key] = item.value; -// } - -// return result; -// } - -export const parseDisplaySettings = ( - settings: DisplaySettingsType[] | null | undefined -): Infer => { - const result = {} as Infer; - settings?.forEach(s => { - result[s.key as keyof Infer] = s.value as Infer[keyof Infer]; + +export function parseDisplaySettings( + settings: DisplaySettingsType[] | null | undefined, + templateSettings: Record }> +): Record { + if (!settings || settings.length === 0) return {}; + + const result: Record = {}; + + settings.forEach(s => { + const settingDef = templateSettings[s.key]; + if (!settingDef) return; + + if (settingDef.editor === 'select') { + result[s.key] = s.value; + } else if (settingDef.editor === 'checkbox') { + result[s.key] = s.value === 'true' ? 'true' : 'false'; + } }); + return result; -}; +} + diff --git a/packages/optimizely-cms-sdk/src/react/server.tsx b/packages/optimizely-cms-sdk/src/react/server.tsx index bb64a54e..2b12dc6f 100644 --- a/packages/optimizely-cms-sdk/src/react/server.tsx +++ b/packages/optimizely-cms-sdk/src/react/server.tsx @@ -11,10 +11,11 @@ import { DisplaySettingsType, ExperienceCompositionNode, InferredContentReference, + Infer, } from '../infer.js'; import { isComponentNode } from '../util/baseTypeUtil.js'; -import { parseDisplaySettings } from '../model/displayTemplates.js'; -import { getDisplayTemplateTag } from '../model/displayTemplateRegistry.js'; +import { DisplayTemplate, parseDisplaySettings } from '../model/displayTemplates.js'; +import { getDisplayTemplate, getDisplayTemplateTag } from '../model/displayTemplateRegistry.js'; import { isDev } from '../util/environment.js'; import { appendToken } from '../util/preview.js'; @@ -72,7 +73,7 @@ export function initReactComponentRegistry(options: InitOptions) { } /** Props for the {@linkcode OptimizelyComponent} component */ -type OptimizelyComponentProps = { +type OptimizelyComponentProps = { /** Data read from the CMS */ opti: { /** Content type name */ @@ -89,7 +90,7 @@ type OptimizelyComponentProps = { __composition?: ExperienceCompositionNode; }; - displaySettings?: Record; + displaySettings?: Partial>; }; export async function OptimizelyComponent({ @@ -107,6 +108,11 @@ export async function OptimizelyComponent({ }); if (!Component) { + console.log( + `[optimizely-cms-sdk] No component found for content type ${opti.__typename + } ${opti.__tag ? `with tag "${opti.__tag}"` : ''}` + ); + return ( No component found for content type {opti.__typename} @@ -123,16 +129,16 @@ export async function OptimizelyComponent({ ); } -export type StructureContainerProps = { +export type StructureContainerProps = { node: ExperienceStructureNode; children: React.ReactNode; index?: number; - displaySettings?: Record; + displaySettings?: Partial>; }; -export type ComponentContainerProps = { +export type ComponentContainerProps = { node: ExperienceComponentNode; children: React.ReactNode; - displaySettings?: Record; + displaySettings?: Partial>; }; export type StructureContainer = ( props: StructureContainerProps @@ -150,7 +156,14 @@ export function OptimizelyExperience({ }) { return nodes.map((node) => { const tag = getDisplayTemplateTag(node.displayTemplateKey); - const parsedDisplaySettings = parseDisplaySettings(node.displaySettings); + const template = node.displayTemplateKey + ? getDisplayTemplate(node.displayTemplateKey) + : null; + + const parsedDisplaySettings = template + ? parseDisplaySettings(node.displaySettings, template.settings) + : {}; + console.error(parsedDisplaySettings); if (isComponentNode(node)) { const Wrapper = ComponentWrapper ?? React.Fragment; @@ -251,7 +264,13 @@ export function OptimizelyGridSection({ return nodes.map((node, i) => { const tag = getDisplayTemplateTag(node.displayTemplateKey); - const parsedDisplaySettings = parseDisplaySettings(node.displaySettings); + const template = node.displayTemplateKey + ? getDisplayTemplate(node.displayTemplateKey) + : null; + + const parsedDisplaySettings = template + ? parseDisplaySettings(node.displaySettings, template.settings) + : {}; if (isComponentNode(node)) { return ( From d7edd8a295b181d9125b47ecfe6564de2dcde75a Mon Sep 17 00:00:00 2001 From: Giang Nguyen Date: Tue, 2 Dec 2025 10:39:24 +0700 Subject: [PATCH 3/3] Fix PR comments Feature: CMS-47083 --- .../__test__/parseDisplaySettings.test.ts | 46 ++++++------------- .../src/model/displayTemplates.ts | 28 +++++------ .../optimizely-cms-sdk/src/react/server.tsx | 28 +++++------ 3 files changed, 36 insertions(+), 66 deletions(-) diff --git a/packages/optimizely-cms-sdk/src/model/__test__/parseDisplaySettings.test.ts b/packages/optimizely-cms-sdk/src/model/__test__/parseDisplaySettings.test.ts index 38ddedd7..8cbf2782 100644 --- a/packages/optimizely-cms-sdk/src/model/__test__/parseDisplaySettings.test.ts +++ b/packages/optimizely-cms-sdk/src/model/__test__/parseDisplaySettings.test.ts @@ -1,54 +1,36 @@ import { describe, it, expect } from 'vitest'; import { parseDisplaySettings } from '../displayTemplates.js'; - describe('parseDisplaySettings', () => { it('should parse valid display settings correctly', () => { - const template = { - settings: { - layout: { editor: 'select', choices: { grid: {}, list: {} } }, - theme: { editor: 'select', choices: { dark: {}, light: {} } } - } - } as any; - const input = [ { key: 'layout', value: 'grid' }, - { key: 'theme', value: 'dark' } + { key: 'theme', value: 'dark' }, ]; - - const result = parseDisplaySettings(input, template.settings); + const result = parseDisplaySettings(input); expect(result).toEqual({ layout: 'grid', - theme: 'dark' + theme: 'dark', }); }); - it('should handle checkbox correctly', () => { - const template = { - settings: { - showImage: { editor: 'checkbox', choices: {} } - } - } as any; - - const input = [{ key: 'showImage', value: 'true' }]; - const result = parseDisplaySettings(input, template.settings); + it('should handle missing properties gracefully', () => { + const input = [{ key: 'layout', value: 'grid' }]; + const result = parseDisplaySettings(input); expect(result).toEqual({ - showImage: 'true' + layout: 'grid', }); }); - it('should return undefined for null input', () => { - const template = { settings: {} } as any; + it('should return an empty object for null input', () => { const input = null; - const result = parseDisplaySettings(input, template.settings); - expect(result).toEqual({}); + const result = parseDisplaySettings(input ?? undefined); + expect(result).toEqual(undefined); }); - it('should return undefined for undefined input', () => { - const template = { settings: {} } as any; + it('should return an empty object for undefined input', () => { const input = undefined; - const result = parseDisplaySettings(input, template.settings); - expect(result).toEqual({}); + const result = parseDisplaySettings(input); + expect(result).toEqual(undefined); }); -}); - +}); \ No newline at end of file diff --git a/packages/optimizely-cms-sdk/src/model/displayTemplates.ts b/packages/optimizely-cms-sdk/src/model/displayTemplates.ts index 784eb79f..e15a5e83 100644 --- a/packages/optimizely-cms-sdk/src/model/displayTemplates.ts +++ b/packages/optimizely-cms-sdk/src/model/displayTemplates.ts @@ -62,26 +62,20 @@ export type DisplayTemplate = T & { __type: 'displayTemplate'; }; - export function parseDisplaySettings( - settings: DisplaySettingsType[] | null | undefined, - templateSettings: Record }> -): Record { - if (!settings || settings.length === 0) return {}; - - const result: Record = {}; + displaySettings?: DisplaySettingsType[] | null +): Record | undefined { + if (!displaySettings) { + return undefined; // Return undefined if displaySettings is not provided + } - settings.forEach(s => { - const settingDef = templateSettings[s.key]; - if (!settingDef) return; + const result: Record = {}; // Initialize an empty object - if (settingDef.editor === 'select') { - result[s.key] = s.value; - } else if (settingDef.editor === 'checkbox') { - result[s.key] = s.value === 'true' ? 'true' : 'false'; - } - }); + // Iterate over the input array + for (const { key, value } of displaySettings) { + // Assign the value to the key in the result object + result[key] = value === 'true' ? true : value === 'false' ? false : value; + } return result; } - diff --git a/packages/optimizely-cms-sdk/src/react/server.tsx b/packages/optimizely-cms-sdk/src/react/server.tsx index 2b12dc6f..3a7c1513 100644 --- a/packages/optimizely-cms-sdk/src/react/server.tsx +++ b/packages/optimizely-cms-sdk/src/react/server.tsx @@ -14,7 +14,7 @@ import { Infer, } from '../infer.js'; import { isComponentNode } from '../util/baseTypeUtil.js'; -import { DisplayTemplate, parseDisplaySettings } from '../model/displayTemplates.js'; +import { parseDisplaySettings } from '../model/displayTemplates.js'; import { getDisplayTemplate, getDisplayTemplateTag } from '../model/displayTemplateRegistry.js'; import { isDev } from '../util/environment.js'; import { appendToken } from '../util/preview.js'; @@ -73,7 +73,7 @@ export function initReactComponentRegistry(options: InitOptions) { } /** Props for the {@linkcode OptimizelyComponent} component */ -type OptimizelyComponentProps = { +type OptimizelyComponentProps = { /** Data read from the CMS */ opti: { /** Content type name */ @@ -90,7 +90,7 @@ type OptimizelyComponentProps = { __composition?: ExperienceCompositionNode; }; - displaySettings?: Partial>; + displaySettings?: Record; }; export async function OptimizelyComponent({ @@ -108,11 +108,6 @@ export async function OptimizelyComponent({ }); if (!Component) { - console.log( - `[optimizely-cms-sdk] No component found for content type ${opti.__typename - } ${opti.__tag ? `with tag "${opti.__tag}"` : ''}` - ); - return ( No component found for content type {opti.__typename} @@ -129,16 +124,16 @@ export async function OptimizelyComponent({ ); } -export type StructureContainerProps = { +export type StructureContainerProps = { node: ExperienceStructureNode; children: React.ReactNode; index?: number; - displaySettings?: Partial>; + displaySettings?: Record; }; -export type ComponentContainerProps = { +export type ComponentContainerProps = { node: ExperienceComponentNode; children: React.ReactNode; - displaySettings?: Partial>; + displaySettings?: Record; }; export type StructureContainer = ( props: StructureContainerProps @@ -161,9 +156,8 @@ export function OptimizelyExperience({ : null; const parsedDisplaySettings = template - ? parseDisplaySettings(node.displaySettings, template.settings) - : {}; - console.error(parsedDisplaySettings); + ? parseDisplaySettings(node.displaySettings) + : undefined; if (isComponentNode(node)) { const Wrapper = ComponentWrapper ?? React.Fragment; @@ -269,8 +263,8 @@ export function OptimizelyGridSection({ : null; const parsedDisplaySettings = template - ? parseDisplaySettings(node.displaySettings, template.settings) - : {}; + ? parseDisplaySettings(node.displaySettings) + : undefined; if (isComponentNode(node)) { return (