canopycms 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +188 -47
- package/dist/api/branch-review.d.ts +11 -1
- package/dist/api/branch-review.js +96 -18
- package/dist/api/branch-status.d.ts +26 -2
- package/dist/api/branch-status.js +38 -2
- package/dist/api/branch-withdraw.js +4 -7
- package/dist/api/branch.d.ts +2 -0
- package/dist/api/client.d.ts +3 -3
- package/dist/api/client.js +4 -4
- package/dist/api/comments.d.ts +2 -2
- package/dist/api/comments.js +4 -0
- package/dist/api/content.d.ts +2 -2
- package/dist/api/github-sync.d.ts +4 -0
- package/dist/api/github-sync.js +38 -6
- package/dist/api/pr-comment-sync.d.ts +18 -0
- package/dist/api/pr-comment-sync.js +77 -0
- package/dist/api/request-deadline.d.ts +15 -7
- package/dist/api/request-deadline.js +15 -7
- package/dist/api/schema.d.ts +16 -16
- package/dist/authorization/review-role.d.ts +33 -0
- package/dist/authorization/review-role.js +17 -0
- package/dist/branch-metadata-file.d.ts +3 -0
- package/dist/branch-metadata-file.js +1 -0
- package/dist/cli/cli.js +699 -130
- package/dist/cli/generate-ai-content.js +524 -59
- package/dist/cli/github-app-output.d.ts +34 -0
- package/dist/cli/github-app-output.js +85 -0
- package/dist/cli/init-github-app.d.ts +18 -0
- package/dist/cli/init-github-app.js +66 -58
- package/dist/cli/init.js +10 -1
- package/dist/comment-store.d.ts +5 -0
- package/dist/comment-store.js +24 -0
- package/dist/config/helpers.js +1 -3
- package/dist/config/schemas/collection.d.ts +2 -2
- package/dist/config/schemas/config.d.ts +3 -0
- package/dist/config/schemas/config.js +2 -1
- package/dist/config/schemas/url.d.ts +6 -0
- package/dist/config/schemas/url.js +16 -0
- package/dist/config/types.d.ts +12 -1
- package/dist/content-id-index.d.ts +9 -0
- package/dist/content-id-index.js +31 -0
- package/dist/editor/BranchManager.d.ts +3 -0
- package/dist/editor/BranchManager.js +75 -44
- package/dist/editor/CanopyEditor.js +2 -2
- package/dist/editor/CanopyEditorPage.js +3 -4
- package/dist/editor/Editor.js +47 -8
- package/dist/editor/branch-lock.d.ts +7 -0
- package/dist/editor/branch-lock.js +9 -2
- package/dist/editor/client-reference-resolver.d.ts +5 -3
- package/dist/editor/client-reference-resolver.js +10 -5
- package/dist/editor/components/BranchesLoadFailedNotice.d.ts +13 -0
- package/dist/editor/components/BranchesLoadFailedNotice.js +12 -0
- package/dist/editor/components/EditorHeader.d.ts +10 -4
- package/dist/editor/components/EditorHeader.js +108 -28
- package/dist/editor/components/EntryHeader.d.ts +5 -1
- package/dist/editor/components/EntryHeader.js +3 -1
- package/dist/editor/components/ReasonedMenuItem.d.ts +8 -0
- package/dist/editor/components/ReasonedMenuItem.js +2 -2
- package/dist/editor/components/ReviewDialogs.d.ts +19 -0
- package/dist/editor/components/ReviewDialogs.js +41 -0
- package/dist/editor/copy.d.ts +4 -0
- package/dist/editor/copy.js +4 -0
- package/dist/editor/fields/MarkdownField.js +1 -1
- package/dist/editor/hooks/create-branch-request.d.ts +6 -2
- package/dist/editor/hooks/create-branch-request.js +7 -2
- package/dist/editor/hooks/useBranchManager.d.ts +7 -2
- package/dist/editor/hooks/useBranchManager.js +252 -37
- package/dist/editor-url.d.ts +20 -0
- package/dist/editor-url.js +71 -0
- package/dist/git-manager.js +10 -6
- package/dist/github-service.d.ts +2 -2
- package/dist/github-service.js +4 -2
- package/dist/pr-comments.d.ts +33 -0
- package/dist/pr-comments.js +41 -0
- package/dist/schema/schema-store.d.ts +8 -8
- package/dist/services.d.ts +2 -17
- package/dist/services.js +38 -1
- package/dist/settings-workspace.d.ts +21 -7
- package/dist/settings-workspace.js +91 -16
- package/dist/submission-attribution.d.ts +38 -1
- package/dist/submission-attribution.js +197 -18
- package/dist/task-queue/cms-task-queue.d.ts +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/utils/content-serialize.js +7 -6
- package/dist/utils/markdown-body-splice.d.ts +15 -10
- package/dist/utils/markdown-body-splice.js +71 -28
- package/dist/utils/markdown-export-options.d.ts +13 -0
- package/dist/utils/markdown-export-options.js +13 -0
- package/dist/utils/markdown-restyle.d.ts +58 -0
- package/dist/utils/markdown-restyle.js +482 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/bundle-header.d.ts +13 -9
- package/dist/worker/bundle-header.js +17 -11
- package/dist/worker/git-process.d.ts +2 -2
- package/dist/worker/git-sync.js +5 -0
- package/dist/worker/github-gateway-http.js +3 -2
- package/dist/worker/github-gateway-wire.d.ts +3 -0
- package/dist/worker/github-gateway-wire.js +1 -0
- package/dist/worker/github-gateway.d.ts +1 -1
- package/dist/worker/github-transfer.js +72 -2
- package/dist/worker/task-runner.js +53 -3
- package/package.json +2 -2
- package/dist/editor/fields/markdown-export-options.d.ts +0 -2
- package/dist/editor/fields/markdown-export-options.js +0 -8
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button, Stack, Text } from '@mantine/core';
|
|
3
|
+
/**
|
|
4
|
+
* The error state of an editor no branch is pinned in, while the branch list it takes its
|
|
5
|
+
* branch from has failed to load. It outlasts the dismissible toast for the same failure.
|
|
6
|
+
*/
|
|
7
|
+
export function BranchesLoadFailedNotice({ onRetry, retrying = false, align = 'flex-start', 'data-testid': testId, }) {
|
|
8
|
+
return (_jsxs(Stack, { gap: 4, py: "sm", align: align, "data-testid": testId, children: [_jsx(Text, { size: "sm", c: "var(--canopy-danger-text)", children: "Couldn't load the branch list, so no branch can open." }), onRetry && (_jsx(Button, { size: "compact-sm", variant: "subtle", "aria-disabled": retrying || undefined, style: { cursor: retrying ? 'default' : undefined }, onClick: () => {
|
|
9
|
+
if (!retrying)
|
|
10
|
+
onRetry();
|
|
11
|
+
}, children: retrying ? 'Retrying…' : 'Retry' }))] }));
|
|
12
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Ref } from 'react';
|
|
2
|
-
import type { BranchStatus } from '../../types.js';
|
|
2
|
+
import type { BranchStatus, PullRequestState } from '../../types.js';
|
|
3
3
|
import type { SaveAllProgress } from '../hooks/save-all.js';
|
|
4
4
|
export interface EditorHeaderProps {
|
|
5
5
|
branchName: string;
|
|
@@ -27,6 +27,10 @@ export interface EditorHeaderProps {
|
|
|
27
27
|
onSubmit: (returnFocusTo?: () => HTMLElement | null) => void;
|
|
28
28
|
/** Opens the withdraw dialog, which returns focus to `returnFocusTo()` when given. */
|
|
29
29
|
onWithdraw: (returnFocusTo?: () => HTMLElement | null) => void;
|
|
30
|
+
/** Opens the approve dialog, which returns focus to `returnFocusTo()` when given. */
|
|
31
|
+
onApprove?: (returnFocusTo?: () => HTMLElement | null) => void;
|
|
32
|
+
/** Opens the request-changes dialog, which returns focus to `returnFocusTo()` when given. */
|
|
33
|
+
onRequestChanges?: (returnFocusTo?: () => HTMLElement | null) => void;
|
|
30
34
|
/** Writing drafts to localStorage failed, so they live only in this tab. */
|
|
31
35
|
draftStorageFailed?: boolean;
|
|
32
36
|
/** Current branch status (undefined if unknown). */
|
|
@@ -37,6 +41,8 @@ export interface EditorHeaderProps {
|
|
|
37
41
|
groups?: string[];
|
|
38
42
|
};
|
|
39
43
|
branchCreatedBy?: string;
|
|
44
|
+
/** The PR's state as last observed; a closed one blocks Approve and Request changes. */
|
|
45
|
+
branchPullRequestState?: PullRequestState;
|
|
40
46
|
branchAccess?: {
|
|
41
47
|
allowedUsers?: string[];
|
|
42
48
|
allowedGroups?: string[];
|
|
@@ -63,7 +69,7 @@ export interface EditorHeaderProps {
|
|
|
63
69
|
}
|
|
64
70
|
/**
|
|
65
71
|
* The branch bar: the pill, the branch's status and its unsaved count, then the draft-storage
|
|
66
|
-
* warning, Save all, the way forward and Submit or Withdraw
|
|
67
|
-
* `EntryHeader`.
|
|
72
|
+
* warning, Save all, the way forward and Submit or Withdraw, or to a reviewer of someone else's
|
|
73
|
+
* submission Approve and Request changes. The open entry's own controls are in `EntryHeader`.
|
|
68
74
|
*/
|
|
69
|
-
export declare function EditorHeader({ branchName, busy, modifiedCount, onShowUnsaved, branchToggleRef, onBranchReloadData, onBranchDiscardDrafts, onBranchManagerOpen, onNewBranch, onSaveAll, saveAllProgress, singleSaving, entryListFailed, onSubmit, onWithdraw, draftStorageFailed, branchStatus, userContext, branchCreatedBy, branchAccess, branchIsProtected, branchServerProtected, branchReadOnly, branchWriteBlocked, branchLoading, branchPending, branchMissing, }: EditorHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
75
|
+
export declare function EditorHeader({ branchName, busy, modifiedCount, onShowUnsaved, branchToggleRef, onBranchReloadData, onBranchDiscardDrafts, onBranchManagerOpen, onNewBranch, onSaveAll, saveAllProgress, singleSaving, entryListFailed, onSubmit, onWithdraw, onApprove, onRequestChanges, draftStorageFailed, branchStatus, userContext, branchCreatedBy, branchPullRequestState, branchAccess, branchIsProtected, branchServerProtected, branchReadOnly, branchWriteBlocked, branchLoading, branchPending, branchMissing, }: EditorHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -6,6 +6,7 @@ import { IconChevronDown, IconLock } from '@tabler/icons-react';
|
|
|
6
6
|
import { isAdmin, isLockedOutOfBranch, isReviewer } from '../../authorization/helpers.js';
|
|
7
7
|
import { branchBarStatus, describeBranchLock } from '../branch-lock.js';
|
|
8
8
|
import { EDITOR_ACTIONS } from '../copy.js';
|
|
9
|
+
import { reviewRoleOf } from '../../authorization/review-role.js';
|
|
9
10
|
import { SYSTEM_USER_ID } from '../../system-users.js';
|
|
10
11
|
import { TopBar } from '../shell/TopBar.js';
|
|
11
12
|
import { BranchPill } from './BranchPill.js';
|
|
@@ -15,18 +16,18 @@ import { DraftStorageWarning, HIDDEN_STYLE, SaveAllButton } from './SaveAllButto
|
|
|
15
16
|
import { useFocusHandOff, useMenuItemFocusReturn } from '../utils/focus-return.js';
|
|
16
17
|
/**
|
|
17
18
|
* The branch bar: the pill, the branch's status and its unsaved count, then the draft-storage
|
|
18
|
-
* warning, Save all, the way forward and Submit or Withdraw
|
|
19
|
-
* `EntryHeader`.
|
|
19
|
+
* warning, Save all, the way forward and Submit or Withdraw, or to a reviewer of someone else's
|
|
20
|
+
* submission Approve and Request changes. The open entry's own controls are in `EntryHeader`.
|
|
20
21
|
*/
|
|
21
|
-
export function EditorHeader({ branchName, busy, modifiedCount, onShowUnsaved, branchToggleRef, onBranchReloadData, onBranchDiscardDrafts, onBranchManagerOpen, onNewBranch = onBranchManagerOpen, onSaveAll, saveAllProgress, singleSaving = false, entryListFailed = false, onSubmit, onWithdraw, draftStorageFailed = false, branchStatus, userContext, branchCreatedBy, branchAccess, branchIsProtected = false, branchServerProtected, branchReadOnly = false, branchWriteBlocked = false, branchLoading = false, branchPending = false, branchMissing = false, }) {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
export function EditorHeader({ branchName, busy, modifiedCount, onShowUnsaved, branchToggleRef, onBranchReloadData, onBranchDiscardDrafts, onBranchManagerOpen, onNewBranch = onBranchManagerOpen, onSaveAll, saveAllProgress, singleSaving = false, entryListFailed = false, onSubmit, onWithdraw, onApprove = noopAction, onRequestChanges = noopAction, draftStorageFailed = false, branchStatus, userContext, branchCreatedBy, branchPullRequestState, branchAccess, branchIsProtected = false, branchServerProtected, branchReadOnly = false, branchWriteBlocked = false, branchLoading = false, branchPending = false, branchMissing = false, }) {
|
|
23
|
+
const reviewRole = reviewRoleOf({
|
|
24
|
+
status: branchStatus,
|
|
25
|
+
createdBy: branchCreatedBy,
|
|
26
|
+
users: branchAccess?.allowedUsers,
|
|
27
|
+
groups: branchAccess?.allowedGroups,
|
|
28
|
+
isProtected: branchIsProtected,
|
|
29
|
+
pullRequestState: branchPullRequestState,
|
|
30
|
+
}, userContext);
|
|
30
31
|
const workflow = workflowAction({
|
|
31
32
|
branchName,
|
|
32
33
|
busy,
|
|
@@ -35,13 +36,31 @@ export function EditorHeader({ branchName, busy, modifiedCount, onShowUnsaved, b
|
|
|
35
36
|
branchCreatedBy,
|
|
36
37
|
branchAccess,
|
|
37
38
|
branchIsProtected,
|
|
39
|
+
reviewRole,
|
|
38
40
|
savingAll: !!saveAllProgress,
|
|
39
41
|
});
|
|
42
|
+
const lock = {
|
|
43
|
+
branchStatus,
|
|
44
|
+
branchReadOnly,
|
|
45
|
+
branchWriteBlocked,
|
|
46
|
+
branchPending,
|
|
47
|
+
branchMissing,
|
|
48
|
+
reviewing: workflow?.kind === 'review',
|
|
49
|
+
pullRequestClosed: reviewRole === 'reviewing-closed',
|
|
50
|
+
};
|
|
51
|
+
const branchLockReason = describeBranchLock(lock);
|
|
52
|
+
const status = branchBarStatus({ ...lock, isProtected: branchServerProtected });
|
|
53
|
+
const theme = useMantineTheme();
|
|
54
|
+
// Mantine's own below-`md` query, the one `visibleFrom="md"` hides under.
|
|
55
|
+
const belowMd = `(max-width: ${em(getBreakpointValue('md', theme.breakpoints) - 0.1)})`;
|
|
56
|
+
const narrow = useMediaQuery(belowMd, false, { getInitialValueInEffect: false }) === true;
|
|
57
|
+
const editable = !!branchName && !branchLoading && !branchLockReason;
|
|
40
58
|
// Below `md` one split button holds the bar's actions: Save all on an editable branch, else the
|
|
41
59
|
// row's way forward, with the workflow action in its caret. Withdraw leads only for a user who
|
|
42
60
|
// may withdraw; anyone else gets Switch branch…, the blocked Withdraw and its reason in the
|
|
43
|
-
// caret. A
|
|
44
|
-
//
|
|
61
|
+
// caret. A reviewer's Approve leads the same way, with Request changes in its own caret. A
|
|
62
|
+
// protected branch keeps New branch… either way. Wide, a Withdraw the user can't take shows
|
|
63
|
+
// blocked, with no Switch branch… beside it: that stays in the pill's menu.
|
|
45
64
|
const split = narrow && editable;
|
|
46
65
|
const workflowLeads = status.action === 'workflow' && !!workflow?.permitted;
|
|
47
66
|
const barAction = status.action === 'workflow'
|
|
@@ -50,16 +69,19 @@ export function EditorHeader({ branchName, busy, modifiedCount, onShowUnsaved, b
|
|
|
50
69
|
: undefined
|
|
51
70
|
: status.action;
|
|
52
71
|
// Controls come and go as a submit or withdraw changes the lock, and focus follows them.
|
|
53
|
-
const [[wayRef, saveAllRef, workflowRef], focusTarget] = useFocusHandOff(
|
|
72
|
+
const [[wayRef, saveAllRef, workflowRef, secondaryRef], focusTarget] = useFocusHandOff(4);
|
|
54
73
|
const workflowControl = {
|
|
55
74
|
onSubmit,
|
|
56
75
|
onWithdraw,
|
|
76
|
+
onApprove,
|
|
77
|
+
onRequestChanges,
|
|
57
78
|
controlRef: workflowRef,
|
|
79
|
+
secondaryRef,
|
|
58
80
|
// A withdraw that leaves no workflow action returns focus to the way forward, else Save all.
|
|
59
81
|
returnFocusTo: () => focusTarget(2),
|
|
60
82
|
};
|
|
61
83
|
const caretOnWay = narrow && !!barAction && !!workflow;
|
|
62
|
-
return (_jsx(TopBar, { leading: _jsxs(Group, { gap: "xs", wrap: "wrap", align: "center", style: { rowGap: 4 }, children: [_jsx(BranchPill, { branchName: branchName, loading: busy, unsavedCount: modifiedCount, onSwitchBranch: onBranchManagerOpen, onRefreshBranch: onBranchReloadData, onDiscardAll: onBranchDiscardDrafts, pillRef: branchToggleRef }), _jsx(BranchStatusLabel, { status: branchName ? status : { value: 'none', readOnly: false }, reason: branchLockReason }), modifiedCount > 0 && (_jsx(Tooltip, { label: unsavedTooltip(branchName ? status : undefined, modifiedCount,
|
|
84
|
+
return (_jsx(TopBar, { leading: _jsxs(Group, { gap: "xs", wrap: "wrap", align: "center", style: { rowGap: 4 }, children: [_jsx(BranchPill, { branchName: branchName, loading: busy, unsavedCount: modifiedCount, onSwitchBranch: onBranchManagerOpen, onRefreshBranch: onBranchReloadData, onDiscardAll: onBranchDiscardDrafts, pillRef: branchToggleRef }), _jsx(BranchStatusLabel, { status: branchName ? status : { value: 'none', readOnly: false }, reason: branchLockReason }), modifiedCount > 0 && (_jsx(Tooltip, { label: unsavedTooltip(branchName ? status : undefined, modifiedCount, workflow?.kind === 'withdraw' && workflow.permitted), events: TOOLTIP_EVENTS, multiline: true, maw: 320, children: _jsx(Button, { variant: "subtle", color: "gray", size: "compact-sm", style: { flexShrink: 0 }, leftSection: _jsx(UnsavedMark, { "aria-hidden": "true" }), onClick: onShowUnsaved, "data-testid": "branch-unsaved-count", children: `${modifiedCount} unsaved` }) }))] }), trailing: _jsxs(_Fragment, { children: [_jsx(DraftStorageWarning, { shown: draftStorageFailed && modifiedCount > 0 }), (!narrow || editable) && (_jsx(SaveAllButton
|
|
63
85
|
// Its reserved width follows this branch's counts, not the session's largest.
|
|
64
86
|
, { onSaveAll: onSaveAll, unsavedCount: modifiedCount, blockedReason: branchLockReason ??
|
|
65
87
|
(!branchName
|
|
@@ -70,8 +92,11 @@ export function EditorHeader({ branchName, busy, modifiedCount, onShowUnsaved, b
|
|
|
70
92
|
? onNewBranch
|
|
71
93
|
: barAction === 'refreshBranch'
|
|
72
94
|
? onBranchReloadData
|
|
73
|
-
: onBranchManagerOpen, caret: caretOnWay && workflow ? (_jsx(WorkflowCaret, { ...workflowControl, action: workflow })) : undefined })), !split && !caretOnWay && workflow && (_jsx(WorkflowButton, { ...workflowControl, action: workflow }))] }) }));
|
|
95
|
+
: onBranchManagerOpen, caret: caretOnWay && workflow ? (_jsx(WorkflowCaret, { ...workflowControl, action: workflow })) : undefined })), !split && !caretOnWay && workflow && (_jsx(WorkflowButton, { ...workflowControl, action: workflow, narrow: narrow }))] }) }));
|
|
74
96
|
}
|
|
97
|
+
const noopAction = () => { };
|
|
98
|
+
/** Why a workflow control waits when the bar is busy and nothing else holds it. */
|
|
99
|
+
const BUSY_REASON = 'Wait for the current save or load to finish.';
|
|
75
100
|
const TOOLTIP_EVENTS = { hover: true, focus: true, touch: false };
|
|
76
101
|
/**
|
|
77
102
|
* On a locked branch, what would let its drafts be saved. A new branch starts without them, so a
|
|
@@ -111,13 +136,16 @@ function BranchWayButton({ action, onClick, caret, buttonRef, }) {
|
|
|
111
136
|
// Grouped even alone, as Save all is, so a caret coming or going never remounts the button.
|
|
112
137
|
_jsxs(Button.Group, { style: { flexShrink: 0 }, renderRoot: (props) => (_jsx("div", { ...props, role: caret ? 'group' : undefined, "aria-label": caret ? 'Branch controls' : undefined })), children: [_jsx(Button, { ref: buttonRef, variant: "light", size: "sm", onClick: () => onClick(), "data-testid": `${action.replace('Branch', '')}-branch-button`, children: WAY_LABEL[action] }), caret] }));
|
|
113
138
|
}
|
|
114
|
-
/**
|
|
115
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Submit for review, or Withdraw from review once submitted or approved, or Approve and Request
|
|
141
|
+
* changes to a reviewer of someone else's submission; null when none shows.
|
|
142
|
+
*/
|
|
143
|
+
function workflowAction({ branchName, busy, branchStatus, userContext, branchCreatedBy, branchAccess, branchIsProtected, reviewRole, savingAll, }) {
|
|
116
144
|
const isSubmitted = branchStatus === 'submitted';
|
|
117
145
|
const isEditing = branchStatus === 'editing';
|
|
118
|
-
// 'approved' withdraws too -- it is
|
|
119
|
-
//
|
|
120
|
-
// non-editing branch (api/branch-
|
|
146
|
+
// 'approved' withdraws too -- it is the author's way back from
|
|
147
|
+
// either status, since the editor offers an author no Request changes
|
|
148
|
+
// and the submit gate refuses a non-editing branch (api/branch-withdraw.ts).
|
|
121
149
|
// This button is a two-state toggle, so every withdrawable status
|
|
122
150
|
// has to drive the withdraw side of it, not just 'submitted'.
|
|
123
151
|
const isWithdrawable = isSubmitted || branchStatus === 'approved';
|
|
@@ -127,6 +155,15 @@ function workflowAction({ branchName, busy, branchStatus, userContext, branchCre
|
|
|
127
155
|
// wrongly stuck in 'submitted'.
|
|
128
156
|
if (branchIsProtected && !isWithdrawable)
|
|
129
157
|
return null;
|
|
158
|
+
if (reviewRole === 'reviewing') {
|
|
159
|
+
return {
|
|
160
|
+
kind: 'review',
|
|
161
|
+
offersApprove: isSubmitted,
|
|
162
|
+
blocked: !branchName || busy || savingAll,
|
|
163
|
+
permitted: true,
|
|
164
|
+
reason: savingAll ? 'Wait for Save all to finish.' : busy ? BUSY_REASON : '',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
130
167
|
// Who may submit/withdraw: creator, ACL access, the system branch, or an
|
|
131
168
|
// admin/reviewer (alone on a locked-down branch, and even without ACL access
|
|
132
169
|
// on a protected base branch stuck in 'submitted'). Mirrors the server's
|
|
@@ -167,7 +204,42 @@ function workflowAction({ branchName, busy, branchStatus, userContext, branchCre
|
|
|
167
204
|
: '',
|
|
168
205
|
};
|
|
169
206
|
}
|
|
170
|
-
function WorkflowButton({
|
|
207
|
+
function WorkflowButton({ narrow, ...props }) {
|
|
208
|
+
return props.action.kind === 'review' ? (_jsx(ReviewButtons, { ...props, narrow: narrow })) : (_jsx(SubmitOrWithdrawButton, { ...props }));
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Approve (leading) and Request changes. Below `md`, Approve is a split button whose caret holds
|
|
212
|
+
* Request changes; with nothing to approve, Request changes stands alone.
|
|
213
|
+
*/
|
|
214
|
+
function ReviewButtons({ action, onApprove, onRequestChanges, controlRef, secondaryRef, returnFocusTo, narrow, ...rest }) {
|
|
215
|
+
// The bar's hand-off slot goes to Approve while there is one, in either layout, so focus on it
|
|
216
|
+
// moves on when it goes; then Request changes, the one control left, takes the slot. Beside
|
|
217
|
+
// Approve, Request changes (or the caret that holds it) has a slot of its own, so focus on it
|
|
218
|
+
// moves on when the branch leaves review.
|
|
219
|
+
const approveEl = useRef(null);
|
|
220
|
+
const requestChangesEl = useRef(null);
|
|
221
|
+
const approveRef = useMergedRef(approveEl, action.offersApprove ? controlRef : undefined);
|
|
222
|
+
const requestChangesRef = useMergedRef(requestChangesEl, action.offersApprove ? (narrow ? undefined : secondaryRef) : controlRef);
|
|
223
|
+
// Each returns focus to its own button while that is mounted, else to the control left.
|
|
224
|
+
const approveReturn = () => (approveEl.current?.isConnected ? approveEl.current : returnFocusTo());
|
|
225
|
+
const requestChangesReturn = () => requestChangesEl.current?.isConnected ? requestChangesEl.current : returnFocusTo();
|
|
226
|
+
const approve = (_jsx(ReviewButton, { buttonRef: approveRef, action: action, color: "brand", testId: "approve-button", label: EDITOR_ACTIONS.approve, onClick: () => onApprove(approveReturn) }));
|
|
227
|
+
const requestChanges = (_jsx(ReviewButton, { buttonRef: requestChangesRef, action: action, color: "orange", testId: "request-changes-button", label: EDITOR_ACTIONS.requestChanges, onClick: () => onRequestChanges(requestChangesReturn) }));
|
|
228
|
+
if (!action.offersApprove)
|
|
229
|
+
return requestChanges;
|
|
230
|
+
if (!narrow) {
|
|
231
|
+
return (_jsxs(Group, { gap: 8, wrap: "nowrap", style: { flexShrink: 0 }, children: [requestChanges, approve] }));
|
|
232
|
+
}
|
|
233
|
+
return (_jsxs(Button.Group, { style: { flexShrink: 0 }, renderRoot: (props) => _jsx("div", { ...props, role: "group", "aria-label": "Review controls" }), children: [approve, _jsx(WorkflowCaret, { ...rest, action: action, onApprove: onApprove, onRequestChanges: onRequestChanges, returnFocusTo: returnFocusTo, includeApprove: false, color: "brand", controlRef: secondaryRef })] }));
|
|
234
|
+
}
|
|
235
|
+
/** One review button, blocked as a whole with the action's reason. */
|
|
236
|
+
function ReviewButton({ action, buttonRef, color, testId, label, onClick, }) {
|
|
237
|
+
return (_jsx(Tooltip, { label: action.reason, disabled: !action.reason, multiline: true, maw: 320, children: _jsx(Button, { ref: buttonRef, size: "sm", variant: "light", color: color, style: { flexShrink: 0 }, onClick: () => {
|
|
238
|
+
if (!action.blocked)
|
|
239
|
+
onClick();
|
|
240
|
+
}, "data-disabled": action.blocked ? true : undefined, "aria-disabled": action.blocked ? true : undefined, "data-testid": testId, children: label }) }));
|
|
241
|
+
}
|
|
242
|
+
function SubmitOrWithdrawButton({ action, onSubmit, onWithdraw, controlRef, returnFocusTo, }) {
|
|
171
243
|
const withdraw = action.kind === 'withdraw';
|
|
172
244
|
return (_jsx(Tooltip, { label: action.reason, disabled: !action.reason, multiline: true, maw: 320, children: _jsx(Button, { ref: controlRef, size: "sm",
|
|
173
245
|
// The entry's Save is the one filled primary; the rest take their colour's light variant.
|
|
@@ -180,20 +252,28 @@ function WorkflowButton({ action, onSubmit, onWithdraw, controlRef, returnFocusT
|
|
|
180
252
|
onSubmit(returnFocusTo);
|
|
181
253
|
}, "data-disabled": action.blocked ? true : undefined, "aria-disabled": action.blocked ? true : undefined, "data-testid": withdraw ? 'withdraw-button' : 'submit-button', children: withdraw ? EDITOR_ACTIONS.withdrawFromReview : EDITOR_ACTIONS.submitForReview }) }));
|
|
182
254
|
}
|
|
183
|
-
/**
|
|
184
|
-
|
|
255
|
+
/**
|
|
256
|
+
* The workflow action's caret below `md`, on Save all or the way forward or, for a review, beside
|
|
257
|
+
* Approve. `includeApprove` is false there, where Approve is already a button.
|
|
258
|
+
*/
|
|
259
|
+
function WorkflowCaret({ action, onSubmit, onWithdraw, onApprove, onRequestChanges, controlRef, returnFocusTo, includeApprove = true, color, }) {
|
|
185
260
|
const caret = useRef(null);
|
|
186
261
|
const caretRef = useMergedRef(caret, controlRef);
|
|
187
262
|
const menuFocus = useMenuItemFocusReturn(caret);
|
|
188
263
|
const withdraw = action.kind === 'withdraw';
|
|
264
|
+
// Beside Approve it holds only Request changes, and a dialog it opens returns to the caret.
|
|
265
|
+
const reviewCaret = action.kind === 'review' && !includeApprove;
|
|
266
|
+
const caretLabel = reviewCaret ? 'More review actions' : 'Branch actions';
|
|
267
|
+
const returnToCaret = reviewCaret
|
|
268
|
+
? () => (caret.current?.isConnected ? caret.current : returnFocusTo())
|
|
269
|
+
: returnFocusTo;
|
|
270
|
+
const blockedReason = action.blocked ? action.reason || BUSY_REASON : undefined;
|
|
189
271
|
return (
|
|
190
272
|
// Bounded, so a blocked item's reason wraps on a phone rather than run off the screen.
|
|
191
|
-
_jsxs(Menu, { withinPortal: true, shadow: "sm", position: "bottom-end", width: 280, ...menuFocus.menu, children: [_jsx(Tooltip, { label:
|
|
192
|
-
? action.reason || 'Wait for the current save or load to finish.'
|
|
193
|
-
: undefined, onClick: () => {
|
|
273
|
+
_jsxs(Menu, { withinPortal: true, shadow: "sm", position: "bottom-end", width: 280, ...menuFocus.menu, children: [_jsx(Tooltip, { label: caretLabel, events: { hover: true, focus: true, touch: false }, children: _jsx(Menu.Target, { children: _jsx(Button, { ref: caretRef, variant: "light", color: color, size: "sm", px: 8, "aria-label": caretLabel, "data-testid": "branch-actions-button", style: { borderInlineStart: '1px solid var(--canopy-input-border)' }, children: _jsx(IconChevronDown, { size: 14, "aria-hidden": "true" }) }) }) }), _jsx(Menu.Dropdown, { ...menuFocus.dropdown, children: action.kind === 'review' ? (_jsxs(_Fragment, { children: [action.offersApprove && includeApprove && (_jsx(ReasonedMenuItem, { "data-testid": "approve-menu-item", blockedReason: blockedReason, onClick: () => onApprove(returnToCaret), children: EDITOR_ACTIONS.approve })), _jsx(ReasonedMenuItem, { "data-testid": "request-changes-menu-item", blockedReason: blockedReason, onClick: () => onRequestChanges(returnToCaret), children: EDITOR_ACTIONS.requestChanges })] })) : (_jsx(ReasonedMenuItem, { "data-testid": withdraw ? 'withdraw-menu-item' : 'submit-menu-item', blockedReason: blockedReason, onClick: () => {
|
|
194
274
|
if (withdraw)
|
|
195
275
|
onWithdraw(returnFocusTo);
|
|
196
276
|
else
|
|
197
277
|
onSubmit(returnFocusTo);
|
|
198
|
-
}, children: withdraw ? EDITOR_ACTIONS.withdrawFromReview : EDITOR_ACTIONS.submitForReview }) })] }));
|
|
278
|
+
}, children: withdraw ? EDITOR_ACTIONS.withdrawFromReview : EDITOR_ACTIONS.submitForReview })) })] }));
|
|
199
279
|
}
|
|
@@ -55,6 +55,10 @@ export interface EntryHeaderProps {
|
|
|
55
55
|
branchPending?: boolean;
|
|
56
56
|
/** The branch list answered without this branch. */
|
|
57
57
|
branchMissing?: boolean;
|
|
58
|
+
/** The user reviews this branch rather than owns it; the lock reason then names Approve. */
|
|
59
|
+
branchReviewing?: boolean;
|
|
60
|
+
/** The user would review this branch but its pull request was closed; the lock reason says so. */
|
|
61
|
+
branchPullRequestClosed?: boolean;
|
|
58
62
|
}
|
|
59
63
|
/** The open entry's breadcrumb, menu, unsaved dot and "Read-only" chip, then Save, over the panes. */
|
|
60
|
-
export declare function EntryHeader({ currentEntry, entryLoading, branchName, busy, breadcrumbSegments, onCollectionOpen, onEntryReveal, navigatorToggleRef, onFileReload, onFileDiscardDraft, onEntryRename, onEntryDelete, onSave, saveAllRunning, saving, savedSignal, savedHere, hasUnsavedChanges, entryHasDraft, draftStorageFailed, entryReadOnly, entryUnavailable, branchStatus, branchReadOnly, branchWriteBlocked, branchLoading, branchPending, branchMissing, }: EntryHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
64
|
+
export declare function EntryHeader({ currentEntry, entryLoading, branchName, busy, breadcrumbSegments, onCollectionOpen, onEntryReveal, navigatorToggleRef, onFileReload, onFileDiscardDraft, onEntryRename, onEntryDelete, onSave, saveAllRunning, saving, savedSignal, savedHere, hasUnsavedChanges, entryHasDraft, draftStorageFailed, entryReadOnly, entryUnavailable, branchStatus, branchReadOnly, branchWriteBlocked, branchLoading, branchPending, branchMissing, branchReviewing, branchPullRequestClosed, }: EntryHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -6,13 +6,15 @@ import { describeBranchLock } from '../branch-lock.js';
|
|
|
6
6
|
import { EntryBreadcrumb } from './EntryBreadcrumb.js';
|
|
7
7
|
import { EntrySaveButton } from './EntrySaveButton.js';
|
|
8
8
|
/** The open entry's breadcrumb, menu, unsaved dot and "Read-only" chip, then Save, over the panes. */
|
|
9
|
-
export function EntryHeader({ currentEntry, entryLoading = false, branchName, busy, breadcrumbSegments, onCollectionOpen, onEntryReveal, navigatorToggleRef, onFileReload, onFileDiscardDraft, onEntryRename, onEntryDelete, onSave, saveAllRunning, saving = false, savedSignal, savedHere = false, hasUnsavedChanges, entryHasDraft = hasUnsavedChanges, draftStorageFailed = false, entryReadOnly = false, entryUnavailable = false, branchStatus, branchReadOnly = false, branchWriteBlocked = false, branchLoading = false, branchPending = false, branchMissing = false, }) {
|
|
9
|
+
export function EntryHeader({ currentEntry, entryLoading = false, branchName, busy, breadcrumbSegments, onCollectionOpen, onEntryReveal, navigatorToggleRef, onFileReload, onFileDiscardDraft, onEntryRename, onEntryDelete, onSave, saveAllRunning, saving = false, savedSignal, savedHere = false, hasUnsavedChanges, entryHasDraft = hasUnsavedChanges, draftStorageFailed = false, entryReadOnly = false, entryUnavailable = false, branchStatus, branchReadOnly = false, branchWriteBlocked = false, branchLoading = false, branchPending = false, branchMissing = false, branchReviewing = false, branchPullRequestClosed = false, }) {
|
|
10
10
|
const branchLockReason = describeBranchLock({
|
|
11
11
|
branchStatus,
|
|
12
12
|
branchReadOnly,
|
|
13
13
|
branchWriteBlocked,
|
|
14
14
|
branchPending,
|
|
15
15
|
branchMissing,
|
|
16
|
+
reviewing: branchReviewing,
|
|
17
|
+
pullRequestClosed: branchPullRequestClosed,
|
|
16
18
|
});
|
|
17
19
|
const entryLockReason = entryReadOnly
|
|
18
20
|
? "You don't have edit access to this entry. Ask a CanopyCMS admin to grant it."
|
|
@@ -3,8 +3,16 @@ import { type MenuItemProps } from '@mantine/core';
|
|
|
3
3
|
export interface ReasonedMenuItemProps extends MenuItemProps {
|
|
4
4
|
/** Why the item can't act now. Set, the item is blocked and shows this beneath its label. */
|
|
5
5
|
blockedReason?: string;
|
|
6
|
+
/** False when a neighbouring item already says why, so the reason is not printed twice. */
|
|
7
|
+
showReason?: boolean;
|
|
8
|
+
/** The reason's element id, which another blocked item that shows no reason can point to. */
|
|
9
|
+
reasonId?: string;
|
|
10
|
+
/** The id of the element that gives this item's reason, when it is not printed beneath it. */
|
|
11
|
+
reasonDescribedBy?: string;
|
|
6
12
|
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
7
13
|
'data-testid'?: string;
|
|
14
|
+
/** The caller's own description, kept unless a shared reason points elsewhere. */
|
|
15
|
+
'aria-describedby'?: string;
|
|
8
16
|
children: React.ReactNode;
|
|
9
17
|
}
|
|
10
18
|
/**
|
|
@@ -6,10 +6,10 @@ import { Menu, Text } from '@mantine/core';
|
|
|
6
6
|
* Menu drops a `disabled` or `data-disabled` item from its arrow-key order, hiding the reason from
|
|
7
7
|
* a keyboard user; a blocked item is `aria-disabled` instead and ignores its click.
|
|
8
8
|
*/
|
|
9
|
-
export const ReasonedMenuItem = ({ blockedReason, onClick, children, color, style, ...rest }) => (_jsxs(Menu.Item, { ...rest, color: blockedReason ? undefined : color, c: blockedReason ? 'dimmed' : rest.c, "aria-disabled": blockedReason ? true : undefined, closeMenuOnClick: blockedReason ? false : undefined, style: blockedReason ? { ...style, cursor: 'not-allowed' } : style, onClick: (event) => {
|
|
9
|
+
export const ReasonedMenuItem = ({ blockedReason, showReason = true, reasonId, reasonDescribedBy, onClick, children, color, style, ...rest }) => (_jsxs(Menu.Item, { ...rest, color: blockedReason ? undefined : color, c: blockedReason ? 'dimmed' : rest.c, "aria-disabled": blockedReason ? true : undefined, "aria-describedby": (blockedReason && reasonDescribedBy) || rest['aria-describedby'], closeMenuOnClick: blockedReason ? false : undefined, style: blockedReason ? { ...style, cursor: 'not-allowed' } : style, onClick: (event) => {
|
|
10
10
|
if (blockedReason) {
|
|
11
11
|
event.stopPropagation();
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
onClick?.(event);
|
|
15
|
-
}, children: [children, blockedReason && (_jsx(Text, { size: "xs", c: "dimmed", component: "span", display: "block", style: { whiteSpace: 'normal' }, children: blockedReason }))] }));
|
|
15
|
+
}, children: [children, blockedReason && showReason && (_jsx(Text, { id: reasonId, size: "xs", c: "dimmed", component: "span", display: "block", style: { whiteSpace: 'normal' }, children: blockedReason }))] }));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** What the Approve dialog says; the pull request clause needs a pull request to be true. */
|
|
2
|
+
export declare function approveMessage(branchName: string, hasPullRequest: boolean): string;
|
|
3
|
+
/** The Request changes dialog's body: what happens, and an optional note for the editor. */
|
|
4
|
+
export declare function RequestChangesForm({ branchName, hasPullRequest, initialNote, onNoteChange, }: {
|
|
5
|
+
branchName: string;
|
|
6
|
+
hasPullRequest: boolean;
|
|
7
|
+
/** A note typed before, such as one a failed request left behind. */
|
|
8
|
+
initialNote?: string;
|
|
9
|
+
onNoteChange: (note: string) => void;
|
|
10
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
/**
|
|
12
|
+
* A notification body that holds a note the reviewer typed, which is not saved: the message, the
|
|
13
|
+
* note quoted, and a button to copy it. `hint` says what to do with the copy.
|
|
14
|
+
*/
|
|
15
|
+
export declare function NoteNotSavedMessage({ message, note, hint, }: {
|
|
16
|
+
message: string;
|
|
17
|
+
note: string;
|
|
18
|
+
hint?: string;
|
|
19
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useRef, useState } from 'react';
|
|
4
|
+
import { Button, CopyButton, Stack, Text, Textarea, VisuallyHidden } from '@mantine/core';
|
|
5
|
+
import { REQUEST_CHANGES_NOTE_MAX_LENGTH } from '../../authorization/review-role.js';
|
|
6
|
+
/** The note counter appears once a note is this long, and is announced in the last 100. */
|
|
7
|
+
const NOTE_COUNT_FROM = 1800;
|
|
8
|
+
const NOTE_ANNOUNCE_FROM = REQUEST_CHANGES_NOTE_MAX_LENGTH - 100;
|
|
9
|
+
/** What the Approve dialog says; the pull request clause needs a pull request to be true. */
|
|
10
|
+
export function approveMessage(branchName, hasPullRequest) {
|
|
11
|
+
return hasPullRequest
|
|
12
|
+
? `Approve "${branchName}"? Its pull request stays open on GitHub for someone with access to merge it.`
|
|
13
|
+
: `Approve "${branchName}"? It stays locked until it is withdrawn or changes are requested.`;
|
|
14
|
+
}
|
|
15
|
+
/** The Request changes dialog's body: what happens, and an optional note for the editor. */
|
|
16
|
+
export function RequestChangesForm({ branchName, hasPullRequest, initialNote = '', onNoteChange, }) {
|
|
17
|
+
const [note, setNote] = useState(initialNote);
|
|
18
|
+
// The first focus, which the dialog's focus trap gives, puts the caret after a prefilled note so
|
|
19
|
+
// typing carries on from it; later focus keeps wherever the caret was left.
|
|
20
|
+
const caretPlaced = useRef(false);
|
|
21
|
+
return (_jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", children: `Send "${branchName}" back to its editor? It goes back to editing${hasPullRequest ? ', and its pull request becomes a draft' : ''}.` }), _jsxs(Stack, { gap: 4, children: [_jsx(Textarea, { label: "Note to the editor", description: "Saved as a comment on the branch.", value: note, onChange: (event) => {
|
|
22
|
+
setNote(event.currentTarget.value);
|
|
23
|
+
onNoteChange(event.currentTarget.value);
|
|
24
|
+
}, maxLength: REQUEST_CHANGES_NOTE_MAX_LENGTH, autosize: true, minRows: 3, maxRows: 8, onFocus: (event) => {
|
|
25
|
+
if (caretPlaced.current)
|
|
26
|
+
return;
|
|
27
|
+
caretPlaced.current = true;
|
|
28
|
+
const end = event.currentTarget.value.length;
|
|
29
|
+
event.currentTarget.setSelectionRange(end, end);
|
|
30
|
+
}, "data-autofocus": true, "data-testid": "request-changes-note" }), note.length > NOTE_COUNT_FROM && (_jsx(Text, { size: "xs", c: "dimmed", ta: "end", "aria-live": note.length >= NOTE_ANNOUNCE_FROM ? 'polite' : undefined, "data-testid": "request-changes-note-count", children: `${note.length.toLocaleString('en-US')} / ${REQUEST_CHANGES_NOTE_MAX_LENGTH.toLocaleString('en-US')}` }))] })] }));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A notification body that holds a note the reviewer typed, which is not saved: the message, the
|
|
34
|
+
* note quoted, and a button to copy it. `hint` says what to do with the copy.
|
|
35
|
+
*/
|
|
36
|
+
export function NoteNotSavedMessage({ message, note, hint = 'Copy it into a comment on the branch.', }) {
|
|
37
|
+
return (_jsxs(Stack, { gap: 6, children: [_jsx(Text, { size: "sm", children: `${message} ${hint}` }), _jsx(Text, { component: "blockquote", size: "sm", m: 0, ps: "sm", lineClamp: 6, style: {
|
|
38
|
+
whiteSpace: 'pre-wrap',
|
|
39
|
+
borderInlineStart: '2px solid var(--canopy-input-border)',
|
|
40
|
+
}, "data-testid": "note-not-saved-text", children: note }), _jsx(CopyButton, { value: note, children: ({ copied, copy }) => (_jsxs(_Fragment, { children: [_jsx(Button, { size: "compact-sm", variant: "light", style: { alignSelf: 'flex-start' }, onClick: copy, "data-testid": "copy-note-button", children: copied ? 'Copied' : 'Copy note' }), _jsx(VisuallyHidden, { role: "status", "aria-live": "polite", children: copied ? 'Note copied' : '' })] })) })] }));
|
|
41
|
+
}
|
package/dist/editor/copy.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export declare const EDITOR_ACTIONS: {
|
|
|
9
9
|
readonly refreshFromServer: "Refresh from server";
|
|
10
10
|
readonly submitForReview: "Submit for review";
|
|
11
11
|
readonly withdrawFromReview: "Withdraw from review";
|
|
12
|
+
readonly approve: "Approve";
|
|
13
|
+
readonly review: "Review";
|
|
12
14
|
readonly requestChanges: "Request changes";
|
|
13
15
|
readonly switchBranch: "Switch branch";
|
|
14
16
|
readonly newBranch: "New branch";
|
|
@@ -24,3 +26,5 @@ export declare const EDITOR_ACTIONS: {
|
|
|
24
26
|
* `one` and `many` name what the list holds.
|
|
25
27
|
*/
|
|
26
28
|
export declare const listFullReason: (maxItems: number, one?: string, many?: string) => string;
|
|
29
|
+
/** Why Approve and Request changes can't act: the pull request they act on is gone. */
|
|
30
|
+
export declare const CLOSED_PULL_REQUEST_REASON = "The pull request was closed on GitHub. Use \"Withdraw from review\", then submit it again to open a new one.";
|
package/dist/editor/copy.js
CHANGED
|
@@ -9,6 +9,8 @@ export const EDITOR_ACTIONS = {
|
|
|
9
9
|
refreshFromServer: 'Refresh from server',
|
|
10
10
|
submitForReview: 'Submit for review',
|
|
11
11
|
withdrawFromReview: 'Withdraw from review',
|
|
12
|
+
approve: 'Approve',
|
|
13
|
+
review: 'Review',
|
|
12
14
|
requestChanges: 'Request changes',
|
|
13
15
|
switchBranch: 'Switch branch',
|
|
14
16
|
newBranch: 'New branch',
|
|
@@ -24,3 +26,5 @@ export const EDITOR_ACTIONS = {
|
|
|
24
26
|
* `one` and `many` name what the list holds.
|
|
25
27
|
*/
|
|
26
28
|
export const listFullReason = (maxItems, one = 'item', many = 'items') => `This list holds at most ${maxItems} ${maxItems === 1 ? one : many}. Remove one to add another.`;
|
|
29
|
+
/** Why Approve and Request changes can't act: the pull request they act on is gone. */
|
|
30
|
+
export const CLOSED_PULL_REQUEST_REASON = 'The pull request was closed on GitHub. Use "Withdraw from review", then submit it again to open a new one.';
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
import React, { Suspense, useId, useRef, useCallback, useEffect, useReducer, useState } from 'react';
|
|
4
4
|
import { Alert, Box, Button, Text, Textarea, useComputedColorScheme } from '@mantine/core';
|
|
5
5
|
import { InsertEntryLink } from './entry-link/index.js';
|
|
6
|
-
import { MARKDOWN_EXPORT_OPTIONS } from '
|
|
6
|
+
import { MARKDOWN_EXPORT_OPTIONS } from '../../utils/markdown-export-options.js';
|
|
7
7
|
import { createMarkdownFidelityPlugin } from './markdown-fidelity-visitors.js';
|
|
8
8
|
import { MdxImageDialog } from './MdxImageDialog.js';
|
|
9
9
|
import { createMdxJsxPlugins } from './mdx-jsx-support.js';
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { BranchListItem, CreateBranchBody } from '../../api/branch.js';
|
|
2
2
|
import { type CanopyApiClient } from '../../api/client.js';
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* @internal Exported for tests. Past the gateway's limit, so its 504 lands first, and under
|
|
5
|
+
* `REQUEST_TIMEOUT_MS.long`. The limit comes from `request-deadline` directly: a test that mocks
|
|
6
|
+
* `api/client` would otherwise lose it at module load.
|
|
7
|
+
*/
|
|
8
|
+
export declare const CREATE_BRANCH_DEADLINE_MS: number;
|
|
5
9
|
/** @internal Exported for tests. */
|
|
6
10
|
export declare const CREATE_TIMED_OUT_MESSAGE = "Creating the branch didn't finish (the server timed out). It's safe to try again.";
|
|
7
11
|
export type CreateBranchOutcome = {
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { isCreatorsRecentBranch } from '../../api/branch-create-window.js';
|
|
2
2
|
import { isApiTimeoutError, isNonApiResponse } from '../../api/client.js';
|
|
3
|
+
import { GATEWAY_MAX_TIMEOUT_MS } from '../../api/request-deadline.js';
|
|
3
4
|
import { sanitizeBranchName } from '../../paths/branch-name.js';
|
|
4
|
-
/**
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* @internal Exported for tests. Past the gateway's limit, so its 504 lands first, and under
|
|
7
|
+
* `REQUEST_TIMEOUT_MS.long`. The limit comes from `request-deadline` directly: a test that mocks
|
|
8
|
+
* `api/client` would otherwise lose it at module load.
|
|
9
|
+
*/
|
|
10
|
+
export const CREATE_BRANCH_DEADLINE_MS = GATEWAY_MAX_TIMEOUT_MS + 30_000;
|
|
6
11
|
/** @internal Exported for tests. */
|
|
7
12
|
export const CREATE_TIMED_OUT_MESSAGE = "Creating the branch didn't finish (the server timed out). It's safe to try again.";
|
|
8
13
|
/**
|
|
@@ -5,7 +5,7 @@ import type { BranchListItem } from '../../api/branch.js';
|
|
|
5
5
|
import type { UnsavedSummary } from './useDraftManager.js';
|
|
6
6
|
import type { StampedEntry } from '../../stamp-on-publish.js';
|
|
7
7
|
/** @internal Exported for tests. */
|
|
8
|
-
export declare const branchActionOutcomeUnknown: (action: string) => string;
|
|
8
|
+
export declare const branchActionOutcomeUnknown: (action: string, check?: string) => string;
|
|
9
9
|
/** What the withdraw and delete dialogs say about the branch's pull request. */
|
|
10
10
|
type BranchPullRequest = Pick<BranchListItem, 'pullRequestNumber' | 'pullRequestState'>;
|
|
11
11
|
/**
|
|
@@ -81,6 +81,8 @@ export interface UseBranchManagerOptions {
|
|
|
81
81
|
getUnsaved?: () => Promise<UnsavedSummary>;
|
|
82
82
|
/** The entries a submit of the open branch stamped (useDraftManager's `applySubmitStamps`). */
|
|
83
83
|
onStamped?: (stamped: StampedEntry[]) => void;
|
|
84
|
+
/** A request for changes went through; the branch may now carry the reviewer's note as a comment. */
|
|
85
|
+
onChangesRequested?: (branchName: string) => void;
|
|
84
86
|
}
|
|
85
87
|
export interface UseBranchManagerReturn {
|
|
86
88
|
branchName: string;
|
|
@@ -98,7 +100,10 @@ export interface UseBranchManagerReturn {
|
|
|
98
100
|
handleSubmit: (branchName: string, returnFocusTo?: () => HTMLElement | null) => Promise<void>;
|
|
99
101
|
/** Confirms first; the dialog returns focus to `returnFocusTo()` when given, else its opener. */
|
|
100
102
|
handleWithdraw: (branchName: string, returnFocusTo?: () => HTMLElement | null) => Promise<void>;
|
|
101
|
-
|
|
103
|
+
/** Confirms first; the dialog returns focus to `returnFocusTo()` when given, else its opener. */
|
|
104
|
+
handleApprove: (branchName: string, returnFocusTo?: () => HTMLElement | null) => Promise<void>;
|
|
105
|
+
/** Confirms first, taking an optional note; focus returns as for `handleApprove`. */
|
|
106
|
+
handleRequestChanges: (branchName: string, returnFocusTo?: () => HTMLElement | null) => Promise<void>;
|
|
102
107
|
handleDelete: (branchName: string) => Promise<void>;
|
|
103
108
|
handleReloadBranchData: () => Promise<void>;
|
|
104
109
|
loadBranches: () => Promise<void>;
|