@asteby/metacore-runtime-react 49.4.0 → 49.6.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 +14 -0
- package/dist/api-context.d.ts +9 -1
- package/dist/api-context.d.ts.map +1 -1
- package/dist/api-context.js +6 -4
- package/dist/business/customer-picker.d.ts +8 -2
- package/dist/business/customer-picker.d.ts.map +1 -1
- package/dist/business/customer-picker.js +30 -27
- package/dist/business/index.d.ts +1 -0
- package/dist/business/index.d.ts.map +1 -1
- package/dist/business/index.js +1 -0
- package/dist/business/line-product-cell.d.ts +8 -10
- package/dist/business/line-product-cell.d.ts.map +1 -1
- package/dist/business/line-product-cell.js +13 -90
- package/dist/business/product-options.d.ts +41 -0
- package/dist/business/product-options.d.ts.map +1 -0
- package/dist/business/product-options.js +58 -0
- package/dist/business/product-picker.d.ts +5 -1
- package/dist/business/product-picker.d.ts.map +1 -1
- package/dist/business/product-picker.js +26 -35
- package/dist/business/relate-documents.d.ts +1 -1
- package/dist/business/relate-documents.d.ts.map +1 -1
- package/dist/business/relate-documents.js +6 -8
- package/dist/business/vehicle-picker.d.ts +8 -2
- package/dist/business/vehicle-picker.d.ts.map +1 -1
- package/dist/business/vehicle-picker.js +28 -28
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +28 -43
- package/dist/dynamic-multi-select-field.d.ts.map +1 -1
- package/dist/dynamic-multi-select-field.js +17 -5
- package/dist/dynamic-select-field.d.ts +7 -23
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +67 -161
- package/dist/dynamic-table.d.ts +31 -1
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +37 -4
- package/dist/entity-select.d.ts +5 -2
- package/dist/entity-select.d.ts.map +1 -1
- package/dist/entity-select.js +23 -116
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/query-batch.d.ts.map +1 -1
- package/dist/query-batch.js +17 -1
- package/dist/record-picker-actions.d.ts +40 -2
- package/dist/record-picker-actions.d.ts.map +1 -1
- package/dist/record-picker-actions.js +44 -4
- package/dist/record-picker-dialog.d.ts +23 -0
- package/dist/record-picker-dialog.d.ts.map +1 -0
- package/dist/record-picker-dialog.js +37 -0
- package/dist/record-picker-option.d.ts +25 -0
- package/dist/record-picker-option.d.ts.map +1 -0
- package/dist/record-picker-option.js +55 -0
- package/dist/record-picker.d.ts +149 -0
- package/dist/record-picker.d.ts.map +1 -0
- package/dist/record-picker.js +372 -0
- package/package.json +1 -1
- package/src/__tests__/api-batch-opt-out.test.tsx +83 -0
- package/src/__tests__/business-pickers.test.tsx +6 -3
- package/src/__tests__/document-editor-inline-selects.test.tsx +2 -1
- package/src/__tests__/document-form-dialog.test.tsx +4 -3
- package/src/__tests__/document-lines-grid.test.tsx +2 -1
- package/src/__tests__/dynamic-table-bulk-actions.test.tsx +140 -0
- package/src/__tests__/dynamic-table-load-error.test.tsx +97 -0
- package/src/__tests__/record-picker.test.tsx +469 -0
- package/src/api-context.tsx +19 -4
- package/src/business/customer-picker.tsx +87 -98
- package/src/business/index.ts +1 -0
- package/src/business/line-product-cell.tsx +55 -180
- package/src/business/product-options.tsx +119 -0
- package/src/business/product-picker.tsx +63 -126
- package/src/business/relate-documents.tsx +36 -45
- package/src/business/vehicle-picker.tsx +76 -99
- package/src/dialogs/dynamic-record.tsx +44 -97
- package/src/dynamic-multi-select-field.tsx +39 -13
- package/src/dynamic-select-field.tsx +143 -368
- package/src/dynamic-table.tsx +91 -4
- package/src/entity-select.tsx +53 -202
- package/src/index.ts +27 -2
- package/src/query-batch.ts +18 -1
- package/src/record-picker-actions.tsx +101 -8
- package/src/record-picker-dialog.tsx +80 -0
- package/src/record-picker-option.tsx +96 -0
- package/src/record-picker.tsx +971 -0
- package/dist/business/use-async-search.d.ts +0 -10
- package/dist/business/use-async-search.d.ts.map +0 -1
- package/dist/business/use-async-search.js +0 -38
- package/src/business/use-async-search.ts +0 -43
|
@@ -1,48 +1,39 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// ProductPicker — una sola búsqueda para productos, llantas y variantes
|
|
3
3
|
// (medida «205/55R16», SKU, código de barras, clave de proveedor). Benchmark §7.
|
|
4
|
-
|
|
4
|
+
// Configuración del <RecordPicker> compartido (mismas filas que la celda de
|
|
5
|
+
// producto del DocumentEditor, ver product-options.tsx).
|
|
6
|
+
import { useState } from 'react';
|
|
5
7
|
import { useTranslation } from 'react-i18next';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
import { RecordPicker } from '../record-picker';
|
|
9
|
+
import { ProductHitRow, productHitKey, productHitLabel, useProductSearch } from './product-options';
|
|
10
|
+
import { availableStock } from './product-search';
|
|
11
|
+
/**
|
|
12
|
+
* @deprecated Configuración fina de {@link RecordPicker}; para pantallas nuevas
|
|
13
|
+
* usa RecordPicker con `useProductSearch` / `ProductHitRow`.
|
|
14
|
+
*/
|
|
12
15
|
export function ProductPicker({ search, onSelect, warehouseId, autoSelectBarcode = true, allowOutOfStock = true, placeholder, autoFocus, currency, disabled, }) {
|
|
13
16
|
const { t } = useTranslation();
|
|
14
|
-
const fmt = useFormatter({ currency });
|
|
15
17
|
const [text, setText] = useState('');
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
return
|
|
21
|
-
}
|
|
22
|
-
const { results, loading, error } = useAsyncSearch(text, run, {
|
|
23
|
-
minChars: parsed.kind === 'barcode' ? 8 : 2,
|
|
24
|
-
delay: parsed.kind === 'barcode' ? 0 : 250,
|
|
25
|
-
});
|
|
18
|
+
const [open, setOpen] = useState(false);
|
|
19
|
+
const { parsed, results, hits, loading, error, minChars } = useProductSearch(text, search);
|
|
20
|
+
const outOfStock = (h) => {
|
|
21
|
+
const stock = availableStock(h.variant ?? h.product, warehouseId);
|
|
22
|
+
return !allowOutOfStock && stock != null && stock <= 0;
|
|
23
|
+
};
|
|
26
24
|
const pick = (p, v) => {
|
|
27
|
-
|
|
28
|
-
if (!allowOutOfStock && stock != null && stock <= 0)
|
|
25
|
+
if (outOfStock({ product: p, variant: v }))
|
|
29
26
|
return;
|
|
30
27
|
onSelect(p, v);
|
|
31
28
|
setText('');
|
|
32
29
|
};
|
|
33
|
-
return (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const hasVariants = (p.variants?.length ?? 0) > 0;
|
|
43
|
-
return (_jsxs("li", { role: "option", "aria-selected": false, className: "px-2 py-1.5", children: [_jsxs("button", { type: "button", className: "flex w-full items-center justify-between gap-2 text-left disabled:opacity-50", disabled: hasVariants || (!allowOutOfStock && stock != null && stock <= 0), onClick: () => pick(p), children: [_jsxs("span", { children: [_jsx("span", { className: "block text-sm font-medium", children: p.name }), _jsx("span", { className: "block text-xs text-muted-foreground", children: [p.sku, p.tire?.normalized, p.supplier_sku && `Prov. ${p.supplier_sku}`].filter(Boolean).join(' · ') })] }), _jsxs("span", { className: "flex items-center gap-2", children: [stock != null && (_jsx(Badge, { variant: stock > 0 ? 'success' : 'danger', children: stock > 0 ? t('productPicker.inStock', { defaultValue: '{{n}} disp.', n: stock }) : t('productPicker.outOfStock', { defaultValue: 'Sin existencia' }) })), p.price != null && !hasVariants && _jsx("span", { className: "text-sm tabular-nums", children: fmt.money(p.price) })] })] }), hasVariants && (_jsx("ul", { className: "mt-1 space-y-0.5 pl-3", children: p.variants.map((v) => {
|
|
44
|
-
const vs = availableStock(v, warehouseId);
|
|
45
|
-
return (_jsx("li", { children: _jsxs("button", { type: "button", className: "flex w-full items-center justify-between gap-2 rounded px-1 py-0.5 text-left text-sm hover:bg-accent disabled:opacity-50", disabled: !allowOutOfStock && vs != null && vs <= 0, onClick: () => pick(p, v), children: [_jsx("span", { children: v.label }), _jsxs("span", { className: "flex items-center gap-2", children: [vs != null && _jsx(Badge, { variant: vs > 0 ? 'success' : 'danger', children: vs }), (v.price ?? p.price) != null && _jsx("span", { className: "tabular-nums", children: fmt.money(v.price ?? p.price) })] })] }) }, v.id));
|
|
46
|
-
}) }))] }, p.id));
|
|
47
|
-
}) }))] }));
|
|
30
|
+
return (_jsx(RecordPicker, { trigger: "input", slot: "product-picker", items: hits, loading: loading, error: error, minChars: minChars, query: text, onQueryChange: setText, open: open, onOpenChange: setOpen, getKey: productHitKey, getLabel: productHitLabel, renderItem: (hit) => _jsx(ProductHitRow, { hit: hit, warehouseId: warehouseId, currency: currency }), isItemDisabled: outOfStock, onSelect: (hit) => pick(hit.product, hit.variant), onInputKeyDown: (e) => {
|
|
31
|
+
// Código de barras exacto con un único resultado: Enter lo elige.
|
|
32
|
+
if (e.key === 'Enter' && autoSelectBarcode && parsed.kind === 'barcode' && results.length === 1) {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
const only = results[0];
|
|
35
|
+
pick(only, only.variants?.find((v) => v.barcode === parsed.barcode));
|
|
36
|
+
setOpen(false);
|
|
37
|
+
}
|
|
38
|
+
}, disabled: disabled, triggerProps: { autoFocus }, minListWidth: "22rem", placeholder: placeholder ?? t('productPicker.placeholder', { defaultValue: 'Producto, medida (205/55R16), SKU o código de barras' }), loadingText: t('common.searching', { defaultValue: 'Buscando…' }), errorText: t('productPicker.error', { defaultValue: 'No se pudo buscar. Revisa tu conexión e inténtalo de nuevo.' }), emptyText: _jsxs("span", { className: "block space-y-0.5", children: [_jsx("span", { className: "block font-medium text-foreground", children: t('productPicker.empty', { defaultValue: 'No encontramos ese producto' }) }), _jsx("span", { className: "block text-xs", children: t('productPicker.emptyHint', { defaultValue: 'Prueba con otra medida, SKU o código de barras.' }) })] }), below: parsed.kind === 'tire_size' ? (_jsxs("p", { className: "text-xs text-muted-foreground", children: [t('productPicker.tireSize', { defaultValue: 'Medida de llanta' }), ": ", parsed.tire.normalized] })) : null }));
|
|
48
39
|
}
|
|
@@ -26,7 +26,7 @@ export interface RelateDocumentsProps {
|
|
|
26
26
|
onChange: (value: RelatedDocument[]) => void;
|
|
27
27
|
/** Catálogo de tipos de relación (lo aporta el addon). */
|
|
28
28
|
relationTypes: RelationTypeOption[];
|
|
29
|
-
/** Búsqueda de documentos origen.
|
|
29
|
+
/** Búsqueda de documentos origen. */
|
|
30
30
|
search: (q: string, signal: AbortSignal) => Promise<RelatableDocument[]>;
|
|
31
31
|
/** Tipo preseleccionado al agregar. Default: el primero del catálogo. */
|
|
32
32
|
defaultRelationType?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relate-documents.d.ts","sourceRoot":"","sources":["../../src/business/relate-documents.tsx"],"names":[],"mappings":"AAWA,MAAM,WAAW,iBAAiB;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,mDAAmD;IACnD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IAC/B,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CAChB;AAED,6DAA6D;AAC7D,MAAM,WAAW,eAAe;IAC5B,mBAAmB,EAAE,MAAM,CAAA;IAC3B,aAAa,EAAE,MAAM,CAAA;IACrB,yBAAyB;IACzB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,oBAAoB;IACjC,KAAK,EAAE,eAAe,EAAE,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,IAAI,CAAA;IAC5C,0DAA0D;IAC1D,aAAa,EAAE,kBAAkB,EAAE,CAAA;IACnC,
|
|
1
|
+
{"version":3,"file":"relate-documents.d.ts","sourceRoot":"","sources":["../../src/business/relate-documents.tsx"],"names":[],"mappings":"AAWA,MAAM,WAAW,iBAAiB;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,mDAAmD;IACnD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IAC/B,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CAChB;AAED,6DAA6D;AAC7D,MAAM,WAAW,eAAe;IAC5B,mBAAmB,EAAE,MAAM,CAAA;IAC3B,aAAa,EAAE,MAAM,CAAA;IACrB,yBAAyB;IACzB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,oBAAoB;IACjC,KAAK,EAAE,eAAe,EAAE,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,IAAI,CAAA;IAC5C,0DAA0D;IAC1D,aAAa,EAAE,kBAAkB,EAAE,CAAA;IACnC,qCAAqC;IACrC,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAA;IACxE,yEAAyE;IACzE,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAMD,wBAAgB,eAAe,CAAC,EAC5B,KAAK,EACL,QAAQ,EACR,aAAa,EACb,MAAM,EACN,mBAAmB,EACnB,GAAG,EACH,QAAQ,EACR,WAAW,GACd,EAAE,oBAAoB,+BAsGtB;AAED,mFAAmF;AACnF,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,eAAe,EAAE,qBAAqB,GAAG,eAAe,CAAC,CAAC,CAEzI"}
|
|
@@ -6,17 +6,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
// (p. ej. SAT 01/03/04) llegan por la prop `relationTypes`.
|
|
7
7
|
import { useState } from 'react';
|
|
8
8
|
import { useTranslation } from 'react-i18next';
|
|
9
|
-
import { Link2,
|
|
10
|
-
import { Badge, Button
|
|
11
|
-
import {
|
|
9
|
+
import { Link2, X } from 'lucide-react';
|
|
10
|
+
import { Badge, Button } from '@asteby/metacore-ui';
|
|
11
|
+
import { RecordPicker, useLatestSearch } from '../record-picker';
|
|
12
12
|
function docTitle(d) {
|
|
13
13
|
return d.label || d.folio || d.uuid || d.id || '';
|
|
14
14
|
}
|
|
15
15
|
export function RelateDocuments({ value, onChange, relationTypes, search, defaultRelationType, max, disabled, placeholder, }) {
|
|
16
16
|
const { t } = useTranslation();
|
|
17
17
|
const [text, setText] = useState('');
|
|
18
|
+
const [open, setOpen] = useState(false);
|
|
18
19
|
const [pendingType, setPendingType] = useState(defaultRelationType ?? relationTypes[0]?.value ?? '');
|
|
19
|
-
const
|
|
20
|
+
const searchFn = useLatestSearch(search);
|
|
20
21
|
const full = max != null && value.length >= max;
|
|
21
22
|
const typeLabel = (v) => relationTypes.find((r) => r.value === v)?.label ?? v;
|
|
22
23
|
const add = (d) => {
|
|
@@ -31,10 +32,7 @@ export function RelateDocuments({ value, onChange, relationTypes, search, defaul
|
|
|
31
32
|
const remove = (id) => onChange(value.filter((x) => x.related_document_id !== id));
|
|
32
33
|
const setType = (id, relation_type) => onChange(value.map((x) => (x.related_document_id === id ? { ...x, relation_type } : x)));
|
|
33
34
|
const selectCls = 'h-9 rounded-md border bg-background px-2 text-sm';
|
|
34
|
-
return (_jsxs("div", { "data-slot": "relate-documents", className: "space-y-2", children: [value.length > 0 && (_jsx("ul", { className: "divide-y rounded-md border", children: value.map((d) => (_jsxs("li", { className: "flex flex-wrap items-center gap-2 px-2 py-1.5", children: [_jsx(Link2, { className: "size-4 text-muted-foreground", "aria-hidden": true }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "truncate text-sm font-medium", children: docTitle({ folio: d.folio, label: d.label, id: d.related_document_id }) }), d.uuid && _jsx("p", { className: "break-all font-mono text-xs text-muted-foreground", children: d.uuid })] }), _jsxs("select", { className: selectCls, disabled: disabled, value: d.relation_type, "aria-label": t('relateDocuments.type', { defaultValue: 'Tipo de relación' }), onChange: (e) => setType(d.related_document_id, e.target.value), children: [!relationTypes.some((r) => r.value === d.relation_type) && (_jsx("option", { value: d.relation_type, children: typeLabel(d.relation_type) })), relationTypes.map((r) => (_jsx("option", { value: r.value, children: r.label }, r.value)))] }), !disabled && (_jsx(Button, { type: "button", size: "icon", variant: "ghost", onClick: () => remove(d.related_document_id), "aria-label": t('relateDocuments.remove', { defaultValue: 'Quitar relación' }), children: _jsx(X, { className: "size-4" }) }))] }, d.related_document_id))) })), !full && !disabled && (
|
|
35
|
-
const already = value.some((x) => x.related_document_id === d.id);
|
|
36
|
-
return (_jsx("li", { role: "option", "aria-selected": already, children: _jsxs("button", { type: "button", disabled: already, className: "flex w-full items-center justify-between gap-2 px-2 py-1.5 text-left hover:bg-accent disabled:opacity-50", onClick: () => add(d), children: [_jsxs("span", { children: [_jsx("span", { className: "block text-sm font-medium", children: docTitle(d) }), d.uuid && _jsx("span", { className: "block break-all font-mono text-xs text-muted-foreground", children: d.uuid })] }), already && _jsx(Badge, { variant: "muted", children: t('relateDocuments.added', { defaultValue: 'Agregado' }) })] }) }, d.id));
|
|
37
|
-
}) }))] }))] }));
|
|
35
|
+
return (_jsxs("div", { "data-slot": "relate-documents", className: "space-y-2", children: [value.length > 0 && (_jsx("ul", { className: "divide-y rounded-md border", children: value.map((d) => (_jsxs("li", { className: "flex flex-wrap items-center gap-2 px-2 py-1.5", children: [_jsx(Link2, { className: "size-4 text-muted-foreground", "aria-hidden": true }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "truncate text-sm font-medium", children: docTitle({ folio: d.folio, label: d.label, id: d.related_document_id }) }), d.uuid && _jsx("p", { className: "break-all font-mono text-xs text-muted-foreground", children: d.uuid })] }), _jsxs("select", { className: selectCls, disabled: disabled, value: d.relation_type, "aria-label": t('relateDocuments.type', { defaultValue: 'Tipo de relación' }), onChange: (e) => setType(d.related_document_id, e.target.value), children: [!relationTypes.some((r) => r.value === d.relation_type) && (_jsx("option", { value: d.relation_type, children: typeLabel(d.relation_type) })), relationTypes.map((r) => (_jsx("option", { value: r.value, children: r.label }, r.value)))] }), !disabled && (_jsx(Button, { type: "button", size: "icon", variant: "ghost", onClick: () => remove(d.related_document_id), "aria-label": t('relateDocuments.remove', { defaultValue: 'Quitar relación' }), children: _jsx(X, { className: "size-4" }) }))] }, d.related_document_id))) })), !full && !disabled && (_jsx(RecordPicker, { trigger: "input", search: searchFn, minChars: 2, query: text, onQueryChange: setText, open: open, onOpenChange: setOpen, getKey: (d) => d.id, getLabel: docTitle, isItemDisabled: (d) => value.some((x) => x.related_document_id === d.id), renderItem: (d, { disabled: already }) => (_jsxs("span", { className: "flex w-full min-w-0 items-center justify-between gap-2", children: [_jsxs("span", { className: "min-w-0", children: [_jsx("span", { className: "block truncate text-sm font-medium", children: docTitle(d) }), d.uuid && _jsx("span", { className: "block break-all font-mono text-xs text-muted-foreground", children: d.uuid })] }), already && _jsx(Badge, { variant: "muted", children: t('relateDocuments.added', { defaultValue: 'Agregado' }) })] })), onSelect: add, placeholder: placeholder ?? t('relateDocuments.placeholder', { defaultValue: 'Buscar por folio o UUID' }), loadingText: t('common.searching', { defaultValue: 'Buscando…' }), errorText: t('relateDocuments.error', { defaultValue: 'No se pudo buscar documentos. Inténtalo de nuevo.' }), emptyText: t('relateDocuments.empty', { defaultValue: 'No encontramos ese documento.' }), after: _jsx("select", { className: selectCls, value: pendingType, "aria-label": t('relateDocuments.newType', { defaultValue: 'Tipo para el documento nuevo' }), onChange: (e) => setPendingType(e.target.value), children: relationTypes.map((r) => (_jsx("option", { value: r.value, children: r.label }, r.value))) }) }))] }));
|
|
38
36
|
}
|
|
39
37
|
/** Valor a enviar al servidor: solo los datos, sin los campos de visualización. */
|
|
40
38
|
export function serializeRelatedDocuments(value) {
|
|
@@ -15,7 +15,7 @@ export interface VehiclePickerProps {
|
|
|
15
15
|
value: VehicleResult | null;
|
|
16
16
|
/** Evento único de cambio: vehículo elegido/creado, o `null` al limpiar. */
|
|
17
17
|
onChange: (vehicle: VehicleResult | null) => void;
|
|
18
|
-
/** Búsqueda por placa o VIN.
|
|
18
|
+
/** Búsqueda por placa o VIN. */
|
|
19
19
|
search: (q: string, signal: AbortSignal) => Promise<VehicleResult[]>;
|
|
20
20
|
/** Modelo del kernel para el alta rápida. Default `Vehicle`. */
|
|
21
21
|
model?: string;
|
|
@@ -25,6 +25,8 @@ export interface VehiclePickerProps {
|
|
|
25
25
|
mapRecord?: (rec: Record<string, unknown>) => VehicleResult;
|
|
26
26
|
/** Override de permiso (default `<model>.create` con useCan). */
|
|
27
27
|
canCreate?: boolean;
|
|
28
|
+
/** Override de permiso para editar al elegido (default `<model>.update`). */
|
|
29
|
+
canEdit?: boolean;
|
|
28
30
|
/** Valores iniciales del alta (p. ej. `customer_id` del cliente ya elegido). */
|
|
29
31
|
createDefaults?: Record<string, unknown>;
|
|
30
32
|
disabled?: boolean;
|
|
@@ -32,5 +34,9 @@ export interface VehiclePickerProps {
|
|
|
32
34
|
}
|
|
33
35
|
/** Línea corta de un vehículo: «Nissan Versa 2019 · ABC-123 · VIN…». */
|
|
34
36
|
export declare function describeVehicle(v: VehicleResult): string;
|
|
35
|
-
|
|
37
|
+
/**
|
|
38
|
+
* @deprecated Configuración fina de {@link RecordPicker}; se conserva para no
|
|
39
|
+
* romper consumidores.
|
|
40
|
+
*/
|
|
41
|
+
export declare function VehiclePicker({ value, onChange, search, model, endpoint, mapRecord, canCreate, canEdit, createDefaults, disabled, placeholder, }: VehiclePickerProps): import("react").JSX.Element;
|
|
36
42
|
//# sourceMappingURL=vehicle-picker.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vehicle-picker.d.ts","sourceRoot":"","sources":["../../src/business/vehicle-picker.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"vehicle-picker.d.ts","sourceRoot":"","sources":["../../src/business/vehicle-picker.tsx"],"names":[],"mappings":"AAaA,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,kBAAkB;IAC/B,KAAK,EAAE,aAAa,GAAG,IAAI,CAAA;IAC3B,4EAA4E;IAC5E,QAAQ,EAAE,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,KAAK,IAAI,CAAA;IACjD,gCAAgC;IAChC,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC,CAAA;IACpE,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,qGAAqG;IACrG,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,aAAa,CAAA;IAC3D,iEAAiE;IACjE,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAQD,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,CAAC,EAAE,aAAa,GAAG,MAAM,CAGxD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAC1B,KAAK,EACL,QAAQ,EACR,MAAM,EACN,KAAiB,EACjB,QAAQ,EACR,SAAS,EACT,SAAS,EACT,OAAO,EACP,cAAc,EACd,QAAQ,EACR,WAAW,GACd,EAAE,kBAAkB,+BAuFpB"}
|
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
// VehiclePicker + alta rápida — busca por placa o VIN (y marca/modelo/cliente que
|
|
3
|
-
// el host decida indexar)
|
|
4
|
-
//
|
|
3
|
+
// el host decida indexar). Configuración del <RecordPicker> compartido: el alta
|
|
4
|
+
// rápida (y la edición del elegido) reutiliza el <CreateRecordDialog> dinámico
|
|
5
|
+
// del modelo vehículo. Mismo contrato que <CustomerPicker>: controlado
|
|
5
6
|
// (`value` / `onChange`), `search` inyectado por el host, permisos con useCan
|
|
6
7
|
// (sin <PermissionsProvider> no bloquea nada: el backend autoriza).
|
|
7
8
|
import { useCallback, useState } from 'react';
|
|
8
9
|
import { useTranslation } from 'react-i18next';
|
|
9
|
-
import { Car
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { useApi } from '../api-context';
|
|
10
|
+
import { Car } from 'lucide-react';
|
|
11
|
+
import { RecordPicker, useLatestSearch } from '../record-picker';
|
|
12
|
+
import { useRecordPickerDialog } from '../record-picker-dialog';
|
|
13
13
|
import { useCan } from '../permissions-context';
|
|
14
|
-
import { useAsyncSearch } from './use-async-search';
|
|
15
|
-
import { EmptyState } from './feedback';
|
|
16
14
|
function snake(model) {
|
|
17
15
|
return model.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
18
16
|
}
|
|
@@ -22,19 +20,18 @@ export function describeVehicle(v) {
|
|
|
22
20
|
const unit = [v.make, v.model, v.year].filter((x) => x != null && x !== '').join(' ');
|
|
23
21
|
return [unit, v.plate, v.vin].filter(Boolean).join(' · ');
|
|
24
22
|
}
|
|
25
|
-
|
|
23
|
+
/**
|
|
24
|
+
* @deprecated Configuración fina de {@link RecordPicker}; se conserva para no
|
|
25
|
+
* romper consumidores.
|
|
26
|
+
*/
|
|
27
|
+
export function VehiclePicker({ value, onChange, search, model = 'Vehicle', endpoint, mapRecord, canCreate, canEdit, createDefaults, disabled, placeholder, }) {
|
|
26
28
|
const { t } = useTranslation();
|
|
27
|
-
const api = useApi();
|
|
28
29
|
const can = useCan();
|
|
29
30
|
const [text, setText] = useState('');
|
|
30
|
-
const [
|
|
31
|
-
const
|
|
31
|
+
const [open, setOpen] = useState(false);
|
|
32
|
+
const searchFn = useLatestSearch(search);
|
|
32
33
|
const mayCreate = canCreate ?? can(`${snake(model)}.create`);
|
|
33
|
-
const
|
|
34
|
-
const pick = (v) => {
|
|
35
|
-
onChange(v);
|
|
36
|
-
setText('');
|
|
37
|
-
};
|
|
34
|
+
const mayEdit = canEdit ?? can(`${snake(model)}.update`);
|
|
38
35
|
const toResult = useCallback((rec) => mapRecord
|
|
39
36
|
? mapRecord(rec)
|
|
40
37
|
: {
|
|
@@ -47,15 +44,18 @@ export function VehiclePicker({ value, onChange, search, model = 'Vehicle', endp
|
|
|
47
44
|
color: str(rec.color),
|
|
48
45
|
customer_id: str(rec.customer_id),
|
|
49
46
|
}, [mapRecord]);
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
const { openCreate, openEdit, dialog } = useRecordPickerDialog({
|
|
48
|
+
model,
|
|
49
|
+
endpoint,
|
|
50
|
+
prefillField: 'plate',
|
|
51
|
+
createDefaults,
|
|
52
|
+
onSaved: (rec, kind) => {
|
|
53
|
+
const next = toResult(rec);
|
|
54
|
+
onChange(kind === 'edit' && value ? { ...value, ...next } : next);
|
|
55
|
+
setText('');
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
return (_jsxs(_Fragment, { children: [_jsx(RecordPicker, { trigger: "input", slot: "vehicle-picker", search: searchFn, minChars: 2, query: text, onQueryChange: setText, open: open, onOpenChange: setOpen, getKey: (v) => v.id, getLabel: (v) => v.plate || v.vin || v.id, getDescription: (v) => [describeVehicle({ ...v, plate: undefined }), v.customer_name].filter(Boolean).join(' · '), value: value?.id ?? null, selected: value, onSelect: (v) => onChange(v), onClear: disabled ? undefined : () => onChange(null), clearLabel: t('vehiclePicker.clear', { defaultValue: 'Quitar vehículo' }), renderValue: (v) => v ? (_jsxs("span", { className: "flex min-w-0 items-start gap-2", children: [_jsx(Car, { className: "mt-0.5 size-4 shrink-0 text-muted-foreground", "aria-hidden": true }), _jsxs("span", { className: "block min-w-0", children: [_jsx("span", { className: "block truncate text-sm font-medium", children: v.plate || v.vin || t('vehiclePicker.noPlate', { defaultValue: 'Sin placa' }) }), _jsx("span", { className: "block truncate text-xs text-muted-foreground", children: [[v.make, v.model, v.year].filter((x) => x != null && x !== '').join(' '), v.vin && v.plate ? v.vin : '', v.customer_name]
|
|
52
59
|
.filter(Boolean)
|
|
53
|
-
.join(' · ') })] })] }),
|
|
54
|
-
}
|
|
55
|
-
return (_jsxs("div", { "data-slot": "vehicle-picker", className: "space-y-2", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-2.5 size-4 text-muted-foreground", "aria-hidden": true }), _jsx(Input, { className: "pl-8", disabled: disabled, value: text, placeholder: placeholder ?? t('vehiclePicker.placeholder', { defaultValue: 'Placa o VIN' }), onChange: (e) => setText(e.target.value) })] }), mayCreate && (_jsx(Button, { type: "button", variant: "outline", size: "icon", disabled: disabled, onClick: () => setCreating(true), "aria-label": t('vehiclePicker.create', { defaultValue: 'Nuevo vehículo' }), children: _jsx(Plus, { className: "size-4" }) }))] }), loading && _jsx("p", { className: "text-sm text-muted-foreground", children: t('common.searching', { defaultValue: 'Buscando…' }) }), error && !loading && (_jsx("p", { role: "alert", className: "text-sm text-destructive", children: t('vehiclePicker.error', { defaultValue: 'No se pudo buscar vehículos. Inténtalo de nuevo.' }) })), !loading && !error && text.trim().length >= 2 && results.length === 0 && (_jsx(EmptyState, { title: t('vehiclePicker.empty', { defaultValue: 'No encontramos ese vehículo' }), action: mayCreate ? { label: t('vehiclePicker.createAction', { defaultValue: 'Crear vehículo' }), onClick: () => setCreating(true) } : undefined })), results.length > 0 && (_jsx("ul", { role: "listbox", className: "max-h-64 divide-y overflow-auto rounded-md border", children: results.map((v) => (_jsx("li", { role: "option", "aria-selected": false, children: _jsxs("button", { type: "button", className: "flex w-full flex-col px-2 py-1.5 text-left hover:bg-accent", onClick: () => pick(v), children: [_jsx("span", { className: "text-sm font-medium", children: v.plate || v.vin || v.id }), _jsx("span", { className: "text-xs text-muted-foreground", children: [describeVehicle({ ...v, plate: undefined }), v.customer_name].filter(Boolean).join(' · ') })] }) }, v.id))) })), creating && (_jsx(CreateRecordDialog, { modelKey: model, open: creating, onOpenChange: setCreating, endpoint: base, defaults: createDefaults ?? (text.trim() ? { plate: text.trim() } : undefined), onCreate: async (data) => {
|
|
56
|
-
const res = await api.post(base, data);
|
|
57
|
-
const rec = (res.data?.data ?? res.data);
|
|
58
|
-
pick(toResult(rec));
|
|
59
|
-
return rec.id != null ? { id: String(rec.id) } : undefined;
|
|
60
|
-
} }))] }));
|
|
60
|
+
.join(' · ') })] })] })) : null, disabled: disabled, placeholder: placeholder ?? t('vehiclePicker.placeholder', { defaultValue: 'Placa o VIN' }), loadingText: t('common.searching', { defaultValue: 'Buscando…' }), errorText: t('vehiclePicker.error', { defaultValue: 'No se pudo buscar vehículos. Inténtalo de nuevo.' }), emptyText: t('vehiclePicker.empty', { defaultValue: 'No encontramos ese vehículo' }), entityLabel: t('vehiclePicker.entity', { defaultValue: 'vehículo' }), createLabel: t('vehiclePicker.create', { defaultValue: 'Nuevo vehículo' }), editLabel: t('vehiclePicker.edit', { defaultValue: 'Editar vehículo' }), createFooter: "empty", createFooterLabel: () => t('vehiclePicker.createAction', { defaultValue: 'Crear vehículo' }), onCreate: mayCreate ? (q) => openCreate(q) : undefined, onEdit: mayEdit && value ? () => openEdit(value.id) : undefined }), dialog] }));
|
|
61
61
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
|
|
@@ -16,18 +16,19 @@ import { useTranslation } from 'react-i18next';
|
|
|
16
16
|
/** Model key of the open create/edit dialog — used to hide "+" on self-FK pickers
|
|
17
17
|
* (e.g. Customer.parent_id) so they don't nest another "Crear Cliente" modal. */
|
|
18
18
|
const RecordDialogModelContext = createContext(undefined);
|
|
19
|
-
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Button, Input, Textarea, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Skeleton, Badge, Popover, PopoverContent, PopoverTrigger,
|
|
19
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Button, Input, Textarea, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Skeleton, Badge, Popover, PopoverContent, PopoverTrigger, Calendar, } from '@asteby/metacore-ui/primitives';
|
|
20
20
|
import { cn } from '@asteby/metacore-ui/lib';
|
|
21
21
|
import { toast } from 'sonner';
|
|
22
22
|
import { format, parseISO } from 'date-fns';
|
|
23
23
|
import { parseCalendarDate } from '../calendar-date';
|
|
24
24
|
import { es } from 'date-fns/locale';
|
|
25
|
-
import { ExternalLink, Loader2, CalendarIcon,
|
|
25
|
+
import { ExternalLink, Loader2, CalendarIcon, Upload, X as XIcon, ScanLine } from 'lucide-react';
|
|
26
26
|
import { BarcodeScanner } from '../barcode-scanner';
|
|
27
27
|
import { useApi } from '../api-context';
|
|
28
28
|
import { useBranchCreateGate } from '../branch-create-gate';
|
|
29
29
|
import { toastServerError, extractFieldErrors, localizeFieldIssue, localizeFieldErrorMap } from '../server-error';
|
|
30
30
|
import { DynamicSelectField, OptionLead, OptionThumb } from '../dynamic-select-field';
|
|
31
|
+
import { RecordPicker } from '../record-picker';
|
|
31
32
|
import { DynamicMultiSelectField } from '../dynamic-multi-select-field';
|
|
32
33
|
import { DynamicRelations } from '../dynamic-relations';
|
|
33
34
|
import { AuditInfo, readAuditMeta } from '../audit-info';
|
|
@@ -1305,15 +1306,12 @@ function extractArray(res) {
|
|
|
1305
1306
|
const searchCache = new Map();
|
|
1306
1307
|
function SearchField({ field, value, onChange }) {
|
|
1307
1308
|
const api = useApi();
|
|
1308
|
-
const [open, setOpen] = useState(false);
|
|
1309
|
-
const [query, setQuery] = useState('');
|
|
1310
|
-
const [results, setResults] = useState([]);
|
|
1311
|
-
const [loading, setLoading] = useState(false);
|
|
1312
1309
|
const [selectedLabel, setSelectedLabel] = useState('');
|
|
1310
|
+
const ep = field.searchEndpoint;
|
|
1313
1311
|
useEffect(() => {
|
|
1314
|
-
if (!value || !
|
|
1312
|
+
if (!value || !ep)
|
|
1315
1313
|
return;
|
|
1316
|
-
const cached = searchCache.get(
|
|
1314
|
+
const cached = searchCache.get(ep);
|
|
1317
1315
|
if (cached) {
|
|
1318
1316
|
const match = cached.find((item) => item.value === value || item.id === value);
|
|
1319
1317
|
if (match) {
|
|
@@ -1321,45 +1319,32 @@ function SearchField({ field, value, onChange }) {
|
|
|
1321
1319
|
return;
|
|
1322
1320
|
}
|
|
1323
1321
|
}
|
|
1324
|
-
api.get(
|
|
1322
|
+
api.get(ep, { params: { search: '', limit: 50 } }).then(res => {
|
|
1325
1323
|
const items = extractArray(res);
|
|
1326
|
-
searchCache.set(
|
|
1324
|
+
searchCache.set(ep, items);
|
|
1327
1325
|
const match = items.find((item) => item.value === value || item.id === value);
|
|
1328
1326
|
if (match)
|
|
1329
1327
|
setSelectedLabel(match.label || match.name || '');
|
|
1330
1328
|
}).catch(() => { });
|
|
1331
|
-
}, [value,
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
if (!
|
|
1336
|
-
const cached = searchCache.get(
|
|
1337
|
-
if (cached)
|
|
1338
|
-
|
|
1339
|
-
return;
|
|
1340
|
-
}
|
|
1329
|
+
}, [value, ep, api]);
|
|
1330
|
+
// Legacy `type: search` + searchEndpoint — same <RecordPicker> as every
|
|
1331
|
+
// relation field; the empty query is served from the per-endpoint cache.
|
|
1332
|
+
const search = useCallback(async (q) => {
|
|
1333
|
+
if (!q) {
|
|
1334
|
+
const cached = searchCache.get(ep);
|
|
1335
|
+
if (cached)
|
|
1336
|
+
return cached;
|
|
1341
1337
|
}
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", role: "combobox", className: cn("w-full justify-between font-normal h-9", !value && "text-muted-foreground"), children: [_jsx("span", { className: "truncate", children: selectedLabel || `Seleccionar ${field.label?.toLowerCase() || ''}...` }), _jsx(ChevronDown, { className: "ml-auto h-4 w-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "w-[--radix-popover-trigger-width] p-0", align: "start", side: "bottom", sideOffset: 4, children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { placeholder: `Buscar ${field.label?.toLowerCase() || ''}...`, value: query, onValueChange: setQuery }), _jsx(CommandList, { className: "max-h-[200px]", children: loading ? (_jsxs("div", { className: "py-6 text-center text-sm", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin mx-auto mb-1 text-muted-foreground" }), _jsx("span", { className: "text-muted-foreground text-xs", children: "Buscando..." })] })) : results.length === 0 ? (_jsx(CommandEmpty, { children: "Sin resultados." })) : (_jsx(CommandGroup, { children: results.map((item) => {
|
|
1355
|
-
const itemValue = item.value ?? item.id;
|
|
1356
|
-
const itemLabel = item.label ?? item.name ?? '';
|
|
1357
|
-
const isSelected = value === itemValue;
|
|
1358
|
-
return (_jsxs(CommandItem, { value: String(itemValue), onSelect: () => {
|
|
1359
|
-
onChange(itemValue);
|
|
1360
|
-
setSelectedLabel(itemLabel);
|
|
1361
|
-
setOpen(false);
|
|
1362
|
-
setQuery('');
|
|
1363
|
-
}, children: [isSelected && _jsx(Check, { className: "mr-2 h-3.5 w-3.5 shrink-0 text-primary" }), _jsx(OptionThumb, { image: item.image, name: itemLabel, size: 20 }), _jsxs("div", { className: "flex flex-col min-w-0 ml-2", children: [_jsx("span", { className: "truncate", children: itemLabel }), item.description && (_jsx("span", { className: "text-[11px] text-muted-foreground truncate", children: item.description }))] })] }, itemValue));
|
|
1364
|
-
}) })) })] }) })] }));
|
|
1338
|
+
const items = extractArray(await api.get(ep, { params: { search: q, limit: 20 } }));
|
|
1339
|
+
if (!q)
|
|
1340
|
+
searchCache.set(ep, items);
|
|
1341
|
+
return items;
|
|
1342
|
+
}, [api, ep]);
|
|
1343
|
+
const itemValue = (item) => item.value ?? item.id;
|
|
1344
|
+
const itemLabel = (item) => item.label ?? item.name ?? '';
|
|
1345
|
+
const what = field.label?.toLowerCase() || '';
|
|
1346
|
+
return (_jsx(RecordPicker, { search: search, minChars: 0, getKey: (item) => String(itemValue(item)), getLabel: itemLabel, getDescription: (item) => item.description, renderLead: (item) => _jsx(OptionThumb, { image: item.image, name: itemLabel(item), size: 20 }), value: value, selected: value ? { value, label: selectedLabel } : null, renderValue: () => (_jsx("span", { className: cn('min-w-0 flex-1 truncate', !selectedLabel && 'text-muted-foreground'), children: selectedLabel || `Seleccionar ${what}...` })), onSelect: (item) => {
|
|
1347
|
+
onChange(itemValue(item));
|
|
1348
|
+
setSelectedLabel(itemLabel(item));
|
|
1349
|
+
}, searchPlaceholder: `Buscar ${what}...`, emptyText: "Sin resultados." }));
|
|
1365
1350
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-multi-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-multi-select-field.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dynamic-multi-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-multi-select-field.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C,MAAM,WAAW,4BAA4B;IACzC,KAAK,EAAE,cAAc,CAAA;IACrB,qFAAqF;IACrF,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;CACtC;AAED,wBAAgB,uBAAuB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,4BAA4B,+BAoD/F"}
|
|
@@ -13,18 +13,20 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
13
13
|
//
|
|
14
14
|
// Options are resolved once (a single page, not per-keystroke) through the
|
|
15
15
|
// same canonical `/api/options/<ref>?field=id` endpoint DynamicSelectField
|
|
16
|
-
// uses — the
|
|
16
|
+
// uses — the shared <RecordPicker multiple> then filters that page client-side as the
|
|
17
17
|
// user types. That's the right tradeoff for the FK sets this targets
|
|
18
18
|
// (segments, tags, categories — tens, not thousands of rows); a field with a
|
|
19
19
|
// genuinely large option set should keep using a single dynamic_select per
|
|
20
20
|
// value instead.
|
|
21
|
-
import { useMemo } from 'react';
|
|
22
|
-
import { MultiSelect } from '@asteby/metacore-ui/primitives';
|
|
21
|
+
import { useMemo, useState } from 'react';
|
|
23
22
|
import { getOptionFilter } from './option-filter';
|
|
23
|
+
import { RecordPicker } from './record-picker';
|
|
24
|
+
import { OptionLead } from './record-picker-option';
|
|
24
25
|
import { useOptionsResolver } from './use-options-resolver';
|
|
25
26
|
import { getFieldRef } from './dynamic-form-schema';
|
|
26
27
|
export function DynamicMultiSelectField({ field, value, onChange }) {
|
|
27
28
|
const ref = getFieldRef(field);
|
|
29
|
+
const [query, setQuery] = useState('');
|
|
28
30
|
const { options, loading } = useOptionsResolver({
|
|
29
31
|
modelKey: '',
|
|
30
32
|
fieldKey: 'id',
|
|
@@ -34,6 +36,16 @@ export function DynamicMultiSelectField({ field, value, onChange }) {
|
|
|
34
36
|
optionFilter: getOptionFilter(field),
|
|
35
37
|
});
|
|
36
38
|
const selected = useMemo(() => (Array.isArray(value) ? value.map(String) : []), [value]);
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
+
const selectedItems = useMemo(() => selected
|
|
40
|
+
.map((id) => options.find((o) => String(o.id) === id))
|
|
41
|
+
.filter((o) => !!o), [selected, options]);
|
|
42
|
+
const shown = useMemo(() => {
|
|
43
|
+
const q = query.trim().toLowerCase();
|
|
44
|
+
return q ? options.filter((o) => String(o.label ?? '').toLowerCase().includes(q)) : options;
|
|
45
|
+
}, [options, query]);
|
|
46
|
+
const toggle = (o) => {
|
|
47
|
+
const id = String(o.id);
|
|
48
|
+
onChange(selected.includes(id) ? selected.filter((x) => x !== id) : [...selected, id]);
|
|
49
|
+
};
|
|
50
|
+
return (_jsx(RecordPicker, { multiple: true, slot: "dynamic-multi-select", id: field.key, items: shown, loading: loading, getKey: (o) => String(o.id), getLabel: (o) => o.label, renderLead: (o) => _jsx(OptionLead, { option: o, size: 20 }), getDescription: (o) => o.description, value: selected, selected: selectedItems, onSelect: toggle, onRemove: toggle, query: query, onQueryChange: setQuery, placeholder: loading ? 'Cargando…' : field.placeholder || 'Seleccionar...', searchPlaceholder: "Buscar...", emptyText: "Sin resultados." }));
|
|
39
51
|
}
|
|
@@ -1,34 +1,12 @@
|
|
|
1
1
|
import { type ResolvedOption } from './use-options-resolver';
|
|
2
2
|
import type { ActionFieldDef } from './types';
|
|
3
|
+
export { OptionLead, OptionThumb } from './record-picker-option';
|
|
3
4
|
/**
|
|
4
5
|
* Default hint shown when a cascading picker's depended-on field is still
|
|
5
6
|
* empty. Domain-neutral on purpose; a caller may override it per field via
|
|
6
7
|
* `dependsHint`.
|
|
7
8
|
*/
|
|
8
9
|
export declare const DEFAULT_DEPENDS_HINT = "Selecciona primero el campo del que depende";
|
|
9
|
-
/**
|
|
10
|
-
* Small square thumbnail for an option's `image`. When the option has no image
|
|
11
|
-
* it falls back to a deterministic initials avatar (shared `InitialsAvatar`)
|
|
12
|
-
* derived from `name`, so an imageless reference reads as a colored badge rather
|
|
13
|
-
* than an empty placeholder — and rows/triggers stay aligned. `size` is in
|
|
14
|
-
* pixels (kept small — 20–24px — so the picker reads as a list, not a gallery).
|
|
15
|
-
* Inline style for the box dimensions: arbitrary Tailwind classes from a
|
|
16
|
-
* federated addon don't always survive the host's class scan.
|
|
17
|
-
*/
|
|
18
|
-
export declare function OptionThumb({ image, name, size, }: {
|
|
19
|
-
image?: string | null;
|
|
20
|
-
name?: string | null;
|
|
21
|
-
size?: number;
|
|
22
|
-
}): import("react").JSX.Element;
|
|
23
|
-
/**
|
|
24
|
-
* Leading visual for an option: a photo thumbnail (FK relations with an image),
|
|
25
|
-
* else a declared icon, else a color dot (enum/status options with a color).
|
|
26
|
-
* Returns null when the option carries none, so plain text options stay plain.
|
|
27
|
-
*/
|
|
28
|
-
export declare function OptionLead({ option, size, }: {
|
|
29
|
-
option?: Pick<ResolvedOption, 'image' | 'color' | 'icon' | 'label'> | null;
|
|
30
|
-
size?: number;
|
|
31
|
-
}): import("react").JSX.Element | null;
|
|
32
10
|
export interface DynamicSelectFieldProps {
|
|
33
11
|
field: ActionFieldDef;
|
|
34
12
|
value: any;
|
|
@@ -94,6 +72,12 @@ export interface DynamicSelectFieldProps {
|
|
|
94
72
|
/** Paint trigger with destructive border when validation failed. */
|
|
95
73
|
invalid?: boolean;
|
|
96
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Declarative `dynamic_select` (manifest `ref` / `optionsConfig.source`) on top
|
|
77
|
+
* of the shared {@link RecordPicker}: options from the canonical options
|
|
78
|
+
* endpoint (`useOptionsResolver`: option_filter, options[].when keep-value,
|
|
79
|
+
* cascade `filter_value`), inline create/edit through the host modal events.
|
|
80
|
+
*/
|
|
97
81
|
export declare function DynamicSelectField({ field, value, onChange, onPick, seedOption, dependsValue, dependsHint, readonly, staticOptions, descriptionAsBadge, hideCreate, createDefaults, createLockedFields, invalid, }: DynamicSelectFieldProps): import("react").JSX.Element;
|
|
98
82
|
export default DynamicSelectField;
|
|
99
83
|
//# sourceMappingURL=dynamic-select-field.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AAiCA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAGhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAEhE;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,gDAAgD,CAAA;AAEjF,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAC1B,yFAAyF;IACzF,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,IAAI,CAAA;IACtC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,cAAc,GAAG,IAAI,CAAA;IAClC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,SAAS,cAAc,EAAE,GAAG,IAAI,CAAA;IAChD;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC7B,oEAAoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAA;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAC/B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,MAAM,EACN,UAAU,EACV,YAAY,EACZ,WAAW,EACX,QAAgB,EAChB,aAAoB,EACpB,kBAA0B,EAC1B,UAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,OAAe,GAClB,EAAE,uBAAuB,+BAwNzB;AAED,eAAe,kBAAkB,CAAA"}
|