@blinkk/root-cms 3.1.4 → 3.1.6
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/app.js +3 -2
- package/dist/{chunk-AFBSITEO.js → chunk-PLSNGCYU.js} +200 -22
- package/dist/{chunk-MYN76V3L.js → chunk-U4TMO2LI.js} +2 -2
- package/dist/{chunk-NC3CRIFA.js → chunk-Z2CTQVYR.js} +1 -1
- package/dist/cli.js +1 -1
- package/dist/client.js +1 -1
- package/dist/core/client.d.ts +30 -2
- package/dist/core/cron-schedule.d.ts +63 -0
- package/dist/core/plugin.d.ts +6 -1
- package/dist/core/schema.d.ts +88 -3
- package/dist/core.js +1 -1
- package/dist/functions.js +2 -2
- package/dist/plugin.js +7 -5
- package/dist/richtext.js +1 -1
- package/dist/shared/embed-protocol.d.ts +19 -0
- package/dist/ui/AIPage-EO5WH25R.js +1 -0
- package/dist/ui/{AssetsPage-QUL2NBWK.js → AssetsPage-ESLB4R3I.js} +1 -1
- package/dist/ui/CollectionPage-MXN7XKUX.js +1 -0
- package/dist/ui/{ComparePage-DCP5QHDE.js → ComparePage-TY7VJVE3.js} +1 -1
- package/dist/ui/{DataPage-TUVFBALT.js → DataPage-YEIS7HBN.js} +1 -1
- package/dist/ui/{DataSourcePage-YE2ZOHYI.js → DataSourcePage-VD6YBDRW.js} +1 -1
- package/dist/ui/{DocTranslationsPage-3ILTQOP2.js → DocTranslationsPage-6XLREBPQ.js} +1 -1
- package/dist/ui/DocumentPage-2HNNEGRI.js +1 -0
- package/dist/ui/{EditDataSourcePage-WHPZDMD4.js → EditDataSourcePage-E4PSKSG3.js} +1 -1
- package/dist/ui/EditReleasePage-DOMXJXCU.js +1 -0
- package/dist/ui/EmbeddedAIPage-U5LONYH2.js +1 -0
- package/dist/ui/EmbeddedDocumentPage-HYV5GPK6.js +1 -0
- package/dist/ui/{ImageEditorDialog-Q44YTLB5.js → ImageEditorDialog-YDKOGVJV.js} +1 -1
- package/dist/ui/LogsPage-6KV3CQLH.js +1 -0
- package/dist/ui/NewDataSourcePage-NJJDHGPX.js +1 -0
- package/dist/ui/NewReleasePage-FXHPFUVG.js +1 -0
- package/dist/ui/{NotFoundPage-RMNPVSD3.js → NotFoundPage-CA7XVXUO.js} +1 -1
- package/dist/ui/{ProjectPage-VY4EJX6J.js → ProjectPage-XUBJSCWB.js} +1 -1
- package/dist/ui/ReleasePage-NLGRYJX7.js +1 -0
- package/dist/ui/{ReleasesPage-A4BLUNJE.js → ReleasesPage-OY7OKC3C.js} +1 -1
- package/dist/ui/{SettingsPage-MP3YHQDP.js → SettingsPage-5EQUY52A.js} +1 -1
- package/dist/ui/SidebarToolsPage-FWG6TIDI.js +1 -0
- package/dist/ui/TaskPage-APNRUOYG.js +1 -0
- package/dist/ui/TasksPage-FTEELXVZ.js +1 -0
- package/dist/ui/{TranslationsArbPage-B7TIRREV.js → TranslationsArbPage-PG5USRYB.js} +1 -1
- package/dist/ui/{TranslationsEditPage-VQG4YHA6.js → TranslationsEditPage-GV3ZJXTY.js} +1 -1
- package/dist/ui/{TranslationsPage-6VCVMNXI.js → TranslationsPage-3AKSMCNF.js} +1 -1
- package/dist/ui/{chunk-5SY2SVEX.js → chunk-235TRWJF.js} +2 -2
- package/dist/ui/{chunk-EYVQCOAD.js → chunk-2PTLQRTU.js} +1 -1
- package/dist/ui/{chunk-FF4CLAZS.js → chunk-2RRM4HEQ.js} +1 -1
- package/dist/ui/{chunk-MAZJRNAL.js → chunk-34W6H4YG.js} +1 -1
- package/dist/ui/{chunk-AZ2SK2ZK.js → chunk-3LEYSXZP.js} +1 -1
- package/dist/ui/{chunk-L3O3CLWO.js → chunk-44ZU5WMI.js} +1 -1
- package/dist/ui/{chunk-MYKJ6VAM.js → chunk-535JU7FU.js} +1 -1
- package/dist/ui/chunk-56NMZYX6.js +19 -0
- package/dist/ui/{chunk-3WQCVKK4.js → chunk-6WJQBFN5.js} +1 -1
- package/dist/ui/{chunk-UHK5N75L.js → chunk-75NMQ3LW.js} +1 -1
- package/dist/ui/{chunk-4N6CPIOQ.js → chunk-A7IZAHBZ.js} +1 -1
- package/dist/ui/{chunk-2PDMFZBF.js → chunk-C5FRMXDS.js} +1 -1
- package/dist/ui/chunk-EYJ6GKZR.js +1 -0
- package/dist/ui/{chunk-VB5VMEN7.js → chunk-F7RQT4XN.js} +1 -1
- package/dist/ui/chunk-G5647K25.js +1 -0
- package/dist/ui/{chunk-74OB6FM4.js → chunk-H37RXS4R.js} +1 -1
- package/dist/ui/{chunk-LC6WLVRS.js → chunk-HV3JJ5UT.js} +1 -1
- package/dist/ui/{chunk-K3UM5OQF.js → chunk-HXSB6YB2.js} +1 -1
- package/dist/ui/chunk-ISUGBMXC.js +1 -0
- package/dist/ui/{chunk-WN7HUDGY.js → chunk-JBLSUM6W.js} +1 -1
- package/dist/ui/chunk-LAXQZPE3.js +31 -0
- package/dist/ui/chunk-MYPLMHFW.js +1 -0
- package/dist/ui/{chunk-KBJ4AK5O.js → chunk-NCZD5IWS.js} +1 -1
- package/dist/ui/{chunk-XJUQOX4E.js → chunk-O7ESOZQO.js} +1 -1
- package/dist/ui/chunk-OWYV43BT.js +1 -0
- package/dist/ui/{chunk-XM35ZJBQ.js → chunk-PIJ3E47R.js} +1 -1
- package/dist/ui/{chunk-WAT22UHZ.js → chunk-PLYSJ7W5.js} +1 -1
- package/dist/ui/{chunk-H3PYPGNR.js → chunk-QFDU6FXP.js} +1 -1
- package/dist/ui/{chunk-JSWRHHTZ.js → chunk-QGAPMQEJ.js} +1 -1
- package/dist/ui/{chunk-W3ICOHMF.js → chunk-QLIIVG5Q.js} +1 -1
- package/dist/ui/{chunk-ELKXEMOU.js → chunk-QRAFYIAW.js} +1 -1
- package/dist/ui/{chunk-VRRRFAIV.js → chunk-QWB6MAPJ.js} +1 -1
- package/dist/ui/{chunk-GCMX5V3P.js → chunk-RB23LLKL.js} +1 -1
- package/dist/ui/{chunk-W2ZAVRAT.js → chunk-ST6QNIWT.js} +57 -57
- package/dist/ui/{chunk-76PG55CM.js → chunk-U7LSFUHT.js} +1 -1
- package/dist/ui/{chunk-HVYNDEA7.js → chunk-UCYWQEAS.js} +1 -1
- package/dist/ui/{chunk-6P75AZ3B.js → chunk-UJX25CXD.js} +1 -1
- package/dist/ui/chunk-UOKVF54Y.js +3 -0
- package/dist/ui/{chunk-CSUKZZK5.js → chunk-VJRMR2JM.js} +1 -1
- package/dist/ui/{chunk-WS37MDM3.js → chunk-WASID5UU.js} +1 -1
- package/dist/ui/chunk-WRA24P66.js +7 -0
- package/dist/ui/{chunk-ETPZ3TXX.js.LEGAL.txt → chunk-WRA24P66.js.LEGAL.txt} +4 -1
- package/dist/ui/{chunk-QHOZXW7Y.js → chunk-Y5EOMMNI.js} +1 -1
- package/dist/ui/{chunk-EIMRPYCD.js → chunk-YBHFGQ3Z.js} +1 -1
- package/dist/ui/{chunk-62VPTZJL.js → chunk-YP6ZH3MT.js} +1 -1
- package/dist/ui/chunk-Z2CRNO3I.js +1 -0
- package/dist/ui/{chunk-PR7NB5VK.js → chunk-Z3ZJJWFT.js} +1 -1
- package/dist/ui/{chunk-F4F5MH7A.js → chunk-ZAJPNVYS.js} +1 -1
- package/dist/ui/ui.css +1 -1
- package/dist/ui/ui.js +1 -1
- package/package.json +3 -3
- package/dist/ui/AIPage-4W4MFELY.js +0 -1
- package/dist/ui/CollectionPage-7VU6SUUP.js +0 -1
- package/dist/ui/DocumentPage-JFQ3Y2GH.js +0 -1
- package/dist/ui/EditReleasePage-QKWHIURV.js +0 -1
- package/dist/ui/EmbeddedAIPage-SQLOV4IT.js +0 -1
- package/dist/ui/EmbeddedDocumentPage-6FYHUHTE.js +0 -1
- package/dist/ui/LogsPage-RC5O722W.js +0 -1
- package/dist/ui/NewDataSourcePage-QXW54CHQ.js +0 -1
- package/dist/ui/NewReleasePage-TKYWVTOH.js +0 -1
- package/dist/ui/ReleasePage-VZAL4KUI.js +0 -1
- package/dist/ui/SidebarToolsPage-S5DMIEQ6.js +0 -1
- package/dist/ui/TaskPage-LCBDMA4I.js +0 -1
- package/dist/ui/TasksPage-72KVSUQY.js +0 -1
- package/dist/ui/chunk-ETPZ3TXX.js +0 -7
- package/dist/ui/chunk-IMXZTYAA.js +0 -3
- package/dist/ui/chunk-J5KDO4RM.js +0 -1
- package/dist/ui/chunk-NEA3XU6N.js +0 -19
- package/dist/ui/chunk-RN4XVVWD.js +0 -1
- package/dist/ui/chunk-SLLTYDVX.js +0 -1
- package/dist/ui/chunk-SWTNOWNE.js +0 -1
- package/dist/ui/chunk-V3G3554T.js +0 -31
- package/dist/ui/chunk-YV5SGR24.js +0 -1
- /package/dist/ui/{ImageEditorDialog-Q44YTLB5.js.LEGAL.txt → ImageEditorDialog-YDKOGVJV.js.LEGAL.txt} +0 -0
- /package/dist/ui/{chunk-5SY2SVEX.js.LEGAL.txt → chunk-235TRWJF.js.LEGAL.txt} +0 -0
- /package/dist/ui/{chunk-W2ZAVRAT.js.LEGAL.txt → chunk-ST6QNIWT.js.LEGAL.txt} +0 -0
package/dist/app.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getServerVersion,
|
|
3
3
|
serializeAiConfig
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-U4TMO2LI.js";
|
|
5
5
|
import {
|
|
6
6
|
getCollectionSchema,
|
|
7
7
|
getProjectSchemas
|
|
@@ -177,7 +177,8 @@ function serializeCollection(collection) {
|
|
|
177
177
|
autoSlug: collection.autoSlug,
|
|
178
178
|
autolock: collection.autolock,
|
|
179
179
|
autolockReason: collection.autolockReason,
|
|
180
|
-
sortOptions: collection.sortOptions
|
|
180
|
+
sortOptions: collection.sortOptions,
|
|
181
|
+
viewOptions: collection.viewOptions
|
|
181
182
|
};
|
|
182
183
|
}
|
|
183
184
|
function isFunction(value) {
|
|
@@ -9,6 +9,155 @@ import {
|
|
|
9
9
|
Timestamp as Timestamp2
|
|
10
10
|
} from "firebase-admin/firestore";
|
|
11
11
|
|
|
12
|
+
// core/cron-schedule.ts
|
|
13
|
+
var ONE_MINUTE_MS = 60 * 1e3;
|
|
14
|
+
var MAX_LOOKBACK_MS = 366 * 24 * 60 * 60 * 1e3;
|
|
15
|
+
var FIELD_RANGES = [
|
|
16
|
+
{ min: 0, max: 59 },
|
|
17
|
+
// minute
|
|
18
|
+
{ min: 0, max: 23 },
|
|
19
|
+
// hour
|
|
20
|
+
{ min: 1, max: 31 },
|
|
21
|
+
// day of month
|
|
22
|
+
{ min: 1, max: 12 },
|
|
23
|
+
// month
|
|
24
|
+
{ min: 0, max: 7 }
|
|
25
|
+
// day of week (0 and 7 both mean Sunday)
|
|
26
|
+
];
|
|
27
|
+
function parseField(field, range) {
|
|
28
|
+
const values = /* @__PURE__ */ new Set();
|
|
29
|
+
const parts = field.split(",");
|
|
30
|
+
for (const part of parts) {
|
|
31
|
+
const trimmed = part.trim();
|
|
32
|
+
if (!trimmed) {
|
|
33
|
+
throw new Error(`invalid cron field: "${field}"`);
|
|
34
|
+
}
|
|
35
|
+
let rangePart = trimmed;
|
|
36
|
+
let step = 1;
|
|
37
|
+
if (trimmed.includes("/")) {
|
|
38
|
+
const [rangeStr, stepStr] = trimmed.split("/");
|
|
39
|
+
rangePart = rangeStr;
|
|
40
|
+
step = Number(stepStr);
|
|
41
|
+
if (!Number.isInteger(step) || step <= 0) {
|
|
42
|
+
throw new Error(`invalid cron step: "${trimmed}"`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
let start;
|
|
46
|
+
let end;
|
|
47
|
+
if (rangePart === "*") {
|
|
48
|
+
start = range.min;
|
|
49
|
+
end = range.max;
|
|
50
|
+
} else if (rangePart.includes("-")) {
|
|
51
|
+
const [startStr, endStr] = rangePart.split("-");
|
|
52
|
+
start = Number(startStr);
|
|
53
|
+
end = Number(endStr);
|
|
54
|
+
} else {
|
|
55
|
+
start = Number(rangePart);
|
|
56
|
+
end = start;
|
|
57
|
+
}
|
|
58
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < range.min || end > range.max || start > end) {
|
|
59
|
+
throw new Error(`invalid cron field: "${field}"`);
|
|
60
|
+
}
|
|
61
|
+
for (let value = start; value <= end; value += step) {
|
|
62
|
+
values.add(value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return values;
|
|
66
|
+
}
|
|
67
|
+
function parseCronExpression(expression) {
|
|
68
|
+
const fields = expression.trim().split(/\s+/);
|
|
69
|
+
if (fields.length !== 5) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`invalid cron expression: "${expression}" (expected 5 fields)`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const [minute, hour, dayOfMonth, month, dayOfWeek] = fields;
|
|
75
|
+
const parsedDayOfWeek = parseField(dayOfWeek, FIELD_RANGES[4]);
|
|
76
|
+
if (parsedDayOfWeek.has(7)) {
|
|
77
|
+
parsedDayOfWeek.delete(7);
|
|
78
|
+
parsedDayOfWeek.add(0);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
minute: parseField(minute, FIELD_RANGES[0]),
|
|
82
|
+
hour: parseField(hour, FIELD_RANGES[1]),
|
|
83
|
+
dayOfMonth: parseField(dayOfMonth, FIELD_RANGES[2]),
|
|
84
|
+
month: parseField(month, FIELD_RANGES[3]),
|
|
85
|
+
dayOfWeek: parsedDayOfWeek,
|
|
86
|
+
dayOfMonthRestricted: dayOfMonth.trim() !== "*",
|
|
87
|
+
dayOfWeekRestricted: dayOfWeek.trim() !== "*"
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function getZonedFields(date, timeZone) {
|
|
91
|
+
const dtf = new Intl.DateTimeFormat("en-US", {
|
|
92
|
+
timeZone,
|
|
93
|
+
hour12: false,
|
|
94
|
+
year: "numeric",
|
|
95
|
+
month: "2-digit",
|
|
96
|
+
day: "2-digit",
|
|
97
|
+
hour: "2-digit",
|
|
98
|
+
minute: "2-digit"
|
|
99
|
+
});
|
|
100
|
+
const parts = {};
|
|
101
|
+
for (const part of dtf.formatToParts(date)) {
|
|
102
|
+
parts[part.type] = part.value;
|
|
103
|
+
}
|
|
104
|
+
const year = Number(parts.year);
|
|
105
|
+
const month = Number(parts.month);
|
|
106
|
+
const day = Number(parts.day);
|
|
107
|
+
let hour = Number(parts.hour);
|
|
108
|
+
if (hour === 24) {
|
|
109
|
+
hour = 0;
|
|
110
|
+
}
|
|
111
|
+
const minute = Number(parts.minute);
|
|
112
|
+
const dayOfWeek = new Date(Date.UTC(year, month - 1, day)).getUTCDay();
|
|
113
|
+
return { minute, hour, dayOfMonth: day, month, dayOfWeek };
|
|
114
|
+
}
|
|
115
|
+
function matchesCron(parsed, fields) {
|
|
116
|
+
if (!parsed.minute.has(fields.minute)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
if (!parsed.hour.has(fields.hour)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
if (!parsed.month.has(fields.month)) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
const domMatch = parsed.dayOfMonth.has(fields.dayOfMonth);
|
|
126
|
+
const dowMatch = parsed.dayOfWeek.has(fields.dayOfWeek);
|
|
127
|
+
if (parsed.dayOfMonthRestricted && parsed.dayOfWeekRestricted) {
|
|
128
|
+
return domMatch || dowMatch;
|
|
129
|
+
}
|
|
130
|
+
if (parsed.dayOfMonthRestricted) {
|
|
131
|
+
return domMatch;
|
|
132
|
+
}
|
|
133
|
+
if (parsed.dayOfWeekRestricted) {
|
|
134
|
+
return dowMatch;
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
function isCronDue(options) {
|
|
139
|
+
const { expression, now } = options;
|
|
140
|
+
const timezone = options.timezone || "UTC";
|
|
141
|
+
const parsed = parseCronExpression(expression);
|
|
142
|
+
let lastSyncMs = options.lastSyncMs;
|
|
143
|
+
if (lastSyncMs > now) {
|
|
144
|
+
lastSyncMs = now;
|
|
145
|
+
}
|
|
146
|
+
const nowMinute = Math.floor(now / ONE_MINUTE_MS) * ONE_MINUTE_MS;
|
|
147
|
+
let lowerBound;
|
|
148
|
+
if (lastSyncMs > 0) {
|
|
149
|
+
lowerBound = Math.max(lastSyncMs, now - MAX_LOOKBACK_MS);
|
|
150
|
+
} else {
|
|
151
|
+
lowerBound = nowMinute - ONE_MINUTE_MS;
|
|
152
|
+
}
|
|
153
|
+
for (let t = nowMinute; t > lowerBound; t -= ONE_MINUTE_MS) {
|
|
154
|
+
if (matchesCron(parsed, getZonedFields(new Date(t), timezone))) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
12
161
|
// core/translations-manager.ts
|
|
13
162
|
import {
|
|
14
163
|
FieldValue,
|
|
@@ -670,6 +819,7 @@ function setValueAtPath(obj, path, value) {
|
|
|
670
819
|
}
|
|
671
820
|
|
|
672
821
|
// core/client.ts
|
|
822
|
+
var FIRESTORE_DOC_ID_MAX_BYTES = 1500;
|
|
673
823
|
var RootCMSClient = class {
|
|
674
824
|
constructor(rootConfig) {
|
|
675
825
|
this.rootConfig = rootConfig;
|
|
@@ -708,6 +858,9 @@ var RootCMSClient = class {
|
|
|
708
858
|
}
|
|
709
859
|
const modeCollection = this.getModeCollection(options.mode);
|
|
710
860
|
slug = normalizeSlug(slug);
|
|
861
|
+
if (Buffer.byteLength(slug, "utf8") > FIRESTORE_DOC_ID_MAX_BYTES) {
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
711
864
|
const dbPath = `Projects/${this.projectId}/Collections/${collectionId}/${modeCollection}/${slug}`;
|
|
712
865
|
const docRef = this.db.doc(dbPath);
|
|
713
866
|
const doc = await docRef.get();
|
|
@@ -1298,32 +1451,35 @@ ${errorMessages}`);
|
|
|
1298
1451
|
for (const snapshot of querySnapshot.docs) {
|
|
1299
1452
|
const dataSource = snapshot.data();
|
|
1300
1453
|
const cron = dataSource.cron;
|
|
1301
|
-
if (!cron || !cron.enabled
|
|
1302
|
-
continue;
|
|
1303
|
-
}
|
|
1304
|
-
const interval = Number(cron.interval);
|
|
1305
|
-
if (!Number.isFinite(interval) || interval <= 0 || !Number.isInteger(interval)) {
|
|
1454
|
+
if (!cron || !cron.enabled) {
|
|
1306
1455
|
continue;
|
|
1307
1456
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
case "days":
|
|
1317
|
-
intervalMs *= 24 * 60 * 60 * 1e3;
|
|
1318
|
-
break;
|
|
1319
|
-
default:
|
|
1457
|
+
const lastSyncMs = dataSource.syncedAt ? dataSource.syncedAt.toMillis() : 0;
|
|
1458
|
+
const schedule = cron.schedule || "interval";
|
|
1459
|
+
let due;
|
|
1460
|
+
if (schedule === "interval") {
|
|
1461
|
+
due = isIntervalDue(cron, lastSyncMs, now);
|
|
1462
|
+
} else {
|
|
1463
|
+
const expression = (cron.expression || "").trim();
|
|
1464
|
+
if (!expression) {
|
|
1320
1465
|
continue;
|
|
1466
|
+
}
|
|
1467
|
+
try {
|
|
1468
|
+
due = isCronDue({
|
|
1469
|
+
expression,
|
|
1470
|
+
timezone: cron.timezone,
|
|
1471
|
+
lastSyncMs,
|
|
1472
|
+
now
|
|
1473
|
+
});
|
|
1474
|
+
} catch (err) {
|
|
1475
|
+
console.error(
|
|
1476
|
+
`cron: invalid cron expression for data source ${dataSource.id}: "${expression}":`,
|
|
1477
|
+
String(err)
|
|
1478
|
+
);
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1321
1481
|
}
|
|
1322
|
-
|
|
1323
|
-
if (lastSyncMs > now) {
|
|
1324
|
-
lastSyncMs = now;
|
|
1325
|
-
}
|
|
1326
|
-
if (now - lastSyncMs < intervalMs) {
|
|
1482
|
+
if (!due) {
|
|
1327
1483
|
continue;
|
|
1328
1484
|
}
|
|
1329
1485
|
try {
|
|
@@ -2147,6 +2303,28 @@ function unmarshalArray(arrObject) {
|
|
|
2147
2303
|
function isObject(data) {
|
|
2148
2304
|
return typeof data === "object" && !Array.isArray(data) && data !== null;
|
|
2149
2305
|
}
|
|
2306
|
+
function isIntervalDue(cron, lastSyncMs, now) {
|
|
2307
|
+
const interval = Number(cron.interval);
|
|
2308
|
+
if (!cron.unit || !Number.isFinite(interval) || interval <= 0 || !Number.isInteger(interval)) {
|
|
2309
|
+
return false;
|
|
2310
|
+
}
|
|
2311
|
+
let intervalMs = interval;
|
|
2312
|
+
switch (cron.unit) {
|
|
2313
|
+
case "minutes":
|
|
2314
|
+
intervalMs *= 60 * 1e3;
|
|
2315
|
+
break;
|
|
2316
|
+
case "hours":
|
|
2317
|
+
intervalMs *= 60 * 60 * 1e3;
|
|
2318
|
+
break;
|
|
2319
|
+
case "days":
|
|
2320
|
+
intervalMs *= 24 * 60 * 60 * 1e3;
|
|
2321
|
+
break;
|
|
2322
|
+
default:
|
|
2323
|
+
return false;
|
|
2324
|
+
}
|
|
2325
|
+
const normalizedLastSyncMs = lastSyncMs > now ? now : lastSyncMs;
|
|
2326
|
+
return now - normalizedLastSyncMs >= intervalMs;
|
|
2327
|
+
}
|
|
2150
2328
|
function parseSpreadsheetUrl(url) {
|
|
2151
2329
|
if (!url.startsWith("https://docs.google.com/spreadsheets/d/")) {
|
|
2152
2330
|
return null;
|
|
@@ -552,7 +552,7 @@ async function generateImage(rootConfig, options) {
|
|
|
552
552
|
// package.json
|
|
553
553
|
var package_default = {
|
|
554
554
|
name: "@blinkk/root-cms",
|
|
555
|
-
version: "3.1.
|
|
555
|
+
version: "3.1.6",
|
|
556
556
|
author: "s@blinkk.com",
|
|
557
557
|
license: "MIT",
|
|
558
558
|
engines: {
|
|
@@ -727,7 +727,7 @@ var package_default = {
|
|
|
727
727
|
yjs: "13.6.27"
|
|
728
728
|
},
|
|
729
729
|
peerDependencies: {
|
|
730
|
-
"@blinkk/root": "3.1.
|
|
730
|
+
"@blinkk/root": "3.1.6",
|
|
731
731
|
"firebase-admin": ">=11",
|
|
732
732
|
"firebase-functions": ">=4"
|
|
733
733
|
},
|
package/dist/cli.js
CHANGED
package/dist/client.js
CHANGED
package/dist/core/client.d.ts
CHANGED
|
@@ -39,10 +39,38 @@ export type DocMode = 'draft' | 'published';
|
|
|
39
39
|
export type UserRole = 'ADMIN' | 'EDITOR' | 'CONTRIBUTOR' | 'VIEWER';
|
|
40
40
|
export type HttpMethod = 'GET' | 'POST';
|
|
41
41
|
export type CronUnit = 'minutes' | 'hours' | 'days';
|
|
42
|
+
/**
|
|
43
|
+
* Data source sync schedule type.
|
|
44
|
+
*
|
|
45
|
+
* - `interval`: sync every N minutes/hours/days (uses `interval` + `unit`).
|
|
46
|
+
* - `daily` / `weekly` / `custom`: sync on a specific cron schedule (uses
|
|
47
|
+
* `expression` + `timezone`). `daily` and `weekly` are UI presets that are
|
|
48
|
+
* stored as standard cron expressions; `custom` allows an arbitrary
|
|
49
|
+
* expression.
|
|
50
|
+
*/
|
|
51
|
+
export type CronScheduleType = 'interval' | 'daily' | 'weekly' | 'custom';
|
|
42
52
|
export interface DataSourceCron {
|
|
43
53
|
enabled: boolean;
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Scheduling mode. Defaults to `interval` when unset (for backwards
|
|
56
|
+
* compatibility with data sources created before specific schedules were
|
|
57
|
+
* supported).
|
|
58
|
+
*/
|
|
59
|
+
schedule?: CronScheduleType;
|
|
60
|
+
/** Interval value, used when `schedule` is `interval`. */
|
|
61
|
+
interval?: number;
|
|
62
|
+
/** Interval unit, used when `schedule` is `interval`. */
|
|
63
|
+
unit?: CronUnit;
|
|
64
|
+
/**
|
|
65
|
+
* Standard 5-field cron expression, used when `schedule` is `daily`,
|
|
66
|
+
* `weekly`, or `custom`, e.g. `0 19 * * *` for "every day at 7pm".
|
|
67
|
+
*/
|
|
68
|
+
expression?: string;
|
|
69
|
+
/**
|
|
70
|
+
* IANA timezone used to evaluate `expression`, e.g. `America/New_York`.
|
|
71
|
+
* Defaults to UTC when unset.
|
|
72
|
+
*/
|
|
73
|
+
timezone?: string;
|
|
46
74
|
autoPublish?: boolean;
|
|
47
75
|
}
|
|
48
76
|
export interface DataSource {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for evaluating standard cron expressions for data source
|
|
3
|
+
* scheduling.
|
|
4
|
+
*
|
|
5
|
+
* Supports the standard 5-field cron format:
|
|
6
|
+
*
|
|
7
|
+
* ┌───────────── minute (0 - 59)
|
|
8
|
+
* │ ┌───────────── hour (0 - 23)
|
|
9
|
+
* │ │ ┌───────────── day of month (1 - 31)
|
|
10
|
+
* │ │ │ ┌───────────── month (1 - 12)
|
|
11
|
+
* │ │ │ │ ┌───────────── day of week (0 - 6, where 0 and 7 are Sunday)
|
|
12
|
+
* │ │ │ │ │
|
|
13
|
+
* * * * * *
|
|
14
|
+
*
|
|
15
|
+
* Each field supports `*`, single values, ranges (`a-b`), lists (`a,b,c`),
|
|
16
|
+
* steps (`* /n`, `a-b/n`), and combinations thereof. Expressions are evaluated
|
|
17
|
+
* against a specific IANA timezone so that schedules like "every day at 7pm"
|
|
18
|
+
* fire at the expected wall-clock time.
|
|
19
|
+
*/
|
|
20
|
+
interface ParsedCron {
|
|
21
|
+
minute: Set<number>;
|
|
22
|
+
hour: Set<number>;
|
|
23
|
+
dayOfMonth: Set<number>;
|
|
24
|
+
month: Set<number>;
|
|
25
|
+
dayOfWeek: Set<number>;
|
|
26
|
+
/** Whether the day-of-month field was restricted (not `*`). */
|
|
27
|
+
dayOfMonthRestricted: boolean;
|
|
28
|
+
/** Whether the day-of-week field was restricted (not `*`). */
|
|
29
|
+
dayOfWeekRestricted: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parses a standard 5-field cron expression. Throws if the expression is
|
|
33
|
+
* malformed.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseCronExpression(expression: string): ParsedCron;
|
|
36
|
+
/**
|
|
37
|
+
* Returns true if the given cron expression is syntactically valid.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isValidCronExpression(expression: string): boolean;
|
|
40
|
+
export interface IsCronDueOptions {
|
|
41
|
+
/** Standard 5-field cron expression, e.g. "0 19 * * *". */
|
|
42
|
+
expression: string;
|
|
43
|
+
/** IANA timezone used to evaluate the expression. Defaults to "UTC". */
|
|
44
|
+
timezone?: string;
|
|
45
|
+
/** Timestamp (ms) of the last successful sync, or 0 if never synced. */
|
|
46
|
+
lastSyncMs: number;
|
|
47
|
+
/** Current time (ms). */
|
|
48
|
+
now: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Determines whether a cron-scheduled data source is due for sync.
|
|
52
|
+
*
|
|
53
|
+
* Uses catch-up semantics: the source is due if there is any scheduled minute
|
|
54
|
+
* in the window `(lastSyncMs, now]` that matches the cron expression. This
|
|
55
|
+
* ensures a missed cron tick (e.g. the scheduler was briefly down at the
|
|
56
|
+
* scheduled minute) still triggers a sync on a later tick.
|
|
57
|
+
*
|
|
58
|
+
* When the source has never synced (`lastSyncMs <= 0`), only the current minute
|
|
59
|
+
* is considered so that enabling a schedule does not trigger an unexpected
|
|
60
|
+
* immediate backfill — the source will first sync at its next scheduled time.
|
|
61
|
+
*/
|
|
62
|
+
export declare function isCronDue(options: IsCronDueOptions): boolean;
|
|
63
|
+
export {};
|
package/dist/core/plugin.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { App } from 'firebase-admin/app';
|
|
|
3
3
|
import { Firestore } from 'firebase-admin/firestore';
|
|
4
4
|
import { type AiConfig } from './ai.js';
|
|
5
5
|
import { type CMSCheck } from './checks.js';
|
|
6
|
-
import { Action } from './client.js';
|
|
6
|
+
import { Action, UserRole } from './client.js';
|
|
7
7
|
import { type CMSNotificationService } from './services-notifications.js';
|
|
8
8
|
import { type CMSTranslationService } from './translations.js';
|
|
9
9
|
export type { AiConfig, AiModelConfig, AiProvider } from './ai.js';
|
|
@@ -19,6 +19,11 @@ export type { CMSTranslationService, RootLocale, TranslationExportResult, Transl
|
|
|
19
19
|
export type CMSBuiltInSidebarTool = 'home' | 'content' | 'releases' | 'data' | 'assets' | 'translations' | 'ai' | 'settings';
|
|
20
20
|
export interface CMSUser {
|
|
21
21
|
email: string;
|
|
22
|
+
/**
|
|
23
|
+
* The user's role within the project's ACL, or `null` if unassigned. This is
|
|
24
|
+
* populated on `req.user` for every authenticated request.
|
|
25
|
+
*/
|
|
26
|
+
role?: UserRole | null;
|
|
22
27
|
}
|
|
23
28
|
/**
|
|
24
29
|
* @deprecated The `experiments.ai` flag is now used only as a sidebar toggle.
|
package/dist/core/schema.d.ts
CHANGED
|
@@ -17,6 +17,64 @@ export interface CommonFieldProps {
|
|
|
17
17
|
/** Hides just the field label in the CMS UI. */
|
|
18
18
|
hideLabel?: boolean;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Sources the selectable values for a field from a field within another CMS
|
|
22
|
+
* document.
|
|
23
|
+
*
|
|
24
|
+
* Use this when a set of allowed values is managed centrally in the CMS (e.g. a
|
|
25
|
+
* list of feature flags defined in a global module) and you want other fields
|
|
26
|
+
* to pick from that list instead of typing free-form strings.
|
|
27
|
+
*
|
|
28
|
+
* Example: source a `flag` select's options from the `name` of each item in
|
|
29
|
+
* the `flags` array of the `GlobalModules/flags` document.
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* schema.select({
|
|
33
|
+
* id: 'flag',
|
|
34
|
+
* source: {
|
|
35
|
+
* doc: 'GlobalModules/flags',
|
|
36
|
+
* field: 'flags',
|
|
37
|
+
* valueKey: 'name',
|
|
38
|
+
* helpKey: 'description',
|
|
39
|
+
* },
|
|
40
|
+
* creatable: true,
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export interface DocumentSource {
|
|
45
|
+
/**
|
|
46
|
+
* The document to source values from, in `<collection>/<slug>` format, e.g.
|
|
47
|
+
* `GlobalModules/flags`.
|
|
48
|
+
*/
|
|
49
|
+
doc: string;
|
|
50
|
+
/**
|
|
51
|
+
* The field within the document that holds the values, e.g. `flags`. This is
|
|
52
|
+
* usually an array; for an array of objects use `valueKey`/`labelKey` to map
|
|
53
|
+
* each item, and for an array of strings each string is used directly.
|
|
54
|
+
*/
|
|
55
|
+
field: string;
|
|
56
|
+
/**
|
|
57
|
+
* The key on each item to use as the stored value, e.g. `name`. Dot-paths are
|
|
58
|
+
* supported for nested keys (e.g. `meta.id`). Omit for arrays of strings.
|
|
59
|
+
*/
|
|
60
|
+
valueKey?: string;
|
|
61
|
+
/**
|
|
62
|
+
* The key on each item to use as the display label, e.g. `name`. Dot-paths
|
|
63
|
+
* are supported. Defaults to `valueKey`.
|
|
64
|
+
*/
|
|
65
|
+
labelKey?: string;
|
|
66
|
+
/**
|
|
67
|
+
* The key on each item to use as secondary help text, shown in a smaller,
|
|
68
|
+
* dimmed line beneath the label in the dropdown, e.g. `description`. Dot-paths
|
|
69
|
+
* are supported.
|
|
70
|
+
*/
|
|
71
|
+
helpKey?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The source of selectable values for a field. Currently only a CMS document
|
|
75
|
+
* source is supported; other source types may be added in the future.
|
|
76
|
+
*/
|
|
77
|
+
export type FieldValueSource = DocumentSource;
|
|
20
78
|
export type StringField = CommonFieldProps & {
|
|
21
79
|
type: 'string';
|
|
22
80
|
default?: string;
|
|
@@ -67,19 +125,27 @@ export declare function boolean(field: Omit<BooleanField, 'type'>): BooleanField
|
|
|
67
125
|
export type SelectField = CommonFieldProps & {
|
|
68
126
|
type: 'select';
|
|
69
127
|
default?: string;
|
|
128
|
+
/** A static list of options to choose from. */
|
|
70
129
|
options?: Array<{
|
|
71
130
|
value: string;
|
|
72
131
|
label?: string;
|
|
73
132
|
}> | string[];
|
|
133
|
+
/**
|
|
134
|
+
* Dynamically source the options from a field in another CMS document,
|
|
135
|
+
* instead of the static `options` list. See {@link DocumentSource}.
|
|
136
|
+
*/
|
|
137
|
+
source?: FieldValueSource;
|
|
138
|
+
/**
|
|
139
|
+
* Whether to allow the user to enter arbitrary values that aren't in the
|
|
140
|
+
* options/source list. Defaults to `false`.
|
|
141
|
+
*/
|
|
142
|
+
creatable?: boolean;
|
|
74
143
|
translate?: boolean;
|
|
75
144
|
searchable?: boolean;
|
|
76
145
|
};
|
|
77
146
|
export declare function select(field: Omit<SelectField, 'type'>): SelectField;
|
|
78
147
|
export type MultiSelectField = Omit<SelectField, 'type'> & {
|
|
79
148
|
type: 'multiselect';
|
|
80
|
-
/** Set to `true` to allow users to create arbitrary values. */
|
|
81
|
-
creatable?: boolean;
|
|
82
|
-
translate?: boolean;
|
|
83
149
|
};
|
|
84
150
|
export declare function multiselect(field: Omit<MultiSelectField, 'type'>): MultiSelectField;
|
|
85
151
|
export type ImageField = CommonFieldProps & {
|
|
@@ -416,6 +482,25 @@ export type Collection = SchemaWithTypes & {
|
|
|
416
482
|
/** Sort direction. Defaults to ascending. */
|
|
417
483
|
direction?: 'asc' | 'desc';
|
|
418
484
|
}>;
|
|
485
|
+
/**
|
|
486
|
+
* Options that control how the document listing for this collection is
|
|
487
|
+
* rendered in the CMS.
|
|
488
|
+
*/
|
|
489
|
+
viewOptions?: {
|
|
490
|
+
/**
|
|
491
|
+
* Forces the document listing to always use the "compact" view, which
|
|
492
|
+
* renders a condensed table (image, doc id, title, badges, and
|
|
493
|
+
* created/modified timestamps) instead of the default cards with large
|
|
494
|
+
* titles and preview URLs. Useful for collections that don't have
|
|
495
|
+
* meaningful preview metadata (e.g. redirects).
|
|
496
|
+
*
|
|
497
|
+
* When set, the listing is locked to the compact layout and the CMS
|
|
498
|
+
* "View" density control is disabled. When unset, users can choose the
|
|
499
|
+
* listing density (their choice is remembered globally and stays sticky as
|
|
500
|
+
* they navigate between collections).
|
|
501
|
+
*/
|
|
502
|
+
compact?: boolean;
|
|
503
|
+
};
|
|
419
504
|
};
|
|
420
505
|
export declare function defineCollection(collection: Omit<Collection, 'id'>): Omit<Collection, 'id'>;
|
|
421
506
|
export declare const collection: typeof defineCollection;
|
package/dist/core.js
CHANGED
package/dist/functions.js
CHANGED
package/dist/plugin.js
CHANGED
|
@@ -12,16 +12,16 @@ import {
|
|
|
12
12
|
serializeAiConfig,
|
|
13
13
|
summarizeDiff,
|
|
14
14
|
translateString
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-U4TMO2LI.js";
|
|
16
16
|
import {
|
|
17
17
|
SearchIndexService,
|
|
18
18
|
runCronJobs
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-Z2CTQVYR.js";
|
|
20
20
|
import {
|
|
21
21
|
RootCMSClient,
|
|
22
22
|
parseDocId,
|
|
23
23
|
unmarshalData
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-PLSNGCYU.js";
|
|
25
25
|
import "./chunk-J72JK4KR.js";
|
|
26
26
|
import "./chunk-MLKGABMK.js";
|
|
27
27
|
|
|
@@ -1389,7 +1389,8 @@ function cmsPlugin(options) {
|
|
|
1389
1389
|
console.log("session failed: user is not in the firestore acl list");
|
|
1390
1390
|
return null;
|
|
1391
1391
|
}
|
|
1392
|
-
|
|
1392
|
+
const role = await cmsClient.getUserRole(jwt.email).catch(() => null);
|
|
1393
|
+
return { email: jwt.email, role };
|
|
1393
1394
|
}
|
|
1394
1395
|
try {
|
|
1395
1396
|
const jwt = await auth.verifySessionCookie(sessionCookie, true);
|
|
@@ -1416,7 +1417,8 @@ function cmsPlugin(options) {
|
|
|
1416
1417
|
console.log("session failed: user is not in the firestore acl list");
|
|
1417
1418
|
return null;
|
|
1418
1419
|
}
|
|
1419
|
-
|
|
1420
|
+
const role = await cmsClient.getUserRole(jwt.email).catch(() => null);
|
|
1421
|
+
return { email: jwt.email, role };
|
|
1420
1422
|
} catch (err) {
|
|
1421
1423
|
console.error("failed to verify jwt token");
|
|
1422
1424
|
console.error(err);
|
package/dist/richtext.js
CHANGED
|
@@ -63,7 +63,7 @@ var ALLOWED_ATTRIBUTES = {
|
|
|
63
63
|
img: ["src", "alt", "title", "width", "height"]
|
|
64
64
|
};
|
|
65
65
|
var COMMON_OPTIONS = {
|
|
66
|
-
allowedSchemes: ["http", "https", "mailto"],
|
|
66
|
+
allowedSchemes: ["http", "https", "mailto", "tel", "sms"],
|
|
67
67
|
allowedSchemesByTag: {
|
|
68
68
|
img: ["http", "https", "data"]
|
|
69
69
|
},
|
|
@@ -44,6 +44,23 @@ export interface RootEmbedMessage {
|
|
|
44
44
|
error?: string;
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Posted from an iframed sidebar tool to the parent CMS window to report the
|
|
49
|
+
* tool's current location. The CMS mirrors this into its own address bar so
|
|
50
|
+
* the tool's sub-path, query params, and hash survive a refresh and can be
|
|
51
|
+
* deep-linked/shared.
|
|
52
|
+
*
|
|
53
|
+
* Same-origin tools are synced automatically (the CMS reads
|
|
54
|
+
* `contentWindow.location` directly), so this message only needs to be posted
|
|
55
|
+
* by cross-origin tools, which the browser prevents the CMS from reading.
|
|
56
|
+
*/
|
|
57
|
+
export interface RootToolLocationMessage {
|
|
58
|
+
rootTool: {
|
|
59
|
+
type: 'locationchange';
|
|
60
|
+
/** The tool's current URL, absolute or relative to the tool's origin. */
|
|
61
|
+
url: string;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
47
64
|
/** Requests that the doc editor scroll to (focus) a specific field. */
|
|
48
65
|
export interface ScrollToDeeplinkMessage {
|
|
49
66
|
scrollToDeeplink: {
|
|
@@ -66,6 +83,8 @@ export interface HighlightNodeMessage {
|
|
|
66
83
|
}
|
|
67
84
|
/** Returns whether a postMessage payload is a {@link RootEmbedMessage}. */
|
|
68
85
|
export declare function isRootEmbedMessage(data: unknown): data is RootEmbedMessage;
|
|
86
|
+
/** Returns whether a postMessage payload is a {@link RootToolLocationMessage}. */
|
|
87
|
+
export declare function isRootToolLocationMessage(data: unknown): data is RootToolLocationMessage;
|
|
69
88
|
/** Returns whether a postMessage payload is a {@link ScrollToDeeplinkMessage}. */
|
|
70
89
|
export declare function isScrollToDeeplinkMessage(data: unknown): data is ScrollToDeeplinkMessage;
|
|
71
90
|
/** Returns whether a postMessage payload is a {@link HighlightNodeMessage}. */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a}from"./chunk-56NMZYX6.js";import"./chunk-LAXQZPE3.js";import"./chunk-QWB6MAPJ.js";import"./chunk-235TRWJF.js";import"./chunk-AAR7DBEN.js";import"./chunk-YDO2EDVY.js";import"./chunk-5MV6XVGA.js";import{a as i}from"./chunk-QLIIVG5Q.js";import{c as o}from"./chunk-A7IZAHBZ.js";import"./chunk-AAHM6RYJ.js";import"./chunk-ISUGBMXC.js";import"./chunk-DZYYWCHX.js";import"./chunk-XOWWPE7X.js";import"./chunk-WASID5UU.js";import{Ga as t}from"./chunk-WRA24P66.js";import"./chunk-KUCVLANF.js";import"./chunk-7SK47HHZ.js";import"./chunk-DTMZLUNY.js";import"./chunk-PVAE643M.js";function n(r){return i("AI"),t(o,{children:t(a,{variant:"page",initialChatId:r.chatId})})}export{n as AIPage};
|