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
package/dist/editor/Editor.js
CHANGED
|
@@ -10,6 +10,7 @@ import { PreviewFrame } from './preview-bridge.js';
|
|
|
10
10
|
import { EditorPanes } from './EditorPanes.js';
|
|
11
11
|
import { CanopyCMSProvider } from './theme.js';
|
|
12
12
|
import { BranchManager } from './BranchManager.js';
|
|
13
|
+
import { BranchesDrawer } from './components/BranchesDrawer.js';
|
|
13
14
|
import { CommentsPanel } from './CommentsPanel.js';
|
|
14
15
|
import { GroupManager } from './GroupManager.js';
|
|
15
16
|
import { PermissionManager } from './PermissionManager.js';
|
|
@@ -24,15 +25,16 @@ import { EditorFooter, EditorHeader, EditorSidebar } from './components/index.js
|
|
|
24
25
|
import { RenameEntryModal } from './components/RenameEntryModal.js';
|
|
25
26
|
import { EntryCreateModal } from './components/EntryCreateModal.js';
|
|
26
27
|
import { ConfirmDeleteModal } from './components/ConfirmDeleteModal.js';
|
|
28
|
+
import { NoEditPermissionNotice } from './components/NoEditPermissionNotice.js';
|
|
27
29
|
import { CollectionEditor } from './schema-editor/index.js';
|
|
28
|
-
import { AssetContextProvider, useApiClient } from './context/index.js';
|
|
30
|
+
import { AssetContextProvider, authenticatedAssetBase, useApiClient } from './context/index.js';
|
|
29
31
|
import { EntryLinkContext } from './fields/entry-link/index.js';
|
|
30
32
|
import { MediaLibrary } from './media/MediaLibrary.js';
|
|
31
33
|
/**
|
|
32
34
|
* High-level editor wrapper that wires entry navigation, form rendering,
|
|
33
35
|
* saving/loading, and preview rendering using entry definitions.
|
|
34
36
|
*/
|
|
35
|
-
export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtitle = subtitle, branchName = '', collections, contentRoot, entryLinkUrl, initialSelectedId, initialValues, renderPreview, onCreateEntry, themeOptions, operatingMode, previewBaseByCollection, currentUser = 'current-user', canResolveComments = true,
|
|
37
|
+
export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtitle = subtitle, branchName = '', collections, contentRoot, entryLinkUrl, initialSelectedId, initialValues, renderPreview, onCreateEntry, themeOptions, operatingMode, previewBaseByCollection, previewPrefix, currentUser = 'current-user', canResolveComments = true, basePath, AccountComponent, onAccountClick, onLogoutClick, customRenderers, }) => {
|
|
36
38
|
const [branchesLoading, setBranchesLoading] = useState(false);
|
|
37
39
|
const [entriesLoading, setEntriesLoading] = useState(false);
|
|
38
40
|
const [commentsLoading] = useState(false);
|
|
@@ -70,7 +72,7 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
70
72
|
// Comments state (shared between useCommentSystem and useBranchManager)
|
|
71
73
|
const [commentsForBranchSummaries, setCommentsForBranchSummaries] = useState([]);
|
|
72
74
|
// 1. Branch manager (provides branchNameState, no dependencies)
|
|
73
|
-
const { branchName: branchNameState, setBranchName, branchSummaries, currentBranch, handleSubmit, handleWithdraw, handleRequestChanges, handleDelete, handleReloadBranchData, loadBranches, } = useBranchManager({
|
|
75
|
+
const { branchName: branchNameState, setBranchName, branchSummaries, currentBranch, addCreatedBranch, handleSubmit, handleWithdraw, handleRequestChanges, handleDelete, handleReloadBranchData, loadBranches, } = useBranchManager({
|
|
74
76
|
initialBranch: branchName,
|
|
75
77
|
operatingMode,
|
|
76
78
|
setBusy: setBranchesLoading,
|
|
@@ -89,15 +91,15 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
89
91
|
// loading state during that window anyway.
|
|
90
92
|
const branchContentLocked = currentBranch?.writeBlocked ?? true;
|
|
91
93
|
// 2. Entry manager (depends on branchNameState, owns selectedPath)
|
|
92
|
-
const { selectedPath, setSelectedPath, entries: entriesState, collections: collectionsFromApi, currentEntry, availableSchemas, entriesInitializing, navigatorOpen, setNavigatorOpen, refreshEntries, handleCreateEntry, renameEntry, loadEntry, saveEntry, getEntryVersion, createModalOpen, createModalCollection, createModalError, createModalCreating, createModalExistingSlugs, handleCreateModalSubmit, closeCreateModal, } = useEntryManager({
|
|
94
|
+
const { selectedPath, setSelectedPath, entries: entriesState, collections: collectionsFromApi, currentEntry, availableSchemas, entriesInitializing, navigatorOpen, setNavigatorOpen, refreshEntries, handleCreateEntry, renameEntry, loadEntry, readEntryValue, saveEntry, getEntryVersion, createModalOpen, createModalCollection, createModalError, createModalCreating, createModalExistingSlugs, handleCreateModalSubmit, closeCreateModal, } = useEntryManager({
|
|
93
95
|
initialEntries: entries,
|
|
94
96
|
initialSelectedId,
|
|
95
97
|
branchName: branchNameState,
|
|
96
98
|
collections,
|
|
97
|
-
previewBaseByCollection,
|
|
98
99
|
resolvePreviewSrc: (entry) => buildPreviewSrc(entry, {
|
|
99
100
|
branchName: branchNameState,
|
|
100
101
|
previewBaseByCollection,
|
|
102
|
+
previewPrefix,
|
|
101
103
|
contentRoot,
|
|
102
104
|
basePath,
|
|
103
105
|
}),
|
|
@@ -125,13 +127,14 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
125
127
|
setPreviewLoadingState({});
|
|
126
128
|
}, [currentEntry?.contentId]);
|
|
127
129
|
// 3. Draft manager (depends on branchNameState, selectedPath from useEntryManager)
|
|
128
|
-
const { setDrafts, loadedValues, setLoadedValues, effectiveValue, modifiedCount, editedFiles, handleSave, handleDiscardDrafts, handleDiscardFileDraft, handleReload, isSelectedDirty,
|
|
130
|
+
const { setDrafts, loadedValues, setLoadedValues, effectiveValue, modifiedCount, editedFiles, handleSave, handleDiscardDrafts, handleDiscardFileDraft, handleReload, isSelectedDirty, resolveUnsaved, fieldErrors, } = useDraftManager({
|
|
129
131
|
branchName: branchNameState,
|
|
130
132
|
selectedPath,
|
|
131
133
|
currentEntry,
|
|
132
134
|
entries: entriesState,
|
|
133
135
|
initialValues,
|
|
134
136
|
loadEntry,
|
|
137
|
+
readEntryValue,
|
|
135
138
|
saveEntry,
|
|
136
139
|
getEntryVersion,
|
|
137
140
|
setBusy: setEntriesLoading,
|
|
@@ -139,12 +142,14 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
139
142
|
void refreshEntries(branchNameState).catch((err) => console.error('Failed to refresh entries after save', err));
|
|
140
143
|
},
|
|
141
144
|
});
|
|
142
|
-
// 4. Branch actions (depends on
|
|
143
|
-
const { handleBranchChange, handleCreateBranch } = useBranchActions({
|
|
145
|
+
// 4. Branch actions (depends on resolveUnsaved, setBranchName)
|
|
146
|
+
const { handleBranchChange, confirmCreate: confirmCreateBranch, handleCreateBranch, confirmOpen: branchConfirmOpen, } = useBranchActions({
|
|
144
147
|
branchName: branchNameState,
|
|
145
148
|
setBranchName,
|
|
146
|
-
|
|
149
|
+
getUnsaved: resolveUnsaved,
|
|
147
150
|
onReloadBranches: () => loadBranches(),
|
|
151
|
+
onBranchCreated: addCreatedBranch,
|
|
152
|
+
userId: userContext?.userId,
|
|
148
153
|
});
|
|
149
154
|
// 5. Comment system (depends on branchNameState)
|
|
150
155
|
const { comments, focusedFieldPath, highlightThreadId, commentsPanelOpen, setCommentsPanelOpen, handleAddComment, handleResolveThread, handleJumpToField, handleJumpToEntry, handleJumpToBranch, } = useCommentSystem({
|
|
@@ -224,9 +229,8 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
224
229
|
// branch's content. Writing it would (a) show it as the new branch's
|
|
225
230
|
// content and (b) satisfy the skip gate above, permanently suppressing
|
|
226
231
|
// the new branch's own load -- which also leaves the new branch's OCC
|
|
227
|
-
// token unset,
|
|
228
|
-
//
|
|
229
|
-
// under its own key.
|
|
232
|
+
// token unset, so saveEntry refuses every save of the entry. Drop it
|
|
233
|
+
// instead; the new branch has its own load running under its own key.
|
|
230
234
|
if (currentBranchRef.current !== requestBranch)
|
|
231
235
|
return;
|
|
232
236
|
setLoadedValues((prev) => ({ ...prev, [contentId]: loaded }));
|
|
@@ -599,7 +603,7 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
599
603
|
width: '100%',
|
|
600
604
|
height: '100%',
|
|
601
605
|
border: '1px solid var(--mantine-color-gray-3)',
|
|
602
|
-
}, highlightEnabled: highlightEnabled, onPreviewError: setPreviewError })] })) : (_jsx(Paper, { withBorder: true, shadow: "xs", h: "100%", bg: "white", style: {
|
|
606
|
+
}, highlightEnabled: highlightEnabled, onPreviewError: setPreviewError, assetBase: authenticatedAssetBase(basePath) })] })) : (_jsx(Paper, { withBorder: true, shadow: "xs", h: "100%", bg: "white", style: {
|
|
603
607
|
display: 'flex',
|
|
604
608
|
alignItems: 'center',
|
|
605
609
|
justifyContent: 'center',
|
|
@@ -607,12 +611,14 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
607
611
|
? 'Setting up your branch workspace…'
|
|
608
612
|
: entriesInitializing
|
|
609
613
|
? 'Loading content…'
|
|
610
|
-
:
|
|
614
|
+
: currentEntry && !currentEntry.previewSrc
|
|
615
|
+
? 'No preview for this entry.'
|
|
616
|
+
: 'Select an item to start editing.' }) }));
|
|
611
617
|
const sidebarWidth = 64;
|
|
612
618
|
const footerHeight = 40;
|
|
613
619
|
const headerTitle = currentEntry?.label ?? currentEntry?.slug ?? title;
|
|
614
620
|
const breadcrumbSegments = useMemo(() => buildBreadcrumbSegments(currentEntry, collectionLabels), [collectionLabels, currentEntry]);
|
|
615
|
-
return (_jsx(CanopyCMSProvider, { ...(themeOptions ?? {}), children: _jsx(AssetContextProvider, {
|
|
621
|
+
return (_jsx(CanopyCMSProvider, { ...(themeOptions ?? {}), children: _jsx(AssetContextProvider, { basePath: basePath, children: _jsxs(Box, { bg: "gray.0", style: { minHeight: '100vh', width: '100%' }, children: [_jsx(EditorHeader, { ref: headerRef, siteTitle: siteTitle, siteSubtitle: siteSubtitle, headerTitle: headerTitle, currentEntry: currentEntry, branchName: branchNameState, operatingMode: operatingMode, branchStatus: currentBranch?.status, busy: busy, breadcrumbSegments: breadcrumbSegments, editedFiles: editedFiles, modifiedCount: modifiedCount, unresolvedCommentCount: comments.filter((t) => !t.resolved).length, comments: comments, hasUnsavedChanges: isSelectedDirty(), userContext: userContext, branchCreatedBy: currentBranch?.createdBy, branchAccess: currentBranch?.access,
|
|
616
622
|
// Fail-closed for the same reason branchContentLocked above is:
|
|
617
623
|
// isProtected/writeBlocked gate real actions in EditorHeader (hiding
|
|
618
624
|
// Submit, disabling Save), so a missing wire value must read as
|
|
@@ -644,7 +650,7 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
644
650
|
? 'Setting up your branch workspace…'
|
|
645
651
|
: entriesInitializing
|
|
646
652
|
? 'Loading content…'
|
|
647
|
-
: 'Select an item to start editing.' })) : currentEntry.canEdit === false ? (_jsx(
|
|
653
|
+
: 'Select an item to start editing.' })) : currentEntry.canEdit === false ? (_jsx(NoEditPermissionNotice, { entryPath: currentEntry.path })) : schema.length > 0 && effectiveValue ? (_jsx(EntryLinkContext.Provider, { value: entryLinkContextValue, children: _jsx(FormRenderer, { fields: schema, value: effectiveValue, onChange: (next) => {
|
|
648
654
|
const contentId = currentEntry?.contentId;
|
|
649
655
|
if (contentId) {
|
|
650
656
|
setDrafts((prev) => ({ ...prev, [contentId]: next }));
|
|
@@ -662,7 +668,7 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
662
668
|
})), selectedPath: selectedPath, onSelect: (id) => {
|
|
663
669
|
setSelectedPath(id);
|
|
664
670
|
setNavigatorOpen(false);
|
|
665
|
-
}, onTreeControllerReady: handleTreeControllerReady, expandedStateRef: treeExpandedStateRef, onExpandedStateChange: handleExpandedStateChange, onDeleteEntry: handleDeleteEntry, onRenameEntry: handleRenameEntry, onReorderEntry: handleReorderEntry, hiddenRootPath: hiddenRootPath, loading: entriesInitializing, readOnly: branchContentLocked }) }) })] })] }), _jsx(
|
|
671
|
+
}, onTreeControllerReady: handleTreeControllerReady, expandedStateRef: treeExpandedStateRef, onExpandedStateChange: handleExpandedStateChange, onDeleteEntry: handleDeleteEntry, onRenameEntry: handleRenameEntry, onReorderEntry: handleReorderEntry, hiddenRootPath: hiddenRootPath, loading: entriesInitializing, readOnly: branchContentLocked }) }) })] })] }), _jsx(BranchesDrawer, { opened: branchManagerOpen, onClose: () => setBranchManagerOpen(false), confirmOpen: branchConfirmOpen, children: _jsx(BranchManager, { branches: branchSummaries, mode: operatingMode, user: userContext, onSelect: async (name) => {
|
|
666
672
|
try {
|
|
667
673
|
await handleBranchChange(name);
|
|
668
674
|
setBranchManagerOpen(false);
|
|
@@ -671,9 +677,7 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
671
677
|
console.error('Branch change failed or was cancelled:', err);
|
|
672
678
|
// Don't close branch manager if there was an error or user cancelled
|
|
673
679
|
}
|
|
674
|
-
}, onCreate: (
|
|
675
|
-
handleCreateBranch(branch).catch((err) => console.error(err));
|
|
676
|
-
}, onSubmit: (name) => {
|
|
680
|
+
}, onBeforeCreate: confirmCreateBranch, onCreate: handleCreateBranch, onSubmit: (name) => {
|
|
677
681
|
handleSubmit(name).catch((err) => console.error(err));
|
|
678
682
|
}, onWithdraw: (name) => {
|
|
679
683
|
handleWithdraw(name).catch((err) => console.error(err));
|
|
@@ -15,11 +15,24 @@ import { ReferenceField } from './fields/ReferenceField.js';
|
|
|
15
15
|
import { CodeField } from './fields/CodeField.js';
|
|
16
16
|
import { ImageField } from './fields/ImageField.js';
|
|
17
17
|
import { ObjectField } from './fields/ObjectField.js';
|
|
18
|
+
import { FieldDescription, groupDescriptionProps } from './fields/FieldDescription.js';
|
|
18
19
|
import { InlineGroupField } from './fields/InlineGroupField.js';
|
|
19
20
|
import { formatCanopyPath, normalizeCanopyPath } from './canopy-path.js';
|
|
20
21
|
import { FieldWrapper } from './comments/FieldWrapper.js';
|
|
21
22
|
import { EntryComments } from './comments/EntryComments.js';
|
|
22
23
|
import { useReferenceResolution } from './hooks/useReferenceResolution.js';
|
|
24
|
+
/**
|
|
25
|
+
* Card heading for an object-list item: the value of its `itemTitleField` when that is a
|
|
26
|
+
* non-blank string or a number, otherwise `fallback`.
|
|
27
|
+
*/
|
|
28
|
+
const listItemTitle = (item, itemTitleField, fallback) => {
|
|
29
|
+
const raw = itemTitleField ? item?.[itemTitleField] : undefined;
|
|
30
|
+
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
31
|
+
return String(raw);
|
|
32
|
+
if (typeof raw === 'string' && raw.trim() !== '')
|
|
33
|
+
return raw.trim();
|
|
34
|
+
return fallback;
|
|
35
|
+
};
|
|
23
36
|
const normalizeOptions = (options) => {
|
|
24
37
|
if (!options)
|
|
25
38
|
return [];
|
|
@@ -75,28 +88,28 @@ export const FormRenderer = ({ fields, value, onChange, customRenderers, branch
|
|
|
75
88
|
switch (field.type) {
|
|
76
89
|
case 'string':
|
|
77
90
|
if (field.list) {
|
|
78
|
-
return wrapWithComments(_jsx(StringListField, { id: fieldId, label: label, value: Array.isArray(currentValue) ? currentValue : [], onChange: (v) => update(v), dataCanopyField: normalizeCanopyPath(path) }, fieldKey(path)));
|
|
91
|
+
return wrapWithComments(_jsx(StringListField, { id: fieldId, label: label, description: field.description, value: Array.isArray(currentValue) ? currentValue : [], onChange: (v) => update(v), dataCanopyField: normalizeCanopyPath(path) }, fieldKey(path)));
|
|
79
92
|
}
|
|
80
|
-
return wrapWithComments(_jsx(TextField, { id: fieldId, label: label, value: currentValue ?? '', onChange: update, dataCanopyField: normalizeCanopyPath(path) }, fieldKey(path)));
|
|
93
|
+
return wrapWithComments(_jsx(TextField, { id: fieldId, label: label, description: field.description, value: currentValue ?? '', onChange: update, dataCanopyField: normalizeCanopyPath(path) }, fieldKey(path)));
|
|
81
94
|
case 'boolean':
|
|
82
|
-
return wrapWithComments(_jsx(ToggleField, { id: fieldId, label: label, value: Boolean(currentValue), onChange: (v) => update(Boolean(v)), dataCanopyField: normalizeCanopyPath(path), testId: `field-toggle-${field.name}` }, fieldKey(path)));
|
|
95
|
+
return wrapWithComments(_jsx(ToggleField, { id: fieldId, label: label, description: field.description, value: Boolean(currentValue), onChange: (v) => update(Boolean(v)), dataCanopyField: normalizeCanopyPath(path), testId: `field-toggle-${field.name}` }, fieldKey(path)));
|
|
83
96
|
case 'number':
|
|
84
97
|
if (field.list) {
|
|
85
|
-
return wrapWithComments(_jsx(NumberListField, { id: fieldId, label: label, value: Array.isArray(currentValue)
|
|
98
|
+
return wrapWithComments(_jsx(NumberListField, { id: fieldId, label: label, description: field.description, value: Array.isArray(currentValue)
|
|
86
99
|
? currentValue.filter((v) => typeof v === 'number')
|
|
87
100
|
: [], onChange: (v) => update(v), dataCanopyField: normalizeCanopyPath(path) }, fieldKey(path)));
|
|
88
101
|
}
|
|
89
|
-
return wrapWithComments(_jsx(NumberField, { id: fieldId, label: label, value: typeof currentValue === 'number' ? currentValue : undefined, onChange: (v) => update(v), dataCanopyField: normalizeCanopyPath(path) }, fieldKey(path)));
|
|
102
|
+
return wrapWithComments(_jsx(NumberField, { id: fieldId, label: label, description: field.description, value: typeof currentValue === 'number' ? currentValue : undefined, onChange: (v) => update(v), dataCanopyField: normalizeCanopyPath(path) }, fieldKey(path)));
|
|
90
103
|
case 'datetime':
|
|
91
|
-
return wrapWithComments(_jsx(DateTimeField, { id: fieldId, label: label, value: typeof currentValue === 'string' ? currentValue : '', onChange: (v) => update(v), dataCanopyField: normalizeCanopyPath(path) }, fieldKey(path)));
|
|
104
|
+
return wrapWithComments(_jsx(DateTimeField, { id: fieldId, label: label, description: field.description, value: typeof currentValue === 'string' ? currentValue : '', onChange: (v) => update(v), dataCanopyField: normalizeCanopyPath(path) }, fieldKey(path)));
|
|
92
105
|
case 'markdown':
|
|
93
106
|
case 'mdx':
|
|
94
|
-
return wrapWithComments(_jsx(MarkdownField, { id: fieldId, label: label, value: currentValue ?? '', onChange: (v) => update(v), dataCanopyField: normalizeCanopyPath(path) }, fieldKey(path)));
|
|
107
|
+
return wrapWithComments(_jsx(MarkdownField, { id: fieldId, label: label, description: field.description, value: currentValue ?? '', onChange: (v) => update(v), dataCanopyField: normalizeCanopyPath(path) }, fieldKey(path)));
|
|
95
108
|
case 'select': {
|
|
96
109
|
const selectField = field;
|
|
97
110
|
const options = normalizeOptions(selectField.options);
|
|
98
111
|
const isMulti = Boolean(selectField.list);
|
|
99
|
-
return wrapWithComments(_jsx(SelectField, { id: fieldId, label: label, options: options, value: isMulti
|
|
112
|
+
return wrapWithComments(_jsx(SelectField, { id: fieldId, label: label, description: field.description, options: options, value: isMulti
|
|
100
113
|
? Array.isArray(currentValue)
|
|
101
114
|
? currentValue
|
|
102
115
|
: []
|
|
@@ -108,7 +121,7 @@ export const FormRenderer = ({ fields, value, onChange, customRenderers, branch
|
|
|
108
121
|
? normalizeOptions(referenceField.options)
|
|
109
122
|
: undefined;
|
|
110
123
|
const isMulti = Boolean(referenceField.list);
|
|
111
|
-
return wrapWithComments(_jsx(ReferenceField, { id: fieldId, label: label, options: staticOptions?.map((opt) => ({
|
|
124
|
+
return wrapWithComments(_jsx(ReferenceField, { id: fieldId, label: label, description: field.description, options: staticOptions?.map((opt) => ({
|
|
112
125
|
label: opt.label,
|
|
113
126
|
value: opt.value,
|
|
114
127
|
})), collections: referenceField.collections, entryTypes: referenceField.entryTypes, displayField: referenceField.displayField, branch: branch, value: isMulti
|
|
@@ -119,7 +132,7 @@ export const FormRenderer = ({ fields, value, onChange, customRenderers, branch
|
|
|
119
132
|
}
|
|
120
133
|
case 'image': {
|
|
121
134
|
const imageField = field;
|
|
122
|
-
return wrapWithComments(_jsx(ImageField, { id: fieldId, label: label, value: currentValue, onChange: (next) => update(next), aspect: imageField.aspect, altOptional: imageField.altOptional, dataCanopyField: canopyPath, errors: {
|
|
135
|
+
return wrapWithComments(_jsx(ImageField, { id: fieldId, label: label, description: field.description, value: currentValue, onChange: (next) => update(next), aspect: imageField.aspect, altOptional: imageField.altOptional, dataCanopyField: canopyPath, errors: {
|
|
123
136
|
src: fieldErrors?.[`${canopyPath}.src`],
|
|
124
137
|
alt: fieldErrors?.[`${canopyPath}.alt`],
|
|
125
138
|
crop: fieldErrors?.[`${canopyPath}.crop`],
|
|
@@ -127,7 +140,7 @@ export const FormRenderer = ({ fields, value, onChange, customRenderers, branch
|
|
|
127
140
|
}
|
|
128
141
|
case 'block': {
|
|
129
142
|
const blockField = field;
|
|
130
|
-
return wrapWithComments(_jsx(BlockField, { label: label, templates: blockField.templates, value: (Array.isArray(currentValue) ? currentValue : []), onChange: (next) => update(next), renderField: renderField, path: path, dataCanopyField: normalizeCanopyPath(path) }, fieldKey(path)));
|
|
143
|
+
return wrapWithComments(_jsx(BlockField, { label: label, description: field.description, templates: blockField.templates, value: (Array.isArray(currentValue) ? currentValue : []), onChange: (next) => update(next), renderField: renderField, path: path, dataCanopyField: normalizeCanopyPath(path) }, fieldKey(path)));
|
|
131
144
|
}
|
|
132
145
|
case 'object': {
|
|
133
146
|
const objectField = field;
|
|
@@ -135,11 +148,14 @@ export const FormRenderer = ({ fields, value, onChange, customRenderers, branch
|
|
|
135
148
|
const items = Array.isArray(currentValue)
|
|
136
149
|
? currentValue
|
|
137
150
|
: [];
|
|
138
|
-
return wrapWithComments(_jsx(Paper, { withBorder: true, radius: "md", p: "md", shadow: "xs", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Group, { justify: "space-between", children: [_jsx(Text, { size: "sm", fw: 600, children: label }), _jsx(Button, { size: "xs", variant: "light", onClick: () => update([...items, {}]), children: "Add item" })] }),
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
151
|
+
return wrapWithComments(_jsx(Paper, { withBorder: true, radius: "md", p: "md", shadow: "xs", ...groupDescriptionProps(fieldId, field.description), children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Group, { justify: "space-between", children: [_jsx(Text, { size: "sm", fw: 600, children: label }), _jsx(Button, { size: "xs", variant: "light", onClick: () => update([...items, {}]), children: "Add item" })] }), _jsx(FieldDescription, { baseId: fieldId, description: field.description }), _jsxs(Stack, { gap: "sm", children: [items.map((item, idx) => {
|
|
152
|
+
const itemTitle = listItemTitle(item, objectField.itemTitleField, `${label} #${idx + 1}`);
|
|
153
|
+
return (_jsx(Paper, { role: "group", "aria-label": itemTitle, withBorder: true, radius: "md", p: "sm", shadow: "xs", children: _jsxs(Stack, { gap: "xs", children: [_jsxs(Group, { justify: "space-between", children: [_jsx(Text, { size: "xs", fw: 700, children: itemTitle }), _jsx(Button, { size: "xs", variant: "subtle", color: "red", onClick: () => update(items.filter((_, i) => i !== idx)), children: "Remove" })] }), _jsx(ObjectField, { fields: objectField.fields, value: item, onChange: (next) => {
|
|
154
|
+
const nextItems = [...items];
|
|
155
|
+
nextItems[idx] = next;
|
|
156
|
+
update(nextItems);
|
|
157
|
+
}, renderField: renderField, path: [...path, idx], dataCanopyField: normalizeCanopyPath([...path, idx]) })] }) }, fieldKey([...path, idx])));
|
|
158
|
+
}), items.length === 0 && (_jsx(Text, { size: "xs", c: "dimmed", children: "No items yet. Add one to get started." }))] })] }) }, fieldKey(path)));
|
|
143
159
|
}
|
|
144
160
|
// Unlike the list branch above (each item has its own Remove button), a
|
|
145
161
|
// non-list object has no other way back to `undefined`: a required child
|
|
@@ -151,10 +167,10 @@ export const FormRenderer = ({ fields, value, onChange, customRenderers, branch
|
|
|
151
167
|
// doesn't read as deleting the field from the schema.
|
|
152
168
|
const hasValue = currentValue !== undefined && currentValue !== null;
|
|
153
169
|
const canClear = objectField.required !== true && hasValue;
|
|
154
|
-
return wrapWithComments(_jsx(ObjectField, { label: label, fields: objectField.fields, value: currentValue, onChange: (next) => update(next), onRemove: canClear ? () => update(undefined) : undefined, renderField: renderField, path: path, dataCanopyField: normalizeCanopyPath(path) }, fieldKey(path)));
|
|
170
|
+
return wrapWithComments(_jsx(ObjectField, { label: label, description: field.description, fields: objectField.fields, value: currentValue, onChange: (next) => update(next), onRemove: canClear ? () => update(undefined) : undefined, renderField: renderField, path: path, dataCanopyField: normalizeCanopyPath(path) }, fieldKey(path)));
|
|
155
171
|
}
|
|
156
172
|
case 'code':
|
|
157
|
-
return wrapWithComments(_jsx(CodeField, { id: fieldId, label: label, value: typeof currentValue === 'string' ? currentValue : '', onChange: (v) => update(v), dataCanopyField: normalizeCanopyPath(path) }, fieldKey(path)));
|
|
173
|
+
return wrapWithComments(_jsx(CodeField, { id: fieldId, label: label, description: field.description, value: typeof currentValue === 'string' ? currentValue : '', onChange: (v) => update(v), dataCanopyField: normalizeCanopyPath(path) }, fieldKey(path)));
|
|
158
174
|
default:
|
|
159
175
|
return (_jsxs(Text, { size: "xs", c: "dimmed", children: ["Unsupported field: ", field.type] }, fieldKey(path)));
|
|
160
176
|
}
|
|
@@ -59,7 +59,9 @@ function purgeGateFor(entry) {
|
|
|
59
59
|
}
|
|
60
60
|
return {
|
|
61
61
|
disabled: lockFresh,
|
|
62
|
-
tooltip: lockFresh
|
|
62
|
+
tooltip: lockFresh
|
|
63
|
+
? 'Provisioning, or the worker syncing this branch, may be in progress'
|
|
64
|
+
: undefined,
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
function workerLivenessBadge(worker, mode) {
|
|
@@ -76,6 +78,33 @@ function workerLivenessBadge(worker, mode) {
|
|
|
76
78
|
return { color: 'red', label: 'Worker: absent' };
|
|
77
79
|
}
|
|
78
80
|
}
|
|
81
|
+
const BASE_REFRESH_LABELS = {
|
|
82
|
+
refreshed: 'fast-forwarded',
|
|
83
|
+
'up-to-date': 'up to date',
|
|
84
|
+
'skipped-dirty': 'refresh skipped (uncommitted changes)',
|
|
85
|
+
'skipped-locked': 'refresh skipped (workspace busy: provisioning or an admin action)',
|
|
86
|
+
'skipped-not-provisioned': 'not yet provisioned',
|
|
87
|
+
failed: 'refresh failed',
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Why the base branch needs an operator, or null when its last refresh needs
|
|
91
|
+
* nothing. Shared by the overview and the base row's warning tooltip.
|
|
92
|
+
*/
|
|
93
|
+
function baseRefreshWarning(report) {
|
|
94
|
+
if (!report)
|
|
95
|
+
return null;
|
|
96
|
+
const lines = [];
|
|
97
|
+
if (report.outcome === 'skipped-dirty' || report.outcome === 'failed') {
|
|
98
|
+
lines.push(`Base branch ${BASE_REFRESH_LABELS[report.outcome]}${report.message ? `: ${report.message}` : ''}`);
|
|
99
|
+
if (report.dirtyFiles?.length)
|
|
100
|
+
lines.push(`Uncommitted: ${report.dirtyFiles.join(', ')}`);
|
|
101
|
+
}
|
|
102
|
+
if (report.trackedCanopyMeta?.length) {
|
|
103
|
+
lines.push(`The site repo tracks canopycms state (${report.trackedCanopyMeta.join(', ')}). ` +
|
|
104
|
+
'Untrack it with `git rm -r --cached .canopy-meta`, add `.canopy-meta/` to .gitignore, and commit.');
|
|
105
|
+
}
|
|
106
|
+
return lines.length > 0 ? lines.join('\n') : null;
|
|
107
|
+
}
|
|
79
108
|
// Mirrors BranchManager.tsx's statusColorMap -- kept local (not exported
|
|
80
109
|
// there) rather than shared, same tiny lookup either way.
|
|
81
110
|
const branchStatusColorMap = {
|
|
@@ -121,9 +150,19 @@ function OverviewTab({ health }) {
|
|
|
121
150
|
const liveness = workerLivenessBadge(status.worker, status.mode);
|
|
122
151
|
const lastFatalError = status.workerStatus?.lastFatalError;
|
|
123
152
|
const lastGitSync = status.workerStatus?.lastGitSync;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
153
|
+
const baseWarning = baseRefreshWarning(lastGitSync?.baseRefresh);
|
|
154
|
+
const { build } = status;
|
|
155
|
+
// Absent for a worker that predates the field, which is not evidence of skew;
|
|
156
|
+
// nor is a stale or absent worker's leftover status file, which names no running build.
|
|
157
|
+
const workerVersion = status.workerStatus?.workerVersion || undefined;
|
|
158
|
+
const versionSkew = status.worker.state === 'alive' &&
|
|
159
|
+
workerVersion !== undefined &&
|
|
160
|
+
workerVersion !== build.canopycmsVersion;
|
|
161
|
+
return (_jsxs(Stack, { gap: "md", children: [error && (_jsx(Alert, { color: "red", icon: _jsx(IconAlertCircle, { size: 16 }), variant: "light", children: error })), _jsxs(Group, { gap: "sm", children: [_jsx(Badge, { color: liveness.color, variant: "light", size: "lg", children: liveness.label }), status.mode === 'dev' && (_jsx(Text, { size: "xs", c: "dimmed", children: "No worker runs in dev mode" }))] }), isRecentFatalError && lastFatalError && (_jsxs(Alert, { color: "red", icon: _jsx(IconAlertCircle, { size: 16 }), title: "Worker crash detected", children: [_jsx(Text, { size: "sm", children: lastFatalError.message }), _jsxs(Text, { size: "xs", c: "dimmed", mt: 4, children: ["at ", lastFatalError.at, " (", lastFatalError.phase, ")"] })] })), status.settingsWorkspaceError && (_jsx(Alert, { color: "red", icon: _jsx(IconAlertCircle, { size: 16 }), title: "Settings workspace unavailable: groups and path rules are not loading", children: _jsx(Text, { size: "sm", children: status.settingsWorkspaceError }) })), status.statusReadError && (_jsxs(Text, { size: "xs", c: "orange", children: ["Warning: could not read worker status (", status.statusReadError, ")"] })), status.workerStatus?.lastGitSyncError && (_jsxs(Alert, { color: "orange", icon: _jsx(IconAlertCircle, { size: 16 }), title: "Last git sync failed", children: [_jsx(Text, { size: "sm", children: status.workerStatus.lastGitSyncError.message }), _jsxs(Text, { size: "xs", c: "dimmed", mt: 4, children: ["at ", status.workerStatus.lastGitSyncError.at] })] })), versionSkew && (_jsx(Alert, { color: "orange", icon: _jsx(IconAlertCircle, { size: 16 }), title: "API and worker versions differ", "data-testid": "version-skew-warning", children: _jsxs(Text, { size: "sm", children: ["The API runs canopycms ", build.canopycmsVersion, " but the worker runs canopycms", ' ', workerVersion, ". They were deployed from different builds."] }) })), _jsxs(Paper, { withBorder: true, p: "sm", radius: "md", children: [_jsx(Text, { size: "sm", fw: 600, children: "Build" }), _jsxs(Text, { size: "xs", c: "dimmed", "data-testid": "build-api-version", children: ["API: canopycms ", build.canopycmsVersion] }), _jsxs(Text, { size: "xs", c: "dimmed", "data-testid": "build-source-revision", children: ["Source revision:", ' ', build.sourceRevision ? (_jsx(Tooltip, { label: build.sourceRevision, children: _jsx(Code, { children: build.sourceRevision.slice(0, 12) }) })) : ('not set (pass the CANOPY_SOURCE_SHA build arg to the image build)')] }), _jsxs(Text, { size: "xs", c: "dimmed", "data-testid": "build-worker-version", children: ["Worker version: ", workerVersion ? `canopycms ${workerVersion}` : 'unknown'] }), _jsxs(Text, { size: "xs", c: "dimmed", "data-testid": "build-media", children: ["Media storage:", ' ', status.assetStore.configured ? 'configured' : 'not configured — uploads are disabled'] }), _jsxs(Text, { size: "xs", c: status.imageProcessing.available ? 'dimmed' : 'orange', "data-testid": "build-image-processing", children: ["Image processing:", ' ', status.imageProcessing.available
|
|
162
|
+
? 'available'
|
|
163
|
+
: `unavailable — new editor image sizes and crops fail, and uploads skip decode validation${status.imageProcessing.error ? ` (${status.imageProcessing.error})` : ''}`] })] }), lastGitSync && (_jsxs(Paper, { withBorder: true, p: "sm", radius: "md", children: [_jsx(Text, { size: "sm", fw: 600, children: "Last git sync" }), _jsxs(Text, { size: "xs", c: "dimmed", children: [status.workerStatus?.lastGitSyncAt ?? 'unknown time', " \u00B7 ", lastGitSync.durationMs, "ms \u00B7", ' ', lastGitSync.rebased.length, " rebased \u00B7 ", lastGitSync.skippedDirty.length, " skipped (dirty)", lastGitSync.skippedLocked && lastGitSync.skippedLocked.length > 0
|
|
164
|
+
? ` · ${lastGitSync.skippedLocked.length} skipped (busy: a content write, provisioning or purge)`
|
|
165
|
+
: ''] }), lastGitSync.baseRefresh && (_jsxs(Text, { size: "xs", c: "dimmed", "data-testid": "base-refresh-outcome", children: ["Base branch: ", BASE_REFRESH_LABELS[lastGitSync.baseRefresh.outcome]] })), baseWarning && (_jsx(Text, { size: "xs", c: "orange", style: { whiteSpace: 'pre-line' }, "data-testid": "base-refresh-warning", children: baseWarning })), lastGitSync.failed.length > 0 && (_jsx(Spoiler, { maxHeight: 0, showLabel: `${lastGitSync.failed.length} failed — show details`, hideLabel: "Hide", children: _jsx(Stack, { gap: 4, mt: 4, children: lastGitSync.failed.map((f) => (_jsxs(Text, { size: "xs", c: "red", children: [f.branch, ": ", f.error] }, f.branch))) }) }))] })), _jsx(SimpleGrid, { cols: 5, spacing: "xs", children: ['pending', 'processing', 'completed', 'failed', 'corrupt'].map((key) => (_jsxs(Paper, { withBorder: true, p: "xs", radius: "md", ta: "center", children: [_jsx(Text, { size: "xs", c: "dimmed", tt: "capitalize", children: key }), _jsx(Text, { size: "lg", fw: 700, c: (key === 'failed' || key === 'corrupt') && status.queue[key] > 0 ? 'red' : undefined, children: status.queue[key] })] }, key))) }), status.queue.oldestPendingAgeMs !== undefined && (_jsxs(Text, { size: "xs", c: "dimmed", children: ["Oldest pending task: ", formatAgeMs(status.queue.oldestPendingAgeMs), " old"] })), _jsxs(Group, { justify: "space-between", align: "center", children: [_jsxs(Text, { size: "xs", c: "dimmed", children: ["Generated at ", status.generatedAt] }), _jsx(Button, { size: "xs", variant: "light", onClick: () => refresh(), loading: statusLoading, children: "Refresh" })] }), _jsx(Text, { size: "xs", c: "dimmed", children: "Data may lag up to ~60s (shared-filesystem caching)" })] }));
|
|
127
166
|
}
|
|
128
167
|
function TasksTab({ health }) {
|
|
129
168
|
const { taskStatus, setTaskStatus, tasks, tasksLoading } = health;
|
|
@@ -160,6 +199,7 @@ function TasksTab({ health }) {
|
|
|
160
199
|
function BranchesTab({ health }) {
|
|
161
200
|
const { branchHealth, branchHealthLoading } = health;
|
|
162
201
|
const entries = branchHealth?.entries ?? [];
|
|
202
|
+
const baseWarning = baseRefreshWarning(health.status?.workerStatus?.lastGitSync?.baseRefresh);
|
|
163
203
|
const handleMarkMergedClick = (branchName) => {
|
|
164
204
|
modals.openConfirmModal({
|
|
165
205
|
title: 'Mark branch as merged',
|
|
@@ -193,9 +233,9 @@ function BranchesTab({ health }) {
|
|
|
193
233
|
if (entries.length === 0) {
|
|
194
234
|
return (_jsx(Text, { size: "sm", c: "dimmed", py: "md", children: "No branch directories found." }));
|
|
195
235
|
}
|
|
196
|
-
return (_jsx(Table.ScrollContainer, { minWidth: 700, children: _jsxs(Table, { children: [_jsx(Table.Thead, { children: _jsxs(Table.Tr, { children: [_jsx(Table.Th, { children: "Name" }), _jsx(Table.Th, { children: "Status" }), _jsx(Table.Th, { children: "PR" }), _jsx(Table.Th, { children: "Sync" }), _jsx(Table.Th, { children: "Warnings" }), _jsx(Table.Th, { children: "Updated" }), _jsx(Table.Th, { children: "Actions" })] }) }), _jsx(Table.Tbody, { children: entries.map((entry) => (_jsx(BranchHealthRow, { entry: entry, onMarkMerged: handleMarkMergedClick, onRepair: handleRepairClick, onPurge: handlePurgeClick }, entry.dirName))) })] }) }));
|
|
236
|
+
return (_jsx(Table.ScrollContainer, { minWidth: 700, children: _jsxs(Table, { children: [_jsx(Table.Thead, { children: _jsxs(Table.Tr, { children: [_jsx(Table.Th, { children: "Name" }), _jsx(Table.Th, { children: "Status" }), _jsx(Table.Th, { children: "PR" }), _jsx(Table.Th, { children: "Sync" }), _jsx(Table.Th, { children: "Warnings" }), _jsx(Table.Th, { children: "Updated" }), _jsx(Table.Th, { children: "Actions" })] }) }), _jsx(Table.Tbody, { children: entries.map((entry) => (_jsx(BranchHealthRow, { entry: entry, baseWarning: entry.isBaseBranch ? baseWarning : null, onMarkMerged: handleMarkMergedClick, onRepair: handleRepairClick, onPurge: handlePurgeClick }, entry.dirName))) })] }) }));
|
|
197
237
|
}
|
|
198
|
-
function BranchHealthRow({ entry, onMarkMerged, onRepair, onPurge, }) {
|
|
238
|
+
function BranchHealthRow({ entry, baseWarning, onMarkMerged, onRepair, onPurge, }) {
|
|
199
239
|
if (entry.kind === 'healthy' && entry.branch) {
|
|
200
240
|
const b = entry.branch;
|
|
201
241
|
// Mirrors the rebase loop's skip logic (worker/rebase.ts): the worker
|
|
@@ -207,7 +247,7 @@ function BranchHealthRow({ entry, onMarkMerged, onRepair, onPurge, }) {
|
|
|
207
247
|
const canMarkMerged = (b.status === 'submitted' || b.status === 'approved') && !!b.pullRequestNumber;
|
|
208
248
|
return (_jsxs(Table.Tr, { children: [_jsx(Table.Td, { children: _jsxs(Group, { gap: 4, wrap: "nowrap", children: [_jsx(Text, { size: "sm", children: entry.dirName }), entry.isBaseBranch && (_jsx(Badge, { size: "xs", color: "gray", variant: "outline", children: "base" }))] }) }), _jsx(Table.Td, { children: _jsx(Badge, { color: branchStatusColorMap[b.status] ?? 'neutral', variant: "light", children: b.status }) }), _jsx(Table.Td, { children: b.pullRequestNumber ? (_jsxs(Text, { size: "xs", c: "blue", component: "a", href: b.pullRequestUrl, target: "_blank", rel: "noopener noreferrer", style: { textDecoration: 'underline' }, children: ["PR #", b.pullRequestNumber, b.pullRequestState ? ` (${b.pullRequestState})` : ''] })) : (_jsx(Text, { size: "xs", c: "dimmed", children: "\u2014" })) }), _jsx(Table.Td, { children: b.syncStatus === 'sync-failed' ? (_jsx(Tooltip, { label: b.syncFailureReason
|
|
209
249
|
? `${b.syncFailureReason} — retry from the Tasks tab`
|
|
210
|
-
: 'GitHub sync failed — retry from the Tasks tab', multiline: true, maw: 320, children: _jsx(Badge, { color: "red", variant: "light", children: "sync-failed" }) })) : b.syncStatus === 'pending-sync' ? (_jsx(Badge, { color: "gray", variant: "light", children: "pending-sync" })) : null }), _jsx(Table.Td, { children: _jsxs(Group, { gap: 6, wrap: "nowrap", children: [!!b.conflictFiles?.length && (_jsxs(Badge, { color: "orange", variant: "light", children: [b.conflictFiles.length, " conflict", b.conflictFiles.length === 1 ? '' : 's'] })), showRebaseFailure && b.rebaseFailure && (_jsx(Tooltip, { label: `${b.rebaseFailure.message} (failing since ${b.rebaseFailure.firstAt})`, multiline: true, maw: 320, children: _jsx(ThemeIcon, { color: "yellow", variant: "light", size: "sm", radius: "xl", "data-testid": `rebase-failure-${entry.dirName}`, children: _jsx(IconAlertTriangle, { size: 12 }) }) })), !!entry.duplicateContentIds?.length && (_jsx(Tooltip, { label: entry.duplicateContentIds
|
|
250
|
+
: 'GitHub sync failed — retry from the Tasks tab', multiline: true, maw: 320, children: _jsx(Badge, { color: "red", variant: "light", children: "sync-failed" }) })) : b.syncStatus === 'pending-sync' ? (_jsx(Badge, { color: "gray", variant: "light", children: "pending-sync" })) : null }), _jsx(Table.Td, { children: _jsxs(Group, { gap: 6, wrap: "nowrap", children: [!!b.conflictFiles?.length && (_jsxs(Badge, { color: "orange", variant: "light", children: [b.conflictFiles.length, " conflict", b.conflictFiles.length === 1 ? '' : 's'] })), showRebaseFailure && b.rebaseFailure && (_jsx(Tooltip, { label: `${b.rebaseFailure.message} (failing since ${b.rebaseFailure.firstAt})`, multiline: true, maw: 320, children: _jsx(ThemeIcon, { color: "yellow", variant: "light", size: "sm", radius: "xl", "data-testid": `rebase-failure-${entry.dirName}`, children: _jsx(IconAlertTriangle, { size: 12 }) }) })), baseWarning && (_jsx(Tooltip, { label: baseWarning, multiline: true, maw: 420, style: { whiteSpace: 'pre-line' }, children: _jsx(ThemeIcon, { color: "yellow", variant: "light", size: "sm", radius: "xl", "data-testid": `base-refresh-warning-${entry.dirName}`, children: _jsx(IconAlertTriangle, { size: 12 }) }) })), !!entry.duplicateContentIds?.length && (_jsx(Tooltip, { label: entry.duplicateContentIds
|
|
211
251
|
.map((d) => `${d.id}: kept ${d.keptPath}; also on ${d.droppedPaths.join(', ')}`)
|
|
212
252
|
.join('\n'), multiline: true, maw: 420, style: { whiteSpace: 'pre-line' }, children: _jsxs(Badge, { color: "orange", variant: "light", "data-testid": `duplicate-content-ids-${entry.dirName}`, children: [entry.duplicateContentIds.length, " duplicate ID", entry.duplicateContentIds.length === 1 ? '' : 's'] }) }))] }) }), _jsx(Table.Td, { children: _jsx(Text, { size: "xs", c: "dimmed", children: b.updatedAt }) }), _jsx(Table.Td, { children: canMarkMerged && (_jsx(Button, { size: "xs", variant: "light", "data-testid": `mark-merged-${b.name}`, onClick: () => onMarkMerged(b.name), children: "Mark merged" })) })] }));
|
|
213
253
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface BranchesDrawerProps {
|
|
3
|
+
opened: boolean;
|
|
4
|
+
onClose: () => void;
|
|
5
|
+
/** A confirm opened from inside is on screen: the drawer yields Escape to it (Mantine listens for Escape on window, capture phase). */
|
|
6
|
+
confirmOpen: boolean;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
export declare const BranchesDrawer: React.FC<BranchesDrawerProps>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Drawer, Text, Title } from '@mantine/core';
|
|
4
|
+
export const BranchesDrawer = ({ opened, onClose, confirmOpen, children, }) => (_jsx(Drawer, { opened: opened, onClose: onClose, closeOnEscape: !confirmOpen, position: "right", title: _jsxs("div", { children: [_jsx(Title, { order: 4, children: "Branches" }), _jsx(Text, { size: "xs", c: "dimmed", children: "Manage access, status, and lifecycle" })] }), padding: "md", size: 420, overlayProps: { blur: 2 }, children: children }));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface NoEditPermissionNoticeProps {
|
|
2
|
+
/** Logical path of the entry the user cannot edit. */
|
|
3
|
+
entryPath: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Explains why an entry cannot be edited and whom to ask. An entry's `canEdit` is false only
|
|
7
|
+
* when a path rule (or `defaultPathAccess`) denies edit, so that is the reason it gives; a
|
|
8
|
+
* locked or protected branch shows its own banner in the header and a read-only form instead.
|
|
9
|
+
*/
|
|
10
|
+
export declare function NoEditPermissionNotice({ entryPath }: NoEditPermissionNoticeProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from '@mantine/core';
|
|
3
|
+
/**
|
|
4
|
+
* Explains why an entry cannot be edited and whom to ask. An entry's `canEdit` is false only
|
|
5
|
+
* when a path rule (or `defaultPathAccess`) denies edit, so that is the reason it gives; a
|
|
6
|
+
* locked or protected branch shows its own banner in the header and a read-only form instead.
|
|
7
|
+
*/
|
|
8
|
+
export function NoEditPermissionNotice({ entryPath }) {
|
|
9
|
+
return (_jsx("div", { role: "status", "data-testid": "no-edit-permission-notice", style: {
|
|
10
|
+
display: 'flex',
|
|
11
|
+
alignItems: 'center',
|
|
12
|
+
justifyContent: 'center',
|
|
13
|
+
height: '100%',
|
|
14
|
+
padding: '0 var(--mantine-spacing-md)',
|
|
15
|
+
textAlign: 'center',
|
|
16
|
+
}, children: _jsxs(Text, { size: "sm", c: "dimmed", children: ["You don't have edit access to \"", entryPath, "\". Ask a CanopyCMS admin to grant it in Manage Permissions."] }) }));
|
|
17
|
+
}
|
|
@@ -1,44 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Carries
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
* `EntryLinkContext` exists for the entry-link toolbar button). Unlike
|
|
8
|
-
* `ApiClientContext`, reading this without a provider is safe and intentional:
|
|
9
|
-
* no provider means root-relative URLs, which is the correct default for the
|
|
10
|
-
* common case (editor and site share an origin).
|
|
2
|
+
* Carries the prefix every editor-built asset URL is put behind - MediaLibrary thumbnails,
|
|
3
|
+
* ImageField's preview and crop source, and the MDX editor's image previews. A context (rather
|
|
4
|
+
* than a prop) because the MDX dialog is rendered deep inside MDXEditor's lazy-loaded internals
|
|
5
|
+
* via `imagePlugin`'s `ImageDialog` param, with no prop channel of its own (the same reason
|
|
6
|
+
* `EntryLinkContext` exists for the entry-link toolbar button).
|
|
11
7
|
*/
|
|
12
8
|
import React from 'react';
|
|
9
|
+
/**
|
|
10
|
+
* Where the editor loads asset bytes from: the authenticated raw route under `basePath`, never the
|
|
11
|
+
* public `/assets` space. An editor asks for derivatives no build has seen (a fresh crop, a
|
|
12
|
+
* thumbnail), which the public path need not serve, and the raw route computes them on demand.
|
|
13
|
+
* Also handed to the live preview, through `PreviewFrame`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function authenticatedAssetBase(basePath?: string): string;
|
|
13
16
|
export interface AssetContextValue {
|
|
14
|
-
/**
|
|
15
|
-
baseUrl
|
|
17
|
+
/** The `baseUrl` to pass to `assetUrl` for every asset URL the editor renders. */
|
|
18
|
+
baseUrl: string;
|
|
16
19
|
}
|
|
17
|
-
export interface AssetContextProviderProps
|
|
20
|
+
export interface AssetContextProviderProps {
|
|
18
21
|
children: React.ReactNode;
|
|
19
|
-
/**
|
|
20
|
-
* `CanopyClientConfig.basePath` - the deployment prefix the host app is served under. Used only
|
|
21
|
-
* when `baseUrl` is unset.
|
|
22
|
-
*/
|
|
22
|
+
/** `CanopyClientConfig.basePath` - the deployment prefix the host app is served under. */
|
|
23
23
|
basePath?: string;
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*
|
|
28
|
-
* They are ALTERNATIVES, never composed: whichever is used names where `/assets` is mounted, in
|
|
29
|
-
* full, and a deployment `basePath` cannot apply on top of it. Where the two differ is topology,
|
|
30
|
-
* not precedence - see the asset-mount table in the README.
|
|
31
|
-
*
|
|
32
|
-
* The fallback is kept only so that existing basePath deployments do not break on upgrade,
|
|
33
|
-
* which makes removing it a deliberate decision rather than a cleanup - it is option 1 of
|
|
34
|
-
* `.claude/future-tasks/editor-asset-mount-topology.md`, still open.
|
|
35
|
-
*
|
|
36
|
-
* KNOWN LIMITATION: on a CloudFront/CDK deployment, a basePath does NOT move the asset
|
|
37
|
-
* space (behaviors are anchored at the distribution root), so this fallback prefixes
|
|
38
|
-
* wrongly — requests still resolve, via `withCanopy`'s auto-prefixed `/assets/:path*`
|
|
39
|
-
* rewrite, but bypass the CDN cache and the dedicated transform Lambda. Workaround: set
|
|
40
|
-
* `media.publicBaseUrl` to the distribution origin instead, which takes precedence here.
|
|
41
|
-
*/
|
|
42
|
-
export declare function AssetContextProvider({ baseUrl, basePath, children }: AssetContextProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
43
|
-
/** Safe to call outside a provider - returns `{ baseUrl: undefined }` (root-relative URLs). */
|
|
25
|
+
export declare function AssetContextProvider({ basePath, children }: AssetContextProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
/** Safe to call outside a provider - see the context's default. */
|
|
44
27
|
export declare function useAssetContext(): AssetContextValue;
|
|
@@ -1,41 +1,32 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
/**
|
|
4
|
-
* Carries
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
9
|
-
* `EntryLinkContext` exists for the entry-link toolbar button). Unlike
|
|
10
|
-
* `ApiClientContext`, reading this without a provider is safe and intentional:
|
|
11
|
-
* no provider means root-relative URLs, which is the correct default for the
|
|
12
|
-
* common case (editor and site share an origin).
|
|
4
|
+
* Carries the prefix every editor-built asset URL is put behind - MediaLibrary thumbnails,
|
|
5
|
+
* ImageField's preview and crop source, and the MDX editor's image previews. A context (rather
|
|
6
|
+
* than a prop) because the MDX dialog is rendered deep inside MDXEditor's lazy-loaded internals
|
|
7
|
+
* via `imagePlugin`'s `ImageDialog` param, with no prop channel of its own (the same reason
|
|
8
|
+
* `EntryLinkContext` exists for the entry-link toolbar button).
|
|
13
9
|
*/
|
|
14
10
|
import { createContext, useContext, useMemo } from 'react';
|
|
15
|
-
|
|
11
|
+
import { joinUrlPrefix } from '../../utils/url-prefix.js';
|
|
12
|
+
/** The raw asset route (`api/assets.ts`'s `assetRawRoute`) under the default API base. */
|
|
13
|
+
const RAW_ASSET_ROUTE = '/api/canopycms/assets/raw';
|
|
16
14
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* not precedence - see the asset-mount table in the README.
|
|
22
|
-
*
|
|
23
|
-
* The fallback is kept only so that existing basePath deployments do not break on upgrade,
|
|
24
|
-
* which makes removing it a deliberate decision rather than a cleanup - it is option 1 of
|
|
25
|
-
* `.claude/future-tasks/editor-asset-mount-topology.md`, still open.
|
|
26
|
-
*
|
|
27
|
-
* KNOWN LIMITATION: on a CloudFront/CDK deployment, a basePath does NOT move the asset
|
|
28
|
-
* space (behaviors are anchored at the distribution root), so this fallback prefixes
|
|
29
|
-
* wrongly — requests still resolve, via `withCanopy`'s auto-prefixed `/assets/:path*`
|
|
30
|
-
* rewrite, but bypass the CDN cache and the dedicated transform Lambda. Workaround: set
|
|
31
|
-
* `media.publicBaseUrl` to the distribution origin instead, which takes precedence here.
|
|
15
|
+
* Where the editor loads asset bytes from: the authenticated raw route under `basePath`, never the
|
|
16
|
+
* public `/assets` space. An editor asks for derivatives no build has seen (a fresh crop, a
|
|
17
|
+
* thumbnail), which the public path need not serve, and the raw route computes them on demand.
|
|
18
|
+
* Also handed to the live preview, through `PreviewFrame`.
|
|
32
19
|
*/
|
|
33
|
-
export function
|
|
34
|
-
|
|
35
|
-
|
|
20
|
+
export function authenticatedAssetBase(basePath) {
|
|
21
|
+
return joinUrlPrefix(basePath, RAW_ASSET_ROUTE);
|
|
22
|
+
}
|
|
23
|
+
/** No provider means no `basePath`: the API is mounted at the origin root. */
|
|
24
|
+
const AssetContext = createContext({ baseUrl: authenticatedAssetBase() });
|
|
25
|
+
export function AssetContextProvider({ basePath, children }) {
|
|
26
|
+
const value = useMemo(() => ({ baseUrl: authenticatedAssetBase(basePath) }), [basePath]);
|
|
36
27
|
return _jsx(AssetContext.Provider, { value: value, children: children });
|
|
37
28
|
}
|
|
38
|
-
/** Safe to call outside a provider -
|
|
29
|
+
/** Safe to call outside a provider - see the context's default. */
|
|
39
30
|
export function useAssetContext() {
|
|
40
31
|
return useContext(AssetContext);
|
|
41
32
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { ApiClientProvider, useApiClient, useOptionalApiClient, type ApiClient, type ApiClientProviderProps, } from './ApiClientContext.js';
|
|
2
|
-
export { AssetContextProvider, useAssetContext, type AssetContextValue, type AssetContextProviderProps, } from './AssetContext.js';
|
|
2
|
+
export { AssetContextProvider, authenticatedAssetBase, useAssetContext, type AssetContextValue, type AssetContextProviderProps, } from './AssetContext.js';
|
|
3
3
|
export { SWRProvider, type SWRProviderProps } from './SWRProvider.js';
|
|
4
4
|
export { useEditorLoading, useEditorModals, useEditorPreview } from './EditorStateContext.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { ApiClientProvider, useApiClient, useOptionalApiClient, } from './ApiClientContext.js';
|
|
2
|
-
export { AssetContextProvider, useAssetContext, } from './AssetContext.js';
|
|
2
|
+
export { AssetContextProvider, authenticatedAssetBase, useAssetContext, } from './AssetContext.js';
|
|
3
3
|
export { SWRProvider } from './SWRProvider.js';
|
|
4
4
|
export { useEditorLoading, useEditorModals, useEditorPreview } from './EditorStateContext.js';
|