canopycms 0.11.1 → 0.12.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 +174 -48
- package/dist/api/admin.d.ts +4 -4
- package/dist/api/branch-changes.d.ts +1 -0
- package/dist/api/branch-changes.js +1 -1
- package/dist/api/branch-entry-changes.d.ts +92 -7
- package/dist/api/branch-entry-changes.js +272 -38
- package/dist/api/branch-review.js +45 -13
- package/dist/api/branch-status.d.ts +12 -0
- package/dist/api/branch-status.js +13 -0
- package/dist/api/branch-update-from-base.d.ts +23 -0
- package/dist/api/branch-update-from-base.js +176 -0
- package/dist/api/branch.d.ts +5 -0
- package/dist/api/branch.js +1 -0
- package/dist/api/client.d.ts +17 -2
- package/dist/api/client.js +26 -0
- package/dist/api/comments.d.ts +2 -2
- package/dist/api/content.d.ts +38 -1
- package/dist/api/content.js +41 -5
- package/dist/api/merge-conflicts.d.ts +226 -0
- package/dist/api/merge-conflicts.js +674 -0
- package/dist/api/routes.js +2 -0
- package/dist/api/types.d.ts +7 -3
- package/dist/base-update.d.ts +26 -0
- package/dist/base-update.js +31 -0
- package/dist/branch-metadata-file.d.ts +51 -0
- package/dist/branch-metadata-file.js +11 -0
- package/dist/branch-schema-cache.d.ts +12 -0
- package/dist/branch-schema-cache.js +27 -2
- package/dist/cli/backfill-item-keys.d.ts +77 -0
- package/dist/cli/backfill-item-keys.js +320 -0
- package/dist/cli/cli.d.ts +1 -1
- package/dist/cli/cli.js +9713 -1493
- package/dist/cli/generate-ai-content.js +415 -244
- package/dist/cli/init.js +6 -0
- package/dist/cli/site-config.d.ts +14 -2
- package/dist/cli/site-config.js +20 -4
- package/dist/comment-store.d.ts +8 -0
- package/dist/comment-store.js +28 -0
- package/dist/content-index-generation.d.ts +1 -4
- package/dist/content-index-generation.js +1 -4
- package/dist/content-store.d.ts +65 -3
- package/dist/content-store.js +121 -15
- package/dist/editor/BranchManager.d.ts +4 -1
- package/dist/editor/BranchManager.js +9 -5
- package/dist/editor/Editor.js +549 -53
- package/dist/editor/EntryNavigator.d.ts +10 -2
- package/dist/editor/EntryNavigator.js +12 -4
- package/dist/editor/FormRenderer.d.ts +8 -2
- package/dist/editor/FormRenderer.js +5 -6
- package/dist/editor/base-update-wait.d.ts +74 -0
- package/dist/editor/base-update-wait.js +91 -0
- package/dist/editor/branch-status.js +1 -1
- package/dist/editor/components/BaseUpdateNotice.d.ts +12 -0
- package/dist/editor/components/BaseUpdateNotice.js +24 -0
- package/dist/editor/components/BranchPill.d.ts +4 -1
- package/dist/editor/components/BranchPill.js +5 -2
- package/dist/editor/components/ConflictsGroup.d.ts +100 -0
- package/dist/editor/components/ConflictsGroup.js +364 -0
- package/dist/editor/components/EditorHeader.d.ts +16 -1
- package/dist/editor/components/EditorHeader.js +92 -30
- package/dist/editor/components/EntryChangesDrawer.d.ts +52 -2
- package/dist/editor/components/EntryChangesDrawer.js +164 -162
- package/dist/editor/components/EntryHeader.d.ts +3 -1
- package/dist/editor/components/EntryHeader.js +4 -6
- package/dist/editor/components/GetLatestChangesButton.d.ts +15 -0
- package/dist/editor/components/GetLatestChangesButton.js +29 -0
- package/dist/editor/components/ReleaseHoldButton.d.ts +20 -0
- package/dist/editor/components/ReleaseHoldButton.js +36 -0
- package/dist/editor/components/RetryToastMessage.js +1 -1
- package/dist/editor/components/change-parts.d.ts +51 -0
- package/dist/editor/components/change-parts.js +171 -0
- package/dist/editor/conflict-cards.d.ts +90 -0
- package/dist/editor/conflict-cards.js +279 -0
- package/dist/editor/conflict-choices.d.ts +60 -0
- package/dist/editor/conflict-choices.js +108 -0
- package/dist/editor/conflict-notice.d.ts +25 -0
- package/dist/editor/conflict-notice.js +39 -0
- package/dist/editor/copy.d.ts +20 -0
- package/dist/editor/copy.js +20 -0
- package/dist/editor/entry-diff.d.ts +18 -2
- package/dist/editor/entry-diff.js +26 -32
- package/dist/editor/hooks/useBaseUpdate.d.ts +36 -0
- package/dist/editor/hooks/useBaseUpdate.js +269 -0
- package/dist/editor/hooks/useBranchManager.d.ts +6 -1
- package/dist/editor/hooks/useBranchManager.js +28 -7
- package/dist/editor/hooks/useBranchesData.js +3 -0
- package/dist/editor/hooks/useConflictChoices.d.ts +24 -0
- package/dist/editor/hooks/useConflictChoices.js +75 -0
- package/dist/editor/hooks/useConflictsData.d.ts +26 -0
- package/dist/editor/hooks/useConflictsData.js +89 -0
- package/dist/editor/hooks/useDraftManager.d.ts +5 -0
- package/dist/editor/hooks/useDraftManager.js +24 -8
- package/dist/editor/hooks/useEntryManager.d.ts +9 -0
- package/dist/editor/hooks/useEntryManager.js +5 -0
- package/dist/editor/release-hold.d.ts +12 -0
- package/dist/editor/release-hold.js +39 -0
- package/dist/editor/server-clock.d.ts +4 -0
- package/dist/editor/server-clock.js +15 -0
- package/dist/editor/undo-change.d.ts +52 -0
- package/dist/editor/undo-change.js +241 -0
- package/dist/git-manager.d.ts +5 -0
- package/dist/git-manager.js +30 -0
- package/dist/item-keys.d.ts +13 -0
- package/dist/item-keys.js +36 -14
- package/dist/merge/conflict-path.d.ts +40 -0
- package/dist/merge/conflict-path.js +128 -0
- package/dist/merge/diff3.d.ts +27 -0
- package/dist/merge/diff3.js +113 -0
- package/dist/merge/entry-merge.d.ts +94 -0
- package/dist/merge/entry-merge.js +875 -0
- package/dist/merge/list-alignment.d.ts +52 -0
- package/dist/merge/list-alignment.js +441 -0
- package/dist/merge/text-merge.d.ts +29 -0
- package/dist/merge/text-merge.js +59 -0
- package/dist/merge/value-equality.d.ts +18 -0
- package/dist/merge/value-equality.js +50 -0
- package/dist/merge-conflicts.d.ts +232 -0
- package/dist/merge-conflicts.js +451 -0
- package/dist/operating-mode/client-safe-strategy.d.ts +2 -0
- package/dist/operating-mode/client-safe-strategy.js +6 -0
- package/dist/operating-mode/types.d.ts +5 -0
- package/dist/services.d.ts +16 -1
- package/dist/services.js +82 -22
- package/dist/task-queue/cms-task-queue.d.ts +3 -1
- package/dist/types.d.ts +28 -2
- package/dist/utils/content-serialize.d.ts +18 -1
- package/dist/utils/content-serialize.js +12 -0
- package/dist/utils/content-write-lock.d.ts +3 -3
- package/dist/utils/content-write-lock.js +3 -3
- package/dist/utils/markdown-body-splice.d.ts +1 -1
- package/dist/utils/markdown-body-splice.js +1 -19
- package/dist/utils/markdown-parse.d.ts +9 -0
- package/dist/utils/markdown-parse.js +27 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-merge.d.ts +53 -37
- package/dist/worker/branch-merge.js +139 -54
- package/dist/worker/cms-worker.d.ts +16 -3
- package/dist/worker/cms-worker.js +59 -12
- package/dist/worker/field-merge.d.ts +65 -0
- package/dist/worker/field-merge.js +636 -0
- package/dist/worker/git-sync.d.ts +1 -1
- package/dist/worker/git-sync.js +7 -6
- package/dist/worker/github-commits.d.ts +42 -10
- package/dist/worker/github-commits.js +287 -47
- package/dist/worker/github-gateway-http.js +17 -9
- package/dist/worker/github-gateway-wire.d.ts +21 -12
- package/dist/worker/github-gateway-wire.js +5 -1
- package/dist/worker/github-gateway.d.ts +10 -1
- package/dist/worker/github-gateway.js +18 -6
- package/dist/worker/github-transfer.d.ts +5 -3
- package/dist/worker/github-transfer.js +8 -5
- package/dist/worker/rebase.d.ts +5 -4
- package/dist/worker/rebase.js +293 -156
- package/dist/worker/task-runner.d.ts +1 -1
- package/dist/worker/task-runner.js +14 -2
- package/dist/worker/worker-context.d.ts +18 -2
- package/package.json +1 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useRef } from 'react';
|
|
3
|
-
import { Badge, Box, Button, Group, Menu, Text, Tooltip, VisuallyHidden, em, getBreakpointValue, useMantineTheme, } from '@mantine/core';
|
|
2
|
+
import { useLayoutEffect, useRef } from 'react';
|
|
3
|
+
import { Badge, Box, Button, Group, Loader, Menu, Text, Tooltip, VisuallyHidden, em, getBreakpointValue, useMantineTheme, } from '@mantine/core';
|
|
4
4
|
import { useMediaQuery, useMergedRef } from '@mantine/hooks';
|
|
5
5
|
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 { HOLD_BADGE, branchHoldMessage } from '../branch-status.js';
|
|
9
|
+
import { conflictRecordHeldAdminMessage, conflictRecordHeldMessage, conflictsHoldMessage, conflictsSubmitReason, } from '../conflict-notice.js';
|
|
10
|
+
import { ReleaseHoldButton } from './ReleaseHoldButton.js';
|
|
9
11
|
import { EDITOR_ACTIONS, UNFINISHED_SUBMIT_APPROVE_REASON, rewrittenSubmitReason } from '../copy.js';
|
|
10
12
|
import { reviewRoleOf } from '../../authorization/review-role.js';
|
|
11
13
|
import { SYSTEM_USER_ID } from '../../system-users.js';
|
|
@@ -15,13 +17,15 @@ import { TooltipDetail } from './TooltipDetail.js';
|
|
|
15
17
|
import { ReasonedMenuItem } from './ReasonedMenuItem.js';
|
|
16
18
|
import { UnsavedMark } from './UnsavedMark.js';
|
|
17
19
|
import { DraftStorageWarning, HIDDEN_STYLE, SaveAllButton } from './SaveAllButton.js';
|
|
20
|
+
import { GetLatestChangesButton } from './GetLatestChangesButton.js';
|
|
21
|
+
import { GETTING_LATEST_CHANGES, WAITING_NOTICE, WAITING_SUBMIT_REASON, } from '../base-update-wait.js';
|
|
18
22
|
import { useFocusHandOff, useMenuItemFocusReturn } from '../utils/focus-return.js';
|
|
19
23
|
/**
|
|
20
24
|
* The branch bar: the pill, the branch's status and its unsaved count, then the draft-storage
|
|
21
25
|
* warning, Save all, the way forward and Submit or Withdraw, or to a reviewer of someone else's
|
|
22
26
|
* submission Approve and Request changes. The open entry's own controls are in `EntryHeader`.
|
|
23
27
|
*/
|
|
24
|
-
export function EditorHeader({ branchName, branchLabel, 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, branchHold, branchApproveBlocked = false, branchWriteBlocked = false, branchLoading = false, branchPending = false, branchMissing = false, }) {
|
|
28
|
+
export function EditorHeader({ branchName, branchLabel, 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, branchHold, branchApproveBlocked = false, branchWriteBlocked = false, branchLoading = false, branchPending = false, branchMissing = false, baseUpdate, latestWait = 'none', openConflicts = 0, conflictRecordHeld, }) {
|
|
25
29
|
const reviewRole = reviewRoleOf({
|
|
26
30
|
status: branchStatus,
|
|
27
31
|
createdBy: branchCreatedBy,
|
|
@@ -30,6 +34,12 @@ export function EditorHeader({ branchName, branchLabel, busy, modifiedCount, onS
|
|
|
30
34
|
isProtected: branchIsProtected,
|
|
31
35
|
pullRequestState: branchPullRequestState,
|
|
32
36
|
}, userContext);
|
|
37
|
+
const releaseHold = branchName ? conflictRecordHeld?.onRelease : undefined;
|
|
38
|
+
const heldMessage = !conflictRecordHeld
|
|
39
|
+
? undefined
|
|
40
|
+
: releaseHold
|
|
41
|
+
? conflictRecordHeldAdminMessage
|
|
42
|
+
: conflictRecordHeldMessage;
|
|
33
43
|
const workflow = workflowAction({
|
|
34
44
|
branchName,
|
|
35
45
|
busy,
|
|
@@ -42,6 +52,9 @@ export function EditorHeader({ branchName, branchLabel, busy, modifiedCount, onS
|
|
|
42
52
|
savingAll: !!saveAllProgress,
|
|
43
53
|
rewritten: branchHold?.kind === 'rewritten',
|
|
44
54
|
approveBlocked: branchApproveBlocked,
|
|
55
|
+
waitingForLatest: latestWait === 'waiting',
|
|
56
|
+
openConflicts,
|
|
57
|
+
heldReason: heldMessage,
|
|
45
58
|
});
|
|
46
59
|
const lock = {
|
|
47
60
|
branchStatus,
|
|
@@ -55,12 +68,19 @@ export function EditorHeader({ branchName, branchLabel, busy, modifiedCount, onS
|
|
|
55
68
|
};
|
|
56
69
|
const branchLockReason = describeBranchLock(lock);
|
|
57
70
|
const status = branchBarStatus({ ...lock, isProtected: branchServerProtected });
|
|
71
|
+
// A hold of the pull request outranks open conflicts, which only hold the next submit.
|
|
72
|
+
const conflictsHeld = openConflicts > 0 && branchStatus === 'editing';
|
|
58
73
|
const holdMessage = branchName && branchHold && status.value !== 'loading'
|
|
59
74
|
? branchHoldMessage(branchHold, {
|
|
60
75
|
role: workflow?.kind === 'review' ? 'review' : workflow?.permitted ? 'withdraw' : 'none',
|
|
61
76
|
isAdmin: isAdmin(userContext?.groups ?? []),
|
|
62
77
|
})
|
|
63
|
-
:
|
|
78
|
+
: branchName && heldMessage && status.value !== 'loading'
|
|
79
|
+
? heldMessage
|
|
80
|
+
: branchName && conflictsHeld && status.value !== 'loading'
|
|
81
|
+
? conflictsHoldMessage(openConflicts)
|
|
82
|
+
: undefined;
|
|
83
|
+
const holdCount = !branchHold && !conflictRecordHeld && conflictsHeld ? openConflicts : undefined;
|
|
64
84
|
const theme = useMantineTheme();
|
|
65
85
|
// Mantine's own below-`md` query, the one `visibleFrom="md"` hides under.
|
|
66
86
|
const belowMd = `(max-width: ${em(getBreakpointValue('md', theme.breakpoints) - 0.1)})`;
|
|
@@ -81,6 +101,20 @@ export function EditorHeader({ branchName, branchLabel, busy, modifiedCount, onS
|
|
|
81
101
|
: status.action;
|
|
82
102
|
// Controls come and go as a submit or withdraw changes the lock, and focus follows them.
|
|
83
103
|
const [[wayRef, saveAllRef, workflowRef, secondaryRef], focusTarget] = useFocusHandOff(4);
|
|
104
|
+
// A release unmounts its confirm with the button that opened it, which leaves focus nowhere: it
|
|
105
|
+
// goes to the workflow control, which the release has just freed.
|
|
106
|
+
const releasable = !!releaseHold;
|
|
107
|
+
const wasReleasable = useRef(releasable);
|
|
108
|
+
const releasedHere = useRef(false);
|
|
109
|
+
useLayoutEffect(() => {
|
|
110
|
+
const active = document.activeElement;
|
|
111
|
+
if (wasReleasable.current && !releasable && releasedHere.current) {
|
|
112
|
+
releasedHere.current = false;
|
|
113
|
+
if (!active || active === document.body)
|
|
114
|
+
focusTarget(2)?.focus({ preventScroll: true });
|
|
115
|
+
}
|
|
116
|
+
wasReleasable.current = releasable;
|
|
117
|
+
});
|
|
84
118
|
const workflowControl = {
|
|
85
119
|
onSubmit,
|
|
86
120
|
onWithdraw,
|
|
@@ -92,14 +126,27 @@ export function EditorHeader({ branchName, branchLabel, busy, modifiedCount, onS
|
|
|
92
126
|
returnFocusTo: () => focusTarget(2),
|
|
93
127
|
};
|
|
94
128
|
const caretOnWay = narrow && !!barAction && !!workflow;
|
|
95
|
-
return (_jsx(TopBar, { leading: _jsxs(Group, { gap: "xs", wrap: "wrap", align: "center", style: { rowGap: 4 }, children: [_jsx(BranchPill, { branchName: branchLabel ?? branchName, loading: busy, unsavedCount: modifiedCount, onSwitchBranch: onBranchManagerOpen, onRefreshBranch: onBranchReloadData, onDiscardAll: onBranchDiscardDrafts, pillRef: branchToggleRef
|
|
129
|
+
return (_jsx(TopBar, { leading: _jsxs(Group, { gap: "xs", wrap: "wrap", align: "center", style: { rowGap: 4 }, children: [_jsx(BranchPill, { branchName: branchLabel ?? branchName, loading: busy, unsavedCount: modifiedCount, onSwitchBranch: onBranchManagerOpen, onRefreshBranch: onBranchReloadData, onDiscardAll: onBranchDiscardDrafts, pillRef: branchToggleRef,
|
|
130
|
+
// Below `md` the bar has no room for it, so the branch's menu offers it.
|
|
131
|
+
getLatest: narrow ? baseUpdate : undefined }), _jsxs(Group, { gap: "xs", wrap: "nowrap", style: { flexShrink: 0 }, children: [_jsx(BranchStatusLabel, { status: branchName ? status : { value: 'none', readOnly: false }, reason: branchLockReason, holdMessage: holdMessage, holdCount: holdCount, working: latestWait !== 'none' ? GETTING_LATEST_CHANGES : undefined,
|
|
132
|
+
// The bar's button carries the spinner where it shows.
|
|
133
|
+
workingSpinner: narrow || !baseUpdate }), releaseHold && (_jsx(ReleaseHoldButton, { branchLabel: branchLabel ?? branchName, onRelease: async () => {
|
|
134
|
+
// Marked while in flight: the re-read that clears the hold can land first.
|
|
135
|
+
releasedHere.current = true;
|
|
136
|
+
const outcome = await releaseHold();
|
|
137
|
+
if (!('released' in outcome))
|
|
138
|
+
releasedHere.current = false;
|
|
139
|
+
return outcome;
|
|
140
|
+
} }))] }), 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 && baseUpdate && _jsx(GetLatestChangesButton, { control: baseUpdate }), (!narrow || editable) && (_jsx(SaveAllButton
|
|
96
141
|
// Its reserved width follows this branch's counts, not the session's largest.
|
|
97
142
|
, { onSaveAll: onSaveAll, unsavedCount: modifiedCount, blockedReason: branchLockReason ??
|
|
98
143
|
(!branchName
|
|
99
144
|
? 'Open a branch first.'
|
|
100
|
-
:
|
|
101
|
-
?
|
|
102
|
-
:
|
|
145
|
+
: latestWait === 'waiting'
|
|
146
|
+
? WAITING_NOTICE
|
|
147
|
+
: entryListFailed && modifiedCount > 0
|
|
148
|
+
? 'The list of entries couldn\'t be loaded, so these changes can\'t be saved yet. Choose "Refresh branch" in the branch menu to try again.'
|
|
149
|
+
: undefined), saveAllProgress: saveAllProgress, singleSaving: singleSaving, busy: busy, buttonRef: saveAllRef, caret: split && workflow ? (_jsx(WorkflowCaret, { ...workflowControl, action: workflow })) : undefined }, branchName)), barAction && (_jsx(BranchWayButton, { action: barAction, buttonRef: wayRef, onClick: barAction === 'newBranch'
|
|
103
150
|
? onNewBranch
|
|
104
151
|
: barAction === 'refreshBranch'
|
|
105
152
|
? onBranchReloadData
|
|
@@ -131,13 +178,18 @@ function unsavedTooltip(status, count, mayWithdraw) {
|
|
|
131
178
|
* The branch's state beside the pill, heard in words with its reason, but not while it loads. A
|
|
132
179
|
* hold follows it whatever the lock, since a lock doesn't say that the branch is stuck.
|
|
133
180
|
*/
|
|
134
|
-
function BranchStatusLabel({ status, reason, holdMessage, }) {
|
|
135
|
-
const spoken =
|
|
136
|
-
|
|
137
|
-
|
|
181
|
+
function BranchStatusLabel({ status, reason, holdMessage, holdCount, working, workingSpinner = true, }) {
|
|
182
|
+
const spoken = [
|
|
183
|
+
status.label && status.value !== 'loading'
|
|
184
|
+
? [status.label.replace(' · ', ', '), reason].filter(Boolean).join('. ')
|
|
185
|
+
: '',
|
|
186
|
+
working,
|
|
187
|
+
]
|
|
188
|
+
.filter(Boolean)
|
|
189
|
+
.join('. ');
|
|
138
190
|
return (
|
|
139
191
|
// Always mounted, so a lock that arrives is announced.
|
|
140
|
-
_jsxs(Box, { role: "status", style: status.label || holdMessage ? { flexShrink: 0 } : HIDDEN_STYLE, children: [_jsxs(Group, { gap: "xs", wrap: "nowrap", children: [status.label && (_jsxs(Group, { gap: 4, wrap: "nowrap", "aria-hidden": "true", "data-testid": "branch-status", "data-status": status.value, children: [status.readOnly && _jsx(IconLock, { size: 14 }), _jsx(Text, { span: true, size: "sm", children: status.label })] })), holdMessage && (_jsx(TooltipDetail, { detail: holdMessage, children: _jsx(Badge, { color: HOLD_BADGE.color, variant: "light", "data-testid": "branch-hold-status", children: HOLD_BADGE.label }) }))] }), _jsx(VisuallyHidden, { children: spoken })] }));
|
|
192
|
+
_jsxs(Box, { role: "status", style: status.label || holdMessage || working ? { flexShrink: 0 } : HIDDEN_STYLE, children: [_jsxs(Group, { gap: "xs", wrap: "nowrap", children: [working && (_jsxs(Group, { gap: 4, wrap: "nowrap", "aria-hidden": "true", "data-testid": "branch-working-status", children: [workingSpinner && _jsx(Loader, { size: 12 }), _jsx(Text, { span: true, size: "sm", children: working })] })), status.label && (_jsxs(Group, { gap: 4, wrap: "nowrap", "aria-hidden": "true", "data-testid": "branch-status", "data-status": status.value, children: [status.readOnly && _jsx(IconLock, { size: 14 }), _jsx(Text, { span: true, size: "sm", children: status.label })] })), holdMessage && (_jsx(TooltipDetail, { detail: holdMessage, children: _jsx(Badge, { color: HOLD_BADGE.color, variant: "light", "data-testid": "branch-hold-status", children: holdCount === undefined ? HOLD_BADGE.label : `${HOLD_BADGE.label} (${holdCount})` }) }))] }), _jsx(VisuallyHidden, { children: spoken })] }));
|
|
141
193
|
}
|
|
142
194
|
const WAY_LABEL = {
|
|
143
195
|
newBranch: `${EDITOR_ACTIONS.newBranch}…`,
|
|
@@ -154,7 +206,7 @@ function BranchWayButton({ action, onClick, caret, buttonRef, }) {
|
|
|
154
206
|
* Submit for review, or Withdraw from review once submitted or approved, or Approve and Request
|
|
155
207
|
* changes to a reviewer of someone else's submission; null when none shows.
|
|
156
208
|
*/
|
|
157
|
-
function workflowAction({ branchName, busy, branchStatus, userContext, branchCreatedBy, branchAccess, branchIsProtected, reviewRole, savingAll, rewritten, approveBlocked, }) {
|
|
209
|
+
function workflowAction({ branchName, busy, branchStatus, userContext, branchCreatedBy, branchAccess, branchIsProtected, reviewRole, savingAll, rewritten, approveBlocked, waitingForLatest, openConflicts, heldReason, }) {
|
|
158
210
|
const isSubmitted = branchStatus === 'submitted';
|
|
159
211
|
const isEditing = branchStatus === 'editing';
|
|
160
212
|
// 'approved' withdraws too -- it is the author's way back from
|
|
@@ -176,7 +228,7 @@ function workflowAction({ branchName, busy, branchStatus, userContext, branchCre
|
|
|
176
228
|
blocked: !branchName || busy || savingAll,
|
|
177
229
|
permitted: true,
|
|
178
230
|
reason: savingAll ? 'Wait for Save all to finish.' : busy ? BUSY_REASON : '',
|
|
179
|
-
approveReason: approveBlocked ? UNFINISHED_SUBMIT_APPROVE_REASON :
|
|
231
|
+
approveReason: approveBlocked ? UNFINISHED_SUBMIT_APPROVE_REASON : heldReason,
|
|
180
232
|
};
|
|
181
233
|
}
|
|
182
234
|
// Who may submit/withdraw: creator, ACL access, the system branch, or an
|
|
@@ -201,24 +253,34 @@ function workflowAction({ branchName, busy, branchStatus, userContext, branchCre
|
|
|
201
253
|
return {
|
|
202
254
|
kind: isWithdrawable ? 'withdraw' : 'submit',
|
|
203
255
|
// `busy` alone can't hold it: an entry load clears that shared flag mid-run.
|
|
204
|
-
blocked: !branchName ||
|
|
256
|
+
blocked: !branchName ||
|
|
257
|
+
busy ||
|
|
258
|
+
savingAll ||
|
|
259
|
+
!canPerformAction ||
|
|
260
|
+
(isEditing && (rewritten || waitingForLatest || openConflicts > 0 || !!heldReason)),
|
|
205
261
|
permitted: canPerformAction,
|
|
206
262
|
reason: savingAll
|
|
207
263
|
? 'Wait for Save all to finish.'
|
|
208
|
-
:
|
|
209
|
-
?
|
|
210
|
-
:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
264
|
+
: waitingForLatest && isEditing
|
|
265
|
+
? WAITING_SUBMIT_REASON
|
|
266
|
+
: heldReason && isEditing
|
|
267
|
+
? heldReason
|
|
268
|
+
: openConflicts > 0 && isEditing
|
|
269
|
+
? conflictsSubmitReason(openConflicts)
|
|
270
|
+
: rewritten && isEditing
|
|
271
|
+
? rewrittenSubmitReason(isAdmin(userContext?.groups ?? []))
|
|
272
|
+
: // No branch data (`branchStatus === undefined`) must not be
|
|
273
|
+
// interpolated raw into this label. Guarded explicitly here rather
|
|
274
|
+
// than left to the early return above, which hides the action
|
|
275
|
+
// today only as a side effect of `branchIsProtected`'s `?? true`
|
|
276
|
+
// default -- not a guarantee this arm stays unreachable.
|
|
277
|
+
branchStatus === undefined
|
|
278
|
+
? 'Branch details could not be loaded. Use "Refresh branch" to try again.'
|
|
279
|
+
: !statusHasAction
|
|
280
|
+
? `This branch is ${branchStatus}, so it can't be submitted for review or withdrawn from review.`
|
|
281
|
+
: !userHasPermission
|
|
282
|
+
? 'You do not have permission to submit this branch for review or withdraw it from review. Ask an admin for access.'
|
|
283
|
+
: '',
|
|
222
284
|
};
|
|
223
285
|
}
|
|
224
286
|
function WorkflowButton({ narrow, ...props }) {
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
import React from 'react';
|
|
7
7
|
import type { BranchChangeEntry } from '../../api/branch-changes.js';
|
|
8
8
|
import type { FieldConfig } from '../../config/index.js';
|
|
9
|
+
import type { CanopyPathSegment } from '../canopy-path.js';
|
|
9
10
|
import type { EntryChangeState } from '../hooks/useBranchChangesData.js';
|
|
11
|
+
import type { ConflictsState } from '../hooks/useConflictsData.js';
|
|
12
|
+
import type { EntryConflictData } from '../../api/merge-conflicts.js';
|
|
13
|
+
import { type ConflictsGroupProps } from './ConflictsGroup.js';
|
|
10
14
|
/** The entry a drawer compares, as the editor knows it. */
|
|
11
15
|
export interface EntryChangesSubject {
|
|
12
16
|
logicalPath: string;
|
|
@@ -41,11 +45,57 @@ export interface EntryChangesDrawerProps {
|
|
|
41
45
|
onOpenEntry?: () => void;
|
|
42
46
|
/** The branch's pull request, offered where this drawer can't compare. */
|
|
43
47
|
pullRequestUrl?: string;
|
|
48
|
+
/** Undoing the entry's changes in its form; given only for the open entry. */
|
|
49
|
+
undo?: EntryChangesUndo;
|
|
50
|
+
/**
|
|
51
|
+
* Why this user can't change the entry at all, such as a path rule: its undo, delete and
|
|
52
|
+
* restore controls are hidden and this one line says why.
|
|
53
|
+
*/
|
|
54
|
+
noEditReason?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Why those controls can't act now, such as a locked branch: they stay, disabled, saying so,
|
|
57
|
+
* as the form's do.
|
|
58
|
+
*/
|
|
59
|
+
lockedReason?: string;
|
|
60
|
+
/** Opens the confirm that deletes an entry added on the branch, closing the drawer first. */
|
|
61
|
+
onDeleteEntry?: () => void;
|
|
62
|
+
/** Puts back an entry deleted on the branch; resolves with why it couldn't, when it couldn't. */
|
|
63
|
+
onRestoreEntry?: () => Promise<RestoreRefusal | undefined>;
|
|
64
|
+
/** The entry's open conflicts, given while the branch lists any for it. */
|
|
65
|
+
conflicts?: EntryChangesConflicts;
|
|
44
66
|
}
|
|
45
|
-
/**
|
|
46
|
-
export
|
|
67
|
+
/** What the "Conflicts" group needs beyond the drawer's own props (ConflictsGroup.tsx). */
|
|
68
|
+
export interface EntryChangesConflicts extends Pick<ConflictsGroupProps, 'adminBaseBranch' | 'choices' | 'onChoose' | 'onEdit'> {
|
|
69
|
+
state: ConflictsState<EntryConflictData | null>;
|
|
70
|
+
onResolveEntry?: ConflictsGroupProps['onResolveEntry'];
|
|
71
|
+
/** The entry has a draft, open or not, which an entry-level choice would leave behind. */
|
|
72
|
+
unsavedEdits?: boolean;
|
|
73
|
+
}
|
|
74
|
+
/** Why "Restore entry" didn't: `warning` where nothing is lost, or the outcome is unknown. */
|
|
75
|
+
export interface RestoreRefusal {
|
|
76
|
+
message: string;
|
|
77
|
+
tone: 'warning' | 'error';
|
|
78
|
+
}
|
|
79
|
+
/** The open entry's form value, and the way to change its draft; see `undo-change.ts`. */
|
|
80
|
+
export interface EntryChangesUndo {
|
|
81
|
+
/** The entry's value as its form holds it, unsaved edits included. */
|
|
82
|
+
value: Record<string, unknown>;
|
|
83
|
+
/**
|
|
84
|
+
* Its saved value as the form loaded it, which a redo puts back: references resolved, so a
|
|
85
|
+
* redone entry matches its saved state and isn't left unsaved. Else the comparison's saved side.
|
|
86
|
+
*/
|
|
87
|
+
saved?: Record<string, unknown>;
|
|
88
|
+
/** Sets the entry's draft to what `update` makes of its value at that moment. */
|
|
89
|
+
update: (update: (value: Record<string, unknown>) => Record<string, unknown>) => void;
|
|
90
|
+
/** Open field threads inside a canopy path, which taking out an item there moves to the entry. */
|
|
91
|
+
threadsWithin?: (path: CanopyPathSegment[]) => number;
|
|
92
|
+
}
|
|
93
|
+
type EntryChangesViewProps = Omit<EntryChangesDrawerProps, 'opened' | 'onClose' | 'onExited' | 'returnFocusTo'>;
|
|
94
|
+
/** The drawer's body: the entry's conflicts, if any, above its changes. */
|
|
95
|
+
export declare const EntryChangesView: React.FC<EntryChangesViewProps>;
|
|
47
96
|
/**
|
|
48
97
|
* A modal right-hand drawer, as the Branches, Comments and Media drawers are: it traps focus,
|
|
49
98
|
* and the form and preview wait behind its backdrop.
|
|
50
99
|
*/
|
|
51
100
|
export declare const EntryChangesDrawer: React.FC<EntryChangesDrawerProps>;
|
|
101
|
+
export {};
|