@asteby/metacore-runtime-react 18.0.0 → 18.2.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.
@@ -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 (
@@ -48,7 +48,7 @@ import type { ActionFieldDef } from './types'
48
48
  * not a gallery). Inline style for the box dimensions: arbitrary Tailwind
49
49
  * classes from a federated addon don't always survive the host's class scan.
50
50
  */
51
- function OptionThumb({ image, size = 20 }: { image?: string | null; size?: number }) {
51
+ export function OptionThumb({ image, size = 20 }: { image?: string | null; size?: number }) {
52
52
  const box = { width: size, height: size }
53
53
  if (!image) {
54
54
  return (
@@ -83,7 +83,7 @@ function OptionThumb({ image, size = 20 }: { image?: string | null; size?: numbe
83
83
  * else a declared icon, else a color dot (enum/status options with a color).
84
84
  * Returns null when the option carries none, so plain text options stay plain.
85
85
  */
86
- function OptionLead({
86
+ export function OptionLead({
87
87
  option,
88
88
  size = 20,
89
89
  }: {
@@ -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
 
package/src/index.ts CHANGED
@@ -68,7 +68,8 @@ export {
68
68
  } from './dynamic-columns'
69
69
  export { humanizeToken } from './dynamic-columns-helpers'
70
70
  export { NIL_UUID, isNilUuid, normalizeNilUuid } from './nil-uuid'
71
- export { DynamicRecordDialog } from './dialogs/dynamic-record'
71
+ export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record'
72
+ export type { DynamicRecordDialogProps, FieldDef, FieldOption, GetImageUrl } from './dialogs/dynamic-record'
72
73
  export { CreateRecordDialog } from './dialogs/create-record-dialog'
73
74
  export { ViewRecordDialog } from './dialogs/view-record-dialog'
74
75
  export type {