@asteby/metacore-runtime-react 5.0.0 → 6.1.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 +7 -0
- package/dist/dynamic-columns.d.ts +29 -0
- package/dist/dynamic-columns.d.ts.map +1 -0
- package/dist/dynamic-columns.js +308 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +4 -4
- package/src/dynamic-columns.tsx +527 -0
- package/src/index.ts +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { GetDynamicColumns } from './dynamic-columns-shim';
|
|
2
|
+
/** Host-supplied helpers consumed by avatar/image cell renderers. */
|
|
3
|
+
export interface DynamicColumnsHelpers {
|
|
4
|
+
/**
|
|
5
|
+
* Resolves a relative or absolute media path into a renderable URL. Hosts
|
|
6
|
+
* typically prepend their CDN/storage base. If omitted, paths are passed
|
|
7
|
+
* through verbatim.
|
|
8
|
+
*/
|
|
9
|
+
getImageUrl?: (path: string) => string;
|
|
10
|
+
/**
|
|
11
|
+
* API origin used to build avatar URLs when the row carries a bare filename
|
|
12
|
+
* instead of an absolute URL or sibling `.avatar` field. Usually
|
|
13
|
+
* `import.meta.env.VITE_API_URL.replace('/api', '')`.
|
|
14
|
+
*/
|
|
15
|
+
apiBaseUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Builds the canonical column factory used by `<DynamicTable>` when the host
|
|
19
|
+
* does not supply its own. Pass `{ getImageUrl, apiBaseUrl }` to wire avatar
|
|
20
|
+
* URL resolution.
|
|
21
|
+
*/
|
|
22
|
+
export declare function makeDefaultGetDynamicColumns(helpers?: DynamicColumnsHelpers): GetDynamicColumns;
|
|
23
|
+
/**
|
|
24
|
+
* Eager-built variant — equivalent to `makeDefaultGetDynamicColumns()`. Use
|
|
25
|
+
* this when the host has no helpers to inject and a stable function reference
|
|
26
|
+
* suffices.
|
|
27
|
+
*/
|
|
28
|
+
export declare const defaultGetDynamicColumns: GetDynamicColumns;
|
|
29
|
+
//# sourceMappingURL=dynamic-columns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAqCA,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;AAwHD;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CA+UnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Default `getDynamicColumns` factory used by hosts that don't need a custom
|
|
3
|
+
// renderer. Supports every cell type produced by kernel/dynamic metadata:
|
|
4
|
+
// badge (static + endpoint-loaded options), avatar, phone, date, boolean,
|
|
5
|
+
// relation-badge-list, media-gallery, image, plus a generic text fallback.
|
|
6
|
+
//
|
|
7
|
+
// The implementation was previously duplicated across `link` and `ops`
|
|
8
|
+
// (~550 LOC each, drifting). It now lives here so a single fix propagates
|
|
9
|
+
// to every host. Hosts inject app-specific URL helpers via the `helpers`
|
|
10
|
+
// argument so the SDK stays free of environment-bound code.
|
|
11
|
+
import * as React from 'react';
|
|
12
|
+
import { format } from 'date-fns';
|
|
13
|
+
import { es, enUS } from 'date-fns/locale';
|
|
14
|
+
import * as icons from 'lucide-react';
|
|
15
|
+
import { MoreHorizontal } from 'lucide-react';
|
|
16
|
+
import { Avatar, AvatarFallback, AvatarImage, Badge, Button, Checkbox, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@asteby/metacore-ui';
|
|
17
|
+
import { DataTableColumnHeader, FilterableColumnHeader, } from '@asteby/metacore-ui/data-table';
|
|
18
|
+
import { generateBadgeStyles } from '@asteby/metacore-ui/lib';
|
|
19
|
+
import { OptionsContext } from './options-context';
|
|
20
|
+
import { DynamicIcon } from './dynamic-icon';
|
|
21
|
+
const defaultGetImageUrl = (path) => path;
|
|
22
|
+
const getNestedValue = (obj, path) => path.split('.').reduce((acc, part) => acc && acc[part], obj);
|
|
23
|
+
const lowerFirst = (value) => {
|
|
24
|
+
if (!value)
|
|
25
|
+
return value;
|
|
26
|
+
return value.charAt(0).toLowerCase() + value.slice(1);
|
|
27
|
+
};
|
|
28
|
+
const getPathVariants = (path) => {
|
|
29
|
+
if (!path)
|
|
30
|
+
return [];
|
|
31
|
+
const normalized = path
|
|
32
|
+
.split('.')
|
|
33
|
+
.map((segment) => lowerFirst(segment) || segment)
|
|
34
|
+
.join('.');
|
|
35
|
+
return Array.from(new Set([path, normalized])).filter(Boolean);
|
|
36
|
+
};
|
|
37
|
+
const getValueFromPathVariants = (obj, path) => {
|
|
38
|
+
if (!path)
|
|
39
|
+
return undefined;
|
|
40
|
+
for (const candidate of getPathVariants(path)) {
|
|
41
|
+
const value = getNestedValue(obj, candidate);
|
|
42
|
+
if (value !== undefined && value !== null)
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
};
|
|
47
|
+
const useIsDarkTheme = () => {
|
|
48
|
+
const [isDark, setIsDark] = React.useState(() => typeof document !== 'undefined' &&
|
|
49
|
+
document.documentElement.classList.contains('dark'));
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
if (typeof document === 'undefined')
|
|
52
|
+
return;
|
|
53
|
+
const sync = () => setIsDark(document.documentElement.classList.contains('dark'));
|
|
54
|
+
sync();
|
|
55
|
+
const observer = new MutationObserver(sync);
|
|
56
|
+
observer.observe(document.documentElement, {
|
|
57
|
+
attributes: true,
|
|
58
|
+
attributeFilter: ['class'],
|
|
59
|
+
});
|
|
60
|
+
return () => observer.disconnect();
|
|
61
|
+
}, []);
|
|
62
|
+
return isDark;
|
|
63
|
+
};
|
|
64
|
+
const renderRelationBadges = (items, col) => {
|
|
65
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
66
|
+
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
67
|
+
}
|
|
68
|
+
return (_jsx("div", { className: "flex flex-wrap gap-1", children: items.map((item, idx) => {
|
|
69
|
+
const relationTarget = col.relationPath
|
|
70
|
+
? getValueFromPathVariants(item, col.relationPath) ?? item
|
|
71
|
+
: item;
|
|
72
|
+
const displaySource = relationTarget ?? item;
|
|
73
|
+
let displayValue = col.displayField !== undefined && col.displayField !== null
|
|
74
|
+
? getValueFromPathVariants(displaySource, col.displayField)
|
|
75
|
+
: displaySource;
|
|
76
|
+
if (displayValue === undefined || displayValue === null) {
|
|
77
|
+
displayValue = displaySource;
|
|
78
|
+
}
|
|
79
|
+
const label = displayValue !== undefined && displayValue !== null
|
|
80
|
+
? String(displayValue)
|
|
81
|
+
: '-';
|
|
82
|
+
let iconValue;
|
|
83
|
+
if (col.iconField) {
|
|
84
|
+
const rawIcon = getValueFromPathVariants(displaySource, col.iconField);
|
|
85
|
+
if (rawIcon !== undefined && rawIcon !== null) {
|
|
86
|
+
iconValue = String(rawIcon);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return (_jsxs(Badge, { variant: "outline", className: "flex items-center gap-1", children: [iconValue && (_jsx(DynamicIcon, { name: iconValue, className: "h-3 w-3" })), _jsx("span", { children: label })] }, `${col.key}-${idx}`));
|
|
90
|
+
}) }));
|
|
91
|
+
};
|
|
92
|
+
const OptionBadge = ({ option }) => {
|
|
93
|
+
const isDark = useIsDarkTheme();
|
|
94
|
+
const colorStyles = option.color ? generateBadgeStyles(option.color, { isDark }) : {};
|
|
95
|
+
return (_jsxs(Badge, { variant: "outline", className: "flex items-center gap-1 border-0", style: colorStyles, children: [option.icon && _jsx(DynamicIcon, { name: option.icon, className: "h-3.5 w-3.5" }), _jsx("span", { children: option.label })] }));
|
|
96
|
+
};
|
|
97
|
+
const BadgeWithEndpointOptions = ({ endpoint, value }) => {
|
|
98
|
+
const { optionsMap } = React.useContext(OptionsContext);
|
|
99
|
+
const options = optionsMap.get(endpoint) || [];
|
|
100
|
+
const option = options.find((opt) => opt.value === value);
|
|
101
|
+
if (option)
|
|
102
|
+
return _jsx(OptionBadge, { option: option, fallback: String(value) });
|
|
103
|
+
return _jsx(Badge, { variant: "outline", children: String(value) });
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Builds the canonical column factory used by `<DynamicTable>` when the host
|
|
107
|
+
* does not supply its own. Pass `{ getImageUrl, apiBaseUrl }` to wire avatar
|
|
108
|
+
* URL resolution.
|
|
109
|
+
*/
|
|
110
|
+
export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
111
|
+
const getImageUrl = helpers.getImageUrl ?? defaultGetImageUrl;
|
|
112
|
+
const apiBaseUrl = helpers.apiBaseUrl ?? '';
|
|
113
|
+
return function defaultGetDynamicColumns(metadata, onAction, t, currentLanguage, filterConfigs) {
|
|
114
|
+
const dateLocale = currentLanguage === 'en' ? enUS : es;
|
|
115
|
+
const columns = [
|
|
116
|
+
{
|
|
117
|
+
id: 'select',
|
|
118
|
+
header: ({ table }) => (_jsx(Checkbox, { checked: table.getIsAllPageRowsSelected() ||
|
|
119
|
+
(table.getIsSomePageRowsSelected() && 'indeterminate'), onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value), "aria-label": "Select all", className: "translate-y-[2px]" })),
|
|
120
|
+
cell: ({ row }) => (_jsx(Checkbox, { checked: row.getIsSelected(), onCheckedChange: (value) => row.toggleSelected(!!value), "aria-label": "Select row", className: "translate-y-[2px]" })),
|
|
121
|
+
enableSorting: false,
|
|
122
|
+
enableHiding: false,
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
metadata.columns.forEach((col) => {
|
|
126
|
+
if (col.hidden)
|
|
127
|
+
return;
|
|
128
|
+
const translatedLabel = col.label;
|
|
129
|
+
const filterConfig = filterConfigs?.get(col.key);
|
|
130
|
+
const columnMeta = {
|
|
131
|
+
label: translatedLabel,
|
|
132
|
+
};
|
|
133
|
+
if (filterConfig) {
|
|
134
|
+
const fm = {
|
|
135
|
+
filterable: true,
|
|
136
|
+
filterType: filterConfig.filterType,
|
|
137
|
+
filterKey: filterConfig.filterKey,
|
|
138
|
+
filterOptions: filterConfig.options,
|
|
139
|
+
filterLoading: filterConfig.loading,
|
|
140
|
+
filterSearchEndpoint: filterConfig.searchEndpoint,
|
|
141
|
+
selectedValues: filterConfig.selectedValues,
|
|
142
|
+
onFilterChange: filterConfig.onFilterChange,
|
|
143
|
+
};
|
|
144
|
+
Object.assign(columnMeta, fm);
|
|
145
|
+
}
|
|
146
|
+
columns.push({
|
|
147
|
+
accessorKey: col.key,
|
|
148
|
+
id: col.key,
|
|
149
|
+
meta: columnMeta,
|
|
150
|
+
header: ({ column }) => filterConfig ? (_jsx(FilterableColumnHeader, { column: column, title: translatedLabel })) : (_jsx(DataTableColumnHeader, { column: column, title: translatedLabel })),
|
|
151
|
+
cell: ({ row }) => {
|
|
152
|
+
const value = getNestedValue(row.original, col.key);
|
|
153
|
+
// Endpoint-loaded badge options (preloaded into OptionsContext)
|
|
154
|
+
if (col.cellStyle === 'badge' && col.useOptions && col.searchEndpoint) {
|
|
155
|
+
if (!value)
|
|
156
|
+
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
157
|
+
return _jsx(BadgeWithEndpointOptions, { endpoint: col.searchEndpoint, value: value });
|
|
158
|
+
}
|
|
159
|
+
// Static badge options — map value → label/icon/color
|
|
160
|
+
if (col.cellStyle === 'badge' && col.options && col.options.length > 0) {
|
|
161
|
+
if (!value && value !== 0)
|
|
162
|
+
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
163
|
+
const option = col.options.find((o) => o.value === String(value));
|
|
164
|
+
if (option)
|
|
165
|
+
return _jsx(OptionBadge, { option: option, fallback: String(value) });
|
|
166
|
+
return _jsx(Badge, { variant: "outline", children: String(value) });
|
|
167
|
+
}
|
|
168
|
+
if (col.cellStyle === 'relation-badge-list') {
|
|
169
|
+
return renderRelationBadges(value, col);
|
|
170
|
+
}
|
|
171
|
+
switch (col.type) {
|
|
172
|
+
case 'date': {
|
|
173
|
+
if (!value)
|
|
174
|
+
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
175
|
+
try {
|
|
176
|
+
const date = new Date(value);
|
|
177
|
+
if (isNaN(date.getTime()) || date.getFullYear() <= 1) {
|
|
178
|
+
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
179
|
+
}
|
|
180
|
+
return (_jsxs("div", { className: "flex items-center gap-1.5 text-muted-foreground", children: [_jsx(icons.Calendar, { className: "h-3.5 w-3.5 opacity-70" }), _jsx("span", { className: "text-sm font-medium", children: format(date, 'PPP', { locale: dateLocale }) })] }));
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return _jsx("span", { children: String(value) });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
case 'search':
|
|
187
|
+
case 'avatar': {
|
|
188
|
+
const namePath = col.tooltip || col.key;
|
|
189
|
+
const name = getNestedValue(row.original, namePath) || 'N/A';
|
|
190
|
+
const desc = getNestedValue(row.original, col.description || '');
|
|
191
|
+
let avatarSrc;
|
|
192
|
+
if (col.key.includes('.')) {
|
|
193
|
+
const parentPath = col.key.split('.').slice(0, -1).join('.');
|
|
194
|
+
const avatarPath = `${parentPath}.avatar`;
|
|
195
|
+
const possibleAvatar = getNestedValue(row.original, avatarPath);
|
|
196
|
+
if (possibleAvatar)
|
|
197
|
+
avatarSrc = String(possibleAvatar);
|
|
198
|
+
}
|
|
199
|
+
else if (value &&
|
|
200
|
+
(String(value).startsWith('http') || String(value).startsWith('https'))) {
|
|
201
|
+
avatarSrc = String(value);
|
|
202
|
+
}
|
|
203
|
+
else if (value) {
|
|
204
|
+
avatarSrc = `${apiBaseUrl}${col.basePath || ''}${value}`;
|
|
205
|
+
}
|
|
206
|
+
return (_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: getImageUrl(avatarSrc || ''), alt: String(name), className: "object-cover" }), _jsx(AvatarFallback, { className: "text-[10px] font-bold bg-primary/5 text-primary rounded-lg", children: String(name)
|
|
207
|
+
.split(' ')
|
|
208
|
+
.map((n) => n[0])
|
|
209
|
+
.slice(0, 2)
|
|
210
|
+
.join('')
|
|
211
|
+
.toUpperCase() })] }), _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: String(name) }), desc && (_jsx("span", { className: "text-[11px] text-muted-foreground truncate leading-none", children: String(desc) }))] })] }));
|
|
212
|
+
}
|
|
213
|
+
case 'relation-badge-list':
|
|
214
|
+
return renderRelationBadges(value, col);
|
|
215
|
+
case 'phone': {
|
|
216
|
+
if (!value)
|
|
217
|
+
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
218
|
+
return _jsx("span", { className: "font-medium text-sm", children: String(value) });
|
|
219
|
+
}
|
|
220
|
+
case 'boolean':
|
|
221
|
+
return value ? _jsx(Badge, { children: "S\u00ED" }) : _jsx(Badge, { variant: "secondary", children: "No" });
|
|
222
|
+
case 'media-gallery': {
|
|
223
|
+
if (!value || (Array.isArray(value) && value.length === 0)) {
|
|
224
|
+
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
225
|
+
}
|
|
226
|
+
const mediaItems = Array.isArray(value) ? value : [];
|
|
227
|
+
const visibleItems = mediaItems.slice(0, 3);
|
|
228
|
+
const remaining = mediaItems.length - 3;
|
|
229
|
+
return (_jsxs("div", { className: "flex -space-x-2 overflow-hidden", children: [visibleItems.map((item, i) => {
|
|
230
|
+
const src = item.url;
|
|
231
|
+
if (item.type === 'image') {
|
|
232
|
+
return (_jsxs(Avatar, { className: "inline-block h-8 w-8 rounded-full ring-2 ring-background", children: [_jsx(AvatarImage, { src: src, className: "object-cover" }), _jsx(AvatarFallback, { children: item.type?.[0] })] }, i));
|
|
233
|
+
}
|
|
234
|
+
return (_jsx("div", { className: "inline-flex h-8 w-8 items-center justify-center rounded-full bg-muted ring-2 ring-background", children: _jsx(DynamicIcon, { name: item.type === 'video'
|
|
235
|
+
? 'Video'
|
|
236
|
+
: item.type === 'audio'
|
|
237
|
+
? 'AudioLines'
|
|
238
|
+
: 'FileText', className: "h-4 w-4" }) }, i));
|
|
239
|
+
}), remaining > 0 && (_jsxs("div", { className: "flex h-8 w-8 items-center justify-center rounded-full bg-muted text-xs font-medium ring-2 ring-background", children: ["+", remaining] }))] }));
|
|
240
|
+
}
|
|
241
|
+
case 'image': {
|
|
242
|
+
const imageValue = value ||
|
|
243
|
+
(Array.isArray(row.original.media)
|
|
244
|
+
? row.original.media.find((m) => m.type === 'image')?.url
|
|
245
|
+
: null);
|
|
246
|
+
if (!imageValue)
|
|
247
|
+
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
248
|
+
return (_jsx("div", { className: "h-10 w-10 relative rounded overflow-hidden bg-muted flex items-center justify-center", children: _jsx("img", { src: getImageUrl(String(imageValue)), alt: "Thumbnail", className: "h-full w-full object-contain", onError: (e) => {
|
|
249
|
+
;
|
|
250
|
+
e.currentTarget.style.display = 'none';
|
|
251
|
+
} }) }));
|
|
252
|
+
}
|
|
253
|
+
default: {
|
|
254
|
+
if (typeof value === 'object' && value !== null) {
|
|
255
|
+
return (_jsx("span", { className: "text-muted-foreground text-xs", children: JSON.stringify(value) }));
|
|
256
|
+
}
|
|
257
|
+
if (col.key === 'description' ||
|
|
258
|
+
col.key === 'features' ||
|
|
259
|
+
col.key.includes('description')) {
|
|
260
|
+
return (_jsx("div", { className: "max-w-[350px]", title: String(value), children: _jsx("span", { className: "truncate font-medium block", children: value !== null && value !== undefined ? String(value) : '-' }) }));
|
|
261
|
+
}
|
|
262
|
+
return (_jsx("span", { className: "truncate font-medium", children: value !== null && value !== undefined ? String(value) : '-' }));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
enableSorting: col.sortable,
|
|
267
|
+
enableHiding: true,
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
if (metadata.hasActions && metadata.actions.length > 0) {
|
|
271
|
+
columns.push({
|
|
272
|
+
id: 'actions',
|
|
273
|
+
header: () => _jsx("div", { className: "text-right", children: t ? t('common.actions') : 'Acciones' }),
|
|
274
|
+
size: 80,
|
|
275
|
+
maxSize: 80,
|
|
276
|
+
meta: {},
|
|
277
|
+
cell: ({ row }) => (_jsx("div", { className: "flex items-center justify-end", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", className: "h-8 w-8 p-0", children: [_jsx("span", { className: "sr-only", children: "Abrir men\u00FA" }), _jsx(MoreHorizontal, { className: "h-4 w-4" })] }) }), _jsx(DropdownMenuContent, { align: "end", children: metadata.actions
|
|
278
|
+
.filter((action) => {
|
|
279
|
+
if (!action.condition)
|
|
280
|
+
return true;
|
|
281
|
+
const { field, operator, value } = action.condition;
|
|
282
|
+
const rowValue = String(row.original[field] ?? '');
|
|
283
|
+
const values = Array.isArray(value) ? value : [value];
|
|
284
|
+
switch (operator) {
|
|
285
|
+
case 'eq':
|
|
286
|
+
return rowValue === values[0];
|
|
287
|
+
case 'neq':
|
|
288
|
+
return rowValue !== values[0];
|
|
289
|
+
case 'in':
|
|
290
|
+
return values.includes(rowValue);
|
|
291
|
+
case 'not_in':
|
|
292
|
+
return !values.includes(rowValue);
|
|
293
|
+
default:
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
.map((action) => (_jsxs(DropdownMenuItem, { onClick: () => onAction && onAction(action.key, row.original), children: [_jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), action.label] }, action.key))) })] }) })),
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return columns;
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Eager-built variant — equivalent to `makeDefaultGetDynamicColumns()`. Use
|
|
305
|
+
* this when the host has no helpers to inject and a stable function reference
|
|
306
|
+
* suffices.
|
|
307
|
+
*/
|
|
308
|
+
export const defaultGetDynamicColumns = makeDefaultGetDynamicColumns();
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from './api-context';
|
|
|
12
12
|
export * from './metadata-cache';
|
|
13
13
|
export * from './dynamic-icon';
|
|
14
14
|
export type { ColumnFilterConfig, FilterOption as DynamicColumnFilterOption, GetDynamicColumns, DynamicIconComponent, } from './dynamic-columns-shim';
|
|
15
|
+
export { defaultGetDynamicColumns, makeDefaultGetDynamicColumns, type DynamicColumnsHelpers, } from './dynamic-columns';
|
|
15
16
|
export { DynamicRecordDialog } from './dialogs/dynamic-record';
|
|
16
17
|
export { ExportDialog } from './dialogs/export';
|
|
17
18
|
export { ImportDialog } from './dialogs/import';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,OAAO,EACH,qBAAqB,EACrB,KAAK,gBAAgB,GACxB,MAAM,2BAA2B,CAAA;AAClC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,QAAQ,CAAA;AACtB,cAAc,mBAAmB,CAAA;AACjC,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,YAAY,EACR,kBAAkB,EAClB,YAAY,IAAI,yBAAyB,EACzC,iBAAiB,EACjB,oBAAoB,GACvB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,OAAO,EACH,qBAAqB,EACrB,KAAK,gBAAgB,GACxB,MAAM,2BAA2B,CAAA;AAClC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,QAAQ,CAAA;AACtB,cAAc,mBAAmB,CAAA;AACjC,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,YAAY,EACR,kBAAkB,EAClB,YAAY,IAAI,yBAAyB,EACzC,iBAAiB,EACjB,oBAAoB,GACvB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACH,wBAAwB,EACxB,4BAA4B,EAC5B,KAAK,qBAAqB,GAC7B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export * from './i18n-provider';
|
|
|
16
16
|
export * from './api-context';
|
|
17
17
|
export * from './metadata-cache';
|
|
18
18
|
export * from './dynamic-icon';
|
|
19
|
+
export { defaultGetDynamicColumns, makeDefaultGetDynamicColumns, } from './dynamic-columns';
|
|
19
20
|
export { DynamicRecordDialog } from './dialogs/dynamic-record';
|
|
20
21
|
export { ExportDialog } from './dialogs/export';
|
|
21
22
|
export { ImportDialog } from './dialogs/import';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asteby/metacore-runtime-react",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"description": "React runtime for metacore hosts — renders addon contributions dynamically",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"lucide-react": ">=0.460",
|
|
30
30
|
"date-fns": ">=3",
|
|
31
31
|
"react-day-picker": ">=8",
|
|
32
|
-
"@asteby/metacore-
|
|
33
|
-
"@asteby/metacore-
|
|
32
|
+
"@asteby/metacore-sdk": "^2.2.0",
|
|
33
|
+
"@asteby/metacore-ui": "^0.6.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependenciesMeta": {
|
|
36
36
|
"@tanstack/react-router": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"typescript": "^5.6.0",
|
|
57
57
|
"zustand": "^5.0.0",
|
|
58
58
|
"@asteby/metacore-sdk": "2.2.0",
|
|
59
|
-
"@asteby/metacore-ui": "0.
|
|
59
|
+
"@asteby/metacore-ui": "0.6.0"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"build": "tsc -p tsconfig.json",
|
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
// Default `getDynamicColumns` factory used by hosts that don't need a custom
|
|
2
|
+
// renderer. Supports every cell type produced by kernel/dynamic metadata:
|
|
3
|
+
// badge (static + endpoint-loaded options), avatar, phone, date, boolean,
|
|
4
|
+
// relation-badge-list, media-gallery, image, plus a generic text fallback.
|
|
5
|
+
//
|
|
6
|
+
// The implementation was previously duplicated across `link` and `ops`
|
|
7
|
+
// (~550 LOC each, drifting). It now lives here so a single fix propagates
|
|
8
|
+
// to every host. Hosts inject app-specific URL helpers via the `helpers`
|
|
9
|
+
// argument so the SDK stays free of environment-bound code.
|
|
10
|
+
|
|
11
|
+
import * as React from 'react'
|
|
12
|
+
import { ColumnDef } from '@tanstack/react-table'
|
|
13
|
+
import { format } from 'date-fns'
|
|
14
|
+
import { es, enUS } from 'date-fns/locale'
|
|
15
|
+
import * as icons from 'lucide-react'
|
|
16
|
+
import { MoreHorizontal } from 'lucide-react'
|
|
17
|
+
import {
|
|
18
|
+
Avatar,
|
|
19
|
+
AvatarFallback,
|
|
20
|
+
AvatarImage,
|
|
21
|
+
Badge,
|
|
22
|
+
Button,
|
|
23
|
+
Checkbox,
|
|
24
|
+
DropdownMenu,
|
|
25
|
+
DropdownMenuContent,
|
|
26
|
+
DropdownMenuItem,
|
|
27
|
+
DropdownMenuTrigger,
|
|
28
|
+
} from '@asteby/metacore-ui'
|
|
29
|
+
import {
|
|
30
|
+
DataTableColumnHeader,
|
|
31
|
+
FilterableColumnHeader,
|
|
32
|
+
type ColumnFilterMeta,
|
|
33
|
+
} from '@asteby/metacore-ui/data-table'
|
|
34
|
+
import { generateBadgeStyles } from '@asteby/metacore-ui/lib'
|
|
35
|
+
import { OptionsContext } from './options-context'
|
|
36
|
+
import { DynamicIcon } from './dynamic-icon'
|
|
37
|
+
import type { TableMetadata, ColumnDefinition } from './types'
|
|
38
|
+
import type {
|
|
39
|
+
ColumnFilterConfig,
|
|
40
|
+
GetDynamicColumns,
|
|
41
|
+
} from './dynamic-columns-shim'
|
|
42
|
+
|
|
43
|
+
/** Host-supplied helpers consumed by avatar/image cell renderers. */
|
|
44
|
+
export interface DynamicColumnsHelpers {
|
|
45
|
+
/**
|
|
46
|
+
* Resolves a relative or absolute media path into a renderable URL. Hosts
|
|
47
|
+
* typically prepend their CDN/storage base. If omitted, paths are passed
|
|
48
|
+
* through verbatim.
|
|
49
|
+
*/
|
|
50
|
+
getImageUrl?: (path: string) => string
|
|
51
|
+
/**
|
|
52
|
+
* API origin used to build avatar URLs when the row carries a bare filename
|
|
53
|
+
* instead of an absolute URL or sibling `.avatar` field. Usually
|
|
54
|
+
* `import.meta.env.VITE_API_URL.replace('/api', '')`.
|
|
55
|
+
*/
|
|
56
|
+
apiBaseUrl?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const defaultGetImageUrl = (path: string) => path
|
|
60
|
+
|
|
61
|
+
const getNestedValue = (obj: any, path: string) =>
|
|
62
|
+
path.split('.').reduce((acc, part) => acc && acc[part], obj)
|
|
63
|
+
|
|
64
|
+
const lowerFirst = (value?: string) => {
|
|
65
|
+
if (!value) return value
|
|
66
|
+
return value.charAt(0).toLowerCase() + value.slice(1)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const getPathVariants = (path?: string) => {
|
|
70
|
+
if (!path) return []
|
|
71
|
+
const normalized = path
|
|
72
|
+
.split('.')
|
|
73
|
+
.map((segment) => lowerFirst(segment) || segment)
|
|
74
|
+
.join('.')
|
|
75
|
+
return Array.from(new Set([path, normalized])).filter(Boolean)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const getValueFromPathVariants = (obj: any, path?: string) => {
|
|
79
|
+
if (!path) return undefined
|
|
80
|
+
for (const candidate of getPathVariants(path)) {
|
|
81
|
+
const value = getNestedValue(obj, candidate as string)
|
|
82
|
+
if (value !== undefined && value !== null) return value
|
|
83
|
+
}
|
|
84
|
+
return undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const useIsDarkTheme = () => {
|
|
88
|
+
const [isDark, setIsDark] = React.useState(() =>
|
|
89
|
+
typeof document !== 'undefined' &&
|
|
90
|
+
document.documentElement.classList.contains('dark')
|
|
91
|
+
)
|
|
92
|
+
React.useEffect(() => {
|
|
93
|
+
if (typeof document === 'undefined') return
|
|
94
|
+
const sync = () =>
|
|
95
|
+
setIsDark(document.documentElement.classList.contains('dark'))
|
|
96
|
+
sync()
|
|
97
|
+
const observer = new MutationObserver(sync)
|
|
98
|
+
observer.observe(document.documentElement, {
|
|
99
|
+
attributes: true,
|
|
100
|
+
attributeFilter: ['class'],
|
|
101
|
+
})
|
|
102
|
+
return () => observer.disconnect()
|
|
103
|
+
}, [])
|
|
104
|
+
return isDark
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const renderRelationBadges = (items: any, col: ColumnDefinition) => {
|
|
108
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
109
|
+
return <span className="text-muted-foreground">-</span>
|
|
110
|
+
}
|
|
111
|
+
return (
|
|
112
|
+
<div className="flex flex-wrap gap-1">
|
|
113
|
+
{items.map((item: any, idx: number) => {
|
|
114
|
+
const relationTarget = col.relationPath
|
|
115
|
+
? getValueFromPathVariants(item, col.relationPath) ?? item
|
|
116
|
+
: item
|
|
117
|
+
const displaySource = relationTarget ?? item
|
|
118
|
+
let displayValue =
|
|
119
|
+
col.displayField !== undefined && col.displayField !== null
|
|
120
|
+
? getValueFromPathVariants(displaySource, col.displayField)
|
|
121
|
+
: displaySource
|
|
122
|
+
if (displayValue === undefined || displayValue === null) {
|
|
123
|
+
displayValue = displaySource
|
|
124
|
+
}
|
|
125
|
+
const label =
|
|
126
|
+
displayValue !== undefined && displayValue !== null
|
|
127
|
+
? String(displayValue)
|
|
128
|
+
: '-'
|
|
129
|
+
let iconValue: string | undefined
|
|
130
|
+
if (col.iconField) {
|
|
131
|
+
const rawIcon = getValueFromPathVariants(displaySource, col.iconField)
|
|
132
|
+
if (rawIcon !== undefined && rawIcon !== null) {
|
|
133
|
+
iconValue = String(rawIcon)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return (
|
|
137
|
+
<Badge
|
|
138
|
+
key={`${col.key}-${idx}`}
|
|
139
|
+
variant="outline"
|
|
140
|
+
className="flex items-center gap-1"
|
|
141
|
+
>
|
|
142
|
+
{iconValue && (
|
|
143
|
+
<DynamicIcon name={iconValue} className="h-3 w-3" />
|
|
144
|
+
)}
|
|
145
|
+
<span>{label}</span>
|
|
146
|
+
</Badge>
|
|
147
|
+
)
|
|
148
|
+
})}
|
|
149
|
+
</div>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface OptionBadgeProps {
|
|
154
|
+
option: { value: string; label: string; icon?: string; color?: string }
|
|
155
|
+
fallback: string
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const OptionBadge: React.FC<OptionBadgeProps> = ({ option }) => {
|
|
159
|
+
const isDark = useIsDarkTheme()
|
|
160
|
+
const colorStyles = option.color ? generateBadgeStyles(option.color, { isDark }) : {}
|
|
161
|
+
return (
|
|
162
|
+
<Badge variant="outline" className="flex items-center gap-1 border-0" style={colorStyles}>
|
|
163
|
+
{option.icon && <DynamicIcon name={option.icon} className="h-3.5 w-3.5" />}
|
|
164
|
+
<span>{option.label}</span>
|
|
165
|
+
</Badge>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const BadgeWithEndpointOptions: React.FC<{ endpoint: string; value: any }> = ({ endpoint, value }) => {
|
|
170
|
+
const { optionsMap } = React.useContext(OptionsContext)
|
|
171
|
+
const options = optionsMap.get(endpoint) || []
|
|
172
|
+
const option = options.find((opt: any) => opt.value === value)
|
|
173
|
+
if (option) return <OptionBadge option={option} fallback={String(value)} />
|
|
174
|
+
return <Badge variant="outline">{String(value)}</Badge>
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Builds the canonical column factory used by `<DynamicTable>` when the host
|
|
179
|
+
* does not supply its own. Pass `{ getImageUrl, apiBaseUrl }` to wire avatar
|
|
180
|
+
* URL resolution.
|
|
181
|
+
*/
|
|
182
|
+
export function makeDefaultGetDynamicColumns(
|
|
183
|
+
helpers: DynamicColumnsHelpers = {},
|
|
184
|
+
): GetDynamicColumns {
|
|
185
|
+
const getImageUrl = helpers.getImageUrl ?? defaultGetImageUrl
|
|
186
|
+
const apiBaseUrl = helpers.apiBaseUrl ?? ''
|
|
187
|
+
|
|
188
|
+
return function defaultGetDynamicColumns(
|
|
189
|
+
metadata: TableMetadata,
|
|
190
|
+
onAction?: (action: string, row: any) => void,
|
|
191
|
+
t?: (key: string, options?: any) => string,
|
|
192
|
+
currentLanguage?: string,
|
|
193
|
+
filterConfigs?: Map<string, ColumnFilterConfig>,
|
|
194
|
+
): ColumnDef<any>[] {
|
|
195
|
+
const dateLocale = currentLanguage === 'en' ? enUS : es
|
|
196
|
+
const columns: ColumnDef<any>[] = [
|
|
197
|
+
{
|
|
198
|
+
id: 'select',
|
|
199
|
+
header: ({ table }) => (
|
|
200
|
+
<Checkbox
|
|
201
|
+
checked={
|
|
202
|
+
table.getIsAllPageRowsSelected() ||
|
|
203
|
+
(table.getIsSomePageRowsSelected() && 'indeterminate')
|
|
204
|
+
}
|
|
205
|
+
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
|
206
|
+
aria-label="Select all"
|
|
207
|
+
className="translate-y-[2px]"
|
|
208
|
+
/>
|
|
209
|
+
),
|
|
210
|
+
cell: ({ row }) => (
|
|
211
|
+
<Checkbox
|
|
212
|
+
checked={row.getIsSelected()}
|
|
213
|
+
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
214
|
+
aria-label="Select row"
|
|
215
|
+
className="translate-y-[2px]"
|
|
216
|
+
/>
|
|
217
|
+
),
|
|
218
|
+
enableSorting: false,
|
|
219
|
+
enableHiding: false,
|
|
220
|
+
},
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
metadata.columns.forEach((col) => {
|
|
224
|
+
if (col.hidden) return
|
|
225
|
+
|
|
226
|
+
const translatedLabel = col.label
|
|
227
|
+
const filterConfig = filterConfigs?.get(col.key)
|
|
228
|
+
|
|
229
|
+
const columnMeta: Record<string, unknown> = {
|
|
230
|
+
label: translatedLabel,
|
|
231
|
+
}
|
|
232
|
+
if (filterConfig) {
|
|
233
|
+
const fm: ColumnFilterMeta = {
|
|
234
|
+
filterable: true,
|
|
235
|
+
filterType: filterConfig.filterType as ColumnFilterMeta['filterType'],
|
|
236
|
+
filterKey: filterConfig.filterKey,
|
|
237
|
+
filterOptions: filterConfig.options,
|
|
238
|
+
filterLoading: filterConfig.loading,
|
|
239
|
+
filterSearchEndpoint: filterConfig.searchEndpoint,
|
|
240
|
+
selectedValues: filterConfig.selectedValues,
|
|
241
|
+
onFilterChange: filterConfig.onFilterChange,
|
|
242
|
+
}
|
|
243
|
+
Object.assign(columnMeta, fm)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
columns.push({
|
|
247
|
+
accessorKey: col.key,
|
|
248
|
+
id: col.key,
|
|
249
|
+
meta: columnMeta,
|
|
250
|
+
header: ({ column }) =>
|
|
251
|
+
filterConfig ? (
|
|
252
|
+
<FilterableColumnHeader column={column} title={translatedLabel} />
|
|
253
|
+
) : (
|
|
254
|
+
<DataTableColumnHeader column={column} title={translatedLabel} />
|
|
255
|
+
),
|
|
256
|
+
cell: ({ row }) => {
|
|
257
|
+
const value = getNestedValue(row.original, col.key)
|
|
258
|
+
|
|
259
|
+
// Endpoint-loaded badge options (preloaded into OptionsContext)
|
|
260
|
+
if (col.cellStyle === 'badge' && col.useOptions && col.searchEndpoint) {
|
|
261
|
+
if (!value) return <span className="text-muted-foreground">-</span>
|
|
262
|
+
return <BadgeWithEndpointOptions endpoint={col.searchEndpoint} value={value} />
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Static badge options — map value → label/icon/color
|
|
266
|
+
if (col.cellStyle === 'badge' && col.options && col.options.length > 0) {
|
|
267
|
+
if (!value && value !== 0) return <span className="text-muted-foreground">-</span>
|
|
268
|
+
const option = col.options.find((o) => o.value === String(value))
|
|
269
|
+
if (option) return <OptionBadge option={option} fallback={String(value)} />
|
|
270
|
+
return <Badge variant="outline">{String(value)}</Badge>
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (col.cellStyle === 'relation-badge-list') {
|
|
274
|
+
return renderRelationBadges(value, col)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
switch (col.type) {
|
|
278
|
+
case 'date': {
|
|
279
|
+
if (!value) return <span className="text-muted-foreground">-</span>
|
|
280
|
+
try {
|
|
281
|
+
const date = new Date(value)
|
|
282
|
+
if (isNaN(date.getTime()) || date.getFullYear() <= 1) {
|
|
283
|
+
return <span className="text-muted-foreground">-</span>
|
|
284
|
+
}
|
|
285
|
+
return (
|
|
286
|
+
<div className="flex items-center gap-1.5 text-muted-foreground">
|
|
287
|
+
<icons.Calendar className="h-3.5 w-3.5 opacity-70" />
|
|
288
|
+
<span className="text-sm font-medium">
|
|
289
|
+
{format(date, 'PPP', { locale: dateLocale })}
|
|
290
|
+
</span>
|
|
291
|
+
</div>
|
|
292
|
+
)
|
|
293
|
+
} catch {
|
|
294
|
+
return <span>{String(value)}</span>
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
case 'search':
|
|
299
|
+
case 'avatar': {
|
|
300
|
+
const namePath = col.tooltip || col.key
|
|
301
|
+
const name = getNestedValue(row.original, namePath) || 'N/A'
|
|
302
|
+
const desc = getNestedValue(row.original, col.description || '')
|
|
303
|
+
|
|
304
|
+
let avatarSrc: string | undefined
|
|
305
|
+
if (col.key.includes('.')) {
|
|
306
|
+
const parentPath = col.key.split('.').slice(0, -1).join('.')
|
|
307
|
+
const avatarPath = `${parentPath}.avatar`
|
|
308
|
+
const possibleAvatar = getNestedValue(row.original, avatarPath)
|
|
309
|
+
if (possibleAvatar) avatarSrc = String(possibleAvatar)
|
|
310
|
+
} else if (
|
|
311
|
+
value &&
|
|
312
|
+
(String(value).startsWith('http') || String(value).startsWith('https'))
|
|
313
|
+
) {
|
|
314
|
+
avatarSrc = String(value)
|
|
315
|
+
} else if (value) {
|
|
316
|
+
avatarSrc = `${apiBaseUrl}${col.basePath || ''}${value}`
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
321
|
+
<Avatar className="h-8 w-8 rounded-lg ring-1 ring-border/50">
|
|
322
|
+
<AvatarImage
|
|
323
|
+
src={getImageUrl(avatarSrc || '')}
|
|
324
|
+
alt={String(name)}
|
|
325
|
+
className="object-cover"
|
|
326
|
+
/>
|
|
327
|
+
<AvatarFallback className="text-[10px] font-bold bg-primary/5 text-primary rounded-lg">
|
|
328
|
+
{String(name)
|
|
329
|
+
.split(' ')
|
|
330
|
+
.map((n: string) => n[0])
|
|
331
|
+
.slice(0, 2)
|
|
332
|
+
.join('')
|
|
333
|
+
.toUpperCase()}
|
|
334
|
+
</AvatarFallback>
|
|
335
|
+
</Avatar>
|
|
336
|
+
<div className="flex flex-col min-w-0 overflow-hidden">
|
|
337
|
+
<span className="font-medium text-sm truncate leading-none mb-0.5 text-foreground/90">
|
|
338
|
+
{String(name)}
|
|
339
|
+
</span>
|
|
340
|
+
{desc && (
|
|
341
|
+
<span className="text-[11px] text-muted-foreground truncate leading-none">
|
|
342
|
+
{String(desc)}
|
|
343
|
+
</span>
|
|
344
|
+
)}
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
case 'relation-badge-list':
|
|
351
|
+
return renderRelationBadges(value, col)
|
|
352
|
+
|
|
353
|
+
case 'phone': {
|
|
354
|
+
if (!value) return <span className="text-muted-foreground">-</span>
|
|
355
|
+
return <span className="font-medium text-sm">{String(value)}</span>
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
case 'boolean':
|
|
359
|
+
return value ? <Badge>Sí</Badge> : <Badge variant="secondary">No</Badge>
|
|
360
|
+
|
|
361
|
+
case 'media-gallery': {
|
|
362
|
+
if (!value || (Array.isArray(value) && value.length === 0)) {
|
|
363
|
+
return <span className="text-muted-foreground">-</span>
|
|
364
|
+
}
|
|
365
|
+
const mediaItems = Array.isArray(value) ? value : []
|
|
366
|
+
const visibleItems = mediaItems.slice(0, 3)
|
|
367
|
+
const remaining = mediaItems.length - 3
|
|
368
|
+
return (
|
|
369
|
+
<div className="flex -space-x-2 overflow-hidden">
|
|
370
|
+
{visibleItems.map((item: any, i: number) => {
|
|
371
|
+
const src = item.url
|
|
372
|
+
if (item.type === 'image') {
|
|
373
|
+
return (
|
|
374
|
+
<Avatar
|
|
375
|
+
key={i}
|
|
376
|
+
className="inline-block h-8 w-8 rounded-full ring-2 ring-background"
|
|
377
|
+
>
|
|
378
|
+
<AvatarImage src={src} className="object-cover" />
|
|
379
|
+
<AvatarFallback>{item.type?.[0]}</AvatarFallback>
|
|
380
|
+
</Avatar>
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
return (
|
|
384
|
+
<div
|
|
385
|
+
key={i}
|
|
386
|
+
className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-muted ring-2 ring-background"
|
|
387
|
+
>
|
|
388
|
+
<DynamicIcon
|
|
389
|
+
name={
|
|
390
|
+
item.type === 'video'
|
|
391
|
+
? 'Video'
|
|
392
|
+
: item.type === 'audio'
|
|
393
|
+
? 'AudioLines'
|
|
394
|
+
: 'FileText'
|
|
395
|
+
}
|
|
396
|
+
className="h-4 w-4"
|
|
397
|
+
/>
|
|
398
|
+
</div>
|
|
399
|
+
)
|
|
400
|
+
})}
|
|
401
|
+
{remaining > 0 && (
|
|
402
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-muted text-xs font-medium ring-2 ring-background">
|
|
403
|
+
+{remaining}
|
|
404
|
+
</div>
|
|
405
|
+
)}
|
|
406
|
+
</div>
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
case 'image': {
|
|
411
|
+
const imageValue =
|
|
412
|
+
value ||
|
|
413
|
+
(Array.isArray(row.original.media)
|
|
414
|
+
? row.original.media.find((m: any) => m.type === 'image')?.url
|
|
415
|
+
: null)
|
|
416
|
+
if (!imageValue) return <span className="text-muted-foreground">-</span>
|
|
417
|
+
return (
|
|
418
|
+
<div className="h-10 w-10 relative rounded overflow-hidden bg-muted flex items-center justify-center">
|
|
419
|
+
<img
|
|
420
|
+
src={getImageUrl(String(imageValue))}
|
|
421
|
+
alt="Thumbnail"
|
|
422
|
+
className="h-full w-full object-contain"
|
|
423
|
+
onError={(e) => {
|
|
424
|
+
;(e.currentTarget as HTMLImageElement).style.display = 'none'
|
|
425
|
+
}}
|
|
426
|
+
/>
|
|
427
|
+
</div>
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
default: {
|
|
432
|
+
if (typeof value === 'object' && value !== null) {
|
|
433
|
+
return (
|
|
434
|
+
<span className="text-muted-foreground text-xs">
|
|
435
|
+
{JSON.stringify(value)}
|
|
436
|
+
</span>
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
if (
|
|
440
|
+
col.key === 'description' ||
|
|
441
|
+
col.key === 'features' ||
|
|
442
|
+
col.key.includes('description')
|
|
443
|
+
) {
|
|
444
|
+
return (
|
|
445
|
+
<div className="max-w-[350px]" title={String(value)}>
|
|
446
|
+
<span className="truncate font-medium block">
|
|
447
|
+
{value !== null && value !== undefined ? String(value) : '-'}
|
|
448
|
+
</span>
|
|
449
|
+
</div>
|
|
450
|
+
)
|
|
451
|
+
}
|
|
452
|
+
return (
|
|
453
|
+
<span className="truncate font-medium">
|
|
454
|
+
{value !== null && value !== undefined ? String(value) : '-'}
|
|
455
|
+
</span>
|
|
456
|
+
)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
enableSorting: col.sortable,
|
|
461
|
+
enableHiding: true,
|
|
462
|
+
})
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
if (metadata.hasActions && metadata.actions.length > 0) {
|
|
466
|
+
columns.push({
|
|
467
|
+
id: 'actions',
|
|
468
|
+
header: () => <div className="text-right">{t ? t('common.actions') : 'Acciones'}</div>,
|
|
469
|
+
size: 80,
|
|
470
|
+
maxSize: 80,
|
|
471
|
+
meta: {},
|
|
472
|
+
cell: ({ row }) => (
|
|
473
|
+
<div className="flex items-center justify-end">
|
|
474
|
+
<DropdownMenu>
|
|
475
|
+
<DropdownMenuTrigger asChild>
|
|
476
|
+
<Button variant="ghost" className="h-8 w-8 p-0">
|
|
477
|
+
<span className="sr-only">Abrir menú</span>
|
|
478
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
479
|
+
</Button>
|
|
480
|
+
</DropdownMenuTrigger>
|
|
481
|
+
<DropdownMenuContent align="end">
|
|
482
|
+
{metadata.actions
|
|
483
|
+
.filter((action) => {
|
|
484
|
+
if (!action.condition) return true
|
|
485
|
+
const { field, operator, value } = action.condition
|
|
486
|
+
const rowValue = String((row.original as any)[field] ?? '')
|
|
487
|
+
const values = Array.isArray(value) ? value : [value]
|
|
488
|
+
switch (operator) {
|
|
489
|
+
case 'eq':
|
|
490
|
+
return rowValue === values[0]
|
|
491
|
+
case 'neq':
|
|
492
|
+
return rowValue !== values[0]
|
|
493
|
+
case 'in':
|
|
494
|
+
return values.includes(rowValue)
|
|
495
|
+
case 'not_in':
|
|
496
|
+
return !values.includes(rowValue)
|
|
497
|
+
default:
|
|
498
|
+
return true
|
|
499
|
+
}
|
|
500
|
+
})
|
|
501
|
+
.map((action) => (
|
|
502
|
+
<DropdownMenuItem
|
|
503
|
+
key={action.key}
|
|
504
|
+
onClick={() => onAction && onAction(action.key, row.original)}
|
|
505
|
+
>
|
|
506
|
+
<DynamicIcon name={action.icon} className="mr-2 h-4 w-4" />
|
|
507
|
+
{action.label}
|
|
508
|
+
</DropdownMenuItem>
|
|
509
|
+
))}
|
|
510
|
+
</DropdownMenuContent>
|
|
511
|
+
</DropdownMenu>
|
|
512
|
+
</div>
|
|
513
|
+
),
|
|
514
|
+
})
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return columns
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Eager-built variant — equivalent to `makeDefaultGetDynamicColumns()`. Use
|
|
523
|
+
* this when the host has no helpers to inject and a stable function reference
|
|
524
|
+
* suffices.
|
|
525
|
+
*/
|
|
526
|
+
export const defaultGetDynamicColumns: GetDynamicColumns =
|
|
527
|
+
makeDefaultGetDynamicColumns()
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,11 @@ export type {
|
|
|
25
25
|
GetDynamicColumns,
|
|
26
26
|
DynamicIconComponent,
|
|
27
27
|
} from './dynamic-columns-shim'
|
|
28
|
+
export {
|
|
29
|
+
defaultGetDynamicColumns,
|
|
30
|
+
makeDefaultGetDynamicColumns,
|
|
31
|
+
type DynamicColumnsHelpers,
|
|
32
|
+
} from './dynamic-columns'
|
|
28
33
|
export { DynamicRecordDialog } from './dialogs/dynamic-record'
|
|
29
34
|
export { ExportDialog } from './dialogs/export'
|
|
30
35
|
export { ImportDialog } from './dialogs/import'
|