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.
Files changed (147) hide show
  1. package/CHANGELOG.md +201 -47
  2. package/dist/api/admin-branch-health.js +47 -20
  3. package/dist/api/admin.d.ts +13 -1
  4. package/dist/api/admin.js +14 -24
  5. package/dist/api/branch-changes.d.ts +87 -0
  6. package/dist/api/branch-changes.js +269 -0
  7. package/dist/api/branch-entry-changes.d.ts +83 -0
  8. package/dist/api/branch-entry-changes.js +262 -0
  9. package/dist/api/branch-merge.js +2 -2
  10. package/dist/api/branch-review.js +35 -4
  11. package/dist/api/branch-status.js +14 -5
  12. package/dist/api/branch-withdraw.js +7 -0
  13. package/dist/api/branch.d.ts +13 -1
  14. package/dist/api/branch.js +171 -97
  15. package/dist/api/client.d.ts +15 -0
  16. package/dist/api/client.js +13 -1
  17. package/dist/api/content.d.ts +18 -18
  18. package/dist/api/content.js +1 -1
  19. package/dist/api/github-sync.js +7 -3
  20. package/dist/api/groups.d.ts +4 -4
  21. package/dist/api/pr-comment-sync.d.ts +9 -8
  22. package/dist/api/pr-comment-sync.js +23 -13
  23. package/dist/api/routes.js +4 -0
  24. package/dist/api/types.d.ts +3 -1
  25. package/dist/authorization/groups/schema.d.ts +4 -4
  26. package/dist/authorization/protected-branch.d.ts +3 -3
  27. package/dist/authorization/protected-branch.js +3 -3
  28. package/dist/branch-metadata-file.d.ts +84 -0
  29. package/dist/branch-metadata-file.js +9 -0
  30. package/dist/branch-metadata.d.ts +4 -4
  31. package/dist/branch-metadata.js +4 -5
  32. package/dist/branch-param.d.ts +24 -0
  33. package/dist/branch-param.js +87 -0
  34. package/dist/branch-provisioning.d.ts +6 -3
  35. package/dist/branch-provisioning.js +12 -10
  36. package/dist/branch-registry.d.ts +6 -0
  37. package/dist/branch-registry.js +12 -3
  38. package/dist/branch-workspace.js +22 -15
  39. package/dist/cli/cli.js +1060 -172
  40. package/dist/cli/generate-ai-content.js +732 -101
  41. package/dist/cli/init.js +126 -0
  42. package/dist/comment-store.d.ts +3 -0
  43. package/dist/comment-store.js +4 -1
  44. package/dist/config/schemas/config.d.ts +3 -0
  45. package/dist/config/schemas/config.js +2 -0
  46. package/dist/config/types.d.ts +11 -0
  47. package/dist/content-id-index.d.ts +5 -0
  48. package/dist/content-id-index.js +1 -1
  49. package/dist/content-listing.d.ts +53 -0
  50. package/dist/content-listing.js +123 -27
  51. package/dist/content-reader.js +23 -16
  52. package/dist/context.js +8 -7
  53. package/dist/editor/BranchManager.d.ts +7 -3
  54. package/dist/editor/BranchManager.js +51 -6
  55. package/dist/editor/Editor.js +166 -34
  56. package/dist/editor/EntryNavigator.d.ts +34 -10
  57. package/dist/editor/EntryNavigator.js +185 -29
  58. package/dist/editor/admin/SystemHealthPanel.js +36 -1
  59. package/dist/editor/admin/useSystemHealth.js +1 -0
  60. package/dist/editor/branch-names.d.ts +43 -0
  61. package/dist/editor/branch-names.js +75 -0
  62. package/dist/editor/components/BranchPill.d.ts +2 -2
  63. package/dist/editor/components/BranchPill.js +2 -2
  64. package/dist/editor/components/EditorHeader.d.ts +3 -1
  65. package/dist/editor/components/EditorHeader.js +2 -2
  66. package/dist/editor/components/EntryChangesDrawer.d.ts +51 -0
  67. package/dist/editor/components/EntryChangesDrawer.js +363 -0
  68. package/dist/editor/components/EntryChangesSkeleton.d.ts +9 -0
  69. package/dist/editor/components/EntryChangesSkeleton.js +7 -0
  70. package/dist/editor/components/EntryHeader.d.ts +9 -1
  71. package/dist/editor/components/EntryHeader.js +25 -3
  72. package/dist/editor/components/LazyEntryChangesDrawer.d.ts +16 -0
  73. package/dist/editor/components/LazyEntryChangesDrawer.js +45 -0
  74. package/dist/editor/components/ReviewDialogs.js +3 -3
  75. package/dist/editor/context/ApiClientContext.d.ts +9 -0
  76. package/dist/editor/context/ApiClientContext.js +26 -2
  77. package/dist/editor/context/index.d.ts +1 -1
  78. package/dist/editor/context/index.js +1 -1
  79. package/dist/editor/copy.d.ts +1 -0
  80. package/dist/editor/copy.js +1 -0
  81. package/dist/editor/editor-utils.d.ts +2 -0
  82. package/dist/editor/editor-utils.js +15 -0
  83. package/dist/editor/entry-diff.d.ts +143 -0
  84. package/dist/editor/entry-diff.js +593 -0
  85. package/dist/editor/hooks/create-branch-request.d.ts +11 -1
  86. package/dist/editor/hooks/create-branch-request.js +16 -6
  87. package/dist/editor/hooks/useBranchActions.d.ts +19 -2
  88. package/dist/editor/hooks/useBranchActions.js +29 -20
  89. package/dist/editor/hooks/useBranchChangesData.d.ts +57 -0
  90. package/dist/editor/hooks/useBranchChangesData.js +134 -0
  91. package/dist/editor/hooks/useBranchManager.d.ts +16 -0
  92. package/dist/editor/hooks/useBranchManager.js +142 -54
  93. package/dist/editor/hooks/useDraftManager.js +6 -2
  94. package/dist/editor/hooks/useEntryManager.d.ts +18 -2
  95. package/dist/editor/hooks/useEntryManager.js +36 -10
  96. package/dist/editor/utils/editor-errors.d.ts +5 -2
  97. package/dist/git-manager.d.ts +71 -0
  98. package/dist/git-manager.js +270 -1
  99. package/dist/http/handler.js +11 -9
  100. package/dist/operating-mode/branch-namespace-fixtures.d.ts +17 -0
  101. package/dist/operating-mode/branch-namespace-fixtures.js +65 -0
  102. package/dist/operating-mode/branch-namespace.d.ts +34 -0
  103. package/dist/operating-mode/branch-namespace.js +121 -0
  104. package/dist/operating-mode/client-unsafe-strategy.js +7 -0
  105. package/dist/operating-mode/types.d.ts +8 -0
  106. package/dist/paths/branch-name.d.ts +11 -19
  107. package/dist/paths/branch-name.js +17 -27
  108. package/dist/paths/index.d.ts +2 -2
  109. package/dist/paths/index.js +2 -2
  110. package/dist/paths/resolve.d.ts +0 -1
  111. package/dist/paths/resolve.js +0 -1
  112. package/dist/paths/validation.d.ts +40 -1
  113. package/dist/paths/validation.js +83 -4
  114. package/dist/pr-comments.d.ts +17 -9
  115. package/dist/pr-comments.js +53 -15
  116. package/dist/services.js +2 -1
  117. package/dist/submission-attribution.d.ts +22 -0
  118. package/dist/submission-attribution.js +62 -6
  119. package/dist/task-queue/worker-status.d.ts +6 -1
  120. package/dist/task-queue/worker-status.js +23 -2
  121. package/dist/types.d.ts +59 -0
  122. package/dist/version.d.ts +1 -1
  123. package/dist/version.js +1 -1
  124. package/dist/worker/branch-policy.d.ts +111 -16
  125. package/dist/worker/branch-policy.js +222 -17
  126. package/dist/worker/cms-worker.d.ts +24 -2
  127. package/dist/worker/cms-worker.js +35 -24
  128. package/dist/worker/git-sync.d.ts +3 -3
  129. package/dist/worker/git-sync.js +47 -1
  130. package/dist/worker/github-gateway-http.js +2 -2
  131. package/dist/worker/github-gateway-wire.d.ts +40 -25
  132. package/dist/worker/github-gateway-wire.js +14 -3
  133. package/dist/worker/github-gateway.d.ts +17 -6
  134. package/dist/worker/github-gateway.js +62 -6
  135. package/dist/worker/github-mirror.d.ts +7 -4
  136. package/dist/worker/github-mirror.js +27 -16
  137. package/dist/worker/history-rewrite.d.ts +23 -10
  138. package/dist/worker/history-rewrite.js +43 -20
  139. package/dist/worker/legacy-branch-fixtures.d.ts +21 -0
  140. package/dist/worker/legacy-branch-fixtures.js +71 -0
  141. package/dist/worker/rebase.d.ts +2 -1
  142. package/dist/worker/rebase.js +93 -36
  143. package/dist/worker/task-runner.d.ts +4 -1
  144. package/dist/worker/task-runner.js +77 -48
  145. package/dist/worker/unpublishable-branches.d.ts +34 -0
  146. package/dist/worker/unpublishable-branches.js +182 -0
  147. 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
- handleBranchChange: (branch: string | null) => Promise<void>;
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; a Back onto another branch already shows this URL, so writes nothing.
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(), true);
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
- performBranchSwitch(next);
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
- /** Resolves true when the branch was created and switched to, false when it was not. */
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 sanitizes the branch name (e.g. "feature/x" -> "feature-x")
87
- // before persisting it. Adopt the canonical name from the response so
88
- // the client doesn't end up stuck on a name the server never saved.
89
- const createdName = created?.name ?? branch.name;
90
- notifications.show({
91
- message: createdName === branch.name
92
- ? `Branch "${createdName}" created`
93
- : `Branch "${createdName}" created (renamed from "${branch.name}")`,
94
- color: 'green',
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. */