@asteby/metacore-runtime-react 18.17.3 → 18.19.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/action-modal-dispatcher.js +16 -5
  3. package/dist/collection-cell.d.ts +22 -0
  4. package/dist/collection-cell.d.ts.map +1 -0
  5. package/dist/collection-cell.js +141 -0
  6. package/dist/dynamic-columns.d.ts.map +1 -1
  7. package/dist/dynamic-columns.js +2 -1
  8. package/dist/dynamic-form-schema.d.ts +41 -1
  9. package/dist/dynamic-form-schema.d.ts.map +1 -1
  10. package/dist/dynamic-form-schema.js +61 -0
  11. package/dist/dynamic-line-items.d.ts +7 -1
  12. package/dist/dynamic-line-items.d.ts.map +1 -1
  13. package/dist/dynamic-line-items.js +46 -12
  14. package/dist/dynamic-select-field.d.ts +17 -1
  15. package/dist/dynamic-select-field.d.ts.map +1 -1
  16. package/dist/dynamic-select-field.js +48 -11
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -1
  20. package/dist/types.d.ts +48 -0
  21. package/dist/types.d.ts.map +1 -1
  22. package/dist/use-options-resolver.d.ts +9 -0
  23. package/dist/use-options-resolver.d.ts.map +1 -1
  24. package/dist/use-options-resolver.js +7 -2
  25. package/package.json +1 -1
  26. package/src/__tests__/collection-cell.test.tsx +115 -0
  27. package/src/__tests__/dependent-options.test.tsx +337 -0
  28. package/src/action-modal-dispatcher.tsx +15 -4
  29. package/src/collection-cell.tsx +277 -0
  30. package/src/dynamic-columns.tsx +2 -5
  31. package/src/dynamic-form-schema.ts +72 -1
  32. package/src/dynamic-line-items.tsx +86 -13
  33. package/src/dynamic-select-field.tsx +69 -12
  34. package/src/index.ts +13 -1
  35. package/src/types.ts +49 -0
  36. package/src/use-options-resolver.ts +15 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,54 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 18.19.0
4
+
5
+ ### Minor Changes
6
+
7
+ - de0b4bb: Add a generic `CollectionCell` renderer for jsonb / array / object table-cell values.
8
+
9
+ Previously the `default:` branch of `defaultGetDynamicColumns` rendered such
10
+ values as raw `JSON.stringify(value)`, which was unreadable. Every jsonb column
11
+ now renders a compact, brand-neutral, dark-mode-friendly cell with no per-addon
12
+ config:
13
+ - **Array of objects** (e.g. line items): a count Badge (`2 ítems`) that opens a
14
+ Popover mini-table — columns are the prettified union of row keys, cells go
15
+ through a shared `formatScalar` (uuid/long strings truncated, booleans as
16
+ ✓/✗, nested shapes summarized).
17
+ - **Array of scalars**: first few joined inline with a `+N` overflow, full list
18
+ in the popover.
19
+ - **Plain object**: first few `key: value` pairs inline, all pairs in the popover.
20
+ - **null / empty**: muted `-`.
21
+ - JSON-string values are defensively parsed; unparseable strings are truncated.
22
+
23
+ Exports `CollectionCell`, `formatScalar`, `prettifyKey`, and `CollectionCellProps`.
24
+
25
+ ## 18.18.0
26
+
27
+ ### Minor Changes
28
+
29
+ - be0d2b8: Dependent (cascading) options for declarative pickers. A field/item_field may
30
+ declare `dependsOn` (camelCase) / `depends_on` (snake_case) naming another field
31
+ in the same action form — a header field (e.g. `source_warehouse_id`) or a
32
+ sibling row cell — whose current value scopes this picker's options. The value
33
+ is forwarded to the options endpoint as `filter_value` (`useOptionsResolver`
34
+ gains a `filterValue` arg) and the picker re-fetches when it changes, clearing
35
+ the stale selection. While the depended-on field is empty the picker is disabled
36
+ with an overridable hint. Header form context flows down through
37
+ `DynamicLineItems` → `CellRenderer`/`RefCell` so a line-items cell can depend on
38
+ a header field, not just same-row values. Option `description` (e.g. available
39
+ qty) is now shown in the line-items `RefCell` select as well as
40
+ `DynamicSelectField`.
41
+
42
+ A field/item_field may also carry an `optionsConfig` (camelCase) /
43
+ `options_config` (snake_case) object — the kernel's enriched options routing,
44
+ shaped `{ type, source, filter_by, value, label_ref, description }`. When it
45
+ declares a `source`, the picker queries that SOURCE model instead of the field's
46
+ `ref`: URL `/options/<source>` with query field `<value ?? field.key>` and the
47
+ cascade `filter_value`. Without `optionsConfig.source` the picker keeps its
48
+ `ref`-based behaviour (retrocompat). New `getOptionsConfig` / `resolveOptionsSource`
49
+ helpers (and `FieldOptionsConfig` type) are exported. Fully generic — no domain
50
+ knowledge in the SDK.
51
+
3
52
  ## 18.17.3
4
53
 
5
54
  ### 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.
@@ -0,0 +1,22 @@
1
+ import * as React from 'react';
2
+ /** snake_case / dotted / kebab key → Title Case (`product_id` → "Product ID"). */
3
+ export declare function prettifyKey(key: string): string;
4
+ /**
5
+ * Render a single scalar (or near-scalar) value for compact display.
6
+ * - uuid-like or very long (32+ char) strings → first 8 chars + "…"
7
+ * - numbers / booleans → rendered as-is (booleans as ✓ / ✗)
8
+ * - nested object → "{…}", nested array → "[N]"
9
+ * - null / undefined / "" → "-"
10
+ */
11
+ export declare function formatScalar(value: unknown): string;
12
+ export interface CollectionCellProps {
13
+ value: unknown;
14
+ /** Max items previewed inline for scalar arrays. */
15
+ maxInline?: number;
16
+ }
17
+ /**
18
+ * Generic renderer for jsonb / array / object cell values. Brand-neutral,
19
+ * compact, dark-mode friendly. Never throws on unexpected shapes.
20
+ */
21
+ export declare function CollectionCell({ value, maxInline }: CollectionCellProps): React.JSX.Element;
22
+ //# sourceMappingURL=collection-cell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection-cell.d.ts","sourceRoot":"","sources":["../src/collection-cell.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAoB9B,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG/C;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAYnD;AAyID,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,OAAO,CAAA;IACd,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,SAAa,EAAE,EAAE,mBAAmB,qBA6E3E"}
@@ -0,0 +1,141 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { List } from 'lucide-react';
3
+ import { Badge, Popover, PopoverContent, PopoverTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, cn, } from '@asteby/metacore-ui';
4
+ import { humanizeToken } from './dynamic-columns-helpers';
5
+ const UUID_LIKE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6
+ /** snake_case / dotted / kebab key → Title Case (`product_id` → "Product ID"). */
7
+ export function prettifyKey(key) {
8
+ const pretty = humanizeToken(key);
9
+ return pretty || key;
10
+ }
11
+ /**
12
+ * Render a single scalar (or near-scalar) value for compact display.
13
+ * - uuid-like or very long (32+ char) strings → first 8 chars + "…"
14
+ * - numbers / booleans → rendered as-is (booleans as ✓ / ✗)
15
+ * - nested object → "{…}", nested array → "[N]"
16
+ * - null / undefined / "" → "-"
17
+ */
18
+ export function formatScalar(value) {
19
+ if (value === null || value === undefined)
20
+ return '-';
21
+ if (typeof value === 'boolean')
22
+ return value ? '✓' : '✗';
23
+ if (typeof value === 'number')
24
+ return String(value);
25
+ if (Array.isArray(value))
26
+ return `[${value.length}]`;
27
+ if (typeof value === 'object')
28
+ return '{…}';
29
+ const str = String(value);
30
+ if (str === '')
31
+ return '-';
32
+ if (UUID_LIKE_RE.test(str) || str.length >= 32) {
33
+ return `${str.slice(0, 8)}…`;
34
+ }
35
+ return str;
36
+ }
37
+ const isPlainObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
38
+ /**
39
+ * Defensively coerce a raw cell value into something renderable. Strings that
40
+ * look like JSON (`[`/`{` start) are parsed; everything else is passed through.
41
+ */
42
+ function parseValue(value) {
43
+ if (typeof value !== 'string')
44
+ return value;
45
+ const trimmed = value.trim();
46
+ if ((trimmed.startsWith('[') && trimmed.endsWith(']')) ||
47
+ (trimmed.startsWith('{') && trimmed.endsWith('}'))) {
48
+ try {
49
+ return JSON.parse(trimmed);
50
+ }
51
+ catch {
52
+ return value;
53
+ }
54
+ }
55
+ return value;
56
+ }
57
+ /** Stable union of keys across an array of row objects, first-seen order. */
58
+ function unionKeys(rows) {
59
+ const seen = [];
60
+ const set = new Set();
61
+ for (const row of rows) {
62
+ for (const key of Object.keys(row)) {
63
+ if (!set.has(key)) {
64
+ set.add(key);
65
+ seen.push(key);
66
+ }
67
+ }
68
+ }
69
+ return seen;
70
+ }
71
+ const PANEL_CLASS = 'w-auto max-w-[480px] max-h-[320px] overflow-auto p-0';
72
+ function MiniTable({ rows }) {
73
+ const keys = unionKeys(rows);
74
+ if (keys.length === 0) {
75
+ return _jsx("div", { className: "p-3 text-xs text-muted-foreground", children: "-" });
76
+ }
77
+ return (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsx(TableRow, { children: keys.map((key) => (_jsx(TableHead, { className: "text-xs whitespace-nowrap", children: prettifyKey(key) }, key))) }) }), _jsx(TableBody, { children: rows.map((row, i) => (_jsx(TableRow, { children: keys.map((key) => (_jsx(TableCell, { className: "text-xs whitespace-nowrap", children: formatScalar(row[key]) }, key))) }, i))) })] }));
78
+ }
79
+ function ScalarList({ values }) {
80
+ return (_jsx("ul", { className: "p-3 space-y-1", children: values.map((v, i) => (_jsx("li", { className: "text-xs text-foreground", children: formatScalar(v) }, i))) }));
81
+ }
82
+ function PairList({ entries }) {
83
+ return (_jsx("ul", { className: "p-3 space-y-1", children: entries.map(([key, v]) => (_jsxs("li", { className: "text-xs", children: [_jsxs("span", { className: "text-muted-foreground", children: [prettifyKey(key), ":"] }), ' ', _jsx("span", { className: "text-foreground", children: formatScalar(v) })] }, key))) }));
84
+ }
85
+ /** Compact badge trigger that opens a popover panel. */
86
+ function PopoverShell({ label, title, children, icon = true, }) {
87
+ return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Badge, { variant: "secondary", className: "cursor-pointer gap-1 font-normal", title: title, children: [icon ? _jsx(List, { className: "h-3 w-3" }) : null, label] }) }), _jsx(PopoverContent, { align: "start", className: cn(PANEL_CLASS), children: children })] }));
88
+ }
89
+ /**
90
+ * Generic renderer for jsonb / array / object cell values. Brand-neutral,
91
+ * compact, dark-mode friendly. Never throws on unexpected shapes.
92
+ */
93
+ export function CollectionCell({ value, maxInline = 3 }) {
94
+ const parsed = parseValue(value);
95
+ // Empty-ish → muted dash.
96
+ if (parsed === null ||
97
+ parsed === undefined ||
98
+ parsed === '' ||
99
+ (Array.isArray(parsed) && parsed.length === 0) ||
100
+ (isPlainObject(parsed) && Object.keys(parsed).length === 0)) {
101
+ return _jsx("span", { className: "text-muted-foreground text-xs", children: "-" });
102
+ }
103
+ // Non-collection scalar fell through here (e.g. unparseable string): truncate.
104
+ if (!Array.isArray(parsed) && !isPlainObject(parsed)) {
105
+ const str = String(parsed);
106
+ return (_jsx("span", { className: "text-muted-foreground text-xs truncate block max-w-[300px]", title: str, children: str.length > 80 ? `${str.slice(0, 80)}…` : str }));
107
+ }
108
+ // ARRAY ------------------------------------------------------------------
109
+ if (Array.isArray(parsed)) {
110
+ const allObjects = parsed.every((item) => isPlainObject(item));
111
+ if (allObjects) {
112
+ const rows = parsed;
113
+ const count = rows.length;
114
+ const label = count === 1 ? '1 ítem' : `${count} ítems`;
115
+ const title = rows
116
+ .map((row) => Object.entries(row)
117
+ .map(([k, v]) => `${prettifyKey(k)}: ${formatScalar(v)}`)
118
+ .join(', '))
119
+ .join(' | ');
120
+ return (_jsx(PopoverShell, { label: label, title: title, children: _jsx(MiniTable, { rows: rows }) }));
121
+ }
122
+ // Array of scalars (or mixed): preview first N joined, "+N" overflow.
123
+ const preview = parsed.slice(0, maxInline).map(formatScalar).join(', ');
124
+ const overflow = parsed.length - maxInline;
125
+ const label = overflow > 0 ? `${preview} +${overflow}` : preview;
126
+ const title = parsed.map(formatScalar).join(', ');
127
+ return (_jsx(PopoverShell, { label: label, title: title, icon: false, children: _jsx(ScalarList, { values: parsed }) }));
128
+ }
129
+ // PLAIN OBJECT -----------------------------------------------------------
130
+ const entries = Object.entries(parsed);
131
+ const inline = entries
132
+ .slice(0, maxInline)
133
+ .map(([k, v]) => `${prettifyKey(k)}: ${formatScalar(v)}`)
134
+ .join(', ');
135
+ const overflow = entries.length - maxInline;
136
+ const label = overflow > 0 ? `${inline} +${overflow}` : inline;
137
+ const title = entries
138
+ .map(([k, v]) => `${prettifyKey(k)}: ${formatScalar(v)}`)
139
+ .join(', ');
140
+ return (_jsx(PopoverShell, { label: label, title: title, icon: false, children: _jsx(PairList, { entries: entries }) }));
141
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAgC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AA8DD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAqKD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AAsID;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CA2nBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
1
+ {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAiC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AA8DD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAqKD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AAsID;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAunBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
@@ -24,6 +24,7 @@ import { Progress } from './dialogs/_primitives';
24
24
  import { humanizeToken } from './dynamic-columns-helpers';
25
25
  import { OptionsContext } from './options-context';
26
26
  import { DynamicIcon, isLucideIconName } from './dynamic-icon';
27
+ import { CollectionCell } from './collection-cell';
27
28
  import { isNilUuid, normalizeNilUuid } from './nil-uuid';
28
29
  import { isColumnVisibleInTable } from './column-visibility';
29
30
  const defaultGetImageUrl = (path) => path;
@@ -735,7 +736,7 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
735
736
  }
736
737
  default: {
737
738
  if (typeof value === 'object' && value !== null) {
738
- return (_jsx("span", { className: "text-muted-foreground text-xs", children: JSON.stringify(value) }));
739
+ return _jsx(CollectionCell, { value: value });
739
740
  }
740
741
  if (col.key === 'description' ||
741
742
  col.key === 'features' ||
@@ -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"}