import { useEffect, useState } from 'react'; import { PickerShell } from '../core/PickerShell'; import { useZino } from '../../api/provider'; export interface LookupValue { [col: string]: unknown; } export interface LookupFieldProps { label?: string; required?: boolean; templateUid: string; /** Columns joined to form the option label. */ displayCols: string[]; /** Columns persisted on the value object. */ storageCols: string[]; activityId: string; fieldId: string; instanceId?: number | string; value: LookupValue | null; onChange: (v: LookupValue | null) => void; /** Current form values — sent to the lookup endpoint so the server applies * the activity's field_rules filter_options (cascading lookups, e.g. owner * agents scoped to the picked region, regions scoped to the picked city). * Keys MUST match the rules' `form_field`s; lookup values stay objects so * the server can read a rule's `source_value_key` sub-column. */ formData?: Record; } type Row = Record; /** Searchable RDBMS-lookup picker (owner agent etc.). Mirrors the renderer's * FFLookupField: projects the picked row onto storage + display columns. * Renders through the shared PickerShell so it's identical to SelectField. */ export function LookupField({ label, required, templateUid, displayCols, storageCols, activityId, fieldId, instanceId, value, onChange, formData, }: LookupFieldProps) { const { client } = useZino(); // Serialized so the effect refetches when a dependency value changes. const formKey = JSON.stringify(formData ?? {}); const [open, setOpen] = useState(false); const [search, setSearch] = useState(''); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(false); const labelOf = (src: LookupValue | Row | null) => src ? displayCols .map((c) => src[c]) .filter((v) => v !== null && v !== undefined && v !== '') .join(', ') : ''; // Collapse rows that project to the same stored value — e.g. many branches // sharing one region show "North" once. Lookups whose storage carries a // unique id never collapse. const dedupe = (list: Row[]) => { const cols = storageCols.length ? storageCols : displayCols; const seen = new Set(); return list.filter((row) => { const sig = JSON.stringify(cols.map((c) => row[c] ?? null)); if (seen.has(sig)) return false; seen.add(sig); return true; }); }; // Debounced fetch while the dropdown is open. useEffect(() => { if (!open) return; let live = true; setLoading(true); const t = setTimeout(() => { client .lookupRecords(templateUid, { activityId, fieldId, instanceId, search, limit: 50, formData }) .then((r) => { // The server scopes rows via filter_options using the form_data we // sent (target columns like `city` aren't selected into the rows, so // they can only be filtered server-side). if (live) setRows(dedupe(r.records ?? [])); }) .catch(() => { if (live) setRows([]); }) .finally(() => { if (live) setLoading(false); }); }, 220); return () => { live = false; clearTimeout(t); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, search, templateUid, activityId, fieldId, instanceId, client, formKey]); function pick(row: Row) { const stored: LookupValue = {}; storageCols.forEach((c) => (stored[c] = row[c] ?? null)); displayCols.forEach((c) => { if (!(c in stored)) stored[c] = row[c] ?? null; }); onChange(stored); } return ( label={label} required={required} displayLabel={labelOf(value)} items={rows} renderItem={(row) => labelOf(row) || 'Row'} itemKey={(_row, i) => i} onPick={pick} onClear={() => onChange(null)} search={search} onSearch={setSearch} loading={loading} onOpenChange={(o) => { setOpen(o); if (!o) setSearch(''); }} /> ); }