diff --git a/examples/block-extension/src/blocks/HeroBlock/index.tsx b/examples/block-extension/src/blocks/HeroBlock/index.tsx index cfc4ccf..1301e64 100644 --- a/examples/block-extension/src/blocks/HeroBlock/index.tsx +++ b/examples/block-extension/src/blocks/HeroBlock/index.tsx @@ -1,11 +1,17 @@ -import { defineBlock, useBlockState } from '@instantcommerce/sdk'; +import { + defineBlock, + InferBlockState, + useBlockState, +} from '@instantcommerce/sdk'; import './index.css'; const Hero = () => { - const { content } = useBlockState(); + const { content, customizer } = + useBlockState>(); return (
+ {content.cards![0].value}

{content.title}

); @@ -16,12 +22,116 @@ const HeroBlock = defineBlock({ customizerSchema: { fields: { color: { type: 'text' }, + number: { type: 'number', min: 2, max: 80, fractionDigits: 3 }, + padding: { + type: 'select', + options: [ + { label: 'Small', value: 'sm' }, + { label: 'Large', value: 'lg' }, + ], + preview: 'sm', + }, + textColor: { type: 'color', preview: '#A020F0' }, + text: { type: 'text', maxLength: 1, isRequired: true }, + toggle: { type: 'toggle', preview: true }, }, - }, + } as const, contentSchema: { fields: { title: { type: 'text', label: 'Title', preview: 'Hero title' }, - }, + select: { + type: 'select', + preview: 'value1', + options: [ + { value: 'value1', label: 'Option 1' }, + { value: 'value2', label: 'Option 2' }, + ], + }, + date: { + type: 'date', + preview: '2022-12-12 08:12', + withTime: true, + }, + image: { + type: 'image', + preview: + 'https://images.unsplash.com/photo-1669962367460-00b711b2e3f3?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=700&q=80', + }, + link: { + type: 'link', + preview: 'https://google.com', + }, + richText: { + type: 'richText', + preview: 'Rich text', + toolbar: [], + }, + cards: { + type: 'subschema', + allowed: ['card'], + max: 3, + preview: [ + { + subschema: 'card', + value: { + cardTitle: 'Card title', + cardImage: + 'https://a.storyblok.com/f/145828/5000x3333/564e281ca1/force-majeure-du8abwm5z2g-unsplash.jpg', + cardButtons: [ + { + subschema: 'button', + value: { + text: 'Button', + link: 'https://a.storyblok.com/f/145828/5000x3333/564e281ca1/force-majeure-du8abwm5z2g-unsplash.jpg', + }, + }, + ], + }, + }, + ], + }, + } as const, + subschemas: { + button: { + fields: { + text: { + type: 'text', + label: 'Text', + isTranslatable: true, + isRequired: true, + maxLength: 40, + }, + link: { + type: 'link', + label: 'Link', + isTranslatable: true, + isRequired: true, + }, + }, + }, + card: { + fields: { + cardImage: { + type: 'image', + label: 'Image', + isRequired: true, + }, + cardTitle: { + type: 'text', + label: 'Title', + isTranslatable: true, + isRequired: true, + }, + cardButtons: { + type: 'subschema', + label: 'Buttons', + isRequired: false, + max: 2, + allowed: ['button'], + }, + }, + }, + } as const, }, }); diff --git a/packages/client/package.json b/packages/client/package.json index 520e19b..7a126c9 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -11,6 +11,9 @@ ".": { "import": "./dist/index.js", "require": "./dist/index.cjs" + }, + "./src": { + "import": "./src/index.ts" } }, "main": "./dist/index.cjs", @@ -35,6 +38,7 @@ "react-dom": "18.2.0", "storyblok-rich-text-react-renderer": "2.6.1", "tsconfig": "*", + "ts-toolbelt": "9.6.0", "types": "*" }, "devDependencies": { @@ -42,6 +46,6 @@ "@types/react-dom": "18.0.1", "@vitejs/plugin-react": "2.0.0", "tsup": "6.5.0", - "typescript": "4.3.5" + "typescript": "4.9.3" } } diff --git a/packages/client/src/defineBlock.ts b/packages/client/src/defineBlock.ts index 6091329..277a836 100644 --- a/packages/client/src/defineBlock.ts +++ b/packages/client/src/defineBlock.ts @@ -1,5 +1,6 @@ import { createElement, ReactElement, ReactNode } from 'react'; import { render as remoteRender, RemoteRoot } from '@remote-ui/react'; +import { Narrow } from 'ts-toolbelt/out/Function/Narrow'; /** This relative import forces types from this package to be included in this bundle */ import { DefineContentSchema, @@ -24,7 +25,7 @@ function render( (self as any).render(callback, contentSchema, customizerSchema); } -interface DefineBlockParams { +type DefineBlockParams = { /** React component rendered by the block */ component: () => RenderElement | ReactElement; preview?: { @@ -35,14 +36,16 @@ interface DefineBlockParams { customizerSchema?: DefineCustomizerSchema; /** The content schema that is synced to the CMS */ contentSchema?: DefineContentSchema; -} +}; + +export const defineBlock = (params: Narrow) => { + const { + component, + preview: { decorators } = {}, + customizerSchema, + contentSchema, + } = params as DefineBlockParams; -export const defineBlock = ({ - component, - preview: { decorators } = {}, - customizerSchema, - contentSchema, -}: DefineBlockParams) => { render( (root, blockProps) => { const renderedComponent = @@ -64,4 +67,147 @@ export const defineBlock = ({ contentSchema, customizerSchema, ); + + return params; +}; + +export interface AssetStoryblok { + alt?: string; + copyright?: string; + id: number; + filename: string; + name: string; + title?: string; + focus?: string; + [k: string]: any; +} + +export type LinkStoryblok = + | { + cached_url?: string; + linktype?: string; + [k: string]: any; + } + | { + id?: string; + cached_url?: string; + linktype?: 'story'; + [k: string]: any; + } + | { + url?: string; + cached_url?: string; + linktype?: 'asset' | 'url'; + [k: string]: any; + } + | { + email?: string; + linktype?: 'email'; + [k: string]: any; + }; + +export interface RichTextStoryblok { + content?: RichTextStoryblok[]; + marks?: RichTextStoryblok[]; + attrs?: any; + text?: string; + type: string; +} + +type ContentFieldValues< + C extends DefineContentSchema< + C['subschemas'] extends { subschemas: any } ? C['subschemas'] : {} + >, + T extends C['fields'][keyof C['fields']], + S extends C['subschemas'], +> = T extends { + type: 'image'; +} + ? AssetStoryblok + : T extends { + type: 'link'; + } + ? LinkStoryblok + : T extends { + type: 'richText'; + } + ? RichTextStoryblok + : T extends { + type: 'select'; + } + ? T['options'][number]['value'] + : T extends { + type: 'subschema'; + } + ? ContentSubschema + : string; + +type ContentSubschema< + C extends DefineContentSchema< + C['subschemas'] extends { subschemas: any } ? C['subschemas'] : {} + >, + T extends C['fields'][keyof C['fields']], + S extends C['subschemas'], + A extends keyof S = keyof S, +> = { + subschema: A; + value: ContentSubschemaValues; +}[]; + +type ContentSubschemaValues< + C extends DefineContentSchema< + C['subschemas'] extends { subschemas: any } ? C['subschemas'] : {} + >, + T extends C['fields'][keyof C['fields']], + S extends C['subschemas'], + A extends keyof S, + B extends keyof S[A] = keyof S[A], + D = S[A][B], +> = { + [K in keyof D]: D[K]; // ContentFieldValues; +}; + +type ContentFields< + T extends DefineContentSchema< + T['subschemas'] extends { subschemas: any } ? T['subschemas'] : {} + >, +> = { + [name in keyof T['fields']]: T['fields'][name] extends { + isRequired: true; + } + ? ContentFieldValues + : ContentFieldValues | undefined; }; + +type CustomizerFieldValues< + T extends DefineCustomizerSchema['fields'][keyof DefineCustomizerSchema['fields']], +> = T extends { + type: 'number'; +} + ? number + : T extends { + type: 'toggle'; + } + ? boolean + : T extends { + type: 'select'; + } + ? T['options'][number]['value'] + : string; + +type CustomizerFields = { + [name in keyof T]: T[name] extends { + isRequired: true; + } + ? CustomizerFieldValues + : CustomizerFieldValues | undefined; +}; + +export interface InferBlockState { + content: T['contentSchema'] extends { fields: any } + ? ContentFields + : {}; + customizer: T['customizerSchema'] extends { fields: any } + ? CustomizerFields + : {}; +} diff --git a/packages/client/src/hooks/useBlockState.ts b/packages/client/src/hooks/useBlockState.ts index 9126c02..12de863 100644 --- a/packages/client/src/hooks/useBlockState.ts +++ b/packages/client/src/hooks/useBlockState.ts @@ -1,7 +1,15 @@ import { useBlockContext } from '../BlockProvider'; -export function useBlockState() { +export function useBlockState< + T extends { content: any; customizer: any } = { + content: any; + customizer: any; + }, +>() { const { content, customizer } = useBlockContext(); - return { content, customizer }; + return { content, customizer } as { + content: T['content']; + customizer: T['customizer']; + }; } diff --git a/packages/types/schemas.ts b/packages/types/schemas.ts index 92c2598..a355533 100644 --- a/packages/types/schemas.ts +++ b/packages/types/schemas.ts @@ -13,6 +13,7 @@ import { CustomizerSchemaNumberField, CustomizerSchemaColorField, ContentSchemaSubschemaField, + SelectOption, } from './api'; type EnhancedContentSchemaField< @@ -39,17 +40,25 @@ type ContentLinkField = EnhancedContentSchemaField< >; type ContentRichTextField = EnhancedContentSchemaField< - ContentSchemaRichTextField, + Omit & { + toolbar: Array | ReadonlyArray; + }, 'richText' >; type ContentSelectField = EnhancedContentSchemaField< - ContentSchemaSelectField, + Omit & { + options: Array | ReadonlyArray; + }, 'select' >; -type ContentSubschemaField = EnhancedContentSchemaField< - ContentSchemaSubschemaField, +type ContentSubschemaField< + Subschemas extends Record, +> = EnhancedContentSchemaField< + Omit & { + allowed: Array | ReadonlyArray; + }, 'subschema' >; @@ -58,12 +67,14 @@ type ContentTextField = EnhancedContentSchemaField< 'text' >; -export type ContentSchemaInputField = +export type ContentSchemaInputField< + Subschemas extends Record = {}, +> = | ContentDateField | ContentImageField | ContentRichTextField | ContentSelectField - | ContentSubschemaField + | ContentSubschemaField | ContentTextField | ContentLinkField; @@ -71,9 +82,11 @@ type ContentSubschema = Pick & { fields: Record; }; -export interface DefineContentSchema { - fields: Record; - subschemas?: Record; +export interface DefineContentSchema< + Subschemas extends Record = {}, +> { + fields: Record>; + subschemas?: Subschemas; } type EnhancedCustomizerSchemaField< @@ -97,7 +110,9 @@ type CustomizerNumberField = EnhancedCustomizerSchemaField< >; type CustomizerSelectField = EnhancedCustomizerSchemaField< - CustomizerSchemaSelectField, + Omit & { + options: Array | ReadonlyArray; + }, 'select' >; diff --git a/yarn.lock b/yarn.lock index 9082329..93b61ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12207,6 +12207,11 @@ ts-node@^10.8.1: v8-compile-cache-lib "^3.0.1" yn "3.1.1" +ts-toolbelt@9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz#50a25426cfed500d4a09bd1b3afb6f28879edfd5" + integrity sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w== + tsconfig-paths@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" @@ -12433,11 +12438,6 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typescript@4.3.5: - version "4.3.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" - integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== - typescript@4.6.4: version "4.6.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" @@ -12448,7 +12448,7 @@ typescript@4.8.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== -typescript@^4.2.4, typescript@^4.6.4: +typescript@4.9.3, typescript@^4.2.4, typescript@^4.6.4: version "4.9.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.3.tgz#3aea307c1746b8c384435d8ac36b8a2e580d85db" integrity sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==