Skip to content

refactor: add image fieldType#1184

Merged
briantstephan merged 31 commits intomainfrom
image-field-type
Apr 30, 2026
Merged

refactor: add image fieldType#1184
briantstephan merged 31 commits intomainfrom
image-field-type

Conversation

@briantstephan
Copy link
Copy Markdown
Contributor

@briantstephan briantstephan commented Apr 28, 2026

This adds a new ImageField type rather than relying on the YextField helper.

Tested on the following test site and in the fake starter and confirmed that all images work as expected.
https://dev.yext.com/s/1000167375/yextsites/67121/pagesets

@briantstephan briantstephan self-assigned this Apr 28, 2026
@briantstephan briantstephan added the create-dev-release Triggers dev release workflow label Apr 28, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Warning: Component files have been updated but no migrations have been added. See https://github.com/yext/visual-editor/blob/main/packages/visual-editor/src/components/migrations/README.md for more information.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 28, 2026

commit: f58fff7

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR extracts image handling from the generic YextField flow into a dedicated ImageField type and ImageFieldOverride; implements a new ImageField (with payload/type, locator display option builder, alt-text integration, and UI for Choose/Change/Delete); registers image on YextPuckFields/YextPuckFieldOverrides; replaces the prior custom constant-field renderer with minimal image config objects; updates selector/typing to use YextFieldDefinition; and adds editor docs and tests for the image field and alt-text options.

Sequence Diagram(s)

sequenceDiagram
  participant EditorUI as Visual Editor UI
  participant ImageField as ImageFieldOverride
  participant Parent as Host Window
  participant Template as TemplateMetadata

  EditorUI->>ImageField: render(fieldConfig, locale, templateMetadata)
  ImageField->>Template: buildLocatorDisplayOptions() / getAltTextOptions()
  ImageField-->>EditorUI: render Choose/Change/Delete UI + TranslatableStringField
  EditorUI->>ImageField: user clicks "Change"
  ImageField->>Parent: postMessage(constantValueEditorOpened, msgId, payload)
  Parent-->>ImageField: postMessage(constantValueEditorClosed, msgId, selectedImage)
  ImageField->>ImageField: match msgId -> apply pending session -> build ImagePayload
  ImageField-->>EditorUI: call onChange(updated localized ImagePayload)
  EditorUI->>ImageField: user edits alt text
  ImageField-->>EditorUI: update alternateText for current locale and set hasLocalizedValue
Loading

Possibly related PRs

Suggested reviewers

  • asanehisa
  • mkilpatrick
  • benlife5
  • jwartofsky-yext
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: introducing a new ImageField type as a refactoring effort, which aligns with the core modifications across multiple files.
Description check ✅ Passed The description is directly related to the changeset, explaining that a new ImageField type is being added instead of relying on YextField, and includes testing confirmation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch image-field-type

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/visual-editor/src/fields/ImageField.tsx (1)

192-208: Memoize altTextOptions to avoid unnecessary re-computation.

field.getAltTextOptions?.(templateMetadata) is called on every render and buildLocatorDisplayOptions returns a new array reference each time. This causes the altTextField useMemo to re-run on every render, defeating its purpose.

♻️ Proposed fix
+  const altTextOptions = React.useMemo(
+    () => field.getAltTextOptions?.(templateMetadata),
+    [field, templateMetadata]
+  );
-  const altTextOptions = field.getAltTextOptions?.(templateMetadata);
   const altTextField = React.useMemo(() => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/visual-editor/src/fields/ImageField.tsx` around lines 192 - 208,
Compute and memoize altTextOptions before creating altTextField so it doesn't
re-run when buildLocatorDisplayOptions returns a new array; replace the direct
call to field.getAltTextOptions?.(templateMetadata) with a React.useMemo that
returns field.getAltTextOptions?.(templateMetadata) and has
[field.getAltTextOptions, templateMetadata] as dependencies, then use that
memoized altTextOptions in the existing altTextField useMemo (which can keep
[altTextOptions] deps).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/visual-editor/src/editor/README.md`:
- Around line 218-223: The props table incorrectly shows getAltTextOptions as
required; update the README table so the prop name is marked optional (e.g.,
change `getAltTextOptions` to `getAltTextOptions?`) and ensure its
type/signature `(templateMetadata) => DynamicOption<string>[]` and the
"Optional" wording remain; edit the Visual Editor props table entry referencing
getAltTextOptions to match the actual API shape.

In `@packages/visual-editor/src/fields/ImageField.tsx`:
- Around line 271-288: The alt text input (AutoField using altTextField) is
rendered even when resolvedValue is undefined, causing onChange to set [locale]:
undefined and drop the entered text; update the JSX to only render the
FieldLabel/AutoField block when resolvedValue exists (i.e., guard with if
(resolvedValue) or conditional &&) so that edits to alternateText only occur
when there is an image, and ensure onChange only writes a TranslatableAssetImage
into localizedContainer[locale] (via updatedImage) when resolvedValue is
present; reference FieldLabel, AutoField, altTextField, resolvedValue, onChange,
localizedContainer, TranslatableAssetImage, and locale to locate the code to
change.

---

Nitpick comments:
In `@packages/visual-editor/src/fields/ImageField.tsx`:
- Around line 192-208: Compute and memoize altTextOptions before creating
altTextField so it doesn't re-run when buildLocatorDisplayOptions returns a new
array; replace the direct call to field.getAltTextOptions?.(templateMetadata)
with a React.useMemo that returns field.getAltTextOptions?.(templateMetadata)
and has [field.getAltTextOptions, templateMetadata] as dependencies, then use
that memoized altTextOptions in the existing altTextField useMemo (which can
keep [altTextOptions] deps).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ecebb98c-2f8a-49e3-8c17-4a175af35143

📥 Commits

Reviewing files that changed from the base of the PR and between 67fd06c and a8145f2.

📒 Files selected for processing (11)
  • packages/visual-editor/src/components/footer/FooterUtilityImagesSlot.tsx
  • packages/visual-editor/src/editor/README.md
  • packages/visual-editor/src/editor/YextEntityFieldSelector.tsx
  • packages/visual-editor/src/editor/YextField.test.tsx
  • packages/visual-editor/src/editor/YextField.tsx
  • packages/visual-editor/src/fields/ImageField.test.tsx
  • packages/visual-editor/src/fields/ImageField.tsx
  • packages/visual-editor/src/fields/fields.ts
  • packages/visual-editor/src/fields/index.ts
  • packages/visual-editor/src/internal/puck/constant-value-fields/Image.tsx
  • packages/visual-editor/src/internal/puck/constant-value-fields/ImageList.tsx
💤 Files with no reviewable changes (1)
  • packages/visual-editor/src/editor/YextField.test.tsx

Comment thread packages/visual-editor/src/editor/README.md Outdated
Comment thread packages/visual-editor/src/fields/ImageField.tsx Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
packages/visual-editor/src/editor/YextField.tsx (1)

127-130: Keep the image exclusion aligned with the wider type surface.

This only removes image from the YextPuckFields[...] fallback branch. YextPuckField is still derived from keyof YextPuckFields, so image can still leak through the broader Field<..., YextPuckField>/YextFieldDefinition paths. If ImageField is now the sole supported path, tighten the shared alias too.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/visual-editor/src/editor/YextField.tsx` around lines 127 - 130, The
YextPuckField alias currently excludes "image" only in the fallback branch,
allowing "image" to still appear via the broader keyof YextPuckFields-derived
types (affecting Field<... , YextPuckField> and YextFieldDefinition); update the
shared YextPuckField type alias itself to exclude "image" (and keep the same
exclusions: "basicSelector" | "optionalNumber" | "video") so that keyof
YextPuckFields never includes "image", and ensure consumers that should accept
images use the explicit ImageField type instead (adjust references to
YextPuckField, Field, and YextFieldDefinition accordingly).
packages/visual-editor/src/fields/ImageField.tsx (1)

43-45: Scope pending image session state to the component instance.

pendingImageSession at module scope couples all ImageFieldOverride instances. Moving it to a useRef inside the component reduces cross-instance interference and keeps lifecycle ownership local.

♻️ Proposed refactor
-let pendingImageSession:
-  | { messageId: string; apply: (payload: ImagePayload) => void }
-  | undefined;
-
 export type ImageField = BaseField & {
@@
 export const ImageFieldOverride = ({
   field,
   onChange,
   value,
 }: ImageFieldOverrideProps) => {
@@
   const locale = i18n.language;
+  const pendingImageSessionRef = React.useRef<
+    { messageId: string; apply: (payload: ImagePayload) => void } | undefined
+  >(undefined);
@@
   useReceiveMessage(
     "constantValueEditorClosed",
     TARGET_ORIGINS,
     (_, payload) => {
       const imagePayload = payload as ImagePayload;
-      if (pendingImageSession?.messageId === imagePayload?.id) {
-        const { apply } = pendingImageSession;
-        pendingImageSession = undefined;
+      if (pendingImageSessionRef.current?.messageId === imagePayload?.id) {
+        const { apply } = pendingImageSessionRef.current;
+        pendingImageSessionRef.current = undefined;
         apply(imagePayload);
       }
     }
   );
@@
-      pendingImageSession = undefined;
+      pendingImageSessionRef.current = undefined;
       return;
     }
@@
-    pendingImageSession = {
+    pendingImageSessionRef.current = {
       messageId,
       apply: (imagePayload) => {
         const imageData =

Also applies to: 105-108, 139-166

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/visual-editor/src/fields/ImageField.tsx` around lines 43 - 45, The
module-scoped variable pendingImageSession is shared across all
ImageFieldOverride instances and should be moved into the component instance;
change pendingImageSession to a React ref (e.g., const pendingImageSessionRef =
useRef<{ messageId: string; apply: (payload: ImagePayload) => void } |
undefined>(undefined)) inside the ImageFieldOverride component, update all
reads/writes to use pendingImageSessionRef.current, ensure you clear it on
unmount or after apply, and apply the same ref-based scoping for the other
related usages referenced in this diff (the apply logic and messageId checks
within ImageFieldOverride and any handlers that currently reference the
module-scope variable).
packages/visual-editor/src/components/pageSections/heroVariants/CompactHero.tsx (1)

47-50: Consider extracting shared image-order class logic.

This conditional class block now matches ClassicHero and is a good candidate for a shared helper to prevent drift across hero variants.

♻️ Suggested direction
+// e.g. packages/visual-editor/src/components/pageSections/heroVariants/imageOrder.ts
+export const getHeroImageOrderClasses = (
+  mobileImagePosition: "top" | "bottom",
+  desktopImagePosition: "left" | "right",
+) =>
+  themeManagerCn(
+    mobileImagePosition === "top" ? "order-first" : "order-last",
+    desktopImagePosition === "left" ? "sm:order-first" : "sm:order-last",
+  );

 // in CompactHero.tsx / ClassicHero.tsx
- className={themeManagerCn(
-   styles.mobileImagePosition === "top" ? "order-first" : "order-last",
-   styles.desktopImagePosition === "left" ? "sm:order-first" : "sm:order-last"
- )}
+ className={getHeroImageOrderClasses(
+   styles.mobileImagePosition,
+   styles.desktopImagePosition,
+ )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/visual-editor/src/components/pageSections/heroVariants/CompactHero.tsx`
around lines 47 - 50, Extract the repeated image-order class logic from
CompactHero.tsx (the conditional using styles.mobileImagePosition and
styles.desktopImagePosition) into a shared helper function (e.g.,
getImageOrderClasses or computeImageOrderClass) located in a common
utils/helpers module used by hero variants, update CompactHero and ClassicHero
to call that helper (replace the inline ternary expressions with the helper
call), and add a small unit test or type check to ensure the helper returns the
expected class string for "top"/"left" and fallback values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@packages/visual-editor/src/components/pageSections/heroVariants/CompactHero.tsx`:
- Around line 47-50: Extract the repeated image-order class logic from
CompactHero.tsx (the conditional using styles.mobileImagePosition and
styles.desktopImagePosition) into a shared helper function (e.g.,
getImageOrderClasses or computeImageOrderClass) located in a common
utils/helpers module used by hero variants, update CompactHero and ClassicHero
to call that helper (replace the inline ternary expressions with the helper
call), and add a small unit test or type check to ensure the helper returns the
expected class string for "top"/"left" and fallback values.

In `@packages/visual-editor/src/editor/YextField.tsx`:
- Around line 127-130: The YextPuckField alias currently excludes "image" only
in the fallback branch, allowing "image" to still appear via the broader keyof
YextPuckFields-derived types (affecting Field<... , YextPuckField> and
YextFieldDefinition); update the shared YextPuckField type alias itself to
exclude "image" (and keep the same exclusions: "basicSelector" |
"optionalNumber" | "video") so that keyof YextPuckFields never includes "image",
and ensure consumers that should accept images use the explicit ImageField type
instead (adjust references to YextPuckField, Field, and YextFieldDefinition
accordingly).

In `@packages/visual-editor/src/fields/ImageField.tsx`:
- Around line 43-45: The module-scoped variable pendingImageSession is shared
across all ImageFieldOverride instances and should be moved into the component
instance; change pendingImageSession to a React ref (e.g., const
pendingImageSessionRef = useRef<{ messageId: string; apply: (payload:
ImagePayload) => void } | undefined>(undefined)) inside the ImageFieldOverride
component, update all reads/writes to use pendingImageSessionRef.current, ensure
you clear it on unmount or after apply, and apply the same ref-based scoping for
the other related usages referenced in this diff (the apply logic and messageId
checks within ImageFieldOverride and any handlers that currently reference the
module-scope variable).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7228b754-a058-486e-b43a-2d3ae3074497

📥 Commits

Reviewing files that changed from the base of the PR and between a8145f2 and 00186cd.

📒 Files selected for processing (8)
  • packages/visual-editor/src/components/pageSections/heroVariants/ClassicHero.tsx
  • packages/visual-editor/src/components/pageSections/heroVariants/CompactHero.tsx
  • packages/visual-editor/src/editor/README.md
  • packages/visual-editor/src/editor/YextField.test.tsx
  • packages/visual-editor/src/editor/YextField.tsx
  • packages/visual-editor/src/fields/ImageField.tsx
  • packages/visual-editor/src/fields/fields.ts
  • packages/visual-editor/src/fields/index.ts
💤 Files with no reviewable changes (1)
  • packages/visual-editor/src/editor/YextField.test.tsx
✅ Files skipped from review due to trivial changes (2)
  • packages/visual-editor/src/editor/README.md
  • packages/visual-editor/src/fields/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/visual-editor/src/fields/fields.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/visual-editor/src/components/contentBlocks/image/HeroImage.tsx`:
- Line 52: The code in the HeroImage component accesses puck.dragRef without
ensuring puck exists; update both occurrences where dragRef is passed (currently
written as dragRef={puck.dragRef ?? undefined} and the other at line 64) to
guard puck first — e.g., use optional chaining on puck (puck?.dragRef) or
conditionally pass undefined when puck is falsy so that the component never
dereferences puck when it's undefined; look for the HeroImage component and the
props/variables named puck and dragRef to apply this fix consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 900ddafe-93af-4b9f-844e-9a088aedcfb3

📥 Commits

Reviewing files that changed from the base of the PR and between 00186cd and 31b05fc.

📒 Files selected for processing (1)
  • packages/visual-editor/src/components/contentBlocks/image/HeroImage.tsx

Comment thread packages/visual-editor/src/components/contentBlocks/image/HeroImage.tsx Outdated
@briantstephan briantstephan marked this pull request as ready for review April 30, 2026 14:06
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/visual-editor/src/fields/ImageField.tsx`:
- Around line 233-261: The three Button elements in ImageField (the buttons
rendered when resolvedValue?.url is truthy and the "Choose Image" fallback) are
missing an explicit type attribute and therefore default to type="submit";
update those Button usages to include type="button" (the two inside the image
overlay that call handleSelectImage and handleDeleteImage, and the fallback
Button that calls handleSelectImage) so clicks won't accidentally submit a
parent form.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: bd0ec1c0-05ac-4e5d-914b-4fb425952df9

📥 Commits

Reviewing files that changed from the base of the PR and between af82419 and 03b3d13.

📒 Files selected for processing (1)
  • packages/visual-editor/src/fields/ImageField.tsx

Comment thread packages/visual-editor/src/fields/ImageField.tsx
Comment thread packages/visual-editor/src/components/contentBlocks/image/HeroImage.tsx Outdated
@briantstephan briantstephan requested a review from benlife5 April 30, 2026 15:40
Comment thread packages/visual-editor/src/editor/YextField.tsx
Comment thread packages/visual-editor/src/fields/ImageField.tsx
Comment thread packages/visual-editor/src/editor/YextField.tsx Outdated
Comment thread packages/visual-editor/src/internal/puck/constant-value-fields/Image.tsx Outdated
Comment thread packages/visual-editor/src/internal/puck/constant-value-fields/ImageList.tsx Outdated
Comment thread packages/visual-editor/src/components/contentBlocks/image/HeroImage.tsx Outdated
Comment thread packages/visual-editor/src/editor/YextField.tsx Outdated
@briantstephan briantstephan marked this pull request as draft April 30, 2026 16:20
@briantstephan briantstephan marked this pull request as ready for review April 30, 2026 16:43
mkilpatrick
mkilpatrick previously approved these changes Apr 30, 2026
Comment thread packages/visual-editor/src/editor/YextEntityFieldSelector.tsx
Comment thread packages/visual-editor/src/components/LocatorResultCard.tsx Outdated
@briantstephan briantstephan merged commit 9ebdce1 into main Apr 30, 2026
18 checks passed
@briantstephan briantstephan deleted the image-field-type branch April 30, 2026 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

create-dev-release Triggers dev release workflow

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants