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:
Adding a New *SelectField¶
- Wrap
SearchableSingleSelect(orMulti) - Fetch options with the entity's list hook; map to
SearchableSelectOption[] - Wire
onAddNew— openuseInlineModal(config)with the create form; close + refetch on success - 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 |