@micelio/strapi-plugin-core 0.1.0 → 0.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.
Files changed (54) hide show
  1. package/README.md +5 -3
  2. package/dist/admin/MicelioLayout-Bmu5xCw9.js +1023 -0
  3. package/dist/admin/MicelioLayout-DMMsKnMo.mjs +1021 -0
  4. package/dist/admin/PreviewHeaderAction-BaduMvYy.mjs +61 -0
  5. package/dist/admin/PreviewHeaderAction-CGHycv4I.js +61 -0
  6. package/dist/admin/{en-D-T8z3qh.js → en-BDVuq0fN.js} +37 -1
  7. package/dist/admin/{en-C2O-S2ym.mjs → en-Bahc6y6N.mjs} +37 -1
  8. package/dist/admin/{es-RV0vkOHh.js → es-DStBGkm7.js} +37 -1
  9. package/dist/admin/{es-CQ2tv9N8.mjs → es-nUHBIoZG.mjs} +37 -1
  10. package/dist/admin/{index-D3TGbaQ7.js → index-D_Dbvo2F.js} +8 -2
  11. package/dist/admin/{index-CiSujTES.mjs → index-Dwr15ZsB.mjs} +8 -2
  12. package/dist/admin/index.js +1 -1
  13. package/dist/admin/index.mjs +1 -1
  14. package/dist/admin/permissions-BAI0qDr9.js +24 -0
  15. package/dist/admin/permissions-CYirM30e.mjs +25 -0
  16. package/dist/admin/src/components/EyeIcon.d.ts +8 -0
  17. package/dist/admin/src/components/PreviewFrame.d.ts +47 -0
  18. package/dist/admin/src/components/PreviewHeaderAction.d.ts +28 -0
  19. package/dist/admin/src/hooks/useEntry.d.ts +31 -0
  20. package/dist/admin/src/hooks/usePreviewSource.d.ts +31 -0
  21. package/dist/admin/src/pages/MicelioLayout.d.ts +2 -1
  22. package/dist/admin/src/pages/PreviewPage.d.ts +2 -0
  23. package/dist/admin/src/permissions.d.ts +5 -0
  24. package/dist/admin/src/translations/en.json.d.ts +37 -1
  25. package/dist/admin/src/utils/fetchError.d.ts +2 -0
  26. package/dist/admin/src/utils/previewAction.d.ts +22 -0
  27. package/dist/admin/src/utils/previewPane.d.ts +59 -0
  28. package/dist/server/index.js +796 -105
  29. package/dist/server/index.mjs +796 -105
  30. package/dist/server/src/constants/preview.d.ts +2 -0
  31. package/dist/server/src/constants/taxonomy.d.ts +13 -0
  32. package/dist/server/src/content-types/category/schema.json.d.ts +78 -0
  33. package/dist/server/src/content-types/index.d.ts +125 -2
  34. package/dist/server/src/content-types/preview-credential/schema.json.d.ts +7 -6
  35. package/dist/server/src/content-types/tag/schema.json.d.ts +46 -0
  36. package/dist/server/src/controllers/index.d.ts +6 -0
  37. package/dist/server/src/editorial/transitions.d.ts +5 -2
  38. package/dist/server/src/index.d.ts +140 -2
  39. package/dist/server/src/migrations/rename-legacy-uids.d.ts +26 -0
  40. package/dist/server/src/migrations/site-backfill.d.ts +13 -0
  41. package/dist/server/src/preview/credentials.d.ts +7 -5
  42. package/dist/server/src/preview/draft-query.d.ts +62 -0
  43. package/dist/server/src/preview/revocation.d.ts +9 -1
  44. package/dist/server/src/preview/shortcut.d.ts +24 -0
  45. package/dist/server/src/routes/index.d.ts +2 -0
  46. package/dist/server/src/routes/taxonomy.d.ts +12 -0
  47. package/dist/server/src/services/index.d.ts +6 -0
  48. package/dist/server/src/services/preview.d.ts +25 -6
  49. package/dist/server/src/site-scope/documents.d.ts +13 -3
  50. package/dist/server/src/types/hooks.d.ts +5 -4
  51. package/dist/server/src/types/preview.d.ts +35 -10
  52. package/package.json +4 -3
  53. package/dist/admin/MicelioLayout-3sKGPS_0.mjs +0 -443
  54. package/dist/admin/MicelioLayout-DXzRS4l3.js +0 -443
@@ -0,0 +1,1021 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { Typography, Link, Flex, Button, Box, Badge, useNotifyAT, Table, Thead, Tr, Th, Tbody, Td } from "@strapi/design-system";
3
+ import { useFetchClient, Page, Layouts, useAuth, SubNav } from "@strapi/strapi/admin";
4
+ import { useRef, useState, useEffect, forwardRef, useCallback, useImperativeHandle, useMemo, lazy, Suspense } from "react";
5
+ import { useIntl } from "react-intl";
6
+ import { useParams, useSearchParams, useNavigate, NavLink, useHref, Routes, Route, Navigate } from "react-router-dom";
7
+ import { S as STATUS_READ_PERMISSION, u as useMessages, P as PREVIEW_PERMISSION, h as hasMessage } from "./permissions-CYirM30e.mjs";
8
+ import { P as PLUGIN_ID, p as pageLoaderOf } from "./index-Dwr15ZsB.mjs";
9
+ import styled from "styled-components";
10
+ import { z } from "zod";
11
+ function useAdminPages() {
12
+ const client = useFetchClient();
13
+ const clientRef = useRef(client);
14
+ clientRef.current = client;
15
+ const [state, setState] = useState({ pages: [], hasFailed: false, isLoading: true });
16
+ useEffect(() => {
17
+ let isCurrent = true;
18
+ void (async () => {
19
+ try {
20
+ const response = await clientRef.current.get(
21
+ "/micelio/admin-pages"
22
+ );
23
+ if (isCurrent) setState({ pages: response.data.data, hasFailed: false, isLoading: false });
24
+ } catch {
25
+ if (isCurrent) setState({ pages: [], hasFailed: true, isLoading: false });
26
+ }
27
+ })();
28
+ return () => {
29
+ isCurrent = false;
30
+ };
31
+ }, []);
32
+ return state;
33
+ }
34
+ const NAV_GROUPS = ["overview", "content", "site", "about"];
35
+ const CORE_NAV_ITEMS = [
36
+ { id: "status", group: "overview", to: "", permission: STATUS_READ_PERMISSION },
37
+ { id: "about", group: "about", to: "about" }
38
+ ];
39
+ function visibleNavItems(items, permissions) {
40
+ return items.filter(
41
+ (item) => !item.permission || permissions.some(({ action }) => action === item.permission?.action)
42
+ );
43
+ }
44
+ function localised(text2, locale) {
45
+ const language = locale.split("-")[0] ?? "en";
46
+ const found = language === "es" ? text2.es : void 0;
47
+ return found && found.trim() !== "" ? found : text2.en;
48
+ }
49
+ function AboutPage() {
50
+ const { t } = useMessages();
51
+ return /* @__PURE__ */ jsxs(Page.Main, { children: [
52
+ /* @__PURE__ */ jsx(Page.Title, { children: t("about.title") }),
53
+ /* @__PURE__ */ jsx(Layouts.Header, { title: t("about.title"), subtitle: t("about.subtitle") }),
54
+ /* @__PURE__ */ jsx(Layouts.Content, { children: /* @__PURE__ */ jsx(Typography, { children: t("about.body") }) })
55
+ ] });
56
+ }
57
+ const PREVIEW_PROTOCOL_VERSION = 1;
58
+ const envelope = z.object({
59
+ v: z.literal(PREVIEW_PROTOCOL_VERSION),
60
+ type: z.string().min(1),
61
+ payload: z.unknown()
62
+ });
63
+ const readyMessage = envelope.extend({
64
+ type: z.literal("ready"),
65
+ payload: z.object({
66
+ /** The envelope versions the frame speaks. */
67
+ v: z.array(z.int().positive()).min(1),
68
+ documentId: z.string().min(1),
69
+ /** `null` for a draft that is not localised. */
70
+ locale: z.string().min(1).nullable(),
71
+ version: z.string().min(1)
72
+ })
73
+ }).meta({
74
+ id: "ready",
75
+ title: "ready",
76
+ description: "Frame → admin: the frame has loaded a draft."
77
+ });
78
+ const loadDraftMessage = envelope.extend({
79
+ type: z.literal("load-draft"),
80
+ payload: z.object({
81
+ documentId: z.string().min(1),
82
+ /** `null` for a draft that is not localised. */
83
+ locale: z.string().min(1).nullable()
84
+ })
85
+ }).meta({
86
+ id: "load-draft",
87
+ title: "load-draft",
88
+ description: "Admin → frame: reload the draft after a save."
89
+ });
90
+ const previewMessage = z.union([readyMessage, loadDraftMessage]);
91
+ function parsePreviewMessage(data) {
92
+ const result = previewMessage.safeParse(data);
93
+ return result.success ? result.data : void 0;
94
+ }
95
+ const DEVICES = [
96
+ { id: "phone", width: 320 },
97
+ { id: "tablet", width: 768 },
98
+ { id: "desktop", width: 1280 }
99
+ ];
100
+ const GRANT_REFRESH_MINUTES = 14;
101
+ const SAVE_POLL_MS = 4e3;
102
+ const SPOKEN_VERSIONS = [PREVIEW_PROTOCOL_VERSION];
103
+ function frameTarget(url, mode) {
104
+ let parsed;
105
+ try {
106
+ parsed = new URL(url);
107
+ } catch {
108
+ return void 0;
109
+ }
110
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return void 0;
111
+ if (mode === "static") parsed.searchParams.set("mode", "static");
112
+ return { src: parsed.toString(), origin: parsed.origin };
113
+ }
114
+ const NON_TEXT_INPUTS = [
115
+ "button",
116
+ "checkbox",
117
+ "radio",
118
+ "submit",
119
+ "reset",
120
+ "image",
121
+ "range",
122
+ "color"
123
+ ];
124
+ function isTypingTarget(target) {
125
+ const element = target;
126
+ if (!element || typeof element.tagName !== "string") return false;
127
+ const tag = element.tagName.toUpperCase();
128
+ if (tag === "INPUT")
129
+ return !NON_TEXT_INPUTS.includes(String(element.type ?? "text").toLowerCase());
130
+ return element.isContentEditable === true || tag === "TEXTAREA" || tag === "SELECT";
131
+ }
132
+ function previewShortcut(event) {
133
+ if (isTypingTarget(event.target)) return void 0;
134
+ if (event.ctrlKey === event.metaKey || !event.shiftKey || event.altKey) return void 0;
135
+ const device = DEVICES[["Digit1", "Digit2", "Digit3"].indexOf(event.code)];
136
+ if (device) return { action: "device", device: device.id };
137
+ if (event.code === "KeyS") return { action: "static" };
138
+ if (event.key === "ArrowLeft") return { action: "back" };
139
+ return void 0;
140
+ }
141
+ const FADE_OUT_MS = 400;
142
+ const NO_VERSIONS = [];
143
+ const Stage = styled.div`
144
+ position: relative;
145
+ box-sizing: border-box;
146
+ width: min(${({ $width }) => $width}px, 100%);
147
+ max-width: 100%;
148
+ height: 100%;
149
+ min-height: 32rem;
150
+ margin-inline: auto;
151
+ background: #ffffff;
152
+ box-shadow: 0 0 0 1px #dcdce4;
153
+ border-radius: 4px;
154
+ overflow: hidden;
155
+ transition: width var(--m-base) cubic-bezier(0.2, 0, 0, 1);
156
+ `;
157
+ const FrameLayer = styled.iframe`
158
+ position: absolute;
159
+ inset: 0;
160
+ width: 100%;
161
+ height: 100%;
162
+ border: 0;
163
+ background: #ffffff;
164
+ opacity: ${({ $phase }) => $phase === "shown" ? 1 : 0};
165
+ visibility: ${({ $phase }) => $phase === "loading" ? "hidden" : "visible"};
166
+ pointer-events: ${({ $phase }) => $phase === "shown" ? "auto" : "none"};
167
+ transition: opacity var(--m-slow) linear;
168
+ `;
169
+ const PreviewFrame = forwardRef(function PreviewFrame2(props, ref) {
170
+ const { src, origin, documentId, locale, title, width, onChange, onBlocked } = props;
171
+ const [layers, setLayers] = useState([]);
172
+ const [isUpdating, setIsUpdating] = useState(false);
173
+ const [protocol, setProtocol] = useState({ key: -1, protocol: "waiting", versions: NO_VERSIONS });
174
+ const frames = useRef(/* @__PURE__ */ new Map());
175
+ const protocols = useRef(/* @__PURE__ */ new Map());
176
+ const nextKey = useRef(0);
177
+ const shownKey = layers.find((layer) => layer.phase === "shown")?.key;
178
+ const timers = useRef([]);
179
+ const onChangeRef = useRef(onChange);
180
+ onChangeRef.current = onChange;
181
+ const onBlockedRef = useRef(onBlocked);
182
+ onBlockedRef.current = onBlocked;
183
+ useEffect(
184
+ () => () => {
185
+ for (const timer of timers.current) window.clearTimeout(timer);
186
+ },
187
+ []
188
+ );
189
+ useEffect(() => {
190
+ if (!src) {
191
+ setLayers([]);
192
+ return;
193
+ }
194
+ const key = nextKey.current++;
195
+ setLayers((current) => [
196
+ ...current,
197
+ { key, src, phase: current.length === 0 ? "shown" : "loading" }
198
+ ]);
199
+ }, [src]);
200
+ useEffect(() => {
201
+ if (!origin) return;
202
+ function onViolation(event) {
203
+ const isFrameDirective = ["frame-src", "child-src", "default-src"].includes(
204
+ event.effectiveDirective
205
+ );
206
+ if (isFrameDirective && event.blockedURI.startsWith(origin)) onBlockedRef.current();
207
+ }
208
+ document.addEventListener("securitypolicyviolation", onViolation);
209
+ return () => document.removeEventListener("securitypolicyviolation", onViolation);
210
+ }, [origin]);
211
+ useEffect(() => {
212
+ for (const key of protocols.current.keys()) {
213
+ if (!layers.some((layer) => layer.key === key)) protocols.current.delete(key);
214
+ }
215
+ }, [layers]);
216
+ const reveal = useCallback((key) => {
217
+ setLayers((current) => {
218
+ if (current.find((layer) => layer.key === key)?.phase !== "loading") return current;
219
+ return current.map(
220
+ (layer) => layer.key === key ? { ...layer, phase: "shown" } : layer.phase === "shown" ? { ...layer, phase: "leaving" } : layer
221
+ );
222
+ });
223
+ timers.current.push(
224
+ window.setTimeout(
225
+ () => setLayers((current) => current.filter((layer) => layer.phase !== "leaving")),
226
+ FADE_OUT_MS
227
+ )
228
+ );
229
+ }, []);
230
+ useEffect(() => {
231
+ if (!origin) return;
232
+ function onMessage(event) {
233
+ if (event.origin !== origin) return;
234
+ const key = [...frames.current].find(([, frame]) => frame.contentWindow === event.source)?.[0];
235
+ if (key === void 0) return;
236
+ const message = parsePreviewMessage(event.data);
237
+ if (message?.type !== "ready") return;
238
+ if (message.payload.documentId !== documentId || message.payload.locale !== locale) return;
239
+ const versions = message.payload.v;
240
+ const state = {
241
+ protocol: SPOKEN_VERSIONS.some((spoken) => versions.includes(spoken)) ? "ok" : "upgrade",
242
+ versions
243
+ };
244
+ protocols.current.set(key, state);
245
+ setProtocol({ key, ...state });
246
+ setIsUpdating(false);
247
+ }
248
+ window.addEventListener("message", onMessage);
249
+ return () => window.removeEventListener("message", onMessage);
250
+ }, [origin, documentId, locale]);
251
+ useImperativeHandle(
252
+ ref,
253
+ () => ({
254
+ loadDraft() {
255
+ const frame = shownKey === void 0 ? void 0 : frames.current.get(shownKey);
256
+ if (!origin || !frame?.contentWindow || protocols.current.get(shownKey ?? -1)?.protocol !== "ok") {
257
+ return false;
258
+ }
259
+ const message = {
260
+ v: PREVIEW_PROTOCOL_VERSION,
261
+ type: "load-draft",
262
+ payload: { documentId, locale }
263
+ };
264
+ frame.contentWindow.postMessage(message, origin);
265
+ setIsUpdating(true);
266
+ return true;
267
+ }
268
+ }),
269
+ [shownKey, origin, documentId, locale]
270
+ );
271
+ const inView = protocol.key === shownKey ? protocol : { protocol: "waiting", versions: NO_VERSIONS };
272
+ const isBusy = isUpdating || layers.some((layer) => layer.phase === "loading");
273
+ useEffect(() => {
274
+ onChangeRef.current({ protocol: inView.protocol, versions: inView.versions, isBusy });
275
+ }, [inView.protocol, inView.versions, isBusy]);
276
+ return /* @__PURE__ */ jsx(Stage, { $width: width, children: layers.map((layer) => /* @__PURE__ */ jsx(
277
+ FrameLayer,
278
+ {
279
+ $phase: layer.phase,
280
+ src: layer.src,
281
+ title,
282
+ sandbox: "allow-scripts allow-same-origin allow-forms",
283
+ referrerPolicy: "no-referrer",
284
+ ref: (frame) => {
285
+ if (frame) frames.current.set(layer.key, frame);
286
+ else frames.current.delete(layer.key);
287
+ },
288
+ onLoad: () => {
289
+ reveal(layer.key);
290
+ if (layer.phase === "shown") setIsUpdating(false);
291
+ }
292
+ },
293
+ layer.key
294
+ )) });
295
+ });
296
+ function statusOf(error) {
297
+ if (typeof error !== "object" || error === null) return void 0;
298
+ const { status, response } = error;
299
+ if (typeof status === "number") return status;
300
+ return typeof response?.status === "number" ? response.status : void 0;
301
+ }
302
+ function text(value) {
303
+ return typeof value === "string" && value !== "" ? value : void 0;
304
+ }
305
+ function toEntry(answer, documentId) {
306
+ const locale = text(answer.data.locale) ?? null;
307
+ const others = (answer.meta?.availableLocales ?? []).flatMap(
308
+ ({ locale: other }) => text(other) ?? []
309
+ );
310
+ return {
311
+ title: text(answer.data.title) ?? text(answer.data.name) ?? text(answer.data.slug) ?? documentId,
312
+ locale,
313
+ locales: locale === null ? [] : [.../* @__PURE__ */ new Set([locale, ...others])],
314
+ updatedAt: text(answer.data.updatedAt) ?? ""
315
+ };
316
+ }
317
+ function useEntry(uid, documentId, locale) {
318
+ const client = useFetchClient();
319
+ const clientRef = useRef(client);
320
+ clientRef.current = client;
321
+ const [resource, setResource] = useState({ kind: "loading" });
322
+ const scope = `${uid}/${documentId}/${locale ?? ""}`;
323
+ const [saves, setSaves] = useState({ scope, count: 0 });
324
+ useEffect(() => {
325
+ let isCurrent = true;
326
+ let isReading = false;
327
+ let lastSeen;
328
+ async function read(isFirst) {
329
+ if (isReading) return;
330
+ isReading = true;
331
+ try {
332
+ const response = await clientRef.current.get(
333
+ `/content-manager/collection-types/${uid}/${documentId}`,
334
+ { params: locale ? { locale } : {} }
335
+ );
336
+ if (!isCurrent) return;
337
+ const entry = toEntry(response.data, documentId);
338
+ if (lastSeen !== void 0 && entry.updatedAt !== lastSeen) {
339
+ setSaves((current) => ({ scope, count: current.scope === scope ? current.count + 1 : 1 }));
340
+ }
341
+ lastSeen = entry.updatedAt;
342
+ setResource({ kind: "ready", entry });
343
+ } catch (error) {
344
+ if (isCurrent && isFirst) setResource({ kind: "error", status: statusOf(error) });
345
+ } finally {
346
+ isReading = false;
347
+ }
348
+ }
349
+ setResource({ kind: "loading" });
350
+ setSaves({ scope, count: 0 });
351
+ void read(true);
352
+ const timer = window.setInterval(() => {
353
+ if (document.visibilityState === "visible") void read(false);
354
+ }, SAVE_POLL_MS);
355
+ const onVisible = () => {
356
+ if (document.visibilityState === "visible") void read(false);
357
+ };
358
+ document.addEventListener("visibilitychange", onVisible);
359
+ return () => {
360
+ isCurrent = false;
361
+ window.clearInterval(timer);
362
+ document.removeEventListener("visibilitychange", onVisible);
363
+ };
364
+ }, [uid, documentId, locale]);
365
+ return { resource, savedCount: saves.scope === scope ? saves.count : 0 };
366
+ }
367
+ const REASONS = {
368
+ 400: "unsupported",
369
+ 403: "forbidden",
370
+ 404: "missing",
371
+ 409: "unavailable",
372
+ 429: "busy"
373
+ };
374
+ function usePreviewSource(entry, mode) {
375
+ const client = useFetchClient();
376
+ const clientRef = useRef(client);
377
+ clientRef.current = client;
378
+ const [source, setSource] = useState({ kind: "loading" });
379
+ const [count, setCount] = useState(0);
380
+ const uid = entry?.uid;
381
+ const documentId = entry?.documentId;
382
+ const locale = entry?.locale;
383
+ useEffect(() => {
384
+ if (!uid || !documentId) return;
385
+ let isCurrent = true;
386
+ let timer;
387
+ void (async () => {
388
+ try {
389
+ const response = await clientRef.current.post(
390
+ "/micelio/preview/token",
391
+ { uid, documentId, locale }
392
+ );
393
+ const target = frameTarget(response.data.data.url, mode);
394
+ if (!isCurrent) return;
395
+ if (!target) {
396
+ setSource({ kind: "error", reason: "unavailable" });
397
+ return;
398
+ }
399
+ setSource({ kind: "ready", ...target });
400
+ timer = window.setTimeout(
401
+ () => setCount((value) => value + 1),
402
+ GRANT_REFRESH_MINUTES * 6e4
403
+ );
404
+ } catch (error) {
405
+ const status = statusOf(error);
406
+ if (isCurrent) setSource({ kind: "error", reason: status && REASONS[status] || "failed" });
407
+ }
408
+ })();
409
+ return () => {
410
+ isCurrent = false;
411
+ window.clearTimeout(timer);
412
+ };
413
+ }, [uid, documentId, locale, mode, count]);
414
+ const reload = useCallback(() => setCount((value) => value + 1), []);
415
+ return { source, reload };
416
+ }
417
+ const Root = styled.div`
418
+ --m-fast: 120ms;
419
+ --m-base: 200ms;
420
+ --m-slow: 320ms;
421
+ @media (prefers-reduced-motion: reduce) {
422
+ --m-fast: 1ms;
423
+ --m-base: 1ms;
424
+ --m-slow: 1ms;
425
+ }
426
+ `;
427
+ const Columns = styled.div`
428
+ display: grid;
429
+ grid-template-columns: minmax(16rem, 1fr) minmax(0, 2fr);
430
+ gap: 1.5rem;
431
+ align-items: start;
432
+ @media (max-width: 62rem) {
433
+ grid-template-columns: minmax(0, 1fr);
434
+ }
435
+ `;
436
+ const Select = styled.select`
437
+ min-height: 2rem;
438
+ padding: 0 0.5rem;
439
+ border: 1px solid #8e8ea9;
440
+ border-radius: 4px;
441
+ background: #ffffff;
442
+ color: #32324d;
443
+ font: inherit;
444
+ `;
445
+ const StageBox = styled.div`
446
+ height: calc(100vh - 20rem);
447
+ min-height: 32rem;
448
+ `;
449
+ const SHORTCUTS = {
450
+ phone: "Control+Shift+1 Meta+Shift+1",
451
+ tablet: "Control+Shift+2 Meta+Shift+2",
452
+ desktop: "Control+Shift+3 Meta+Shift+3",
453
+ static: "Control+Shift+S Meta+Shift+S"
454
+ };
455
+ function PreviewPage() {
456
+ return /* @__PURE__ */ jsx(Page.Protect, { permissions: [PREVIEW_PERMISSION], children: /* @__PURE__ */ jsx(PreviewPane, {}) });
457
+ }
458
+ function PreviewPane() {
459
+ const { t, date } = useMessages();
460
+ const { uid = "", documentId = "" } = useParams();
461
+ const [searchParams] = useSearchParams();
462
+ const navigate = useNavigate();
463
+ const permissions = useAuth("PreviewPage", (state) => state.permissions);
464
+ const [locale, setLocale] = useState(searchParams.get("locale"));
465
+ const [device, setDevice] = useState("desktop");
466
+ const [mode, setMode] = useState("live");
467
+ const [frameState, setFrameState] = useState({
468
+ protocol: "waiting",
469
+ versions: [],
470
+ isBusy: false
471
+ });
472
+ const [isBlocked, setIsBlocked] = useState(false);
473
+ const frame = useRef(null);
474
+ const { resource, savedCount } = useEntry(uid, documentId, locale);
475
+ const entry = resource.kind === "ready" ? resource.entry : void 0;
476
+ const { source, reload } = usePreviewSource(
477
+ entry ? { uid, documentId, locale: entry.locale } : void 0,
478
+ mode
479
+ );
480
+ const canEdit = permissions.some(
481
+ ({ action, subject }) => action === "plugin::content-manager.explorer.update" && subject === uid
482
+ );
483
+ const entryPath = `/content-manager/collection-types/${uid}/${documentId}${entry?.locale ? `?plugins[i18n][locale]=${entry.locale}` : ""}`;
484
+ const handled = useRef({ locale, count: 0 });
485
+ useEffect(() => {
486
+ if (handled.current.locale !== locale) handled.current = { locale, count: 0 };
487
+ if (savedCount === 0 || savedCount === handled.current.count) return;
488
+ handled.current.count = savedCount;
489
+ if (!frame.current?.loadDraft()) reload();
490
+ }, [savedCount, locale, reload]);
491
+ const origin = source.kind === "ready" ? source.origin : void 0;
492
+ useEffect(() => setIsBlocked(false), [origin]);
493
+ useEffect(() => {
494
+ function onKeyDown(event) {
495
+ const shortcut = previewShortcut(event);
496
+ if (!shortcut) return;
497
+ event.preventDefault();
498
+ if (shortcut.action === "device") setDevice(shortcut.device);
499
+ else if (shortcut.action === "static")
500
+ setMode((current) => current === "live" ? "static" : "live");
501
+ else navigate(entryPath);
502
+ }
503
+ window.addEventListener("keydown", onKeyDown);
504
+ return () => window.removeEventListener("keydown", onKeyDown);
505
+ }, [navigate, entryPath]);
506
+ if (resource.kind === "error") {
507
+ return /* @__PURE__ */ jsxs(Page.Main, { children: [
508
+ /* @__PURE__ */ jsx(Page.Title, { children: t("preview.title") }),
509
+ /* @__PURE__ */ jsx(Layouts.Header, { title: t("preview.title") }),
510
+ /* @__PURE__ */ jsx(Layouts.Content, { children: /* @__PURE__ */ jsx(Typography, { role: "alert", children: t(resource.status === 404 ? "preview.error.missing" : "preview.error.entry") }) })
511
+ ] });
512
+ }
513
+ const title = entry?.title ?? t("preview.title");
514
+ const width = DEVICES.find((item) => item.id === device)?.width ?? 1280;
515
+ const reason = source.kind === "error" ? source.reason : void 0;
516
+ const isUpgradeNeeded = frameState.protocol === "upgrade";
517
+ return /* @__PURE__ */ jsx(Root, { children: /* @__PURE__ */ jsxs(Page.Main, { children: [
518
+ /* @__PURE__ */ jsx(Page.Title, { children: t("preview.page", { title }) }),
519
+ /* @__PURE__ */ jsx(
520
+ Layouts.Header,
521
+ {
522
+ title,
523
+ subtitle: t("preview.subtitle"),
524
+ navigationAction: /* @__PURE__ */ jsx(
525
+ Link,
526
+ {
527
+ tag: NavLink,
528
+ to: entryPath,
529
+ "aria-keyshortcuts": "Control+Shift+ArrowLeft Meta+Shift+ArrowLeft",
530
+ children: t("preview.back")
531
+ }
532
+ )
533
+ }
534
+ ),
535
+ /* @__PURE__ */ jsx(Layouts.Content, { children: /* @__PURE__ */ jsxs(Columns, { children: [
536
+ /* @__PURE__ */ jsx(EntryColumn, { entry, canEdit, entryPath, date }),
537
+ /* @__PURE__ */ jsxs("section", { "aria-label": t("preview.pane"), children: [
538
+ /* @__PURE__ */ jsxs(Flex, { gap: 4, wrap: "wrap", alignItems: "center", marginBottom: 3, children: [
539
+ /* @__PURE__ */ jsx(Flex, { gap: 2, role: "group", "aria-label": t("preview.device.group"), children: DEVICES.map((item) => /* @__PURE__ */ jsx(
540
+ Button,
541
+ {
542
+ type: "button",
543
+ size: "S",
544
+ variant: device === item.id ? "default" : "tertiary",
545
+ "aria-pressed": device === item.id,
546
+ "aria-keyshortcuts": SHORTCUTS[item.id],
547
+ onClick: () => setDevice(item.id),
548
+ children: t(`preview.device.${item.id}`, { width: item.width })
549
+ },
550
+ item.id
551
+ )) }),
552
+ /* @__PURE__ */ jsxs(Flex, { tag: "label", gap: 2, alignItems: "center", children: [
553
+ /* @__PURE__ */ jsx(
554
+ "input",
555
+ {
556
+ type: "checkbox",
557
+ checked: mode === "static",
558
+ "aria-keyshortcuts": SHORTCUTS.static,
559
+ onChange: (event) => setMode(event.target.checked ? "static" : "live")
560
+ }
561
+ ),
562
+ /* @__PURE__ */ jsx(Typography, { children: t("preview.static") })
563
+ ] }),
564
+ entry && entry.locales.length > 1 ? /* @__PURE__ */ jsxs(Flex, { tag: "label", gap: 2, alignItems: "center", children: [
565
+ /* @__PURE__ */ jsx(Typography, { children: t("preview.locale") }),
566
+ /* @__PURE__ */ jsx(
567
+ Select,
568
+ {
569
+ value: entry.locale ?? "",
570
+ onChange: (event) => setLocale(event.target.value),
571
+ children: entry.locales.map((code) => /* @__PURE__ */ jsx("option", { value: code, children: code }, code))
572
+ }
573
+ )
574
+ ] }) : null,
575
+ /* @__PURE__ */ jsx(Button, { type: "button", size: "S", variant: "tertiary", onClick: reload, children: t("preview.reload") }),
576
+ /* @__PURE__ */ jsx(Typography, { role: "status", textColor: "neutral700", children: frameState.isBusy ? t("preview.updating") : "" })
577
+ ] }),
578
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral700", tag: "p", children: t("preview.shortcuts") }),
579
+ /* @__PURE__ */ jsxs(Box, { marginTop: 3, marginBottom: 3, children: [
580
+ reason ? /* @__PURE__ */ jsx(Notice, { role: "alert", text: t(`preview.error.${reason}`) }) : null,
581
+ isBlocked ? /* @__PURE__ */ jsx(
582
+ Notice,
583
+ {
584
+ role: "alert",
585
+ title: t("preview.blocked.title"),
586
+ text: t("preview.blocked.body", { origin: origin ?? "" })
587
+ }
588
+ ) : null,
589
+ isUpgradeNeeded ? /* @__PURE__ */ jsx(
590
+ Notice,
591
+ {
592
+ role: "status",
593
+ title: t("preview.upgrade.title"),
594
+ text: t("preview.upgrade.body", { versions: frameState.versions.join(", ") })
595
+ }
596
+ ) : null
597
+ ] }),
598
+ /* @__PURE__ */ jsx(StageBox, { children: isBlocked || source.kind === "error" ? null : /* @__PURE__ */ jsx(
599
+ PreviewFrame,
600
+ {
601
+ ref: frame,
602
+ src: source.kind === "ready" ? source.src : void 0,
603
+ origin,
604
+ documentId,
605
+ locale: entry?.locale ?? null,
606
+ title: t("preview.frame.title", { title }),
607
+ width,
608
+ onChange: setFrameState,
609
+ onBlocked: () => setIsBlocked(true)
610
+ }
611
+ ) })
612
+ ] })
613
+ ] }) })
614
+ ] }) });
615
+ }
616
+ function EntryColumn(props) {
617
+ const { t } = useMessages();
618
+ const { locale } = useIntl();
619
+ const { entry, canEdit, entryPath, date } = props;
620
+ const href = useHref(entryPath);
621
+ return /* @__PURE__ */ jsx("section", { "aria-label": t("preview.entry.title"), children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 3, children: [
622
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", children: t("preview.entry.title") }),
623
+ canEdit ? null : /* @__PURE__ */ jsx(Badge, { backgroundColor: "neutral150", textColor: "neutral700", children: t("preview.entry.readOnly") }),
624
+ /* @__PURE__ */ jsx(Typography, { children: t(canEdit ? "preview.entry.body" : "preview.entry.bodyReadOnly") }),
625
+ entry?.locale ? /* @__PURE__ */ jsx(Typography, { textColor: "neutral700", children: t("preview.entry.locale", { locale: entry.locale }) }) : null,
626
+ entry?.updatedAt ? /* @__PURE__ */ jsx(Typography, { textColor: "neutral700", lang: locale, children: t("preview.entry.saved", { date: date(entry.updatedAt) }) }) : null,
627
+ /* @__PURE__ */ jsx(Link, { href, isExternal: true, children: t(canEdit ? "preview.entry.open" : "preview.entry.view") })
628
+ ] }) });
629
+ }
630
+ function Notice(props) {
631
+ return /* @__PURE__ */ jsxs(
632
+ Box,
633
+ {
634
+ role: props.role,
635
+ padding: 3,
636
+ marginBottom: 2,
637
+ background: "warning100",
638
+ borderColor: "warning200",
639
+ hasRadius: true,
640
+ children: [
641
+ props.title ? /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", tag: "p", children: props.title }) : null,
642
+ /* @__PURE__ */ jsx(Typography, { tag: "p", children: props.text })
643
+ ]
644
+ }
645
+ );
646
+ }
647
+ function useHostCheckRun(initial) {
648
+ const client = useFetchClient();
649
+ const clientRef = useRef(client);
650
+ clientRef.current = client;
651
+ const [report, setReport] = useState(initial);
652
+ const [isRunning, setIsRunning] = useState(false);
653
+ const [hasFailed, setHasFailed] = useState(false);
654
+ const run = useCallback(async () => {
655
+ setIsRunning(true);
656
+ setHasFailed(false);
657
+ try {
658
+ const response = await clientRef.current.post(
659
+ "/micelio/status/host-check"
660
+ );
661
+ setReport(response.data.data);
662
+ return response.data.data;
663
+ } catch {
664
+ setHasFailed(true);
665
+ return null;
666
+ } finally {
667
+ setIsRunning(false);
668
+ }
669
+ }, []);
670
+ return { report, isRunning, hasFailed, run };
671
+ }
672
+ function useStatus() {
673
+ const client = useFetchClient();
674
+ const clientRef = useRef(client);
675
+ clientRef.current = client;
676
+ const [resource, setResource] = useState({ kind: "loading" });
677
+ const [count, setCount] = useState(0);
678
+ useEffect(() => {
679
+ let isCurrent = true;
680
+ void (async () => {
681
+ try {
682
+ const response = await clientRef.current.get("/micelio/status");
683
+ if (isCurrent) setResource({ kind: "ready", data: response.data.data });
684
+ } catch {
685
+ if (isCurrent) setResource({ kind: "error" });
686
+ }
687
+ })();
688
+ return () => {
689
+ isCurrent = false;
690
+ };
691
+ }, [count]);
692
+ const reload = useCallback(() => {
693
+ setResource({ kind: "loading" });
694
+ setCount((value) => value + 1);
695
+ }, []);
696
+ const refresh = useCallback(async () => {
697
+ try {
698
+ const response = await clientRef.current.get("/micelio/status");
699
+ setResource({ kind: "ready", data: response.data.data });
700
+ } catch {
701
+ }
702
+ }, []);
703
+ return { resource, reload, refresh };
704
+ }
705
+ function checkText(result, part, isKnown) {
706
+ const fallback = part === "title" ? result.title : result.fix;
707
+ if (fallback === void 0) return null;
708
+ const messageId = `status.hostCheck.check.${result.id}.${part}`;
709
+ return isKnown(messageId) ? { messageId } : { text: fallback };
710
+ }
711
+ function diagnosticsText(report) {
712
+ const lines = [`Micelio ${report.plugin.version} (core release line ${report.plugin.coreLine})`];
713
+ if (report.hostCheck) {
714
+ const failed = report.hostCheck.results.filter(({ status }) => status === "fail").length;
715
+ lines.push(`Host check: ${report.hostCheck.ok ? "ok" : "fails"} (${failed} failed)`);
716
+ } else {
717
+ lines.push("Host check: not run");
718
+ }
719
+ for (const [id, result] of Object.entries(report.plugins)) lines.push(pluginLine(id, result));
720
+ return lines.join("\n");
721
+ }
722
+ function pluginLine(id, result) {
723
+ if (result.ranAt === null) return `${id}: checks not run`;
724
+ const count = (status) => result.checks.filter((check) => check.status === status).length;
725
+ return `${id}: ${count("ok")} ok, ${count("warn")} warn, ${count("fail")} fail`;
726
+ }
727
+ const TONES = {
728
+ success: { backgroundColor: "success100", textColor: "success700" },
729
+ danger: { backgroundColor: "danger100", textColor: "danger700" },
730
+ // warning700 on warning100 is 4.0:1, below the 4.5:1 of WCAG AA.
731
+ warning: { backgroundColor: "warning100", textColor: "neutral800" },
732
+ neutral: { backgroundColor: "neutral150", textColor: "neutral700" }
733
+ };
734
+ const CHECK_TONES = {
735
+ pass: "success",
736
+ fail: "danger",
737
+ skip: "neutral"
738
+ };
739
+ function StatusPage() {
740
+ const { t } = useMessages();
741
+ return /* @__PURE__ */ jsx(Page.Protect, { permissions: [STATUS_READ_PERMISSION], children: /* @__PURE__ */ jsxs(Page.Main, { children: [
742
+ /* @__PURE__ */ jsx(Page.Title, { children: t("status.title") }),
743
+ /* @__PURE__ */ jsx(Layouts.Header, { title: t("status.title"), subtitle: t("status.subtitle") }),
744
+ /* @__PURE__ */ jsx(Layouts.Content, { children: /* @__PURE__ */ jsx(StatusContent, {}) })
745
+ ] }) });
746
+ }
747
+ function StatusContent() {
748
+ const { t } = useMessages();
749
+ const { resource, reload, refresh } = useStatus();
750
+ if (resource.kind === "loading") return /* @__PURE__ */ jsx(Page.Loading, { children: t("status.loading") });
751
+ if (resource.kind === "error") {
752
+ return /* @__PURE__ */ jsx(Section, { title: t("status.error.title"), children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 2, children: [
753
+ /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: t("status.error") }),
754
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "S", onClick: reload, children: t("status.retry") })
755
+ ] }) });
756
+ }
757
+ const report = resource.data;
758
+ return /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 6, children: [
759
+ /* @__PURE__ */ jsx(PluginSection, { report }),
760
+ /* @__PURE__ */ jsx(HostCheckSection, { initial: report.hostCheck, onRan: refresh }),
761
+ /* @__PURE__ */ jsx(PluginChecksSections, { report }),
762
+ /* @__PURE__ */ jsx(FrontendsSection, { frontends: report.frontends }),
763
+ /* @__PURE__ */ jsx(SitesSection, { sites: report.sites }),
764
+ /* @__PURE__ */ jsx(DiagnosticsButton, { report })
765
+ ] });
766
+ }
767
+ function Section({ title, children }) {
768
+ return /* @__PURE__ */ jsx(Box, { tag: "section", background: "neutral0", hasRadius: true, shadow: "filterShadow", padding: 6, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
769
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", children: title }),
770
+ children
771
+ ] }) });
772
+ }
773
+ function StatusBadge({ tone, children }) {
774
+ return /* @__PURE__ */ jsx(Badge, { ...TONES[tone], children });
775
+ }
776
+ function Fact({ label, value }) {
777
+ return /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
778
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", tag: "dt", children: label }),
779
+ /* @__PURE__ */ jsx(Typography, { tag: "dd", children: value })
780
+ ] });
781
+ }
782
+ function PluginSection({ report }) {
783
+ const { t } = useMessages();
784
+ return /* @__PURE__ */ jsx(Section, { title: t("status.plugin.title"), children: /* @__PURE__ */ jsxs(Flex, { tag: "dl", gap: 8, wrap: "wrap", alignItems: "flex-start", children: [
785
+ /* @__PURE__ */ jsx(Fact, { label: t("status.plugin.version"), value: report.plugin.version }),
786
+ /* @__PURE__ */ jsx(Fact, { label: t("status.plugin.coreLine"), value: report.plugin.coreLine }),
787
+ /* @__PURE__ */ jsx(Fact, { label: t("status.blocks.count"), value: report.blocks.count }),
788
+ /* @__PURE__ */ jsx(Fact, { label: t("status.sites.count"), value: report.sites.length })
789
+ ] }) });
790
+ }
791
+ function HostCheckSection({
792
+ initial,
793
+ onRan
794
+ }) {
795
+ const { t, date } = useMessages();
796
+ const { notifyStatus, notifyAlert } = useNotifyAT();
797
+ const { report, isRunning, hasFailed, run } = useHostCheckRun(initial);
798
+ const say = (text2) => "messageId" in text2 ? t(text2.messageId) : text2.text;
799
+ const verdict = (current) => `${t(current.ok ? "status.hostCheck.ok" : "status.hostCheck.failed")} ${t(
800
+ "status.hostCheck.checkedAt",
801
+ { date: date(current.checkedAt), environment: current.environment }
802
+ )}`;
803
+ const runAgain = async () => {
804
+ const answer = await run();
805
+ if (answer) await onRan();
806
+ if (answer) notifyStatus(verdict(answer));
807
+ else notifyAlert(t("status.hostCheck.runFailed"));
808
+ };
809
+ const header = /* @__PURE__ */ jsxs(Flex, { gap: 4, wrap: "wrap", justifyContent: "space-between", alignItems: "center", children: [
810
+ /* @__PURE__ */ jsx(Typography, { textColor: report ? void 0 : "neutral600", children: report ? verdict(report) : t("status.hostCheck.notRun") }),
811
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "S", loading: isRunning, onClick: runAgain, children: t(isRunning ? "status.hostCheck.running" : "status.hostCheck.runAgain") })
812
+ ] });
813
+ const failure = hasFailed ? /* @__PURE__ */ jsx(Typography, { textColor: "danger700", children: t("status.hostCheck.runFailed") }) : null;
814
+ if (!report) {
815
+ return /* @__PURE__ */ jsxs(Section, { title: t("status.hostCheck.title"), children: [
816
+ header,
817
+ failure
818
+ ] });
819
+ }
820
+ return /* @__PURE__ */ jsxs(Section, { title: t("status.hostCheck.title"), children: [
821
+ header,
822
+ failure,
823
+ /* @__PURE__ */ jsxs(Table, { colCount: 3, rowCount: report.results.length + 1, children: [
824
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
825
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("status.hostCheck.column.check") }) }),
826
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("status.hostCheck.column.status") }) }),
827
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("status.hostCheck.column.found") }) })
828
+ ] }) }),
829
+ /* @__PURE__ */ jsx(Tbody, { children: report.results.map((result) => {
830
+ const title = checkText(result, "title", hasMessage);
831
+ const fix = result.status === "fail" ? checkText(result, "fix", hasMessage) : null;
832
+ return /* @__PURE__ */ jsxs(Tr, { children: [
833
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
834
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", children: title ? say(title) : result.id }),
835
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t(
836
+ result.required ? "status.hostCheck.required" : "status.hostCheck.optional"
837
+ ) })
838
+ ] }) }),
839
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(StatusBadge, { tone: CHECK_TONES[result.status], children: t(`status.hostCheck.status.${result.status}`) }) }),
840
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
841
+ /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", children: result.detail }),
842
+ fix ? /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral800", children: [
843
+ /* @__PURE__ */ jsxs("strong", { children: [
844
+ t("status.hostCheck.fix"),
845
+ ":"
846
+ ] }),
847
+ " ",
848
+ say(fix)
849
+ ] }) : null
850
+ ] }) })
851
+ ] }, result.id);
852
+ }) })
853
+ ] }),
854
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t("status.hostCheck.rerun") })
855
+ ] });
856
+ }
857
+ function FrontendsSection({ frontends }) {
858
+ const { t } = useMessages();
859
+ if (frontends.length === 0) {
860
+ return /* @__PURE__ */ jsx(Section, { title: t("status.frontends.title"), children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: t("status.frontends.none") }) });
861
+ }
862
+ return /* @__PURE__ */ jsx(Section, { title: t("status.frontends.title"), children: /* @__PURE__ */ jsxs(Table, { colCount: 4, rowCount: frontends.length + 1, children: [
863
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
864
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("status.frontends.column.origin") }) }),
865
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("status.frontends.column.sites") }) }),
866
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("status.frontends.column.health") }) }),
867
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("status.frontends.column.version") }) })
868
+ ] }) }),
869
+ /* @__PURE__ */ jsx(Tbody, { children: frontends.map((frontend) => /* @__PURE__ */ jsxs(Tr, { children: [
870
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { children: frontend.origin }) }),
871
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { children: frontend.sites.join(", ") }) }),
872
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
873
+ /* @__PURE__ */ jsx(StatusBadge, { tone: frontend.health === "reachable" ? "success" : "neutral", children: t(`status.frontends.health.${frontend.health}`) }),
874
+ frontend.health === "unavailable" ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t("status.frontends.unavailable") }) : null
875
+ ] }) }),
876
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { children: frontend.version ?? t("status.frontends.noVersion") }) })
877
+ ] }, frontend.origin)) })
878
+ ] }) });
879
+ }
880
+ function SitesSection({ sites }) {
881
+ const { t } = useMessages();
882
+ return /* @__PURE__ */ jsx(Section, { title: t("status.sites.title"), children: /* @__PURE__ */ jsxs(Table, { colCount: 4, rowCount: sites.length + 1, children: [
883
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
884
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("status.sites.column.name") }) }),
885
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("status.sites.column.slug") }) }),
886
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("status.sites.column.hosts") }) }),
887
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("status.sites.column.url") }) })
888
+ ] }) }),
889
+ /* @__PURE__ */ jsx(Tbody, { children: sites.map((site) => /* @__PURE__ */ jsxs(Tr, { children: [
890
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
891
+ /* @__PURE__ */ jsx(Typography, { children: site.name }),
892
+ site.isDefault ? /* @__PURE__ */ jsx(Badge, { children: t("status.sites.default") }) : null
893
+ ] }) }),
894
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { children: site.slug }) }),
895
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { children: site.hosts.length > 0 ? site.hosts.join(", ") : t("status.sites.noHosts") }) }),
896
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { children: site.origin ?? t("status.sites.noUrl") }) })
897
+ ] }, site.documentId)) })
898
+ ] }) });
899
+ }
900
+ const PLUGIN_TONES = {
901
+ ok: "success",
902
+ warn: "warning",
903
+ fail: "danger"
904
+ };
905
+ function PluginChecksSections({ report }) {
906
+ return /* @__PURE__ */ jsx(Fragment, { children: Object.entries(report.plugins).map(([id, result]) => /* @__PURE__ */ jsx(PluginChecksCard, { result, sites: report.sites }, id)) });
907
+ }
908
+ function PluginChecksCard({ result, sites }) {
909
+ const { t, date } = useMessages();
910
+ const { locale } = useIntl();
911
+ const slugOf = (documentId) => sites.find((site) => site.documentId === documentId)?.slug ?? documentId;
912
+ return /* @__PURE__ */ jsxs(Section, { title: localised(result.label, locale), children: [
913
+ /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: result.ranAt === null ? t("status.plugins.notRun") : t("status.plugins.ranAt", { date: date(result.ranAt) }) }),
914
+ result.ranAt !== null && result.checks.length === 0 ? /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: t("status.plugins.empty") }) : null,
915
+ result.checks.length > 0 ? /* @__PURE__ */ jsxs(Table, { colCount: 4, rowCount: result.checks.length + 1, children: [
916
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
917
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("status.plugins.column.site") }) }),
918
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("status.plugins.column.check") }) }),
919
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("status.plugins.column.status") }) }),
920
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("status.plugins.column.found") }) })
921
+ ] }) }),
922
+ /* @__PURE__ */ jsx(Tbody, { children: result.checks.map((check) => /* @__PURE__ */ jsxs(Tr, { children: [
923
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { children: slugOf(check.site) }) }),
924
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", children: localised(check.title, locale) }) }),
925
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(StatusBadge, { tone: PLUGIN_TONES[check.status], children: t(`status.plugins.status.${check.status}`) }) }),
926
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
927
+ check.detail ? /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", children: localised(check.detail, locale) }) : null,
928
+ check.fix ? /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral800", children: [
929
+ /* @__PURE__ */ jsxs("strong", { children: [
930
+ t("status.plugins.fix"),
931
+ ":"
932
+ ] }),
933
+ " ",
934
+ localised(check.fix, locale)
935
+ ] }) : null
936
+ ] }) })
937
+ ] }, `${check.site}:${check.id}`)) })
938
+ ] }) : null
939
+ ] });
940
+ }
941
+ function DiagnosticsButton({ report }) {
942
+ const { t } = useMessages();
943
+ const { notifyStatus, notifyAlert } = useNotifyAT();
944
+ const copy = async () => {
945
+ try {
946
+ await navigator.clipboard.writeText(diagnosticsText(report));
947
+ notifyStatus(t("status.diagnostics.copied"));
948
+ } catch {
949
+ notifyAlert(t("status.diagnostics.failed"));
950
+ }
951
+ };
952
+ return /* @__PURE__ */ jsx(Flex, { children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "S", onClick: copy, children: t("status.diagnostics.copy") }) });
953
+ }
954
+ function MicelioLayout() {
955
+ const { t } = useMessages();
956
+ const { locale } = useIntl();
957
+ const { pages, hasFailed, isLoading } = useAdminPages();
958
+ const isLoadingPermissions = useAuth("MicelioLayout", (state) => state.isLoading);
959
+ const permissions = useAuth("MicelioLayout", (state) => state.permissions);
960
+ const canReadStatus = permissions.some(({ action }) => action === STATUS_READ_PERMISSION.action);
961
+ const coreItems = visibleNavItems(CORE_NAV_ITEMS, permissions);
962
+ const linksOf = (group) => [
963
+ ...coreItems.filter((item) => item.group === group).map((item) => /* @__PURE__ */ jsx(
964
+ SubNav.Link,
965
+ {
966
+ to: item.to ? `/plugins/${PLUGIN_ID}/${item.to}` : `/plugins/${PLUGIN_ID}`,
967
+ end: !item.to,
968
+ label: t(`nav.${item.id}`)
969
+ },
970
+ item.id
971
+ )),
972
+ ...pages.filter((page) => page.group === group).map((page) => /* @__PURE__ */ jsx(
973
+ SubNav.Link,
974
+ {
975
+ to: `/plugins/${PLUGIN_ID}/${page.to}`,
976
+ label: localised(page.label, locale)
977
+ },
978
+ page.id
979
+ ))
980
+ ];
981
+ if (isLoadingPermissions) return /* @__PURE__ */ jsx(Page.Loading, {});
982
+ return /* @__PURE__ */ jsx(
983
+ Layouts.Root,
984
+ {
985
+ sideNav: /* @__PURE__ */ jsxs(SubNav.Main, { "aria-label": t("nav.label"), children: [
986
+ /* @__PURE__ */ jsx(SubNav.Header, { label: t("nav.title") }),
987
+ /* @__PURE__ */ jsxs(SubNav.Sections, { children: [
988
+ NAV_GROUPS.map((group) => ({ group, links: linksOf(group) })).filter(({ links }) => links.length > 0).map(({ group, links }) => /* @__PURE__ */ jsx(SubNav.Section, { label: t(`nav.group.${group}`), children: links }, group)),
989
+ hasFailed ? /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: t("nav.error") }) : null
990
+ ] })
991
+ ] }),
992
+ children: /* @__PURE__ */ jsxs(Routes, { children: [
993
+ /* @__PURE__ */ jsx(Route, { index: true, element: canReadStatus ? /* @__PURE__ */ jsx(StatusPage, {}) : /* @__PURE__ */ jsx(Navigate, { to: "about", replace: true }) }),
994
+ /* @__PURE__ */ jsx(Route, { path: "about", element: /* @__PURE__ */ jsx(AboutPage, {}) }),
995
+ /* @__PURE__ */ jsx(Route, { path: "preview/:uid/:documentId", element: /* @__PURE__ */ jsx(PreviewPage, {}) }),
996
+ /* @__PURE__ */ jsx(Route, { path: "*", element: /* @__PURE__ */ jsx(ExtensionPage, { pages, isLoading }) })
997
+ ] })
998
+ }
999
+ );
1000
+ }
1001
+ function ExtensionPage({ pages, isLoading }) {
1002
+ const { t } = useMessages();
1003
+ const path = useParams()["*"] ?? "";
1004
+ const page = pages.find((candidate) => candidate.to === path);
1005
+ const Component = useMemo(() => {
1006
+ const loader = page ? pageLoaderOf(page.id) : void 0;
1007
+ return loader ? lazy(loader) : null;
1008
+ }, [page]);
1009
+ if (isLoading) return /* @__PURE__ */ jsx(Page.Loading, {});
1010
+ if (!Component) {
1011
+ return /* @__PURE__ */ jsxs(Page.Main, { children: [
1012
+ /* @__PURE__ */ jsx(Page.Title, { children: t("page.unavailable.title") }),
1013
+ /* @__PURE__ */ jsx(Layouts.Header, { title: t("page.unavailable.title") }),
1014
+ /* @__PURE__ */ jsx(Layouts.Content, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: t("page.unavailable") }) })
1015
+ ] });
1016
+ }
1017
+ return /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(Page.Loading, {}), children: /* @__PURE__ */ jsx(Component, {}) });
1018
+ }
1019
+ export {
1020
+ MicelioLayout as default
1021
+ };