@asteby/metacore-runtime-react 18.17.3 → 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 +27 -0
- package/dist/action-modal-dispatcher.js +16 -5
- package/dist/dynamic-form-schema.d.ts +41 -1
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +61 -0
- package/dist/dynamic-line-items.d.ts +7 -1
- package/dist/dynamic-line-items.d.ts.map +1 -1
- package/dist/dynamic-line-items.js +46 -12
- package/dist/dynamic-select-field.d.ts +17 -1
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +48 -11
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +48 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/use-options-resolver.d.ts +9 -0
- package/dist/use-options-resolver.d.ts.map +1 -1
- package/dist/use-options-resolver.js +7 -2
- package/package.json +1 -1
- package/src/__tests__/dependent-options.test.tsx +337 -0
- package/src/action-modal-dispatcher.tsx +15 -4
- package/src/dynamic-form-schema.ts +72 -1
- package/src/dynamic-line-items.tsx +86 -13
- package/src/dynamic-select-field.tsx +69 -12
- package/src/index.ts +7 -1
- package/src/types.ts +49 -0
- package/src/use-options-resolver.ts +15 -1
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
SelectTrigger,
|
|
19
19
|
SelectValue,
|
|
20
20
|
} from '@asteby/metacore-ui/primitives'
|
|
21
|
+
import { useEffect, useRef } from 'react'
|
|
21
22
|
import { Plus, Trash2, Check } from 'lucide-react'
|
|
22
23
|
import type { ActionFieldDef } from './types'
|
|
23
24
|
import {
|
|
@@ -26,8 +27,12 @@ import {
|
|
|
26
27
|
computeLineItemTotals,
|
|
27
28
|
evaluateBalance,
|
|
28
29
|
toNumber,
|
|
30
|
+
getDependsOn,
|
|
31
|
+
resolveDependsValue,
|
|
32
|
+
getOptionsConfig,
|
|
33
|
+
resolveOptionsSource,
|
|
29
34
|
} from './dynamic-form-schema'
|
|
30
|
-
import { DynamicSelectField } from './dynamic-select-field'
|
|
35
|
+
import { DynamicSelectField, DEFAULT_DEPENDS_HINT } from './dynamic-select-field'
|
|
31
36
|
import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
|
|
32
37
|
|
|
33
38
|
export interface DynamicLineItemsProps {
|
|
@@ -35,6 +40,12 @@ export interface DynamicLineItemsProps {
|
|
|
35
40
|
value: any[] | undefined
|
|
36
41
|
onChange: (rows: any[]) => void
|
|
37
42
|
disabled?: boolean
|
|
43
|
+
/**
|
|
44
|
+
* Current values of the surrounding (header) form. Threaded into each cell
|
|
45
|
+
* so a cell field with `dependsOn` can scope its options by a HEADER field
|
|
46
|
+
* (e.g. `source_warehouse_id`), not just a sibling cell on the same row.
|
|
47
|
+
*/
|
|
48
|
+
formValues?: Record<string, any>
|
|
38
49
|
}
|
|
39
50
|
|
|
40
51
|
const fmtNumber = (n: number): string =>
|
|
@@ -53,7 +64,7 @@ function emptyRow(itemFields: ActionFieldDef[]): Record<string, any> {
|
|
|
53
64
|
return row
|
|
54
65
|
}
|
|
55
66
|
|
|
56
|
-
export function DynamicLineItems({ field, value, onChange, disabled = false }: DynamicLineItemsProps) {
|
|
67
|
+
export function DynamicLineItems({ field, value, onChange, disabled = false, formValues }: DynamicLineItemsProps) {
|
|
57
68
|
const itemFields = getItemFields(field)
|
|
58
69
|
const rows: any[] = Array.isArray(value) ? value : []
|
|
59
70
|
|
|
@@ -137,6 +148,8 @@ export function DynamicLineItems({ field, value, onChange, disabled = false }: D
|
|
|
137
148
|
value={row?.[col.key]}
|
|
138
149
|
onChange={(v: any) => handleCell(idx, col.key, v)}
|
|
139
150
|
disabled={disabled}
|
|
151
|
+
formValues={formValues}
|
|
152
|
+
rowValues={row}
|
|
140
153
|
/>
|
|
141
154
|
</td>
|
|
142
155
|
))}
|
|
@@ -234,21 +247,39 @@ interface CellRendererProps {
|
|
|
234
247
|
value: any
|
|
235
248
|
onChange: (v: any) => void
|
|
236
249
|
disabled?: boolean
|
|
250
|
+
/** Header form values — for resolving a cell's `dependsOn` to a header field. */
|
|
251
|
+
formValues?: Record<string, any>
|
|
252
|
+
/** This row's values — for resolving a cell's `dependsOn` to a sibling cell. */
|
|
253
|
+
rowValues?: Record<string, any>
|
|
237
254
|
}
|
|
238
255
|
|
|
239
256
|
// Per-cell widget. Mirrors the flat FieldRenderer in dynamic-form.tsx but
|
|
240
257
|
// without the per-field Label (the column header is the label) and sized for a
|
|
241
258
|
// table cell. Nested line-items inside a row are not supported (a row column is
|
|
242
259
|
// a scalar widget).
|
|
243
|
-
function CellRenderer({ field, value, onChange, disabled }: CellRendererProps) {
|
|
260
|
+
function CellRenderer({ field, value, onChange, disabled, formValues, rowValues }: CellRendererProps) {
|
|
244
261
|
const widget = resolveWidget(field)
|
|
262
|
+
// Cascade scope for a cell with `dependsOn`: resolved from this row first
|
|
263
|
+
// (a sibling cell) then the header form (e.g. `source_warehouse_id`).
|
|
264
|
+
const dependsValue = getDependsOn(field)
|
|
265
|
+
? resolveDependsValue(field, formValues, rowValues)
|
|
266
|
+
: undefined
|
|
245
267
|
// Async searchable picker per row cell — e.g. the account_id column of a
|
|
246
268
|
// journal entry's debit/credit lines. Same widget as the flat form.
|
|
247
269
|
if (widget === 'dynamic_select') {
|
|
248
|
-
return <DynamicSelectField field={field} value={value} onChange={onChange} />
|
|
270
|
+
return <DynamicSelectField field={field} value={value} onChange={onChange} dependsValue={dependsValue} />
|
|
249
271
|
}
|
|
250
|
-
if (widget === 'select' && field.ref) {
|
|
251
|
-
return
|
|
272
|
+
if (widget === 'select' && (field.ref || getOptionsConfig(field)?.source)) {
|
|
273
|
+
return (
|
|
274
|
+
<RefCell
|
|
275
|
+
field={field}
|
|
276
|
+
value={value}
|
|
277
|
+
onChange={onChange}
|
|
278
|
+
disabled={disabled}
|
|
279
|
+
formValues={formValues}
|
|
280
|
+
rowValues={rowValues}
|
|
281
|
+
/>
|
|
282
|
+
)
|
|
252
283
|
}
|
|
253
284
|
switch (widget) {
|
|
254
285
|
case 'textarea':
|
|
@@ -320,21 +351,63 @@ function CellRenderer({ field, value, onChange, disabled }: CellRendererProps) {
|
|
|
320
351
|
}
|
|
321
352
|
}
|
|
322
353
|
|
|
323
|
-
function RefCell({ field, value, onChange, disabled }: CellRendererProps) {
|
|
354
|
+
function RefCell({ field, value, onChange, disabled, formValues, rowValues }: CellRendererProps) {
|
|
355
|
+
// Cascade: resolve the value of the field this cell `dependsOn` from the
|
|
356
|
+
// row (sibling) first, then the header form. While empty, the picker is
|
|
357
|
+
// disabled with a hint instead of listing the whole (unscoped) table.
|
|
358
|
+
const dependsOn = getDependsOn(field)
|
|
359
|
+
const scope = dependsOn ? resolveDependsValue(field, formValues, rowValues) : ''
|
|
360
|
+
const blockedByDependency = !!dependsOn && scope === ''
|
|
361
|
+
|
|
362
|
+
// optionsConfig.source → query the source model (`/options/<source>` with
|
|
363
|
+
// `field=<value>`); else fall back to the field's `ref`.
|
|
364
|
+
const optSource = resolveOptionsSource(field)
|
|
365
|
+
|
|
324
366
|
const { options, loading } = useOptionsResolver({
|
|
325
367
|
modelKey: '',
|
|
326
|
-
fieldKey:
|
|
327
|
-
ref:
|
|
368
|
+
fieldKey: optSource.fieldKey,
|
|
369
|
+
ref: optSource.ref,
|
|
370
|
+
endpoint: optSource.endpoint,
|
|
371
|
+
filterValue: dependsOn ? scope : undefined,
|
|
372
|
+
enabled: !blockedByDependency,
|
|
328
373
|
})
|
|
374
|
+
|
|
375
|
+
// Clear the selection when the parent scope changes (skip initial mount).
|
|
376
|
+
const prevScopeRef = useRef<string>(scope)
|
|
377
|
+
useEffect(() => {
|
|
378
|
+
if (!dependsOn) return
|
|
379
|
+
if (prevScopeRef.current !== scope) {
|
|
380
|
+
prevScopeRef.current = scope
|
|
381
|
+
if (value) onChange('')
|
|
382
|
+
}
|
|
383
|
+
}, [dependsOn, scope, value, onChange])
|
|
384
|
+
|
|
385
|
+
const placeholder = blockedByDependency
|
|
386
|
+
? DEFAULT_DEPENDS_HINT
|
|
387
|
+
: loading
|
|
388
|
+
? 'Cargando…'
|
|
389
|
+
: field.placeholder || 'Seleccionar...'
|
|
390
|
+
|
|
329
391
|
return (
|
|
330
|
-
<Select
|
|
331
|
-
|
|
332
|
-
|
|
392
|
+
<Select
|
|
393
|
+
value={value || ''}
|
|
394
|
+
onValueChange={onChange}
|
|
395
|
+
disabled={disabled || loading || blockedByDependency}
|
|
396
|
+
>
|
|
397
|
+
<SelectTrigger className="w-full" data-depends-blocked={blockedByDependency ? '' : undefined}>
|
|
398
|
+
<SelectValue placeholder={placeholder} />
|
|
333
399
|
</SelectTrigger>
|
|
334
400
|
<SelectContent>
|
|
335
401
|
{options.map((opt: ResolvedOption) => (
|
|
336
402
|
<SelectItem key={String(opt.id)} value={String(opt.id)}>
|
|
337
|
-
|
|
403
|
+
<span className="flex flex-col">
|
|
404
|
+
<span className="truncate">{opt.label}</span>
|
|
405
|
+
{opt.description && (
|
|
406
|
+
<span className="text-muted-foreground truncate text-xs">
|
|
407
|
+
{opt.description}
|
|
408
|
+
</span>
|
|
409
|
+
)}
|
|
410
|
+
</span>
|
|
338
411
|
</SelectItem>
|
|
339
412
|
))}
|
|
340
413
|
</SelectContent>
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// in a fetched page (we match by id against loaded options, else show the raw
|
|
22
22
|
// value). A dedicated `?ids=` lookup is a follow-up; create flows — the common
|
|
23
23
|
// case — start empty and never hit this.
|
|
24
|
-
import { useEffect, useState } from 'react'
|
|
24
|
+
import { useEffect, useRef, useState } from 'react'
|
|
25
25
|
import {
|
|
26
26
|
Button,
|
|
27
27
|
Command,
|
|
@@ -38,9 +38,16 @@ import { Check, ChevronsUpDown, ImageIcon, Loader2, Plus } from 'lucide-react'
|
|
|
38
38
|
import { resolveColorCss } from '@asteby/metacore-ui/lib'
|
|
39
39
|
import { DynamicIcon } from './dynamic-icon'
|
|
40
40
|
import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
|
|
41
|
-
import { getFieldRef } from './dynamic-form-schema'
|
|
41
|
+
import { getDependsOn, getFieldRef, resolveOptionsSource } from './dynamic-form-schema'
|
|
42
42
|
import type { ActionFieldDef } from './types'
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Default hint shown when a cascading picker's depended-on field is still
|
|
46
|
+
* empty. Domain-neutral on purpose; a caller may override it per field via
|
|
47
|
+
* `dependsHint`.
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_DEPENDS_HINT = 'Selecciona primero el campo del que depende'
|
|
50
|
+
|
|
44
51
|
/**
|
|
45
52
|
* Small square thumbnail for an option's `image`. Falls back to a neutral
|
|
46
53
|
* placeholder icon when the option has no image so rows/triggers stay aligned.
|
|
@@ -145,9 +152,26 @@ export interface DynamicSelectFieldProps {
|
|
|
145
152
|
* a lookup (which only loads once the popover opens). Matched by id == value.
|
|
146
153
|
*/
|
|
147
154
|
seedOption?: ResolvedOption | null
|
|
155
|
+
/**
|
|
156
|
+
* Cascade scope: the current value of the field this picker `dependsOn`
|
|
157
|
+
* (the caller resolves it from the form context). Forwarded as
|
|
158
|
+
* `filter_value`. When the field declares a `dependsOn` and this is empty,
|
|
159
|
+
* the picker is disabled with `dependsHint` and the current selection is
|
|
160
|
+
* cleared. Changing it re-fetches and clears the selection.
|
|
161
|
+
*/
|
|
162
|
+
dependsValue?: string
|
|
163
|
+
/** Overrides the disabled-state hint shown while `dependsValue` is empty. */
|
|
164
|
+
dependsHint?: string
|
|
148
165
|
}
|
|
149
166
|
|
|
150
|
-
export function DynamicSelectField({
|
|
167
|
+
export function DynamicSelectField({
|
|
168
|
+
field,
|
|
169
|
+
value,
|
|
170
|
+
onChange,
|
|
171
|
+
seedOption,
|
|
172
|
+
dependsValue,
|
|
173
|
+
dependsHint,
|
|
174
|
+
}: DynamicSelectFieldProps) {
|
|
151
175
|
const [open, setOpen] = useState(false)
|
|
152
176
|
const [search, setSearch] = useState('')
|
|
153
177
|
const debounced = useDebounced(search, 250)
|
|
@@ -159,20 +183,49 @@ export function DynamicSelectField({ field, value, onChange, seedOption }: Dynam
|
|
|
159
183
|
// for the FK target, not just camelCase `ref`.
|
|
160
184
|
const fieldRef = getFieldRef(field)
|
|
161
185
|
|
|
186
|
+
// Options routing: an `optionsConfig.source` (dependent/scoped picker) wins
|
|
187
|
+
// over the field's `ref` — query the SOURCE model with `field=<value>`;
|
|
188
|
+
// otherwise keep the canonical `ref`-based resolution.
|
|
189
|
+
const source = resolveOptionsSource(field)
|
|
190
|
+
|
|
191
|
+
// Cascade: a `dependsOn` field whose value is still empty leaves this
|
|
192
|
+
// picker disabled until the parent is set. `dependsValue` is the resolved
|
|
193
|
+
// value the caller threaded from the form context.
|
|
194
|
+
const dependsOn = getDependsOn(field)
|
|
195
|
+
const scope = dependsValue ? String(dependsValue) : ''
|
|
196
|
+
const blockedByDependency = !!dependsOn && scope === ''
|
|
197
|
+
|
|
162
198
|
const { options, loading } = useOptionsResolver({
|
|
163
199
|
modelKey: '',
|
|
164
|
-
fieldKey:
|
|
165
|
-
ref:
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
|
|
200
|
+
fieldKey: source.fieldKey,
|
|
201
|
+
ref: source.ref,
|
|
202
|
+
// optionsConfig.source → `/options/<source>`. Else searchEndpoint only
|
|
203
|
+
// drives the URL when there's no ref — ref is the canonical,
|
|
204
|
+
// kernel-derived path and wins.
|
|
205
|
+
endpoint: source.endpoint ?? (source.ref ? undefined : field.searchEndpoint),
|
|
169
206
|
query: debounced,
|
|
170
207
|
limit: 20,
|
|
208
|
+
// Cascade scope forwarded as filter_value (only when this field
|
|
209
|
+
// declares a dependency). Re-fetches when the parent value changes.
|
|
210
|
+
filterValue: dependsOn ? scope : undefined,
|
|
171
211
|
// Don't fetch until the popover opens (and keep fetching as the query
|
|
172
|
-
// changes while open).
|
|
173
|
-
|
|
212
|
+
// changes while open). A picker blocked by an unset dependency never
|
|
213
|
+
// fetches.
|
|
214
|
+
enabled: open && !blockedByDependency,
|
|
174
215
|
})
|
|
175
216
|
|
|
217
|
+
// When the depended-on value changes, the previously-picked option no longer
|
|
218
|
+
// belongs to the new scope, so clear the selection (skip the initial mount).
|
|
219
|
+
const prevScopeRef = useRef<string>(scope)
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
if (!dependsOn) return
|
|
222
|
+
if (prevScopeRef.current !== scope) {
|
|
223
|
+
prevScopeRef.current = scope
|
|
224
|
+
setPicked(null)
|
|
225
|
+
if (value) onChange('')
|
|
226
|
+
}
|
|
227
|
+
}, [dependsOn, scope, value, onChange])
|
|
228
|
+
|
|
176
229
|
// The currently-selected option, resolved either from what the user picked
|
|
177
230
|
// (cached in `picked`) or from the loaded page. Drives both the trigger
|
|
178
231
|
// label and its thumbnail.
|
|
@@ -225,7 +278,7 @@ export function DynamicSelectField({ field, value, onChange, seedOption }: Dynam
|
|
|
225
278
|
// "+" off-screen — it only "fit" once a short value was selected.
|
|
226
279
|
return (
|
|
227
280
|
<div className="flex w-full min-w-0 items-center gap-1.5">
|
|
228
|
-
<Popover open={open} onOpenChange={setOpen}>
|
|
281
|
+
<Popover open={open && !blockedByDependency} onOpenChange={(o: boolean) => { if (!blockedByDependency) setOpen(o) }}>
|
|
229
282
|
<PopoverTrigger asChild>
|
|
230
283
|
<Button
|
|
231
284
|
type="button"
|
|
@@ -233,15 +286,19 @@ export function DynamicSelectField({ field, value, onChange, seedOption }: Dynam
|
|
|
233
286
|
role="combobox"
|
|
234
287
|
aria-expanded={open}
|
|
235
288
|
id={field.key}
|
|
289
|
+
disabled={blockedByDependency}
|
|
236
290
|
className="min-w-0 flex-1 justify-between font-normal"
|
|
237
291
|
data-empty={!value}
|
|
292
|
+
data-depends-blocked={blockedByDependency ? '' : undefined}
|
|
238
293
|
>
|
|
239
294
|
<span className="flex min-w-0 flex-1 items-center gap-2 text-left">
|
|
240
295
|
{hasVisual && value ? (
|
|
241
296
|
<OptionLead option={selectedOption} size={20} />
|
|
242
297
|
) : null}
|
|
243
298
|
<span className={'min-w-0 flex-1 truncate ' + (selectedLabel ? '' : 'text-muted-foreground')}>
|
|
244
|
-
{
|
|
299
|
+
{blockedByDependency
|
|
300
|
+
? (dependsHint || DEFAULT_DEPENDS_HINT)
|
|
301
|
+
: selectedLabel || field.placeholder || 'Buscar…'}
|
|
245
302
|
</span>
|
|
246
303
|
</span>
|
|
247
304
|
<ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
|
package/src/index.ts
CHANGED
|
@@ -163,7 +163,13 @@ export {
|
|
|
163
163
|
resolveValidatorToken,
|
|
164
164
|
type OrgConfigBridge,
|
|
165
165
|
} from './use-org-config-bridge'
|
|
166
|
-
export {
|
|
166
|
+
export {
|
|
167
|
+
registerValidator,
|
|
168
|
+
getDependsOn,
|
|
169
|
+
resolveDependsValue,
|
|
170
|
+
getOptionsConfig,
|
|
171
|
+
resolveOptionsSource,
|
|
172
|
+
} from './dynamic-form-schema'
|
|
167
173
|
export {
|
|
168
174
|
ActivityValueRenderer,
|
|
169
175
|
type ActivityValueRendererProps,
|
package/src/types.ts
CHANGED
|
@@ -231,6 +231,30 @@ export interface ActionFieldDef {
|
|
|
231
231
|
*/
|
|
232
232
|
source?: string
|
|
233
233
|
relation?: string
|
|
234
|
+
/**
|
|
235
|
+
* Cascade dependency: the key of ANOTHER field in the same action form
|
|
236
|
+
* (a header field or a sibling item-field) whose current value supplies
|
|
237
|
+
* this picker's `filter_value`. While the depended-on field is empty the
|
|
238
|
+
* picker is disabled with a hint; once it has a value the picker fetches
|
|
239
|
+
* options scoped by it and re-fetches whenever it changes (clearing the
|
|
240
|
+
* current selection). Without `dependsOn` the picker lists everything
|
|
241
|
+
* (retrocompat). Tolerates the snake_case `depends_on` the kernel serves.
|
|
242
|
+
*/
|
|
243
|
+
dependsOn?: string
|
|
244
|
+
/** snake_case alias served by the kernel manifest for `dependsOn`. */
|
|
245
|
+
depends_on?: string
|
|
246
|
+
/**
|
|
247
|
+
* Enriched options routing the kernel serves for a dependent/scoped picker.
|
|
248
|
+
* When it carries a `source`, the picker queries that source MODEL (not the
|
|
249
|
+
* field's `ref`): URL `/options/<source>`, query field = `value` (falling
|
|
250
|
+
* back to the field's own key), and the cascade `filter_value` is the value
|
|
251
|
+
* of the `dependsOn` field. `description` is projected into the option
|
|
252
|
+
* subtitle. Tolerates the snake_case `options_config` the kernel emits.
|
|
253
|
+
* Absent → the picker keeps its `ref`-based behaviour (retrocompat).
|
|
254
|
+
*/
|
|
255
|
+
optionsConfig?: FieldOptionsConfig
|
|
256
|
+
/** snake_case alias served by the kernel manifest for `optionsConfig`. */
|
|
257
|
+
options_config?: FieldOptionsConfig
|
|
234
258
|
/**
|
|
235
259
|
* Columns of a repeatable line-items group. Mirrors the kernel v3
|
|
236
260
|
* `ActionField.item_fields` (json `item_fields`). Present on a field
|
|
@@ -298,6 +322,31 @@ export interface FieldBalanceRule {
|
|
|
298
322
|
require_nonzero?: boolean
|
|
299
323
|
}
|
|
300
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Enriched options-resolution config the kernel attaches to a dependent/scoped
|
|
327
|
+
* picker field (json `options_config`). When `source` is present the SDK queries
|
|
328
|
+
* the source model instead of the field's `ref`. All keys are snake_case as the
|
|
329
|
+
* kernel serves them; the SDK reads them as-is via `getOptionsConfig`.
|
|
330
|
+
*/
|
|
331
|
+
export interface FieldOptionsConfig {
|
|
332
|
+
/** Discriminator the kernel sets (e.g. `'dynamic'`). Informational. */
|
|
333
|
+
type?: string
|
|
334
|
+
/** Source MODEL the candidates come from → URL `/options/<source>`. */
|
|
335
|
+
source?: string
|
|
336
|
+
/** Column of `source` compared against the cascade `filter_value`. */
|
|
337
|
+
filter_by?: string
|
|
338
|
+
/** Column of `source` used as the option value → query `?field=<value>`. */
|
|
339
|
+
value?: string
|
|
340
|
+
/** Related model used to resolve the option label by id (host-side enrich). */
|
|
341
|
+
label_ref?: string
|
|
342
|
+
/** Column of `source` projected into `option.description` (e.g. qty). */
|
|
343
|
+
description?: string
|
|
344
|
+
/** Optional ordering column. */
|
|
345
|
+
order_by?: string
|
|
346
|
+
/** Optional column projected into `option.image`. */
|
|
347
|
+
image?: string
|
|
348
|
+
}
|
|
349
|
+
|
|
301
350
|
export interface ActionDefinition {
|
|
302
351
|
key: string
|
|
303
352
|
name: string
|
|
@@ -58,6 +58,15 @@ export interface UseOptionsResolverArgs {
|
|
|
58
58
|
* server returns the first page unfiltered.
|
|
59
59
|
*/
|
|
60
60
|
query?: string
|
|
61
|
+
/**
|
|
62
|
+
* Cascade scope forwarded as `?filter_value=`. Set by a dependent picker
|
|
63
|
+
* from the current value of the field it `dependsOn` (e.g. a product
|
|
64
|
+
* picker scoped to the header's `source_warehouse_id`). When empty/undefined
|
|
65
|
+
* the param is omitted (no scope — the picker lists everything). Changing it
|
|
66
|
+
* re-fetches; an empty string is treated as "not set" so a cleared parent
|
|
67
|
+
* does not query for the empty-string scope.
|
|
68
|
+
*/
|
|
69
|
+
filterValue?: string
|
|
61
70
|
/**
|
|
62
71
|
* Server-side pagination cap. Defaults to 50 (kernel
|
|
63
72
|
* DefaultOptionsLimit) if omitted.
|
|
@@ -105,6 +114,7 @@ export function useOptionsResolver(args: UseOptionsResolverArgs): UseOptionsReso
|
|
|
105
114
|
limit,
|
|
106
115
|
enabled = true,
|
|
107
116
|
endpoint,
|
|
117
|
+
filterValue,
|
|
108
118
|
} = args
|
|
109
119
|
|
|
110
120
|
const api = useApi()
|
|
@@ -159,6 +169,10 @@ export function useOptionsResolver(args: UseOptionsResolverArgs): UseOptionsReso
|
|
|
159
169
|
const params: Record<string, string | number> = { field: effectiveField }
|
|
160
170
|
if (query) params.q = query
|
|
161
171
|
if (typeof limit === 'number' && limit > 0) params.limit = limit
|
|
172
|
+
// Cascade scope: a dependent picker passes the value of the field it
|
|
173
|
+
// `dependsOn`. Skip empty strings so a cleared parent omits the param
|
|
174
|
+
// (no scope) rather than querying for the empty-string filter_value.
|
|
175
|
+
if (filterValue) params.filter_value = filterValue
|
|
162
176
|
|
|
163
177
|
api.get(url, { params, signal: controller.signal })
|
|
164
178
|
.then((res) => {
|
|
@@ -200,7 +214,7 @@ export function useOptionsResolver(args: UseOptionsResolverArgs): UseOptionsReso
|
|
|
200
214
|
return () => {
|
|
201
215
|
controller.abort()
|
|
202
216
|
}
|
|
203
|
-
}, [api, url, effectiveField, query, limit, enabled, refreshKey])
|
|
217
|
+
}, [api, url, effectiveField, query, limit, enabled, filterValue, refreshKey])
|
|
204
218
|
|
|
205
219
|
return {
|
|
206
220
|
options,
|