@asteby/metacore-runtime-react 49.5.0 → 49.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/business/customer-picker.d.ts +8 -2
  3. package/dist/business/customer-picker.d.ts.map +1 -1
  4. package/dist/business/customer-picker.js +30 -27
  5. package/dist/business/index.d.ts +1 -0
  6. package/dist/business/index.d.ts.map +1 -1
  7. package/dist/business/index.js +1 -0
  8. package/dist/business/line-product-cell.d.ts +8 -10
  9. package/dist/business/line-product-cell.d.ts.map +1 -1
  10. package/dist/business/line-product-cell.js +13 -90
  11. package/dist/business/product-options.d.ts +41 -0
  12. package/dist/business/product-options.d.ts.map +1 -0
  13. package/dist/business/product-options.js +58 -0
  14. package/dist/business/product-picker.d.ts +5 -1
  15. package/dist/business/product-picker.d.ts.map +1 -1
  16. package/dist/business/product-picker.js +26 -35
  17. package/dist/business/relate-documents.d.ts +1 -1
  18. package/dist/business/relate-documents.d.ts.map +1 -1
  19. package/dist/business/relate-documents.js +6 -8
  20. package/dist/business/vehicle-picker.d.ts +8 -2
  21. package/dist/business/vehicle-picker.d.ts.map +1 -1
  22. package/dist/business/vehicle-picker.js +28 -28
  23. package/dist/dialogs/dynamic-record.d.ts.map +1 -1
  24. package/dist/dialogs/dynamic-record.js +28 -43
  25. package/dist/dynamic-multi-select-field.d.ts.map +1 -1
  26. package/dist/dynamic-multi-select-field.js +36 -6
  27. package/dist/dynamic-select-field.d.ts +9 -23
  28. package/dist/dynamic-select-field.d.ts.map +1 -1
  29. package/dist/dynamic-select-field.js +105 -172
  30. package/dist/entity-select.d.ts +5 -2
  31. package/dist/entity-select.d.ts.map +1 -1
  32. package/dist/entity-select.js +23 -116
  33. package/dist/index.d.ts +9 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +10 -1
  36. package/dist/query-batch.d.ts.map +1 -1
  37. package/dist/query-batch.js +4 -0
  38. package/dist/record-picker-actions.d.ts +40 -2
  39. package/dist/record-picker-actions.d.ts.map +1 -1
  40. package/dist/record-picker-actions.js +44 -4
  41. package/dist/record-picker-dialog.d.ts +23 -0
  42. package/dist/record-picker-dialog.d.ts.map +1 -0
  43. package/dist/record-picker-dialog.js +37 -0
  44. package/dist/record-picker-option.d.ts +25 -0
  45. package/dist/record-picker-option.d.ts.map +1 -0
  46. package/dist/record-picker-option.js +55 -0
  47. package/dist/record-picker.d.ts +149 -0
  48. package/dist/record-picker.d.ts.map +1 -0
  49. package/dist/record-picker.js +372 -0
  50. package/dist/use-option-ids.d.ts +34 -0
  51. package/dist/use-option-ids.d.ts.map +1 -0
  52. package/dist/use-option-ids.js +138 -0
  53. package/dist/use-options-resolver.d.ts +4 -0
  54. package/dist/use-options-resolver.d.ts.map +1 -1
  55. package/dist/use-options-resolver.js +10 -1
  56. package/package.json +1 -1
  57. package/src/__tests__/business-pickers.test.tsx +6 -3
  58. package/src/__tests__/document-editor-inline-selects.test.tsx +2 -1
  59. package/src/__tests__/document-form-dialog.test.tsx +4 -3
  60. package/src/__tests__/document-lines-grid.test.tsx +2 -1
  61. package/src/__tests__/edit-field-polish.test.tsx +6 -3
  62. package/src/__tests__/option-ids.test.tsx +181 -0
  63. package/src/__tests__/record-picker.test.tsx +469 -0
  64. package/src/business/customer-picker.tsx +87 -98
  65. package/src/business/index.ts +1 -0
  66. package/src/business/line-product-cell.tsx +55 -180
  67. package/src/business/product-options.tsx +119 -0
  68. package/src/business/product-picker.tsx +63 -126
  69. package/src/business/relate-documents.tsx +36 -45
  70. package/src/business/vehicle-picker.tsx +76 -99
  71. package/src/dialogs/dynamic-record.tsx +44 -97
  72. package/src/dynamic-multi-select-field.tsx +53 -15
  73. package/src/dynamic-select-field.tsx +188 -379
  74. package/src/entity-select.tsx +53 -202
  75. package/src/index.ts +35 -2
  76. package/src/query-batch.ts +3 -0
  77. package/src/record-picker-actions.tsx +101 -8
  78. package/src/record-picker-dialog.tsx +80 -0
  79. package/src/record-picker-option.tsx +96 -0
  80. package/src/record-picker.tsx +971 -0
  81. package/src/use-option-ids.ts +168 -0
  82. package/src/use-options-resolver.ts +11 -1
  83. package/dist/business/use-async-search.d.ts +0 -10
  84. package/dist/business/use-async-search.d.ts.map +0 -1
  85. package/dist/business/use-async-search.js +0 -38
  86. package/src/business/use-async-search.ts +0 -43
@@ -1,5 +1,6 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // DynamicSelectField — async, searchable single-select for declarative forms.
3
+ // A configuration of the shared <RecordPicker> (record-picker.tsx).
3
4
  //
4
5
  // This is the declarative answer to "I don't want to type a raw FK UUID".
5
6
  // Instead of a plain <select> that dumps every option (RefSelect) or a free
@@ -18,135 +19,86 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
19
  // 1. field.ref → /options/<ref>?field=id (canonical, preferred)
19
20
  // 2. field.searchEndpoint→ used verbatim as the options endpoint (escape hatch)
20
21
  //
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.
22
+ // Edit mode: an EXISTING value is labelled from (in order) the option the user
23
+ // just picked, the loaded page, the caller's `seedOption`, and otherwise one
24
+ // `?ids=` lookup (useResolveOptionIds — batched and shared with every picker of
25
+ // the same ref on screen). While it resolves the trigger reads "Cargando…",
26
+ // never the raw id; an id that no longer exists reads "(registro eliminado)".
25
27
  import { useEffect, useRef, useState } from 'react';
26
28
  import { getOptionFilter } from './option-filter';
27
29
  import { useTranslation } from 'react-i18next';
28
- import { Badge, Button, Command, CommandGroup, CommandInput, CommandItem, CommandList, Popover, PopoverContent, PopoverTrigger, InitialsAvatar, } from '@asteby/metacore-ui/primitives';
29
- import { Check, ChevronsUpDown, Loader2, ScanLine } from 'lucide-react';
30
- import { resolveColorCss } from '@asteby/metacore-ui/lib';
30
+ import { Badge, Button } from '@asteby/metacore-ui/primitives';
31
+ import { ScanLine } from 'lucide-react';
31
32
  import { BarcodeScanner } from './barcode-scanner';
32
- import { JOINED_TRIGGER_CLASS, PickerCreateItem, RecordPickerAction, hasRecordPickerAction } from './record-picker-actions';
33
- import { DynamicIcon, isLucideIconName } from './dynamic-icon';
34
- import { useDebouncedValue } from './use-debounced-value';
33
+ import { RecordPicker } from './record-picker';
34
+ import { OptionLead } from './record-picker-option';
35
+ import { recordLabel, requestRecordCreate, requestRecordEdit, withSearchPrefill } from './record-picker-actions';
35
36
  import { useOptionsResolver } from './use-options-resolver';
37
+ import { useResolveOptionIds } from './use-option-ids';
38
+ import { useDebouncedValue } from './use-debounced-value';
36
39
  import { getDependsOn, getFieldRef, resolveOptionsSource } from './dynamic-form-schema';
40
+ export { OptionLead, OptionThumb } from './record-picker-option';
37
41
  /**
38
42
  * Default hint shown when a cascading picker's depended-on field is still
39
43
  * empty. Domain-neutral on purpose; a caller may override it per field via
40
44
  * `dependsHint`.
41
45
  */
42
46
  export const DEFAULT_DEPENDS_HINT = 'Selecciona primero el campo del que depende';
47
+ /** Label of a held value whose record no longer exists (or is not visible). */
48
+ export const DELETED_RECORD_LABEL = '(registro eliminado)';
43
49
  /**
44
- * Small square thumbnail for an option's `image`. When the option has no image
45
- * it falls back to a deterministic initials avatar (shared `InitialsAvatar`)
46
- * derived from `name`, so an imageless reference reads as a colored badge rather
47
- * than an empty placeholder — and rows/triggers stay aligned. `size` is in
48
- * pixels (kept small — 20–24px — so the picker reads as a list, not a gallery).
49
- * Inline style for the box dimensions: arbitrary Tailwind classes from a
50
- * federated addon don't always survive the host's class scan.
51
- */
52
- export function OptionThumb({ image, name, size = 20, }) {
53
- const [broken, setBroken] = useState(false);
54
- const box = { width: size, height: size };
55
- // Missing or 404 image (bare avatar filenames often 404) → initials, never
56
- // an invisible box that looks like "no avatar".
57
- if (!image || broken) {
58
- return _jsx(InitialsAvatar, { name: name, size: size, rounded: "sm", tone: "neutral" });
59
- }
60
- // A lucide icon name stored where an image url/path is expected (the `icon`
61
- // form widget's icon mode) → render the glyph instead of a broken <img>.
62
- if (isLucideIconName(image)) {
63
- return (_jsx("span", { className: "flex shrink-0 items-center justify-center rounded-sm bg-muted", style: box, "aria-hidden": true, children: _jsx(DynamicIcon, { name: image, className: "size-4" }) }));
64
- }
65
- return (_jsx("img", { src: image, alt: "", "aria-hidden": true, loading: "lazy", className: "shrink-0 rounded-sm object-cover", style: box, onError: () => setBroken(true) }));
66
- }
67
- /**
68
- * Leading visual for an option: a photo thumbnail (FK relations with an image),
69
- * else a declared icon, else a color dot (enum/status options with a color).
70
- * Returns null when the option carries none, so plain text options stay plain.
50
+ * Declarative `dynamic_select` (manifest `ref` / `optionsConfig.source`) on top
51
+ * of the shared {@link RecordPicker}: options from the canonical options
52
+ * endpoint (`useOptionsResolver`: option_filter, options[].when keep-value,
53
+ * cascade `filter_value`), inline create/edit through the host modal events.
71
54
  */
72
- export function OptionLead({ option, size = 20, }) {
73
- if (!option)
74
- return null;
75
- if (option.image)
76
- return _jsx(OptionThumb, { image: option.image, name: option.label, size: size });
77
- if (option.icon) {
78
- return (_jsx("span", { className: "flex shrink-0 items-center justify-center", style: { width: size, height: size, color: option.color ? resolveColorCss(option.color) : undefined }, "aria-hidden": true, children: _jsx(DynamicIcon, { name: option.icon, className: "size-4" }) }));
79
- }
80
- if (option.color) {
81
- return (_jsx("span", { className: "shrink-0 rounded-full", style: { width: Math.round(size * 0.5), height: Math.round(size * 0.5), background: resolveColorCss(option.color) }, "aria-hidden": true }));
82
- }
83
- // No image/icon/color: an imageless reference option. Show its initials
84
- // (shared InitialsAvatar) so it stays visually aligned with the sibling
85
- // options that DO carry a thumbnail, rather than a blank gap.
86
- if (option.label)
87
- return _jsx(InitialsAvatar, { name: option.label, size: size, rounded: "sm", tone: "neutral" });
88
- return null;
89
- }
90
- function useDebounced(value, ms) {
91
- return useDebouncedValue(value, ms);
92
- }
93
55
  export function DynamicSelectField({ field, value, onChange, onPick, seedOption, dependsValue, dependsHint, readonly = false, staticOptions = null, descriptionAsBadge = false, hideCreate = false, createDefaults, createLockedFields, invalid = false, }) {
94
56
  const { t } = useTranslation();
95
57
  const ph = (fallback) => field.placeholder ? t(field.placeholder, { defaultValue: field.placeholder }) : fallback;
96
58
  const [open, setOpen] = useState(false);
97
59
  const [search, setSearch] = useState('');
98
60
  const [scanOpen, setScanOpen] = useState(false);
99
- const debounced = useDebounced(search, 250);
61
+ const debounced = useDebouncedValue(search, 250);
100
62
  // Escaneo por cámara para "llenar rápido": opt-in por campo (`scan`). El
101
- // botón aparece SIEMPRE que el campo lo declara (igual que el POS), sin
102
- // esconderse por `isCameraScanSupported()` — en escritorio el botón no salía
103
- // aunque el campo lo pidiera. El BarcodeScanner ya degrada con un mensaje
104
- // cuando no hay cámara. Un código escaneado alimenta la búsqueda y abre el
105
- // picker para elegir la referencia sin tipear el UUID.
63
+ // botón aparece SIEMPRE que el campo lo declara (igual que el POS). El
64
+ // BarcodeScanner ya degrada con un mensaje cuando no hay cámara. Un código
65
+ // escaneado alimenta la búsqueda y abre el picker.
106
66
  const scanEnabled = !!(field.scan ?? field.scannable);
107
67
  const handleScanDetected = (code) => {
108
68
  setSearch(code);
109
69
  setOpen(true);
110
70
  };
111
- // Remember the label of the option the user actually picked so the trigger
112
- // shows a name (not a UUID) without a round-trip.
71
+ // Remember the option the user actually picked so the trigger shows a name
72
+ // (not a UUID) without a round-trip.
113
73
  const [picked, setPicked] = useState(null);
114
- // Tolerate the snake_case `source`/`relation` aliases the kernel may serve
115
- // for the FK target, not just camelCase `ref`.
74
+ // Tolerate the snake_case `source`/`relation` aliases for the FK target.
116
75
  const fieldRef = getFieldRef(field);
117
- // Options routing: an `optionsConfig.source` (dependent/scoped picker) wins
118
- // over the field's `ref` — query the SOURCE model with `field=<value>`;
119
- // otherwise keep the canonical `ref`-based resolution.
76
+ // An `optionsConfig.source` (dependent/scoped picker) wins over `ref`.
120
77
  const source = resolveOptionsSource(field);
121
78
  // Cascade: a `dependsOn` field whose value is still empty leaves this
122
- // picker disabled until the parent is set. `dependsValue` is the resolved
123
- // value the caller threaded from the form context.
79
+ // picker disabled until the parent is set.
124
80
  const dependsOn = getDependsOn(field);
125
81
  const scope = dependsValue ? String(dependsValue) : '';
126
82
  const blockedByDependency = !!dependsOn && scope === '';
127
83
  const useStatic = Array.isArray(staticOptions);
128
84
  const optionFilter = getOptionFilter(field);
85
+ // optionsConfig.source → `/options/<source>`. Else searchEndpoint only
86
+ // drives the URL when there's no ref (ref is canonical and wins).
87
+ const optionsEndpoint = source.endpoint ?? (source.ref ? undefined : field.searchEndpoint);
129
88
  const { options: fetchedOptions, loading: fetchLoading } = useOptionsResolver({
130
89
  modelKey: '',
131
90
  fieldKey: source.fieldKey,
132
91
  ref: source.ref,
133
- // optionsConfig.source → `/options/<source>`. Else searchEndpoint only
134
- // drives the URL when there's no ref — ref is the canonical,
135
- // kernel-derived path and wins.
136
- endpoint: source.endpoint ?? (source.ref ? undefined : field.searchEndpoint),
92
+ endpoint: optionsEndpoint,
137
93
  query: debounced,
138
94
  limit: 20,
139
- // Cascade scope forwarded as filter_value (only when this field
140
- // declares a dependency). Re-fetches when the parent value changes.
141
95
  filterValue: dependsOn ? scope : undefined,
142
96
  optionFilter,
143
97
  keepValue: value,
144
- // Don't fetch until the popover opens (and keep fetching as the query
145
- // changes while open). A picker blocked by an unset dependency never
146
- // fetches. A readonly cell fetches eagerly so its value's label resolves
147
- // to the name without the user ever opening it.
148
- // Static lists never hit the network.
149
- enabled: !useStatic && (open || readonly) && !blockedByDependency,
98
+ // Fetch only while open (a held value's label comes from the `?ids=`
99
+ // lookup below, readonly included). Blocked cascades and static lists
100
+ // never fetch.
101
+ enabled: !useStatic && open && !blockedByDependency,
150
102
  });
151
103
  const options = useStatic
152
104
  ? staticOptions.filter((o) => {
@@ -158,8 +110,7 @@ export function DynamicSelectField({ field, value, onChange, onPick, seedOption,
158
110
  })
159
111
  : fetchedOptions;
160
112
  const loading = useStatic ? false : fetchLoading;
161
- // When the depended-on value changes, the previously-picked option no longer
162
- // belongs to the new scope, so clear the selection (skip the initial mount).
113
+ // A new cascade scope invalidates the selection (skip the initial mount).
163
114
  const prevScopeRef = useRef(scope);
164
115
  useEffect(() => {
165
116
  if (!dependsOn)
@@ -171,17 +122,27 @@ export function DynamicSelectField({ field, value, onChange, onPick, seedOption,
171
122
  onChange('');
172
123
  }
173
124
  }, [dependsOn, scope, value, onChange]);
174
- // The currently-selected option, resolved either from what the user picked
175
- // (cached in `picked`) or from the loaded page. Drives both the trigger
176
- // label and its thumbnail.
177
- const selectedOption = (picked && String(picked.id) === String(value) ? picked : null) ??
178
- options.find((o) => String(o.id) === String(value)) ??
179
- (seedOption && String(seedOption.id) === String(value) ? seedOption : null) ??
125
+ const valueKey = value != null && value !== '' ? String(value) : '';
126
+ const knownOption = (picked && String(picked.id) === valueKey ? picked : null) ??
127
+ options.find((o) => String(o.id) === valueKey) ??
128
+ (seedOption && String(seedOption.id) === valueKey ? seedOption : null) ??
180
129
  null;
181
- const selectedLabel = selectedOption?.label ?? (value ? String(value) : '');
182
- // Only switch the picker into "with thumbnails" mode when the data actually
183
- // carries images — a relation whose options have no `image` keeps the plain
184
- // text list it had before (no empty placeholder column).
130
+ // A held value nobody has labelled yet: one `?ids=` lookup (no popover).
131
+ const needsLookup = !!valueKey && !knownOption && !useStatic;
132
+ const { resolved, loading: idsLoading } = useResolveOptionIds({
133
+ ref: source.ref,
134
+ endpoint: optionsEndpoint,
135
+ field: source.fieldKey,
136
+ ids: needsLookup ? [valueKey] : [],
137
+ enabled: needsLookup,
138
+ });
139
+ const resolution = needsLookup ? resolved.get(valueKey) : undefined;
140
+ const selectedOption = knownOption ?? (resolution?.status === 'found' ? resolution.option : null);
141
+ const valueResolving = needsLookup && !selectedOption && idsLoading;
142
+ const valueMissing = needsLookup && !selectedOption && resolution?.status === 'missing';
143
+ const selectedLabel = selectedOption?.label ?? (valueResolving ? '' : valueMissing ? DELETED_RECORD_LABEL : valueKey);
144
+ const resolvingNode = (_jsx("span", { className: "min-w-0 flex-1 truncate animate-pulse text-muted-foreground", "data-slot": "record-picker-value-loading", children: "Cargando\u2026" }));
145
+ const missingNode = (_jsx("span", { className: "min-w-0 flex-1 truncate italic text-muted-foreground", "data-slot": "record-picker-value-missing", children: DELETED_RECORD_LABEL }));
185
146
  const handlePick = (opt) => {
186
147
  setPicked(opt);
187
148
  onChange(String(opt.id));
@@ -189,88 +150,60 @@ export function DynamicSelectField({ field, value, onChange, onPick, seedOption,
189
150
  setOpen(false);
190
151
  setSearch('');
191
152
  };
192
- // Inline-create: the "+" opens the REFERENCED model's own create modal (the
193
- // real one the host renders for that model — full fields, not a duplicate),
194
- // via a decoupled window event the host listens for. On success the host
195
- // hands back the new record and we select it immediately. No host import →
196
- // no circular dependency; works for ANY dynamic_select with a `ref`.
197
- // Inline-edit: with a value selected the joined pencil opens THAT record's
198
- // edit modal through the sibling `metacore:edit-record` event (the host's
199
- // RecordCreateBridge already listens for it). On save the trigger label
200
- // refreshes from the returned record.
201
- const openEdit = () => {
202
- if (!fieldRef || !value || typeof window === 'undefined')
153
+ const toOption = (id, label) => ({ id, value: id, label, name: label });
154
+ // Inline create / edit: the REFERENCED model's real modal, rendered by the
155
+ // host (RecordCreateBridge) via decoupled window events — no host import.
156
+ const openCreate = (query) => {
157
+ if (!fieldRef)
203
158
  return;
204
159
  setOpen(false);
205
- window.dispatchEvent(new CustomEvent('metacore:edit-record', {
206
- detail: {
207
- model: fieldRef,
208
- recordId: String(value),
209
- onSaved: (rec) => {
210
- const id = String(value);
211
- const label = rec ? rec.name ?? rec.label ?? rec.title : undefined;
212
- if (label != null)
213
- setPicked({ ...(selectedOption ?? { id, value: id }), id, value: id, label: String(label), name: String(label) });
214
- },
160
+ requestRecordCreate({
161
+ model: fieldRef,
162
+ // The searched text seeds the new record's name; explicit
163
+ // createDefaults win. Keys that aren't fields are ignored.
164
+ defaults: withSearchPrefill(query, 'name', createDefaults),
165
+ lockedFields: createLockedFields,
166
+ onCreated: (rec) => {
167
+ if (rec && rec.id != null) {
168
+ const id = String(rec.id);
169
+ handlePick(toOption(id, recordLabel(rec) ?? id));
170
+ }
215
171
  },
216
- }));
172
+ });
217
173
  };
218
- const openCreate = () => {
219
- if (!fieldRef || typeof window === 'undefined')
174
+ const openEdit = () => {
175
+ if (!fieldRef || !value)
220
176
  return;
221
177
  setOpen(false);
222
- window.dispatchEvent(new CustomEvent('metacore:create-record', {
223
- detail: {
224
- model: fieldRef,
225
- // Generic passthrough: the host's create modal seeds these
226
- // values and locks these fields (DynamicRecordDialog's own
227
- // defaults/lockedFields props). Undefined when unused.
228
- defaults: createDefaults,
229
- lockedFields: createLockedFields,
230
- onCreated: (rec) => {
231
- if (rec && rec.id != null) {
232
- const id = String(rec.id);
233
- const label = String(rec.name ?? rec.label ?? rec.title ?? rec.id);
234
- handlePick({ id, value: id, label, name: label });
235
- }
236
- },
178
+ const id = String(value);
179
+ requestRecordEdit({
180
+ model: fieldRef,
181
+ recordId: id,
182
+ onSaved: (rec) => {
183
+ const label = recordLabel(rec);
184
+ if (label != null)
185
+ setPicked({ ...(selectedOption ?? toOption(id, label)), id, value: id, label, name: label });
237
186
  },
238
- }));
187
+ });
239
188
  };
240
- // Locked display: the value is fixed by context, so render the resolved
241
- // label (name) as a disabled control — no popover, no inline-create. While
242
- // the eager fetch is in flight the label falls back to the seed/raw value,
243
- // then snaps to the name once options arrive.
244
- if (readonly) {
245
- return (_jsx(Button, { type: "button", variant: "outline", role: "combobox", id: field.key, disabled: true, "aria-readonly": "true", className: "w-full min-w-0 cursor-default justify-start font-normal opacity-100", children: _jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-2 text-left", children: [selectedOption ? _jsx(OptionLead, { option: selectedOption, size: 20 }) : null, _jsx("span", { className: 'min-w-0 flex-1 truncate ' + (selectedOption ? '' : 'text-muted-foreground'), children: selectedOption?.label ?? (loading ? 'Cargando…' : ph('—')) })] }) }));
246
- }
247
- // w-full + min-w-0: as a grid cell child, the row must be allowed to shrink
248
- // to the cell. Without min-w-0 the combobox+button row sizes to its content
249
- // (the long empty-state placeholder) and overflows the column, pushing the
250
- // "+" off-screen — it only "fit" once a short value was selected.
251
- const canMutate = !!fieldRef && !hideCreate && !useStatic;
252
- const joined = canMutate && !blockedByDependency && hasRecordPickerAction(!!value, openCreate, openEdit);
189
+ const canMutate = !!fieldRef && !hideCreate && !useStatic && !blockedByDependency;
253
190
  const fieldName = field.label ? t(field.label, { defaultValue: field.label }) : fieldRef ?? '';
254
- return (_jsxs("div", { className: "flex w-full min-w-0 items-center gap-1.5", "data-slot": "dynamic-select", children: [_jsxs("div", { className: "flex min-w-0 flex-1 items-center", children: [_jsxs(Popover, { open: open && !blockedByDependency, onOpenChange: (o) => { if (!blockedByDependency)
255
- setOpen(o); }, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", role: "combobox", "aria-expanded": open, id: field.key, disabled: blockedByDependency, "aria-invalid": invalid || undefined, className: 'min-w-0 flex-1 justify-between font-normal' +
256
- (joined ? ` ${JOINED_TRIGGER_CLASS}` : '') +
257
- (invalid ? ' border-destructive ring-1 ring-destructive/30' : ''), "data-empty": !value, "data-depends-blocked": blockedByDependency ? '' : undefined, children: [_jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-2 text-left", children: [value && selectedOption ? (_jsx(OptionLead, { option: selectedOption, size: 20 })) : null, _jsx("span", { className: 'min-w-0 flex-1 truncate ' + (selectedLabel ? '' : 'text-muted-foreground'), children: blockedByDependency
258
- ? (dependsHint || DEFAULT_DEPENDS_HINT)
259
- : selectedLabel || ph('Buscar…') }), descriptionAsBadge && selectedOption?.description ? (_jsx(Badge, { variant: "secondary", className: "shrink-0 font-normal tabular-nums", children: selectedOption.description })) : null] }), _jsx(ChevronsUpDown, { className: "ml-2 size-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "p-0", align: "start",
260
- // Portaled (never clipped by a table/modal overflow) and
261
- // collision-aware: flips above the trigger when there is no
262
- // room below. Match the trigger width without an arbitrary
263
- // Tailwind class (those don't always survive a consuming app's
264
- // Tailwind scan), with a floor so short cells stay readable.
265
- collisionPadding: 8, style: { width: 'max(var(--radix-popover-trigger-width), 14rem)', maxWidth: 'calc(100vw - 1rem)' }, children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { placeholder: ph('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("div", { className: "text-muted-foreground py-6 text-center text-sm", "data-slot": "picker-empty", children: useStatic
266
- ? debounced
267
- ? 'Sin resultados'
268
- : 'Sin opciones'
269
- : debounced
270
- ? 'Sin resultados'
271
- : 'Sin resultados — escribe para filtrar' })), !loading && options.length > 0 && (_jsx(CommandGroup, { className: "max-h-64 overflow-auto", children: options.map((opt) => {
272
- const isSel = String(opt.id) === String(value);
273
- return (_jsxs(CommandItem, { value: String(opt.id), onSelect: () => handlePick(opt), children: [_jsx(Check, { className: 'mr-2 size-4 shrink-0 ' + (isSel ? 'opacity-100' : 'opacity-0') }), _jsx(OptionLead, { option: opt, size: 24 }), descriptionAsBadge ? (_jsxs("div", { className: "ml-2 flex min-w-0 flex-1 items-center gap-2", children: [_jsx("span", { className: "min-w-0 flex-1 truncate", children: opt.label }), opt.description ? (_jsx(Badge, { variant: "secondary", className: "shrink-0 font-normal tabular-nums", children: opt.description })) : null] })) : (_jsxs("div", { className: "ml-2 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)));
274
- }) })), canMutate && !loading && (_jsx(PickerCreateItem, { label: fieldName, onSelect: openCreate }))] })] }) })] }), canMutate && !blockedByDependency && (_jsx(RecordPickerAction, { hasValue: !!value, label: fieldName, onCreate: openCreate, onEdit: openEdit }))] }), scanEnabled && (_jsx(Button, { type: "button", variant: "outline", size: "icon", className: "size-9 shrink-0", onClick: () => setScanOpen(true), title: "Escanear con la c\u00E1mara", "aria-label": "Escanear c\u00F3digo de barras con la c\u00E1mara", children: _jsx(ScanLine, { className: "size-4" }) })), scanEnabled && (_jsx(BarcodeScanner, { open: scanOpen, onClose: () => setScanOpen(false), onDetected: handleScanDetected, continuous: false, position: "fixed", title: `Escanear ${field.label ?? ''}`.trim() }))] }));
191
+ const badge = (opt) => opt.description ? (_jsx(Badge, { variant: "secondary", className: "shrink-0 font-normal tabular-nums", children: opt.description })) : null;
192
+ return (_jsx(RecordPicker, { slot: "dynamic-select", id: field.key, items: options, loading: loading, getKey: (o) => String(o.id), getLabel: (o) => o.label, value: value, selected: selectedOption, onSelect: handlePick, query: search, onQueryChange: setSearch, open: open && !blockedByDependency, onOpenChange: (o) => {
193
+ if (!blockedByDependency)
194
+ setOpen(o);
195
+ }, disabled: blockedByDependency, readOnly: readonly, invalid: invalid, renderLead: (o, where) => _jsx(OptionLead, { option: o, size: where === 'option' ? 24 : 20 }), getDescription: descriptionAsBadge ? undefined : (o) => o.description, renderTrailing: descriptionAsBadge ? (o) => badge(o) : undefined, renderValue: () => {
196
+ // Never flash the raw id while the label resolves.
197
+ if (valueResolving)
198
+ return resolvingNode;
199
+ if (valueMissing)
200
+ return missingNode;
201
+ if (readonly) {
202
+ return (_jsxs(_Fragment, { children: [selectedOption ? _jsx(OptionLead, { option: selectedOption, size: 20 }) : null, _jsx("span", { className: 'min-w-0 flex-1 truncate ' + (selectedOption ? '' : 'text-muted-foreground'), children: selectedOption?.label ?? ph('—') })] }));
203
+ }
204
+ return (_jsxs(_Fragment, { children: [value && selectedOption ? _jsx(OptionLead, { option: selectedOption, size: 20 }) : null, _jsx("span", { className: 'min-w-0 flex-1 truncate ' + (selectedLabel ? '' : 'text-muted-foreground'), children: blockedByDependency ? dependsHint || DEFAULT_DEPENDS_HINT : selectedLabel || ph('Buscar…') }), descriptionAsBadge && selectedOption ? badge(selectedOption) : null] }));
205
+ }, placeholder: ph('Buscar…'), searchPlaceholder: ph('Buscar…'), emptyText: debounced ? 'Sin resultados' : useStatic ? 'Sin opciones' : 'Sin resultados — escribe para filtrar', entityLabel: fieldName, onCreate: canMutate ? openCreate : undefined, onEdit: canMutate ? openEdit : undefined, triggerProps: {
206
+ 'data-depends-blocked': blockedByDependency ? '' : undefined,
207
+ }, after: scanEnabled ? (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", variant: "outline", size: "icon", className: "size-9 shrink-0", onClick: () => setScanOpen(true), title: "Escanear con la c\u00E1mara", "aria-label": "Escanear c\u00F3digo de barras con la c\u00E1mara", children: _jsx(ScanLine, { className: "size-4" }) }), _jsx(BarcodeScanner, { open: scanOpen, onClose: () => setScanOpen(false), onDetected: handleScanDetected, continuous: false, position: "fixed", title: `Escanear ${field.label ?? ''}`.trim() })] })) : null }));
275
208
  }
276
209
  export default DynamicSelectField;
@@ -1,4 +1,4 @@
1
- import { type LucideIcon } from 'lucide-react';
1
+ import type { LucideIcon } from 'lucide-react';
2
2
  /** One searchable option: `value` is the id, `label` the display text. */
3
3
  export interface EntitySelectOption {
4
4
  value: string;
@@ -52,5 +52,8 @@ export interface EntitySelectProps {
52
52
  */
53
53
  lockedCreateFields?: string[];
54
54
  }
55
- export declare function EntitySelect({ model, value, label, onSelect, fetcher, icon: Icon, placeholder, searchPlaceholder, emptyText, preload, canCreate, canEdit, endpoint, labelField, disabled, createDefaults, lockedCreateFields, }: EntitySelectProps): import("react").JSX.Element;
55
+ /**
56
+ * @deprecated Thin wrapper over {@link RecordPicker}; prefer it for new code.
57
+ */
58
+ export declare function EntitySelect({ model, value, label, onSelect, fetcher, icon, placeholder, searchPlaceholder, emptyText, preload, canCreate, canEdit, endpoint, labelField, disabled, createDefaults, lockedCreateFields, }: EntitySelectProps): import("react").JSX.Element;
56
59
  //# sourceMappingURL=entity-select.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"entity-select.d.ts","sourceRoot":"","sources":["../src/entity-select.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAAa,KAAK,UAAU,EAAE,MAAM,cAAc,CAAA;AAiBzD,0EAA0E;AAC1E,MAAM,WAAW,kBAAkB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,iBAAiB;IAC9B,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,kEAAkE;IAClE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,sDAAsD;IACtD,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC3D,sFAAsF;IACtF,OAAO,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAA;IAE1E,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAElB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;CAChC;AAOD,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,KAAK,EACL,KAAK,EACL,QAAQ,EACR,OAAO,EACP,IAAI,EAAE,IAAI,EACV,WAA4B,EAC5B,iBAA6B,EAC7B,SAA4B,EAC5B,OAAe,EACf,SAAS,EACT,OAAO,EACP,QAAQ,EACR,UAAmB,EACnB,QAAgB,EAChB,cAAc,EACd,kBAAkB,GACrB,EAAE,iBAAiB,+BAmMnB"}
1
+ {"version":3,"file":"entity-select.d.ts","sourceRoot":"","sources":["../src/entity-select.tsx"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAM9C,0EAA0E;AAC1E,MAAM,WAAW,kBAAkB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,iBAAiB;IAC9B,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,kEAAkE;IAClE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,sDAAsD;IACtD,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC3D,sFAAsF;IACtF,OAAO,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAA;IAE1E,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAElB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;CAChC;AAOD;;GAEG;AACH,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,KAAK,EACL,KAAK,EACL,QAAQ,EACR,OAAO,EACP,IAAI,EACJ,WAA4B,EAC5B,iBAA6B,EAC7B,SAA4B,EAC5B,OAAe,EACf,SAAS,EACT,OAAO,EACP,QAAQ,EACR,UAAmB,EACnB,QAAgB,EAChB,cAAc,EACd,kBAAkB,GACrB,EAAE,iBAAiB,+BAmDnB"}
@@ -1,126 +1,33 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // EntitySelect — the shared, permission-aware single-select for a related model.
3
- //
4
- // A searchable async combobox over a kernel model's records, plus the two
5
- // affordances every "pick a related record" control should have, exactly like
6
- // the dynamic create modal's relation fields (Categoría/Marca) do:
7
- //
8
- // - nothing selected → a "+" that opens the model's CREATE dialog, and
9
- // auto-selects the record it creates;
10
- // - a record selected → a pencil that opens that record's EDIT dialog.
11
- //
12
- // Both affordances are gated by the kernel permissions (useCan): the "+" only
13
- // shows when the user can create the model, the pencil only when they can edit
14
- // it. Everything is DYNAMIC — the create/edit form comes from the model's
15
- // `/metadata/modal/:model` schema via <CreateRecordDialog>, so no per-model form
16
- // code is needed. This lives in the SDK so POS, purchases and any future addon
17
- // share ONE implementation instead of each re-porting a bespoke picker.
18
- import { useCallback, useEffect, useState } from 'react';
19
- import { Search, X } from 'lucide-react';
20
- import { Button, Command, CommandGroup, CommandInput, CommandItem, CommandList, Popover, PopoverContent, PopoverTrigger, } from '@asteby/metacore-ui';
21
- import { CreateRecordDialog } from './dialogs/create-record-dialog';
22
- import { JOINED_TRIGGER_CLASS, PickerCreateItem, RecordPickerAction, hasRecordPickerAction } from './record-picker-actions';
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { RecordPicker, useLatestSearch } from './record-picker';
3
+ import { recordLabel } from './record-picker-actions';
4
+ import { useRecordPickerDialog } from './record-picker-dialog';
23
5
  import { useCan } from './permissions-context';
24
- import { useApi } from './api-context';
25
6
  /** Lowercase model → permission capability namespace (Supplier → supplier). */
26
7
  function capabilityNamespace(model) {
27
8
  return model.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
28
9
  }
29
- export function EntitySelect({ model, value, label, onSelect, fetcher, icon: Icon, placeholder = 'Seleccionar…', searchPlaceholder = 'Buscar…', emptyText = 'Sin resultados', preload = false, canCreate, canEdit, endpoint, labelField = 'name', disabled = false, createDefaults, lockedCreateFields, }) {
10
+ /**
11
+ * @deprecated Thin wrapper over {@link RecordPicker}; prefer it for new code.
12
+ */
13
+ export function EntitySelect({ model, value, label, onSelect, fetcher, icon, placeholder = 'Seleccionar…', searchPlaceholder = 'Buscar…', emptyText = 'Sin resultados', preload = false, canCreate, canEdit, endpoint, labelField = 'name', disabled = false, createDefaults, lockedCreateFields, }) {
30
14
  const can = useCan();
31
- const api = useApi();
32
15
  const ns = capabilityNamespace(model);
33
16
  const mayCreate = canCreate ?? can(`${ns}.create`);
34
17
  const mayEdit = canEdit ?? can(`${ns}.update`);
35
- const base = endpoint ?? `/data/${model}/me`;
36
- const [open, setOpen] = useState(false);
37
- const [dialogOpen, setDialogOpen] = useState(false);
38
- const [dialogRecordId, setDialogRecordId] = useState(undefined);
39
- const [searchTerm, setSearchTerm] = useState('');
40
- const [results, setResults] = useState([]);
41
- const [isLoading, setIsLoading] = useState(false);
42
- const minChars = preload ? 0 : 2;
43
- const run = useCallback(async (q, signal) => {
44
- if (q.length < minChars) {
45
- setResults([]);
46
- return;
47
- }
48
- setIsLoading(true);
49
- try {
50
- const rows = await fetcher(q, signal);
51
- if (!signal.aborted)
52
- setResults(rows);
53
- }
54
- catch {
55
- if (!signal.aborted)
56
- setResults([]);
57
- }
58
- finally {
59
- if (!signal.aborted)
60
- setIsLoading(false);
61
- }
62
- }, [fetcher, minChars]);
63
- useEffect(() => {
64
- if (!open)
65
- return;
66
- const controller = new AbortController();
67
- const timeout = setTimeout(() => run(searchTerm, controller.signal), 250);
68
- return () => {
69
- clearTimeout(timeout);
70
- controller.abort();
71
- };
72
- }, [searchTerm, run, open]);
73
- const pick = (row) => {
74
- onSelect(row.value, row.label);
75
- setOpen(false);
76
- setSearchTerm('');
77
- };
78
- const clear = (e) => {
79
- e.stopPropagation();
80
- onSelect(null, null);
81
- };
82
- const openCreate = () => {
83
- setOpen(false);
84
- setDialogRecordId(undefined);
85
- setDialogOpen(true);
86
- };
87
- const openEdit = () => {
88
- if (!value)
89
- return;
90
- setOpen(false);
91
- setDialogRecordId(value);
92
- setDialogOpen(true);
93
- };
94
- // Read the {id,label} off a saved record and select it. The transport
95
- // matches the standard org-scoped CRUD the dynamic modal uses, so create/edit
96
- // stay consistent with the rest of the app.
97
- const selectSaved = (rec) => {
98
- if (!rec)
99
- return;
100
- const id = rec.id != null ? String(rec.id) : value ?? '';
101
- const lbl = (rec[labelField] != null && String(rec[labelField])) ||
102
- (rec.name != null && String(rec.name)) ||
103
- label ||
104
- id;
105
- onSelect(id, lbl);
106
- };
107
- const onCreate = mayCreate ? openCreate : undefined;
108
- const onEdit = mayEdit ? openEdit : undefined;
109
- const joined = hasRecordPickerAction(!!value, onCreate, onEdit);
110
- return (_jsxs("div", { className: "flex items-center", "data-slot": "entity-select", children: [_jsxs(Popover, { open: open, onOpenChange: disabled ? undefined : setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", disabled: disabled, className: 'w-full min-w-0 flex-1 justify-start gap-2 font-normal' + (joined ? ` ${JOINED_TRIGGER_CLASS}` : ''), children: [Icon && _jsx(Icon, { className: "text-muted-foreground size-4 shrink-0" }), _jsx("span", { className: "flex-1 truncate text-left", children: label ?? placeholder }), value && (_jsx("span", { role: "button", tabIndex: 0, onClick: clear, onKeyDown: (e) => {
111
- if (e.key === 'Enter' || e.key === ' ')
112
- clear(e);
113
- }, className: "hover:bg-accent ml-auto shrink-0 rounded-sm p-0.5", children: _jsx(X, { className: "size-3.5" }) }))] }) }), _jsx(PopoverContent, { className: "p-0", align: "start", collisionPadding: 8, style: { width: 'max(var(--radix-popover-trigger-width), 14rem)', maxWidth: 'calc(100vw - 1rem)' }, children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { placeholder: searchPlaceholder, value: searchTerm, onValueChange: setSearchTerm }), _jsxs(CommandList, { children: [isLoading && (_jsx("div", { className: "text-muted-foreground py-4 text-center text-sm", children: "Buscando\u2026" })), !isLoading &&
114
- searchTerm.length >= minChars &&
115
- results.length === 0 && _jsx("div", { className: "text-muted-foreground py-6 text-center text-sm", children: emptyText }), !isLoading && results.length > 0 && (_jsx(CommandGroup, { className: "max-h-64 overflow-auto", children: results.map((row) => (_jsxs(CommandItem, { value: row.value, onSelect: () => pick(row), className: "flex flex-col items-start gap-0.5", children: [_jsx("span", { className: "text-sm font-medium", children: row.label }), row.description && (_jsx("span", { className: "text-muted-foreground text-xs", children: row.description }))] }, row.value))) })), !isLoading && !preload && searchTerm.length < minChars && (_jsxs("div", { className: "text-muted-foreground flex flex-col items-center gap-1 py-6", children: [_jsx(Search, { className: "size-5" }), _jsx("span", { className: "text-xs", children: "Escribe al menos 2 caracteres" })] })), onCreate && !isLoading && _jsx(PickerCreateItem, { label: model, onSelect: openCreate })] })] }) })] }), _jsx(RecordPickerAction, { hasValue: !!value, label: model, onCreate: onCreate, onEdit: onEdit, disabled: disabled }), dialogOpen && (_jsx(CreateRecordDialog, { modelKey: model, open: dialogOpen, onOpenChange: setDialogOpen, recordId: dialogRecordId, endpoint: base, defaults: dialogRecordId ? undefined : createDefaults, lockedFields: dialogRecordId ? undefined : lockedCreateFields, onCreate: async (data) => {
116
- const res = await api.post(base, data);
117
- const rec = (res.data?.data ?? res.data);
118
- selectSaved(rec);
119
- return rec.id != null ? { id: String(rec.id) } : undefined;
120
- }, onUpdate: async (id, data) => {
121
- const res = await api.put(`${base}/${id}`, data);
122
- const rec = (res.data?.data ?? res.data);
123
- selectSaved({ id, ...rec });
124
- return { id: String(id) };
125
- } }))] }));
18
+ // Read the {id,label} off a saved record and select it (create) or
19
+ // refresh its label (edit).
20
+ const { openCreate, openEdit, dialog } = useRecordPickerDialog({
21
+ model,
22
+ endpoint,
23
+ prefillField: labelField,
24
+ createDefaults,
25
+ lockedCreateFields,
26
+ onSaved: (rec) => {
27
+ const id = rec.id != null ? String(rec.id) : value ?? '';
28
+ onSelect(id, recordLabel(rec, labelField) ?? label ?? id);
29
+ },
30
+ });
31
+ const search = useLatestSearch(fetcher);
32
+ return (_jsxs(_Fragment, { children: [_jsx(RecordPicker, { slot: "entity-select", search: search, minChars: preload ? 0 : 2, minCharsText: "Escribe al menos 2 caracteres", getKey: (o) => o.value, getLabel: (o) => o.label, getDescription: (o) => o.description, showCheck: false, value: value, selected: value ? { value, label: label ?? value } : null, onSelect: (o) => onSelect(o.value, o.label), onClear: () => onSelect(null, null), icon: icon, placeholder: placeholder, renderValue: (sel) => (_jsx("span", { className: 'min-w-0 flex-1 truncate' + (sel ? '' : ' text-muted-foreground'), children: label ?? placeholder })), searchPlaceholder: searchPlaceholder, emptyText: emptyText, entityLabel: model, onCreate: mayCreate ? (q) => openCreate(q) : undefined, onEdit: mayEdit && value ? () => openEdit(value) : undefined, disabled: disabled }), dialog] }));
126
33
  }
package/dist/index.d.ts CHANGED
@@ -85,8 +85,13 @@ export type { DynamicRecordDialogProps, FieldDef, FieldOption, GetImageUrl } fro
85
85
  export { CreateRecordDialog } from './dialogs/create-record-dialog';
86
86
  export { EntitySelect } from './entity-select';
87
87
  export type { EntitySelectProps, EntitySelectOption } from './entity-select';
88
- export { RecordPickerAction, PickerCreateItem, JOINED_TRIGGER_CLASS, hasRecordPickerAction } from './record-picker-actions';
89
- export type { RecordPickerActionProps } from './record-picker-actions';
88
+ export { RecordPicker, useRecordSearch, useLatestSearch, RECORD_PICKER_Z_INDEX } from './record-picker';
89
+ export type { RecordPickerProps, RecordPickerItemState, RecordPickerVariant, RecordPickerTrigger, UseRecordSearchOptions, UseRecordSearchResult, } from './record-picker';
90
+ export { RecordPickerAction, PickerCreateItem, JOINED_TRIGGER_CLASS, hasRecordPickerAction, requestRecordCreate, requestRecordEdit, recordLabel, withSearchPrefill, } from './record-picker-actions';
91
+ export type { RecordPickerActionProps, RecordCreateRequest, RecordEditRequest } from './record-picker-actions';
92
+ export { useRecordPickerDialog } from './record-picker-dialog';
93
+ export type { UseRecordPickerDialogOptions, UseRecordPickerDialogResult } from './record-picker-dialog';
94
+ export { OptionLead, OptionThumb } from './record-picker-option';
90
95
  export { ViewRecordDialog } from './dialogs/view-record-dialog';
91
96
  export type { ModelKey, ModelSchema, CreateResult, RecordDialogProps, CreateRecordDialogProps, ViewRecordDialogProps, } from './dialogs/types';
92
97
  export { ExportDialog } from './dialogs/export';
@@ -99,6 +104,8 @@ export { registerAgentResultRenderer, resolveAgentResultRenderer, listAgentResul
99
104
  export { isColumnVisibleInTable, isColumnVisibleInModal, isColumnVisibleInLineSubtable, AUDIT_SUBTABLE_COLUMN_KEYS, getSearchableColumnKeys, } from './column-visibility';
100
105
  export { batchGet, loadQueryPart, nameBatchTokens, narrowInToken, optionsBatchToken, optionsModelFromUrl, rememberInRows, invalidateQueryBatchData, resetQueryBatchCache, splitInToken, tokenForGet, type InListToken, type NarrowedIn, type QueryPart, } from './query-batch';
101
106
  export { useOptionsResolver, invalidateOptionsCache, projectOption, type ResolvedOption, type OptionsMeta, type UseOptionsResolverArgs, type UseOptionsResolverResult, } from './use-options-resolver';
107
+ export { useResolveOptionIds, OPTION_IDS_CHUNK, type OptionIdResolution, type UseResolveOptionIdsArgs, type UseResolveOptionIdsResult, } from './use-option-ids';
108
+ export { DELETED_RECORD_LABEL } from './dynamic-select-field';
102
109
  export { setOrgConfigBridge, getOrgConfigBridge, resolveValidatorToken, type OrgConfigBridge, } from './use-org-config-bridge';
103
110
  export { registerValidator, getDependsOn, resolveDependsValue, getOptionsConfig, resolveOptionsSource, getVisibleWhen, evaluateVisibleWhen, ATTRIBUTE_CLASSES_KEY, scopeValueFromFilterToken, buildListScopeValues, evaluateVisibleWhenForListScope, applyLineItemRowFormulas, computeLineItemTotals, toNumber, } from './dynamic-form-schema';
104
111
  export { BranchCreateGateProvider, useBranchCreateGate, type BranchCreateGateApi, } from './branch-create-gate';