canopycms 0.0.68-int.113 → 0.0.68-int.114
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/dist/ai/generate.d.ts +4 -4
- package/dist/ai/generate.js +75 -14
- package/dist/ai/types.d.ts +12 -0
- package/dist/api/branch.js +13 -13
- package/dist/auth/cache.d.ts +1 -1
- package/dist/auth/cache.js +1 -1
- package/dist/auth/file-based-auth-cache.d.ts +8 -0
- package/dist/auth/file-based-auth-cache.js +11 -0
- package/dist/authorization/branch.d.ts +1 -1
- package/dist/authorization/branch.js +2 -1
- package/dist/cli/cli.js +160 -28
- package/dist/cli/generate-ai-content.js +156 -24
- package/dist/content-reader.js +2 -1
- package/dist/context.js +2 -1
- package/dist/editor/BranchManager.js +13 -17
- package/dist/editor/CommentsPanel.js +7 -6
- package/dist/editor/Editor.js +152 -30
- package/dist/editor/EditorAuthGate.js +2 -2
- package/dist/editor/EditorPanes.d.ts +10 -0
- package/dist/editor/EditorPanes.js +32 -8
- package/dist/editor/FormRenderer.d.ts +10 -0
- package/dist/editor/FormRenderer.js +92 -73
- package/dist/editor/PreviewFrame.d.ts +3 -1
- package/dist/editor/PreviewFrame.js +72 -9
- package/dist/editor/admin/SystemHealthPanel.js +27 -24
- package/dist/editor/branch-status.d.ts +16 -0
- package/dist/editor/branch-status.js +28 -0
- package/dist/editor/comments/BranchComments.js +1 -1
- package/dist/editor/comments/EntryComments.d.ts +3 -0
- package/dist/editor/comments/EntryComments.js +3 -3
- package/dist/editor/comments/FieldWrapper.d.ts +3 -0
- package/dist/editor/comments/FieldWrapper.js +2 -2
- package/dist/editor/comments/InlineCommentThread.js +2 -1
- package/dist/editor/comments/ThreadCarousel.js +2 -2
- package/dist/editor/components/ConfirmDeleteModal.js +1 -1
- package/dist/editor/components/EditorCrashScreen.js +1 -1
- package/dist/editor/components/EditorHeader.d.ts +2 -0
- package/dist/editor/components/EditorHeader.js +15 -36
- package/dist/editor/components/EditorSidebar.d.ts +5 -6
- package/dist/editor/components/EditorSidebar.js +2 -2
- package/dist/editor/components/EntryCreateModal.js +1 -1
- package/dist/editor/components/NoEditPermissionNotice.d.ts +3 -2
- package/dist/editor/components/NoEditPermissionNotice.js +3 -2
- package/dist/editor/components/ReadOnlyDraftNotice.d.ts +11 -0
- package/dist/editor/components/ReadOnlyDraftNotice.js +12 -0
- package/dist/editor/components/RenameEntryModal.js +1 -1
- package/dist/editor/components/StagedChangesDrawer.js +3 -3
- package/dist/editor/components/UndoToastMessage.d.ts +14 -0
- package/dist/editor/components/UndoToastMessage.js +9 -0
- package/dist/editor/components/UserBadge.js +9 -5
- package/dist/editor/copy.d.ts +14 -0
- package/dist/editor/copy.js +14 -0
- package/dist/editor/fields/BlockField.d.ts +5 -0
- package/dist/editor/fields/BlockField.js +38 -32
- package/dist/editor/fields/CodeField.d.ts +2 -0
- package/dist/editor/fields/CodeField.js +5 -2
- package/dist/editor/fields/DateTimeField.d.ts +2 -0
- package/dist/editor/fields/DateTimeField.js +5 -2
- package/dist/editor/fields/FieldCrashFallback.d.ts +3 -3
- package/dist/editor/fields/FieldCrashFallback.js +4 -4
- package/dist/editor/fields/FieldLabel.d.ts +24 -0
- package/dist/editor/fields/FieldLabel.js +9 -0
- package/dist/editor/fields/ImageField.d.ts +4 -0
- package/dist/editor/fields/ImageField.js +17 -8
- package/dist/editor/fields/InlineGroupField.js +4 -3
- package/dist/editor/fields/MarkdownField.d.ts +6 -0
- package/dist/editor/fields/MarkdownField.js +41 -11
- package/dist/editor/fields/MdxImageDialog.js +1 -1
- package/dist/editor/fields/NumberField.d.ts +2 -0
- package/dist/editor/fields/NumberField.js +4 -2
- package/dist/editor/fields/NumberListField.d.ts +2 -0
- package/dist/editor/fields/NumberListField.js +4 -2
- package/dist/editor/fields/ObjectField.d.ts +1 -0
- package/dist/editor/fields/ObjectField.js +5 -4
- package/dist/editor/fields/ReferenceField.d.ts +2 -0
- package/dist/editor/fields/ReferenceField.js +9 -3
- package/dist/editor/fields/SelectField.d.ts +2 -0
- package/dist/editor/fields/SelectField.js +8 -2
- package/dist/editor/fields/StringListField.d.ts +2 -0
- package/dist/editor/fields/StringListField.js +5 -2
- package/dist/editor/fields/TextField.d.ts +2 -0
- package/dist/editor/fields/TextField.js +5 -2
- package/dist/editor/fields/ToggleField.d.ts +2 -0
- package/dist/editor/fields/ToggleField.js +6 -3
- package/dist/editor/fields/entry-link/InsertEntryLink.js +1 -1
- package/dist/editor/fields/list-item-keys.d.ts +8 -0
- package/dist/editor/fields/list-item-keys.js +50 -0
- package/dist/editor/group-manager/GroupCard.js +1 -1
- package/dist/editor/group-manager/GroupForm.js +1 -1
- package/dist/editor/group-manager/InternalGroupsTab.js +1 -1
- package/dist/editor/group-manager/MemberList.js +1 -1
- package/dist/editor/group-manager/hooks/useGroupState.d.ts +1 -0
- package/dist/editor/group-manager/hooks/useGroupState.js +1 -1
- package/dist/editor/group-manager/index.js +26 -2
- package/dist/editor/hooks/useBranchActions.js +2 -2
- package/dist/editor/hooks/useBranchManager.js +36 -22
- package/dist/editor/hooks/useDraftManager.d.ts +8 -0
- package/dist/editor/hooks/useDraftManager.js +27 -5
- package/dist/editor/hooks/useEditorLayout.d.ts +45 -3
- package/dist/editor/hooks/useEditorLayout.js +97 -7
- package/dist/editor/hooks/useUserMetadata.js +12 -1
- package/dist/editor/media/AssetCard.js +1 -1
- package/dist/editor/media/CropStep.js +6 -1
- package/dist/editor/media/MediaLibraryBody.js +3 -3
- package/dist/editor/permission-manager/PermissionEditor.js +3 -3
- package/dist/editor/permission-manager/index.js +1 -1
- package/dist/editor/preview-bridge.js +2 -2
- package/dist/editor/schema-editor/CollectionEditor.js +1 -1
- package/dist/editor/schema-editor/EntryTypeEditor.js +1 -1
- package/dist/editor/theme.d.ts +1 -1
- package/dist/editor/theme.js +11 -11
- package/dist/editor/undo-removal.d.ts +44 -0
- package/dist/editor/undo-removal.js +129 -0
- package/dist/editor/user-display.d.ts +4 -0
- package/dist/editor/user-display.js +5 -0
- package/dist/editor/utils/confirm-modal.d.ts +8 -0
- package/dist/editor/utils/confirm-modal.js +12 -0
- package/dist/entry-link-resolver.d.ts +11 -1
- package/dist/entry-link-resolver.js +65 -5
- package/dist/git-manager.d.ts +6 -6
- package/dist/git-manager.js +16 -15
- package/dist/github-service.d.ts +25 -1
- package/dist/github-service.js +65 -6
- package/dist/http/handler.js +2 -1
- package/dist/static/seo.d.ts +5 -5
- package/dist/static/seo.js +5 -5
- package/dist/system-users.d.ts +12 -0
- package/dist/system-users.js +12 -0
- package/dist/types.d.ts +9 -1
- package/dist/utils/error.js +4 -3
- package/dist/utils/escape-text.d.ts +7 -0
- package/dist/utils/escape-text.js +27 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-policy.d.ts +124 -0
- package/dist/worker/branch-policy.js +243 -0
- package/dist/worker/bundle-header.d.ts +117 -0
- package/dist/worker/bundle-header.js +253 -0
- package/dist/worker/cms-worker.d.ts +68 -17
- package/dist/worker/cms-worker.js +116 -33
- package/dist/worker/gateway-egress.d.ts +43 -0
- package/dist/worker/gateway-egress.js +209 -0
- package/dist/worker/gateway-instance.d.ts +42 -0
- package/dist/worker/gateway-instance.js +129 -0
- package/dist/worker/git-process.d.ts +81 -0
- package/dist/worker/git-process.js +234 -0
- package/dist/worker/git-sync.d.ts +8 -7
- package/dist/worker/git-sync.js +184 -74
- package/dist/worker/github-gateway-http.d.ts +62 -0
- package/dist/worker/github-gateway-http.js +977 -0
- package/dist/worker/github-gateway-wire.d.ts +589 -0
- package/dist/worker/github-gateway-wire.js +484 -0
- package/dist/worker/github-gateway.d.ts +152 -58
- package/dist/worker/github-gateway.js +485 -129
- package/dist/worker/github-mirror-upkeep.d.ts +75 -0
- package/dist/worker/github-mirror-upkeep.js +266 -0
- package/dist/worker/github-mirror.d.ts +271 -71
- package/dist/worker/github-mirror.js +755 -238
- package/dist/worker/github-transfer.d.ts +56 -0
- package/dist/worker/github-transfer.js +444 -0
- package/dist/worker/self-checks.d.ts +107 -0
- package/dist/worker/self-checks.js +358 -0
- package/dist/worker/shared-repo-git.d.ts +20 -7
- package/dist/worker/shared-repo-git.js +51 -22
- package/dist/worker/stale-mirror.d.ts +82 -0
- package/dist/worker/stale-mirror.js +130 -0
- package/dist/worker/task-runner.d.ts +2 -0
- package/dist/worker/task-runner.js +49 -20
- package/dist/worker/worker-context.d.ts +6 -0
- package/package.json +1 -1
package/dist/editor/Editor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useMemo, useState, useRef } from 'react';
|
|
4
|
-
import { ActionIcon, Alert, Box, Drawer, Group, Menu, Paper, Text } from '@mantine/core';
|
|
4
|
+
import { ActionIcon, Alert, Box, Button, Drawer, Group, Menu, Paper, Skeleton, Stack, Text, } from '@mantine/core';
|
|
5
5
|
import { IconChevronDown, IconChevronUp, IconDots, IconFolderPlus, IconPlus, } from '@tabler/icons-react';
|
|
6
6
|
import { notifications } from '@mantine/notifications';
|
|
7
7
|
const noop = () => { };
|
|
@@ -17,10 +17,11 @@ import { GroupManager } from './GroupManager.js';
|
|
|
17
17
|
import { PermissionManager } from './PermissionManager.js';
|
|
18
18
|
import { SystemHealthPanel } from './admin/SystemHealthPanel.js';
|
|
19
19
|
// Import directly from helpers to avoid server-only code in authorization barrel
|
|
20
|
-
import { isAdmin } from '../authorization/helpers.js';
|
|
20
|
+
import { isAdmin, isPrivileged } from '../authorization/helpers.js';
|
|
21
21
|
import { buildPreviewSrc, buildCollectionLabels, buildBreadcrumbSegments } from './editor-utils.js';
|
|
22
22
|
import { useEditorLayout, useDraftManager, useEntryManager, useGroupManager, usePermissionManager, useCommentSystem, useBranchManager, useUserContext, useSchemaManager, } from './hooks/index.js';
|
|
23
23
|
import { useBranchActions } from './hooks/useBranchActions.js';
|
|
24
|
+
import { editorLayoutStorageKey } from './hooks/useEditorLayout.js';
|
|
24
25
|
import { EntrySchemaUnavailableError } from './hooks/useEntryManager.js';
|
|
25
26
|
import { useEntryLinkResolution } from './hooks/useEntryLinkResolution.js';
|
|
26
27
|
import { useReferenceResolution } from './hooks/useReferenceResolution.js';
|
|
@@ -31,6 +32,7 @@ import { EntryCreateModal } from './components/EntryCreateModal.js';
|
|
|
31
32
|
import { ConfirmDeleteModal } from './components/ConfirmDeleteModal.js';
|
|
32
33
|
import { ReferencedByList, referencedDeleteMessage } from './components/ReferencedByList.js';
|
|
33
34
|
import { NoEditPermissionNotice } from './components/NoEditPermissionNotice.js';
|
|
35
|
+
import { ReadOnlyDraftNotice } from './components/ReadOnlyDraftNotice.js';
|
|
34
36
|
import { UnavailableEntryNotice } from './components/UnavailableEntryNotice.js';
|
|
35
37
|
import { unavailableSchemaRefs } from './unavailable-entry-type.js';
|
|
36
38
|
import { EditorCrashBoundary } from './components/EditorCrashScreen.js';
|
|
@@ -38,6 +40,9 @@ import { CollectionEditor } from './schema-editor/index.js';
|
|
|
38
40
|
import { AssetContextProvider, SiteMdxAllowContext, authenticatedAssetBase, useApiClient, } from './context/index.js';
|
|
39
41
|
import { EntryLinkContext } from './fields/entry-link/index.js';
|
|
40
42
|
import { MediaLibrary } from './media/MediaLibrary.js';
|
|
43
|
+
import { removalAnchors, restoreRemoval } from './undo-removal.js';
|
|
44
|
+
import { UndoToastMessage } from './components/UndoToastMessage.js';
|
|
45
|
+
import { getNotificationDuration } from './utils/env.js';
|
|
41
46
|
// Stable fallbacks, so reference resolution's memo and effect don't re-run on every render
|
|
42
47
|
// while no entry is open.
|
|
43
48
|
const EMPTY_SCHEMA = [];
|
|
@@ -76,11 +81,14 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
76
81
|
const [previewError, setPreviewError] = useState(null);
|
|
77
82
|
const apiClient = useApiClient();
|
|
78
83
|
const { userContext } = useUserContext();
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
+
// Admin surfaces (permissions, groups, system health, collection management and entry order,
|
|
85
|
+
// whose API routes all take the `admin` guard) render only for admins: each menu item, action
|
|
86
|
+
// and drawer mount is gated here. Until whoami answers, the user counts as no admin.
|
|
87
|
+
const userIsAdmin = isAdmin(userContext?.groups);
|
|
88
|
+
// Name lookups take the `privileged` guard (admins and reviewers); anyone else sees ids, and
|
|
89
|
+
// no lookup is sent that would only be refused.
|
|
90
|
+
const canResolveUserNames = isPrivileged(userContext?.groups);
|
|
91
|
+
const { layout, setLayout, highlightEnabled, setHighlightEnabled, sideSplitPercent, stackedSplitPercent, setSplitPercent, headerRef, headerHeight, } = useEditorLayout({ storageKey: editorLayoutStorageKey(basePath) });
|
|
84
92
|
// Comments state (shared between useCommentSystem and useBranchManager)
|
|
85
93
|
const [commentsForBranchSummaries, setCommentsForBranchSummaries] = useState([]);
|
|
86
94
|
// 1. Branch manager (provides branchNameState, no dependencies)
|
|
@@ -136,7 +144,7 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
136
144
|
setPreviewError(null);
|
|
137
145
|
}, [currentEntry?.contentId]);
|
|
138
146
|
// 3. Draft manager (depends on branchNameState, selectedPath from useEntryManager)
|
|
139
|
-
const { setDrafts, loadedValues, setLoadedValues, effectiveValue, modifiedCount, editedFiles, handleSave, handleDiscardDrafts, handleDiscardFileDraft, handleReload, isSelectedDirty, resolveUnsaved, fieldErrors, } = useDraftManager({
|
|
147
|
+
const { drafts, setDrafts, loadedValues, setLoadedValues, loadedValue, effectiveValue, modifiedCount, editedFiles, handleSave, handleDiscardDrafts, handleDiscardFileDraft, handleReload, isSelectedDirty, resolveUnsaved, fieldErrors, draftStorageFailed, } = useDraftManager({
|
|
140
148
|
branchName: branchNameState,
|
|
141
149
|
selectedPath,
|
|
142
150
|
currentEntry,
|
|
@@ -150,6 +158,8 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
150
158
|
onSaved: () => {
|
|
151
159
|
void refreshEntries(branchNameState).catch((err) => console.error('Failed to refresh entries after save', err));
|
|
152
160
|
},
|
|
161
|
+
// An Undo built on a discarded draft would put back an item the server's copy already has.
|
|
162
|
+
onDraftsReset: (contentId) => dismissUndoToasts(contentId),
|
|
153
163
|
});
|
|
154
164
|
// 4. Branch actions (depends on resolveUnsaved, setBranchName)
|
|
155
165
|
const { handleBranchChange, confirmCreate: confirmCreateBranch, handleCreateBranch, confirmOpen: branchConfirmOpen, } = useBranchActions({
|
|
@@ -180,6 +190,92 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
180
190
|
branchName: branchNameState,
|
|
181
191
|
onSchemaChange: () => refreshEntries(branchNameState),
|
|
182
192
|
});
|
|
193
|
+
// Read-only content shows the saved value, never a draft; a restored draft is kept, unseen,
|
|
194
|
+
// until it is discarded or the branch unlocks.
|
|
195
|
+
const contentReadOnly = branchContentLocked || currentEntry?.canEdit === false;
|
|
196
|
+
const displayValue = contentReadOnly ? loadedValue : effectiveValue;
|
|
197
|
+
// Read when an edit lands, which an upload or other async field work can do renders later.
|
|
198
|
+
const contentReadOnlyRef = useRef(contentReadOnly);
|
|
199
|
+
contentReadOnlyRef.current = contentReadOnly;
|
|
200
|
+
// Undo for a form Remove: a toast whose Undo puts the item back into the entry's draft as it
|
|
201
|
+
// is then, through a functional update, so edits made since are kept. It acts only on the
|
|
202
|
+
// branch and entry it was raised for, and its toast closes when either changes.
|
|
203
|
+
const loadedValuesRef = useRef(loadedValues);
|
|
204
|
+
loadedValuesRef.current = loadedValues;
|
|
205
|
+
const draftsRef = useRef(drafts);
|
|
206
|
+
draftsRef.current = drafts;
|
|
207
|
+
// Open Undo toasts, by id, with the entry each belongs to.
|
|
208
|
+
const undoToastsRef = useRef(new Map());
|
|
209
|
+
const nextUndoToastRef = useRef(0);
|
|
210
|
+
const dismissUndoToasts = (contentId) => {
|
|
211
|
+
undoToastsRef.current.forEach((entry, id) => {
|
|
212
|
+
if (contentId !== undefined && entry !== contentId)
|
|
213
|
+
return;
|
|
214
|
+
notifications.hide(id);
|
|
215
|
+
undoToastsRef.current.delete(id);
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
const handleFieldRemoved = (removal) => {
|
|
219
|
+
const contentId = currentEntry?.contentId;
|
|
220
|
+
if (!contentId || contentReadOnlyRef.current)
|
|
221
|
+
return;
|
|
222
|
+
const branch = branchNameState;
|
|
223
|
+
const fields = schema;
|
|
224
|
+
// Taken from the value this render shows, which the removal has not reached yet.
|
|
225
|
+
const anchored = {
|
|
226
|
+
...removal,
|
|
227
|
+
anchors: displayValue ? removalAnchors(fields, displayValue, removal) : undefined,
|
|
228
|
+
};
|
|
229
|
+
nextUndoToastRef.current += 1;
|
|
230
|
+
const id = `canopy-undo-${nextUndoToastRef.current}`;
|
|
231
|
+
let used = false;
|
|
232
|
+
const undo = () => {
|
|
233
|
+
// A closing toast stays clickable while it animates out: act once, and only while open.
|
|
234
|
+
if (used)
|
|
235
|
+
return;
|
|
236
|
+
used = true;
|
|
237
|
+
const open = undoToastsRef.current.has(id);
|
|
238
|
+
notifications.hide(id);
|
|
239
|
+
undoToastsRef.current.delete(id);
|
|
240
|
+
if (!open) {
|
|
241
|
+
notifications.show({
|
|
242
|
+
message: `Too late to undo removing ${removal.label}.`,
|
|
243
|
+
color: 'yellow',
|
|
244
|
+
});
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (currentBranchRef.current !== branch ||
|
|
248
|
+
currentContentIdRef.current !== contentId ||
|
|
249
|
+
contentReadOnlyRef.current) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const now = draftsRef.current[contentId] ?? loadedValuesRef.current[contentId];
|
|
253
|
+
if (!now || !restoreRemoval(fields, now, anchored)) {
|
|
254
|
+
notifications.show({
|
|
255
|
+
message: `Couldn't undo removing ${removal.label}: that part of the entry has changed. Add it again if you still need it.`,
|
|
256
|
+
color: 'yellow',
|
|
257
|
+
});
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
setDrafts((prev) => {
|
|
261
|
+
const base = prev[contentId] ?? loadedValuesRef.current[contentId];
|
|
262
|
+
const restored = base ? restoreRemoval(fields, base, anchored) : undefined;
|
|
263
|
+
return restored ? { ...prev, [contentId]: restored } : prev;
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
undoToastsRef.current.set(id, contentId);
|
|
267
|
+
const toast = {
|
|
268
|
+
id,
|
|
269
|
+
message: (_jsx(UndoToastMessage, { label: removal.label, onUndo: undo, onHold: () => notifications.update({ ...toast, autoClose: false }), onRelease: () => notifications.update(toast) })),
|
|
270
|
+
autoClose: getNotificationDuration(8000),
|
|
271
|
+
withCloseButton: true,
|
|
272
|
+
onClose: () => undoToastsRef.current.delete(id),
|
|
273
|
+
};
|
|
274
|
+
notifications.show(toast);
|
|
275
|
+
};
|
|
276
|
+
useEffect(() => () => dismissUndoToasts(), [currentEntry?.contentId, branchNameState]);
|
|
277
|
+
// Only once the branch has answered, so the fail-closed lock while it loads raises no notice.
|
|
278
|
+
const hiddenDraftNotice = contentReadOnly && currentBranch !== undefined && !!currentEntry && isSelectedDirty();
|
|
183
279
|
const collectionLabels = useMemo(() => buildCollectionLabels(activeCollections), [activeCollections]);
|
|
184
280
|
const schema = currentEntry?.schema ?? EMPTY_SCHEMA;
|
|
185
281
|
// Gated on `loadedValues[contentId]` (not `drafts[contentId]`) so a
|
|
@@ -198,6 +294,18 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
198
294
|
// that same id. Qualifying the key lets both loads coexist; the branch check
|
|
199
295
|
// at settle time decides which one is allowed to write.
|
|
200
296
|
const loadingEntryIdsRef = useRef(new Set());
|
|
297
|
+
// The entry (keyed like `loadingEntryIdsRef`) whose last read failed, and Retry's bump that
|
|
298
|
+
// runs the load effect below again.
|
|
299
|
+
const [loadFailedKey, setLoadFailedKey] = useState(null);
|
|
300
|
+
const [loadAttempt, setLoadAttempt] = useState(0);
|
|
301
|
+
// Reset during render, not in an effect, so returning to a failed entry never paints its Alert
|
|
302
|
+
// for the frame before its load starts again.
|
|
303
|
+
const selectionKey = `${branchNameState}:${currentEntry?.contentId ?? ''}`;
|
|
304
|
+
const [loadFailedSelection, setLoadFailedSelection] = useState(selectionKey);
|
|
305
|
+
if (loadFailedSelection !== selectionKey) {
|
|
306
|
+
setLoadFailedSelection(selectionKey);
|
|
307
|
+
setLoadFailedKey(null);
|
|
308
|
+
}
|
|
201
309
|
// Entries (keyed like `loadingEntryIdsRef`) whose read the API refused as SCHEMA_UNAVAILABLE.
|
|
202
310
|
// The ref stops the load effect retrying them; the state re-renders the notice.
|
|
203
311
|
const schemaUnavailableRef = useRef(new Set());
|
|
@@ -261,6 +369,7 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
261
369
|
return;
|
|
262
370
|
loadingEntryIdsRef.current.add(inFlightKey);
|
|
263
371
|
setEntriesLoading(true);
|
|
372
|
+
setLoadFailedKey((failed) => (failed === inFlightKey ? null : failed));
|
|
264
373
|
try {
|
|
265
374
|
const loaded = await loadEntry(currentEntry);
|
|
266
375
|
// A branch switch during the await makes this response another
|
|
@@ -307,10 +416,11 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
307
416
|
// different branch).
|
|
308
417
|
if (currentBranchRef.current === requestBranch && currentContentIdRef.current === contentId) {
|
|
309
418
|
notifications.show({ message: 'Failed to load entry', color: 'red' });
|
|
419
|
+
setLoadFailedKey(inFlightKey);
|
|
310
420
|
}
|
|
311
421
|
});
|
|
312
422
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- stable setters, run only on entry/path/branch/loadedValues change
|
|
313
|
-
}, [currentEntry, loadedValues, selectedPath, branchNameState]);
|
|
423
|
+
}, [currentEntry, loadedValues, selectedPath, branchNameState, loadAttempt]);
|
|
314
424
|
const handleOpenCollectionEditor = async (collection, parentPath) => {
|
|
315
425
|
if (collection) {
|
|
316
426
|
try {
|
|
@@ -601,11 +711,15 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
601
711
|
onAdd: canAddEntry
|
|
602
712
|
? () => (onCreateEntry ? onCreateEntry(node.path) : handleCreateEntry(node.path))
|
|
603
713
|
: undefined,
|
|
604
|
-
onEdit: node.type === 'collection'
|
|
605
|
-
|
|
714
|
+
onEdit: userIsAdmin && node.type === 'collection'
|
|
715
|
+
? () => handleOpenCollectionEditor(node)
|
|
716
|
+
: undefined,
|
|
717
|
+
onAddSubCollection: userIsAdmin && node.type === 'collection'
|
|
606
718
|
? () => handleOpenCollectionEditor(null, node.path)
|
|
607
719
|
: undefined,
|
|
608
|
-
onDelete: node.type === 'collection'
|
|
720
|
+
onDelete: userIsAdmin && node.type === 'collection'
|
|
721
|
+
? () => handleDeleteCollection(node.path)
|
|
722
|
+
: undefined,
|
|
609
723
|
};
|
|
610
724
|
};
|
|
611
725
|
return activeCollections.map((node) => build(node));
|
|
@@ -618,6 +732,7 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
618
732
|
contentRoot,
|
|
619
733
|
currentBranch,
|
|
620
734
|
schemaUnavailableKeys,
|
|
735
|
+
userIsAdmin,
|
|
621
736
|
]);
|
|
622
737
|
// Tree expansion state - persists across drawer close/open
|
|
623
738
|
const treeExpandedStateRef = useRef({});
|
|
@@ -644,12 +759,12 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
644
759
|
// Resolved here, outside the form and its per-field crash boundaries, and during render, so
|
|
645
760
|
// the preview never receives a bare reference id: a reference is its target or `null`.
|
|
646
761
|
const { resolvedValue: previewValue, loadingState: previewLoadingState } = useReferenceResolution({
|
|
647
|
-
value:
|
|
762
|
+
value: displayValue ?? EMPTY_VALUE,
|
|
648
763
|
fields: schema,
|
|
649
764
|
branch: branchNameState,
|
|
650
765
|
entryKey: currentEntry?.contentId,
|
|
651
766
|
});
|
|
652
|
-
const previewFrameData = useMemo(() => (
|
|
767
|
+
const previewFrameData = useMemo(() => (displayValue ? resolveEntryLinks(previewValue) : displayValue), [previewValue, displayValue, resolveEntryLinks]);
|
|
653
768
|
// Entry link context for the InsertEntryLink toolbar button
|
|
654
769
|
const entryLinkContextValue = useMemo(() => ({
|
|
655
770
|
entries: entriesState.map((e) => ({
|
|
@@ -670,7 +785,7 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
670
785
|
src: currentEntry?.previewSrc,
|
|
671
786
|
highlightEnabled,
|
|
672
787
|
fields: schema,
|
|
673
|
-
data:
|
|
788
|
+
data: displayValue,
|
|
674
789
|
});
|
|
675
790
|
// An unavailable entry previews nothing: the site cannot read it, and a stored draft would
|
|
676
791
|
// otherwise mount the frame at a page that answers not-found.
|
|
@@ -697,7 +812,7 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
697
812
|
const breadcrumbSegments = useMemo(() => buildBreadcrumbSegments(currentEntry, collectionLabels), [collectionLabels, currentEntry]);
|
|
698
813
|
return (_jsx(CanopyCMSProvider, { ...(themeOptions ?? {}), children: _jsx(AssetContextProvider, { basePath: basePath, children: _jsxs(Box, { bg: "gray.0", style: { minHeight: '100vh', width: '100%' }, children: [_jsx(EditorHeader, { ref: headerRef, siteTitle: siteTitle, siteSubtitle: siteSubtitle, headerTitle: headerTitle, currentEntry: currentEntry, branchName: branchNameState, operatingMode: operatingMode, branchStatus: currentBranch?.status, busy: busy, breadcrumbSegments: breadcrumbSegments, editedFiles: editedFiles, modifiedCount: modifiedCount, unresolvedCommentCount: comments.filter((t) => !t.resolved).length, comments: comments,
|
|
699
814
|
// A draft kept for an unavailable entry stays in storage but cannot be saved.
|
|
700
|
-
hasUnsavedChanges: !currentEntryUnavailable && isSelectedDirty(), userContext: userContext, branchCreatedBy: currentBranch?.createdBy, branchAccess: currentBranch?.access,
|
|
815
|
+
hasUnsavedChanges: !currentEntryUnavailable && !contentReadOnly && isSelectedDirty(), draftStorageFailed: draftStorageFailed, userContext: userContext, branchCreatedBy: currentBranch?.createdBy, branchAccess: currentBranch?.access,
|
|
701
816
|
// Fail-closed for the same reason branchContentLocked above is:
|
|
702
817
|
// isProtected/writeBlocked gate real actions in EditorHeader (hiding
|
|
703
818
|
// Submit, disabling Save), so a missing wire value must read as
|
|
@@ -729,25 +844,32 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
729
844
|
height: `calc(100vh - ${headerHeight + footerHeight}px)`,
|
|
730
845
|
minHeight: 0,
|
|
731
846
|
position: 'relative',
|
|
732
|
-
}, children: [_jsx(Box, { style: { flex: 1, minHeight: 0, height: '100%', width: '100%' }, children: _jsx(EditorPanes, { layout: layout, onLayoutChange: (next) => setLayout(next), preview: renderPreview && currentEntry && !currentEntryUnavailable
|
|
733
|
-
? renderPreview(currentEntry,
|
|
847
|
+
}, children: [_jsx(Box, { style: { flex: 1, minHeight: 0, height: '100%', width: '100%' }, children: _jsx(EditorPanes, { layout: layout, onLayoutChange: (next) => setLayout(next), sideSplitPercent: sideSplitPercent, stackedSplitPercent: stackedSplitPercent, onSplitPercentChange: setSplitPercent, preview: renderPreview && currentEntry && !currentEntryUnavailable
|
|
848
|
+
? renderPreview(currentEntry, displayValue)
|
|
734
849
|
: defaultPreview, form: !currentEntry ? (_jsx(CenteredMessage, { children: !branchNameState && busy
|
|
735
850
|
? 'Setting up your branch workspace…'
|
|
736
851
|
: entriesInitializing
|
|
737
852
|
? 'Loading content…'
|
|
738
|
-
: 'Select an item to start editing.' })) : currentEntryUnavailable ? (_jsx(UnavailableEntryNotice, { schemaRefs: currentEntry.unavailable ? [currentEntry.unavailable.schemaRef] : [] })) : currentEntry.canEdit === false ? (_jsx(NoEditPermissionNotice, { entryPath: currentEntry.path })) : schema.length > 0 &&
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
853
|
+
: 'Select an item to start editing.' })) : currentEntryUnavailable ? (_jsx(UnavailableEntryNotice, { schemaRefs: currentEntry.unavailable ? [currentEntry.unavailable.schemaRef] : [] })) : currentEntry.canEdit === false ? (_jsx(NoEditPermissionNotice, { entryPath: currentEntry.path })) : schema.length > 0 && displayValue ? (_jsx(EntryLinkContext.Provider, { value: entryLinkContextValue, children: _jsxs(SiteMdxAllowContext.Provider, { value: mdxAllow, children: [hiddenDraftNotice && (_jsx(ReadOnlyDraftNotice, { permanentlyReadOnly: (currentBranch?.isProtected ?? true) ||
|
|
854
|
+
currentBranch?.status === 'archived', onDiscard: handleDiscardFileDraft })), _jsx(FormRenderer, { fields: schema, value: displayValue, readOnly: contentReadOnly, onRemoved: handleFieldRemoved, onGetUserMetadata: canResolveUserNames ? handleGetUserMetadata : undefined,
|
|
855
|
+
// The one writer of drafts. Read-only content gets none, whatever a
|
|
856
|
+
// field emits (MDXEditor's mount-time normalisation included).
|
|
857
|
+
onChange: (next) => {
|
|
858
|
+
const contentId = currentEntry?.contentId;
|
|
859
|
+
if (contentId && !contentReadOnlyRef.current) {
|
|
860
|
+
setDrafts((prev) => ({ ...prev, [contentId]: next }));
|
|
861
|
+
}
|
|
862
|
+
}, customRenderers: customRenderers, branch: branchNameState, comments: comments, currentEntryPath: selectedPath, currentUserId: currentUser, canResolve: canResolveComments, focusedFieldPath: focusedFieldPath, highlightThreadId: highlightThreadId, onAddComment: handleAddComment, onResolveThread: handleResolveThread, fieldErrors: fieldErrors, format: currentEntry.format, conflictNotice: !!(currentEntry?.contentId &&
|
|
863
|
+
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: () => {
|
|
864
|
+
setLoadFailedKey(null);
|
|
865
|
+
setLoadAttempt((attempt) => attempt + 1);
|
|
866
|
+
}, children: "Retry" }) })) }) }), _jsx(EditorSidebar, { layout: layout, highlightEnabled: highlightEnabled, previewMarksNothing: previewMarksNothing, previewInexactMarks: previewInexactMarks, sidebarWidth: sidebarWidth, headerHeight: headerHeight, footerHeight: footerHeight, onLayoutChange: setLayout, onHighlightToggle: () => {
|
|
745
867
|
setHighlightEnabled(!highlightEnabled);
|
|
746
868
|
clearPreviewMarks();
|
|
747
|
-
}, onPermissionManagerOpen: () => setPermissionManagerOpen(true), onGroupManagerOpen: () => setGroupManagerOpen(true), onMediaLibraryOpen: () => setMediaLibraryOpen(true), onSystemHealthOpen:
|
|
869
|
+
}, onPermissionManagerOpen: userIsAdmin ? () => setPermissionManagerOpen(true) : undefined, onGroupManagerOpen: userIsAdmin ? () => setGroupManagerOpen(true) : undefined, onMediaLibraryOpen: () => setMediaLibraryOpen(true), onSystemHealthOpen: userIsAdmin ? () => setSystemHealthOpen(true) : undefined, AccountComponent: AccountComponent, onAccountClick: onAccountClick, onLogoutClick: onLogoutClick })] }) }), _jsx(EditorFooter, {}), _jsxs(Drawer.Root, { opened: navigatorOpen, onClose: () => setNavigatorOpen(false), position: "left", size: 360, children: [_jsx(Drawer.Overlay, { blur: 2 }), _jsxs(Drawer.Content, { children: [_jsxs(Drawer.Header, { children: [_jsx(Drawer.Title, { children: "Content" }), _jsxs(Group, { gap: "xs", children: [navCollections &&
|
|
748
870
|
navCollections.length > 0 &&
|
|
749
871
|
!branchContentLocked &&
|
|
750
|
-
(navCollections[0].onAdd || navCollections[0].onAddSubCollection) && (_jsxs(Menu, { shadow: "md", width: 200, withinPortal: true, position: "bottom-end", children: [_jsx(Menu.Target, { children: _jsx(ActionIcon, { variant: "subtle", color: "gray",
|
|
872
|
+
(navCollections[0].onAdd || navCollections[0].onAddSubCollection) && (_jsxs(Menu, { shadow: "md", width: 200, withinPortal: true, position: "bottom-end", children: [_jsx(Menu.Target, { children: _jsx(ActionIcon, { variant: "subtle", color: "gray", "aria-label": "Content actions", children: _jsx(IconDots, { size: 16 }) }) }), _jsxs(Menu.Dropdown, { children: [navCollections[0].onAdd && (_jsx(Menu.Item, { leftSection: _jsx(IconPlus, { size: 14 }), onClick: () => navCollections[0].onAdd?.(), children: "Add Entry" })), navCollections[0].onAddSubCollection && (_jsx(Menu.Item, { leftSection: _jsx(IconFolderPlus, { size: 14 }), onClick: () => navCollections[0].onAddSubCollection?.(), children: "Add Collection" }))] })] })), _jsx(ActionIcon, { variant: "subtle", color: "gray", onClick: handleCollapseAll, title: "Collapse all folders", "aria-label": "Collapse all folders", children: _jsx(IconChevronUp, { size: 16 }) }), _jsx(ActionIcon, { variant: "subtle", color: "gray", onClick: handleExpandAll, title: "Expand all folders", "aria-label": "Expand all folders", children: _jsx(IconChevronDown, { size: 16 }) }), _jsx(Drawer.CloseButton, {})] })] }), _jsx(Drawer.Body, { p: 0, children: _jsx(Box, { px: "md", children: _jsx(EntryNavigator, { collections: navCollections, items: navCollections
|
|
751
873
|
? undefined
|
|
752
874
|
: entriesState.map((e) => ({
|
|
753
875
|
path: e.path,
|
|
@@ -756,7 +878,7 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
756
878
|
})), selectedPath: selectedPath, onSelect: (id) => {
|
|
757
879
|
setSelectedPath(id);
|
|
758
880
|
setNavigatorOpen(false);
|
|
759
|
-
}, onTreeControllerReady: handleTreeControllerReady, expandedStateRef: treeExpandedStateRef, onExpandedStateChange: handleExpandedStateChange, onDeleteEntry: handleDeleteEntry, onRenameEntry: handleRenameEntry, onReorderEntry: handleReorderEntry, hiddenRootPath: hiddenRootPath, loading: entriesInitializing, readOnly: branchContentLocked }) }) })] })] }), _jsx(BranchesDrawer, { opened: branchManagerOpen, onClose: () => setBranchManagerOpen(false), confirmOpen: branchConfirmOpen, children: _jsx(BranchManager, { branches: branchSummaries, mode: operatingMode, user: userContext, onSelect: async (name) => {
|
|
881
|
+
}, onTreeControllerReady: handleTreeControllerReady, expandedStateRef: treeExpandedStateRef, onExpandedStateChange: handleExpandedStateChange, onDeleteEntry: handleDeleteEntry, onRenameEntry: handleRenameEntry, onReorderEntry: userIsAdmin ? handleReorderEntry : undefined, hiddenRootPath: hiddenRootPath, loading: entriesInitializing, readOnly: branchContentLocked }) }) })] })] }), _jsx(BranchesDrawer, { opened: branchManagerOpen, onClose: () => setBranchManagerOpen(false), confirmOpen: branchConfirmOpen, children: _jsx(BranchManager, { branches: branchSummaries, mode: operatingMode, user: userContext, onSelect: async (name) => {
|
|
760
882
|
try {
|
|
761
883
|
await handleBranchChange(name);
|
|
762
884
|
setBranchManagerOpen(false);
|
|
@@ -773,9 +895,9 @@ const EditorContent = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
773
895
|
handleRequestChanges(name).catch((err) => console.error(err));
|
|
774
896
|
}, onDelete: (name) => {
|
|
775
897
|
handleDelete(name).catch((err) => console.error(err));
|
|
776
|
-
}, onClose: () => setBranchManagerOpen(false), comments: comments, currentUserId: currentUser, canResolve: canResolveComments, onAddComment: handleAddComment, onResolveThread: handleResolveThread, highlightThreadId: highlightThreadId, onGetUserMetadata: handleGetUserMetadata }) }), commentsPanelOpen && branchNameState && (_jsx(CommentsPanel, { branchName: branchNameState, comments: comments, canResolve: true, onAddComment: handleAddComment, onResolveThread: handleResolveThread, onClose: () => setCommentsPanelOpen(false), onJumpToField: handleJumpToField, onGetUserMetadata: handleGetUserMetadata, onJumpToEntry: handleJumpToEntry, onJumpToBranch: handleJumpToBranch })), _jsx(GroupManager, { opened: groupManagerOpen, onClose: () => setGroupManagerOpen(false), internalGroups: groupsData, loading: groupsLoading, canEdit:
|
|
898
|
+
}, onClose: () => setBranchManagerOpen(false), comments: comments, currentUserId: currentUser, canResolve: canResolveComments, onAddComment: handleAddComment, onResolveThread: handleResolveThread, highlightThreadId: highlightThreadId, onGetUserMetadata: canResolveUserNames ? handleGetUserMetadata : undefined }) }), commentsPanelOpen && branchNameState && (_jsx(CommentsPanel, { branchName: branchNameState, comments: comments, canResolve: true, onAddComment: handleAddComment, onResolveThread: handleResolveThread, onClose: () => setCommentsPanelOpen(false), onJumpToField: handleJumpToField, onGetUserMetadata: canResolveUserNames ? handleGetUserMetadata : undefined, onJumpToEntry: handleJumpToEntry, onJumpToBranch: handleJumpToBranch })), userIsAdmin && (_jsx(GroupManager, { opened: groupManagerOpen, onClose: () => setGroupManagerOpen(false), internalGroups: groupsData, loading: groupsLoading, canEdit: userIsAdmin, onSave: handleSaveGroups, onSearchUsers: handleSearchUsers, onGetUserMetadata: handleGetUserMetadata, onSearchExternalGroups: handleSearchExternalGroups })), userIsAdmin && (_jsx(PermissionManager, { opened: permissionManagerOpen, onClose: () => setPermissionManagerOpen(false), 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) })), _jsx(MediaLibrary, { opened: mediaLibraryOpen, onClose: () => setMediaLibraryOpen(false), mode: "manage" }), userIsAdmin && (_jsx(CollectionEditor, { isOpen: collectionEditorOpen, editingCollection: editingCollection, parentPath: collectionEditorParentPath, availableSchemas: availableSchemas, onSave: handleCollectionSave, onAddEntryType: editingCollection ? (path, entryType) => addEntryType(path, entryType) : undefined, onUpdateEntryType: editingCollection
|
|
777
899
|
? (path, name, updates) => updateEntryType(path, name, updates)
|
|
778
|
-
: undefined, onRemoveEntryType: editingCollection ? (path, name) => removeEntryType(path, name) : undefined, onClose: handleCloseCollectionEditor, isSaving: schemaLoading, error: collectionEditorError }), renamingEntry && (_jsx(RenameEntryModal, { isOpen: renameModalOpen, entryLabel: renamingEntry.label, currentSlug: renamingEntry.slug || '', onSave: handleRenameSubmit, onClose: () => {
|
|
900
|
+
: undefined, onRemoveEntryType: editingCollection ? (path, name) => removeEntryType(path, name) : undefined, onClose: handleCloseCollectionEditor, isSaving: schemaLoading, error: collectionEditorError })), renamingEntry && (_jsx(RenameEntryModal, { isOpen: renameModalOpen, entryLabel: renamingEntry.label, currentSlug: renamingEntry.slug || '', onSave: handleRenameSubmit, onClose: () => {
|
|
779
901
|
setRenameModalOpen(false);
|
|
780
902
|
setRenamingEntry(null);
|
|
781
903
|
setRenameModalError(null);
|
|
@@ -159,9 +159,9 @@ function RevalidateOnReauth({ reauthCount }) {
|
|
|
159
159
|
return null;
|
|
160
160
|
}
|
|
161
161
|
function RetryNotice(props) {
|
|
162
|
-
return (_jsx(Alert, { color: "red", title: props.title, maw: 480, "data-testid": "canopy-auth-error", children: _jsxs(Stack, { gap: "xs", align: "flex-start", children: [_jsx(Text, { size: "sm", children: props.error }), _jsx(Button, {
|
|
162
|
+
return (_jsx(Alert, { color: "red", title: props.title, maw: 480, "data-testid": "canopy-auth-error", children: _jsxs(Stack, { gap: "xs", align: "flex-start", children: [_jsx(Text, { size: "sm", children: props.error }), _jsx(Button, { variant: "light", onClick: props.onRetry, children: "Retry" })] }) }));
|
|
163
163
|
}
|
|
164
164
|
/** Re-checks rather than reloading: in the overlay, a reload would discard the unsaved edits. */
|
|
165
165
|
function DefaultSignedOutNotice({ onSignedIn, sessionRejected }) {
|
|
166
|
-
return (_jsx(Paper, { withBorder: true, p: "xl",
|
|
166
|
+
return (_jsx(Paper, { withBorder: true, p: "xl", maw: 420, children: _jsxs(Stack, { gap: "sm", children: [_jsx(Title, { order: 3, children: "Sign in required" }), _jsx(Text, { size: "sm", children: "You are not signed in to the CMS. Sign in through this site's auth provider, then continue." }), sessionRejected && (_jsx(Text, { size: "sm", c: "red", children: "Still not signed in." })), _jsx(Button, { size: "sm", onClick: onSignedIn, children: "Continue" })] }) }));
|
|
167
167
|
}
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
export type PaneLayout = 'side' | 'stacked';
|
|
3
|
+
export declare const SPLIT_PERCENT_MIN = 15;
|
|
4
|
+
export declare const SPLIT_PERCENT_MAX = 85;
|
|
5
|
+
export declare const DEFAULT_SIDE_SPLIT_PERCENT = 52;
|
|
6
|
+
export declare const DEFAULT_STACKED_SPLIT_PERCENT = 58;
|
|
3
7
|
export interface EditorPanesProps {
|
|
4
8
|
layout?: PaneLayout;
|
|
5
9
|
onLayoutChange?: (layout: PaneLayout) => void;
|
|
10
|
+
/** Primary-pane size (percent) in the side layout; follows this prop when it changes. */
|
|
11
|
+
sideSplitPercent?: number;
|
|
12
|
+
/** Primary-pane size (percent) in the stacked layout; follows this prop when it changes. */
|
|
13
|
+
stackedSplitPercent?: number;
|
|
14
|
+
/** Called once per drag, when it ends, with the clamped percent; live dragging never calls it. */
|
|
15
|
+
onSplitPercentChange?: (layout: PaneLayout, percent: number) => void;
|
|
6
16
|
preview?: React.ReactNode;
|
|
7
17
|
form?: React.ReactNode;
|
|
8
18
|
}
|
|
@@ -3,17 +3,32 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useMemo, useRef, useState, useEffect } from 'react';
|
|
4
4
|
import { Box, Paper, rem } from '@mantine/core';
|
|
5
5
|
import SplitPane from 'react-split-pane';
|
|
6
|
-
export const
|
|
6
|
+
export const SPLIT_PERCENT_MIN = 15;
|
|
7
|
+
export const SPLIT_PERCENT_MAX = 85;
|
|
8
|
+
export const DEFAULT_SIDE_SPLIT_PERCENT = 52;
|
|
9
|
+
export const DEFAULT_STACKED_SPLIT_PERCENT = 58;
|
|
10
|
+
export const EditorPanes = ({ layout: layoutProp = 'side', onLayoutChange: _, sideSplitPercent = DEFAULT_SIDE_SPLIT_PERCENT, stackedSplitPercent = DEFAULT_STACKED_SPLIT_PERCENT, onSplitPercentChange, preview, form, }) => {
|
|
7
11
|
const TypedSplitPane = SplitPane;
|
|
8
12
|
const splitContainerRef = useRef(null);
|
|
9
13
|
const [layout, setLayout] = useState(layoutProp);
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
// Live drag state: the props change only when a drag ends or stored prefs load, so dragging
|
|
15
|
+
// never writes storage.
|
|
16
|
+
const [sidePrimarySize, setSidePrimarySize] = useState(sideSplitPercent);
|
|
17
|
+
const [stackedPrimarySize, setStackedPrimarySize] = useState(stackedSplitPercent);
|
|
12
18
|
// Turn off iframe/pane pointer events while dragging so the gutter keeps receiving mouse events.
|
|
13
19
|
const [isDragging, setIsDragging] = useState(false);
|
|
20
|
+
// The last percent a drag produced. `onDragFinished` cannot supply it: with `size` controlled,
|
|
21
|
+
// react-split-pane hands back that prop's string ("37%"), a click with no movement included.
|
|
22
|
+
const dragPercentRef = useRef(undefined);
|
|
14
23
|
useEffect(() => {
|
|
15
24
|
setLayout(layoutProp);
|
|
16
25
|
}, [layoutProp]);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
setSidePrimarySize(sideSplitPercent);
|
|
28
|
+
}, [sideSplitPercent]);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
setStackedPrimarySize(stackedSplitPercent);
|
|
31
|
+
}, [stackedSplitPercent]);
|
|
17
32
|
const direction = layout === 'side' ? 'vertical' : 'horizontal';
|
|
18
33
|
const primarySize = useMemo(() => (layout === 'side' ? sidePrimarySize : stackedPrimarySize), [layout, sidePrimarySize, stackedPrimarySize]);
|
|
19
34
|
const resizerStyle = useMemo(() => ({
|
|
@@ -36,15 +51,16 @@ export const EditorPanes = ({ layout: layoutProp = 'side', onLayoutChange: _, pr
|
|
|
36
51
|
? (splitContainerRef.current?.clientWidth ?? 0)
|
|
37
52
|
: (splitContainerRef.current?.clientHeight ?? 0);
|
|
38
53
|
// Guard: ensure valid dimensions before calculating
|
|
39
|
-
if (!total || total <= 0 || nextPixels <= 0)
|
|
40
|
-
return;
|
|
41
|
-
const percent = Math.min(
|
|
54
|
+
if (!total || total <= 0 || !Number.isFinite(nextPixels) || nextPixels <= 0)
|
|
55
|
+
return undefined;
|
|
56
|
+
const percent = Math.min(SPLIT_PERCENT_MAX, Math.max(SPLIT_PERCENT_MIN, (nextPixels / total) * 100));
|
|
42
57
|
if (nextLayout === 'side') {
|
|
43
58
|
setSidePrimarySize(percent);
|
|
44
59
|
}
|
|
45
60
|
else {
|
|
46
61
|
setStackedPrimarySize(percent);
|
|
47
62
|
}
|
|
63
|
+
return percent;
|
|
48
64
|
};
|
|
49
65
|
return (_jsx(Box, { h: "100%", style: { minHeight: '70vh' }, children: _jsx(Paper, { radius: 0, shadow: "xs", withBorder: true, style: {
|
|
50
66
|
display: 'flex',
|
|
@@ -53,8 +69,16 @@ export const EditorPanes = ({ layout: layoutProp = 'side', onLayoutChange: _, pr
|
|
|
53
69
|
overflow: 'hidden',
|
|
54
70
|
flexDirection: layout === 'side' ? 'row' : 'column',
|
|
55
71
|
width: '100%',
|
|
56
|
-
}, children: _jsx(Box, { ref: splitContainerRef, style: { display: 'flex', flex: 1, minHeight: 0, width: '100%' }, children: _jsxs(TypedSplitPane, { split: direction, minSize: 120, size: `${primarySize}%`, allowResize: true, onChange: (next) =>
|
|
57
|
-
updateSizeFromPixels(next, layout);
|
|
72
|
+
}, children: _jsx(Box, { ref: splitContainerRef, style: { display: 'flex', flex: 1, minHeight: 0, width: '100%' }, children: _jsxs(TypedSplitPane, { split: direction, minSize: 120, size: `${primarySize}%`, allowResize: true, onChange: (next) => {
|
|
73
|
+
dragPercentRef.current = updateSizeFromPixels(next, layout) ?? dragPercentRef.current;
|
|
74
|
+
}, onDragStarted: () => {
|
|
75
|
+
dragPercentRef.current = undefined;
|
|
76
|
+
setIsDragging(true);
|
|
77
|
+
}, onDragFinished: () => {
|
|
78
|
+
const percent = dragPercentRef.current;
|
|
79
|
+
dragPercentRef.current = undefined;
|
|
80
|
+
if (percent !== undefined)
|
|
81
|
+
onSplitPercentChange?.(layout, percent);
|
|
58
82
|
setIsDragging(false);
|
|
59
83
|
}, style: {
|
|
60
84
|
position: 'relative',
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { ContentFormat, EntrySchema, FieldConfig } from '../config/index.js';
|
|
3
3
|
import type { CommentThread } from '../comment-store.js';
|
|
4
|
+
import type { UserSearchResult } from '../auth/types.js';
|
|
5
|
+
import type { FieldRemoval } from './undo-removal.js';
|
|
4
6
|
export type FormValue = Record<string, unknown>;
|
|
5
7
|
export interface CustomFieldRenderProps {
|
|
6
8
|
field: FieldConfig;
|
|
@@ -8,6 +10,8 @@ export interface CustomFieldRenderProps {
|
|
|
8
10
|
onChange: (v: unknown) => void;
|
|
9
11
|
path: Array<string | number>;
|
|
10
12
|
id: string;
|
|
13
|
+
/** The branch is locked (protected, or in review): show the value; `onChange` is ignored. */
|
|
14
|
+
readOnly: boolean;
|
|
11
15
|
}
|
|
12
16
|
export type CustomFieldRenderers = Record<string, (props: CustomFieldRenderProps) => React.ReactNode>;
|
|
13
17
|
export interface FormRendererProps {
|
|
@@ -35,5 +39,11 @@ export interface FormRendererProps {
|
|
|
35
39
|
fieldErrors?: Record<string, string>;
|
|
36
40
|
/** The entry's format, which decides how its body field is checked. */
|
|
37
41
|
format?: ContentFormat;
|
|
42
|
+
/** Shows the value without accepting edits. Comments stay open. */
|
|
43
|
+
readOnly?: boolean;
|
|
44
|
+
/** Told of each list item, block or image that Remove takes out, so the caller can offer Undo. */
|
|
45
|
+
onRemoved?: (removal: FieldRemoval) => void;
|
|
46
|
+
/** Resolves comment authors to names in field and entry threads; without it they show as ids. */
|
|
47
|
+
onGetUserMetadata?: (userId: string) => Promise<UserSearchResult | null>;
|
|
38
48
|
}
|
|
39
49
|
export declare const FormRenderer: React.FC<FormRendererProps>;
|