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
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { BranchListItem } from '../../api/branch.js';
|
|
2
|
+
import { type CreateRefusal } from './create-branch-request.js';
|
|
2
3
|
import type { UnsavedSummary } from './useDraftManager.js';
|
|
3
4
|
export interface UseBranchActionsOptions {
|
|
4
5
|
branchName: string;
|
|
6
|
+
/** What the dialogs call the open branch, when it is not `branchName`. */
|
|
7
|
+
branchLabel?: string;
|
|
5
8
|
setBranchName: (name: string) => void;
|
|
6
9
|
/** From useDraftManager's `resolveUnsaved`: what a branch-level action would leave behind. */
|
|
7
10
|
getUnsaved: () => Promise<UnsavedSummary>;
|
|
@@ -9,18 +12,32 @@ export interface UseBranchActionsOptions {
|
|
|
9
12
|
/** Receives the branch the server just created, so it can be shown before any listing includes it. */
|
|
10
13
|
onBranchCreated: (branch: BranchListItem) => void;
|
|
11
14
|
onBranchSwitch?: (branch: string) => void;
|
|
15
|
+
/** What `?branch=` carries for a branch (its short name); defaults to the name as given. */
|
|
16
|
+
urlBranchName?: (branch: string) => string;
|
|
17
|
+
/**
|
|
18
|
+
* Whether `next`, the name a card or history entry gives, opens `branchName`, so a change to it
|
|
19
|
+
* is no switch; defaults to equality.
|
|
20
|
+
*/
|
|
21
|
+
sameBranch?: (next: string, branchName: string) => boolean;
|
|
12
22
|
/** The current user; a create the server never answered adopts only a listed branch they made. */
|
|
13
23
|
userId?: string;
|
|
14
24
|
}
|
|
15
25
|
export interface UseBranchActionsReturn {
|
|
16
|
-
|
|
26
|
+
/**
|
|
27
|
+
* `push: false` for a switch a Back or Forward makes, which arrived at its history entry already;
|
|
28
|
+
* it rejects instead once its `superseded()` holds.
|
|
29
|
+
*/
|
|
30
|
+
handleBranchChange: (branch: string | null, options?: {
|
|
31
|
+
push?: boolean;
|
|
32
|
+
superseded?: () => boolean;
|
|
33
|
+
}) => Promise<void>;
|
|
17
34
|
/** True when nothing is unsaved or the user accepted the warning. Run before any in-flight UI. */
|
|
18
35
|
confirmCreate: () => Promise<boolean>;
|
|
19
36
|
handleCreateBranch: (branch: {
|
|
20
37
|
name: string;
|
|
21
38
|
title?: string;
|
|
22
39
|
description?: string;
|
|
23
|
-
}) => Promise<boolean>;
|
|
40
|
+
}) => Promise<boolean | CreateRefusal>;
|
|
24
41
|
/** True while the unsaved-changes confirm is on screen. */
|
|
25
42
|
confirmOpen: boolean;
|
|
26
43
|
}
|
|
@@ -5,7 +5,9 @@ import { notifications } from '@mantine/notifications';
|
|
|
5
5
|
import { Text } from '@mantine/core';
|
|
6
6
|
import { useApiClient } from '../context/index.js';
|
|
7
7
|
import { requestBranchCreate } from './create-branch-request.js';
|
|
8
|
+
import { displayBranchName } from '../branch-names.js';
|
|
8
9
|
import { writeEditorUrl } from './editor-history.js';
|
|
10
|
+
import { parseCreatedBranchName } from '../../paths/validation.js';
|
|
9
11
|
const MAX_LISTED_LABELS = 5;
|
|
10
12
|
/** 'Unsaved changes in: "A", "B".' */
|
|
11
13
|
export const describeUnsaved = ({ labels }) => {
|
|
@@ -24,13 +26,13 @@ export const describeUnsaved = ({ labels }) => {
|
|
|
24
26
|
export function useBranchActions(options) {
|
|
25
27
|
const apiClient = useApiClient();
|
|
26
28
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
27
|
-
const performBranchSwitch = (next) => {
|
|
29
|
+
const performBranchSwitch = (next, urlName, push = true) => {
|
|
28
30
|
options.setBranchName(next);
|
|
29
|
-
// Its own history entry
|
|
31
|
+
// Its own history entry, unless a Back or Forward made it: that entry's URL is replaced.
|
|
30
32
|
if (typeof window !== 'undefined') {
|
|
31
33
|
const url = new URL(window.location.href);
|
|
32
|
-
url.searchParams.set('branch', next);
|
|
33
|
-
writeEditorUrl(url.toString(),
|
|
34
|
+
url.searchParams.set('branch', urlName ?? options.urlBranchName?.(next) ?? next);
|
|
35
|
+
writeEditorUrl(url.toString(), push);
|
|
34
36
|
}
|
|
35
37
|
options.onBranchSwitch?.(next);
|
|
36
38
|
};
|
|
@@ -49,7 +51,7 @@ export function useBranchActions(options) {
|
|
|
49
51
|
openConfirm({
|
|
50
52
|
title: 'Unsaved Changes',
|
|
51
53
|
// Drafts are stored per branch, so leaving does not lose them.
|
|
52
|
-
children: (_jsxs(Text, { size: "sm", children: [describeUnsaved(unsaved), " Your drafts stay on \u201C", options.branchName, "\u201D and come back when you return. ", question] })),
|
|
54
|
+
children: (_jsxs(Text, { size: "sm", children: [describeUnsaved(unsaved), " Your drafts stay on \u201C", options.branchLabel ?? options.branchName, "\u201D and come back when you return. ", question] })),
|
|
53
55
|
labels: { confirm: 'Continue Anyway', cancel: 'Cancel' },
|
|
54
56
|
confirmProps: { color: 'red' },
|
|
55
57
|
onCancel: () => settle(false),
|
|
@@ -60,16 +62,21 @@ export function useBranchActions(options) {
|
|
|
60
62
|
});
|
|
61
63
|
});
|
|
62
64
|
};
|
|
63
|
-
const handleBranchChange = async (next) => {
|
|
64
|
-
if (!next || next === options.branchName)
|
|
65
|
+
const handleBranchChange = async (next, change = {}) => {
|
|
66
|
+
if (!next || (options.sameBranch?.(next, options.branchName) ?? next === options.branchName))
|
|
65
67
|
return;
|
|
66
68
|
const confirmed = await confirmIfDirty('Switch branches anyway?');
|
|
67
69
|
if (!confirmed)
|
|
68
70
|
throw new Error('User cancelled branch switch');
|
|
69
|
-
|
|
71
|
+
if (change.superseded?.())
|
|
72
|
+
throw new Error('A later history move superseded the branch switch');
|
|
73
|
+
performBranchSwitch(next, undefined, change.push ?? true);
|
|
70
74
|
};
|
|
71
75
|
const confirmCreate = () => confirmIfDirty('Create the new branch anyway?');
|
|
72
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Resolves true when the branch was created and switched to, false when it was not and a toast
|
|
78
|
+
* says why, or a {@link CreateRefusal} when the name cannot be created.
|
|
79
|
+
*/
|
|
73
80
|
const handleCreateBranch = async (branch) => {
|
|
74
81
|
try {
|
|
75
82
|
const outcome = await requestBranchCreate(apiClient, {
|
|
@@ -78,22 +85,24 @@ export function useBranchActions(options) {
|
|
|
78
85
|
description: branch.description,
|
|
79
86
|
}, options.userId);
|
|
80
87
|
if (outcome.kind === 'failed') {
|
|
88
|
+
if (outcome.refused)
|
|
89
|
+
return { refusal: outcome.message };
|
|
81
90
|
throw new Error(outcome.message);
|
|
82
91
|
}
|
|
83
92
|
const created = outcome.branch;
|
|
84
93
|
if (created)
|
|
85
94
|
options.onBranchCreated(created);
|
|
86
|
-
// The server
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
?
|
|
93
|
-
:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
performBranchSwitch(createdName);
|
|
95
|
+
// The server adds the namespace, which the editor is never told; its short name resolves too.
|
|
96
|
+
const typed = parseCreatedBranchName(branch.name);
|
|
97
|
+
const createdName = created?.name ?? (typed.ok ? typed.shortName : branch.name);
|
|
98
|
+
const createdLabel = created
|
|
99
|
+
? displayBranchName(created)
|
|
100
|
+
: typed.ok
|
|
101
|
+
? typed.shortName
|
|
102
|
+
: branch.name;
|
|
103
|
+
notifications.show({ message: `Branch “${createdLabel}” created`, color: 'green' });
|
|
104
|
+
// A listing may not hold it yet, so its own short name stands in for the lookup.
|
|
105
|
+
performBranchSwitch(createdName, created?.shortName);
|
|
97
106
|
// Not awaited: a listing can lag the create, and the switch must not wait
|
|
98
107
|
// on it. loadBranches reports its own failures and never rejects.
|
|
99
108
|
void options.onReloadBranches();
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ApiClient } from '../context/index.js';
|
|
2
|
+
import type { BranchChangesData } from '../../api/branch-changes.js';
|
|
3
|
+
import type { BranchEntryChangeData } from '../../api/branch-entry-changes.js';
|
|
4
|
+
/** An entry's change on the branch, and where a move started. */
|
|
5
|
+
export interface EntryChangeTarget {
|
|
6
|
+
logicalPath: string;
|
|
7
|
+
/** A moved entry's path at the base. */
|
|
8
|
+
from?: string;
|
|
9
|
+
/** The entry's content id, so a slug another entry reuses isn't read as this one. */
|
|
10
|
+
id?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Why a branch's changes could not be loaded: the branch moved under the read (a 409, nothing
|
|
14
|
+
* lost; `message` is the server's) or anything else.
|
|
15
|
+
*/
|
|
16
|
+
export type BranchChangesError = {
|
|
17
|
+
kind: 'syncing';
|
|
18
|
+
message?: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'failed';
|
|
21
|
+
message: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Why an entry's change could not be loaded: as the list's, or a refusal no retry passes: too
|
|
25
|
+
* large to compare (413), on neither side (404), or a side that doesn't parse (422).
|
|
26
|
+
*/
|
|
27
|
+
type EntryChangeError = BranchChangesError | {
|
|
28
|
+
kind: 'tooLarge';
|
|
29
|
+
message: string;
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'notFound';
|
|
32
|
+
message: string;
|
|
33
|
+
} | {
|
|
34
|
+
kind: 'unreadable';
|
|
35
|
+
message: string;
|
|
36
|
+
};
|
|
37
|
+
interface ChangesState<T, E> {
|
|
38
|
+
data: T | undefined;
|
|
39
|
+
error: E | undefined;
|
|
40
|
+
isLoading: boolean;
|
|
41
|
+
isValidating: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Starts a fresh fetch through SWR's revalidating `mutate()`, never coalesced with one in
|
|
44
|
+
* flight; SWR keeps whichever fetch started last (hooks/README.md).
|
|
45
|
+
*/
|
|
46
|
+
reload: () => Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
export type BranchChangesState = ChangesState<BranchChangesData, BranchChangesError>;
|
|
49
|
+
/**
|
|
50
|
+
* The branch's changes. `reload` also revalidates every entry change of the branch that is
|
|
51
|
+
* shown, so one save refreshes the list and an open "Show changes" alike.
|
|
52
|
+
*/
|
|
53
|
+
export declare function useBranchChangesData(apiClient: Pick<ApiClient, 'changes'>, branch: string, enabled: boolean): BranchChangesState;
|
|
54
|
+
export type EntryChangeState = ChangesState<BranchEntryChangeData, EntryChangeError>;
|
|
55
|
+
/** One entry's change on the branch, fetched while `target` is set (the drawer is open). */
|
|
56
|
+
export declare function useEntryChangeData(apiClient: Pick<ApiClient, 'changes'>, branch: string, target: EntryChangeTarget | undefined): EntryChangeState;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
/**
|
|
3
|
+
* SWR-backed reads of what a branch changes against its base: the list (GET /:branch/changes)
|
|
4
|
+
* for the Content panel's Changed filter, and one entry's two sides (GET
|
|
5
|
+
* /:branch/changes/...entryPath) for its "Show changes" drawer. Keyed per branch like
|
|
6
|
+
* useCommentsData.ts; a null key fetches nothing.
|
|
7
|
+
*/
|
|
8
|
+
import { useCallback, useRef } from 'react';
|
|
9
|
+
import useSWR, { useSWRConfig } from 'swr';
|
|
10
|
+
const KEY_PREFIX = 'canopy:changes:';
|
|
11
|
+
/** Cache key for a branch's changes against its base. */
|
|
12
|
+
const branchChangesKey = (branch) => `${KEY_PREFIX}${branch}`;
|
|
13
|
+
/**
|
|
14
|
+
* Cache key for one entry's change. Under its branch's list key and a `:`, which no branch name
|
|
15
|
+
* holds, so a reload of the list finds every entry key of its branch by prefix.
|
|
16
|
+
*/
|
|
17
|
+
const entryChangeKey = (branch, target) => `${KEY_PREFIX}${branch}:${target.logicalPath}${target.from ? `?from=${target.from}` : ''}${target.id ? `#${target.id}` : ''}`;
|
|
18
|
+
class ChangesLoadError extends Error {
|
|
19
|
+
constructor(reason) {
|
|
20
|
+
super(`Couldn't load the branch's changes (${reason.kind})`);
|
|
21
|
+
this.reason = reason;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function fetchBranchChanges(apiClient, branch) {
|
|
25
|
+
const result = await apiClient.changes.list({ branch });
|
|
26
|
+
// A body no CanopyCMS handler wrote, such as a proxy's, is a failure, not an empty list.
|
|
27
|
+
if (result.ok && Array.isArray(result.data?.entries) && Array.isArray(result.data.collections)) {
|
|
28
|
+
return result.data;
|
|
29
|
+
}
|
|
30
|
+
throw new ChangesLoadError(result.status === 409
|
|
31
|
+
? { kind: 'syncing', ...(result.error && { message: result.error }) }
|
|
32
|
+
: { kind: 'failed', message: result.error ?? `HTTP ${result.status}` });
|
|
33
|
+
}
|
|
34
|
+
async function fetchEntryChange(apiClient, branch, target) {
|
|
35
|
+
const result = await apiClient.changes.entry({
|
|
36
|
+
branch,
|
|
37
|
+
entryPath: target.logicalPath,
|
|
38
|
+
...(target.from && { from: target.from }),
|
|
39
|
+
...(target.id && { id: target.id }),
|
|
40
|
+
});
|
|
41
|
+
if (result.ok && result.data && 'before' in result.data && 'after' in result.data) {
|
|
42
|
+
return result.data;
|
|
43
|
+
}
|
|
44
|
+
const message = result.error ?? `HTTP ${result.status}`;
|
|
45
|
+
throw new ChangesLoadError(result.status === 409
|
|
46
|
+
? { kind: 'syncing', ...(result.error && { message: result.error }) }
|
|
47
|
+
: result.status === 413
|
|
48
|
+
? { kind: 'tooLarge', message }
|
|
49
|
+
: result.status === 404
|
|
50
|
+
? { kind: 'notFound', message }
|
|
51
|
+
: result.status === 422
|
|
52
|
+
? { kind: 'unreadable', message }
|
|
53
|
+
: { kind: 'failed', message });
|
|
54
|
+
}
|
|
55
|
+
const toError = (err) => err instanceof ChangesLoadError
|
|
56
|
+
? err.reason
|
|
57
|
+
: { kind: 'failed', message: err instanceof Error ? err.message : String(err) };
|
|
58
|
+
/**
|
|
59
|
+
* `fetch` as an SWR fetcher that never lets an overtaken fetch's error show. SWR 2.4 drops an
|
|
60
|
+
* overtaken fetch's result but still shows its error, so per key it counts the fetches started,
|
|
61
|
+
* and one overtaken resolves with the last answer instead of throwing, which SWR then drops: a
|
|
62
|
+
* newer fetch holds the key, or has finished.
|
|
63
|
+
*/
|
|
64
|
+
function useOvertakenGuard(fetch) {
|
|
65
|
+
const startsRef = useRef(new Map());
|
|
66
|
+
const answersRef = useRef(new Map());
|
|
67
|
+
return async (key) => {
|
|
68
|
+
const started = (startsRef.current.get(key) ?? 0) + 1;
|
|
69
|
+
startsRef.current.set(key, started);
|
|
70
|
+
try {
|
|
71
|
+
const data = await fetch(key);
|
|
72
|
+
answersRef.current.set(key, data);
|
|
73
|
+
return data;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
if ((startsRef.current.get(key) ?? 0) > started)
|
|
77
|
+
return answersRef.current.get(key);
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The branch's changes. `reload` also revalidates every entry change of the branch that is
|
|
84
|
+
* shown, so one save refreshes the list and an open "Show changes" alike.
|
|
85
|
+
*/
|
|
86
|
+
export function useBranchChangesData(apiClient, branch, enabled) {
|
|
87
|
+
const key = branch && enabled ? branchChangesKey(branch) : null;
|
|
88
|
+
const fetcher = useOvertakenGuard((cacheKey) =>
|
|
89
|
+
// The branch is read from the key the fetch was started for, never from the render.
|
|
90
|
+
fetchBranchChanges(apiClient, cacheKey.slice(KEY_PREFIX.length)));
|
|
91
|
+
const swr = useSWR(key, fetcher);
|
|
92
|
+
const { mutate } = swr;
|
|
93
|
+
const { mutate: mutateKeys } = useSWRConfig();
|
|
94
|
+
const reload = useCallback(async () => {
|
|
95
|
+
const entryPrefix = `${branchChangesKey(branch)}:`;
|
|
96
|
+
await Promise.all([
|
|
97
|
+
mutate(),
|
|
98
|
+
mutateKeys((k) => typeof k === 'string' && k.startsWith(entryPrefix)),
|
|
99
|
+
]);
|
|
100
|
+
}, [mutate, mutateKeys, branch]);
|
|
101
|
+
return {
|
|
102
|
+
data: swr.data,
|
|
103
|
+
error: key && swr.error !== undefined ? toError(swr.error) : undefined,
|
|
104
|
+
isLoading: swr.isLoading,
|
|
105
|
+
isValidating: swr.isValidating,
|
|
106
|
+
reload,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** One entry's change on the branch, fetched while `target` is set (the drawer is open). */
|
|
110
|
+
export function useEntryChangeData(apiClient, branch, target) {
|
|
111
|
+
const key = branch && target ? entryChangeKey(branch, target) : null;
|
|
112
|
+
// What each key fetches, recorded as it is rendered, so a fetch reads its own key's target.
|
|
113
|
+
const targetsRef = useRef(new Map());
|
|
114
|
+
if (key && target)
|
|
115
|
+
targetsRef.current.set(key, { branch, target });
|
|
116
|
+
const fetcher = useOvertakenGuard((cacheKey) => {
|
|
117
|
+
const keyed = targetsRef.current.get(cacheKey);
|
|
118
|
+
if (!keyed)
|
|
119
|
+
throw new Error(`No entry change is keyed ${cacheKey}`);
|
|
120
|
+
return fetchEntryChange(apiClient, keyed.branch, keyed.target);
|
|
121
|
+
});
|
|
122
|
+
const swr = useSWR(key, fetcher);
|
|
123
|
+
const { mutate } = swr;
|
|
124
|
+
const reload = useCallback(async () => {
|
|
125
|
+
await mutate();
|
|
126
|
+
}, [mutate]);
|
|
127
|
+
return {
|
|
128
|
+
data: swr.data,
|
|
129
|
+
error: key && swr.error !== undefined ? toError(swr.error) : undefined,
|
|
130
|
+
isLoading: swr.isLoading,
|
|
131
|
+
isValidating: swr.isValidating,
|
|
132
|
+
reload,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -27,6 +27,7 @@ export declare function withdrawPullRequestBullet(pr: BranchPullRequest): string
|
|
|
27
27
|
export declare const CREATED_BRANCH_GRACE_MS = 120000;
|
|
28
28
|
interface BranchSummary {
|
|
29
29
|
name: string;
|
|
30
|
+
shortName?: string;
|
|
30
31
|
status: string;
|
|
31
32
|
createdBy?: string;
|
|
32
33
|
updatedAt?: string;
|
|
@@ -85,11 +86,26 @@ export interface UseBranchManagerOptions {
|
|
|
85
86
|
onChangesRequested?: (branchName: string) => void;
|
|
86
87
|
}
|
|
87
88
|
export interface UseBranchManagerReturn {
|
|
89
|
+
/**
|
|
90
|
+
* The listed full name of the open branch, which keys everything stored or fetched per branch.
|
|
91
|
+
* Until a listing resolves the name it arrived by, that name as given.
|
|
92
|
+
*/
|
|
88
93
|
branchName: string;
|
|
94
|
+
/** Opens the branch `name` names, a URL value or card address, resolved once a listing holds it. */
|
|
89
95
|
setBranchName: (name: string) => void;
|
|
90
96
|
branches: BranchListItem[];
|
|
91
97
|
branchSummaries: BranchSummary[];
|
|
92
98
|
currentBranch: BranchListItem | undefined;
|
|
99
|
+
/** What the open branch's own actions hand `handleSubmit` and the rest (`branchAddress`). */
|
|
100
|
+
currentAddress: string;
|
|
101
|
+
/** The listed branch `name`, a URL value or card address, opens. */
|
|
102
|
+
listedBranch: (name: string) => BranchListItem | undefined;
|
|
103
|
+
/** The full name of the listed branch `name` opens, else `name` sanitized. */
|
|
104
|
+
listedName: (name: string) => string;
|
|
105
|
+
/** Whether `name`, a URL value or card address, opens `branch`, a `branchName` this gave. */
|
|
106
|
+
namesBranch: (name: string, branch: string) => boolean;
|
|
107
|
+
/** What `?branch=` carries for the branch `name`, a full name or its exact form, names, else `name`. */
|
|
108
|
+
urlBranchName: (name: string) => string;
|
|
93
109
|
/** The listing failed and nothing earlier stands in for it. */
|
|
94
110
|
branchesLoadFailed: boolean;
|
|
95
111
|
/** A listing has arrived, so a branch missing from it is not still loading. */
|