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; /** 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(); 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([]); 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 = {}; 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 ( ); }