canopycms 0.0.67 → 0.0.68-int.100
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/README.md +21 -14
- package/dist/ai/handler.js +8 -0
- package/dist/api/admin-branch-health.js +17 -20
- package/dist/api/admin.d.ts +30 -9
- package/dist/api/admin.js +32 -2
- package/dist/api/assets.js +84 -76
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +14 -5
- package/dist/api/branch-status.d.ts +5 -5
- package/dist/api/branch-status.js +26 -6
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +7 -2
- package/dist/api/branch.d.ts +15 -2
- package/dist/api/branch.js +111 -92
- package/dist/api/client.d.ts +18 -6
- package/dist/api/client.js +79 -12
- package/dist/api/content.d.ts +6 -5
- package/dist/api/content.js +44 -41
- package/dist/api/entries.js +16 -15
- package/dist/api/github-sync.d.ts +10 -1
- package/dist/api/github-sync.js +20 -10
- package/dist/api/index.d.ts +1 -1
- package/dist/api/reference-options.js +1 -1
- package/dist/api/resolve-references.js +14 -40
- package/dist/api/settings-helpers.d.ts +1 -1
- package/dist/api/settings-helpers.js +1 -4
- package/dist/api/user.d.ts +3 -0
- package/dist/api/user.js +3 -0
- package/dist/assets/asset-url.d.ts +22 -16
- package/dist/assets/asset-url.js +42 -25
- package/dist/assets/factory.d.ts +5 -0
- package/dist/assets/factory.js +5 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +122 -0
- package/dist/assets/materialize.js +305 -0
- package/dist/assets/store-local.d.ts +4 -0
- package/dist/assets/store-local.js +11 -0
- package/dist/assets/store-s3.d.ts +10 -1
- package/dist/assets/store-s3.js +55 -3
- package/dist/assets/transform-directives.d.ts +52 -13
- package/dist/assets/transform-directives.js +87 -27
- package/dist/assets/transform.d.ts +3 -3
- package/dist/assets/transform.js +13 -8
- package/dist/assets/types.d.ts +14 -1
- package/dist/auth/file-based-auth-cache.js +7 -8
- package/dist/authorization/content.d.ts +5 -4
- package/dist/authorization/content.js +5 -5
- package/dist/authorization/path.d.ts +11 -6
- package/dist/authorization/path.js +11 -6
- package/dist/authorization/types.d.ts +2 -2
- package/dist/branch-health.d.ts +7 -1
- package/dist/branch-health.js +8 -3
- package/dist/branch-metadata.d.ts +5 -5
- package/dist/branch-metadata.js +44 -36
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- package/dist/branch-sparse.d.ts +23 -0
- package/dist/branch-sparse.js +118 -0
- package/dist/branch-workspace.d.ts +42 -3
- package/dist/branch-workspace.js +240 -94
- package/dist/build/asset-refs.d.ts +93 -0
- package/dist/build/asset-refs.js +251 -0
- package/dist/build-identity.d.ts +3 -0
- package/dist/build-identity.js +13 -0
- package/dist/cli/asset-refs.d.ts +30 -0
- package/dist/cli/asset-refs.js +168 -0
- package/dist/cli/cli.d.ts +2 -1
- package/dist/cli/cli.js +4843 -1870
- package/dist/cli/generate-ai-content.js +1584 -444
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +30 -27
- package/dist/cli/migrate.js +12 -8
- package/dist/cli/sync.js +83 -37
- package/dist/cli/template-files/Dockerfile.cms.template +8 -0
- package/dist/cli/template-files/cdk-app.ts.template +4 -0
- package/dist/cli/template-files/cms-stack.ts.template +23 -12
- package/dist/cli/template-files/deploy-cms.yml.template +2 -0
- package/dist/config/helpers.js +1 -2
- package/dist/config/schemas/config.d.ts +19 -32
- package/dist/config/schemas/config.js +9 -11
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +4 -20
- package/dist/config/schemas/url.js +6 -22
- package/dist/config/types.d.ts +30 -20
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +37 -0
- package/dist/content-listing.d.ts +12 -12
- package/dist/content-listing.js +10 -6
- package/dist/content-reader.d.ts +11 -2
- package/dist/content-reader.js +22 -14
- package/dist/content-store.d.ts +37 -9
- package/dist/content-store.js +70 -43
- package/dist/content-tree.d.ts +1 -1
- package/dist/content-tree.js +2 -2
- package/dist/context.d.ts +27 -9
- package/dist/context.js +154 -70
- package/dist/editor/BranchManager.d.ts +7 -1
- package/dist/editor/BranchManager.js +29 -14
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +2 -6
- package/dist/editor/Editor.d.ts +4 -3
- package/dist/editor/Editor.js +24 -20
- package/dist/editor/FormRenderer.js +34 -18
- package/dist/editor/admin/SystemHealthPanel.js +47 -7
- package/dist/editor/components/BranchesDrawer.d.ts +9 -0
- package/dist/editor/components/BranchesDrawer.js +4 -0
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/context/AssetContext.d.ts +18 -35
- package/dist/editor/context/AssetContext.js +20 -29
- package/dist/editor/context/index.d.ts +1 -1
- package/dist/editor/context/index.js +1 -1
- package/dist/editor/editor-config.d.ts +1 -2
- package/dist/editor/editor-config.js +0 -23
- package/dist/editor/editor-utils.d.ts +24 -21
- package/dist/editor/editor-utils.js +75 -62
- package/dist/editor/fields/BlockField.d.ts +1 -0
- package/dist/editor/fields/BlockField.js +6 -4
- package/dist/editor/fields/CodeField.d.ts +1 -0
- package/dist/editor/fields/CodeField.js +2 -2
- package/dist/editor/fields/DateTimeField.d.ts +1 -0
- package/dist/editor/fields/DateTimeField.js +2 -2
- package/dist/editor/fields/FieldDescription.d.ts +16 -0
- package/dist/editor/fields/FieldDescription.js +11 -0
- package/dist/editor/fields/ImageField.d.ts +1 -0
- package/dist/editor/fields/ImageField.js +3 -2
- package/dist/editor/fields/InlineGroupField.js +4 -1
- package/dist/editor/fields/MarkdownField.d.ts +1 -0
- package/dist/editor/fields/MarkdownField.js +115 -20
- package/dist/editor/fields/NumberField.d.ts +1 -0
- package/dist/editor/fields/NumberField.js +2 -2
- package/dist/editor/fields/NumberListField.d.ts +1 -0
- package/dist/editor/fields/NumberListField.js +2 -2
- package/dist/editor/fields/ObjectField.d.ts +1 -0
- package/dist/editor/fields/ObjectField.js +5 -2
- package/dist/editor/fields/ReferenceField.d.ts +1 -0
- package/dist/editor/fields/ReferenceField.js +5 -4
- package/dist/editor/fields/SelectField.d.ts +1 -0
- package/dist/editor/fields/SelectField.js +2 -2
- package/dist/editor/fields/StringListField.d.ts +1 -0
- package/dist/editor/fields/StringListField.js +2 -2
- package/dist/editor/fields/TextField.d.ts +1 -0
- package/dist/editor/fields/TextField.js +2 -2
- package/dist/editor/fields/ToggleField.d.ts +1 -0
- package/dist/editor/fields/ToggleField.js +7 -4
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
- package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
- package/dist/editor/fields/mdx-jsx-support.js +137 -0
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +52 -0
- package/dist/editor/hooks/useBranchActions.d.ts +13 -2
- package/dist/editor/hooks/useBranchActions.js +48 -17
- package/dist/editor/hooks/useBranchManager.d.ts +11 -0
- package/dist/editor/hooks/useBranchManager.js +80 -2
- package/dist/editor/hooks/useBranchesData.d.ts +6 -0
- package/dist/editor/hooks/useBranchesData.js +9 -3
- package/dist/editor/hooks/useCommentSystem.js +25 -6
- package/dist/editor/hooks/useDraftManager.d.ts +16 -1
- package/dist/editor/hooks/useDraftManager.js +193 -36
- package/dist/editor/hooks/useEntriesData.js +3 -3
- package/dist/editor/hooks/useEntryManager.d.ts +2 -1
- package/dist/editor/hooks/useEntryManager.js +34 -19
- package/dist/editor/media/AssetCard.d.ts +1 -1
- package/dist/editor/media/crop-math.d.ts +3 -4
- package/dist/editor/media/crop-math.js +11 -22
- package/dist/editor/media/editor-image-src.d.ts +7 -0
- package/dist/editor/media/editor-image-src.js +12 -0
- package/dist/editor/preview-asset-base.d.ts +8 -0
- package/dist/editor/preview-asset-base.js +13 -0
- package/dist/editor/preview-bridge.d.ts +8 -1
- package/dist/editor/preview-bridge.js +15 -2
- package/dist/editor/preview-path.d.ts +10 -0
- package/dist/editor/preview-path.js +20 -0
- package/dist/editor/theme.js +2 -2
- package/dist/entry-schema-registry.d.ts +4 -4
- package/dist/entry-schema-registry.js +10 -6
- package/dist/entry-schema.d.ts +27 -4
- package/dist/entry-schema.js +20 -3
- package/dist/git-manager.d.ts +173 -6
- package/dist/git-manager.js +594 -76
- package/dist/github-service.d.ts +8 -0
- package/dist/github-service.js +5 -1
- package/dist/http/handler.js +37 -13
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +2 -0
- package/dist/http/router.d.ts +2 -0
- package/dist/http/router.js +2 -1
- package/dist/http/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +10 -0
- package/dist/http/worker-not-ready.js +24 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +1 -4
- package/dist/paths/branch-name.d.ts +5 -0
- package/dist/paths/branch-name.js +9 -0
- package/dist/paths/branch.d.ts +6 -2
- package/dist/paths/branch.js +16 -10
- package/dist/paths/index.d.ts +3 -3
- package/dist/paths/index.js +3 -3
- package/dist/paths/normalize.d.ts +7 -0
- package/dist/paths/normalize.js +9 -0
- package/dist/paths/validation.js +0 -2
- package/dist/reference-resolver.d.ts +5 -21
- package/dist/reference-resolver.js +9 -44
- package/dist/resolve-canopy-user.js +2 -1
- package/dist/schema/schema-store.d.ts +54 -31
- package/dist/schema/schema-store.js +77 -31
- package/dist/server.d.ts +24 -9
- package/dist/server.js +22 -8
- package/dist/services.d.ts +18 -6
- package/dist/services.js +85 -70
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +197 -45
- package/dist/static/seo.d.ts +2 -14
- package/dist/static/seo.js +2 -25
- package/dist/submission-attribution.d.ts +73 -0
- package/dist/submission-attribution.js +221 -0
- package/dist/sync-core.d.ts +12 -1
- package/dist/sync-core.js +31 -13
- package/dist/task-queue/worker-status.d.ts +8 -0
- package/dist/task-queue/worker-status.js +17 -0
- package/dist/types.d.ts +31 -2
- package/dist/utils/content-write-lock.d.ts +18 -7
- package/dist/utils/content-write-lock.js +18 -9
- package/dist/utils/debug.d.ts +8 -0
- package/dist/utils/debug.js +10 -2
- package/dist/utils/git.d.ts +32 -0
- package/dist/utils/git.js +42 -0
- package/dist/utils/occ-json-write.js +10 -2
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +35 -5
- package/dist/utils/provisioning-lock.js +73 -12
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/sanitize-href.d.ts +8 -22
- package/dist/utils/sanitize-href.js +11 -28
- package/dist/utils/url-prefix.d.ts +30 -0
- package/dist/utils/url-prefix.js +61 -0
- package/dist/validation/entry-validator.js +9 -3
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/worker/canopy-state.d.ts +45 -0
- package/dist/worker/canopy-state.js +75 -0
- package/dist/worker/cms-worker.d.ts +8 -3
- package/dist/worker/cms-worker.js +38 -2
- package/dist/worker/git-sync.d.ts +30 -12
- package/dist/worker/git-sync.js +253 -58
- package/dist/worker/history-rewrite.d.ts +1 -1
- package/dist/worker/history-rewrite.js +1 -1
- package/dist/worker/provisioned-workspace.d.ts +35 -0
- package/dist/worker/provisioned-workspace.js +50 -0
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +95 -23
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +11 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +132 -0
- package/dist/worker/task-runner.js +39 -4
- package/dist/worker/worker-context.d.ts +6 -1
- package/package.json +2 -1
|
@@ -63,6 +63,9 @@ const parsePersistedDrafts = (raw) => {
|
|
|
63
63
|
baseVersions[id] = null;
|
|
64
64
|
return { drafts, baseVersions };
|
|
65
65
|
};
|
|
66
|
+
const VERIFY_CONCURRENCY = 4;
|
|
67
|
+
/** How long `whenDraftsVerified` waits before answering with whatever is known. */
|
|
68
|
+
const VERIFY_WAIT_CAP_MS = 3000;
|
|
66
69
|
/**
|
|
67
70
|
* Custom hook for managing draft state (localStorage persistence, save/discard).
|
|
68
71
|
*/
|
|
@@ -103,40 +106,79 @@ export function useDraftManager(options) {
|
|
|
103
106
|
const loadedValue = currentId ? loadedValues[currentId] : undefined;
|
|
104
107
|
const effectiveValue = selectedValue ?? loadedValue;
|
|
105
108
|
const fieldErrors = useMemo(() => (errorState && errorState.entryId === currentId ? errorState.errors : {}), [errorState, currentId]);
|
|
106
|
-
//
|
|
109
|
+
// Server values read for restored drafts of unopened entries that differ from
|
|
110
|
+
// the draft. Not `loadedValues`: that map gates the entry load and OCC token.
|
|
111
|
+
const [verifiedBaselines, setVerifiedBaselines] = useState({});
|
|
112
|
+
// Ids whose verification read failed: still dirty, but no longer awaited.
|
|
113
|
+
const [unreadableIds, setUnreadableIds] = useState(() => new Set());
|
|
114
|
+
const requestedIdsRef = useRef(new Set());
|
|
115
|
+
const verifyQueueRef = useRef([]);
|
|
116
|
+
const verifyActiveRef = useRef(0);
|
|
117
|
+
const verifyWaitersRef = useRef([]);
|
|
118
|
+
const entryById = useMemo(() => new Map(options.entries.map((e) => [e.contentId, e])), [options.entries]);
|
|
119
|
+
// Empty while a branch switch loads, so only a non-empty list can say an entry is gone.
|
|
120
|
+
const entriesKnown = options.entries.length > 0;
|
|
121
|
+
// The draft ids that count as unsaved work:
|
|
107
122
|
//
|
|
108
|
-
//
|
|
123
|
+
// 1. A draft with no baseline (loaded or verified value) counts as dirty: it
|
|
124
|
+
// cannot be proven to match the server, and counting it keeps the
|
|
125
|
+
// branch-switch guard from silently discarding restored drafts.
|
|
126
|
+
// Verification (below) removes the ones that turn out pristine.
|
|
109
127
|
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
// cannot prove such a draft matches server state, so we conservatively treat it
|
|
113
|
-
// as unsaved work — this is what keeps the branch-switch guard from silently
|
|
114
|
-
// discarding restored drafts.
|
|
128
|
+
// 2. A draft for an entry that no longer exists can never be loaded or saved,
|
|
129
|
+
// so it is not counted. It stays in storage.
|
|
115
130
|
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
// 3. The comparison is `fast-deep-equal`, so key insertion order of a
|
|
132
|
+
// rehydrated draft does not make it dirty.
|
|
133
|
+
const dirtyIds = useMemo(() => Object.keys(drafts).filter((id) => {
|
|
134
|
+
if (entriesKnown && !entryById.has(id))
|
|
135
|
+
return false;
|
|
136
|
+
const baseline = loadedValues[id] ?? verifiedBaselines[id];
|
|
137
|
+
return baseline === undefined || !equal(drafts[id], baseline);
|
|
138
|
+
}), [drafts, loadedValues, verifiedBaselines, entriesKnown, entryById]);
|
|
139
|
+
const modifiedCount = dirtyIds.length;
|
|
140
|
+
const editedFiles = useMemo(() => dirtyIds
|
|
141
|
+
.map((id) => {
|
|
142
|
+
const entry = entryById.get(id);
|
|
143
|
+
return entry ? { path: entry.path, label: entry.label } : null;
|
|
144
|
+
})
|
|
145
|
+
.filter((x) => x !== null), [dirtyIds, entryById]);
|
|
146
|
+
// Draft ids verification can still settle.
|
|
147
|
+
const unresolvedIds = useMemo(() => options.readEntryValue && entriesKnown
|
|
148
|
+
? Object.keys(drafts).filter((id) => entryById.has(id) &&
|
|
149
|
+
loadedValues[id] === undefined &&
|
|
150
|
+
verifiedBaselines[id] === undefined &&
|
|
151
|
+
!unreadableIds.has(id))
|
|
152
|
+
: [], [
|
|
153
|
+
drafts,
|
|
154
|
+
loadedValues,
|
|
155
|
+
verifiedBaselines,
|
|
156
|
+
unreadableIds,
|
|
157
|
+
entriesKnown,
|
|
158
|
+
entryById,
|
|
159
|
+
options.readEntryValue,
|
|
160
|
+
]);
|
|
161
|
+
// Async callbacks outlive the render that created them; these give them current values.
|
|
162
|
+
const latestRef = useRef({ drafts, loadedValues, unresolvedCount: 0, modifiedCount, editedFiles });
|
|
163
|
+
latestRef.current = {
|
|
164
|
+
drafts,
|
|
165
|
+
loadedValues,
|
|
166
|
+
unresolvedCount: unresolvedIds.length,
|
|
167
|
+
modifiedCount,
|
|
168
|
+
editedFiles,
|
|
169
|
+
};
|
|
170
|
+
const latestOptionsRef = useRef(options);
|
|
171
|
+
latestOptionsRef.current = options;
|
|
134
172
|
// Clear drafts when branch changes (before localStorage restore)
|
|
135
173
|
const prevBranchRef = useRef(options.branchName);
|
|
136
174
|
useEffect(() => {
|
|
137
175
|
if (prevBranchRef.current && prevBranchRef.current !== options.branchName) {
|
|
138
176
|
setDrafts({});
|
|
139
177
|
setLoadedValues({});
|
|
178
|
+
setVerifiedBaselines({});
|
|
179
|
+
setUnreadableIds(new Set());
|
|
180
|
+
requestedIdsRef.current = new Set();
|
|
181
|
+
verifyQueueRef.current = [];
|
|
140
182
|
// Base versions are file mtimes, i.e. inherently per-branch -- carrying
|
|
141
183
|
// them across a switch would compare one branch's version against
|
|
142
184
|
// another's.
|
|
@@ -261,6 +303,102 @@ export function useDraftManager(options) {
|
|
|
261
303
|
console.warn('Failed to persist drafts', err);
|
|
262
304
|
}
|
|
263
305
|
}, [drafts, storageKey]);
|
|
306
|
+
// A draft equal to its entry's loaded value is not an edit. Drops it so it
|
|
307
|
+
// neither counts as dirty nor lingers in storage.
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
const redundant = Object.keys(drafts).filter((id) => loadedValues[id] !== undefined && equal(drafts[id], loadedValues[id]));
|
|
310
|
+
if (redundant.length === 0)
|
|
311
|
+
return;
|
|
312
|
+
setDrafts((prev) => {
|
|
313
|
+
const next = { ...prev };
|
|
314
|
+
let changed = false;
|
|
315
|
+
for (const id of redundant) {
|
|
316
|
+
if (id in next && equal(next[id], loadedValues[id])) {
|
|
317
|
+
delete next[id];
|
|
318
|
+
changed = true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return changed ? next : prev;
|
|
322
|
+
});
|
|
323
|
+
}, [drafts, loadedValues]);
|
|
324
|
+
// Checks one restored draft against the server. The branch is re-read at
|
|
325
|
+
// settle time: a switch mid-read makes the result another branch's value.
|
|
326
|
+
const verifyDraft = async (job) => {
|
|
327
|
+
const id = job.entry.contentId;
|
|
328
|
+
const read = latestOptionsRef.current.readEntryValue;
|
|
329
|
+
if (!read)
|
|
330
|
+
return;
|
|
331
|
+
try {
|
|
332
|
+
const server = await read(job.entry);
|
|
333
|
+
if (latestOptionsRef.current.branchName !== job.branch)
|
|
334
|
+
return;
|
|
335
|
+
const { drafts: latestDrafts, loadedValues: latestLoaded } = latestRef.current;
|
|
336
|
+
// Gone, or the entry's own load has taken over as the baseline.
|
|
337
|
+
if (latestDrafts[id] === undefined || latestLoaded[id] !== undefined)
|
|
338
|
+
return;
|
|
339
|
+
if (equal(latestDrafts[id], server)) {
|
|
340
|
+
setDrafts((prev) => {
|
|
341
|
+
if (!(id in prev) || !equal(prev[id], server))
|
|
342
|
+
return prev;
|
|
343
|
+
const next = { ...prev };
|
|
344
|
+
delete next[id];
|
|
345
|
+
return next;
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
setVerifiedBaselines((prev) => ({ ...prev, [id]: server }));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// Stays unverified, so the draft keeps counting as dirty.
|
|
354
|
+
if (latestOptionsRef.current.branchName !== job.branch)
|
|
355
|
+
return;
|
|
356
|
+
setUnreadableIds((prev) => new Set(prev).add(id));
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
const pumpVerification = () => {
|
|
360
|
+
while (verifyActiveRef.current < VERIFY_CONCURRENCY && verifyQueueRef.current.length > 0) {
|
|
361
|
+
const job = verifyQueueRef.current.shift();
|
|
362
|
+
if (!job)
|
|
363
|
+
break;
|
|
364
|
+
verifyActiveRef.current++;
|
|
365
|
+
void verifyDraft(job).finally(() => {
|
|
366
|
+
verifyActiveRef.current--;
|
|
367
|
+
pumpVerification();
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
// Once per branch session, reads the server value of every restored draft
|
|
372
|
+
// whose entry has not loaded. The selected entry is left to its own load,
|
|
373
|
+
// which fills `loadedValues` and lets the effect above drop a pristine draft.
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
if (!options.readEntryValue || !entriesKnown)
|
|
376
|
+
return;
|
|
377
|
+
// `drafts` lags a branch switch by a render (see draftsStorageKeyRef); reading
|
|
378
|
+
// the old branch's drafts under the new branch's name would be wrong.
|
|
379
|
+
if (draftsStorageKeyRef.current !== storageKey)
|
|
380
|
+
return;
|
|
381
|
+
const todo = unresolvedIds.filter((id) => id !== currentId && !requestedIdsRef.current.has(id));
|
|
382
|
+
if (todo.length === 0)
|
|
383
|
+
return;
|
|
384
|
+
for (const id of todo) {
|
|
385
|
+
requestedIdsRef.current.add(id);
|
|
386
|
+
const entry = entryById.get(id);
|
|
387
|
+
if (entry)
|
|
388
|
+
verifyQueueRef.current.push({ entry, branch: options.branchName });
|
|
389
|
+
}
|
|
390
|
+
pumpVerification();
|
|
391
|
+
// pumpVerification/verifyDraft only touch refs and stable setters.
|
|
392
|
+
}, [unresolvedIds, currentId, storageKey]);
|
|
393
|
+
// Wakes `whenDraftsVerified` callers once nothing is left to settle.
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
if (unresolvedIds.length > 0)
|
|
396
|
+
return;
|
|
397
|
+
const waiters = verifyWaitersRef.current;
|
|
398
|
+
verifyWaitersRef.current = [];
|
|
399
|
+
for (const wake of waiters)
|
|
400
|
+
wake();
|
|
401
|
+
}, [unresolvedIds]);
|
|
264
402
|
// Recomputes field errors as the user edits, or as the selected entry's
|
|
265
403
|
// schema/format changes while it stays open, so each error clears when its
|
|
266
404
|
// field is fixed or no longer required. Errors for a different entry are
|
|
@@ -316,7 +454,8 @@ export function useDraftManager(options) {
|
|
|
316
454
|
return 'ok';
|
|
317
455
|
const currentVersion = options.getEntryVersion?.(currentId);
|
|
318
456
|
// No server version known for this entry at all -- nothing to compare
|
|
319
|
-
// against, so this check has no opinion
|
|
457
|
+
// against, so this check has no opinion: saveEntry refuses a save without
|
|
458
|
+
// a token, and the server refuses a version-less update.
|
|
320
459
|
if (currentVersion === undefined)
|
|
321
460
|
return 'ok';
|
|
322
461
|
const base = draftBaseVersionsRef.current[currentId];
|
|
@@ -406,9 +545,8 @@ export function useDraftManager(options) {
|
|
|
406
545
|
// to become `saved` below, so removing the draft key is a no-op for
|
|
407
546
|
// the rendered value while fixing the "phantom dirty" bug: a draft
|
|
408
547
|
// that lingers forever is what made every fresh page load show Save
|
|
409
|
-
// enabled with zero real edits (see
|
|
410
|
-
//
|
|
411
|
-
// treated as dirty).
|
|
548
|
+
// enabled with zero real edits (see `dirtyIds` above: a draft with no
|
|
549
|
+
// baseline is conservatively treated as dirty).
|
|
412
550
|
setDrafts((prev) => {
|
|
413
551
|
if (!(currentId in prev))
|
|
414
552
|
return prev;
|
|
@@ -627,7 +765,8 @@ export function useDraftManager(options) {
|
|
|
627
765
|
const id = entry.contentId;
|
|
628
766
|
if (!drafts[id])
|
|
629
767
|
return false;
|
|
630
|
-
|
|
768
|
+
const baseline = loadedValues[id] ?? verifiedBaselines[id];
|
|
769
|
+
return baseline === undefined || !equal(drafts[id], baseline);
|
|
631
770
|
};
|
|
632
771
|
const isSelectedDirty = () => {
|
|
633
772
|
if (!currentId)
|
|
@@ -636,13 +775,29 @@ export function useDraftManager(options) {
|
|
|
636
775
|
return false;
|
|
637
776
|
return !loadedValues[currentId] || !equal(drafts[currentId], loadedValues[currentId]);
|
|
638
777
|
};
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
// Used for branch-switch guards so unsaved work in non-selected entries is not
|
|
642
|
-
// silently discarded. Derived from `modifiedCount`, so its semantics note
|
|
643
|
-
// above also applies: localStorage-restored drafts without a loaded value
|
|
644
|
-
// count as dirty.
|
|
778
|
+
// True if ANY counted draft is dirty (see `dirtyIds`). Used for branch-level
|
|
779
|
+
// guards so unsaved work in non-selected entries is not silently discarded.
|
|
645
780
|
const isAnyDirty = () => modifiedCount > 0;
|
|
781
|
+
const whenDraftsVerified = () => {
|
|
782
|
+
if (latestRef.current.unresolvedCount === 0)
|
|
783
|
+
return Promise.resolve();
|
|
784
|
+
return new Promise((resolve) => {
|
|
785
|
+
const wake = () => {
|
|
786
|
+
clearTimeout(timer);
|
|
787
|
+
resolve();
|
|
788
|
+
};
|
|
789
|
+
const timer = setTimeout(() => {
|
|
790
|
+
verifyWaitersRef.current = verifyWaitersRef.current.filter((w) => w !== wake);
|
|
791
|
+
resolve();
|
|
792
|
+
}, VERIFY_WAIT_CAP_MS);
|
|
793
|
+
verifyWaitersRef.current.push(wake);
|
|
794
|
+
});
|
|
795
|
+
};
|
|
796
|
+
const resolveUnsaved = async () => {
|
|
797
|
+
await whenDraftsVerified();
|
|
798
|
+
const { modifiedCount: count, editedFiles: files } = latestRef.current;
|
|
799
|
+
return { count, labels: files.map((f) => f.label) };
|
|
800
|
+
};
|
|
646
801
|
return {
|
|
647
802
|
drafts,
|
|
648
803
|
setDrafts,
|
|
@@ -660,6 +815,8 @@ export function useDraftManager(options) {
|
|
|
660
815
|
isDirtyForEntry,
|
|
661
816
|
isSelectedDirty,
|
|
662
817
|
isAnyDirty,
|
|
818
|
+
whenDraftsVerified,
|
|
819
|
+
resolveUnsaved,
|
|
663
820
|
fieldErrors,
|
|
664
821
|
};
|
|
665
822
|
}
|
|
@@ -40,7 +40,7 @@ export async function listAllEntries(apiClient, branch) {
|
|
|
40
40
|
...(cursor !== undefined ? { cursor } : {}),
|
|
41
41
|
});
|
|
42
42
|
if (!result.ok || !result.data)
|
|
43
|
-
throw new Error(`Refresh failed: ${result.status}`);
|
|
43
|
+
throw new Error(`Refresh failed: ${result.status}${result.error ? ` — ${result.error}` : ''}`);
|
|
44
44
|
const data = result.data;
|
|
45
45
|
for (const entry of data.entries)
|
|
46
46
|
byPath.set(entry.logicalPath, entry);
|
|
@@ -60,7 +60,7 @@ export async function fetchEntriesAndSchema(apiClient, branch, params) {
|
|
|
60
60
|
// Fetch schema from schema API
|
|
61
61
|
const schemaResult = await apiClient.schema.get({ branch });
|
|
62
62
|
if (!schemaResult.ok || !schemaResult.data) {
|
|
63
|
-
throw new Error(`Schema fetch failed: ${schemaResult.status}`);
|
|
63
|
+
throw new Error(`Schema fetch failed: ${schemaResult.status}${schemaResult.error ? ` — ${schemaResult.error}` : ''}`);
|
|
64
64
|
}
|
|
65
65
|
// Hydrate wire flatSchema: resolve schemaRef -> schema from entrySchemas dict
|
|
66
66
|
const { entrySchemas } = schemaResult.data;
|
|
@@ -85,7 +85,7 @@ export async function fetchEntriesAndSchema(apiClient, branch, params) {
|
|
|
85
85
|
entries: allEntries,
|
|
86
86
|
pagination: { hasMore: false, limit: ENTRIES_PAGE_LIMIT },
|
|
87
87
|
},
|
|
88
|
-
resolvePreviewSrc:
|
|
88
|
+
resolvePreviewSrc: params.resolvePreviewSrc,
|
|
89
89
|
flatSchema: hydratedFlatSchema,
|
|
90
90
|
});
|
|
91
91
|
return { collections, entries, availableSchemas: Object.keys(entrySchemas) };
|
|
@@ -20,7 +20,6 @@ export interface UseEntryManagerOptions {
|
|
|
20
20
|
initialSelectedId?: string;
|
|
21
21
|
branchName: string;
|
|
22
22
|
collections?: EditorCollection[];
|
|
23
|
-
previewBaseByCollection?: Record<string, string>;
|
|
24
23
|
resolvePreviewSrc: (entry: Partial<EditorEntry>) => string | undefined;
|
|
25
24
|
setBusy: (busy: boolean) => void;
|
|
26
25
|
}
|
|
@@ -46,6 +45,8 @@ export interface UseEntryManagerReturn {
|
|
|
46
45
|
handleCreateEntry: (collectionPath: LogicalPath, entryTypeName?: string) => Promise<void>;
|
|
47
46
|
renameEntry: (path: string, newSlug: string) => Promise<void>;
|
|
48
47
|
loadEntry: (entry: EditorEntry) => Promise<FormValue>;
|
|
48
|
+
/** Reads an entry's server value without recording the OCC token that `loadEntry` records. */
|
|
49
|
+
readEntryValue: (entry: EditorEntry) => Promise<FormValue>;
|
|
49
50
|
saveEntry: (entry: EditorEntry, value: FormValue) => Promise<FormValue>;
|
|
50
51
|
/**
|
|
51
52
|
* The OCC version token currently held for `contentId` ON THE BRANCH BEING
|
|
@@ -50,11 +50,9 @@ export function useEntryManager(options) {
|
|
|
50
50
|
// case -- any first visit to a branch, for the whole duration of its fetch),
|
|
51
51
|
// or a stale SWR slot. The editor then auto-selected one of those stale
|
|
52
52
|
// entries (see the selection effect below), and a save could file its OCC
|
|
53
|
-
// token under one contentId and look it up under another
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
// Deriving from a stamped record fixes that structurally instead of relying
|
|
57
|
-
// on every code path remembering to clear.
|
|
53
|
+
// token under one contentId and look it up under another. Deriving from a
|
|
54
|
+
// stamped record fixes that structurally instead of relying on every code
|
|
55
|
+
// path remembering to clear.
|
|
58
56
|
const [view, setView] = useState(() => ({
|
|
59
57
|
branch: options.branchName,
|
|
60
58
|
entries: options.initialEntries,
|
|
@@ -86,6 +84,9 @@ export function useEntryManager(options) {
|
|
|
86
84
|
// branch can repopulate the map after the branch-change clear() below and
|
|
87
85
|
// poison the next save with the old branch's mtime — a deterministic 409
|
|
88
86
|
// ("modified by another editor") on save-after-switch, proven by e2e trace.
|
|
87
|
+
// The contentId half matches how useDraftManager keys drafts and loaded
|
|
88
|
+
// values, so a form value and its token always belong to the same file even
|
|
89
|
+
// when a path is deleted and recreated; it also survives a rename.
|
|
89
90
|
const entryVersionsRef = useRef(new Map());
|
|
90
91
|
const versionKey = (branch, contentId) => `${branch}:${contentId}`;
|
|
91
92
|
// PER-BRANCH monotonic tokens guarding every commit of the fetched
|
|
@@ -177,14 +178,11 @@ export function useEntryManager(options) {
|
|
|
177
178
|
.filter((e) => e.collectionPath === createModalCollection.path && e.slug)
|
|
178
179
|
.map((e) => e.slug));
|
|
179
180
|
}, [entriesState, createModalCollection]);
|
|
180
|
-
|
|
181
|
+
// The read both entry paths share; returns the raw payload so `loadEntry` can take its version.
|
|
182
|
+
const readEntryPayload = async (entry, requestBranch) => {
|
|
181
183
|
if (!entry.collectionPath) {
|
|
182
184
|
throw new Error('Entry missing collectionPath');
|
|
183
185
|
}
|
|
184
|
-
// Pin the branch this request targets: if the user switches branches
|
|
185
|
-
// while the read is in flight, the token must be recorded under the
|
|
186
|
-
// branch that actually served it, not the current one.
|
|
187
|
-
const requestBranch = options.branchName;
|
|
188
186
|
// Build path from collectionPath and slug (if it's a collection entry)
|
|
189
187
|
const path = entry.slug ? `${entry.collectionPath}/${entry.slug}` : entry.collectionPath;
|
|
190
188
|
const result = await apiClient.content.read({
|
|
@@ -192,13 +190,24 @@ export function useEntryManager(options) {
|
|
|
192
190
|
path,
|
|
193
191
|
});
|
|
194
192
|
if (!result.ok)
|
|
195
|
-
throw new Error(`Load failed: ${result.status}`);
|
|
193
|
+
throw new Error(`Load failed: ${result.status}${result.error ? ` — ${result.error}` : ''}`);
|
|
194
|
+
return result.data;
|
|
195
|
+
};
|
|
196
|
+
const loadEntry = async (entry) => {
|
|
197
|
+
// Pin the branch this request targets: if the user switches branches
|
|
198
|
+
// while the read is in flight, the token must be recorded under the
|
|
199
|
+
// branch that actually served it, not the current one.
|
|
200
|
+
const requestBranch = options.branchName;
|
|
201
|
+
const data = await readEntryPayload(entry, requestBranch);
|
|
196
202
|
// Capture OCC version token for next save
|
|
197
|
-
if (entry.contentId && typeof
|
|
198
|
-
entryVersionsRef.current.set(versionKey(requestBranch, entry.contentId),
|
|
203
|
+
if (entry.contentId && typeof data?.version === 'number') {
|
|
204
|
+
entryVersionsRef.current.set(versionKey(requestBranch, entry.contentId), data.version);
|
|
199
205
|
}
|
|
200
|
-
return normalizeContentPayload(
|
|
206
|
+
return normalizeContentPayload(data);
|
|
201
207
|
};
|
|
208
|
+
// The same read as `loadEntry`, minus the token capture. A value read this way can be
|
|
209
|
+
// compared against but never saved against: only `loadEntry` makes an entry saveable.
|
|
210
|
+
const readEntryValue = async (entry) => normalizeContentPayload(await readEntryPayload(entry, options.branchName));
|
|
202
211
|
// Read side of the same branch-qualified token map `saveEntry` writes/reads.
|
|
203
212
|
// Deliberately keyed off the CURRENT branch (not a pinned one): callers use
|
|
204
213
|
// it to reason about the entry they are looking at right now.
|
|
@@ -220,18 +229,23 @@ export function useEntryManager(options) {
|
|
|
220
229
|
};
|
|
221
230
|
if (entry.entryType)
|
|
222
231
|
writeParams.entryType = entry.entryType;
|
|
223
|
-
const expectedVersion = entry.contentId
|
|
224
|
-
|
|
225
|
-
|
|
232
|
+
const expectedVersion = entryVersionsRef.current.get(versionKey(requestBranch, entry.contentId));
|
|
233
|
+
// Every entry saved here already exists (creates go through handleCreateModalSubmit), so
|
|
234
|
+
// no token means this entry was never successfully read on this branch and nothing could check the save
|
|
235
|
+
// against other editors' work. Refused as a conflict; the server refuses it too.
|
|
236
|
+
if (expectedVersion === undefined) {
|
|
237
|
+
throw new SaveApiError(409, 'This entry has not been loaded from the server, so the save cannot be checked ' +
|
|
238
|
+
"against other editors' changes. Reload it and try again.");
|
|
239
|
+
}
|
|
226
240
|
const writeBody = {
|
|
227
241
|
...payload,
|
|
228
|
-
|
|
242
|
+
expectedVersion,
|
|
229
243
|
};
|
|
230
244
|
const result = await apiClient.content.write(writeParams, writeBody);
|
|
231
245
|
if (!result.ok)
|
|
232
246
|
throw new SaveApiError(result.status, result.error, result.fieldErrors);
|
|
233
247
|
// Update stored version token from write response
|
|
234
|
-
if (
|
|
248
|
+
if (typeof result.data?.version === 'number') {
|
|
235
249
|
entryVersionsRef.current.set(versionKey(requestBranch, entry.contentId), result.data.version);
|
|
236
250
|
}
|
|
237
251
|
// Warning-level issues from the adopter's validateEntry hook: saved, but surface them
|
|
@@ -558,6 +572,7 @@ export function useEntryManager(options) {
|
|
|
558
572
|
handleCreateEntry,
|
|
559
573
|
renameEntry,
|
|
560
574
|
loadEntry,
|
|
575
|
+
readEntryValue,
|
|
561
576
|
saveEntry,
|
|
562
577
|
getEntryVersion,
|
|
563
578
|
collectionByPath: collectionByPath,
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import type { AssetRecord } from '../../api/index.js';
|
|
3
3
|
export interface AssetCardProps {
|
|
4
4
|
asset: AssetRecord;
|
|
5
|
-
/** `
|
|
5
|
+
/** `useAssetContext().baseUrl`, the authenticated raw route - see AssetContext. */
|
|
6
6
|
baseUrl?: string;
|
|
7
7
|
/** Present in picker mode - clicking the card selects the asset. */
|
|
8
8
|
onSelect?: (asset: AssetRecord) => void;
|
|
@@ -16,10 +16,9 @@ export interface CropAreaPercent {
|
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
18
|
* Convert react-easy-crop's `onCropComplete(croppedArea, croppedAreaPixels)`
|
|
19
|
-
* percentage area into a normalized `CropRect`,
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* space rather than reject a rect that was valid before rounding.
|
|
19
|
+
* percentage area into a normalized `CropRect`, clamped to [0,1] and then
|
|
20
|
+
* rounded by `roundCropRect` — the same rounding the transform URL's canonical
|
|
21
|
+
* form applies, so the stored crop is already canonical.
|
|
23
22
|
*
|
|
24
23
|
* Returns `null` if the input can't be coerced into a valid rect (e.g. a
|
|
25
24
|
* zero-area selection).
|
|
@@ -6,13 +6,7 @@
|
|
|
6
6
|
* (no react-easy-crop import) so it's trivially unit-testable and reusable
|
|
7
7
|
* without pulling in the cropper UI library.
|
|
8
8
|
*/
|
|
9
|
-
import { isValidCropRect } from '../../assets/transform-directives.js';
|
|
10
|
-
/** Matches transform-directives.ts's CROP_PRECISION so a crop rect round-trips through the URL directive unchanged. */
|
|
11
|
-
const CROP_PRECISION = 4;
|
|
12
|
-
function roundTo(value, decimals) {
|
|
13
|
-
const factor = 10 ** decimals;
|
|
14
|
-
return Math.round(value * factor) / factor;
|
|
15
|
-
}
|
|
9
|
+
import { isValidCropRect, roundCropRect } from '../../assets/transform-directives.js';
|
|
16
10
|
function clamp01(value) {
|
|
17
11
|
if (!Number.isFinite(value))
|
|
18
12
|
return 0;
|
|
@@ -20,26 +14,21 @@ function clamp01(value) {
|
|
|
20
14
|
}
|
|
21
15
|
/**
|
|
22
16
|
* Convert react-easy-crop's `onCropComplete(croppedArea, croppedAreaPixels)`
|
|
23
|
-
* percentage area into a normalized `CropRect`,
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* space rather than reject a rect that was valid before rounding.
|
|
17
|
+
* percentage area into a normalized `CropRect`, clamped to [0,1] and then
|
|
18
|
+
* rounded by `roundCropRect` — the same rounding the transform URL's canonical
|
|
19
|
+
* form applies, so the stored crop is already canonical.
|
|
27
20
|
*
|
|
28
21
|
* Returns `null` if the input can't be coerced into a valid rect (e.g. a
|
|
29
22
|
* zero-area selection).
|
|
30
23
|
*/
|
|
31
24
|
export function cropAreaPercentToRect(area) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
h = roundTo(1 - y, CROP_PRECISION);
|
|
40
|
-
if (!isValidCropRect(x, y, w, h))
|
|
41
|
-
return null;
|
|
42
|
-
return { x, y, w, h };
|
|
25
|
+
const rect = roundCropRect({
|
|
26
|
+
x: clamp01(area.x / 100),
|
|
27
|
+
y: clamp01(area.y / 100),
|
|
28
|
+
w: clamp01(area.width / 100),
|
|
29
|
+
h: clamp01(area.height / 100),
|
|
30
|
+
});
|
|
31
|
+
return isValidCropRect(rect.x, rect.y, rect.w, rect.h) ? rect : null;
|
|
43
32
|
}
|
|
44
33
|
/**
|
|
45
34
|
* Inverse of `cropAreaPercentToRect` - seeds react-easy-crop's
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The URL the editor displays a content image at. A src in canopycms's own `/assets` space is put
|
|
3
|
+
* behind `baseUrl` (the authenticated raw route, see `AssetContext`); any other src - a site's own
|
|
4
|
+
* static file, an off-site URL, a `data:` URI - is shown exactly as written, because the raw route
|
|
5
|
+
* serves only the asset store.
|
|
6
|
+
*/
|
|
7
|
+
export declare function editorImageSrc(src: string, baseUrl: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { assetUrl } from '../../assets/asset-url.js';
|
|
2
|
+
import { ASSET_PREFIXES } from '../../assets/asset-prefixes.js';
|
|
3
|
+
const ASSET_SPACE = `/${ASSET_PREFIXES.public}/`;
|
|
4
|
+
/**
|
|
5
|
+
* The URL the editor displays a content image at. A src in canopycms's own `/assets` space is put
|
|
6
|
+
* behind `baseUrl` (the authenticated raw route, see `AssetContext`); any other src - a site's own
|
|
7
|
+
* static file, an off-site URL, a `data:` URI - is shown exactly as written, because the raw route
|
|
8
|
+
* serves only the asset store.
|
|
9
|
+
*/
|
|
10
|
+
export function editorImageSrc(src, baseUrl) {
|
|
11
|
+
return src.startsWith(ASSET_SPACE) ? assetUrl({ src }, { baseUrl }) : src;
|
|
12
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The asset prefix a framing editor hands its live preview, held for `assets/asset-url.ts`. Its
|
|
3
|
+
* only writer outside tests is `usePreviewData`, on a trusted draft message; the read is gated on `window` too,
|
|
4
|
+
* because `assetUrl` also runs in server renders and static builds. Imports nothing.
|
|
5
|
+
*/
|
|
6
|
+
export declare function setPreviewAssetBase(base: string | undefined): void;
|
|
7
|
+
/** The stored prefix in a browser; always `undefined` on the server. */
|
|
8
|
+
export declare function getPreviewAssetBase(): string | undefined;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The asset prefix a framing editor hands its live preview, held for `assets/asset-url.ts`. Its
|
|
3
|
+
* only writer outside tests is `usePreviewData`, on a trusted draft message; the read is gated on `window` too,
|
|
4
|
+
* because `assetUrl` also runs in server renders and static builds. Imports nothing.
|
|
5
|
+
*/
|
|
6
|
+
let previewAssetBase;
|
|
7
|
+
export function setPreviewAssetBase(base) {
|
|
8
|
+
previewAssetBase = base;
|
|
9
|
+
}
|
|
10
|
+
/** The stored prefix in a browser; always `undefined` on the server. */
|
|
11
|
+
export function getPreviewAssetBase() {
|
|
12
|
+
return typeof window === 'undefined' ? undefined : previewAssetBase;
|
|
13
|
+
}
|
|
@@ -11,6 +11,8 @@ export interface DraftUpdateMessage {
|
|
|
11
11
|
path: string;
|
|
12
12
|
data?: unknown;
|
|
13
13
|
isLoading?: unknown;
|
|
14
|
+
/** The editor's authenticated asset route; see `PreviewFrame`'s `assetBase`. */
|
|
15
|
+
assetBase?: unknown;
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
18
|
* Resolve a (possibly relative) URL to an origin for postMessage targeting.
|
|
@@ -92,7 +94,7 @@ export declare const usePreviewFocusEmitter: (entryPath: string, opts?: {
|
|
|
92
94
|
* Lightweight iframe wrapper to keep the preview in sync with form state.
|
|
93
95
|
* It posts the latest draft data to the iframe after load and when data changes.
|
|
94
96
|
*/
|
|
95
|
-
export declare const PreviewFrame: ({ src, path, data, isLoading, className, style, highlightEnabled, onPreviewError, }: {
|
|
97
|
+
export declare const PreviewFrame: ({ src, path, data, isLoading, className, style, highlightEnabled, onPreviewError, assetBase, }: {
|
|
96
98
|
src: string;
|
|
97
99
|
path: string;
|
|
98
100
|
data?: unknown;
|
|
@@ -100,6 +102,11 @@ export declare const PreviewFrame: ({ src, path, data, isLoading, className, sty
|
|
|
100
102
|
className?: string;
|
|
101
103
|
style?: CSSProperties;
|
|
102
104
|
highlightEnabled?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* The editor's authenticated asset route, a same-origin path, so it is sent with drafts only to
|
|
107
|
+
* a same-origin preview: another origin would resolve it against itself.
|
|
108
|
+
*/
|
|
109
|
+
assetBase?: string;
|
|
103
110
|
/** Called when the preview reports a draft compile/render error; null clears it. */
|
|
104
111
|
onPreviewError?: (error: {
|
|
105
112
|
message: string;
|