Skip to content

Conversation

@codegen-sh
Copy link
Contributor

@codegen-sh codegen-sh bot commented Sep 23, 2025

Requested by Jake Ruesink.

This recreates the changes from PR #135 against current main:

  • Make SelectOption and SelectProps generic so value can be typed (defaults to string)
  • Constrain value to React.Key-compatible types via DOM usage and keys
  • Stringify option.value for data-* attrs and exact-match search
  • Keep UI behavior the same

Versioning (Cursor rule):

  • Applied our .cursor/rules/versioning-with-npm.mdc patch flow to bump @lambdacurry/forms from 0.22.2 → 0.22.3

Notes:

  • Only minimal scope changes were made to ui/select to mirror the original PR intent
  • Remix wrappers remain source-compatible (default generic = string)

Related: #135


💻 View my workAbout Codegen
⛔ Remove Codegen from PR🚫 Ban action checks

Summary by CodeRabbit

  • New Features

    • Select now supports non-string option values (e.g., numbers or IDs), improving flexibility for diverse data sources.
    • Improved selection/display behavior for placeholders, created options, and comparisons to ensure accurate value handling across types.
    • Backward compatible with existing string-based usage; UI unchanged unless using non-string values.
  • Chores

    • Bumped components package version to 0.22.3.

@bolt-new-by-stackblitz
Copy link

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@coderabbitai
Copy link

coderabbitai bot commented Sep 23, 2025

Walkthrough

This change bumps the components package version and makes the Select UI component generic over its option value type, updating related props, handlers, comparisons, and runtime stringification to support non-string values.

Changes

Cohort / File(s) Summary of Changes
Version bump
packages/components/package.json
Incremented version from 0.22.2 to 0.22.3. No other fields changed.
Generic Select component
packages/components/src/ui/select.tsx
Made SelectOption, SelectProps, and Select generic over T extends React.Key (default string). Updated options, value, onValueChange, and onCreateOption signatures; adjusted display, comparisons, data attributes, and creation flow to stringify values for DOM/test attributes while preserving typed values for callbacks.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant S as Select<T>
  participant P as Parent
  participant D as DOM

  U->>S: Open dropdown / type
  S->>S: Render options (each has value: T)
  U->>S: Select option
  S->>P: onValueChange(value: T)
  Note over S,P: Value type preserved (generic T)

  alt Create new option
    U->>S: Submit new label
    S->>S: onCreateOption(label) -> SelectOption<T> | Promise
    S->>P: onValueChange(created.value: T)
  end

  S->>D: set data-value = String(option.value)
  S->>U: Display label (uses String(value) when rendering)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I nibble through types with whiskered delight,
Turning strings into keys that fit just right.
Options hop by, in a tidy array—
T-shaped footprints mark the pathway.
Version ticks up with a gentle thump,
I twitch my nose: shipped with a hop-a-bump! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly and concisely describes the primary change (making the Select value generic) and notes the related patch version bump and PR recreation, so it accurately reflects the changeset without being vague or noisy.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codegen-bot/recreate-pr-135-generic-select-patch-bae1bf

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

@github-actions
Copy link
Contributor

github-actions bot commented Sep 23, 2025

📝 Storybook Preview: View Storybook

This preview will be updated automatically when you push new changes to this PR.

Note: The preview will be available after the workflow completes and the PR is approved for deployment.

@codegen-sh
Copy link
Contributor Author

codegen-sh bot commented Sep 23, 2025

🔍 Broken test auto-fixer • Learn more

Check Suite Agent Status Commit Time
GitHub Actions Agent Fix ✅ 06474b1 Sep 23, 19:38:55 UTC

💻 View my work🛑 Stop🚫 Ban all checks

⚙️ Check suite settings

The generic Select changes broke placeholder display when form values
are initialized with empty strings. The condition 'value != null'
returns true for empty strings, causing String(value) to display
instead of the placeholder.

Fixed by checking both null/undefined AND empty string conditions.

Fixes failing test in select.stories.tsx that expected placeholder
text to be displayed initially.
@codegen-sh
Copy link
Contributor Author

codegen-sh bot commented Sep 23, 2025

Fixed the failing test on PR #157!

Issue Identified

The test was failing because the generic Select component changes broke placeholder display when form values are initialized with empty strings ('').

The problem was in this line:

{value != null ? (selectedOption?.label ?? String(value)) : placeholder}

Since empty strings are not null or undefined, the condition value != null returned true, causing String(value) (empty string) to display instead of the placeholder text.

Solution Applied

Fixed the condition to check for both null/undefined AND empty strings:

{value != null && value !== '' ? (selectedOption?.label ?? String(value)) : placeholder}

Results

  • ✅ All tests now pass (32 test suites, 47 tests)
  • ✅ Components package typechecks correctly
  • ✅ Linting passes
  • ✅ Fix committed and pushed to the existing PR branch

The Select component now properly displays placeholder text when values are empty strings, maintaining backward compatibility while supporting the new generic value types.

🔗 View Updated PR #157

💻 View my work🛑 Stop🚫 Ban all checks • React 👍 or 👎

Copy link

@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: 0

🧹 Nitpick comments (3)
packages/components/src/ui/select.tsx (3)

37-41: Generic SelectProps looks good; consider ReadonlyArray for options.

Avoid accidental mutation by making options read-only.

Apply:

-export interface SelectProps<T extends React.Key = string>
+export interface SelectProps<T extends React.Key = string>
   extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'value' | 'onChange'> {
-  options: SelectOption<T>[];
+  readonly options: ReadonlyArray<SelectOption<T>>;
   value?: T;
   onValueChange?: (value: T) => void;

136-136: Prefer explicit nullish check over != null.

To satisfy strict-equality lint rules and be explicit.

-          {value != null ? (selectedOption?.label ?? String(value)) : placeholder}
+          {value !== undefined && value !== null ? (selectedOption?.label ?? String(value)) : placeholder}

161-169: Prevent SearchInput props from overriding internal controlled state.

Spreading searchInputProps after onValueChange and value allows consumers to overwrite them, breaking search state updates.

Apply:

-                <SearchInput
-                  placeholder="Search..."
-                  value={searchQuery}
-                  onValueChange={(v: string) => {
-                    setSearchQuery(v);
-                    searchInputProps?.onValueChange?.(v);
-                  }}
-                  {...searchInputProps}
-                />
+                <SearchInput
+                  placeholder="Search..."
+                  {...searchInputProps}
+                  value={searchQuery}
+                  onValueChange={(v: string) => {
+                    setSearchQuery(v);
+                    searchInputProps?.onValueChange?.(v);
+                  }}
+                />
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 01eb068 and 06474b1.

📒 Files selected for processing (2)
  • packages/components/package.json (1 hunks)
  • packages/components/src/ui/select.tsx (9 hunks)
🧰 Additional context used
📓 Path-based instructions (18)
{package.json,packages/**/package.json,apps/**/package.json}

📄 CodeRabbit inference engine (.cursor/rules/monorepo-organization.mdc)

Use consistent versioning across packages

Files:

  • packages/components/package.json
{packages/**/package.json,apps/**/package.json}

📄 CodeRabbit inference engine (.cursor/rules/monorepo-organization.mdc)

Define peerDependencies, dependencies, and devDependencies appropriately in package.json

Files:

  • packages/components/package.json
packages/components/package.json

📄 CodeRabbit inference engine (.cursor/rules/versioning-with-npm.mdc)

Version bumps for @lambdacurry/forms update packages/components/package.json

Files:

  • packages/components/package.json
packages/**/package.json

📄 CodeRabbit inference engine (AGENTS.md)

Verify each package’s exports field is correct

Files:

  • packages/components/package.json
packages/components/src/ui/**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/form-component-patterns.mdc)

packages/components/src/ui/**/*.tsx: Build on @radix-ui components as the foundation for base UI components
Follow the component composition pattern for UI components that accept form integration

Files:

  • packages/components/src/ui/select.tsx
packages/components/src/ui/*.{tsx,ts}

📄 CodeRabbit inference engine (.cursor/rules/form-component-patterns.mdc)

Base UI components should be named as ComponentName in ui/ directory

Files:

  • packages/components/src/ui/select.tsx
**/*.{tsx,ts}

📄 CodeRabbit inference engine (.cursor/rules/form-component-patterns.mdc)

**/*.{tsx,ts}: Props interfaces should be named as ComponentNameProps
Form schemas should be named formSchema or componentNameSchema

Files:

  • packages/components/src/ui/select.tsx
packages/components/src/{remix-hook-form,ui}/*.{tsx,ts}

📄 CodeRabbit inference engine (.cursor/rules/form-component-patterns.mdc)

Always export both the component and its props type

Files:

  • packages/components/src/ui/select.tsx
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/monorepo-organization.mdc)

{apps,packages}/**/*.{ts,tsx}: Use package name imports for published packages (e.g., import { TextField } from '@lambdacurry/forms/remix-hook-form')
Import from specific entry points (e.g., import { TextField } from '@lambdacurry/forms/remix-hook-form/text-field')
Do not use relative imports across packages (e.g., avoid import { TextField } from '../../packages/components/src/remix-hook-form/text-field')
Order imports: 1) external libraries, 2) internal package imports, 3) cross-package imports, 4) type-only imports (grouped separately)

Files:

  • packages/components/src/ui/select.tsx
{apps,packages}/**/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/monorepo-organization.mdc)

{apps,packages}/**/src/**/*.{ts,tsx}: Use relative imports within the same package (e.g., import { FormControl } from './form')
Use relative imports for sibling directories (e.g., import { Button } from '../ui/button')

Files:

  • packages/components/src/ui/select.tsx
packages/components/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/monorepo-organization.mdc)

packages/components/src/**/*.{ts,tsx}: Always export both component and props type (e.g., export { ComponentName }; export type { ComponentNameProps };)
Use named exports for components for better tree-shaking (e.g., export const ComponentName = ...; avoid default exports)
Avoid default exports for components
Use tree-shaking friendly exports

Files:

  • packages/components/src/ui/select.tsx
{apps,packages}/**/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (.cursor/rules/monorepo-organization.mdc)

{apps,packages}/**/src/**/*.{tsx,ts}: Use kebab-case for component file names (e.g., text-field.tsx, data-table.tsx)
Match component name in PascalCase to the exported component from the file (e.g., text-field.tsx exports TextField)

Files:

  • packages/components/src/ui/select.tsx
packages/components/src/ui/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/monorepo-organization.mdc)

Don't import from remix-hook-form package in ui components

Files:

  • packages/components/src/ui/select.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/monorepo-organization.mdc)

Avoid circular dependencies

**/*.{ts,tsx}: Indent with 2 spaces, max line width 120, and use single quotes (Biome-enforced)
Use PascalCase for component and type names
Name hooks in camelCase with a use* prefix
Organize imports automatically with Biome
Use named exports only

Files:

  • packages/components/src/ui/select.tsx
packages/components/src/**

📄 CodeRabbit inference engine (AGENTS.md)

Place all @lambdacurry/forms source under packages/components/src and treat it as the only editable source for the package

Files:

  • packages/components/src/ui/select.tsx
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

**/*.tsx: Keep React components pure and fully typed
Define explicit props interfaces for React components
Use React 19 ref patterns (forwardRef, useImperativeHandle as appropriate)

Files:

  • packages/components/src/ui/select.tsx
**/*.{ts,tsx,mdx}

📄 CodeRabbit inference engine (AGENTS.md)

Use kebab-case for filenames (e.g., text-field.tsx, data-table-filter/**)

Files:

  • packages/components/src/ui/select.tsx
packages/**/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Avoid cross-package relative imports in the monorepo

Files:

  • packages/components/src/ui/select.tsx
🧬 Code graph analysis (1)
packages/components/src/ui/select.tsx (1)
packages/components/src/remix-hook-form/select.tsx (2)
  • SelectProps (7-20)
  • Select (22-52)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (7)
packages/components/src/ui/select.tsx (6)

20-23: Generic SelectOption over React.Key is appropriate.

Good constraint for DOM/React key safety while enabling number/string values.


53-56: onCreateOption returning SelectOption aligns with the generic model.

This preserves type safety through creation.


64-82: Generic component signature with default = string preserves source compatibility.

API surface is typed correctly and remains non-breaking for existing callers.


175-181: Stringifying value for data- and test id is correct.*

Prevents non-string dataset issues and stabilizes test selectors.


243-245: Exact-match gate accounts for label and value.

This avoids duplicate creation when the typed value matches an existing option.


254-257: Creation flow returns the typed value.

Correctly propagates created.value of type T.

packages/components/package.json (1)

3-3: Patch bump to 0.22.3 looks correct — confirm no exact 0.22.2 pins remain

components version = 0.22.3; sandbox scan was inconclusive. Run these locally to confirm no consumers pin @lambdacurry/forms to 0.22.2:

rg -n --hidden --no-ignore '"@lambdacurry/forms"\s*:\s*".+?"' -S || echo "None found"
rg -n --hidden --no-ignore '"@lambdacurry/forms"\s*:\s*"0.22.2"' -S || echo "None found"
rg -n --hidden --no-ignore '@lambdacurry/forms[^0-9]*0.22.2' -S || echo "None found"

Copy link

@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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 06474b1 and 78b6c81.

📒 Files selected for processing (1)
  • packages/components/src/ui/select.tsx (9 hunks)
🧰 Additional context used
📓 Path-based instructions (14)
packages/components/src/ui/**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/form-component-patterns.mdc)

packages/components/src/ui/**/*.tsx: Build on @radix-ui components as the foundation for base UI components
Follow the component composition pattern for UI components that accept form integration

Files:

  • packages/components/src/ui/select.tsx
packages/components/src/ui/*.{tsx,ts}

📄 CodeRabbit inference engine (.cursor/rules/form-component-patterns.mdc)

Base UI components should be named as ComponentName in ui/ directory

Files:

  • packages/components/src/ui/select.tsx
**/*.{tsx,ts}

📄 CodeRabbit inference engine (.cursor/rules/form-component-patterns.mdc)

**/*.{tsx,ts}: Props interfaces should be named as ComponentNameProps
Form schemas should be named formSchema or componentNameSchema

Files:

  • packages/components/src/ui/select.tsx
packages/components/src/{remix-hook-form,ui}/*.{tsx,ts}

📄 CodeRabbit inference engine (.cursor/rules/form-component-patterns.mdc)

Always export both the component and its props type

Files:

  • packages/components/src/ui/select.tsx
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/monorepo-organization.mdc)

{apps,packages}/**/*.{ts,tsx}: Use package name imports for published packages (e.g., import { TextField } from '@lambdacurry/forms/remix-hook-form')
Import from specific entry points (e.g., import { TextField } from '@lambdacurry/forms/remix-hook-form/text-field')
Do not use relative imports across packages (e.g., avoid import { TextField } from '../../packages/components/src/remix-hook-form/text-field')
Order imports: 1) external libraries, 2) internal package imports, 3) cross-package imports, 4) type-only imports (grouped separately)

Files:

  • packages/components/src/ui/select.tsx
{apps,packages}/**/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/monorepo-organization.mdc)

{apps,packages}/**/src/**/*.{ts,tsx}: Use relative imports within the same package (e.g., import { FormControl } from './form')
Use relative imports for sibling directories (e.g., import { Button } from '../ui/button')

Files:

  • packages/components/src/ui/select.tsx
packages/components/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/monorepo-organization.mdc)

packages/components/src/**/*.{ts,tsx}: Always export both component and props type (e.g., export { ComponentName }; export type { ComponentNameProps };)
Use named exports for components for better tree-shaking (e.g., export const ComponentName = ...; avoid default exports)
Avoid default exports for components
Use tree-shaking friendly exports

Files:

  • packages/components/src/ui/select.tsx
{apps,packages}/**/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (.cursor/rules/monorepo-organization.mdc)

{apps,packages}/**/src/**/*.{tsx,ts}: Use kebab-case for component file names (e.g., text-field.tsx, data-table.tsx)
Match component name in PascalCase to the exported component from the file (e.g., text-field.tsx exports TextField)

Files:

  • packages/components/src/ui/select.tsx
packages/components/src/ui/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/monorepo-organization.mdc)

Don't import from remix-hook-form package in ui components

Files:

  • packages/components/src/ui/select.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/monorepo-organization.mdc)

Avoid circular dependencies

**/*.{ts,tsx}: Indent with 2 spaces, max line width 120, and use single quotes (Biome-enforced)
Use PascalCase for component and type names
Name hooks in camelCase with a use* prefix
Organize imports automatically with Biome
Use named exports only

Files:

  • packages/components/src/ui/select.tsx
packages/components/src/**

📄 CodeRabbit inference engine (AGENTS.md)

Place all @lambdacurry/forms source under packages/components/src and treat it as the only editable source for the package

Files:

  • packages/components/src/ui/select.tsx
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

**/*.tsx: Keep React components pure and fully typed
Define explicit props interfaces for React components
Use React 19 ref patterns (forwardRef, useImperativeHandle as appropriate)

Files:

  • packages/components/src/ui/select.tsx
**/*.{ts,tsx,mdx}

📄 CodeRabbit inference engine (AGENTS.md)

Use kebab-case for filenames (e.g., text-field.tsx, data-table-filter/**)

Files:

  • packages/components/src/ui/select.tsx
packages/**/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Avoid cross-package relative imports in the monorepo

Files:

  • packages/components/src/ui/select.tsx
🧬 Code graph analysis (1)
packages/components/src/ui/select.tsx (1)
packages/components/src/remix-hook-form/select.tsx (2)
  • SelectProps (7-20)
  • Select (22-52)
🔇 Additional comments (7)
packages/components/src/ui/select.tsx (7)

20-23: Generic SelectOption typed to React.Key is solid

Constraint to React.Key keeps keys/DOM-safe and aligns with React element keys. No issues.


54-54: onCreateOption generic return type looks correct

The union with Promise maintains flexibility for async create flows.


64-81: Generic Select component signature: OK

Default generic preserves existing usage; inference from options/value will work in TSX.


179-181: Stringifying values for data- and test ids: good*

Ensures stable attributes across number/string values and keeps DOM compliant.


256-257: Created option piping its value to onValueChange is correct

Matches the new generic typing and expected controlled Select behavior.


136-136: Treat only null/undefined as “no selection” (allow empty-string values)

Empty-string values are currently treated as “no selection” and their labels won’t display; change the check to only exclude null/undefined:

-          {value != null && value !== '' ? (selectedOption?.label ?? String(value)) : placeholder}
+          {value !== undefined && value !== null ? (selectedOption?.label ?? String(value)) : placeholder}

If '' is intentionally used to mean “no selection”, keep as-is and document it.


37-41: Generic SelectProps correctly propagates typed value/onValueChange

Omitting HTML 'value'/'onChange' avoids collisions; generic typing with default = string is sound and backward-compatible. Repo search for UISelect/Select returned no matches — verify downstream wrappers/external consumers still type-check and update call sites to specify the generic if they rely on the string default.

Comment on lines +244 to 245
options.some((o) => o.label.toLowerCase() === lower || String(o.value).toLowerCase() === lower);
if (!creatable || !q || hasExactMatch) return null;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Creatable exact-match can suppress “Create” while showing no results

You added exact-match against String(o.value), but CommandItem search still uses only label, so typing a value that matches an existing option’s value (but not its label) produces “No results” and also hides the create option.

Two fixes (pick one):

  • Include value in the item’s search string:
    • Change each CommandItem value prop to include both: value={${option.label} ${String(option.value)}}
  • If supported by your Command/CommandItem, add keywords with the stringified value:
    • keywords={[String(option.value)]}

Example (outside the changed hunk):

// For both branches where CommandItem is rendered
<CommandItem
  ...
- value={option.label}
+ value={`${option.label} ${String(option.value)}`}
  ...
>

This aligns the exact-match guard with the visible search results and avoids a confusing “No results” state.

🤖 Prompt for AI Agents
In packages/components/src/ui/select.tsx around lines 244-245, the exact-match
check compares the query to String(o.value) but CommandItem search only uses the
label, causing a hidden "Create" when a value (not label) matches; fix by making
CommandItem searchable on both label and value or by adding keywords: either set
each CommandItem value to include both label and stringified value
(value={`${option.label} ${String(option.value)}`}) or, if supported, add
keywords={[String(option.value)]} to the CommandItem props so the visible search
results and the creatable exact-match guard stay in sync.

@jaruesink jaruesink merged commit 5588387 into main Sep 24, 2025
6 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.

2 participants