@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 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>) => ColumnDef<any>[];
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,KACnD,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"}
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;;;;;;;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"}
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"}
@@ -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":"AAuCA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAgD7C,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,+BA2KrF;AAED,eAAe,kBAAkB,CAAA"}
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 hasImages = !!selectedOption?.image || options.some((o) => !!o.image);
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: [hasImages && value ? (_jsx(OptionThumb, { image: selectedOption?.image, 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",
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') }), hasImages && (_jsx(OptionThumb, { image: opt.image, 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)));
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;
@@ -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;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"}
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"}
@@ -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": "17.0.4",
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.4.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.4.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. */
@@ -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 hasImages =
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
- {hasImages && value ? (
185
- <OptionThumb image={selectedOption?.image} size={20} />
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
- {hasImages && (
231
- <OptionThumb image={opt.image} size={24} />
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>
@@ -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