canopycms 0.0.67 → 0.0.68-int.101
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/README.md +24 -17
- package/dist/ai/handler.js +8 -0
- package/dist/api/admin-branch-health.js +17 -20
- package/dist/api/admin.d.ts +30 -9
- package/dist/api/admin.js +32 -2
- package/dist/api/assets.js +84 -76
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +14 -5
- package/dist/api/branch-status.d.ts +5 -5
- package/dist/api/branch-status.js +26 -6
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +7 -2
- package/dist/api/branch.d.ts +15 -2
- package/dist/api/branch.js +118 -92
- package/dist/api/client.d.ts +18 -6
- package/dist/api/client.js +79 -12
- package/dist/api/content.d.ts +6 -5
- package/dist/api/content.js +44 -41
- package/dist/api/entries.js +16 -15
- package/dist/api/github-sync.d.ts +19 -1
- package/dist/api/github-sync.js +57 -10
- package/dist/api/index.d.ts +1 -1
- package/dist/api/reference-options.js +1 -1
- package/dist/api/resolve-references.js +14 -40
- package/dist/api/settings-helpers.d.ts +1 -1
- package/dist/api/settings-helpers.js +1 -4
- package/dist/api/user.d.ts +3 -0
- package/dist/api/user.js +3 -0
- package/dist/assets/asset-url.d.ts +22 -16
- package/dist/assets/asset-url.js +42 -25
- package/dist/assets/factory.d.ts +5 -0
- package/dist/assets/factory.js +5 -0
- package/dist/assets/finalize.js +3 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +153 -0
- package/dist/assets/materialize.js +402 -0
- package/dist/assets/store-local.d.ts +19 -5
- package/dist/assets/store-local.js +69 -10
- package/dist/assets/store-s3.d.ts +38 -7
- package/dist/assets/store-s3.js +128 -22
- package/dist/assets/transform-directives.d.ts +52 -13
- package/dist/assets/transform-directives.js +87 -27
- package/dist/assets/transform.d.ts +3 -3
- package/dist/assets/transform.js +13 -8
- package/dist/assets/types.d.ts +31 -4
- package/dist/auth/file-based-auth-cache.js +7 -8
- package/dist/authorization/content.d.ts +5 -4
- package/dist/authorization/content.js +5 -5
- package/dist/authorization/path.d.ts +11 -6
- package/dist/authorization/path.js +11 -6
- package/dist/authorization/types.d.ts +2 -2
- package/dist/branch-health.d.ts +7 -1
- package/dist/branch-health.js +8 -3
- package/dist/branch-metadata.d.ts +5 -5
- package/dist/branch-metadata.js +44 -36
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- package/dist/branch-sparse.d.ts +23 -0
- package/dist/branch-sparse.js +118 -0
- package/dist/branch-workspace.d.ts +42 -3
- package/dist/branch-workspace.js +240 -94
- package/dist/build/asset-refs.d.ts +93 -0
- package/dist/build/asset-refs.js +251 -0
- package/dist/build-identity.d.ts +3 -0
- package/dist/build-identity.js +13 -0
- package/dist/cli/asset-refs.d.ts +52 -0
- package/dist/cli/asset-refs.js +194 -0
- package/dist/cli/cli.d.ts +15 -1
- package/dist/cli/cli.js +5928 -2195
- package/dist/cli/configured-asset-store.d.ts +12 -0
- package/dist/cli/configured-asset-store.js +46 -0
- package/dist/cli/generate-ai-content.js +2185 -587
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +30 -27
- package/dist/cli/migrate.js +12 -8
- package/dist/cli/sync.js +83 -37
- package/dist/cli/template-files/Dockerfile.cms.template +8 -0
- package/dist/cli/template-files/cdk-app.ts.template +4 -0
- package/dist/cli/template-files/cms-stack.ts.template +23 -12
- package/dist/cli/template-files/deploy-cms.yml.template +2 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/config/helpers.js +1 -2
- package/dist/config/schemas/config.d.ts +19 -32
- package/dist/config/schemas/config.js +9 -11
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +4 -20
- package/dist/config/schemas/url.js +6 -22
- package/dist/config/types.d.ts +30 -20
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +37 -0
- package/dist/content-listing.d.ts +12 -12
- package/dist/content-listing.js +10 -6
- package/dist/content-reader.d.ts +11 -2
- package/dist/content-reader.js +22 -14
- package/dist/content-store.d.ts +37 -9
- package/dist/content-store.js +70 -43
- package/dist/content-tree.d.ts +1 -1
- package/dist/content-tree.js +2 -2
- package/dist/context.d.ts +27 -9
- package/dist/context.js +154 -70
- package/dist/editor/BranchManager.d.ts +7 -1
- package/dist/editor/BranchManager.js +29 -14
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +2 -6
- package/dist/editor/Editor.d.ts +4 -3
- package/dist/editor/Editor.js +32 -22
- package/dist/editor/FormRenderer.js +34 -18
- package/dist/editor/PreviewFrame.d.ts +24 -0
- package/dist/editor/PreviewFrame.js +140 -0
- package/dist/editor/admin/SystemHealthPanel.js +47 -7
- package/dist/editor/components/BranchesDrawer.d.ts +9 -0
- package/dist/editor/components/BranchesDrawer.js +4 -0
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/context/AssetContext.d.ts +18 -35
- package/dist/editor/context/AssetContext.js +20 -29
- package/dist/editor/context/index.d.ts +1 -1
- package/dist/editor/context/index.js +1 -1
- package/dist/editor/editor-config.d.ts +1 -2
- package/dist/editor/editor-config.js +0 -23
- package/dist/editor/editor-utils.d.ts +24 -21
- package/dist/editor/editor-utils.js +75 -62
- package/dist/editor/fields/BlockField.d.ts +1 -0
- package/dist/editor/fields/BlockField.js +6 -4
- package/dist/editor/fields/CodeField.d.ts +1 -0
- package/dist/editor/fields/CodeField.js +2 -2
- package/dist/editor/fields/DateTimeField.d.ts +1 -0
- package/dist/editor/fields/DateTimeField.js +2 -2
- package/dist/editor/fields/FieldDescription.d.ts +16 -0
- package/dist/editor/fields/FieldDescription.js +11 -0
- package/dist/editor/fields/ImageField.d.ts +1 -0
- package/dist/editor/fields/ImageField.js +3 -2
- package/dist/editor/fields/InlineGroupField.js +4 -1
- package/dist/editor/fields/MarkdownField.d.ts +1 -0
- package/dist/editor/fields/MarkdownField.js +115 -20
- package/dist/editor/fields/NumberField.d.ts +1 -0
- package/dist/editor/fields/NumberField.js +2 -2
- package/dist/editor/fields/NumberListField.d.ts +1 -0
- package/dist/editor/fields/NumberListField.js +2 -2
- package/dist/editor/fields/ObjectField.d.ts +1 -0
- package/dist/editor/fields/ObjectField.js +5 -2
- package/dist/editor/fields/ReferenceField.d.ts +1 -0
- package/dist/editor/fields/ReferenceField.js +5 -4
- package/dist/editor/fields/SelectField.d.ts +1 -0
- package/dist/editor/fields/SelectField.js +2 -2
- package/dist/editor/fields/StringListField.d.ts +1 -0
- package/dist/editor/fields/StringListField.js +2 -2
- package/dist/editor/fields/TextField.d.ts +1 -0
- package/dist/editor/fields/TextField.js +2 -2
- package/dist/editor/fields/ToggleField.d.ts +1 -0
- package/dist/editor/fields/ToggleField.js +7 -4
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
- package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
- package/dist/editor/fields/mdx-jsx-support.js +137 -0
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +52 -0
- package/dist/editor/hooks/useBranchActions.d.ts +13 -2
- package/dist/editor/hooks/useBranchActions.js +48 -17
- package/dist/editor/hooks/useBranchManager.d.ts +21 -0
- package/dist/editor/hooks/useBranchManager.js +204 -37
- package/dist/editor/hooks/useBranchesData.d.ts +6 -0
- package/dist/editor/hooks/useBranchesData.js +9 -3
- package/dist/editor/hooks/useCommentSystem.js +25 -6
- package/dist/editor/hooks/useDraftManager.d.ts +16 -1
- package/dist/editor/hooks/useDraftManager.js +193 -36
- package/dist/editor/hooks/useEntriesData.js +3 -3
- package/dist/editor/hooks/useEntryManager.d.ts +2 -1
- package/dist/editor/hooks/useEntryManager.js +34 -19
- package/dist/editor/media/AssetCard.d.ts +1 -1
- package/dist/editor/media/crop-math.d.ts +3 -4
- package/dist/editor/media/crop-math.js +11 -22
- package/dist/editor/media/editor-image-src.d.ts +7 -0
- package/dist/editor/media/editor-image-src.js +12 -0
- package/dist/editor/preview-asset-base.d.ts +8 -0
- package/dist/editor/preview-asset-base.js +13 -0
- package/dist/editor/preview-bridge.d.ts +9 -21
- package/dist/editor/preview-bridge.js +19 -139
- package/dist/editor/preview-path.d.ts +10 -0
- package/dist/editor/preview-path.js +20 -0
- package/dist/editor/theme.js +2 -2
- package/dist/entry-schema-registry.d.ts +4 -4
- package/dist/entry-schema-registry.js +10 -6
- package/dist/entry-schema.d.ts +27 -4
- package/dist/entry-schema.js +20 -3
- package/dist/git-manager.d.ts +173 -6
- package/dist/git-manager.js +594 -76
- package/dist/github-service.d.ts +15 -0
- package/dist/github-service.js +19 -1
- package/dist/http/handler.js +37 -13
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +2 -0
- package/dist/http/router.d.ts +2 -0
- package/dist/http/router.js +2 -1
- package/dist/http/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +10 -0
- package/dist/http/worker-not-ready.js +24 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +1 -4
- package/dist/paths/branch-name.d.ts +5 -0
- package/dist/paths/branch-name.js +9 -0
- package/dist/paths/branch.d.ts +6 -2
- package/dist/paths/branch.js +16 -10
- package/dist/paths/index.d.ts +3 -3
- package/dist/paths/index.js +3 -3
- package/dist/paths/normalize.d.ts +7 -0
- package/dist/paths/normalize.js +9 -0
- package/dist/paths/validation.js +0 -2
- package/dist/preview.d.ts +8 -0
- package/dist/preview.js +7 -0
- package/dist/reference-resolver.d.ts +5 -21
- package/dist/reference-resolver.js +9 -44
- package/dist/resolve-canopy-user.js +2 -1
- package/dist/schema/schema-store.d.ts +54 -31
- package/dist/schema/schema-store.js +77 -31
- package/dist/server.d.ts +64 -9
- package/dist/server.js +48 -8
- package/dist/services.d.ts +18 -6
- package/dist/services.js +85 -70
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +197 -45
- package/dist/static/seo.d.ts +2 -14
- package/dist/static/seo.js +2 -25
- package/dist/submission-attribution.d.ts +73 -0
- package/dist/submission-attribution.js +221 -0
- package/dist/sync-core.d.ts +12 -1
- package/dist/sync-core.js +31 -13
- package/dist/task-queue/worker-status.d.ts +8 -0
- package/dist/task-queue/worker-status.js +17 -0
- package/dist/types.d.ts +31 -2
- package/dist/utils/content-serialize.d.ts +5 -2
- package/dist/utils/content-serialize.js +98 -24
- package/dist/utils/content-write-lock.d.ts +18 -7
- package/dist/utils/content-write-lock.js +18 -9
- package/dist/utils/debug.d.ts +8 -0
- package/dist/utils/debug.js +10 -2
- package/dist/utils/git.d.ts +32 -0
- package/dist/utils/git.js +42 -0
- package/dist/utils/occ-json-write.js +10 -2
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +35 -5
- package/dist/utils/provisioning-lock.js +73 -12
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/sanitize-href.d.ts +8 -22
- package/dist/utils/sanitize-href.js +11 -28
- package/dist/utils/url-prefix.d.ts +30 -0
- package/dist/utils/url-prefix.js +61 -0
- package/dist/utils/yaml-source-splice.d.ts +58 -0
- package/dist/utils/yaml-source-splice.js +515 -0
- package/dist/validation/entry-validator.js +9 -3
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/worker/canopy-state.d.ts +45 -0
- package/dist/worker/canopy-state.js +75 -0
- package/dist/worker/cms-worker.d.ts +8 -3
- package/dist/worker/cms-worker.js +38 -2
- package/dist/worker/git-sync.d.ts +30 -12
- package/dist/worker/git-sync.js +253 -58
- package/dist/worker/history-rewrite.d.ts +1 -1
- package/dist/worker/history-rewrite.js +1 -1
- package/dist/worker/provisioned-workspace.d.ts +35 -0
- package/dist/worker/provisioned-workspace.js +50 -0
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +95 -23
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +11 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +132 -0
- package/dist/worker/task-runner.js +99 -14
- package/dist/worker/worker-context.d.ts +6 -1
- package/package.json +8 -2
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { BranchListItem, CreateBranchBody } from '../../api/branch.js';
|
|
2
|
+
import { type CanopyApiClient } from '../../api/client.js';
|
|
3
|
+
/** @internal Exported for tests. Past the 60 s a Lambda behind CloudFront gets, so its 504 lands first. */
|
|
4
|
+
export declare const CREATE_BRANCH_DEADLINE_MS = 90000;
|
|
5
|
+
/** @internal Exported for tests. */
|
|
6
|
+
export declare const CREATE_TIMED_OUT_MESSAGE = "Creating the branch didn't finish (the server timed out). It's safe to try again.";
|
|
7
|
+
export type CreateBranchOutcome = {
|
|
8
|
+
kind: 'created';
|
|
9
|
+
branch: BranchListItem | undefined;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'failed';
|
|
12
|
+
message: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Create a branch. Past the deadline, after an error page from in front of the API, or after a
|
|
16
|
+
* busy 503 the branch may exist anyway, so the branch list decides; retrying is safe because a
|
|
17
|
+
* create publishes the whole branch or nothing. `userId` is the current user, the only creator
|
|
18
|
+
* whose listed branch can be this create's result.
|
|
19
|
+
*/
|
|
20
|
+
export declare function requestBranchCreate(apiClient: Pick<CanopyApiClient, 'branches'>, body: CreateBranchBody, userId: string | undefined, deadlineMs?: number): Promise<CreateBranchOutcome>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { isCreatorsRecentBranch } from '../../api/branch-create-window.js';
|
|
2
|
+
import { isNonApiResponse } from '../../api/client.js';
|
|
3
|
+
import { sanitizeBranchName } from '../../paths/branch-name.js';
|
|
4
|
+
/** @internal Exported for tests. Past the 60 s a Lambda behind CloudFront gets, so its 504 lands first. */
|
|
5
|
+
export const CREATE_BRANCH_DEADLINE_MS = 90_000;
|
|
6
|
+
/** @internal Exported for tests. */
|
|
7
|
+
export const CREATE_TIMED_OUT_MESSAGE = "Creating the branch didn't finish (the server timed out). It's safe to try again.";
|
|
8
|
+
const DEADLINE = Symbol('deadline');
|
|
9
|
+
/**
|
|
10
|
+
* Create a branch. Past the deadline, after an error page from in front of the API, or after a
|
|
11
|
+
* busy 503 the branch may exist anyway, so the branch list decides; retrying is safe because a
|
|
12
|
+
* create publishes the whole branch or nothing. `userId` is the current user, the only creator
|
|
13
|
+
* whose listed branch can be this create's result.
|
|
14
|
+
*/
|
|
15
|
+
export async function requestBranchCreate(apiClient, body, userId, deadlineMs = CREATE_BRANCH_DEADLINE_MS) {
|
|
16
|
+
let timer;
|
|
17
|
+
const deadline = new Promise((resolve) => {
|
|
18
|
+
timer = setTimeout(() => resolve(DEADLINE), deadlineMs);
|
|
19
|
+
});
|
|
20
|
+
const result = await Promise.race([apiClient.branches.create(body), deadline]).finally(() => clearTimeout(timer));
|
|
21
|
+
if (result === DEADLINE)
|
|
22
|
+
return settle(apiClient, body.branch, userId, CREATE_TIMED_OUT_MESSAGE);
|
|
23
|
+
if (result.ok)
|
|
24
|
+
return { kind: 'created', branch: result.data?.branch };
|
|
25
|
+
if (isNonApiResponse(result))
|
|
26
|
+
return settle(apiClient, body.branch, userId, CREATE_TIMED_OUT_MESSAGE);
|
|
27
|
+
const message = result.error || 'Failed to create branch';
|
|
28
|
+
return result.status === 503
|
|
29
|
+
? settle(apiClient, body.branch, userId, message)
|
|
30
|
+
: { kind: 'failed', message };
|
|
31
|
+
}
|
|
32
|
+
/** A same-named branch the server would not answer this create with is a name conflict. */
|
|
33
|
+
async function settle(apiClient, requested, userId, message) {
|
|
34
|
+
const listed = await findListedBranch(apiClient, requested);
|
|
35
|
+
if (!listed)
|
|
36
|
+
return { kind: 'failed', message };
|
|
37
|
+
if (isCreatorsRecentBranch(listed, userId))
|
|
38
|
+
return { kind: 'created', branch: listed };
|
|
39
|
+
return { kind: 'failed', message: `A branch named "${listed.name}" already exists` };
|
|
40
|
+
}
|
|
41
|
+
async function findListedBranch(apiClient, requested) {
|
|
42
|
+
const name = sanitizeBranchName(requested);
|
|
43
|
+
try {
|
|
44
|
+
const listing = await apiClient.branches.list();
|
|
45
|
+
if (!listing.ok)
|
|
46
|
+
return undefined;
|
|
47
|
+
return listing.data?.branches.find((branch) => sanitizeBranchName(branch.name) === name);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -1,17 +1,28 @@
|
|
|
1
|
+
import type { BranchListItem } from '../../api/branch.js';
|
|
2
|
+
import type { UnsavedSummary } from './useDraftManager.js';
|
|
1
3
|
export interface UseBranchActionsOptions {
|
|
2
4
|
branchName: string;
|
|
3
5
|
setBranchName: (name: string) => void;
|
|
4
|
-
|
|
6
|
+
/** From useDraftManager's `resolveUnsaved`: what a branch-level action would leave behind. */
|
|
7
|
+
getUnsaved: () => Promise<UnsavedSummary>;
|
|
5
8
|
onReloadBranches: () => Promise<void>;
|
|
9
|
+
/** Receives the branch the server just created, so it can be shown before any listing includes it. */
|
|
10
|
+
onBranchCreated: (branch: BranchListItem) => void;
|
|
6
11
|
onBranchSwitch?: (branch: string) => void;
|
|
12
|
+
/** The current user; a create the server never answered adopts only a listed branch they made. */
|
|
13
|
+
userId?: string;
|
|
7
14
|
}
|
|
8
15
|
export interface UseBranchActionsReturn {
|
|
9
16
|
handleBranchChange: (branch: string | null) => Promise<void>;
|
|
17
|
+
/** True when nothing is unsaved or the user accepted the warning. Run before any in-flight UI. */
|
|
18
|
+
confirmCreate: () => Promise<boolean>;
|
|
10
19
|
handleCreateBranch: (branch: {
|
|
11
20
|
name: string;
|
|
12
21
|
title?: string;
|
|
13
22
|
description?: string;
|
|
14
|
-
}) => Promise<
|
|
23
|
+
}) => Promise<boolean>;
|
|
24
|
+
/** True while the unsaved-changes confirm is on screen. */
|
|
25
|
+
confirmOpen: boolean;
|
|
15
26
|
}
|
|
16
27
|
/**
|
|
17
28
|
* Custom hook for branch navigation actions with dirty check support.
|
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import { modals } from '@mantine/modals';
|
|
3
4
|
import { notifications } from '@mantine/notifications';
|
|
4
5
|
import { Text } from '@mantine/core';
|
|
5
6
|
import { useApiClient } from '../context/index.js';
|
|
7
|
+
import { requestBranchCreate } from './create-branch-request.js';
|
|
8
|
+
const MAX_LISTED_LABELS = 5;
|
|
9
|
+
/** "Unsaved changes in: A, B." */
|
|
10
|
+
const describeUnsaved = ({ labels }) => {
|
|
11
|
+
if (labels.length === 0)
|
|
12
|
+
return 'You have unsaved changes.';
|
|
13
|
+
const shown = labels.slice(0, MAX_LISTED_LABELS).join(', ');
|
|
14
|
+
const rest = labels.length - MAX_LISTED_LABELS;
|
|
15
|
+
return `Unsaved changes in: ${shown}${rest > 0 ? ` and ${rest} more` : ''}.`;
|
|
16
|
+
};
|
|
6
17
|
/**
|
|
7
18
|
* Custom hook for branch navigation actions with dirty check support.
|
|
8
19
|
*/
|
|
9
20
|
export function useBranchActions(options) {
|
|
10
21
|
const apiClient = useApiClient();
|
|
22
|
+
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
11
23
|
const performBranchSwitch = (next) => {
|
|
12
24
|
options.setBranchName(next);
|
|
13
25
|
if (typeof window !== 'undefined') {
|
|
@@ -17,62 +29,81 @@ export function useBranchActions(options) {
|
|
|
17
29
|
}
|
|
18
30
|
options.onBranchSwitch?.(next);
|
|
19
31
|
};
|
|
20
|
-
const confirmIfDirty = async (
|
|
21
|
-
|
|
32
|
+
const confirmIfDirty = async (question) => {
|
|
33
|
+
const unsaved = await options.getUnsaved();
|
|
34
|
+
if (unsaved.count === 0)
|
|
22
35
|
return true;
|
|
23
36
|
return new Promise((resolve) => {
|
|
37
|
+
const settle = (value) => {
|
|
38
|
+
// Mantine calls onClose and onCancel from its modals reducer, which runs while
|
|
39
|
+
// ModalsProvider renders, so a setState here must be deferred; resolving needs no deferral.
|
|
40
|
+
queueMicrotask(() => setConfirmOpen(false));
|
|
41
|
+
resolve(value);
|
|
42
|
+
};
|
|
43
|
+
setConfirmOpen(true);
|
|
24
44
|
modals.openConfirmModal({
|
|
25
45
|
title: 'Unsaved Changes',
|
|
26
|
-
|
|
46
|
+
// Drafts are stored per branch, so leaving does not lose them.
|
|
47
|
+
children: (_jsxs(Text, { size: "sm", children: [describeUnsaved(unsaved), " Your drafts stay on \u201C", options.branchName, "\u201D and come back when you return. ", question] })),
|
|
27
48
|
labels: { confirm: 'Continue Anyway', cancel: 'Cancel' },
|
|
28
49
|
confirmProps: { color: 'red' },
|
|
29
|
-
onCancel: () =>
|
|
30
|
-
onConfirm: () =>
|
|
50
|
+
onCancel: () => settle(false),
|
|
51
|
+
onConfirm: () => settle(true),
|
|
52
|
+
// Escape and overlay dismissals fire only onClose. Mantine also calls it
|
|
53
|
+
// right after onConfirm, which is harmless: a promise settles once.
|
|
54
|
+
onClose: () => settle(false),
|
|
31
55
|
});
|
|
32
56
|
});
|
|
33
57
|
};
|
|
34
58
|
const handleBranchChange = async (next) => {
|
|
35
59
|
if (!next || next === options.branchName)
|
|
36
60
|
return;
|
|
37
|
-
const confirmed = await confirmIfDirty('
|
|
61
|
+
const confirmed = await confirmIfDirty('Switch branches anyway?');
|
|
38
62
|
if (!confirmed)
|
|
39
63
|
throw new Error('User cancelled branch switch');
|
|
40
64
|
performBranchSwitch(next);
|
|
41
65
|
};
|
|
66
|
+
const confirmCreate = () => confirmIfDirty('Create the new branch anyway?');
|
|
67
|
+
/** Resolves true when the branch was created and switched to, false when it was not. */
|
|
42
68
|
const handleCreateBranch = async (branch) => {
|
|
43
|
-
const confirmed = await confirmIfDirty('Create new branch without saving changes?');
|
|
44
|
-
if (!confirmed)
|
|
45
|
-
return;
|
|
46
69
|
try {
|
|
47
|
-
const
|
|
70
|
+
const outcome = await requestBranchCreate(apiClient, {
|
|
48
71
|
branch: branch.name,
|
|
49
72
|
title: branch.title,
|
|
50
73
|
description: branch.description,
|
|
51
|
-
});
|
|
52
|
-
if (
|
|
53
|
-
throw new Error(
|
|
74
|
+
}, options.userId);
|
|
75
|
+
if (outcome.kind === 'failed') {
|
|
76
|
+
throw new Error(outcome.message);
|
|
54
77
|
}
|
|
78
|
+
const created = outcome.branch;
|
|
79
|
+
if (created)
|
|
80
|
+
options.onBranchCreated(created);
|
|
55
81
|
// The server sanitizes the branch name (e.g. "feature/x" -> "feature-x")
|
|
56
82
|
// before persisting it. Adopt the canonical name from the response so
|
|
57
83
|
// the client doesn't end up stuck on a name the server never saved.
|
|
58
|
-
const createdName =
|
|
84
|
+
const createdName = created?.name ?? branch.name;
|
|
59
85
|
notifications.show({
|
|
60
86
|
message: createdName === branch.name
|
|
61
87
|
? `Branch "${createdName}" created`
|
|
62
88
|
: `Branch "${createdName}" created (renamed from "${branch.name}")`,
|
|
63
89
|
color: 'green',
|
|
64
90
|
});
|
|
65
|
-
await options.onReloadBranches();
|
|
66
|
-
// Switch to new branch (already confirmed dirty check)
|
|
67
91
|
performBranchSwitch(createdName);
|
|
92
|
+
// Not awaited: a listing can lag the create, and the switch must not wait
|
|
93
|
+
// on it. loadBranches reports its own failures and never rejects.
|
|
94
|
+
void options.onReloadBranches();
|
|
95
|
+
return true;
|
|
68
96
|
}
|
|
69
97
|
catch (err) {
|
|
70
98
|
const message = err instanceof Error ? err.message : 'Failed to create branch';
|
|
71
99
|
notifications.show({ message, color: 'red' });
|
|
100
|
+
return false;
|
|
72
101
|
}
|
|
73
102
|
};
|
|
74
103
|
return {
|
|
75
104
|
handleBranchChange,
|
|
105
|
+
confirmCreate,
|
|
76
106
|
handleCreateBranch,
|
|
107
|
+
confirmOpen,
|
|
77
108
|
};
|
|
78
109
|
}
|
|
@@ -2,6 +2,25 @@ import type { ConflictStatus, PullRequestState, SyncStatus } from '../../types.j
|
|
|
2
2
|
import type { OperatingMode } from '../../operating-mode/index.js';
|
|
3
3
|
import type { CommentThread } from '../../comment-store.js';
|
|
4
4
|
import type { BranchListItem } from '../../api/branch.js';
|
|
5
|
+
/** What the withdraw and delete dialogs say about the branch's pull request. */
|
|
6
|
+
type BranchPullRequest = Pick<BranchListItem, 'pullRequestNumber' | 'pullRequestState'>;
|
|
7
|
+
/**
|
|
8
|
+
* The withdraw dialog's pull-request bullet, matching what api/branch-withdraw.ts does: an open
|
|
9
|
+
* PR becomes a draft; a closed one is left alone and its number dropped from the branch, so a
|
|
10
|
+
* resubmit opens a new one. A merged PR archives its branch, which withdraw refuses, so that
|
|
11
|
+
* bullet is only seen on a listing that lags.
|
|
12
|
+
* @internal Exported for tests.
|
|
13
|
+
*/
|
|
14
|
+
export declare function withdrawPullRequestBullet(pr: BranchPullRequest): string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* How long a branch this session created stays overlaid on listings. Each warm
|
|
17
|
+
* server container caches the shared filesystem separately, so for its
|
|
18
|
+
* attribute/dentry cache window (~60s, see docs/concurrency.md window A) a
|
|
19
|
+
* listing can lack the branch even after another container's listing showed
|
|
20
|
+
* it. Twice that absorbs request latency; past it, listings alone decide.
|
|
21
|
+
* @internal Exported only for the test that pins it.
|
|
22
|
+
*/
|
|
23
|
+
export declare const CREATED_BRANCH_GRACE_MS = 120000;
|
|
5
24
|
interface BranchSummary {
|
|
6
25
|
name: string;
|
|
7
26
|
status: string;
|
|
@@ -58,6 +77,8 @@ export interface UseBranchManagerReturn {
|
|
|
58
77
|
branches: BranchListItem[];
|
|
59
78
|
branchSummaries: BranchSummary[];
|
|
60
79
|
currentBranch: BranchListItem | undefined;
|
|
80
|
+
/** Shows a branch the server just created before any listing includes it. */
|
|
81
|
+
addCreatedBranch: (branch: BranchListItem) => void;
|
|
61
82
|
handleSubmit: (branchName: string) => Promise<void>;
|
|
62
83
|
handleWithdraw: (branchName: string) => Promise<void>;
|
|
63
84
|
handleRequestChanges: (branchName: string) => Promise<void>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { useSWRConfig } from 'swr';
|
|
5
5
|
import { Text } from '@mantine/core';
|
|
6
6
|
import { modals } from '@mantine/modals';
|
|
@@ -58,30 +58,57 @@ const showSubmitConfirmation = (branchName, onConfirm, onDismiss) => {
|
|
|
58
58
|
...confirmModalHandlers(onConfirm, onDismiss),
|
|
59
59
|
});
|
|
60
60
|
};
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* The withdraw dialog's pull-request bullet, matching what api/branch-withdraw.ts does: an open
|
|
63
|
+
* PR becomes a draft; a closed one is left alone and its number dropped from the branch, so a
|
|
64
|
+
* resubmit opens a new one. A merged PR archives its branch, which withdraw refuses, so that
|
|
65
|
+
* bullet is only seen on a listing that lags.
|
|
66
|
+
* @internal Exported for tests.
|
|
67
|
+
*/
|
|
68
|
+
export function withdrawPullRequestBullet(pr) {
|
|
69
|
+
if (!pr.pullRequestNumber)
|
|
70
|
+
return undefined;
|
|
71
|
+
const label = `pull request #${pr.pullRequestNumber}`;
|
|
72
|
+
if (pr.pullRequestState === 'closed') {
|
|
73
|
+
return `Leave the closed ${label} as it is; submitting again opens a new one`;
|
|
74
|
+
}
|
|
75
|
+
if (pr.pullRequestState === 'merged')
|
|
76
|
+
return `Leave the merged ${label} as it is`;
|
|
77
|
+
return `Convert ${label} to a draft`;
|
|
78
|
+
}
|
|
79
|
+
const showWithdrawConfirmation = (branchName, pr, onConfirm, onDismiss) => {
|
|
80
|
+
const bullets = [
|
|
81
|
+
withdrawPullRequestBullet(pr),
|
|
82
|
+
'Change the branch status back to "editing"',
|
|
83
|
+
'Remove from review queue',
|
|
84
|
+
].filter((b) => b !== undefined);
|
|
62
85
|
modals.openConfirmModal({
|
|
63
86
|
title: 'Withdraw Branch from Review',
|
|
64
|
-
children: (_jsx(Text, { size: "sm", style: { whiteSpace: 'pre-line' }, children: `Are you sure you want to withdraw "${branchName}" from review?\n\nThis will:\n
|
|
87
|
+
children: (_jsx(Text, { size: "sm", style: { whiteSpace: 'pre-line' }, children: `Are you sure you want to withdraw "${branchName}" from review?\n\nThis will:\n${bullets.map((b) => `• ${b}`).join('\n')}` })),
|
|
65
88
|
labels: { confirm: 'Withdraw Branch', cancel: 'Cancel' },
|
|
66
89
|
confirmProps: { color: 'orange' },
|
|
67
90
|
...confirmModalHandlers(onConfirm, onDismiss),
|
|
68
91
|
});
|
|
69
92
|
};
|
|
70
93
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* `
|
|
75
|
-
* anywhere in the chain (BranchManager.tsx's delete button calls `onDelete`
|
|
76
|
-
* directly), while submit and withdraw -- both reversible -- each went
|
|
77
|
-
* through this same `modals.openConfirmModal` pattern. This closes that gap;
|
|
78
|
-
* `color: 'red'` marks it as the destructive one, matching the button's own
|
|
79
|
-
* color in BranchManager.tsx.
|
|
94
|
+
* Delete is the one irreversible branch action: it unlinks branch.json and removes the clone,
|
|
95
|
+
* the local mirror's head and, for a branch with a PR, the GitHub branch. BranchManager.tsx's
|
|
96
|
+
* delete button calls `onDelete` directly, so this dialog is the only confirmation; `color:
|
|
97
|
+
* 'red'` matches that button.
|
|
80
98
|
*/
|
|
81
|
-
const showDeleteConfirmation = (branchName, onConfirm, onDismiss) => {
|
|
99
|
+
const showDeleteConfirmation = (branchName, pr, onConfirm, onDismiss) => {
|
|
100
|
+
// The server deletes the GitHub branch only for a branch with a PR (api/github-sync.ts's
|
|
101
|
+
// syncDeleteRemoteBranch); GitHub closes a PR whose branch is deleted.
|
|
102
|
+
const bullets = [
|
|
103
|
+
'Permanently remove the branch and its clone',
|
|
104
|
+
pr.pullRequestNumber
|
|
105
|
+
? `Delete its branch on GitHub too, which closes pull request #${pr.pullRequestNumber} if it is still open`
|
|
106
|
+
: undefined,
|
|
107
|
+
'Discard any unsaved or unmerged changes',
|
|
108
|
+
].filter((b) => b !== undefined);
|
|
82
109
|
modals.openConfirmModal({
|
|
83
110
|
title: 'Delete Branch',
|
|
84
|
-
children: (_jsx(Text, { size: "sm", style: { whiteSpace: 'pre-line' }, children: `Are you sure you want to delete "${branchName}"?\n\nThis will:\n
|
|
111
|
+
children: (_jsx(Text, { size: "sm", style: { whiteSpace: 'pre-line' }, children: `Are you sure you want to delete "${branchName}"?\n\nThis will:\n${bullets.map((b) => `• ${b}`).join('\n')}\n\nThis cannot be undone.` })),
|
|
85
112
|
labels: { confirm: 'Delete Branch', cancel: 'Cancel' },
|
|
86
113
|
// data-testid is load-bearing, not decoration: the e2e helper
|
|
87
114
|
// (apps/test-app/e2e/fixtures/branch-page.ts's deleteBranch) already
|
|
@@ -92,6 +119,46 @@ const showDeleteConfirmation = (branchName, onConfirm, onDismiss) => {
|
|
|
92
119
|
...confirmModalHandlers(onConfirm, onDismiss),
|
|
93
120
|
});
|
|
94
121
|
};
|
|
122
|
+
/**
|
|
123
|
+
* How long a branch this session created stays overlaid on listings. Each warm
|
|
124
|
+
* server container caches the shared filesystem separately, so for its
|
|
125
|
+
* attribute/dentry cache window (~60s, see docs/concurrency.md window A) a
|
|
126
|
+
* listing can lack the branch even after another container's listing showed
|
|
127
|
+
* it. Twice that absorbs request latency; past it, listings alone decide.
|
|
128
|
+
* @internal Exported only for the test that pins it.
|
|
129
|
+
*/
|
|
130
|
+
export const CREATED_BRANCH_GRACE_MS = 120_000;
|
|
131
|
+
/** The server listing plus any pending branches it lacks; the server's copy always wins. */
|
|
132
|
+
function mergePendingBranches(listed, pending) {
|
|
133
|
+
const listedNames = new Set(listed.map((b) => b.name));
|
|
134
|
+
const missing = pending.filter((p) => !listedNames.has(p.branch.name));
|
|
135
|
+
return missing.length === 0 ? listed : [...listed, ...missing.map((p) => p.branch)];
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Applies one received listing to the pending branches: drops those it arrived
|
|
139
|
+
* past the grace window for, and takes the listing's copy of the rest. Written
|
|
140
|
+
* so a listing with no arrival time (NaN) drops nothing.
|
|
141
|
+
*/
|
|
142
|
+
function reconcilePendingBranches(pending, listed, receivedAt) {
|
|
143
|
+
const listedByName = new Map(listed.map((b) => [b.name, b]));
|
|
144
|
+
let changed = false;
|
|
145
|
+
const next = [];
|
|
146
|
+
for (const p of pending) {
|
|
147
|
+
if (receivedAt - p.addedAt > CREATED_BRANCH_GRACE_MS) {
|
|
148
|
+
changed = true;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const listedCopy = listedByName.get(p.branch.name);
|
|
152
|
+
if (listedCopy && listedCopy !== p.branch) {
|
|
153
|
+
changed = true;
|
|
154
|
+
next.push({ branch: listedCopy, addedAt: p.addedAt });
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
next.push(p);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return changed ? next : pending;
|
|
161
|
+
}
|
|
95
162
|
/**
|
|
96
163
|
* Custom hook for managing git branches.
|
|
97
164
|
*/
|
|
@@ -100,7 +167,40 @@ export function useBranchManager(options) {
|
|
|
100
167
|
const { mutate: globalMutate } = useSWRConfig();
|
|
101
168
|
const [branchName, setBranchName] = useState(options.initialBranch);
|
|
102
169
|
const { data: branchesData, error: branchesError, isValidating: branchesIsValidating, } = useBranchesData(apiClient);
|
|
103
|
-
const
|
|
170
|
+
const [pendingBranches, setPendingBranches] = useState([]);
|
|
171
|
+
const branches = useMemo(() => mergePendingBranches(branchesData?.branches ?? [], pendingBranches), [branchesData, pendingBranches]);
|
|
172
|
+
const addCreatedBranch = useCallback((branch) => {
|
|
173
|
+
setPendingBranches((prev) => [
|
|
174
|
+
...prev.filter((p) => p.branch.name !== branch.name),
|
|
175
|
+
{ branch, addedAt: Date.now() },
|
|
176
|
+
]);
|
|
177
|
+
}, []);
|
|
178
|
+
const forgetCreatedBranch = (name) => {
|
|
179
|
+
setPendingBranches((prev) => {
|
|
180
|
+
const kept = prev.filter((p) => p.branch.name !== name);
|
|
181
|
+
return kept.length === prev.length ? prev : kept;
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
// After a workflow action, a pending branch's overlaid copy is stale. The
|
|
185
|
+
// server's returned copy replaces it, so a listing that still lacks the
|
|
186
|
+
// branch shows its new status instead of hiding it; without one, forgetting
|
|
187
|
+
// it fails closed rather than showing the pre-action state.
|
|
188
|
+
const updateCreatedBranch = (name, branch) => {
|
|
189
|
+
if (!branch) {
|
|
190
|
+
forgetCreatedBranch(name);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
setPendingBranches((prev) => prev.some((p) => p.branch.name === name)
|
|
194
|
+
? prev.map((p) => (p.branch.name === name ? { ...p, branch } : p))
|
|
195
|
+
: prev);
|
|
196
|
+
};
|
|
197
|
+
// Each received listing is a new `branchesData` (see `BranchesData.receivedAt`);
|
|
198
|
+
// the grace window is measured to its arrival.
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (!branchesData)
|
|
201
|
+
return;
|
|
202
|
+
setPendingBranches((prev) => reconcilePendingBranches(prev, branchesData.branches, branchesData.receivedAt));
|
|
203
|
+
}, [branchesData]);
|
|
104
204
|
// Adopt the server's default branch once data arrives, if nothing pinned one.
|
|
105
205
|
useEffect(() => {
|
|
106
206
|
if (!branchName && branchesData?.defaultBranch) {
|
|
@@ -190,11 +290,13 @@ export function useBranchManager(options) {
|
|
|
190
290
|
// bound hook (and anything else reading BRANCHES_KEY) picks it up.
|
|
191
291
|
// Side effects (default-branch adoption, error/success notifications) are
|
|
192
292
|
// driven by the effects above, which react to that same cache write.
|
|
193
|
-
|
|
293
|
+
// Resolves to the fresh listing, or undefined when the fetch failed.
|
|
294
|
+
const reloadBranches = async () => {
|
|
194
295
|
options.setBusy(true);
|
|
195
296
|
try {
|
|
196
297
|
const fresh = await fetchBranches(apiClient);
|
|
197
298
|
await globalMutate(BRANCHES_KEY, fresh, { revalidate: false });
|
|
299
|
+
return fresh;
|
|
198
300
|
}
|
|
199
301
|
catch (err) {
|
|
200
302
|
console.error(err);
|
|
@@ -205,14 +307,61 @@ export function useBranchManager(options) {
|
|
|
205
307
|
color: 'red',
|
|
206
308
|
autoClose: false,
|
|
207
309
|
});
|
|
310
|
+
return undefined;
|
|
208
311
|
}
|
|
209
312
|
finally {
|
|
210
313
|
options.setBusy(false);
|
|
211
314
|
}
|
|
212
315
|
};
|
|
213
|
-
const
|
|
214
|
-
|
|
316
|
+
const loadBranches = async () => {
|
|
317
|
+
await reloadBranches();
|
|
318
|
+
};
|
|
319
|
+
// The confirm dialogs' handlers run long after the render that opened them, so they read
|
|
320
|
+
// these at confirm time rather than from that render's closure.
|
|
321
|
+
const latest = useRef({ branchName, branches, branchesData });
|
|
322
|
+
latest.current = { branchName, branches, branchesData };
|
|
323
|
+
// A legacy deep link can carry the raw form of a listed name; the actions key, look up and
|
|
324
|
+
// send the listed name, resolved the way `currentBranch` resolves it.
|
|
325
|
+
const listedName = (name) => latest.current.branches.some((b) => b.name === name) ? name : sanitizeBranchName(name);
|
|
326
|
+
const pullRequestOf = (name) => {
|
|
327
|
+
const b = latest.current.branches.find((x) => x.name === name);
|
|
328
|
+
return { pullRequestNumber: b?.pullRequestNumber, pullRequestState: b?.pullRequestState };
|
|
329
|
+
};
|
|
330
|
+
// Keys of the actions whose confirm is open or whose work is in flight.
|
|
331
|
+
const actionsInFlight = useRef(new Set());
|
|
332
|
+
/**
|
|
333
|
+
* Runs `run` unless the same action on the same branch already has its confirm open or its
|
|
334
|
+
* work in flight; then this request opens nothing and resolves at once. Mantine closes only
|
|
335
|
+
* the confirm that was confirmed, so a second one opened by a repeated click would stay up
|
|
336
|
+
* after the action succeeded.
|
|
337
|
+
*/
|
|
338
|
+
const singleFlight = async (action, branch, run) => {
|
|
339
|
+
const key = JSON.stringify([action, branch]);
|
|
340
|
+
if (actionsInFlight.current.has(key))
|
|
341
|
+
return;
|
|
342
|
+
actionsInFlight.current.add(key);
|
|
343
|
+
try {
|
|
344
|
+
await run();
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
actionsInFlight.current.delete(key);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
const handleSubmit = (requested) => {
|
|
351
|
+
const branchNameToSubmit = listedName(requested);
|
|
352
|
+
return singleFlight('submit', branchNameToSubmit, () => new Promise((resolve, reject) => {
|
|
215
353
|
showSubmitConfirmation(branchNameToSubmit, async () => {
|
|
354
|
+
// A listing that arrived while the dialog was open may show the branch already
|
|
355
|
+
// submitted; submitting it again would only fail.
|
|
356
|
+
const listed = latest.current.branches.find((b) => b.name === branchNameToSubmit);
|
|
357
|
+
if (listed?.status === 'submitted') {
|
|
358
|
+
notifications.show({
|
|
359
|
+
message: 'Branch is already submitted for review',
|
|
360
|
+
color: 'blue',
|
|
361
|
+
});
|
|
362
|
+
resolve();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
216
365
|
options.setBusy(true);
|
|
217
366
|
try {
|
|
218
367
|
const result = await apiClient.workflow.submit({
|
|
@@ -225,6 +374,7 @@ export function useBranchManager(options) {
|
|
|
225
374
|
message: 'Branch submitted for review',
|
|
226
375
|
color: 'green',
|
|
227
376
|
});
|
|
377
|
+
updateCreatedBranch(branchNameToSubmit, result.data?.branch);
|
|
228
378
|
await loadBranches();
|
|
229
379
|
resolve();
|
|
230
380
|
}
|
|
@@ -237,17 +387,15 @@ export function useBranchManager(options) {
|
|
|
237
387
|
options.setBusy(false);
|
|
238
388
|
}
|
|
239
389
|
},
|
|
240
|
-
// Dismissal is not an error
|
|
241
|
-
//
|
|
242
|
-
// these into console.error), which is both noise and a trip hazard
|
|
243
|
-
// for the CI=1 stray-console gate. Settled once, by onCancel OR
|
|
244
|
-
// onClose, so an Escape/overlay dismissal can't leave this pending.
|
|
390
|
+
// Dismissal is not an error: the call sites log rejections to the console. Settled
|
|
391
|
+
// once, by onCancel OR onClose, so an Escape/overlay dismissal can't leave this pending.
|
|
245
392
|
() => resolve());
|
|
246
|
-
});
|
|
393
|
+
}));
|
|
247
394
|
};
|
|
248
|
-
const handleWithdraw =
|
|
249
|
-
|
|
250
|
-
|
|
395
|
+
const handleWithdraw = (requested) => {
|
|
396
|
+
const branchNameToWithdraw = listedName(requested);
|
|
397
|
+
return singleFlight('withdraw', branchNameToWithdraw, () => new Promise((resolve, reject) => {
|
|
398
|
+
showWithdrawConfirmation(branchNameToWithdraw, pullRequestOf(branchNameToWithdraw), async () => {
|
|
251
399
|
options.setBusy(true);
|
|
252
400
|
try {
|
|
253
401
|
const result = await apiClient.workflow.withdraw({
|
|
@@ -257,6 +405,7 @@ export function useBranchManager(options) {
|
|
|
257
405
|
throw new Error(result.error || 'Failed to withdraw branch');
|
|
258
406
|
}
|
|
259
407
|
notifications.show({ message: 'Branch withdrawn', color: 'blue' });
|
|
408
|
+
updateCreatedBranch(branchNameToWithdraw, result.data?.branch);
|
|
260
409
|
await loadBranches();
|
|
261
410
|
resolve();
|
|
262
411
|
}
|
|
@@ -269,7 +418,7 @@ export function useBranchManager(options) {
|
|
|
269
418
|
options.setBusy(false);
|
|
270
419
|
}
|
|
271
420
|
}, () => resolve());
|
|
272
|
-
});
|
|
421
|
+
}));
|
|
273
422
|
};
|
|
274
423
|
const handleRequestChanges = async (branchNameForChanges) => {
|
|
275
424
|
options.setBusy(true);
|
|
@@ -279,6 +428,7 @@ export function useBranchManager(options) {
|
|
|
279
428
|
throw new Error(result.error || 'Failed to request changes');
|
|
280
429
|
}
|
|
281
430
|
notifications.show({ message: 'Changes requested', color: 'orange' });
|
|
431
|
+
updateCreatedBranch(branchNameForChanges, result.data?.branch);
|
|
282
432
|
await loadBranches();
|
|
283
433
|
}
|
|
284
434
|
catch (err) {
|
|
@@ -289,9 +439,10 @@ export function useBranchManager(options) {
|
|
|
289
439
|
options.setBusy(false);
|
|
290
440
|
}
|
|
291
441
|
};
|
|
292
|
-
const handleDelete =
|
|
293
|
-
|
|
294
|
-
|
|
442
|
+
const handleDelete = (requested) => {
|
|
443
|
+
const branchNameToDelete = listedName(requested);
|
|
444
|
+
return singleFlight('delete', branchNameToDelete, () => new Promise((resolve) => {
|
|
445
|
+
showDeleteConfirmation(branchNameToDelete, pullRequestOf(branchNameToDelete), async () => {
|
|
295
446
|
options.setBusy(true);
|
|
296
447
|
try {
|
|
297
448
|
const result = await apiClient.branches.delete({
|
|
@@ -300,8 +451,25 @@ export function useBranchManager(options) {
|
|
|
300
451
|
if (!result.ok) {
|
|
301
452
|
throw new Error(result.error || 'Failed to delete branch');
|
|
302
453
|
}
|
|
303
|
-
|
|
304
|
-
|
|
454
|
+
const cleanupWarning = result.data?.cleanupWarning;
|
|
455
|
+
notifications.show(cleanupWarning
|
|
456
|
+
? {
|
|
457
|
+
message: `Branch deleted, with a warning: ${cleanupWarning}`,
|
|
458
|
+
color: 'yellow',
|
|
459
|
+
autoClose: false,
|
|
460
|
+
}
|
|
461
|
+
: { message: 'Branch deleted', color: 'green' });
|
|
462
|
+
forgetCreatedBranch(branchNameToDelete);
|
|
463
|
+
const fresh = await reloadBranches();
|
|
464
|
+
// Matched the way `currentBranch` resolves a name, since the deleted branch is
|
|
465
|
+
// no longer listed to resolve against.
|
|
466
|
+
const open = latest.current.branchName;
|
|
467
|
+
if (open === branchNameToDelete ||
|
|
468
|
+
sanitizeBranchName(open) === branchNameToDelete) {
|
|
469
|
+
// Nothing is left to keep, so no unsaved-changes prompt. With no default known
|
|
470
|
+
// yet, the empty name lets the default-branch adoption effect pick it.
|
|
471
|
+
setBranchName(fresh?.defaultBranch ?? latest.current.branchesData?.defaultBranch ?? '');
|
|
472
|
+
}
|
|
305
473
|
}
|
|
306
474
|
catch (err) {
|
|
307
475
|
const message = err instanceof Error ? err.message : 'Failed to delete branch';
|
|
@@ -309,13 +477,11 @@ export function useBranchManager(options) {
|
|
|
309
477
|
}
|
|
310
478
|
finally {
|
|
311
479
|
options.setBusy(false);
|
|
312
|
-
//
|
|
313
|
-
// pre-confirmation behavior (errors were shown as a toast, not
|
|
314
|
-
// rethrown). Cancellation resolves too -- see below.
|
|
480
|
+
// Failures surface as the toast above, never as a rejection; cancelling resolves too.
|
|
315
481
|
resolve();
|
|
316
482
|
}
|
|
317
483
|
}, () => resolve());
|
|
318
|
-
});
|
|
484
|
+
}));
|
|
319
485
|
};
|
|
320
486
|
const handleReloadBranchData = async () => {
|
|
321
487
|
await loadBranches();
|
|
@@ -338,6 +504,7 @@ export function useBranchManager(options) {
|
|
|
338
504
|
branches,
|
|
339
505
|
branchSummaries,
|
|
340
506
|
currentBranch,
|
|
507
|
+
addCreatedBranch,
|
|
341
508
|
handleSubmit,
|
|
342
509
|
handleWithdraw,
|
|
343
510
|
handleRequestChanges,
|
|
@@ -19,6 +19,12 @@ export declare const BRANCHES_KEY = "canopy:branches";
|
|
|
19
19
|
export interface BranchesData {
|
|
20
20
|
branches: BranchListItem[];
|
|
21
21
|
defaultBranch?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Client clock when this listing arrived. It makes listings fetched at
|
|
24
|
+
* different moments distinct `data` values even when their branches match,
|
|
25
|
+
* while a failed fetch (SWR keeps the previous `data`) changes nothing.
|
|
26
|
+
*/
|
|
27
|
+
receivedAt: number;
|
|
22
28
|
}
|
|
23
29
|
/**
|
|
24
30
|
* Fetch the branches list. Exported for direct testing and for the
|