lead-to-policy/src/components/form/LookupField.tsx
Bhanu Prakash Sai Potteri 66d4c91f32 Initial import of lead-to-policy (dev env)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:28:12 +05:30

135 lines
4.1 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;
/** 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<string, unknown>;
}
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,
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<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, 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 (
<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('');
}}
/>
);
}