From 1ec2a78968e10efc5666aaf994b6feea6c820449 Mon Sep 17 00:00:00 2001 From: Chris Villa Date: Fri, 24 Nov 2023 18:37:36 +0000 Subject: [PATCH] feat: add transformProps lib to migrate component props --- apps/demo/config/index.tsx | 4 +- .../pages/docs/api-reference/functions.mdx | 1 + .../functions/transform-props.mdx | 50 ++++++++++++++ .../docs/integrating-puck/prop-migration.mdx | 68 ++++++++++++++++++ packages/core/components/Puck/index.tsx | 4 +- packages/core/index.ts | 1 + packages/core/transforms/index.ts | 69 +++++++++++++++---- 7 files changed, 180 insertions(+), 17 deletions(-) create mode 100644 apps/docs/pages/docs/api-reference/functions/transform-props.mdx create mode 100644 apps/docs/pages/docs/integrating-puck/prop-migration.mdx diff --git a/apps/demo/config/index.tsx b/apps/demo/config/index.tsx index 891e78406f..7a50ee91c6 100644 --- a/apps/demo/config/index.tsx +++ b/apps/demo/config/index.tsx @@ -12,7 +12,9 @@ import { VerticalSpace, VerticalSpaceProps } from "./blocks/VerticalSpace"; import Root, { RootProps } from "./root"; -type Props = { +export type { RootProps } from "./root"; + +export type Props = { ButtonGroup: ButtonGroupProps; Card: CardProps; Columns: ColumnsProps; diff --git a/apps/docs/pages/docs/api-reference/functions.mdx b/apps/docs/pages/docs/api-reference/functions.mdx index 9e17731b7a..41a56d4eef 100644 --- a/apps/docs/pages/docs/api-reference/functions.mdx +++ b/apps/docs/pages/docs/api-reference/functions.mdx @@ -1,3 +1,4 @@ # Functions - [resolveAllData](functions/resolve-all-data) - Utility function to execute all [`resolveData` methods](/docs/api-reference/configuration/component-config#resolvedatadata-params) on a data payload. +- [transformProps](functions/transform-props) - Transform component props stored in the [data payload](/docs/api-reference/data). Use this for migrations, like prop renames. diff --git a/apps/docs/pages/docs/api-reference/functions/transform-props.mdx b/apps/docs/pages/docs/api-reference/functions/transform-props.mdx new file mode 100644 index 0000000000..7b927dd6cb --- /dev/null +++ b/apps/docs/pages/docs/api-reference/functions/transform-props.mdx @@ -0,0 +1,50 @@ +--- +title: transformProps +--- + +# transformProps + +Transform component props stored in a [Data payload](/docs/api-reference/data). This convenience method can be used for [prop renames and other data migrations](/docs/integrating-puck/prop-migration). + +This method will modify all data in [`content`](/docs/api-reference/data#content) and [`zones`](/docs/api-reference/data#zones). + +```tsx copy showLineNumbers {7-10} +import { transformProps } from "@measured/puck"; + +const data = { + content: [{ type: "HeadingBlock", props: { title: "Hello, world" } }], +}; + +const updatedData = transformProps(data, { + // Rename `title` to `heading` + HeadingBlock: ({ title, ...props }) => ({ heading: title, ...props }), +}); + +console.log(updatedData); +// { content: [{ type: "HeadingBlock", props: { heading: "Hello, world" } }] }; +``` + +## Args + +| Param | Example | Type | +| ------------ | -------------------------------------- | -------------------------------- | +| `data` | `{ content: {}, root: {} }` | [Data](/docs/api-reference/data) | +| `transforms` | `{ HeadingBlock: (props) => (props) }` | Object | + +### `data` + +The [Data payload](/docs/api-reference/data) to be transformed. + +### `transforms` + +An object describing the transform functions for each component defined in your [`config`](/docs/api-reference/configuration/config). + +- `root` is a reserved property, and can be used to update the [`root` component](/docs/api-reference/configuration/config#root) props. + +## Returns + +The updated [Data](/docs/api-reference/data) object. + +## Notes + +- It's important to consider that data may include both components with old data and new data, and write your transform accordingly. diff --git a/apps/docs/pages/docs/integrating-puck/prop-migration.mdx b/apps/docs/pages/docs/integrating-puck/prop-migration.mdx new file mode 100644 index 0000000000..d548775987 --- /dev/null +++ b/apps/docs/pages/docs/integrating-puck/prop-migration.mdx @@ -0,0 +1,68 @@ +# Prop Migration + +Renaming or removing the props passed to your components are considering breaking changes. Any existing [Data](/docs/api-reference/data) payloads that reference these props will be unable to render. + +There are two strategies for dealing with this: + +1. Retaining backwards-compatible props +2. Implementing a prop migration + +## Retaining backwards-compatibility + +The easiest way to avoid breaking changes is to implement your prop changes in a backwards compatible manor: + +```tsx copy showLineNumbers {2} +const config = { + HeadingBlock: ({ title, heading }) =>

{heading || title}

, +}; +``` + +## Implementing a prop migration + +It will often be preferrable to update the underlying [Data](/docs/api-reference/data) payload. Puck provides the [`transformProps`](/docs/api-reference/functions/transform-props) utility method to conveniently transform the props for a given component throughout the payload. + +```tsx copy showLineNumbers {15-18} +import { transformProps } from "@measured/puck"; + +const config = { + // Renamed `title` prop to `heading` + HeadingBlock: ({ heading }) =>

{heading}

, +}; + +const data = { + content: [ + // HeadingBlock references the legacy `title` prop + { type: "HeadingBlock", props: { title: "Hello, world" } }, + ], +}; + +const updatedData = transformProps(data, { + // Map `heading` to the legacy `title` prop + HeadingBlock: ({ title, ...props }) => ({ heading: title, ...props }), +}); + +console.log(updatedData); +// { content: [{ type: "HeadingBlock", props: { heading: "Hello, world" } }] }; +``` + +You may choose to run this transform every time you render your content, or perform a batch operation against your database. + +```tsx copy showLineNumbers filename="Example showing data being updated before rendering" +import { Puck, Render, transformProps } from "@measured/puck"; + +const transforms = { + HeadingBlock: ({ title, ...props }) => ({ heading: title, ...props }), +}; + +export const MyEditor = ({ data, config }) => ( + +); + +export const MyPage = ({ data, config }) => ( + +); +``` + +## Further reading + +- [`transformProps` API reference](/docs/api-reference/functions/transform-props) diff --git a/packages/core/components/Puck/index.tsx b/packages/core/components/Puck/index.tsx index e1cce9ea77..cc87fa6dd2 100644 --- a/packages/core/components/Puck/index.tsx +++ b/packages/core/components/Puck/index.tsx @@ -48,7 +48,7 @@ import { useComponentList } from "../../lib/use-component-list"; import { useResolvedData } from "../../lib/use-resolved-data"; import { MenuBar } from "../MenuBar"; import styles from "./styles.module.css"; -import { runTransforms } from "../../transforms"; +import { transformData } from "../../transforms"; const getClassName = getClassNameFactory("Puck", styles); @@ -126,7 +126,7 @@ export function Puck({ const [initialAppState] = useState(() => ({ ...defaultAppState, - data: runTransforms(initialData as CurrentData, []), + data: transformData(initialData), ui: { ...defaultAppState.ui, // Store categories under componentList on state to allow render functions and plugins to modify diff --git a/packages/core/index.ts b/packages/core/index.ts index 96fbc8ef5e..5e15e92ab6 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -11,5 +11,6 @@ export * from "./components/Puck"; export * from "./components/Render"; export * from "./lib/resolve-all-data"; +export { transformProps } from "./transforms"; export { FieldLabel } from "./components/InputOrGroup"; diff --git a/packages/core/transforms/index.ts b/packages/core/transforms/index.ts index 9519111cda..da547ef303 100644 --- a/packages/core/transforms/index.ts +++ b/packages/core/transforms/index.ts @@ -1,24 +1,65 @@ -import { CurrentData, LegacyData } from "../types/Config"; +import { + CurrentData, + Data, + DefaultComponentProps, + LegacyData, +} from "../types/Config"; import { dataTransforms } from "./data-transforms"; export type DataTransform = ( props: LegacyData & { [key: string]: any } ) => CurrentData; -type PropTransform = { - [ComponentName in keyof Components]: ( - props: Components[ComponentName] & { [key: string]: any } - ) => Components[ComponentName]; -}; - -export const runTransforms = ( - data: LegacyData, - propTransforms: PropTransform[] -): CurrentData => { - const afterDataTransforms = dataTransforms.reduce( +// type TransformFn = +type PropTransform< + Props extends DefaultComponentProps = DefaultComponentProps, + RootProps extends DefaultComponentProps = DefaultComponentProps +> = Partial< + { + [ComponentName in keyof Props]: ( + props: Props[ComponentName] & { [key: string]: any } + ) => Props[ComponentName]; + } & { root: (props: RootProps & { [key: string]: any }) => RootProps } +>; + +export function transformData(data: Data): CurrentData { + return dataTransforms?.reduce( (acc, dataTransform) => dataTransform(acc), data ) as CurrentData; +} + +export function transformProps< + Props extends DefaultComponentProps = DefaultComponentProps, + RootProps extends DefaultComponentProps = DefaultComponentProps +>(data: Data, propTransforms: PropTransform): CurrentData { + const afterDataTransform = transformData(data); + + const mapItem = (item) => { + if (propTransforms[item.type]) { + return { + ...item, + props: propTransforms[item.type]!(item.props as any), + }; + } + + return item; + }; + + const afterPropTransforms: CurrentData = { + ...data, + root: propTransforms["root"] + ? propTransforms["root"](afterDataTransform.root.props as any) + : afterDataTransform.root, + content: data.content.map(mapItem), + zones: Object.keys(data.zones || {}).reduce( + (acc, zoneKey) => ({ + ...acc, + [zoneKey]: data.zones![zoneKey].map(mapItem), + }), + {} + ), + }; - return afterDataTransforms; -}; + return afterPropTransforms; +}