@asteby/metacore-runtime-react 4.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +201 -0
  3. package/README.md +59 -0
  4. package/dist/action-modal-dispatcher.d.ts +4 -0
  5. package/dist/action-modal-dispatcher.d.ts.map +1 -0
  6. package/dist/action-modal-dispatcher.js +123 -0
  7. package/dist/addon-loader.d.ts +27 -0
  8. package/dist/addon-loader.d.ts.map +1 -0
  9. package/dist/addon-loader.js +73 -0
  10. package/dist/api-context.d.ts +40 -0
  11. package/dist/api-context.d.ts.map +1 -0
  12. package/dist/api-context.js +25 -0
  13. package/dist/capability-gate.d.ts +29 -0
  14. package/dist/capability-gate.d.ts.map +1 -0
  15. package/dist/capability-gate.js +43 -0
  16. package/dist/dialogs/_primitives.d.ts +29 -0
  17. package/dist/dialogs/_primitives.d.ts.map +1 -0
  18. package/dist/dialogs/_primitives.js +35 -0
  19. package/dist/dialogs/dynamic-record.d.ts +11 -0
  20. package/dist/dialogs/dynamic-record.d.ts.map +1 -0
  21. package/dist/dialogs/dynamic-record.js +377 -0
  22. package/dist/dialogs/export.d.ts +12 -0
  23. package/dist/dialogs/export.d.ts.map +1 -0
  24. package/dist/dialogs/export.js +146 -0
  25. package/dist/dialogs/import.d.ts +11 -0
  26. package/dist/dialogs/import.d.ts.map +1 -0
  27. package/dist/dialogs/import.js +128 -0
  28. package/dist/dynamic-columns-shim.d.ts +25 -0
  29. package/dist/dynamic-columns-shim.d.ts.map +1 -0
  30. package/dist/dynamic-columns-shim.js +1 -0
  31. package/dist/dynamic-form.d.ts +12 -0
  32. package/dist/dynamic-form.d.ts.map +1 -0
  33. package/dist/dynamic-form.js +51 -0
  34. package/dist/dynamic-icon.d.ts +6 -0
  35. package/dist/dynamic-icon.d.ts.map +1 -0
  36. package/dist/dynamic-icon.js +11 -0
  37. package/dist/dynamic-table.d.ts +22 -0
  38. package/dist/dynamic-table.d.ts.map +1 -0
  39. package/dist/dynamic-table.js +516 -0
  40. package/dist/i18n-provider.d.ts +16 -0
  41. package/dist/i18n-provider.d.ts.map +1 -0
  42. package/dist/i18n-provider.js +16 -0
  43. package/dist/index.d.ts +18 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +21 -0
  46. package/dist/metadata-cache.d.ts +42 -0
  47. package/dist/metadata-cache.d.ts.map +1 -0
  48. package/dist/metadata-cache.js +71 -0
  49. package/dist/navigation-builder.d.ts +34 -0
  50. package/dist/navigation-builder.d.ts.map +1 -0
  51. package/dist/navigation-builder.js +45 -0
  52. package/dist/options-context.d.ts +8 -0
  53. package/dist/options-context.d.ts.map +1 -0
  54. package/dist/options-context.js +5 -0
  55. package/dist/slot.d.ts +32 -0
  56. package/dist/slot.d.ts.map +1 -0
  57. package/dist/slot.js +45 -0
  58. package/dist/types.d.ts +114 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +1 -0
  61. package/package.json +67 -0
  62. package/src/action-modal-dispatcher.tsx +275 -0
  63. package/src/addon-loader.tsx +111 -0
  64. package/src/api-context.tsx +55 -0
  65. package/src/capability-gate.tsx +69 -0
  66. package/src/dialogs/_primitives.tsx +114 -0
  67. package/src/dialogs/dynamic-record.tsx +770 -0
  68. package/src/dialogs/export.tsx +339 -0
  69. package/src/dialogs/import.tsx +404 -0
  70. package/src/dynamic-columns-shim.ts +36 -0
  71. package/src/dynamic-form.tsx +108 -0
  72. package/src/dynamic-icon.tsx +15 -0
  73. package/src/dynamic-table.tsx +766 -0
  74. package/src/i18n-provider.tsx +33 -0
  75. package/src/index.ts +30 -0
  76. package/src/metadata-cache.ts +103 -0
  77. package/src/navigation-builder.tsx +77 -0
  78. package/src/options-context.tsx +11 -0
  79. package/src/slot.tsx +77 -0
  80. package/src/types.ts +112 -0
  81. package/tsconfig.json +16 -0
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // CapabilityGate — conditionally renders its children based on the current
3
+ // user's capability set. Capabilities are sourced from AddonAPI.capabilities
4
+ // or a React context the host provides.
5
+ import { createContext, useContext, useMemo } from 'react';
6
+ const CapabilityContext = createContext({
7
+ has: () => false,
8
+ all: () => false,
9
+ any: () => false,
10
+ });
11
+ function normalize(cs) {
12
+ if (cs instanceof Set)
13
+ return cs;
14
+ if (Array.isArray(cs))
15
+ return new Set(cs);
16
+ return new Set(Object.entries(cs).filter(([, v]) => v).map(([k]) => k));
17
+ }
18
+ export function CapabilityProvider({ capabilities, children }) {
19
+ const value = useMemo(() => {
20
+ const set = normalize(capabilities);
21
+ return {
22
+ has: (c) => set.has(c),
23
+ all: (cs) => cs.every(c => set.has(c)),
24
+ any: (cs) => cs.some(c => set.has(c)),
25
+ };
26
+ }, [capabilities]);
27
+ return _jsx(CapabilityContext.Provider, { value: value, children: children });
28
+ }
29
+ export function useCapabilities() {
30
+ return useContext(CapabilityContext);
31
+ }
32
+ export function CapabilityGate({ require, all, any, fallback = null, invert = false, children }) {
33
+ const ctx = useCapabilities();
34
+ let allowed = true;
35
+ if (require)
36
+ allowed = allowed && ctx.has(require);
37
+ if (all && all.length)
38
+ allowed = allowed && ctx.all(all);
39
+ if (any && any.length)
40
+ allowed = allowed && ctx.any(any);
41
+ const show = invert ? !allowed : allowed;
42
+ return _jsx(_Fragment, { children: show ? children : fallback });
43
+ }
@@ -0,0 +1,29 @@
1
+ import * as React from 'react';
2
+ export interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
3
+ value?: number;
4
+ }
5
+ export declare function Progress({ value, className, ...props }: ProgressProps): import("react/jsx-runtime").JSX.Element;
6
+ export interface RadioGroupProps {
7
+ value: string;
8
+ onValueChange: (value: string) => void;
9
+ className?: string;
10
+ children: React.ReactNode;
11
+ name?: string;
12
+ }
13
+ export declare function RadioGroup({ value, onValueChange, className, children, name }: RadioGroupProps): import("react/jsx-runtime").JSX.Element;
14
+ export interface RadioGroupItemProps {
15
+ value: string;
16
+ id?: string;
17
+ className?: string;
18
+ disabled?: boolean;
19
+ }
20
+ export declare function RadioGroupItem({ value, id, className, disabled }: RadioGroupItemProps): import("react/jsx-runtime").JSX.Element;
21
+ export interface CalendarProps {
22
+ mode?: 'single';
23
+ selected?: Date;
24
+ onSelect?: (date: Date | undefined) => void;
25
+ locale?: any;
26
+ className?: string;
27
+ }
28
+ export declare function Calendar({ selected, onSelect, className }: CalendarProps): import("react/jsx-runtime").JSX.Element;
29
+ //# sourceMappingURL=_primitives.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_primitives.d.ts","sourceRoot":"","sources":["../../src/dialogs/_primitives.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,MAAM,WAAW,aAAc,SAAQ,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC;IACvE,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,wBAAgB,QAAQ,CAAC,EAAE,KAAS,EAAE,SAAc,EAAE,GAAG,KAAK,EAAE,EAAE,aAAa,2CAiB9E;AAWD,MAAM,WAAW,eAAe;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,SAAc,EAAE,QAAQ,EAAE,IAAoB,EAAE,EAAE,eAAe,2CAMnH;AAED,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,SAAc,EAAE,QAAQ,EAAE,EAAE,mBAAmB,2CAgB1F;AAQD,MAAM,WAAW,aAAa;IAC1B,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf,QAAQ,CAAC,EAAE,IAAI,CAAA;IACf,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,KAAK,IAAI,CAAA;IAC3C,MAAM,CAAC,EAAE,GAAG,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,QAAQ,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAc,EAAE,EAAE,aAAa,2CAkB7E"}
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // Minimal local primitives used by the dialogs copied into runtime-react.
3
+ // These are intentionally dependency-free shims — hosts that want richer
4
+ // visuals (date picker calendar, radix progress, radix radio-group) should
5
+ // override by wrapping these components. The UI package does not currently
6
+ // export these three primitives so we keep them inside the runtime.
7
+ import * as React from 'react';
8
+ export function Progress({ value = 0, className = '', ...props }) {
9
+ const pct = Math.max(0, Math.min(100, value));
10
+ return (_jsx("div", { role: "progressbar", "aria-valuenow": pct, "aria-valuemin": 0, "aria-valuemax": 100, className: `relative h-2 w-full overflow-hidden rounded-full bg-secondary ${className}`, ...props, children: _jsx("div", { className: "h-full bg-primary transition-all", style: { width: `${pct}%` } }) }));
11
+ }
12
+ const RadioGroupContext = React.createContext(null);
13
+ export function RadioGroup({ value, onValueChange, className = '', children, name = 'radio-group' }) {
14
+ return (_jsx(RadioGroupContext.Provider, { value: { value, onChange: onValueChange, name }, children: _jsx("div", { role: "radiogroup", className: className, children: children }) }));
15
+ }
16
+ export function RadioGroupItem({ value, id, className = '', disabled }) {
17
+ const ctx = React.useContext(RadioGroupContext);
18
+ if (!ctx)
19
+ throw new Error('RadioGroupItem must be used inside <RadioGroup>');
20
+ const checked = ctx.value === value;
21
+ return (_jsx("input", { type: "radio", id: id, name: ctx.name, value: value, checked: checked, disabled: disabled, onChange: () => ctx.onChange(value), className: `h-4 w-4 border-primary text-primary ${className}` }));
22
+ }
23
+ export function Calendar({ selected, onSelect, className = '' }) {
24
+ const value = selected && !isNaN(selected.getTime())
25
+ ? selected.toISOString().slice(0, 10)
26
+ : '';
27
+ return (_jsx("div", { className: `p-3 ${className}`, children: _jsx("input", { type: "date", value: value, onChange: (e) => {
28
+ const v = e.target.value;
29
+ if (!v) {
30
+ onSelect?.(undefined);
31
+ return;
32
+ }
33
+ onSelect?.(new Date(v + 'T00:00:00'));
34
+ }, className: "w-full rounded-md border px-3 py-2 text-sm" }) }));
35
+ }
@@ -0,0 +1,11 @@
1
+ export interface DynamicRecordDialogProps {
2
+ open: boolean;
3
+ onOpenChange: (open: boolean) => void;
4
+ mode: 'view' | 'edit' | 'create';
5
+ model: string;
6
+ recordId?: string | null;
7
+ endpoint?: string;
8
+ onSaved?: () => void;
9
+ }
10
+ export declare function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, }: DynamicRecordDialogProps): import("react/jsx-runtime").JSX.Element;
11
+ //# sourceMappingURL=dynamic-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAoEA,MAAM,WAAW,wBAAwB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAsDD,wBAAgB,mBAAmB,CAAC,EAChC,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,OAAO,GACV,EAAE,wBAAwB,2CAgM1B"}
@@ -0,0 +1,377 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // DynamicRecordDialog — renders a create/edit/view modal for a model based
3
+ // on metadata fetched from `/metadata/modal/:model`. Ported from the ops
4
+ // starter. Host-owned infra that was referenced by alias (axios client,
5
+ // branch store) now flows through <ApiProvider> from runtime-react.
6
+ import { createContext, useContext, useEffect, useRef, useState } from 'react';
7
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Button, Input, Textarea, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Skeleton, Badge, Popover, PopoverContent, PopoverTrigger, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from '@asteby/metacore-ui/primitives';
8
+ import { cn } from '@asteby/metacore-ui/lib';
9
+ import { Calendar } from './_primitives';
10
+ import { toast } from 'sonner';
11
+ import { format, parseISO } from 'date-fns';
12
+ import { es } from 'date-fns/locale';
13
+ import { ExternalLink, Loader2, CalendarIcon, ChevronDown, Check, Upload, X as XIcon } from 'lucide-react';
14
+ import { useApi } from '../api-context';
15
+ function resolvePath(obj, path) {
16
+ return path.split('.').reduce((acc, part) => acc?.[part], obj);
17
+ }
18
+ function formatDisplayValue(value, field) {
19
+ if (value === null || value === undefined || value === '')
20
+ return '—';
21
+ if (field.type === 'boolean' || typeof value === 'boolean')
22
+ return value ? 'Sí' : 'No';
23
+ if (field.type === 'date') {
24
+ try {
25
+ return new Date(value).toLocaleDateString('es-MX', {
26
+ day: 'numeric', month: 'long', year: 'numeric',
27
+ });
28
+ }
29
+ catch {
30
+ return String(value);
31
+ }
32
+ }
33
+ if (field.type === 'select' && field.options?.length) {
34
+ const match = field.options.find(o => o.value === String(value));
35
+ return match?.label ?? String(value);
36
+ }
37
+ return String(value);
38
+ }
39
+ const MODE_CONFIG = {
40
+ create: {
41
+ getTitle: (meta) => meta.createTitle || meta.title || 'Nuevo registro',
42
+ description: 'Completa los campos para crear un nuevo registro.',
43
+ submitLabel: 'Crear',
44
+ submittingLabel: 'Creando...',
45
+ cancelLabel: 'Cancelar',
46
+ },
47
+ edit: {
48
+ getTitle: (meta) => meta.editTitle || meta.title || 'Editar registro',
49
+ description: 'Modifica los campos y guarda los cambios.',
50
+ submitLabel: 'Guardar cambios',
51
+ submittingLabel: 'Guardando...',
52
+ cancelLabel: 'Cancelar',
53
+ },
54
+ view: {
55
+ getTitle: (meta) => meta.title || 'Ver registro',
56
+ description: 'Información detallada del registro.',
57
+ submitLabel: '',
58
+ submittingLabel: '',
59
+ cancelLabel: 'Cerrar',
60
+ },
61
+ };
62
+ const ModelContext = createContext('');
63
+ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, }) {
64
+ const api = useApi();
65
+ const [modalMeta, setModalMeta] = useState(null);
66
+ const [record, setRecord] = useState(null);
67
+ const [formValues, setFormValues] = useState({});
68
+ const [loading, setLoading] = useState(false);
69
+ const [saving, setSaving] = useState(false);
70
+ const isCreate = mode === 'create';
71
+ const isEditable = mode === 'create' || mode === 'edit';
72
+ const config = MODE_CONFIG[mode];
73
+ useEffect(() => {
74
+ if (!open)
75
+ return;
76
+ if (!isCreate && !recordId)
77
+ return;
78
+ let cancelled = false;
79
+ const load = async () => {
80
+ setLoading(true);
81
+ try {
82
+ const metaRes = await api.get(`/metadata/modal/${model}`);
83
+ if (cancelled)
84
+ return;
85
+ const meta = metaRes.data?.data ?? metaRes.data;
86
+ setModalMeta(meta);
87
+ if (isCreate) {
88
+ const initial = {};
89
+ for (const field of meta.fields ?? []) {
90
+ initial[field.key] = field.defaultValue ?? '';
91
+ }
92
+ setFormValues(initial);
93
+ }
94
+ else {
95
+ const recordEndpoint = endpoint
96
+ ? `${endpoint}/${recordId}`
97
+ : `/data/${model}/${recordId}`;
98
+ const recRes = await api.get(recordEndpoint);
99
+ if (cancelled)
100
+ return;
101
+ const rec = recRes.data?.data ?? recRes.data;
102
+ setRecord(rec);
103
+ const initial = {};
104
+ for (const field of meta.fields ?? []) {
105
+ initial[field.key] = resolvePath(rec, field.key) ?? field.defaultValue ?? '';
106
+ }
107
+ setFormValues(initial);
108
+ }
109
+ }
110
+ catch (err) {
111
+ console.error('[DynamicRecordDialog] load error:', err);
112
+ toast.error('Error al cargar los datos');
113
+ }
114
+ finally {
115
+ if (!cancelled)
116
+ setLoading(false);
117
+ }
118
+ };
119
+ load();
120
+ return () => { cancelled = true; };
121
+ }, [open, recordId, model, endpoint, isCreate]);
122
+ useEffect(() => {
123
+ if (!open) {
124
+ setModalMeta(null);
125
+ setRecord(null);
126
+ setFormValues({});
127
+ }
128
+ }, [open]);
129
+ const handleSubmit = async (e) => {
130
+ e?.preventDefault();
131
+ if (!modalMeta)
132
+ return;
133
+ if (isEditable) {
134
+ for (const field of modalMeta.fields) {
135
+ if (field.required && !formValues[field.key] && formValues[field.key] !== 0 && formValues[field.key] !== false) {
136
+ toast.error(`El campo "${field.label}" es obligatorio`);
137
+ return;
138
+ }
139
+ }
140
+ }
141
+ setSaving(true);
142
+ try {
143
+ let res;
144
+ if (isCreate) {
145
+ const createEndpoint = endpoint || `/data/${model}`;
146
+ res = await api.post(createEndpoint, formValues);
147
+ }
148
+ else {
149
+ const updateEndpoint = endpoint
150
+ ? `${endpoint}/${recordId}`
151
+ : `/data/${model}/${recordId}`;
152
+ res = await api.put(updateEndpoint, formValues);
153
+ }
154
+ if (res.data?.success !== false) {
155
+ toast.success(res.data?.message || (isCreate ? 'Registro creado correctamente' : 'Guardado correctamente'));
156
+ onSaved?.();
157
+ onOpenChange(false);
158
+ }
159
+ else {
160
+ toast.error(res.data?.message || 'Error al guardar');
161
+ }
162
+ }
163
+ catch (err) {
164
+ toast.error(err?.response?.data?.message || 'Error al guardar');
165
+ }
166
+ finally {
167
+ setSaving(false);
168
+ }
169
+ };
170
+ const title = modalMeta ? config.getTitle(modalMeta) : '';
171
+ const visibleFields = modalMeta?.fields?.filter(f => {
172
+ if (f.hidden)
173
+ return false;
174
+ if (isCreate && f.readonly)
175
+ return false;
176
+ return true;
177
+ }) ?? [];
178
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden", 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: _jsxs("form", { id: "dynamic-record-form", onSubmit: handleSubmit, className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4", children: [visibleFields.map(field => {
179
+ const isFullWidth = field.type === 'textarea';
180
+ return (_jsx("div", { className: isFullWidth ? 'sm:col-span-2' : '', children: _jsx(FieldRow, { field: field, record: record, value: formValues[field.key] ?? '', mode: mode, onChange: val => setFormValues((prev) => ({ ...prev, [field.key]: val })) }) }, field.key));
181
+ }), record?.external_url && (_jsx("div", { className: "sm:col-span-2", 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'] }) }))] }) })) : null }), _jsxs(DialogFooter, { className: "p-4 border-t shrink-0", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: saving, children: config.cancelLabel }), isEditable && (_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] }))] })] }) }));
182
+ }
183
+ function LoadingSkeleton() {
184
+ 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))) }));
185
+ }
186
+ function FieldRow({ field, record, value, mode, onChange }) {
187
+ const isReadonly = field.readonly || mode === 'view';
188
+ return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs(Label, { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: [field.label, field.required && mode !== 'view' && (_jsx("span", { className: "text-destructive ml-0.5", children: "*" }))] }), isReadonly ? (_jsx(ViewValue, { field: field, value: value, record: record })) : (_jsx(EditField, { field: field, value: value, onChange: onChange }))] }));
189
+ }
190
+ function ViewValue({ field, value }) {
191
+ if (field.type === 'search' && value) {
192
+ return _jsx(SearchViewValue, { field: field, value: value });
193
+ }
194
+ if (field.type === 'boolean' || typeof value === 'boolean') {
195
+ return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [_jsx(Switch, { checked: !!value, disabled: true }), _jsx("span", { className: "text-sm text-muted-foreground", children: value ? 'Sí' : 'No' })] }));
196
+ }
197
+ if (field.type === 'color') {
198
+ 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: "-" }));
199
+ }
200
+ if (field.type === 'image') {
201
+ return value ? (_jsx("img", { src: value, alt: field.label, className: "h-16 w-16 rounded-lg object-cover border" })) : (_jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "Sin imagen" }));
202
+ }
203
+ if (field.type === 'url' && value) {
204
+ return (_jsx("a", { href: value, target: "_blank", rel: "noreferrer", className: "text-sm text-primary hover:underline truncate", children: value }));
205
+ }
206
+ if (field.type === 'select' && field.options?.length) {
207
+ const match = field.options.find(o => o.value === String(value ?? ''));
208
+ if (match) {
209
+ return _jsx(Badge, { variant: "secondary", className: "w-fit", children: match.label });
210
+ }
211
+ }
212
+ const display = formatDisplayValue(value, field);
213
+ if (field.type === 'textarea') {
214
+ return (_jsx("p", { className: "text-sm whitespace-pre-wrap rounded-md bg-muted/40 p-3 min-h-[60px]", children: display }));
215
+ }
216
+ return _jsx("p", { className: "text-sm py-1", children: display });
217
+ }
218
+ function EditField({ field, value, onChange }) {
219
+ if (field.type === 'boolean') {
220
+ return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [_jsx(Switch, { checked: !!value, onCheckedChange: onChange }), _jsx("span", { className: "text-sm text-muted-foreground", children: value ? 'Sí' : 'No' })] }));
221
+ }
222
+ if (field.type === 'textarea') {
223
+ return (_jsx(Textarea, { value: value ?? '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, rows: 4 }));
224
+ }
225
+ if (field.type === 'image') {
226
+ return _jsx(ImageUploadField, { field: field, value: value, onChange: onChange });
227
+ }
228
+ if (field.type === 'search' && field.searchEndpoint) {
229
+ return _jsx(SearchField, { field: field, value: value, onChange: onChange });
230
+ }
231
+ if (field.type === 'select' && field.searchEndpoint && !field.options?.length) {
232
+ return _jsx(SearchField, { field: { ...field, type: 'search' }, value: value, onChange: onChange });
233
+ }
234
+ if (field.type === 'select' && field.options?.length) {
235
+ return (_jsxs(Select, { value: String(value ?? ''), onValueChange: onChange, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Seleccionar..." }) }), _jsx(SelectContent, { children: field.options.map(opt => (_jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] }));
236
+ }
237
+ if (field.type === 'color') {
238
+ return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { type: "color", value: value || '#6366f1', onChange: (e) => onChange(e.target.value), className: "h-9 w-14 cursor-pointer rounded-md border p-1" }), _jsx(Input, { value: value || '', onChange: (e) => onChange(e.target.value), placeholder: "#6366f1", className: "flex-1 h-9" })] }));
239
+ }
240
+ if (field.type === 'date') {
241
+ const dateValue = value ? (typeof value === 'string' ? parseISO(value) : new Date(value)) : undefined;
242
+ const validDate = dateValue && !isNaN(dateValue.getTime()) ? dateValue : undefined;
243
+ return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", className: cn("w-full justify-start text-left font-normal h-9", !validDate && "text-muted-foreground"), children: [_jsx(CalendarIcon, { className: "mr-2 h-4 w-4" }), validDate
244
+ ? format(validDate, 'PPP', { locale: es })
245
+ : "Seleccionar fecha"] }) }), _jsx(PopoverContent, { className: "w-auto p-0", align: "start", children: _jsx(Calendar, { mode: "single", selected: validDate, onSelect: (date) => onChange(date ? format(date, 'yyyy-MM-dd') : ''), locale: es }) })] }));
246
+ }
247
+ const inputType = field.type === 'number'
248
+ ? 'number'
249
+ : field.type === 'email'
250
+ ? 'email'
251
+ : 'text';
252
+ return (_jsx(Input, { type: inputType, value: value ?? '', onChange: (e) => onChange(field.type === 'number' ? (e.target.value === '' ? '' : Number(e.target.value)) : e.target.value), placeholder: field.placeholder }));
253
+ }
254
+ function ImageUploadField({ field: _field, value, onChange }) {
255
+ const api = useApi();
256
+ const model = useContext(ModelContext);
257
+ const [uploading, setUploading] = useState(false);
258
+ const inputRef = useRef(null);
259
+ async function handleFile(e) {
260
+ const file = e.target.files?.[0];
261
+ if (!file)
262
+ return;
263
+ setUploading(true);
264
+ try {
265
+ const formData = new FormData();
266
+ formData.append('file', file);
267
+ formData.append('folder', model || 'uploads');
268
+ const res = await api.post('/upload', formData);
269
+ const url = res.data?.data?.url || res.data?.url;
270
+ if (url)
271
+ onChange(url);
272
+ }
273
+ catch {
274
+ toast.error('Error al subir imagen');
275
+ }
276
+ finally {
277
+ setUploading(false);
278
+ if (inputRef.current)
279
+ inputRef.current.value = '';
280
+ }
281
+ }
282
+ return (_jsxs("div", { className: "flex items-center gap-3", children: [value ? (_jsxs("div", { className: "relative", children: [_jsx("img", { src: value, alt: "", className: "h-16 w-16 rounded-lg object-cover border" }), _jsx("button", { type: "button", onClick: () => onChange(''), className: "absolute -top-1.5 -right-1.5 size-5 bg-destructive text-white rounded-full flex items-center justify-center hover:bg-destructive/90", children: _jsx(XIcon, { className: "size-3" }) })] })) : (_jsx("button", { type: "button", onClick: () => inputRef.current?.click(), disabled: uploading, className: "h-16 w-16 rounded-lg border-2 border-dashed border-muted-foreground/30 flex flex-col items-center justify-center gap-1 hover:border-primary/50 hover:bg-muted/50 transition-colors disabled:opacity-50", children: uploading ? (_jsx(Loader2, { className: "size-4 animate-spin text-muted-foreground" })) : (_jsx(Upload, { className: "size-4 text-muted-foreground" })) })), _jsx("input", { ref: inputRef, type: "file", accept: "image/*", onChange: handleFile, className: "hidden" }), !value && _jsx("span", { className: "text-xs text-muted-foreground", children: "PNG, JPG, WebP" })] }));
283
+ }
284
+ function extractArray(res) {
285
+ const d = res.data;
286
+ if (Array.isArray(d))
287
+ return d;
288
+ if (d?.data && Array.isArray(d.data))
289
+ return d.data;
290
+ return [];
291
+ }
292
+ const searchCache = new Map();
293
+ function SearchViewValue({ field, value }) {
294
+ const api = useApi();
295
+ const [label, setLabel] = useState(String(value));
296
+ useEffect(() => {
297
+ if (!field.searchEndpoint || !value)
298
+ return;
299
+ const cacheKey = field.searchEndpoint;
300
+ const cached = searchCache.get(cacheKey);
301
+ if (cached) {
302
+ const match = cached.find((item) => item.value === value || item.id === value);
303
+ if (match) {
304
+ setLabel(match.label || match.name || String(value));
305
+ return;
306
+ }
307
+ }
308
+ api.get(field.searchEndpoint, { params: { search: '', limit: 50 } }).then(res => {
309
+ const items = extractArray(res);
310
+ searchCache.set(cacheKey, items);
311
+ const match = items.find((item) => item.value === value || item.id === value);
312
+ if (match)
313
+ setLabel(match.label || match.name || String(value));
314
+ }).catch(() => { });
315
+ }, [value, field.searchEndpoint]);
316
+ return _jsx("p", { className: "text-sm py-1", children: label });
317
+ }
318
+ function SearchField({ field, value, onChange }) {
319
+ const api = useApi();
320
+ const [open, setOpen] = useState(false);
321
+ const [query, setQuery] = useState('');
322
+ const [results, setResults] = useState([]);
323
+ const [loading, setLoading] = useState(false);
324
+ const [selectedLabel, setSelectedLabel] = useState('');
325
+ useEffect(() => {
326
+ if (!value || !field.searchEndpoint)
327
+ return;
328
+ const cached = searchCache.get(field.searchEndpoint);
329
+ if (cached) {
330
+ const match = cached.find((item) => item.value === value || item.id === value);
331
+ if (match) {
332
+ setSelectedLabel(match.label || match.name || '');
333
+ return;
334
+ }
335
+ }
336
+ api.get(field.searchEndpoint, { params: { search: '', limit: 50 } }).then(res => {
337
+ const items = extractArray(res);
338
+ searchCache.set(field.searchEndpoint, items);
339
+ const match = items.find((item) => item.value === value || item.id === value);
340
+ if (match)
341
+ setSelectedLabel(match.label || match.name || '');
342
+ }).catch(() => { });
343
+ }, [value, field.searchEndpoint]);
344
+ useEffect(() => {
345
+ if (!open || !field.searchEndpoint)
346
+ return;
347
+ if (!query) {
348
+ const cached = searchCache.get(field.searchEndpoint);
349
+ if (cached) {
350
+ setResults(cached);
351
+ return;
352
+ }
353
+ }
354
+ setLoading(true);
355
+ const timer = setTimeout(() => {
356
+ api.get(field.searchEndpoint, { params: { search: query, limit: 20 } }).then(res => {
357
+ const items = extractArray(res);
358
+ if (!query)
359
+ searchCache.set(field.searchEndpoint, items);
360
+ setResults(items);
361
+ }).catch(() => setResults([]))
362
+ .finally(() => setLoading(false));
363
+ }, query ? 250 : 0);
364
+ return () => clearTimeout(timer);
365
+ }, [query, open, field.searchEndpoint]);
366
+ 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) => {
367
+ const itemValue = item.value ?? item.id;
368
+ const itemLabel = item.label ?? item.name ?? '';
369
+ const isSelected = value === itemValue;
370
+ return (_jsxs(CommandItem, { value: String(itemValue), onSelect: () => {
371
+ onChange(itemValue);
372
+ setSelectedLabel(itemLabel);
373
+ setOpen(false);
374
+ setQuery('');
375
+ }, children: [isSelected && _jsx(Check, { className: "mr-2 h-3.5 w-3.5 shrink-0 text-primary" }), item.image && (_jsx("img", { src: item.image, className: "h-5 w-5 rounded mr-2 object-cover shrink-0", alt: "" })), _jsxs("div", { className: "flex flex-col min-w-0", children: [_jsx("span", { className: "truncate", children: itemLabel }), item.description && (_jsx("span", { className: "text-[11px] text-muted-foreground truncate", children: item.description }))] })] }, itemValue));
376
+ }) })) })] }) })] }));
377
+ }
@@ -0,0 +1,12 @@
1
+ import type { TableMetadata } from '../types';
2
+ interface ExportDialogProps {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ model: string;
6
+ metadata: TableMetadata;
7
+ currentFilters?: Record<string, any>;
8
+ hasActiveFilters?: boolean;
9
+ }
10
+ export declare function ExportDialog({ open, onOpenChange, model, metadata, currentFilters, hasActiveFilters, }: ExportDialogProps): import("react/jsx-runtime").JSX.Element;
11
+ export {};
12
+ //# sourceMappingURL=export.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../src/dialogs/export.tsx"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAG7C,UAAU,iBAAiB;IACvB,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,aAAa,CAAA;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC7B;AAED,wBAAgB,YAAY,CAAC,EACzB,IAAI,EACJ,YAAY,EACZ,KAAK,EACL,QAAQ,EACR,cAAc,EACd,gBAAgB,GACnB,EAAE,iBAAiB,2CA0SnB"}