sud-life-insurance/src/components/form/DatasetSelectField.tsx
2026-07-03 14:52:45 +05:30

111 lines
3.9 KiB
TypeScript

import { useEffect, useMemo, useRef, useState } from 'react';
import { SelectField, type SelectFieldOption } from '../core/SelectField';
import { useZino } from '../../api/provider';
import type { FieldRule } from '../../api/types';
export interface DatasetSelectFieldProps {
label?: string;
required?: boolean;
value: string;
onChange: (v: string) => void;
activityId: string;
fieldId: string;
instanceId?: number | string;
/** Current form values — sent to /dataset-options so the server applies the
* activity's filter_options (e.g. City options scoped to the chosen State). */
formData?: Record<string, unknown>;
/** Activity field_rules — used to find which fields this one depends on, so we
* only refetch when a dependency value changes (not on every keystroke). */
fieldRules?: FieldRule[];
placeholder?: string;
}
/** The form fields whose values drive this field's filter_options. */
function depFieldsFor(rules: FieldRule[] | undefined, fieldId: string): string[] {
const out = new Set<string>();
for (const r of rules ?? []) {
if (r.action !== 'filter_options' || r.active === false || r.target_field !== fieldId) continue;
for (const c of r.filter_config?.conditions ?? []) {
if (c.form_field) out.add(c.form_field);
}
}
return [...out];
}
/** Dataset-backed single-select. Fetches options from /dataset-options (the
* server applies the activity's filter_options against the dependency values we
* send), refetching when a dependency changes, and renders the shared
* SelectField so it looks identical to a static select. When a dependency
* changes and the current selection is no longer valid, it clears it. */
export function DatasetSelectField({
label,
required,
value,
onChange,
activityId,
fieldId,
instanceId,
formData,
fieldRules,
placeholder,
}: DatasetSelectFieldProps) {
const { client } = useZino();
const [options, setOptions] = useState<SelectFieldOption[]>([]);
const deps = useMemo(() => depFieldsFor(fieldRules, fieldId), [fieldRules, fieldId]);
// Stable key over the dependency VALUES — refetch only when these change.
const depKey = useMemo(
() => deps.map((f) => `${f}=${JSON.stringify(formData?.[f] ?? null)}`).join('&'),
[deps, formData],
);
// Send only the dependency values — enough to resolve filter_options server-side.
const depData = useMemo(() => {
const fd: Record<string, unknown> = {};
for (const f of deps) fd[f] = formData?.[f];
return fd;
}, [deps, formData]);
// Refs so stale-selection cleanup reads the latest value/onChange without
// re-running the fetch effect (which keys only on depKey).
const onChangeRef = useRef(onChange);
onChangeRef.current = onChange;
const valueRef = useRef(value);
valueRef.current = value;
const firstDepKey = useRef(depKey);
useEffect(() => {
let live = true;
client
.datasetOptions({ activityId, fieldId, instanceId, limit: 200, formData: depData })
.then((r) => {
if (!live) return;
const opts = (r.options ?? []).map((o) => ({ value: String(o.value), label: o.label }));
setOptions(opts);
// After a dependency actually CHANGED, drop a now-invalid selection
// (skip the initial load so a prefilled value isn't cleared).
if (depKey !== firstDepKey.current) {
const v = valueRef.current;
if (v && !opts.some((o) => o.value === String(v))) onChangeRef.current('');
}
})
.catch(() => {
if (live) setOptions([]);
});
return () => {
live = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [client, activityId, fieldId, instanceId, depKey]);
return (
<SelectField
label={label}
required={required}
value={String(value ?? '')}
onChange={onChange}
options={options}
placeholder={placeholder}
/>
);
}