@asteby/metacore-runtime-react 34.0.2 → 35.0.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 +5 -84
- package/dist/action-modal-dispatcher.d.ts +4 -0
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +102 -23
- package/dist/addon-loader.d.ts +1 -1
- package/dist/addon-loader.d.ts.map +1 -1
- package/dist/addon-loader.js +8 -1
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +107 -52
- package/dist/display-value.d.ts +23 -0
- package/dist/display-value.d.ts.map +1 -1
- package/dist/display-value.js +34 -1
- package/dist/dynamic-columns.d.ts +3 -0
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +51 -6
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +8 -9
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +10 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/metadata-cache.d.ts.map +1 -1
- package/dist/metadata-cache.js +8 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/use-debounced-value.d.ts +8 -0
- package/dist/use-debounced-value.d.ts.map +1 -0
- package/dist/use-debounced-value.js +15 -0
- package/dist/use-dynamic-filters.d.ts.map +1 -1
- package/dist/use-dynamic-filters.js +6 -4
- package/dist/use-print-document.d.ts +10 -1
- package/dist/use-print-document.d.ts.map +1 -1
- package/dist/use-print-document.js +28 -1
- package/package.json +1 -1
- package/src/__tests__/filename-from-disposition.test.ts +30 -0
- package/src/__tests__/image-stack.test.tsx +52 -0
- package/src/__tests__/prefill-scalar-from-record.test.ts +37 -0
- package/src/__tests__/record-detail-display.test.tsx +30 -0
- package/src/__tests__/use-debounced-value.test.ts +56 -0
- package/src/action-modal-dispatcher.tsx +110 -22
- package/src/addon-loader.tsx +7 -1
- package/src/dialogs/dynamic-record.tsx +167 -51
- package/src/display-value.tsx +82 -4
- package/src/dynamic-columns.tsx +94 -5
- package/src/dynamic-select-field.tsx +9 -9
- package/src/dynamic-table.tsx +9 -6
- package/src/index.ts +7 -0
- package/src/metadata-cache.ts +8 -1
- package/src/types.ts +2 -0
- package/src/use-debounced-value.ts +17 -0
- package/src/use-dynamic-filters.ts +6 -4
- package/src/use-print-document.ts +38 -2
|
@@ -10,7 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
10
|
// flows through <ApiProvider> from runtime-react. Host-specific runtime values —
|
|
11
11
|
// the image-url resolver and the org IANA timezone — are passed as props so the
|
|
12
12
|
// SDK stays transport- and host-agnostic.
|
|
13
|
-
import { createContext, useCallback, useContext, useEffect,
|
|
13
|
+
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
14
14
|
import { useTranslation } from 'react-i18next';
|
|
15
15
|
/** Model key of the open create/edit dialog — used to hide "+" on self-FK pickers
|
|
16
16
|
* (e.g. Customer.parent_id) so they don't nest another "Crear Cliente" modal. */
|
|
@@ -23,9 +23,7 @@ import { es } from 'date-fns/locale';
|
|
|
23
23
|
import { ExternalLink, Loader2, CalendarIcon, ChevronDown, Check, Upload, X as XIcon, ScanLine } from 'lucide-react';
|
|
24
24
|
import { BarcodeScanner } from '../barcode-scanner';
|
|
25
25
|
import { useApi } from '../api-context';
|
|
26
|
-
import { toastServerError, extractFieldErrors,
|
|
27
|
-
import { validateValues, bagHasErrors } from '../validator';
|
|
28
|
-
import { validationCatalog } from '../validation-catalog';
|
|
26
|
+
import { toastServerError, extractFieldErrors, localizeFieldIssue } from '../server-error';
|
|
29
27
|
import { DynamicSelectField, OptionLead, OptionThumb } from '../dynamic-select-field';
|
|
30
28
|
import { DynamicRelations } from '../dynamic-relations';
|
|
31
29
|
import { useOptionsResolver } from '../use-options-resolver';
|
|
@@ -39,7 +37,7 @@ import { DynamicIcon, isLucideIconName } from '../dynamic-icon';
|
|
|
39
37
|
import { IconPickerField } from '../icon-picker-field';
|
|
40
38
|
import { humanizeToken } from '../dynamic-columns-helpers';
|
|
41
39
|
import { formatDateCell } from '../dynamic-columns';
|
|
42
|
-
import { OptionBadge, statusColorFor, useIsDarkTheme, } from '../display-value';
|
|
40
|
+
import { ImageStack, OptionBadge, statusColorFor, useIsDarkTheme, } from '../display-value';
|
|
43
41
|
import { MediaValue, RichText } from '../rich-url';
|
|
44
42
|
import { generateBadgeStyles } from '@asteby/metacore-ui/lib';
|
|
45
43
|
import { CollectionCell } from '../collection-cell';
|
|
@@ -169,6 +167,23 @@ function isRelationField(field) {
|
|
|
169
167
|
!!getFieldRef(field) ||
|
|
170
168
|
!!field.searchEndpoint);
|
|
171
169
|
}
|
|
170
|
+
// looksLikeForeignKey — true only for real FKs. A bare `*_id` suffix is NOT
|
|
171
|
+
// enough: columns like `external_id`, `trace_id`, or `invoice_uid` are plain
|
|
172
|
+
// text identifiers from a PAC/provider, not belongs_to relations. Treating them
|
|
173
|
+
// as relations rendered an InitialsAvatar ("6" chip next to "6a8c…") and made
|
|
174
|
+
// fiscal detail modals look broken.
|
|
175
|
+
function looksLikeForeignKey(field) {
|
|
176
|
+
if (isRelationField(field))
|
|
177
|
+
return true;
|
|
178
|
+
if (typeof field.key !== 'string' || !field.key.endsWith('_id'))
|
|
179
|
+
return false;
|
|
180
|
+
const t = String(field.type || '').toLowerCase();
|
|
181
|
+
return (t === 'uuid' ||
|
|
182
|
+
t === 'search' ||
|
|
183
|
+
t === 'relation' ||
|
|
184
|
+
t === 'dynamic_select' ||
|
|
185
|
+
t === 'belongs_to');
|
|
186
|
+
}
|
|
172
187
|
function formatDisplayValue(rawValue, field) {
|
|
173
188
|
// Unset nullable FK serialized as the nil UUID renders as empty, not zeros.
|
|
174
189
|
const value = normalizeNilUuid(rawValue);
|
|
@@ -284,19 +299,9 @@ export function stripHiddenFieldValues(values, fields, mode) {
|
|
|
284
299
|
}
|
|
285
300
|
return out;
|
|
286
301
|
}
|
|
287
|
-
function toastValidationFailed(t, lang, localized) {
|
|
288
|
-
toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }), {
|
|
289
|
-
description: Object.values(localized).filter(Boolean).join('\n'),
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
302
|
export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, lockedFields, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl = identityImageUrl, timeZone, currency, onChange, }) {
|
|
293
303
|
const api = useApi();
|
|
294
|
-
const { t
|
|
295
|
-
// Unique per dialog instance. The footer submit lives OUTSIDE <form>, so
|
|
296
|
-
// it binds via `form={id}`. A hardcoded id made nested create (product +
|
|
297
|
-
// "Crear categoría") submit the PARENT form — toast "Revisa los campos
|
|
298
|
-
// marcados" with no marks on the inner modal.
|
|
299
|
-
const formId = useId();
|
|
304
|
+
const { t } = useTranslation();
|
|
300
305
|
const [modalMeta, setModalMeta] = useState(schema ? schema : null);
|
|
301
306
|
const [relations, setRelations] = useState([]);
|
|
302
307
|
const [record, setRecord] = useState(null);
|
|
@@ -490,25 +495,24 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
490
495
|
const labelForKey = (key) => {
|
|
491
496
|
const f = (modalMeta?.fields ?? []).find(x => x.key === key);
|
|
492
497
|
if (f?.label)
|
|
493
|
-
return
|
|
498
|
+
return f.label;
|
|
494
499
|
return key.replace(/[._-]+/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
495
500
|
};
|
|
496
|
-
const lang = i18n.language;
|
|
497
501
|
// Turn a failed submit (422 `errors` map, or a bare `{errors}` body) into
|
|
498
502
|
// inline field errors + a summary toast. When there is no field map, fall
|
|
499
503
|
// back to the existing single cause-carrying toast.
|
|
500
504
|
const handleSubmitError = (err) => {
|
|
501
505
|
const map = extractFieldErrors(err);
|
|
502
506
|
if (map) {
|
|
503
|
-
const
|
|
504
|
-
for (const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
setFieldErrors(
|
|
508
|
-
|
|
507
|
+
const next = {};
|
|
508
|
+
for (const [key, issues] of Object.entries(map)) {
|
|
509
|
+
next[key] = localizeFieldIssue(issues[0], labelForKey(key), t);
|
|
510
|
+
}
|
|
511
|
+
setFieldErrors(next);
|
|
512
|
+
toast.error(t('dynamic.validation_failed', { defaultValue: 'Revisa los campos marcados' }));
|
|
509
513
|
return;
|
|
510
514
|
}
|
|
511
|
-
toastServerError(err, { t,
|
|
515
|
+
toastServerError(err, { t, fallback: t('dynamic.save_error', { defaultValue: 'No se pudo guardar' }) });
|
|
512
516
|
};
|
|
513
517
|
const handleSubmit = async (e) => {
|
|
514
518
|
e?.preventDefault();
|
|
@@ -520,15 +524,15 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
520
524
|
// fields are gated: a field hidden by its `visible_when` predicate
|
|
521
525
|
// must not block submit even when it is declared required (matching
|
|
522
526
|
// the render, which drops it via the same filter).
|
|
523
|
-
const
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
setFieldErrors(
|
|
531
|
-
|
|
527
|
+
const missing = {};
|
|
528
|
+
for (const field of filterVisibleFields(modalMeta.fields, mode, formValues)) {
|
|
529
|
+
if (field.required && !formValues[field.key] && formValues[field.key] !== 0 && formValues[field.key] !== false) {
|
|
530
|
+
missing[field.key] = localizeFieldIssue({ code: 'required' }, field.label, t);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (Object.keys(missing).length) {
|
|
534
|
+
setFieldErrors(missing);
|
|
535
|
+
toast.error(t('dynamic.validation_failed', { defaultValue: 'Revisa los campos marcados' }));
|
|
532
536
|
return;
|
|
533
537
|
}
|
|
534
538
|
}
|
|
@@ -649,21 +653,22 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
649
653
|
// then advance. Mirrors handleSubmit's required check but scoped to the step.
|
|
650
654
|
const goNextStep = () => {
|
|
651
655
|
const step = groups[clampedStep];
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
656
|
+
const missing = {};
|
|
657
|
+
for (const field of step?.fields ?? []) {
|
|
658
|
+
if (field.required && !formValues[field.key] && formValues[field.key] !== 0 && formValues[field.key] !== false) {
|
|
659
|
+
missing[field.key] = localizeFieldIssue({ code: 'required' }, field.label, t);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (Object.keys(missing).length) {
|
|
663
|
+
setFieldErrors(missing);
|
|
664
|
+
toast.error(t('dynamic.validation_failed', { defaultValue: 'Revisa los campos marcados' }));
|
|
660
665
|
return;
|
|
661
666
|
}
|
|
662
667
|
setFieldErrors({});
|
|
663
668
|
setStepIndex(Math.min(clampedStep + 1, groups.length - 1));
|
|
664
669
|
};
|
|
665
670
|
const goBackStep = () => setStepIndex(Math.max(clampedStep - 1, 0));
|
|
666
|
-
return (_jsx(RecordDialogModelContext.Provider, { value: model, children: _jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl max-h-[90dvh] flex flex-col p-0 gap-0 overflow-hidden", style: { maxHeight: '90dvh' }, children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: config.description })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-6", children: loading ? (_jsx(LoadingSkeleton, {})) : modalMeta ? (_jsx(ModelContext.Provider, { value: model, children: _jsx(ImageUrlContext.Provider, { value: getImageUrl, children: _jsx(TimeZoneContext.Provider, { value: timeZone, children: _jsxs(CurrencyContext.Provider, { value: currency, children: [_jsxs("form", { id:
|
|
671
|
+
return (_jsx(RecordDialogModelContext.Provider, { value: model, children: _jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl max-h-[90dvh] flex flex-col p-0 gap-0 overflow-hidden", style: { maxHeight: '90dvh' }, children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: config.description })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-6", children: loading ? (_jsx(LoadingSkeleton, {})) : modalMeta ? (_jsx(ModelContext.Provider, { value: model, children: _jsx(ImageUrlContext.Provider, { value: getImageUrl, children: _jsx(TimeZoneContext.Provider, { value: timeZone, children: _jsxs(CurrencyContext.Provider, { value: currency, children: [_jsxs("form", { id: "dynamic-record-form", onSubmit: handleSubmit, className: "grid gap-y-4", children: [isSteps && (_jsx(WizardProgress, { groups: groups, stepIndex: clampedStep })), (isSteps ? [groups[clampedStep]] : groups).map(group => (_jsx(FieldSection, { group: group, children: _jsx("div", { className: "grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2", children: renderFields(group.fields) }) }, group.key))), record?.external_url && (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2", children: _jsx("div", { className: "sm:col-span-2 min-w-0", children: _jsxs("a", { href: record.external_url, target: "_blank", rel: "noreferrer", className: "inline-flex items-center gap-1.5 text-sm text-primary hover:underline mt-1", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5" }), "Ver en ", record.external_provider ?? 'proveedor externo'] }) }) }))] }), !isCreate && record && relations.length > 0 && (_jsx("div", { className: "mt-6", children: _jsx(DynamicRelations, { record: record, relations: relations, lineSubtable: true, embedOnly: true, canCreate: mode === 'edit', canEdit: mode === 'edit', canDelete: mode === 'edit', onChange: handleChildChange }) }))] }) }) }) })) : null }), _jsxs(DialogFooter, { className: "p-4 border-t shrink-0 sm:justify-between", children: [isView && onOpenFullPage ? (_jsxs(Button, { variant: "ghost", size: "sm", className: "text-muted-foreground", onClick: () => { onOpenChange(false); onOpenFullPage(); }, children: [_jsx(ExternalLink, { className: "mr-1.5 h-3.5 w-3.5" }), "Ver p\u00E1gina completa"] })) : _jsx("span", {}), _jsxs("div", { className: "flex items-center gap-2", children: [isSteps && clampedStep > 0 ? (_jsx(Button, { variant: "outline", onClick: goBackStep, disabled: saving || deleting, children: "Anterior" })) : (_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: saving || deleting, children: config.cancelLabel })), isView && onDelete && (_jsxs(Button, { variant: "destructive", onClick: handleDelete, disabled: deleting || loading, children: [deleting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), deleting ? 'Eliminando...' : 'Eliminar'] })), isView && onEdit && (_jsx(Button, { onClick: onEdit, disabled: deleting || loading, children: "Editar" })), isEditable && isSteps && !isLastStep && (_jsx(Button, { type: "button", onClick: goNextStep, disabled: saving || loading, children: "Siguiente" })), isEditable && (!isSteps || isLastStep) && (_jsxs(Button, { type: "submit", form: "dynamic-record-form", disabled: saving || loading, children: [saving && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), saving ? config.submittingLabel : config.submitLabel] }))] })] })] }) }) }));
|
|
667
672
|
}
|
|
668
673
|
function LoadingSkeleton() {
|
|
669
674
|
return (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4", children: Array.from({ length: 6 }).map((_, i) => (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Skeleton, { className: "h-3.5 w-24" }), _jsx(Skeleton, { className: "h-9 w-full" })] }, i))) }));
|
|
@@ -698,9 +703,7 @@ export function ReadonlyEditField({ field, value }) {
|
|
|
698
703
|
// ReadonlyRelationField — a locked/readonly FK field (customer_id, category_id…)
|
|
699
704
|
// resolves the record's label instead of showing the raw id, mirroring
|
|
700
705
|
// RelationViewValue's lookup but rendered as a disabled input to match the rest
|
|
701
|
-
// of ReadonlyEditField.
|
|
702
|
-
// seeding a POS-selected customer into a vehicle create modal) would show a bare
|
|
703
|
-
// UUID — the exact readability bug this dialog otherwise avoids elsewhere.
|
|
706
|
+
// of ReadonlyEditField.
|
|
704
707
|
function ReadonlyRelationField({ field, value, fieldRef, }) {
|
|
705
708
|
const rawVal = value && typeof value === 'object' ? (value.value ?? value.id) : value;
|
|
706
709
|
const needResolve = fieldRef != null || !!field.searchEndpoint;
|
|
@@ -720,7 +723,9 @@ function ReadonlyRelationField({ field, value, fieldRef, }) {
|
|
|
720
723
|
// RelationViewValue — read-only FK lead. Resolves the relation's label + image
|
|
721
724
|
// from (1) the sibling object the table served, then (2) the canonical options
|
|
722
725
|
// endpoint, and renders an OptionLead (thumbnail / icon / color dot) + label.
|
|
723
|
-
|
|
726
|
+
// When `stack` is true (`display: "image_stack"`), the landscape mark sits ON
|
|
727
|
+
// TOP of the label — wide logos (brand marks) fit without cropping.
|
|
728
|
+
function RelationViewValue({ field, value, record, stack = false, }) {
|
|
724
729
|
const getImageUrl = useContext(ImageUrlContext);
|
|
725
730
|
const sib = relationSiblingValue(field, record);
|
|
726
731
|
const sibLabel = typeof sib === 'string' ? sib : objectLabel(sib);
|
|
@@ -750,6 +755,9 @@ function RelationViewValue({ field, value, record }) {
|
|
|
750
755
|
if (!label && !image) {
|
|
751
756
|
return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
|
|
752
757
|
}
|
|
758
|
+
if (stack) {
|
|
759
|
+
return (_jsx("div", { className: "py-1", children: _jsx(ImageStack, { src: image || undefined, label: label, getImageUrl: getImageUrl, size: "lg" }) }));
|
|
760
|
+
}
|
|
753
761
|
const lead = {
|
|
754
762
|
image: image ? getImageUrl(image) : null,
|
|
755
763
|
color: resolved?.color ?? null,
|
|
@@ -801,10 +809,15 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
|
|
|
801
809
|
return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
|
|
802
810
|
}
|
|
803
811
|
const value = normalizeNilUuid(rawValue);
|
|
804
|
-
//
|
|
805
|
-
// label.
|
|
806
|
-
//
|
|
807
|
-
if (
|
|
812
|
+
// Landscape stack on a relation FK (brand marks, product cards): image ON
|
|
813
|
+
// TOP, label UNDERNEATH. Checked before the default relation lead so a
|
|
814
|
+
// `display: "image_stack"` FK does not fall through to the side-by-side chip.
|
|
815
|
+
if (renderAs === 'image_stack' && looksLikeForeignKey(field)) {
|
|
816
|
+
return _jsx(RelationViewValue, { field: field, value: value, record: record, stack: true });
|
|
817
|
+
}
|
|
818
|
+
// Relation (search / dynamic_select / ref / uuid *_id FK) → resolved
|
|
819
|
+
// thumbnail + label. Plain text `*_id` columns (external_id, …) stay text.
|
|
820
|
+
if (looksLikeForeignKey(field)) {
|
|
808
821
|
return _jsx(RelationViewValue, { field: field, value: value, record: record });
|
|
809
822
|
}
|
|
810
823
|
// The value is itself a resolved object the backend served inline — render
|
|
@@ -819,7 +832,24 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
|
|
|
819
832
|
if (field.type === 'color') {
|
|
820
833
|
return value ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "h-5 w-5 rounded-full border shadow-sm", style: { backgroundColor: value } }), _jsx("span", { className: "text-sm", children: value })] })) : (_jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "-" }));
|
|
821
834
|
}
|
|
822
|
-
|
|
835
|
+
// Landscape stack for image/logo URL columns (and `type: image` with
|
|
836
|
+
// `cellStyle: image_stack`). Wide marks sit above an optional caption.
|
|
837
|
+
if (renderAs === 'image_stack') {
|
|
838
|
+
if (value && isLucideIconName(value)) {
|
|
839
|
+
return _jsx(IconNameViewValue, { name: value });
|
|
840
|
+
}
|
|
841
|
+
const labelField = (field.styleConfig &&
|
|
842
|
+
field.styleConfig.label_field) ||
|
|
843
|
+
(field.styleConfig && field.styleConfig.labelField);
|
|
844
|
+
let caption;
|
|
845
|
+
if (labelField && record && typeof record === 'object') {
|
|
846
|
+
const raw = record[labelField];
|
|
847
|
+
if (raw != null && String(raw) !== '')
|
|
848
|
+
caption = String(raw);
|
|
849
|
+
}
|
|
850
|
+
return value || caption ? (_jsx("div", { className: "py-1", children: _jsx(ImageStack, { src: value ? String(value) : undefined, label: caption, getImageUrl: getImageUrl, size: "lg" }) })) : (_jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "Sin imagen" }));
|
|
851
|
+
}
|
|
852
|
+
if (field.type === 'image' || renderAs === 'image') {
|
|
823
853
|
if (isLucideIconName(value)) {
|
|
824
854
|
return _jsx(IconNameViewValue, { name: value });
|
|
825
855
|
}
|
|
@@ -990,8 +1020,33 @@ function StructuredViewValue({ value, field, locale, t, }) {
|
|
|
990
1020
|
if (isEmpty) {
|
|
991
1021
|
return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
|
|
992
1022
|
}
|
|
1023
|
+
// Line-items arrays with a declared itemFields schema → mini-table.
|
|
1024
|
+
// Plain objects (PAC provider_data, fiscal_data bags) → readable key/value
|
|
1025
|
+
// list; nested objects/arrays render as pretty JSON instead of `key: {…}`
|
|
1026
|
+
// stubs that looked broken in fiscal detail modals.
|
|
1027
|
+
const hasItemFields = !!(field?.itemFields ?? field?.item_fields);
|
|
1028
|
+
if (!hasItemFields &&
|
|
1029
|
+
value !== null &&
|
|
1030
|
+
typeof value === 'object' &&
|
|
1031
|
+
!Array.isArray(value)) {
|
|
1032
|
+
return _jsx(JsonObjectViewValue, { value: value });
|
|
1033
|
+
}
|
|
993
1034
|
return (_jsx("div", { className: "text-sm py-1", children: _jsx(CollectionCell, { value: value, itemFields: field?.itemFields ?? field?.item_fields, variant: "inline", locale: locale, t: t, getImageUrl: getImageUrl }) }));
|
|
994
1035
|
}
|
|
1036
|
+
/** Flatten a jsonb/object bag into labeled rows; nest as pretty JSON. */
|
|
1037
|
+
function JsonObjectViewValue({ value }) {
|
|
1038
|
+
const entries = Object.entries(value).filter(([, v]) => v !== undefined);
|
|
1039
|
+
if (entries.length === 0) {
|
|
1040
|
+
return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
|
|
1041
|
+
}
|
|
1042
|
+
return (_jsx("dl", { className: "grid gap-2 py-1 text-sm", children: entries.map(([key, raw]) => {
|
|
1043
|
+
const label = humanizeToken(key);
|
|
1044
|
+
const isNest = raw !== null &&
|
|
1045
|
+
typeof raw === 'object' &&
|
|
1046
|
+
!(raw instanceof Date);
|
|
1047
|
+
return (_jsxs("div", { className: "min-w-0", children: [_jsx("dt", { className: "text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: label }), _jsx("dd", { className: "mt-0.5 break-words text-foreground", children: isNest ? (_jsx("pre", { className: "max-h-48 overflow-auto rounded-md bg-muted/40 p-2 text-[11px] leading-relaxed whitespace-pre-wrap", children: JSON.stringify(raw, null, 2) })) : raw === null || raw === '' ? (_jsx("span", { className: "text-muted-foreground", children: "\u2014" })) : (String(raw)) })] }, key));
|
|
1048
|
+
}) }));
|
|
1049
|
+
}
|
|
995
1050
|
export function EditField({ field, value, onChange, record }) {
|
|
996
1051
|
const { t, i18n } = useTranslation();
|
|
997
1052
|
const editFieldImageUrl = useContext(ImageUrlContext);
|
package/dist/display-value.d.ts
CHANGED
|
@@ -33,6 +33,29 @@ export declare const RelationThumbnail: React.FC<{
|
|
|
33
33
|
getImageUrl?: (path: string) => string;
|
|
34
34
|
size?: number;
|
|
35
35
|
}>;
|
|
36
|
+
/**
|
|
37
|
+
* Landscape-friendly identity block: wide image ON TOP, label (and optional
|
|
38
|
+
* subtitle) UNDERNEATH. Use for brand logos, product photos and any mark that
|
|
39
|
+
* is wider than it is tall — a square thumb next to text crops logos and
|
|
40
|
+
* wastes the cell. Declared via `display: "image_stack"` on a column (image
|
|
41
|
+
* URL or FK relation with a logo/photo sibling).
|
|
42
|
+
*
|
|
43
|
+
* No padded card / muted plate behind the mark — that ate table row height.
|
|
44
|
+
* The image is sized with max-width/max-height and `object-contain` only.
|
|
45
|
+
*
|
|
46
|
+
* Sizes (CSS, not Tailwind arbitrary — host safelists vary):
|
|
47
|
+
* sm — compact table (~88×32)
|
|
48
|
+
* md — default table (~112×40)
|
|
49
|
+
* lg — detail/modal (~180×72)
|
|
50
|
+
*/
|
|
51
|
+
export declare const ImageStack: React.FC<{
|
|
52
|
+
src?: string | null;
|
|
53
|
+
label?: string | null;
|
|
54
|
+
subtitle?: string | null;
|
|
55
|
+
getImageUrl?: (path: string) => string;
|
|
56
|
+
size?: 'sm' | 'md' | 'lg';
|
|
57
|
+
className?: string;
|
|
58
|
+
}>;
|
|
36
59
|
export interface DisplayOption {
|
|
37
60
|
value: string;
|
|
38
61
|
label: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"display-value.d.ts","sourceRoot":"","sources":["../src/display-value.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB;;;GAGG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAmBxC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAepD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC;IACrC,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,
|
|
1
|
+
{"version":3,"file":"display-value.d.ts","sourceRoot":"","sources":["../src/display-value.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB;;;GAGG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAmBxC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAepD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC;IACrC,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,CAiBA,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB,CAuDA,CAAA;AAED,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC;IAC/B,MAAM,EAAE,aAAa,CAAA;IACrB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB,CA8BA,CAAA"}
|
package/dist/display-value.js
CHANGED
|
@@ -58,7 +58,40 @@ export function statusColorFor(value) {
|
|
|
58
58
|
* an addon-arbitrary Tailwind class never gets dropped by a consuming app's
|
|
59
59
|
* class scan). Rendered inline alongside a label — never alone.
|
|
60
60
|
*/
|
|
61
|
-
export const RelationThumbnail = ({ src, alt, getImageUrl, size = 20 }) => (_jsxs(Avatar, { className: "shrink-0 rounded-md
|
|
61
|
+
export const RelationThumbnail = ({ src, alt, getImageUrl, size = 20 }) => (_jsxs(Avatar, { className: "shrink-0 rounded-md", style: { width: size, height: size }, children: [_jsx(AvatarImage, { src: getImageUrl ? getImageUrl(src) : src, alt: alt, className: "object-contain" }), _jsx(AvatarFallback, { className: "rounded-md bg-transparent text-[8px] font-bold text-muted-foreground", children: getInitials(alt) })] }));
|
|
62
|
+
/**
|
|
63
|
+
* Landscape-friendly identity block: wide image ON TOP, label (and optional
|
|
64
|
+
* subtitle) UNDERNEATH. Use for brand logos, product photos and any mark that
|
|
65
|
+
* is wider than it is tall — a square thumb next to text crops logos and
|
|
66
|
+
* wastes the cell. Declared via `display: "image_stack"` on a column (image
|
|
67
|
+
* URL or FK relation with a logo/photo sibling).
|
|
68
|
+
*
|
|
69
|
+
* No padded card / muted plate behind the mark — that ate table row height.
|
|
70
|
+
* The image is sized with max-width/max-height and `object-contain` only.
|
|
71
|
+
*
|
|
72
|
+
* Sizes (CSS, not Tailwind arbitrary — host safelists vary):
|
|
73
|
+
* sm — compact table (~88×32)
|
|
74
|
+
* md — default table (~112×40)
|
|
75
|
+
* lg — detail/modal (~180×72)
|
|
76
|
+
*/
|
|
77
|
+
export const ImageStack = ({ src, label, subtitle, getImageUrl, size = 'md', className }) => {
|
|
78
|
+
const dims = size === 'lg'
|
|
79
|
+
? { w: 180, h: 72, text: 'text-sm' }
|
|
80
|
+
: size === 'sm'
|
|
81
|
+
? { w: 88, h: 32, text: 'text-[11px]' }
|
|
82
|
+
: { w: 112, h: 40, text: 'text-xs' };
|
|
83
|
+
const alt = label || '';
|
|
84
|
+
const resolved = src
|
|
85
|
+
? getImageUrl
|
|
86
|
+
? getImageUrl(src)
|
|
87
|
+
: src
|
|
88
|
+
: undefined;
|
|
89
|
+
const [broken, setBroken] = React.useState(false);
|
|
90
|
+
React.useEffect(() => {
|
|
91
|
+
setBroken(false);
|
|
92
|
+
}, [resolved]);
|
|
93
|
+
return (_jsxs("span", { className: `inline-flex max-w-[220px] flex-col items-start gap-0.5 ${className ?? ''}`, title: subtitle ? `${alt} · ${subtitle}` : alt || undefined, children: [resolved && !broken ? (_jsx("img", { src: resolved, alt: alt, className: "block object-contain", style: { maxWidth: dims.w, maxHeight: dims.h, width: 'auto', height: 'auto' }, onError: () => setBroken(true) })) : (_jsx("span", { className: "flex items-center text-[10px] font-semibold tracking-wide text-muted-foreground", style: { minHeight: Math.round(dims.h * 0.55) }, children: getInitials(alt || '?') })), (label || subtitle) && (_jsxs("span", { className: `flex w-full min-w-0 flex-col items-start leading-tight ${dims.text}`, children: [label ? (_jsx("span", { className: "w-full truncate text-left font-medium text-foreground/90", children: label })) : null, subtitle ? (_jsx("span", { className: "w-full truncate text-left text-[0.7em] text-muted-foreground", children: subtitle })) : null] }))] }));
|
|
94
|
+
};
|
|
62
95
|
/**
|
|
63
96
|
* Colored option badge — the canonical "pro" pill for a select/status/badge
|
|
64
97
|
* value. Explicit backend `color` wins; otherwise a stable, cohesive color is
|
|
@@ -173,6 +173,9 @@ export declare const resolveRelationSubtitle: (col: ColumnDefinition, row: any)
|
|
|
173
173
|
export declare const ImageCell: React.FC<{
|
|
174
174
|
value: unknown;
|
|
175
175
|
getImageUrl: (path: string) => string;
|
|
176
|
+
/** Optional caption under the image (display: image_stack). */
|
|
177
|
+
label?: string;
|
|
178
|
+
stack?: boolean;
|
|
176
179
|
}>;
|
|
177
180
|
export declare function makeDefaultGetDynamicColumns(helpers?: DynamicColumnsHelpers): GetDynamicColumns;
|
|
178
181
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"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"}
|
package/dist/dynamic-columns.js
CHANGED
|
@@ -23,7 +23,7 @@ import { generateBadgeStyles, getInitials, relationChipStyles, } from '@asteby/m
|
|
|
23
23
|
import { Progress } from './dialogs/_primitives';
|
|
24
24
|
import { humanizeToken } from './dynamic-columns-helpers';
|
|
25
25
|
import { objectLabel } from './dynamic-relation-helpers';
|
|
26
|
-
import { OptionBadge, RelationThumbnail, statusColorFor, useIsDarkTheme, } from './display-value';
|
|
26
|
+
import { OptionBadge, RelationThumbnail, ImageStack, statusColorFor, useIsDarkTheme, } from './display-value';
|
|
27
27
|
import { MediaValue } from './rich-url';
|
|
28
28
|
import { OptionsContext } from './options-context';
|
|
29
29
|
import { DynamicIcon, isLucideIconName } from './dynamic-icon';
|
|
@@ -369,7 +369,7 @@ export const resolveRelationLabel = (col, row) => {
|
|
|
369
369
|
export const resolveRelationImage = (col, row) => {
|
|
370
370
|
const sibling = getNestedValue(row, relationKeyFor(col));
|
|
371
371
|
if (sibling && typeof sibling === 'object') {
|
|
372
|
-
const img = sibling.image ?? sibling.avatar ?? sibling.photo;
|
|
372
|
+
const img = sibling.image ?? sibling.avatar ?? sibling.photo ?? sibling.logo ?? sibling.thumbnail;
|
|
373
373
|
if (img !== undefined && img !== null && img !== '')
|
|
374
374
|
return String(img);
|
|
375
375
|
}
|
|
@@ -468,13 +468,20 @@ export const resolveRelationSubtitle = (col, row) => {
|
|
|
468
468
|
* carries an `image`. Falls back to the raw id when no sibling was resolved, and
|
|
469
469
|
* to an empty marker when there is no value at all. Domain-agnostic: works for
|
|
470
470
|
* every `belongs_to` column (category, supplier, brand, …) without per-addon code.
|
|
471
|
+
*
|
|
472
|
+
* When `stack` is true (column `display: "image_stack"`), the landscape mark
|
|
473
|
+
* sits ON TOP of the label — wide logos fit without cropping and the cell
|
|
474
|
+
* stays readable in dense tables.
|
|
471
475
|
*/
|
|
472
|
-
const RelationCell = ({ col, row, getImageUrl }) => {
|
|
476
|
+
const RelationCell = ({ col, row, getImageUrl, stack = false }) => {
|
|
473
477
|
const display = resolveRelationLabel(col, row);
|
|
474
478
|
if (!display)
|
|
475
479
|
return _jsx(EmptyCell, {});
|
|
476
480
|
const image = resolveRelationImage(col, row);
|
|
477
481
|
const subtitle = resolveRelationSubtitle(col, row);
|
|
482
|
+
if (stack) {
|
|
483
|
+
return (_jsx(ImageStack, { src: image || undefined, label: display, subtitle: subtitle || undefined, getImageUrl: getImageUrl, size: "md" }));
|
|
484
|
+
}
|
|
478
485
|
// FLAT reference cell: no tinted capsule around the pair. A reference is
|
|
479
486
|
// data, not a status — the pill treatment (and its per-label tint) made a
|
|
480
487
|
// products/warehouses listing read as a wall of badges. What identifies the
|
|
@@ -537,12 +544,17 @@ const AvatarCell = ({ name, desc, avatarSrc, getImageUrl }) => (_jsxs("div", { c
|
|
|
537
544
|
* form widget stores — renders the glyph instead of an <img> that would 404
|
|
538
545
|
* into an empty grey box. Exported for tests.
|
|
539
546
|
*/
|
|
540
|
-
export const ImageCell = ({ value, getImageUrl }) => {
|
|
541
|
-
if (!value)
|
|
547
|
+
export const ImageCell = ({ value, getImageUrl, label, stack = false }) => {
|
|
548
|
+
if (!value && !label)
|
|
542
549
|
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
543
|
-
if (isLucideIconName(value)) {
|
|
550
|
+
if (value && isLucideIconName(value)) {
|
|
544
551
|
return (_jsx("div", { className: "h-10 w-10 flex items-center justify-center rounded bg-muted", children: _jsx(DynamicIcon, { name: value, className: "h-5 w-5" }) }));
|
|
545
552
|
}
|
|
553
|
+
if (stack) {
|
|
554
|
+
return (_jsx(ImageStack, { src: value ? String(value) : undefined, label: label, getImageUrl: getImageUrl, size: "md" }));
|
|
555
|
+
}
|
|
556
|
+
if (!value)
|
|
557
|
+
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
546
558
|
return (_jsx("div", { className: "h-10 w-10 relative rounded overflow-hidden bg-muted flex items-center justify-center", children: _jsx("img", { src: getImageUrl(String(value)), alt: "Thumbnail", className: "h-full w-full object-contain", onError: (e) => {
|
|
547
559
|
;
|
|
548
560
|
e.currentTarget.style.display = 'none';
|
|
@@ -649,6 +661,29 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
|
649
661
|
if (renderAs === 'reference') {
|
|
650
662
|
return _jsx(ReferenceCell, { col: col, row: row.original });
|
|
651
663
|
}
|
|
664
|
+
// Landscape stack: wide image ON TOP, label UNDERNEATH.
|
|
665
|
+
// Declared via `display: "image_stack"` on an image column
|
|
666
|
+
// or on a belongs_to FK whose sibling carries a logo/photo
|
|
667
|
+
// (brand marks, product cards). Fits logos that are wider
|
|
668
|
+
// than tall without cropping into a square thumb.
|
|
669
|
+
if (renderAs === 'image_stack') {
|
|
670
|
+
// FK relation (brand_id → brands) OR any column that
|
|
671
|
+
// already resolved a sibling with an image — stack it.
|
|
672
|
+
// Don't require `col.ref` alone: enrichment sometimes
|
|
673
|
+
// leaves type=text while cellStyle carries image_stack.
|
|
674
|
+
const looksRelation = !!col.ref ||
|
|
675
|
+
(typeof col.key === 'string' &&
|
|
676
|
+
col.key.endsWith('_id') &&
|
|
677
|
+
resolveRelationLabel(col, row.original) != null);
|
|
678
|
+
if (looksRelation) {
|
|
679
|
+
return (_jsx(RelationCell, { col: col, row: row.original, getImageUrl: getImageUrl, stack: true }));
|
|
680
|
+
}
|
|
681
|
+
const labelField = styleCfg(col, 'label_field', 'labelField');
|
|
682
|
+
const caption = labelField
|
|
683
|
+
? String(getNestedValue(row.original, labelField) ?? '')
|
|
684
|
+
: undefined;
|
|
685
|
+
return (_jsx(ImageCell, { value: value, getImageUrl: getImageUrl, label: caption || undefined, stack: true }));
|
|
686
|
+
}
|
|
652
687
|
// Resolved FK relation chip. Triggers on an explicit
|
|
653
688
|
// `cellStyle: 'relation'` or on any column carrying a `ref`
|
|
654
689
|
// (a belongs_to FK) that isn't being rendered as an
|
|
@@ -856,6 +891,16 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
|
856
891
|
: null);
|
|
857
892
|
return _jsx(ImageCell, { value: imageValue, getImageUrl: getImageUrl });
|
|
858
893
|
}
|
|
894
|
+
case 'image_stack': {
|
|
895
|
+
// Defensive: normally handled above before the
|
|
896
|
+
// switch; kept so a late `type: image_stack` without
|
|
897
|
+
// cellStyle still stacks.
|
|
898
|
+
const labelField = styleCfg(col, 'label_field', 'labelField');
|
|
899
|
+
const caption = labelField
|
|
900
|
+
? String(getNestedValue(row.original, labelField) ?? '')
|
|
901
|
+
: undefined;
|
|
902
|
+
return (_jsx(ImageCell, { value: value, getImageUrl: getImageUrl, label: caption || undefined, stack: true }));
|
|
903
|
+
}
|
|
859
904
|
default: {
|
|
860
905
|
if (typeof value === 'object' && value !== null) {
|
|
861
906
|
return (_jsx(CollectionCell, { value: value, locale: currentLanguage, t: t, itemFields: col.itemFields ?? col.item_fields, getImageUrl: getImageUrl }));
|
|
@@ -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":"AA4CA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,gDAAgD,CAAA;AAEjF;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EACxB,KAAK,EACL,IAAI,EACJ,IAAS,GACZ,EAAE;IACC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,+BA4BA;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EACvB,MAAM,EACN,IAAS,GACZ,EAAE;IACC,MAAM,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,CAAA;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,sCA4BA;AAMD,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAC1B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,cAAc,GAAG,IAAI,CAAA;IAClC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAA;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;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;CACvB;AAED,wBAAgB,kBAAkB,CAAC,EAC/B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,WAAW,EACX,QAAgB,EAChB,aAAoB,EACpB,kBAA0B,EAC1B,UAAkB,GACrB,EAAE,uBAAuB,+BAiTzB;AAED,eAAe,kBAAkB,CAAA"}
|
|
@@ -23,11 +23,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
23
23
|
// value). A dedicated `?ids=` lookup is a follow-up; create flows — the common
|
|
24
24
|
// case — start empty and never hit this.
|
|
25
25
|
import { useEffect, useRef, useState } from 'react';
|
|
26
|
+
import { useTranslation } from 'react-i18next';
|
|
26
27
|
import { Badge, Button, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Popover, PopoverContent, PopoverTrigger, InitialsAvatar, } from '@asteby/metacore-ui/primitives';
|
|
27
28
|
import { Check, ChevronsUpDown, Loader2, Plus, ScanLine } from 'lucide-react';
|
|
28
29
|
import { resolveColorCss } from '@asteby/metacore-ui/lib';
|
|
29
30
|
import { BarcodeScanner } from './barcode-scanner';
|
|
30
31
|
import { DynamicIcon, isLucideIconName } from './dynamic-icon';
|
|
32
|
+
import { useDebouncedValue } from './use-debounced-value';
|
|
31
33
|
import { useOptionsResolver } from './use-options-resolver';
|
|
32
34
|
import { getDependsOn, getFieldRef, resolveOptionsSource } from './dynamic-form-schema';
|
|
33
35
|
/**
|
|
@@ -84,14 +86,11 @@ export function OptionLead({ option, size = 20, }) {
|
|
|
84
86
|
return null;
|
|
85
87
|
}
|
|
86
88
|
function useDebounced(value, ms) {
|
|
87
|
-
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
const t = setTimeout(() => setDebounced(value), ms);
|
|
90
|
-
return () => clearTimeout(t);
|
|
91
|
-
}, [value, ms]);
|
|
92
|
-
return debounced;
|
|
89
|
+
return useDebouncedValue(value, ms);
|
|
93
90
|
}
|
|
94
91
|
export function DynamicSelectField({ field, value, onChange, seedOption, dependsValue, dependsHint, readonly = false, staticOptions = null, descriptionAsBadge = false, hideCreate = false, }) {
|
|
92
|
+
const { t } = useTranslation();
|
|
93
|
+
const ph = (fallback) => field.placeholder ? t(field.placeholder, { defaultValue: field.placeholder }) : fallback;
|
|
95
94
|
const [open, setOpen] = useState(false);
|
|
96
95
|
const [search, setSearch] = useState('');
|
|
97
96
|
const [scanOpen, setScanOpen] = useState(false);
|
|
@@ -210,7 +209,7 @@ export function DynamicSelectField({ field, value, onChange, seedOption, depends
|
|
|
210
209
|
// the eager fetch is in flight the label falls back to the seed/raw value,
|
|
211
210
|
// then snaps to the name once options arrive.
|
|
212
211
|
if (readonly) {
|
|
213
|
-
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…' :
|
|
212
|
+
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('—')) })] }) }));
|
|
214
213
|
}
|
|
215
214
|
// w-full + min-w-0: as a grid cell child, the row must be allowed to shrink
|
|
216
215
|
// to the cell. Without min-w-0 the combobox+button row sizes to its content
|
|
@@ -219,10 +218,10 @@ export function DynamicSelectField({ field, value, onChange, seedOption, depends
|
|
|
219
218
|
return (_jsxs("div", { className: "flex w-full min-w-0 items-center gap-1.5", children: [_jsxs(Popover, { open: open && !blockedByDependency, onOpenChange: (o) => { if (!blockedByDependency)
|
|
220
219
|
setOpen(o); }, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", role: "combobox", "aria-expanded": open, id: field.key, disabled: blockedByDependency, className: "min-w-0 flex-1 justify-between font-normal", "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
|
|
221
220
|
? (dependsHint || DEFAULT_DEPENDS_HINT)
|
|
222
|
-
: selectedLabel ||
|
|
221
|
+
: 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",
|
|
223
222
|
// Match the trigger width without an arbitrary Tailwind class
|
|
224
223
|
// (those don't always survive a consuming app's Tailwind scan).
|
|
225
|
-
style: { width: 'var(--radix-popover-trigger-width)' }, children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { placeholder:
|
|
224
|
+
style: { width: 'var(--radix-popover-trigger-width)' }, 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(CommandEmpty, { children: useStatic
|
|
226
225
|
? debounced
|
|
227
226
|
? 'Sin resultados'
|
|
228
227
|
: 'Sin opciones'
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AAgC9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AAgC9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AA4FnF,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,6DAA6D;IAC7D,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC/B,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;;;;;;OAaG;IACH,UAAU,CAAC,EAAE,OAAO,GAAG,UAAU,CAAA;IACjC;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,gBAAgB,EAChB,aAAoB,EACpB,UAAU,EACV,UAAU,EACV,aAAkB,EAClB,QAAQ,EACR,UAAU,EACV,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,EACR,QAAQ,EACR,UAAU,EAAE,cAAc,EAC1B,cAAc,EAAE,kBAA0B,GAC7C,EAAE,iBAAiB,+BAkvCnB"}
|