@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.
- package/CHANGELOG.md +49 -0
- package/dist/action-modal-dispatcher.js +16 -5
- package/dist/collection-cell.d.ts +22 -0
- package/dist/collection-cell.d.ts.map +1 -0
- package/dist/collection-cell.js +141 -0
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +2 -1
- 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 +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -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__/collection-cell.test.tsx +115 -0
- package/src/__tests__/dependent-options.test.tsx +337 -0
- package/src/action-modal-dispatcher.tsx +15 -4
- package/src/collection-cell.tsx +277 -0
- package/src/dynamic-columns.tsx +2 -5
- 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 +13 -1
- package/src/types.ts +49 -0
- 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
|
-
|
|
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;
|
|
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"}
|
package/dist/dynamic-columns.js
CHANGED
|
@@ -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
|
|
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;
|
|
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":"
|
|
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:
|
|
115
|
-
ref:
|
|
129
|
+
fieldKey: optSource.fieldKey,
|
|
130
|
+
ref: optSource.ref,
|
|
131
|
+
endpoint: optSource.endpoint,
|
|
132
|
+
filterValue: dependsOn ? scope : undefined,
|
|
133
|
+
enabled: !blockedByDependency,
|
|
116
134
|
});
|
|
117
|
-
|
|
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;
|
|
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"}
|