Skip to content

Searchable Select

Two layers: generic SearchableSelect primitives (UI only, no data fetching) and domain-specific *SelectField wrappers that fetch data and wire create/edit modals.


Generic Primitives

SearchableSelectOption

type SearchableSelectOption = {
  value: string;
  label: string;
  secondary?: string;  // Subtitle shown below the label
  imageUrl?: string;   // Thumbnail shown in list and grid views
  isPublic?: boolean;  // Used by the public/private toggle
};

SearchableSingleSelect

import { SearchableSingleSelect } from "@/components/SearchableSelect";

<SearchableSingleSelect
  value={selectedId}
  options={options}
  onChange={(id) => setSelectedId(id)}
  placeholder="Select a subject…"
  searchPlaceholder="Search subjects…"
/>

All props:

Prop Type Description
value string \| null Currently selected option value
options SearchableSelectOption[] Available options
onChange (value: string \| null) => void Selection change callback
placeholder string Trigger button placeholder
searchPlaceholder string Search input placeholder
onAddNew () => void Called when user clicks "Add new"
addLabel string Label for the add button
disableAdd boolean Hide the add button
onEditOption (opt) => void Called when user clicks edit on an option
onDeleteOption (opt) => void \| Promise<void> Called when user confirms delete
disableEdit boolean Hide edit buttons
disableDelete boolean Hide delete buttons
deleteConfirmMessage (opt) => string Custom confirmation message for delete
showPublicToggle boolean Show public/private filter toggle
showPublic boolean Current state of the public toggle
onShowPublicChange (value: boolean) => void Toggle change callback
publicToggleLabel string Label for the toggle
disabled boolean Disable the entire select
allowClear boolean Show clear button to deselect
className string Container class

SearchableMultiSelect

Same props as SearchableSingleSelect except:

Prop Type Description
selectedIds string[] Currently selected values
onChange (ids: string[]) => void Selection change callback

Selected items render as chips below the trigger. There is no allowClear — chips have individual remove buttons.


Features

  • List and grid view modes (toggle in the header)
  • Image thumbnails with text fallback
  • Inline add / edit / delete buttons per option
  • Delete confirmation dialog (custom message via deleteConfirmMessage)
  • Public/private toggle in the header
  • Keyboard-navigable command palette

"Create New" Flow

Wire onAddNew to open an inline modal with the entity form. Signal success by closing the modal and refreshing options:

const modal = useInlineModal();

<SearchableSingleSelect
  options={options}
  onAddNew={() =>
    modal.open({
      title: "New Subject",
      mainContent: (
        <SubjectForm
          onSuccess={() => {
            modal.close();
            refetch();
          }}
        />
      ),
    })
  }
/>

Domain *SelectField Wrappers

Pre-built wrappers that handle data fetching, option mapping, and create/edit modals:

Component Entity In-modal Form Location
SubjectSelectField Subject SubjectForm src/components/fields/SubjectSelectField.tsx
SetDesignSelectField Set Design SetDesignForm src/components/fields/SetDesignSelectField.tsx
GuidelineSelectField Shooting Guideline GuidelineForm src/components/fields/GuidelineSelectField.tsx
ShotTypeSelectField Shot Type ShotTypeForm src/components/fields/ShotTypeSelectField.tsx
ResolutionPresetSelectField Resolution Preset ResolutionPresetForm src/components/fields/ResolutionPresetSelectField.tsx
BackgroundPresetSelectField Background Preset BackgroundPresetForm src/components/fields/BackgroundPresetSelectField.tsx
PosePresetSelectField Pose Preset PosePresetForm src/components/fields/PosePresetSelectField.tsx

Usage is identical across all wrappers — they accept the same value/onChange API plus an orgId:

<SubjectSelectField
  orgId={orgId}
  value={subjectId}
  onChange={setSubjectId}
/>

Adding a New *SelectField

  1. Wrap SearchableSingleSelect (or Multi)
  2. Fetch options with the entity's list hook; map to SearchableSelectOption[]
  3. Wire onAddNew — open useInlineModal(config) with the create form; close + refetch on success
  4. Wire onEditOption / onDeleteOption — open the edit form or call the delete mutation
export function WidgetSelectField({ orgId, value, onChange }) {
  const { widgets, refetch } = useWidgetsList(orgId);
  const modal = useInlineModal();

  const options = widgets.map((w) => ({
    value: w.id,
    label: w.name,
    imageUrl: w.thumbnailUrl,
  }));

  return (
    <SearchableSingleSelect
      value={value}
      options={options}
      onChange={onChange}
      placeholder="Select a widget…"
      searchPlaceholder="Search widgets…"
      onAddNew={() =>
        modal.open({
          title: "New Widget",
          mainContent: (
            <WidgetForm orgId={orgId} onSuccess={() => { modal.close(); refetch(); }} />
          ),
        })
      }
    />
  );
}

Source Files

File Contents
src/components/SearchableSelect/SearchableSelect.tsx SearchableSingleSelect, SearchableMultiSelect, SearchableSelectOption
src/components/SearchableSelect/index.ts Public exports
src/components/fields/SubjectSelectField.tsx Subject domain wrapper
src/components/fields/SetDesignSelectField.tsx Set Design domain wrapper
src/components/fields/GuidelineSelectField.tsx Guideline domain wrapper
src/components/fields/ShotTypeSelectField.tsx Shot Type domain wrapper
src/components/fields/ResolutionPresetSelectField.tsx Resolution Preset domain wrapper
src/components/fields/BackgroundPresetSelectField.tsx Background Preset domain wrapper
src/components/fields/PosePresetSelectField.tsx Pose Preset domain wrapper
src/components/fields/index.ts Public exports