@asteby/metacore-runtime-react 13.8.5 → 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 +39 -0
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +206 -18
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/dynamic-columns.tsx +374 -39
- package/src/types.ts +27 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
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
|
+
|
|
3
42
|
## 13.8.5
|
|
4
43
|
|
|
5
44
|
### Patch Changes
|
|
@@ -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/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
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/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
|