@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 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,CAU3D"}
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';
@@ -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;AAIvF,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAEvD,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"}
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"}
@@ -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;AAI7C,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"}
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;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "13.2.0",
3
+ "version": "13.3.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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'
@@ -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
package/src/types.ts CHANGED
@@ -116,6 +116,7 @@ export type FieldWidget =
116
116
  | 'number'
117
117
  | 'date'
118
118
  | 'select'
119
+ | 'dynamic_select'
119
120
  | 'switch'
120
121
 
121
122
  export interface ActionFieldDef {