canopycms 0.1.0-int.115 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/CHANGELOG.md +267 -231
  2. package/README.md +0 -1
  3. package/dist/ai/references.js +3 -1
  4. package/dist/api/assets.js +2 -2
  5. package/dist/api/branch-merge.d.ts +1 -1
  6. package/dist/api/branch.js +1 -1
  7. package/dist/api/content.js +1 -1
  8. package/dist/api/entries-constants.d.ts +1 -1
  9. package/dist/api/entries-constants.js +1 -1
  10. package/dist/api/entries.d.ts +2 -2
  11. package/dist/api/permissions.d.ts +2 -2
  12. package/dist/api/types.d.ts +3 -2
  13. package/dist/cli/cli.js +16 -14
  14. package/dist/cli/generate-ai-content.js +7 -5
  15. package/dist/cli/init.js +4 -4
  16. package/dist/client.d.ts +0 -1
  17. package/dist/client.js +0 -1
  18. package/dist/config/schemas/config.d.ts +0 -5
  19. package/dist/config/schemas/config.js +0 -1
  20. package/dist/config/types.d.ts +0 -1
  21. package/dist/content-index-generation.d.ts +1 -1
  22. package/dist/content-index-generation.js +1 -1
  23. package/dist/content-listing.js +1 -1
  24. package/dist/content-reader.js +1 -1
  25. package/dist/content-store.d.ts +1 -1
  26. package/dist/content-store.js +1 -1
  27. package/dist/dev-content-watcher.d.ts +1 -1
  28. package/dist/dev-content-watcher.js +1 -1
  29. package/dist/editor/BranchManager.d.ts +2 -0
  30. package/dist/editor/BranchManager.js +5 -4
  31. package/dist/editor/CanopyEditor.d.ts +1 -1
  32. package/dist/editor/CanopyEditor.js +1 -2
  33. package/dist/editor/CommentsPanel.js +3 -6
  34. package/dist/editor/Editor.d.ts +0 -2
  35. package/dist/editor/Editor.js +329 -181
  36. package/dist/editor/EditorAuthGate.js +8 -3
  37. package/dist/editor/EntryNavigator.d.ts +10 -0
  38. package/dist/editor/EntryNavigator.js +51 -2
  39. package/dist/editor/FormHeader.js +1 -1
  40. package/dist/editor/PreviewFrame.d.ts +5 -1
  41. package/dist/editor/PreviewFrame.js +3 -2
  42. package/dist/editor/PreviewPane.d.ts +47 -0
  43. package/dist/editor/PreviewPane.js +147 -0
  44. package/dist/editor/PreviewToolbarTools.d.ts +22 -0
  45. package/dist/editor/PreviewToolbarTools.js +73 -0
  46. package/dist/editor/comments/form-comments.js +2 -1
  47. package/dist/editor/components/BranchLockBanner.d.ts +22 -0
  48. package/dist/editor/components/BranchLockBanner.js +39 -0
  49. package/dist/editor/components/BranchPill.d.ts +32 -0
  50. package/dist/editor/components/BranchPill.js +33 -0
  51. package/dist/editor/components/ConfirmDeleteModal.d.ts +3 -1
  52. package/dist/editor/components/ConfirmDeleteModal.js +3 -2
  53. package/dist/editor/components/EditorHeader.d.ts +50 -108
  54. package/dist/editor/components/EditorHeader.js +117 -138
  55. package/dist/editor/components/EditorRail.d.ts +49 -0
  56. package/dist/editor/components/EditorRail.js +65 -0
  57. package/dist/editor/components/EntryBreadcrumb.d.ts +33 -0
  58. package/dist/editor/components/EntryBreadcrumb.js +56 -0
  59. package/dist/editor/components/QuickOpenPalette.d.ts +1 -1
  60. package/dist/editor/components/QuickOpenPalette.js +3 -2
  61. package/dist/editor/components/RenameEntryModal.d.ts +3 -1
  62. package/dist/editor/components/RenameEntryModal.js +3 -2
  63. package/dist/editor/components/SaveAllResult.d.ts +25 -0
  64. package/dist/editor/components/SaveAllResult.js +86 -0
  65. package/dist/editor/components/SaveSplitButton.d.ts +30 -0
  66. package/dist/editor/components/SaveSplitButton.js +108 -0
  67. package/dist/editor/components/index.d.ts +2 -2
  68. package/dist/editor/components/index.js +2 -2
  69. package/dist/editor/context/EditorStateContext.d.ts +4 -4
  70. package/dist/editor/context/EditorStateContext.js +4 -4
  71. package/dist/editor/copy.d.ts +3 -0
  72. package/dist/editor/copy.js +3 -0
  73. package/dist/editor/editor-utils.d.ts +11 -6
  74. package/dist/editor/editor-utils.js +8 -17
  75. package/dist/editor/fields/MarkdownField.js +3 -0
  76. package/dist/editor/hooks/save-all.d.ts +68 -0
  77. package/dist/editor/hooks/save-all.js +61 -0
  78. package/dist/editor/hooks/useCommentSystem.d.ts +2 -0
  79. package/dist/editor/hooks/useCommentSystem.js +3 -0
  80. package/dist/editor/hooks/useDraftManager.d.ts +24 -1
  81. package/dist/editor/hooks/useDraftManager.js +297 -16
  82. package/dist/editor/hooks/useEditorLayout.d.ts +15 -11
  83. package/dist/editor/hooks/useEditorLayout.js +13 -25
  84. package/dist/editor/hooks/useEntryManager.d.ts +4 -0
  85. package/dist/editor/hooks/useEntryManager.js +30 -7
  86. package/dist/editor/media/MediaLibrary.d.ts +1 -1
  87. package/dist/editor/preview-path.d.ts +12 -0
  88. package/dist/editor/preview-path.js +45 -1
  89. package/dist/editor/preview-scale.d.ts +27 -0
  90. package/dist/editor/preview-scale.js +34 -0
  91. package/dist/editor/shell/ActivityRail.d.ts +37 -0
  92. package/dist/editor/shell/ActivityRail.js +91 -0
  93. package/dist/editor/shell/EditorShell.d.ts +17 -0
  94. package/dist/editor/shell/EditorShell.js +31 -0
  95. package/dist/editor/shell/PaneLayout.d.ts +28 -0
  96. package/dist/editor/shell/PaneLayout.js +116 -0
  97. package/dist/editor/shell/SidePanelHost.d.ts +35 -0
  98. package/dist/editor/shell/SidePanelHost.js +184 -0
  99. package/dist/editor/shell/TopBar.d.ts +12 -0
  100. package/dist/editor/shell/TopBar.js +14 -0
  101. package/dist/editor/shell/host-chrome.d.ts +16 -0
  102. package/dist/editor/shell/host-chrome.js +69 -0
  103. package/dist/editor/theme.js +18 -3
  104. package/dist/git-manager.d.ts +1 -1
  105. package/dist/git-manager.js +2 -2
  106. package/dist/operating-mode/types.d.ts +1 -1
  107. package/dist/resolve-canopy-user.d.ts +1 -1
  108. package/dist/resolve-canopy-user.js +1 -1
  109. package/dist/schema/resolver.d.ts +1 -1
  110. package/dist/schema/resolver.js +1 -1
  111. package/dist/schema/schema-store.d.ts +1 -1
  112. package/dist/schema/schema-store.js +2 -2
  113. package/dist/url-collision.js +3 -3
  114. package/dist/url-exclusivity-fixtures.d.ts +1 -1
  115. package/dist/url-exclusivity-fixtures.js +1 -1
  116. package/dist/utils/content-write-lock.d.ts +1 -1
  117. package/dist/utils/content-write-lock.js +1 -1
  118. package/dist/utils/debug.d.ts +1 -1
  119. package/dist/utils/debug.js +5 -3
  120. package/dist/utils/git.d.ts +4 -0
  121. package/dist/utils/git.js +27 -5
  122. package/dist/validation/block-structural-keys.d.ts +1 -1
  123. package/dist/validation/block-structural-keys.js +1 -1
  124. package/dist/version.d.ts +1 -1
  125. package/dist/version.js +1 -1
  126. package/dist/worker/cms-worker.d.ts +1 -1
  127. package/dist/worker/cms-worker.js +2 -2
  128. package/dist/worker/github-auth.d.ts +1 -1
  129. package/dist/worker/github-auth.js +1 -1
  130. package/dist/worker/github-gateway-wire.d.ts +2 -1
  131. package/dist/worker/github-gateway-wire.js +7 -3
  132. package/dist/worker/github-gateway.js +1 -1
  133. package/dist/worker/github-mirror.d.ts +2 -2
  134. package/dist/worker/github-mirror.js +2 -2
  135. package/dist/worker/history-rewrite.d.ts +1 -1
  136. package/dist/worker/history-rewrite.js +1 -1
  137. package/dist/worker/self-checks.d.ts +1 -1
  138. package/dist/worker/shared-repo-git.js +1 -1
  139. package/package.json +5 -4
  140. package/dist/editor/EditorPanes.d.ts +0 -20
  141. package/dist/editor/EditorPanes.js +0 -124
  142. package/dist/editor/components/EditorFooter.d.ts +0 -11
  143. package/dist/editor/components/EditorFooter.js +0 -9
  144. package/dist/editor/components/EditorSidebar.d.ts +0 -70
  145. package/dist/editor/components/EditorSidebar.js +0 -50
@@ -1,14 +1,18 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useMemo, useState, useRef } from 'react';
4
- import { ActionIcon, Alert, Box, Button, Drawer, Group, Menu, Paper, Skeleton, Stack, Text, Tooltip, } from '@mantine/core';
4
+ import { ActionIcon, Alert, Box, Button, Menu, Paper, Skeleton, Stack, Text, Tooltip, } from '@mantine/core';
5
5
  import { IconChevronDown, IconChevronUp, IconDots, IconFolderPlus, IconPlus, IconSearch, } from '@tabler/icons-react';
6
+ import { useMediaQuery } from '@mantine/hooks';
6
7
  import { notifications } from '@mantine/notifications';
7
8
  const noop = () => { };
8
9
  import { EntryNavigator } from './EntryNavigator.js';
9
10
  import { FormRenderer } from './FormRenderer.js';
10
- import { PreviewFrame } from './PreviewFrame.js';
11
- import { EditorPanes } from './EditorPanes.js';
11
+ import { PreviewPane } from './PreviewPane.js';
12
+ import { clientOperatingStrategy } from '../operating-mode/client.js';
13
+ import { EditorShell } from './shell/EditorShell.js';
14
+ import { PaneLayout } from './shell/PaneLayout.js';
15
+ import { SidePanelHost } from './shell/SidePanelHost.js';
12
16
  import { CanopyCMSProvider } from './theme.js';
13
17
  import { BranchManager } from './BranchManager.js';
14
18
  import { BranchesDrawer } from './components/BranchesDrawer.js';
@@ -22,16 +26,18 @@ import { fieldBreadcrumb } from './field-breadcrumb.js';
22
26
  import { buildPreviewRoute, buildPreviewSrc, buildCollectionLabels, entryStatusLabel, entryTypeLabel, buildBreadcrumbSegments, } from './editor-utils.js';
23
27
  import { useEditorLayout, useDraftManager, useEntryManager, useGroupManager, usePermissionManager, useCommentSystem, useBranchManager, useUserContext, useSchemaManager, } from './hooks/index.js';
24
28
  import { useBranchActions } from './hooks/useBranchActions.js';
25
- import { editorLayoutStorageKey } from './hooks/useEditorLayout.js';
29
+ import { CONTENT_PANEL_WIDTH_MAX, CONTENT_PANEL_WIDTH_MIN, editorLayoutStorageKey, } from './hooks/useEditorLayout.js';
26
30
  import { EntrySchemaUnavailableError } from './hooks/useEntryManager.js';
27
31
  import { useEntryLinkResolution } from './hooks/useEntryLinkResolution.js';
28
32
  import { useReferenceResolution } from './hooks/useReferenceResolution.js';
29
33
  import { usePreviewMarks } from './hooks/usePreviewMarks.js';
30
- import { EditorFooter, EditorHeader, EditorSidebar } from './components/index.js';
34
+ import { BranchLockBanner, EditorHeader, EditorRail } from './components/index.js';
35
+ import { showSaveAllResult } from './components/SaveAllResult.js';
31
36
  import { RenameEntryModal } from './components/RenameEntryModal.js';
32
- import { QUICK_OPEN_REPLACES_ATTR, QuickOpenPalette } from './components/QuickOpenPalette.js';
33
- import { FOCUS_RETURN_ATTR, focusIfIdle, rememberOpener, returnTarget } from './utils/focus-return.js';
37
+ import { QuickOpenPalette } from './components/QuickOpenPalette.js';
38
+ import { rememberOpener, returnTarget } from './utils/focus-return.js';
34
39
  import { modKeyLabel } from './platform.js';
40
+ import { EDITOR_ACTIONS } from './copy.js';
35
41
  import { EntryCreateModal } from './components/EntryCreateModal.js';
36
42
  import { ConfirmDeleteModal } from './components/ConfirmDeleteModal.js';
37
43
  import { ReferencedByList, referencedDeleteMessage } from './components/ReferencedByList.js';
@@ -57,13 +63,14 @@ import { MayHaveLandedError } from './unanswered.js';
57
63
  // Stable fallbacks, so reference resolution's memo and effect don't re-run on every render
58
64
  // while no entry is open.
59
65
  const EMPTY_SCHEMA = [];
66
+ const CONTENT_PANEL_ID = 'canopy-content-panel';
60
67
  const EMPTY_VALUE = {};
61
68
  /**
62
69
  * High-level editor wrapper that wires entry navigation, form rendering,
63
70
  * saving/loading, and preview rendering using entry definitions.
64
71
  */
65
72
  export const Editor = (props) => (_jsx(EditorCrashBoundary, { themeOptions: props.themeOptions, children: _jsx(EditorContent, { ...props }) }));
66
- const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtitle = subtitle, branchName = '', collections, contentRoot, entryLinkUrl, initialSelectedId, initialValues, renderPreview, onCreateEntry, themeOptions, operatingMode, previewBaseByCollection, previewPrefix, mdxAllow, currentUser = 'current-user', canResolveComments = true, basePath, AccountComponent, onAccountClick, onLogoutClick, customRenderers, }) => {
73
+ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', collections, contentRoot, entryLinkUrl, initialSelectedId, initialValues, renderPreview, onCreateEntry, themeOptions, operatingMode, previewBaseByCollection, previewPrefix, mdxAllow, currentUser = 'current-user', canResolveComments = true, basePath, AccountComponent, onAccountClick, onLogoutClick, customRenderers, }) => {
67
74
  const [branchesLoading, setBranchesLoading] = useState(false);
68
75
  const [entriesLoading, setEntriesLoading] = useState(false);
69
76
  const [commentsLoading] = useState(false);
@@ -87,13 +94,14 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
87
94
  // Each open or close of the collection dialog moves it on, so a slow open answers only if current.
88
95
  const collectionOpeningRef = useRef(0);
89
96
  const [quickOpenOpen, setQuickOpenOpen] = useState(false);
90
- const [quickOpenFromDrawer, setQuickOpenFromDrawer] = useState(false);
97
+ const [quickOpenFromOverlay, setQuickOpenFromOverlay] = useState(false);
91
98
  const navigatorToggleRef = useRef(null);
99
+ const mediaButtonRef = useRef(null);
92
100
  const branchToggleRef = useRef(null);
93
101
  const settingsToggleRef = useRef(null);
94
102
  // Null when the branches drawer opened from a menu item; see `rememberOpener`.
95
103
  const branchOpenerRef = useRef(null);
96
- // Null when the drawer opened from a menu item, which unmounts on click, or with focus on the page.
104
+ // Null when the panel opened from a menu item, which unmounts on click, or with focus on the page.
97
105
  const navigatorOpenerRef = useRef(null);
98
106
  const quickOpenOpenRef = useRef(false);
99
107
  useEffect(() => {
@@ -107,9 +115,14 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
107
115
  const [deletingCollectionPath, setDeletingCollectionPath] = useState(null);
108
116
  const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false);
109
117
  const [deletingEntryPath, setDeletingEntryPath] = useState(null);
118
+ // Named at open: a delete that lands takes the entry out of the list while the dialog fades.
119
+ const [deletingEntryLabel, setDeletingEntryLabel] = useState();
110
120
  // Set once the server refuses a delete because other entries reference the entry.
111
121
  const [deletingEntryReferencedBy, setDeletingEntryReferencedBy] = useState(null);
112
122
  const [deleteInProgress, setDeleteInProgress] = useState(false);
123
+ // A rename or delete asked for from the top bar's entry menu, whose item is gone once it acts.
124
+ const [entryChangeFromMenu, setEntryChangeFromMenu] = useState(false);
125
+ const allContentCrumbRef = useRef(null);
113
126
  // Draft compile/render error reported by the preview page (null = renders cleanly)
114
127
  const [previewError, setPreviewError] = useState(null);
115
128
  const apiClient = useApiClient();
@@ -125,7 +138,14 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
125
138
  const canResolveAnyComment = canResolveComments && isReviewer(userContext?.groups);
126
139
  // The signed-in user, so "started by me" matches the thread authors the server records.
127
140
  const commentUserId = userContext?.userId ?? currentUser;
128
- const { layout, setLayout, highlightEnabled, setHighlightEnabled, sideSplitPercent, stackedSplitPercent, setSplitPercent, headerRef, headerHeight, } = useEditorLayout({ storageKey: editorLayoutStorageKey(basePath) });
141
+ // Below 64em (1024px) the editor starts stacked and the content panel never docks.
142
+ const wideWindow = useMediaQuery('(min-width: 64em)', true, { getInitialValueInEffect: false }) !== false;
143
+ const { loaded: layoutLoaded, layout, setLayout, highlightEnabled, setHighlightEnabled, sideSplitPercent, stackedSplitPercent, setSplitPercent, previewWidth, setPreviewWidth, contentPanelOpen, setContentPanelOpen, contentPanelWidth, setContentPanelWidth, } = useEditorLayout({
144
+ storageKey: editorLayoutStorageKey(basePath),
145
+ defaultLayout: wideWindow ? 'side' : 'stacked',
146
+ });
147
+ // In the stacked layout the content panel lays over the form only, so the preview stays in view.
148
+ const contentPanelDocked = layout === 'side' && wideWindow;
129
149
  // Comments state (shared between useCommentSystem and useBranchManager)
130
150
  const [commentsForBranchSummaries, setCommentsForBranchSummaries] = useState([]);
131
151
  // 1. Branch manager (provides branchNameState, no dependencies)
@@ -156,7 +176,7 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
156
176
  // loading state during that window anyway.
157
177
  const branchContentLocked = currentBranch?.writeBlocked ?? true;
158
178
  // 2. Entry manager (depends on branchNameState, owns selectedPath)
159
- const { selectedPath, setSelectedPath, entries: entriesState, collections: collectionsFromApi, currentEntry, availableSchemas, entriesInitializing, navigatorOpen, setNavigatorOpen, refreshEntries, refreshContent, handleCreateEntry, renameEntry, loadEntry, readEntryValue, saveEntry, getEntryVersion, adoptEntryVersion, followMovedEntry, recreateEntry, createModalOpen, createModalCollection, createModalError, createModalCreating, createModalExistingSlugs, handleCreateModalSubmit, closeCreateModal, } = useEntryManager({
179
+ const { selectedPath, setSelectedPath, closeEntry, entries: entriesState, collections: collectionsFromApi, currentEntry, availableSchemas, entriesInitializing, entriesLoadFailed, navigatorOpen, setNavigatorOpen, refreshEntries, refreshContent, handleCreateEntry, renameEntry, loadEntry, readEntryValue, saveEntry, getEntryVersion, adoptEntryVersion, followMovedEntry, recreateEntry, createModalOpen, createModalCollection, createModalError, createModalCreating, createModalExistingSlugs, handleCreateModalSubmit, closeCreateModal, } = useEntryManager({
160
180
  initialEntries: entries,
161
181
  initialSelectedId,
162
182
  branchName: branchNameState,
@@ -170,6 +190,28 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
170
190
  }),
171
191
  setBusy: setEntriesLoading,
172
192
  });
193
+ // The content overlay and a maximized preview hide the form; `navigatorOpen` is only the
194
+ // overlay's. Anything that focuses something in the form reveals it first. Opening an entry
195
+ // closes the overlay only, so the preview stays maximized across entries (see
196
+ // `previewShownMaximized` for the loads in between).
197
+ const [previewMaximized, setPreviewMaximized] = useState(false);
198
+ const revealForm = () => {
199
+ setNavigatorOpen(false);
200
+ setPreviewMaximized(false);
201
+ };
202
+ const openEntry = (path) => {
203
+ setSelectedPath(path);
204
+ setNavigatorOpen(false);
205
+ };
206
+ // An open overlay docks open, as the URL's ask for no entry does at load; `navigatorOpen` is only
207
+ // the overlay's, so a docked panel never resurfaces as one. Waits for the stored layout, whose
208
+ // arrival could otherwise dock an overlay the stacked layout keeps.
209
+ useEffect(() => {
210
+ if (layoutLoaded && contentPanelDocked && navigatorOpen) {
211
+ setContentPanelOpen(true);
212
+ setNavigatorOpen(false);
213
+ }
214
+ }, [layoutLoaded, contentPanelDocked, navigatorOpen, setContentPanelOpen, setNavigatorOpen]);
173
215
  // Keeps the entry-type list referentially stable while the create modal shows
174
216
  // the same collection: a new array on every render would churn Mantine's
175
217
  // Select `data` identity and give EntryCreateModal's form-seeding effect a
@@ -193,7 +235,7 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
193
235
  setPreviewError(null);
194
236
  }, [currentEntry?.contentId]);
195
237
  // 3. Draft manager (depends on branchNameState, selectedPath from useEntryManager)
196
- const { drafts, setDrafts, loadedValues, setLoadedValues, loadedValue, effectiveValue, modifiedCount, editedFiles, handleSave, handleDiscardDrafts, handleDiscardFileDraft, handleReload, isSelectedDirty, resolveUnsaved, fieldErrors, draftStorageFailed, applySubmitStamps, } = useDraftManager({
238
+ const { drafts, setDrafts, loadedValues, setLoadedValues, loadedValue, effectiveValue, modifiedCount, editedFiles, handleSave, singleSaving, lastSaved, handleSaveAll, saveAllProgress, isSaveAllRunning, handleDiscardDrafts, handleDiscardFileDraft, handleReload, isSelectedDirty, resolveUnsaved, fieldErrors, draftStorageFailed, applySubmitStamps, } = useDraftManager({
197
239
  branchName: branchNameState,
198
240
  selectedPath,
199
241
  currentEntry,
@@ -207,6 +249,8 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
207
249
  followMovedEntry,
208
250
  recreateEntry,
209
251
  setBusy: setEntriesLoading,
252
+ // The errors are in the form.
253
+ onSaveBlocked: () => setPreviewMaximized(false),
210
254
  onSaved: () => {
211
255
  void refreshEntries(branchNameState).catch((err) => console.error('Failed to refresh entries after save', err));
212
256
  },
@@ -238,6 +282,7 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
238
282
  setBranchManagerOpen(open);
239
283
  },
240
284
  onCommentsChange: setCommentsForBranchSummaries,
285
+ onRevealForm: revealForm,
241
286
  });
242
287
  const { groupsData, groupsLoading, handleSaveGroups, handleSearchUsers, handleGetUserMetadata, handleSearchExternalGroups, } = useGroupManager({ isOpen: groupManagerOpen });
243
288
  // The comments and branches responses name their people, without email; a privileged lookup
@@ -330,6 +375,7 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
330
375
  return restored ? { ...prev, [contentId]: restored } : prev;
331
376
  });
332
377
  // The toast's Undo button unmounts with it, so focus goes to the restored row once rendered.
378
+ revealForm();
333
379
  requestAnimationFrame(() => {
334
380
  const target = findFieldTarget(restoredPath);
335
381
  if (target)
@@ -790,18 +836,18 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
790
836
  setDeleteInProgress(false);
791
837
  }
792
838
  };
793
- const handleDeleteEntry = (entryPath) => {
839
+ const handleDeleteEntry = (entryPath, fromMenu = false) => {
840
+ setEntryChangeFromMenu(fromMenu);
794
841
  setDeletingEntryPath(entryPath);
842
+ setDeletingEntryLabel(entriesState.find((e) => e.path === entryPath)?.label);
795
843
  setDeletingEntryReferencedBy(null);
796
844
  setDeleteEntryModalOpen(true);
797
845
  };
798
- const closeDeleteEntryModal = () => {
799
- setDeleteEntryModalOpen(false);
800
- setDeletingEntryPath(null);
801
- setDeletingEntryReferencedBy(null);
802
- };
846
+ // What the dialog names stays until the next delete, so its exit shows no change of text.
847
+ const closeDeleteEntryModal = () => setDeleteEntryModalOpen(false);
803
848
  const confirmDeleteEntry = async () => {
804
- if (!deletingEntryPath)
849
+ // Its button stays clickable while the closed dialog fades; that click deletes nothing.
850
+ if (!deleteEntryModalOpen || !deletingEntryPath)
805
851
  return;
806
852
  setDeleteInProgress(true);
807
853
  try {
@@ -813,35 +859,33 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
813
859
  setDeletingEntryReferencedBy(result.referencedBy);
814
860
  return;
815
861
  }
816
- if (result.ok && selectedPath === deletingEntryPath) {
817
- setSelectedPath('');
818
- }
862
+ if (result.ok && selectedPath === deletingEntryPath)
863
+ closeEntry();
819
864
  closeDeleteEntryModal();
820
865
  }
821
866
  finally {
822
867
  setDeleteInProgress(false);
823
868
  }
824
869
  };
825
- const handleRenameEntry = (entryPath) => {
826
- // Find the entry being renamed
870
+ const handleRenameEntry = (entryPath, fromMenu = false) => {
827
871
  const entry = entriesState.find((e) => e.path === entryPath);
828
872
  if (!entry) {
829
873
  notifications.show({ message: 'Entry not found', color: 'red' });
830
874
  return;
831
875
  }
876
+ setEntryChangeFromMenu(fromMenu);
832
877
  setRenamingEntry(entry);
833
878
  setRenameModalError(null);
834
879
  setRenameModalOpen(true);
835
880
  };
836
881
  const handleRenameSubmit = async (newSlug) => {
837
- if (!renamingEntry)
882
+ if (!renameModalOpen || !renamingEntry)
838
883
  return;
839
884
  setRenameModalSaving(true);
840
885
  setRenameModalError(null);
841
886
  try {
842
887
  await renameEntry(renamingEntry.path, newSlug);
843
888
  setRenameModalOpen(false);
844
- setRenamingEntry(null);
845
889
  }
846
890
  catch (err) {
847
891
  setRenameModalError({
@@ -989,25 +1033,83 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
989
1033
  userIsAdmin,
990
1034
  ]);
991
1035
  const quickOpenShortcut = modKeyLabel() === '⌘' ? '⌘P' : 'Ctrl+P';
1036
+ const quickOpenKeys = modKeyLabel() === '⌘' ? 'Meta+P' : 'Control+P';
1037
+ // Docked, the content panel's open state is the stored preference; as an overlay it is
1038
+ // `navigatorOpen`, which starts open when the URL asks for no entry.
1039
+ const contentPanelShown = contentPanelDocked ? contentPanelOpen : navigatorOpen;
1040
+ const setContentPanelShown = (open) => contentPanelDocked ? setContentPanelOpen(open) : setNavigatorOpen(open);
1041
+ // The overlay lives in the form pane, under a maximized preview.
1042
+ const contentOverlayShown = !contentPanelDocked && contentPanelShown;
1043
+ useEffect(() => {
1044
+ if (contentOverlayShown)
1045
+ setPreviewMaximized(false);
1046
+ }, [contentOverlayShown]);
1047
+ // Bumped on every ask for the panel, so one already open still takes focus.
1048
+ const [contentPanelFocusRequest, setContentPanelFocusRequest] = useState(0);
992
1049
  const openNavigator = () => {
993
1050
  navigatorOpenerRef.current = rememberOpener();
994
- setNavigatorOpen(true);
1051
+ // In the same commit as the overlay mounts, so its focus request meets no inert form panel.
1052
+ if (!contentPanelDocked)
1053
+ setPreviewMaximized(false);
1054
+ setContentPanelShown(true);
1055
+ setContentPanelFocusRequest((request) => request + 1);
1056
+ };
1057
+ // A collection crumb opens the panel as "All content" does, then its collection's row takes focus.
1058
+ const [collectionReveal, setCollectionReveal] = useState();
1059
+ // Only ever rises: the navigator serves each seq once, and the request is cleared once served.
1060
+ const revealSeqRef = useRef(0);
1061
+ const revealCollection = (path) => {
1062
+ openNavigator();
1063
+ revealSeqRef.current += 1;
1064
+ setCollectionReveal({ path, seq: revealSeqRef.current });
995
1065
  };
1066
+ // An ask the panel closed on before serving it must not fire when the panel next opens.
1067
+ useEffect(() => {
1068
+ if (!contentPanelShown)
1069
+ setCollectionReveal(undefined);
1070
+ }, [contentPanelShown]);
1071
+ const runSaveAll = () => {
1072
+ void handleSaveAll()
1073
+ .then((result) => {
1074
+ if (!result)
1075
+ return;
1076
+ showSaveAllResult(result, {
1077
+ // Focus goes to the entry's menu, where its reason's "Refresh from server" is.
1078
+ onOpenEntry: (path) => {
1079
+ openEntry(path);
1080
+ requestAnimationFrame(() => navigatorToggleRef.current?.focus());
1081
+ },
1082
+ onRetry: runSaveAll,
1083
+ });
1084
+ })
1085
+ .catch(console.error);
1086
+ };
1087
+ // The entry menu's trigger stands in for its item; once the entry is deleted, "All content".
1088
+ const entryChangeReturnTarget = entryChangeFromMenu
1089
+ ? () => navigatorToggleRef.current ?? allContentCrumbRef.current
1090
+ : undefined;
996
1091
  const navigatorReturnTarget = () => returnTarget(navigatorOpenerRef.current, () => navigatorToggleRef.current);
997
1092
  const branchReturnTarget = () => returnTarget(branchOpenerRef.current, () => branchToggleRef.current);
998
1093
  const settingsReturnTarget = () => settingsToggleRef.current;
1094
+ // The palette replaces the overlay panel; a docked one stays. Opened from inside the overlay,
1095
+ // whose tree then unmounts, it returns focus to the panel's opener.
999
1096
  const openQuickOpen = () => {
1000
- if (!quickOpenOpen)
1001
- setQuickOpenFromDrawer(navigatorOpen);
1002
- setNavigatorOpen(false);
1097
+ if (!quickOpenOpen) {
1098
+ const panel = document.getElementById(CONTENT_PANEL_ID);
1099
+ setQuickOpenFromOverlay(!contentPanelDocked && navigatorOpen && !!panel?.contains(document.activeElement));
1100
+ }
1101
+ if (!contentPanelDocked)
1102
+ setNavigatorOpen(false);
1003
1103
  setQuickOpenOpen(true);
1004
1104
  };
1005
1105
  const dirtyPaths = useMemo(() => new Set(editedFiles.map((file) => file.path)), [editedFiles]);
1006
1106
  // Slugs of the entries beside the one being renamed, for the duplicate check.
1007
1107
  const renameExistingSlugs = useMemo(() => new Set(entriesState
1008
- .filter((e) => e.collectionPath === renamingEntry?.collectionPath && e.slug)
1108
+ .filter((e) => e.collectionPath === renamingEntry?.collectionPath &&
1109
+ e.contentId !== renamingEntry?.contentId &&
1110
+ e.slug)
1009
1111
  .map((e) => e.slug)), [entriesState, renamingEntry]);
1010
- // Tree expansion state - persists across drawer close/open
1112
+ // Tree expansion state - persists across panel close/open
1011
1113
  const treeExpandedStateRef = useRef({});
1012
1114
  const treeControllerRef = useRef(null);
1013
1115
  const handleTreeControllerReady = (controller) => {
@@ -1060,15 +1162,32 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
1060
1162
  fields: schema,
1061
1163
  data: displayValue,
1062
1164
  });
1165
+ // Only the default preview has the toolbar that maximizes and restores it. With no value to
1166
+ // preview (an entry loading, or failed to load, with no draft) the form shows instead, and the
1167
+ // choice reapplies once a value arrives.
1168
+ const previewCanMaximize = !renderPreview && Boolean(currentEntry?.previewSrc) && !currentEntryUnavailable;
1169
+ const previewShownMaximized = previewMaximized && previewCanMaximize && Boolean(previewFrameData);
1170
+ useEffect(() => {
1171
+ if (!previewCanMaximize)
1172
+ setPreviewMaximized(false);
1173
+ }, [previewCanMaximize]);
1174
+ const [paneResizing, setPaneResizing] = useState(false);
1175
+ const previewErrorField = previewError?.fieldPath
1176
+ ? fieldBreadcrumb(schema, displayValue, previewError.fieldPath).join(' › ')
1177
+ : '';
1063
1178
  // An unavailable entry previews nothing: the site cannot read it, and a stored draft would
1064
1179
  // otherwise mount the frame at a page that answers not-found.
1065
- const defaultPreview = currentEntry?.previewSrc && previewFrameData && !currentEntryUnavailable ? (_jsxs(Box, { pos: "relative", w: "100%", h: "100%", children: [previewError && (_jsx(Alert, { color: "red", title: "Preview error", withCloseButton: true, onClose: () => setPreviewError(null), style: { position: 'absolute', top: 8, left: 8, right: 8, zIndex: 2 }, children: previewError.fieldPath
1066
- ? `${previewError.fieldPath}: ${previewError.message}`
1067
- : previewError.message })), _jsx(PreviewFrame, { src: currentEntry.previewSrc, path: currentEntry.previewSrc, data: previewFrameData, isLoading: previewLoadingState, style: {
1068
- width: '100%',
1069
- height: '100%',
1070
- border: '1px solid var(--mantine-color-default-border)',
1071
- }, highlightEnabled: highlightEnabled, onPreviewError: setPreviewError, onMarks: onPreviewMarks, assetBase: authenticatedAssetBase(basePath) })] })) : (_jsx(Paper, { withBorder: true, shadow: "xs", h: "100%", bg: "var(--mantine-color-body)", style: {
1180
+ const defaultPreview = currentEntry?.previewSrc && previewFrameData && !currentEntryUnavailable ? (_jsx(PreviewPane, { src: currentEntry.previewSrc, data: previewFrameData, isLoading: previewLoadingState, highlightEnabled: highlightEnabled, onPreviewError: setPreviewError, onMarks: onPreviewMarks, assetBase: authenticatedAssetBase(basePath), mount: { basePath, previewPrefix }, previewWidth: previewWidth, onPreviewWidthChange: setPreviewWidth, onHighlightToggle: () => {
1181
+ setHighlightEnabled(!highlightEnabled);
1182
+ clearPreviewMarks();
1183
+ }, marksNothing: previewMarksNothing, inexactMarks: previewInexactMarks, showMarkDiagnostics: userIsAdmin, layout: layout, maximized: previewShownMaximized, onMaximizedChange: (maximized) => {
1184
+ // A docked panel stays beside a maximized preview; an overlay one would sit inert under it.
1185
+ if (maximized && !contentPanelDocked)
1186
+ setNavigatorOpen(false);
1187
+ setPreviewMaximized(maximized);
1188
+ }, resizing: paneResizing, notice: previewError && (_jsx(Alert, { color: "red", title: "Preview error", withCloseButton: true, onClose: () => setPreviewError(null), radius: 0, children: previewErrorField
1189
+ ? `${previewErrorField}: ${previewError.message}`
1190
+ : previewError.message })) })) : (_jsx(Paper, { withBorder: true, shadow: "xs", h: "100%", bg: "var(--mantine-color-body)", style: {
1072
1191
  display: 'flex',
1073
1192
  alignItems: 'center',
1074
1193
  justifyContent: 'center',
@@ -1079,146 +1198,175 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
1079
1198
  : currentEntry && !currentEntry.previewSrc
1080
1199
  ? 'No preview for this entry.'
1081
1200
  : 'Select an item to start editing.' }) }));
1082
- const sidebarWidth = 64;
1083
- const footerHeight = 40;
1084
- const headerTitle = currentEntry?.label ?? currentEntry?.slug ?? title;
1085
1201
  const breadcrumbSegments = useMemo(() => buildBreadcrumbSegments(currentEntry, collectionLabels), [collectionLabels, currentEntry]);
1086
- return (_jsx(CanopyCMSProvider, { ...(themeOptions ?? {}), children: _jsx(AssetContextProvider, { basePath: basePath, children: _jsxs(Box, { bg: "var(--canopy-surface-subtle)", 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,
1087
- // A draft kept for an unavailable entry stays in storage but cannot be saved.
1088
- hasUnsavedChanges: !currentEntryUnavailable && !contentReadOnly && isSelectedDirty(), draftStorageFailed: draftStorageFailed, userContext: userContext, branchCreatedBy: currentBranch?.createdBy, branchAccess: currentBranch?.access,
1089
- // Fail-closed for the same reason branchContentLocked above is:
1090
- // isProtected/writeBlocked gate real actions in EditorHeader (hiding
1091
- // Submit, disabling Save), so a missing wire value must read as
1092
- // "protected"/"blocked", not "unprotected"/"unblocked".
1093
- // branchWriteBlocked reuses branchContentLocked itself rather than
1094
- // re-deriving `?? true` a second time here, so there is exactly one
1095
- // fail-closed computation of "are writes blocked" in this component.
1096
- branchIsProtected: currentBranch?.isProtected ?? true, branchReadOnly: currentBranch?.readOnly, branchWriteBlocked: branchContentLocked, onNavigatorOpen: openNavigator, navigatorToggleRef: navigatorToggleRef, branchToggleRef: branchToggleRef, onFileReload: () => handleReload(() => navigatorToggleRef.current), onFileDiscardDraft: () => handleDiscardFileDraft(() => navigatorToggleRef.current), onEntrySelect: setSelectedPath, onBranchReloadData: handleReloadBranchData, onBranchDiscardDrafts: () => handleDiscardDrafts(() => branchToggleRef.current), onBranchManagerOpen: () => {
1097
- branchOpenerRef.current = rememberOpener();
1098
- setBranchManagerOpen(true);
1099
- // Always retry, not only when branchless. Opening the manager is
1100
- // the one in-app retry for a failed branches fetch, and the fetch
1101
- // is terminal on its own: SWRProvider sets shouldRetryOnError and
1102
- // revalidateOnFocus false, and useBranchesData's refreshInterval
1103
- // is 0 without data. Reloading is idempotent.
1104
- loadBranches().catch(console.error);
1105
- }, onCommentsPanelOpen: () => setCommentsPanelOpen(true), onSave: currentEntryUnavailable ? noop : handleSave, onSubmit: () => {
1106
- if (branchNameState)
1107
- handleSubmit(branchNameState).catch(console.error);
1108
- }, onWithdraw: () => {
1109
- if (branchNameState)
1110
- handleWithdraw(branchNameState).catch(console.error);
1111
- } }), _jsx(Box, { style: {
1112
- paddingTop: headerHeight,
1113
- paddingBottom: footerHeight,
1114
- paddingRight: sidebarWidth,
1115
- minHeight: '100vh',
1116
- width: '100%',
1117
- }, children: _jsxs(Box, { style: {
1118
- height: `calc(100vh - ${headerHeight + footerHeight}px)`,
1119
- minHeight: 0,
1120
- position: 'relative',
1121
- }, children: [_jsx(Box, { style: { flex: 1, minHeight: 0, height: '100%', width: '100%' }, children: _jsx(EditorPanes, { layout: layout, onLayoutChange: (next) => setLayout(next), sideSplitPercent: sideSplitPercent, stackedSplitPercent: stackedSplitPercent, onSplitPercentChange: setSplitPercent, preview: renderPreview && currentEntry && !currentEntryUnavailable
1122
- ? renderPreview(currentEntry, displayValue)
1123
- : defaultPreview, form: !currentEntry ? (_jsx(CenteredMessage, { children: !branchNameState && busy
1124
- ? 'Setting up your branch workspace…'
1125
- : entriesInitializing
1126
- ? 'Loading content…'
1127
- : 'Select an item to start editing.' })) : currentEntryUnavailable ? (_jsx(UnavailableEntryNotice, { schemaRefs: currentEntry.unavailable ? [currentEntry.unavailable.schemaRef] : [] })) : currentEntry.canEdit === false ? (_jsx(NoEditPermissionNotice, { entryLabel: currentEntry.label })) : schema.length > 0 && displayValue ? (_jsx(EntryLinkContext.Provider, { value: entryLinkContextValue, children: _jsx(SiteMdxAllowContext.Provider, { value: mdxAllow, children: _jsx(FormRenderer, { notice: hiddenDraftNotice && (_jsx(ReadOnlyDraftNotice, { permanentlyReadOnly: (currentBranch?.isProtected ?? true) ||
1128
- currentBranch?.status === 'archived',
1129
- // A hidden draft's discard changes nothing on screen, so it says so.
1130
- onDiscard: () => handleDiscardFileDraft(() => navigatorToggleRef.current, {
1131
- announce: true,
1132
- }) })), fields: schema, value: displayValue, readOnly: contentReadOnly, onRemoved: handleFieldRemoved, commentBlockedReason: commentBlockedReason, removedItemFieldLabel: commentFieldLabel, onGetUserMetadata: getCommentUserMetadata, header: {
1133
- title: currentEntry.label,
1134
- typeLabel: entryTypeLabel(activeCollections, currentEntry),
1135
- },
1136
- // The one writer of drafts. Read-only content gets none, whatever a
1137
- // field emits (MDXEditor's mount-time normalisation included).
1138
- onChange: (next) => {
1139
- const contentId = currentEntry?.contentId;
1140
- if (contentId && !contentReadOnlyRef.current) {
1141
- // Every list item keeps a key, whatever field made it.
1142
- const keyed = withItemKeys(schema, next, 'random');
1143
- setDrafts((prev) => ({ ...prev, [contentId]: keyed }));
1144
- }
1145
- }, customRenderers: customRenderers, branch: branchNameState, comments: displayComments, currentEntryPath: selectedPath, currentUserId: commentUserId, canResolve: canResolveAnyComment, focusedFieldPath: focusedFieldPath, highlightThreadId: highlightThreadId, onAddComment: handleAddDisplayedComment, onResolveThread: handleResolveThread, fieldErrors: fieldErrors, format: currentEntry.format, conflictNotice: !!(currentEntry?.contentId &&
1146
- currentBranch?.conflictFiles?.includes(currentEntry.contentId)) }) }) })) : schema.length === 0 ? (_jsx(CenteredMessage, { children: "No fields to edit." })) : loadFailedKey !== `${branchNameState}:${currentEntry.contentId}` ? (_jsx(Stack, { gap: "lg", p: "md", "data-testid": "entry-loading-skeleton", children: [0, 1, 2].map((row) => (_jsxs(Stack, { gap: 6, children: [_jsx(Skeleton, { h: 12, w: 120 }), _jsx(Skeleton, { h: 36 })] }, row))) })) : (_jsx(Alert, { color: "red", variant: "light", m: "md", title: "This entry couldn't be loaded.", "data-testid": "entry-load-failed", children: _jsx(Button, { variant: "default", onClick: () => {
1147
- setLoadFailedKey(null);
1148
- setLoadAttempt((attempt) => attempt + 1);
1149
- }, children: "Retry" }) })) }) }), _jsx(EditorSidebar, { layout: layout, highlightEnabled: highlightEnabled, previewMarksNothing: previewMarksNothing, previewInexactMarks: previewInexactMarks, sidebarWidth: sidebarWidth, headerHeight: headerHeight, footerHeight: footerHeight, onLayoutChange: setLayout, onHighlightToggle: () => {
1150
- setHighlightEnabled(!highlightEnabled);
1151
- clearPreviewMarks();
1152
- }, onPermissionManagerOpen: userIsAdmin ? () => setPermissionManagerOpen(true) : undefined, onGroupManagerOpen: userIsAdmin ? () => setGroupManagerOpen(true) : undefined, onMediaLibraryOpen: () => setMediaLibraryOpen(true), onSystemHealthOpen: userIsAdmin ? () => setSystemHealthOpen(true) : undefined, settingsToggleRef: settingsToggleRef, AccountComponent: AccountComponent, onAccountClick: onAccountClick, onLogoutClick: onLogoutClick })] }) }), _jsx(EditorFooter, {}), _jsxs(Drawer.Root, { opened: navigatorOpen, onClose: () => setNavigatorOpen(false), returnFocus: false, onExitTransitionEnd: () => {
1153
- if (!quickOpenOpenRef.current)
1154
- focusIfIdle(navigatorReturnTarget());
1155
- },
1156
- // Mantine closes every open overlay on one Escape, so the drawer leaves it to a dialog
1157
- // opened from it.
1158
- closeOnEscape: !createModalOpen &&
1159
- !renameModalOpen &&
1160
- !deleteEntryModalOpen &&
1161
- !deleteCollectionModalOpen &&
1162
- !collectionEditorOpen, position: "left", size: 360, children: [_jsx(Drawer.Overlay, { blur: 2 }), _jsxs(Drawer.Content, { [QUICK_OPEN_REPLACES_ATTR]: true, [FOCUS_RETURN_ATTR]: true, children: [_jsxs(Drawer.Header, { children: [_jsx(Drawer.Title, { children: "Content" }), _jsxs(Group, { gap: "xs", children: [navCollections &&
1163
- navCollections.length > 0 &&
1164
- !branchContentLocked &&
1165
- (navCollections[0].onAdd || navCollections[0].onAddSubCollection) && (_jsxs(Menu, { shadow: "md", width: 200, withinPortal: true, position: "bottom-end", children: [_jsx(Menu.Target, { children: _jsx(ActionIcon, { variant: "subtle", color: "gray", "aria-label": "Content actions", children: _jsx(IconDots, { size: 16 }) }) }), _jsxs(Menu.Dropdown, { children: [navCollections[0].onAdd && (_jsx(Menu.Item, { leftSection: _jsx(IconPlus, { size: 14 }), onClick: () => navCollections[0].onAdd?.(), children: "Add entry\u2026" })), navCollections[0].onAddSubCollection && (_jsxs(_Fragment, { children: [navCollections[0].onAdd && _jsx(Menu.Divider, {}), _jsx(Menu.Label, { children: "Admin" }), _jsx(Menu.Item, { leftSection: _jsx(IconFolderPlus, { size: 14 }), onClick: () => navCollections[0].onAddSubCollection?.(), children: "Add collection\u2026" })] }))] })] })), _jsx(Tooltip, { label: `Quick open (${quickOpenShortcut})`, withinPortal: true, children: _jsx(ActionIcon, { variant: "subtle", color: "gray", onClick: openQuickOpen, "aria-label": "Quick open", "data-testid": "quick-open-button", children: _jsx(IconSearch, { size: 16 }) }) }), _jsx(Tooltip, { label: "Collapse all folders", withinPortal: true, children: _jsx(ActionIcon, { variant: "subtle", color: "gray", onClick: handleCollapseAll, "aria-label": "Collapse all folders", children: _jsx(IconChevronUp, { size: 16 }) }) }), _jsx(Tooltip, { label: "Expand all folders", withinPortal: true, children: _jsx(ActionIcon, { variant: "subtle", color: "gray", onClick: handleExpandAll, "aria-label": "Expand all folders", children: _jsx(IconChevronDown, { size: 16 }) }) }), _jsx(Drawer.CloseButton, {})] })] }), _jsx(Drawer.Body, { p: 0, children: _jsx(Box, { px: "md", children: _jsx(EntryNavigator, { collections: navCollections, items: navCollections
1166
- ? undefined
1167
- : entriesState.map((e) => ({
1168
- path: e.path,
1169
- label: e.label,
1170
- slug: e.slug,
1171
- status: entryStatusLabel(activeCollections, e),
1172
- readOnly: e.canEdit === false || undefined,
1173
- })), selectedPath: selectedPath, onSelect: (id) => {
1174
- setSelectedPath(id);
1175
- setNavigatorOpen(false);
1176
- }, onTreeControllerReady: handleTreeControllerReady, expandedStateRef: treeExpandedStateRef, onExpandedStateChange: handleExpandedStateChange, onDeleteEntry: handleDeleteEntry, onRenameEntry: handleRenameEntry, onReorderEntry: userIsAdmin ? handleReorderEntry : undefined, hiddenRootPath: hiddenRootPath, loading: entriesInitializing, readOnly: branchContentLocked, dirtyPaths: dirtyPaths }) }) })] })] }), _jsx(QuickOpenPalette, { opened: quickOpenOpen, onOpen: openQuickOpen, onClose: () => setQuickOpenOpen(false), returnFocusTo: quickOpenFromDrawer ? navigatorReturnTarget : undefined, entries: entriesState,
1177
- // The hidden root's entries sit at the top of the tree, so they get no heading here either.
1178
- collectionLabel: (path) => (path === hiddenRootPath ? '' : collectionLabels.get(path)), onSelect: setSelectedPath }), _jsx(BranchesDrawer, { opened: branchManagerOpen, onClose: () => setBranchManagerOpen(false), confirmOpen: branchConfirmOpen, returnFocusTo: branchReturnTarget, children: _jsx(BranchManager, { branches: branchSummaries, mode: operatingMode, user: userContext, currentBranchName: currentBranch?.name ?? branchNameState, onSelect: async (name) => {
1179
- try {
1180
- await handleBranchChange(name);
1181
- setBranchManagerOpen(false);
1182
- }
1183
- catch (err) {
1184
- console.error('Branch change failed or was cancelled:', err);
1185
- // Don't close branch manager if there was an error or user cancelled
1186
- }
1187
- }, onBeforeCreate: confirmCreateBranch, onCreate: handleCreateBranch, onSubmit: (name) => {
1188
- handleSubmit(name).catch((err) => console.error(err));
1189
- }, onWithdraw: (name) => {
1190
- handleWithdraw(name).catch((err) => console.error(err));
1191
- }, onRequestChanges: (name) => {
1192
- handleRequestChanges(name).catch((err) => console.error(err));
1193
- }, onDelete: (name) => {
1194
- handleDelete(name).catch((err) => console.error(err));
1195
- }, onClose: () => setBranchManagerOpen(false), comments: comments, currentUserId: commentUserId, canResolve: canResolveAnyComment, onAddComment: handleAddComment, onResolveThread: handleResolveThread, highlightThreadId: highlightThreadId, onGetUserMetadata: canResolveUserNames ? handleGetUserMetadata : undefined, onGetOwnerMetadata: getCommentUserMetadata, onGetCommentUserMetadata: getCommentUserMetadata }) }), branchNameState && (_jsx(CommentsPanel, { opened: commentsPanelOpen, branchName: branchNameState, comments: displayComments, canResolve: canResolveAnyComment, currentUserId: commentUserId, entryLabel: commentEntryLabel, fieldLabel: commentFieldLabel, onAddComment: handleAddDisplayedComment, onResolveThread: handleResolveThread, onClose: () => setCommentsPanelOpen(false), onJumpToField: jumpToThreadField, onGetUserMetadata: getCommentUserMetadata, onJumpToEntry: handleJumpToEntry, onJumpToBranch: handleJumpToBranch }, branchNameState)), userIsAdmin && (_jsx(GroupManager, { opened: groupManagerOpen, onClose: () => setGroupManagerOpen(false), returnFocusTo: settingsReturnTarget, internalGroups: groupsData, loading: groupsLoading, canEdit: userIsAdmin, onSave: handleSaveGroups, onSearchUsers: handleSearchUsers, onGetUserMetadata: handleGetUserMetadata, onSearchExternalGroups: handleSearchExternalGroups })), userIsAdmin && (_jsx(PermissionManager, { opened: permissionManagerOpen, onClose: () => setPermissionManagerOpen(false), returnFocusTo: settingsReturnTarget, collections: activeCollections, contentRoot: contentRoot, permissions: permissionsData, loading: permissionsLoading, canEdit: userIsAdmin, onSave: handleSavePermissions, onSearchUsers: handleSearchUsers, onGetUserMetadata: handleGetUserMetadata, onListGroups: handleListGroups })), userIsAdmin && (_jsx(SystemHealthPanel, { opened: systemHealthOpen, onClose: () => setSystemHealthOpen(false), returnFocusTo: settingsReturnTarget })), _jsx(MediaLibrary, { opened: mediaLibraryOpen, onClose: () => setMediaLibraryOpen(false), mode: "manage", returnFocusTo: settingsReturnTarget }), userIsAdmin && (_jsx(CollectionEditor, { isOpen: collectionEditorOpen, editingCollection: editingCollection, parentPath: collectionEditorParentPath, availableSchemas: availableSchemas, onSave: handleCollectionSave, onAddEntryType: editingCollection ? (path, entryType) => addEntryType(path, entryType) : undefined, onUpdateEntryType: editingCollection
1196
- ? (path, name, updates) => updateEntryType(path, name, updates)
1197
- : undefined, onRemoveEntryType: editingCollection ? (path, name) => removeEntryType(path, name) : undefined, onReloadEntryTypes: editingCollection ? readEntryTypes : undefined, onClose: handleCloseCollectionEditor, isSaving: schemaLoading, error: collectionEditorError, restored: restoredCollectionInput, onDiscardRestored: () => {
1198
- collectionDraftRef.current = null;
1199
- setRestoredCollectionInput(null);
1200
- } })), renamingEntry && (_jsx(RenameEntryModal, { isOpen: renameModalOpen, entryLabel: renamingEntry.label, currentSlug: renamingEntry.slug || '', onSave: handleRenameSubmit, onClose: () => {
1201
- setRenameModalOpen(false);
1202
- setRenamingEntry(null);
1203
- setRenameModalError(null);
1204
- }, isSaving: renameModalSaving, error: renameModalError?.mayHaveLanded ? null : renameModalError?.message, warning: renameModalError?.mayHaveLanded ? renameModalError.message : null, existingSlugs: renameExistingSlugs, previewPath: (slug) => previewPathFor({
1205
- collectionPath: renamingEntry.collectionPath ?? '',
1206
- collectionName: renamingEntry.collectionName,
1207
- slug,
1208
- entryType: renamingEntry.entryType,
1209
- }) })), createModalCollection && (_jsx(EntryCreateModal, { isOpen: createModalOpen, collectionLabel: createModalCollection.label || createModalCollection.name, entryTypes: createModalEntryTypes, onCreate: handleCreateModalSubmit, onClose: closeCreateModal, isCreating: createModalCreating, error: createModalError, existingSlugs: createModalExistingSlugs, previewPath: (slug, entryTypeName) => previewPathFor({
1210
- collectionPath: createModalCollection.path,
1211
- collectionName: createModalCollection.name,
1212
- slug,
1213
- entryType: entryTypeName,
1214
- }) })), _jsx(ConfirmDeleteModal, { isOpen: deleteCollectionModalOpen, title: "Delete Collection", message: "Are you sure you want to delete this collection? This cannot be undone.", confirmLabel: "Delete Collection", onConfirm: confirmDeleteCollection, onClose: () => {
1215
- setDeleteCollectionModalOpen(false);
1216
- setDeletingCollectionPath(null);
1217
- }, loading: deleteInProgress }), _jsx(ConfirmDeleteModal, { isOpen: deleteEntryModalOpen, title: "Delete Entry", message: deletingEntryReferencedBy
1218
- ? referencedDeleteMessage(deletingEntryReferencedBy)
1219
- : 'Are you sure you want to delete this entry? This cannot be undone.', confirmLabel: deletingEntryReferencedBy ? 'Delete anyway' : 'Delete Entry', onConfirm: confirmDeleteEntry, onClose: closeDeleteEntryModal, loading: deleteInProgress, children: deletingEntryReferencedBy && (_jsx(ReferencedByList, { referencedBy: deletingEntryReferencedBy, onOpenEntry: (entryPath) => {
1220
- closeDeleteEntryModal();
1221
- setSelectedPath(entryPath);
1222
- } })) })] }) }) }));
1202
+ // The browser tab names the open entry and the site.
1203
+ const tabTitle = currentEntry
1204
+ ? `${currentEntry.label ?? currentEntry.slug} · ${siteTitle}`
1205
+ : siteTitle;
1206
+ // The host page's title comes back on unmount only if the tab still shows the editor's: a route
1207
+ // navigated to may have set its own by then.
1208
+ const writtenTitleRef = useRef(null);
1209
+ useEffect(() => {
1210
+ const hostTitle = document.title;
1211
+ return () => {
1212
+ if (document.title === writtenTitleRef.current)
1213
+ document.title = hostTitle;
1214
+ };
1215
+ }, []);
1216
+ useEffect(() => {
1217
+ document.title = tabTitle;
1218
+ writtenTitleRef.current = tabTitle;
1219
+ }, [tabTitle]);
1220
+ const openBranchManager = () => {
1221
+ branchOpenerRef.current = rememberOpener();
1222
+ setBranchManagerOpen(true);
1223
+ // Always retry, not only when branchless. Opening the manager is
1224
+ // the one in-app retry for a failed branches fetch, and the fetch
1225
+ // is terminal on its own: SWRProvider sets shouldRetryOnError and
1226
+ // revalidateOnFocus false, and useBranchesData's refreshInterval
1227
+ // is 0 without data. Reloading is idempotent.
1228
+ loadBranches().catch(console.error);
1229
+ };
1230
+ const contentPanel = (_jsx(SidePanelHost, { id: CONTENT_PANEL_ID, title: "Content", mode: contentPanelDocked ? 'docked' : 'overlay', opened: contentPanelShown, width: contentPanelWidth, minWidth: CONTENT_PANEL_WIDTH_MIN, maxWidth: CONTENT_PANEL_WIDTH_MAX, onWidthChange: setContentPanelWidth, onClose: () => setContentPanelShown(false), focusRequest: contentPanelFocusRequest,
1231
+ // Quick open takes focus as it replaces the overlay.
1232
+ returnFocusTo: () => (quickOpenOpen ? null : navigatorReturnTarget()), "data-testid": "content-panel", actions: _jsxs(_Fragment, { children: [navCollections &&
1233
+ navCollections.length > 0 &&
1234
+ !branchContentLocked &&
1235
+ (navCollections[0].onAdd || navCollections[0].onAddSubCollection) && (_jsxs(Menu, { shadow: "md", width: 200, withinPortal: true, position: "bottom-end", children: [_jsx(Tooltip, { label: "Content actions", withinPortal: true, children: _jsx(Menu.Target, { children: _jsx(ActionIcon, { variant: "subtle", color: "gray", "aria-label": "Content actions", children: _jsx(IconDots, { size: 16 }) }) }) }), _jsxs(Menu.Dropdown, { children: [navCollections[0].onAdd && (_jsx(Menu.Item, { leftSection: _jsx(IconPlus, { size: 14 }), onClick: () => navCollections[0].onAdd?.(), children: "Add entry\u2026" })), navCollections[0].onAddSubCollection && (_jsxs(_Fragment, { children: [navCollections[0].onAdd && _jsx(Menu.Divider, {}), _jsx(Menu.Label, { children: "Admin" }), _jsx(Menu.Item, { leftSection: _jsx(IconFolderPlus, { size: 14 }), onClick: () => navCollections[0].onAddSubCollection?.(), children: "Add collection\u2026" })] }))] })] })), _jsx(Tooltip, { label: `${EDITOR_ACTIONS.openEntry} (${quickOpenShortcut})`, withinPortal: true, children: _jsx(ActionIcon, { variant: "subtle", color: "gray", onClick: openQuickOpen, "aria-label": EDITOR_ACTIONS.openEntry, "aria-keyshortcuts": quickOpenKeys, "data-testid": "quick-open-button", children: _jsx(IconSearch, { size: 16 }) }) }), _jsx(Tooltip, { label: "Collapse all folders", withinPortal: true, children: _jsx(ActionIcon, { variant: "subtle", color: "gray", onClick: handleCollapseAll, "aria-label": "Collapse all folders", children: _jsx(IconChevronUp, { size: 16 }) }) }), _jsx(Tooltip, { label: "Expand all folders", withinPortal: true, children: _jsx(ActionIcon, { variant: "subtle", color: "gray", onClick: handleExpandAll, "aria-label": "Expand all folders", children: _jsx(IconChevronDown, { size: 16 }) }) })] }), children: _jsx(Box, { px: "md", children: _jsx(EntryNavigator, { collections: navCollections, items: navCollections
1236
+ ? undefined
1237
+ : entriesState.map((e) => ({
1238
+ path: e.path,
1239
+ label: e.label,
1240
+ slug: e.slug,
1241
+ status: entryStatusLabel(activeCollections, e),
1242
+ readOnly: e.canEdit === false || undefined,
1243
+ })), selectedPath: selectedPath, onSelect: openEntry, onTreeControllerReady: handleTreeControllerReady, expandedStateRef: treeExpandedStateRef, onExpandedStateChange: handleExpandedStateChange, onDeleteEntry: handleDeleteEntry, onRenameEntry: handleRenameEntry, onReorderEntry: userIsAdmin ? handleReorderEntry : undefined, hiddenRootPath: hiddenRootPath, loading: entriesInitializing, readOnly: branchContentLocked, dirtyPaths: dirtyPaths, revealCollection: collectionReveal, onRevealed: () => setCollectionReveal(undefined) }) }) }));
1244
+ return (_jsx(CanopyCMSProvider, { ...(themeOptions ?? {}), children: _jsx(AssetContextProvider, { basePath: basePath, children: _jsx(EditorShell, { topBar: _jsx(EditorHeader, { currentEntry: currentEntry, branchName: branchNameState, operatingMode: operatingMode, branchStatus: currentBranch?.status, busy: busy, breadcrumbSegments: breadcrumbSegments, editedFiles: editedFiles, modifiedCount: modifiedCount,
1245
+ // A draft kept for an unavailable entry stays in storage but cannot be saved.
1246
+ hasUnsavedChanges: !currentEntryUnavailable && !contentReadOnly && isSelectedDirty(), entryHasDraft: isSelectedDirty(), entryReadOnly: currentEntry?.canEdit === false, draftStorageFailed: draftStorageFailed, userContext: userContext, branchCreatedBy: currentBranch?.createdBy, branchAccess: currentBranch?.access,
1247
+ // Fail-closed for the same reason branchContentLocked above is:
1248
+ // isProtected/writeBlocked gate real actions in EditorHeader (hiding
1249
+ // Submit, disabling Save), so a missing wire value must read as
1250
+ // "protected"/"blocked", not "unprotected"/"unblocked".
1251
+ // branchWriteBlocked reuses branchContentLocked itself rather than
1252
+ // re-deriving `?? true` a second time here, so there is exactly one
1253
+ // fail-closed computation of "are writes blocked" in this component.
1254
+ branchIsProtected: currentBranch?.isProtected ?? true, branchReadOnly: currentBranch?.readOnly, branchWriteBlocked: branchContentLocked,
1255
+ // A failed fetch leaves the list empty too; the banner says that one.
1256
+ branchLoading: !currentBranch && (branchesLoading || branchSummaries.length === 0), onNavigatorOpen: openNavigator, onCollectionOpen: revealCollection, navigatorToggleRef: navigatorToggleRef, branchToggleRef: branchToggleRef, onFileReload: () => handleReload(() => navigatorToggleRef.current), onFileDiscardDraft: () => handleDiscardFileDraft(() => navigatorToggleRef.current), onEntryRename: () => currentEntry && handleRenameEntry(currentEntry.path, true), onEntryDelete: () => currentEntry && handleDeleteEntry(currentEntry.path, true), allContentRef: allContentCrumbRef, entryUnavailable: currentEntryUnavailable, onEntrySelect: openEntry, onBranchReloadData: () => {
1257
+ void Promise.all([handleReloadBranchData(), refreshEntries(branchNameState)]).catch(console.error);
1258
+ }, onBranchDiscardDrafts: () => handleDiscardDrafts(() => branchToggleRef.current), onBranchManagerOpen: openBranchManager, onSave: currentEntryUnavailable ? noop : handleSave, onSaveAll: runSaveAll, saveAllProgress: saveAllProgress, saving: singleSaving?.branch === branchNameState &&
1259
+ singleSaving.contentId === currentEntry?.contentId, savedSignal: lastSaved?.seq, savedHere: lastSaved?.branch === branchNameState &&
1260
+ lastSaved.contentId === currentEntry?.contentId,
1261
+ // Save all finds each draft's entry in the list, so with none listed it would save nothing.
1262
+ entryListFailed: entriesLoadFailed && entriesState.length === 0, onSubmit: () => {
1263
+ if (branchNameState && !isSaveAllRunning()) {
1264
+ handleSubmit(branchNameState).catch(console.error);
1265
+ }
1266
+ }, onWithdraw: () => {
1267
+ if (branchNameState && !isSaveAllRunning()) {
1268
+ handleWithdraw(branchNameState).catch(console.error);
1269
+ }
1270
+ } }), banner: _jsx(BranchLockBanner, { branchName: branchNameState, branchStatus: currentBranch?.status, branchReadOnly: currentBranch?.readOnly, branchWriteBlocked: branchContentLocked, onBranchManagerOpen: openBranchManager }), rail: _jsx(EditorRail, { siteTitle: siteTitle, onHomeOpen: () => {
1271
+ const first = entriesState[0];
1272
+ if (first)
1273
+ openEntry(first.path);
1274
+ }, contentPanelOpen: contentPanelShown, contentPanelId: CONTENT_PANEL_ID, onContentToggle: () => contentPanelShown ? setContentPanelShown(false) : openNavigator(), onQuickOpen: openQuickOpen, quickOpenShortcut: quickOpenShortcut, quickOpenKeys: quickOpenKeys, branchesOpen: branchManagerOpen, onBranchesOpen: openBranchManager, onCommentsOpen: clientOperatingStrategy(operatingMode ?? 'prod').supportsComments() &&
1275
+ branchNameState
1276
+ ? () => setCommentsPanelOpen(true)
1277
+ : undefined, commentsOpen: commentsPanelOpen, unresolvedCommentCount: comments.filter((t) => !t.resolved).length, mediaLibraryOpen: mediaLibraryOpen, onMediaLibraryOpen: () => setMediaLibraryOpen(true), mediaButtonRef: mediaButtonRef, layout: layout, onLayoutChange: setLayout, layoutBlocked: previewShownMaximized, onPermissionManagerOpen: userIsAdmin ? () => setPermissionManagerOpen(true) : undefined, onGroupManagerOpen: userIsAdmin ? () => setGroupManagerOpen(true) : undefined, onSystemHealthOpen: userIsAdmin ? () => setSystemHealthOpen(true) : undefined, settingsToggleRef: settingsToggleRef, AccountComponent: AccountComponent, onAccountClick: onAccountClick, onLogoutClick: onLogoutClick }), panel: contentPanelDocked ? contentPanel : undefined, main: _jsx(Box, { h: "100%", children: _jsx(PaneLayout, { formOverlay: contentPanelDocked ? undefined : contentPanel, formInert: contentOverlayShown, previewMaximized: previewShownMaximized,
1278
+ // Sizing the pane, by pointer or key, gives the page the pane's width.
1279
+ onSeparatorDrag: (dragging) => {
1280
+ setPaneResizing(dragging);
1281
+ if (dragging)
1282
+ setPreviewWidth('fit');
1283
+ }, arrangement: layout, sideSplitPercent: sideSplitPercent, stackedSplitPercent: stackedSplitPercent, onSplitPercentChange: (arrangement, percent) => {
1284
+ setSplitPercent(arrangement, percent);
1285
+ setPreviewWidth('fit');
1286
+ }, preview: renderPreview && currentEntry && !currentEntryUnavailable
1287
+ ? renderPreview(currentEntry, displayValue)
1288
+ : defaultPreview, form: !currentEntry ? (_jsx(CenteredMessage, { children: !branchNameState && busy
1289
+ ? 'Setting up your branch workspace…'
1290
+ : entriesInitializing
1291
+ ? 'Loading content…'
1292
+ : 'Select an item to start editing.' })) : currentEntryUnavailable ? (_jsx(UnavailableEntryNotice, { schemaRefs: currentEntry.unavailable ? [currentEntry.unavailable.schemaRef] : [] })) : currentEntry.canEdit === false ? (_jsx(NoEditPermissionNotice, { entryLabel: currentEntry.label })) : schema.length > 0 && displayValue ? (_jsx(EntryLinkContext.Provider, { value: entryLinkContextValue, children: _jsx(SiteMdxAllowContext.Provider, { value: mdxAllow, children: _jsx(FormRenderer, { notice: hiddenDraftNotice && (_jsx(ReadOnlyDraftNotice, { permanentlyReadOnly: (currentBranch?.isProtected ?? true) ||
1293
+ currentBranch?.status === 'archived',
1294
+ // A hidden draft's discard changes nothing on screen, so it says so.
1295
+ onDiscard: () => handleDiscardFileDraft(() => navigatorToggleRef.current, {
1296
+ announce: true,
1297
+ }) })), fields: schema, value: displayValue, readOnly: contentReadOnly, onRemoved: handleFieldRemoved, commentBlockedReason: commentBlockedReason, removedItemFieldLabel: commentFieldLabel, onGetUserMetadata: getCommentUserMetadata, header: {
1298
+ title: currentEntry.label,
1299
+ typeLabel: entryTypeLabel(activeCollections, currentEntry),
1300
+ },
1301
+ // The one writer of drafts. Read-only content gets none, whatever a
1302
+ // field emits (MDXEditor's mount-time normalisation included).
1303
+ onChange: (next) => {
1304
+ const contentId = currentEntry?.contentId;
1305
+ if (contentId && !contentReadOnlyRef.current) {
1306
+ // Every list item keeps a key, whatever field made it.
1307
+ const keyed = withItemKeys(schema, next, 'random');
1308
+ setDrafts((prev) => ({ ...prev, [contentId]: keyed }));
1309
+ }
1310
+ }, customRenderers: customRenderers, branch: branchNameState, comments: displayComments, currentEntryPath: selectedPath, currentUserId: commentUserId, canResolve: canResolveAnyComment, focusedFieldPath: focusedFieldPath, highlightThreadId: highlightThreadId, onAddComment: handleAddDisplayedComment, onResolveThread: handleResolveThread, fieldErrors: fieldErrors, format: currentEntry.format, conflictNotice: !!(currentEntry?.contentId &&
1311
+ currentBranch?.conflictFiles?.includes(currentEntry.contentId)) }) }) })) : schema.length === 0 ? (_jsx(CenteredMessage, { children: "No fields to edit." })) : loadFailedKey !== `${branchNameState}:${currentEntry.contentId}` ? (_jsx(Stack, { gap: "lg", p: "md", "data-testid": "entry-loading-skeleton", children: [0, 1, 2].map((row) => (_jsxs(Stack, { gap: 6, children: [_jsx(Skeleton, { h: 12, w: 120 }), _jsx(Skeleton, { h: 36 })] }, row))) })) : (_jsx(Alert, { color: "red", variant: "light", m: "md", title: "This entry couldn't be loaded.", "data-testid": "entry-load-failed", children: _jsx(Button, { variant: "default", onClick: () => {
1312
+ setLoadFailedKey(null);
1313
+ setLoadAttempt((attempt) => attempt + 1);
1314
+ }, children: "Retry" }) })) }) }), overlays: _jsxs(_Fragment, { children: [_jsx(QuickOpenPalette, { opened: quickOpenOpen, onOpen: openQuickOpen, onClose: () => setQuickOpenOpen(false), returnFocusTo: quickOpenFromOverlay ? navigatorReturnTarget : undefined, entries: entriesState,
1315
+ // The hidden root's entries sit at the top of the tree, so they get no heading here either.
1316
+ collectionLabel: (path) => path === hiddenRootPath ? '' : collectionLabels.get(path), onSelect: setSelectedPath }), _jsx(BranchesDrawer, { opened: branchManagerOpen, onClose: () => setBranchManagerOpen(false), confirmOpen: branchConfirmOpen, returnFocusTo: branchReturnTarget, children: _jsx(BranchManager, { branches: branchSummaries, mode: operatingMode, user: userContext, currentBranchName: currentBranch?.name ?? branchNameState, currentWorkflowBlockedReason: saveAllProgress ? 'Wait for Save all to finish.' : undefined, onSelect: async (name) => {
1317
+ try {
1318
+ await handleBranchChange(name);
1319
+ setBranchManagerOpen(false);
1320
+ }
1321
+ catch (err) {
1322
+ console.error('Branch change failed or was cancelled:', err);
1323
+ // Don't close branch manager if there was an error or user cancelled
1324
+ }
1325
+ }, onBeforeCreate: confirmCreateBranch, onCreate: handleCreateBranch,
1326
+ // A Save all mid-run on the open branch would be submitted partway through.
1327
+ onSubmit: (name) => {
1328
+ if (name === branchNameState && isSaveAllRunning())
1329
+ return;
1330
+ handleSubmit(name).catch((err) => console.error(err));
1331
+ }, onWithdraw: (name) => {
1332
+ if (name === branchNameState && isSaveAllRunning())
1333
+ return;
1334
+ handleWithdraw(name).catch((err) => console.error(err));
1335
+ }, onRequestChanges: (name) => {
1336
+ handleRequestChanges(name).catch((err) => console.error(err));
1337
+ }, onDelete: (name) => {
1338
+ handleDelete(name).catch((err) => console.error(err));
1339
+ }, onClose: () => setBranchManagerOpen(false), comments: comments, currentUserId: commentUserId, canResolve: canResolveAnyComment, onAddComment: handleAddComment, onResolveThread: handleResolveThread, highlightThreadId: highlightThreadId, onGetUserMetadata: canResolveUserNames ? handleGetUserMetadata : undefined, onGetOwnerMetadata: getCommentUserMetadata, onGetCommentUserMetadata: getCommentUserMetadata }) }), branchNameState && (_jsx(CommentsPanel, { opened: commentsPanelOpen, branchName: branchNameState, comments: displayComments, canResolve: canResolveAnyComment, currentUserId: commentUserId, entryLabel: commentEntryLabel, fieldLabel: commentFieldLabel, onAddComment: handleAddDisplayedComment, onResolveThread: handleResolveThread, onClose: () => setCommentsPanelOpen(false), onJumpToField: jumpToThreadField, onGetUserMetadata: getCommentUserMetadata, onJumpToEntry: handleJumpToEntry, onJumpToBranch: handleJumpToBranch }, branchNameState)), userIsAdmin && (_jsx(GroupManager, { opened: groupManagerOpen, onClose: () => setGroupManagerOpen(false), returnFocusTo: settingsReturnTarget, internalGroups: groupsData, loading: groupsLoading, canEdit: userIsAdmin, onSave: handleSaveGroups, onSearchUsers: handleSearchUsers, onGetUserMetadata: handleGetUserMetadata, onSearchExternalGroups: handleSearchExternalGroups })), userIsAdmin && (_jsx(PermissionManager, { opened: permissionManagerOpen, onClose: () => setPermissionManagerOpen(false), returnFocusTo: settingsReturnTarget, collections: activeCollections, contentRoot: contentRoot, permissions: permissionsData, loading: permissionsLoading, canEdit: userIsAdmin, onSave: handleSavePermissions, onSearchUsers: handleSearchUsers, onGetUserMetadata: handleGetUserMetadata, onListGroups: handleListGroups })), userIsAdmin && (_jsx(SystemHealthPanel, { opened: systemHealthOpen, onClose: () => setSystemHealthOpen(false), returnFocusTo: settingsReturnTarget })), _jsx(MediaLibrary, { opened: mediaLibraryOpen, onClose: () => setMediaLibraryOpen(false), mode: "manage", returnFocusTo: () => mediaButtonRef.current }), userIsAdmin && (_jsx(CollectionEditor, { isOpen: collectionEditorOpen, editingCollection: editingCollection, parentPath: collectionEditorParentPath, availableSchemas: availableSchemas, onSave: handleCollectionSave, onAddEntryType: editingCollection
1340
+ ? (path, entryType) => addEntryType(path, entryType)
1341
+ : undefined, onUpdateEntryType: editingCollection
1342
+ ? (path, name, updates) => updateEntryType(path, name, updates)
1343
+ : undefined, onRemoveEntryType: editingCollection ? (path, name) => removeEntryType(path, name) : undefined, onReloadEntryTypes: editingCollection ? readEntryTypes : undefined, onClose: handleCloseCollectionEditor, isSaving: schemaLoading, error: collectionEditorError, restored: restoredCollectionInput, onDiscardRestored: () => {
1344
+ collectionDraftRef.current = null;
1345
+ setRestoredCollectionInput(null);
1346
+ } })), renamingEntry && (_jsx(RenameEntryModal, { isOpen: renameModalOpen, entryLabel: renamingEntry.label, currentSlug: renamingEntry.slug || '', onSave: handleRenameSubmit,
1347
+ // `renamingEntry` stays until the next rename, so the dialog plays its exit and
1348
+ // returns focus.
1349
+ onClose: () => {
1350
+ setRenameModalOpen(false);
1351
+ setRenameModalError(null);
1352
+ }, isSaving: renameModalSaving, returnFocusTo: entryChangeReturnTarget, error: renameModalError?.mayHaveLanded ? null : renameModalError?.message, warning: renameModalError?.mayHaveLanded ? renameModalError.message : null, existingSlugs: renameExistingSlugs, previewPath: (slug) => previewPathFor({
1353
+ collectionPath: renamingEntry.collectionPath ?? '',
1354
+ collectionName: renamingEntry.collectionName,
1355
+ slug,
1356
+ entryType: renamingEntry.entryType,
1357
+ }) })), createModalCollection && (_jsx(EntryCreateModal, { isOpen: createModalOpen, collectionLabel: createModalCollection.label || createModalCollection.name, entryTypes: createModalEntryTypes, onCreate: handleCreateModalSubmit, onClose: closeCreateModal, isCreating: createModalCreating, error: createModalError, existingSlugs: createModalExistingSlugs, previewPath: (slug, entryTypeName) => previewPathFor({
1358
+ collectionPath: createModalCollection.path,
1359
+ collectionName: createModalCollection.name,
1360
+ slug,
1361
+ entryType: entryTypeName,
1362
+ }) })), _jsx(ConfirmDeleteModal, { isOpen: deleteCollectionModalOpen, title: "Delete Collection", message: "Are you sure you want to delete this collection? This cannot be undone.", confirmLabel: "Delete Collection", onConfirm: confirmDeleteCollection, onClose: () => {
1363
+ setDeleteCollectionModalOpen(false);
1364
+ setDeletingCollectionPath(null);
1365
+ }, loading: deleteInProgress }), _jsx(ConfirmDeleteModal, { isOpen: deleteEntryModalOpen, title: "Delete entry", message: deletingEntryReferencedBy
1366
+ ? referencedDeleteMessage(deletingEntryReferencedBy)
1367
+ : `Delete ${deletingEntryLabel ? `"${deletingEntryLabel}"` : 'this entry'}? This cannot be undone.`, confirmLabel: deletingEntryReferencedBy ? 'Delete anyway' : 'Delete entry', onConfirm: confirmDeleteEntry, onClose: closeDeleteEntryModal, loading: deleteInProgress, returnFocusTo: entryChangeReturnTarget, children: deletingEntryReferencedBy && (_jsx(ReferencedByList, { referencedBy: deletingEntryReferencedBy, onOpenEntry: (entryPath) => {
1368
+ closeDeleteEntryModal();
1369
+ openEntry(entryPath);
1370
+ } })) })] }) }) }) }));
1223
1371
  };
1224
1372
  export default Editor;