@asteby/metacore-runtime-react 17.0.2 → 17.0.4
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 +25 -0
- package/dist/dynamic-columns.d.ts +23 -0
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +74 -27
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +2 -2
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/format-date-cell.test.ts +52 -0
- package/src/__tests__/relation-option-cells.test.ts +42 -1
- package/src/dynamic-columns.tsx +132 -35
- package/src/dynamic-table.tsx +3 -2
- package/src/types.ts +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 17.0.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a745f5c: Relation/option thumbnails: resolved FK relation chips and option badges now
|
|
8
|
+
render a small thumbnail when the backend stamps an `image` on the sibling
|
|
9
|
+
`{ value, label }` object or the option (brand logo, product photo, customer
|
|
10
|
+
avatar), with a graceful initials fallback when the image is missing or fails to
|
|
11
|
+
load. Applies to the table `relation`/`select`/`status`/`badge` cells; the
|
|
12
|
+
searchable picker (`DynamicSelectField`) and the detail-view picker already
|
|
13
|
+
rendered option images. Adds a pure `resolveRelationImage` helper (+ tests).
|
|
14
|
+
|
|
15
|
+
## 17.0.3
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- b5c8f5f: Pro datetime columns: `datetime`/`timestamp`/`timestamptz` columns now use the
|
|
20
|
+
date cell renderer instead of falling through to the raw-ISO fallback. Datetime
|
|
21
|
+
variants show day + time with a full-precision tooltip on hover (the 7Leguas
|
|
22
|
+
pattern); plain `date` columns stay day-only. Null and the Go zero-time render
|
|
23
|
+
an em-dash. Date-typed columns (including the timestamp variants) now infer the
|
|
24
|
+
`date_range` filter. Adds a pure `formatDateCell` helper (+ tests).
|
|
25
|
+
- Updated dependencies [b5c8f5f]
|
|
26
|
+
- @asteby/metacore-ui@2.4.2
|
|
27
|
+
|
|
3
28
|
## 17.0.2
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Locale } from 'date-fns';
|
|
1
2
|
import type { ColumnDefinition } from './types';
|
|
2
3
|
import type { GetDynamicColumns } from './dynamic-columns-shim';
|
|
3
4
|
/** Host-supplied helpers consumed by avatar/image cell renderers. */
|
|
@@ -37,6 +38,20 @@ export declare const isActionAllowedForRowState: (action: any, row: any) => bool
|
|
|
37
38
|
* cell can read `row[relationKeyFor(col)]`.
|
|
38
39
|
*/
|
|
39
40
|
export declare const relationKeyFor: (col: Pick<ColumnDefinition, "key">) => string;
|
|
41
|
+
/** Cell renderers (`cellStyle`/`type`) that resolve to the date renderer. */
|
|
42
|
+
export declare const DATE_CELL_TYPES: readonly ["date", "datetime", "timestamp", "timestamptz"];
|
|
43
|
+
/**
|
|
44
|
+
* Pure formatter behind the date/datetime cell. Returns the display string and
|
|
45
|
+
* an optional full-precision `title` (tooltip), or `null` when the value is
|
|
46
|
+
* empty/invalid/the Go zero-time so the cell renders an em-dash.
|
|
47
|
+
* - `date`: day only (`PPP`), no tooltip.
|
|
48
|
+
* - `datetime`/`timestamp(tz)`: day + time (`Pp`) with a full-precision
|
|
49
|
+
* tooltip (`PPpp`) — the 7Leguas pattern.
|
|
50
|
+
*/
|
|
51
|
+
export declare function formatDateCell(value: unknown, renderAs: string | undefined, locale: Locale): {
|
|
52
|
+
display: string;
|
|
53
|
+
title?: string;
|
|
54
|
+
} | null;
|
|
40
55
|
/**
|
|
41
56
|
* Reads the resolved relation/option label a backend serves for an FK or
|
|
42
57
|
* option column, falling back to the raw value. Pure so the cell renderers and
|
|
@@ -46,6 +61,14 @@ export declare const relationKeyFor: (col: Pick<ColumnDefinition, "key">) => str
|
|
|
46
61
|
* - else: the raw value coerced to string ('' when nullish).
|
|
47
62
|
*/
|
|
48
63
|
export declare const resolveRelationLabel: (col: ColumnDefinition, row: any) => string;
|
|
64
|
+
/**
|
|
65
|
+
* Reads the thumbnail URL a backend serves on a resolved FK sibling, when
|
|
66
|
+
* present. The backend stamps `image` onto the `{ value, label }` relation
|
|
67
|
+
* object when the referenced model carries an image column (brand logo,
|
|
68
|
+
* product photo, customer avatar). Returns '' when there is no sibling image —
|
|
69
|
+
* the chip then renders text-only, exactly as before.
|
|
70
|
+
*/
|
|
71
|
+
export declare const resolveRelationImage: (col: ColumnDefinition, row: any) => string;
|
|
49
72
|
/**
|
|
50
73
|
* Builds the canonical column factory used by `<DynamicTable>` when the host
|
|
51
74
|
* does not supply its own. Pass `{ getImageUrl, apiBaseUrl }` to wire avatar
|
|
@@ -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":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAgC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,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;AAqKD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,GACf;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAY5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AA0ED;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAsmBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
|
package/dist/dynamic-columns.js
CHANGED
|
@@ -169,7 +169,16 @@ const renderRelationBadges = (items, col) => {
|
|
|
169
169
|
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}`));
|
|
170
170
|
}) }));
|
|
171
171
|
};
|
|
172
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Tiny square thumbnail for a resolved relation/option that carries an `image`
|
|
174
|
+
* (brand logo, product photo, customer/user avatar). Uses the same Avatar
|
|
175
|
+
* primitive as the `avatar`/`creator` cells so a broken/loading image
|
|
176
|
+
* gracefully falls back to the record's initials. Sized small (the box is an
|
|
177
|
+
* inline style so an addon-arbitrary Tailwind class never gets dropped by a
|
|
178
|
+
* consuming app's class scan). Rendered inline alongside a label — never alone.
|
|
179
|
+
*/
|
|
180
|
+
const RelationThumbnail = ({ src, alt, getImageUrl, size = 18 }) => (_jsxs(Avatar, { className: "shrink-0 rounded-sm ring-1 ring-border/40", style: { width: size, height: size }, children: [_jsx(AvatarImage, { src: getImageUrl ? getImageUrl(src) : src, alt: alt, className: "object-cover" }), _jsx(AvatarFallback, { className: "rounded-sm bg-primary/10 text-[8px] font-bold text-primary", children: getInitials(alt) })] }));
|
|
181
|
+
const OptionBadge = ({ option, getImageUrl }) => {
|
|
173
182
|
const isDark = useIsDarkTheme();
|
|
174
183
|
// Explicit backend color wins; otherwise derive a stable, cohesive color
|
|
175
184
|
// from the option's value (fallback label) so "dead" gray badges come
|
|
@@ -177,14 +186,14 @@ const OptionBadge = ({ option }) => {
|
|
|
177
186
|
// tailwind safelist — addon-arbitrary classes aren't in the host scan.
|
|
178
187
|
const colorSource = option.color || optionColor(option.value || option.label);
|
|
179
188
|
const colorStyles = generateBadgeStyles(colorSource, { isDark });
|
|
180
|
-
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 })] }));
|
|
189
|
+
return (_jsxs(Badge, { variant: "outline", className: "flex items-center gap-1 border-0", style: colorStyles, children: [option.image ? (_jsx(RelationThumbnail, { src: option.image, alt: option.label, getImageUrl: getImageUrl, size: 16 })) : (option.icon && _jsx(DynamicIcon, { name: option.icon, className: "h-3.5 w-3.5" })), _jsx("span", { children: option.label })] }));
|
|
181
190
|
};
|
|
182
|
-
const BadgeWithEndpointOptions = ({ endpoint, value }) => {
|
|
191
|
+
const BadgeWithEndpointOptions = ({ endpoint, value, getImageUrl }) => {
|
|
183
192
|
const { optionsMap } = React.useContext(OptionsContext);
|
|
184
193
|
const options = optionsMap.get(endpoint) || [];
|
|
185
194
|
const option = options.find((opt) => opt.value === value);
|
|
186
195
|
if (option)
|
|
187
|
-
return _jsx(OptionBadge, { option: option, fallback: String(value) });
|
|
196
|
+
return _jsx(OptionBadge, { option: option, fallback: String(value), getImageUrl: getImageUrl });
|
|
188
197
|
// No declared option matched → humanize the raw token as a safety net so a
|
|
189
198
|
// cell never shows `in_progress` verbatim (option.label still wins above).
|
|
190
199
|
return _jsx(Badge, { variant: "outline", children: humanizeToken(value) });
|
|
@@ -201,6 +210,31 @@ export const relationKeyFor = (col) => {
|
|
|
201
210
|
const k = col.key;
|
|
202
211
|
return k.endsWith('_id') ? k.slice(0, -3) : k;
|
|
203
212
|
};
|
|
213
|
+
/** Cell renderers (`cellStyle`/`type`) that resolve to the date renderer. */
|
|
214
|
+
export const DATE_CELL_TYPES = ['date', 'datetime', 'timestamp', 'timestamptz'];
|
|
215
|
+
/**
|
|
216
|
+
* Pure formatter behind the date/datetime cell. Returns the display string and
|
|
217
|
+
* an optional full-precision `title` (tooltip), or `null` when the value is
|
|
218
|
+
* empty/invalid/the Go zero-time so the cell renders an em-dash.
|
|
219
|
+
* - `date`: day only (`PPP`), no tooltip.
|
|
220
|
+
* - `datetime`/`timestamp(tz)`: day + time (`Pp`) with a full-precision
|
|
221
|
+
* tooltip (`PPpp`) — the 7Leguas pattern.
|
|
222
|
+
*/
|
|
223
|
+
export function formatDateCell(value, renderAs, locale) {
|
|
224
|
+
if (value === null || value === undefined || value === '')
|
|
225
|
+
return null;
|
|
226
|
+
const date = new Date(value);
|
|
227
|
+
if (isNaN(date.getTime()) || date.getFullYear() <= 1)
|
|
228
|
+
return null;
|
|
229
|
+
const withTime = renderAs !== 'date';
|
|
230
|
+
if (withTime) {
|
|
231
|
+
return {
|
|
232
|
+
display: format(date, 'Pp', { locale }),
|
|
233
|
+
title: format(date, 'PPpp', { locale }),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return { display: format(date, 'PPP', { locale }) };
|
|
237
|
+
}
|
|
204
238
|
/**
|
|
205
239
|
* Reads the resolved relation/option label a backend serves for an FK or
|
|
206
240
|
* option column, falling back to the raw value. Pure so the cell renderers and
|
|
@@ -222,24 +256,42 @@ export const resolveRelationLabel = (col, row) => {
|
|
|
222
256
|
return '';
|
|
223
257
|
return String(raw);
|
|
224
258
|
};
|
|
259
|
+
/**
|
|
260
|
+
* Reads the thumbnail URL a backend serves on a resolved FK sibling, when
|
|
261
|
+
* present. The backend stamps `image` onto the `{ value, label }` relation
|
|
262
|
+
* object when the referenced model carries an image column (brand logo,
|
|
263
|
+
* product photo, customer avatar). Returns '' when there is no sibling image —
|
|
264
|
+
* the chip then renders text-only, exactly as before.
|
|
265
|
+
*/
|
|
266
|
+
export const resolveRelationImage = (col, row) => {
|
|
267
|
+
const sibling = getNestedValue(row, relationKeyFor(col));
|
|
268
|
+
if (sibling && typeof sibling === 'object') {
|
|
269
|
+
const img = sibling.image ?? sibling.avatar ?? sibling.photo;
|
|
270
|
+
if (img !== undefined && img !== null && img !== '')
|
|
271
|
+
return String(img);
|
|
272
|
+
}
|
|
273
|
+
return '';
|
|
274
|
+
};
|
|
225
275
|
/**
|
|
226
276
|
* Renders a resolved FK relation as a clean, truncated chip. Reads the
|
|
227
|
-
* backend-resolved sibling `{ value, label }` (see `relationKeyFor`)
|
|
228
|
-
* its `label
|
|
229
|
-
*
|
|
230
|
-
*
|
|
277
|
+
* backend-resolved sibling `{ value, label[, image] }` (see `relationKeyFor`)
|
|
278
|
+
* and shows its `label`, prefixed with a small thumbnail when the sibling
|
|
279
|
+
* carries an `image`. Falls back to the raw id when no sibling was resolved, and
|
|
280
|
+
* to an empty marker when there is no value at all. Domain-agnostic: works for
|
|
281
|
+
* every `belongs_to` column (category, supplier, brand, …) without per-addon code.
|
|
231
282
|
*/
|
|
232
|
-
const RelationCell = ({ col, row }) => {
|
|
283
|
+
const RelationCell = ({ col, row, getImageUrl }) => {
|
|
233
284
|
const isDark = useIsDarkTheme();
|
|
234
285
|
const display = resolveRelationLabel(col, row);
|
|
235
286
|
if (!display)
|
|
236
287
|
return _jsx(EmptyCell, {});
|
|
288
|
+
const image = resolveRelationImage(col, row);
|
|
237
289
|
// Deterministic, SUBTLE color keyed on the resolved label — lighter than
|
|
238
290
|
// enum badges (soft tint, no heavy fill) so category/brand chips read as
|
|
239
291
|
// alive yet stay visually distinct from option/status badges. Inline style
|
|
240
292
|
// (hex-derived) bypasses the host tailwind safelist.
|
|
241
293
|
const chipStyles = relationChipStyles(display, { isDark });
|
|
242
|
-
return (
|
|
294
|
+
return (_jsxs("span", { className: "inline-flex max-w-[220px] items-center gap-1.5 rounded-md px-2 py-0.5 text-sm font-medium", style: chipStyles, title: display, children: [image && (_jsx(RelationThumbnail, { src: image, alt: display, getImageUrl: getImageUrl, size: 18 })), _jsx("span", { className: "truncate", children: display })] }));
|
|
243
295
|
};
|
|
244
296
|
/**
|
|
245
297
|
* Generic avatar-style cell: round/rounded photo (or initials fallback) +
|
|
@@ -309,7 +361,7 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
|
309
361
|
if (renderAs === 'badge' && col.useOptions && col.searchEndpoint) {
|
|
310
362
|
if (!value)
|
|
311
363
|
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
312
|
-
return _jsx(BadgeWithEndpointOptions, { endpoint: col.searchEndpoint, value: value });
|
|
364
|
+
return _jsx(BadgeWithEndpointOptions, { endpoint: col.searchEndpoint, value: value, getImageUrl: getImageUrl });
|
|
313
365
|
}
|
|
314
366
|
// Static badge options — map value → label/icon/color
|
|
315
367
|
if (renderAs === 'badge' && col.options && col.options.length > 0) {
|
|
@@ -317,7 +369,7 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
|
317
369
|
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
318
370
|
const option = col.options.find((o) => o.value === String(value));
|
|
319
371
|
if (option)
|
|
320
|
-
return _jsx(OptionBadge, { option: option, fallback: String(value) });
|
|
372
|
+
return _jsx(OptionBadge, { option: option, fallback: String(value), getImageUrl: getImageUrl });
|
|
321
373
|
return _jsx(Badge, { variant: "outline", children: humanizeToken(value) });
|
|
322
374
|
}
|
|
323
375
|
if (renderAs === 'relation-badge-list') {
|
|
@@ -337,7 +389,7 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
|
337
389
|
const sv = String(value);
|
|
338
390
|
const option = col.options?.find((o) => o.value === sv);
|
|
339
391
|
if (option)
|
|
340
|
-
return _jsx(OptionBadge, { option: option, fallback: sv });
|
|
392
|
+
return _jsx(OptionBadge, { option: option, fallback: sv, getImageUrl: getImageUrl });
|
|
341
393
|
const isDark = typeof document !== 'undefined' &&
|
|
342
394
|
document.documentElement.classList.contains('dark');
|
|
343
395
|
const styles = generateBadgeStyles(statusColorFor(sv), { isDark });
|
|
@@ -352,7 +404,7 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
|
352
404
|
// `row[<key w/o _id>] = { value, label }` sibling.
|
|
353
405
|
if (renderAs === 'relation' ||
|
|
354
406
|
(col.ref && !col.options?.length && renderAs !== 'badge' && renderAs !== 'status')) {
|
|
355
|
-
return _jsx(RelationCell, { col: col, row: row.original });
|
|
407
|
+
return _jsx(RelationCell, { col: col, row: row.original, getImageUrl: getImageUrl });
|
|
356
408
|
}
|
|
357
409
|
// Option/type column: a `select`-style column ships its
|
|
358
410
|
// localized `options: [{value,label,color,icon}]` inline and
|
|
@@ -366,23 +418,18 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
|
366
418
|
return _jsx(EmptyCell, {});
|
|
367
419
|
const option = col.options.find((o) => o.value === String(value));
|
|
368
420
|
if (option)
|
|
369
|
-
return _jsx(OptionBadge, { option: option, fallback: String(value) });
|
|
421
|
+
return _jsx(OptionBadge, { option: option, fallback: String(value), getImageUrl: getImageUrl });
|
|
370
422
|
return _jsx(Badge, { variant: "outline", children: humanizeToken(value) });
|
|
371
423
|
}
|
|
372
424
|
switch (renderAs) {
|
|
373
|
-
case 'date':
|
|
374
|
-
|
|
425
|
+
case 'date':
|
|
426
|
+
case 'datetime':
|
|
427
|
+
case 'timestamp':
|
|
428
|
+
case 'timestamptz': {
|
|
429
|
+
const formatted = formatDateCell(value, renderAs, dateLocale);
|
|
430
|
+
if (!formatted)
|
|
375
431
|
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
376
|
-
|
|
377
|
-
const date = new Date(value);
|
|
378
|
-
if (isNaN(date.getTime()) || date.getFullYear() <= 1) {
|
|
379
|
-
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
380
|
-
}
|
|
381
|
-
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 }) })] }));
|
|
382
|
-
}
|
|
383
|
-
catch {
|
|
384
|
-
return _jsx("span", { children: String(value) });
|
|
385
|
-
}
|
|
432
|
+
return (_jsxs("div", { className: "flex items-center gap-1.5 text-muted-foreground", title: formatted.title, children: [_jsx(icons.Calendar, { className: "h-3.5 w-3.5 opacity-70" }), _jsx("span", { className: "text-sm font-medium", children: formatted.display })] }));
|
|
386
433
|
}
|
|
387
434
|
case 'search':
|
|
388
435
|
case 'avatar':
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AA+B9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAUnF,UAAU,iBAAiB;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;CACxC;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,GAC/C,EAAE,iBAAiB,+
|
|
1
|
+
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AA+B9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAUnF,UAAU,iBAAiB;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;CACxC;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,GAC/C,EAAE,iBAAiB,+BAgyBnB"}
|
package/dist/dynamic-table.js
CHANGED
|
@@ -24,7 +24,7 @@ import { toast } from 'sonner';
|
|
|
24
24
|
import { Progress } from './dialogs/_primitives';
|
|
25
25
|
import { useMetadataCache } from './metadata-cache';
|
|
26
26
|
import { useApi, useCurrentBranch } from './api-context';
|
|
27
|
-
import { defaultGetDynamicColumns } from './dynamic-columns';
|
|
27
|
+
import { defaultGetDynamicColumns, DATE_CELL_TYPES } from './dynamic-columns';
|
|
28
28
|
import { OptionsContext } from './options-context';
|
|
29
29
|
import { ActionModalDispatcher } from './action-modal-dispatcher';
|
|
30
30
|
import { getSearchableColumnKeys } from './column-visibility';
|
|
@@ -520,7 +520,7 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
520
520
|
filterType = 'boolean';
|
|
521
521
|
else if (c.type === 'number')
|
|
522
522
|
filterType = 'number_range';
|
|
523
|
-
else if (c.type
|
|
523
|
+
else if (DATE_CELL_TYPES.includes(c.type))
|
|
524
524
|
filterType = 'date_range';
|
|
525
525
|
else
|
|
526
526
|
filterType = 'text';
|
package/dist/types.d.ts
CHANGED
|
@@ -81,7 +81,7 @@ export type ColumnVisibility = 'all' | 'table' | 'modal' | 'list' | (string & {}
|
|
|
81
81
|
export interface ColumnDefinition {
|
|
82
82
|
key: string;
|
|
83
83
|
label: string;
|
|
84
|
-
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' | 'relation';
|
|
84
|
+
type: 'text' | 'number' | 'date' | 'datetime' | 'timestamp' | 'timestamptz' | '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' | 'relation';
|
|
85
85
|
sortable: boolean;
|
|
86
86
|
filterable: boolean;
|
|
87
87
|
/**
|
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;;;;;OAKG;IACH,IAAI,EAAE,QAAQ,GAAG,gBAAgB,GAAG,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,MAAM,CAAA;IACtF,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,EACE,MAAM,GACN,QAAQ,GACR,MAAM,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asteby/metacore-runtime-react",
|
|
3
|
-
"version": "17.0.
|
|
3
|
+
"version": "17.0.4",
|
|
4
4
|
"description": "React runtime for metacore hosts — renders addon contributions dynamically",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"date-fns": ">=3",
|
|
35
35
|
"react-day-picker": ">=8",
|
|
36
36
|
"@asteby/metacore-sdk": "^3.2.0",
|
|
37
|
-
"@asteby/metacore-ui": "^2.4.
|
|
37
|
+
"@asteby/metacore-ui": "^2.4.2"
|
|
38
38
|
},
|
|
39
39
|
"peerDependenciesMeta": {
|
|
40
40
|
"@tanstack/react-router": {
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"vitest": "^4.0.0",
|
|
63
63
|
"zustand": "^5.0.0",
|
|
64
64
|
"@asteby/metacore-sdk": "3.2.0",
|
|
65
|
-
"@asteby/metacore-ui": "2.4.
|
|
65
|
+
"@asteby/metacore-ui": "2.4.2"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
68
|
"build": "tsc -p tsconfig.json",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Pure-logic coverage for the date/datetime cell formatter behind
|
|
2
|
+
// `defaultGetDynamicColumns`. Locks the contract the JSX cell relies on:
|
|
3
|
+
// `date` → day only (no tooltip), `datetime`/`timestamp(tz)` → day + time with
|
|
4
|
+
// a full-precision tooltip, and null/invalid/Go-zero-time → `null` (em-dash).
|
|
5
|
+
import { describe, it, expect } from 'vitest'
|
|
6
|
+
import { enUS } from 'date-fns/locale'
|
|
7
|
+
import { formatDateCell } from '../dynamic-columns'
|
|
8
|
+
|
|
9
|
+
describe('formatDateCell', () => {
|
|
10
|
+
const iso = '2026-06-06T17:47:33.201Z'
|
|
11
|
+
|
|
12
|
+
it('renders day only with no tooltip for `date`', () => {
|
|
13
|
+
const out = formatDateCell(iso, 'date', enUS)
|
|
14
|
+
expect(out).not.toBeNull()
|
|
15
|
+
expect(out!.title).toBeUndefined()
|
|
16
|
+
// `PPP` is the long day form — no time component.
|
|
17
|
+
expect(out!.display).not.toMatch(/\d{1,2}:\d{2}/)
|
|
18
|
+
expect(out!.display).toMatch(/2026/)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('renders day + time and a full-precision tooltip for `datetime`', () => {
|
|
22
|
+
const out = formatDateCell(iso, 'datetime', enUS)
|
|
23
|
+
expect(out).not.toBeNull()
|
|
24
|
+
expect(out!.display).toMatch(/\d{1,2}:\d{2}/)
|
|
25
|
+
expect(out!.title).toBeDefined()
|
|
26
|
+
expect(out!.title).toMatch(/\d{1,2}:\d{2}/)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('treats timestamp/timestamptz like datetime (day + time + tooltip)', () => {
|
|
30
|
+
for (const t of ['timestamp', 'timestamptz']) {
|
|
31
|
+
const out = formatDateCell(iso, t, enUS)
|
|
32
|
+
expect(out).not.toBeNull()
|
|
33
|
+
expect(out!.display).toMatch(/\d{1,2}:\d{2}/)
|
|
34
|
+
expect(out!.title).toBeDefined()
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('returns null for null/undefined/empty (empty cell)', () => {
|
|
39
|
+
expect(formatDateCell(null, 'datetime', enUS)).toBeNull()
|
|
40
|
+
expect(formatDateCell(undefined, 'datetime', enUS)).toBeNull()
|
|
41
|
+
expect(formatDateCell('', 'datetime', enUS)).toBeNull()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('returns null for the Go zero-time (0001-01-01T00:00:00Z)', () => {
|
|
45
|
+
expect(formatDateCell('0001-01-01T00:00:00Z', 'datetime', enUS)).toBeNull()
|
|
46
|
+
expect(formatDateCell('0001-01-01T00:00:00Z', 'date', enUS)).toBeNull()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('returns null for an unparseable value', () => {
|
|
50
|
+
expect(formatDateCell('not-a-date', 'datetime', enUS)).toBeNull()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// host's render tests); here we lock the value-resolution contract that drives
|
|
4
4
|
// them so a backend shape change is caught without a DOM.
|
|
5
5
|
import { describe, it, expect } from 'vitest'
|
|
6
|
-
import { relationKeyFor, resolveRelationLabel } from '../dynamic-columns'
|
|
6
|
+
import { relationKeyFor, resolveRelationImage, resolveRelationLabel } from '../dynamic-columns'
|
|
7
7
|
import type { ColumnDefinition } from '../types'
|
|
8
8
|
|
|
9
9
|
const col = (over: Partial<ColumnDefinition>): ColumnDefinition => ({
|
|
@@ -64,3 +64,44 @@ describe('resolveRelationLabel', () => {
|
|
|
64
64
|
expect(resolveRelationLabel(col({ ref: 'categories' }), row)).toBe('Sin categoría')
|
|
65
65
|
})
|
|
66
66
|
})
|
|
67
|
+
|
|
68
|
+
describe('resolveRelationImage', () => {
|
|
69
|
+
const brandCol = (over: Partial<ColumnDefinition> = {}): ColumnDefinition =>
|
|
70
|
+
col({ key: 'brand_id', label: 'Marca', ref: 'brands', ...over })
|
|
71
|
+
|
|
72
|
+
it('reads the thumbnail the backend stamps on the resolved sibling', () => {
|
|
73
|
+
const row = {
|
|
74
|
+
brand_id: 'uuid-1',
|
|
75
|
+
brand: { value: 'uuid-1', label: 'Michelin', image: 'https://cdn/x/m.png' },
|
|
76
|
+
}
|
|
77
|
+
expect(resolveRelationImage(brandCol(), row)).toBe('https://cdn/x/m.png')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('also accepts avatar/photo aliases on the sibling', () => {
|
|
81
|
+
expect(
|
|
82
|
+
resolveRelationImage(brandCol(), {
|
|
83
|
+
brand: { value: 'u', label: 'X', avatar: 'a.png' },
|
|
84
|
+
}),
|
|
85
|
+
).toBe('a.png')
|
|
86
|
+
expect(
|
|
87
|
+
resolveRelationImage(brandCol(), {
|
|
88
|
+
brand: { value: 'u', label: 'X', photo: 'p.png' },
|
|
89
|
+
}),
|
|
90
|
+
).toBe('p.png')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('returns empty string when the sibling carries no image (text-only chip)', () => {
|
|
94
|
+
const row = { brand_id: 'uuid-2', brand: { value: 'uuid-2', label: 'Genérica' } }
|
|
95
|
+
expect(resolveRelationImage(brandCol(), row)).toBe('')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('returns empty string when there is no sibling at all', () => {
|
|
99
|
+
expect(resolveRelationImage(brandCol(), { brand_id: 'uuid-3' })).toBe('')
|
|
100
|
+
expect(resolveRelationImage(brandCol(), {})).toBe('')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('ignores an empty-string image (no broken thumbnail)', () => {
|
|
104
|
+
const row = { brand: { value: 'u', label: 'X', image: '' } }
|
|
105
|
+
expect(resolveRelationImage(brandCol(), row)).toBe('')
|
|
106
|
+
})
|
|
107
|
+
})
|
package/src/dynamic-columns.tsx
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import * as React from 'react'
|
|
16
16
|
import { ColumnDef } from '@tanstack/react-table'
|
|
17
|
-
import { format } from 'date-fns'
|
|
17
|
+
import { format, type Locale } from 'date-fns'
|
|
18
18
|
import { es, enUS } from 'date-fns/locale'
|
|
19
19
|
import * as icons from 'lucide-react'
|
|
20
20
|
import { MoreHorizontal } from 'lucide-react'
|
|
@@ -272,12 +272,42 @@ const renderRelationBadges = (items: any, col: ColumnDefinition) => {
|
|
|
272
272
|
)
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Tiny square thumbnail for a resolved relation/option that carries an `image`
|
|
277
|
+
* (brand logo, product photo, customer/user avatar). Uses the same Avatar
|
|
278
|
+
* primitive as the `avatar`/`creator` cells so a broken/loading image
|
|
279
|
+
* gracefully falls back to the record's initials. Sized small (the box is an
|
|
280
|
+
* inline style so an addon-arbitrary Tailwind class never gets dropped by a
|
|
281
|
+
* consuming app's class scan). Rendered inline alongside a label — never alone.
|
|
282
|
+
*/
|
|
283
|
+
const RelationThumbnail: React.FC<{
|
|
284
|
+
src: string
|
|
285
|
+
alt: string
|
|
286
|
+
getImageUrl?: (path: string) => string
|
|
287
|
+
size?: number
|
|
288
|
+
}> = ({ src, alt, getImageUrl, size = 18 }) => (
|
|
289
|
+
<Avatar
|
|
290
|
+
className="shrink-0 rounded-sm ring-1 ring-border/40"
|
|
291
|
+
style={{ width: size, height: size }}
|
|
292
|
+
>
|
|
293
|
+
<AvatarImage
|
|
294
|
+
src={getImageUrl ? getImageUrl(src) : src}
|
|
295
|
+
alt={alt}
|
|
296
|
+
className="object-cover"
|
|
297
|
+
/>
|
|
298
|
+
<AvatarFallback className="rounded-sm bg-primary/10 text-[8px] font-bold text-primary">
|
|
299
|
+
{getInitials(alt)}
|
|
300
|
+
</AvatarFallback>
|
|
301
|
+
</Avatar>
|
|
302
|
+
)
|
|
303
|
+
|
|
275
304
|
interface OptionBadgeProps {
|
|
276
|
-
option: { value: string; label: string; icon?: string; color?: string }
|
|
305
|
+
option: { value: string; label: string; icon?: string; color?: string; image?: string }
|
|
277
306
|
fallback: string
|
|
307
|
+
getImageUrl?: (path: string) => string
|
|
278
308
|
}
|
|
279
309
|
|
|
280
|
-
const OptionBadge: React.FC<OptionBadgeProps> = ({ option }) => {
|
|
310
|
+
const OptionBadge: React.FC<OptionBadgeProps> = ({ option, getImageUrl }) => {
|
|
281
311
|
const isDark = useIsDarkTheme()
|
|
282
312
|
// Explicit backend color wins; otherwise derive a stable, cohesive color
|
|
283
313
|
// from the option's value (fallback label) so "dead" gray badges come
|
|
@@ -287,17 +317,30 @@ const OptionBadge: React.FC<OptionBadgeProps> = ({ option }) => {
|
|
|
287
317
|
const colorStyles = generateBadgeStyles(colorSource, { isDark })
|
|
288
318
|
return (
|
|
289
319
|
<Badge variant="outline" className="flex items-center gap-1 border-0" style={colorStyles}>
|
|
290
|
-
{option.
|
|
320
|
+
{option.image ? (
|
|
321
|
+
<RelationThumbnail
|
|
322
|
+
src={option.image}
|
|
323
|
+
alt={option.label}
|
|
324
|
+
getImageUrl={getImageUrl}
|
|
325
|
+
size={16}
|
|
326
|
+
/>
|
|
327
|
+
) : (
|
|
328
|
+
option.icon && <DynamicIcon name={option.icon} className="h-3.5 w-3.5" />
|
|
329
|
+
)}
|
|
291
330
|
<span>{option.label}</span>
|
|
292
331
|
</Badge>
|
|
293
332
|
)
|
|
294
333
|
}
|
|
295
334
|
|
|
296
|
-
const BadgeWithEndpointOptions: React.FC<{
|
|
335
|
+
const BadgeWithEndpointOptions: React.FC<{
|
|
336
|
+
endpoint: string
|
|
337
|
+
value: any
|
|
338
|
+
getImageUrl?: (path: string) => string
|
|
339
|
+
}> = ({ endpoint, value, getImageUrl }) => {
|
|
297
340
|
const { optionsMap } = React.useContext(OptionsContext)
|
|
298
341
|
const options = optionsMap.get(endpoint) || []
|
|
299
342
|
const option = options.find((opt: any) => opt.value === value)
|
|
300
|
-
if (option) return <OptionBadge option={option} fallback={String(value)} />
|
|
343
|
+
if (option) return <OptionBadge option={option} fallback={String(value)} getImageUrl={getImageUrl} />
|
|
301
344
|
// No declared option matched → humanize the raw token as a safety net so a
|
|
302
345
|
// cell never shows `in_progress` verbatim (option.label still wins above).
|
|
303
346
|
return <Badge variant="outline">{humanizeToken(value)}</Badge>
|
|
@@ -316,6 +359,35 @@ export const relationKeyFor = (col: Pick<ColumnDefinition, 'key'>): string => {
|
|
|
316
359
|
return k.endsWith('_id') ? k.slice(0, -3) : k
|
|
317
360
|
}
|
|
318
361
|
|
|
362
|
+
/** Cell renderers (`cellStyle`/`type`) that resolve to the date renderer. */
|
|
363
|
+
export const DATE_CELL_TYPES = ['date', 'datetime', 'timestamp', 'timestamptz'] as const
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Pure formatter behind the date/datetime cell. Returns the display string and
|
|
367
|
+
* an optional full-precision `title` (tooltip), or `null` when the value is
|
|
368
|
+
* empty/invalid/the Go zero-time so the cell renders an em-dash.
|
|
369
|
+
* - `date`: day only (`PPP`), no tooltip.
|
|
370
|
+
* - `datetime`/`timestamp(tz)`: day + time (`Pp`) with a full-precision
|
|
371
|
+
* tooltip (`PPpp`) — the 7Leguas pattern.
|
|
372
|
+
*/
|
|
373
|
+
export function formatDateCell(
|
|
374
|
+
value: unknown,
|
|
375
|
+
renderAs: string | undefined,
|
|
376
|
+
locale: Locale,
|
|
377
|
+
): { display: string; title?: string } | null {
|
|
378
|
+
if (value === null || value === undefined || value === '') return null
|
|
379
|
+
const date = new Date(value as any)
|
|
380
|
+
if (isNaN(date.getTime()) || date.getFullYear() <= 1) return null
|
|
381
|
+
const withTime = renderAs !== 'date'
|
|
382
|
+
if (withTime) {
|
|
383
|
+
return {
|
|
384
|
+
display: format(date, 'Pp', { locale }),
|
|
385
|
+
title: format(date, 'PPpp', { locale }),
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return { display: format(date, 'PPP', { locale }) }
|
|
389
|
+
}
|
|
390
|
+
|
|
319
391
|
/**
|
|
320
392
|
* Reads the resolved relation/option label a backend serves for an FK or
|
|
321
393
|
* option column, falling back to the raw value. Pure so the cell renderers and
|
|
@@ -337,17 +409,39 @@ export const resolveRelationLabel = (col: ColumnDefinition, row: any): string =>
|
|
|
337
409
|
return String(raw)
|
|
338
410
|
}
|
|
339
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Reads the thumbnail URL a backend serves on a resolved FK sibling, when
|
|
414
|
+
* present. The backend stamps `image` onto the `{ value, label }` relation
|
|
415
|
+
* object when the referenced model carries an image column (brand logo,
|
|
416
|
+
* product photo, customer avatar). Returns '' when there is no sibling image —
|
|
417
|
+
* the chip then renders text-only, exactly as before.
|
|
418
|
+
*/
|
|
419
|
+
export const resolveRelationImage = (col: ColumnDefinition, row: any): string => {
|
|
420
|
+
const sibling = getNestedValue(row, relationKeyFor(col))
|
|
421
|
+
if (sibling && typeof sibling === 'object') {
|
|
422
|
+
const img = sibling.image ?? sibling.avatar ?? sibling.photo
|
|
423
|
+
if (img !== undefined && img !== null && img !== '') return String(img)
|
|
424
|
+
}
|
|
425
|
+
return ''
|
|
426
|
+
}
|
|
427
|
+
|
|
340
428
|
/**
|
|
341
429
|
* Renders a resolved FK relation as a clean, truncated chip. Reads the
|
|
342
|
-
* backend-resolved sibling `{ value, label }` (see `relationKeyFor`)
|
|
343
|
-
* its `label
|
|
344
|
-
*
|
|
345
|
-
*
|
|
430
|
+
* backend-resolved sibling `{ value, label[, image] }` (see `relationKeyFor`)
|
|
431
|
+
* and shows its `label`, prefixed with a small thumbnail when the sibling
|
|
432
|
+
* carries an `image`. Falls back to the raw id when no sibling was resolved, and
|
|
433
|
+
* to an empty marker when there is no value at all. Domain-agnostic: works for
|
|
434
|
+
* every `belongs_to` column (category, supplier, brand, …) without per-addon code.
|
|
346
435
|
*/
|
|
347
|
-
const RelationCell: React.FC<{
|
|
436
|
+
const RelationCell: React.FC<{
|
|
437
|
+
col: ColumnDefinition
|
|
438
|
+
row: any
|
|
439
|
+
getImageUrl?: (path: string) => string
|
|
440
|
+
}> = ({ col, row, getImageUrl }) => {
|
|
348
441
|
const isDark = useIsDarkTheme()
|
|
349
442
|
const display = resolveRelationLabel(col, row)
|
|
350
443
|
if (!display) return <EmptyCell />
|
|
444
|
+
const image = resolveRelationImage(col, row)
|
|
351
445
|
// Deterministic, SUBTLE color keyed on the resolved label — lighter than
|
|
352
446
|
// enum badges (soft tint, no heavy fill) so category/brand chips read as
|
|
353
447
|
// alive yet stay visually distinct from option/status badges. Inline style
|
|
@@ -355,10 +449,13 @@ const RelationCell: React.FC<{ col: ColumnDefinition; row: any }> = ({ col, row
|
|
|
355
449
|
const chipStyles = relationChipStyles(display, { isDark })
|
|
356
450
|
return (
|
|
357
451
|
<span
|
|
358
|
-
className="inline-flex max-w-[220px] items-center
|
|
452
|
+
className="inline-flex max-w-[220px] items-center gap-1.5 rounded-md px-2 py-0.5 text-sm font-medium"
|
|
359
453
|
style={chipStyles}
|
|
360
454
|
title={display}
|
|
361
455
|
>
|
|
456
|
+
{image && (
|
|
457
|
+
<RelationThumbnail src={image} alt={display} getImageUrl={getImageUrl} size={18} />
|
|
458
|
+
)}
|
|
362
459
|
<span className="truncate">{display}</span>
|
|
363
460
|
</span>
|
|
364
461
|
)
|
|
@@ -494,14 +591,14 @@ export function makeDefaultGetDynamicColumns(
|
|
|
494
591
|
// Endpoint-loaded badge options (preloaded into OptionsContext)
|
|
495
592
|
if (renderAs === 'badge' && col.useOptions && col.searchEndpoint) {
|
|
496
593
|
if (!value) return <span className="text-muted-foreground">-</span>
|
|
497
|
-
return <BadgeWithEndpointOptions endpoint={col.searchEndpoint} value={value} />
|
|
594
|
+
return <BadgeWithEndpointOptions endpoint={col.searchEndpoint} value={value} getImageUrl={getImageUrl} />
|
|
498
595
|
}
|
|
499
596
|
|
|
500
597
|
// Static badge options — map value → label/icon/color
|
|
501
598
|
if (renderAs === 'badge' && col.options && col.options.length > 0) {
|
|
502
599
|
if (!value && value !== 0) return <span className="text-muted-foreground">-</span>
|
|
503
600
|
const option = col.options.find((o) => o.value === String(value))
|
|
504
|
-
if (option) return <OptionBadge option={option} fallback={String(value)} />
|
|
601
|
+
if (option) return <OptionBadge option={option} fallback={String(value)} getImageUrl={getImageUrl} />
|
|
505
602
|
return <Badge variant="outline">{humanizeToken(value)}</Badge>
|
|
506
603
|
}
|
|
507
604
|
|
|
@@ -521,7 +618,7 @@ export function makeDefaultGetDynamicColumns(
|
|
|
521
618
|
if (!value && value !== 0) return <EmptyCell />
|
|
522
619
|
const sv = String(value)
|
|
523
620
|
const option = col.options?.find((o) => o.value === sv)
|
|
524
|
-
if (option) return <OptionBadge option={option} fallback={sv} />
|
|
621
|
+
if (option) return <OptionBadge option={option} fallback={sv} getImageUrl={getImageUrl} />
|
|
525
622
|
const isDark =
|
|
526
623
|
typeof document !== 'undefined' &&
|
|
527
624
|
document.documentElement.classList.contains('dark')
|
|
@@ -544,7 +641,7 @@ export function makeDefaultGetDynamicColumns(
|
|
|
544
641
|
renderAs === 'relation' ||
|
|
545
642
|
(col.ref && !col.options?.length && renderAs !== 'badge' && renderAs !== 'status')
|
|
546
643
|
) {
|
|
547
|
-
return <RelationCell col={col} row={row.original} />
|
|
644
|
+
return <RelationCell col={col} row={row.original} getImageUrl={getImageUrl} />
|
|
548
645
|
}
|
|
549
646
|
|
|
550
647
|
// Option/type column: a `select`-style column ships its
|
|
@@ -559,29 +656,29 @@ export function makeDefaultGetDynamicColumns(
|
|
|
559
656
|
) {
|
|
560
657
|
if (!value && value !== 0) return <EmptyCell />
|
|
561
658
|
const option = col.options.find((o) => o.value === String(value))
|
|
562
|
-
if (option) return <OptionBadge option={option} fallback={String(value)} />
|
|
659
|
+
if (option) return <OptionBadge option={option} fallback={String(value)} getImageUrl={getImageUrl} />
|
|
563
660
|
return <Badge variant="outline">{humanizeToken(value)}</Badge>
|
|
564
661
|
}
|
|
565
662
|
|
|
566
663
|
switch (renderAs) {
|
|
567
|
-
case 'date':
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
664
|
+
case 'date':
|
|
665
|
+
case 'datetime':
|
|
666
|
+
case 'timestamp':
|
|
667
|
+
case 'timestamptz': {
|
|
668
|
+
const formatted = formatDateCell(value, renderAs, dateLocale)
|
|
669
|
+
if (!formatted)
|
|
670
|
+
return <span className="text-muted-foreground">-</span>
|
|
671
|
+
return (
|
|
672
|
+
<div
|
|
673
|
+
className="flex items-center gap-1.5 text-muted-foreground"
|
|
674
|
+
title={formatted.title}
|
|
675
|
+
>
|
|
676
|
+
<icons.Calendar className="h-3.5 w-3.5 opacity-70" />
|
|
677
|
+
<span className="text-sm font-medium">
|
|
678
|
+
{formatted.display}
|
|
679
|
+
</span>
|
|
680
|
+
</div>
|
|
681
|
+
)
|
|
585
682
|
}
|
|
586
683
|
|
|
587
684
|
case 'search':
|
package/src/dynamic-table.tsx
CHANGED
|
@@ -65,7 +65,7 @@ import { Progress } from './dialogs/_primitives'
|
|
|
65
65
|
import { useMetadataCache } from './metadata-cache'
|
|
66
66
|
import { useApi, useCurrentBranch } from './api-context'
|
|
67
67
|
import type { ColumnFilterConfig, GetDynamicColumns } from './dynamic-columns-shim'
|
|
68
|
-
import { defaultGetDynamicColumns } from './dynamic-columns'
|
|
68
|
+
import { defaultGetDynamicColumns, DATE_CELL_TYPES } from './dynamic-columns'
|
|
69
69
|
import { OptionsContext } from './options-context'
|
|
70
70
|
import { ActionModalDispatcher } from './action-modal-dispatcher'
|
|
71
71
|
import type { TableMetadata, ApiResponse, ActionMetadata } from './types'
|
|
@@ -555,7 +555,8 @@ export function DynamicTable({
|
|
|
555
555
|
else if (hasStaticOptions || hasEndpoint) filterType = 'select'
|
|
556
556
|
else if (c.type === 'boolean') filterType = 'boolean'
|
|
557
557
|
else if (c.type === 'number') filterType = 'number_range'
|
|
558
|
-
else if (c.type
|
|
558
|
+
else if ((DATE_CELL_TYPES as readonly string[]).includes(c.type))
|
|
559
|
+
filterType = 'date_range'
|
|
559
560
|
else filterType = 'text'
|
|
560
561
|
|
|
561
562
|
const options = hasStaticOptions
|
package/src/types.ts
CHANGED
|
@@ -87,6 +87,11 @@ export interface ColumnDefinition {
|
|
|
87
87
|
| 'text'
|
|
88
88
|
| 'number'
|
|
89
89
|
| 'date'
|
|
90
|
+
// Timestamp variants. They share the `date` cell renderer but append
|
|
91
|
+
// the time + a full-precision tooltip (see formatDateCell).
|
|
92
|
+
| 'datetime'
|
|
93
|
+
| 'timestamp'
|
|
94
|
+
| 'timestamptz'
|
|
90
95
|
| 'select'
|
|
91
96
|
| 'search'
|
|
92
97
|
| 'relation-badge-list'
|