@asteby/metacore-runtime-react 46.2.0 → 46.2.2
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 +12 -0
- package/dist/calendar-date.d.ts +8 -0
- package/dist/calendar-date.d.ts.map +1 -1
- package/dist/calendar-date.js +12 -0
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +6 -3
- package/dist/dynamic-relation.js +8 -1
- package/package.json +1 -1
- package/src/__tests__/calendar-date.test.ts +14 -1
- package/src/__tests__/format-date-cell.test.ts +15 -0
- package/src/calendar-date.ts +12 -0
- package/src/dynamic-columns.tsx +6 -3
- package/src/dynamic-relation.tsx +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 46.2.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 97f77eb: Una relación muchos-a-muchos distingue «sin permiso» y un error de carga de una lista vacía, igual que la subtabla de uno-a-muchos.
|
|
8
|
+
|
|
9
|
+
## 46.2.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 712f605: fix(runtime-react): una columna con `display: date` que guarda un INSTANTE (timestamptz, p. ej. `invoice_date`) se pinta en la zona horaria de la organización y no en UTC (PIT-025). Con zona de la org, `formatDateCell` fijaba a UTC toda celda de día, así que una factura de la tarde del 29-sep en México (30-sep 00:41Z) aparecía como «30 de septiembre». Solo `YYYY-MM-DD` o medianoche UTC exacta (`isCalendarDayValue`) se siguen tratando como día de calendario.
|
|
14
|
+
|
|
3
15
|
## 46.2.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/dist/calendar-date.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
/** Parse a calendar day ("YYYY-MM-DD…" or any Date-parseable value) into a
|
|
2
2
|
* local Date at noon, or undefined when empty/invalid. */
|
|
3
3
|
export declare function parseCalendarDate(v: unknown): Date | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* True when a serialized value is a pure CALENDAR DAY: `YYYY-MM-DD`, or that day at
|
|
6
|
+
* exactly UTC midnight (how the backend serves a `date` column). Anything with a
|
|
7
|
+
* real time of day is an INSTANT: a `timestamptz` shown with a day-only renderer
|
|
8
|
+
* must be read in the organization's zone, or an evening invoice (29 Sep 19:41 in
|
|
9
|
+
* Mexico City = 30 Sep 00:41Z) shows as the 30th (PIT-025).
|
|
10
|
+
*/
|
|
11
|
+
export declare function isCalendarDayValue(v: unknown): boolean;
|
|
4
12
|
//# sourceMappingURL=calendar-date.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calendar-date.d.ts","sourceRoot":"","sources":["../src/calendar-date.ts"],"names":[],"mappings":"AAKA;2DAC2D;AAC3D,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,CAQ9D"}
|
|
1
|
+
{"version":3,"file":"calendar-date.d.ts","sourceRoot":"","sources":["../src/calendar-date.ts"],"names":[],"mappings":"AAKA;2DAC2D;AAC3D,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,CAQ9D;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAGtD"}
|
package/dist/calendar-date.js
CHANGED
|
@@ -16,3 +16,15 @@ export function parseCalendarDate(v) {
|
|
|
16
16
|
const d = new Date(s);
|
|
17
17
|
return isNaN(d.getTime()) ? undefined : d;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* True when a serialized value is a pure CALENDAR DAY: `YYYY-MM-DD`, or that day at
|
|
21
|
+
* exactly UTC midnight (how the backend serves a `date` column). Anything with a
|
|
22
|
+
* real time of day is an INSTANT: a `timestamptz` shown with a day-only renderer
|
|
23
|
+
* must be read in the organization's zone, or an evening invoice (29 Sep 19:41 in
|
|
24
|
+
* Mexico City = 30 Sep 00:41Z) shows as the 30th (PIT-025).
|
|
25
|
+
*/
|
|
26
|
+
export function isCalendarDayValue(v) {
|
|
27
|
+
if (typeof v !== 'string')
|
|
28
|
+
return false;
|
|
29
|
+
return /^\d{4}-\d{2}-\d{2}(?:[T ]00:00:00(?:\.0+)?(?:Z|\+00:00)?)?$/.test(v.trim());
|
|
30
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AA4C9C,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;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AAyCD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,EAC9B,MAAM,EACN,KAAK,EACL,QAAQ,GACX,EAAE;IACC,MAAM,EAAE,GAAG,CAAA;IACX,KAAK,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,QAAQ,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAA;CAC1C,qBAcA;AAeD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OA0C5D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OACqB,CAAA;AAkHhF;;;;;;;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,
|
|
1
|
+
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"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"}
|
package/dist/dynamic-columns.js
CHANGED
|
@@ -14,7 +14,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
14
14
|
// argument so the SDK stays free of environment-bound code.
|
|
15
15
|
import * as React from 'react';
|
|
16
16
|
import { format } from 'date-fns';
|
|
17
|
-
import { parseCalendarDate } from './calendar-date';
|
|
17
|
+
import { isCalendarDayValue, parseCalendarDate } from './calendar-date';
|
|
18
18
|
import { es, enUS } from 'date-fns/locale';
|
|
19
19
|
import { Calendar, Check, Copy, Mail, Minus, MoreHorizontal } from 'lucide-react';
|
|
20
20
|
import { Avatar, AvatarFallback, AvatarImage, Badge, Button, Checkbox, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, InitialsAvatar, } from '@asteby/metacore-ui';
|
|
@@ -373,10 +373,13 @@ export function formatDateCell(value, renderAs, locale, timeZone) {
|
|
|
373
373
|
}).format(date),
|
|
374
374
|
};
|
|
375
375
|
}
|
|
376
|
-
// Pure calendar date: pin to UTC so it never shifts across zones.
|
|
376
|
+
// Pure calendar date: pin to UTC so it never shifts across zones. A value
|
|
377
|
+
// with a real time of day is an instant even under a day-only renderer
|
|
378
|
+
// (timestamptz columns like invoice_date): show ITS day in the org zone.
|
|
379
|
+
const calendarDay = typeof value !== 'string' || isCalendarDayValue(value);
|
|
377
380
|
return {
|
|
378
381
|
display: new Intl.DateTimeFormat(localeTag, {
|
|
379
|
-
timeZone: 'UTC',
|
|
382
|
+
timeZone: calendarDay ? 'UTC' : timeZone,
|
|
380
383
|
dateStyle: 'long',
|
|
381
384
|
}).format(date),
|
|
382
385
|
};
|
package/dist/dynamic-relation.js
CHANGED
|
@@ -341,6 +341,9 @@ function ManyToManyRelation({ kind, through, references, foreignKey, referencesK
|
|
|
341
341
|
const [pivotRows, setPivotRows] = useState([]);
|
|
342
342
|
const [loading, setLoading] = useState(true);
|
|
343
343
|
const [syncing, setSyncing] = useState(false);
|
|
344
|
+
// Same distinction as the 1:N sub-table (PIT-045): a 403 is not "no rows",
|
|
345
|
+
// and an empty pivot is not "sin permiso".
|
|
346
|
+
const [fetchErrorStatus, setFetchErrorStatus] = useState(null);
|
|
344
347
|
// Canonical path: SDK options resolver. Only fires when no legacy
|
|
345
348
|
// override is set. The hook is a no-op when `useResolver` is false.
|
|
346
349
|
const resolved = useOptionsResolver({
|
|
@@ -366,6 +369,7 @@ function ManyToManyRelation({ kind, through, references, foreignKey, referencesK
|
|
|
366
369
|
tasks.push(api.get(legacyTargetPath));
|
|
367
370
|
const results = await Promise.all(tasks);
|
|
368
371
|
const pivotRes = results[0];
|
|
372
|
+
setFetchErrorStatus(null);
|
|
369
373
|
if (pivotRes.data.success)
|
|
370
374
|
setPivotRows(pivotRes.data.data || []);
|
|
371
375
|
let cursor = 1;
|
|
@@ -384,6 +388,7 @@ function ManyToManyRelation({ kind, through, references, foreignKey, referencesK
|
|
|
384
388
|
}
|
|
385
389
|
catch (err) {
|
|
386
390
|
console.error('DynamicRelation m2m fetch error', err);
|
|
391
|
+
setFetchErrorStatus(err?.response?.status ?? 0);
|
|
387
392
|
}
|
|
388
393
|
finally {
|
|
389
394
|
setLoading(false);
|
|
@@ -449,5 +454,7 @@ function ManyToManyRelation({ kind, through, references, foreignKey, referencesK
|
|
|
449
454
|
}
|
|
450
455
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
451
456
|
}, [api, canCreate, canDelete, fetchPivotAndMeta, useResolver, resolved, foreignKey, filtersKey, onChange, parentId, pivotIndex, pivotPath, refKey, selectedIds, syncing]);
|
|
452
|
-
return (_jsxs("div", { className: className, "data-relation-kind": kind, "data-relation-through": through, "data-relation-references": references, children: [labels.title && (_jsx("div", { className: "pb-3", children: _jsx("h3", { className: "text-sm font-medium", children: labels.title }) })), (loading || (useResolver && resolved.loading)) ? (_jsx(Skeleton, { className: "h-10 w-full" })) :
|
|
457
|
+
return (_jsxs("div", { className: className, "data-relation-kind": kind, "data-relation-through": through, "data-relation-references": references, children: [labels.title && (_jsx("div", { className: "pb-3", children: _jsx("h3", { className: "text-sm font-medium", children: labels.title }) })), (loading || (useResolver && resolved.loading)) ? (_jsx(Skeleton, { className: "h-10 w-full" })) : fetchErrorStatus !== null ? (_jsx("div", { className: "text-center text-sm text-muted-foreground py-8 border rounded-md bg-muted/30", children: fetchErrorStatus === 403
|
|
458
|
+
? (labels.forbiddenState ?? 'Sin permiso para ver estos registros.')
|
|
459
|
+
: (labels.errorState ?? 'No se pudieron cargar los registros relacionados.') })) : options.length === 0 ? (_jsx("div", { className: "text-center text-sm text-muted-foreground py-8 border rounded-md bg-muted/30", children: labels.emptyState })) : (_jsx(MultiSelect, { options: options, selected: selectedIds, onChange: handleChange, placeholder: labels.selectPlaceholder, searchPlaceholder: labels.selectSearchPlaceholder, emptyMessage: labels.selectEmpty }))] }));
|
|
453
460
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { es } from 'date-fns/locale'
|
|
3
|
-
import { parseCalendarDate } from '../calendar-date'
|
|
3
|
+
import { isCalendarDayValue, parseCalendarDate } from '../calendar-date'
|
|
4
4
|
import { formatDateCell } from '../dynamic-columns'
|
|
5
5
|
|
|
6
6
|
describe('parseCalendarDate', () => {
|
|
@@ -20,3 +20,16 @@ describe('parseCalendarDate', () => {
|
|
|
20
20
|
expect(formatDateCell('2026-09-27T00:00:00Z', 'date', es)?.display).toContain('27')
|
|
21
21
|
})
|
|
22
22
|
})
|
|
23
|
+
|
|
24
|
+
describe('isCalendarDayValue', () => {
|
|
25
|
+
it('día de calendario: YYYY-MM-DD o medianoche UTC', () => {
|
|
26
|
+
for (const v of ['2026-09-27', '2026-09-27T00:00:00Z', '2026-09-27T00:00:00.000Z', '2026-09-27 00:00:00', '2026-09-27T00:00:00+00:00']) {
|
|
27
|
+
expect(isCalendarDayValue(v)).toBe(true)
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
it('un instante con hora, vacío o no-string NO lo es', () => {
|
|
31
|
+
for (const v of ['2026-09-30T00:41:06Z', '2026-09-27T00:00:01Z', '2026-09-27T05:00:00Z', '', null, undefined, 123, new Date()]) {
|
|
32
|
+
expect(isCalendarDayValue(v)).toBe(false)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -75,5 +75,20 @@ describe('formatDateCell', () => {
|
|
|
75
75
|
expect(out!.display).not.toMatch(/\d{1,2}:\d{2}/)
|
|
76
76
|
expect(out!.title).toBeUndefined()
|
|
77
77
|
})
|
|
78
|
+
|
|
79
|
+
// PIT-025: invoice_date is a timestamptz shown with the day-only renderer.
|
|
80
|
+
// 2026-09-30T00:41:06Z is the 29th (18:41) in Mexico City: an evening
|
|
81
|
+
// invoice must not roll to the 30th.
|
|
82
|
+
it('renders a `date` column that holds an INSTANT in the org zone, not UTC', () => {
|
|
83
|
+
const out = formatDateCell('2026-09-30T00:41:06Z', 'date', enUS, 'America/Mexico_City')
|
|
84
|
+
expect(out!.display).toMatch(/September 29, 2026/)
|
|
85
|
+
expect(out!.title).toBeUndefined()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('keeps UTC-midnight and YYYY-MM-DD as calendar days', () => {
|
|
89
|
+
for (const v of ['2026-09-27T00:00:00Z', '2026-09-27T00:00:00.000Z', '2026-09-27']) {
|
|
90
|
+
expect(formatDateCell(v, 'date', enUS, 'America/Mexico_City')!.display).toMatch(/September 27, 2026/)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
78
93
|
})
|
|
79
94
|
})
|
package/src/calendar-date.ts
CHANGED
|
@@ -14,3 +14,15 @@ export function parseCalendarDate(v: unknown): Date | undefined {
|
|
|
14
14
|
const d = new Date(s)
|
|
15
15
|
return isNaN(d.getTime()) ? undefined : d
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* True when a serialized value is a pure CALENDAR DAY: `YYYY-MM-DD`, or that day at
|
|
20
|
+
* exactly UTC midnight (how the backend serves a `date` column). Anything with a
|
|
21
|
+
* real time of day is an INSTANT: a `timestamptz` shown with a day-only renderer
|
|
22
|
+
* must be read in the organization's zone, or an evening invoice (29 Sep 19:41 in
|
|
23
|
+
* Mexico City = 30 Sep 00:41Z) shows as the 30th (PIT-025).
|
|
24
|
+
*/
|
|
25
|
+
export function isCalendarDayValue(v: unknown): boolean {
|
|
26
|
+
if (typeof v !== 'string') return false
|
|
27
|
+
return /^\d{4}-\d{2}-\d{2}(?:[T ]00:00:00(?:\.0+)?(?:Z|\+00:00)?)?$/.test(v.trim())
|
|
28
|
+
}
|
package/src/dynamic-columns.tsx
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import * as React from 'react'
|
|
16
16
|
import { ColumnDef } from '@tanstack/react-table'
|
|
17
17
|
import { format, type Locale } from 'date-fns'
|
|
18
|
-
import { parseCalendarDate } from './calendar-date'
|
|
18
|
+
import { isCalendarDayValue, parseCalendarDate } from './calendar-date'
|
|
19
19
|
import { es, enUS } from 'date-fns/locale'
|
|
20
20
|
import { Calendar, Check, Copy, Mail, Minus, MoreHorizontal } from 'lucide-react'
|
|
21
21
|
import {
|
|
@@ -525,10 +525,13 @@ export function formatDateCell(
|
|
|
525
525
|
}).format(date),
|
|
526
526
|
}
|
|
527
527
|
}
|
|
528
|
-
// Pure calendar date: pin to UTC so it never shifts across zones.
|
|
528
|
+
// Pure calendar date: pin to UTC so it never shifts across zones. A value
|
|
529
|
+
// with a real time of day is an instant even under a day-only renderer
|
|
530
|
+
// (timestamptz columns like invoice_date): show ITS day in the org zone.
|
|
531
|
+
const calendarDay = typeof value !== 'string' || isCalendarDayValue(value)
|
|
529
532
|
return {
|
|
530
533
|
display: new Intl.DateTimeFormat(localeTag, {
|
|
531
|
-
timeZone: 'UTC',
|
|
534
|
+
timeZone: calendarDay ? 'UTC' : timeZone,
|
|
532
535
|
dateStyle: 'long',
|
|
533
536
|
}).format(date),
|
|
534
537
|
}
|
package/src/dynamic-relation.tsx
CHANGED
|
@@ -718,6 +718,9 @@ function ManyToManyRelation({
|
|
|
718
718
|
const [pivotRows, setPivotRows] = useState<any[]>([])
|
|
719
719
|
const [loading, setLoading] = useState(true)
|
|
720
720
|
const [syncing, setSyncing] = useState(false)
|
|
721
|
+
// Same distinction as the 1:N sub-table (PIT-045): a 403 is not "no rows",
|
|
722
|
+
// and an empty pivot is not "sin permiso".
|
|
723
|
+
const [fetchErrorStatus, setFetchErrorStatus] = useState<number | null>(null)
|
|
721
724
|
|
|
722
725
|
// Canonical path: SDK options resolver. Only fires when no legacy
|
|
723
726
|
// override is set. The hook is a no-op when `useResolver` is false.
|
|
@@ -744,6 +747,7 @@ function ManyToManyRelation({
|
|
|
744
747
|
if (!useResolver) tasks.push(api.get(legacyTargetPath))
|
|
745
748
|
const results = await Promise.all(tasks)
|
|
746
749
|
const pivotRes = results[0] as { data: ApiResponse<any[]> }
|
|
750
|
+
setFetchErrorStatus(null)
|
|
747
751
|
if (pivotRes.data.success) setPivotRows(pivotRes.data.data || [])
|
|
748
752
|
let cursor = 1
|
|
749
753
|
if (!targetMeta) {
|
|
@@ -759,6 +763,7 @@ function ManyToManyRelation({
|
|
|
759
763
|
}
|
|
760
764
|
} catch (err) {
|
|
761
765
|
console.error('DynamicRelation m2m fetch error', err)
|
|
766
|
+
setFetchErrorStatus((err as { response?: { status?: number } })?.response?.status ?? 0)
|
|
762
767
|
} finally {
|
|
763
768
|
setLoading(false)
|
|
764
769
|
}
|
|
@@ -840,6 +845,12 @@ function ManyToManyRelation({
|
|
|
840
845
|
|
|
841
846
|
{(loading || (useResolver && resolved.loading)) ? (
|
|
842
847
|
<Skeleton className="h-10 w-full" />
|
|
848
|
+
) : fetchErrorStatus !== null ? (
|
|
849
|
+
<div className="text-center text-sm text-muted-foreground py-8 border rounded-md bg-muted/30">
|
|
850
|
+
{fetchErrorStatus === 403
|
|
851
|
+
? (labels.forbiddenState ?? 'Sin permiso para ver estos registros.')
|
|
852
|
+
: (labels.errorState ?? 'No se pudieron cargar los registros relacionados.')}
|
|
853
|
+
</div>
|
|
843
854
|
) : options.length === 0 ? (
|
|
844
855
|
<div className="text-center text-sm text-muted-foreground py-8 border rounded-md bg-muted/30">
|
|
845
856
|
{labels.emptyState}
|