@asteby/metacore-runtime-react 13.2.0 → 13.3.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 +16 -0
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +3 -0
- package/dist/dynamic-form.d.ts +1 -0
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +8 -0
- package/dist/dynamic-line-items.d.ts.map +1 -1
- package/dist/dynamic-line-items.js +6 -0
- package/dist/dynamic-select-field.d.ts +9 -0
- package/dist/dynamic-select-field.d.ts.map +1 -0
- package/dist/dynamic-select-field.js +74 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/dynamic-form-schema.ts +3 -0
- package/src/dynamic-form.tsx +8 -0
- package/src/dynamic-line-items.tsx +6 -0
- package/src/dynamic-select-field.tsx +164 -0
- package/src/types.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 13.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 99477d6: feat(runtime-react): add `dynamic_select` field widget — async searchable FK picker
|
|
8
|
+
|
|
9
|
+
Declarative answer to "I don't want to type a raw FK UUID". A field with
|
|
10
|
+
`type: "dynamic_select"` (or `widget: "dynamic_select"`) + `ref` renders a
|
|
11
|
+
typeahead combobox that queries the canonical options endpoint as the user
|
|
12
|
+
types (`GET /api/options/<ref>?field=id&q=<text>&limit=<n>`), reusing
|
|
13
|
+
`useOptionsResolver` (debounced, abortable). Works both as a flat form field
|
|
14
|
+
and as a line-items column cell (e.g. the account_id per debit/credit row of a
|
|
15
|
+
journal entry). The metacore equivalent of 7leguas' `type: search`, driven
|
|
16
|
+
entirely from the manifest — addons get a searchable picker with zero custom
|
|
17
|
+
React, keeping custom federated frontends for genuinely page-level UIs (POS).
|
|
18
|
+
|
|
3
19
|
## 13.2.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
|
@@ -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;AAqDD,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,
|
|
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;AAqDD,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAa3D"}
|
|
@@ -116,6 +116,9 @@ export function resolveWidget(field) {
|
|
|
116
116
|
switch (field.type) {
|
|
117
117
|
case 'textarea': return 'textarea';
|
|
118
118
|
case 'select': return 'select';
|
|
119
|
+
// Async searchable single-select against /api/options/<ref>. The
|
|
120
|
+
// declarative replacement for typing a raw FK UUID.
|
|
121
|
+
case 'dynamic_select': return 'dynamic_select';
|
|
119
122
|
case 'boolean': return 'switch';
|
|
120
123
|
case 'number': return 'number';
|
|
121
124
|
case 'date': return 'date';
|
package/dist/dynamic-form.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ActionFieldDef } from './types';
|
|
|
2
2
|
import { buildZodSchema, resolveWidget } from './dynamic-form-schema';
|
|
3
3
|
export { buildZodSchema, resolveWidget };
|
|
4
4
|
export { DynamicLineItems } from './dynamic-line-items';
|
|
5
|
+
export { DynamicSelectField } from './dynamic-select-field';
|
|
5
6
|
export interface DynamicFormProps {
|
|
6
7
|
fields: ActionFieldDef[];
|
|
7
8
|
initialValues?: Record<string, any>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,aAAa,EAAoB,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,aAAa,EAAoB,MAAM,uBAAuB,CAAA;AAKvF,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAE3D,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,cAAc,EAAE,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,WAAuB,EACvB,WAAwB,EACxB,QAAgB,GACnB,EAAE,gBAAgB,2CAoElB"}
|
package/dist/dynamic-form.js
CHANGED
|
@@ -7,8 +7,10 @@ import { Input, Textarea, Label, Switch, Button, Select, SelectContent, SelectIt
|
|
|
7
7
|
import { buildZodSchema, resolveWidget, isLineItemsField } from './dynamic-form-schema';
|
|
8
8
|
import { useOptionsResolver } from './use-options-resolver';
|
|
9
9
|
import { DynamicLineItems } from './dynamic-line-items';
|
|
10
|
+
import { DynamicSelectField } from './dynamic-select-field';
|
|
10
11
|
export { buildZodSchema, resolveWidget };
|
|
11
12
|
export { DynamicLineItems } from './dynamic-line-items';
|
|
13
|
+
export { DynamicSelectField } from './dynamic-select-field';
|
|
12
14
|
export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitLabel = 'Guardar', cancelLabel = 'Cancelar', disabled = false, }) {
|
|
13
15
|
const [values, setValues] = useState({});
|
|
14
16
|
const [errors, setErrors] = useState({});
|
|
@@ -58,6 +60,12 @@ function FieldRenderer({ field, value, onChange }) {
|
|
|
58
60
|
return _jsx(DynamicLineItems, { field: field, value: value, onChange: onChange });
|
|
59
61
|
}
|
|
60
62
|
const widget = resolveWidget(field);
|
|
63
|
+
// Async searchable picker (typeahead against /api/options/<ref>?q=…).
|
|
64
|
+
// Preferred for FK fields with large option sets — no UUID typing, no
|
|
65
|
+
// dumping every row into a plain <select>.
|
|
66
|
+
if (widget === 'dynamic_select') {
|
|
67
|
+
return _jsx(DynamicSelectField, { field: field, value: value, onChange: onChange });
|
|
68
|
+
}
|
|
61
69
|
// Ref-driven select: hook into useOptionsResolver so the canonical
|
|
62
70
|
// /api/options/<ref>?field=id endpoint feeds the dropdown. This is
|
|
63
71
|
// the path the kernel auto-derives for FK columns; legacy callers
|
|
@@ -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;
|
|
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;AAK7C,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;AAUD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAgB,EAAE,EAAE,qBAAqB,2CAwEnG"}
|
|
@@ -11,6 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
import { Input, Textarea, Switch, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@asteby/metacore-ui/primitives';
|
|
12
12
|
import { Plus, Trash2 } from 'lucide-react';
|
|
13
13
|
import { resolveWidget, getItemFields } from './dynamic-form-schema';
|
|
14
|
+
import { DynamicSelectField } from './dynamic-select-field';
|
|
14
15
|
import { useOptionsResolver } from './use-options-resolver';
|
|
15
16
|
function emptyRow(itemFields) {
|
|
16
17
|
const row = {};
|
|
@@ -33,6 +34,11 @@ export function DynamicLineItems({ field, value, onChange, disabled = false }) {
|
|
|
33
34
|
// a scalar widget).
|
|
34
35
|
function CellRenderer({ field, value, onChange, disabled }) {
|
|
35
36
|
const widget = resolveWidget(field);
|
|
37
|
+
// Async searchable picker per row cell — e.g. the account_id column of a
|
|
38
|
+
// journal entry's debit/credit lines. Same widget as the flat form.
|
|
39
|
+
if (widget === 'dynamic_select') {
|
|
40
|
+
return _jsx(DynamicSelectField, { field: field, value: value, onChange: onChange });
|
|
41
|
+
}
|
|
36
42
|
if (widget === 'select' && field.ref) {
|
|
37
43
|
return _jsx(RefCell, { field: field, value: value, onChange: onChange, disabled: disabled });
|
|
38
44
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ActionFieldDef } from './types';
|
|
2
|
+
export interface DynamicSelectFieldProps {
|
|
3
|
+
field: ActionFieldDef;
|
|
4
|
+
value: any;
|
|
5
|
+
onChange: (v: any) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function DynamicSelectField({ field, value, onChange }: DynamicSelectFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export default DynamicSelectField;
|
|
9
|
+
//# sourceMappingURL=dynamic-select-field.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAW7C,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;CAC7B;AAED,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,uBAAuB,2CA0GrF;AAED,eAAe,kBAAkB,CAAA"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// DynamicSelectField — async, searchable single-select for declarative forms.
|
|
3
|
+
//
|
|
4
|
+
// This is the declarative answer to "I don't want to type a raw FK UUID".
|
|
5
|
+
// Instead of a plain <select> that dumps every option (RefSelect) or a free
|
|
6
|
+
// text input, it renders a typeahead combobox that queries the canonical
|
|
7
|
+
// options endpoint as the user types:
|
|
8
|
+
//
|
|
9
|
+
// GET /api/options/<ref>?field=id&q=<text>&limit=<n>
|
|
10
|
+
//
|
|
11
|
+
// reusing `useOptionsResolver` (which already debounce-aborts in-flight
|
|
12
|
+
// requests). It is the metacore equivalent of 7leguas' `search.go` / dynamic
|
|
13
|
+
// `type: search` field, but driven entirely from the manifest — so an addon
|
|
14
|
+
// declares `type: "dynamic_select"` + `ref` and gets a searchable picker with
|
|
15
|
+
// zero custom React.
|
|
16
|
+
//
|
|
17
|
+
// Resolution path (highest priority first):
|
|
18
|
+
// 1. field.ref → /options/<ref>?field=id (canonical, preferred)
|
|
19
|
+
// 2. field.searchEndpoint→ used verbatim as the options endpoint (escape hatch)
|
|
20
|
+
//
|
|
21
|
+
// Edit-mode caveat: resolving an EXISTING value's label requires the id to be
|
|
22
|
+
// in a fetched page (we match by id against loaded options, else show the raw
|
|
23
|
+
// value). A dedicated `?ids=` lookup is a follow-up; create flows — the common
|
|
24
|
+
// case — start empty and never hit this.
|
|
25
|
+
import { useEffect, useState } from 'react';
|
|
26
|
+
import { Button, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Popover, PopoverContent, PopoverTrigger, } from '@asteby/metacore-ui/primitives';
|
|
27
|
+
import { Check, ChevronsUpDown, Loader2 } from 'lucide-react';
|
|
28
|
+
import { useOptionsResolver } from './use-options-resolver';
|
|
29
|
+
function useDebounced(value, ms) {
|
|
30
|
+
const [debounced, setDebounced] = useState(value);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const t = setTimeout(() => setDebounced(value), ms);
|
|
33
|
+
return () => clearTimeout(t);
|
|
34
|
+
}, [value, ms]);
|
|
35
|
+
return debounced;
|
|
36
|
+
}
|
|
37
|
+
export function DynamicSelectField({ field, value, onChange }) {
|
|
38
|
+
const [open, setOpen] = useState(false);
|
|
39
|
+
const [search, setSearch] = useState('');
|
|
40
|
+
const debounced = useDebounced(search, 250);
|
|
41
|
+
// Remember the label of the option the user actually picked so the trigger
|
|
42
|
+
// shows a name (not a UUID) without a round-trip.
|
|
43
|
+
const [picked, setPicked] = useState(null);
|
|
44
|
+
const { options, loading } = useOptionsResolver({
|
|
45
|
+
modelKey: '',
|
|
46
|
+
fieldKey: 'id',
|
|
47
|
+
ref: field.ref,
|
|
48
|
+
// searchEndpoint only drives the URL when there's no ref — ref is the
|
|
49
|
+
// canonical, kernel-derived path and wins.
|
|
50
|
+
endpoint: field.ref ? undefined : field.searchEndpoint,
|
|
51
|
+
query: debounced,
|
|
52
|
+
limit: 20,
|
|
53
|
+
// Don't fetch until the popover opens (and keep fetching as the query
|
|
54
|
+
// changes while open).
|
|
55
|
+
enabled: open,
|
|
56
|
+
});
|
|
57
|
+
const selectedLabel = (picked && String(picked.id) === String(value) ? picked.label : null) ??
|
|
58
|
+
options.find((o) => String(o.id) === String(value))?.label ??
|
|
59
|
+
(value ? String(value) : '');
|
|
60
|
+
const handlePick = (opt) => {
|
|
61
|
+
setPicked(opt);
|
|
62
|
+
onChange(String(opt.id));
|
|
63
|
+
setOpen(false);
|
|
64
|
+
setSearch('');
|
|
65
|
+
};
|
|
66
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", role: "combobox", "aria-expanded": open, id: field.key, className: "w-full justify-between font-normal", "data-empty": !value, children: [_jsx("span", { className: 'truncate ' + (selectedLabel ? '' : 'text-muted-foreground'), children: selectedLabel || field.placeholder || 'Buscar…' }), _jsx(ChevronsUpDown, { className: "ml-2 size-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "p-0", align: "start",
|
|
67
|
+
// Match the trigger width without an arbitrary Tailwind class
|
|
68
|
+
// (those don't always survive a consuming app's Tailwind scan).
|
|
69
|
+
style: { width: 'var(--radix-popover-trigger-width)' }, children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { placeholder: field.placeholder || 'Buscar…', value: search, onValueChange: setSearch }), _jsxs(CommandList, { children: [loading && (_jsxs("div", { className: "text-muted-foreground flex items-center justify-center gap-2 py-6 text-sm", children: [_jsx(Loader2, { className: "size-4 animate-spin" }), "Buscando\u2026"] })), !loading && options.length === 0 && (_jsx(CommandEmpty, { children: debounced ? 'Sin resultados' : 'Escribí para buscar…' })), !loading && options.length > 0 && (_jsx(CommandGroup, { className: "max-h-64 overflow-auto", children: options.map((opt) => {
|
|
70
|
+
const isSel = String(opt.id) === String(value);
|
|
71
|
+
return (_jsxs(CommandItem, { value: String(opt.id), onSelect: () => handlePick(opt), children: [_jsx(Check, { className: 'mr-2 size-4 ' + (isSel ? 'opacity-100' : 'opacity-0') }), _jsxs("div", { className: "flex min-w-0 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)));
|
|
72
|
+
}) }))] })] }) })] }));
|
|
73
|
+
}
|
|
74
|
+
export default DynamicSelectField;
|
package/dist/types.d.ts
CHANGED
|
@@ -100,7 +100,7 @@ export interface FieldValidation {
|
|
|
100
100
|
max?: number;
|
|
101
101
|
custom?: string;
|
|
102
102
|
}
|
|
103
|
-
export type FieldWidget = 'text' | 'textarea' | 'richtext' | 'color' | 'number' | 'date' | 'select' | 'switch';
|
|
103
|
+
export type FieldWidget = 'text' | 'textarea' | 'richtext' | 'color' | 'number' | 'date' | 'select' | 'dynamic_select' | 'switch';
|
|
104
104
|
export interface ActionFieldDef {
|
|
105
105
|
key: string;
|
|
106
106
|
label: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,cAAc,EAAE,MAAM,CAAA;IACtB,iBAAiB,EAAE,MAAM,CAAA;IACzB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,UAAU,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,MAAM,CAAA;IACnE,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACrF,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAEjF,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,qBAAqB,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,eAAe,GAAG,OAAO,CAAA;IAC3I,QAAQ,EAAE,OAAO,CAAA;IACjB,UAAU,EAAE,OAAO,CAAA;IACnB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAA;IAC7B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACjC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC3E;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,UAAU,CAAC,EAAE,eAAe,CAAA;CAC/B;AAED,MAAM,WAAW,eAAe;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,QAAQ,CAAA;IACxC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAC3B;AASD,MAAM,WAAW,eAAe;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;CAClB;AAID,MAAM,MAAM,WAAW,GACjB,MAAM,GACN,UAAU,GACV,UAAU,GACV,OAAO,GACP,QAAQ,GACR,MAAM,GACN,QAAQ,GACR,gBAAgB,GAChB,QAAQ,CAAA;AAEd,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC5C,YAAY,CAAC,EAAE,GAAG,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,MAAM,CAAC,EAAE,WAAW,GAAG,MAAM,CAAA;IAC7B;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,cAAc,EAAE,CAAA;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAA;IACpD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,cAAc,EAAE,CAAA;IACzB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAA;CACzC;AAED,MAAM,WAAW,WAAW,CAAC,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,CAAC,CAAA;IACP,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,cAAc;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;CAChB;AAKD,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,cAAc,EAAE,CAAA;IACzB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,SAAS,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAA;CACzC"}
|
package/package.json
CHANGED
|
@@ -119,6 +119,9 @@ export function resolveWidget(field: ActionFieldDef): string {
|
|
|
119
119
|
switch (field.type) {
|
|
120
120
|
case 'textarea': return 'textarea'
|
|
121
121
|
case 'select': return 'select'
|
|
122
|
+
// Async searchable single-select against /api/options/<ref>. The
|
|
123
|
+
// declarative replacement for typing a raw FK UUID.
|
|
124
|
+
case 'dynamic_select': return 'dynamic_select'
|
|
122
125
|
case 'boolean': return 'switch'
|
|
123
126
|
case 'number': return 'number'
|
|
124
127
|
case 'date': return 'date'
|
package/src/dynamic-form.tsx
CHANGED
|
@@ -18,9 +18,11 @@ import type { ActionFieldDef } from './types'
|
|
|
18
18
|
import { buildZodSchema, resolveWidget, isLineItemsField } from './dynamic-form-schema'
|
|
19
19
|
import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
|
|
20
20
|
import { DynamicLineItems } from './dynamic-line-items'
|
|
21
|
+
import { DynamicSelectField } from './dynamic-select-field'
|
|
21
22
|
|
|
22
23
|
export { buildZodSchema, resolveWidget }
|
|
23
24
|
export { DynamicLineItems } from './dynamic-line-items'
|
|
25
|
+
export { DynamicSelectField } from './dynamic-select-field'
|
|
24
26
|
|
|
25
27
|
export interface DynamicFormProps {
|
|
26
28
|
fields: ActionFieldDef[]
|
|
@@ -123,6 +125,12 @@ function FieldRenderer({ field, value, onChange }: FieldRendererProps) {
|
|
|
123
125
|
return <DynamicLineItems field={field} value={value} onChange={onChange} />
|
|
124
126
|
}
|
|
125
127
|
const widget = resolveWidget(field)
|
|
128
|
+
// Async searchable picker (typeahead against /api/options/<ref>?q=…).
|
|
129
|
+
// Preferred for FK fields with large option sets — no UUID typing, no
|
|
130
|
+
// dumping every row into a plain <select>.
|
|
131
|
+
if (widget === 'dynamic_select') {
|
|
132
|
+
return <DynamicSelectField field={field} value={value} onChange={onChange} />
|
|
133
|
+
}
|
|
126
134
|
// Ref-driven select: hook into useOptionsResolver so the canonical
|
|
127
135
|
// /api/options/<ref>?field=id endpoint feeds the dropdown. This is
|
|
128
136
|
// the path the kernel auto-derives for FK columns; legacy callers
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { Plus, Trash2 } from 'lucide-react'
|
|
22
22
|
import type { ActionFieldDef } from './types'
|
|
23
23
|
import { resolveWidget, getItemFields } from './dynamic-form-schema'
|
|
24
|
+
import { DynamicSelectField } from './dynamic-select-field'
|
|
24
25
|
import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
|
|
25
26
|
|
|
26
27
|
export interface DynamicLineItemsProps {
|
|
@@ -125,6 +126,11 @@ interface CellRendererProps {
|
|
|
125
126
|
// a scalar widget).
|
|
126
127
|
function CellRenderer({ field, value, onChange, disabled }: CellRendererProps) {
|
|
127
128
|
const widget = resolveWidget(field)
|
|
129
|
+
// Async searchable picker per row cell — e.g. the account_id column of a
|
|
130
|
+
// journal entry's debit/credit lines. Same widget as the flat form.
|
|
131
|
+
if (widget === 'dynamic_select') {
|
|
132
|
+
return <DynamicSelectField field={field} value={value} onChange={onChange} />
|
|
133
|
+
}
|
|
128
134
|
if (widget === 'select' && field.ref) {
|
|
129
135
|
return <RefCell field={field} value={value} onChange={onChange} disabled={disabled} />
|
|
130
136
|
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// DynamicSelectField — async, searchable single-select for declarative forms.
|
|
2
|
+
//
|
|
3
|
+
// This is the declarative answer to "I don't want to type a raw FK UUID".
|
|
4
|
+
// Instead of a plain <select> that dumps every option (RefSelect) or a free
|
|
5
|
+
// text input, it renders a typeahead combobox that queries the canonical
|
|
6
|
+
// options endpoint as the user types:
|
|
7
|
+
//
|
|
8
|
+
// GET /api/options/<ref>?field=id&q=<text>&limit=<n>
|
|
9
|
+
//
|
|
10
|
+
// reusing `useOptionsResolver` (which already debounce-aborts in-flight
|
|
11
|
+
// requests). It is the metacore equivalent of 7leguas' `search.go` / dynamic
|
|
12
|
+
// `type: search` field, but driven entirely from the manifest — so an addon
|
|
13
|
+
// declares `type: "dynamic_select"` + `ref` and gets a searchable picker with
|
|
14
|
+
// zero custom React.
|
|
15
|
+
//
|
|
16
|
+
// Resolution path (highest priority first):
|
|
17
|
+
// 1. field.ref → /options/<ref>?field=id (canonical, preferred)
|
|
18
|
+
// 2. field.searchEndpoint→ used verbatim as the options endpoint (escape hatch)
|
|
19
|
+
//
|
|
20
|
+
// Edit-mode caveat: resolving an EXISTING value's label requires the id to be
|
|
21
|
+
// in a fetched page (we match by id against loaded options, else show the raw
|
|
22
|
+
// value). A dedicated `?ids=` lookup is a follow-up; create flows — the common
|
|
23
|
+
// case — start empty and never hit this.
|
|
24
|
+
import { useEffect, useState } from 'react'
|
|
25
|
+
import {
|
|
26
|
+
Button,
|
|
27
|
+
Command,
|
|
28
|
+
CommandEmpty,
|
|
29
|
+
CommandGroup,
|
|
30
|
+
CommandInput,
|
|
31
|
+
CommandItem,
|
|
32
|
+
CommandList,
|
|
33
|
+
Popover,
|
|
34
|
+
PopoverContent,
|
|
35
|
+
PopoverTrigger,
|
|
36
|
+
} from '@asteby/metacore-ui/primitives'
|
|
37
|
+
import { Check, ChevronsUpDown, Loader2 } from 'lucide-react'
|
|
38
|
+
import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
|
|
39
|
+
import type { ActionFieldDef } from './types'
|
|
40
|
+
|
|
41
|
+
function useDebounced<T>(value: T, ms: number): T {
|
|
42
|
+
const [debounced, setDebounced] = useState(value)
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const t = setTimeout(() => setDebounced(value), ms)
|
|
45
|
+
return () => clearTimeout(t)
|
|
46
|
+
}, [value, ms])
|
|
47
|
+
return debounced
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface DynamicSelectFieldProps {
|
|
51
|
+
field: ActionFieldDef
|
|
52
|
+
value: any
|
|
53
|
+
onChange: (v: any) => void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function DynamicSelectField({ field, value, onChange }: DynamicSelectFieldProps) {
|
|
57
|
+
const [open, setOpen] = useState(false)
|
|
58
|
+
const [search, setSearch] = useState('')
|
|
59
|
+
const debounced = useDebounced(search, 250)
|
|
60
|
+
// Remember the label of the option the user actually picked so the trigger
|
|
61
|
+
// shows a name (not a UUID) without a round-trip.
|
|
62
|
+
const [picked, setPicked] = useState<ResolvedOption | null>(null)
|
|
63
|
+
|
|
64
|
+
const { options, loading } = useOptionsResolver({
|
|
65
|
+
modelKey: '',
|
|
66
|
+
fieldKey: 'id',
|
|
67
|
+
ref: field.ref,
|
|
68
|
+
// searchEndpoint only drives the URL when there's no ref — ref is the
|
|
69
|
+
// canonical, kernel-derived path and wins.
|
|
70
|
+
endpoint: field.ref ? undefined : field.searchEndpoint,
|
|
71
|
+
query: debounced,
|
|
72
|
+
limit: 20,
|
|
73
|
+
// Don't fetch until the popover opens (and keep fetching as the query
|
|
74
|
+
// changes while open).
|
|
75
|
+
enabled: open,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const selectedLabel =
|
|
79
|
+
(picked && String(picked.id) === String(value) ? picked.label : null) ??
|
|
80
|
+
options.find((o) => String(o.id) === String(value))?.label ??
|
|
81
|
+
(value ? String(value) : '')
|
|
82
|
+
|
|
83
|
+
const handlePick = (opt: ResolvedOption) => {
|
|
84
|
+
setPicked(opt)
|
|
85
|
+
onChange(String(opt.id))
|
|
86
|
+
setOpen(false)
|
|
87
|
+
setSearch('')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
92
|
+
<PopoverTrigger asChild>
|
|
93
|
+
<Button
|
|
94
|
+
type="button"
|
|
95
|
+
variant="outline"
|
|
96
|
+
role="combobox"
|
|
97
|
+
aria-expanded={open}
|
|
98
|
+
id={field.key}
|
|
99
|
+
className="w-full justify-between font-normal"
|
|
100
|
+
data-empty={!value}
|
|
101
|
+
>
|
|
102
|
+
<span className={'truncate ' + (selectedLabel ? '' : 'text-muted-foreground')}>
|
|
103
|
+
{selectedLabel || field.placeholder || 'Buscar…'}
|
|
104
|
+
</span>
|
|
105
|
+
<ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
|
|
106
|
+
</Button>
|
|
107
|
+
</PopoverTrigger>
|
|
108
|
+
<PopoverContent
|
|
109
|
+
className="p-0"
|
|
110
|
+
align="start"
|
|
111
|
+
// Match the trigger width without an arbitrary Tailwind class
|
|
112
|
+
// (those don't always survive a consuming app's Tailwind scan).
|
|
113
|
+
style={{ width: 'var(--radix-popover-trigger-width)' }}
|
|
114
|
+
>
|
|
115
|
+
<Command shouldFilter={false}>
|
|
116
|
+
<CommandInput
|
|
117
|
+
placeholder={field.placeholder || 'Buscar…'}
|
|
118
|
+
value={search}
|
|
119
|
+
onValueChange={setSearch}
|
|
120
|
+
/>
|
|
121
|
+
<CommandList>
|
|
122
|
+
{loading && (
|
|
123
|
+
<div className="text-muted-foreground flex items-center justify-center gap-2 py-6 text-sm">
|
|
124
|
+
<Loader2 className="size-4 animate-spin" />
|
|
125
|
+
Buscando…
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
{!loading && options.length === 0 && (
|
|
129
|
+
<CommandEmpty>
|
|
130
|
+
{debounced ? 'Sin resultados' : 'Escribí para buscar…'}
|
|
131
|
+
</CommandEmpty>
|
|
132
|
+
)}
|
|
133
|
+
{!loading && options.length > 0 && (
|
|
134
|
+
<CommandGroup className="max-h-64 overflow-auto">
|
|
135
|
+
{options.map((opt) => {
|
|
136
|
+
const isSel = String(opt.id) === String(value)
|
|
137
|
+
return (
|
|
138
|
+
<CommandItem
|
|
139
|
+
key={String(opt.id)}
|
|
140
|
+
value={String(opt.id)}
|
|
141
|
+
onSelect={() => handlePick(opt)}
|
|
142
|
+
>
|
|
143
|
+
<Check className={'mr-2 size-4 ' + (isSel ? 'opacity-100' : 'opacity-0')} />
|
|
144
|
+
<div className="flex min-w-0 flex-col">
|
|
145
|
+
<span className="truncate">{opt.label}</span>
|
|
146
|
+
{opt.description && (
|
|
147
|
+
<span className="text-muted-foreground truncate text-xs">
|
|
148
|
+
{opt.description}
|
|
149
|
+
</span>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
</CommandItem>
|
|
153
|
+
)
|
|
154
|
+
})}
|
|
155
|
+
</CommandGroup>
|
|
156
|
+
)}
|
|
157
|
+
</CommandList>
|
|
158
|
+
</Command>
|
|
159
|
+
</PopoverContent>
|
|
160
|
+
</Popover>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export default DynamicSelectField
|