canopycms 0.0.67 → 0.0.68-int.101
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 +24 -17
- 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 +118 -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 +19 -1
- package/dist/api/github-sync.js +57 -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/finalize.js +3 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +153 -0
- package/dist/assets/materialize.js +402 -0
- package/dist/assets/store-local.d.ts +19 -5
- package/dist/assets/store-local.js +69 -10
- package/dist/assets/store-s3.d.ts +38 -7
- package/dist/assets/store-s3.js +128 -22
- 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 +31 -4
- 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 +52 -0
- package/dist/cli/asset-refs.js +194 -0
- package/dist/cli/cli.d.ts +15 -1
- package/dist/cli/cli.js +5928 -2195
- package/dist/cli/configured-asset-store.d.ts +12 -0
- package/dist/cli/configured-asset-store.js +46 -0
- package/dist/cli/generate-ai-content.js +2185 -587
- 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/client.d.ts +1 -1
- package/dist/client.js +1 -1
- 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 +32 -22
- package/dist/editor/FormRenderer.js +34 -18
- package/dist/editor/PreviewFrame.d.ts +24 -0
- package/dist/editor/PreviewFrame.js +140 -0
- 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 +21 -0
- package/dist/editor/hooks/useBranchManager.js +204 -37
- 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 +9 -21
- package/dist/editor/preview-bridge.js +19 -139
- 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 +15 -0
- package/dist/github-service.js +19 -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/preview.d.ts +8 -0
- package/dist/preview.js +7 -0
- 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 +64 -9
- package/dist/server.js +48 -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-serialize.d.ts +5 -2
- package/dist/utils/content-serialize.js +98 -24
- 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/utils/yaml-source-splice.d.ts +58 -0
- package/dist/utils/yaml-source-splice.js +515 -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 +99 -14
- package/dist/worker/worker-context.d.ts +6 -1
- package/package.json +8 -2
package/dist/editor/Editor.js
CHANGED
|
@@ -6,10 +6,11 @@ import { IconChevronDown, IconChevronUp, IconDots, IconFolderPlus, IconPlus, } f
|
|
|
6
6
|
import { notifications } from '@mantine/notifications';
|
|
7
7
|
import { EntryNavigator } from './EntryNavigator.js';
|
|
8
8
|
import { FormRenderer } from './FormRenderer.js';
|
|
9
|
-
import { PreviewFrame } from './
|
|
9
|
+
import { PreviewFrame } from './PreviewFrame.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
|
|
@@ -628,7 +634,13 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
628
634
|
// revalidateOnFocus false, and useBranchesData's refreshInterval
|
|
629
635
|
// is 0 without data. Reloading is idempotent.
|
|
630
636
|
loadBranches().catch(console.error);
|
|
631
|
-
}, onCommentsPanelOpen: () => setCommentsPanelOpen(true), onSave: handleSave, onSubmit: () =>
|
|
637
|
+
}, onCommentsPanelOpen: () => setCommentsPanelOpen(true), onSave: handleSave, onSubmit: () => {
|
|
638
|
+
if (branchNameState)
|
|
639
|
+
handleSubmit(branchNameState).catch(console.error);
|
|
640
|
+
}, onWithdraw: () => {
|
|
641
|
+
if (branchNameState)
|
|
642
|
+
handleWithdraw(branchNameState).catch(console.error);
|
|
643
|
+
} }), _jsx(Box, { style: {
|
|
632
644
|
paddingTop: headerHeight,
|
|
633
645
|
paddingBottom: footerHeight,
|
|
634
646
|
paddingRight: sidebarWidth,
|
|
@@ -644,7 +656,7 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
644
656
|
? 'Setting up your branch workspace…'
|
|
645
657
|
: entriesInitializing
|
|
646
658
|
? 'Loading content…'
|
|
647
|
-
: 'Select an item to start editing.' })) : currentEntry.canEdit === false ? (_jsx(
|
|
659
|
+
: '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
660
|
const contentId = currentEntry?.contentId;
|
|
649
661
|
if (contentId) {
|
|
650
662
|
setDrafts((prev) => ({ ...prev, [contentId]: next }));
|
|
@@ -662,7 +674,7 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
662
674
|
})), selectedPath: selectedPath, onSelect: (id) => {
|
|
663
675
|
setSelectedPath(id);
|
|
664
676
|
setNavigatorOpen(false);
|
|
665
|
-
}, onTreeControllerReady: handleTreeControllerReady, expandedStateRef: treeExpandedStateRef, onExpandedStateChange: handleExpandedStateChange, onDeleteEntry: handleDeleteEntry, onRenameEntry: handleRenameEntry, onReorderEntry: handleReorderEntry, hiddenRootPath: hiddenRootPath, loading: entriesInitializing, readOnly: branchContentLocked }) }) })] })] }), _jsx(
|
|
677
|
+
}, 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
678
|
try {
|
|
667
679
|
await handleBranchChange(name);
|
|
668
680
|
setBranchManagerOpen(false);
|
|
@@ -671,9 +683,7 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
671
683
|
console.error('Branch change failed or was cancelled:', err);
|
|
672
684
|
// Don't close branch manager if there was an error or user cancelled
|
|
673
685
|
}
|
|
674
|
-
}, onCreate: (
|
|
675
|
-
handleCreateBranch(branch).catch((err) => console.error(err));
|
|
676
|
-
}, onSubmit: (name) => {
|
|
686
|
+
}, onBeforeCreate: confirmCreateBranch, onCreate: handleCreateBranch, onSubmit: (name) => {
|
|
677
687
|
handleSubmit(name).catch((err) => console.error(err));
|
|
678
688
|
}, onWithdraw: (name) => {
|
|
679
689
|
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
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Lightweight iframe wrapper to keep the preview in sync with form state.
|
|
4
|
+
* It posts the latest draft data to the iframe after load and when data changes.
|
|
5
|
+
*/
|
|
6
|
+
export declare const PreviewFrame: ({ src, path, data, isLoading, className, style, highlightEnabled, onPreviewError, assetBase, }: {
|
|
7
|
+
src: string;
|
|
8
|
+
path: string;
|
|
9
|
+
data?: unknown;
|
|
10
|
+
isLoading?: unknown;
|
|
11
|
+
className?: string;
|
|
12
|
+
style?: CSSProperties;
|
|
13
|
+
highlightEnabled?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The editor's authenticated asset route, a same-origin path, so it is sent with drafts only to
|
|
16
|
+
* a same-origin preview: another origin would resolve it against itself.
|
|
17
|
+
*/
|
|
18
|
+
assetBase?: string;
|
|
19
|
+
/** Called when the preview reports a draft compile/render error; null clears it. */
|
|
20
|
+
onPreviewError?: (error: {
|
|
21
|
+
message: string;
|
|
22
|
+
fieldPath?: string;
|
|
23
|
+
} | null) => void;
|
|
24
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { CANOPY_PREVIEW_ERROR, CANOPY_PREVIEW_HIGHLIGHT, CANOPY_PREVIEW_MESSAGE, CANOPY_PREVIEW_READY, isOpaqueOrigin, resolveMessageOrigin, } from './preview-bridge.js';
|
|
5
|
+
const sendDraftUpdate = (iframe, message, targetOrigin) => {
|
|
6
|
+
if (!iframe?.contentWindow)
|
|
7
|
+
return;
|
|
8
|
+
const target = targetOrigin ?? resolveMessageOrigin(iframe.src);
|
|
9
|
+
if (isOpaqueOrigin(target))
|
|
10
|
+
return;
|
|
11
|
+
iframe.contentWindow.postMessage(message, target);
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Lightweight iframe wrapper to keep the preview in sync with form state.
|
|
15
|
+
* It posts the latest draft data to the iframe after load and when data changes.
|
|
16
|
+
*/
|
|
17
|
+
export const PreviewFrame = ({ src, path, data, isLoading, className, style, highlightEnabled, onPreviewError, assetBase, }) => {
|
|
18
|
+
const iframeRef = useRef(null);
|
|
19
|
+
// Pin the preview origin from the src prop: outbound messages target it (never '*'),
|
|
20
|
+
// and inbound messages must come from it. An iframe that navigates cross-origin
|
|
21
|
+
// silently stops participating in the bridge.
|
|
22
|
+
const previewOrigin = resolveMessageOrigin(src);
|
|
23
|
+
// Show progress bar while waiting for the preview's ready handshake.
|
|
24
|
+
const [syncPending, setSyncPending] = useState(data !== undefined);
|
|
25
|
+
// Reset when navigating to a different entry (src change = new iframe page load).
|
|
26
|
+
const [prevSrc, setPrevSrc] = useState(src);
|
|
27
|
+
if (src !== prevSrc) {
|
|
28
|
+
setPrevSrc(src);
|
|
29
|
+
setSyncPending(data !== undefined);
|
|
30
|
+
}
|
|
31
|
+
// Inject the progress bar keyframe animation once per page.
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const styleId = 'canopycms-preview-sync-style';
|
|
34
|
+
if (!document.getElementById(styleId)) {
|
|
35
|
+
const el = document.createElement('style');
|
|
36
|
+
el.id = styleId;
|
|
37
|
+
el.textContent = `@keyframes canopy-preview-sync { 0% { transform: translateX(-100%); } 100% { transform: translateX(250%); } }`;
|
|
38
|
+
document.head.appendChild(el);
|
|
39
|
+
}
|
|
40
|
+
}, []);
|
|
41
|
+
const post = () => {
|
|
42
|
+
if (data === undefined)
|
|
43
|
+
return;
|
|
44
|
+
const sameOrigin = previewOrigin === window.location.origin;
|
|
45
|
+
sendDraftUpdate(iframeRef.current, {
|
|
46
|
+
type: CANOPY_PREVIEW_MESSAGE,
|
|
47
|
+
path,
|
|
48
|
+
data,
|
|
49
|
+
isLoading,
|
|
50
|
+
...(assetBase !== undefined && sameOrigin ? { assetBase } : {}),
|
|
51
|
+
}, previewOrigin);
|
|
52
|
+
};
|
|
53
|
+
const postHighlight = () => {
|
|
54
|
+
if (!iframeRef.current?.contentWindow)
|
|
55
|
+
return;
|
|
56
|
+
if (isOpaqueOrigin(previewOrigin))
|
|
57
|
+
return;
|
|
58
|
+
const msg = {
|
|
59
|
+
type: CANOPY_PREVIEW_HIGHLIGHT,
|
|
60
|
+
enabled: Boolean(highlightEnabled),
|
|
61
|
+
};
|
|
62
|
+
iframeRef.current.contentWindow.postMessage(msg, previewOrigin);
|
|
63
|
+
};
|
|
64
|
+
// Keep refs pointing at the latest closures so the message handler below never goes stale.
|
|
65
|
+
const postRef = useRef(post);
|
|
66
|
+
const postHighlightRef = useRef(postHighlight);
|
|
67
|
+
const onPreviewErrorRef = useRef(onPreviewError);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
postRef.current = post;
|
|
70
|
+
postHighlightRef.current = postHighlight;
|
|
71
|
+
onPreviewErrorRef.current = onPreviewError;
|
|
72
|
+
});
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
post();
|
|
75
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- post is tracked via ref
|
|
76
|
+
}, [data, isLoading]);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
postHighlight();
|
|
79
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- postHighlight is tracked via ref
|
|
80
|
+
}, [highlightEnabled]);
|
|
81
|
+
// When the preview page's React effects have run and its message listener is ready,
|
|
82
|
+
// it sends CANOPY_PREVIEW_READY. We respond with the current data so the preview
|
|
83
|
+
// receives the draft even if it wasn't ready when onLoad fired.
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const handleMessage = (event) => {
|
|
86
|
+
if (!iframeRef.current || event.source !== iframeRef.current.contentWindow)
|
|
87
|
+
return;
|
|
88
|
+
// Source check alone is insufficient if the iframe navigated cross-origin.
|
|
89
|
+
if (isOpaqueOrigin(previewOrigin) || event.origin !== previewOrigin)
|
|
90
|
+
return;
|
|
91
|
+
const type = event.data?.type;
|
|
92
|
+
if (type === CANOPY_PREVIEW_READY) {
|
|
93
|
+
postRef.current();
|
|
94
|
+
postHighlightRef.current();
|
|
95
|
+
setSyncPending(false);
|
|
96
|
+
}
|
|
97
|
+
else if (type === CANOPY_PREVIEW_ERROR) {
|
|
98
|
+
const msg = event.data;
|
|
99
|
+
// Shape-check the payload: an adopter passing an Error object (instead of
|
|
100
|
+
// err.message) must not crash the editor when rendered as a React child.
|
|
101
|
+
if (msg.message != null && typeof msg.message !== 'string')
|
|
102
|
+
return;
|
|
103
|
+
const fieldPath = typeof msg.fieldPath === 'string' ? msg.fieldPath : undefined;
|
|
104
|
+
onPreviewErrorRef.current?.(msg.message == null
|
|
105
|
+
? null
|
|
106
|
+
: { message: msg.message, ...(fieldPath ? { fieldPath } : {}) });
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
window.addEventListener('message', handleMessage);
|
|
110
|
+
return () => window.removeEventListener('message', handleMessage);
|
|
111
|
+
}, [previewOrigin]);
|
|
112
|
+
return (_jsxs("div", { className: className, style: { position: 'relative', overflow: 'hidden', ...style }, children: [syncPending && (_jsx("div", { style: {
|
|
113
|
+
position: 'absolute',
|
|
114
|
+
top: 0,
|
|
115
|
+
left: 0,
|
|
116
|
+
right: 0,
|
|
117
|
+
height: 3,
|
|
118
|
+
zIndex: 1,
|
|
119
|
+
overflow: 'hidden',
|
|
120
|
+
}, children: _jsx("div", { style: {
|
|
121
|
+
position: 'absolute',
|
|
122
|
+
top: 0,
|
|
123
|
+
left: 0,
|
|
124
|
+
height: '100%',
|
|
125
|
+
width: '40%',
|
|
126
|
+
background: 'var(--mantine-color-blue-filled, #228be6)',
|
|
127
|
+
borderRadius: '0 2px 2px 0',
|
|
128
|
+
animation: 'canopy-preview-sync 1.5s ease-in-out infinite',
|
|
129
|
+
} }) })), _jsx("iframe", { ref: iframeRef, src: src, style: {
|
|
130
|
+
display: 'block',
|
|
131
|
+
position: 'absolute',
|
|
132
|
+
inset: 0,
|
|
133
|
+
width: '100%',
|
|
134
|
+
height: '100%',
|
|
135
|
+
border: 'none',
|
|
136
|
+
}, onLoad: () => {
|
|
137
|
+
post();
|
|
138
|
+
postHighlight();
|
|
139
|
+
} })] }));
|
|
140
|
+
};
|
|
@@ -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 }));
|