Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/visual-editor/THIRD-PARTY-NOTICES
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

The following npm packages may be included in this product:

- @puckeditor/core@0.22.2
- @puckeditor/core@0.24.0-canary.c1ec9773
- next-themes@0.3.0

These packages each contain the following license:
Expand Down
2 changes: 1 addition & 1 deletion packages/visual-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
},
"dependencies": {
"@mapbox/maki": "^8.2.0",
"@puckeditor/core": "0.22.2",
"@puckeditor/core": "0.24.0-canary.c1ec9773",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.13",
Expand Down
95 changes: 45 additions & 50 deletions packages/visual-editor/src/components/helpers/ComprehensiveCTA.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { useDocument } from "../../hooks/useDocument.tsx";
import { resolveComprehensiveCTAValue } from "../../fields/fieldTransforms/index.ts";
import { type CTAProps, CTA } from "../atoms/cta.tsx";
import { themeManagerCn } from "../../utils/cn.ts";
import { useDocument } from "../../hooks/useDocument.tsx";
import { resolveComponentData } from "../../utils/resolveComponentData.tsx";
import { getCTAType } from "../../internal/utils/ctaFieldUtils.ts";
import {
type ComprehensiveCTAValue,
defaultButtonStyleValue,
defaultLinkStyleValue,
normalizeComprehensiveCTAValue,
} from "../../fields/styledFields/ComprehensiveCTAField.tsx";
import {
FOOD_DELIVERY_SERVICES,
type EnhancedTranslatableCTA,
} from "../../types/types.ts";
import { FOOD_DELIVERY_SERVICES } from "../../types/types.ts";
import type { ResolvedComprehensiveCTAValue } from "../../fields/fields.ts";
import { type StyledButtonValue } from "../../fields/styledFields/StyledButtonField.tsx";
import { type StyledLinkValue } from "../../fields/styledFields/StyledLinkField.tsx";

export type ComprehensiveCTARenderProps = {
value?: Partial<ComprehensiveCTAValue>;
value?: Partial<ComprehensiveCTAValue> | ResolvedComprehensiveCTAValue;
label?: React.ReactNode;
ariaLabel?: string;
className?: string;
Expand All @@ -34,12 +30,12 @@ const resolveTextStyleValue = (value: string | undefined) =>
value && value !== "default" ? value : undefined;

const getComprehensiveCTAStyle = (
value: ComprehensiveCTAValue
value: ResolvedComprehensiveCTAValue
): React.CSSProperties | undefined => {
const ctaType =
value.data.actionType === "button"
? "textAndLink"
: getCTAType(value.data.cta).ctaType;
: value.data.cta?.ctaType;

if (ctaType === "presetImage") {
return value.sx;
Expand Down Expand Up @@ -123,6 +119,18 @@ const toDataAttributes = (
);
};

/**
* Supports both authored props from fields without `transform: true` and resolved
Comment thread
jwartofsky-yext marked this conversation as resolved.
* props from fields with transforms enabled. Both paths retain styles,
* accessibility, and interaction behavior.
*
* 1. Temporarily resolve authored bindings and translated text through the same
* resolver used by field transforms, including defaults for partial values.
* 2. Use transformed values directly without resolving or interpolating them again.
* 3. Render the resolved data using the shared CTA presentation.
*
* Remove the authored-value compatibility path once all libraries use transforms.
*/
export const ComprehensiveCTA = ({
value,
label,
Expand All @@ -135,39 +143,29 @@ export const ComprehensiveCTA = ({
onClick,
}: ComprehensiveCTARenderProps) => {
const streamDocument = useDocument();
const { t, i18n } = useTranslation();
const locale = i18n.language;
const currentValue = normalizeComprehensiveCTAValue(value);

const { i18n } = useTranslation();
// Authored values retain field/constant bindings or localized text objects.
// Partial authored values also need defaults before they can be rendered.
const currentValue =
!value?.data ||
!value.styles ||
(value.data.cta &&
("field" in value.data.cta || "constantValue" in value.data.cta)) ||
typeof value.data.buttonText === "object" ||
typeof value.data.ariaLabel === "object"
? // Without transforms, resolve the authored value using the page and locale.
resolveComprehensiveCTAValue(value as Partial<ComprehensiveCTAValue>, {
streamDocument,
locale: i18n.language,
})
: // With transforms, the value already contains resolved data; keep it as is.
(value as ResolvedComprehensiveCTAValue);
const actionType = currentValue.data.actionType;
const { ctaType } =
actionType === "link"
? getCTAType(currentValue.data.cta)
: { ctaType: "textAndLink" as const };
const resolvedCta =
actionType === "link"
? (resolveComponentData(currentValue.data.cta, locale, streamDocument) as
EnhancedTranslatableCTA | undefined)
: undefined;

const resolvedButtonLabel = currentValue.data.buttonText
? resolveComponentData(currentValue.data.buttonText, locale, streamDocument)
: "";
const resolvedFieldAriaLabel = currentValue.data.ariaLabel
? resolveComponentData(currentValue.data.ariaLabel, locale, streamDocument)
: "";

let resolvedLinkLabel =
resolvedCta &&
resolveComponentData(resolvedCta.label, locale, streamDocument);

if (
actionType === "link" &&
!currentValue.data.cta.constantValueEnabled &&
ctaType === "getDirections"
) {
resolvedLinkLabel = t("getDirections", "Get Directions");
}
const ctaType =
actionType === "button" ? "textAndLink" : currentValue.data.cta?.ctaType;
const resolvedCta = currentValue.data.cta;
const resolvedButtonLabel = currentValue.data.buttonText;
const resolvedLinkLabel = resolvedCta?.label;

const effectiveLabel =
label !== undefined
Expand Down Expand Up @@ -211,7 +209,7 @@ export const ComprehensiveCTA = ({
const resolvedAriaLabel =
ariaLabel ??
(actionType === "button"
? resolvedFieldAriaLabel || undefined
? currentValue.data.ariaLabel || undefined
: typeof effectiveLabel === "string"
? effectiveLabel
: undefined);
Expand All @@ -223,7 +221,8 @@ export const ComprehensiveCTA = ({
alwaysHideCaret={alwaysHideCaret}
className={resolvedClassName}
color={currentValue.styles.color}
ctaType={ctaType}
// Directions are already resolved and use the ordinary link presentation.
ctaType={ctaType === "presetImage" ? "presetImage" : "textAndLink"}
dataAttributes={
actionType === "button"
? toDataAttributes(currentValue.data.dataAttributes)
Expand All @@ -232,11 +231,7 @@ export const ComprehensiveCTA = ({
eventName={eventName ?? currentValue.eventName}
id={actionType === "button" ? currentValue.data.customId : undefined}
label={effectiveLabel}
link={
actionType === "link" && ctaType !== "getDirections" && resolvedCta
? resolveComponentData(resolvedCta.link, locale, streamDocument)
: undefined
}
link={actionType === "link" && resolvedCta ? resolvedCta.link : undefined}
linkType={
actionType === "link" && resolvedCta ? resolvedCta.linkType : undefined
}
Expand Down
78 changes: 78 additions & 0 deletions packages/visual-editor/src/editor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,84 @@ const Example = ({ myField }: ExampleProps) => {
};
```

## Opt-in field transforms

Add `transform: true` to the props of a supported field type and the transform behavior will be enabled, making props come through
resolved in the render function. The following field types are supported for transforms and the behavior is disabled by default.

| Field definition | Render value |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `translatableString` | Localized, interpolated string |
| Ordinary `entityField` | Resolved entity or constant value with nested localization and interpolation |
| `entityField` including `type.rich_text_v2` | Resolved rich-text object or plain string |
| Rich-text list with `includeListsOnly: true` | Array of resolved rich-text objects or strings |
| `image` | Localized asset image with resolved alt text and preserved asset metadata |
| `multiSelector` | Array of selected values, preserving zero, false, and empty strings |
| `optionalNumber` | Number or `undefined` when hidden or unset |
| `ctaSelector` | Resolved CTA object with localized label and link; Get Directions includes a generated URL |
| `comprehensiveCTA` | Resolved nested CTA, button text, and aria label; styles and interaction settings are preserved |
| Repeated `entityField` | Array of item props resolved against each selected item or manual item |
| `video` | Asset video data with interpolated text and preserved metadata |

````

Title, description, and image still allow entity bindings and constant values.
The transform returns resolved data, including translated and interpolated rich-text
HTML and JSON. `MaybeRTF` renders that data, with optional `className`, `style`,
`bodyVariant`, and `richTextStyleOverrides`. CTAs keep their existing field and
renderer API.

Opted-in fields appear in the editor's component-level entity tooltip when
entity tooltips are enabled. The tooltip reads authored bindings and does not
add source metadata to rendered props or require `EntityField` wrappers. Fields
without `transform: true` retain their existing tooltip behavior.

### Adding field transforms

`fields/fieldTransforms/createYextFieldTransforms.ts` registers the transforms,
while `fields/fieldTransforms/fieldToTransform.ts` dispatches by authored field
type. Puck registration and adapted `custom` fields use that same map.
Each handler receives the authored definition, its value, and the page document
and locale. `resolveValue.ts` handles localization and interpolation across
objects and arrays while retaining rich-text and asset structures. `cta.ts`
contains CTA-specific resolution.

To support another field, add its `transform?: boolean` option, a handler in
`fieldToTransform`, and its output type in `TransformedFieldValues` in `fields.ts`.
The handler map requires an entry for each transformable field definition.
Keep field-specific source selection in its handler and return resolved data.
For composite bindings, also describe their authored sources in the component
tooltip. Defaults, editor fields, and saved data keep their authored shapes.

Ordinary entity lists, repeated item sources, and nested Puck arrays support
transforms. For repeated sources, opt in on the parent field; its mappings are
resolved against each selected linked item, including translations and embedded
fields. Manual items resolve against the page document. Missing lists return `[]`.

```tsx
const fields = {
cards: { ...cardSource.field, transform: true },
primaryCta: { type: "comprehensiveCTA", transform: true },
video: { type: "video", transform: true },
} satisfies YextFieldMap<AuthoredProps>;

// In the component's render function:
<ComprehensiveCTA value={primaryCta} />
<VideoAtom
youTubeEmbedUrl={video.video.embeddedUrl}
title={video.video.title}
/>
{cards.map((card, index) => (
<article key={index}>
<h2>{card.title}</h2>
<MaybeRTF data={card.description} />
</article>
))}
````

The library no longer calls `resolveItems` or performs text resolution in its
render function. Video asset structure and comprehensive CTA styles are retained.

## Linked Entity Item Sources

Use `createItemSource(...)` when a component needs to render repeated content
Expand Down
Loading
Loading