canopycms 0.7.1 → 0.8.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 +197 -47
- package/dist/api/admin-branch-health.js +47 -20
- package/dist/api/admin.d.ts +13 -1
- package/dist/api/admin.js +14 -24
- package/dist/api/branch-changes.d.ts +87 -0
- package/dist/api/branch-changes.js +269 -0
- package/dist/api/branch-entry-changes.d.ts +83 -0
- package/dist/api/branch-entry-changes.js +262 -0
- package/dist/api/branch-merge.js +2 -2
- package/dist/api/branch-review.js +35 -4
- package/dist/api/branch-status.js +14 -5
- package/dist/api/branch-withdraw.js +7 -0
- package/dist/api/branch.d.ts +13 -1
- package/dist/api/branch.js +171 -97
- package/dist/api/client.d.ts +15 -0
- package/dist/api/client.js +13 -1
- package/dist/api/content.d.ts +18 -18
- package/dist/api/content.js +1 -1
- package/dist/api/github-sync.js +7 -3
- package/dist/api/groups.d.ts +4 -4
- package/dist/api/pr-comment-sync.d.ts +9 -8
- package/dist/api/pr-comment-sync.js +23 -13
- package/dist/api/routes.js +4 -0
- package/dist/api/types.d.ts +3 -1
- package/dist/authorization/groups/schema.d.ts +4 -4
- package/dist/authorization/protected-branch.d.ts +3 -3
- package/dist/authorization/protected-branch.js +3 -3
- package/dist/branch-metadata-file.d.ts +84 -0
- package/dist/branch-metadata-file.js +9 -0
- package/dist/branch-metadata.d.ts +4 -4
- package/dist/branch-metadata.js +4 -5
- package/dist/branch-param.d.ts +24 -0
- package/dist/branch-param.js +87 -0
- package/dist/branch-provisioning.d.ts +6 -3
- package/dist/branch-provisioning.js +12 -10
- package/dist/branch-registry.d.ts +6 -0
- package/dist/branch-registry.js +12 -3
- package/dist/branch-schema-cache.d.ts +22 -2
- package/dist/branch-schema-cache.js +88 -12
- package/dist/branch-workspace.js +22 -15
- package/dist/cli/cli.js +1122 -181
- package/dist/cli/generate-ai-content.js +791 -109
- package/dist/cli/init.js +126 -0
- package/dist/comment-store.d.ts +3 -0
- package/dist/comment-store.js +4 -1
- package/dist/config/schemas/config.d.ts +3 -0
- package/dist/config/schemas/config.js +2 -0
- package/dist/config/types.d.ts +11 -0
- package/dist/content-id-index.d.ts +5 -0
- package/dist/content-id-index.js +1 -1
- package/dist/content-listing.d.ts +53 -0
- package/dist/content-listing.js +123 -27
- package/dist/content-reader.d.ts +24 -10
- package/dist/content-reader.js +66 -22
- package/dist/context.js +51 -48
- package/dist/editor/BranchManager.d.ts +7 -3
- package/dist/editor/BranchManager.js +51 -6
- package/dist/editor/Editor.js +166 -34
- package/dist/editor/EntryNavigator.d.ts +34 -10
- package/dist/editor/EntryNavigator.js +185 -29
- package/dist/editor/admin/SystemHealthPanel.js +36 -1
- package/dist/editor/admin/useSystemHealth.js +1 -0
- package/dist/editor/branch-names.d.ts +43 -0
- package/dist/editor/branch-names.js +75 -0
- package/dist/editor/components/BranchPill.d.ts +2 -2
- package/dist/editor/components/BranchPill.js +2 -2
- package/dist/editor/components/EditorHeader.d.ts +3 -1
- package/dist/editor/components/EditorHeader.js +2 -2
- package/dist/editor/components/EntryChangesDrawer.d.ts +51 -0
- package/dist/editor/components/EntryChangesDrawer.js +363 -0
- package/dist/editor/components/EntryChangesSkeleton.d.ts +9 -0
- package/dist/editor/components/EntryChangesSkeleton.js +7 -0
- package/dist/editor/components/EntryHeader.d.ts +9 -1
- package/dist/editor/components/EntryHeader.js +25 -3
- package/dist/editor/components/LazyEntryChangesDrawer.d.ts +16 -0
- package/dist/editor/components/LazyEntryChangesDrawer.js +45 -0
- package/dist/editor/components/ReviewDialogs.js +3 -3
- package/dist/editor/context/ApiClientContext.d.ts +9 -0
- package/dist/editor/context/ApiClientContext.js +26 -2
- package/dist/editor/context/index.d.ts +1 -1
- package/dist/editor/context/index.js +1 -1
- package/dist/editor/copy.d.ts +1 -0
- package/dist/editor/copy.js +1 -0
- package/dist/editor/editor-utils.d.ts +2 -0
- package/dist/editor/editor-utils.js +15 -0
- package/dist/editor/entry-diff.d.ts +143 -0
- package/dist/editor/entry-diff.js +593 -0
- package/dist/editor/hooks/create-branch-request.d.ts +11 -1
- package/dist/editor/hooks/create-branch-request.js +16 -6
- package/dist/editor/hooks/useBranchActions.d.ts +19 -2
- package/dist/editor/hooks/useBranchActions.js +29 -20
- package/dist/editor/hooks/useBranchChangesData.d.ts +57 -0
- package/dist/editor/hooks/useBranchChangesData.js +134 -0
- package/dist/editor/hooks/useBranchManager.d.ts +16 -0
- package/dist/editor/hooks/useBranchManager.js +142 -54
- package/dist/editor/hooks/useDraftManager.js +6 -2
- package/dist/editor/hooks/useEntryManager.d.ts +18 -2
- package/dist/editor/hooks/useEntryManager.js +36 -10
- package/dist/editor/utils/editor-errors.d.ts +5 -2
- package/dist/git-manager.d.ts +71 -0
- package/dist/git-manager.js +270 -1
- package/dist/http/handler.js +11 -9
- package/dist/operating-mode/branch-namespace-fixtures.d.ts +17 -0
- package/dist/operating-mode/branch-namespace-fixtures.js +65 -0
- package/dist/operating-mode/branch-namespace.d.ts +34 -0
- package/dist/operating-mode/branch-namespace.js +121 -0
- package/dist/operating-mode/client-unsafe-strategy.js +7 -0
- package/dist/operating-mode/types.d.ts +8 -0
- package/dist/paths/branch-name.d.ts +11 -19
- package/dist/paths/branch-name.js +17 -27
- package/dist/paths/index.d.ts +2 -2
- package/dist/paths/index.js +2 -2
- package/dist/paths/resolve.d.ts +0 -1
- package/dist/paths/resolve.js +0 -1
- package/dist/paths/validation.d.ts +40 -1
- package/dist/paths/validation.js +83 -4
- package/dist/pr-comments.d.ts +17 -9
- package/dist/pr-comments.js +53 -15
- package/dist/server.d.ts +6 -1
- package/dist/server.js +6 -1
- package/dist/services.js +2 -1
- package/dist/submission-attribution.d.ts +22 -0
- package/dist/submission-attribution.js +62 -6
- package/dist/task-queue/worker-status.d.ts +6 -1
- package/dist/task-queue/worker-status.js +23 -2
- package/dist/types.d.ts +59 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-policy.d.ts +111 -16
- package/dist/worker/branch-policy.js +222 -17
- package/dist/worker/cms-worker.d.ts +24 -2
- package/dist/worker/cms-worker.js +35 -24
- package/dist/worker/git-sync.d.ts +3 -3
- package/dist/worker/git-sync.js +42 -1
- package/dist/worker/github-gateway-http.js +2 -2
- package/dist/worker/github-gateway-wire.d.ts +42 -27
- package/dist/worker/github-gateway-wire.js +14 -3
- package/dist/worker/github-gateway.d.ts +17 -6
- package/dist/worker/github-gateway.js +62 -6
- package/dist/worker/github-mirror.d.ts +7 -4
- package/dist/worker/github-mirror.js +27 -16
- package/dist/worker/history-rewrite.d.ts +23 -10
- package/dist/worker/history-rewrite.js +43 -20
- package/dist/worker/legacy-branch-fixtures.d.ts +21 -0
- package/dist/worker/legacy-branch-fixtures.js +71 -0
- package/dist/worker/rebase.d.ts +2 -1
- package/dist/worker/rebase.js +93 -36
- package/dist/worker/task-runner.d.ts +4 -1
- package/dist/worker/task-runner.js +77 -48
- package/dist/worker/unpublishable-branches.d.ts +34 -0
- package/dist/worker/unpublishable-branches.js +182 -0
- package/package.json +2 -1
package/dist/editor/Editor.js
CHANGED
|
@@ -6,7 +6,7 @@ import { IconChevronDown, IconChevronUp, IconDots, IconFolderPlus, IconPlus, } f
|
|
|
6
6
|
import { useHotkeys, useMediaQuery } from '@mantine/hooks';
|
|
7
7
|
import { notifications } from '@mantine/notifications';
|
|
8
8
|
const noop = () => { };
|
|
9
|
-
import { EntryNavigator } from './EntryNavigator.js';
|
|
9
|
+
import { EntryNavigator, } from './EntryNavigator.js';
|
|
10
10
|
import { FormRenderer } from './FormRenderer.js';
|
|
11
11
|
import { PreviewPane } from './PreviewPane.js';
|
|
12
12
|
import { displayPreviewPath, previewPageKey } from './preview-path.js';
|
|
@@ -29,6 +29,9 @@ import { fieldBreadcrumb } from './field-breadcrumb.js';
|
|
|
29
29
|
import { buildPreviewRoute, buildPreviewSrc, buildCollectionLabels, entryStatusLabel, entryTypeLabel, buildBreadcrumbSegments, entryNameSource, } from './editor-utils.js';
|
|
30
30
|
import { useEditorLayout, useDraftManager, useEntryManager, useGroupManager, usePermissionManager, useCommentSystem, useBranchManager, useUserContext, useSchemaManager, } from './hooks/index.js';
|
|
31
31
|
import { useBranchActions } from './hooks/useBranchActions.js';
|
|
32
|
+
import { branchAddress, displayBranchName } from './branch-names.js';
|
|
33
|
+
import { useBranchChangesData, useEntryChangeData } from './hooks/useBranchChangesData.js';
|
|
34
|
+
import { LazyEntryChangesDrawer } from './components/LazyEntryChangesDrawer.js';
|
|
32
35
|
import { CONTENT_PANEL_WIDTH_MAX, CONTENT_PANEL_WIDTH_MIN, editorLayoutStorageKey, } from './hooks/useEditorLayout.js';
|
|
33
36
|
import { EntrySchemaUnavailableError } from './hooks/useEntryManager.js';
|
|
34
37
|
import { useEntryLinkResolution } from './hooks/useEntryLinkResolution.js';
|
|
@@ -64,7 +67,6 @@ import { findFieldTarget, focusFieldTarget } from './field-target.js';
|
|
|
64
67
|
import { UndoToastMessage } from './components/UndoToastMessage.js';
|
|
65
68
|
import { getNotificationDuration } from './utils/env.js';
|
|
66
69
|
import { MayHaveLandedError } from './unanswered.js';
|
|
67
|
-
import { sanitizeBranchName } from '../paths/branch-name.js';
|
|
68
70
|
// Stable fallbacks, so reference resolution's memo and effect don't re-run on every render
|
|
69
71
|
// while no entry is open.
|
|
70
72
|
const EMPTY_SCHEMA = [];
|
|
@@ -165,7 +167,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
165
167
|
// Reloads the open branch's comments after a request for changes, whose note is a comment; the
|
|
166
168
|
// comment system is built further down.
|
|
167
169
|
const reloadCommentsRef = useRef(() => { });
|
|
168
|
-
const { branchName: branchNameState, setBranchName, branchSummaries, currentBranch, branchesLoadFailed, branchesLoaded, addCreatedBranch, handleSubmit, handleWithdraw, handleApprove, handleRequestChanges, handleDelete, handleReloadBranchData, loadBranches, } = useBranchManager({
|
|
170
|
+
const { branchName: branchNameState, setBranchName, branches, branchSummaries, currentBranch, currentAddress, listedBranch, namesBranch, urlBranchName, branchesLoadFailed, branchesLoaded, addCreatedBranch, handleSubmit, handleWithdraw, handleApprove, handleRequestChanges, handleDelete, handleReloadBranchData, loadBranches, } = useBranchManager({
|
|
169
171
|
initialBranch: branchName,
|
|
170
172
|
operatingMode,
|
|
171
173
|
setBusy: setBranchesLoading,
|
|
@@ -187,6 +189,8 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
187
189
|
// of unlocked that invites a click the server rejects -- and the pane shows a
|
|
188
190
|
// loading state during that window anyway.
|
|
189
191
|
const branchContentLocked = currentBranch?.writeBlocked ?? true;
|
|
192
|
+
// What the page calls the open branch; everything keyed or sent per branch uses branchNameState.
|
|
193
|
+
const branchLabel = currentBranch ? displayBranchName(currentBranch) : branchNameState;
|
|
190
194
|
const branchLoading = !currentBranch && (branchesLoading || branchSummaries.length === 0);
|
|
191
195
|
// No lock reason is true yet while the list is unanswered; once it has failed, or answered
|
|
192
196
|
// without this branch, the lock says which.
|
|
@@ -216,6 +220,8 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
216
220
|
const branchNotAvailable = !!branchNameState && branchBarState === 'not-available';
|
|
217
221
|
// Set once the branch actions below exist; they depend on the drafts, which depend on the entries.
|
|
218
222
|
const switchBranchRef = useRef(async () => { });
|
|
223
|
+
// Set once the changes read exists; it waits on the panel state, declared after the entries.
|
|
224
|
+
const reloadChangesRef = useRef(() => { });
|
|
219
225
|
// 2. Entry manager (depends on branchNameState, owns selectedPath)
|
|
220
226
|
const { selectedPath, setSelectedPath, closeEntry, entries: entriesState, collections: collectionsFromApi, currentEntry, availableSchemas, entriesInitializing, entriesLoadFailed, navigatorOpen, setNavigatorOpen, refreshEntries, refreshContent, handleCreateEntry, renameEntry, loadEntry, readEntryValue, saveEntry, getEntryVersion, adoptEntryVersion, followMovedEntry, recreateEntry, createModalOpen, createModalCollection, createModalError, createModalCreating, createModalExistingSlugs, handleCreateModalSubmit, closeCreateModal, } = useEntryManager({
|
|
221
227
|
initialEntries: entries,
|
|
@@ -223,7 +229,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
223
229
|
branchName: branchNameState,
|
|
224
230
|
collections,
|
|
225
231
|
resolvePreviewSrc: (entry) => buildPreviewSrc(entry, {
|
|
226
|
-
branchName: branchNameState,
|
|
232
|
+
branchName: urlBranchName(branchNameState),
|
|
227
233
|
previewBaseByCollection,
|
|
228
234
|
previewPrefix,
|
|
229
235
|
contentRoot,
|
|
@@ -232,7 +238,11 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
232
238
|
resolvePreviewRoute: (entry) => buildPreviewRoute(entry, { previewBaseByCollection, contentRoot }),
|
|
233
239
|
setBusy: setEntriesLoading,
|
|
234
240
|
branchBarState,
|
|
235
|
-
switchBranch: (branch) => switchBranchRef.current(branch),
|
|
241
|
+
switchBranch: (branch, superseded) => switchBranchRef.current(branch, superseded),
|
|
242
|
+
// A short name in the URL is no switch to another branch.
|
|
243
|
+
sameBranch: namesBranch,
|
|
244
|
+
urlBranchName,
|
|
245
|
+
onContentRefresh: (branch) => reloadChangesRef.current(branch),
|
|
236
246
|
// To the entry menu, which names the entry opened; a dialog keeps focus.
|
|
237
247
|
onHistoryRestore: () => requestAnimationFrame(() => {
|
|
238
248
|
if (!document.activeElement?.closest('[role="dialog"]')) {
|
|
@@ -337,22 +347,30 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
337
347
|
// 4. Branch actions (depends on resolveUnsaved, setBranchName)
|
|
338
348
|
const { handleBranchChange, confirmCreate: confirmCreateBranch, handleCreateBranch, confirmOpen: branchConfirmOpen, } = useBranchActions({
|
|
339
349
|
branchName: branchNameState,
|
|
350
|
+
branchLabel,
|
|
340
351
|
setBranchName,
|
|
341
352
|
getUnsaved: resolveUnsaved,
|
|
342
353
|
onReloadBranches: () => loadBranches(),
|
|
343
354
|
onBranchCreated: addCreatedBranch,
|
|
355
|
+
urlBranchName,
|
|
356
|
+
sameBranch: namesBranch,
|
|
344
357
|
userId: userContext?.userId,
|
|
345
358
|
});
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
359
|
+
// Only a Back or Forward calls this: it switches to the listed branch, by an address that opens
|
|
360
|
+
// it alone, replacing the entry it arrived at. With no listing, the name is unchecked and could
|
|
361
|
+
// be either form, so the move is refused.
|
|
362
|
+
switchBranchRef.current = async (branch, superseded) => {
|
|
363
|
+
const listed = listedBranch(branch);
|
|
364
|
+
if (!listed) {
|
|
365
|
+
if (branches.length > 0) {
|
|
366
|
+
notifications.show({
|
|
367
|
+
message: `Branch “${branch}” no longer exists or you can no longer open it, so you're still on “${branchLabel}”.`,
|
|
368
|
+
color: 'yellow',
|
|
369
|
+
});
|
|
370
|
+
}
|
|
353
371
|
throw new Error(`Branch ${branch} is not listed`);
|
|
354
372
|
}
|
|
355
|
-
await handleBranchChange(
|
|
373
|
+
await handleBranchChange(branchAddress(branches, listed), { push: false, superseded });
|
|
356
374
|
};
|
|
357
375
|
// 5. Comment system (depends on branchNameState)
|
|
358
376
|
const { comments, getCommentUser, focusedFieldPath, highlightThreadId, commentsPanelOpen, setCommentsPanelOpen, handleAddComment, handleResolveThread, handleJumpToField, handleJumpToEntry, handleJumpToBranch, loadComments, } = useCommentSystem({
|
|
@@ -367,8 +385,8 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
367
385
|
onRevealForm: revealForm,
|
|
368
386
|
});
|
|
369
387
|
reloadCommentsRef.current = (branch) => {
|
|
370
|
-
if (
|
|
371
|
-
void loadComments(
|
|
388
|
+
if (branch === branchNameState)
|
|
389
|
+
void loadComments(branchNameState);
|
|
372
390
|
};
|
|
373
391
|
const { groupsData, groupsLoading, handleSaveGroups, handleSearchUsers, handleGetUserMetadata, handleSearchExternalGroups, } = useGroupManager({ isOpen: groupManagerOpen });
|
|
374
392
|
// The comments and branches responses name their people, without email; a privileged lookup
|
|
@@ -1186,7 +1204,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1186
1204
|
// Opened under a modal or drawer, it would take the keyboard from the one in use.
|
|
1187
1205
|
if (document.querySelector('[role="dialog"][aria-modal="true"]'))
|
|
1188
1206
|
return;
|
|
1189
|
-
|
|
1207
|
+
setEntryFilter('all');
|
|
1190
1208
|
const field = entryFilterRef.current;
|
|
1191
1209
|
if (field && document.activeElement === field)
|
|
1192
1210
|
field.select();
|
|
@@ -1211,14 +1229,115 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1211
1229
|
if (!contentPanelShown)
|
|
1212
1230
|
setContentReveal(undefined);
|
|
1213
1231
|
}, [contentPanelShown]);
|
|
1214
|
-
// The content panel's All / Changed filter. It stays as chosen, emptied by a save or
|
|
1215
|
-
// that Mod+P finds among all entries and another branch has its own
|
|
1216
|
-
const [
|
|
1217
|
-
useEffect(() =>
|
|
1232
|
+
// The content panel's All / Unsaved / Changed filter. It stays as chosen, emptied by a save or
|
|
1233
|
+
// not, except that Mod+P finds among all entries and another branch has its own sets: both reset it.
|
|
1234
|
+
const [entryFilter, setEntryFilter] = useState('all');
|
|
1235
|
+
useEffect(() => setEntryFilter('all'), [branchNameState]);
|
|
1218
1236
|
const showUnsaved = () => {
|
|
1219
|
-
|
|
1220
|
-
revealInContentPanel({ kind: '
|
|
1237
|
+
setEntryFilter('unsaved');
|
|
1238
|
+
revealInContentPanel({ kind: 'unsaved' });
|
|
1239
|
+
};
|
|
1240
|
+
// What the branch changes against its base, read while the panel shows so Changed has its count,
|
|
1241
|
+
// and while an entry is open so its header knows to offer "Show changes".
|
|
1242
|
+
// The protected base has nothing to compare. Changed keeps its place while the branch list loads,
|
|
1243
|
+
// so the control doesn't re-flow then, and reads once the branch is known not to be the base. A
|
|
1244
|
+
// list that failed, or lacks the branch, offers none, so a branch just created can lack Changed
|
|
1245
|
+
// until the list catches up.
|
|
1246
|
+
const changesOffered = branchPending || currentBranch?.isProtected === false;
|
|
1247
|
+
const changesAvailable = !!currentBranch && currentBranch.isProtected === false;
|
|
1248
|
+
const changesRead = (contentPanelShown || !!currentEntry) && changesAvailable;
|
|
1249
|
+
const branchChanges = useBranchChangesData(apiClient, branchNameState, changesRead);
|
|
1250
|
+
reloadChangesRef.current = (branch) => {
|
|
1251
|
+
if (branch === branchNameState)
|
|
1252
|
+
void branchChanges.reload();
|
|
1221
1253
|
};
|
|
1254
|
+
// Submit, withdraw and a review move the branch's status, which no entries refresh follows.
|
|
1255
|
+
const changesStatusRef = useRef({ branch: branchNameState, status: currentBranch?.status });
|
|
1256
|
+
useEffect(() => {
|
|
1257
|
+
const seen = changesStatusRef.current;
|
|
1258
|
+
changesStatusRef.current = { branch: branchNameState, status: currentBranch?.status };
|
|
1259
|
+
if (seen.branch === branchNameState && seen.status && seen.status !== currentBranch?.status) {
|
|
1260
|
+
void branchChanges.reload();
|
|
1261
|
+
}
|
|
1262
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- `reload` is fresh each branch; only a status move matters
|
|
1263
|
+
}, [branchNameState, currentBranch?.status]);
|
|
1264
|
+
// "Show changes": the entry compared stays set after a close, so the closing drawer keeps it.
|
|
1265
|
+
const [changesSubject, setChangesSubject] = useState();
|
|
1266
|
+
const [changesOpen, setChangesOpen] = useState(false);
|
|
1267
|
+
// Its comparison stays keyed until the close transition ends, so the closing drawer keeps it.
|
|
1268
|
+
const [changesShown, setChangesShown] = useState(false);
|
|
1269
|
+
// Who opened the drawer, and the control that stands in for it once it is gone.
|
|
1270
|
+
const changesOpenerRef = useRef({
|
|
1271
|
+
el: null,
|
|
1272
|
+
standIn: () => null,
|
|
1273
|
+
});
|
|
1274
|
+
const showChangesRef = useRef(null);
|
|
1275
|
+
useEffect(() => {
|
|
1276
|
+
setChangesOpen(false);
|
|
1277
|
+
setChangesShown(false);
|
|
1278
|
+
}, [branchNameState]);
|
|
1279
|
+
const entryChange = useEntryChangeData(apiClient, branchNameState, (changesOpen || changesShown) && changesSubject
|
|
1280
|
+
? {
|
|
1281
|
+
logicalPath: changesSubject.logicalPath,
|
|
1282
|
+
from: changesSubject.from,
|
|
1283
|
+
id: changesSubject.contentId,
|
|
1284
|
+
}
|
|
1285
|
+
: undefined);
|
|
1286
|
+
const openEntryChange = currentEntry &&
|
|
1287
|
+
branchChanges.data?.entries.find((e) => e.status !== 'deleted' && e.logicalPath === currentEntry.path);
|
|
1288
|
+
const referenceTitles = useMemo(() => new Map(entriesState.map((e) => [e.contentId, e.title ?? e.label])), [entriesState]);
|
|
1289
|
+
const referenceTitle = useMemo(() => (id) => referenceTitles.get(id), [referenceTitles]);
|
|
1290
|
+
/** A logical path in the labels the content tree shows: "Blog › hello". */
|
|
1291
|
+
const humanEntryPath = (logicalPath) => {
|
|
1292
|
+
const parts = logicalPath.split('/').filter(Boolean);
|
|
1293
|
+
const collections = parts
|
|
1294
|
+
.slice(1, -1)
|
|
1295
|
+
.map((_, i) => collectionLabels.get(parts.slice(0, i + 2).join('/')))
|
|
1296
|
+
.filter((label) => !!label);
|
|
1297
|
+
return [...collections, parts[parts.length - 1]].join(' › ');
|
|
1298
|
+
};
|
|
1299
|
+
const showEntryChanges = (entry, title, from) => {
|
|
1300
|
+
// The header's button gives way to the entry menu's trigger; a deleted entry's row, gone with
|
|
1301
|
+
// its panel, to the rail's Content button, never to another entry's controls.
|
|
1302
|
+
changesOpenerRef.current = {
|
|
1303
|
+
el: rememberOpener(),
|
|
1304
|
+
standIn: from === 'header'
|
|
1305
|
+
? () => showChangesRef.current ?? navigatorToggleRef.current ?? contentToggleRef.current
|
|
1306
|
+
: () => contentToggleRef.current,
|
|
1307
|
+
};
|
|
1308
|
+
// A deleted entry has no row to read a schema from, so it borrows its type's from a sibling.
|
|
1309
|
+
const fields = entriesState.find((e) => e.path === entry.logicalPath)?.schema ??
|
|
1310
|
+
entriesState.find((e) => e.collectionPath === entry.collectionPath && e.entryType === entry.entryType)?.schema ??
|
|
1311
|
+
EMPTY_SCHEMA;
|
|
1312
|
+
setChangesSubject({
|
|
1313
|
+
logicalPath: entry.logicalPath,
|
|
1314
|
+
contentId: entry.contentId,
|
|
1315
|
+
status: entry.status,
|
|
1316
|
+
title,
|
|
1317
|
+
fields,
|
|
1318
|
+
...(entry.previousLogicalPath && {
|
|
1319
|
+
from: entry.previousLogicalPath,
|
|
1320
|
+
movedFromLabel: humanEntryPath(entry.previousLogicalPath),
|
|
1321
|
+
}),
|
|
1322
|
+
});
|
|
1323
|
+
setChangesOpen(true);
|
|
1324
|
+
setChangesShown(true);
|
|
1325
|
+
};
|
|
1326
|
+
const changesReturnTarget = () => returnTarget(changesOpenerRef.current.el, changesOpenerRef.current.standIn);
|
|
1327
|
+
// Where a refused comparison can still be looked at: the entry itself, if the branch has it and
|
|
1328
|
+
// it isn't the one open, and the pull request, for those who review it.
|
|
1329
|
+
const changesOpenEntry = changesSubject &&
|
|
1330
|
+
changesSubject.status !== 'deleted' &&
|
|
1331
|
+
changesSubject.logicalPath !== currentEntry?.path &&
|
|
1332
|
+
entriesState.some((e) => e.path === changesSubject.logicalPath)
|
|
1333
|
+
? () => {
|
|
1334
|
+
setChangesOpen(false);
|
|
1335
|
+
openEntry(changesSubject.logicalPath);
|
|
1336
|
+
}
|
|
1337
|
+
: undefined;
|
|
1338
|
+
const changesPullRequestUrl = isReviewer(userContext?.groups)
|
|
1339
|
+
? currentBranch?.pullRequestUrl
|
|
1340
|
+
: undefined;
|
|
1222
1341
|
const runSaveAll = () => {
|
|
1223
1342
|
void handleSaveAll()
|
|
1224
1343
|
.then((result) => {
|
|
@@ -1459,14 +1578,22 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1459
1578
|
branchMissing,
|
|
1460
1579
|
reviewing: branchReviewing,
|
|
1461
1580
|
pullRequestClosed: branchPullRequestClosed,
|
|
1462
|
-
}), dirtyPaths: dirtyPaths, reveal: contentReveal, onRevealed: () => setContentReveal(undefined),
|
|
1581
|
+
}), 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
|
|
1582
|
+
? {
|
|
1583
|
+
base: branchChanges.data?.base ?? currentBranch?.baseBranch ?? 'the base branch',
|
|
1584
|
+
data: branchChanges.data,
|
|
1585
|
+
error: branchChanges.error,
|
|
1586
|
+
onRetry: () => void branchChanges.reload(),
|
|
1587
|
+
retrying: branchChanges.isValidating,
|
|
1588
|
+
}
|
|
1589
|
+
: undefined, unsavedCount: modifiedCount, unsavedEntries: editedFiles, entryListFailed: entriesLoadFailed && entriesState.length === 0, branchNotAvailable: branchShowsNotAvailable, onSwitchBranch: () => openBranchManager(), filterRef: entryFilterRef, filterShortcut: findShortcut, onFilterEnter: contentPanelTransient
|
|
1463
1590
|
? (path) => {
|
|
1464
1591
|
openEntry(path);
|
|
1465
1592
|
setContentPanelTransient(false);
|
|
1466
1593
|
setFormFocusPath(path);
|
|
1467
1594
|
}
|
|
1468
1595
|
: undefined, onFilterEscape: contentPanelTransient ? () => setContentPanelTransient(false) : undefined, onRowMenuOpen: keepContentPanelOpen }) }) }));
|
|
1469
|
-
return (_jsx(CanopyCMSProvider, { ...(themeOptions ?? {}), children: _jsx(AssetContextProvider, { basePath: basePath, children: _jsx(EditorShell, { topBar: _jsx(EditorHeader, { branchName: branchNameState, branchStatus: currentBranch?.status, busy: busy, modifiedCount: modifiedCount, onShowUnsaved: showUnsaved, draftStorageFailed: draftStorageFailed, userContext: userContext, branchCreatedBy: currentBranch?.createdBy, branchPullRequestState: currentBranch?.pullRequestState, branchAccess: currentBranch?.access,
|
|
1596
|
+
return (_jsx(CanopyCMSProvider, { ...(themeOptions ?? {}), children: _jsx(AssetContextProvider, { basePath: basePath, children: _jsx(EditorShell, { topBar: _jsx(EditorHeader, { branchName: branchNameState, branchLabel: branchLabel, branchStatus: currentBranch?.status, busy: busy, modifiedCount: modifiedCount, onShowUnsaved: showUnsaved, draftStorageFailed: draftStorageFailed, userContext: userContext, branchCreatedBy: currentBranch?.createdBy, branchPullRequestState: currentBranch?.pullRequestState, branchAccess: currentBranch?.access,
|
|
1470
1597
|
// Fail-closed for the same reason branchContentLocked above is:
|
|
1471
1598
|
// isProtected/writeBlocked gate real actions in EditorHeader and EntryHeader
|
|
1472
1599
|
// (hiding Submit, disabling Save and Save all), so a missing wire value must read as
|
|
@@ -1482,19 +1609,19 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1482
1609
|
// Save all finds each draft's entry in the list, so with none listed it would save nothing.
|
|
1483
1610
|
entryListFailed: entriesLoadFailed && entriesState.length === 0, onSubmit: (returnFocusTo) => {
|
|
1484
1611
|
if (branchNameState && !isSaveAllRunning()) {
|
|
1485
|
-
handleSubmit(
|
|
1612
|
+
handleSubmit(currentAddress, returnFocusTo).catch(console.error);
|
|
1486
1613
|
}
|
|
1487
1614
|
}, onWithdraw: (returnFocusTo) => {
|
|
1488
1615
|
if (branchNameState && !isSaveAllRunning()) {
|
|
1489
|
-
handleWithdraw(
|
|
1616
|
+
handleWithdraw(currentAddress, returnFocusTo).catch(console.error);
|
|
1490
1617
|
}
|
|
1491
1618
|
}, onApprove: (returnFocusTo) => {
|
|
1492
1619
|
if (branchNameState && !isSaveAllRunning()) {
|
|
1493
|
-
handleApprove(
|
|
1620
|
+
handleApprove(currentAddress, returnFocusTo).catch(console.error);
|
|
1494
1621
|
}
|
|
1495
1622
|
}, onRequestChanges: (returnFocusTo) => {
|
|
1496
1623
|
if (branchNameState && !isSaveAllRunning()) {
|
|
1497
|
-
handleRequestChanges(
|
|
1624
|
+
handleRequestChanges(currentAddress, returnFocusTo).catch(console.error);
|
|
1498
1625
|
}
|
|
1499
1626
|
} }), rail: _jsx(EditorRail, { contentPanelOpen: contentPanelShown, contentPanelId: CONTENT_PANEL_ID, onContentToggle: () => contentPanelTransient
|
|
1500
1627
|
? keepContentPanelOpen()
|
|
@@ -1507,7 +1634,9 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1507
1634
|
// True exactly while the form below shows "Setting up…" or "Loading content…".
|
|
1508
1635
|
entryLoading: !currentEntry && ((!branchNameState && busy) || entriesInitializing), branchName: branchNameState, busy: busy, breadcrumbSegments: breadcrumbSegments,
|
|
1509
1636
|
// A draft kept for an unavailable entry stays in storage but cannot be saved.
|
|
1510
|
-
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, onCollectionOpen: (path) => revealInContentPanel({ kind: 'collection', path }), onEntryReveal: () => currentEntry && revealInContentPanel({ kind: 'entry', path: currentEntry.path }), navigatorToggleRef: navigatorToggleRef,
|
|
1637
|
+
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, onCollectionOpen: (path) => revealInContentPanel({ kind: 'collection', path }), onEntryReveal: () => currentEntry && revealInContentPanel({ kind: 'entry', path: currentEntry.path }), navigatorToggleRef: navigatorToggleRef, onShowChanges: openEntryChange
|
|
1638
|
+
? () => showEntryChanges(openEntryChange, currentEntry?.label ?? currentEntry?.slug ?? '', 'header')
|
|
1639
|
+
: 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 &&
|
|
1511
1640
|
singleSaving.contentId === currentEntry?.contentId, savedSignal: lastSaved?.seq, savedHere: lastSaved?.branch === branchNameState &&
|
|
1512
1641
|
lastSaved.contentId === currentEntry?.contentId }), _jsx(Box, { style: { flex: '1 1 0%', minHeight: 0 }, children: _jsx(PaneLayout, { formAside: contentPanelDocked && layout === 'stacked' ? contentPanel : undefined, formOverlay: contentPanelDocked ? undefined : contentPanel, formInert: contentOverlayShown, previewMaximized: previewShownMaximized,
|
|
1513
1642
|
// Sizing the pane, by pointer or key, gives the page the pane's width.
|
|
@@ -1558,24 +1687,27 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1558
1687
|
}, onBeforeCreate: confirmCreateBranch, onCreate: handleCreateBranch,
|
|
1559
1688
|
// A Save all mid-run on the open branch would be submitted partway through.
|
|
1560
1689
|
onSubmit: (name) => {
|
|
1561
|
-
if (name
|
|
1690
|
+
if (namesBranch(name, branchNameState) && isSaveAllRunning())
|
|
1562
1691
|
return;
|
|
1563
1692
|
handleSubmit(name).catch((err) => console.error(err));
|
|
1564
1693
|
}, onWithdraw: (name) => {
|
|
1565
|
-
if (name
|
|
1694
|
+
if (namesBranch(name, branchNameState) && isSaveAllRunning())
|
|
1566
1695
|
return;
|
|
1567
1696
|
handleWithdraw(name).catch((err) => console.error(err));
|
|
1568
1697
|
}, onApprove: (name) => {
|
|
1569
|
-
if (name
|
|
1698
|
+
if (namesBranch(name, branchNameState) && isSaveAllRunning())
|
|
1570
1699
|
return;
|
|
1571
1700
|
handleApprove(name).catch((err) => console.error(err));
|
|
1572
1701
|
}, onRequestChanges: (name) => {
|
|
1573
|
-
if (name
|
|
1702
|
+
if (namesBranch(name, branchNameState) && isSaveAllRunning())
|
|
1574
1703
|
return;
|
|
1575
1704
|
handleRequestChanges(name).catch((err) => console.error(err));
|
|
1576
1705
|
}, onDelete: (name) => {
|
|
1577
1706
|
handleDelete(name).catch((err) => console.error(err));
|
|
1578
|
-
}, 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) }),
|
|
1707
|
+
}, 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: () => setChangesShown(false), returnFocusTo: changesReturnTarget, base: branchChanges.data?.base ?? currentBranch?.baseBranch ?? 'the base branch', subject: {
|
|
1708
|
+
...changesSubject,
|
|
1709
|
+
unsavedEdits: changesSubject.logicalPath === currentEntry?.path && isSelectedDirty(),
|
|
1710
|
+
}, state: entryChange, referenceTitle: referenceTitle, onOpenEntry: changesOpenEntry, pullRequestUrl: changesPullRequestUrl })), 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
|
|
1579
1711
|
? (path, entryType) => addEntryType(path, entryType)
|
|
1580
1712
|
: undefined, onUpdateEntryType: editingCollection
|
|
1581
1713
|
? (path, name, updates) => updateEntryType(path, name, updates)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { ContentId, LogicalPath } from '../paths/types.js';
|
|
3
3
|
import { useTree } from '@mantine/core';
|
|
4
|
+
import type { BranchChangesData, BranchChangeEntry } from '../api/branch-changes.js';
|
|
5
|
+
import type { BranchChangesError } from './hooks/useBranchChangesData.js';
|
|
4
6
|
type TreeController = ReturnType<typeof useTree>;
|
|
5
7
|
export interface EntryNavItem {
|
|
6
8
|
path: LogicalPath;
|
|
@@ -77,21 +79,30 @@ export interface EntryNavigatorProps {
|
|
|
77
79
|
readOnlyReason?: string;
|
|
78
80
|
/** Entry paths with unsaved changes; each shows a dot, and so does every collection holding one. */
|
|
79
81
|
dirtyPaths?: ReadonlySet<string>;
|
|
80
|
-
/** A row to bring into view, or (`
|
|
82
|
+
/** A row to bring into view, or (`unsaved`) the unsaved entries; `onRevealed` once done. */
|
|
81
83
|
reveal?: ContentPanelReveal;
|
|
82
84
|
/** The reveal is done, or its row is not in the tree. */
|
|
83
85
|
onRevealed?: () => void;
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
/**
|
|
87
|
+
* The All / Unsaved / Changed control shows only with this. Unsaved lists the entries in
|
|
88
|
+
* `dirtyPaths`; Changed, those in `branchChanges`.
|
|
89
|
+
*/
|
|
90
|
+
onEntryFilterChange?: (filter: EntryFilter) => void;
|
|
91
|
+
entryFilter?: EntryFilter;
|
|
92
|
+
/** What the branch changes against its base. Without it the control offers no Changed. */
|
|
93
|
+
branchChanges?: BranchChangesView;
|
|
94
|
+
/** Lists the unsaved entries, as the top bar's count does; else Unsaved is chosen here. */
|
|
95
|
+
onShowUnsaved?: () => void;
|
|
96
|
+
/** Opens a deleted entry's changes, read-only; without it its row is plain text. */
|
|
97
|
+
onShowDeletedChanges?: (entry: BranchChangeEntry, label: string) => void;
|
|
87
98
|
/** Entries with unsaved changes, counting drafts whose entries the tree doesn't list yet. */
|
|
88
99
|
unsavedCount?: number;
|
|
89
|
-
/** The named unsaved entries;
|
|
100
|
+
/** The named unsaved entries; Unsaved offers any the tree has no row for beneath it. */
|
|
90
101
|
unsavedEntries?: ReadonlyArray<{
|
|
91
102
|
path: string;
|
|
92
103
|
label: string;
|
|
93
104
|
}>;
|
|
94
|
-
/** The branch's entry list failed to load, so
|
|
105
|
+
/** The branch's entry list failed to load, so Unsaved can't name what is unsaved. */
|
|
95
106
|
entryListFailed?: boolean;
|
|
96
107
|
/** The bar's "Not available" row: an empty tree says so, with `onSwitchBranch`, not "No content". */
|
|
97
108
|
branchNotAvailable?: boolean;
|
|
@@ -109,17 +120,30 @@ export interface EntryNavigatorProps {
|
|
|
109
120
|
};
|
|
110
121
|
}
|
|
111
122
|
/**
|
|
112
|
-
* A collection or entry reveal clears the query, switches
|
|
113
|
-
* ancestors (and a collection itself) and focuses the row.
|
|
114
|
-
* only, for
|
|
123
|
+
* A collection or entry reveal clears the query, switches the filter back to All, expands the
|
|
124
|
+
* row's ancestors (and a collection itself) and focuses the row. An `unsaved` reveal clears the
|
|
125
|
+
* query only, for Unsaved set alongside it. `seq` makes a repeat ask a new one.
|
|
115
126
|
*/
|
|
116
127
|
export type ContentPanelReveal = {
|
|
117
128
|
kind: 'collection' | 'entry';
|
|
118
129
|
path: string;
|
|
119
130
|
seq: number;
|
|
120
131
|
} | {
|
|
121
|
-
kind: '
|
|
132
|
+
kind: 'unsaved';
|
|
122
133
|
seq: number;
|
|
123
134
|
};
|
|
135
|
+
/** Which entries the Content panel lists: all, those with unsaved edits, or those differing from the base. */
|
|
136
|
+
export type EntryFilter = 'all' | 'unsaved' | 'changed';
|
|
137
|
+
/** The branch's changes against its base, as the Changed filter shows them. */
|
|
138
|
+
export interface BranchChangesView {
|
|
139
|
+
/** The base branch's name, for the copy. */
|
|
140
|
+
base: string;
|
|
141
|
+
/** The last answer, kept while a reload or its error shows. */
|
|
142
|
+
data?: BranchChangesData;
|
|
143
|
+
error?: BranchChangesError;
|
|
144
|
+
onRetry: () => void;
|
|
145
|
+
/** A reload is in flight, so Retry shows it is working. */
|
|
146
|
+
retrying?: boolean;
|
|
147
|
+
}
|
|
124
148
|
export declare const EntryNavigator: React.FC<EntryNavigatorProps>;
|
|
125
149
|
export default EntryNavigator;
|