canopycms 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +213 -41
- package/dist/api/branch.d.ts +2 -2
- package/dist/api/branch.js +4 -31
- package/dist/authorization/branch.js +3 -4
- package/dist/cli/cli.js +0 -18
- package/dist/cli/generate-ai-content.js +0 -6
- package/dist/cli/init.js +0 -6
- package/dist/editor/BranchManager.d.ts +2 -0
- package/dist/editor/BranchManager.js +3 -3
- package/dist/editor/Editor.js +253 -128
- package/dist/editor/EntryNavigator.d.ts +44 -9
- package/dist/editor/EntryNavigator.js +275 -103
- package/dist/editor/branch-lock.d.ts +42 -0
- package/dist/editor/branch-lock.js +79 -0
- package/dist/editor/branch-status.d.ts +0 -6
- package/dist/editor/branch-status.js +0 -10
- package/dist/editor/components/BranchNotAvailableNotice.d.ts +11 -0
- package/dist/editor/components/BranchNotAvailableNotice.js +10 -0
- package/dist/editor/components/BranchPill.d.ts +3 -18
- package/dist/editor/components/BranchPill.js +7 -10
- package/dist/editor/components/EditorHeader.d.ts +24 -52
- package/dist/editor/components/EditorHeader.js +151 -71
- package/dist/editor/components/EditorRail.d.ts +8 -8
- package/dist/editor/components/EditorRail.js +7 -5
- package/dist/editor/components/EntryBreadcrumb.d.ts +5 -1
- package/dist/editor/components/EntryBreadcrumb.js +3 -3
- package/dist/editor/components/EntryHeader.d.ts +60 -0
- package/dist/editor/components/EntryHeader.js +45 -0
- package/dist/editor/components/EntrySaveButton.d.ts +24 -0
- package/dist/editor/components/EntrySaveButton.js +101 -0
- package/dist/editor/components/NoEditPermissionNotice.d.ts +1 -1
- package/dist/editor/components/NoEditPermissionNotice.js +1 -1
- package/dist/editor/components/RetryToastMessage.d.ts +3 -1
- package/dist/editor/components/RetryToastMessage.js +8 -4
- package/dist/editor/components/SaveAllButton.d.ts +25 -0
- package/dist/editor/components/SaveAllButton.js +81 -0
- package/dist/editor/components/UnsavedMark.d.ts +1 -1
- package/dist/editor/components/UnsavedMark.js +1 -1
- package/dist/editor/components/index.d.ts +1 -1
- package/dist/editor/components/index.js +1 -1
- package/dist/editor/copy.d.ts +0 -1
- package/dist/editor/copy.js +0 -1
- package/dist/editor/editor-utils.d.ts +2 -2
- package/dist/editor/editor-utils.js +2 -2
- package/dist/editor/entry-search.d.ts +10 -16
- package/dist/editor/entry-search.js +4 -24
- package/dist/editor/hooks/useBranchManager.d.ts +7 -3
- package/dist/editor/hooks/useBranchManager.js +13 -9
- package/dist/editor/hooks/useEntryManager.d.ts +3 -0
- package/dist/editor/hooks/useEntryManager.js +10 -3
- package/dist/editor/shell/ActivityRail.d.ts +4 -0
- package/dist/editor/shell/ActivityRail.js +13 -4
- package/dist/editor/shell/EditorShell.d.ts +6 -7
- package/dist/editor/shell/EditorShell.js +9 -13
- package/dist/editor/shell/PaneLayout.d.ts +1 -0
- package/dist/editor/shell/PaneLayout.js +23 -5
- package/dist/editor/shell/SidePanelHost.d.ts +11 -4
- package/dist/editor/shell/SidePanelHost.js +95 -7
- package/dist/editor/shell/TopBar.d.ts +4 -4
- package/dist/editor/shell/TopBar.js +6 -3
- package/dist/editor/utils/focus-return.d.ts +10 -2
- package/dist/editor/utils/focus-return.js +40 -6
- package/dist/operating-mode/client-safe-strategy.d.ts +1 -6
- package/dist/operating-mode/client-safe-strategy.js +1 -10
- package/dist/operating-mode/types.d.ts +0 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/editor/components/BranchLockBanner.d.ts +0 -23
- package/dist/editor/components/BranchLockBanner.js +0 -66
- package/dist/editor/components/QuickOpenPalette.d.ts +0 -28
- package/dist/editor/components/QuickOpenPalette.js +0 -71
- package/dist/editor/components/SaveSplitButton.d.ts +0 -31
- package/dist/editor/components/SaveSplitButton.js +0 -121
package/dist/editor/Editor.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
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, Menu, Paper, Skeleton, Stack, Text, Tooltip, } from '@mantine/core';
|
|
5
|
-
import { IconChevronDown, IconChevronUp, IconDots, IconFolderPlus, IconPlus,
|
|
6
|
-
import { useMediaQuery } from '@mantine/hooks';
|
|
4
|
+
import { ActionIcon, Alert, Box, Button, Center, Menu, Paper, Skeleton, Stack, Text, Tooltip, } from '@mantine/core';
|
|
5
|
+
import { IconChevronDown, IconChevronUp, IconDots, IconFolderPlus, IconPlus, } from '@tabler/icons-react';
|
|
6
|
+
import { useHotkeys, useMediaQuery } from '@mantine/hooks';
|
|
7
7
|
import { notifications } from '@mantine/notifications';
|
|
8
8
|
const noop = () => { };
|
|
9
9
|
import { EntryNavigator } from './EntryNavigator.js';
|
|
@@ -13,7 +13,7 @@ import { displayPreviewPath, previewPageKey } from './preview-path.js';
|
|
|
13
13
|
import { showPreviewLinkOutcome } from './components/PreviewLinkToast.js';
|
|
14
14
|
import { clientOperatingStrategy } from '../operating-mode/client.js';
|
|
15
15
|
import { EditorShell } from './shell/EditorShell.js';
|
|
16
|
-
import { PaneLayout } from './shell/PaneLayout.js';
|
|
16
|
+
import { FORM_PANE_ID, PaneLayout } from './shell/PaneLayout.js';
|
|
17
17
|
import { SidePanelHost } from './shell/SidePanelHost.js';
|
|
18
18
|
import { CanopyCMSProvider } from './theme.js';
|
|
19
19
|
import { BranchManager } from './BranchManager.js';
|
|
@@ -25,7 +25,7 @@ import { SystemHealthPanel } from './admin/SystemHealthPanel.js';
|
|
|
25
25
|
// Import directly from helpers to avoid server-only code in authorization barrel
|
|
26
26
|
import { isAdmin, isPrivileged, isReviewer } from '../authorization/helpers.js';
|
|
27
27
|
import { fieldBreadcrumb } from './field-breadcrumb.js';
|
|
28
|
-
import { buildPreviewRoute, buildPreviewSrc, buildCollectionLabels, entryStatusLabel, entryTypeLabel,
|
|
28
|
+
import { buildPreviewRoute, buildPreviewSrc, buildCollectionLabels, entryStatusLabel, entryTypeLabel, buildBreadcrumbSegments, entryNameSource, } from './editor-utils.js';
|
|
29
29
|
import { useEditorLayout, useDraftManager, useEntryManager, useGroupManager, usePermissionManager, useCommentSystem, useBranchManager, useUserContext, useSchemaManager, } from './hooks/index.js';
|
|
30
30
|
import { useBranchActions } from './hooks/useBranchActions.js';
|
|
31
31
|
import { CONTENT_PANEL_WIDTH_MAX, CONTENT_PANEL_WIDTH_MIN, editorLayoutStorageKey, } from './hooks/useEditorLayout.js';
|
|
@@ -33,17 +33,17 @@ import { EntrySchemaUnavailableError } from './hooks/useEntryManager.js';
|
|
|
33
33
|
import { useEntryLinkResolution } from './hooks/useEntryLinkResolution.js';
|
|
34
34
|
import { useReferenceResolution } from './hooks/useReferenceResolution.js';
|
|
35
35
|
import { usePreviewMarks } from './hooks/usePreviewMarks.js';
|
|
36
|
-
import {
|
|
36
|
+
import { branchBarStatus, describeBranchLock } from './branch-lock.js';
|
|
37
|
+
import { EditorHeader, EditorRail, EntryHeader } from './components/index.js';
|
|
37
38
|
import { showSaveAllResult } from './components/SaveAllResult.js';
|
|
38
39
|
import { RenameEntryModal } from './components/RenameEntryModal.js';
|
|
39
|
-
import {
|
|
40
|
-
import { rememberOpener, returnTarget } from './utils/focus-return.js';
|
|
40
|
+
import { focusIsIdle, rememberOpener, returnTarget } from './utils/focus-return.js';
|
|
41
41
|
import { modKeyLabel } from './platform.js';
|
|
42
|
-
import { EDITOR_ACTIONS } from './copy.js';
|
|
43
42
|
import { EntryCreateModal } from './components/EntryCreateModal.js';
|
|
44
43
|
import { ConfirmDeleteModal } from './components/ConfirmDeleteModal.js';
|
|
45
44
|
import { ReferencedByList, referencedDeleteMessage } from './components/ReferencedByList.js';
|
|
46
45
|
import { NoEditPermissionNotice } from './components/NoEditPermissionNotice.js';
|
|
46
|
+
import { BranchNotAvailableNotice } from './components/BranchNotAvailableNotice.js';
|
|
47
47
|
import { ReadOnlyDraftNotice } from './components/ReadOnlyDraftNotice.js';
|
|
48
48
|
import { UnavailableEntryNotice } from './components/UnavailableEntryNotice.js';
|
|
49
49
|
import { unavailableSchemaRefs } from './unavailable-entry-type.js';
|
|
@@ -81,6 +81,13 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
81
81
|
const [groupManagerOpen, setGroupManagerOpen] = useState(false);
|
|
82
82
|
const [permissionManagerOpen, setPermissionManagerOpen] = useState(false);
|
|
83
83
|
const [branchManagerOpen, setBranchManagerOpen] = useState(false);
|
|
84
|
+
// Each opening remounts Branches; one from New branch… starts on its new-branch form.
|
|
85
|
+
const [branchManagerOpening, setBranchManagerOpening] = useState({ seq: 0, create: false });
|
|
86
|
+
const openBranchesDrawer = (create) => {
|
|
87
|
+
branchOpenerRef.current = rememberOpener();
|
|
88
|
+
setBranchManagerOpening(({ seq }) => ({ seq: seq + 1, create }));
|
|
89
|
+
setBranchManagerOpen(true);
|
|
90
|
+
};
|
|
84
91
|
const [mediaLibraryOpen, setMediaLibraryOpen] = useState(false);
|
|
85
92
|
const [systemHealthOpen, setSystemHealthOpen] = useState(false);
|
|
86
93
|
const [collectionEditorOpen, setCollectionEditorOpen] = useState(false);
|
|
@@ -96,8 +103,6 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
96
103
|
const [restoredCollectionInput, setRestoredCollectionInput] = useState(null);
|
|
97
104
|
// Each open or close of the collection dialog moves it on, so a slow open answers only if current.
|
|
98
105
|
const collectionOpeningRef = useRef(0);
|
|
99
|
-
const [quickOpenOpen, setQuickOpenOpen] = useState(false);
|
|
100
|
-
const [quickOpenFromOverlay, setQuickOpenFromOverlay] = useState(false);
|
|
101
106
|
const navigatorToggleRef = useRef(null);
|
|
102
107
|
const mediaButtonRef = useRef(null);
|
|
103
108
|
const branchToggleRef = useRef(null);
|
|
@@ -106,10 +111,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
106
111
|
const branchOpenerRef = useRef(null);
|
|
107
112
|
// Null when the panel opened from a menu item, which unmounts on click, or with focus on the page.
|
|
108
113
|
const navigatorOpenerRef = useRef(null);
|
|
109
|
-
const
|
|
110
|
-
useEffect(() => {
|
|
111
|
-
quickOpenOpenRef.current = quickOpenOpen;
|
|
112
|
-
}, [quickOpenOpen]);
|
|
114
|
+
const entryFilterRef = useRef(null);
|
|
113
115
|
const [renameModalOpen, setRenameModalOpen] = useState(false);
|
|
114
116
|
const [renamingEntry, setRenamingEntry] = useState(null);
|
|
115
117
|
const [renameModalError, setRenameModalError] = useState(null);
|
|
@@ -158,7 +160,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
158
160
|
labels: [],
|
|
159
161
|
}));
|
|
160
162
|
const applySubmitStampsRef = useRef(() => { });
|
|
161
|
-
const { branchName: branchNameState, setBranchName, branchSummaries, currentBranch, branchesLoadFailed, addCreatedBranch, handleSubmit, handleWithdraw, handleRequestChanges, handleDelete, handleReloadBranchData, loadBranches, } = useBranchManager({
|
|
163
|
+
const { branchName: branchNameState, setBranchName, branchSummaries, currentBranch, branchesLoadFailed, branchesLoaded, addCreatedBranch, handleSubmit, handleWithdraw, handleRequestChanges, handleDelete, handleReloadBranchData, loadBranches, } = useBranchManager({
|
|
162
164
|
initialBranch: branchName,
|
|
163
165
|
operatingMode,
|
|
164
166
|
setBusy: setBranchesLoading,
|
|
@@ -180,9 +182,21 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
180
182
|
// loading state during that window anyway.
|
|
181
183
|
const branchContentLocked = currentBranch?.writeBlocked ?? true;
|
|
182
184
|
const branchLoading = !currentBranch && (branchesLoading || branchSummaries.length === 0);
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
const
|
|
185
|
+
// No lock reason is true yet while the list is unanswered; once it has failed, or answered
|
|
186
|
+
// without this branch, the lock says which.
|
|
187
|
+
const branchPending = !currentBranch && !branchesLoaded && !branchesLoadFailed;
|
|
188
|
+
const branchMissing = !currentBranch && branchesLoaded;
|
|
189
|
+
// The bar's lock-state row, with the inputs EditorHeader gets, for the states that must agree.
|
|
190
|
+
const branchBarState = branchBarStatus({
|
|
191
|
+
branchStatus: currentBranch?.status,
|
|
192
|
+
branchReadOnly: currentBranch?.readOnly,
|
|
193
|
+
branchWriteBlocked: branchContentLocked,
|
|
194
|
+
branchPending,
|
|
195
|
+
branchMissing,
|
|
196
|
+
isProtected: currentBranch?.isProtected,
|
|
197
|
+
}).value;
|
|
198
|
+
// With no branch named, the bar shows no row, so neither do the empty states.
|
|
199
|
+
const branchNotAvailable = !!branchNameState && branchBarState === 'not-available';
|
|
186
200
|
// Set once the branch actions below exist; they depend on the drafts, which depend on the entries.
|
|
187
201
|
const switchBranchRef = useRef(async () => { });
|
|
188
202
|
// 2. Entry manager (depends on branchNameState, owns selectedPath)
|
|
@@ -200,6 +214,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
200
214
|
}),
|
|
201
215
|
resolvePreviewRoute: (entry) => buildPreviewRoute(entry, { previewBaseByCollection, contentRoot }),
|
|
202
216
|
setBusy: setEntriesLoading,
|
|
217
|
+
branchBarState,
|
|
203
218
|
switchBranch: (branch) => switchBranchRef.current(branch),
|
|
204
219
|
// To the entry menu, which names the entry opened; a dialog keeps focus.
|
|
205
220
|
onHistoryRestore: () => requestAnimationFrame(() => {
|
|
@@ -208,6 +223,9 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
208
223
|
}
|
|
209
224
|
}),
|
|
210
225
|
});
|
|
226
|
+
// The "Not available" empty states wait for the load to answer with nothing: content that does
|
|
227
|
+
// load, for a branch the list lags in granting, shows as usual.
|
|
228
|
+
const branchShowsNotAvailable = branchNotAvailable && !entriesInitializing && entriesState.length === 0;
|
|
211
229
|
// The content overlay and a maximized preview hide the form; `navigatorOpen` is only the
|
|
212
230
|
// overlay's. Anything that focuses something in the form reveals it first. Opening an entry
|
|
213
231
|
// closes the overlay only, so the preview stays maximized across entries (see
|
|
@@ -325,11 +343,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
325
343
|
currentUser: commentUserId,
|
|
326
344
|
canResolveComments: canResolveAnyComment,
|
|
327
345
|
setSelectedPath,
|
|
328
|
-
setBranchManagerOpen: (open) =>
|
|
329
|
-
if (open)
|
|
330
|
-
branchOpenerRef.current = rememberOpener();
|
|
331
|
-
setBranchManagerOpen(open);
|
|
332
|
-
},
|
|
346
|
+
setBranchManagerOpen: (open) => open ? openBranchesDrawer(false) : setBranchManagerOpen(false),
|
|
333
347
|
onCommentsChange: setCommentsForBranchSummaries,
|
|
334
348
|
onRevealForm: revealForm,
|
|
335
349
|
});
|
|
@@ -1026,6 +1040,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1026
1040
|
title: entry.title,
|
|
1027
1041
|
slug: entry.slug,
|
|
1028
1042
|
status: entryStatusLabel(activeCollections, entry),
|
|
1043
|
+
route: buildPreviewRoute(entry, { previewBaseByCollection, contentRoot }),
|
|
1029
1044
|
missing: entry.status === 'missing' || undefined,
|
|
1030
1045
|
noPage: !entry.previewSrc || undefined,
|
|
1031
1046
|
contentId: entry.contentId,
|
|
@@ -1078,16 +1093,29 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1078
1093
|
onCreateEntry,
|
|
1079
1094
|
handleCreateEntry,
|
|
1080
1095
|
contentRoot,
|
|
1096
|
+
previewBaseByCollection,
|
|
1081
1097
|
currentBranch,
|
|
1082
1098
|
schemaUnavailableKeys,
|
|
1083
1099
|
userIsAdmin,
|
|
1084
1100
|
]);
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1101
|
+
const findShortcut = modKeyLabel() === '⌘' ? { label: '⌘P', keys: 'Meta+P' } : { label: 'Ctrl+P', keys: 'Control+P' };
|
|
1102
|
+
// Mod+P opens a closed docked panel only until Enter, Escape from inside it or focus leaves it;
|
|
1103
|
+
// the stored preference stays closed. A row menu, the Content button or any other ask opens it
|
|
1104
|
+
// for good.
|
|
1105
|
+
const [contentPanelTransient, setContentPanelTransient] = useState(false);
|
|
1106
|
+
if (contentPanelTransient && !contentPanelDocked)
|
|
1107
|
+
setContentPanelTransient(false);
|
|
1087
1108
|
// Docked, the content panel's open state is the stored preference; as an overlay it is
|
|
1088
1109
|
// `navigatorOpen`, which starts open when the URL asks for no entry.
|
|
1089
|
-
const contentPanelShown = contentPanelDocked
|
|
1090
|
-
|
|
1110
|
+
const contentPanelShown = contentPanelDocked
|
|
1111
|
+
? contentPanelOpen || contentPanelTransient
|
|
1112
|
+
: navigatorOpen;
|
|
1113
|
+
const setContentPanelShown = (open) => {
|
|
1114
|
+
if (!contentPanelDocked)
|
|
1115
|
+
return setNavigatorOpen(open);
|
|
1116
|
+
setContentPanelOpen(open);
|
|
1117
|
+
setContentPanelTransient(false);
|
|
1118
|
+
};
|
|
1091
1119
|
// The overlay lives in the form pane, under a maximized preview.
|
|
1092
1120
|
const contentOverlayShown = !contentPanelDocked && contentPanelShown;
|
|
1093
1121
|
useEffect(() => {
|
|
@@ -1096,30 +1124,78 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1096
1124
|
}, [contentOverlayShown]);
|
|
1097
1125
|
// Bumped on every ask for the panel, so one already open still takes focus.
|
|
1098
1126
|
const [contentPanelFocusRequest, setContentPanelFocusRequest] = useState(0);
|
|
1099
|
-
const openNavigator = () => {
|
|
1100
|
-
|
|
1127
|
+
const openNavigator = ({ transient = false } = {}) => {
|
|
1128
|
+
// Focus already in the panel isn't an opener: the panel's own close would hand focus back to it.
|
|
1129
|
+
// Nor is the page, once the panel is shown: focus dropped there from where it came.
|
|
1130
|
+
const opener = rememberOpener();
|
|
1131
|
+
if (!document.getElementById(CONTENT_PANEL_ID)?.contains(document.activeElement) &&
|
|
1132
|
+
(opener || !contentPanelShown)) {
|
|
1133
|
+
navigatorOpenerRef.current = opener;
|
|
1134
|
+
}
|
|
1101
1135
|
// In the commit that opens the overlay, so the form panel is no longer inert when the overlay
|
|
1102
1136
|
// mounts, then or a frame later as its slide starts, and serves its focus request.
|
|
1103
1137
|
if (!contentPanelDocked)
|
|
1104
1138
|
setPreviewMaximized(false);
|
|
1105
|
-
|
|
1139
|
+
// A pending Enter focus nulls the panel's return of focus, and only Enter's close may lose it.
|
|
1140
|
+
setFormFocusPath(null);
|
|
1141
|
+
if (transient && contentPanelDocked && !contentPanelOpen)
|
|
1142
|
+
setContentPanelTransient(true);
|
|
1143
|
+
else
|
|
1144
|
+
setContentPanelShown(true);
|
|
1106
1145
|
setContentPanelFocusRequest((request) => request + 1);
|
|
1107
1146
|
};
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1147
|
+
const keepContentPanelOpen = () => {
|
|
1148
|
+
if (contentPanelTransient)
|
|
1149
|
+
setContentPanelShown(true);
|
|
1150
|
+
};
|
|
1151
|
+
const [formFocusPath, setFormFocusPath] = useState(null);
|
|
1152
|
+
const formFocusRestoredRef = useRef(false);
|
|
1153
|
+
// `[]` (tagsToIgnore) and `true` (contenteditable) let it fire from fields and the MDX editor.
|
|
1154
|
+
useHotkeys([
|
|
1155
|
+
[
|
|
1156
|
+
'mod+P',
|
|
1157
|
+
(event) => {
|
|
1158
|
+
// Mantine's `mod` is Ctrl or ⌘ everywhere; on macOS Ctrl+P moves the caret up a line.
|
|
1159
|
+
if (modKeyLabel() === '⌘' && !event.metaKey)
|
|
1160
|
+
return;
|
|
1161
|
+
// Keeps the browser's print dialog shut, even when the panel can't open.
|
|
1162
|
+
event.preventDefault();
|
|
1163
|
+
// Opened under a modal or drawer, it would take the keyboard from the one in use.
|
|
1164
|
+
if (document.querySelector('[role="dialog"][aria-modal="true"]'))
|
|
1165
|
+
return;
|
|
1166
|
+
setChangedOnly(false);
|
|
1167
|
+
const field = entryFilterRef.current;
|
|
1168
|
+
if (field && document.activeElement === field)
|
|
1169
|
+
field.select();
|
|
1170
|
+
else
|
|
1171
|
+
openNavigator({ transient: true });
|
|
1172
|
+
},
|
|
1173
|
+
{ preventDefault: false },
|
|
1174
|
+
],
|
|
1175
|
+
], [], true);
|
|
1176
|
+
// A collection crumb, or the entry menu's "Show in content panel", opens the panel as the rail's
|
|
1177
|
+
// Content button does, then the row takes focus.
|
|
1178
|
+
const [contentReveal, setContentReveal] = useState();
|
|
1111
1179
|
// Only ever rises: the navigator serves each seq once, and the request is cleared once served.
|
|
1112
1180
|
const revealSeqRef = useRef(0);
|
|
1113
|
-
const
|
|
1181
|
+
const revealInContentPanel = (reveal) => {
|
|
1114
1182
|
openNavigator();
|
|
1115
1183
|
revealSeqRef.current += 1;
|
|
1116
|
-
|
|
1184
|
+
setContentReveal({ ...reveal, seq: revealSeqRef.current });
|
|
1117
1185
|
};
|
|
1118
1186
|
// An ask the panel closed on before serving it must not fire when the panel next opens.
|
|
1119
1187
|
useEffect(() => {
|
|
1120
1188
|
if (!contentPanelShown)
|
|
1121
|
-
|
|
1189
|
+
setContentReveal(undefined);
|
|
1122
1190
|
}, [contentPanelShown]);
|
|
1191
|
+
// The content panel's All / Changed filter. It stays as chosen, emptied by a save or not, except
|
|
1192
|
+
// that Mod+P finds among all entries and another branch has its own unsaved set: both reset it.
|
|
1193
|
+
const [changedOnly, setChangedOnly] = useState(false);
|
|
1194
|
+
useEffect(() => setChangedOnly(false), [branchNameState]);
|
|
1195
|
+
const showUnsaved = () => {
|
|
1196
|
+
setChangedOnly(true);
|
|
1197
|
+
revealInContentPanel({ kind: 'changed' });
|
|
1198
|
+
};
|
|
1123
1199
|
const runSaveAll = () => {
|
|
1124
1200
|
void handleSaveAll()
|
|
1125
1201
|
.then((result) => {
|
|
@@ -1144,18 +1220,12 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1144
1220
|
const navigatorReturnTarget = () => returnTarget(navigatorOpenerRef.current, () => navigatorToggleRef.current);
|
|
1145
1221
|
const branchReturnTarget = () => returnTarget(branchOpenerRef.current, () => branchToggleRef.current);
|
|
1146
1222
|
const settingsReturnTarget = () => settingsToggleRef.current;
|
|
1147
|
-
//
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
}
|
|
1154
|
-
if (!contentPanelDocked)
|
|
1155
|
-
setNavigatorOpen(false);
|
|
1156
|
-
setQuickOpenOpen(true);
|
|
1157
|
-
};
|
|
1158
|
-
const dirtyPaths = useMemo(() => new Set(editedFiles.map((file) => file.path)), [editedFiles]);
|
|
1223
|
+
// Keyed by its members, so the same set keeps its identity as drafts change on every keystroke.
|
|
1224
|
+
const dirtyKey = editedFiles
|
|
1225
|
+
.map((file) => file.path)
|
|
1226
|
+
.sort()
|
|
1227
|
+
.join('\n');
|
|
1228
|
+
const dirtyPaths = useMemo(() => new Set(dirtyKey ? dirtyKey.split('\n') : []), [dirtyKey]);
|
|
1159
1229
|
// Slugs of the entries beside the one being renamed, for the duplicate check.
|
|
1160
1230
|
const renameExistingSlugs = useMemo(() => new Set(entriesState
|
|
1161
1231
|
.filter((e) => e.collectionPath === renamingEntry?.collectionPath &&
|
|
@@ -1204,17 +1274,6 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1204
1274
|
collectionName: e.collectionName,
|
|
1205
1275
|
})),
|
|
1206
1276
|
}), [entriesState]);
|
|
1207
|
-
// Quick open names each entry's type and route under its label.
|
|
1208
|
-
const quickOpenEntries = useMemo(() => entriesState.map((e) => ({
|
|
1209
|
-
path: e.path,
|
|
1210
|
-
label: e.label,
|
|
1211
|
-
title: e.title,
|
|
1212
|
-
slug: e.slug,
|
|
1213
|
-
collectionPath: e.collectionPath,
|
|
1214
|
-
collectionName: e.collectionName,
|
|
1215
|
-
typeLabel: entryTypeLine(activeCollections, e),
|
|
1216
|
-
route: buildPreviewRoute(e, { previewBaseByCollection, contentRoot }),
|
|
1217
|
-
})), [entriesState, activeCollections, previewBaseByCollection, contentRoot]);
|
|
1218
1277
|
const CenteredMessage = ({ children }) => (_jsx("div", { style: {
|
|
1219
1278
|
display: 'flex',
|
|
1220
1279
|
alignItems: 'center',
|
|
@@ -1240,6 +1299,54 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1240
1299
|
if (!previewCanMaximize)
|
|
1241
1300
|
setPreviewMaximized(false);
|
|
1242
1301
|
}, [previewCanMaximize]);
|
|
1302
|
+
// Enter in the transient panel focuses the opened entry's first field once it renders, else the
|
|
1303
|
+
// form pane; focus the user moved by then stays. A tree click, preview link or Back moves none.
|
|
1304
|
+
useEffect(() => {
|
|
1305
|
+
if (!formFocusPath)
|
|
1306
|
+
return;
|
|
1307
|
+
if (selectedPath !== formFocusPath)
|
|
1308
|
+
return setFormFocusPath(null);
|
|
1309
|
+
if (currentEntry?.path !== formFocusPath)
|
|
1310
|
+
return;
|
|
1311
|
+
const formShown = schema.length > 0 && !!displayValue;
|
|
1312
|
+
const fieldless = currentEntryUnavailable ||
|
|
1313
|
+
currentEntry.canEdit === false ||
|
|
1314
|
+
schema.length === 0 ||
|
|
1315
|
+
loadFailedKey === `${branchNameState}:${currentEntry.contentId}`;
|
|
1316
|
+
if (!formShown && !fieldless)
|
|
1317
|
+
return;
|
|
1318
|
+
// Restoring the preview hands focus to its Maximize button; that isn't the user moving on.
|
|
1319
|
+
const focusedByRestore = formFocusRestoredRef.current ? document.activeElement : null;
|
|
1320
|
+
formFocusRestoredRef.current = false;
|
|
1321
|
+
const frame = requestAnimationFrame(() => {
|
|
1322
|
+
if (!focusIsIdle() && document.activeElement !== focusedByRestore) {
|
|
1323
|
+
return setFormFocusPath(null);
|
|
1324
|
+
}
|
|
1325
|
+
// The form is in view first; the next run focuses it.
|
|
1326
|
+
if (previewShownMaximized) {
|
|
1327
|
+
formFocusRestoredRef.current = true;
|
|
1328
|
+
return setPreviewMaximized(false);
|
|
1329
|
+
}
|
|
1330
|
+
setFormFocusPath(null);
|
|
1331
|
+
const pane = document.getElementById(FORM_PANE_ID);
|
|
1332
|
+
const field = formShown ? pane?.querySelector('[data-canopy-field]') : null;
|
|
1333
|
+
if (field)
|
|
1334
|
+
focusFieldTarget(field);
|
|
1335
|
+
else
|
|
1336
|
+
pane?.focus({ preventScroll: true });
|
|
1337
|
+
});
|
|
1338
|
+
return () => cancelAnimationFrame(frame);
|
|
1339
|
+
}, [
|
|
1340
|
+
formFocusPath,
|
|
1341
|
+
selectedPath,
|
|
1342
|
+
currentEntry,
|
|
1343
|
+
currentEntryUnavailable,
|
|
1344
|
+
schema.length,
|
|
1345
|
+
displayValue,
|
|
1346
|
+
loadFailedKey,
|
|
1347
|
+
branchNameState,
|
|
1348
|
+
previewShownMaximized,
|
|
1349
|
+
]);
|
|
1243
1350
|
const [paneResizing, setPaneResizing] = useState(false);
|
|
1244
1351
|
const previewErrorField = previewError?.fieldPath
|
|
1245
1352
|
? fieldBreadcrumb(schema, displayValue, previewError.fieldPath).join(' › ')
|
|
@@ -1261,13 +1368,13 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1261
1368
|
display: 'flex',
|
|
1262
1369
|
alignItems: 'center',
|
|
1263
1370
|
justifyContent: 'center',
|
|
1264
|
-
}, children: _jsx(Text, { size: "sm", c: "dimmed", children: !branchNameState && busy
|
|
1371
|
+
}, children: !currentEntry && branchShowsNotAvailable ? (_jsx(BranchNotAvailableNotice, { align: "center", "data-testid": "preview-branch-not-available" })) : (_jsx(Text, { size: "sm", c: "dimmed", children: !branchNameState && busy
|
|
1265
1372
|
? 'Setting up your branch workspace…'
|
|
1266
1373
|
: entriesInitializing
|
|
1267
1374
|
? 'Loading content…'
|
|
1268
1375
|
: currentEntry && !currentEntry.previewSrc
|
|
1269
1376
|
? 'No preview for this entry.'
|
|
1270
|
-
: 'Select an item to start editing.' }) }));
|
|
1377
|
+
: 'Select an item to start editing.' })) }));
|
|
1271
1378
|
const breadcrumbSegments = useMemo(() => buildBreadcrumbSegments(currentEntry, collectionLabels), [collectionLabels, currentEntry]);
|
|
1272
1379
|
// The browser tab names the open entry and the site.
|
|
1273
1380
|
const tabTitle = currentEntry
|
|
@@ -1287,9 +1394,8 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1287
1394
|
document.title = tabTitle;
|
|
1288
1395
|
writtenTitleRef.current = tabTitle;
|
|
1289
1396
|
}, [tabTitle]);
|
|
1290
|
-
const openBranchManager = () => {
|
|
1291
|
-
|
|
1292
|
-
setBranchManagerOpen(true);
|
|
1397
|
+
const openBranchManager = (options) => {
|
|
1398
|
+
openBranchesDrawer(options?.create === true);
|
|
1293
1399
|
// Always retry, not only when branchless. Opening the manager is
|
|
1294
1400
|
// the one in-app retry for a failed branches fetch, and the fetch
|
|
1295
1401
|
// is terminal on its own: SWRProvider sets shouldRetryOnError and
|
|
@@ -1297,12 +1403,19 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1297
1403
|
// is 0 without data. Reloading is idempotent.
|
|
1298
1404
|
loadBranches().catch(console.error);
|
|
1299
1405
|
};
|
|
1300
|
-
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,
|
|
1301
|
-
//
|
|
1302
|
-
returnFocusTo: () => (
|
|
1406
|
+
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, closeOnEscape: contentPanelTransient,
|
|
1407
|
+
// An entry opened by Enter takes focus in its form instead.
|
|
1408
|
+
returnFocusTo: () => (formFocusPath ? null : navigatorReturnTarget()), onFocusLeave: contentPanelTransient
|
|
1409
|
+
? (target) => {
|
|
1410
|
+
if (!contentToggleRef.current?.contains(target))
|
|
1411
|
+
setContentPanelTransient(false);
|
|
1412
|
+
}
|
|
1413
|
+
: undefined, "data-testid": "content-panel", actions: _jsxs(_Fragment, { children: [navCollections &&
|
|
1303
1414
|
navCollections.length > 0 &&
|
|
1304
1415
|
!branchContentLocked &&
|
|
1305
|
-
(navCollections[0].onAdd || navCollections[0].onAddSubCollection) && (_jsxs(Menu, { shadow: "md", width: 200, withinPortal: true, position: "bottom-end",
|
|
1416
|
+
(navCollections[0].onAdd || navCollections[0].onAddSubCollection) && (_jsxs(Menu, { shadow: "md", width: 200, withinPortal: true, position: "bottom-end",
|
|
1417
|
+
// Its items open dialogs outside the panel, which would close a transient one.
|
|
1418
|
+
onOpen: keepContentPanelOpen, 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: "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
|
|
1306
1419
|
? undefined
|
|
1307
1420
|
: entriesState.map((e) => ({
|
|
1308
1421
|
path: e.path,
|
|
@@ -1310,85 +1423,97 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1310
1423
|
title: e.title,
|
|
1311
1424
|
slug: e.slug,
|
|
1312
1425
|
status: entryStatusLabel(activeCollections, e),
|
|
1426
|
+
route: buildPreviewRoute(e, { previewBaseByCollection, contentRoot }),
|
|
1313
1427
|
missing: e.status === 'missing' || undefined,
|
|
1314
1428
|
noPage: !e.previewSrc || undefined,
|
|
1315
1429
|
readOnly: e.canEdit === false || undefined,
|
|
1316
|
-
})), selectedPath: selectedPath, onSelect: openEntry, onTreeControllerReady: handleTreeControllerReady, expandedStateRef: treeExpandedStateRef, onExpandedStateChange: handleExpandedStateChange, onDeleteEntry: handleDeleteEntry, onRenameEntry: handleRenameEntry, onReorderEntry: userIsAdmin ? handleReorderEntry : undefined, hiddenRootPath: hiddenRootPath, loading: entriesInitializing, readOnly: branchContentLocked, readOnlyReason:
|
|
1430
|
+
})), selectedPath: selectedPath, onSelect: openEntry, onTreeControllerReady: handleTreeControllerReady, expandedStateRef: treeExpandedStateRef, onExpandedStateChange: handleExpandedStateChange, onDeleteEntry: handleDeleteEntry, onRenameEntry: handleRenameEntry, onReorderEntry: userIsAdmin ? handleReorderEntry : undefined, hiddenRootPath: hiddenRootPath, loading: entriesInitializing, readOnly: branchContentLocked, readOnlyReason: branchPending
|
|
1317
1431
|
? undefined
|
|
1318
1432
|
: describeBranchLock({
|
|
1319
1433
|
branchStatus: currentBranch?.status,
|
|
1320
1434
|
branchReadOnly: currentBranch?.readOnly,
|
|
1321
1435
|
branchWriteBlocked: branchContentLocked,
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1436
|
+
branchMissing,
|
|
1437
|
+
}), dirtyPaths: dirtyPaths, reveal: contentReveal, onRevealed: () => setContentReveal(undefined), changedOnly: changedOnly, onChangedOnlyChange: setChangedOnly, unsavedCount: modifiedCount, unsavedEntries: editedFiles, entryListFailed: entriesLoadFailed && entriesState.length === 0, branchNotAvailable: branchShowsNotAvailable, onSwitchBranch: () => openBranchManager(), filterRef: entryFilterRef, filterShortcut: findShortcut, onFilterEnter: contentPanelTransient
|
|
1438
|
+
? (path) => {
|
|
1439
|
+
openEntry(path);
|
|
1440
|
+
setContentPanelTransient(false);
|
|
1441
|
+
setFormFocusPath(path);
|
|
1442
|
+
}
|
|
1443
|
+
: undefined, onFilterEscape: contentPanelTransient ? () => setContentPanelTransient(false) : undefined, onRowMenuOpen: keepContentPanelOpen }) }) }));
|
|
1444
|
+
return (_jsx(CanopyCMSProvider, { ...(themeOptions ?? {}), children: _jsx(AssetContextProvider, { basePath: basePath, children: _jsx(EditorShell, { topBar: _jsx(EditorHeader, { branchName: branchNameState, branchStatus: currentBranch?.status, busy: busy, modifiedCount: modifiedCount, onShowUnsaved: showUnsaved, draftStorageFailed: draftStorageFailed, userContext: userContext, branchCreatedBy: currentBranch?.createdBy, branchAccess: currentBranch?.access,
|
|
1326
1445
|
// Fail-closed for the same reason branchContentLocked above is:
|
|
1327
|
-
// isProtected/writeBlocked gate real actions in EditorHeader
|
|
1328
|
-
// Submit, disabling Save), so a missing wire value must read as
|
|
1446
|
+
// isProtected/writeBlocked gate real actions in EditorHeader and EntryHeader
|
|
1447
|
+
// (hiding Submit, disabling Save and Save all), so a missing wire value must read as
|
|
1329
1448
|
// "protected"/"blocked", not "unprotected"/"unblocked".
|
|
1330
1449
|
// branchWriteBlocked reuses branchContentLocked itself rather than
|
|
1331
1450
|
// re-deriving `?? true` a second time here, so there is exactly one
|
|
1332
1451
|
// fail-closed computation of "are writes blocked" in this component.
|
|
1333
|
-
branchIsProtected: currentBranch?.isProtected ?? true, branchReadOnly: currentBranch?.readOnly, branchWriteBlocked: branchContentLocked,
|
|
1334
|
-
// A failed fetch leaves the list empty too; the banner says that one.
|
|
1335
|
-
branchLoading: branchLoading, 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), entryUnavailable: currentEntryUnavailable, onEntrySelect: openEntry, onBranchReloadData: () => {
|
|
1452
|
+
branchIsProtected: currentBranch?.isProtected ?? true, branchServerProtected: currentBranch?.isProtected, branchReadOnly: currentBranch?.readOnly, branchWriteBlocked: branchContentLocked, branchLoading: branchLoading, branchPending: branchPending, branchMissing: branchMissing, branchToggleRef: branchToggleRef, onBranchReloadData: () => {
|
|
1336
1453
|
void Promise.all([handleReloadBranchData(), refreshEntries(branchNameState)]).catch(console.error);
|
|
1337
|
-
}, onBranchDiscardDrafts: () => handleDiscardDrafts(() => branchToggleRef.current), onBranchManagerOpen: openBranchManager,
|
|
1338
|
-
|
|
1339
|
-
|
|
1454
|
+
}, onBranchDiscardDrafts: () => handleDiscardDrafts(() => branchToggleRef.current), onBranchManagerOpen: openBranchManager, onNewBranch: () => openBranchManager({ create: true }), onSaveAll: runSaveAll, saveAllProgress: saveAllProgress,
|
|
1455
|
+
// On any entry: EntryHeader's `saving` is the open entry's alone.
|
|
1456
|
+
singleSaving: singleSaving !== null,
|
|
1340
1457
|
// Save all finds each draft's entry in the list, so with none listed it would save nothing.
|
|
1341
|
-
entryListFailed: entriesLoadFailed && entriesState.length === 0, onSubmit: () => {
|
|
1458
|
+
entryListFailed: entriesLoadFailed && entriesState.length === 0, onSubmit: (returnFocusTo) => {
|
|
1342
1459
|
if (branchNameState && !isSaveAllRunning()) {
|
|
1343
|
-
handleSubmit(branchNameState).catch(console.error);
|
|
1460
|
+
handleSubmit(branchNameState, returnFocusTo).catch(console.error);
|
|
1344
1461
|
}
|
|
1345
|
-
}, onWithdraw: () => {
|
|
1462
|
+
}, onWithdraw: (returnFocusTo) => {
|
|
1346
1463
|
if (branchNameState && !isSaveAllRunning()) {
|
|
1347
|
-
handleWithdraw(branchNameState).catch(console.error);
|
|
1464
|
+
handleWithdraw(branchNameState, returnFocusTo).catch(console.error);
|
|
1348
1465
|
}
|
|
1349
|
-
} }),
|
|
1466
|
+
} }), rail: _jsx(EditorRail, { contentPanelOpen: contentPanelShown, contentPanelId: CONTENT_PANEL_ID, onContentToggle: () => contentPanelTransient
|
|
1467
|
+
? keepContentPanelOpen()
|
|
1468
|
+
: contentPanelShown
|
|
1469
|
+
? setContentPanelShown(false)
|
|
1470
|
+
: openNavigator(), contentToggleRef: contentToggleRef, contentShortcut: findShortcut, unsavedCount: modifiedCount, onCommentsOpen: clientOperatingStrategy(operatingMode ?? 'prod').supportsComments() &&
|
|
1350
1471
|
branchNameState
|
|
1351
1472
|
? () => setCommentsPanelOpen(true)
|
|
1352
|
-
: 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 && layout === 'side' ? contentPanel : undefined, main:
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1473
|
+
: 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 && layout === 'side' ? contentPanel : undefined, main: _jsxs(Box, { h: "100%", style: { display: 'flex', flexDirection: 'column' }, children: [_jsx(EntryHeader, { currentEntry: currentEntry,
|
|
1474
|
+
// True exactly while the form below shows "Setting up…" or "Loading content…".
|
|
1475
|
+
entryLoading: !currentEntry && ((!branchNameState && busy) || entriesInitializing), branchName: branchNameState, busy: busy, breadcrumbSegments: breadcrumbSegments,
|
|
1476
|
+
// A draft kept for an unavailable entry stays in storage but cannot be saved.
|
|
1477
|
+
hasUnsavedChanges: !currentEntryUnavailable && !contentReadOnly && isSelectedDirty(), entryHasDraft: isSelectedDirty(), entryReadOnly: currentEntry?.canEdit === false, entryUnavailable: currentEntryUnavailable, draftStorageFailed: draftStorageFailed, branchStatus: currentBranch?.status, branchReadOnly: currentBranch?.readOnly, branchWriteBlocked: branchContentLocked, branchLoading: branchLoading, branchPending: branchPending, branchMissing: branchMissing, onCollectionOpen: (path) => revealInContentPanel({ kind: 'collection', path }), onEntryReveal: () => currentEntry && revealInContentPanel({ kind: 'entry', path: currentEntry.path }), navigatorToggleRef: navigatorToggleRef, onFileReload: () => handleReload(() => navigatorToggleRef.current), onFileDiscardDraft: () => handleDiscardFileDraft(() => navigatorToggleRef.current), onEntryRename: () => currentEntry && handleRenameEntry(currentEntry.path, true), onEntryDelete: () => currentEntry && handleDeleteEntry(currentEntry.path, true), onSave: currentEntryUnavailable ? noop : handleSave, saveAllRunning: !!saveAllProgress, saving: singleSaving?.branch === branchNameState &&
|
|
1478
|
+
singleSaving.contentId === currentEntry?.contentId, savedSignal: lastSaved?.seq, savedHere: lastSaved?.branch === branchNameState &&
|
|
1479
|
+
lastSaved.contentId === currentEntry?.contentId }), _jsx(Box, { style: { flex: '1 1 0%', minHeight: 0 }, children: _jsx(PaneLayout, { formAside: contentPanelDocked && layout === 'stacked' ? contentPanel : undefined, formOverlay: contentPanelDocked ? undefined : contentPanel, formInert: contentOverlayShown, previewMaximized: previewShownMaximized,
|
|
1480
|
+
// Sizing the pane, by pointer or key, gives the page the pane's width.
|
|
1481
|
+
onSeparatorDrag: (dragging) => {
|
|
1482
|
+
setPaneResizing(dragging);
|
|
1483
|
+
if (dragging)
|
|
1484
|
+
setPreviewWidth('fit');
|
|
1485
|
+
}, arrangement: layout, sideSplitPercent: sideSplitPercent, stackedSplitPercent: stackedSplitPercent, onSplitPercentChange: (arrangement, percent) => {
|
|
1486
|
+
setSplitPercent(arrangement, percent);
|
|
1487
|
+
setPreviewWidth('fit');
|
|
1488
|
+
}, preview: renderPreview && currentEntry && !currentEntryUnavailable
|
|
1489
|
+
? renderPreview(currentEntry, displayValue)
|
|
1490
|
+
: defaultPreview, form: !currentEntry && branchShowsNotAvailable ? (_jsx(Center, { h: "100%", children: _jsx(BranchNotAvailableNotice, { align: "center", onSwitchBranch: () => openBranchManager(), "data-testid": "form-branch-not-available" }) })) : !currentEntry ? (_jsx(CenteredMessage, { children: !branchNameState && busy
|
|
1491
|
+
? 'Setting up your branch workspace…'
|
|
1492
|
+
: entriesInitializing
|
|
1493
|
+
? 'Loading content…'
|
|
1494
|
+
: '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) ||
|
|
1495
|
+
currentBranch?.status === 'archived',
|
|
1496
|
+
// A hidden draft's discard changes nothing on screen, so it says so.
|
|
1497
|
+
onDiscard: () => handleDiscardFileDraft(() => navigatorToggleRef.current, {
|
|
1498
|
+
announce: true,
|
|
1499
|
+
}) })), fields: schema, value: displayValue, readOnly: contentReadOnly, onRemoved: handleFieldRemoved, savedValue: loadedValue, commentBlockedReason: commentBlockedReason, removedItemFieldLabel: commentFieldLabel, onGetUserMetadata: getCommentUserMetadata, header: {
|
|
1500
|
+
title: currentEntry.label,
|
|
1501
|
+
typeLabel: entryTypeLabel(activeCollections, currentEntry),
|
|
1502
|
+
},
|
|
1503
|
+
// The one writer of drafts. Read-only content gets none, whatever a
|
|
1504
|
+
// field emits (MDXEditor's mount-time normalisation included).
|
|
1505
|
+
onChange: (next) => {
|
|
1506
|
+
const contentId = currentEntry?.contentId;
|
|
1507
|
+
if (contentId && !contentReadOnlyRef.current) {
|
|
1508
|
+
// Every list item keeps a key, whatever field made it.
|
|
1509
|
+
const keyed = withItemKeys(schema, next, 'random');
|
|
1510
|
+
setDrafts((prev) => ({ ...prev, [contentId]: keyed }));
|
|
1511
|
+
}
|
|
1512
|
+
}, 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 &&
|
|
1513
|
+
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: () => {
|
|
1514
|
+
setLoadFailedKey(null);
|
|
1515
|
+
setLoadAttempt((attempt) => attempt + 1);
|
|
1516
|
+
}, children: "Retry" }) })) }) })] }), overlays: _jsxs(_Fragment, { children: [_jsx(BranchesDrawer, { opened: branchManagerOpen, onClose: () => setBranchManagerOpen(false), confirmOpen: branchConfirmOpen, returnFocusTo: branchReturnTarget, children: _jsx(BranchManager, { startCreating: branchManagerOpening.create, branches: branchSummaries, mode: operatingMode, user: userContext, currentBranchName: currentBranch?.name ?? branchNameState, currentWorkflowBlockedReason: saveAllProgress ? 'Wait for Save all to finish.' : undefined, onSelect: async (name) => {
|
|
1392
1517
|
try {
|
|
1393
1518
|
await handleBranchChange(name);
|
|
1394
1519
|
setBranchManagerOpen(false);
|
|
@@ -1411,7 +1536,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1411
1536
|
handleRequestChanges(name).catch((err) => console.error(err));
|
|
1412
1537
|
}, onDelete: (name) => {
|
|
1413
1538
|
handleDelete(name).catch((err) => console.error(err));
|
|
1414
|
-
}, 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
|
|
1539
|
+
}, onClose: () => setBranchManagerOpen(false), comments: comments, currentUserId: commentUserId, canResolve: canResolveAnyComment, onAddComment: handleAddComment, onResolveThread: handleResolveThread, highlightThreadId: highlightThreadId, onGetUserMetadata: canResolveUserNames ? handleGetUserMetadata : undefined, onGetOwnerMetadata: getCommentUserMetadata, onGetCommentUserMetadata: getCommentUserMetadata }, branchManagerOpening.seq) }), 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
|
|
1415
1540
|
? (path, entryType) => addEntryType(path, entryType)
|
|
1416
1541
|
: undefined, onUpdateEntryType: editingCollection
|
|
1417
1542
|
? (path, name, updates) => updateEntryType(path, name, updates)
|