import type { ReactNode } from 'react'; import type { FilterExpression, SimpleFilter } from './types/filter.types'; import type { Fetcher, OrderBy } from './services/api.types'; /** Generic record type - all records must have an Id (PascalCase standard) */ export interface LookupRecord { Id: string; [key: string]: unknown; } /** * Helpers passed to `onCreateNew` — call `onCreated(record)` when the host's form * successfully creates a new record (LookupField selects it automatically, same as * picking it from the dropdown, and closes the popup), or `onCancel()` to close the * popup without changing the current selection. */ export interface LookupFieldCreateHelpers { onCreated: (record: TRecord) => void; onCancel: () => void; } /** Data passed to onReady callback [UPDATED v0.4 - removed totalRecords] */ export interface LookupFieldReadyData { /** Whether the first page of data has been loaded */ firstPageLoaded: boolean; } /** Imperative ref methods for programmatic control */ export interface LookupFieldRef { /** Clears the current selection */ clear: () => void; /** Focuses the component trigger */ focus: () => void; /** Forces a fresh data fetch from API */ reload: () => void; /** * Returns the currently selected record or null. * In multi-select mode (`isMultiple`), returns only the FIRST selected record — * use the `value`/`onChange` props to read the full selection instead. */ getValue: () => TRecord | null; /** * Programmatically selects a record by GUID. * In multi-select mode (`isMultiple`), this REPLACES the entire selection with just * this one record — it does not add to the existing selection. */ setValue: (Id: string) => void; /** Opens the dropdown programmatically */ open: () => void; /** Closes the dropdown programmatically */ close: () => void; /** * Manually triggers validation and updates internal error state. * Use this before form submission to validate required fields. * Calls onValidationChange callback with the validation result. * @returns true if valid (has selection or not required), false if invalid */ validate: () => boolean; } /** Base props for LookupField shared by both single- and multi-select modes. */ export interface LookupFieldBaseProps { /** Entity name for API endpoint (e.g., 'customers', 'products') */ entity: string; /** Label displayed above the input. Optional when used in table columns where the column header provides the label. */ label?: string; /** Fields to display in dropdown options */ displayFields: (keyof TRecord & string)[]; /** * Fetcher function for API requests (FR29, FR31). * Receives entity name and request, returns the search response. * Gives full control over URL construction, HTTP client, headers, etc. * * Use `createFetcher('/api')` helper for simple cases. * * @example * // Using createFetcher helper * import { createFetcher } from 'siesa-ui-kit' * * * @example * // With auth token * { * const res = await fetch(`/api/${entity}/search`, { * method: 'POST', * headers: { * 'Content-Type': 'application/json', * Authorization: `Bearer ${token}`, * }, * body: JSON.stringify(request), * }) * return res.json() * }} * /> * * @example * // With axios * { * const { data } = await axios.post(`/api/${entity}/search`, request) * return data * }} * /> */ fetcher: Fetcher; /** Show clear button to reset selection (default: true) */ clearable?: boolean; /** * aria-label for the clear ("×") button. @default 'Clear selection' * * The default is English and it is the ONLY string this component still hardcodes for screen * readers, so a Spanish host shipped one English label in the middle of a translated form and * had no way to override it. Pass the host's translation; the default keeps existing callers * rendering exactly as before. */ clearSelectionLabel?: string; /** aria-label for the "remove" (×) button of each chip in multiple mode. @default 'Remove' */ removeLabel?: string; /** Placeholder text when no value selected */ placeholder?: string; /** Additional CSS classes for root element */ className?: string; /** Number of records per page (default: 10) */ pageSize?: number; /** Callback triggered when an API error occurs */ onError?: (error: Error) => void; /** Fields to search against (defaults to displayFields) */ searchFields?: (keyof TRecord & string)[]; /** * Minimum number of characters required before search is triggered. * When user types fewer characters, a message is shown prompting for more input. * @default 1 */ minChars?: number; /** * Single field to display for selected value (priority 3). * When set, only this field is shown in the trigger for the selected record. * Priority order: renderSelected > displayTemplate > displayValue > all displayFields concatenated */ displayValue?: keyof TRecord & string; /** * Template string with {field} placeholders for selected value display (priority 2). * Example: "{codigo} - {nombre}" displays "C001 - Acme Corporation" * Priority order: renderSelected > displayTemplate > displayValue > all displayFields concatenated */ displayTemplate?: string; /** * Custom render function for selected value display (priority 1 - highest). * Receives the selected record and returns the string to display. * Priority order: renderSelected > displayTemplate > displayValue > all displayFields concatenated */ renderSelected?: (record: TRecord) => string; /** * Static filters to apply to all API requests. * Accepts simple object format: { activo: true, tipo: "A" } * Or expressive AND/OR format: [[{a:1},{a:2}], [{b:true}]] */ filters?: SimpleFilter | FilterExpression; /** * Function that returns dynamic filters. * Called before each API request. * Takes precedence over static `filters` prop when both provided. */ getFilters?: () => SimpleFilter | FilterExpression; /** * Array of dependency values that trigger data reload when changed. * Use with `getFilters` to implement cascading lookups. * * **IMPORTANT:** Values must be JSON-serializable (primitives, plain objects, arrays). * Do not include functions, circular references, or non-serializable values. * * @example * // Reload cities when countryId changes * ({ countryId: selectedCountryId })} * dependencies={[selectedCountryId]} * /> */ dependencies?: unknown[]; /** * Sort order for API results. * If not specified, defaults to first displayField ascending. * * @example * // Sort by name descending * */ orderBy?: OrderBy; /** * Additional fields to request from API but not display in dropdown. * These fields are available in the record passed to onChange and other callbacks. * Useful for accessing related data for business logic. * * @example * // Request email and phone for use in form logic * * entity="customers" * displayFields={['codigo', 'nombre']} * bindFields={['email', 'telefono', 'direccion']} * onChange={(customer) => { * console.log(customer?.email) // Available! * }} * /> */ bindFields?: (keyof TRecord & string)[]; /** * Makes the component read-only. * When true: selected value is displayed, dropdown cannot open, no clear button. * Useful for displaying selected values in view-only forms. * @default false */ readOnly?: boolean; /** * Completely disables the component. * When true: * - Cannot receive focus or be clicked * - Removed from tab navigation order (tabIndex=-1) * - Shows dimmed/grayed appearance (opacity: 0.5) * - Clear button is hidden * - No events fire (onChange, onFocus, onBlur) * - Validation is skipped (always valid) * * Use this for fields that are not applicable in the current context * (e.g., City field disabled until Country is selected). * * Note: Different from `readOnly` which allows focus for accessibility * but prevents editing. Use `readOnly` for view-only forms, * use `disabled` for conditionally unavailable fields. * * @default false */ disabled?: boolean; /** * Indicates validation error state. * When true: red border styling applied, aria-invalid="true". * Component remains interactive so user can fix the error. * @default false */ error?: boolean; /** * Callback when component receives focus. * Fires only once when focus enters the component, not on internal focus changes. */ onFocus?: (currentRecord: TRecord | null) => void; /** * Callback when component loses focus. * Fires only when focus leaves the entire component, not on internal focus moves. */ onBlur?: (currentRecord: TRecord | null) => void; /** * Callback when initial data loads. * Provides data info and imperative ref for programmatic control. * Fires only once per component mount. */ onReady?: (data: LookupFieldReadyData, ref: LookupFieldRef) => void; /** * Callback when value prop contains a non-existent GUID. * Use to handle cases where a controlled value no longer exists in the database. */ onInvalidValue?: (Id: string) => void; /** * Override text displayed when no results are found. * Bypasses i18n translation when provided. * @default Uses i18n translation for 'noResults' key */ noResultsText?: string; /** * Override placeholder text for the search input inside dropdown. * Bypasses i18n translation when provided. * @default Uses i18n translation for 'placeholder' key */ searchPlaceholder?: string; /** * Marks the field as required for form validation. * When true: shows asterisk indicator (unless showRequiredIndicator=false), * enables validation on blur (unless validateOnBlur=false), * and sets aria-required="true" for accessibility. * @default false */ required?: boolean; /** * Controls visibility of the required indicator (asterisk). * When undefined, defaults to the value of `required` prop. * Set to false to hide asterisk even when field is required. * Set to true to show asterisk even when field is not required (unusual). * @default undefined (follows `required` prop) */ showRequiredIndicator?: boolean; /** * Enables automatic validation when component loses focus. * When true and required, validates that a selection exists on blur. * Set to false for manual-only validation via ref.validate(). * @default true */ validateOnBlur?: boolean; /** * Callback fired when validation state changes. * Receives true when validation passes, false when it fails. * Useful for form-level validation state management. */ onValidationChange?: (isValid: boolean) => void; /** * Custom error message to display below the field. * When provided, shows a red error message text beneath the input. * Useful for form validation messages from external validation libraries. * @example * */ errorText?: string; /** * Enables the inline "create new" affordance — a "+" button next to the trigger that * opens a popup (modal or sidebar) hosting a creation form for this entity. Renders * the form content: call `onCreated(record)` when the form successfully creates a * record — LookupField selects it automatically (same as picking it from the * dropdown) and closes the popup; call `onCancel()` to close without changing the * selection. Omit to keep LookupField as a plain selection dropdown (no creation UI). */ onCreateNew?: (helpers: LookupFieldCreateHelpers) => ReactNode; /** * Popup shell style for the "create new" popup. * - `'modal'` — centered modal with overlay (default) * - `'sidebar'` — slide-over panel from the right edge * @default 'modal' */ createVariant?: 'modal' | 'sidebar'; /** Header title shown in the "create new" popup. @default 'Crear nuevo' */ createTitle?: string; /** aria-label / tooltip text for the "+" button. @default 'Crear nuevo' */ createButtonLabel?: string; /** * Optional icon rendered in a colored circular badge next to the "create new" popup * title (e.g. a lucide-react icon element). Purely decorative — omit for a plain header. */ createIcon?: ReactNode; /** * Whether the "+" button is actually rendered. Set to `false` to keep the rest of the * `create*` props configured (so it's ready to re-enable) while hiding the affordance * for this particular instance — e.g. gating creation by permission. * @default true */ showCreateButton?: boolean; /** * Custom "create new" popup width — any valid CSS size. Use relative units (`vw`, `%`, * `min(...)`) so it holds up across screen sizes; never a raw `px` value. Use this for * a form big enough to need real room (e.g. a tabbed master) — `'80vw'`. Omit for the * compact default (`32rem` for `'modal'`, `28rem` for `'sidebar'`). */ createWidth?: string; /** * Maximum "create new" popup height — same rules as `createWidth` (relative units * only). Omit for the compact default (`85vh` for `'modal'`, full height for * `'sidebar'`). Always a CAP, never a fixed height — the panel shrinks to fit its * content and only grows up to this value, so a form with few fields doesn't get * stretched into empty space. */ createHeight?: string; /** * Whether the "create new" popup renders its own title bar (icon + title + close * button) and default body padding. Defaults to `false` because `onCreateNew` is * almost always a form that already renders a complete header/actions of its own — * LookupField has no way to know whether it does, so it assumes it does. Set to * `true` only when `onCreateNew`'s content is genuinely headerless and should rely * on the popup's own title bar (using `createTitle`) instead. Escape-to-close and * the host's own cancel action (via `onCancel`) still close the popup either way. * @default false */ createShowHeader?: boolean; /** * Maximum number of records selectable at once. Only meaningful when `isMultiple` is * true; ignored in single-select mode. Once reached, the remaining unselected options * stay visible but disabled (NEVER-HIDE — same convention as MasterPatternView's * permission gates) and clicking one fires `onMaxSelectedReached` instead of selecting. */ maxSelected?: number; /** * Fired when the user tries to select another record while already at `maxSelected`. * LookupField itself has no toast system — wire this to your own notification if you * want to surface it; omit to make the click a silent no-op. */ onMaxSelectedReached?: (max: number) => void; } /** Single-select mode (default) — one selected record identified by its GUID. */ export interface LookupFieldSingleProps extends LookupFieldBaseProps { isMultiple?: false; /** Currently selected record ID (GUID) or null */ value: string | null; /** Callback when selection changes */ onChange: (record: TRecord | null, previousRecord: TRecord | null) => void; } /** * Multi-select mode — `value` is an array of GUIDs and the trigger renders each selected * record as a removable chip. Order of `value` is preserved in the resolved records. */ export interface LookupFieldMultipleProps extends LookupFieldBaseProps { isMultiple: true; /** Currently selected record IDs (GUIDs) */ value: string[]; /** Callback when the selection changes — receives the full new/previous selection, not a diff. */ onChange: (records: TRecord[], previousRecords: TRecord[]) => void; } /** Props for LookupField component — discriminated by `isMultiple`. */ export type LookupFieldProps = LookupFieldSingleProps | LookupFieldMultipleProps; //# sourceMappingURL=LookupField.types.d.ts.map