canopycms 0.7.0 → 0.8.0
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 +201 -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-workspace.js +22 -15
- package/dist/cli/cli.js +1060 -172
- package/dist/cli/generate-ai-content.js +732 -101
- 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.js +23 -16
- package/dist/context.js +8 -7
- 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/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 +47 -1
- package/dist/worker/github-gateway-http.js +2 -2
- package/dist/worker/github-gateway-wire.d.ts +40 -25
- 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
|
@@ -8,13 +8,15 @@ import { openConfirm } from '../utils/confirm-modal.js';
|
|
|
8
8
|
import { focusReturnProps } from '../utils/focus-return.js';
|
|
9
9
|
import { EDITOR_ACTIONS } from '../copy.js';
|
|
10
10
|
import { notifications } from '@mantine/notifications';
|
|
11
|
-
import { useApiClient } from '../context/index.js';
|
|
11
|
+
import { useApiClient, useBranchSegments } from '../context/index.js';
|
|
12
12
|
import { BRANCHES_KEY, fetchBranches, useBranchesData } from './useBranchesData.js';
|
|
13
13
|
// branch-name, NOT branch or the '../../paths' barrel: both of those pull
|
|
14
14
|
// node:fs/promises + node:path at the top level (path RESOLUTION helpers),
|
|
15
15
|
// which breaks adopters' production `next build` of the editor bundle.
|
|
16
16
|
// paths/branch-name.ts is the dependency-free home of sanitizeBranchName.
|
|
17
17
|
import { sanitizeBranchName } from '../../paths/branch-name.js';
|
|
18
|
+
import { exactBranchName, exactBranchSegment } from '../../paths/validation.js';
|
|
19
|
+
import { branchAddress, branchUrlName, displayBranchName, requestSegments, unlistedSegment, resolveListedBranch, } from '../branch-names.js';
|
|
18
20
|
import { RetryToastMessage } from '../components/RetryToastMessage.js';
|
|
19
21
|
import { isNonApiResponse } from '../../api/client.js';
|
|
20
22
|
import { isGatewayFailure, isUnanswered, UnansweredError } from '../unanswered.js';
|
|
@@ -112,7 +114,7 @@ const showSubmitConfirmation = (branchName, unsaved, onConfirm, onDismiss, retur
|
|
|
112
114
|
const leftOut = unsaved && unsaved.count > 0 ? unsaved : undefined;
|
|
113
115
|
openConfirm({
|
|
114
116
|
title: EDITOR_ACTIONS.submitForReview,
|
|
115
|
-
children: (_jsxs(Stack, { gap: "sm", children: [leftOut && (_jsxs(Alert, { color: "yellow", icon: _jsx(IconAlertTriangle, { size: 16 }), title: `${leftOut.count} ${leftOut.count === 1 ? "entry has unsaved changes that won't" : "entries have unsaved changes that won't"} be submitted`, "data-testid": "submit-unsaved-warning", children: [describeUnsaved(leftOut), " To include them, cancel and save first. If you submit now, those changes stay unsaved on this branch while it is in review."] })), _jsx(Text, { size: "sm", style: { whiteSpace: 'pre-line' }, children: `Submit
|
|
117
|
+
children: (_jsxs(Stack, { gap: "sm", children: [leftOut && (_jsxs(Alert, { color: "yellow", icon: _jsx(IconAlertTriangle, { size: 16 }), title: `${leftOut.count} ${leftOut.count === 1 ? "entry has unsaved changes that won't" : "entries have unsaved changes that won't"} be submitted`, "data-testid": "submit-unsaved-warning", children: [describeUnsaved(leftOut), " To include them, cancel and save first. If you submit now, those changes stay unsaved on this branch while it is in review."] })), _jsx(Text, { size: "sm", style: { whiteSpace: 'pre-line' }, children: `Submit “${branchName}” for review?\n\nThis will:\n• Create a pull request for review\n• Change the branch status to “In review”\n• Notify reviewers of pending changes` })] })),
|
|
116
118
|
labels: {
|
|
117
119
|
confirm: leftOut ? 'Submit anyway' : EDITOR_ACTIONS.submitForReview,
|
|
118
120
|
cancel: 'Cancel',
|
|
@@ -143,12 +145,12 @@ export function withdrawPullRequestBullet(pr) {
|
|
|
143
145
|
const showWithdrawConfirmation = (branchName, pr, onConfirm, onDismiss, returnFocusTo) => {
|
|
144
146
|
const bullets = [
|
|
145
147
|
withdrawPullRequestBullet(pr),
|
|
146
|
-
'Change the branch status back to
|
|
148
|
+
'Change the branch status back to “editing”',
|
|
147
149
|
'Remove from review queue',
|
|
148
150
|
].filter((b) => b !== undefined);
|
|
149
151
|
openConfirm({
|
|
150
152
|
title: EDITOR_ACTIONS.withdrawFromReview,
|
|
151
|
-
children: (_jsx(Text, { size: "sm", style: { whiteSpace: 'pre-line' }, children: `Withdraw
|
|
153
|
+
children: (_jsx(Text, { size: "sm", style: { whiteSpace: 'pre-line' }, children: `Withdraw “${branchName}” from review?\n\nThis will:\n${bullets.map((b) => `• ${b}`).join('\n')}` })),
|
|
152
154
|
labels: { confirm: EDITOR_ACTIONS.withdrawFromReview, cancel: 'Cancel' },
|
|
153
155
|
confirmProps: { color: 'orange', 'data-testid': 'confirm-withdraw-branch' },
|
|
154
156
|
...confirmModalHandlers(onConfirm, onDismiss),
|
|
@@ -201,7 +203,7 @@ const showDeleteConfirmation = (branchName, pr, onConfirm, onDismiss) => {
|
|
|
201
203
|
].filter((b) => b !== undefined);
|
|
202
204
|
openConfirm({
|
|
203
205
|
title: 'Delete branch',
|
|
204
|
-
children: (_jsx(Text, { size: "sm", style: { whiteSpace: 'pre-line' }, children: `Delete
|
|
206
|
+
children: (_jsx(Text, { size: "sm", style: { whiteSpace: 'pre-line' }, children: `Delete “${branchName}”?\n\nThis will:\n${bullets.map((b) => `• ${b}`).join('\n')}\n\nThis cannot be undone.` })),
|
|
205
207
|
labels: { confirm: 'Delete branch', cancel: 'Cancel' },
|
|
206
208
|
// data-testid is load-bearing, not decoration: the e2e helper
|
|
207
209
|
// (apps/test-app/e2e/fixtures/branch-page.ts's deleteBranch) already
|
|
@@ -227,6 +229,15 @@ function mergePendingBranches(listed, pending) {
|
|
|
227
229
|
const missing = pending.filter((p) => !listedNames.has(p.branch.name));
|
|
228
230
|
return missing.length === 0 ? listed : [...listed, ...missing.map((p) => p.branch)];
|
|
229
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* What a request sends for `name`, a full name the listing no longer holds: the exact form when a
|
|
234
|
+
* listed short name is `name`, which would open that namespace branch instead, else as unlisted.
|
|
235
|
+
*/
|
|
236
|
+
function unlistedName(listed, name) {
|
|
237
|
+
return listed.some((b) => b.name !== name && displayBranchName(b) === name)
|
|
238
|
+
? exactBranchSegment(name)
|
|
239
|
+
: unlistedSegment(name);
|
|
240
|
+
}
|
|
230
241
|
/**
|
|
231
242
|
* Applies one received listing to the pending branches: drops those it arrived
|
|
232
243
|
* past the grace window for, and takes the listing's copy of the rest. Written
|
|
@@ -258,10 +269,31 @@ function reconcilePendingBranches(pending, listed, receivedAt) {
|
|
|
258
269
|
export function useBranchManager(options) {
|
|
259
270
|
const apiClient = useApiClient();
|
|
260
271
|
const { cache, mutate: globalMutate } = useSWRConfig();
|
|
261
|
-
|
|
272
|
+
// The open branch. Once `exact`, `name` is the listed branch's full name, matched only by that
|
|
273
|
+
// name: a listing that changes which branch a short name opens never moves it. Until then
|
|
274
|
+
// `name` is what a URL, link, history entry or card gave, which `resolveListedBranch` resolves
|
|
275
|
+
// once a listing holds it.
|
|
276
|
+
const [open, setOpen] = useState({ name: options.initialBranch, exact: false });
|
|
277
|
+
const setBranchName = useCallback((name) => setOpen({ name, exact: false }), []);
|
|
278
|
+
const openExactly = useCallback((name) => setOpen({ name, exact: true }), []);
|
|
262
279
|
const { data: branchesData, error: branchesError, isValidating: branchesIsValidating, } = useBranchesData(apiClient);
|
|
263
280
|
const [pendingBranches, setPendingBranches] = useState([]);
|
|
264
281
|
const branches = useMemo(() => mergePendingBranches(branchesData?.branches ?? [], pendingBranches), [branchesData, pendingBranches]);
|
|
282
|
+
const arriving = open.exact ? undefined : resolveListedBranch(branches, open.name);
|
|
283
|
+
const branchName = arriving?.name ?? open.name;
|
|
284
|
+
const resolved = open.exact || !!arriving;
|
|
285
|
+
const currentBranch = open.exact ? branches.find((b) => b.name === open.name) : arriving;
|
|
286
|
+
const currentBranchName = currentBranch?.name;
|
|
287
|
+
// A listed branch by `requestSegments`; the open branch, once the listing drops it, as
|
|
288
|
+
// `unlistedName` does; any other name by the client's own lookup (`unlistedSegment`).
|
|
289
|
+
const segments = useMemo(() => {
|
|
290
|
+
const table = requestSegments(branches);
|
|
291
|
+
if (resolved && branchName && !table.has(branchName)) {
|
|
292
|
+
table.set(branchName, unlistedName(branches, branchName));
|
|
293
|
+
}
|
|
294
|
+
return table;
|
|
295
|
+
}, [branches, resolved, branchName]);
|
|
296
|
+
useBranchSegments(segments);
|
|
265
297
|
const addCreatedBranch = useCallback((branch) => {
|
|
266
298
|
setPendingBranches((prev) => [
|
|
267
299
|
...prev.filter((p) => p.branch.name !== branch.name),
|
|
@@ -296,10 +328,9 @@ export function useBranchManager(options) {
|
|
|
296
328
|
}, [branchesData]);
|
|
297
329
|
// Adopt the server's default branch once data arrives, if nothing pinned one.
|
|
298
330
|
useEffect(() => {
|
|
299
|
-
if (!branchName && branchesData?.defaultBranch)
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}, [branchesData, branchName]);
|
|
331
|
+
if (!branchName && branchesData?.defaultBranch)
|
|
332
|
+
openExactly(branchesData.defaultBranch);
|
|
333
|
+
}, [branchesData, branchName, openExactly]);
|
|
303
334
|
// One toast, replaced on each failure (a shown id ignores `show`); sticky because the editor
|
|
304
335
|
// cannot function without the branch list.
|
|
305
336
|
const showBranchesLoadFailed = (message) => {
|
|
@@ -334,18 +365,19 @@ export function useBranchManager(options) {
|
|
|
334
365
|
options.setBusy(branchesIsValidating);
|
|
335
366
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- options is a new object every Editor.tsx render; only setBusy (a stable setState) matters here
|
|
336
367
|
}, [branchesIsValidating, options.setBusy]);
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
368
|
+
// Resolved once, as it arrives: from here the full name is the branch, and drafts, comments and
|
|
369
|
+
// everything else keyed per branch see one name for it.
|
|
370
|
+
useEffect(() => {
|
|
371
|
+
if (!open.exact && arriving)
|
|
372
|
+
openExactly(arriving.name);
|
|
373
|
+
}, [open, arriving, openExactly]);
|
|
343
374
|
const branchSummaries = useMemo(() => {
|
|
344
375
|
return branches.map((b) => {
|
|
345
|
-
const branchComments = b.name ===
|
|
376
|
+
const branchComments = b.name === currentBranchName ? options.comments : [];
|
|
346
377
|
const unresolvedCount = branchComments.filter((t) => !t.resolved).length;
|
|
347
378
|
return {
|
|
348
379
|
name: b.name,
|
|
380
|
+
shortName: b.shortName,
|
|
349
381
|
status: b.status,
|
|
350
382
|
createdBy: b.createdBy,
|
|
351
383
|
updatedAt: b.updatedAt,
|
|
@@ -386,7 +418,7 @@ export function useBranchManager(options) {
|
|
|
386
418
|
submitBlocked: b.submitBlocked ?? true,
|
|
387
419
|
};
|
|
388
420
|
});
|
|
389
|
-
}, [branches,
|
|
421
|
+
}, [branches, currentBranchName, options.comments]);
|
|
390
422
|
// Explicit reload: always issues a fresh, independent fetch (raw call, not
|
|
391
423
|
// SWR's `mutate()` revalidate path) so callers requesting a reload right
|
|
392
424
|
// after mount aren't coalesced against the still-in-flight automatic load
|
|
@@ -416,11 +448,38 @@ export function useBranchManager(options) {
|
|
|
416
448
|
};
|
|
417
449
|
// The confirm dialogs' handlers run long after the render that opened them, so they read
|
|
418
450
|
// these at confirm time rather than from that render's closure.
|
|
419
|
-
const latest = useRef({ branchName, branches, branchesData });
|
|
420
|
-
latest.current = { branchName, branches, branchesData };
|
|
421
|
-
// A
|
|
422
|
-
//
|
|
423
|
-
|
|
451
|
+
const latest = useRef({ branchName, resolved, branches, branchesData });
|
|
452
|
+
latest.current = { branchName, resolved, branches, branchesData };
|
|
453
|
+
// A card, deep link or URL can carry a short name, an exact form or the raw form of a listed
|
|
454
|
+
// name; the actions resolve it once, the way an arriving name resolves, then key, look up and
|
|
455
|
+
// send the listed name.
|
|
456
|
+
const listedBranch = (name) => resolveListedBranch(latest.current.branches, name);
|
|
457
|
+
const listedName = (name) => listedBranch(name)?.name ?? sanitizeBranchName(name);
|
|
458
|
+
// What an action sends for `name`, resolved when it was asked for: the listing can drop the
|
|
459
|
+
// branch while its dialog is open.
|
|
460
|
+
const sendName = (name) => {
|
|
461
|
+
const listed = latest.current.branches;
|
|
462
|
+
return listed.some((b) => b.name === name) ? name : unlistedName(listed, name);
|
|
463
|
+
};
|
|
464
|
+
// The listed name of the open branch: the state itself once resolved.
|
|
465
|
+
const openListedName = () => latest.current.resolved ? latest.current.branchName : listedName(latest.current.branchName);
|
|
466
|
+
const namesBranch = (name, branch) => {
|
|
467
|
+
const found = listedBranch(name);
|
|
468
|
+
return found ? found.name === branch : sanitizeBranchName(name) === sanitizeBranchName(branch);
|
|
469
|
+
};
|
|
470
|
+
// What a dialog or toast calls the listed branch `name`; the keys, lookups and requests keep `name`.
|
|
471
|
+
const shownName = (name) => {
|
|
472
|
+
const found = latest.current.branches.find((b) => b.name === name);
|
|
473
|
+
return found ? displayBranchName(found) : name;
|
|
474
|
+
};
|
|
475
|
+
// The `?branch=` value for `name`, a full name or a card's address: the URL form of the branch
|
|
476
|
+
// listed by that full name, else `name` as given.
|
|
477
|
+
const urlBranchName = (name) => {
|
|
478
|
+
const listed = latest.current.branches;
|
|
479
|
+
const full = exactBranchName(name) ?? name;
|
|
480
|
+
const found = listed.find((b) => b.name === full);
|
|
481
|
+
return found ? branchUrlName(listed, found) : name;
|
|
482
|
+
};
|
|
424
483
|
const pullRequestOf = (name) => {
|
|
425
484
|
const b = latest.current.branches.find((x) => x.name === name);
|
|
426
485
|
return { pullRequestNumber: b?.pullRequestNumber, pullRequestState: b?.pullRequestState };
|
|
@@ -564,7 +623,7 @@ export function useBranchManager(options) {
|
|
|
564
623
|
return singleFlight('submit', branchNameToSubmit, async () => {
|
|
565
624
|
// Drafts belong to the open branch, so only its submit can leave edits behind. Without
|
|
566
625
|
// that check the dialog opens in this same tick.
|
|
567
|
-
const checking = branchNameToSubmit ===
|
|
626
|
+
const checking = branchNameToSubmit === openListedName()
|
|
568
627
|
? options.getUnsaved?.().catch((err) => {
|
|
569
628
|
console.error(err);
|
|
570
629
|
return undefined;
|
|
@@ -575,7 +634,7 @@ export function useBranchManager(options) {
|
|
|
575
634
|
options.setBusy(true);
|
|
576
635
|
const unsaved = checking && (await checking.finally(() => options.setBusy(false)));
|
|
577
636
|
return new Promise((resolve, reject) => {
|
|
578
|
-
showSubmitConfirmation(branchNameToSubmit, unsaved, async () => {
|
|
637
|
+
showSubmitConfirmation(shownName(branchNameToSubmit), unsaved, async () => {
|
|
579
638
|
// A listing that arrived while the dialog was open may show the branch already
|
|
580
639
|
// submitted; submitting it again would only fail.
|
|
581
640
|
const listed = latest.current.branches.find((b) => b.name === branchNameToSubmit);
|
|
@@ -589,13 +648,13 @@ export function useBranchManager(options) {
|
|
|
589
648
|
}
|
|
590
649
|
options.setBusy(true);
|
|
591
650
|
try {
|
|
592
|
-
const result = await apiClient.workflow.submit({ branch: branchNameToSubmit },
|
|
651
|
+
const result = await apiClient.workflow.submit({ branch: sendName(branchNameToSubmit) },
|
|
593
652
|
// The PR body links back here; the server checks the host (editor-url.ts).
|
|
594
653
|
{ editorUrl: `${window.location.origin}${window.location.pathname}` });
|
|
595
654
|
if (!result.ok)
|
|
596
655
|
throw actionError(result, 'Failed to submit branch');
|
|
597
656
|
const stamped = result.data?.stamped;
|
|
598
|
-
if (stamped && branchNameToSubmit ===
|
|
657
|
+
if (stamped && branchNameToSubmit === openListedName()) {
|
|
599
658
|
options.onStamped?.(stamped);
|
|
600
659
|
}
|
|
601
660
|
notifications.show({
|
|
@@ -623,11 +682,11 @@ export function useBranchManager(options) {
|
|
|
623
682
|
const handleWithdraw = (requested, returnFocusTo) => {
|
|
624
683
|
const branchNameToWithdraw = listedName(requested);
|
|
625
684
|
return singleFlight('withdraw', branchNameToWithdraw, () => new Promise((resolve, reject) => {
|
|
626
|
-
showWithdrawConfirmation(branchNameToWithdraw, pullRequestOf(branchNameToWithdraw), async () => {
|
|
685
|
+
showWithdrawConfirmation(shownName(branchNameToWithdraw), pullRequestOf(branchNameToWithdraw), async () => {
|
|
627
686
|
options.setBusy(true);
|
|
628
687
|
try {
|
|
629
688
|
const result = await apiClient.workflow.withdraw({
|
|
630
|
-
branch: branchNameToWithdraw,
|
|
689
|
+
branch: sendName(branchNameToWithdraw),
|
|
631
690
|
});
|
|
632
691
|
if (!result.ok)
|
|
633
692
|
throw actionError(result, 'Failed to withdraw branch');
|
|
@@ -649,11 +708,11 @@ export function useBranchManager(options) {
|
|
|
649
708
|
const handleApprove = (requested, returnFocusTo) => {
|
|
650
709
|
const branchNameToApprove = listedName(requested);
|
|
651
710
|
return singleFlight('approve', branchNameToApprove, () => new Promise((resolve, reject) => {
|
|
652
|
-
showApproveConfirmation(branchNameToApprove, pullRequestOf(branchNameToApprove), async () => {
|
|
711
|
+
showApproveConfirmation(shownName(branchNameToApprove), pullRequestOf(branchNameToApprove), async () => {
|
|
653
712
|
options.setBusy(true);
|
|
654
713
|
try {
|
|
655
714
|
const result = await apiClient.workflow.approve({
|
|
656
|
-
branch: branchNameToApprove,
|
|
715
|
+
branch: sendName(branchNameToApprove),
|
|
657
716
|
});
|
|
658
717
|
if (!result.ok)
|
|
659
718
|
throw actionError(result, 'Failed to approve branch', true);
|
|
@@ -666,7 +725,7 @@ export function useBranchManager(options) {
|
|
|
666
725
|
catch (err) {
|
|
667
726
|
await reportReviewFailure(err, 'approve', branchNameToApprove, {
|
|
668
727
|
fallback: 'Failed to approve branch',
|
|
669
|
-
label: `${EDITOR_ACTIONS.approve}
|
|
728
|
+
label: `${EDITOR_ACTIONS.approve} “${shownName(branchNameToApprove)}”`,
|
|
670
729
|
retry: () => void handleApprove(branchNameToApprove, returnFocusTo).catch(() => { }),
|
|
671
730
|
});
|
|
672
731
|
reject(err);
|
|
@@ -682,7 +741,7 @@ export function useBranchManager(options) {
|
|
|
682
741
|
// A draft is for the review cycle it was typed in; a branch already out of review starts clean.
|
|
683
742
|
dropDraftsOutOfReview(latest.current.branches.filter((b) => b.name === branchNameForChanges));
|
|
684
743
|
return singleFlight('request-changes', branchNameForChanges, () => new Promise((resolve) => {
|
|
685
|
-
showRequestChangesConfirmation(branchNameForChanges, pullRequestOf(branchNameForChanges), noteDrafts.current.get(branchNameForChanges) ?? '', (draft) => {
|
|
744
|
+
showRequestChangesConfirmation(shownName(branchNameForChanges), pullRequestOf(branchNameForChanges), noteDrafts.current.get(branchNameForChanges) ?? '', (draft) => {
|
|
686
745
|
if (draft)
|
|
687
746
|
noteDrafts.current.set(branchNameForChanges, draft);
|
|
688
747
|
else
|
|
@@ -690,7 +749,7 @@ export function useBranchManager(options) {
|
|
|
690
749
|
}, async (note) => {
|
|
691
750
|
options.setBusy(true);
|
|
692
751
|
try {
|
|
693
|
-
const result = await apiClient.workflow.requestChanges({ branch: branchNameForChanges }, { note: note.trim() || undefined });
|
|
752
|
+
const result = await apiClient.workflow.requestChanges({ branch: sendName(branchNameForChanges) }, { note: note.trim() || undefined });
|
|
694
753
|
if (!result.ok)
|
|
695
754
|
throw actionError(result, 'Failed to request changes', true);
|
|
696
755
|
noteDrafts.current.delete(branchNameForChanges);
|
|
@@ -714,7 +773,7 @@ export function useBranchManager(options) {
|
|
|
714
773
|
catch (err) {
|
|
715
774
|
await reportReviewFailure(err, 'request-changes', branchNameForChanges, {
|
|
716
775
|
fallback: 'Failed to request changes',
|
|
717
|
-
label: `${EDITOR_ACTIONS.requestChanges} on
|
|
776
|
+
label: `${EDITOR_ACTIONS.requestChanges} on “${shownName(branchNameForChanges)}”`,
|
|
718
777
|
note: note.trim() || undefined,
|
|
719
778
|
retry: () => void handleRequestChanges(branchNameForChanges, returnFocusTo).catch(() => { }),
|
|
720
779
|
check: REQUEST_CHANGES_CHECK,
|
|
@@ -733,32 +792,38 @@ export function useBranchManager(options) {
|
|
|
733
792
|
}, () => resolve(), returnFocusTo);
|
|
734
793
|
}));
|
|
735
794
|
};
|
|
736
|
-
/**
|
|
737
|
-
|
|
795
|
+
/**
|
|
796
|
+
* After a delete: drop it from the created-branch overlay, and leave it if it is open. A name
|
|
797
|
+
* still unresolved is resolved against the listing from before the delete (`listedBefore`),
|
|
798
|
+
* since the deleted branch is no longer listed to resolve against.
|
|
799
|
+
*/
|
|
800
|
+
const leaveDeletedBranch = (deleted, listedBefore, fresh) => {
|
|
738
801
|
forgetCreatedBranch(deleted);
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
802
|
+
const { branchName: openName, resolved: openResolved } = latest.current;
|
|
803
|
+
const openListed = openResolved
|
|
804
|
+
? openName
|
|
805
|
+
: (resolveListedBranch(listedBefore, openName)?.name ?? sanitizeBranchName(openName));
|
|
806
|
+
if (openListed === deleted) {
|
|
743
807
|
// Nothing is left to keep, so no unsaved-changes prompt. With no default known yet, the
|
|
744
808
|
// empty name lets the default-branch adoption effect pick it.
|
|
745
|
-
|
|
809
|
+
openExactly(fresh?.defaultBranch ?? latest.current.branchesData?.defaultBranch ?? '');
|
|
746
810
|
}
|
|
747
811
|
};
|
|
748
812
|
const handleDelete = (requested) => {
|
|
749
813
|
const branchNameToDelete = listedName(requested);
|
|
814
|
+
const listedBefore = latest.current.branches;
|
|
750
815
|
return singleFlight('delete', branchNameToDelete, () => new Promise((resolve) => {
|
|
751
|
-
showDeleteConfirmation(branchNameToDelete, pullRequestOf(branchNameToDelete), async () => {
|
|
816
|
+
showDeleteConfirmation(shownName(branchNameToDelete), pullRequestOf(branchNameToDelete), async () => {
|
|
752
817
|
options.setBusy(true);
|
|
753
818
|
try {
|
|
754
819
|
const result = await apiClient.branches.delete({
|
|
755
|
-
branch: branchNameToDelete,
|
|
820
|
+
branch: sendName(branchNameToDelete),
|
|
756
821
|
});
|
|
757
822
|
// The API's own 404: the branch is already gone, by an earlier unanswered delete or
|
|
758
823
|
// another user's.
|
|
759
824
|
if (!result.ok && result.status === 404 && !isNonApiResponse(result)) {
|
|
760
825
|
notifications.show({ message: 'Branch deleted', color: 'green' });
|
|
761
|
-
leaveDeletedBranch(branchNameToDelete, await reloadBranches());
|
|
826
|
+
leaveDeletedBranch(branchNameToDelete, listedBefore, await reloadBranches());
|
|
762
827
|
return;
|
|
763
828
|
}
|
|
764
829
|
if (!result.ok)
|
|
@@ -771,7 +836,7 @@ export function useBranchManager(options) {
|
|
|
771
836
|
autoClose: false,
|
|
772
837
|
}
|
|
773
838
|
: { message: 'Branch deleted', color: 'green' });
|
|
774
|
-
leaveDeletedBranch(branchNameToDelete, await reloadBranches());
|
|
839
|
+
leaveDeletedBranch(branchNameToDelete, listedBefore, await reloadBranches());
|
|
775
840
|
}
|
|
776
841
|
catch (err) {
|
|
777
842
|
if (!isUnanswered(err)) {
|
|
@@ -784,7 +849,7 @@ export function useBranchManager(options) {
|
|
|
784
849
|
const listed = mergePendingBranches(fresh?.branches ?? [], recent).some((b) => sanitizeBranchName(b.name) === sanitizeBranchName(branchNameToDelete));
|
|
785
850
|
if (fresh && !listed) {
|
|
786
851
|
notifications.show({ message: 'Branch deleted', color: 'green' });
|
|
787
|
-
leaveDeletedBranch(branchNameToDelete, fresh);
|
|
852
|
+
leaveDeletedBranch(branchNameToDelete, listedBefore, fresh);
|
|
788
853
|
}
|
|
789
854
|
else {
|
|
790
855
|
showOutcomeUnknown('Delete branch');
|
|
@@ -803,24 +868,47 @@ export function useBranchManager(options) {
|
|
|
803
868
|
};
|
|
804
869
|
// The editor's own page; a Back off it to a page of the host leaves that page's URL alone.
|
|
805
870
|
const editorPathRef = useRef(typeof window === 'undefined' ? '' : window.location.pathname);
|
|
871
|
+
const shownBranch = currentBranch ? branchUrlName(branches, currentBranch) : undefined;
|
|
872
|
+
// Each Back or Forward, which can arrive at an entry naming this branch in another form.
|
|
873
|
+
const [historyMoves, setHistoryMoves] = useState(0);
|
|
806
874
|
useEffect(() => {
|
|
807
875
|
if (typeof window === 'undefined')
|
|
808
876
|
return;
|
|
809
|
-
|
|
877
|
+
const moved = () => setHistoryMoves((n) => n + 1);
|
|
878
|
+
window.addEventListener('popstate', moved);
|
|
879
|
+
return () => window.removeEventListener('popstate', moved);
|
|
880
|
+
}, []);
|
|
881
|
+
const seenMovesRef = useRef(historyMoves);
|
|
882
|
+
// Written once the listing resolves the branch, so a link's own form stays until then; a form
|
|
883
|
+
// other than the shown one (the full name, a raw name) is replaced, never pushed. After a Back
|
|
884
|
+
// or Forward only a form of this branch is: one naming another is the entry manager's switch.
|
|
885
|
+
useEffect(() => {
|
|
886
|
+
const popped = seenMovesRef.current !== historyMoves;
|
|
887
|
+
seenMovesRef.current = historyMoves;
|
|
888
|
+
if (typeof window === 'undefined')
|
|
889
|
+
return;
|
|
890
|
+
if (!shownBranch || window.location.pathname !== editorPathRef.current)
|
|
810
891
|
return;
|
|
811
892
|
const url = new URL(window.location.href);
|
|
812
|
-
const
|
|
813
|
-
if (
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
893
|
+
const named = url.searchParams.get('branch');
|
|
894
|
+
if (named === shownBranch)
|
|
895
|
+
return;
|
|
896
|
+
if (popped && named !== null && listedBranch(named)?.name !== latest.current.branchName)
|
|
897
|
+
return;
|
|
898
|
+
url.searchParams.set('branch', shownBranch);
|
|
899
|
+
writeEditorUrl(url.toString(), false);
|
|
900
|
+
}, [shownBranch, historyMoves]);
|
|
818
901
|
return {
|
|
819
902
|
branchName,
|
|
820
903
|
setBranchName,
|
|
821
904
|
branches,
|
|
822
905
|
branchSummaries,
|
|
823
906
|
currentBranch,
|
|
907
|
+
currentAddress: currentBranch ? branchAddress(branches, currentBranch) : branchName,
|
|
908
|
+
listedBranch,
|
|
909
|
+
listedName,
|
|
910
|
+
namesBranch,
|
|
911
|
+
urlBranchName,
|
|
824
912
|
branchesLoadFailed: !!branchesError && !branchesData,
|
|
825
913
|
branchesLoaded: !!branchesData,
|
|
826
914
|
addCreatedBranch,
|
|
@@ -335,9 +335,13 @@ export function useDraftManager(options) {
|
|
|
335
335
|
// Tracks which storage key the current in-memory `drafts` were loaded for.
|
|
336
336
|
// Lags one render behind a branch switch (it only updates once `drafts`
|
|
337
337
|
// itself has been reset/restored for the new key), which is what lets the
|
|
338
|
-
// persist effect below detect and skip the stale write.
|
|
339
|
-
|
|
338
|
+
// persist effect below detect and skip the stale write. Null while `drafts` is
|
|
339
|
+
// still the empty set it started as, so a load stores nothing under its first key.
|
|
340
|
+
const initialDraftsRef = useRef(drafts);
|
|
341
|
+
const draftsStorageKeyRef = useRef(null);
|
|
340
342
|
useEffect(() => {
|
|
343
|
+
if (drafts === initialDraftsRef.current && Object.keys(drafts).length === 0)
|
|
344
|
+
return;
|
|
341
345
|
draftsStorageKeyRef.current = storageKey;
|
|
342
346
|
}, [drafts]);
|
|
343
347
|
// Keep the per-draft base versions in step with `drafts`.
|
|
@@ -42,10 +42,26 @@ export interface UseEntryManagerOptions {
|
|
|
42
42
|
setBusy: (busy: boolean) => void;
|
|
43
43
|
/** The bar's row: a failed load is toasted after 'loading' ends, and never on 'not-available'. */
|
|
44
44
|
branchBarState?: BranchBarStatus['value'];
|
|
45
|
-
/**
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Switches to the branch a Back or Forward reached; rejects when the switch does not happen,
|
|
47
|
+
* which it must not once `superseded()` holds: a later move has the URL.
|
|
48
|
+
*/
|
|
49
|
+
switchBranch?: (branch: string, superseded: () => boolean) => Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Whether `url`, a `?branch=` value, opens the branch `branchName` names, as a short name opens
|
|
52
|
+
* its namespace branch. A change of `branchName` from a branch whose URL form opens the next one
|
|
53
|
+
* is no switch: the open entry stays. Defaults to comparing sanitized forms.
|
|
54
|
+
*/
|
|
55
|
+
sameBranch?: (url: string, branchName: string) => boolean;
|
|
56
|
+
/** What `?branch=` carries for a branch (its short name); defaults to the name as given. */
|
|
57
|
+
urlBranchName?: (branch: string) => string;
|
|
47
58
|
/** Called when a Back or Forward has opened another entry. */
|
|
48
59
|
onHistoryRestore?: () => void;
|
|
60
|
+
/**
|
|
61
|
+
* Called as each explicit refresh starts: every save, create, rename, delete and schema change
|
|
62
|
+
* refreshes the entries, so what reads the same branch state reloads here.
|
|
63
|
+
*/
|
|
64
|
+
onContentRefresh?: (branch: string) => void;
|
|
49
65
|
}
|
|
50
66
|
export interface UseEntryManagerReturn {
|
|
51
67
|
selectedPath: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import useSWR, { useSWRConfig } from 'swr';
|
|
3
3
|
import { notifications } from '@mantine/notifications';
|
|
4
|
-
import { buildWritePayload, normalizeContentPayload, resolveHomeEntry } from '../editor-utils.js';
|
|
4
|
+
import { buildWritePayload, normalizeContentPayload, resolveHomeEntry, withPreviewBranch, } from '../editor-utils.js';
|
|
5
5
|
import { isDataOnlyFormat } from '../../utils/format.js';
|
|
6
6
|
import { withTitleAtPath } from '../../utils/title-field.js';
|
|
7
7
|
import { getErrorMessage } from '../../utils/error.js';
|
|
@@ -49,6 +49,8 @@ export class EntrySchemaUnavailableError extends Error {
|
|
|
49
49
|
this.name = 'EntrySchemaUnavailableError';
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
/** A legacy name and its sanitized form are one branch. */
|
|
53
|
+
const sameSanitizedName = (a, b) => sanitizeBranchName(a) === sanitizeBranchName(b);
|
|
52
54
|
// Module-level singletons for the derived-empty fallbacks. These must be
|
|
53
55
|
// referentially stable: `collectionByPath` and `currentEntry` memoize on them,
|
|
54
56
|
// and a fresh `[]` each render would invalidate both on every render while a
|
|
@@ -196,6 +198,11 @@ export function useEntryManager(options) {
|
|
|
196
198
|
// the user switches mid-flight, which is exactly when the check matters).
|
|
197
199
|
const currentBranchRef = useRef(options.branchName);
|
|
198
200
|
currentBranchRef.current = options.branchName;
|
|
201
|
+
const sameBranchRef = useRef(options.sameBranch ?? sameSanitizedName);
|
|
202
|
+
sameBranchRef.current = options.sameBranch ?? sameSanitizedName;
|
|
203
|
+
const urlBranchNameRef = useRef(options.urlBranchName);
|
|
204
|
+
urlBranchNameRef.current = options.urlBranchName;
|
|
205
|
+
const urlBranchOf = (branch) => urlBranchNameRef.current?.(branch) ?? branch;
|
|
199
206
|
// Read by the selection check, which runs only when the entries or the selection change.
|
|
200
207
|
const resolvePreviewRouteRef = useRef(options.resolvePreviewRoute);
|
|
201
208
|
resolvePreviewRouteRef.current = options.resolvePreviewRoute;
|
|
@@ -247,7 +254,16 @@ export function useEntryManager(options) {
|
|
|
247
254
|
walk(collectionsState);
|
|
248
255
|
return map;
|
|
249
256
|
}, [collectionsState]);
|
|
250
|
-
|
|
257
|
+
// The entries keep the URL form of their fetch's time, which the listing can change since; the
|
|
258
|
+
// frame keys on `src`, so its branch is the current form.
|
|
259
|
+
const previewBranch = urlBranchOf(options.branchName);
|
|
260
|
+
const currentEntry = useMemo(() => {
|
|
261
|
+
const entry = entriesState.find((e) => e.path === selectedPath);
|
|
262
|
+
const previewSrc = entry?.previewSrc && previewBranch
|
|
263
|
+
? withPreviewBranch(entry.previewSrc, previewBranch)
|
|
264
|
+
: entry?.previewSrc;
|
|
265
|
+
return entry && previewSrc !== entry.previewSrc ? { ...entry, previewSrc } : entry;
|
|
266
|
+
}, [entriesState, selectedPath, previewBranch]);
|
|
251
267
|
// Slugs already used within the collection currently open in the create
|
|
252
268
|
// modal. Collision is keyed on collectionPath + slug only (not entryType):
|
|
253
269
|
// ContentStore resolves an existing entry by slug alone within a
|
|
@@ -441,6 +457,7 @@ export function useEntryManager(options) {
|
|
|
441
457
|
const refreshContent = async (branch = options.branchName) => {
|
|
442
458
|
if (!branch)
|
|
443
459
|
return { entries: [], collections: [] };
|
|
460
|
+
options.onContentRefresh?.(branch);
|
|
444
461
|
const seq = claimRefreshSeq(branch);
|
|
445
462
|
const fetched = await fetchEntriesAndSchema(apiClient, branch, {
|
|
446
463
|
resolvePreviewSrc: options.resolvePreviewSrc,
|
|
@@ -597,18 +614,28 @@ export function useEntryManager(options) {
|
|
|
597
614
|
// Clear selection when branch changes (reactive pattern). Data loading
|
|
598
615
|
// itself is handled below by the SWR-backed fetch -- this effect only
|
|
599
616
|
// owns the "clear stale selection" side effect.
|
|
617
|
+
// The shown branch's URL form, refreshed while it stays shown: a full name can be another
|
|
618
|
+
// branch's short name, and only the URL form says which branch it is against a later listing.
|
|
619
|
+
const shownBranchRef = useRef({ name: options.branchName, url: urlBranchOf(options.branchName) });
|
|
620
|
+
if (shownBranchRef.current.name === options.branchName) {
|
|
621
|
+
shownBranchRef.current.url = urlBranchOf(options.branchName);
|
|
622
|
+
}
|
|
600
623
|
useEffect(() => {
|
|
601
624
|
if (!options.branchName)
|
|
602
625
|
return;
|
|
626
|
+
const previous = shownBranchRef.current.url;
|
|
627
|
+
shownBranchRef.current = { name: options.branchName, url: urlBranchOf(options.branchName) };
|
|
603
628
|
// On initial mount, preserve the initial selection from URL
|
|
604
|
-
// On
|
|
629
|
+
// On a later switch to another branch, clear selection
|
|
605
630
|
if (isInitialMount.current) {
|
|
606
631
|
isInitialMount.current = false;
|
|
607
632
|
}
|
|
608
|
-
else {
|
|
633
|
+
else if (!sameBranchRef.current(previous, options.branchName)) {
|
|
609
634
|
const restored = entryAfterBranchChangeRef.current;
|
|
610
635
|
entryAfterBranchChangeRef.current = undefined;
|
|
611
|
-
const entry = restored
|
|
636
|
+
const entry = restored && sameBranchRef.current(restored.branch, options.branchName)
|
|
637
|
+
? restored.entry
|
|
638
|
+
: undefined;
|
|
612
639
|
noEntryRequested.current = entry === '';
|
|
613
640
|
setSelectedPath(entry ?? '');
|
|
614
641
|
if (entry !== undefined) {
|
|
@@ -782,7 +809,7 @@ export function useEntryManager(options) {
|
|
|
782
809
|
return;
|
|
783
810
|
if (window.location.pathname !== editorPathRef.current)
|
|
784
811
|
return;
|
|
785
|
-
writeEditorUrl(entryUrl(selectedPath, noEntryRequested.current, currentBranchRef.current), push);
|
|
812
|
+
writeEditorUrl(entryUrl(selectedPath, noEntryRequested.current, urlBranchOf(currentBranchRef.current)), push);
|
|
786
813
|
}, [selectedPath]);
|
|
787
814
|
const entriesRef = useRef(entriesState);
|
|
788
815
|
entriesRef.current = entriesState;
|
|
@@ -803,8 +830,7 @@ export function useEntryManager(options) {
|
|
|
803
830
|
const entry = params.get('entry');
|
|
804
831
|
const branch = params.get('branch');
|
|
805
832
|
const current = currentBranchRef.current;
|
|
806
|
-
|
|
807
|
-
if (branch && sanitizeBranchName(branch) !== sanitizeBranchName(current)) {
|
|
833
|
+
if (branch && !sameBranchRef.current(branch, current)) {
|
|
808
834
|
const move = historyMovesRef.current;
|
|
809
835
|
const stay = () => {
|
|
810
836
|
if (move !== historyMovesRef.current)
|
|
@@ -812,7 +838,7 @@ export function useEntryManager(options) {
|
|
|
812
838
|
if (moved)
|
|
813
839
|
undoHistoryMove(moved);
|
|
814
840
|
else {
|
|
815
|
-
const url = entryUrl(selectedPathRef.current, noEntryRequested.current, currentBranchRef.current);
|
|
841
|
+
const url = entryUrl(selectedPathRef.current, noEntryRequested.current, urlBranchOf(currentBranchRef.current));
|
|
816
842
|
writeEditorUrl(url, false);
|
|
817
843
|
}
|
|
818
844
|
};
|
|
@@ -824,7 +850,7 @@ export function useEntryManager(options) {
|
|
|
824
850
|
entryAfterBranchChangeRef.current = { branch, entry };
|
|
825
851
|
if (!hasSyncedFromUrl.current)
|
|
826
852
|
initialUrlEntry.current = entry;
|
|
827
|
-
switchBranch(branch).catch(() => {
|
|
853
|
+
switchBranch(branch, () => move !== historyMovesRef.current).catch(() => {
|
|
828
854
|
if (move === historyMovesRef.current)
|
|
829
855
|
entryAfterBranchChangeRef.current = undefined;
|
|
830
856
|
stay();
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/** Where in the editor an error was caught, for the log line and the copied details. */
|
|
2
2
|
export interface EditorErrorContext {
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* `editor` for the crash screen, `field` for a field boundary, `rich-text` for MDXEditor,
|
|
5
|
+
* `changes` for the "Show changes" drawer.
|
|
6
|
+
*/
|
|
7
|
+
boundary: 'editor' | 'field' | 'rich-text' | 'changes';
|
|
5
8
|
/** The field's canopy path, for a field or rich-text boundary. */
|
|
6
9
|
fieldPath?: string;
|
|
7
10
|
/** React's component stack, when React supplied one. */
|