lead-to-policy/src/components/form/LookupField.tsx
2026-06-23 13:00:16 +05:30

123 lines
3.3 KiB
TypeScript

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;
}
type Row = Record<string, unknown>;
/** 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,
}: LookupFieldProps) {
const { client } = useZino();
const [open, setOpen] = useState(false);
const [search, setSearch] = useState('');
const [rows, setRows] = useState<Row[]>([]);
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<string>();
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 })
.then((r) => {
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]);
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 (
<PickerShell<Row>
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('');
}}
/>
);
}