1515 * Falls back to a flat property order when that key is missing.
1616 */
1717
18- import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
18+ import {
19+ createContext ,
20+ useCallback ,
21+ useContext ,
22+ useEffect ,
23+ useMemo ,
24+ useRef ,
25+ useState ,
26+ } from "react" ;
1927import DOMPurify from "dompurify" ;
2028import type { DynamicChoice , JsonSchema } from "../../runtime/runtime" ;
2129import { t } from "../../i18n/translate" ;
@@ -27,6 +35,13 @@ import { ArrayEditorDialog, normalizeToMatrix } from "../ArrayEditorDialog";
2735
2836type Values = Record < string , unknown > ;
2937
38+ /** Live ``display.active`` overrides keyed by item name. Populated by
39+ * ``resolveActive`` round-trips when the form contains items whose active
40+ * state is dynamic (``x-guidata-active-dynamic``). An entry of ``false``
41+ * greys out the corresponding field; ``true`` re-enables it; a missing
42+ * entry falls back to the static ``x-guidata-active`` baked in the schema. */
43+ const ActiveOverridesContext = createContext < Record < string , boolean > > ( { } ) ;
44+
3045/** Sanitise a guidata label before injecting it as HTML. Labels may
3146 * legitimately carry simple inline markup (``<b>``, ``<sub>``, units
3247 * like ``m<sup>2</sup>``), so we keep an HTML profile but strip any
@@ -53,6 +68,13 @@ export interface DataSetFormProps {
5368 itemName : string ,
5469 currentValues : Values ,
5570 ) => Promise < Values > ;
71+ /** Resolver for guidata ``display.active`` props (when the form has at
72+ * least one item carrying ``x-guidata-active-dynamic``). Runs the
73+ * ``active`` callables Python-side and returns ``{itemName: boolean}``
74+ * for every item, mirroring the Qt ``update_widgets`` enable/disable
75+ * cascade (e.g. ``BlobOpenCVParam.filter_by_circularity`` gating
76+ * ``min_circularity``). */
77+ resolveActive ?: ( currentValues : Values ) => Promise < Record < string , boolean > > ;
5678}
5779
5880// Layout node as emitted by the Python side. Either a property name
@@ -71,6 +93,7 @@ type LayoutNode = LayoutLeaf | LayoutContainer;
7193
7294export function DataSetForm ( props : DataSetFormProps ) {
7395 const { schema, values, onChange, resolveChoices, resolveCallbacks } = props ;
96+ const { resolveActive } = props ;
7497 const properties = useMemo (
7598 ( ) => ( schema . properties as Record < string , JsonSchema > ) ?? { } ,
7699 [ schema ] ,
@@ -80,10 +103,43 @@ export function DataSetForm(props: DataSetFormProps) {
80103 ( schema [ "x-guidata-property-order" ] as string [ ] | undefined ) ??
81104 Object . keys ( properties ) ;
82105
106+ // Whether any item's ``active`` state is dynamic. When true, every edit
107+ // may flip a sibling's enabled state (e.g. toggling
108+ // ``filter_by_circularity`` enables ``min_circularity``), so we re-run the
109+ // ``active`` callables Python-side after each change.
110+ const hasDynamicActive = useMemo (
111+ ( ) =>
112+ Object . values ( properties ) . some (
113+ ( p ) => p [ "x-guidata-active-dynamic" ] === true ,
114+ ) ,
115+ [ properties ] ,
116+ ) ;
117+
118+ // Live ``display.active`` overrides keyed by item name (see context doc).
119+ const [ activeOverrides , setActiveOverrides ] = useState <
120+ Record < string , boolean >
121+ > ( { } ) ;
122+
83123 // Monotonic counter guarding against out-of-order callback responses:
84124 // the last edit wins, so a slow Python round-trip can never clobber a
85125 // newer one (cf. the ``cancelled`` flag in ``useChoices``).
86126 const callbackSeq = useRef ( 0 ) ;
127+ const activeSeq = useRef ( 0 ) ;
128+
129+ // Resolve the initial active state once (covers dynamic items whose
130+ // default state depends on sibling defaults). Subsequent edits refresh it
131+ // through ``setValue`` below.
132+ useEffect ( ( ) => {
133+ if ( ! hasDynamicActive || ! resolveActive ) return ;
134+ const seq = ++ activeSeq . current ;
135+ resolveActive ( values ) . then ( ( map ) => {
136+ if ( seq !== activeSeq . current ) return ;
137+ if ( map ) setActiveOverrides ( map ) ;
138+ } ) ;
139+ // Run only on mount / when the resolver or schema shape changes; per-edit
140+ // refresh is handled in ``setValue``.
141+ // eslint-disable-next-line react-hooks/exhaustive-deps
142+ } , [ hasDynamicActive , resolveActive ] ) ;
87143
88144 const setValue = useCallback (
89145 ( name : string , value : unknown ) => {
@@ -99,23 +155,39 @@ export function DataSetForm(props: DataSetFormProps) {
99155 }
100156 } ) ;
101157 }
158+ if ( hasDynamicActive && resolveActive ) {
159+ const seq = ++ activeSeq . current ;
160+ resolveActive ( newValues ) . then ( ( map ) => {
161+ if ( seq !== activeSeq . current ) return ;
162+ if ( map ) setActiveOverrides ( map ) ;
163+ } ) ;
164+ }
102165 } ,
103- [ onChange , values , properties , resolveCallbacks ] ,
166+ [
167+ onChange ,
168+ values ,
169+ properties ,
170+ resolveCallbacks ,
171+ hasDynamicActive ,
172+ resolveActive ,
173+ ] ,
104174 ) ;
105175
106176 return (
107- < div className = "dataset-form" >
108- { layout . map ( ( node , idx ) => (
109- < LayoutNodeView
110- key = { idx }
111- node = { node }
112- properties = { properties }
113- values = { values }
114- setValue = { setValue }
115- resolveChoices = { resolveChoices }
116- />
117- ) ) }
118- </ div >
177+ < ActiveOverridesContext . Provider value = { activeOverrides } >
178+ < div className = "dataset-form" >
179+ { layout . map ( ( node , idx ) => (
180+ < LayoutNodeView
181+ key = { idx }
182+ node = { node }
183+ properties = { properties }
184+ values = { values }
185+ setValue = { setValue }
186+ resolveChoices = { resolveChoices }
187+ />
188+ ) ) }
189+ </ div >
190+ </ ActiveOverridesContext . Provider >
119191 ) ;
120192}
121193
@@ -251,12 +323,25 @@ function FieldRow(props: FieldRowProps) {
251323 const label = ( prop [ "x-guidata-label" ] as string | undefined ) ?? name ;
252324 const help = prop . description as string | undefined ;
253325 const unit = prop [ "x-guidata-unit" ] as string | undefined ;
326+ const activeOverrides = useContext ( ActiveOverridesContext ) ;
254327 // An item is non-editable when it is read-only (e.g. ``set_computed``)
255328 // or when its ``display.active`` resolved to ``False`` (e.g.
256- // ``ArithmeticParam.operation``, a preview field updated by callbacks).
257- const readOnly = prop . readOnly === true || prop [ "x-guidata-active" ] === false ;
329+ // ``ArithmeticParam.operation``, a preview field updated by callbacks, or
330+ // ``BlobOpenCVParam.min_circularity`` while its filter group is off). A
331+ // live override (from ``resolveActive``) wins over the static schema flag.
332+ const override = activeOverrides [ name ] ;
333+ const readOnly =
334+ prop . readOnly === true ||
335+ override === false ||
336+ ( override === undefined && prop [ "x-guidata-active" ] === false ) ;
258337 return (
259- < div className = "dataset-form-row" >
338+ < div
339+ className = {
340+ readOnly
341+ ? "dataset-form-row dataset-form-row-disabled"
342+ : "dataset-form-row"
343+ }
344+ >
260345 < label
261346 className = "dataset-form-label"
262347 title = { help }
0 commit comments