@growth-labs/cms 0.8.19 → 0.8.20
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/dist/lib/publish-timezone.d.ts +38 -0
- package/dist/lib/publish-timezone.d.ts.map +1 -0
- package/dist/lib/publish-timezone.js +135 -0
- package/dist/lib/publish-timezone.js.map +1 -0
- package/dist/routes/content.d.ts.map +1 -1
- package/dist/routes/content.js +2 -102
- package/dist/routes/content.js.map +1 -1
- package/dist/ui/inspector/PublishTab.d.ts +7 -1
- package/dist/ui/inspector/PublishTab.d.ts.map +1 -1
- package/dist/ui/inspector/PublishTab.js +24 -26
- package/dist/ui/inspector/PublishTab.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/publish-timezone.ts +154 -0
- package/src/routes/content.ts +5 -116
- package/src/ui/inspector/PublishTab.tsx +33 -25
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publication-timezone helpers — ONE implementation, shared by the schedule
|
|
3
|
+
* route and the Publish tab.
|
|
4
|
+
*
|
|
5
|
+
* Why this module exists: the editor types a wall-clock time and expects the
|
|
6
|
+
* post to go out at that time in the publication's timezone. That only holds
|
|
7
|
+
* if the digits the UI DISPLAYS and the zone it SAVES agree. When they
|
|
8
|
+
* disagreed, every resave shifted `publish_at` by the zone offset — including
|
|
9
|
+
* an incidental resave that never touched the field (the 2026-09-08 P0). So
|
|
10
|
+
* display and entry must use the same converter as the server, not a second
|
|
11
|
+
* copy of the arithmetic.
|
|
12
|
+
*/
|
|
13
|
+
/** Fronts default; the server falls back to this when no zone is configured. */
|
|
14
|
+
export declare const PUBLISH_TIMEZONE_FALLBACK = "Europe/Paris";
|
|
15
|
+
type LocalDateTimeParts = {
|
|
16
|
+
year: number;
|
|
17
|
+
month: number;
|
|
18
|
+
day: number;
|
|
19
|
+
hour: number;
|
|
20
|
+
minute: number;
|
|
21
|
+
};
|
|
22
|
+
export declare function parseLocalDateTime(value: string): LocalDateTimeParts | null;
|
|
23
|
+
export declare function getZonedParts(instantMs: number, timeZone: string): LocalDateTimeParts | null;
|
|
24
|
+
export declare function isValidTimeZone(timeZone: string): boolean;
|
|
25
|
+
export declare function getTimeZoneOffsetMs(instantMs: number, timeZone: string): number | null;
|
|
26
|
+
export declare function parseLocalDateTimeInZoneToUnixSeconds(value: string, timeZone: string): number | null;
|
|
27
|
+
/**
|
|
28
|
+
* Inverse of `parseLocalDateTimeInZoneToUnixSeconds`: render an instant as the
|
|
29
|
+
* wall-clock digits an editor in `timeZone` would read, split for the date and
|
|
30
|
+
* time inputs. Returns null when the zone is unusable, so callers can fall back
|
|
31
|
+
* rather than silently display a wrong hour.
|
|
32
|
+
*/
|
|
33
|
+
export declare function formatInstantInZone(instantMs: number, timeZone: string): {
|
|
34
|
+
date: string;
|
|
35
|
+
time: string;
|
|
36
|
+
} | null;
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=publish-timezone.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish-timezone.d.ts","sourceRoot":"","sources":["../../src/lib/publish-timezone.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,gFAAgF;AAChF,eAAO,MAAM,yBAAyB,iBAAiB,CAAA;AAKvD,KAAK,kBAAkB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAEpG,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAiC3E;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAuB5F;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAOzD;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKtF;AAED,wBAAgB,qCAAqC,CACpD,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,GACd,MAAM,GAAG,IAAI,CAkCf;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAClC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACd;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAQvC"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publication-timezone helpers — ONE implementation, shared by the schedule
|
|
3
|
+
* route and the Publish tab.
|
|
4
|
+
*
|
|
5
|
+
* Why this module exists: the editor types a wall-clock time and expects the
|
|
6
|
+
* post to go out at that time in the publication's timezone. That only holds
|
|
7
|
+
* if the digits the UI DISPLAYS and the zone it SAVES agree. When they
|
|
8
|
+
* disagreed, every resave shifted `publish_at` by the zone offset — including
|
|
9
|
+
* an incidental resave that never touched the field (the 2026-09-08 P0). So
|
|
10
|
+
* display and entry must use the same converter as the server, not a second
|
|
11
|
+
* copy of the arithmetic.
|
|
12
|
+
*/
|
|
13
|
+
/** Fronts default; the server falls back to this when no zone is configured. */
|
|
14
|
+
export const PUBLISH_TIMEZONE_FALLBACK = 'Europe/Paris';
|
|
15
|
+
/** `YYYY-MM-DDTHH:MM` exactly — the shape the date and time inputs produce. */
|
|
16
|
+
const LOCAL_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/;
|
|
17
|
+
export function parseLocalDateTime(value) {
|
|
18
|
+
const match = LOCAL_DATE_TIME_RE.exec(value);
|
|
19
|
+
if (!match)
|
|
20
|
+
return null;
|
|
21
|
+
const [, y, m, d, h, min] = match;
|
|
22
|
+
const parts = {
|
|
23
|
+
year: Number.parseInt(y, 10),
|
|
24
|
+
month: Number.parseInt(m, 10),
|
|
25
|
+
day: Number.parseInt(d, 10),
|
|
26
|
+
hour: Number.parseInt(h, 10),
|
|
27
|
+
minute: Number.parseInt(min, 10),
|
|
28
|
+
};
|
|
29
|
+
if (!Number.isInteger(parts.year) ||
|
|
30
|
+
parts.month < 1 ||
|
|
31
|
+
parts.month > 12 ||
|
|
32
|
+
parts.day < 1 ||
|
|
33
|
+
parts.day > 31 ||
|
|
34
|
+
parts.hour < 0 ||
|
|
35
|
+
parts.hour > 23 ||
|
|
36
|
+
parts.minute < 0 ||
|
|
37
|
+
parts.minute > 59) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const dateCheck = new Date(Date.UTC(parts.year, parts.month - 1, parts.day));
|
|
41
|
+
if (dateCheck.getUTCFullYear() !== parts.year ||
|
|
42
|
+
dateCheck.getUTCMonth() + 1 !== parts.month ||
|
|
43
|
+
dateCheck.getUTCDate() !== parts.day) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return parts;
|
|
47
|
+
}
|
|
48
|
+
export function getZonedParts(instantMs, timeZone) {
|
|
49
|
+
if (!isValidTimeZone(timeZone))
|
|
50
|
+
return null;
|
|
51
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
52
|
+
timeZone,
|
|
53
|
+
hour12: false,
|
|
54
|
+
year: 'numeric',
|
|
55
|
+
month: '2-digit',
|
|
56
|
+
day: '2-digit',
|
|
57
|
+
hour: '2-digit',
|
|
58
|
+
minute: '2-digit',
|
|
59
|
+
second: '2-digit',
|
|
60
|
+
});
|
|
61
|
+
const values = { year: '', month: '', day: '', hour: '', minute: '', second: '' };
|
|
62
|
+
for (const part of formatter.formatToParts(new Date(instantMs))) {
|
|
63
|
+
if (part.type in values)
|
|
64
|
+
values[part.type] = part.value;
|
|
65
|
+
}
|
|
66
|
+
const year = Number.parseInt(values.year, 10);
|
|
67
|
+
const month = Number.parseInt(values.month, 10);
|
|
68
|
+
const day = Number.parseInt(values.day, 10);
|
|
69
|
+
const hour = Number.parseInt(values.hour, 10);
|
|
70
|
+
const minute = Number.parseInt(values.minute, 10);
|
|
71
|
+
if (![year, month, day, hour, minute].every(Number.isFinite))
|
|
72
|
+
return null;
|
|
73
|
+
return { year, month, day, hour, minute };
|
|
74
|
+
}
|
|
75
|
+
export function isValidTimeZone(timeZone) {
|
|
76
|
+
try {
|
|
77
|
+
new Intl.DateTimeFormat('en-US', { timeZone }).format(new Date());
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function getTimeZoneOffsetMs(instantMs, timeZone) {
|
|
85
|
+
const zoned = getZonedParts(instantMs, timeZone);
|
|
86
|
+
if (!zoned)
|
|
87
|
+
return null;
|
|
88
|
+
const asUtc = Date.UTC(zoned.year, zoned.month - 1, zoned.day, zoned.hour, zoned.minute, 0);
|
|
89
|
+
return asUtc - instantMs;
|
|
90
|
+
}
|
|
91
|
+
export function parseLocalDateTimeInZoneToUnixSeconds(value, timeZone) {
|
|
92
|
+
const target = parseLocalDateTime(value);
|
|
93
|
+
if (!target)
|
|
94
|
+
return null;
|
|
95
|
+
const targetUtcMs = Date.UTC(target.year, target.month - 1, target.day, target.hour, target.minute, 0);
|
|
96
|
+
let candidateMs = targetUtcMs;
|
|
97
|
+
for (let i = 0; i < 5; i += 1) {
|
|
98
|
+
const offsetMs = getTimeZoneOffsetMs(candidateMs, timeZone);
|
|
99
|
+
if (offsetMs === null)
|
|
100
|
+
return null;
|
|
101
|
+
const next = targetUtcMs - offsetMs;
|
|
102
|
+
if (Math.abs(next - candidateMs) < 1000) {
|
|
103
|
+
candidateMs = next;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
candidateMs = next;
|
|
107
|
+
}
|
|
108
|
+
const resolved = getZonedParts(candidateMs, timeZone);
|
|
109
|
+
if (!resolved ||
|
|
110
|
+
resolved.year !== target.year ||
|
|
111
|
+
resolved.month !== target.month ||
|
|
112
|
+
resolved.day !== target.day ||
|
|
113
|
+
resolved.hour !== target.hour ||
|
|
114
|
+
resolved.minute !== target.minute) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return Math.floor(candidateMs / 1000);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Inverse of `parseLocalDateTimeInZoneToUnixSeconds`: render an instant as the
|
|
121
|
+
* wall-clock digits an editor in `timeZone` would read, split for the date and
|
|
122
|
+
* time inputs. Returns null when the zone is unusable, so callers can fall back
|
|
123
|
+
* rather than silently display a wrong hour.
|
|
124
|
+
*/
|
|
125
|
+
export function formatInstantInZone(instantMs, timeZone) {
|
|
126
|
+
const parts = getZonedParts(instantMs, timeZone);
|
|
127
|
+
if (!parts)
|
|
128
|
+
return null;
|
|
129
|
+
const pad = (n, width = 2) => String(n).padStart(width, '0');
|
|
130
|
+
return {
|
|
131
|
+
date: `${pad(parts.year, 4)}-${pad(parts.month)}-${pad(parts.day)}`,
|
|
132
|
+
time: `${pad(parts.hour)}:${pad(parts.minute)}`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=publish-timezone.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish-timezone.js","sourceRoot":"","sources":["../../src/lib/publish-timezone.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/routes/content.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/routes/content.ts"],"names":[],"mappings":"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"}
|
package/dist/routes/content.js
CHANGED
|
@@ -29,6 +29,7 @@ import { applySlugRenameRedirects } from '../engine/slug-redirects.js';
|
|
|
29
29
|
import { invalidateMediaCaptureStatements, softDeleteContent } from '../engine/soft-delete.js';
|
|
30
30
|
import { normalizeSourceSections } from '../engine/source-sections.js';
|
|
31
31
|
import { TagInputError } from '../engine/taxonomy.js';
|
|
32
|
+
import { isValidTimeZone, PUBLISH_TIMEZONE_FALLBACK, parseLocalDateTimeInZoneToUnixSeconds, } from '../lib/publish-timezone.js';
|
|
32
33
|
import { parseLayoutEnvelope } from '../schema/layout.js';
|
|
33
34
|
import { parsePortableTextEnvelope } from '../schema/portable-text.js';
|
|
34
35
|
import { canPerformContentAction } from './authz-matrix.js';
|
|
@@ -68,8 +69,6 @@ function verifiedMediaDurationConflictResponse(error) {
|
|
|
68
69
|
reason: error.code,
|
|
69
70
|
}, 409);
|
|
70
71
|
}
|
|
71
|
-
const PUBLISH_TIMEZONE_FALLBACK = 'Europe/Paris';
|
|
72
|
-
const LOCAL_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/;
|
|
73
72
|
/**
|
|
74
73
|
* Map a content `action` verb (the ?action= query value) to the capability the
|
|
75
74
|
* 5-role matrix gates it on. Lifecycle/destructive verbs require publish-level
|
|
@@ -474,106 +473,7 @@ const ContributorItemSchema = z.object({
|
|
|
474
473
|
const ContributorsReplaceSchema = z.object({
|
|
475
474
|
contributors: z.array(ContributorItemSchema),
|
|
476
475
|
});
|
|
477
|
-
|
|
478
|
-
const match = LOCAL_DATE_TIME_RE.exec(value);
|
|
479
|
-
if (!match)
|
|
480
|
-
return null;
|
|
481
|
-
const [, y, m, d, h, min] = match;
|
|
482
|
-
const parts = {
|
|
483
|
-
year: Number.parseInt(y, 10),
|
|
484
|
-
month: Number.parseInt(m, 10),
|
|
485
|
-
day: Number.parseInt(d, 10),
|
|
486
|
-
hour: Number.parseInt(h, 10),
|
|
487
|
-
minute: Number.parseInt(min, 10),
|
|
488
|
-
};
|
|
489
|
-
if (!Number.isInteger(parts.year) ||
|
|
490
|
-
parts.month < 1 ||
|
|
491
|
-
parts.month > 12 ||
|
|
492
|
-
parts.day < 1 ||
|
|
493
|
-
parts.day > 31 ||
|
|
494
|
-
parts.hour < 0 ||
|
|
495
|
-
parts.hour > 23 ||
|
|
496
|
-
parts.minute < 0 ||
|
|
497
|
-
parts.minute > 59) {
|
|
498
|
-
return null;
|
|
499
|
-
}
|
|
500
|
-
const dateCheck = new Date(Date.UTC(parts.year, parts.month - 1, parts.day));
|
|
501
|
-
if (dateCheck.getUTCFullYear() !== parts.year ||
|
|
502
|
-
dateCheck.getUTCMonth() + 1 !== parts.month ||
|
|
503
|
-
dateCheck.getUTCDate() !== parts.day) {
|
|
504
|
-
return null;
|
|
505
|
-
}
|
|
506
|
-
return parts;
|
|
507
|
-
}
|
|
508
|
-
function getZonedParts(instantMs, timeZone) {
|
|
509
|
-
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
510
|
-
timeZone,
|
|
511
|
-
hour12: false,
|
|
512
|
-
year: 'numeric',
|
|
513
|
-
month: '2-digit',
|
|
514
|
-
day: '2-digit',
|
|
515
|
-
hour: '2-digit',
|
|
516
|
-
minute: '2-digit',
|
|
517
|
-
second: '2-digit',
|
|
518
|
-
});
|
|
519
|
-
const values = { year: '', month: '', day: '', hour: '', minute: '', second: '' };
|
|
520
|
-
for (const part of formatter.formatToParts(new Date(instantMs))) {
|
|
521
|
-
if (part.type in values)
|
|
522
|
-
values[part.type] = part.value;
|
|
523
|
-
}
|
|
524
|
-
const year = Number.parseInt(values.year, 10);
|
|
525
|
-
const month = Number.parseInt(values.month, 10);
|
|
526
|
-
const day = Number.parseInt(values.day, 10);
|
|
527
|
-
const hour = Number.parseInt(values.hour, 10);
|
|
528
|
-
const minute = Number.parseInt(values.minute, 10);
|
|
529
|
-
if (![year, month, day, hour, minute].every(Number.isFinite))
|
|
530
|
-
return null;
|
|
531
|
-
return { year, month, day, hour, minute };
|
|
532
|
-
}
|
|
533
|
-
function isValidTimeZone(timeZone) {
|
|
534
|
-
try {
|
|
535
|
-
new Intl.DateTimeFormat('en-US', { timeZone }).format(new Date());
|
|
536
|
-
return true;
|
|
537
|
-
}
|
|
538
|
-
catch {
|
|
539
|
-
return false;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
function getTimeZoneOffsetMs(instantMs, timeZone) {
|
|
543
|
-
const zoned = getZonedParts(instantMs, timeZone);
|
|
544
|
-
if (!zoned)
|
|
545
|
-
return null;
|
|
546
|
-
const asUtc = Date.UTC(zoned.year, zoned.month - 1, zoned.day, zoned.hour, zoned.minute, 0);
|
|
547
|
-
return asUtc - instantMs;
|
|
548
|
-
}
|
|
549
|
-
function parseLocalDateTimeInZoneToUnixSeconds(value, timeZone) {
|
|
550
|
-
const target = parseLocalDateTime(value);
|
|
551
|
-
if (!target)
|
|
552
|
-
return null;
|
|
553
|
-
const targetUtcMs = Date.UTC(target.year, target.month - 1, target.day, target.hour, target.minute, 0);
|
|
554
|
-
let candidateMs = targetUtcMs;
|
|
555
|
-
for (let i = 0; i < 5; i += 1) {
|
|
556
|
-
const offsetMs = getTimeZoneOffsetMs(candidateMs, timeZone);
|
|
557
|
-
if (offsetMs === null)
|
|
558
|
-
return null;
|
|
559
|
-
const next = targetUtcMs - offsetMs;
|
|
560
|
-
if (Math.abs(next - candidateMs) < 1000) {
|
|
561
|
-
candidateMs = next;
|
|
562
|
-
break;
|
|
563
|
-
}
|
|
564
|
-
candidateMs = next;
|
|
565
|
-
}
|
|
566
|
-
const resolved = getZonedParts(candidateMs, timeZone);
|
|
567
|
-
if (!resolved ||
|
|
568
|
-
resolved.year !== target.year ||
|
|
569
|
-
resolved.month !== target.month ||
|
|
570
|
-
resolved.day !== target.day ||
|
|
571
|
-
resolved.hour !== target.hour ||
|
|
572
|
-
resolved.minute !== target.minute) {
|
|
573
|
-
return null;
|
|
574
|
-
}
|
|
575
|
-
return Math.floor(candidateMs / 1000);
|
|
576
|
-
}
|
|
476
|
+
// --- timezone-aware local-datetime parsing (ported verbatim) --------------
|
|
577
477
|
// --- read-side payload helpers (ported verbatim) --------------------------
|
|
578
478
|
async function getContentPayload(ctx, type, id) {
|
|
579
479
|
if (type === 'article' || type === 'newsletter' || type === 'page') {
|