@asteby/metacore-runtime-react 18.17.2 → 18.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 18.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - be0d2b8: Dependent (cascading) options for declarative pickers. A field/item_field may
8
+ declare `dependsOn` (camelCase) / `depends_on` (snake_case) naming another field
9
+ in the same action form — a header field (e.g. `source_warehouse_id`) or a
10
+ sibling row cell — whose current value scopes this picker's options. The value
11
+ is forwarded to the options endpoint as `filter_value` (`useOptionsResolver`
12
+ gains a `filterValue` arg) and the picker re-fetches when it changes, clearing
13
+ the stale selection. While the depended-on field is empty the picker is disabled
14
+ with an overridable hint. Header form context flows down through
15
+ `DynamicLineItems` → `CellRenderer`/`RefCell` so a line-items cell can depend on
16
+ a header field, not just same-row values. Option `description` (e.g. available
17
+ qty) is now shown in the line-items `RefCell` select as well as
18
+ `DynamicSelectField`.
19
+
20
+ A field/item_field may also carry an `optionsConfig` (camelCase) /
21
+ `options_config` (snake_case) object — the kernel's enriched options routing,
22
+ shaped `{ type, source, filter_by, value, label_ref, description }`. When it
23
+ declares a `source`, the picker queries that SOURCE model instead of the field's
24
+ `ref`: URL `/options/<source>` with query field `<value ?? field.key>` and the
25
+ cascade `filter_value`. Without `optionsConfig.source` the picker keeps its
26
+ `ref`-based behaviour (retrocompat). New `getOptionsConfig` / `resolveOptionsSource`
27
+ helpers (and `FieldOptionsConfig` type) are exported. Fully generic — no domain
28
+ knowledge in the SDK.
29
+
30
+ ## 18.17.3
31
+
32
+ ### Patch Changes
33
+
34
+ - d2c92e1: Fix React #310: move flatten/order useMemo before the empty-state early return (conditional hook crashed on empty→populated)
35
+
3
36
  ## 18.17.2
4
37
 
5
38
  ### Patch Changes
@@ -18,7 +18,7 @@ import { DynamicLineItems } from './dynamic-line-items';
18
18
  import { DynamicSelectField } from './dynamic-select-field';
19
19
  import { DynamicDateField } from './dynamic-date-field';
20
20
  import { UploadField } from './upload-field';
21
- import { isLineItemsField, resolveWidget } from './dynamic-form-schema';
21
+ import { isLineItemsField, resolveWidget, resolveDependsValue, getDependsOn } from './dynamic-form-schema';
22
22
  // Canonical registry lives in @asteby/metacore-sdk
23
23
  import { getActionComponent, } from '@asteby/metacore-sdk';
24
24
  export function ActionModalDispatcher({ open, onOpenChange, action, model, record, endpoint, onSuccess, }) {
@@ -149,13 +149,19 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
149
149
  const fullWidth = isLineItemsField(field) ||
150
150
  resolveWidget(field) === 'textarea' ||
151
151
  resolveWidget(field) === 'richtext';
152
- return (_jsxs("div", { className: 'grid gap-2 ' + (fullWidth ? 'sm:col-span-2' : ''), children: [_jsxs(Label, { htmlFor: field.key, children: [field.label, field.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }), renderField(field, formData[field.key], (v) => updateField(field.key, v))] }, field.key));
152
+ return (_jsxs("div", { className: 'grid gap-2 ' + (fullWidth ? 'sm:col-span-2' : ''), children: [_jsxs(Label, { htmlFor: field.key, children: [field.label, field.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }), renderField(field, formData[field.key], (v) => updateField(field.key, v), formData)] }, field.key));
153
153
  }) }), _jsxs(DialogFooter, { className: "shrink-0", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') }), _jsxs(Button, { onClick: execute, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), action.label] })] })] }) }));
154
154
  }
155
- function renderField(field, value, onChange) {
155
+ function renderField(field, value, onChange,
156
+ // Full current form values — lets a line-items grid (and any cascading
157
+ // header picker) resolve a `dependsOn` reference against sibling header
158
+ // fields. Omitted by callers that have no surrounding form (the field is
159
+ // then treated as having no resolvable dependency).
160
+ formValues) {
156
161
  // Repeatable line-items group → row grid (value is an array of row objects).
162
+ // The header form values flow in so a cell can depend on a header field.
157
163
  if (isLineItemsField(field)) {
158
- return _jsx(DynamicLineItems, { field: field, value: value, onChange: onChange });
164
+ return _jsx(DynamicLineItems, { field: field, value: value, onChange: onChange, formValues: formValues });
159
165
  }
160
166
  // Resolve the widget the same way DynamicForm does (explicit widget wins,
161
167
  // else inferred from type) so action modals and the standalone form stay in
@@ -163,7 +169,12 @@ function renderField(field, value, onChange) {
163
169
  // dropped `dynamic_select` to a plain text input.
164
170
  const widget = resolveWidget(field);
165
171
  if (widget === 'dynamic_select') {
166
- return _jsx(DynamicSelectField, { field: field, value: value, onChange: onChange });
172
+ // A header-level dynamic_select may itself depend on another header
173
+ // field; resolve its filter_value from the form context.
174
+ const dependsValue = getDependsOn(field)
175
+ ? resolveDependsValue(field, formValues)
176
+ : undefined;
177
+ return _jsx(DynamicSelectField, { field: field, value: value, onChange: onChange, dependsValue: dependsValue });
167
178
  }
168
179
  // File upload → themed picker that POSTs the file to the host upload
169
180
  // endpoint and stores the returned url/path. Kept in sync with DynamicForm.
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard-grid.d.ts","sourceRoot":"","sources":["../src/dashboard-grid.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B,OAAO,KAAK,EACR,kBAAkB,EAElB,oBAAoB,EACpB,mBAAmB,EACtB,MAAM,mBAAmB,CAAA;AAW1B,uEAAuE;AACvE,wBAAgB,eAAe,CAC3B,MAAM,CAAC,EAAE,oBAAoB,EAAE,EAC/B,OAAO,CAAC,EAAE,mBAAmB,EAAE,GAChC,oBAAoB,EAAE,CAgBxB;AASD,wBAAgB,aAAa,CAAC,EAC1B,MAAM,EACN,OAAO,EACP,QAAQ,EACR,OAAO,EACP,MAAM,EACN,QAAQ,EACR,SAAS,EACT,OAAO,GACV,EAAE,kBAAkB,qBAkJpB"}
1
+ {"version":3,"file":"dashboard-grid.d.ts","sourceRoot":"","sources":["../src/dashboard-grid.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B,OAAO,KAAK,EACR,kBAAkB,EAElB,oBAAoB,EACpB,mBAAmB,EACtB,MAAM,mBAAmB,CAAA;AAW1B,uEAAuE;AACvE,wBAAgB,eAAe,CAC3B,MAAM,CAAC,EAAE,oBAAoB,EAAE,EAC/B,OAAO,CAAC,EAAE,mBAAmB,EAAE,GAChC,oBAAoB,EAAE,CAgBxB;AASD,wBAAgB,aAAa,CAAC,EAC1B,MAAM,EACN,OAAO,EACP,QAAQ,EACR,OAAO,EACP,MAAM,EACN,QAAQ,EACR,SAAS,EACT,OAAO,GACV,EAAE,kBAAkB,qBAiJpB"}
@@ -106,15 +106,10 @@ export function DashboardGrid({ groups, widgets, loadData, isAdmin, locale, curr
106
106
  };
107
107
  // eslint-disable-next-line react-hooks/exhaustive-deps
108
108
  }, [keySig, loadData]);
109
- // Global empty state (no widgets at all / none visible after gating).
110
- if (visibleGroups.length === 0) {
111
- return (_jsxs("div", { "data-testid": "dashboard-empty", className: cn('flex min-h-[40vh] flex-col items-center justify-center rounded-xl border border-dashed border-border/60 p-10 text-center', className), children: [_jsx("div", { className: "mb-4 flex size-14 items-center justify-center rounded-2xl bg-muted text-muted-foreground", children: _jsx(DynamicIcon, { name: "LayoutDashboard", className: "size-7" }) }), _jsx("h3", { className: "text-base font-semibold text-foreground", children: tr(undefined, s.emptyTitle) || s.emptyTitle }), _jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: s.emptyDescription })] }));
112
- }
113
- // ONE unified dense grid across every group. Per-group sections used to
114
- // break the layout into rows, so a lone-widget group (e.g. a single KPI)
115
- // left the rest of its row blank. Flattening + `grid-flow-row-dense`
116
- // backfills those holes; ordering compact KPIs before charts makes the top
117
- // read as a metric band and the charts mosaic below it. No blank space.
109
+ // Flatten every group into ONE ordered list (compact KPIs before charts) for
110
+ // the masonry grid. MUST run before any early return — it is a hook, and a
111
+ // conditional hook (placed after the empty-state return) trips React #310
112
+ // when the dashboard transitions empty → populated.
118
113
  const ordered = React.useMemo(() => {
119
114
  const flat = visibleGroups.flatMap((g) => g.widgets);
120
115
  return flat
@@ -123,6 +118,10 @@ export function DashboardGrid({ groups, widgets, loadData, isAdmin, locale, curr
123
118
  a.i - b.i)
124
119
  .map((x) => x.w);
125
120
  }, [visibleGroups]);
121
+ // Global empty state (no widgets at all / none visible after gating).
122
+ if (visibleGroups.length === 0) {
123
+ return (_jsxs("div", { "data-testid": "dashboard-empty", className: cn('flex min-h-[40vh] flex-col items-center justify-center rounded-xl border border-dashed border-border/60 p-10 text-center', className), children: [_jsx("div", { className: "mb-4 flex size-14 items-center justify-center rounded-2xl bg-muted text-muted-foreground", children: _jsx(DynamicIcon, { name: "LayoutDashboard", className: "size-7" }) }), _jsx("h3", { className: "text-base font-semibold text-foreground", children: tr(undefined, s.emptyTitle) || s.emptyTitle }), _jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: s.emptyDescription })] }));
124
+ }
126
125
  return (_jsx("div", { "data-testid": "dashboard-grid", className: cn(
127
126
  // Masonry: balanced CSS columns. Cards take their natural height
128
127
  // (compact stats, taller charts) and flow to equalize column
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import type { ActionFieldDef } from './types';
2
+ import type { ActionFieldDef, FieldOptionsConfig } from './types';
3
3
  /**
4
4
  * Apps register validator implementations by slug. The slug is the value
5
5
  * `OrgConfig.validators[<key>]` returns for a $org.<key> reference.
@@ -63,6 +63,46 @@ export declare function resolveWidget(field: ActionFieldDef): string;
63
63
  export declare function getFieldRef(field: ActionFieldDef): string | undefined;
64
64
  /** True when a field declares an FK target the SDK can resolve options against. */
65
65
  export declare function fieldHasRef(field: ActionFieldDef): boolean;
66
+ /**
67
+ * Resolves a field's cascade dependency — the key of another form field whose
68
+ * current value scopes this picker's options (`filter_value`). Tolerates the
69
+ * camelCase `dependsOn` (authored SDK shape) and the snake_case `depends_on`
70
+ * the kernel manifest serves. Returns the trimmed field key, or `undefined`
71
+ * when the field declares no dependency.
72
+ */
73
+ export declare function getDependsOn(field: ActionFieldDef): string | undefined;
74
+ /**
75
+ * Resolves the cascade `filter_value` for a field from the surrounding form
76
+ * context. The depended-on key is matched against the current row first (a
77
+ * sibling item-field on the same line) and then the header form values, so a
78
+ * line-items cell can depend on either a sibling cell OR a header field (e.g.
79
+ * `source_warehouse_id`). Returns the stringified value, or `''` when the
80
+ * field has no dependency or the depended-on value is empty/unset.
81
+ */
82
+ export declare function resolveDependsValue(field: ActionFieldDef, formValues?: Record<string, any> | null, rowValues?: Record<string, any> | null): string;
83
+ /**
84
+ * Reads a field's enriched options-resolution config, tolerating the camelCase
85
+ * `optionsConfig` (authored SDK shape) and the snake_case `options_config` the
86
+ * kernel manifest serves. Returns `undefined` when the field declares none.
87
+ */
88
+ export declare function getOptionsConfig(field: ActionFieldDef): FieldOptionsConfig | undefined;
89
+ /**
90
+ * Resolves where a picker should fetch its options from, honouring an
91
+ * `optionsConfig.source` (the dependent/scoped routing the kernel serves) and
92
+ * falling back to the field's `ref` for retrocompat.
93
+ *
94
+ * - With `optionsConfig.source`: query the SOURCE model →
95
+ * `{ endpoint: '/options/<source>', fieldKey: <value ?? field.key> }`.
96
+ * - Without it: keep `ref`-based resolution → `{ ref }` (the hook's canonical
97
+ * path), `fieldKey` defaulting to `'id'`.
98
+ *
99
+ * The returned shape feeds straight into `useOptionsResolver` args.
100
+ */
101
+ export declare function resolveOptionsSource(field: ActionFieldDef): {
102
+ endpoint?: string;
103
+ ref?: string;
104
+ fieldKey: string;
105
+ };
66
106
  /**
67
107
  * Normalizes an upload field's config, tolerating both the camelCase authored
68
108
  * SDK shape and the snake_case the kernel serves (`max_size`, `storage_path`).
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-form-schema.d.ts","sourceRoot":"","sources":["../src/dynamic-form-schema.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAmB,MAAM,KAAK,CAAA;AACxC,OAAO,KAAK,EAAE,cAAc,EAAmB,MAAM,SAAS,CAAA;AAiB9D;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,SAAS,GAAG,IAAI,CAEzF;AAcD,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE;;kBAMtD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,EAAE,CAGrE;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,cAAc,GACtB;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAatG;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAO3C;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACjC,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,GAAG,EAAE,GAAG,SAAS,GACxB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWxB;AAED,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC3B,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,GAAG,EAAE,GAAG,SAAS,GACxB,YAAY,GAAG,SAAS,CAgB1B;AAqDD,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CA4B3D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS,CAIrE;AAED,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAE1D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,cAAc,GAAG;IACpD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB,CAaA"}
1
+ {"version":3,"file":"dynamic-form-schema.d.ts","sourceRoot":"","sources":["../src/dynamic-form-schema.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAmB,MAAM,KAAK,CAAA;AACxC,OAAO,KAAK,EAAE,cAAc,EAAmB,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAiBlF;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,SAAS,GAAG,IAAI,CAEzF;AAcD,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE;;kBAMtD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,EAAE,CAGrE;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,cAAc,GACtB;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAatG;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAO3C;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACjC,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,GAAG,EAAE,GAAG,SAAS,GACxB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWxB;AAED,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC3B,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,GAAG,EAAE,GAAG,SAAS,GACxB,YAAY,GAAG,SAAS,CAgB1B;AAqDD,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CA4B3D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS,CAIrE;AAED,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAE1D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS,CAItE;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAC/B,KAAK,EAAE,cAAc,EACrB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,EACvC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,GACvC,MAAM,CAQR;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,kBAAkB,GAAG,SAAS,CAGtF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,cAAc,GAAG;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;CACnB,CAQA;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,cAAc,GAAG;IACpD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB,CAaA"}
@@ -234,6 +234,67 @@ export function getFieldRef(field) {
234
234
  export function fieldHasRef(field) {
235
235
  return getFieldRef(field) !== undefined;
236
236
  }
237
+ /**
238
+ * Resolves a field's cascade dependency — the key of another form field whose
239
+ * current value scopes this picker's options (`filter_value`). Tolerates the
240
+ * camelCase `dependsOn` (authored SDK shape) and the snake_case `depends_on`
241
+ * the kernel manifest serves. Returns the trimmed field key, or `undefined`
242
+ * when the field declares no dependency.
243
+ */
244
+ export function getDependsOn(field) {
245
+ const dep = field.dependsOn ?? field.depends_on;
246
+ if (typeof dep === 'string' && dep.trim() !== '')
247
+ return dep.trim();
248
+ return undefined;
249
+ }
250
+ /**
251
+ * Resolves the cascade `filter_value` for a field from the surrounding form
252
+ * context. The depended-on key is matched against the current row first (a
253
+ * sibling item-field on the same line) and then the header form values, so a
254
+ * line-items cell can depend on either a sibling cell OR a header field (e.g.
255
+ * `source_warehouse_id`). Returns the stringified value, or `''` when the
256
+ * field has no dependency or the depended-on value is empty/unset.
257
+ */
258
+ export function resolveDependsValue(field, formValues, rowValues) {
259
+ const dep = getDependsOn(field);
260
+ if (!dep)
261
+ return '';
262
+ const raw = (rowValues && rowValues[dep] != null && rowValues[dep] !== '' ? rowValues[dep] : undefined) ??
263
+ (formValues ? formValues[dep] : undefined);
264
+ if (raw == null || raw === '')
265
+ return '';
266
+ return String(raw);
267
+ }
268
+ /**
269
+ * Reads a field's enriched options-resolution config, tolerating the camelCase
270
+ * `optionsConfig` (authored SDK shape) and the snake_case `options_config` the
271
+ * kernel manifest serves. Returns `undefined` when the field declares none.
272
+ */
273
+ export function getOptionsConfig(field) {
274
+ const cfg = field.optionsConfig ?? field.options_config;
275
+ return cfg && typeof cfg === 'object' ? cfg : undefined;
276
+ }
277
+ /**
278
+ * Resolves where a picker should fetch its options from, honouring an
279
+ * `optionsConfig.source` (the dependent/scoped routing the kernel serves) and
280
+ * falling back to the field's `ref` for retrocompat.
281
+ *
282
+ * - With `optionsConfig.source`: query the SOURCE model →
283
+ * `{ endpoint: '/options/<source>', fieldKey: <value ?? field.key> }`.
284
+ * - Without it: keep `ref`-based resolution → `{ ref }` (the hook's canonical
285
+ * path), `fieldKey` defaulting to `'id'`.
286
+ *
287
+ * The returned shape feeds straight into `useOptionsResolver` args.
288
+ */
289
+ export function resolveOptionsSource(field) {
290
+ const cfg = getOptionsConfig(field);
291
+ const source = typeof cfg?.source === 'string' ? cfg.source.trim() : '';
292
+ if (source) {
293
+ const value = typeof cfg?.value === 'string' && cfg.value.trim() !== '' ? cfg.value.trim() : field.key;
294
+ return { endpoint: `/options/${source}`, fieldKey: value };
295
+ }
296
+ return { ref: getFieldRef(field), fieldKey: 'id' };
297
+ }
237
298
  /**
238
299
  * Normalizes an upload field's config, tolerating both the camelCase authored
239
300
  * SDK shape and the snake_case the kernel serves (`max_size`, `storage_path`).
@@ -4,6 +4,12 @@ export interface DynamicLineItemsProps {
4
4
  value: any[] | undefined;
5
5
  onChange: (rows: any[]) => void;
6
6
  disabled?: boolean;
7
+ /**
8
+ * Current values of the surrounding (header) form. Threaded into each cell
9
+ * so a cell field with `dependsOn` can scope its options by a HEADER field
10
+ * (e.g. `source_warehouse_id`), not just a sibling cell on the same row.
11
+ */
12
+ formValues?: Record<string, any>;
7
13
  }
8
- export declare function DynamicLineItems({ field, value, onChange, disabled }: DynamicLineItemsProps): import("react").JSX.Element;
14
+ export declare function DynamicLineItems({ field, value, onChange, disabled, formValues }: DynamicLineItemsProps): import("react").JSX.Element;
9
15
  //# sourceMappingURL=dynamic-line-items.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-line-items.d.ts","sourceRoot":"","sources":["../src/dynamic-line-items.tsx"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAW7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,EAAE,GAAG,SAAS,CAAA;IACxB,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAkBD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAgB,EAAE,EAAE,qBAAqB,+BAiJnG"}
1
+ {"version":3,"file":"dynamic-line-items.d.ts","sourceRoot":"","sources":["../src/dynamic-line-items.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAe7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,EAAE,GAAG,SAAS,CAAA;IACxB,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACnC;AAkBD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAgB,EAAE,UAAU,EAAE,EAAE,qBAAqB,+BAmJ/G"}
@@ -9,9 +9,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
9
  // row controls mutate the array; each cell is a widget resolved via
10
10
  // `resolveWidget`, matching the flat-field renderer in dynamic-form.tsx.
11
11
  import { Input, Textarea, Switch, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@asteby/metacore-ui/primitives';
12
+ import { useEffect, useRef } from 'react';
12
13
  import { Plus, Trash2, Check } from 'lucide-react';
13
- import { resolveWidget, getItemFields, computeLineItemTotals, evaluateBalance, toNumber, } from './dynamic-form-schema';
14
- import { DynamicSelectField } from './dynamic-select-field';
14
+ import { resolveWidget, getItemFields, computeLineItemTotals, evaluateBalance, toNumber, getDependsOn, resolveDependsValue, getOptionsConfig, resolveOptionsSource, } from './dynamic-form-schema';
15
+ import { DynamicSelectField, DEFAULT_DEPENDS_HINT } from './dynamic-select-field';
15
16
  import { useOptionsResolver } from './use-options-resolver';
16
17
  const fmtNumber = (n) => n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
17
18
  /** Numeric columns render right-aligned (debit/credit/amount feel). */
@@ -25,7 +26,7 @@ function emptyRow(itemFields) {
25
26
  }
26
27
  return row;
27
28
  }
28
- export function DynamicLineItems({ field, value, onChange, disabled = false }) {
29
+ export function DynamicLineItems({ field, value, onChange, disabled = false, formValues }) {
29
30
  const itemFields = getItemFields(field);
30
31
  const rows = Array.isArray(value) ? value : [];
31
32
  // Columns flagged `total` get a per-column sum in the footer; the balance
@@ -59,7 +60,7 @@ export function DynamicLineItems({ field, value, onChange, disabled = false }) {
59
60
  updateCell(idx, key, cellValue);
60
61
  };
61
62
  return (_jsxs("div", { className: "grid gap-2", "data-widget": "line_items", children: [_jsx("div", { className: "overflow-x-auto rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50", children: _jsxs("tr", { children: [itemFields.map((col) => (_jsxs("th", { className: 'px-3 py-2 font-medium ' +
62
- (isNumericCol(col) ? 'text-right' : 'text-left'), children: [col.label, col.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }, col.key))), _jsx("th", { className: "w-12 px-3 py-2", "aria-label": "acciones" })] }) }), _jsxs("tbody", { children: [rows.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: itemFields.length + 1, className: "px-3 py-4 text-center text-muted-foreground", children: "Sin renglones" }) })), rows.map((row, idx) => (_jsxs("tr", { className: "border-t align-top", children: [itemFields.map((col) => (_jsx("td", { className: "px-2 py-1.5", children: _jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), disabled: disabled }) }, col.key))), _jsx("td", { className: "px-2 py-1.5 text-center", children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeRow(idx), disabled: disabled, "aria-label": "Eliminar rengl\u00F3n", children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }) })] }, idx)))] }), hasTotals && rows.length > 0 && (_jsx("tfoot", { className: "border-t bg-muted/30", children: _jsxs("tr", { children: [itemFields.map((col, ci) => {
63
+ (isNumericCol(col) ? 'text-right' : 'text-left'), children: [col.label, col.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }, col.key))), _jsx("th", { className: "w-12 px-3 py-2", "aria-label": "acciones" })] }) }), _jsxs("tbody", { children: [rows.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: itemFields.length + 1, className: "px-3 py-4 text-center text-muted-foreground", children: "Sin renglones" }) })), rows.map((row, idx) => (_jsxs("tr", { className: "border-t align-top", children: [itemFields.map((col) => (_jsx("td", { className: "px-2 py-1.5", children: _jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), disabled: disabled, formValues: formValues, rowValues: row }) }, col.key))), _jsx("td", { className: "px-2 py-1.5 text-center", children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeRow(idx), disabled: disabled, "aria-label": "Eliminar rengl\u00F3n", children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }) })] }, idx)))] }), hasTotals && rows.length > 0 && (_jsx("tfoot", { className: "border-t bg-muted/30", children: _jsxs("tr", { children: [itemFields.map((col, ci) => {
63
64
  if (ci === 0) {
64
65
  return (_jsx("td", { className: "px-3 py-2 text-left font-medium text-muted-foreground", children: "Totales" }, col.key));
65
66
  }
@@ -80,15 +81,20 @@ function BalanceBadge({ state, }) {
80
81
  // without the per-field Label (the column header is the label) and sized for a
81
82
  // table cell. Nested line-items inside a row are not supported (a row column is
82
83
  // a scalar widget).
83
- function CellRenderer({ field, value, onChange, disabled }) {
84
+ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues }) {
84
85
  const widget = resolveWidget(field);
86
+ // Cascade scope for a cell with `dependsOn`: resolved from this row first
87
+ // (a sibling cell) then the header form (e.g. `source_warehouse_id`).
88
+ const dependsValue = getDependsOn(field)
89
+ ? resolveDependsValue(field, formValues, rowValues)
90
+ : undefined;
85
91
  // Async searchable picker per row cell — e.g. the account_id column of a
86
92
  // journal entry's debit/credit lines. Same widget as the flat form.
87
93
  if (widget === 'dynamic_select') {
88
- return _jsx(DynamicSelectField, { field: field, value: value, onChange: onChange });
94
+ return _jsx(DynamicSelectField, { field: field, value: value, onChange: onChange, dependsValue: dependsValue });
89
95
  }
90
- if (widget === 'select' && field.ref) {
91
- return _jsx(RefCell, { field: field, value: value, onChange: onChange, disabled: disabled });
96
+ if (widget === 'select' && (field.ref || getOptionsConfig(field)?.source)) {
97
+ return (_jsx(RefCell, { field: field, value: value, onChange: onChange, disabled: disabled, formValues: formValues, rowValues: rowValues }));
92
98
  }
93
99
  switch (widget) {
94
100
  case 'textarea':
@@ -108,11 +114,39 @@ function CellRenderer({ field, value, onChange, disabled }) {
108
114
  return (_jsx(Input, { type: field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text', value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, disabled: disabled }));
109
115
  }
110
116
  }
111
- function RefCell({ field, value, onChange, disabled }) {
117
+ function RefCell({ field, value, onChange, disabled, formValues, rowValues }) {
118
+ // Cascade: resolve the value of the field this cell `dependsOn` from the
119
+ // row (sibling) first, then the header form. While empty, the picker is
120
+ // disabled with a hint instead of listing the whole (unscoped) table.
121
+ const dependsOn = getDependsOn(field);
122
+ const scope = dependsOn ? resolveDependsValue(field, formValues, rowValues) : '';
123
+ const blockedByDependency = !!dependsOn && scope === '';
124
+ // optionsConfig.source → query the source model (`/options/<source>` with
125
+ // `field=<value>`); else fall back to the field's `ref`.
126
+ const optSource = resolveOptionsSource(field);
112
127
  const { options, loading } = useOptionsResolver({
113
128
  modelKey: '',
114
- fieldKey: 'id',
115
- ref: field.ref,
129
+ fieldKey: optSource.fieldKey,
130
+ ref: optSource.ref,
131
+ endpoint: optSource.endpoint,
132
+ filterValue: dependsOn ? scope : undefined,
133
+ enabled: !blockedByDependency,
116
134
  });
117
- return (_jsxs(Select, { value: value || '', onValueChange: onChange, disabled: disabled || loading, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: loading ? 'Cargando…' : field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: options.map((opt) => (_jsx(SelectItem, { value: String(opt.id), children: opt.label }, String(opt.id)))) })] }));
135
+ // Clear the selection when the parent scope changes (skip initial mount).
136
+ const prevScopeRef = useRef(scope);
137
+ useEffect(() => {
138
+ if (!dependsOn)
139
+ return;
140
+ if (prevScopeRef.current !== scope) {
141
+ prevScopeRef.current = scope;
142
+ if (value)
143
+ onChange('');
144
+ }
145
+ }, [dependsOn, scope, value, onChange]);
146
+ const placeholder = blockedByDependency
147
+ ? DEFAULT_DEPENDS_HINT
148
+ : loading
149
+ ? 'Cargando…'
150
+ : field.placeholder || 'Seleccionar...';
151
+ return (_jsxs(Select, { value: value || '', onValueChange: onChange, disabled: disabled || loading || blockedByDependency, children: [_jsx(SelectTrigger, { className: "w-full", "data-depends-blocked": blockedByDependency ? '' : undefined, children: _jsx(SelectValue, { placeholder: placeholder }) }), _jsx(SelectContent, { children: options.map((opt) => (_jsx(SelectItem, { value: String(opt.id), children: _jsxs("span", { className: "flex flex-col", children: [_jsx("span", { className: "truncate", children: opt.label }), opt.description && (_jsx("span", { className: "text-muted-foreground truncate text-xs", children: opt.description }))] }) }, String(opt.id)))) })] }));
118
152
  }
@@ -1,5 +1,11 @@
1
1
  import { type ResolvedOption } from './use-options-resolver';
2
2
  import type { ActionFieldDef } from './types';
3
+ /**
4
+ * Default hint shown when a cascading picker's depended-on field is still
5
+ * empty. Domain-neutral on purpose; a caller may override it per field via
6
+ * `dependsHint`.
7
+ */
8
+ export declare const DEFAULT_DEPENDS_HINT = "Selecciona primero el campo del que depende";
3
9
  /**
4
10
  * Small square thumbnail for an option's `image`. Falls back to a neutral
5
11
  * placeholder icon when the option has no image so rows/triggers stay aligned.
@@ -31,7 +37,17 @@ export interface DynamicSelectFieldProps {
31
37
  * a lookup (which only loads once the popover opens). Matched by id == value.
32
38
  */
33
39
  seedOption?: ResolvedOption | null;
40
+ /**
41
+ * Cascade scope: the current value of the field this picker `dependsOn`
42
+ * (the caller resolves it from the form context). Forwarded as
43
+ * `filter_value`. When the field declares a `dependsOn` and this is empty,
44
+ * the picker is disabled with `dependsHint` and the current selection is
45
+ * cleared. Changing it re-fetches and clears the selection.
46
+ */
47
+ dependsValue?: string;
48
+ /** Overrides the disabled-state hint shown while `dependsValue` is empty. */
49
+ dependsHint?: string;
34
50
  }
35
- export declare function DynamicSelectField({ field, value, onChange, seedOption }: DynamicSelectFieldProps): import("react").JSX.Element;
51
+ export declare function DynamicSelectField({ field, value, onChange, seedOption, dependsValue, dependsHint, }: DynamicSelectFieldProps): import("react").JSX.Element;
36
52
  export default DynamicSelectField;
37
53
  //# sourceMappingURL=dynamic-select-field.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AAuCA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,IAAS,EAAE,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,+BA4BzF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EACvB,MAAM,EACN,IAAS,GACZ,EAAE;IACC,MAAM,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC,GAAG,IAAI,CAAA;IAChE,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,sCAwBA;AAqBD,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAC1B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,cAAc,GAAG,IAAI,CAAA;CACrC;AAED,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,uBAAuB,+BA2KjG;AAED,eAAe,kBAAkB,CAAA"}
1
+ {"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AAuCA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,gDAAgD,CAAA;AAEjF;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,IAAS,EAAE,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,+BA4BzF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EACvB,MAAM,EACN,IAAS,GACZ,EAAE;IACC,MAAM,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC,GAAG,IAAI,CAAA;IAChE,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,sCAwBA;AAqBD,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAC1B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,cAAc,GAAG,IAAI,CAAA;IAClC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wBAAgB,kBAAkB,CAAC,EAC/B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,WAAW,GACd,EAAE,uBAAuB,+BA4MzB;AAED,eAAe,kBAAkB,CAAA"}
@@ -22,13 +22,19 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
22
22
  // in a fetched page (we match by id against loaded options, else show the raw
23
23
  // value). A dedicated `?ids=` lookup is a follow-up; create flows — the common
24
24
  // case — start empty and never hit this.
25
- import { useEffect, useState } from 'react';
25
+ import { useEffect, useRef, useState } from 'react';
26
26
  import { Button, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Popover, PopoverContent, PopoverTrigger, } from '@asteby/metacore-ui/primitives';
27
27
  import { Check, ChevronsUpDown, ImageIcon, Loader2, Plus } from 'lucide-react';
28
28
  import { resolveColorCss } from '@asteby/metacore-ui/lib';
29
29
  import { DynamicIcon } from './dynamic-icon';
30
30
  import { useOptionsResolver } from './use-options-resolver';
31
- import { getFieldRef } from './dynamic-form-schema';
31
+ import { getDependsOn, getFieldRef, resolveOptionsSource } from './dynamic-form-schema';
32
+ /**
33
+ * Default hint shown when a cascading picker's depended-on field is still
34
+ * empty. Domain-neutral on purpose; a caller may override it per field via
35
+ * `dependsHint`.
36
+ */
37
+ export const DEFAULT_DEPENDS_HINT = 'Selecciona primero el campo del que depende';
32
38
  /**
33
39
  * Small square thumbnail for an option's `image`. Falls back to a neutral
34
40
  * placeholder icon when the option has no image so rows/triggers stay aligned.
@@ -79,7 +85,7 @@ function useDebounced(value, ms) {
79
85
  }, [value, ms]);
80
86
  return debounced;
81
87
  }
82
- export function DynamicSelectField({ field, value, onChange, seedOption }) {
88
+ export function DynamicSelectField({ field, value, onChange, seedOption, dependsValue, dependsHint, }) {
83
89
  const [open, setOpen] = useState(false);
84
90
  const [search, setSearch] = useState('');
85
91
  const debounced = useDebounced(search, 250);
@@ -89,19 +95,47 @@ export function DynamicSelectField({ field, value, onChange, seedOption }) {
89
95
  // Tolerate the snake_case `source`/`relation` aliases the kernel may serve
90
96
  // for the FK target, not just camelCase `ref`.
91
97
  const fieldRef = getFieldRef(field);
98
+ // Options routing: an `optionsConfig.source` (dependent/scoped picker) wins
99
+ // over the field's `ref` — query the SOURCE model with `field=<value>`;
100
+ // otherwise keep the canonical `ref`-based resolution.
101
+ const source = resolveOptionsSource(field);
102
+ // Cascade: a `dependsOn` field whose value is still empty leaves this
103
+ // picker disabled until the parent is set. `dependsValue` is the resolved
104
+ // value the caller threaded from the form context.
105
+ const dependsOn = getDependsOn(field);
106
+ const scope = dependsValue ? String(dependsValue) : '';
107
+ const blockedByDependency = !!dependsOn && scope === '';
92
108
  const { options, loading } = useOptionsResolver({
93
109
  modelKey: '',
94
- fieldKey: 'id',
95
- ref: fieldRef,
96
- // searchEndpoint only drives the URL when there's no ref — ref is the
97
- // canonical, kernel-derived path and wins.
98
- endpoint: fieldRef ? undefined : field.searchEndpoint,
110
+ fieldKey: source.fieldKey,
111
+ ref: source.ref,
112
+ // optionsConfig.source `/options/<source>`. Else searchEndpoint only
113
+ // drives the URL when there's no ref — ref is the canonical,
114
+ // kernel-derived path and wins.
115
+ endpoint: source.endpoint ?? (source.ref ? undefined : field.searchEndpoint),
99
116
  query: debounced,
100
117
  limit: 20,
118
+ // Cascade scope forwarded as filter_value (only when this field
119
+ // declares a dependency). Re-fetches when the parent value changes.
120
+ filterValue: dependsOn ? scope : undefined,
101
121
  // Don't fetch until the popover opens (and keep fetching as the query
102
- // changes while open).
103
- enabled: open,
122
+ // changes while open). A picker blocked by an unset dependency never
123
+ // fetches.
124
+ enabled: open && !blockedByDependency,
104
125
  });
126
+ // When the depended-on value changes, the previously-picked option no longer
127
+ // belongs to the new scope, so clear the selection (skip the initial mount).
128
+ const prevScopeRef = useRef(scope);
129
+ useEffect(() => {
130
+ if (!dependsOn)
131
+ return;
132
+ if (prevScopeRef.current !== scope) {
133
+ prevScopeRef.current = scope;
134
+ setPicked(null);
135
+ if (value)
136
+ onChange('');
137
+ }
138
+ }, [dependsOn, scope, value, onChange]);
105
139
  // The currently-selected option, resolved either from what the user picked
106
140
  // (cached in `picked`) or from the loaded page. Drives both the trigger
107
141
  // label and its thumbnail.
@@ -145,7 +179,10 @@ export function DynamicSelectField({ field, value, onChange, seedOption }) {
145
179
  // to the cell. Without min-w-0 the combobox+button row sizes to its content
146
180
  // (the long empty-state placeholder) and overflows the column, pushing the
147
181
  // "+" off-screen — it only "fit" once a short value was selected.
148
- return (_jsxs("div", { className: "flex w-full min-w-0 items-center gap-1.5", children: [_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", role: "combobox", "aria-expanded": open, id: field.key, className: "min-w-0 flex-1 justify-between font-normal", "data-empty": !value, children: [_jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-2 text-left", children: [hasVisual && value ? (_jsx(OptionLead, { option: selectedOption, size: 20 })) : null, _jsx("span", { className: 'min-w-0 flex-1 truncate ' + (selectedLabel ? '' : 'text-muted-foreground'), children: selectedLabel || field.placeholder || 'Buscar…' })] }), _jsx(ChevronsUpDown, { className: "ml-2 size-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "p-0", align: "start",
182
+ return (_jsxs("div", { className: "flex w-full min-w-0 items-center gap-1.5", children: [_jsxs(Popover, { open: open && !blockedByDependency, onOpenChange: (o) => { if (!blockedByDependency)
183
+ setOpen(o); }, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", role: "combobox", "aria-expanded": open, id: field.key, disabled: blockedByDependency, className: "min-w-0 flex-1 justify-between font-normal", "data-empty": !value, "data-depends-blocked": blockedByDependency ? '' : undefined, children: [_jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-2 text-left", children: [hasVisual && value ? (_jsx(OptionLead, { option: selectedOption, size: 20 })) : null, _jsx("span", { className: 'min-w-0 flex-1 truncate ' + (selectedLabel ? '' : 'text-muted-foreground'), children: blockedByDependency
184
+ ? (dependsHint || DEFAULT_DEPENDS_HINT)
185
+ : selectedLabel || field.placeholder || 'Buscar…' })] }), _jsx(ChevronsUpDown, { className: "ml-2 size-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "p-0", align: "start",
149
186
  // Match the trigger width without an arbitrary Tailwind class
150
187
  // (those don't always survive a consuming app's Tailwind scan).
151
188
  style: { width: 'var(--radix-popover-trigger-width)' }, children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { placeholder: field.placeholder || 'Buscar…', value: search, onValueChange: setSearch }), _jsxs(CommandList, { children: [loading && (_jsxs("div", { className: "text-muted-foreground flex items-center justify-center gap-2 py-6 text-sm", children: [_jsx(Loader2, { className: "size-4 animate-spin" }), "Buscando\u2026"] })), !loading && options.length === 0 && (_jsx(CommandEmpty, { children: debounced ? 'Sin resultados' : 'Escribí para buscar…' })), !loading && options.length > 0 && (_jsx(CommandGroup, { className: "max-h-64 overflow-auto", children: options.map((opt) => {
package/dist/index.d.ts CHANGED
@@ -37,7 +37,7 @@ export { registerModelExtension, getModelExtension, clearModelExtensions, type M
37
37
  export { isColumnVisibleInTable, getSearchableColumnKeys, } from './column-visibility';
38
38
  export { useOptionsResolver, projectOption, type ResolvedOption, type OptionsMeta, type UseOptionsResolverArgs, type UseOptionsResolverResult, } from './use-options-resolver';
39
39
  export { setOrgConfigBridge, getOrgConfigBridge, resolveValidatorToken, type OrgConfigBridge, } from './use-org-config-bridge';
40
- export { registerValidator } from './dynamic-form-schema';
40
+ export { registerValidator, getDependsOn, resolveDependsValue, getOptionsConfig, resolveOptionsSource, } from './dynamic-form-schema';
41
41
  export { ActivityValueRenderer, type ActivityValueRendererProps, } from './activity-value-renderer';
42
42
  export { ActivityDiff, type ActivityEvent, type ActivityDiffProps, } from './activity-diff';
43
43
  export { RecordHistory, type RecordHistoryProps, } from './record-history';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -39,7 +39,7 @@ export { registerModelExtension, getModelExtension, clearModelExtensions, } from
39
39
  export { isColumnVisibleInTable, getSearchableColumnKeys, } from './column-visibility';
40
40
  export { useOptionsResolver, projectOption, } from './use-options-resolver';
41
41
  export { setOrgConfigBridge, getOrgConfigBridge, resolveValidatorToken, } from './use-org-config-bridge';
42
- export { registerValidator } from './dynamic-form-schema';
42
+ export { registerValidator, getDependsOn, resolveDependsValue, getOptionsConfig, resolveOptionsSource, } from './dynamic-form-schema';
43
43
  export { ActivityValueRenderer, } from './activity-value-renderer';
44
44
  export { ActivityDiff, } from './activity-diff';
45
45
  export { RecordHistory, } from './record-history';