canopycms 0.1.0 → 0.2.0-int.121
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 +202 -43
- package/dist/api/assets.d.ts +27 -7
- package/dist/api/assets.js +118 -47
- package/dist/api/client.d.ts +5 -3
- package/dist/api/client.js +7 -3
- package/dist/api/index.d.ts +1 -1
- package/dist/assets/asset-prefixes.d.ts +4 -1
- package/dist/assets/asset-prefixes.js +3 -1
- package/dist/assets/editor-thumbnail-widths.d.ts +11 -0
- package/dist/assets/editor-thumbnail-widths.js +7 -0
- package/dist/assets/finalize.d.ts +7 -6
- package/dist/assets/finalize.js +29 -10
- package/dist/assets/keys.d.ts +4 -1
- package/dist/assets/keys.js +4 -1
- package/dist/assets/materialize.d.ts +17 -2
- package/dist/assets/materialize.js +57 -4
- package/dist/assets/store-local.d.ts +18 -9
- package/dist/assets/store-local.js +58 -26
- package/dist/assets/store-s3.d.ts +25 -6
- package/dist/assets/store-s3.js +81 -19
- package/dist/assets/types.d.ts +33 -4
- package/dist/cli/cli.js +151 -56
- package/dist/config/types.d.ts +3 -2
- package/dist/editor/BranchManager.js +2 -1
- package/dist/editor/CommentsPanel.js +67 -29
- package/dist/editor/Editor.d.ts +3 -0
- package/dist/editor/Editor.js +93 -39
- package/dist/editor/EntryNavigator.d.ts +17 -5
- package/dist/editor/EntryNavigator.js +66 -31
- package/dist/editor/FormHeader.d.ts +2 -1
- package/dist/editor/FormHeader.js +9 -5
- package/dist/editor/FormRenderer.d.ts +7 -2
- package/dist/editor/FormRenderer.js +51 -16
- package/dist/editor/PreviewFrame.d.ts +13 -2
- package/dist/editor/PreviewFrame.js +62 -5
- package/dist/editor/PreviewPane.d.ts +17 -1
- package/dist/editor/PreviewPane.js +55 -8
- package/dist/editor/PreviewToolbarTools.d.ts +8 -0
- package/dist/editor/PreviewToolbarTools.js +9 -1
- package/dist/editor/branch-status.d.ts +6 -0
- package/dist/editor/branch-status.js +10 -0
- package/dist/editor/comments/BranchComments.d.ts +2 -4
- package/dist/editor/comments/BranchComments.js +7 -11
- package/dist/editor/comments/ThreadCarousel.d.ts +2 -8
- package/dist/editor/comments/ThreadCarousel.js +4 -49
- package/dist/editor/comments/ThreadReplyControls.js +5 -2
- package/dist/editor/components/BranchLockBanner.d.ts +8 -7
- package/dist/editor/components/BranchLockBanner.js +30 -3
- package/dist/editor/components/BranchPill.d.ts +5 -4
- package/dist/editor/components/BranchPill.js +6 -8
- package/dist/editor/components/BranchRowLock.d.ts +12 -0
- package/dist/editor/components/BranchRowLock.js +19 -0
- package/dist/editor/components/BranchesDrawer.js +3 -3
- package/dist/editor/components/EditorHeader.d.ts +2 -6
- package/dist/editor/components/EditorHeader.js +14 -28
- package/dist/editor/components/EditorRail.d.ts +3 -4
- package/dist/editor/components/EditorRail.js +3 -4
- package/dist/editor/components/EntryBreadcrumb.d.ts +9 -10
- package/dist/editor/components/EntryBreadcrumb.js +8 -11
- package/dist/editor/components/PreviewLinkToast.d.ts +5 -0
- package/dist/editor/components/PreviewLinkToast.js +33 -0
- package/dist/editor/components/QuickOpenPalette.d.ts +7 -1
- package/dist/editor/components/QuickOpenPalette.js +6 -1
- package/dist/editor/components/RenameEntryModal.d.ts +6 -1
- package/dist/editor/components/RenameEntryModal.js +20 -5
- package/dist/editor/components/SaveSplitButton.d.ts +3 -2
- package/dist/editor/components/SaveSplitButton.js +25 -12
- package/dist/editor/components/StagedChangesDrawer.d.ts +0 -1
- package/dist/editor/components/StagedChangesDrawer.js +5 -4
- package/dist/editor/components/UndoToastMessage.d.ts +6 -5
- package/dist/editor/components/UndoToastMessage.js +6 -4
- package/dist/editor/components/UnsavedMark.d.ts +9 -0
- package/dist/editor/components/UnsavedMark.js +20 -0
- package/dist/editor/components/UserBadge.js +13 -11
- package/dist/editor/copy.d.ts +4 -0
- package/dist/editor/copy.js +4 -0
- package/dist/editor/editor-utils.d.ts +49 -9
- package/dist/editor/editor-utils.js +117 -16
- package/dist/editor/entry-search.d.ts +4 -2
- package/dist/editor/entry-search.js +2 -3
- package/dist/editor/fields/FieldLabel.d.ts +1 -1
- package/dist/editor/fields/FieldLabel.js +1 -1
- package/dist/editor/fields/ImageField.d.ts +1 -1
- package/dist/editor/fields/ImageField.js +36 -27
- package/dist/editor/fields/ObjectField.d.ts +13 -2
- package/dist/editor/fields/ObjectField.js +38 -9
- package/dist/editor/fields/entry-link/EntryLinkContext.d.ts +2 -0
- package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -1
- package/dist/editor/group-manager/index.js +1 -1
- package/dist/editor/hooks/useBranchManager.d.ts +2 -0
- package/dist/editor/hooks/useBranchManager.js +1 -0
- package/dist/editor/hooks/useEditorLayout.d.ts +4 -0
- package/dist/editor/hooks/useEditorLayout.js +5 -0
- package/dist/editor/hooks/useEntryManager.d.ts +2 -0
- package/dist/editor/hooks/useEntryManager.js +10 -8
- package/dist/editor/media/AssetCard.d.ts +6 -3
- package/dist/editor/media/AssetCard.js +24 -9
- package/dist/editor/media/AssetListRow.d.ts +9 -4
- package/dist/editor/media/AssetListRow.js +18 -9
- package/dist/editor/media/CropStep.d.ts +8 -10
- package/dist/editor/media/CropStep.js +39 -27
- package/dist/editor/media/MediaLibrary.js +5 -4
- package/dist/editor/media/MediaLibraryBody.d.ts +1 -1
- package/dist/editor/media/MediaLibraryBody.js +65 -37
- package/dist/editor/media/crop-availability.d.ts +6 -0
- package/dist/editor/media/crop-availability.js +19 -0
- package/dist/editor/media/crop-math.d.ts +19 -13
- package/dist/editor/media/crop-math.js +50 -11
- package/dist/editor/media/useAssetLibrary.d.ts +6 -3
- package/dist/editor/media/useAssetLibrary.js +17 -10
- package/dist/editor/pane-header.d.ts +6 -0
- package/dist/editor/pane-header.js +6 -0
- package/dist/editor/permission-manager/index.js +1 -1
- package/dist/editor/preview-bridge.d.ts +23 -0
- package/dist/editor/preview-bridge.js +105 -4
- package/dist/editor/preview-path.d.ts +16 -4
- package/dist/editor/preview-path.js +80 -19
- package/dist/editor/shell/ActivityRail.d.ts +1 -3
- package/dist/editor/shell/ActivityRail.js +3 -3
- package/dist/editor/shell/EditorShell.d.ts +15 -0
- package/dist/editor/shell/EditorShell.js +17 -1
- package/dist/editor/shell/OverlayHeader.d.ts +19 -0
- package/dist/editor/shell/OverlayHeader.js +15 -0
- package/dist/editor/shell/PaneLayout.d.ts +6 -1
- package/dist/editor/shell/PaneLayout.js +68 -13
- package/dist/editor/shell/SidePanelHost.d.ts +8 -6
- package/dist/editor/shell/SidePanelHost.js +70 -33
- package/dist/editor/shell/TopBar.js +2 -1
- package/dist/editor/theme.d.ts +1 -0
- package/dist/editor/theme.js +19 -1
- package/dist/editor/undo-removal.d.ts +19 -10
- package/dist/editor/undo-removal.js +44 -16
- package/dist/server.d.ts +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/editor/Editor.js
CHANGED
|
@@ -9,6 +9,8 @@ const noop = () => { };
|
|
|
9
9
|
import { EntryNavigator } from './EntryNavigator.js';
|
|
10
10
|
import { FormRenderer } from './FormRenderer.js';
|
|
11
11
|
import { PreviewPane } from './PreviewPane.js';
|
|
12
|
+
import { displayPreviewPath, previewPageKey } from './preview-path.js';
|
|
13
|
+
import { showPreviewLinkOutcome } from './components/PreviewLinkToast.js';
|
|
12
14
|
import { clientOperatingStrategy } from '../operating-mode/client.js';
|
|
13
15
|
import { EditorShell } from './shell/EditorShell.js';
|
|
14
16
|
import { PaneLayout } from './shell/PaneLayout.js';
|
|
@@ -23,7 +25,7 @@ import { SystemHealthPanel } from './admin/SystemHealthPanel.js';
|
|
|
23
25
|
// Import directly from helpers to avoid server-only code in authorization barrel
|
|
24
26
|
import { isAdmin, isPrivileged, isReviewer } from '../authorization/helpers.js';
|
|
25
27
|
import { fieldBreadcrumb } from './field-breadcrumb.js';
|
|
26
|
-
import { buildPreviewRoute, buildPreviewSrc, buildCollectionLabels, entryStatusLabel, entryTypeLabel, buildBreadcrumbSegments, } from './editor-utils.js';
|
|
28
|
+
import { buildPreviewRoute, buildPreviewSrc, buildCollectionLabels, entryStatusLabel, entryTypeLabel, entryTypeLine, buildBreadcrumbSegments, entryNameSource, } from './editor-utils.js';
|
|
27
29
|
import { useEditorLayout, useDraftManager, useEntryManager, useGroupManager, usePermissionManager, useCommentSystem, useBranchManager, useUserContext, useSchemaManager, } from './hooks/index.js';
|
|
28
30
|
import { useBranchActions } from './hooks/useBranchActions.js';
|
|
29
31
|
import { CONTENT_PANEL_WIDTH_MAX, CONTENT_PANEL_WIDTH_MIN, editorLayoutStorageKey, } from './hooks/useEditorLayout.js';
|
|
@@ -31,7 +33,7 @@ import { EntrySchemaUnavailableError } from './hooks/useEntryManager.js';
|
|
|
31
33
|
import { useEntryLinkResolution } from './hooks/useEntryLinkResolution.js';
|
|
32
34
|
import { useReferenceResolution } from './hooks/useReferenceResolution.js';
|
|
33
35
|
import { usePreviewMarks } from './hooks/usePreviewMarks.js';
|
|
34
|
-
import { BranchLockBanner, EditorHeader, EditorRail } from './components/index.js';
|
|
36
|
+
import { BranchLockBanner, describeBranchLock, EditorHeader, EditorRail } from './components/index.js';
|
|
35
37
|
import { showSaveAllResult } from './components/SaveAllResult.js';
|
|
36
38
|
import { RenameEntryModal } from './components/RenameEntryModal.js';
|
|
37
39
|
import { QuickOpenPalette } from './components/QuickOpenPalette.js';
|
|
@@ -52,7 +54,7 @@ import { hideCollectionOutcomeUnknown } from './hooks/useSchemaManager.js';
|
|
|
52
54
|
import { AssetContextProvider, SiteMdxAllowContext, authenticatedAssetBase, useApiClient, } from './context/index.js';
|
|
53
55
|
import { EntryLinkContext } from './fields/entry-link/index.js';
|
|
54
56
|
import { MediaLibrary } from './media/MediaLibrary.js';
|
|
55
|
-
import { removalAnchors, restoreRemoval, openThreadsWithin, restoredFieldPath, } from './undo-removal.js';
|
|
57
|
+
import { removalAnchors, restoreRemoval, movedThreadsNote, openThreadsWithin, restoredFieldPath, } from './undo-removal.js';
|
|
56
58
|
import { formatCanopyPath } from './canopy-path.js';
|
|
57
59
|
import { itemKeysOnPath, resolveThreadPath, withItemKeys } from '../item-keys.js';
|
|
58
60
|
import { ENTRY_NOT_LOADED_COMMENT_MESSAGE, NEW_ITEM_COMMENT_MESSAGE, } from './comments/item-anchor-copy.js';
|
|
@@ -122,7 +124,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
122
124
|
const [deleteInProgress, setDeleteInProgress] = useState(false);
|
|
123
125
|
// A rename or delete asked for from the top bar's entry menu, whose item is gone once it acts.
|
|
124
126
|
const [entryChangeFromMenu, setEntryChangeFromMenu] = useState(false);
|
|
125
|
-
const
|
|
127
|
+
const contentToggleRef = useRef(null);
|
|
126
128
|
// Draft compile/render error reported by the preview page (null = renders cleanly)
|
|
127
129
|
const [previewError, setPreviewError] = useState(null);
|
|
128
130
|
const apiClient = useApiClient();
|
|
@@ -140,12 +142,13 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
140
142
|
const commentUserId = userContext?.userId ?? currentUser;
|
|
141
143
|
// Below 64em (1024px) the editor starts stacked and the content panel never docks.
|
|
142
144
|
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({
|
|
145
|
+
const { loaded: layoutLoaded, layout, setLayout, highlightEnabled, setHighlightEnabled, sideSplitPercent, stackedSplitPercent, setSplitPercent, previewWidth, setPreviewWidth, siteChrome, setSiteChrome, contentPanelOpen, setContentPanelOpen, contentPanelWidth, setContentPanelWidth, } = useEditorLayout({
|
|
144
146
|
storageKey: editorLayoutStorageKey(basePath),
|
|
145
147
|
defaultLayout: wideWindow ? 'side' : 'stacked',
|
|
146
148
|
});
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
+
// A wide window docks the content panel: beside the panes side by side, and beside the form row
|
|
150
|
+
// only when stacked, so the preview keeps its full width. A narrow one lays it over the form.
|
|
151
|
+
const contentPanelDocked = wideWindow;
|
|
149
152
|
// Comments state (shared between useCommentSystem and useBranchManager)
|
|
150
153
|
const [commentsForBranchSummaries, setCommentsForBranchSummaries] = useState([]);
|
|
151
154
|
// 1. Branch manager (provides branchNameState, no dependencies)
|
|
@@ -154,7 +157,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
154
157
|
labels: [],
|
|
155
158
|
}));
|
|
156
159
|
const applySubmitStampsRef = useRef(() => { });
|
|
157
|
-
const { branchName: branchNameState, setBranchName, branchSummaries, currentBranch, addCreatedBranch, handleSubmit, handleWithdraw, handleRequestChanges, handleDelete, handleReloadBranchData, loadBranches, } = useBranchManager({
|
|
160
|
+
const { branchName: branchNameState, setBranchName, branchSummaries, currentBranch, branchesLoadFailed, addCreatedBranch, handleSubmit, handleWithdraw, handleRequestChanges, handleDelete, handleReloadBranchData, loadBranches, } = useBranchManager({
|
|
158
161
|
initialBranch: branchName,
|
|
159
162
|
operatingMode,
|
|
160
163
|
setBusy: setBranchesLoading,
|
|
@@ -175,6 +178,10 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
175
178
|
// of unlocked that invites a click the server rejects -- and the pane shows a
|
|
176
179
|
// loading state during that window anyway.
|
|
177
180
|
const branchContentLocked = currentBranch?.writeBlocked ?? true;
|
|
181
|
+
const branchLoading = !currentBranch && (branchesLoading || branchSummaries.length === 0);
|
|
182
|
+
// A row's lock reason on an unanswered branch would be "details could not be loaded", which is
|
|
183
|
+
// not yet true; once the fetch has failed it is, and the lock says so.
|
|
184
|
+
const branchRowsPending = branchLoading && !branchesLoadFailed;
|
|
178
185
|
// 2. Entry manager (depends on branchNameState, owns selectedPath)
|
|
179
186
|
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({
|
|
180
187
|
initialEntries: entries,
|
|
@@ -188,6 +195,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
188
195
|
contentRoot,
|
|
189
196
|
basePath,
|
|
190
197
|
}),
|
|
198
|
+
resolvePreviewRoute: (entry) => buildPreviewRoute(entry, { previewBaseByCollection, contentRoot }),
|
|
191
199
|
setBusy: setEntriesLoading,
|
|
192
200
|
});
|
|
193
201
|
// The content overlay and a maximized preview hide the form; `navigatorOpen` is only the
|
|
@@ -203,9 +211,29 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
203
211
|
setSelectedPath(path);
|
|
204
212
|
setNavigatorOpen(false);
|
|
205
213
|
};
|
|
214
|
+
// A preview link opens its entry, or a page no entry has in a new tab. The opener is cut by hand,
|
|
215
|
+
// since `noopener` would hide whether the browser blocked the tab.
|
|
216
|
+
const openPreviewLink = (href) => {
|
|
217
|
+
const mount = { basePath, previewPrefix };
|
|
218
|
+
const base = window.location.href;
|
|
219
|
+
const page = previewPageKey(href, mount, base);
|
|
220
|
+
const entry = page === undefined
|
|
221
|
+
? undefined
|
|
222
|
+
: entriesState.find((e) => e.previewSrc && previewPageKey(e.previewSrc, mount, base) === page);
|
|
223
|
+
if (entry) {
|
|
224
|
+
openEntry(entry.path);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const opened = window.open(href, '_blank');
|
|
228
|
+
if (opened)
|
|
229
|
+
opened.opener = null;
|
|
230
|
+
const url = new URL(href);
|
|
231
|
+
const shown = url.origin === window.location.origin ? `${url.pathname}${url.search}` : href;
|
|
232
|
+
showPreviewLinkOutcome(href, displayPreviewPath(shown, mount), Boolean(opened));
|
|
233
|
+
};
|
|
206
234
|
// 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
|
|
208
|
-
//
|
|
235
|
+
// the overlay's, so a docked panel never resurfaces as one. Waits for the stored preferences, so
|
|
236
|
+
// the open state it sets lands on them, not on the defaults they replace.
|
|
209
237
|
useEffect(() => {
|
|
210
238
|
if (layoutLoaded && contentPanelDocked && navigatorOpen) {
|
|
211
239
|
setContentPanelOpen(true);
|
|
@@ -305,7 +333,8 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
305
333
|
contentReadOnlyRef.current = contentReadOnly;
|
|
306
334
|
// Undo for a form Remove: a toast whose Undo puts the item back into the entry's draft as it
|
|
307
335
|
// is then, through a functional update, so edits made since are kept. It acts only on the
|
|
308
|
-
// branch and entry it was raised for, and its toast closes when either changes.
|
|
336
|
+
// branch and entry it was raised for, and its toast closes when either changes. An object's
|
|
337
|
+
// Clear offers its Undo in the form instead (FormRenderer), which a save ends via `savedValue`.
|
|
309
338
|
const loadedValuesRef = useRef(loadedValues);
|
|
310
339
|
loadedValuesRef.current = loadedValues;
|
|
311
340
|
const draftsRef = useRef(drafts);
|
|
@@ -332,7 +361,6 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
332
361
|
...removal,
|
|
333
362
|
anchors: displayValue ? removalAnchors(fields, displayValue, removal) : undefined,
|
|
334
363
|
};
|
|
335
|
-
const doing = removal.action === 'clear' ? 'clearing' : 'removing';
|
|
336
364
|
// Open threads keyed inside what was removed leave their fields for the entry's thread list.
|
|
337
365
|
const movedThreads = currentEntry
|
|
338
366
|
? openThreadsWithin(displayComments, currentEntry.path, removal.kind === 'list-item' ? [...removal.listPath, removal.index] : removal.path)
|
|
@@ -350,7 +378,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
350
378
|
undoToastsRef.current.delete(id);
|
|
351
379
|
if (!open) {
|
|
352
380
|
notifications.show({
|
|
353
|
-
message: `Too late to undo
|
|
381
|
+
message: `Too late to undo removing ${removal.label}.`,
|
|
354
382
|
color: 'yellow',
|
|
355
383
|
});
|
|
356
384
|
return;
|
|
@@ -363,7 +391,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
363
391
|
const now = draftsRef.current[contentId] ?? loadedValuesRef.current[contentId];
|
|
364
392
|
if (!now || !restoreRemoval(fields, now, anchored)) {
|
|
365
393
|
notifications.show({
|
|
366
|
-
message: `Couldn't undo
|
|
394
|
+
message: `Couldn't undo removing ${removal.label}: that part of the entry has changed. Add it again if you still need it.`,
|
|
367
395
|
color: 'yellow',
|
|
368
396
|
});
|
|
369
397
|
return;
|
|
@@ -385,9 +413,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
385
413
|
undoToastsRef.current.set(id, contentId);
|
|
386
414
|
const toast = {
|
|
387
415
|
id,
|
|
388
|
-
message: (_jsx(UndoToastMessage, { label: removal.label,
|
|
389
|
-
? `Its ${movedThreads === 1 ? 'comment thread' : `${movedThreads} comment threads`} moved to "Comment on this entry".`
|
|
390
|
-
: undefined, onUndo: undo, onHold: () => notifications.update({ ...toast, autoClose: false }), onRelease: () => notifications.update(toast) })),
|
|
416
|
+
message: (_jsx(UndoToastMessage, { label: removal.label, note: movedThreadsNote(movedThreads), onUndo: undo, onHold: () => notifications.update({ ...toast, autoClose: false }), onRelease: () => notifications.update(toast) })),
|
|
391
417
|
autoClose: getNotificationDuration(8000),
|
|
392
418
|
withCloseButton: true,
|
|
393
419
|
onClose: () => undoToastsRef.current.delete(id),
|
|
@@ -976,8 +1002,11 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
976
1002
|
list.push({
|
|
977
1003
|
path: entry.path,
|
|
978
1004
|
label: entry.label,
|
|
1005
|
+
title: entry.title,
|
|
979
1006
|
slug: entry.slug,
|
|
980
1007
|
status: entryStatusLabel(activeCollections, entry),
|
|
1008
|
+
missing: entry.status === 'missing' || undefined,
|
|
1009
|
+
noPage: !entry.previewSrc || undefined,
|
|
981
1010
|
contentId: entry.contentId,
|
|
982
1011
|
readOnly: entry.canEdit === false || undefined,
|
|
983
1012
|
conflictNotice: !!(entry.contentId && currentBranch?.conflictFiles?.includes(entry.contentId)),
|
|
@@ -1048,13 +1077,15 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1048
1077
|
const [contentPanelFocusRequest, setContentPanelFocusRequest] = useState(0);
|
|
1049
1078
|
const openNavigator = () => {
|
|
1050
1079
|
navigatorOpenerRef.current = rememberOpener();
|
|
1051
|
-
// In the
|
|
1080
|
+
// In the commit that opens the overlay, so the form panel is no longer inert when the overlay
|
|
1081
|
+
// mounts, then or a frame later as its slide starts, and serves its focus request.
|
|
1052
1082
|
if (!contentPanelDocked)
|
|
1053
1083
|
setPreviewMaximized(false);
|
|
1054
1084
|
setContentPanelShown(true);
|
|
1055
1085
|
setContentPanelFocusRequest((request) => request + 1);
|
|
1056
1086
|
};
|
|
1057
|
-
// A collection crumb opens the panel as
|
|
1087
|
+
// A collection crumb opens the panel as the rail's Content button does, then its collection's row
|
|
1088
|
+
// takes focus.
|
|
1058
1089
|
const [collectionReveal, setCollectionReveal] = useState();
|
|
1059
1090
|
// Only ever rises: the navigator serves each seq once, and the request is cleared once served.
|
|
1060
1091
|
const revealSeqRef = useRef(0);
|
|
@@ -1084,9 +1115,10 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1084
1115
|
})
|
|
1085
1116
|
.catch(console.error);
|
|
1086
1117
|
};
|
|
1087
|
-
// The entry menu's trigger stands in for its item; once the entry is deleted,
|
|
1118
|
+
// The entry menu's trigger stands in for its item; once the entry is deleted, the rail's Content
|
|
1119
|
+
// button, where another entry is found.
|
|
1088
1120
|
const entryChangeReturnTarget = entryChangeFromMenu
|
|
1089
|
-
? () => navigatorToggleRef.current ??
|
|
1121
|
+
? () => navigatorToggleRef.current ?? contentToggleRef.current
|
|
1090
1122
|
: undefined;
|
|
1091
1123
|
const navigatorReturnTarget = () => returnTarget(navigatorOpenerRef.current, () => navigatorToggleRef.current);
|
|
1092
1124
|
const branchReturnTarget = () => returnTarget(branchOpenerRef.current, () => branchToggleRef.current);
|
|
@@ -1145,11 +1177,23 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1145
1177
|
entries: entriesState.map((e) => ({
|
|
1146
1178
|
contentId: e.contentId,
|
|
1147
1179
|
label: e.label,
|
|
1180
|
+
title: e.title,
|
|
1148
1181
|
slug: e.slug,
|
|
1149
1182
|
collectionPath: e.collectionPath,
|
|
1150
1183
|
collectionName: e.collectionName,
|
|
1151
1184
|
})),
|
|
1152
1185
|
}), [entriesState]);
|
|
1186
|
+
// Quick open names each entry's type and route under its label.
|
|
1187
|
+
const quickOpenEntries = useMemo(() => entriesState.map((e) => ({
|
|
1188
|
+
path: e.path,
|
|
1189
|
+
label: e.label,
|
|
1190
|
+
title: e.title,
|
|
1191
|
+
slug: e.slug,
|
|
1192
|
+
collectionPath: e.collectionPath,
|
|
1193
|
+
collectionName: e.collectionName,
|
|
1194
|
+
typeLabel: entryTypeLine(activeCollections, e),
|
|
1195
|
+
route: buildPreviewRoute(e, { previewBaseByCollection, contentRoot }),
|
|
1196
|
+
})), [entriesState, activeCollections, previewBaseByCollection, contentRoot]);
|
|
1153
1197
|
const CenteredMessage = ({ children }) => (_jsx("div", { style: {
|
|
1154
1198
|
display: 'flex',
|
|
1155
1199
|
alignItems: 'center',
|
|
@@ -1162,11 +1206,15 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1162
1206
|
fields: schema,
|
|
1163
1207
|
data: displayValue,
|
|
1164
1208
|
});
|
|
1165
|
-
// Only the default preview has the toolbar that maximizes and restores it.
|
|
1166
|
-
//
|
|
1167
|
-
// choice reapplies once a value arrives.
|
|
1209
|
+
// Only the default preview has the toolbar that maximizes and restores it. It stays maximized
|
|
1210
|
+
// while an entry loads. With no value after a failed load and no draft, the form shows instead,
|
|
1211
|
+
// and the choice reapplies once a value arrives.
|
|
1168
1212
|
const previewCanMaximize = !renderPreview && Boolean(currentEntry?.previewSrc) && !currentEntryUnavailable;
|
|
1169
|
-
|
|
1213
|
+
// The open entry's data is loading, not failed: the preview keeps its last page, dimmed.
|
|
1214
|
+
const previewPending = previewCanMaximize &&
|
|
1215
|
+
!previewFrameData &&
|
|
1216
|
+
loadFailedKey !== `${branchNameState}:${currentEntry?.contentId}`;
|
|
1217
|
+
const previewShownMaximized = previewMaximized && previewCanMaximize && (Boolean(previewFrameData) || previewPending);
|
|
1170
1218
|
useEffect(() => {
|
|
1171
1219
|
if (!previewCanMaximize)
|
|
1172
1220
|
setPreviewMaximized(false);
|
|
@@ -1176,8 +1224,9 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1176
1224
|
? fieldBreadcrumb(schema, displayValue, previewError.fieldPath).join(' › ')
|
|
1177
1225
|
: '';
|
|
1178
1226
|
// An unavailable entry previews nothing: the site cannot read it, and a stored draft would
|
|
1179
|
-
// otherwise mount the frame at a page that answers not-found.
|
|
1180
|
-
|
|
1227
|
+
// otherwise mount the frame at a page that answers not-found. The pane stays mounted while an
|
|
1228
|
+
// entry loads, so its toolbar never leaves on a switch.
|
|
1229
|
+
const defaultPreview = currentEntry?.previewSrc && (previewFrameData || previewPending) && !currentEntryUnavailable ? (_jsx(PreviewPane, { src: currentEntry.previewSrc, pending: previewPending, data: previewFrameData, isLoading: previewLoadingState, highlightEnabled: highlightEnabled, onPreviewError: setPreviewError, onMarks: onPreviewMarks, assetBase: authenticatedAssetBase(basePath), mount: { basePath, previewPrefix }, previewWidth: previewWidth, onPreviewWidthChange: setPreviewWidth, onHighlightToggle: () => {
|
|
1181
1230
|
setHighlightEnabled(!highlightEnabled);
|
|
1182
1231
|
clearPreviewMarks();
|
|
1183
1232
|
}, marksNothing: previewMarksNothing, inexactMarks: previewInexactMarks, showMarkDiagnostics: userIsAdmin, layout: layout, maximized: previewShownMaximized, onMaximizedChange: (maximized) => {
|
|
@@ -1185,7 +1234,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1185
1234
|
if (maximized && !contentPanelDocked)
|
|
1186
1235
|
setNavigatorOpen(false);
|
|
1187
1236
|
setPreviewMaximized(maximized);
|
|
1188
|
-
}, resizing: paneResizing, notice: previewError && (_jsx(Alert, { color: "red", title: "Preview error", withCloseButton: true, onClose: () => setPreviewError(null), radius: 0, children: previewErrorField
|
|
1237
|
+
}, resizing: paneResizing, siteChrome: siteChrome, onSiteChromeChange: setSiteChrome, onNavigate: openPreviewLink, notice: previewError && (_jsx(Alert, { color: "red", title: "Preview error", withCloseButton: true, onClose: () => setPreviewError(null), radius: 0, children: previewErrorField
|
|
1189
1238
|
? `${previewErrorField}: ${previewError.message}`
|
|
1190
1239
|
: previewError.message })) })) : (_jsx(Paper, { withBorder: true, shadow: "xs", h: "100%", bg: "var(--mantine-color-body)", style: {
|
|
1191
1240
|
display: 'flex',
|
|
@@ -1237,10 +1286,19 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1237
1286
|
: entriesState.map((e) => ({
|
|
1238
1287
|
path: e.path,
|
|
1239
1288
|
label: e.label,
|
|
1289
|
+
title: e.title,
|
|
1240
1290
|
slug: e.slug,
|
|
1241
1291
|
status: entryStatusLabel(activeCollections, e),
|
|
1292
|
+
missing: e.status === 'missing' || undefined,
|
|
1293
|
+
noPage: !e.previewSrc || undefined,
|
|
1242
1294
|
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,
|
|
1295
|
+
})), 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: branchRowsPending
|
|
1296
|
+
? undefined
|
|
1297
|
+
: describeBranchLock({
|
|
1298
|
+
branchStatus: currentBranch?.status,
|
|
1299
|
+
branchReadOnly: currentBranch?.readOnly,
|
|
1300
|
+
branchWriteBlocked: branchContentLocked,
|
|
1301
|
+
}), dirtyPaths: dirtyPaths, revealCollection: collectionReveal, onRevealed: () => setCollectionReveal(undefined) }) }) }));
|
|
1244
1302
|
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
1303
|
// A draft kept for an unavailable entry stays in storage but cannot be saved.
|
|
1246
1304
|
hasUnsavedChanges: !currentEntryUnavailable && !contentReadOnly && isSelectedDirty(), entryHasDraft: isSelectedDirty(), entryReadOnly: currentEntry?.canEdit === false, draftStorageFailed: draftStorageFailed, userContext: userContext, branchCreatedBy: currentBranch?.createdBy, branchAccess: currentBranch?.access,
|
|
@@ -1253,7 +1311,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1253
1311
|
// fail-closed computation of "are writes blocked" in this component.
|
|
1254
1312
|
branchIsProtected: currentBranch?.isProtected ?? true, branchReadOnly: currentBranch?.readOnly, branchWriteBlocked: branchContentLocked,
|
|
1255
1313
|
// A failed fetch leaves the list empty too; the banner says that one.
|
|
1256
|
-
branchLoading:
|
|
1314
|
+
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: () => {
|
|
1257
1315
|
void Promise.all([handleReloadBranchData(), refreshEntries(branchNameState)]).catch(console.error);
|
|
1258
1316
|
}, onBranchDiscardDrafts: () => handleDiscardDrafts(() => branchToggleRef.current), onBranchManagerOpen: openBranchManager, onSave: currentEntryUnavailable ? noop : handleSave, onSaveAll: runSaveAll, saveAllProgress: saveAllProgress, saving: singleSaving?.branch === branchNameState &&
|
|
1259
1317
|
singleSaving.contentId === currentEntry?.contentId, savedSignal: lastSaved?.seq, savedHere: lastSaved?.branch === branchNameState &&
|
|
@@ -1267,14 +1325,10 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1267
1325
|
if (branchNameState && !isSaveAllRunning()) {
|
|
1268
1326
|
handleWithdraw(branchNameState).catch(console.error);
|
|
1269
1327
|
}
|
|
1270
|
-
} }), banner: _jsx(BranchLockBanner, { branchName: branchNameState, branchStatus: currentBranch?.status, branchReadOnly: currentBranch?.readOnly, branchWriteBlocked: branchContentLocked, onBranchManagerOpen: openBranchManager }), rail: _jsx(EditorRail, {
|
|
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() &&
|
|
1328
|
+
} }), banner: _jsx(BranchLockBanner, { branchName: branchNameState, branchStatus: currentBranch?.status, branchReadOnly: currentBranch?.readOnly, branchWriteBlocked: branchContentLocked, onBranchManagerOpen: openBranchManager }), rail: _jsx(EditorRail, { contentPanelOpen: contentPanelShown, contentPanelId: CONTENT_PANEL_ID, onContentToggle: () => contentPanelShown ? setContentPanelShown(false) : openNavigator(), contentToggleRef: contentToggleRef, onQuickOpen: openQuickOpen, quickOpenShortcut: quickOpenShortcut, quickOpenKeys: quickOpenKeys, branchesOpen: branchManagerOpen, onBranchesOpen: openBranchManager, onCommentsOpen: clientOperatingStrategy(operatingMode ?? 'prod').supportsComments() &&
|
|
1275
1329
|
branchNameState
|
|
1276
1330
|
? () => 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,
|
|
1331
|
+
: 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: _jsx(Box, { h: "100%", children: _jsx(PaneLayout, { formAside: contentPanelDocked && layout === 'stacked' ? contentPanel : undefined, formOverlay: contentPanelDocked ? undefined : contentPanel, formInert: contentOverlayShown, previewMaximized: previewShownMaximized,
|
|
1278
1332
|
// Sizing the pane, by pointer or key, gives the page the pane's width.
|
|
1279
1333
|
onSeparatorDrag: (dragging) => {
|
|
1280
1334
|
setPaneResizing(dragging);
|
|
@@ -1294,7 +1348,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1294
1348
|
// A hidden draft's discard changes nothing on screen, so it says so.
|
|
1295
1349
|
onDiscard: () => handleDiscardFileDraft(() => navigatorToggleRef.current, {
|
|
1296
1350
|
announce: true,
|
|
1297
|
-
}) })), fields: schema, value: displayValue, readOnly: contentReadOnly, onRemoved: handleFieldRemoved, commentBlockedReason: commentBlockedReason, removedItemFieldLabel: commentFieldLabel, onGetUserMetadata: getCommentUserMetadata, header: {
|
|
1351
|
+
}) })), fields: schema, value: displayValue, readOnly: contentReadOnly, onRemoved: handleFieldRemoved, savedValue: loadedValue, commentBlockedReason: commentBlockedReason, removedItemFieldLabel: commentFieldLabel, onGetUserMetadata: getCommentUserMetadata, header: {
|
|
1298
1352
|
title: currentEntry.label,
|
|
1299
1353
|
typeLabel: entryTypeLabel(activeCollections, currentEntry),
|
|
1300
1354
|
},
|
|
@@ -1311,7 +1365,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1311
1365
|
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
1366
|
setLoadFailedKey(null);
|
|
1313
1367
|
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:
|
|
1368
|
+
}, children: "Retry" }) })) }) }), overlays: _jsxs(_Fragment, { children: [_jsx(QuickOpenPalette, { opened: quickOpenOpen, onOpen: openQuickOpen, onClose: () => setQuickOpenOpen(false), returnFocusTo: quickOpenFromOverlay ? navigatorReturnTarget : undefined, entries: quickOpenEntries,
|
|
1315
1369
|
// The hidden root's entries sit at the top of the tree, so they get no heading here either.
|
|
1316
1370
|
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
1371
|
try {
|
|
@@ -1343,7 +1397,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1343
1397
|
: undefined, onRemoveEntryType: editingCollection ? (path, name) => removeEntryType(path, name) : undefined, onReloadEntryTypes: editingCollection ? readEntryTypes : undefined, onClose: handleCloseCollectionEditor, isSaving: schemaLoading, error: collectionEditorError, restored: restoredCollectionInput, onDiscardRestored: () => {
|
|
1344
1398
|
collectionDraftRef.current = null;
|
|
1345
1399
|
setRestoredCollectionInput(null);
|
|
1346
|
-
} })), renamingEntry && (_jsx(RenameEntryModal, { isOpen: renameModalOpen, entryLabel: renamingEntry.label, currentSlug: renamingEntry.slug || '', onSave: handleRenameSubmit,
|
|
1400
|
+
} })), renamingEntry && (_jsx(RenameEntryModal, { isOpen: renameModalOpen, entryLabel: renamingEntry.label, nameSource: entryNameSource(activeCollections, renamingEntry, entryValue(renamingEntry)), hasPage: !!renamingEntry.previewSrc, currentSlug: renamingEntry.slug || '', onSave: handleRenameSubmit,
|
|
1347
1401
|
// `renamingEntry` stays until the next rename, so the dialog plays its exit and
|
|
1348
1402
|
// returns focus.
|
|
1349
1403
|
onClose: () => {
|
|
@@ -5,9 +5,16 @@ type TreeController = ReturnType<typeof useTree>;
|
|
|
5
5
|
export interface EntryNavItem {
|
|
6
6
|
path: LogicalPath;
|
|
7
7
|
label: string;
|
|
8
|
+
/** The entry's title, matched by the filter where the label is a singleton's type label. */
|
|
9
|
+
title?: string;
|
|
8
10
|
/** Matched by the filter; the last segment of `path` when absent. */
|
|
9
11
|
slug?: string;
|
|
12
|
+
/** The row's second line: the entry's type, or "Missing" (`entryStatusLabel`). */
|
|
10
13
|
status?: string;
|
|
14
|
+
/** The entry's file is gone: its second line is error text. */
|
|
15
|
+
missing?: boolean;
|
|
16
|
+
/** The entry has no page on the site, so its menu changes its slug rather than its URL. */
|
|
17
|
+
noPage?: boolean;
|
|
11
18
|
collectionPath?: LogicalPath;
|
|
12
19
|
contentId?: ContentId;
|
|
13
20
|
/** True when this entry's file conflicted during rebase */
|
|
@@ -46,7 +53,7 @@ export interface EntryNavigatorProps {
|
|
|
46
53
|
onExpandedStateChange?: (state: Record<string, boolean>) => void;
|
|
47
54
|
/** Called when user requests to delete an entry */
|
|
48
55
|
onDeleteEntry?: (path: LogicalPath) => void;
|
|
49
|
-
/** Called when user
|
|
56
|
+
/** Called when the user asks to change an entry's URL (its slug) */
|
|
50
57
|
onRenameEntry?: (path: LogicalPath) => void;
|
|
51
58
|
/** Called when user reorders an entry within a collection */
|
|
52
59
|
onReorderEntry?: (collectionPath: LogicalPath, contentId: string, direction: 'up' | 'down') => void;
|
|
@@ -55,12 +62,17 @@ export interface EntryNavigatorProps {
|
|
|
55
62
|
/** When true and the tree is empty, show a loading indicator instead of "No content" */
|
|
56
63
|
loading?: boolean;
|
|
57
64
|
/**
|
|
58
|
-
* When true,
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
65
|
+
* When true, replaces each row's mutation menu (create, rename, delete, reorder, add/edit
|
|
66
|
+
* collection) with a lock -- selection/navigation stays enabled. Set on a branch that takes no
|
|
67
|
+
* writes, where they would only 403 after the fact. An entry the user may not edit keeps its own
|
|
68
|
+
* lock and gets none here. Default false.
|
|
62
69
|
*/
|
|
63
70
|
readOnly?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Why the branch takes no writes (`describeBranchLock`), the tooltip of each row's lock. While
|
|
73
|
+
* readOnly without one, the lock's slot stays empty.
|
|
74
|
+
*/
|
|
75
|
+
readOnlyReason?: string;
|
|
64
76
|
/** Entry paths with unsaved changes; each shows a dot, and so does every collection holding one. */
|
|
65
77
|
dirtyPaths?: ReadonlySet<string>;
|
|
66
78
|
/**
|