@asteby/metacore-runtime-react 13.8.3 → 13.9.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 +51 -0
- package/dist/action-modal-dispatcher.js +1 -1
- package/dist/dialogs/dynamic-record.js +1 -1
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +206 -18
- package/dist/dynamic-form.js +2 -2
- package/dist/dynamic-line-items.js +2 -2
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/action-modal-dispatcher.tsx +1 -1
- package/src/dialogs/dynamic-record.tsx +1 -1
- package/src/dynamic-columns.tsx +374 -39
- package/src/dynamic-form.tsx +2 -2
- package/src/dynamic-line-items.tsx +2 -2
- package/src/types.ts +27 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 13.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 27b37f3: feat(dynamic-columns): declarative pro cell renderers for dynamic tables
|
|
8
|
+
|
|
9
|
+
Adds a library of declarative cell renderers so columns are rendered
|
|
10
|
+
beautifully out of the box instead of raw text. Driven by `col.cellStyle`
|
|
11
|
+
(or `col.type`), resolved via the existing `renderAs = col.cellStyle ?? col.type`.
|
|
12
|
+
Config is read from `col.styleConfig`, tolerating both snake_case (kernel) and
|
|
13
|
+
camelCase (compiled models).
|
|
14
|
+
|
|
15
|
+
New cellStyles:
|
|
16
|
+
- `url` / `link` — clickable link with an `ExternalLink` icon. `styleConfig`:
|
|
17
|
+
`{ label_field?, url_field?, icon?, new_tab? }`. Shows `label_field` text or
|
|
18
|
+
the URL hostname; opens in a new tab for external URLs (or `new_tab`);
|
|
19
|
+
prefixes `https://` when the scheme is missing.
|
|
20
|
+
- `email` — `mailto:` link with a `Mail` icon.
|
|
21
|
+
- `currency` — `Intl.NumberFormat` currency formatting, right-aligned.
|
|
22
|
+
Currency from `styleConfig.currency` (default `USD`), decimals from
|
|
23
|
+
`styleConfig.decimals` (default 2). No hardcoded MXN.
|
|
24
|
+
- `number` — thousands-separated number, right-aligned.
|
|
25
|
+
- `percent` / `progress` — progress bar (shadcn `Progress`) + `NN%` label.
|
|
26
|
+
- `badge` (generic) — pills a plain value even without `options`/`searchEndpoint`.
|
|
27
|
+
- `status` — badge with semantic color by value (active/paid→green,
|
|
28
|
+
pending/draft→amber, cancelled/failed→red, else grey); explicit
|
|
29
|
+
`options` colors win.
|
|
30
|
+
- `tags` — array / comma-separated string → row of small badges.
|
|
31
|
+
- `color` — color swatch + hex code.
|
|
32
|
+
- `code` / `truncate-text` — monospaced, truncated (`styleConfig.max_length`)
|
|
33
|
+
with a hover copy button.
|
|
34
|
+
- `creator` / `user` — avatar + name + subtitle, generalising the existing
|
|
35
|
+
`avatar`/`search` renderer (name from `styleConfig.name_field`, photo from a
|
|
36
|
+
sibling `.avatar`/`.photo` or `base_path + value`, initials fallback).
|
|
37
|
+
|
|
38
|
+
Also improves the existing `boolean` cell (green `Check` / muted `Minus` icon)
|
|
39
|
+
without breaking the `avatar`/`search`, `date`, `phone`, `image`,
|
|
40
|
+
`media-gallery`, `badge+options` and `relation-badge-list` renderers.
|
|
41
|
+
|
|
42
|
+
## 13.8.5
|
|
43
|
+
|
|
44
|
+
### Patch Changes
|
|
45
|
+
|
|
46
|
+
- acb5dcc: fix: every basic `select` field fills its column (`w-full`). shadcn's SelectTrigger defaults to `w-fit`, so enum/option selects shrank to their content instead of aligning with text inputs and FK comboboxes. Covers the declarative action modal (`ActionModalDispatcher`), the record dialog, and line-item rows — the actual renderers behind a model's `placement:create` modal.
|
|
47
|
+
|
|
48
|
+
## 13.8.4
|
|
49
|
+
|
|
50
|
+
### Patch Changes
|
|
51
|
+
|
|
52
|
+
- 7465943: fix(dynamic-form): basic `select` fields fill their column (`w-full`). shadcn's SelectTrigger defaults to `w-fit`, so enum/option selects shrank to their content instead of lining up with text inputs and FK comboboxes. Applies to both the plain `select` renderer and `RefSelect`.
|
|
53
|
+
|
|
3
54
|
## 13.8.3
|
|
4
55
|
|
|
5
56
|
### Patch Changes
|
|
@@ -165,7 +165,7 @@ function renderField(field, value, onChange) {
|
|
|
165
165
|
case 'textarea':
|
|
166
166
|
return _jsx(Textarea, { id: field.key, value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder });
|
|
167
167
|
case 'select':
|
|
168
|
-
return (_jsxs(Select, { value: value || '', onValueChange: onChange, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: field.options?.map((opt) => _jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value)) })] }));
|
|
168
|
+
return (_jsxs(Select, { value: value || '', onValueChange: onChange, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: field.options?.map((opt) => _jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value)) })] }));
|
|
169
169
|
case 'switch':
|
|
170
170
|
return _jsx(Switch, { id: field.key, checked: !!value, onCheckedChange: onChange });
|
|
171
171
|
case 'number':
|
|
@@ -270,7 +270,7 @@ function EditField({ field, value, onChange }) {
|
|
|
270
270
|
return _jsx(SearchField, { field: { ...field, type: 'search' }, value: value, onChange: onChange });
|
|
271
271
|
}
|
|
272
272
|
if (field.type === 'select' && field.options?.length) {
|
|
273
|
-
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))) })] }));
|
|
273
|
+
return (_jsxs(Select, { value: String(value ?? ''), onValueChange: onChange, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: "Seleccionar..." }) }), _jsx(SelectContent, { children: field.options.map(opt => (_jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] }));
|
|
274
274
|
}
|
|
275
275
|
if (field.type === 'color') {
|
|
276
276
|
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" })] }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AA0CA,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAgGD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAuJD;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAokBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
|
package/dist/dynamic-columns.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// Default `getDynamicColumns` factory used by hosts that don't need a custom
|
|
3
3
|
// renderer. Supports every cell type produced by kernel/dynamic metadata:
|
|
4
|
-
// badge (static + endpoint-loaded options), avatar,
|
|
5
|
-
// relation-badge-list, media-gallery, image, plus
|
|
4
|
+
// badge (static + endpoint-loaded options), avatar/search, creator/user,
|
|
5
|
+
// phone, date, boolean, relation-badge-list, media-gallery, image, plus the
|
|
6
|
+
// declarative pro renderers url/link, email, currency, number, percent/
|
|
7
|
+
// progress, status, tags, color, code/truncate-text, and a generic text
|
|
8
|
+
// fallback. The renderer resolves `cellStyle ?? type` for each column.
|
|
6
9
|
//
|
|
7
10
|
// The implementation was previously duplicated across multiple host apps
|
|
8
11
|
// (~550 LOC each, drifting). It now lives here so a single fix propagates
|
|
@@ -16,11 +19,63 @@ import { MoreHorizontal } from 'lucide-react';
|
|
|
16
19
|
import { Avatar, AvatarFallback, AvatarImage, Badge, Button, Checkbox, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@asteby/metacore-ui';
|
|
17
20
|
import { DataTableColumnHeader, FilterableColumnHeader, } from '@asteby/metacore-ui/data-table';
|
|
18
21
|
import { generateBadgeStyles, getInitials } from '@asteby/metacore-ui/lib';
|
|
22
|
+
import { Progress } from './dialogs/_primitives';
|
|
19
23
|
import { OptionsContext } from './options-context';
|
|
20
24
|
import { DynamicIcon } from './dynamic-icon';
|
|
21
25
|
import { isColumnVisibleInTable } from './column-visibility';
|
|
22
26
|
const defaultGetImageUrl = (path) => path;
|
|
23
27
|
const getNestedValue = (obj, path) => path.split('.').reduce((acc, part) => acc && acc[part], obj);
|
|
28
|
+
/**
|
|
29
|
+
* Reads a styleConfig key tolerating both snake_case (emitted by the kernel)
|
|
30
|
+
* and camelCase (sometimes produced by compiled models). Returns the first
|
|
31
|
+
* defined match, e.g. `cfg('label_field', 'labelField')`.
|
|
32
|
+
*/
|
|
33
|
+
const styleCfg = (col, ...keys) => {
|
|
34
|
+
const cfg = col.styleConfig;
|
|
35
|
+
if (!cfg)
|
|
36
|
+
return undefined;
|
|
37
|
+
for (const k of keys) {
|
|
38
|
+
if (cfg[k] !== undefined && cfg[k] !== null)
|
|
39
|
+
return cfg[k];
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
};
|
|
43
|
+
const EmptyCell = () => _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
44
|
+
/** Resolves the active org currency, defaulting to USD when no override. */
|
|
45
|
+
const resolveCurrency = (col) => styleCfg(col, 'currency') || 'USD';
|
|
46
|
+
const formatNumber = (value, opts, locale) => new Intl.NumberFormat(locale || undefined, opts).format(value);
|
|
47
|
+
/**
|
|
48
|
+
* Semantic status → badge color. Used by the `status` cell when no explicit
|
|
49
|
+
* `options` color is declared. Generic, value-driven mapping.
|
|
50
|
+
*/
|
|
51
|
+
const statusColorFor = (value) => {
|
|
52
|
+
const v = value.toLowerCase();
|
|
53
|
+
if (['active', 'enabled', 'paid', 'completed', 'done', 'success', 'approved', 'open']
|
|
54
|
+
.includes(v))
|
|
55
|
+
return '#22c55e';
|
|
56
|
+
if (['pending', 'draft', 'processing', 'in_progress', 'review', 'waiting'].includes(v))
|
|
57
|
+
return '#eab308';
|
|
58
|
+
if (['inactive', 'disabled', 'cancelled', 'canceled', 'failed', 'rejected', 'error', 'closed']
|
|
59
|
+
.includes(v))
|
|
60
|
+
return '#ef4444';
|
|
61
|
+
return '#6b7280';
|
|
62
|
+
};
|
|
63
|
+
/** Copyable monospaced text cell (code/IDs/hashes). */
|
|
64
|
+
const CodeCell = ({ text, maxLength }) => {
|
|
65
|
+
const [copied, setCopied] = React.useState(false);
|
|
66
|
+
const display = maxLength && text.length > maxLength ? `${text.slice(0, maxLength)}…` : text;
|
|
67
|
+
const onCopy = () => {
|
|
68
|
+
try {
|
|
69
|
+
navigator.clipboard?.writeText(text);
|
|
70
|
+
setCopied(true);
|
|
71
|
+
setTimeout(() => setCopied(false), 1200);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
/* clipboard unavailable */
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
return (_jsxs("div", { className: "group flex items-center gap-1.5", children: [_jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground/80", title: text, children: display }), _jsx("button", { type: "button", onClick: onCopy, className: "opacity-0 transition-opacity group-hover:opacity-100 text-muted-foreground hover:text-foreground", "aria-label": "Copiar", title: "Copiar", children: copied ? (_jsx(icons.Check, { className: "h-3.5 w-3.5 text-green-500" })) : (_jsx(icons.Copy, { className: "h-3.5 w-3.5" })) })] }));
|
|
78
|
+
};
|
|
24
79
|
/**
|
|
25
80
|
* State-machine gate for per-row actions.
|
|
26
81
|
*
|
|
@@ -124,6 +179,13 @@ const BadgeWithEndpointOptions = ({ endpoint, value }) => {
|
|
|
124
179
|
return _jsx(OptionBadge, { option: option, fallback: String(value) });
|
|
125
180
|
return _jsx(Badge, { variant: "outline", children: String(value) });
|
|
126
181
|
};
|
|
182
|
+
/**
|
|
183
|
+
* Generic avatar-style cell: round/rounded photo (or initials fallback) +
|
|
184
|
+
* primary name + optional subtitle. Backs the `avatar`/`search` columns as
|
|
185
|
+
* well as the `creator`/`user` cellStyles. Paths are parameterised so the same
|
|
186
|
+
* JSX serves every variant.
|
|
187
|
+
*/
|
|
188
|
+
const AvatarCell = ({ name, desc, avatarSrc, getImageUrl }) => (_jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg ring-1 ring-border/50", children: [_jsx(AvatarImage, { src: avatarSrc ? getImageUrl(avatarSrc) : '', alt: name, className: "object-cover" }), _jsx(AvatarFallback, { className: "text-[10px] font-bold bg-primary/5 text-primary rounded-lg", children: getInitials(name) })] }), _jsxs("div", { className: "flex flex-col min-w-0 overflow-hidden", children: [_jsx("span", { className: "font-medium text-sm truncate leading-none mb-0.5 text-foreground/90", children: name }), desc && (_jsx("span", { className: "text-[11px] text-muted-foreground truncate leading-none", children: desc }))] })] }));
|
|
127
189
|
/**
|
|
128
190
|
* Builds the canonical column factory used by `<DynamicTable>` when the host
|
|
129
191
|
* does not supply its own. Pass `{ getImageUrl, apiBaseUrl }` to wire avatar
|
|
@@ -196,7 +258,26 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
|
196
258
|
if (renderAs === 'relation-badge-list') {
|
|
197
259
|
return renderRelationBadges(value, col);
|
|
198
260
|
}
|
|
199
|
-
|
|
261
|
+
// Generic badge (no options/endpoint) — still pill it.
|
|
262
|
+
if (renderAs === 'badge') {
|
|
263
|
+
if (!value && value !== 0)
|
|
264
|
+
return _jsx(EmptyCell, {});
|
|
265
|
+
return _jsx(Badge, { variant: "outline", children: String(value) });
|
|
266
|
+
}
|
|
267
|
+
// Status — semantic color by value, options color wins.
|
|
268
|
+
if (renderAs === 'status') {
|
|
269
|
+
if (!value && value !== 0)
|
|
270
|
+
return _jsx(EmptyCell, {});
|
|
271
|
+
const sv = String(value);
|
|
272
|
+
const option = col.options?.find((o) => o.value === sv);
|
|
273
|
+
if (option)
|
|
274
|
+
return _jsx(OptionBadge, { option: option, fallback: sv });
|
|
275
|
+
const isDark = typeof document !== 'undefined' &&
|
|
276
|
+
document.documentElement.classList.contains('dark');
|
|
277
|
+
const styles = generateBadgeStyles(statusColorFor(sv), { isDark });
|
|
278
|
+
return (_jsx(Badge, { variant: "outline", className: "border-0 capitalize", style: styles, children: sv }));
|
|
279
|
+
}
|
|
280
|
+
switch (renderAs) {
|
|
200
281
|
case 'date': {
|
|
201
282
|
if (!value)
|
|
202
283
|
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
@@ -212,36 +293,143 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
|
212
293
|
}
|
|
213
294
|
}
|
|
214
295
|
case 'search':
|
|
215
|
-
case 'avatar':
|
|
216
|
-
|
|
296
|
+
case 'avatar':
|
|
297
|
+
case 'creator':
|
|
298
|
+
case 'user': {
|
|
299
|
+
// `creator`/`user` resolve the name from an explicit
|
|
300
|
+
// styleConfig.name_field first, then the legacy
|
|
301
|
+
// tooltip/displayField hints, then the column key.
|
|
302
|
+
const namePath = styleCfg(col, 'name_field', 'nameField') ||
|
|
303
|
+
col.tooltip ||
|
|
304
|
+
col.displayField ||
|
|
305
|
+
col.key;
|
|
217
306
|
const name = getNestedValue(row.original, namePath) || 'N/A';
|
|
218
307
|
const desc = getNestedValue(row.original, col.description || '');
|
|
308
|
+
const basePath = styleCfg(col, 'base_path', 'basePath') ?? col.basePath ?? '';
|
|
219
309
|
let avatarSrc;
|
|
220
310
|
if (col.key.includes('.')) {
|
|
311
|
+
// Look for a sibling `.avatar` or `.photo` field.
|
|
221
312
|
const parentPath = col.key.split('.').slice(0, -1).join('.');
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
if (
|
|
225
|
-
avatarSrc = String(
|
|
226
|
-
}
|
|
227
|
-
else if (value &&
|
|
228
|
-
(String(value).startsWith('http') || String(value).startsWith('https'))) {
|
|
229
|
-
avatarSrc = String(value);
|
|
313
|
+
const sibling = getNestedValue(row.original, `${parentPath}.avatar`) ||
|
|
314
|
+
getNestedValue(row.original, `${parentPath}.photo`);
|
|
315
|
+
if (sibling)
|
|
316
|
+
avatarSrc = String(sibling);
|
|
230
317
|
}
|
|
231
|
-
|
|
232
|
-
|
|
318
|
+
if (!avatarSrc && value) {
|
|
319
|
+
if (String(value).startsWith('http')) {
|
|
320
|
+
avatarSrc = String(value);
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
avatarSrc = `${apiBaseUrl}${basePath}${value}`;
|
|
324
|
+
}
|
|
233
325
|
}
|
|
234
|
-
return (
|
|
326
|
+
return (_jsx(AvatarCell, { name: String(name), desc: desc ? String(desc) : undefined, avatarSrc: avatarSrc, getImageUrl: getImageUrl }));
|
|
235
327
|
}
|
|
236
328
|
case 'relation-badge-list':
|
|
237
329
|
return renderRelationBadges(value, col);
|
|
330
|
+
case 'url':
|
|
331
|
+
case 'link': {
|
|
332
|
+
const labelField = styleCfg(col, 'label_field', 'labelField');
|
|
333
|
+
const urlField = styleCfg(col, 'url_field', 'urlField');
|
|
334
|
+
const rawUrl = urlField
|
|
335
|
+
? getNestedValue(row.original, urlField)
|
|
336
|
+
: value;
|
|
337
|
+
if (!rawUrl)
|
|
338
|
+
return _jsx(EmptyCell, {});
|
|
339
|
+
const urlStr = String(rawUrl);
|
|
340
|
+
const href = /^https?:\/\//i.test(urlStr) ? urlStr : `https://${urlStr}`;
|
|
341
|
+
let label;
|
|
342
|
+
if (labelField) {
|
|
343
|
+
label = String(getNestedValue(row.original, labelField) ?? href);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
try {
|
|
347
|
+
label = new URL(href).hostname;
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
label = urlStr;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const isExternal = !/^https?:\/\/(localhost|127\.)/i.test(href);
|
|
354
|
+
const newTab = styleCfg(col, 'new_tab', 'newTab') === true || isExternal;
|
|
355
|
+
const iconName = styleCfg(col, 'icon') || 'ExternalLink';
|
|
356
|
+
return (_jsxs("a", { href: href, ...(newTab
|
|
357
|
+
? { target: '_blank', rel: 'noopener noreferrer' }
|
|
358
|
+
: {}), className: "inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline", onClick: (e) => e.stopPropagation(), children: [_jsx(DynamicIcon, { name: iconName, className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "truncate max-w-[260px]", children: label })] }));
|
|
359
|
+
}
|
|
360
|
+
case 'email': {
|
|
361
|
+
if (!value)
|
|
362
|
+
return _jsx(EmptyCell, {});
|
|
363
|
+
const email = String(value);
|
|
364
|
+
return (_jsxs("a", { href: `mailto:${email}`, className: "inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline", onClick: (e) => e.stopPropagation(), children: [_jsx(icons.Mail, { className: "h-3.5 w-3.5 shrink-0 opacity-70" }), _jsx("span", { className: "truncate max-w-[260px]", children: email })] }));
|
|
365
|
+
}
|
|
366
|
+
case 'currency': {
|
|
367
|
+
const num = typeof value === 'number' ? value : Number(value);
|
|
368
|
+
if (value === null || value === undefined || isNaN(num))
|
|
369
|
+
return (_jsx("div", { className: "text-right", children: _jsx(EmptyCell, {}) }));
|
|
370
|
+
const decimals = styleCfg(col, 'decimals') ?? 2;
|
|
371
|
+
return (_jsx("span", { className: "block text-right font-medium tabular-nums", children: formatNumber(num, {
|
|
372
|
+
style: 'currency',
|
|
373
|
+
currency: resolveCurrency(col),
|
|
374
|
+
minimumFractionDigits: decimals,
|
|
375
|
+
maximumFractionDigits: decimals,
|
|
376
|
+
}, currentLanguage) }));
|
|
377
|
+
}
|
|
378
|
+
case 'number': {
|
|
379
|
+
const num = typeof value === 'number' ? value : Number(value);
|
|
380
|
+
if (value === null || value === undefined || isNaN(num))
|
|
381
|
+
return (_jsx("div", { className: "text-right", children: _jsx(EmptyCell, {}) }));
|
|
382
|
+
const decimals = styleCfg(col, 'decimals');
|
|
383
|
+
return (_jsx("span", { className: "block text-right font-medium tabular-nums", children: formatNumber(num, decimals !== undefined
|
|
384
|
+
? {
|
|
385
|
+
minimumFractionDigits: decimals,
|
|
386
|
+
maximumFractionDigits: decimals,
|
|
387
|
+
}
|
|
388
|
+
: {}, currentLanguage) }));
|
|
389
|
+
}
|
|
390
|
+
case 'percent':
|
|
391
|
+
case 'progress': {
|
|
392
|
+
const num = typeof value === 'number' ? value : Number(value);
|
|
393
|
+
if (value === null || value === undefined || isNaN(num))
|
|
394
|
+
return _jsx(EmptyCell, {});
|
|
395
|
+
const pct = Math.max(0, Math.min(100, num));
|
|
396
|
+
return (_jsxs("div", { className: "flex items-center gap-2 min-w-[120px]", children: [_jsx(Progress, { value: pct, className: "flex-1" }), _jsxs("span", { className: "text-xs font-medium tabular-nums text-muted-foreground w-9 text-right", children: [Math.round(pct), "%"] })] }));
|
|
397
|
+
}
|
|
398
|
+
case 'tags': {
|
|
399
|
+
const list = Array.isArray(value)
|
|
400
|
+
? value.map(String)
|
|
401
|
+
: value
|
|
402
|
+
? String(value)
|
|
403
|
+
.split(',')
|
|
404
|
+
.map((s) => s.trim())
|
|
405
|
+
.filter(Boolean)
|
|
406
|
+
: [];
|
|
407
|
+
if (list.length === 0)
|
|
408
|
+
return _jsx(EmptyCell, {});
|
|
409
|
+
return (_jsx("div", { className: "flex flex-wrap gap-1", children: list.map((tag, i) => (_jsx(Badge, { variant: "secondary", className: "px-1.5 py-0 text-[10px]", children: tag }, `${col.key}-${i}`))) }));
|
|
410
|
+
}
|
|
411
|
+
case 'color': {
|
|
412
|
+
if (!value)
|
|
413
|
+
return _jsx(EmptyCell, {});
|
|
414
|
+
const hex = String(value);
|
|
415
|
+
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-4 w-4 rounded border border-border/60 shrink-0", style: { background: hex } }), _jsx("code", { className: "font-mono text-xs text-muted-foreground", children: hex })] }));
|
|
416
|
+
}
|
|
417
|
+
case 'code':
|
|
418
|
+
case 'truncate-text': {
|
|
419
|
+
if (value === null || value === undefined || value === '')
|
|
420
|
+
return _jsx(EmptyCell, {});
|
|
421
|
+
const maxLength = styleCfg(col, 'max_length', 'maxLength');
|
|
422
|
+
return _jsx(CodeCell, { text: String(value), maxLength: maxLength });
|
|
423
|
+
}
|
|
238
424
|
case 'phone': {
|
|
239
425
|
if (!value)
|
|
240
426
|
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
241
427
|
return _jsx("span", { className: "font-medium text-sm", children: String(value) });
|
|
242
428
|
}
|
|
243
|
-
case 'boolean':
|
|
244
|
-
|
|
429
|
+
case 'boolean': {
|
|
430
|
+
const showText = styleCfg(col, 'show_text', 'showText') !== false;
|
|
431
|
+
return (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [value ? (_jsx(icons.Check, { className: "h-4 w-4 text-green-500" })) : (_jsx(icons.Minus, { className: "h-4 w-4 text-muted-foreground" })), showText && (_jsx("span", { className: "text-sm text-muted-foreground", children: value ? 'Sí' : 'No' }))] }));
|
|
432
|
+
}
|
|
245
433
|
case 'media-gallery': {
|
|
246
434
|
if (!value || (Array.isArray(value) && value.length === 0)) {
|
|
247
435
|
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
package/dist/dynamic-form.js
CHANGED
|
@@ -116,7 +116,7 @@ function FieldRenderer({ field, value, onChange }) {
|
|
|
116
116
|
case 'color':
|
|
117
117
|
return _jsx(Input, { id: field.key, type: "color", value: value || '#000000', onChange: (e) => onChange(e.target.value) });
|
|
118
118
|
case 'select':
|
|
119
|
-
return (_jsxs(Select, { value: value || '', onValueChange: onChange, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: field.options?.map((opt) => _jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value)) })] }));
|
|
119
|
+
return (_jsxs(Select, { value: value || '', onValueChange: onChange, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: field.options?.map((opt) => _jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value)) })] }));
|
|
120
120
|
case 'switch':
|
|
121
121
|
return _jsx(Switch, { id: field.key, checked: !!value, onCheckedChange: onChange });
|
|
122
122
|
case 'number':
|
|
@@ -133,5 +133,5 @@ function RefSelect({ field, value, onChange }) {
|
|
|
133
133
|
fieldKey: 'id',
|
|
134
134
|
ref: field.ref,
|
|
135
135
|
});
|
|
136
|
-
return (_jsxs(Select, { value: value || '', onValueChange: onChange, disabled: loading, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: loading ? 'Cargando…' : (field.placeholder || 'Seleccionar...') }) }), _jsx(SelectContent, { children: options.map((opt) => (_jsx(SelectItem, { value: String(opt.id), children: opt.label }, String(opt.id)))) })] }));
|
|
136
|
+
return (_jsxs(Select, { value: value || '', onValueChange: onChange, disabled: loading, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: loading ? 'Cargando…' : (field.placeholder || 'Seleccionar...') }) }), _jsx(SelectContent, { children: options.map((opt) => (_jsx(SelectItem, { value: String(opt.id), children: opt.label }, String(opt.id)))) })] }));
|
|
137
137
|
}
|
|
@@ -97,7 +97,7 @@ function CellRenderer({ field, value, onChange, disabled }) {
|
|
|
97
97
|
case 'color':
|
|
98
98
|
return (_jsx(Input, { type: "color", value: value || '#000000', onChange: (e) => onChange(e.target.value), disabled: disabled }));
|
|
99
99
|
case 'select':
|
|
100
|
-
return (_jsxs(Select, { value: value || '', onValueChange: onChange, disabled: disabled, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: field.options?.map((opt) => (_jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] }));
|
|
100
|
+
return (_jsxs(Select, { value: value || '', onValueChange: onChange, disabled: disabled, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: field.options?.map((opt) => (_jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] }));
|
|
101
101
|
case 'switch':
|
|
102
102
|
return _jsx(Switch, { checked: !!value, onCheckedChange: onChange, disabled: disabled });
|
|
103
103
|
case 'number':
|
|
@@ -114,5 +114,5 @@ function RefCell({ field, value, onChange, disabled }) {
|
|
|
114
114
|
fieldKey: 'id',
|
|
115
115
|
ref: field.ref,
|
|
116
116
|
});
|
|
117
|
-
return (_jsxs(Select, { value: value || '', onValueChange: onChange, disabled: disabled || loading, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: loading ? 'Cargando…' : field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: options.map((opt) => (_jsx(SelectItem, { value: String(opt.id), children: opt.label }, String(opt.id)))) })] }));
|
|
117
|
+
return (_jsxs(Select, { value: value || '', onValueChange: onChange, disabled: disabled || loading, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: loading ? 'Cargando…' : field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: options.map((opt) => (_jsx(SelectItem, { value: String(opt.id), children: opt.label }, String(opt.id)))) })] }));
|
|
118
118
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -75,7 +75,7 @@ export type ColumnVisibility = 'all' | 'table' | 'modal' | 'list' | (string & {}
|
|
|
75
75
|
export interface ColumnDefinition {
|
|
76
76
|
key: string;
|
|
77
77
|
label: string;
|
|
78
|
-
type: 'text' | 'number' | 'date' | 'select' | 'search' | 'relation-badge-list' | 'avatar' | 'boolean' | 'phone' | 'media-gallery' | 'image';
|
|
78
|
+
type: 'text' | 'number' | 'date' | 'select' | 'search' | 'relation-badge-list' | 'avatar' | 'boolean' | 'phone' | 'media-gallery' | 'image' | 'url' | 'link' | 'email' | 'currency' | 'percent' | 'progress' | 'badge' | 'status' | 'tags' | 'color' | 'code' | 'truncate-text' | 'creator' | 'user';
|
|
79
79
|
sortable: boolean;
|
|
80
80
|
filterable: boolean;
|
|
81
81
|
hidden?: boolean;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,cAAc,EAAE,MAAM,CAAA;IACtB,iBAAiB,EAAE,MAAM,CAAA;IACzB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,UAAU,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,YAAY,EAAE,CAAA;CAC7B;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IACzB,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAA;IACZ,kEAAkE;IAClE,IAAI,EAAE,aAAa,GAAG,cAAc,CAAA;IACpC;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAA;IACf,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAA;IACnB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,MAAM,CAAA;IACnE,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACrF,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAEjF,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -334,7 +334,7 @@ function renderField(
|
|
|
334
334
|
case 'select':
|
|
335
335
|
return (
|
|
336
336
|
<Select value={value || ''} onValueChange={onChange}>
|
|
337
|
-
<SelectTrigger><SelectValue placeholder={field.placeholder || 'Seleccionar...'} /></SelectTrigger>
|
|
337
|
+
<SelectTrigger className="w-full"><SelectValue placeholder={field.placeholder || 'Seleccionar...'} /></SelectTrigger>
|
|
338
338
|
<SelectContent>
|
|
339
339
|
{field.options?.map((opt) => <SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>)}
|
|
340
340
|
</SelectContent>
|
|
@@ -581,7 +581,7 @@ function EditField({ field, value, onChange }: {
|
|
|
581
581
|
if (field.type === 'select' && field.options?.length) {
|
|
582
582
|
return (
|
|
583
583
|
<Select value={String(value ?? '')} onValueChange={onChange}>
|
|
584
|
-
<SelectTrigger>
|
|
584
|
+
<SelectTrigger className="w-full">
|
|
585
585
|
<SelectValue placeholder="Seleccionar..." />
|
|
586
586
|
</SelectTrigger>
|
|
587
587
|
<SelectContent>
|
package/src/dynamic-columns.tsx
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Default `getDynamicColumns` factory used by hosts that don't need a custom
|
|
2
2
|
// renderer. Supports every cell type produced by kernel/dynamic metadata:
|
|
3
|
-
// badge (static + endpoint-loaded options), avatar,
|
|
4
|
-
// relation-badge-list, media-gallery, image, plus
|
|
3
|
+
// badge (static + endpoint-loaded options), avatar/search, creator/user,
|
|
4
|
+
// phone, date, boolean, relation-badge-list, media-gallery, image, plus the
|
|
5
|
+
// declarative pro renderers url/link, email, currency, number, percent/
|
|
6
|
+
// progress, status, tags, color, code/truncate-text, and a generic text
|
|
7
|
+
// fallback. The renderer resolves `cellStyle ?? type` for each column.
|
|
5
8
|
//
|
|
6
9
|
// The implementation was previously duplicated across multiple host apps
|
|
7
10
|
// (~550 LOC each, drifting). It now lives here so a single fix propagates
|
|
@@ -32,6 +35,7 @@ import {
|
|
|
32
35
|
type ColumnFilterMeta,
|
|
33
36
|
} from '@asteby/metacore-ui/data-table'
|
|
34
37
|
import { generateBadgeStyles, getInitials } from '@asteby/metacore-ui/lib'
|
|
38
|
+
import { Progress } from './dialogs/_primitives'
|
|
35
39
|
import { OptionsContext } from './options-context'
|
|
36
40
|
import { DynamicIcon } from './dynamic-icon'
|
|
37
41
|
import type { TableMetadata, ColumnDefinition } from './types'
|
|
@@ -62,6 +66,95 @@ const defaultGetImageUrl = (path: string) => path
|
|
|
62
66
|
const getNestedValue = (obj: any, path: string) =>
|
|
63
67
|
path.split('.').reduce((acc, part) => acc && acc[part], obj)
|
|
64
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Reads a styleConfig key tolerating both snake_case (emitted by the kernel)
|
|
71
|
+
* and camelCase (sometimes produced by compiled models). Returns the first
|
|
72
|
+
* defined match, e.g. `cfg('label_field', 'labelField')`.
|
|
73
|
+
*/
|
|
74
|
+
const styleCfg = (
|
|
75
|
+
col: ColumnDefinition,
|
|
76
|
+
...keys: string[]
|
|
77
|
+
): any => {
|
|
78
|
+
const cfg = col.styleConfig
|
|
79
|
+
if (!cfg) return undefined
|
|
80
|
+
for (const k of keys) {
|
|
81
|
+
if (cfg[k] !== undefined && cfg[k] !== null) return cfg[k]
|
|
82
|
+
}
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const EmptyCell = () => <span className="text-muted-foreground">-</span>
|
|
87
|
+
|
|
88
|
+
/** Resolves the active org currency, defaulting to USD when no override. */
|
|
89
|
+
const resolveCurrency = (col: ColumnDefinition): string =>
|
|
90
|
+
styleCfg(col, 'currency') || 'USD'
|
|
91
|
+
|
|
92
|
+
const formatNumber = (
|
|
93
|
+
value: number,
|
|
94
|
+
opts: Intl.NumberFormatOptions,
|
|
95
|
+
locale?: string,
|
|
96
|
+
) => new Intl.NumberFormat(locale || undefined, opts).format(value)
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Semantic status → badge color. Used by the `status` cell when no explicit
|
|
100
|
+
* `options` color is declared. Generic, value-driven mapping.
|
|
101
|
+
*/
|
|
102
|
+
const statusColorFor = (value: string): string => {
|
|
103
|
+
const v = value.toLowerCase()
|
|
104
|
+
if (
|
|
105
|
+
['active', 'enabled', 'paid', 'completed', 'done', 'success', 'approved', 'open']
|
|
106
|
+
.includes(v)
|
|
107
|
+
)
|
|
108
|
+
return '#22c55e'
|
|
109
|
+
if (['pending', 'draft', 'processing', 'in_progress', 'review', 'waiting'].includes(v))
|
|
110
|
+
return '#eab308'
|
|
111
|
+
if (
|
|
112
|
+
['inactive', 'disabled', 'cancelled', 'canceled', 'failed', 'rejected', 'error', 'closed']
|
|
113
|
+
.includes(v)
|
|
114
|
+
)
|
|
115
|
+
return '#ef4444'
|
|
116
|
+
return '#6b7280'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Copyable monospaced text cell (code/IDs/hashes). */
|
|
120
|
+
const CodeCell: React.FC<{ text: string; maxLength?: number }> = ({ text, maxLength }) => {
|
|
121
|
+
const [copied, setCopied] = React.useState(false)
|
|
122
|
+
const display =
|
|
123
|
+
maxLength && text.length > maxLength ? `${text.slice(0, maxLength)}…` : text
|
|
124
|
+
const onCopy = () => {
|
|
125
|
+
try {
|
|
126
|
+
navigator.clipboard?.writeText(text)
|
|
127
|
+
setCopied(true)
|
|
128
|
+
setTimeout(() => setCopied(false), 1200)
|
|
129
|
+
} catch {
|
|
130
|
+
/* clipboard unavailable */
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return (
|
|
134
|
+
<div className="group flex items-center gap-1.5">
|
|
135
|
+
<code
|
|
136
|
+
className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground/80"
|
|
137
|
+
title={text}
|
|
138
|
+
>
|
|
139
|
+
{display}
|
|
140
|
+
</code>
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
onClick={onCopy}
|
|
144
|
+
className="opacity-0 transition-opacity group-hover:opacity-100 text-muted-foreground hover:text-foreground"
|
|
145
|
+
aria-label="Copiar"
|
|
146
|
+
title="Copiar"
|
|
147
|
+
>
|
|
148
|
+
{copied ? (
|
|
149
|
+
<icons.Check className="h-3.5 w-3.5 text-green-500" />
|
|
150
|
+
) : (
|
|
151
|
+
<icons.Copy className="h-3.5 w-3.5" />
|
|
152
|
+
)}
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
65
158
|
/**
|
|
66
159
|
* State-machine gate for per-row actions.
|
|
67
160
|
*
|
|
@@ -195,6 +288,42 @@ const BadgeWithEndpointOptions: React.FC<{ endpoint: string; value: any }> = ({
|
|
|
195
288
|
return <Badge variant="outline">{String(value)}</Badge>
|
|
196
289
|
}
|
|
197
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Generic avatar-style cell: round/rounded photo (or initials fallback) +
|
|
293
|
+
* primary name + optional subtitle. Backs the `avatar`/`search` columns as
|
|
294
|
+
* well as the `creator`/`user` cellStyles. Paths are parameterised so the same
|
|
295
|
+
* JSX serves every variant.
|
|
296
|
+
*/
|
|
297
|
+
const AvatarCell: React.FC<{
|
|
298
|
+
name: string
|
|
299
|
+
desc?: string
|
|
300
|
+
avatarSrc?: string
|
|
301
|
+
getImageUrl: (path: string) => string
|
|
302
|
+
}> = ({ name, desc, avatarSrc, getImageUrl }) => (
|
|
303
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
304
|
+
<Avatar className="h-8 w-8 rounded-lg ring-1 ring-border/50">
|
|
305
|
+
<AvatarImage
|
|
306
|
+
src={avatarSrc ? getImageUrl(avatarSrc) : ''}
|
|
307
|
+
alt={name}
|
|
308
|
+
className="object-cover"
|
|
309
|
+
/>
|
|
310
|
+
<AvatarFallback className="text-[10px] font-bold bg-primary/5 text-primary rounded-lg">
|
|
311
|
+
{getInitials(name)}
|
|
312
|
+
</AvatarFallback>
|
|
313
|
+
</Avatar>
|
|
314
|
+
<div className="flex flex-col min-w-0 overflow-hidden">
|
|
315
|
+
<span className="font-medium text-sm truncate leading-none mb-0.5 text-foreground/90">
|
|
316
|
+
{name}
|
|
317
|
+
</span>
|
|
318
|
+
{desc && (
|
|
319
|
+
<span className="text-[11px] text-muted-foreground truncate leading-none">
|
|
320
|
+
{desc}
|
|
321
|
+
</span>
|
|
322
|
+
)}
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
)
|
|
326
|
+
|
|
198
327
|
/**
|
|
199
328
|
* Builds the canonical column factory used by `<DynamicTable>` when the host
|
|
200
329
|
* does not supply its own. Pass `{ getImageUrl, apiBaseUrl }` to wire avatar
|
|
@@ -301,7 +430,30 @@ export function makeDefaultGetDynamicColumns(
|
|
|
301
430
|
return renderRelationBadges(value, col)
|
|
302
431
|
}
|
|
303
432
|
|
|
304
|
-
|
|
433
|
+
// Generic badge (no options/endpoint) — still pill it.
|
|
434
|
+
if (renderAs === 'badge') {
|
|
435
|
+
if (!value && value !== 0) return <EmptyCell />
|
|
436
|
+
return <Badge variant="outline">{String(value)}</Badge>
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Status — semantic color by value, options color wins.
|
|
440
|
+
if (renderAs === 'status') {
|
|
441
|
+
if (!value && value !== 0) return <EmptyCell />
|
|
442
|
+
const sv = String(value)
|
|
443
|
+
const option = col.options?.find((o) => o.value === sv)
|
|
444
|
+
if (option) return <OptionBadge option={option} fallback={sv} />
|
|
445
|
+
const isDark =
|
|
446
|
+
typeof document !== 'undefined' &&
|
|
447
|
+
document.documentElement.classList.contains('dark')
|
|
448
|
+
const styles = generateBadgeStyles(statusColorFor(sv), { isDark })
|
|
449
|
+
return (
|
|
450
|
+
<Badge variant="outline" className="border-0 capitalize" style={styles}>
|
|
451
|
+
{sv}
|
|
452
|
+
</Badge>
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
switch (renderAs) {
|
|
305
457
|
case 'date': {
|
|
306
458
|
if (!value) return <span className="text-muted-foreground">-</span>
|
|
307
459
|
try {
|
|
@@ -323,62 +475,245 @@ export function makeDefaultGetDynamicColumns(
|
|
|
323
475
|
}
|
|
324
476
|
|
|
325
477
|
case 'search':
|
|
326
|
-
case 'avatar':
|
|
327
|
-
|
|
478
|
+
case 'avatar':
|
|
479
|
+
case 'creator':
|
|
480
|
+
case 'user': {
|
|
481
|
+
// `creator`/`user` resolve the name from an explicit
|
|
482
|
+
// styleConfig.name_field first, then the legacy
|
|
483
|
+
// tooltip/displayField hints, then the column key.
|
|
484
|
+
const namePath =
|
|
485
|
+
styleCfg(col, 'name_field', 'nameField') ||
|
|
486
|
+
col.tooltip ||
|
|
487
|
+
col.displayField ||
|
|
488
|
+
col.key
|
|
328
489
|
const name = getNestedValue(row.original, namePath) || 'N/A'
|
|
329
490
|
const desc = getNestedValue(row.original, col.description || '')
|
|
330
491
|
|
|
492
|
+
const basePath = styleCfg(col, 'base_path', 'basePath') ?? col.basePath ?? ''
|
|
331
493
|
let avatarSrc: string | undefined
|
|
332
494
|
if (col.key.includes('.')) {
|
|
495
|
+
// Look for a sibling `.avatar` or `.photo` field.
|
|
333
496
|
const parentPath = col.key.split('.').slice(0, -1).join('.')
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
497
|
+
const sibling =
|
|
498
|
+
getNestedValue(row.original, `${parentPath}.avatar`) ||
|
|
499
|
+
getNestedValue(row.original, `${parentPath}.photo`)
|
|
500
|
+
if (sibling) avatarSrc = String(sibling)
|
|
501
|
+
}
|
|
502
|
+
if (!avatarSrc && value) {
|
|
503
|
+
if (String(value).startsWith('http')) {
|
|
504
|
+
avatarSrc = String(value)
|
|
505
|
+
} else {
|
|
506
|
+
avatarSrc = `${apiBaseUrl}${basePath}${value}`
|
|
507
|
+
}
|
|
344
508
|
}
|
|
345
509
|
|
|
346
510
|
return (
|
|
347
|
-
<
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
/>
|
|
354
|
-
<AvatarFallback className="text-[10px] font-bold bg-primary/5 text-primary rounded-lg">
|
|
355
|
-
{getInitials(String(name))}
|
|
356
|
-
</AvatarFallback>
|
|
357
|
-
</Avatar>
|
|
358
|
-
<div className="flex flex-col min-w-0 overflow-hidden">
|
|
359
|
-
<span className="font-medium text-sm truncate leading-none mb-0.5 text-foreground/90">
|
|
360
|
-
{String(name)}
|
|
361
|
-
</span>
|
|
362
|
-
{desc && (
|
|
363
|
-
<span className="text-[11px] text-muted-foreground truncate leading-none">
|
|
364
|
-
{String(desc)}
|
|
365
|
-
</span>
|
|
366
|
-
)}
|
|
367
|
-
</div>
|
|
368
|
-
</div>
|
|
511
|
+
<AvatarCell
|
|
512
|
+
name={String(name)}
|
|
513
|
+
desc={desc ? String(desc) : undefined}
|
|
514
|
+
avatarSrc={avatarSrc}
|
|
515
|
+
getImageUrl={getImageUrl}
|
|
516
|
+
/>
|
|
369
517
|
)
|
|
370
518
|
}
|
|
371
519
|
|
|
372
520
|
case 'relation-badge-list':
|
|
373
521
|
return renderRelationBadges(value, col)
|
|
374
522
|
|
|
523
|
+
case 'url':
|
|
524
|
+
case 'link': {
|
|
525
|
+
const labelField = styleCfg(col, 'label_field', 'labelField')
|
|
526
|
+
const urlField = styleCfg(col, 'url_field', 'urlField')
|
|
527
|
+
const rawUrl = urlField
|
|
528
|
+
? getNestedValue(row.original, urlField)
|
|
529
|
+
: value
|
|
530
|
+
if (!rawUrl) return <EmptyCell />
|
|
531
|
+
const urlStr = String(rawUrl)
|
|
532
|
+
const href = /^https?:\/\//i.test(urlStr) ? urlStr : `https://${urlStr}`
|
|
533
|
+
let label: string
|
|
534
|
+
if (labelField) {
|
|
535
|
+
label = String(getNestedValue(row.original, labelField) ?? href)
|
|
536
|
+
} else {
|
|
537
|
+
try {
|
|
538
|
+
label = new URL(href).hostname
|
|
539
|
+
} catch {
|
|
540
|
+
label = urlStr
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const isExternal = !/^https?:\/\/(localhost|127\.)/i.test(href)
|
|
544
|
+
const newTab =
|
|
545
|
+
styleCfg(col, 'new_tab', 'newTab') === true || isExternal
|
|
546
|
+
const iconName = styleCfg(col, 'icon') || 'ExternalLink'
|
|
547
|
+
return (
|
|
548
|
+
<a
|
|
549
|
+
href={href}
|
|
550
|
+
{...(newTab
|
|
551
|
+
? { target: '_blank', rel: 'noopener noreferrer' }
|
|
552
|
+
: {})}
|
|
553
|
+
className="inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline"
|
|
554
|
+
onClick={(e) => e.stopPropagation()}
|
|
555
|
+
>
|
|
556
|
+
<DynamicIcon name={iconName} className="h-3.5 w-3.5 shrink-0" />
|
|
557
|
+
<span className="truncate max-w-[260px]">{label}</span>
|
|
558
|
+
</a>
|
|
559
|
+
)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
case 'email': {
|
|
563
|
+
if (!value) return <EmptyCell />
|
|
564
|
+
const email = String(value)
|
|
565
|
+
return (
|
|
566
|
+
<a
|
|
567
|
+
href={`mailto:${email}`}
|
|
568
|
+
className="inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline"
|
|
569
|
+
onClick={(e) => e.stopPropagation()}
|
|
570
|
+
>
|
|
571
|
+
<icons.Mail className="h-3.5 w-3.5 shrink-0 opacity-70" />
|
|
572
|
+
<span className="truncate max-w-[260px]">{email}</span>
|
|
573
|
+
</a>
|
|
574
|
+
)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
case 'currency': {
|
|
578
|
+
const num =
|
|
579
|
+
typeof value === 'number' ? value : Number(value)
|
|
580
|
+
if (value === null || value === undefined || isNaN(num))
|
|
581
|
+
return (
|
|
582
|
+
<div className="text-right">
|
|
583
|
+
<EmptyCell />
|
|
584
|
+
</div>
|
|
585
|
+
)
|
|
586
|
+
const decimals = styleCfg(col, 'decimals') ?? 2
|
|
587
|
+
return (
|
|
588
|
+
<span className="block text-right font-medium tabular-nums">
|
|
589
|
+
{formatNumber(
|
|
590
|
+
num,
|
|
591
|
+
{
|
|
592
|
+
style: 'currency',
|
|
593
|
+
currency: resolveCurrency(col),
|
|
594
|
+
minimumFractionDigits: decimals,
|
|
595
|
+
maximumFractionDigits: decimals,
|
|
596
|
+
},
|
|
597
|
+
currentLanguage,
|
|
598
|
+
)}
|
|
599
|
+
</span>
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
case 'number': {
|
|
604
|
+
const num =
|
|
605
|
+
typeof value === 'number' ? value : Number(value)
|
|
606
|
+
if (value === null || value === undefined || isNaN(num))
|
|
607
|
+
return (
|
|
608
|
+
<div className="text-right">
|
|
609
|
+
<EmptyCell />
|
|
610
|
+
</div>
|
|
611
|
+
)
|
|
612
|
+
const decimals = styleCfg(col, 'decimals')
|
|
613
|
+
return (
|
|
614
|
+
<span className="block text-right font-medium tabular-nums">
|
|
615
|
+
{formatNumber(
|
|
616
|
+
num,
|
|
617
|
+
decimals !== undefined
|
|
618
|
+
? {
|
|
619
|
+
minimumFractionDigits: decimals,
|
|
620
|
+
maximumFractionDigits: decimals,
|
|
621
|
+
}
|
|
622
|
+
: {},
|
|
623
|
+
currentLanguage,
|
|
624
|
+
)}
|
|
625
|
+
</span>
|
|
626
|
+
)
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
case 'percent':
|
|
630
|
+
case 'progress': {
|
|
631
|
+
const num =
|
|
632
|
+
typeof value === 'number' ? value : Number(value)
|
|
633
|
+
if (value === null || value === undefined || isNaN(num))
|
|
634
|
+
return <EmptyCell />
|
|
635
|
+
const pct = Math.max(0, Math.min(100, num))
|
|
636
|
+
return (
|
|
637
|
+
<div className="flex items-center gap-2 min-w-[120px]">
|
|
638
|
+
<Progress value={pct} className="flex-1" />
|
|
639
|
+
<span className="text-xs font-medium tabular-nums text-muted-foreground w-9 text-right">
|
|
640
|
+
{Math.round(pct)}%
|
|
641
|
+
</span>
|
|
642
|
+
</div>
|
|
643
|
+
)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
case 'tags': {
|
|
647
|
+
const list: string[] = Array.isArray(value)
|
|
648
|
+
? value.map(String)
|
|
649
|
+
: value
|
|
650
|
+
? String(value)
|
|
651
|
+
.split(',')
|
|
652
|
+
.map((s) => s.trim())
|
|
653
|
+
.filter(Boolean)
|
|
654
|
+
: []
|
|
655
|
+
if (list.length === 0) return <EmptyCell />
|
|
656
|
+
return (
|
|
657
|
+
<div className="flex flex-wrap gap-1">
|
|
658
|
+
{list.map((tag, i) => (
|
|
659
|
+
<Badge
|
|
660
|
+
key={`${col.key}-${i}`}
|
|
661
|
+
variant="secondary"
|
|
662
|
+
className="px-1.5 py-0 text-[10px]"
|
|
663
|
+
>
|
|
664
|
+
{tag}
|
|
665
|
+
</Badge>
|
|
666
|
+
))}
|
|
667
|
+
</div>
|
|
668
|
+
)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
case 'color': {
|
|
672
|
+
if (!value) return <EmptyCell />
|
|
673
|
+
const hex = String(value)
|
|
674
|
+
return (
|
|
675
|
+
<div className="flex items-center gap-2">
|
|
676
|
+
<span
|
|
677
|
+
className="h-4 w-4 rounded border border-border/60 shrink-0"
|
|
678
|
+
style={{ background: hex }}
|
|
679
|
+
/>
|
|
680
|
+
<code className="font-mono text-xs text-muted-foreground">
|
|
681
|
+
{hex}
|
|
682
|
+
</code>
|
|
683
|
+
</div>
|
|
684
|
+
)
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
case 'code':
|
|
688
|
+
case 'truncate-text': {
|
|
689
|
+
if (value === null || value === undefined || value === '')
|
|
690
|
+
return <EmptyCell />
|
|
691
|
+
const maxLength = styleCfg(col, 'max_length', 'maxLength')
|
|
692
|
+
return <CodeCell text={String(value)} maxLength={maxLength} />
|
|
693
|
+
}
|
|
694
|
+
|
|
375
695
|
case 'phone': {
|
|
376
696
|
if (!value) return <span className="text-muted-foreground">-</span>
|
|
377
697
|
return <span className="font-medium text-sm">{String(value)}</span>
|
|
378
698
|
}
|
|
379
699
|
|
|
380
|
-
case 'boolean':
|
|
381
|
-
|
|
700
|
+
case 'boolean': {
|
|
701
|
+
const showText = styleCfg(col, 'show_text', 'showText') !== false
|
|
702
|
+
return (
|
|
703
|
+
<span className="inline-flex items-center gap-1.5">
|
|
704
|
+
{value ? (
|
|
705
|
+
<icons.Check className="h-4 w-4 text-green-500" />
|
|
706
|
+
) : (
|
|
707
|
+
<icons.Minus className="h-4 w-4 text-muted-foreground" />
|
|
708
|
+
)}
|
|
709
|
+
{showText && (
|
|
710
|
+
<span className="text-sm text-muted-foreground">
|
|
711
|
+
{value ? 'Sí' : 'No'}
|
|
712
|
+
</span>
|
|
713
|
+
)}
|
|
714
|
+
</span>
|
|
715
|
+
)
|
|
716
|
+
}
|
|
382
717
|
|
|
383
718
|
case 'media-gallery': {
|
|
384
719
|
if (!value || (Array.isArray(value) && value.length === 0)) {
|
package/src/dynamic-form.tsx
CHANGED
|
@@ -195,7 +195,7 @@ function FieldRenderer({ field, value, onChange }: FieldRendererProps) {
|
|
|
195
195
|
case 'select':
|
|
196
196
|
return (
|
|
197
197
|
<Select value={value || ''} onValueChange={onChange}>
|
|
198
|
-
<SelectTrigger><SelectValue placeholder={field.placeholder || 'Seleccionar...'} /></SelectTrigger>
|
|
198
|
+
<SelectTrigger className="w-full"><SelectValue placeholder={field.placeholder || 'Seleccionar...'} /></SelectTrigger>
|
|
199
199
|
<SelectContent>
|
|
200
200
|
{field.options?.map((opt) => <SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>)}
|
|
201
201
|
</SelectContent>
|
|
@@ -220,7 +220,7 @@ function RefSelect({ field, value, onChange }: FieldRendererProps) {
|
|
|
220
220
|
})
|
|
221
221
|
return (
|
|
222
222
|
<Select value={value || ''} onValueChange={onChange} disabled={loading}>
|
|
223
|
-
<SelectTrigger>
|
|
223
|
+
<SelectTrigger className="w-full">
|
|
224
224
|
<SelectValue placeholder={loading ? 'Cargando…' : (field.placeholder || 'Seleccionar...')} />
|
|
225
225
|
</SelectTrigger>
|
|
226
226
|
<SelectContent>
|
|
@@ -274,7 +274,7 @@ function CellRenderer({ field, value, onChange, disabled }: CellRendererProps) {
|
|
|
274
274
|
case 'select':
|
|
275
275
|
return (
|
|
276
276
|
<Select value={value || ''} onValueChange={onChange} disabled={disabled}>
|
|
277
|
-
<SelectTrigger>
|
|
277
|
+
<SelectTrigger className="w-full">
|
|
278
278
|
<SelectValue placeholder={field.placeholder || 'Seleccionar...'} />
|
|
279
279
|
</SelectTrigger>
|
|
280
280
|
<SelectContent>
|
|
@@ -328,7 +328,7 @@ function RefCell({ field, value, onChange, disabled }: CellRendererProps) {
|
|
|
328
328
|
})
|
|
329
329
|
return (
|
|
330
330
|
<Select value={value || ''} onValueChange={onChange} disabled={disabled || loading}>
|
|
331
|
-
<SelectTrigger>
|
|
331
|
+
<SelectTrigger className="w-full">
|
|
332
332
|
<SelectValue placeholder={loading ? 'Cargando…' : field.placeholder || 'Seleccionar...'} />
|
|
333
333
|
</SelectTrigger>
|
|
334
334
|
<SelectContent>
|
package/src/types.ts
CHANGED
|
@@ -77,7 +77,33 @@ export type ColumnVisibility = 'all' | 'table' | 'modal' | 'list' | (string & {}
|
|
|
77
77
|
export interface ColumnDefinition {
|
|
78
78
|
key: string
|
|
79
79
|
label: string
|
|
80
|
-
type:
|
|
80
|
+
type:
|
|
81
|
+
| 'text'
|
|
82
|
+
| 'number'
|
|
83
|
+
| 'date'
|
|
84
|
+
| 'select'
|
|
85
|
+
| 'search'
|
|
86
|
+
| 'relation-badge-list'
|
|
87
|
+
| 'avatar'
|
|
88
|
+
| 'boolean'
|
|
89
|
+
| 'phone'
|
|
90
|
+
| 'media-gallery'
|
|
91
|
+
| 'image'
|
|
92
|
+
// Declarative pro cell renderers (resolved via `cellStyle ?? type`).
|
|
93
|
+
| 'url'
|
|
94
|
+
| 'link'
|
|
95
|
+
| 'email'
|
|
96
|
+
| 'currency'
|
|
97
|
+
| 'percent'
|
|
98
|
+
| 'progress'
|
|
99
|
+
| 'badge'
|
|
100
|
+
| 'status'
|
|
101
|
+
| 'tags'
|
|
102
|
+
| 'color'
|
|
103
|
+
| 'code'
|
|
104
|
+
| 'truncate-text'
|
|
105
|
+
| 'creator'
|
|
106
|
+
| 'user'
|
|
81
107
|
sortable: boolean
|
|
82
108
|
filterable: boolean
|
|
83
109
|
hidden?: boolean
|