canopycms 0.11.1 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +174 -48
- package/dist/api/admin.d.ts +4 -4
- package/dist/api/branch-changes.d.ts +1 -0
- package/dist/api/branch-changes.js +1 -1
- package/dist/api/branch-entry-changes.d.ts +92 -7
- package/dist/api/branch-entry-changes.js +272 -38
- package/dist/api/branch-review.js +45 -13
- package/dist/api/branch-status.d.ts +12 -0
- package/dist/api/branch-status.js +13 -0
- package/dist/api/branch-update-from-base.d.ts +23 -0
- package/dist/api/branch-update-from-base.js +176 -0
- package/dist/api/branch.d.ts +5 -0
- package/dist/api/branch.js +1 -0
- package/dist/api/client.d.ts +17 -2
- package/dist/api/client.js +26 -0
- package/dist/api/comments.d.ts +2 -2
- package/dist/api/content.d.ts +38 -1
- package/dist/api/content.js +41 -5
- package/dist/api/merge-conflicts.d.ts +226 -0
- package/dist/api/merge-conflicts.js +674 -0
- package/dist/api/routes.js +2 -0
- package/dist/api/types.d.ts +7 -3
- package/dist/base-update.d.ts +26 -0
- package/dist/base-update.js +31 -0
- package/dist/branch-metadata-file.d.ts +51 -0
- package/dist/branch-metadata-file.js +11 -0
- package/dist/branch-schema-cache.d.ts +12 -0
- package/dist/branch-schema-cache.js +27 -2
- package/dist/cli/backfill-item-keys.d.ts +77 -0
- package/dist/cli/backfill-item-keys.js +320 -0
- package/dist/cli/cli.d.ts +1 -1
- package/dist/cli/cli.js +9713 -1493
- package/dist/cli/generate-ai-content.js +415 -244
- package/dist/cli/init.js +6 -0
- package/dist/cli/site-config.d.ts +14 -2
- package/dist/cli/site-config.js +20 -4
- package/dist/comment-store.d.ts +8 -0
- package/dist/comment-store.js +28 -0
- package/dist/content-index-generation.d.ts +1 -4
- package/dist/content-index-generation.js +1 -4
- package/dist/content-store.d.ts +65 -3
- package/dist/content-store.js +121 -15
- package/dist/editor/BranchManager.d.ts +4 -1
- package/dist/editor/BranchManager.js +9 -5
- package/dist/editor/Editor.js +549 -53
- package/dist/editor/EntryNavigator.d.ts +10 -2
- package/dist/editor/EntryNavigator.js +12 -4
- package/dist/editor/FormRenderer.d.ts +8 -2
- package/dist/editor/FormRenderer.js +5 -6
- package/dist/editor/base-update-wait.d.ts +74 -0
- package/dist/editor/base-update-wait.js +91 -0
- package/dist/editor/branch-status.js +1 -1
- package/dist/editor/components/BaseUpdateNotice.d.ts +12 -0
- package/dist/editor/components/BaseUpdateNotice.js +24 -0
- package/dist/editor/components/BranchPill.d.ts +4 -1
- package/dist/editor/components/BranchPill.js +5 -2
- package/dist/editor/components/ConflictsGroup.d.ts +100 -0
- package/dist/editor/components/ConflictsGroup.js +364 -0
- package/dist/editor/components/EditorHeader.d.ts +16 -1
- package/dist/editor/components/EditorHeader.js +92 -30
- package/dist/editor/components/EntryChangesDrawer.d.ts +52 -2
- package/dist/editor/components/EntryChangesDrawer.js +164 -162
- package/dist/editor/components/EntryHeader.d.ts +3 -1
- package/dist/editor/components/EntryHeader.js +4 -6
- package/dist/editor/components/GetLatestChangesButton.d.ts +15 -0
- package/dist/editor/components/GetLatestChangesButton.js +29 -0
- package/dist/editor/components/ReleaseHoldButton.d.ts +20 -0
- package/dist/editor/components/ReleaseHoldButton.js +36 -0
- package/dist/editor/components/RetryToastMessage.js +1 -1
- package/dist/editor/components/change-parts.d.ts +51 -0
- package/dist/editor/components/change-parts.js +171 -0
- package/dist/editor/conflict-cards.d.ts +90 -0
- package/dist/editor/conflict-cards.js +279 -0
- package/dist/editor/conflict-choices.d.ts +60 -0
- package/dist/editor/conflict-choices.js +108 -0
- package/dist/editor/conflict-notice.d.ts +25 -0
- package/dist/editor/conflict-notice.js +39 -0
- package/dist/editor/copy.d.ts +20 -0
- package/dist/editor/copy.js +20 -0
- package/dist/editor/entry-diff.d.ts +18 -2
- package/dist/editor/entry-diff.js +26 -32
- package/dist/editor/hooks/useBaseUpdate.d.ts +36 -0
- package/dist/editor/hooks/useBaseUpdate.js +269 -0
- package/dist/editor/hooks/useBranchManager.d.ts +6 -1
- package/dist/editor/hooks/useBranchManager.js +28 -7
- package/dist/editor/hooks/useBranchesData.js +3 -0
- package/dist/editor/hooks/useConflictChoices.d.ts +24 -0
- package/dist/editor/hooks/useConflictChoices.js +75 -0
- package/dist/editor/hooks/useConflictsData.d.ts +26 -0
- package/dist/editor/hooks/useConflictsData.js +89 -0
- package/dist/editor/hooks/useDraftManager.d.ts +5 -0
- package/dist/editor/hooks/useDraftManager.js +24 -8
- package/dist/editor/hooks/useEntryManager.d.ts +9 -0
- package/dist/editor/hooks/useEntryManager.js +5 -0
- package/dist/editor/release-hold.d.ts +12 -0
- package/dist/editor/release-hold.js +39 -0
- package/dist/editor/server-clock.d.ts +4 -0
- package/dist/editor/server-clock.js +15 -0
- package/dist/editor/undo-change.d.ts +52 -0
- package/dist/editor/undo-change.js +241 -0
- package/dist/git-manager.d.ts +5 -0
- package/dist/git-manager.js +30 -0
- package/dist/item-keys.d.ts +13 -0
- package/dist/item-keys.js +36 -14
- package/dist/merge/conflict-path.d.ts +40 -0
- package/dist/merge/conflict-path.js +128 -0
- package/dist/merge/diff3.d.ts +27 -0
- package/dist/merge/diff3.js +113 -0
- package/dist/merge/entry-merge.d.ts +94 -0
- package/dist/merge/entry-merge.js +875 -0
- package/dist/merge/list-alignment.d.ts +52 -0
- package/dist/merge/list-alignment.js +441 -0
- package/dist/merge/text-merge.d.ts +29 -0
- package/dist/merge/text-merge.js +59 -0
- package/dist/merge/value-equality.d.ts +18 -0
- package/dist/merge/value-equality.js +50 -0
- package/dist/merge-conflicts.d.ts +232 -0
- package/dist/merge-conflicts.js +451 -0
- package/dist/operating-mode/client-safe-strategy.d.ts +2 -0
- package/dist/operating-mode/client-safe-strategy.js +6 -0
- package/dist/operating-mode/types.d.ts +5 -0
- package/dist/services.d.ts +16 -1
- package/dist/services.js +82 -22
- package/dist/task-queue/cms-task-queue.d.ts +3 -1
- package/dist/types.d.ts +28 -2
- package/dist/utils/content-serialize.d.ts +18 -1
- package/dist/utils/content-serialize.js +12 -0
- package/dist/utils/content-write-lock.d.ts +3 -3
- package/dist/utils/content-write-lock.js +3 -3
- package/dist/utils/markdown-body-splice.d.ts +1 -1
- package/dist/utils/markdown-body-splice.js +1 -19
- package/dist/utils/markdown-parse.d.ts +9 -0
- package/dist/utils/markdown-parse.js +27 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-merge.d.ts +53 -37
- package/dist/worker/branch-merge.js +139 -54
- package/dist/worker/cms-worker.d.ts +16 -3
- package/dist/worker/cms-worker.js +59 -12
- package/dist/worker/field-merge.d.ts +65 -0
- package/dist/worker/field-merge.js +636 -0
- package/dist/worker/git-sync.d.ts +1 -1
- package/dist/worker/git-sync.js +7 -6
- package/dist/worker/github-commits.d.ts +42 -10
- package/dist/worker/github-commits.js +287 -47
- package/dist/worker/github-gateway-http.js +17 -9
- package/dist/worker/github-gateway-wire.d.ts +21 -12
- package/dist/worker/github-gateway-wire.js +5 -1
- package/dist/worker/github-gateway.d.ts +10 -1
- package/dist/worker/github-gateway.js +18 -6
- package/dist/worker/github-transfer.d.ts +5 -3
- package/dist/worker/github-transfer.js +8 -5
- package/dist/worker/rebase.d.ts +5 -4
- package/dist/worker/rebase.js +293 -156
- package/dist/worker/task-runner.d.ts +1 -1
- package/dist/worker/task-runner.js +14 -2
- package/dist/worker/worker-context.d.ts +18 -2
- package/package.json +1 -1
package/dist/editor/Editor.js
CHANGED
|
@@ -16,7 +16,10 @@ import { EditorShell } from './shell/EditorShell.js';
|
|
|
16
16
|
import { FORM_PANE_ID, PaneLayout } from './shell/PaneLayout.js';
|
|
17
17
|
import { SidePanelHost } from './shell/SidePanelHost.js';
|
|
18
18
|
import { CanopyCMSProvider } from './theme.js';
|
|
19
|
-
import { BranchManager } from './BranchManager.js';
|
|
19
|
+
import { BranchManager, getBranchPermissions } from './BranchManager.js';
|
|
20
|
+
import { useBaseUpdate } from './hooks/useBaseUpdate.js';
|
|
21
|
+
import { BaseUpdateNotice } from './components/BaseUpdateNotice.js';
|
|
22
|
+
import { WAITING_NOTICE, WAITING_SUBMIT_REASON, getLatestChangesTooltip, } from './base-update-wait.js';
|
|
20
23
|
import { BranchesDrawer } from './components/BranchesDrawer.js';
|
|
21
24
|
import { CommentsPanel } from './CommentsPanel.js';
|
|
22
25
|
import { GroupManager } from './GroupManager.js';
|
|
@@ -31,6 +34,13 @@ import { useEditorLayout, useDraftManager, useEntryManager, useGroupManager, use
|
|
|
31
34
|
import { useBranchActions } from './hooks/useBranchActions.js';
|
|
32
35
|
import { branchAddress, displayBranchName } from './branch-names.js';
|
|
33
36
|
import { useBranchChangesData, useEntryChangeData } from './hooks/useBranchChangesData.js';
|
|
37
|
+
import { useBranchConflictsData, useEntryConflictData } from './hooks/useConflictsData.js';
|
|
38
|
+
import { useConflictChoices } from './hooks/useConflictChoices.js';
|
|
39
|
+
import { openConfirm } from './utils/confirm-modal.js';
|
|
40
|
+
import { canopyPathOf } from './undo-change.js';
|
|
41
|
+
import { placeOf } from './conflict-choices.js';
|
|
42
|
+
import { findBodyFieldName } from '../utils/body-field.js';
|
|
43
|
+
import { entryConflictNotice } from './conflict-notice.js';
|
|
34
44
|
import { LazyEntryChangesDrawer } from './components/LazyEntryChangesDrawer.js';
|
|
35
45
|
import { CONTENT_PANEL_WIDTH_MAX, CONTENT_PANEL_WIDTH_MIN, editorLayoutStorageKey, } from './hooks/useEditorLayout.js';
|
|
36
46
|
import { EntrySchemaUnavailableError } from './hooks/useEntryManager.js';
|
|
@@ -38,6 +48,7 @@ import { useEntryLinkResolution } from './hooks/useEntryLinkResolution.js';
|
|
|
38
48
|
import { useReferenceResolution } from './hooks/useReferenceResolution.js';
|
|
39
49
|
import { usePreviewMarks } from './hooks/usePreviewMarks.js';
|
|
40
50
|
import { branchBarStatus, describeBranchLock } from './branch-lock.js';
|
|
51
|
+
import { EDITOR_ACTIONS, NO_EDIT_ACCESS_REASON } from './copy.js';
|
|
41
52
|
import { EditorHeader, EditorRail, EntryHeader } from './components/index.js';
|
|
42
53
|
import { showSaveAllResult } from './components/SaveAllResult.js';
|
|
43
54
|
import { RenameEntryModal } from './components/RenameEntryModal.js';
|
|
@@ -67,7 +78,21 @@ import { findFieldTarget, focusFieldTarget } from './field-target.js';
|
|
|
67
78
|
import { PreviewFollowContext, usePreviewFollow } from './hooks/usePreviewFollow.js';
|
|
68
79
|
import { UndoToastMessage } from './components/UndoToastMessage.js';
|
|
69
80
|
import { getNotificationDuration } from './utils/env.js';
|
|
70
|
-
import { MayHaveLandedError } from './unanswered.js';
|
|
81
|
+
import { MayHaveLandedError, isGatewayFailure, isUnanswered } from './unanswered.js';
|
|
82
|
+
import { releaseConflictsHold } from './release-hold.js';
|
|
83
|
+
/** The collection at `path` in a collection tree. */
|
|
84
|
+
const findCollection = (cols, path) => {
|
|
85
|
+
if (!cols)
|
|
86
|
+
return undefined;
|
|
87
|
+
for (const col of cols) {
|
|
88
|
+
if (col.path === path)
|
|
89
|
+
return col;
|
|
90
|
+
const found = findCollection(col.children, path);
|
|
91
|
+
if (found)
|
|
92
|
+
return found;
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
};
|
|
71
96
|
// Stable fallbacks, so reference resolution's memo and effect don't re-run on every render
|
|
72
97
|
// while no entry is open.
|
|
73
98
|
const EMPTY_SCHEMA = [];
|
|
@@ -168,6 +193,10 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
168
193
|
// Reloads the open branch's comments after a request for changes, whose note is a comment; the
|
|
169
194
|
// comment system is built further down.
|
|
170
195
|
const reloadCommentsRef = useRef(() => { });
|
|
196
|
+
// Set once useBaseUpdate below exists; it depends on the drafts.
|
|
197
|
+
const requestLatestRef = useRef(async () => { });
|
|
198
|
+
const modifiedCountRef = useRef(0);
|
|
199
|
+
const namesBranchRef = useRef(() => false);
|
|
171
200
|
const { branchName: branchNameState, setBranchName, branches, branchSummaries, currentBranch, currentAddress, listedBranch, namesBranch, urlBranchName, branchesLoadFailed, branchesLoaded, addCreatedBranch, handleSubmit, handleWithdraw, handleApprove, handleRequestChanges, handleDelete, handleArchive, handleReloadBranchData, loadBranches, } = useBranchManager({
|
|
172
201
|
initialBranch: branchName,
|
|
173
202
|
operatingMode,
|
|
@@ -177,6 +206,10 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
177
206
|
getUnsaved: () => resolveUnsavedRef.current(),
|
|
178
207
|
onStamped: (stamped) => applySubmitStampsRef.current(stamped),
|
|
179
208
|
onChangesRequested: (branch) => reloadCommentsRef.current(branch),
|
|
209
|
+
// Only the open branch, with nothing unsaved: its wait and answer show where the user is.
|
|
210
|
+
latestChangesAfterWithdraw: (branch) => namesBranchRef.current(branch) && modifiedCountRef.current === 0
|
|
211
|
+
? () => requestLatestRef.current()
|
|
212
|
+
: undefined,
|
|
180
213
|
});
|
|
181
214
|
// Content mutations (add/delete/rename/reorder entries) are locked whenever the
|
|
182
215
|
// server says writes are blocked -- the protected base branch, or a workflow
|
|
@@ -196,6 +229,21 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
196
229
|
// No lock reason is true yet while the list is unanswered; once it has failed, or answered
|
|
197
230
|
// without this branch, the lock says which.
|
|
198
231
|
const branchPending = !currentBranch && !branchesLoaded && !branchesLoadFailed;
|
|
232
|
+
// Conflicts a merge left on the open branch (api/merge-conflicts.ts): read while its listing
|
|
233
|
+
// names any, and the choices made on their cards that no save has sent yet.
|
|
234
|
+
const branchConflicts = useBranchConflictsData(apiClient, branchNameState, currentBranch?.conflictFiles,
|
|
235
|
+
// Every viewer reads it, as a hold on the branch names no entry.
|
|
236
|
+
true);
|
|
237
|
+
const openConflictCount = currentBranch?.conflictFiles?.length
|
|
238
|
+
? (branchConflicts.data?.openCount ?? currentBranch.conflictFiles.length)
|
|
239
|
+
: 0;
|
|
240
|
+
const conflictChoices = useConflictChoices(branchNameState);
|
|
241
|
+
// A save that sent choices re-reads what they resolved; set once the readers exist, below.
|
|
242
|
+
const conflictsSavedRef = useRef(() => { });
|
|
243
|
+
// An admin's "Keep your version" on a collection's conflicts; set once the resolver exists, below.
|
|
244
|
+
const keepConflictsRef = useRef(() => { });
|
|
245
|
+
// The "Get latest changes" toast's "Show changes"; set once the drawer exists, below.
|
|
246
|
+
const showConflictsRef = useRef(() => { });
|
|
199
247
|
// The same inputs EditorHeader gives its own check, for the lock copy that names the way forward.
|
|
200
248
|
const branchReviewRole = reviewRoleOf({
|
|
201
249
|
status: currentBranch?.status,
|
|
@@ -244,6 +292,13 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
244
292
|
sameBranch: namesBranch,
|
|
245
293
|
urlBranchName,
|
|
246
294
|
onContentRefresh: (branch) => reloadChangesRef.current(branch),
|
|
295
|
+
conflictResolutions: {
|
|
296
|
+
pending: conflictChoices.pending,
|
|
297
|
+
onSaved: (branch, contentId, sent) => {
|
|
298
|
+
conflictChoices.sent(branch, contentId, sent);
|
|
299
|
+
conflictsSavedRef.current();
|
|
300
|
+
},
|
|
301
|
+
},
|
|
247
302
|
// To the entry menu, which names the entry opened; a dialog keeps focus.
|
|
248
303
|
onHistoryRestore: () => requestAnimationFrame(() => {
|
|
249
304
|
if (!document.activeElement?.closest('[role="dialog"]')) {
|
|
@@ -321,7 +376,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
321
376
|
setPreviewError(null);
|
|
322
377
|
}, [currentEntry?.contentId]);
|
|
323
378
|
// 3. Draft manager (depends on branchNameState, selectedPath from useEntryManager)
|
|
324
|
-
const { drafts, setDrafts, loadedValues, setLoadedValues, loadedValue, effectiveValue, modifiedCount, editedFiles, handleSave, singleSaving, lastSaved, handleSaveAll, saveAllProgress, isSaveAllRunning, handleDiscardDrafts, handleDiscardFileDraft, handleReload, isSelectedDirty, resolveUnsaved, fieldErrors, draftStorageFailed, applySubmitStamps, } = useDraftManager({
|
|
379
|
+
const { drafts, setDrafts, loadedValues, setLoadedValues, loadedValue, effectiveValue, modifiedCount, editedFiles, handleSave, singleSaving, lastSaved, handleSaveAll, saveAllProgress, isSaveAllRunning, handleDiscardDrafts, handleDiscardFileDraft, handleReload, isSelectedDirty, isDirtyForEntry, resolveUnsaved, fieldErrors, draftStorageFailed, applySubmitStamps, } = useDraftManager({
|
|
325
380
|
branchName: branchNameState,
|
|
326
381
|
selectedPath,
|
|
327
382
|
currentEntry,
|
|
@@ -341,10 +396,47 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
341
396
|
void refreshEntries(branchNameState).catch((err) => console.error('Failed to refresh entries after save', err));
|
|
342
397
|
},
|
|
343
398
|
// An Undo built on a discarded draft would put back an item the server's copy already has.
|
|
344
|
-
|
|
399
|
+
// A conflict choice is part of the draft, so it goes with it.
|
|
400
|
+
onDraftsReset: (contentId) => {
|
|
401
|
+
dismissUndoToasts(contentId);
|
|
402
|
+
conflictChoices.clear(contentId);
|
|
403
|
+
},
|
|
404
|
+
pendingIds: conflictChoices.pendingIds,
|
|
345
405
|
});
|
|
346
406
|
resolveUnsavedRef.current = resolveUnsaved;
|
|
347
407
|
applySubmitStampsRef.current = applySubmitStamps;
|
|
408
|
+
// "Get latest changes" on the open branch: the worker rewrites its files meanwhile, so the form
|
|
409
|
+
// is read-only while it waits (`latestWaiting`), and every clean entry is read again after.
|
|
410
|
+
const currentSummary = branchSummaries.find((b) => b.name === currentBranch?.name);
|
|
411
|
+
const baseUpdate = useBaseUpdate({
|
|
412
|
+
branch: currentBranch,
|
|
413
|
+
operatingMode,
|
|
414
|
+
permitted: !!currentSummary && getBranchPermissions(currentSummary, userContext).canSubmit,
|
|
415
|
+
unsavedCount: modifiedCount,
|
|
416
|
+
onUpdated: () => {
|
|
417
|
+
setLoadedValues((prev) => Object.fromEntries(Object.entries(prev).filter(([contentId]) => draftsRef.current[contentId] !== undefined)));
|
|
418
|
+
void Promise.all([handleReloadBranchData(), refreshEntries(branchNameState)]).catch(console.error);
|
|
419
|
+
},
|
|
420
|
+
onShowConflicts: () => showConflictsRef.current(),
|
|
421
|
+
});
|
|
422
|
+
requestLatestRef.current = baseUpdate.request;
|
|
423
|
+
modifiedCountRef.current = modifiedCount;
|
|
424
|
+
namesBranchRef.current = (branch) => namesBranch(branch, branchNameState);
|
|
425
|
+
// The request in flight counts as waiting: a save landing before the worker reads the branch
|
|
426
|
+
// would make it fail.
|
|
427
|
+
const latestWait = baseUpdate.requesting && baseUpdate.wait === 'none' ? 'waiting' : baseUpdate.wait;
|
|
428
|
+
const latestWaiting = latestWait === 'waiting';
|
|
429
|
+
const baseUpdateControl = baseUpdate.available
|
|
430
|
+
? {
|
|
431
|
+
wait: baseUpdate.wait,
|
|
432
|
+
requesting: baseUpdate.requesting,
|
|
433
|
+
blockedReason: baseUpdate.blockedReason,
|
|
434
|
+
tooltip: getLatestChangesTooltip(currentBranch?.baseBranch, userIsAdmin),
|
|
435
|
+
onRequest: () => void baseUpdate.request(),
|
|
436
|
+
}
|
|
437
|
+
: undefined;
|
|
438
|
+
// Content writes wait on the branch's lock, and on the latest changes coming in.
|
|
439
|
+
const contentLocked = branchContentLocked || latestWaiting;
|
|
348
440
|
// 4. Branch actions (depends on resolveUnsaved, setBranchName)
|
|
349
441
|
const { handleBranchChange, confirmCreate: confirmCreateBranch, handleCreateBranch, confirmOpen: branchConfirmOpen, } = useBranchActions({
|
|
350
442
|
branchName: branchNameState,
|
|
@@ -408,7 +500,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
408
500
|
});
|
|
409
501
|
// Read-only content shows the saved value, never a draft; a restored draft is kept, unseen,
|
|
410
502
|
// until it is discarded or the branch unlocks.
|
|
411
|
-
const contentReadOnly =
|
|
503
|
+
const contentReadOnly = contentLocked || currentEntry?.canEdit === false;
|
|
412
504
|
const displayValue = contentReadOnly ? loadedValue : effectiveValue;
|
|
413
505
|
// Read when an edit lands, which an upload or other async field work can do renders later.
|
|
414
506
|
const contentReadOnlyRef = useRef(contentReadOnly);
|
|
@@ -504,7 +596,9 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
504
596
|
};
|
|
505
597
|
useEffect(() => () => dismissUndoToasts(), [currentEntry?.contentId, branchNameState]);
|
|
506
598
|
// Only once the branch has answered, so the fail-closed lock while it loads raises no notice.
|
|
507
|
-
const
|
|
599
|
+
const draftHidden = contentReadOnly && currentBranch !== undefined && !!currentEntry && isSelectedDirty();
|
|
600
|
+
// While the latest changes come in, the waiting notice says the draft comes back; it is the one notice.
|
|
601
|
+
const hiddenDraftNotice = draftHidden && !latestWaiting;
|
|
508
602
|
const collectionLabels = useMemo(() => buildCollectionLabels(activeCollections), [activeCollections]);
|
|
509
603
|
const schema = currentEntry?.schema ?? EMPTY_SCHEMA;
|
|
510
604
|
// Field threads follow their items' keys, placed in their entry's value where it is at hand.
|
|
@@ -512,7 +606,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
512
606
|
if (entry.path === currentEntry?.path)
|
|
513
607
|
return displayValue;
|
|
514
608
|
// As `displayValue` would show it: read-only content shows its saved value, not a draft.
|
|
515
|
-
const readOnly =
|
|
609
|
+
const readOnly = contentLocked || entry.canEdit === false;
|
|
516
610
|
return (readOnly ? undefined : drafts[entry.contentId]) ?? loadedValues[entry.contentId];
|
|
517
611
|
};
|
|
518
612
|
const displayComments = useMemo(() => {
|
|
@@ -530,15 +624,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
530
624
|
return path === thread.canopyPath ? thread : { ...thread, canopyPath: path };
|
|
531
625
|
});
|
|
532
626
|
// `entryValue` reads the open entry, its shown value, and the drafts and loads.
|
|
533
|
-
}, [
|
|
534
|
-
comments,
|
|
535
|
-
entriesState,
|
|
536
|
-
currentEntry,
|
|
537
|
-
displayValue,
|
|
538
|
-
drafts,
|
|
539
|
-
loadedValues,
|
|
540
|
-
branchContentLocked,
|
|
541
|
-
]);
|
|
627
|
+
}, [comments, entriesState, currentEntry, displayValue, drafts, loadedValues, contentLocked]);
|
|
542
628
|
// A jump into another entry waits for its value to place the thread; the stored path may be stale.
|
|
543
629
|
const [pendingFieldJump, setPendingFieldJump] = useState(null);
|
|
544
630
|
// A field whose comments are already open is still followed in the preview.
|
|
@@ -1012,18 +1098,6 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1012
1098
|
}
|
|
1013
1099
|
};
|
|
1014
1100
|
const handleReorderEntry = async (collectionPath, contentId, direction) => {
|
|
1015
|
-
const findCollection = (cols, path) => {
|
|
1016
|
-
if (!cols)
|
|
1017
|
-
return undefined;
|
|
1018
|
-
for (const col of cols) {
|
|
1019
|
-
if (col.path === path)
|
|
1020
|
-
return col;
|
|
1021
|
-
const found = findCollection(col.children, path);
|
|
1022
|
-
if (found)
|
|
1023
|
-
return found;
|
|
1024
|
-
}
|
|
1025
|
-
return undefined;
|
|
1026
|
-
};
|
|
1027
1101
|
// Resolve against `collectionsFromApi`, NOT `activeCollections`: the latter
|
|
1028
1102
|
// falls back to the build-time `collections` prop whenever the fetch is
|
|
1029
1103
|
// still empty -- exactly the state mid-branch-switch. A write resolved
|
|
@@ -1107,6 +1181,11 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1107
1181
|
const entries = grouped.get(node.path) ?? [];
|
|
1108
1182
|
const children = node.children?.map((child) => build(child)) ?? [];
|
|
1109
1183
|
const unavailableRefs = unavailableSchemaRefs(node.entryTypes);
|
|
1184
|
+
// Conflicts on the collection's settings or other files, which no entry's "Show changes"
|
|
1185
|
+
// reaches; the API names them to admins only, by the collection's path.
|
|
1186
|
+
const keepable = userIsAdmin
|
|
1187
|
+
? (branchConflicts.data?.conflicts ?? []).filter((c) => c.logicalPath === node.path && !entriesState.some((e) => e.contentId === c.contentId))
|
|
1188
|
+
: [];
|
|
1110
1189
|
// No entry can be created where every entry type is unavailable.
|
|
1111
1190
|
const allUnavailable = !!node.entryTypes?.length && node.entryTypes.every((et) => et.unavailable);
|
|
1112
1191
|
// A collection that holds only collections has no entry type to add (`entryTypes` is
|
|
@@ -1121,7 +1200,16 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1121
1200
|
order: node.order,
|
|
1122
1201
|
entries: entries.length > 0 ? entries : undefined,
|
|
1123
1202
|
children: children.length > 0 ? children : undefined,
|
|
1124
|
-
conflictNotice: !!(node.contentId && currentBranch?.conflictFiles?.includes(node.contentId))
|
|
1203
|
+
conflictNotice: !!(node.contentId && currentBranch?.conflictFiles?.includes(node.contentId)) ||
|
|
1204
|
+
keepable.length > 0,
|
|
1205
|
+
...(keepable.length > 0 && {
|
|
1206
|
+
conflictKeep: {
|
|
1207
|
+
label: keepable.every((c) => c.path.endsWith('.collection.json'))
|
|
1208
|
+
? 'Keep your version of its settings'
|
|
1209
|
+
: 'Keep your version of its files',
|
|
1210
|
+
onKeep: () => keepConflictsRef.current(keepable),
|
|
1211
|
+
},
|
|
1212
|
+
}),
|
|
1125
1213
|
unavailableSchemaRefs: unavailableRefs.length > 0 ? unavailableRefs : undefined,
|
|
1126
1214
|
addRefused: (addable && node.canAddEntry === false) || undefined,
|
|
1127
1215
|
onAdd: canAddEntry
|
|
@@ -1150,6 +1238,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1150
1238
|
currentBranch,
|
|
1151
1239
|
schemaUnavailableKeys,
|
|
1152
1240
|
userIsAdmin,
|
|
1241
|
+
branchConflicts.data,
|
|
1153
1242
|
]);
|
|
1154
1243
|
const findShortcut = modKeyLabel() === '⌘' ? { label: '⌘P', keys: 'Meta+P' } : { label: 'Ctrl+P', keys: 'Control+P' };
|
|
1155
1244
|
// Mod+P opens a closed docked panel only until Enter, Escape from inside it or focus leaves it;
|
|
@@ -1284,7 +1373,11 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1284
1373
|
standIn: () => null,
|
|
1285
1374
|
});
|
|
1286
1375
|
const showChangesRef = useRef(null);
|
|
1376
|
+
// A confirm never opens over the drawer, nor while it closes: its focus return would take
|
|
1377
|
+
// focus out of the confirm. So "Delete entry" closes the drawer, and its exit opens the confirm.
|
|
1378
|
+
const deleteAfterChangesRef = useRef();
|
|
1287
1379
|
useEffect(() => {
|
|
1380
|
+
deleteAfterChangesRef.current = undefined;
|
|
1288
1381
|
setChangesOpen(false);
|
|
1289
1382
|
setChangesShown(false);
|
|
1290
1383
|
}, [branchNameState]);
|
|
@@ -1295,6 +1388,259 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1295
1388
|
id: changesSubject.contentId,
|
|
1296
1389
|
}
|
|
1297
1390
|
: undefined);
|
|
1391
|
+
// The drawer's entry's conflicts, read while it shows one the branch lists.
|
|
1392
|
+
const changesConflictTarget = (changesOpen || changesShown) &&
|
|
1393
|
+
changesSubject?.contentId &&
|
|
1394
|
+
currentBranch?.conflictFiles?.includes(changesSubject.contentId)
|
|
1395
|
+
? { logicalPath: changesSubject.logicalPath, id: changesSubject.contentId }
|
|
1396
|
+
: undefined;
|
|
1397
|
+
const entryConflict = useEntryConflictData(apiClient, branchNameState, changesConflictTarget);
|
|
1398
|
+
conflictsSavedRef.current = () => {
|
|
1399
|
+
void Promise.all([
|
|
1400
|
+
branchConflicts.reload(),
|
|
1401
|
+
entryConflict.reload(),
|
|
1402
|
+
handleReloadBranchData(),
|
|
1403
|
+
]).catch(console.error);
|
|
1404
|
+
};
|
|
1405
|
+
// "Edit" on a card closes the drawer, and its exit focuses the field in the form.
|
|
1406
|
+
const focusAfterChangesRef = useRef();
|
|
1407
|
+
// An entry-level choice that replaces stored work, "Delete entry" or "Use latest version",
|
|
1408
|
+
// confirms once the drawer has closed; any way out of the confirm but Confirm opens it again.
|
|
1409
|
+
const confirmAfterChangesRef = useRef();
|
|
1410
|
+
const focusConflictField = (field) => {
|
|
1411
|
+
revealForm();
|
|
1412
|
+
requestAnimationFrame(() => {
|
|
1413
|
+
const id = currentEntry?.contentId;
|
|
1414
|
+
const value = id && (draftsRef.current[id] ?? loadedValuesRef.current[id]);
|
|
1415
|
+
const schema = currentEntry?.schema;
|
|
1416
|
+
if (!value || !schema)
|
|
1417
|
+
return;
|
|
1418
|
+
const bodyField = currentEntry.format === 'md' || currentEntry.format === 'mdx'
|
|
1419
|
+
? findBodyFieldName(schema)
|
|
1420
|
+
: undefined;
|
|
1421
|
+
const at = canopyPathOf(value, placeOf(field), bodyField);
|
|
1422
|
+
const target = at && findFieldTarget(formatCanopyPath(at));
|
|
1423
|
+
if (target)
|
|
1424
|
+
focusFieldTarget(target.element);
|
|
1425
|
+
});
|
|
1426
|
+
};
|
|
1427
|
+
const resolveEntryConflict = async (logicalPath, id, mergeSha, choice) => {
|
|
1428
|
+
const branch = branchNameState;
|
|
1429
|
+
const failed = {
|
|
1430
|
+
tone: 'error',
|
|
1431
|
+
message: 'Couldn’t save your choice. Try again.',
|
|
1432
|
+
};
|
|
1433
|
+
let answer;
|
|
1434
|
+
try {
|
|
1435
|
+
answer = await apiClient.conflicts.resolveEntry({ branch, entryPath: logicalPath }, { id: id, mergeSha, choice });
|
|
1436
|
+
}
|
|
1437
|
+
catch (err) {
|
|
1438
|
+
if (!isUnanswered(err))
|
|
1439
|
+
return failed;
|
|
1440
|
+
}
|
|
1441
|
+
if (!answer || isGatewayFailure(answer) || answer.code === 'WRITE_OUTCOME_UNKNOWN') {
|
|
1442
|
+
// Reading the conflict again settles it: the card goes if the choice went through.
|
|
1443
|
+
void Promise.all([entryConflict.reload(), branchConflicts.reload()]).catch(console.error);
|
|
1444
|
+
return {
|
|
1445
|
+
tone: 'warning',
|
|
1446
|
+
message: 'Your choice may have gone through even though the server didn’t answer. If this card is still here, choose again.',
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
if (!answer.ok) {
|
|
1450
|
+
// The record moved on, or the entry moved or left the branch: what is left to choose shows.
|
|
1451
|
+
if (answer.code === 'CONFLICT_CHANGED' || answer.code === 'ENTRY_GONE') {
|
|
1452
|
+
void Promise.all([
|
|
1453
|
+
entryConflict.reload(),
|
|
1454
|
+
branchConflicts.reload(),
|
|
1455
|
+
refreshEntries(branch),
|
|
1456
|
+
]).catch(console.error);
|
|
1457
|
+
}
|
|
1458
|
+
if (answer.status !== 409)
|
|
1459
|
+
return { ...failed, message: answer.error ?? failed.message };
|
|
1460
|
+
const error = answer.error ?? failed.message;
|
|
1461
|
+
// A latest version that can't replace this entry leaves keeping yours, then editing it.
|
|
1462
|
+
return {
|
|
1463
|
+
tone: 'warning',
|
|
1464
|
+
message: choice === 'use-latest' && !answer.code
|
|
1465
|
+
? `${error} Choose "${EDITOR_ACTIONS.keepYourVersion}", then edit it to match.`
|
|
1466
|
+
: error,
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
// The entry's file may have changed, so its saved copy is read again.
|
|
1470
|
+
setLoadedValues((prev) => {
|
|
1471
|
+
if (!(id in prev))
|
|
1472
|
+
return prev;
|
|
1473
|
+
const { [id]: _reread, ...rest } = prev;
|
|
1474
|
+
return rest;
|
|
1475
|
+
});
|
|
1476
|
+
void Promise.all([
|
|
1477
|
+
branchConflicts.reload(),
|
|
1478
|
+
entryConflict.reload(),
|
|
1479
|
+
handleReloadBranchData(),
|
|
1480
|
+
refreshEntries(branch),
|
|
1481
|
+
]).catch(console.error);
|
|
1482
|
+
return undefined;
|
|
1483
|
+
};
|
|
1484
|
+
// An admin's release of a branch held as its record of conflicts was set aside.
|
|
1485
|
+
const handleReleaseConflictsHold = async () => {
|
|
1486
|
+
const result = await releaseConflictsHold(apiClient, branchNameState);
|
|
1487
|
+
if ('released' in result) {
|
|
1488
|
+
notifications.show({
|
|
1489
|
+
message: result.wasHeld === true
|
|
1490
|
+
? 'Hold released.'
|
|
1491
|
+
: result.wasHeld === false
|
|
1492
|
+
? 'This branch was already released.'
|
|
1493
|
+
: 'This branch is no longer on hold.',
|
|
1494
|
+
color: 'green',
|
|
1495
|
+
});
|
|
1496
|
+
void branchConflicts.reload();
|
|
1497
|
+
}
|
|
1498
|
+
return result;
|
|
1499
|
+
};
|
|
1500
|
+
keepConflictsRef.current = (items) => {
|
|
1501
|
+
void Promise.all(items.map((c) => c.logicalPath
|
|
1502
|
+
? resolveEntryConflict(c.logicalPath, c.contentId, c.mergeSha, 'keep')
|
|
1503
|
+
: Promise.resolve(undefined))).then((answers) => {
|
|
1504
|
+
const refusals = answers.filter((r) => r !== undefined);
|
|
1505
|
+
if (refusals.length === 0)
|
|
1506
|
+
return;
|
|
1507
|
+
const more = refusals.length > 1 ? ` (${refusals.length} of ${items.length} weren’t kept.)` : '';
|
|
1508
|
+
notifications.show({
|
|
1509
|
+
message: `${refusals[0].message}${more}`,
|
|
1510
|
+
color: refusals.some((r) => r.tone === 'error') ? 'red' : 'yellow',
|
|
1511
|
+
autoClose: false,
|
|
1512
|
+
});
|
|
1513
|
+
});
|
|
1514
|
+
};
|
|
1515
|
+
const changesResolveEntry = changesSubject && entryConflict.data
|
|
1516
|
+
? async (choice) => {
|
|
1517
|
+
const { contentId: id, mergeSha, kind } = entryConflict.data ?? {};
|
|
1518
|
+
if (!id || !mergeSha)
|
|
1519
|
+
return undefined;
|
|
1520
|
+
if (choice === 'delete' || (choice === 'use-latest' && kind === 'unreadable')) {
|
|
1521
|
+
confirmAfterChangesRef.current = {
|
|
1522
|
+
logicalPath: changesSubject.logicalPath,
|
|
1523
|
+
id,
|
|
1524
|
+
mergeSha,
|
|
1525
|
+
title: changesSubject.title,
|
|
1526
|
+
choice,
|
|
1527
|
+
};
|
|
1528
|
+
setChangesOpen(false);
|
|
1529
|
+
return undefined;
|
|
1530
|
+
}
|
|
1531
|
+
return resolveEntryConflict(changesSubject.logicalPath, id, mergeSha, choice);
|
|
1532
|
+
}
|
|
1533
|
+
: undefined;
|
|
1534
|
+
const confirmEntryChoice = (target) => {
|
|
1535
|
+
const branch = branchNameState;
|
|
1536
|
+
const deleting = target.choice === 'delete';
|
|
1537
|
+
let confirmed = false;
|
|
1538
|
+
openConfirm({
|
|
1539
|
+
title: deleting ? 'Delete entry?' : 'Use the latest version?',
|
|
1540
|
+
children: (_jsx(Text, { size: "sm", children: deleting
|
|
1541
|
+
? `“${target.title}” was deleted in the latest version. Deleting it here too removes your changes to it.`
|
|
1542
|
+
: `Your saved changes to “${target.title}” are replaced by the latest version.` })),
|
|
1543
|
+
labels: {
|
|
1544
|
+
confirm: deleting ? EDITOR_ACTIONS.deleteEntry : EDITOR_ACTIONS.useLatestVersion,
|
|
1545
|
+
cancel: 'Cancel',
|
|
1546
|
+
},
|
|
1547
|
+
confirmProps: { color: 'red' },
|
|
1548
|
+
// Any way out but Confirm, Escape and the close button included, returns to "Show changes".
|
|
1549
|
+
onClose: () => {
|
|
1550
|
+
if (confirmed)
|
|
1551
|
+
return;
|
|
1552
|
+
setChangesOpen(true);
|
|
1553
|
+
setChangesShown(true);
|
|
1554
|
+
},
|
|
1555
|
+
onConfirm: () => {
|
|
1556
|
+
confirmed = true;
|
|
1557
|
+
void resolveEntryConflict(target.logicalPath, target.id, target.mergeSha, target.choice).then((refusal) => {
|
|
1558
|
+
if (refusal) {
|
|
1559
|
+
notifications.show({
|
|
1560
|
+
message: refusal.message,
|
|
1561
|
+
color: refusal.tone === 'error' ? 'red' : 'yellow',
|
|
1562
|
+
autoClose: false,
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
else if (deleting) {
|
|
1566
|
+
closeEntry(target.logicalPath, branch);
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
},
|
|
1570
|
+
});
|
|
1571
|
+
};
|
|
1572
|
+
// A choice made on an earlier merge of a field no longer counts once the record moved on, and
|
|
1573
|
+
// an entry whose conflicts are gone keeps none.
|
|
1574
|
+
const retainChoices = conflictChoices.retain;
|
|
1575
|
+
const keepEntryChoices = conflictChoices.keepEntries;
|
|
1576
|
+
// An entry no longer listed with conflicts, resolved elsewhere or by a later merge, keeps none.
|
|
1577
|
+
const listedConflicts = currentBranch?.conflictFiles?.length
|
|
1578
|
+
? branchConflicts.data?.conflicts
|
|
1579
|
+
: currentBranch
|
|
1580
|
+
? []
|
|
1581
|
+
: undefined;
|
|
1582
|
+
const listedIds = listedConflicts?.map((c) => c.contentId).join(',');
|
|
1583
|
+
useEffect(() => {
|
|
1584
|
+
if (listedIds !== undefined)
|
|
1585
|
+
keepEntryChoices(new Set(listedIds.split(',').filter(Boolean)));
|
|
1586
|
+
}, [listedIds, keepEntryChoices]);
|
|
1587
|
+
useEffect(() => {
|
|
1588
|
+
const id = changesConflictTarget?.id;
|
|
1589
|
+
if (!id || entryConflict.data === undefined)
|
|
1590
|
+
return;
|
|
1591
|
+
retainChoices(id, entryConflict.data?.fields ?? []);
|
|
1592
|
+
}, [changesConflictTarget?.id, entryConflict.data, retainChoices]);
|
|
1593
|
+
// The toast's "Show changes" opens the first entry with conflicts, then, once it is open and
|
|
1594
|
+
// listed as changed, its drawer.
|
|
1595
|
+
const [pendingConflictShow, setPendingConflictShow] = useState();
|
|
1596
|
+
showConflictsRef.current = () => {
|
|
1597
|
+
const branch = branchNameState;
|
|
1598
|
+
void apiClient.conflicts
|
|
1599
|
+
.list({ branch })
|
|
1600
|
+
.then((answer) => {
|
|
1601
|
+
if (branch !== currentBranchRef.current)
|
|
1602
|
+
return;
|
|
1603
|
+
const first = answer.ok
|
|
1604
|
+
? answer.data?.conflicts.find((c) => c.logicalPath && entriesState.some((e) => e.path === c.logicalPath))
|
|
1605
|
+
: undefined;
|
|
1606
|
+
if (!first?.logicalPath)
|
|
1607
|
+
return;
|
|
1608
|
+
setPendingConflictShow(first.logicalPath);
|
|
1609
|
+
openEntry(first.logicalPath);
|
|
1610
|
+
})
|
|
1611
|
+
.catch(console.error);
|
|
1612
|
+
};
|
|
1613
|
+
useEffect(() => {
|
|
1614
|
+
if (!pendingConflictShow)
|
|
1615
|
+
return;
|
|
1616
|
+
if (selectedPath !== pendingConflictShow) {
|
|
1617
|
+
setPendingConflictShow(undefined);
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
if (currentEntry?.path !== pendingConflictShow)
|
|
1621
|
+
return;
|
|
1622
|
+
const change = branchChanges.data?.entries.find((e) => e.status !== 'deleted' && e.logicalPath === pendingConflictShow);
|
|
1623
|
+
if (!change)
|
|
1624
|
+
return;
|
|
1625
|
+
setPendingConflictShow(undefined);
|
|
1626
|
+
showEntryChanges(change, currentEntry.label ?? currentEntry.slug ?? '', 'header');
|
|
1627
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- showEntryChanges reads this render
|
|
1628
|
+
}, [pendingConflictShow, selectedPath, currentEntry, branchChanges.data]);
|
|
1629
|
+
const showOpenEntryChanges = () => {
|
|
1630
|
+
const change = currentEntry &&
|
|
1631
|
+
branchChanges.data?.entries.find((e) => e.status !== 'deleted' && e.logicalPath === currentEntry.path);
|
|
1632
|
+
if (change)
|
|
1633
|
+
showEntryChanges(change, currentEntry.label ?? currentEntry.slug ?? '', 'header');
|
|
1634
|
+
};
|
|
1635
|
+
// The open entry's conflicts, said above its form, with the way to "Show changes".
|
|
1636
|
+
const openEntryConflictNotice = currentEntry?.contentId && currentBranch?.conflictFiles?.includes(currentEntry.contentId)
|
|
1637
|
+
? {
|
|
1638
|
+
message: entryConflictNotice(branchConflicts.data?.conflicts.find((c) => c.contentId === currentEntry.contentId)),
|
|
1639
|
+
onShowChanges: branchChanges.data?.entries.some((e) => e.status !== 'deleted' && e.logicalPath === currentEntry.path)
|
|
1640
|
+
? showOpenEntryChanges
|
|
1641
|
+
: undefined,
|
|
1642
|
+
}
|
|
1643
|
+
: undefined;
|
|
1298
1644
|
const openEntryChange = currentEntry &&
|
|
1299
1645
|
branchChanges.data?.entries.find((e) => e.status !== 'deleted' && e.logicalPath === currentEntry.path);
|
|
1300
1646
|
const referenceTitles = useMemo(() => new Map(entriesState.map((e) => [e.contentId, e.title ?? e.label])), [entriesState]);
|
|
@@ -1321,6 +1667,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1321
1667
|
const fields = entriesState.find((e) => e.path === entry.logicalPath)?.schema ??
|
|
1322
1668
|
entriesState.find((e) => e.collectionPath === entry.collectionPath && e.entryType === entry.entryType)?.schema ??
|
|
1323
1669
|
EMPTY_SCHEMA;
|
|
1670
|
+
deleteAfterChangesRef.current = undefined;
|
|
1324
1671
|
setChangesSubject({
|
|
1325
1672
|
logicalPath: entry.logicalPath,
|
|
1326
1673
|
contentId: entry.contentId,
|
|
@@ -1350,6 +1697,110 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1350
1697
|
const changesPullRequestUrl = isReviewer(userContext?.groups)
|
|
1351
1698
|
? currentBranch?.pullRequestUrl
|
|
1352
1699
|
: undefined;
|
|
1700
|
+
// The drawer's undo, delete and restore follow the form's rules: hidden without edit access,
|
|
1701
|
+
// disabled with the branch lock's reason while the branch takes no writes. Undo writes the open
|
|
1702
|
+
// entry's draft, through the functional update the remove-Undo uses, so it is an unsaved edit.
|
|
1703
|
+
const changesSubjectOpen = !!changesSubject &&
|
|
1704
|
+
!!currentEntry &&
|
|
1705
|
+
changesSubject.status !== 'deleted' &&
|
|
1706
|
+
changesSubject.logicalPath === currentEntry.path &&
|
|
1707
|
+
(changesSubject.contentId === undefined || changesSubject.contentId === currentEntry.contentId);
|
|
1708
|
+
const changesLockReason = latestWaiting
|
|
1709
|
+
? WAITING_NOTICE
|
|
1710
|
+
: branchContentLocked
|
|
1711
|
+
? describeBranchLock({
|
|
1712
|
+
branchStatus: currentBranch?.status,
|
|
1713
|
+
branchReadOnly: currentBranch?.readOnly,
|
|
1714
|
+
branchWriteBlocked: true,
|
|
1715
|
+
branchPending,
|
|
1716
|
+
branchMissing,
|
|
1717
|
+
reviewing: branchReviewing,
|
|
1718
|
+
pullRequestClosed: branchPullRequestClosed,
|
|
1719
|
+
approveBlocked: currentBranch?.approveBlocked,
|
|
1720
|
+
})
|
|
1721
|
+
: undefined;
|
|
1722
|
+
const changesUndo = changesSubjectOpen && currentEntry && displayValue
|
|
1723
|
+
? {
|
|
1724
|
+
// What the form shows: a draft hidden behind a lock isn't what the cards compare.
|
|
1725
|
+
value: displayValue,
|
|
1726
|
+
saved: loadedValues[currentEntry.contentId],
|
|
1727
|
+
update: (update) => {
|
|
1728
|
+
if (contentReadOnlyRef.current)
|
|
1729
|
+
return;
|
|
1730
|
+
const contentId = currentEntry.contentId;
|
|
1731
|
+
setDrafts((prev) => {
|
|
1732
|
+
const value = prev[contentId] ?? loadedValuesRef.current[contentId];
|
|
1733
|
+
if (!value)
|
|
1734
|
+
return prev;
|
|
1735
|
+
const next = update(value);
|
|
1736
|
+
return next === value ? prev : { ...prev, [contentId]: next };
|
|
1737
|
+
});
|
|
1738
|
+
},
|
|
1739
|
+
threadsWithin: (path) => openThreadsWithin(displayComments, currentEntry.path, path),
|
|
1740
|
+
}
|
|
1741
|
+
: undefined;
|
|
1742
|
+
// An entry the branch deleted has no row to carry `canEdit`; its collection's `canAddEntry`
|
|
1743
|
+
// stands in, and the server judges the entry's own path.
|
|
1744
|
+
const deletedSubjectCollection = changesSubject?.status === 'deleted'
|
|
1745
|
+
? changesSubject.logicalPath.slice(0, changesSubject.logicalPath.lastIndexOf('/'))
|
|
1746
|
+
: undefined;
|
|
1747
|
+
const restoreRefusedByPath = deletedSubjectCollection !== undefined &&
|
|
1748
|
+
findCollection(collectionsFromApi, deletedSubjectCollection)?.canAddEntry === false;
|
|
1749
|
+
const changesRestoreEntry = changesSubject?.status === 'deleted' && changesSubject.contentId
|
|
1750
|
+
? async () => {
|
|
1751
|
+
const { logicalPath, contentId, title } = changesSubject;
|
|
1752
|
+
const branch = branchNameState;
|
|
1753
|
+
let answer;
|
|
1754
|
+
try {
|
|
1755
|
+
answer = await apiClient.changes.restore({ branch, entryPath: logicalPath }, { id: contentId });
|
|
1756
|
+
}
|
|
1757
|
+
catch (err) {
|
|
1758
|
+
if (!isUnanswered(err)) {
|
|
1759
|
+
return { tone: 'error', message: 'Couldn’t restore this entry. Try again.' };
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
// A 409 whose write may have run is as unanswered as a timeout.
|
|
1763
|
+
const outcomeUnknown = !!answer &&
|
|
1764
|
+
!answer.ok &&
|
|
1765
|
+
(isGatewayFailure(answer) || answer.code === 'WRITE_OUTCOME_UNKNOWN');
|
|
1766
|
+
if (answer && !answer.ok && !outcomeUnknown) {
|
|
1767
|
+
return {
|
|
1768
|
+
tone: answer.status === 409 ? 'warning' : 'error',
|
|
1769
|
+
message: answer.error ?? 'Couldn’t restore this entry. Try again.',
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
const entries = await refreshEntries(branch).catch(() => undefined);
|
|
1773
|
+
void branchChanges.reload();
|
|
1774
|
+
if (answer?.ok && answer.data?.warning) {
|
|
1775
|
+
notifications.show({ message: answer.data.warning, color: 'yellow' });
|
|
1776
|
+
}
|
|
1777
|
+
if (answer?.ok && !entries) {
|
|
1778
|
+
notifications.show({
|
|
1779
|
+
message: `"${title}" is restored. Refresh the page to see it in the "Content" panel.`,
|
|
1780
|
+
color: 'yellow',
|
|
1781
|
+
});
|
|
1782
|
+
setChangesOpen(false);
|
|
1783
|
+
return undefined;
|
|
1784
|
+
}
|
|
1785
|
+
// Unanswered, the entry list settles it: this entry, by id, listed again.
|
|
1786
|
+
if (!answer?.ok &&
|
|
1787
|
+
!entries?.some((e) => e.path === logicalPath && e.contentId === contentId)) {
|
|
1788
|
+
return {
|
|
1789
|
+
tone: 'warning',
|
|
1790
|
+
message: `Restoring "${title}" may have gone through, but the server couldn’t confirm it. Check the "Content" panel for it before trying again.`,
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
setChangesOpen(false);
|
|
1794
|
+
openEntry(logicalPath);
|
|
1795
|
+
return undefined;
|
|
1796
|
+
}
|
|
1797
|
+
: undefined;
|
|
1798
|
+
const changesDeleteEntry = changesSubjectOpen && changesSubject?.status === 'added'
|
|
1799
|
+
? () => {
|
|
1800
|
+
deleteAfterChangesRef.current = changesSubject.logicalPath;
|
|
1801
|
+
setChangesOpen(false);
|
|
1802
|
+
}
|
|
1803
|
+
: undefined;
|
|
1353
1804
|
const runSaveAll = () => {
|
|
1354
1805
|
void handleSaveAll()
|
|
1355
1806
|
.then((result) => {
|
|
@@ -1574,7 +2025,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1574
2025
|
}
|
|
1575
2026
|
: undefined, "data-testid": "content-panel", actions: _jsxs(_Fragment, { children: [navCollections &&
|
|
1576
2027
|
navCollections.length > 0 &&
|
|
1577
|
-
!
|
|
2028
|
+
!contentLocked &&
|
|
1578
2029
|
(navCollections[0].onAdd || navCollections[0].onAddSubCollection) && (_jsxs(Menu, { shadow: "md", width: 200, withinPortal: true, position: "bottom-end",
|
|
1579
2030
|
// Its items open dialogs outside the panel, which would close a transient one.
|
|
1580
2031
|
onOpen: keepContentPanelOpen, children: [_jsx(Tooltip, { label: "Content actions", withinPortal: true, children: _jsx(Menu.Target, { children: _jsx(ActionIcon, { variant: "subtle", color: "gray", "aria-label": "Content actions", children: _jsx(IconDots, { size: 16 }) }) }) }), _jsxs(Menu.Dropdown, { children: [navCollections[0].onAdd && (_jsx(Menu.Item, { leftSection: _jsx(IconPlus, { size: 14 }), onClick: () => navCollections[0].onAdd?.(), children: "Add entry\u2026" })), navCollections[0].onAddSubCollection && (_jsxs(_Fragment, { children: [navCollections[0].onAdd && _jsx(Menu.Divider, {}), _jsx(Menu.Label, { children: "Admin" }), _jsx(Menu.Item, { leftSection: _jsx(IconFolderPlus, { size: 14 }), onClick: () => navCollections[0].onAddSubCollection?.(), children: "Add collection\u2026" })] }))] })] })), _jsx(Tooltip, { label: "Collapse all folders", withinPortal: true, children: _jsx(ActionIcon, { variant: "subtle", color: "gray", onClick: handleCollapseAll, "aria-label": "Collapse all folders", children: _jsx(IconChevronUp, { size: 16 }) }) }), _jsx(Tooltip, { label: "Expand all folders", withinPortal: true, children: _jsx(ActionIcon, { variant: "subtle", color: "gray", onClick: handleExpandAll, "aria-label": "Expand all folders", children: _jsx(IconChevronDown, { size: 16 }) }) })] }), children: _jsx(Box, { px: "md", children: _jsx(EntryNavigator, { collections: navCollections, items: navCollections
|
|
@@ -1589,17 +2040,19 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1589
2040
|
missing: e.status === 'missing' || undefined,
|
|
1590
2041
|
noPage: !e.previewSrc || undefined,
|
|
1591
2042
|
readOnly: e.canEdit === false || undefined,
|
|
1592
|
-
})), selectedPath: selectedPath, onSelect: openEntry, onTreeControllerReady: handleTreeControllerReady, expandedStateRef: treeExpandedStateRef, onExpandedStateChange: handleExpandedStateChange, onDeleteEntry: handleDeleteEntry, onRenameEntry: handleRenameEntry, onReorderEntry: userIsAdmin ? handleReorderEntry : undefined, hiddenRootPath: hiddenRootPath, loading: entriesInitializing, readOnly:
|
|
2043
|
+
})), selectedPath: selectedPath, onSelect: openEntry, onTreeControllerReady: handleTreeControllerReady, expandedStateRef: treeExpandedStateRef, onExpandedStateChange: handleExpandedStateChange, onDeleteEntry: handleDeleteEntry, onRenameEntry: handleRenameEntry, onReorderEntry: userIsAdmin ? handleReorderEntry : undefined, hiddenRootPath: hiddenRootPath, loading: entriesInitializing, readOnly: contentLocked, readOnlyReason: branchPending
|
|
1593
2044
|
? undefined
|
|
1594
|
-
:
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
2045
|
+
: latestWaiting
|
|
2046
|
+
? WAITING_NOTICE
|
|
2047
|
+
: describeBranchLock({
|
|
2048
|
+
branchStatus: currentBranch?.status,
|
|
2049
|
+
branchReadOnly: currentBranch?.readOnly,
|
|
2050
|
+
branchWriteBlocked: branchContentLocked,
|
|
2051
|
+
branchMissing,
|
|
2052
|
+
reviewing: branchReviewing,
|
|
2053
|
+
pullRequestClosed: branchPullRequestClosed,
|
|
2054
|
+
approveBlocked: currentBranch?.approveBlocked,
|
|
2055
|
+
}), dirtyPaths: dirtyPaths, reveal: contentReveal, onRevealed: () => setContentReveal(undefined), entryFilter: entryFilter === 'changed' && !changesOffered ? 'all' : entryFilter, onEntryFilterChange: setEntryFilter, onShowUnsaved: showUnsaved, onShowDeletedChanges: (entry, label) => showEntryChanges(entry, label, 'deleted row'), branchChanges: changesOffered
|
|
1603
2056
|
? {
|
|
1604
2057
|
base: branchChanges.data?.base ?? currentBranch?.baseBranch ?? 'the base branch',
|
|
1605
2058
|
data: branchChanges.data,
|
|
@@ -1622,7 +2075,9 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1622
2075
|
// branchWriteBlocked reuses branchContentLocked itself rather than
|
|
1623
2076
|
// re-deriving `?? true` a second time here, so there is exactly one
|
|
1624
2077
|
// fail-closed computation of "are writes blocked" in this component.
|
|
1625
|
-
branchIsProtected: currentBranch?.isProtected ?? true, branchServerProtected: currentBranch?.isProtected, branchReadOnly: currentBranch?.readOnly, branchHold: currentBranch?.hold,
|
|
2078
|
+
branchIsProtected: currentBranch?.isProtected ?? true, branchServerProtected: currentBranch?.isProtected, branchReadOnly: currentBranch?.readOnly, branchHold: currentBranch?.hold, openConflicts: openConflictCount, conflictRecordHeld: branchConflicts.data?.held
|
|
2079
|
+
? { ...(userIsAdmin && { onRelease: handleReleaseConflictsHold }) }
|
|
2080
|
+
: undefined, branchApproveBlocked: currentBranch?.approveBlocked, branchWriteBlocked: branchContentLocked, branchLoading: branchLoading, branchPending: branchPending, branchMissing: branchMissing, branchToggleRef: branchToggleRef, baseUpdate: baseUpdateControl, latestWait: latestWait, onBranchReloadData: () => {
|
|
1626
2081
|
void Promise.all([handleReloadBranchData(), refreshEntries(branchNameState)]).catch(console.error);
|
|
1627
2082
|
}, onBranchDiscardDrafts: () => handleDiscardDrafts(() => branchToggleRef.current), onBranchManagerOpen: openBranchManager, onNewBranch: () => openBranchManager({ create: true }), onSaveAll: runSaveAll, saveAllProgress: saveAllProgress,
|
|
1628
2083
|
// On any entry: EntryHeader's `saving` is the open entry's alone.
|
|
@@ -1655,7 +2110,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1655
2110
|
// True exactly while the form below shows "Setting up…" or "Loading content…".
|
|
1656
2111
|
entryLoading: !currentEntry && ((!branchNameState && busy) || entriesInitializing), branchName: branchNameState, busy: busy, breadcrumbSegments: breadcrumbSegments,
|
|
1657
2112
|
// A draft kept for an unavailable entry stays in storage but cannot be saved.
|
|
1658
|
-
hasUnsavedChanges: !currentEntryUnavailable && !contentReadOnly && isSelectedDirty(), entryHasDraft: isSelectedDirty(), entryReadOnly: currentEntry?.canEdit === false, entryUnavailable: currentEntryUnavailable, draftStorageFailed: draftStorageFailed, branchStatus: currentBranch?.status, branchReadOnly: currentBranch?.readOnly, branchWriteBlocked: branchContentLocked, branchLoading: branchLoading, branchPending: branchPending, branchMissing: branchMissing, branchReviewing: branchReviewing, branchPullRequestClosed: branchPullRequestClosed, branchApproveBlocked: currentBranch?.approveBlocked, onCollectionOpen: (path) => revealInContentPanel({ kind: 'collection', path }), onEntryReveal: () => currentEntry && revealInContentPanel({ kind: 'entry', path: currentEntry.path }), navigatorToggleRef: navigatorToggleRef, onShowChanges: openEntryChange
|
|
2113
|
+
hasUnsavedChanges: !currentEntryUnavailable && !contentReadOnly && isSelectedDirty(), entryHasDraft: isSelectedDirty(), entryReadOnly: currentEntry?.canEdit === false, entryUnavailable: currentEntryUnavailable, draftStorageFailed: draftStorageFailed, branchStatus: currentBranch?.status, branchReadOnly: currentBranch?.readOnly, branchWriteBlocked: branchContentLocked, writesWaitReason: latestWaiting ? WAITING_NOTICE : undefined, branchLoading: branchLoading, branchPending: branchPending, branchMissing: branchMissing, branchReviewing: branchReviewing, branchPullRequestClosed: branchPullRequestClosed, branchApproveBlocked: currentBranch?.approveBlocked, onCollectionOpen: (path) => revealInContentPanel({ kind: 'collection', path }), onEntryReveal: () => currentEntry && revealInContentPanel({ kind: 'entry', path: currentEntry.path }), navigatorToggleRef: navigatorToggleRef, onShowChanges: openEntryChange
|
|
1659
2114
|
? () => showEntryChanges(openEntryChange, currentEntry?.label ?? currentEntry?.slug ?? '', 'header')
|
|
1660
2115
|
: undefined, showChangesPending: changesOffered && !!currentEntry, showChangesRef: showChangesRef, onFileReload: () => handleReload(() => navigatorToggleRef.current), onFileDiscardDraft: () => handleDiscardFileDraft(() => navigatorToggleRef.current), onEntryRename: () => currentEntry && handleRenameEntry(currentEntry.path, true), onEntryDelete: () => currentEntry && handleDeleteEntry(currentEntry.path, true), onSave: currentEntryUnavailable ? noop : handleSave, saveAllRunning: !!saveAllProgress, saving: singleSaving?.branch === branchNameState &&
|
|
1661
2116
|
singleSaving.contentId === currentEntry?.contentId, savedSignal: lastSaved?.seq, savedHere: lastSaved?.branch === branchNameState &&
|
|
@@ -1674,12 +2129,12 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1674
2129
|
? 'Setting up your branch workspace…'
|
|
1675
2130
|
: entriesInitializing
|
|
1676
2131
|
? 'Loading content…'
|
|
1677
|
-
: 'Select an item to start editing.' })) : currentEntryUnavailable ? (_jsx(UnavailableEntryNotice, { schemaRefs: currentEntry.unavailable ? [currentEntry.unavailable.schemaRef] : [] })) : currentEntry.canEdit === false ? (_jsx(NoEditPermissionNotice, { entryLabel: currentEntry.label })) : schema.length > 0 && displayValue ? (_jsx(EntryLinkContext.Provider, { value: entryLinkContextValue, children: _jsx(SiteMdxAllowContext.Provider, { value: mdxAllow, children: _jsx(PreviewFollowContext.Provider, { value: followInPreview ?? null, children: _jsx(FormRenderer, { notice: hiddenDraftNotice && (_jsx(ReadOnlyDraftNotice, { permanentlyReadOnly: (currentBranch?.isProtected ?? true) ||
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
2132
|
+
: 'Select an item to start editing.' })) : currentEntryUnavailable ? (_jsx(UnavailableEntryNotice, { schemaRefs: currentEntry.unavailable ? [currentEntry.unavailable.schemaRef] : [] })) : currentEntry.canEdit === false ? (_jsx(NoEditPermissionNotice, { entryLabel: currentEntry.label })) : schema.length > 0 && displayValue ? (_jsx(EntryLinkContext.Provider, { value: entryLinkContextValue, children: _jsx(SiteMdxAllowContext.Provider, { value: mdxAllow, children: _jsx(PreviewFollowContext.Provider, { value: followInPreview ?? null, children: _jsx(FormRenderer, { notice: (latestWait !== 'none' || hiddenDraftNotice) && (_jsxs(_Fragment, { children: [latestWait !== 'none' && (_jsx(BaseUpdateNotice, { wait: latestWait, hidesDraft: draftHidden && latestWaiting })), hiddenDraftNotice && (_jsx(ReadOnlyDraftNotice, { permanentlyReadOnly: (currentBranch?.isProtected ?? true) ||
|
|
2133
|
+
currentBranch?.status === 'archived',
|
|
2134
|
+
// A hidden draft's discard changes nothing on screen, so it says so.
|
|
2135
|
+
onDiscard: () => handleDiscardFileDraft(() => navigatorToggleRef.current, {
|
|
2136
|
+
announce: true,
|
|
2137
|
+
}) }))] })), fields: schema, value: displayValue, readOnly: contentReadOnly, onRemoved: handleFieldRemoved, savedValue: loadedValue, commentBlockedReason: commentBlockedReason, removedItemFieldLabel: commentFieldLabel, onGetUserMetadata: getCommentUserMetadata, header: {
|
|
1683
2138
|
title: currentEntry.label,
|
|
1684
2139
|
typeLabel: entryTypeLabel(activeCollections, currentEntry),
|
|
1685
2140
|
},
|
|
@@ -1692,11 +2147,14 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1692
2147
|
const keyed = withItemKeys(schema, next, 'random');
|
|
1693
2148
|
setDrafts((prev) => ({ ...prev, [contentId]: keyed }));
|
|
1694
2149
|
}
|
|
1695
|
-
}, customRenderers: customRenderers, branch: branchNameState, comments: displayComments, currentEntryPath: selectedPath, currentUserId: commentUserId, canResolve: canResolveAnyComment, focusedFieldPath: focusedFieldPath, highlightThreadId: highlightThreadId, onAddComment: handleAddDisplayedComment, onResolveThread: handleResolveThread, fieldErrors: fieldErrors, format: currentEntry.format, conflictNotice:
|
|
1696
|
-
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: () => {
|
|
2150
|
+
}, customRenderers: customRenderers, branch: branchNameState, comments: displayComments, currentEntryPath: selectedPath, currentUserId: commentUserId, canResolve: canResolveAnyComment, focusedFieldPath: focusedFieldPath, highlightThreadId: highlightThreadId, onAddComment: handleAddDisplayedComment, onResolveThread: handleResolveThread, fieldErrors: fieldErrors, format: currentEntry.format, conflictNotice: openEntryConflictNotice }) }) }) })) : 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: () => {
|
|
1697
2151
|
setLoadFailedKey(null);
|
|
1698
2152
|
setLoadAttempt((attempt) => attempt + 1);
|
|
1699
|
-
}, children: "Retry" }) })) }) })] }), overlays: _jsxs(_Fragment, { children: [_jsx(BranchesDrawer, { opened: branchManagerOpen, onClose: () => setBranchManagerOpen(false), confirmOpen: branchConfirmOpen, returnFocusTo: branchReturnTarget, children: _jsx(BranchManager, { startCreating: branchManagerOpening.create, branches: branchSummaries, mode: operatingMode, user: userContext, currentBranchName: currentBranch?.name ?? branchNameState, currentWorkflowBlockedReason: saveAllProgress
|
|
2153
|
+
}, children: "Retry" }) })) }) })] }), overlays: _jsxs(_Fragment, { children: [_jsx(BranchesDrawer, { opened: branchManagerOpen, onClose: () => setBranchManagerOpen(false), confirmOpen: branchConfirmOpen, returnFocusTo: branchReturnTarget, children: _jsx(BranchManager, { startCreating: branchManagerOpening.create, branches: branchSummaries, mode: operatingMode, user: userContext, currentBranchName: currentBranch?.name ?? branchNameState, currentWorkflowBlockedReason: saveAllProgress
|
|
2154
|
+
? 'Wait for Save all to finish.'
|
|
2155
|
+
: latestWaiting
|
|
2156
|
+
? WAITING_SUBMIT_REASON
|
|
2157
|
+
: undefined, currentBaseUpdate: baseUpdateControl, onSelect: async (name) => {
|
|
1700
2158
|
try {
|
|
1701
2159
|
await handleBranchChange(name);
|
|
1702
2160
|
setBranchManagerOpen(false);
|
|
@@ -1729,10 +2187,48 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1729
2187
|
if (namesBranch(name, branchNameState) && isSaveAllRunning())
|
|
1730
2188
|
return;
|
|
1731
2189
|
handleArchive(name).catch((err) => console.error(err));
|
|
1732
|
-
}, onClose: () => setBranchManagerOpen(false), comments: comments, currentUserId: commentUserId, canResolve: canResolveAnyComment, onAddComment: handleAddComment, onResolveThread: handleResolveThread, highlightThreadId: highlightThreadId, onGetUserMetadata: canResolveUserNames ? handleGetUserMetadata : undefined, onGetOwnerMetadata: getCommentUserMetadata, onGetCommentUserMetadata: getCommentUserMetadata }, branchManagerOpening.seq) }), changesSubject && (_jsx(LazyEntryChangesDrawer, { opened: changesOpen, onClose: () => setChangesOpen(false), onExited: () =>
|
|
2190
|
+
}, onClose: () => setBranchManagerOpen(false), comments: comments, currentUserId: commentUserId, canResolve: canResolveAnyComment, onAddComment: handleAddComment, onResolveThread: handleResolveThread, highlightThreadId: highlightThreadId, onGetUserMetadata: canResolveUserNames ? handleGetUserMetadata : undefined, onGetOwnerMetadata: getCommentUserMetadata, onGetCommentUserMetadata: getCommentUserMetadata }, branchManagerOpening.seq) }), changesSubject && (_jsx(LazyEntryChangesDrawer, { opened: changesOpen, onClose: () => setChangesOpen(false), onExited: () => {
|
|
2191
|
+
setChangesShown(false);
|
|
2192
|
+
const editing = focusAfterChangesRef.current;
|
|
2193
|
+
focusAfterChangesRef.current = undefined;
|
|
2194
|
+
if (editing)
|
|
2195
|
+
focusConflictField(editing);
|
|
2196
|
+
const confirming = confirmAfterChangesRef.current;
|
|
2197
|
+
confirmAfterChangesRef.current = undefined;
|
|
2198
|
+
if (confirming)
|
|
2199
|
+
confirmEntryChoice(confirming);
|
|
2200
|
+
const deleting = deleteAfterChangesRef.current;
|
|
2201
|
+
deleteAfterChangesRef.current = undefined;
|
|
2202
|
+
// Opened as from the entry menu, whose trigger stands in for the gone button.
|
|
2203
|
+
if (deleting && deleting === changesSubject.logicalPath) {
|
|
2204
|
+
handleDeleteEntry(deleting, true);
|
|
2205
|
+
}
|
|
2206
|
+
}, returnFocusTo: changesReturnTarget, base: branchChanges.data?.base ?? currentBranch?.baseBranch ?? 'the base branch', subject: {
|
|
1733
2207
|
...changesSubject,
|
|
1734
2208
|
unsavedEdits: changesSubject.logicalPath === currentEntry?.path && isSelectedDirty(),
|
|
1735
|
-
}, state: entryChange, referenceTitle: referenceTitle, onOpenEntry: changesOpenEntry, pullRequestUrl: changesPullRequestUrl
|
|
2209
|
+
}, state: entryChange, referenceTitle: referenceTitle, onOpenEntry: changesOpenEntry, pullRequestUrl: changesPullRequestUrl, undo: changesUndo, noEditReason: (changesSubjectOpen && currentEntry?.canEdit === false) || restoreRefusedByPath
|
|
2210
|
+
? NO_EDIT_ACCESS_REASON
|
|
2211
|
+
: undefined, lockedReason: changesLockReason, onDeleteEntry: changesDeleteEntry, onRestoreEntry:
|
|
2212
|
+
// A deleted entry with conflicts open is settled on its conflict card first: a
|
|
2213
|
+
// deleted-on-branch card restores it, and with field conflicts a restore would
|
|
2214
|
+
// bring back the latest version unchanged, which "Show changes" doesn't list,
|
|
2215
|
+
// leaving them with nowhere to be chosen. Once they're kept, this offers it.
|
|
2216
|
+
entryConflict.data ? undefined : changesRestoreEntry, conflicts: changesConflictTarget
|
|
2217
|
+
? {
|
|
2218
|
+
state: entryConflict,
|
|
2219
|
+
adminBaseBranch: userIsAdmin ? currentBranch?.baseBranch : undefined,
|
|
2220
|
+
choices: conflictChoices.choicesFor(changesConflictTarget.id),
|
|
2221
|
+
onChoose: (field, choice, decided) => conflictChoices.choose(changesConflictTarget.id, field.key, field.mergeSha, choice, decided),
|
|
2222
|
+
onEdit: changesSubjectOpen
|
|
2223
|
+
? (field) => {
|
|
2224
|
+
focusAfterChangesRef.current = field;
|
|
2225
|
+
setChangesOpen(false);
|
|
2226
|
+
}
|
|
2227
|
+
: undefined,
|
|
2228
|
+
onResolveEntry: changesResolveEntry,
|
|
2229
|
+
unsavedEdits: isDirtyForEntry(changesSubject.logicalPath),
|
|
2230
|
+
}
|
|
2231
|
+
: undefined })), branchNameState && (_jsx(CommentsPanel, { opened: commentsPanelOpen, branchName: branchLabel, comments: displayComments, canResolve: canResolveAnyComment, currentUserId: commentUserId, entryLabel: commentEntryLabel, fieldLabel: commentFieldLabel, onAddComment: handleAddDisplayedComment, onResolveThread: handleResolveThread, onClose: () => setCommentsPanelOpen(false), onJumpToField: jumpToThreadField, onGetUserMetadata: getCommentUserMetadata, onJumpToEntry: handleJumpToEntry, onJumpToBranch: handleJumpToBranch }, branchNameState)), userIsAdmin && (_jsx(GroupManager, { opened: groupManagerOpen, onClose: () => setGroupManagerOpen(false), returnFocusTo: settingsReturnTarget, internalGroups: groupsData, loading: groupsLoading, canEdit: userIsAdmin, onSave: handleSaveGroups, onSearchUsers: handleSearchUsers, onGetUserMetadata: handleGetUserMetadata, onSearchExternalGroups: handleSearchExternalGroups })), userIsAdmin && (_jsx(PermissionManager, { opened: permissionManagerOpen, onClose: () => setPermissionManagerOpen(false), returnFocusTo: settingsReturnTarget, collections: activeCollections, contentRoot: contentRoot, permissions: permissionsData, loading: permissionsLoading, canEdit: userIsAdmin, onSave: handleSavePermissions, onSearchUsers: handleSearchUsers, onGetUserMetadata: handleGetUserMetadata, onListGroups: handleListGroups })), userIsAdmin && (_jsx(SystemHealthPanel, { opened: systemHealthOpen, onClose: () => setSystemHealthOpen(false), returnFocusTo: settingsReturnTarget })), _jsx(MediaLibrary, { opened: mediaLibraryOpen, onClose: () => setMediaLibraryOpen(false), mode: "manage", returnFocusTo: () => mediaButtonRef.current }), userIsAdmin && (_jsx(CollectionEditor, { isOpen: collectionEditorOpen, editingCollection: editingCollection, parentPath: collectionEditorParentPath, availableSchemas: availableSchemas, onSave: handleCollectionSave, onAddEntryType: editingCollection
|
|
1736
2232
|
? (path, entryType) => addEntryType(path, entryType)
|
|
1737
2233
|
: undefined, onUpdateEntryType: editingCollection
|
|
1738
2234
|
? (path, name, updates) => updateEntryType(path, name, updates)
|