@asteby/metacore-runtime-react 17.0.4 → 18.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 +27 -0
- package/dist/dynamic-columns-shim.d.ts +1 -1
- package/dist/dynamic-columns-shim.d.ts.map +1 -1
- package/dist/dynamic-columns.d.ts +11 -1
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +46 -3
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +28 -3
- package/dist/dynamic-table.d.ts +8 -1
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +3 -3
- package/package.json +3 -3
- package/src/__tests__/format-date-cell.test.ts +27 -0
- package/src/dynamic-columns-shim.ts +1 -0
- package/src/dynamic-columns.tsx +46 -1
- package/src/dynamic-select-field.tsx +54 -6
- package/src/dynamic-table.tsx +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 18.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 4e601ec: Org-timezone-aware date display in dynamic tables.
|
|
8
|
+
|
|
9
|
+
`formatDateCell` and the column factory (`defaultGetDynamicColumns` /
|
|
10
|
+
`makeDefaultGetDynamicColumns`) now accept an optional IANA `timeZone`, and
|
|
11
|
+
`DynamicTable` exposes a matching `timeZone` prop. When provided, datetime /
|
|
12
|
+
timestamp(tz) cells are rendered in that zone via the native
|
|
13
|
+
`Intl.DateTimeFormat` (instead of the viewer's browser zone), so instants no
|
|
14
|
+
longer day-shift; pure `date` columns are pinned to UTC so they never roll to
|
|
15
|
+
the previous/next day. Omitting `timeZone` preserves the exact legacy date-fns
|
|
16
|
+
formatting (fully backward-compatible).
|
|
17
|
+
|
|
18
|
+
## 18.0.0
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- ce9dd72: `DynamicSelectField` (the searchable FK / option picker) now renders each
|
|
23
|
+
option's leading visual: a photo thumbnail (FK relations with an image), else a
|
|
24
|
+
declared icon, else a colored dot for enum/status options that carry a `color`.
|
|
25
|
+
Previously only image thumbnails showed, so enum selects (state, origin, …) read
|
|
26
|
+
as plain text. Plain options with no image/color/icon stay plain.
|
|
27
|
+
- Updated dependencies [8439e9e]
|
|
28
|
+
- @asteby/metacore-ui@2.5.0
|
|
29
|
+
|
|
3
30
|
## 17.0.4
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
|
@@ -16,7 +16,7 @@ export interface ColumnFilterConfig {
|
|
|
16
16
|
searchEndpoint?: string;
|
|
17
17
|
}
|
|
18
18
|
/** Signature for the host-provided `getDynamicColumns` factory. */
|
|
19
|
-
export type GetDynamicColumns = (metadata: TableMetadata, handleAction: (action: string, row: any) => void, t: (key: string, options?: any) => string, language: string, columnFilterConfigs: Map<string, ColumnFilterConfig
|
|
19
|
+
export type GetDynamicColumns = (metadata: TableMetadata, handleAction: (action: string, row: any) => void, t: (key: string, options?: any) => string, language: string, columnFilterConfigs: Map<string, ColumnFilterConfig>, timeZone?: string) => ColumnDef<any>[];
|
|
20
20
|
/** Signature for the host-provided `DynamicIcon` renderer. */
|
|
21
21
|
export type DynamicIconComponent = React.ComponentType<{
|
|
22
22
|
name: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-columns-shim.d.ts","sourceRoot":"","sources":["../src/dynamic-columns-shim.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IAC/B,UAAU,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,MAAM,GAAG,MAAM,CAAA;IAClF,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,mEAAmE;AACnE,MAAM,MAAM,iBAAiB,GAAG,CAC5B,QAAQ,EAAE,aAAa,EACvB,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,EAChD,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,MAAM,EACzC,QAAQ,EAAE,MAAM,EAChB,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,
|
|
1
|
+
{"version":3,"file":"dynamic-columns-shim.d.ts","sourceRoot":"","sources":["../src/dynamic-columns-shim.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IAC/B,UAAU,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,MAAM,GAAG,MAAM,CAAA;IAClF,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,mEAAmE;AACnE,MAAM,MAAM,iBAAiB,GAAG,CAC5B,QAAQ,EAAE,aAAa,EACvB,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,EAChD,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,MAAM,EACzC,QAAQ,EAAE,MAAM,EAChB,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,EACpD,QAAQ,CAAC,EAAE,MAAM,KAChB,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;AAErB,8DAA8D;AAC9D,MAAM,MAAM,oBAAoB,GAAG,KAAK,CAAC,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA"}
|
|
@@ -47,8 +47,18 @@ export declare const DATE_CELL_TYPES: readonly ["date", "datetime", "timestamp",
|
|
|
47
47
|
* - `date`: day only (`PPP`), no tooltip.
|
|
48
48
|
* - `datetime`/`timestamp(tz)`: day + time (`Pp`) with a full-precision
|
|
49
49
|
* tooltip (`PPpp`) — the 7Leguas pattern.
|
|
50
|
+
*
|
|
51
|
+
* When a `timeZone` (IANA, e.g. the org's `America/Mexico_City`) is provided,
|
|
52
|
+
* instants are rendered in that zone via the native `Intl.DateTimeFormat` so
|
|
53
|
+
* the displayed day/time never shifts with the viewer's browser timezone:
|
|
54
|
+
* - instant (datetime/timestamp(tz)): `dateStyle:'medium' timeStyle:'short'`
|
|
55
|
+
* in the org zone, with a `dateStyle:'long' timeStyle:'medium'` +
|
|
56
|
+
* `timeZoneName:'short'` tooltip.
|
|
57
|
+
* - `date` (pure calendar day): rendered pinned to UTC so it never rolls to
|
|
58
|
+
* the previous/next day, no tooltip.
|
|
59
|
+
* Without a `timeZone`, the exact date-fns behavior is preserved (back-compat).
|
|
50
60
|
*/
|
|
51
|
-
export declare function formatDateCell(value: unknown, renderAs: string | undefined, locale: Locale): {
|
|
61
|
+
export declare function formatDateCell(value: unknown, renderAs: string | undefined, locale: Locale, timeZone?: string): {
|
|
52
62
|
display: string;
|
|
53
63
|
title?: string;
|
|
54
64
|
} | null;
|
|
@@ -1 +1 @@
|
|
|
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
|
|
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;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;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,CAumBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
|
package/dist/dynamic-columns.js
CHANGED
|
@@ -219,14 +219,57 @@ export const DATE_CELL_TYPES = ['date', 'datetime', 'timestamp', 'timestamptz'];
|
|
|
219
219
|
* - `date`: day only (`PPP`), no tooltip.
|
|
220
220
|
* - `datetime`/`timestamp(tz)`: day + time (`Pp`) with a full-precision
|
|
221
221
|
* tooltip (`PPpp`) — the 7Leguas pattern.
|
|
222
|
+
*
|
|
223
|
+
* When a `timeZone` (IANA, e.g. the org's `America/Mexico_City`) is provided,
|
|
224
|
+
* instants are rendered in that zone via the native `Intl.DateTimeFormat` so
|
|
225
|
+
* the displayed day/time never shifts with the viewer's browser timezone:
|
|
226
|
+
* - instant (datetime/timestamp(tz)): `dateStyle:'medium' timeStyle:'short'`
|
|
227
|
+
* in the org zone, with a `dateStyle:'long' timeStyle:'medium'` +
|
|
228
|
+
* `timeZoneName:'short'` tooltip.
|
|
229
|
+
* - `date` (pure calendar day): rendered pinned to UTC so it never rolls to
|
|
230
|
+
* the previous/next day, no tooltip.
|
|
231
|
+
* Without a `timeZone`, the exact date-fns behavior is preserved (back-compat).
|
|
222
232
|
*/
|
|
223
|
-
export function formatDateCell(value, renderAs, locale) {
|
|
233
|
+
export function formatDateCell(value, renderAs, locale, timeZone) {
|
|
224
234
|
if (value === null || value === undefined || value === '')
|
|
225
235
|
return null;
|
|
226
236
|
const date = new Date(value);
|
|
227
237
|
if (isNaN(date.getTime()) || date.getFullYear() <= 1)
|
|
228
238
|
return null;
|
|
229
239
|
const withTime = renderAs !== 'date';
|
|
240
|
+
if (timeZone) {
|
|
241
|
+
// `locale.code` is the BCP-47 tag date-fns ships (e.g. 'es', 'en-US').
|
|
242
|
+
const localeTag = locale?.code || undefined;
|
|
243
|
+
if (withTime) {
|
|
244
|
+
return {
|
|
245
|
+
display: new Intl.DateTimeFormat(localeTag, {
|
|
246
|
+
timeZone,
|
|
247
|
+
dateStyle: 'medium',
|
|
248
|
+
timeStyle: 'short',
|
|
249
|
+
}).format(date),
|
|
250
|
+
// `dateStyle`/`timeStyle` can't be combined with explicit
|
|
251
|
+
// component options like `timeZoneName`, so spell the tooltip
|
|
252
|
+
// out: long date + seconds + the zone abbreviation.
|
|
253
|
+
title: new Intl.DateTimeFormat(localeTag, {
|
|
254
|
+
timeZone,
|
|
255
|
+
year: 'numeric',
|
|
256
|
+
month: 'long',
|
|
257
|
+
day: 'numeric',
|
|
258
|
+
hour: '2-digit',
|
|
259
|
+
minute: '2-digit',
|
|
260
|
+
second: '2-digit',
|
|
261
|
+
timeZoneName: 'short',
|
|
262
|
+
}).format(date),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
// Pure calendar date: pin to UTC so it never shifts across zones.
|
|
266
|
+
return {
|
|
267
|
+
display: new Intl.DateTimeFormat(localeTag, {
|
|
268
|
+
timeZone: 'UTC',
|
|
269
|
+
dateStyle: 'long',
|
|
270
|
+
}).format(date),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
230
273
|
if (withTime) {
|
|
231
274
|
return {
|
|
232
275
|
display: format(date, 'Pp', { locale }),
|
|
@@ -308,7 +351,7 @@ const AvatarCell = ({ name, desc, avatarSrc, getImageUrl }) => (_jsxs("div", { c
|
|
|
308
351
|
export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
309
352
|
const getImageUrl = helpers.getImageUrl ?? defaultGetImageUrl;
|
|
310
353
|
const apiBaseUrl = helpers.apiBaseUrl ?? '';
|
|
311
|
-
return function defaultGetDynamicColumns(metadata, onAction, t, currentLanguage, filterConfigs) {
|
|
354
|
+
return function defaultGetDynamicColumns(metadata, onAction, t, currentLanguage, filterConfigs, timeZone) {
|
|
312
355
|
const dateLocale = currentLanguage === 'en' ? enUS : es;
|
|
313
356
|
const columns = [
|
|
314
357
|
{
|
|
@@ -426,7 +469,7 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
|
426
469
|
case 'datetime':
|
|
427
470
|
case 'timestamp':
|
|
428
471
|
case 'timestamptz': {
|
|
429
|
-
const formatted = formatDateCell(value, renderAs, dateLocale);
|
|
472
|
+
const formatted = formatDateCell(value, renderAs, dateLocale, timeZone);
|
|
430
473
|
if (!formatted)
|
|
431
474
|
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
432
475
|
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 })] }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AAyCA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AA+F7C,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;CAC7B;AAED,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,uBAAuB,+BA0KrF;AAED,eAAe,kBAAkB,CAAA"}
|
|
@@ -25,6 +25,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
25
25
|
import { useEffect, useState } from 'react';
|
|
26
26
|
import { Button, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Popover, PopoverContent, PopoverTrigger, } from '@asteby/metacore-ui/primitives';
|
|
27
27
|
import { Check, ChevronsUpDown, ImageIcon, Loader2, Plus } from 'lucide-react';
|
|
28
|
+
import { resolveColorCss } from '@asteby/metacore-ui/lib';
|
|
29
|
+
import { DynamicIcon } from './dynamic-icon';
|
|
28
30
|
import { useOptionsResolver } from './use-options-resolver';
|
|
29
31
|
import { getFieldRef } from './dynamic-form-schema';
|
|
30
32
|
/**
|
|
@@ -46,6 +48,29 @@ function OptionThumb({ image, size = 20 }) {
|
|
|
46
48
|
e.currentTarget.style.visibility = 'hidden';
|
|
47
49
|
} }));
|
|
48
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Leading visual for an option: a photo thumbnail (FK relations with an image),
|
|
53
|
+
* else a declared icon, else a color dot (enum/status options with a color).
|
|
54
|
+
* Returns null when the option carries none, so plain text options stay plain.
|
|
55
|
+
*/
|
|
56
|
+
function OptionLead({ option, size = 20, }) {
|
|
57
|
+
if (!option)
|
|
58
|
+
return null;
|
|
59
|
+
if (option.image)
|
|
60
|
+
return _jsx(OptionThumb, { image: option.image, size: size });
|
|
61
|
+
if (option.icon) {
|
|
62
|
+
return (_jsx("span", { className: "flex shrink-0 items-center justify-center", style: { width: size, height: size, color: option.color ? resolveColorCss(option.color) : undefined }, "aria-hidden": true, children: _jsx(DynamicIcon, { name: option.icon, className: "size-4" }) }));
|
|
63
|
+
}
|
|
64
|
+
if (option.color) {
|
|
65
|
+
return (_jsx("span", { className: "shrink-0 rounded-full", style: { width: Math.round(size * 0.5), height: Math.round(size * 0.5), background: resolveColorCss(option.color) }, "aria-hidden": true }));
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/** True when any option (or the selected one) carries a renderable visual. */
|
|
70
|
+
function optionsHaveVisual(options, selected) {
|
|
71
|
+
const has = (o) => !!(o && (o.image || o.color || o.icon));
|
|
72
|
+
return has(selected) || options.some(has);
|
|
73
|
+
}
|
|
49
74
|
function useDebounced(value, ms) {
|
|
50
75
|
const [debounced, setDebounced] = useState(value);
|
|
51
76
|
useEffect(() => {
|
|
@@ -87,7 +112,7 @@ export function DynamicSelectField({ field, value, onChange }) {
|
|
|
87
112
|
// Only switch the picker into "with thumbnails" mode when the data actually
|
|
88
113
|
// carries images — a relation whose options have no `image` keeps the plain
|
|
89
114
|
// text list it had before (no empty placeholder column).
|
|
90
|
-
const
|
|
115
|
+
const hasVisual = optionsHaveVisual(options, selectedOption);
|
|
91
116
|
const handlePick = (opt) => {
|
|
92
117
|
setPicked(opt);
|
|
93
118
|
onChange(String(opt.id));
|
|
@@ -119,12 +144,12 @@ export function DynamicSelectField({ field, value, onChange }) {
|
|
|
119
144
|
// to the cell. Without min-w-0 the combobox+button row sizes to its content
|
|
120
145
|
// (the long empty-state placeholder) and overflows the column, pushing the
|
|
121
146
|
// "+" off-screen — it only "fit" once a short value was selected.
|
|
122
|
-
return (_jsxs("div", { className: "flex w-full min-w-0 items-center gap-1.5", children: [_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", role: "combobox", "aria-expanded": open, id: field.key, className: "min-w-0 flex-1 justify-between font-normal", "data-empty": !value, children: [_jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-2 text-left", children: [
|
|
147
|
+
return (_jsxs("div", { className: "flex w-full min-w-0 items-center gap-1.5", children: [_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", role: "combobox", "aria-expanded": open, id: field.key, className: "min-w-0 flex-1 justify-between font-normal", "data-empty": !value, children: [_jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-2 text-left", children: [hasVisual && value ? (_jsx(OptionLead, { option: selectedOption, size: 20 })) : null, _jsx("span", { className: 'min-w-0 flex-1 truncate ' + (selectedLabel ? '' : 'text-muted-foreground'), children: selectedLabel || field.placeholder || 'Buscar…' })] }), _jsx(ChevronsUpDown, { className: "ml-2 size-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "p-0", align: "start",
|
|
123
148
|
// Match the trigger width without an arbitrary Tailwind class
|
|
124
149
|
// (those don't always survive a consuming app's Tailwind scan).
|
|
125
150
|
style: { width: 'var(--radix-popover-trigger-width)' }, children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { placeholder: field.placeholder || 'Buscar…', value: search, onValueChange: setSearch }), _jsxs(CommandList, { children: [loading && (_jsxs("div", { className: "text-muted-foreground flex items-center justify-center gap-2 py-6 text-sm", children: [_jsx(Loader2, { className: "size-4 animate-spin" }), "Buscando\u2026"] })), !loading && options.length === 0 && (_jsx(CommandEmpty, { children: debounced ? 'Sin resultados' : 'Escribí para buscar…' })), !loading && options.length > 0 && (_jsx(CommandGroup, { className: "max-h-64 overflow-auto", children: options.map((opt) => {
|
|
126
151
|
const isSel = String(opt.id) === String(value);
|
|
127
|
-
return (_jsxs(CommandItem, { value: String(opt.id), onSelect: () => handlePick(opt), children: [_jsx(Check, { className: 'mr-2 size-4 shrink-0 ' + (isSel ? 'opacity-100' : 'opacity-0') }),
|
|
152
|
+
return (_jsxs(CommandItem, { value: String(opt.id), onSelect: () => handlePick(opt), children: [_jsx(Check, { className: 'mr-2 size-4 shrink-0 ' + (isSel ? 'opacity-100' : 'opacity-0') }), hasVisual && (_jsx(OptionLead, { option: opt, size: 24 })), _jsxs("div", { className: "ml-2 flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate", children: opt.label }), opt.description && (_jsx("span", { className: "text-muted-foreground truncate text-xs", children: opt.description }))] })] }, String(opt.id)));
|
|
128
153
|
}) }))] })] }) })] }), fieldRef && (_jsx(Button, { type: "button", variant: "outline", size: "icon", className: "size-9 shrink-0", onClick: openCreate, title: `Crear ${field.label ?? fieldRef}`, "aria-label": `Crear ${field.label ?? fieldRef}`, children: _jsx(Plus, { className: "size-4" }) }))] }));
|
|
129
154
|
}
|
|
130
155
|
export default DynamicSelectField;
|
package/dist/dynamic-table.d.ts
CHANGED
|
@@ -16,7 +16,14 @@ interface DynamicTableProps {
|
|
|
16
16
|
* Optional — a sensible default maps each column to { accessorKey, header }.
|
|
17
17
|
*/
|
|
18
18
|
getDynamicColumns?: GetDynamicColumns;
|
|
19
|
+
/**
|
|
20
|
+
* IANA timezone (e.g. the org's `America/Mexico_City`) used to render
|
|
21
|
+
* datetime/timestamp cells. When provided, instants are displayed in this
|
|
22
|
+
* zone instead of the viewer's browser zone, so the day/time never shifts.
|
|
23
|
+
* Optional — omitting it preserves the legacy browser-local formatting.
|
|
24
|
+
*/
|
|
25
|
+
timeZone?: string;
|
|
19
26
|
}
|
|
20
|
-
export declare function DynamicTable({ model, endpoint, enableUrlSync, hiddenColumns, onAction, refreshTrigger, defaultFilters, extraColumns, getDynamicColumns, }: DynamicTableProps): import("react").JSX.Element;
|
|
27
|
+
export declare function DynamicTable({ model, endpoint, enableUrlSync, hiddenColumns, onAction, refreshTrigger, defaultFilters, extraColumns, getDynamicColumns, timeZone, }: DynamicTableProps): import("react").JSX.Element;
|
|
21
28
|
export {};
|
|
22
29
|
//# sourceMappingURL=dynamic-table.d.ts.map
|
|
@@ -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;
|
|
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;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,GACX,EAAE,iBAAiB,+BAgyBnB"}
|
package/dist/dynamic-table.js
CHANGED
|
@@ -31,7 +31,7 @@ import { getSearchableColumnKeys } from './column-visibility';
|
|
|
31
31
|
import { DynamicRecordDialog } from './dialogs/dynamic-record';
|
|
32
32
|
import { ExportDialog } from './dialogs/export';
|
|
33
33
|
import { ImportDialog } from './dialogs/import';
|
|
34
|
-
export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColumns = [], onAction, refreshTrigger, defaultFilters, extraColumns = [], getDynamicColumns = defaultGetDynamicColumns, }) {
|
|
34
|
+
export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColumns = [], onAction, refreshTrigger, defaultFilters, extraColumns = [], getDynamicColumns = defaultGetDynamicColumns, timeZone, }) {
|
|
35
35
|
const { t, i18n } = useTranslation();
|
|
36
36
|
const api = useApi();
|
|
37
37
|
const currentBranch = useCurrentBranch();
|
|
@@ -555,12 +555,12 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
555
555
|
const rowMetadata = metadata.actions?.some((a) => a.placement === 'table' || a.placement === 'create')
|
|
556
556
|
? { ...metadata, actions: metadata.actions.filter((a) => !a.placement || a.placement === 'row') }
|
|
557
557
|
: metadata;
|
|
558
|
-
const baseColumns = getDynamicColumns(rowMetadata, handleInternalAction, t, i18n.language, columnFilterConfigs);
|
|
558
|
+
const baseColumns = getDynamicColumns(rowMetadata, handleInternalAction, t, i18n.language, columnFilterConfigs, timeZone);
|
|
559
559
|
const filteredBase = baseColumns.filter((col) => !hiddenColumns.includes(col.id));
|
|
560
560
|
const actionsCol = filteredBase.find((c) => c.id === 'actions');
|
|
561
561
|
const otherCols = filteredBase.filter((c) => c.id !== 'actions');
|
|
562
562
|
return [...otherCols, ...extraColumns, ...(actionsCol ? [actionsCol] : [])];
|
|
563
|
-
}, [metadata, handleInternalAction, hiddenColumns, extraColumns, t, i18n.language, columnFilterConfigs, getDynamicColumns]);
|
|
563
|
+
}, [metadata, handleInternalAction, hiddenColumns, extraColumns, t, i18n.language, columnFilterConfigs, getDynamicColumns, timeZone]);
|
|
564
564
|
const filters = useMemo(() => [], []);
|
|
565
565
|
const table = useReactTable({
|
|
566
566
|
data,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asteby/metacore-runtime-react",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "18.1.0",
|
|
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.
|
|
37
|
+
"@asteby/metacore-ui": "^2.5.0"
|
|
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.
|
|
65
|
+
"@asteby/metacore-ui": "2.5.0"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
68
|
"build": "tsc -p tsconfig.json",
|
|
@@ -49,4 +49,31 @@ describe('formatDateCell', () => {
|
|
|
49
49
|
it('returns null for an unparseable value', () => {
|
|
50
50
|
expect(formatDateCell('not-a-date', 'datetime', enUS)).toBeNull()
|
|
51
51
|
})
|
|
52
|
+
|
|
53
|
+
describe('timeZone-aware (org IANA zone)', () => {
|
|
54
|
+
// 2026-06-07T00:00:00Z is the previous day, 19:00, in America/Mexico_City
|
|
55
|
+
// (UTC-5). A browser-local formatter in a UTC-2 zone would day-shift it;
|
|
56
|
+
// pinning to the org zone must show June 6.
|
|
57
|
+
const midnightUtc = '2026-06-07T00:00:00Z'
|
|
58
|
+
|
|
59
|
+
it('renders an instant in the provided zone, not the browser zone', () => {
|
|
60
|
+
const out = formatDateCell(midnightUtc, 'datetime', enUS, 'America/Mexico_City')
|
|
61
|
+
expect(out).not.toBeNull()
|
|
62
|
+
// Mexico City is UTC-5/-6 → the instant falls on June 6, 19:00.
|
|
63
|
+
expect(out!.display).toMatch(/Jun 6, 2026/)
|
|
64
|
+
expect(out!.display).toMatch(/\d{1,2}:\d{2}/)
|
|
65
|
+
// Tooltip carries full precision + the zone abbreviation.
|
|
66
|
+
expect(out!.title).toBeDefined()
|
|
67
|
+
expect(out!.title).toMatch(/2026/)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('renders a pure `date` pinned to UTC so it never shifts', () => {
|
|
71
|
+
const out = formatDateCell(midnightUtc, 'date', enUS, 'America/Mexico_City')
|
|
72
|
+
expect(out).not.toBeNull()
|
|
73
|
+
// UTC-pinned: stays on June 7 regardless of zone, no time, no tooltip.
|
|
74
|
+
expect(out!.display).toMatch(/June 7, 2026/)
|
|
75
|
+
expect(out!.display).not.toMatch(/\d{1,2}:\d{2}/)
|
|
76
|
+
expect(out!.title).toBeUndefined()
|
|
77
|
+
})
|
|
78
|
+
})
|
|
52
79
|
})
|
|
@@ -30,6 +30,7 @@ export type GetDynamicColumns = (
|
|
|
30
30
|
t: (key: string, options?: any) => string,
|
|
31
31
|
language: string,
|
|
32
32
|
columnFilterConfigs: Map<string, ColumnFilterConfig>,
|
|
33
|
+
timeZone?: string,
|
|
33
34
|
) => ColumnDef<any>[]
|
|
34
35
|
|
|
35
36
|
/** Signature for the host-provided `DynamicIcon` renderer. */
|
package/src/dynamic-columns.tsx
CHANGED
|
@@ -369,16 +369,60 @@ export const DATE_CELL_TYPES = ['date', 'datetime', 'timestamp', 'timestamptz']
|
|
|
369
369
|
* - `date`: day only (`PPP`), no tooltip.
|
|
370
370
|
* - `datetime`/`timestamp(tz)`: day + time (`Pp`) with a full-precision
|
|
371
371
|
* tooltip (`PPpp`) — the 7Leguas pattern.
|
|
372
|
+
*
|
|
373
|
+
* When a `timeZone` (IANA, e.g. the org's `America/Mexico_City`) is provided,
|
|
374
|
+
* instants are rendered in that zone via the native `Intl.DateTimeFormat` so
|
|
375
|
+
* the displayed day/time never shifts with the viewer's browser timezone:
|
|
376
|
+
* - instant (datetime/timestamp(tz)): `dateStyle:'medium' timeStyle:'short'`
|
|
377
|
+
* in the org zone, with a `dateStyle:'long' timeStyle:'medium'` +
|
|
378
|
+
* `timeZoneName:'short'` tooltip.
|
|
379
|
+
* - `date` (pure calendar day): rendered pinned to UTC so it never rolls to
|
|
380
|
+
* the previous/next day, no tooltip.
|
|
381
|
+
* Without a `timeZone`, the exact date-fns behavior is preserved (back-compat).
|
|
372
382
|
*/
|
|
373
383
|
export function formatDateCell(
|
|
374
384
|
value: unknown,
|
|
375
385
|
renderAs: string | undefined,
|
|
376
386
|
locale: Locale,
|
|
387
|
+
timeZone?: string,
|
|
377
388
|
): { display: string; title?: string } | null {
|
|
378
389
|
if (value === null || value === undefined || value === '') return null
|
|
379
390
|
const date = new Date(value as any)
|
|
380
391
|
if (isNaN(date.getTime()) || date.getFullYear() <= 1) return null
|
|
381
392
|
const withTime = renderAs !== 'date'
|
|
393
|
+
if (timeZone) {
|
|
394
|
+
// `locale.code` is the BCP-47 tag date-fns ships (e.g. 'es', 'en-US').
|
|
395
|
+
const localeTag = locale?.code || undefined
|
|
396
|
+
if (withTime) {
|
|
397
|
+
return {
|
|
398
|
+
display: new Intl.DateTimeFormat(localeTag, {
|
|
399
|
+
timeZone,
|
|
400
|
+
dateStyle: 'medium',
|
|
401
|
+
timeStyle: 'short',
|
|
402
|
+
}).format(date),
|
|
403
|
+
// `dateStyle`/`timeStyle` can't be combined with explicit
|
|
404
|
+
// component options like `timeZoneName`, so spell the tooltip
|
|
405
|
+
// out: long date + seconds + the zone abbreviation.
|
|
406
|
+
title: new Intl.DateTimeFormat(localeTag, {
|
|
407
|
+
timeZone,
|
|
408
|
+
year: 'numeric',
|
|
409
|
+
month: 'long',
|
|
410
|
+
day: 'numeric',
|
|
411
|
+
hour: '2-digit',
|
|
412
|
+
minute: '2-digit',
|
|
413
|
+
second: '2-digit',
|
|
414
|
+
timeZoneName: 'short',
|
|
415
|
+
}).format(date),
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// Pure calendar date: pin to UTC so it never shifts across zones.
|
|
419
|
+
return {
|
|
420
|
+
display: new Intl.DateTimeFormat(localeTag, {
|
|
421
|
+
timeZone: 'UTC',
|
|
422
|
+
dateStyle: 'long',
|
|
423
|
+
}).format(date),
|
|
424
|
+
}
|
|
425
|
+
}
|
|
382
426
|
if (withTime) {
|
|
383
427
|
return {
|
|
384
428
|
display: format(date, 'Pp', { locale }),
|
|
@@ -514,6 +558,7 @@ export function makeDefaultGetDynamicColumns(
|
|
|
514
558
|
t?: (key: string, options?: any) => string,
|
|
515
559
|
currentLanguage?: string,
|
|
516
560
|
filterConfigs?: Map<string, ColumnFilterConfig>,
|
|
561
|
+
timeZone?: string,
|
|
517
562
|
): ColumnDef<any>[] {
|
|
518
563
|
const dateLocale = currentLanguage === 'en' ? enUS : es
|
|
519
564
|
const columns: ColumnDef<any>[] = [
|
|
@@ -665,7 +710,7 @@ export function makeDefaultGetDynamicColumns(
|
|
|
665
710
|
case 'datetime':
|
|
666
711
|
case 'timestamp':
|
|
667
712
|
case 'timestamptz': {
|
|
668
|
-
const formatted = formatDateCell(value, renderAs, dateLocale)
|
|
713
|
+
const formatted = formatDateCell(value, renderAs, dateLocale, timeZone)
|
|
669
714
|
if (!formatted)
|
|
670
715
|
return <span className="text-muted-foreground">-</span>
|
|
671
716
|
return (
|
|
@@ -35,6 +35,8 @@ import {
|
|
|
35
35
|
PopoverTrigger,
|
|
36
36
|
} from '@asteby/metacore-ui/primitives'
|
|
37
37
|
import { Check, ChevronsUpDown, ImageIcon, Loader2, Plus } from 'lucide-react'
|
|
38
|
+
import { resolveColorCss } from '@asteby/metacore-ui/lib'
|
|
39
|
+
import { DynamicIcon } from './dynamic-icon'
|
|
38
40
|
import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
|
|
39
41
|
import { getFieldRef } from './dynamic-form-schema'
|
|
40
42
|
import type { ActionFieldDef } from './types'
|
|
@@ -76,6 +78,53 @@ function OptionThumb({ image, size = 20 }: { image?: string | null; size?: numbe
|
|
|
76
78
|
)
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Leading visual for an option: a photo thumbnail (FK relations with an image),
|
|
83
|
+
* else a declared icon, else a color dot (enum/status options with a color).
|
|
84
|
+
* Returns null when the option carries none, so plain text options stay plain.
|
|
85
|
+
*/
|
|
86
|
+
function OptionLead({
|
|
87
|
+
option,
|
|
88
|
+
size = 20,
|
|
89
|
+
}: {
|
|
90
|
+
option?: Pick<ResolvedOption, 'image' | 'color' | 'icon'> | null
|
|
91
|
+
size?: number
|
|
92
|
+
}) {
|
|
93
|
+
if (!option) return null
|
|
94
|
+
if (option.image) return <OptionThumb image={option.image} size={size} />
|
|
95
|
+
if (option.icon) {
|
|
96
|
+
return (
|
|
97
|
+
<span
|
|
98
|
+
className="flex shrink-0 items-center justify-center"
|
|
99
|
+
style={{ width: size, height: size, color: option.color ? resolveColorCss(option.color) : undefined }}
|
|
100
|
+
aria-hidden
|
|
101
|
+
>
|
|
102
|
+
<DynamicIcon name={option.icon} className="size-4" />
|
|
103
|
+
</span>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
if (option.color) {
|
|
107
|
+
return (
|
|
108
|
+
<span
|
|
109
|
+
className="shrink-0 rounded-full"
|
|
110
|
+
style={{ width: Math.round(size * 0.5), height: Math.round(size * 0.5), background: resolveColorCss(option.color) }}
|
|
111
|
+
aria-hidden
|
|
112
|
+
/>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** True when any option (or the selected one) carries a renderable visual. */
|
|
119
|
+
function optionsHaveVisual(
|
|
120
|
+
options: ReadonlyArray<Pick<ResolvedOption, 'image' | 'color' | 'icon'>>,
|
|
121
|
+
selected?: Pick<ResolvedOption, 'image' | 'color' | 'icon'> | null,
|
|
122
|
+
): boolean {
|
|
123
|
+
const has = (o?: Pick<ResolvedOption, 'image' | 'color' | 'icon'> | null) =>
|
|
124
|
+
!!(o && (o.image || o.color || o.icon))
|
|
125
|
+
return has(selected) || options.some(has)
|
|
126
|
+
}
|
|
127
|
+
|
|
79
128
|
function useDebounced<T>(value: T, ms: number): T {
|
|
80
129
|
const [debounced, setDebounced] = useState(value)
|
|
81
130
|
useEffect(() => {
|
|
@@ -130,8 +179,7 @@ export function DynamicSelectField({ field, value, onChange }: DynamicSelectFiel
|
|
|
130
179
|
// Only switch the picker into "with thumbnails" mode when the data actually
|
|
131
180
|
// carries images — a relation whose options have no `image` keeps the plain
|
|
132
181
|
// text list it had before (no empty placeholder column).
|
|
133
|
-
const
|
|
134
|
-
!!selectedOption?.image || options.some((o) => !!o.image)
|
|
182
|
+
const hasVisual = optionsHaveVisual(options, selectedOption)
|
|
135
183
|
|
|
136
184
|
const handlePick = (opt: ResolvedOption) => {
|
|
137
185
|
setPicked(opt)
|
|
@@ -181,8 +229,8 @@ export function DynamicSelectField({ field, value, onChange }: DynamicSelectFiel
|
|
|
181
229
|
data-empty={!value}
|
|
182
230
|
>
|
|
183
231
|
<span className="flex min-w-0 flex-1 items-center gap-2 text-left">
|
|
184
|
-
{
|
|
185
|
-
<
|
|
232
|
+
{hasVisual && value ? (
|
|
233
|
+
<OptionLead option={selectedOption} size={20} />
|
|
186
234
|
) : null}
|
|
187
235
|
<span className={'min-w-0 flex-1 truncate ' + (selectedLabel ? '' : 'text-muted-foreground')}>
|
|
188
236
|
{selectedLabel || field.placeholder || 'Buscar…'}
|
|
@@ -227,8 +275,8 @@ export function DynamicSelectField({ field, value, onChange }: DynamicSelectFiel
|
|
|
227
275
|
onSelect={() => handlePick(opt)}
|
|
228
276
|
>
|
|
229
277
|
<Check className={'mr-2 size-4 shrink-0 ' + (isSel ? 'opacity-100' : 'opacity-0')} />
|
|
230
|
-
{
|
|
231
|
-
<
|
|
278
|
+
{hasVisual && (
|
|
279
|
+
<OptionLead option={opt} size={24} />
|
|
232
280
|
)}
|
|
233
281
|
<div className="ml-2 flex min-w-0 flex-col">
|
|
234
282
|
<span className="truncate">{opt.label}</span>
|
package/src/dynamic-table.tsx
CHANGED
|
@@ -90,6 +90,13 @@ interface DynamicTableProps {
|
|
|
90
90
|
* Optional — a sensible default maps each column to { accessorKey, header }.
|
|
91
91
|
*/
|
|
92
92
|
getDynamicColumns?: GetDynamicColumns
|
|
93
|
+
/**
|
|
94
|
+
* IANA timezone (e.g. the org's `America/Mexico_City`) used to render
|
|
95
|
+
* datetime/timestamp cells. When provided, instants are displayed in this
|
|
96
|
+
* zone instead of the viewer's browser zone, so the day/time never shifts.
|
|
97
|
+
* Optional — omitting it preserves the legacy browser-local formatting.
|
|
98
|
+
*/
|
|
99
|
+
timeZone?: string
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
export function DynamicTable({
|
|
@@ -102,6 +109,7 @@ export function DynamicTable({
|
|
|
102
109
|
defaultFilters,
|
|
103
110
|
extraColumns = [],
|
|
104
111
|
getDynamicColumns = defaultGetDynamicColumns,
|
|
112
|
+
timeZone,
|
|
105
113
|
}: DynamicTableProps) {
|
|
106
114
|
const { t, i18n } = useTranslation()
|
|
107
115
|
const api = useApi()
|
|
@@ -590,12 +598,12 @@ export function DynamicTable({
|
|
|
590
598
|
const rowMetadata = metadata.actions?.some((a) => a.placement === 'table' || a.placement === 'create')
|
|
591
599
|
? { ...metadata, actions: metadata.actions.filter((a) => !a.placement || a.placement === 'row') }
|
|
592
600
|
: metadata
|
|
593
|
-
const baseColumns = getDynamicColumns(rowMetadata, handleInternalAction, t, i18n.language, columnFilterConfigs)
|
|
601
|
+
const baseColumns = getDynamicColumns(rowMetadata, handleInternalAction, t, i18n.language, columnFilterConfigs, timeZone)
|
|
594
602
|
const filteredBase = baseColumns.filter((col: ColumnDef<any>) => !hiddenColumns.includes(col.id as string))
|
|
595
603
|
const actionsCol = filteredBase.find((c: ColumnDef<any>) => c.id === 'actions')
|
|
596
604
|
const otherCols = filteredBase.filter((c: ColumnDef<any>) => c.id !== 'actions')
|
|
597
605
|
return [...otherCols, ...extraColumns, ...(actionsCol ? [actionsCol] : [])]
|
|
598
|
-
}, [metadata, handleInternalAction, hiddenColumns, extraColumns, t, i18n.language, columnFilterConfigs, getDynamicColumns])
|
|
606
|
+
}, [metadata, handleInternalAction, hiddenColumns, extraColumns, t, i18n.language, columnFilterConfigs, getDynamicColumns, timeZone])
|
|
599
607
|
|
|
600
608
|
const filters = useMemo(() => [], [])
|
|
601
609
|
|