camox 0.75.0 → 0.76.1
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/core/editing/createEditableBlock.js +1077 -1681
- package/dist/core/editing/fieldSlot.js +251 -0
- package/dist/core/editing/useFieldSlot.js +159 -0
- package/dist/core/layoutContext.d.ts +2 -0
- package/dist/core/layoutContext.js +27 -1
- package/dist/features/canvas/CanvasPageFrame.js +2 -2
- package/dist/features/canvas/CanvasPreviewPageSlot.js +1 -1
- package/dist/features/canvas/canvasCameraController.js +595 -0
- package/dist/features/canvas/useCanvasCamera.js +141 -587
- package/dist/features/canvas/useCanvasFrame.js +1 -1
- package/dist/features/page/DerivedPageContent.js +4 -8
- package/dist/features/preview/components/AssetLightbox.js +6 -65
- package/dist/features/preview/components/CanvasPreview.js +2 -2
- package/dist/features/preview/components/Frame.js +3 -3
- package/dist/features/preview/components/ItemFieldsEditor.js +376 -460
- package/dist/features/preview/components/Overlays.js +4 -9
- package/dist/features/preview/components/PreviewPanel.js +3 -3
- package/dist/features/preview/components/RemovedBlockPlaceholder.js +2 -2
- package/dist/features/preview/components/SelectionEditor.js +285 -339
- package/dist/features/preview/components/ZoomableImage.js +105 -0
- package/dist/features/preview/components/contentFieldSchema.js +3 -1
- package/dist/features/preview/components/useFieldWriter.js +30 -51
- package/dist/features/preview/components/useRecordView.js +3 -3
- package/dist/features/preview/overlayMessages.js +20 -58
- package/dist/features/preview/previewStore.js +6 -123
- package/dist/features/preview/repeatableItemPath.js +47 -0
- package/dist/features/provider/AuthenticatedCamoxProvider.js +75 -63
- package/dist/features/renderer/RendererApp.js +6 -6
- package/dist/features/renderer/RendererFrames.js +2 -2
- package/dist/features/renderer/rendererHost.js +1 -1
- package/dist/features/routes/pageRuntime.d.ts +8 -12
- package/dist/features/routes/pageRuntime.js +26 -106
- package/dist/features/runtime/pageApp.js +1 -1
- package/dist/features/runtime/pageRead.js +105 -0
- package/dist/features/runtime/reader.d.ts +25 -0
- package/dist/features/runtime/reader.js +142 -0
- package/dist/features/runtime/runtime.d.ts +9 -3
- package/dist/features/runtime/runtime.js +608 -776
- package/dist/features/sdkBundle/buildSdkBundle.d.ts +5 -0
- package/dist/features/sdkBundle/buildSdkBundle.js +9 -3
- package/dist/features/studio/components/ChatWidget.js +137 -121
- package/dist/features/studio/components/ClearThread.js +121 -0
- package/dist/features/studio/components/DeleteDraftDialog.js +67 -65
- package/dist/features/studio/components/DraftBar.js +60 -49
- package/dist/features/studio/components/DraftNotices.js +149 -0
- package/dist/features/studio/components/DraftsControl.js +217 -180
- package/dist/features/studio/components/DraftsSheet.js +446 -0
- package/dist/features/studio/components/PublishedTab.js +511 -0
- package/dist/features/studio/components/ReviewDraftDialog.js +104 -89
- package/dist/features/studio/components/SheetTable.js +46 -0
- package/dist/features/studio/components/Specialists.js +1 -1
- package/dist/features/studio/components/ThreadMessages.js +227 -176
- package/dist/features/studio/draftLabel.js +15 -1
- package/dist/features/studio/draftNotice.js +1 -0
- package/dist/features/studio/showInCanvas.js +9 -19
- package/dist/features/studio/studioStore.js +5 -5
- package/dist/features/studio/useArchiveDraft.js +60 -0
- package/dist/features/studio/useEditorSelection.js +8 -19
- package/dist/features/studio/useRestoreDraft.js +70 -0
- package/dist/features/studio/viewSwitch.js +19 -11
- package/dist/lib/draft-scope.d.ts +1 -1
- package/dist/lib/draft-scope.js +6 -3
- package/dist/lib/queries.js +12 -1
- package/dist/lib/studio-reads.js +22 -0
- package/dist/lib/use-project-room.js +19 -13
- package/dist/lib/use-thread.js +1 -1
- package/dist/package.js +1 -1
- package/dist/studio.css +1 -1
- package/package.json +4 -4
- package/dist/core/hooks/useFieldSelection.js +0 -67
- package/dist/core/hooks/useIsEditable.js +0 -21
- package/dist/core/hooks/useOverlayMessage.js +0 -70
|
@@ -7,18 +7,18 @@ import { useApplyTheme, useThemeActions } from "../studio/useTheme.js";
|
|
|
7
7
|
import { usePreviewPagesActions, useShowCanvasAction } from "../preview/CamoxPreview.js";
|
|
8
8
|
import { useProjectRoom } from "../../lib/use-project-room.js";
|
|
9
9
|
import { useLeaveDraft } from "../studio/viewSwitch.js";
|
|
10
|
+
import { DraftNotices } from "../studio/components/DraftNotices.js";
|
|
10
11
|
import { useNavbarActions } from "../studio/components/Navbar.js";
|
|
11
|
-
import { draftNoticeMessage } from "../studio/draftNotice.js";
|
|
12
12
|
import { CommandPalette, useCommandPaletteActions } from "./components/CommandPalette.js";
|
|
13
13
|
import { c } from "react/compiler-runtime";
|
|
14
14
|
import * as React from "react";
|
|
15
15
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
16
|
-
import { Toaster
|
|
16
|
+
import { Toaster } from "@camox/ui/toaster";
|
|
17
17
|
import studioCssUrl from "virtual:camox-studio-css";
|
|
18
18
|
|
|
19
19
|
//#region src/features/provider/AuthenticatedCamoxProvider.tsx
|
|
20
20
|
function AuthenticatedCamoxProvider(t0) {
|
|
21
|
-
const $ = c(
|
|
21
|
+
const $ = c(24);
|
|
22
22
|
const { children } = t0;
|
|
23
23
|
useAdminShortcuts();
|
|
24
24
|
useCommandPaletteActions();
|
|
@@ -37,85 +37,97 @@ function AuthenticatedCamoxProvider(t0) {
|
|
|
37
37
|
const { data: project } = useReadQuery(t1);
|
|
38
38
|
const draftScope = useDraftScope();
|
|
39
39
|
const studioCache = useStudioCache();
|
|
40
|
+
const [revisions] = React.useState(createRevisions);
|
|
41
|
+
const leaveDraft = useLeaveDraft();
|
|
40
42
|
let t2;
|
|
43
|
+
if ($[2] !== authClient || $[3] !== draftScope || $[4] !== leaveDraft) {
|
|
44
|
+
t2 = (left, by) => {
|
|
45
|
+
if (left.draftId !== draftScope.current()) return;
|
|
46
|
+
if (by === signedInUserId(authClient)) return leaveDraft(left);
|
|
47
|
+
draftScope.leave(left, { notice: true });
|
|
48
|
+
};
|
|
49
|
+
$[2] = authClient;
|
|
50
|
+
$[3] = draftScope;
|
|
51
|
+
$[4] = leaveDraft;
|
|
52
|
+
$[5] = t2;
|
|
53
|
+
} else t2 = $[5];
|
|
54
|
+
const onDraftLeft = t2;
|
|
41
55
|
let t3;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
t3 =
|
|
45
|
-
|
|
46
|
-
$[
|
|
47
|
-
$[
|
|
56
|
+
let t4;
|
|
57
|
+
if ($[6] !== onDraftLeft) {
|
|
58
|
+
t3 = (ended) => onDraftLeft(ended, ended.endedBy.id);
|
|
59
|
+
t4 = (archived) => onDraftLeft(archived, archived.archivedBy.id);
|
|
60
|
+
$[6] = onDraftLeft;
|
|
61
|
+
$[7] = t3;
|
|
62
|
+
$[8] = t4;
|
|
48
63
|
} else {
|
|
49
|
-
|
|
50
|
-
|
|
64
|
+
t3 = $[7];
|
|
65
|
+
t4 = $[8];
|
|
51
66
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
t4 = {
|
|
64
|
-
onDraftEnded(ended) {
|
|
65
|
-
if (ended.draftId !== draftScope.current()) return;
|
|
66
|
-
if (ended.endedBy.id === signedInUserId(authClient)) return leaveDraft(ended);
|
|
67
|
-
draftScope.leave(ended, { notice: true });
|
|
68
|
-
},
|
|
67
|
+
let t5;
|
|
68
|
+
if ($[9] !== revisions) {
|
|
69
|
+
t5 = (revision) => revisions.announce(revision);
|
|
70
|
+
$[9] = revisions;
|
|
71
|
+
$[10] = t5;
|
|
72
|
+
} else t5 = $[10];
|
|
73
|
+
let t6;
|
|
74
|
+
if ($[11] !== t3 || $[12] !== t4 || $[13] !== t5) {
|
|
75
|
+
t6 = {
|
|
76
|
+
onDraftEnded: t3,
|
|
77
|
+
onDraftArchived: t4,
|
|
69
78
|
onRevision: t5
|
|
70
79
|
};
|
|
71
|
-
$[
|
|
72
|
-
$[
|
|
73
|
-
$[
|
|
74
|
-
$[
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
useProjectRoom(apiUrl, project?.id, studioCache, t4);
|
|
80
|
+
$[11] = t3;
|
|
81
|
+
$[12] = t4;
|
|
82
|
+
$[13] = t5;
|
|
83
|
+
$[14] = t6;
|
|
84
|
+
} else t6 = $[14];
|
|
85
|
+
useProjectRoom(apiUrl, project?.id, studioCache, t6);
|
|
78
86
|
const { resolvedTheme } = useApplyTheme();
|
|
79
|
-
let
|
|
80
|
-
if ($[
|
|
81
|
-
|
|
87
|
+
let t7;
|
|
88
|
+
if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
|
|
89
|
+
t7 = /* @__PURE__ */ jsx("link", {
|
|
82
90
|
rel: "stylesheet",
|
|
83
91
|
href: studioCssUrl,
|
|
84
92
|
"data-camox-studio": true
|
|
85
93
|
});
|
|
86
|
-
$[12] = t5;
|
|
87
|
-
} else t5 = $[12];
|
|
88
|
-
let t6;
|
|
89
|
-
if ($[13] !== resolvedTheme) {
|
|
90
|
-
t6 = /* @__PURE__ */ jsx(Toaster, { theme: resolvedTheme });
|
|
91
|
-
$[13] = resolvedTheme;
|
|
92
|
-
$[14] = t6;
|
|
93
|
-
} else t6 = $[14];
|
|
94
|
-
let t7;
|
|
95
|
-
if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
|
|
96
|
-
t7 = /* @__PURE__ */ jsx(CommandPalette, {});
|
|
97
94
|
$[15] = t7;
|
|
98
95
|
} else t7 = $[15];
|
|
99
96
|
let t8;
|
|
100
|
-
if ($[16] !==
|
|
101
|
-
t8 = /* @__PURE__ */
|
|
97
|
+
if ($[16] !== resolvedTheme) {
|
|
98
|
+
t8 = /* @__PURE__ */ jsx(Toaster, { theme: resolvedTheme });
|
|
99
|
+
$[16] = resolvedTheme;
|
|
100
|
+
$[17] = t8;
|
|
101
|
+
} else t8 = $[17];
|
|
102
|
+
let t10;
|
|
103
|
+
let t9;
|
|
104
|
+
if ($[18] === Symbol.for("react.memo_cache_sentinel")) {
|
|
105
|
+
t9 = /* @__PURE__ */ jsx(CommandPalette, {});
|
|
106
|
+
t10 = /* @__PURE__ */ jsx(DraftNotices, {});
|
|
107
|
+
$[18] = t10;
|
|
108
|
+
$[19] = t9;
|
|
109
|
+
} else {
|
|
110
|
+
t10 = $[18];
|
|
111
|
+
t9 = $[19];
|
|
112
|
+
}
|
|
113
|
+
let t11;
|
|
114
|
+
if ($[20] !== children || $[21] !== revisions || $[22] !== t8) {
|
|
115
|
+
t11 = /* @__PURE__ */ jsxs(RevisionsContext, {
|
|
102
116
|
value: revisions,
|
|
103
117
|
children: [
|
|
104
|
-
|
|
118
|
+
t7,
|
|
105
119
|
children,
|
|
106
|
-
|
|
107
|
-
|
|
120
|
+
t8,
|
|
121
|
+
t9,
|
|
122
|
+
t10
|
|
108
123
|
]
|
|
109
124
|
});
|
|
110
|
-
$[
|
|
111
|
-
$[
|
|
112
|
-
$[
|
|
113
|
-
$[
|
|
114
|
-
} else
|
|
115
|
-
return
|
|
116
|
-
}
|
|
117
|
-
function _temp(notice) {
|
|
118
|
-
toast(draftNoticeMessage(notice));
|
|
125
|
+
$[20] = children;
|
|
126
|
+
$[21] = revisions;
|
|
127
|
+
$[22] = t8;
|
|
128
|
+
$[23] = t11;
|
|
129
|
+
} else t11 = $[23];
|
|
130
|
+
return t11;
|
|
119
131
|
}
|
|
120
132
|
|
|
121
133
|
//#endregion
|
|
@@ -3,14 +3,14 @@ import { ViewLocaleContext } from "../../lib/view-locale-context.js";
|
|
|
3
3
|
import { NavigationProvider } from "../navigation/navigation.js";
|
|
4
4
|
import { reads, useRead } from "../../lib/studio-reads.js";
|
|
5
5
|
import { ArtifactStudioContext, ArtifactsRunContext } from "../../core/hooks/useArtifactHost.js";
|
|
6
|
-
import { EditableSurfaceContext } from "../../core/hooks/useIsEditable.js";
|
|
7
|
-
import { CanvasWorkspaceContext } from "../canvas/canvasZoom.js";
|
|
8
|
-
import { PreviewEditingOwnerContext } from "../preview/previewSelection.js";
|
|
9
|
-
import { StudioNoticeContext } from "../preview/studioNotice.js";
|
|
10
|
-
import { ContentEditsContext } from "../../core/editing/contentEdits.js";
|
|
11
|
-
import { restyle } from "../preview/components/previewStyles.js";
|
|
12
6
|
import { PreviewDocumentContext } from "../runtime/PreviewDocumentContext.js";
|
|
7
|
+
import { restyle } from "../preview/components/previewStyles.js";
|
|
13
8
|
import { FrameContext } from "../preview/components/Frame.js";
|
|
9
|
+
import { PreviewEditingOwnerContext } from "../preview/previewSelection.js";
|
|
10
|
+
import { ContentEditsContext } from "../../core/editing/contentEdits.js";
|
|
11
|
+
import { EditableSurfaceContext } from "../../core/editing/useFieldSlot.js";
|
|
12
|
+
import { CanvasWorkspaceContext } from "../canvas/canvasZoom.js";
|
|
13
|
+
import { StudioNoticeContext } from "../preview/studioNotice.js";
|
|
14
14
|
import { DerivedPageContent } from "../page/DerivedPageContent.js";
|
|
15
15
|
import { EditablePageContent } from "../preview/EditablePageContent.js";
|
|
16
16
|
import { c } from "react/compiler-runtime";
|
|
@@ -5,10 +5,10 @@ import { runtimePath } from "../runtime/navigationTarget.js";
|
|
|
5
5
|
import { useViewPreparer } from "../../lib/view-switch.js";
|
|
6
6
|
import { useStudioCache } from "../../lib/studio-reads.js";
|
|
7
7
|
import { ArtifactStudioContext, ArtifactsRunContext } from "../../core/hooks/useArtifactHost.js";
|
|
8
|
-
import {
|
|
8
|
+
import { PreviewEditingOwnerContext } from "../preview/previewSelection.js";
|
|
9
|
+
import { EditableSurfaceContext } from "../../core/editing/useFieldSlot.js";
|
|
9
10
|
import { RevisionsContext } from "../../lib/revisions.js";
|
|
10
11
|
import { CanvasWorkspaceContext } from "../canvas/canvasZoom.js";
|
|
11
|
-
import { PreviewEditingOwnerContext } from "../preview/previewSelection.js";
|
|
12
12
|
import { showNotice } from "../preview/studioNotice.js";
|
|
13
13
|
import { RENDERER_DOCUMENT_PATH, STUDIO_HANDLE_KEY } from "./protocol.js";
|
|
14
14
|
import { createRendererHost } from "./rendererHost.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { canvasStore } from "../canvas/canvasStore.js";
|
|
2
2
|
import { previewStore } from "../preview/previewStore.js";
|
|
3
|
-
import { SDK_VERSION, deferredInbox } from "./protocol.js";
|
|
4
3
|
import { createContentEditor } from "../../core/editing/contentEdits.js";
|
|
4
|
+
import { SDK_VERSION, deferredInbox } from "./protocol.js";
|
|
5
5
|
import { LIVE_KEY_SCOPE, scopeOfKey } from "@camox/api-contract/query-keys";
|
|
6
6
|
|
|
7
7
|
//#region src/features/renderer/rendererHost.ts
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import "../../lib/locale-path.js";
|
|
2
2
|
import "../../core/layoutContext.js";
|
|
3
3
|
import "../../core/structuredData.js";
|
|
4
4
|
import "../../core/createLayout.js";
|
|
5
5
|
import "../../core/createApp.js";
|
|
6
|
+
import "../runtime/reader.js";
|
|
6
7
|
import "../../lib/studio-reads.js";
|
|
7
|
-
import { EndedDraft } from "@camox/api-contract/drafts";
|
|
8
|
+
import { ArchivedDraft, EndedDraft } from "@camox/api-contract/drafts";
|
|
8
9
|
import { QueryClient } from "@tanstack/react-query";
|
|
9
10
|
import { Router } from "@camox/api-contract/router";
|
|
10
11
|
import { RouterClient } from "@orpc/server";
|
|
@@ -22,8 +23,8 @@ export type PageStructure = {
|
|
|
22
23
|
recordStructuredData?: PageWithBlocks["recordStructuredData"];
|
|
23
24
|
};
|
|
24
25
|
/**
|
|
25
|
-
* Why a member can't see the draft a URL names: it doesn't exist, it isn't open to them,
|
|
26
|
-
* was published or deleted (ADR 0011).
|
|
26
|
+
* Why a member can't see the draft a URL names: it doesn't exist, it isn't open to them, it
|
|
27
|
+
* was published or deleted (ADR 0011), or it's archived (ADR 0046).
|
|
27
28
|
*/
|
|
28
29
|
export type DraftNotice = {
|
|
29
30
|
reason: "missing";
|
|
@@ -31,7 +32,9 @@ export type DraftNotice = {
|
|
|
31
32
|
reason: "forbidden";
|
|
32
33
|
} | ({
|
|
33
34
|
reason: "ended";
|
|
34
|
-
} & EndedDraft)
|
|
35
|
+
} & EndedDraft) | ({
|
|
36
|
+
reason: "archived";
|
|
37
|
+
} & ArchivedDraft);
|
|
35
38
|
/** Which draft a render read: the URL's, or Live when there was none or it couldn't be read. */
|
|
36
39
|
export interface DraftRead {
|
|
37
40
|
/** The draft the render read. Absent for Live. */
|
|
@@ -44,13 +47,6 @@ export interface DraftRead {
|
|
|
44
47
|
notice: DraftNotice | null;
|
|
45
48
|
};
|
|
46
49
|
}
|
|
47
|
-
export declare function createServerApiClient(apiUrl: string, options?: {
|
|
48
|
-
authCookieHeader?: string;
|
|
49
|
-
/** A look token, which reads the draft it was signed for without a session (ADR 0032). */
|
|
50
|
-
lookToken?: string;
|
|
51
|
-
draftId?: string;
|
|
52
|
-
locale?: LocaleView;
|
|
53
|
-
}): RouterClient<Router>;
|
|
54
50
|
export type PageHead = {
|
|
55
51
|
meta?: Array<Record<string, string>>;
|
|
56
52
|
links?: Array<Record<string, string>>;
|
|
@@ -6,7 +6,7 @@ import { PAGE_IMAGE_SIZE, pageImageUrl } from "../og/pageImage.js";
|
|
|
6
6
|
import { curatedLayoutContext } from "../../core/layoutContext.js";
|
|
7
7
|
import { isArticle } from "../../core/structuredData.js";
|
|
8
8
|
import { publishingDraft } from "../runtime/publishingDraft.js";
|
|
9
|
-
import { DRAFT_ENDED_ERROR_CODE, DRAFT_HEADER, LOOK_TOKEN_HEADER, PUBLISHING_DRAFT_HEADER } from "@camox/api-contract/drafts";
|
|
9
|
+
import { DRAFT_ARCHIVED_ERROR_CODE, DRAFT_ENDED_ERROR_CODE, DRAFT_HEADER, LOOK_TOKEN_HEADER, PUBLISHING_DRAFT_HEADER } from "@camox/api-contract/drafts";
|
|
10
10
|
import { createORPCClient } from "@orpc/client";
|
|
11
11
|
import { RPCLink } from "@orpc/client/fetch";
|
|
12
12
|
|
|
@@ -36,10 +36,16 @@ function parseMarkdownPath(pathname) {
|
|
|
36
36
|
if (pathname === "/index.md") return "/";
|
|
37
37
|
return pathname.slice(0, -3) || null;
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
/**
|
|
40
|
+
* The API's client for one kind of read: with a credential, in a draft or Live, in a locale. Only
|
|
41
|
+
* a credential can read a draft, so a draft is only ever named with one; a Live read names the
|
|
42
|
+
* draft this Worker publishes, if any.
|
|
43
|
+
*/
|
|
44
|
+
function createServerApiClient(apiUrl, { credential, draftId, locale } = {}) {
|
|
45
|
+
const headers = localeHeaders(locale);
|
|
46
|
+
const session = credential && "session" in credential ? credential.session : void 0;
|
|
47
|
+
if (credential && "lookToken" in credential) headers[LOOK_TOKEN_HEADER] = credential.lookToken;
|
|
48
|
+
if (draftId && credential) headers[DRAFT_HEADER] = draftId;
|
|
43
49
|
else {
|
|
44
50
|
const publishing = publishingDraft();
|
|
45
51
|
if (publishing) headers[PUBLISHING_DRAFT_HEADER] = publishing;
|
|
@@ -48,7 +54,7 @@ function createServerApiClient(apiUrl, options) {
|
|
|
48
54
|
url: `${apiUrl}/rpc`,
|
|
49
55
|
headers,
|
|
50
56
|
fetch: (request, init) => {
|
|
51
|
-
if (
|
|
57
|
+
if (session && request instanceof Request) request.headers.set("Better-Auth-Cookie", session);
|
|
52
58
|
return fetch(request, init);
|
|
53
59
|
}
|
|
54
60
|
}));
|
|
@@ -84,46 +90,24 @@ function draftRefusalNotice(error, draftId) {
|
|
|
84
90
|
reason: "ended",
|
|
85
91
|
...data
|
|
86
92
|
};
|
|
93
|
+
if (code === DRAFT_ARCHIVED_ERROR_CODE && data) return {
|
|
94
|
+
reason: "archived",
|
|
95
|
+
...data
|
|
96
|
+
};
|
|
87
97
|
if (isNotFoundError(error) && data?.draftId === draftId) return { reason: "missing" };
|
|
88
98
|
if (code === "FORBIDDEN" || status === 403) return { reason: "forbidden" };
|
|
89
99
|
if (code === "UNAUTHORIZED" || status === 401) return null;
|
|
90
100
|
}
|
|
91
|
-
/**
|
|
92
|
-
* Reads with the draft a URL names, or Live (ADR 0011). Only a member's request names the draft;
|
|
93
|
-
* anyone else reads Live. When the API refuses the draft, the same response reads Live instead.
|
|
94
|
-
*/
|
|
95
|
-
async function readDraftOrLive(draftId, member, read) {
|
|
96
|
-
if (!draftId) return { value: await read(void 0) };
|
|
97
|
-
if (!member) return {
|
|
98
|
-
value: await read(void 0),
|
|
99
|
-
refusedDraft: { notice: null }
|
|
100
|
-
};
|
|
101
|
-
try {
|
|
102
|
-
return {
|
|
103
|
-
value: await read(draftId),
|
|
104
|
-
draftId
|
|
105
|
-
};
|
|
106
|
-
} catch (error) {
|
|
107
|
-
const notice = draftRefusalNotice(error, draftId);
|
|
108
|
-
if (notice === void 0) throw error;
|
|
109
|
-
return {
|
|
110
|
-
value: await read(void 0),
|
|
111
|
-
refusedDraft: { notice }
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
101
|
/** The URL a render shows: without the draft param when the draft couldn't be read. */
|
|
116
102
|
function renderedHref(url, read) {
|
|
117
103
|
return read.refusedDraft ? withoutDraft(url) : url;
|
|
118
104
|
}
|
|
119
|
-
async function loadCamoxPage({
|
|
105
|
+
async function loadCamoxPage({ reader, draftId, locale, pathname, projectUuid, queryClient }) {
|
|
120
106
|
const studioCache = createServerStudioCache(queryClient, {
|
|
121
|
-
|
|
122
|
-
authCookieHeader,
|
|
123
|
-
lookToken,
|
|
107
|
+
reader,
|
|
124
108
|
locale
|
|
125
109
|
});
|
|
126
|
-
const inDraft = { in: draftId
|
|
110
|
+
const inDraft = { in: draftId ?? null };
|
|
127
111
|
const fallback = locale && linkFallbackLocale(locale);
|
|
128
112
|
const [page] = await Promise.all([
|
|
129
113
|
studioCache.ensure(reads.pageStructure({
|
|
@@ -139,16 +123,14 @@ async function loadCamoxPage({ apiUrl, authCookieHeader, lookToken, draftId, loc
|
|
|
139
123
|
return page;
|
|
140
124
|
}
|
|
141
125
|
/**
|
|
142
|
-
* The studio's reads for one server request, read
|
|
143
|
-
*
|
|
126
|
+
* The studio's reads for one server request, read as its reader. Each operation names the draft
|
|
127
|
+
* it acts in, once the request settled which one it reads.
|
|
144
128
|
*/
|
|
145
|
-
function createServerStudioCache(queryClient, {
|
|
129
|
+
function createServerStudioCache(queryClient, { reader, locale }) {
|
|
146
130
|
return createStudioCache({
|
|
147
131
|
queryClient,
|
|
148
132
|
scope: createDraftScope({ queryClient }),
|
|
149
|
-
api: (draft, inLocale) =>
|
|
150
|
-
authCookieHeader,
|
|
151
|
-
lookToken,
|
|
133
|
+
api: (draft, inLocale) => reader.api({
|
|
152
134
|
draftId: draft ?? void 0,
|
|
153
135
|
locale: inLocale
|
|
154
136
|
}),
|
|
@@ -156,45 +138,6 @@ function createServerStudioCache(queryClient, { apiUrl, authCookieHeader, lookTo
|
|
|
156
138
|
});
|
|
157
139
|
}
|
|
158
140
|
/**
|
|
159
|
-
* The locales of the scope a request reads: the URL's draft's for a session naming one, else
|
|
160
|
-
* Live's. A draft the API refuses reads Live's, as the request's other reads then do. Live's read
|
|
161
|
-
* keeps the session: a member's studio routes the locales Live doesn't serve yet, which visitors
|
|
162
|
-
* never see (ADR 0043), and a render with a session is private.
|
|
163
|
-
*/
|
|
164
|
-
async function readScopeLocales({ apiUrl, authCookieHeader, draftId, projectUuid }) {
|
|
165
|
-
const list = (session, readDraftId) => createServerApiClient(apiUrl, {
|
|
166
|
-
authCookieHeader: session,
|
|
167
|
-
draftId: readDraftId
|
|
168
|
-
}).locales.list({ projectUuid });
|
|
169
|
-
const { value } = await readDraftOrLive(draftId, !!authCookieHeader, async (readDraftId) => {
|
|
170
|
-
if (readDraftId || !authCookieHeader) return list(authCookieHeader, readDraftId);
|
|
171
|
-
try {
|
|
172
|
-
return await list(authCookieHeader, void 0);
|
|
173
|
-
} catch (error) {
|
|
174
|
-
if (!isAuthSessionError(error)) throw error;
|
|
175
|
-
return list(void 0, void 0);
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
return value;
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* The project, when the auth cookie is a valid session of one of its members. A successful
|
|
182
|
-
* protected project lookup validates both session and project access: cookie presence alone
|
|
183
|
-
* is not proof of authentication. An invalid session asks for the cookie to be cleared.
|
|
184
|
-
*/
|
|
185
|
-
async function resolveMemberProject({ apiUrl, authCookieHeader, projectUuid }) {
|
|
186
|
-
if (!authCookieHeader) return { clearAuthCookie: false };
|
|
187
|
-
try {
|
|
188
|
-
return {
|
|
189
|
-
project: await createServerApiClient(apiUrl, { authCookieHeader }).projects.getByUuid({ projectUuid }),
|
|
190
|
-
clearAuthCookie: false
|
|
191
|
-
};
|
|
192
|
-
} catch (error) {
|
|
193
|
-
if (!isAuthSessionError(error)) throw error;
|
|
194
|
-
return { clearAuthCookie: true };
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
141
|
* The project's favicon, which falls back to the default favicon when none was uploaded.
|
|
199
142
|
* Pass `withDefault: false` to get a 404 instead, to learn whether there is one.
|
|
200
143
|
*/
|
|
@@ -213,36 +156,13 @@ function buildCamoxPageLoaderData(apiUrl, page, origin) {
|
|
|
213
156
|
faviconUrl: buildFaviconUrl(apiUrl, page.project)
|
|
214
157
|
};
|
|
215
158
|
}
|
|
216
|
-
async function loadCamoxPageForRequest({ apiUrl, authCookieHeader, draftId, locale, origin, pathname, projectUuid, queryClient }) {
|
|
217
|
-
const member = await resolveMemberProject({
|
|
218
|
-
apiUrl,
|
|
219
|
-
authCookieHeader,
|
|
220
|
-
projectUuid
|
|
221
|
-
});
|
|
222
|
-
if (member.clearAuthCookie) console.warn("[camox] Ignoring stale camox_auth_cookie.");
|
|
223
|
-
const { value: page, ...read } = await readDraftOrLive(draftId, !!member.project, (readDraftId) => loadCamoxPage({
|
|
224
|
-
apiUrl,
|
|
225
|
-
authCookieHeader: member.project ? authCookieHeader : void 0,
|
|
226
|
-
draftId: readDraftId,
|
|
227
|
-
locale,
|
|
228
|
-
pathname,
|
|
229
|
-
projectUuid,
|
|
230
|
-
queryClient
|
|
231
|
-
}));
|
|
232
|
-
return {
|
|
233
|
-
...read,
|
|
234
|
-
data: buildCamoxPageLoaderData(apiUrl, page, origin),
|
|
235
|
-
project: member.project,
|
|
236
|
-
shouldClearAuthCookie: member.clearAuthCookie
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
159
|
/**
|
|
240
160
|
* Reads the Markdown Representation of the page at a runtime path, with its locale's prefix, in
|
|
241
161
|
* one API read: the API finds the locale the prefix names, the page there, its Markdown and what
|
|
242
162
|
* its front matter is built from. Markdown is a public output, so it reads Live whoever asks.
|
|
243
163
|
*/
|
|
244
|
-
function readPageMarkdown({
|
|
245
|
-
return
|
|
164
|
+
function readPageMarkdown({ reader, path, projectUuid }) {
|
|
165
|
+
return reader.api().pages.getMarkdown({
|
|
246
166
|
path,
|
|
247
167
|
projectUuid
|
|
248
168
|
});
|
|
@@ -362,4 +282,4 @@ function buildCamoxPageHead(camoxApp, loaderData, locale) {
|
|
|
362
282
|
}
|
|
363
283
|
|
|
364
284
|
//#endregion
|
|
365
|
-
export { buildCamoxPageHead, buildCamoxPageLoaderData, buildDefaultFaviconUrl, buildFaviconUrl, buildPageHead, buildPageMeta, createServerApiClient, createServerStudioCache, curatedMarkdownResponse, curatedPageMetaInput, declarePage, getMarkdownPath, isAuthSessionError, isNotFoundError, loadCamoxPage,
|
|
285
|
+
export { buildCamoxPageHead, buildCamoxPageLoaderData, buildDefaultFaviconUrl, buildFaviconUrl, buildPageHead, buildPageMeta, createServerApiClient, createServerStudioCache, curatedMarkdownResponse, curatedPageMetaInput, declarePage, draftRefusalNotice, getMarkdownPath, isAuthSessionError, isNotFoundError, loadCamoxPage, parseMarkdownPath, parseQuality, prefersMarkdown, readPageMarkdown, renderedHref, withFrontMatter };
|
|
@@ -2,8 +2,8 @@ import { DraftScopeContext } from "../../lib/draft-scope.js";
|
|
|
2
2
|
import { useAuthState, useSignInRedirect } from "../../lib/auth.js";
|
|
3
3
|
import { ArtifactCaptureContext } from "../../core/hooks/useArtifactHost.js";
|
|
4
4
|
import { useSignInShortcut } from "../provider/signInShortcut.js";
|
|
5
|
-
import { PreviewActivation } from "../preview/components/PreviewActivation.js";
|
|
6
5
|
import { PreviewDocumentContext } from "./PreviewDocumentContext.js";
|
|
6
|
+
import { PreviewActivation } from "../preview/components/PreviewActivation.js";
|
|
7
7
|
import { Frame } from "../preview/components/Frame.js";
|
|
8
8
|
import { PreviewSignInPage } from "../provider/PreviewSignInPage.js";
|
|
9
9
|
import { CoreCamoxProvider } from "../provider/CoreCamoxProvider.js";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createRequestContext } from "../../core/requestContext.js";
|
|
2
|
+
import { asCollectionItem } from "../../core/recordItem.js";
|
|
3
|
+
import { matchDerivedLayout } from "../../core/derivedRoutes.js";
|
|
4
|
+
import { layoutPageContext } from "../../core/layoutContext.js";
|
|
5
|
+
import { isNotFoundError } from "../routes/pageRuntime.js";
|
|
6
|
+
|
|
7
|
+
//#region src/features/runtime/pageRead.ts
|
|
8
|
+
/**
|
|
9
|
+
* What a path shows, in the Locale the path's prefix named: a Singleton Layout's page first, then
|
|
10
|
+
* the curated page at the path, then a Derived Layout's page. Each output reads the curated page
|
|
11
|
+
* and the layout's blocks as it needs them; Page Read decides which it shows and assembles a
|
|
12
|
+
* Layout Page. A Layout Page's loader runs in the Draft its blocks are read in: alongside them
|
|
13
|
+
* when the reader knows the Draft, after them when their read settles it.
|
|
14
|
+
*/
|
|
15
|
+
async function readPage({ app, path, pathname, reader, curated, layout }) {
|
|
16
|
+
const match = app && matchDerivedLayout(app.getLayouts(), path);
|
|
17
|
+
const layoutPage = () => readLayoutPage({
|
|
18
|
+
app,
|
|
19
|
+
match,
|
|
20
|
+
pathname,
|
|
21
|
+
reader,
|
|
22
|
+
readBlocks: layout
|
|
23
|
+
});
|
|
24
|
+
if (!mayShowCuratedPage(app, path)) return layoutPage();
|
|
25
|
+
try {
|
|
26
|
+
const { value: page, ...read } = await curated();
|
|
27
|
+
if (page !== null) return {
|
|
28
|
+
kind: "curated",
|
|
29
|
+
page,
|
|
30
|
+
...read
|
|
31
|
+
};
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (!isNotFoundError(error)) throw error;
|
|
34
|
+
}
|
|
35
|
+
return match ? layoutPage() : { kind: "none" };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether a path may show a curated page, so reading one there may be worth starting early: not
|
|
39
|
+
* a Singleton Layout's route.
|
|
40
|
+
*/
|
|
41
|
+
function mayShowCuratedPage(app, path) {
|
|
42
|
+
return (app && matchDerivedLayout(app.getLayouts(), path))?.layout._internal.kind !== "singleton";
|
|
43
|
+
}
|
|
44
|
+
async function readLayoutPage({ app, match, pathname, reader, readBlocks }) {
|
|
45
|
+
const layout = match.layout._internal;
|
|
46
|
+
const blocksInScope = () => reader.inScope((draftId) => readBlocks(draftId, layout.id));
|
|
47
|
+
const load = (draftId) => loadLayoutResult(app, match, createRequestContext({ records: reader.records(draftId) }), reader.locale);
|
|
48
|
+
let result;
|
|
49
|
+
let read;
|
|
50
|
+
try {
|
|
51
|
+
if (reader.draft) [result, read] = await Promise.all([load(reader.draft.id), blocksInScope()]);
|
|
52
|
+
else {
|
|
53
|
+
read = await blocksInScope();
|
|
54
|
+
result = await load(read.draftId);
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (isNotFoundError(error)) return {
|
|
58
|
+
kind: "none",
|
|
59
|
+
layout: match.layout
|
|
60
|
+
};
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
const { value: blocks, ...draftRead } = read;
|
|
64
|
+
return {
|
|
65
|
+
kind: "layout",
|
|
66
|
+
layout: match.layout,
|
|
67
|
+
params: match.params,
|
|
68
|
+
blocks,
|
|
69
|
+
result,
|
|
70
|
+
...layoutPageContext(app, layout, {
|
|
71
|
+
path: pathname,
|
|
72
|
+
locale: reader.locale.tag,
|
|
73
|
+
projectName: blocks.projectName,
|
|
74
|
+
params: match.params,
|
|
75
|
+
result
|
|
76
|
+
}),
|
|
77
|
+
...draftRead
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const RECORD_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
81
|
+
/** An envelope names its record; it doesn't authorize anything, the API does. */
|
|
82
|
+
function isValidCollectionItem(app, result) {
|
|
83
|
+
const data = result.data;
|
|
84
|
+
return typeof result.collectionId === "string" && !!app.getCollectionById(result.collectionId) && typeof data?.id === "string" && RECORD_ID.test(data.id) && typeof data.content === "object" && data.content !== null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Runs a Layout Page's loader in the page's locale. The envelope survives SSR, navigation and
|
|
88
|
+
* hydration, and its data renders as returned: Camox never translates it.
|
|
89
|
+
*/
|
|
90
|
+
async function loadLayoutResult(app, match, context, locale) {
|
|
91
|
+
const { loader, id } = match.layout._internal;
|
|
92
|
+
if (!loader) return void 0;
|
|
93
|
+
const result = await loader({
|
|
94
|
+
params: match.params,
|
|
95
|
+
context,
|
|
96
|
+
locale: locale.tag
|
|
97
|
+
});
|
|
98
|
+
const serializedResult = result === void 0 ? void 0 : JSON.parse(JSON.stringify(result));
|
|
99
|
+
const record = asCollectionItem(serializedResult);
|
|
100
|
+
if (!(record ? isValidCollectionItem(app, record) : serializedResult?.kind === "data" && Object.hasOwn(serializedResult, "data"))) throw new Error(`Layout "${id}" loader must return { kind: "data", data } with JSON-serializable data, or a collection-item envelope naming a registered collection and a record id`);
|
|
101
|
+
return serializedResult;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
105
|
+
export { mayShowCuratedPage, readPage };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { LocaleView } from "../../lib/locale-path.js";
|
|
2
|
+
import "../routes/pageRuntime.js";
|
|
3
|
+
import { Router } from "@camox/api-contract/router";
|
|
4
|
+
import { RouterClient } from "@orpc/server";
|
|
5
|
+
//#region src/features/runtime/reader.d.ts
|
|
6
|
+
/** The API's procedures, as the content port answers them. */
|
|
7
|
+
export type ContentClient = RouterClient<Router>;
|
|
8
|
+
/** What proves a read may see more than Live: a Member's session, or a look token (ADR 0032). */
|
|
9
|
+
export type Credential = {
|
|
10
|
+
session: string;
|
|
11
|
+
} | {
|
|
12
|
+
lookToken: string;
|
|
13
|
+
};
|
|
14
|
+
/** Which reads a client makes: with which credential, in which Draft (Live without), in which Locale. */
|
|
15
|
+
export interface ContentAccess {
|
|
16
|
+
credential?: Credential;
|
|
17
|
+
draftId?: string;
|
|
18
|
+
locale?: LocaleView;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The content port: the API's reads, as one credential sees them, in Live or a Draft, in a
|
|
22
|
+
* Locale. Production reads the API (`apiContent`); tests read an in-memory one.
|
|
23
|
+
*/
|
|
24
|
+
export type Content = (access: ContentAccess) => ContentClient;
|
|
25
|
+
//#endregion
|