@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/dist/types.d.ts CHANGED
@@ -178,6 +178,30 @@ export interface ActionFieldDef {
178
178
  */
179
179
  source?: string;
180
180
  relation?: string;
181
+ /**
182
+ * Cascade dependency: the key of ANOTHER field in the same action form
183
+ * (a header field or a sibling item-field) whose current value supplies
184
+ * this picker's `filter_value`. While the depended-on field is empty the
185
+ * picker is disabled with a hint; once it has a value the picker fetches
186
+ * options scoped by it and re-fetches whenever it changes (clearing the
187
+ * current selection). Without `dependsOn` the picker lists everything
188
+ * (retrocompat). Tolerates the snake_case `depends_on` the kernel serves.
189
+ */
190
+ dependsOn?: string;
191
+ /** snake_case alias served by the kernel manifest for `dependsOn`. */
192
+ depends_on?: string;
193
+ /**
194
+ * Enriched options routing the kernel serves for a dependent/scoped picker.
195
+ * When it carries a `source`, the picker queries that source MODEL (not the
196
+ * field's `ref`): URL `/options/<source>`, query field = `value` (falling
197
+ * back to the field's own key), and the cascade `filter_value` is the value
198
+ * of the `dependsOn` field. `description` is projected into the option
199
+ * subtitle. Tolerates the snake_case `options_config` the kernel emits.
200
+ * Absent → the picker keeps its `ref`-based behaviour (retrocompat).
201
+ */
202
+ optionsConfig?: FieldOptionsConfig;
203
+ /** snake_case alias served by the kernel manifest for `optionsConfig`. */
204
+ options_config?: FieldOptionsConfig;
181
205
  /**
182
206
  * Columns of a repeatable line-items group. Mirrors the kernel v3
183
207
  * `ActionField.item_fields` (json `item_fields`). Present on a field
@@ -243,6 +267,30 @@ export interface FieldBalanceRule {
243
267
  requireNonzero?: boolean;
244
268
  require_nonzero?: boolean;
245
269
  }
270
+ /**
271
+ * Enriched options-resolution config the kernel attaches to a dependent/scoped
272
+ * picker field (json `options_config`). When `source` is present the SDK queries
273
+ * the source model instead of the field's `ref`. All keys are snake_case as the
274
+ * kernel serves them; the SDK reads them as-is via `getOptionsConfig`.
275
+ */
276
+ export interface FieldOptionsConfig {
277
+ /** Discriminator the kernel sets (e.g. `'dynamic'`). Informational. */
278
+ type?: string;
279
+ /** Source MODEL the candidates come from → URL `/options/<source>`. */
280
+ source?: string;
281
+ /** Column of `source` compared against the cascade `filter_value`. */
282
+ filter_by?: string;
283
+ /** Column of `source` used as the option value → query `?field=<value>`. */
284
+ value?: string;
285
+ /** Related model used to resolve the option label by id (host-side enrich). */
286
+ label_ref?: string;
287
+ /** Column of `source` projected into `option.description` (e.g. qty). */
288
+ description?: string;
289
+ /** Optional ordering column. */
290
+ order_by?: string;
291
+ /** Optional column projected into `option.image`. */
292
+ image?: string;
293
+ }
246
294
  export interface ActionDefinition {
247
295
  key: string;
248
296
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -41,6 +41,15 @@ export interface UseOptionsResolverArgs {
41
41
  * server returns the first page unfiltered.
42
42
  */
43
43
  query?: string;
44
+ /**
45
+ * Cascade scope forwarded as `?filter_value=`. Set by a dependent picker
46
+ * from the current value of the field it `dependsOn` (e.g. a product
47
+ * picker scoped to the header's `source_warehouse_id`). When empty/undefined
48
+ * the param is omitted (no scope — the picker lists everything). Changing it
49
+ * re-fetches; an empty string is treated as "not set" so a cleared parent
50
+ * does not query for the empty-string scope.
51
+ */
52
+ filterValue?: string;
44
53
  /**
45
54
  * Server-side pagination cap. Defaults to 50 (kernel
46
55
  * DefaultOptionsLimit) if omitted.
@@ -1 +1 @@
1
- {"version":3,"file":"use-options-resolver.d.ts","sourceRoot":"","sources":["../src/use-options-resolver.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,cAAc;IAC3B,8CAA8C;IAC9C,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,sBAAsB;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAED,MAAM,WAAW,WAAW;IACxB,oEAAoE;IACpE,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAA;IACnC,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,sBAAsB;IACnC;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,wBAAwB;IACrC,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,IAAI,CAAA;CACtB;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,sBAAsB,GAAG,wBAAwB,CAiHzF;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,cAAc,CAatD"}
1
+ {"version":3,"file":"use-options-resolver.d.ts","sourceRoot":"","sources":["../src/use-options-resolver.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,cAAc;IAC3B,8CAA8C;IAC9C,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,sBAAsB;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAED,MAAM,WAAW,WAAW;IACxB,oEAAoE;IACpE,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAA;IACnC,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,sBAAsB;IACnC;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,wBAAwB;IACrC,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,IAAI,CAAA;CACtB;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,sBAAsB,GAAG,wBAAwB,CAsHzF;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,cAAc,CAatD"}
@@ -23,7 +23,7 @@ import { useApi } from './api-context';
23
23
  * compose this with TanStack Query in their own layer).
24
24
  */
25
25
  export function useOptionsResolver(args) {
26
- const { modelKey, fieldKey, ref, query, limit, enabled = true, endpoint, } = args;
26
+ const { modelKey, fieldKey, ref, query, limit, enabled = true, endpoint, filterValue, } = args;
27
27
  const api = useApi();
28
28
  const [options, setOptions] = useState([]);
29
29
  const [meta, setMeta] = useState(null);
@@ -77,6 +77,11 @@ export function useOptionsResolver(args) {
77
77
  params.q = query;
78
78
  if (typeof limit === 'number' && limit > 0)
79
79
  params.limit = limit;
80
+ // Cascade scope: a dependent picker passes the value of the field it
81
+ // `dependsOn`. Skip empty strings so a cleared parent omits the param
82
+ // (no scope) rather than querying for the empty-string filter_value.
83
+ if (filterValue)
84
+ params.filter_value = filterValue;
80
85
  api.get(url, { params, signal: controller.signal })
81
86
  .then((res) => {
82
87
  if (controller.signal.aborted)
@@ -117,7 +122,7 @@ export function useOptionsResolver(args) {
117
122
  return () => {
118
123
  controller.abort();
119
124
  };
120
- }, [api, url, effectiveField, query, limit, enabled, refreshKey]);
125
+ }, [api, url, effectiveField, query, limit, enabled, filterValue, refreshKey]);
121
126
  return {
122
127
  options,
123
128
  meta,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "18.17.2",
3
+ "version": "18.18.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -64,8 +64,8 @@
64
64
  "typescript": "^6.0.0",
65
65
  "vitest": "^4.0.0",
66
66
  "zustand": "^5.0.0",
67
- "@asteby/metacore-ui": "2.5.2",
68
- "@asteby/metacore-sdk": "3.2.0"
67
+ "@asteby/metacore-sdk": "3.2.0",
68
+ "@asteby/metacore-ui": "2.5.2"
69
69
  },
70
70
  "scripts": {
71
71
  "build": "tsc -p tsconfig.json",
@@ -155,6 +155,24 @@ describe('DashboardGrid render', () => {
155
155
  render(<DashboardGrid widgets={[]} loadData={loaderOf({})} />)
156
156
  expect(screen.getByTestId('dashboard-empty')).toBeTruthy()
157
157
  })
158
+
159
+ it('survives an empty → populated transition (React #310 regression)', async () => {
160
+ // The flatten/order useMemo must run BEFORE the empty-state early return.
161
+ // When it sat after the return, an empty render called one fewer hook
162
+ // than the populated render → "Rendered more hooks" (React #310) crash.
163
+ const { rerender } = render(
164
+ <DashboardGrid widgets={[]} loadData={loaderOf({})} />,
165
+ )
166
+ expect(screen.getByTestId('dashboard-empty')).toBeTruthy()
167
+ rerender(
168
+ <DashboardGrid
169
+ widgets={[spec({ key: 'rev', kind: 'stat' })]}
170
+ loadData={loaderOf({ rev: { value: 5 } })}
171
+ />,
172
+ )
173
+ await waitFor(() => expect(screen.getByTestId('widget-rev')).toBeTruthy())
174
+ expect(screen.getByText('5')).toBeTruthy()
175
+ })
158
176
  })
159
177
 
160
178
  describe('DashboardGrid permission gating', () => {
@@ -0,0 +1,337 @@
1
+ // @vitest-environment happy-dom
2
+ //
3
+ // Dependent (cascading) options contract:
4
+ // - resolveDependsValue (pure): a cell `dependsOn` resolves from the row
5
+ // (sibling) first, then the header form values; empty/unset → ''.
6
+ // - useOptionsResolver: a `filterValue` is forwarded as `&filter_value=` and
7
+ // re-fetches when it changes; an empty value omits the param.
8
+ // - DynamicLineItems → cell with `dependsOn`: the picker is disabled while the
9
+ // header field is empty, and once the header field has a value the options
10
+ // request carries that value as `filter_value` and re-fetches on change.
11
+ import { afterEach, describe, expect, it, vi } from 'vitest'
12
+ import { act, cleanup, render, screen, waitFor } from '@testing-library/react'
13
+
14
+ // Identity translator so any raw i18n keys surface verbatim.
15
+ vi.mock('react-i18next', () => ({
16
+ useTranslation: () => ({ t: (k: string) => k }),
17
+ }))
18
+
19
+ import {
20
+ resolveDependsValue,
21
+ getDependsOn,
22
+ getOptionsConfig,
23
+ resolveOptionsSource,
24
+ } from '../dynamic-form-schema'
25
+ import { useOptionsResolver } from '../use-options-resolver'
26
+ import { DynamicLineItems } from '../dynamic-line-items'
27
+ import { ApiProvider, type ApiClient } from '../api-context'
28
+ import type { ActionFieldDef } from '../types'
29
+ import { useState } from 'react'
30
+
31
+ afterEach(cleanup)
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Pure resolution
35
+ // ---------------------------------------------------------------------------
36
+ describe('getDependsOn / resolveDependsValue', () => {
37
+ const field = (over: Partial<ActionFieldDef>): ActionFieldDef =>
38
+ ({ key: 'product_id', label: 'Producto', type: 'dynamic_select', ...over })
39
+
40
+ it('reads camelCase dependsOn and snake_case depends_on', () => {
41
+ expect(getDependsOn(field({ dependsOn: 'wh' }))).toBe('wh')
42
+ expect(getDependsOn(field({ depends_on: 'wh' }))).toBe('wh')
43
+ expect(getDependsOn(field({}))).toBeUndefined()
44
+ })
45
+
46
+ it('resolves from header form values', () => {
47
+ const f = field({ depends_on: 'source_warehouse_id' })
48
+ expect(resolveDependsValue(f, { source_warehouse_id: 'W1' })).toBe('W1')
49
+ })
50
+
51
+ it('prefers a sibling row value over the header', () => {
52
+ const f = field({ dependsOn: 'warehouse_id' })
53
+ const out = resolveDependsValue(f, { warehouse_id: 'HEADER' }, { warehouse_id: 'ROW' })
54
+ expect(out).toBe('ROW')
55
+ })
56
+
57
+ it('falls back to header when the row value is blank', () => {
58
+ const f = field({ dependsOn: 'warehouse_id' })
59
+ const out = resolveDependsValue(f, { warehouse_id: 'HEADER' }, { warehouse_id: '' })
60
+ expect(out).toBe('HEADER')
61
+ })
62
+
63
+ it('returns empty string when the dependency is unset', () => {
64
+ const f = field({ dependsOn: 'warehouse_id' })
65
+ expect(resolveDependsValue(f, {})).toBe('')
66
+ expect(resolveDependsValue(field({}), { a: 1 })).toBe('')
67
+ })
68
+ })
69
+
70
+ describe('getOptionsConfig / resolveOptionsSource', () => {
71
+ const field = (over: Partial<ActionFieldDef>): ActionFieldDef =>
72
+ ({ key: 'product_id', label: 'Producto', type: 'dynamic_select', ...over })
73
+
74
+ it('reads camelCase optionsConfig and snake_case options_config', () => {
75
+ expect(getOptionsConfig(field({ optionsConfig: { source: 'stock' } }))?.source).toBe('stock')
76
+ expect(getOptionsConfig(field({ options_config: { source: 'stock' } }))?.source).toBe('stock')
77
+ expect(getOptionsConfig(field({}))).toBeUndefined()
78
+ })
79
+
80
+ it('routes to the source model with field=value when optionsConfig.source is present', () => {
81
+ const f = field({
82
+ ref: 'products.Product',
83
+ options_config: { source: 'stock', filter_by: 'warehouse_id', value: 'product_id', description: 'quantity' },
84
+ })
85
+ const out = resolveOptionsSource(f)
86
+ expect(out.endpoint).toBe('/options/stock')
87
+ expect(out.fieldKey).toBe('product_id')
88
+ // source wins over ref — the ref pointer is not used for routing.
89
+ expect(out.ref).toBeUndefined()
90
+ })
91
+
92
+ it('falls back to the field key when optionsConfig.value is absent', () => {
93
+ const f = field({ key: 'item_id', options_config: { source: 'stock' } })
94
+ const out = resolveOptionsSource(f)
95
+ expect(out.endpoint).toBe('/options/stock')
96
+ expect(out.fieldKey).toBe('item_id')
97
+ })
98
+
99
+ it('keeps ref-based resolution when there is no optionsConfig.source', () => {
100
+ const f = field({ ref: 'products.Product' })
101
+ const out = resolveOptionsSource(f)
102
+ expect(out.endpoint).toBeUndefined()
103
+ expect(out.ref).toBe('products.Product')
104
+ expect(out.fieldKey).toBe('id')
105
+ })
106
+ })
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // useOptionsResolver — filter_value forwarding + re-fetch
110
+ // ---------------------------------------------------------------------------
111
+ function Harness({ filterValue }: { filterValue?: string }) {
112
+ const { options } = useOptionsResolver({
113
+ modelKey: '',
114
+ fieldKey: 'id',
115
+ ref: 'products.Product',
116
+ filterValue,
117
+ })
118
+ return <div data-testid="count">{options.length}</div>
119
+ }
120
+
121
+ describe('useOptionsResolver filter_value', () => {
122
+ it('forwards a non-empty filter_value and re-fetches on change', async () => {
123
+ const get = vi.fn(async () => ({
124
+ data: { success: true, data: [{ id: '1', label: 'One' }], meta: { type: 'dynamic', count: 1 } },
125
+ }))
126
+ const client = { get } as unknown as ApiClient
127
+
128
+ const { rerender } = render(
129
+ <ApiProvider client={client}>
130
+ <Harness filterValue="W1" />
131
+ </ApiProvider>,
132
+ )
133
+ await waitFor(() => expect(screen.getByTestId('count').textContent).toBe('1'))
134
+
135
+ // First call carried filter_value=W1.
136
+ const firstParams = get.mock.calls[0][1]?.params
137
+ expect(firstParams.filter_value).toBe('W1')
138
+
139
+ // Change the parent value → a fresh request with the new filter_value.
140
+ rerender(
141
+ <ApiProvider client={client}>
142
+ <Harness filterValue="W2" />
143
+ </ApiProvider>,
144
+ )
145
+ await waitFor(() => expect(get.mock.calls.length).toBeGreaterThan(1))
146
+ const lastParams = get.mock.calls[get.mock.calls.length - 1][1]?.params
147
+ expect(lastParams.filter_value).toBe('W2')
148
+ })
149
+
150
+ it('omits filter_value when empty', async () => {
151
+ const get = vi.fn(async () => ({
152
+ data: { success: true, data: [], meta: { type: 'dynamic', count: 0 } },
153
+ }))
154
+ const client = { get } as unknown as ApiClient
155
+ render(
156
+ <ApiProvider client={client}>
157
+ <Harness filterValue="" />
158
+ </ApiProvider>,
159
+ )
160
+ await waitFor(() => expect(get).toHaveBeenCalled())
161
+ expect(get.mock.calls[0][1]?.params.filter_value).toBeUndefined()
162
+ })
163
+ })
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // DynamicLineItems — a cell that dependsOn a HEADER field
167
+ // ---------------------------------------------------------------------------
168
+ const lineItemsField: ActionFieldDef = {
169
+ key: 'items',
170
+ label: 'Renglones',
171
+ type: 'array',
172
+ itemFields: [
173
+ {
174
+ key: 'product_id',
175
+ label: 'Producto',
176
+ type: 'dynamic_select',
177
+ ref: 'products.Product',
178
+ // depends on the HEADER field, not a sibling row cell.
179
+ depends_on: 'source_warehouse_id',
180
+ },
181
+ ],
182
+ }
183
+
184
+ // Drives DynamicLineItems with one pre-existing row + a switchable header value.
185
+ function LineItemsHost({ headerValue }: { headerValue: string }) {
186
+ const [rows, setRows] = useState<any[]>([{ product_id: '' }])
187
+ return (
188
+ <DynamicLineItems
189
+ field={lineItemsField}
190
+ value={rows}
191
+ onChange={setRows}
192
+ formValues={{ source_warehouse_id: headerValue }}
193
+ />
194
+ )
195
+ }
196
+
197
+ describe('DynamicLineItems cascading cell', () => {
198
+ it('disables the picker while the header field is empty', () => {
199
+ const get = vi.fn(async () => ({
200
+ data: { success: true, data: [], meta: { type: 'dynamic', count: 0 } },
201
+ }))
202
+ const client = { get } as unknown as ApiClient
203
+ render(
204
+ <ApiProvider client={client}>
205
+ <LineItemsHost headerValue="" />
206
+ </ApiProvider>,
207
+ )
208
+ // The combobox trigger is disabled and shows the dependency hint.
209
+ const trigger = screen.getByRole('combobox')
210
+ expect((trigger as HTMLButtonElement).disabled).toBe(true)
211
+ expect(trigger.getAttribute('data-depends-blocked')).toBe('')
212
+ // No options request fires while blocked.
213
+ expect(get).not.toHaveBeenCalled()
214
+ })
215
+
216
+ it('sends filter_value from the header field and re-fetches when it changes', async () => {
217
+ const get = vi.fn(async () => ({
218
+ data: {
219
+ success: true,
220
+ data: [{ id: 'p1', label: 'Tornillo', description: 'disp. 12' }],
221
+ meta: { type: 'dynamic', count: 1 },
222
+ },
223
+ }))
224
+ const client = { get } as unknown as ApiClient
225
+
226
+ const { rerender } = render(
227
+ <ApiProvider client={client}>
228
+ <LineItemsHost headerValue="W1" />
229
+ </ApiProvider>,
230
+ )
231
+
232
+ // Open the popover so the typeahead fetches.
233
+ const trigger = screen.getByRole('combobox')
234
+ expect((trigger as HTMLButtonElement).disabled).toBe(false)
235
+ await act(async () => {
236
+ trigger.click()
237
+ })
238
+ await waitFor(() => expect(get).toHaveBeenCalled())
239
+ const firstParams = get.mock.calls[0][1]?.params
240
+ expect(firstParams.filter_value).toBe('W1')
241
+
242
+ // Switch the header warehouse → the cell picker re-fetches scoped to W2.
243
+ rerender(
244
+ <ApiProvider client={client}>
245
+ <LineItemsHost headerValue="W2" />
246
+ </ApiProvider>,
247
+ )
248
+ await waitFor(() => {
249
+ const last = get.mock.calls[get.mock.calls.length - 1][1]?.params
250
+ expect(last.filter_value).toBe('W2')
251
+ })
252
+ })
253
+ })
254
+
255
+ // ---------------------------------------------------------------------------
256
+ // optionsConfig.source routing — the picker queries the SOURCE model, not `ref`
257
+ // ---------------------------------------------------------------------------
258
+ const sourceLineItemsField: ActionFieldDef = {
259
+ key: 'items',
260
+ label: 'Renglones',
261
+ type: 'array',
262
+ itemFields: [
263
+ {
264
+ key: 'product_id',
265
+ label: 'Producto',
266
+ type: 'dynamic_select',
267
+ // A `ref` is present but optionsConfig.source MUST win for routing.
268
+ ref: 'products.Product',
269
+ depends_on: 'source_warehouse_id',
270
+ options_config: {
271
+ type: 'dynamic',
272
+ source: 'stock',
273
+ filter_by: 'warehouse_id',
274
+ value: 'product_id',
275
+ label_ref: 'products.Product',
276
+ description: 'quantity',
277
+ },
278
+ },
279
+ ],
280
+ }
281
+
282
+ function SourceLineItemsHost({ headerValue }: { headerValue: string }) {
283
+ const [rows, setRows] = useState<any[]>([{ product_id: '' }])
284
+ return (
285
+ <DynamicLineItems
286
+ field={sourceLineItemsField}
287
+ value={rows}
288
+ onChange={setRows}
289
+ formValues={{ source_warehouse_id: headerValue }}
290
+ />
291
+ )
292
+ }
293
+
294
+ describe('DynamicLineItems cell with optionsConfig.source', () => {
295
+ it('queries /options/<source>?field=<value>&filter_value=<dependsOn> and re-fetches on parent change', async () => {
296
+ const get = vi.fn(async () => ({
297
+ data: {
298
+ success: true,
299
+ data: [{ id: 'p1', label: 'Tornillo', description: 'disp. 12' }],
300
+ meta: { type: 'dynamic', count: 1 },
301
+ },
302
+ }))
303
+ const client = { get } as unknown as ApiClient
304
+
305
+ const { rerender } = render(
306
+ <ApiProvider client={client}>
307
+ <SourceLineItemsHost headerValue="W1" />
308
+ </ApiProvider>,
309
+ )
310
+
311
+ const trigger = screen.getByRole('combobox')
312
+ expect((trigger as HTMLButtonElement).disabled).toBe(false)
313
+ await act(async () => {
314
+ trigger.click()
315
+ })
316
+ await waitFor(() => expect(get).toHaveBeenCalled())
317
+
318
+ // URL hits the SOURCE model, not the `ref`.
319
+ const [firstUrl, firstCfg] = get.mock.calls[0]
320
+ expect(firstUrl).toBe('/options/stock')
321
+ expect(firstCfg?.params.field).toBe('product_id')
322
+ expect(firstCfg?.params.filter_value).toBe('W1')
323
+
324
+ // Parent change → re-fetch, still routed to the source, new filter_value.
325
+ rerender(
326
+ <ApiProvider client={client}>
327
+ <SourceLineItemsHost headerValue="W2" />
328
+ </ApiProvider>,
329
+ )
330
+ await waitFor(() => {
331
+ const [url, cfg] = get.mock.calls[get.mock.calls.length - 1]
332
+ expect(url).toBe('/options/stock')
333
+ expect(cfg?.params.field).toBe('product_id')
334
+ expect(cfg?.params.filter_value).toBe('W2')
335
+ })
336
+ })
337
+ })
@@ -42,7 +42,7 @@ import { DynamicLineItems } from './dynamic-line-items'
42
42
  import { DynamicSelectField } from './dynamic-select-field'
43
43
  import { DynamicDateField } from './dynamic-date-field'
44
44
  import { UploadField } from './upload-field'
45
- import { isLineItemsField, resolveWidget } from './dynamic-form-schema'
45
+ import { isLineItemsField, resolveWidget, resolveDependsValue, getDependsOn } from './dynamic-form-schema'
46
46
  import type { ActionFieldDef } from './types'
47
47
  // Canonical registry lives in @asteby/metacore-sdk
48
48
  import {
@@ -292,7 +292,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
292
292
  {field.label}
293
293
  {field.required && <span className="text-red-500 ml-1">*</span>}
294
294
  </Label>
295
- {renderField(field, formData[field.key], (v: any) => updateField(field.key, v))}
295
+ {renderField(field, formData[field.key], (v: any) => updateField(field.key, v), formData)}
296
296
  </div>
297
297
  )
298
298
  })}
@@ -319,10 +319,16 @@ function renderField(
319
319
  field: ActionFieldDef,
320
320
  value: any,
321
321
  onChange: (value: any) => void,
322
+ // Full current form values — lets a line-items grid (and any cascading
323
+ // header picker) resolve a `dependsOn` reference against sibling header
324
+ // fields. Omitted by callers that have no surrounding form (the field is
325
+ // then treated as having no resolvable dependency).
326
+ formValues?: Record<string, any>,
322
327
  ) {
323
328
  // Repeatable line-items group → row grid (value is an array of row objects).
329
+ // The header form values flow in so a cell can depend on a header field.
324
330
  if (isLineItemsField(field)) {
325
- return <DynamicLineItems field={field} value={value} onChange={onChange} />
331
+ return <DynamicLineItems field={field} value={value} onChange={onChange} formValues={formValues} />
326
332
  }
327
333
  // Resolve the widget the same way DynamicForm does (explicit widget wins,
328
334
  // else inferred from type) so action modals and the standalone form stay in
@@ -330,7 +336,12 @@ function renderField(
330
336
  // dropped `dynamic_select` to a plain text input.
331
337
  const widget = resolveWidget(field)
332
338
  if (widget === 'dynamic_select') {
333
- return <DynamicSelectField field={field} value={value} onChange={onChange} />
339
+ // A header-level dynamic_select may itself depend on another header
340
+ // field; resolve its filter_value from the form context.
341
+ const dependsValue = getDependsOn(field)
342
+ ? resolveDependsValue(field, formValues)
343
+ : undefined
344
+ return <DynamicSelectField field={field} value={value} onChange={onChange} dependsValue={dependsValue} />
334
345
  }
335
346
  // File upload → themed picker that POSTs the file to the host upload
336
347
  // endpoint and stores the returned url/path. Kept in sync with DynamicForm.
@@ -132,6 +132,22 @@ export function DashboardGrid({
132
132
  // eslint-disable-next-line react-hooks/exhaustive-deps
133
133
  }, [keySig, loadData])
134
134
 
135
+ // Flatten every group into ONE ordered list (compact KPIs before charts) for
136
+ // the masonry grid. MUST run before any early return — it is a hook, and a
137
+ // conditional hook (placed after the empty-state return) trips React #310
138
+ // when the dashboard transitions empty → populated.
139
+ const ordered = React.useMemo(() => {
140
+ const flat = visibleGroups.flatMap((g) => g.widgets)
141
+ return flat
142
+ .map((w, i) => ({ w, i }))
143
+ .sort(
144
+ (a, b) =>
145
+ (isTallWidget(a.w) ? 1 : 0) - (isTallWidget(b.w) ? 1 : 0) ||
146
+ a.i - b.i,
147
+ )
148
+ .map((x) => x.w)
149
+ }, [visibleGroups])
150
+
135
151
  // Global empty state (no widgets at all / none visible after gating).
136
152
  if (visibleGroups.length === 0) {
137
153
  return (
@@ -155,23 +171,6 @@ export function DashboardGrid({
155
171
  )
156
172
  }
157
173
 
158
- // ONE unified dense grid across every group. Per-group sections used to
159
- // break the layout into rows, so a lone-widget group (e.g. a single KPI)
160
- // left the rest of its row blank. Flattening + `grid-flow-row-dense`
161
- // backfills those holes; ordering compact KPIs before charts makes the top
162
- // read as a metric band and the charts mosaic below it. No blank space.
163
- const ordered = React.useMemo(() => {
164
- const flat = visibleGroups.flatMap((g) => g.widgets)
165
- return flat
166
- .map((w, i) => ({ w, i }))
167
- .sort(
168
- (a, b) =>
169
- (isTallWidget(a.w) ? 1 : 0) - (isTallWidget(b.w) ? 1 : 0) ||
170
- a.i - b.i,
171
- )
172
- .map((x) => x.w)
173
- }, [visibleGroups])
174
-
175
174
  return (
176
175
  <div
177
176
  data-testid="dashboard-grid"