Skip to content

refactor: merge dynamicSingleSelect into basicSelector#1158

Merged
mkilpatrick merged 12 commits intomainfrom
dynamic-options-single
Apr 15, 2026
Merged

refactor: merge dynamicSingleSelect into basicSelector#1158
mkilpatrick merged 12 commits intomainfrom
dynamic-options-single

Conversation

@mkilpatrick
Copy link
Copy Markdown
Collaborator

dynamicSingleSelect's only real differences from basicSelector were:

  • handles dynamic options in the way of a function call
  • used Puck's select field under the hood (while basicSelector uses Combobox)

This merges the dynamic functionality of dynamicSingleSelect into basicSelector. It also doesn't make sense for us to have different types of dropdown fields. They should all look and function the same way.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 14, 2026

Walkthrough

This PR flattens wrapped single-select field payloads used by Locator result cards into plain string field IDs, adds a migration to transform existing documents, updates LocatorResultCard props/defaults to use string fields, replaces dynamic single-select editor wiring with a DisplayFieldSelector rendering a basicSelector, removes the DynamicOptionsSingleSelector single-select type/component from DynamicOptionsSelector and YextField, and updates BasicSelectorField to accept lazy (function) options. Tests were added/updated for the migration and selector behaviors.

Possibly related PRs

Suggested labels

create-dev-release

Suggested reviewers

  • briantstephan
  • benlife5
  • jwartofsky-yext
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: merging dynamicSingleSelect functionality into basicSelector.
Description check ✅ Passed The description clearly explains the rationale for the refactor and relates directly to the changeset modifications.

✏️ 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 dynamic-options-single

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/visual-editor/src/components/LocatorResultCard.tsx (1)

325-350: ⚠️ Potential issue | 🟠 Major

Hook called inside callback function violates Rules of Hooks.

getDisplayFieldOptions calls useTemplateMetadata(), but it's invoked as a callback via options: () => getDisplayFieldOptions(fieldTypeId). When BasicSelectorFieldOverride evaluates field.options(), the hook call is nested inside a callback rather than at the top level of a component.

Consider refactoring to move the hook call to the component level:

Suggested approach
-const DisplayFieldSelector = (fieldTypeId: string | string[]) =>
-  YextField<string | undefined>(msg("fields.field", "Field"), {
-    type: "basicSelector",
-    options: () => getDisplayFieldOptions(fieldTypeId),
-    translateOptions: false,
-  });
+const DisplayFieldSelector = (fieldTypeId: string | string[]) =>
+  YextField<string | undefined>(msg("fields.field", "Field"), {
+    type: "basicSelector",
+    options: () => {
+      // Note: This will be called during render of BasicSelectorFieldOverride
+      // The hook call in getDisplayFieldOptions may work in practice but
+      // violates Rules of Hooks. Consider passing templateMetadata as a parameter
+      // or restructuring to call the hook at the component level.
+      return getDisplayFieldOptions(fieldTypeId);
+    },
+    translateOptions: false,
+  });

A cleaner solution would be to have BasicSelectorFieldOverride accept a context parameter or use a different pattern that doesn't require calling hooks inside callbacks.

Based on learnings: "Do not use React hooks inside arbitrary callback functions. Hooks must be called only at the top level of React function components or custom hooks."

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

In `@packages/visual-editor/src/components/LocatorResultCard.tsx` around lines 325
- 350, getDisplayFieldOptions currently calls useTemplateMetadata inside a
callback (violates Rules of Hooks); refactor so the hook is invoked at the top
level and the options callback only uses captured values. Concretely, call
useTemplateMetadata() inside DisplayFieldSelector (or a new custom hook like
useDisplayFieldOptions) to get templateMetadata, compute the display options
from templateMetadata and the provided fieldTypeId, and then pass a simple
options: () => precomputedOptions (or options that reference only local non-hook
variables) into BasicSelectorFieldOverride/YextField. Ensure
getDisplayFieldOptions either becomes a pure helper that accepts
templateMetadata as an argument or is removed, and update references to
useTemplateMetadata, getDisplayFieldOptions, and DisplayFieldSelector
accordingly so no hooks are called inside the options callback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/visual-editor/src/components/LocatorResultCard.tsx`:
- Around line 325-350: getDisplayFieldOptions currently calls
useTemplateMetadata inside a callback (violates Rules of Hooks); refactor so the
hook is invoked at the top level and the options callback only uses captured
values. Concretely, call useTemplateMetadata() inside DisplayFieldSelector (or a
new custom hook like useDisplayFieldOptions) to get templateMetadata, compute
the display options from templateMetadata and the provided fieldTypeId, and then
pass a simple options: () => precomputedOptions (or options that reference only
local non-hook variables) into BasicSelectorFieldOverride/YextField. Ensure
getDisplayFieldOptions either becomes a pure helper that accepts
templateMetadata as an argument or is removed, and update references to
useTemplateMetadata, getDisplayFieldOptions, and DisplayFieldSelector
accordingly so no hooks are called inside the options callback.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 68c76d2c-ef3d-405f-b077-70ded65322f4

📥 Commits

Reviewing files that changed from the base of the PR and between 19ffc41 and 5da6f22.

📒 Files selected for processing (10)
  • packages/visual-editor/src/components/Locator.test.tsx
  • packages/visual-editor/src/components/LocatorResultCard.tsx
  • packages/visual-editor/src/components/migrations/0074_flatten_locator_result_card_single_select_fields.ts
  • packages/visual-editor/src/components/migrations/migrationRegistry.ts
  • packages/visual-editor/src/editor/DynamicOptionsSelector.tsx
  • packages/visual-editor/src/editor/YextField.test.tsx
  • packages/visual-editor/src/editor/YextField.tsx
  • packages/visual-editor/src/editor/index.ts
  • packages/visual-editor/src/fields/BasicSelectorField.test.tsx
  • packages/visual-editor/src/fields/BasicSelectorField.tsx
💤 Files with no reviewable changes (3)
  • packages/visual-editor/src/editor/index.ts
  • packages/visual-editor/src/editor/DynamicOptionsSelector.tsx
  • packages/visual-editor/src/editor/YextField.tsx

Comment thread packages/visual-editor/src/components/Locator.test.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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/visual-editor/src/components/LocatorResultCard.tsx (1)

325-350: ⚠️ Potential issue | 🔴 Critical

Move useTemplateMetadata() out of this selector-options helper.

Line 329 calls useTemplateMetadata() from getDisplayFieldOptions(), a helper function passed as an options callback to Puck's basicSelector field. Calling hooks outside React function components or custom hooks violates the React Rules of Hooks. Resolve the metadata in a component or custom hook and pass the options directly, rather than lazily computing them in a callback.

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

In `@packages/visual-editor/src/components/LocatorResultCard.tsx` around lines 325
- 350, The helper getDisplayFieldOptions currently calls useTemplateMetadata
(violating Rules of Hooks); move the hook call into the React component or a
custom hook and pass resolved data into the selector helper instead.
Specifically, remove useTemplateMetadata from getDisplayFieldOptions and change
usage so that DisplayFieldSelector (or its parent) calls useTemplateMetadata,
computes locatorDisplayFields and fieldTypeIds, then calls a pure
getDisplayFieldOptions(displayFields, fieldTypeId) or supplies a precomputed
options array to YextField; ensure the options callback passed to YextField no
longer calls any hooks and simply returns the precomputed
DynamicOption<string>[].
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/visual-editor/src/components/LocatorResultCard.tsx`:
- Around line 325-350: The helper getDisplayFieldOptions currently calls
useTemplateMetadata (violating Rules of Hooks); move the hook call into the
React component or a custom hook and pass resolved data into the selector helper
instead. Specifically, remove useTemplateMetadata from getDisplayFieldOptions
and change usage so that DisplayFieldSelector (or its parent) calls
useTemplateMetadata, computes locatorDisplayFields and fieldTypeIds, then calls
a pure getDisplayFieldOptions(displayFields, fieldTypeId) or supplies a
precomputed options array to YextField; ensure the options callback passed to
YextField no longer calls any hooks and simply returns the precomputed
DynamicOption<string>[].

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1443a820-05bf-454f-a6d4-510979567008

📥 Commits

Reviewing files that changed from the base of the PR and between 5da6f22 and ba1873d.

⛔ Files ignored due to path filters (13)
  • packages/visual-editor/src/components/testing/screenshots/Directory/[mobile] default props - country - document data.png is excluded by !**/*.png, !packages/visual-editor/src/components/testing/screenshots/**
  • packages/visual-editor/src/components/testing/screenshots/Directory/[mobile] default props - region - document data.png is excluded by !**/*.png, !packages/visual-editor/src/components/testing/screenshots/**
  • packages/visual-editor/src/components/testing/screenshots/Directory/[mobile] default props - root - document data.png is excluded by !**/*.png, !packages/visual-editor/src/components/testing/screenshots/**
  • packages/visual-editor/src/components/testing/screenshots/Directory/[mobile] version 40 with countryDocument and non-default props.png is excluded by !**/*.png, !packages/visual-editor/src/components/testing/screenshots/**
  • packages/visual-editor/src/components/testing/screenshots/Directory/[mobile] version 8 - country - default props.png is excluded by !**/*.png, !packages/visual-editor/src/components/testing/screenshots/**
  • packages/visual-editor/src/components/testing/screenshots/Directory/[mobile] version 8 - directory list - non-default props.png is excluded by !**/*.png, !packages/visual-editor/src/components/testing/screenshots/**
  • packages/visual-editor/src/components/testing/screenshots/Directory/[mobile] version 8 - region - default props.png is excluded by !**/*.png, !packages/visual-editor/src/components/testing/screenshots/**
  • packages/visual-editor/src/components/testing/screenshots/Directory/[mobile] version 8 - root - default props.png is excluded by !**/*.png, !packages/visual-editor/src/components/testing/screenshots/**
  • packages/visual-editor/src/components/testing/screenshots/Locator/[desktop] latest version multi-pageset default props.png is excluded by !**/*.png, !packages/visual-editor/src/components/testing/screenshots/**
  • packages/visual-editor/src/components/testing/screenshots/Locator/[desktop] version 72 wrapped result card selector values.png is excluded by !**/*.png, !packages/visual-editor/src/components/testing/screenshots/**
  • packages/visual-editor/src/components/testing/screenshots/Locator/[mobile] latest version multi-pageset default props.png is excluded by !**/*.png, !packages/visual-editor/src/components/testing/screenshots/**
  • packages/visual-editor/src/components/testing/screenshots/Locator/[mobile] version 72 wrapped result card selector values.png is excluded by !**/*.png, !packages/visual-editor/src/components/testing/screenshots/**
  • packages/visual-editor/src/components/testing/screenshots/Locator/[tablet] version 72 wrapped result card selector values.png is excluded by !**/*.png, !packages/visual-editor/src/components/testing/screenshots/**
📒 Files selected for processing (1)
  • packages/visual-editor/src/components/LocatorResultCard.tsx

Comment thread packages/visual-editor/src/components/LocatorResultCard.tsx Outdated
benlife5
benlife5 previously approved these changes Apr 15, 2026
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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/visual-editor/src/components/LocatorResultCard.tsx (1)

328-350: ⚠️ Potential issue | 🟠 Major

Move useTemplateMetadata() hook call out of the module-level function.

The getDisplayFieldOptions() function is defined at module level (not inside a component or custom hook) and calls the useTemplateMetadata() hook. This violates React's rules of hooks, which require hooks to be called only at the top level of React function components or custom hooks. The TODO comment on line 329 acknowledges this is a known violation that needs refactoring.

The proper fix is either to:

  1. Convert getDisplayFieldOptions to a custom hook (e.g., useGetDisplayFieldOptions), or
  2. Move the hook call into a component/custom hook and pass the results down
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/visual-editor/src/components/LocatorResultCard.tsx` around lines 328
- 350, getDisplayFieldOptions currently calls the React hook useTemplateMetadata
at module scope which breaks the rules of hooks; refactor by removing
useTemplateMetadata from getDisplayFieldOptions and either (A) convert
getDisplayFieldOptions into a custom hook (e.g., rename to
useGetDisplayFieldOptions) that calls useTemplateMetadata at the top level and
returns the options, or (B) call useTemplateMetadata inside the component that
renders DisplayFieldSelector and pass the resolved locatorDisplayFields (or the
computed options) down to getDisplayFieldOptions as an argument; update
references to getDisplayFieldOptions/DisplayFieldSelector/YextField to use the
new hook or pass-through options so hooks are only invoked from components or
custom hooks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/visual-editor/src/components/LocatorResultCard.tsx`:
- Around line 328-350: getDisplayFieldOptions currently calls the React hook
useTemplateMetadata at module scope which breaks the rules of hooks; refactor by
removing useTemplateMetadata from getDisplayFieldOptions and either (A) convert
getDisplayFieldOptions into a custom hook (e.g., rename to
useGetDisplayFieldOptions) that calls useTemplateMetadata at the top level and
returns the options, or (B) call useTemplateMetadata inside the component that
renders DisplayFieldSelector and pass the resolved locatorDisplayFields (or the
computed options) down to getDisplayFieldOptions as an argument; update
references to getDisplayFieldOptions/DisplayFieldSelector/YextField to use the
new hook or pass-through options so hooks are only invoked from components or
custom hooks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b23e3012-b28a-4b23-ad73-f57075b40202

📥 Commits

Reviewing files that changed from the base of the PR and between ba1873d and c27371d.

📒 Files selected for processing (2)
  • packages/visual-editor/src/components/Locator.test.tsx
  • packages/visual-editor/src/components/LocatorResultCard.tsx

Copy link
Copy Markdown
Contributor

@jwartofsky-yext jwartofsky-yext left a comment

Choose a reason for hiding this comment

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

lgtm!

Comment thread packages/visual-editor/src/components/Locator.test.tsx
@mkilpatrick mkilpatrick dismissed stale reviews from jwartofsky-yext and benlife5 via d219c47 April 15, 2026 19:02
@mkilpatrick mkilpatrick merged commit 1b0c6cf into main Apr 15, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants