canopycms 0.1.0-int.115 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +267 -231
- package/README.md +0 -1
- package/dist/ai/references.js +3 -1
- package/dist/api/assets.js +2 -2
- package/dist/api/branch-merge.d.ts +1 -1
- package/dist/api/branch.js +1 -1
- package/dist/api/content.js +1 -1
- package/dist/api/entries-constants.d.ts +1 -1
- package/dist/api/entries-constants.js +1 -1
- package/dist/api/entries.d.ts +2 -2
- package/dist/api/permissions.d.ts +2 -2
- package/dist/api/types.d.ts +3 -2
- package/dist/cli/cli.js +16 -14
- package/dist/cli/generate-ai-content.js +7 -5
- package/dist/cli/init.js +4 -4
- package/dist/client.d.ts +0 -1
- package/dist/client.js +0 -1
- package/dist/config/schemas/config.d.ts +0 -5
- package/dist/config/schemas/config.js +0 -1
- package/dist/config/types.d.ts +0 -1
- package/dist/content-index-generation.d.ts +1 -1
- package/dist/content-index-generation.js +1 -1
- package/dist/content-listing.js +1 -1
- package/dist/content-reader.js +1 -1
- package/dist/content-store.d.ts +1 -1
- package/dist/content-store.js +1 -1
- package/dist/dev-content-watcher.d.ts +1 -1
- package/dist/dev-content-watcher.js +1 -1
- package/dist/editor/BranchManager.d.ts +2 -0
- package/dist/editor/BranchManager.js +5 -4
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +1 -2
- package/dist/editor/CommentsPanel.js +3 -6
- package/dist/editor/Editor.d.ts +0 -2
- package/dist/editor/Editor.js +329 -181
- package/dist/editor/EditorAuthGate.js +8 -3
- package/dist/editor/EntryNavigator.d.ts +10 -0
- package/dist/editor/EntryNavigator.js +51 -2
- package/dist/editor/FormHeader.js +1 -1
- package/dist/editor/PreviewFrame.d.ts +5 -1
- package/dist/editor/PreviewFrame.js +3 -2
- package/dist/editor/PreviewPane.d.ts +47 -0
- package/dist/editor/PreviewPane.js +147 -0
- package/dist/editor/PreviewToolbarTools.d.ts +22 -0
- package/dist/editor/PreviewToolbarTools.js +73 -0
- package/dist/editor/comments/form-comments.js +2 -1
- package/dist/editor/components/BranchLockBanner.d.ts +22 -0
- package/dist/editor/components/BranchLockBanner.js +39 -0
- package/dist/editor/components/BranchPill.d.ts +32 -0
- package/dist/editor/components/BranchPill.js +33 -0
- package/dist/editor/components/ConfirmDeleteModal.d.ts +3 -1
- package/dist/editor/components/ConfirmDeleteModal.js +3 -2
- package/dist/editor/components/EditorHeader.d.ts +50 -108
- package/dist/editor/components/EditorHeader.js +117 -138
- package/dist/editor/components/EditorRail.d.ts +49 -0
- package/dist/editor/components/EditorRail.js +65 -0
- package/dist/editor/components/EntryBreadcrumb.d.ts +33 -0
- package/dist/editor/components/EntryBreadcrumb.js +56 -0
- package/dist/editor/components/QuickOpenPalette.d.ts +1 -1
- package/dist/editor/components/QuickOpenPalette.js +3 -2
- package/dist/editor/components/RenameEntryModal.d.ts +3 -1
- package/dist/editor/components/RenameEntryModal.js +3 -2
- package/dist/editor/components/SaveAllResult.d.ts +25 -0
- package/dist/editor/components/SaveAllResult.js +86 -0
- package/dist/editor/components/SaveSplitButton.d.ts +30 -0
- package/dist/editor/components/SaveSplitButton.js +108 -0
- package/dist/editor/components/index.d.ts +2 -2
- package/dist/editor/components/index.js +2 -2
- package/dist/editor/context/EditorStateContext.d.ts +4 -4
- package/dist/editor/context/EditorStateContext.js +4 -4
- package/dist/editor/copy.d.ts +3 -0
- package/dist/editor/copy.js +3 -0
- package/dist/editor/editor-utils.d.ts +11 -6
- package/dist/editor/editor-utils.js +8 -17
- package/dist/editor/fields/MarkdownField.js +3 -0
- package/dist/editor/hooks/save-all.d.ts +68 -0
- package/dist/editor/hooks/save-all.js +61 -0
- package/dist/editor/hooks/useCommentSystem.d.ts +2 -0
- package/dist/editor/hooks/useCommentSystem.js +3 -0
- package/dist/editor/hooks/useDraftManager.d.ts +24 -1
- package/dist/editor/hooks/useDraftManager.js +297 -16
- package/dist/editor/hooks/useEditorLayout.d.ts +15 -11
- package/dist/editor/hooks/useEditorLayout.js +13 -25
- package/dist/editor/hooks/useEntryManager.d.ts +4 -0
- package/dist/editor/hooks/useEntryManager.js +30 -7
- package/dist/editor/media/MediaLibrary.d.ts +1 -1
- package/dist/editor/preview-path.d.ts +12 -0
- package/dist/editor/preview-path.js +45 -1
- package/dist/editor/preview-scale.d.ts +27 -0
- package/dist/editor/preview-scale.js +34 -0
- package/dist/editor/shell/ActivityRail.d.ts +37 -0
- package/dist/editor/shell/ActivityRail.js +91 -0
- package/dist/editor/shell/EditorShell.d.ts +17 -0
- package/dist/editor/shell/EditorShell.js +31 -0
- package/dist/editor/shell/PaneLayout.d.ts +28 -0
- package/dist/editor/shell/PaneLayout.js +116 -0
- package/dist/editor/shell/SidePanelHost.d.ts +35 -0
- package/dist/editor/shell/SidePanelHost.js +184 -0
- package/dist/editor/shell/TopBar.d.ts +12 -0
- package/dist/editor/shell/TopBar.js +14 -0
- package/dist/editor/shell/host-chrome.d.ts +16 -0
- package/dist/editor/shell/host-chrome.js +69 -0
- package/dist/editor/theme.js +18 -3
- package/dist/git-manager.d.ts +1 -1
- package/dist/git-manager.js +2 -2
- package/dist/operating-mode/types.d.ts +1 -1
- package/dist/resolve-canopy-user.d.ts +1 -1
- package/dist/resolve-canopy-user.js +1 -1
- package/dist/schema/resolver.d.ts +1 -1
- package/dist/schema/resolver.js +1 -1
- package/dist/schema/schema-store.d.ts +1 -1
- package/dist/schema/schema-store.js +2 -2
- package/dist/url-collision.js +3 -3
- package/dist/url-exclusivity-fixtures.d.ts +1 -1
- package/dist/url-exclusivity-fixtures.js +1 -1
- package/dist/utils/content-write-lock.d.ts +1 -1
- package/dist/utils/content-write-lock.js +1 -1
- package/dist/utils/debug.d.ts +1 -1
- package/dist/utils/debug.js +5 -3
- package/dist/utils/git.d.ts +4 -0
- package/dist/utils/git.js +27 -5
- package/dist/validation/block-structural-keys.d.ts +1 -1
- package/dist/validation/block-structural-keys.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/cms-worker.d.ts +1 -1
- package/dist/worker/cms-worker.js +2 -2
- package/dist/worker/github-auth.d.ts +1 -1
- package/dist/worker/github-auth.js +1 -1
- package/dist/worker/github-gateway-wire.d.ts +2 -1
- package/dist/worker/github-gateway-wire.js +7 -3
- package/dist/worker/github-gateway.js +1 -1
- package/dist/worker/github-mirror.d.ts +2 -2
- package/dist/worker/github-mirror.js +2 -2
- package/dist/worker/history-rewrite.d.ts +1 -1
- package/dist/worker/history-rewrite.js +1 -1
- package/dist/worker/self-checks.d.ts +1 -1
- package/dist/worker/shared-repo-git.js +1 -1
- package/package.json +5 -4
- package/dist/editor/EditorPanes.d.ts +0 -20
- package/dist/editor/EditorPanes.js +0 -124
- package/dist/editor/components/EditorFooter.d.ts +0 -11
- package/dist/editor/components/EditorFooter.js +0 -9
- package/dist/editor/components/EditorSidebar.d.ts +0 -70
- package/dist/editor/components/EditorSidebar.js +0 -50
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useRef } from 'react';
|
|
4
|
+
import { Box, Button, Menu, Text, Tooltip, VisuallyHidden } from '@mantine/core';
|
|
5
|
+
import { useMediaQuery, useMergedRef } from '@mantine/hooks';
|
|
6
|
+
import { IconChevronDown, IconGitBranch } from '@tabler/icons-react';
|
|
7
|
+
import { EDITOR_ACTIONS } from '../copy.js';
|
|
8
|
+
import { DestructiveMenuItem } from './DestructiveMenuItem.js';
|
|
9
|
+
import { ReasonedMenuItem } from './ReasonedMenuItem.js';
|
|
10
|
+
import { useMenuItemFocusReturn } from '../utils/focus-return.js';
|
|
11
|
+
const plural = (count, one, many) => `${count} ${count === 1 ? one : many}`;
|
|
12
|
+
/**
|
|
13
|
+
* The branch's name, status and unsaved count. Its menu holds the branch actions and lists the
|
|
14
|
+
* unsaved entries, each opening that entry. Below `sm` it is the branch icon and the count, its
|
|
15
|
+
* name then in its Tooltip, accessible name and menu label: the row's actions must not clip.
|
|
16
|
+
*/
|
|
17
|
+
export function BranchPill({ branchName, loading, status, unsavedCount: unsaved, unsavedEntries, onSwitchBranch, onRefreshBranch, onDiscardAll, onEntrySelect, pillRef, }) {
|
|
18
|
+
const trigger = useRef(null);
|
|
19
|
+
const triggerRef = useMergedRef(trigger, pillRef);
|
|
20
|
+
const menuFocus = useMenuItemFocusReturn(trigger);
|
|
21
|
+
const noBranch = branchName ? undefined : 'Open a branch first.';
|
|
22
|
+
const wide = useMediaQuery('(min-width: 48em)');
|
|
23
|
+
const name = branchName || (loading ? 'Loading branches…' : 'No branch open');
|
|
24
|
+
return (_jsxs(Menu, { withinPortal: true, shadow: "sm", position: "bottom-end", ...menuFocus.menu, children: [_jsx(Tooltip, { label: status ? `${name} · ${status.label}` : name, disabled: wide, events: { hover: true, focus: true, touch: false }, children: _jsx(Menu.Target, { children: _jsxs(Button, { ref: triggerRef, variant: "default", size: "sm", miw: 0, px: { base: 8, sm: 14 },
|
|
25
|
+
// The count is the bar's one branch-wide unsaved signal: the row wraps before it clips.
|
|
26
|
+
style: { flexShrink: 0 }, leftSection: _jsx(IconGitBranch, { size: 16, "aria-hidden": "true" }), rightSection: wide ? _jsx(IconChevronDown, { size: 14, "aria-hidden": "true" }) : undefined, loading: !branchName && loading, "data-testid": "branch-dropdown-button", styles: { label: { minWidth: 0, gap: 6 } }, children: [_jsx(Text, { span: true, inherit: true, truncate: "end", maw: 200, visibleFrom: "sm", children: name }), _jsx(VisuallyHidden, { component: "span", hiddenFrom: "sm", children: name }), branchName && status && (_jsxs(Text, { span: true, size: "xs", c: "dimmed", visibleFrom: "md", style: { flexShrink: 0 }, children: [_jsx("span", { "aria-hidden": "true", children: "\u00B7 " }), _jsx("span", { "data-testid": status.testId, "data-status": status.value, children: status.label })] })), unsaved > 0 && (_jsxs(Text, { span: true, size: "xs", fw: 600, px: 6, style: {
|
|
27
|
+
flexShrink: 0,
|
|
28
|
+
borderRadius: 'var(--mantine-radius-xl)',
|
|
29
|
+
background: 'var(--canopy-count-bg)',
|
|
30
|
+
}, "data-testid": "branch-unsaved-count", children: [unsaved, _jsx(Box, { component: "span", visibleFrom: "sm", children: ' unsaved' }), _jsx(VisuallyHidden, { component: "span", hiddenFrom: "sm", children: ' unsaved' })] }))] }) }) }), _jsxs(Menu.Dropdown, { "data-testid": "branch-menu", ...menuFocus.dropdown, children: [branchName && status && (_jsx(Menu.Label, { hiddenFrom: "md", children: `${branchName} · ${status.label}` })), _jsx(Menu.Item, { onClick: onSwitchBranch, "data-testid": "manage-branches-menu-item", children: `${EDITOR_ACTIONS.switchBranch}…` }), _jsx(ReasonedMenuItem, { onClick: onRefreshBranch, blockedReason: noBranch, "data-testid": "refresh-branch-menu-item", children: "Refresh branch" }), _jsx(Menu.Divider, {}), _jsx(Menu.Label, { children: "Unsaved changes" }), unsavedEntries.length === 0 ? (_jsx(Text, { size: "sm", c: "dimmed", px: "sm", py: 6, "data-testid": "edited-entries-empty", children: unsaved === 0 ? 'No unsaved changes on this branch.' : 'Loading their names…' })) : (unsavedEntries.map((entry) => (_jsx(Menu.Item, { onClick: () => onEntrySelect(entry.path), children: entry.label }, entry.path)))), _jsx(Menu.Divider, {}), _jsx(DestructiveMenuItem, { "data-testid": "discard-all-drafts-menu-item", onClick: onDiscardAll, blockedReason: noBranch ?? (unsaved === 0 ? 'Nothing to discard.' : undefined), children: unsaved > 0
|
|
31
|
+
? `Discard all changes (${plural(unsaved, 'entry', 'entries')})`
|
|
32
|
+
: 'Discard all changes' })] })] }));
|
|
33
|
+
}
|
|
@@ -9,9 +9,11 @@ export interface ConfirmDeleteModalProps {
|
|
|
9
9
|
loading?: boolean;
|
|
10
10
|
/** Rendered between the message and the buttons. */
|
|
11
11
|
children?: ReactNode;
|
|
12
|
+
/** Where focus goes once it closes; Mantine's default, the element focused at open, without it. */
|
|
13
|
+
returnFocusTo?: () => HTMLElement | null;
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* Confirmation modal for delete operations.
|
|
15
17
|
* Provides a clear warning UI with red/danger theme.
|
|
16
18
|
*/
|
|
17
|
-
export declare function ConfirmDeleteModal({ isOpen, title, message, confirmLabel, onConfirm, onClose, loading, children, }: ConfirmDeleteModalProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
export declare function ConfirmDeleteModal({ isOpen, title, message, confirmLabel, onConfirm, onClose, loading, children, returnFocusTo, }: ConfirmDeleteModalProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Modal, Button, Text, Group, Stack } from '@mantine/core';
|
|
4
|
+
import { focusReturnProps } from '../utils/focus-return.js';
|
|
4
5
|
/**
|
|
5
6
|
* Confirmation modal for delete operations.
|
|
6
7
|
* Provides a clear warning UI with red/danger theme.
|
|
7
8
|
*/
|
|
8
|
-
export function ConfirmDeleteModal({ isOpen, title, message, confirmLabel = 'Delete', onConfirm, onClose, loading = false, children, }) {
|
|
9
|
-
return (_jsx(Modal, { opened: isOpen, onClose: onClose, title: title, centered: true, size: "md", closeButtonProps: { disabled: loading }, closeOnEscape: !loading, closeOnClickOutside: !loading, children: _jsxs(Stack, { gap: "md", "data-testid": "confirm-delete-modal", children: [_jsx(Text, { size: "sm", children: message }), children, _jsxs(Group, { justify: "flex-end", gap: "sm", children: [_jsx(Button, { size: "sm", variant: "default", onClick: onClose, disabled: loading, children: "Cancel" }), _jsx(Button, { size: "sm", color: "red", onClick: onConfirm, loading: loading, "data-testid": "confirm-delete-submit", children: confirmLabel })] })] }) }));
|
|
9
|
+
export function ConfirmDeleteModal({ isOpen, title, message, confirmLabel = 'Delete', onConfirm, onClose, loading = false, children, returnFocusTo, }) {
|
|
10
|
+
return (_jsx(Modal, { opened: isOpen, onClose: onClose, title: title, centered: true, size: "md", closeButtonProps: { disabled: loading }, closeOnEscape: !loading, closeOnClickOutside: !loading, ...focusReturnProps(returnFocusTo), children: _jsxs(Stack, { gap: "md", "data-testid": "confirm-delete-modal", children: [_jsx(Text, { size: "sm", children: message }), children, _jsxs(Group, { justify: "flex-end", gap: "sm", children: [_jsx(Button, { size: "sm", variant: "default", onClick: onClose, disabled: loading, children: "Cancel" }), _jsx(Button, { size: "sm", color: "red", onClick: onConfirm, loading: loading, "data-testid": "confirm-delete-submit", children: confirmLabel })] })] }) }));
|
|
10
11
|
}
|
|
@@ -3,132 +3,76 @@ import type { OperatingMode } from '../../operating-mode/index.js';
|
|
|
3
3
|
import type { BranchStatus } from '../../types.js';
|
|
4
4
|
import type { EditorEntry } from '../Editor.js';
|
|
5
5
|
import type { LogicalPath } from '../../paths/types.js';
|
|
6
|
+
import type { BreadcrumbSegment } from '../editor-utils.js';
|
|
7
|
+
import type { SaveAllProgress } from '../hooks/save-all.js';
|
|
6
8
|
export interface EditorHeaderProps {
|
|
7
|
-
/**
|
|
8
|
-
* Site title displayed in the top-left.
|
|
9
|
-
*/
|
|
10
|
-
siteTitle: string;
|
|
11
|
-
/**
|
|
12
|
-
* Optional site subtitle displayed below the title.
|
|
13
|
-
*/
|
|
14
|
-
siteSubtitle?: string;
|
|
15
|
-
/**
|
|
16
|
-
* Main header title displayed in the center.
|
|
17
|
-
*/
|
|
18
|
-
headerTitle: string;
|
|
19
|
-
/**
|
|
20
|
-
* Currently selected entry.
|
|
21
|
-
*/
|
|
22
9
|
currentEntry: EditorEntry | undefined;
|
|
23
|
-
/**
|
|
24
|
-
* Current branch name.
|
|
25
|
-
*/
|
|
26
10
|
branchName: string;
|
|
27
|
-
/**
|
|
28
|
-
* Operating mode.
|
|
29
|
-
*/
|
|
30
11
|
operatingMode: OperatingMode;
|
|
31
|
-
/**
|
|
32
|
-
* Whether operations are currently in progress.
|
|
33
|
-
*/
|
|
12
|
+
/** Whether operations are currently in progress. */
|
|
34
13
|
busy: boolean;
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
breadcrumbSegments: string[];
|
|
39
|
-
/**
|
|
40
|
-
* List of edited files.
|
|
41
|
-
*/
|
|
14
|
+
/** The crumbs before the open entry's own. */
|
|
15
|
+
breadcrumbSegments: BreadcrumbSegment[];
|
|
16
|
+
/** The listed entries with unsaved changes on the branch. */
|
|
42
17
|
editedFiles: Array<{
|
|
43
18
|
path: LogicalPath;
|
|
44
19
|
label: string;
|
|
45
20
|
}>;
|
|
46
|
-
/**
|
|
47
|
-
* Number of modified files.
|
|
48
|
-
*/
|
|
21
|
+
/** Entries with unsaved changes on the branch. */
|
|
49
22
|
modifiedCount: number;
|
|
50
|
-
/**
|
|
51
|
-
* Number of unresolved comments.
|
|
52
|
-
*/
|
|
53
|
-
unresolvedCommentCount: number;
|
|
54
|
-
/**
|
|
55
|
-
* Unresolved comments (for filtering).
|
|
56
|
-
*/
|
|
57
|
-
comments: Array<{
|
|
58
|
-
resolved: boolean;
|
|
59
|
-
}>;
|
|
60
|
-
/**
|
|
61
|
-
* Callback to open the entry navigator.
|
|
62
|
-
*/
|
|
23
|
+
/** Opens the content panel. */
|
|
63
24
|
onNavigatorOpen: () => void;
|
|
64
|
-
/**
|
|
25
|
+
/** Opens the content panel on a collection. */
|
|
26
|
+
onCollectionOpen: (collectionPath: string) => void;
|
|
27
|
+
/** The entry menu's trigger. */
|
|
65
28
|
navigatorToggleRef?: Ref<HTMLButtonElement>;
|
|
66
|
-
/**
|
|
29
|
+
/** The branch pill. */
|
|
67
30
|
branchToggleRef?: Ref<HTMLButtonElement>;
|
|
68
|
-
/**
|
|
69
|
-
* Callback to reload the current file.
|
|
70
|
-
*/
|
|
31
|
+
/** Replaces the open entry with the server's copy. */
|
|
71
32
|
onFileReload: () => void;
|
|
72
|
-
/**
|
|
73
|
-
* Callback to discard the current file draft.
|
|
74
|
-
*/
|
|
75
33
|
onFileDiscardDraft: () => void;
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
|
|
34
|
+
/** Opens the open entry's rename dialog. */
|
|
35
|
+
onEntryRename: () => void;
|
|
36
|
+
/** Asks to delete the open entry. */
|
|
37
|
+
onEntryDelete: () => void;
|
|
38
|
+
/** The "All content" crumb. */
|
|
39
|
+
allContentRef?: Ref<HTMLButtonElement>;
|
|
40
|
+
onEntrySelect: (path: LogicalPath) => void;
|
|
41
|
+
/** Re-reads the branch and its entry list, keeping drafts. */
|
|
83
42
|
onBranchReloadData: () => void;
|
|
84
|
-
/**
|
|
85
|
-
* Callback to discard all drafts.
|
|
86
|
-
*/
|
|
87
43
|
onBranchDiscardDrafts: () => void;
|
|
88
|
-
/**
|
|
89
|
-
* Callback to open the branch manager.
|
|
90
|
-
*/
|
|
91
44
|
onBranchManagerOpen: () => void;
|
|
92
|
-
/**
|
|
93
|
-
* Callback to open the comments panel.
|
|
94
|
-
*/
|
|
95
|
-
onCommentsPanelOpen: () => void;
|
|
96
|
-
/**
|
|
97
|
-
* Callback to save the current file.
|
|
98
|
-
*/
|
|
99
45
|
onSave: () => void;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
46
|
+
onSaveAll: () => void;
|
|
47
|
+
saveAllProgress: SaveAllProgress | null;
|
|
48
|
+
/** A Save of the open entry is in flight. */
|
|
49
|
+
saving?: boolean;
|
|
50
|
+
/** Rises each time a single Save lands, whichever entry it saved. */
|
|
51
|
+
savedSignal?: number;
|
|
52
|
+
/** The last Save that landed saved the open entry. */
|
|
53
|
+
savedHere?: boolean;
|
|
54
|
+
/** The branch's entry list failed to load and none is held. */
|
|
55
|
+
entryListFailed?: boolean;
|
|
103
56
|
onSubmit: () => void;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
*/
|
|
57
|
+
onWithdraw: () => void;
|
|
58
|
+
/** The open entry has unsaved changes it could save. */
|
|
107
59
|
hasUnsavedChanges: boolean;
|
|
60
|
+
/** The open entry has a draft, savable or not (a read-only branch's still discards). */
|
|
61
|
+
entryHasDraft?: boolean;
|
|
108
62
|
/** Writing drafts to localStorage failed, so they live only in this tab. */
|
|
109
63
|
draftStorageFailed?: boolean;
|
|
110
|
-
/**
|
|
111
|
-
|
|
112
|
-
|
|
64
|
+
/** The open entry can't be edited by this user (a path rule). */
|
|
65
|
+
entryReadOnly?: boolean;
|
|
66
|
+
/** The open entry's type has no schema in the running code. */
|
|
67
|
+
entryUnavailable?: boolean;
|
|
68
|
+
/** Current branch status (undefined if unknown). */
|
|
113
69
|
branchStatus: BranchStatus | undefined;
|
|
114
|
-
/**
|
|
115
|
-
* Callback to withdraw the branch.
|
|
116
|
-
*/
|
|
117
|
-
onWithdraw: () => void;
|
|
118
|
-
/**
|
|
119
|
-
* Current user context for permission checks.
|
|
120
|
-
*/
|
|
70
|
+
/** Current user context for permission checks. */
|
|
121
71
|
userContext?: {
|
|
122
72
|
userId: string;
|
|
123
73
|
groups?: string[];
|
|
124
74
|
};
|
|
125
|
-
/**
|
|
126
|
-
* Branch creator user ID.
|
|
127
|
-
*/
|
|
128
75
|
branchCreatedBy?: string;
|
|
129
|
-
/**
|
|
130
|
-
* Branch access control lists.
|
|
131
|
-
*/
|
|
132
76
|
branchAccess?: {
|
|
133
77
|
allowedUsers?: string[];
|
|
134
78
|
allowedGroups?: string[];
|
|
@@ -140,20 +84,18 @@ export interface EditorHeaderProps {
|
|
|
140
84
|
* stays, as the recovery path). Default false.
|
|
141
85
|
*/
|
|
142
86
|
branchIsProtected?: boolean;
|
|
143
|
-
/**
|
|
144
|
-
* Whether the current branch is read-only (protected base branch in prod).
|
|
145
|
-
* Disables Save and shows the "create a branch" banner below. Default false.
|
|
146
|
-
*/
|
|
87
|
+
/** Whether the current branch is read-only (protected base branch in prod); disables Save. */
|
|
147
88
|
branchReadOnly?: boolean;
|
|
89
|
+
/** Server-computed: content writes are rejected on this branch; see `branchLockState`. */
|
|
90
|
+
branchWriteBlocked?: boolean;
|
|
148
91
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* `branchReadOnly` to pick which of the two banners to show. Default false.
|
|
92
|
+
* No branch details yet (loading, or the list failed and the banner says so): the fail-closed
|
|
93
|
+
* lock defaults are guesses, so neither the chip nor the status names them.
|
|
152
94
|
*/
|
|
153
|
-
|
|
95
|
+
branchLoading?: boolean;
|
|
154
96
|
}
|
|
155
97
|
/**
|
|
156
|
-
*
|
|
157
|
-
*
|
|
98
|
+
* The top bar's contents: the breadcrumb with the entry menu and "Read-only" chip, then the branch
|
|
99
|
+
* pill, Submit or Withdraw, and Save with Save all.
|
|
158
100
|
*/
|
|
159
|
-
export declare
|
|
101
|
+
export declare function EditorHeader({ currentEntry, branchName, operatingMode, busy, breadcrumbSegments, editedFiles, modifiedCount, onNavigatorOpen, onCollectionOpen, navigatorToggleRef, branchToggleRef, onFileReload, onFileDiscardDraft, onEntryRename, onEntryDelete, allContentRef, onEntrySelect, onBranchReloadData, onBranchDiscardDrafts, onBranchManagerOpen, onSave, onSaveAll, saveAllProgress, saving, savedSignal, savedHere, entryListFailed, onSubmit, onWithdraw, hasUnsavedChanges, entryHasDraft, draftStorageFailed, entryReadOnly, entryUnavailable, branchStatus, userContext, branchCreatedBy, branchAccess, branchIsProtected, branchReadOnly, branchWriteBlocked, branchLoading, }: EditorHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,49 +1,27 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import { Alert, Badge, Box, Button, Group, Indicator, Menu, Paper, Stack, Text, Title, Tooltip, VisuallyHidden, } from '@mantine/core';
|
|
4
|
-
import { useMergedRef } from '@mantine/hooks';
|
|
5
|
-
import { IconFolderOpen, IconChevronDown, IconGitBranch, IconLock } from '@tabler/icons-react';
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button, Tooltip } from '@mantine/core';
|
|
6
3
|
import { clientOperatingStrategy } from '../../operating-mode/client.js';
|
|
7
4
|
import { isAdmin, isLockedOutOfBranch, isReviewer } from '../../authorization/helpers.js';
|
|
8
5
|
import { branchStatusPresentation } from '../branch-status.js';
|
|
9
6
|
import { EDITOR_ACTIONS } from '../copy.js';
|
|
10
7
|
import { SYSTEM_USER_ID } from '../../system-users.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return (_jsxs(Text, { size: "sm", c: "dimmed", px: "sm", py: 6, ...rest, children: [children, _jsx(Text, { size: "xs", c: "dimmed", component: "span", display: "block", children: hint })] }));
|
|
17
|
-
}
|
|
8
|
+
import { TopBar } from '../shell/TopBar.js';
|
|
9
|
+
import { branchLockState } from './BranchLockBanner.js';
|
|
10
|
+
import { BranchPill } from './BranchPill.js';
|
|
11
|
+
import { EntryBreadcrumb } from './EntryBreadcrumb.js';
|
|
12
|
+
import { SaveSplitButton } from './SaveSplitButton.js';
|
|
18
13
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
14
|
+
* The top bar's contents: the breadcrumb with the entry menu and "Read-only" chip, then the branch
|
|
15
|
+
* pill, Submit or Withdraw, and Save with Save all.
|
|
21
16
|
*/
|
|
22
|
-
export
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// honestly: branch data still loading (`branchStatus` is undefined, so the
|
|
31
|
-
// banner would otherwise interpolate "Branch is undefined...") and version
|
|
32
|
-
// skew (an old server omits `branchWriteBlocked` while still reporting
|
|
33
|
-
// status: 'editing', which would otherwise assert a lock the status
|
|
34
|
-
// contradicts). In both cases the banner says data is unavailable instead.
|
|
35
|
-
const branchDataUnavailable = statusLocked && (branchStatus === undefined || branchStatus === 'editing');
|
|
36
|
-
const fileToggle = useRef(null);
|
|
37
|
-
const fileToggleRef = useMergedRef(fileToggle, navigatorToggleRef);
|
|
38
|
-
const branchToggle = useRef(null);
|
|
39
|
-
const branchToggleMergedRef = useMergedRef(branchToggle, branchToggleRef);
|
|
40
|
-
const fileMenuFocus = useMenuItemFocusReturn(fileToggle);
|
|
41
|
-
const branchMenuFocus = useMenuItemFocusReturn(branchToggle);
|
|
42
|
-
// Save stays focusable while blocked (`aria-disabled`, not `disabled`), so its reason opens on
|
|
43
|
-
// keyboard focus and touch as well as hover; the click does nothing.
|
|
44
|
-
const saveBlocked = !branchName || !currentEntry || busy || !hasUnsavedChanges || branchReadOnly || statusLocked;
|
|
45
|
-
// Why Save is blocked, with the way forward. The Tooltip hides it while a save is in flight.
|
|
46
|
-
const saveBlockedReason = branchReadOnly
|
|
17
|
+
export function EditorHeader({ currentEntry, branchName, operatingMode, busy, breadcrumbSegments, editedFiles, modifiedCount, onNavigatorOpen, onCollectionOpen, navigatorToggleRef, branchToggleRef, onFileReload, onFileDiscardDraft, onEntryRename, onEntryDelete, allContentRef, onEntrySelect, onBranchReloadData, onBranchDiscardDrafts, onBranchManagerOpen, onSave, onSaveAll, saveAllProgress, saving = false, savedSignal, savedHere = false, entryListFailed = false, onSubmit, onWithdraw, hasUnsavedChanges, entryHasDraft = hasUnsavedChanges, draftStorageFailed = false, entryReadOnly = false, entryUnavailable = false, branchStatus, userContext, branchCreatedBy, branchAccess, branchIsProtected = false, branchReadOnly = false, branchWriteBlocked = false, branchLoading = false, }) {
|
|
18
|
+
const { statusLocked, branchDataUnavailable } = branchLockState({
|
|
19
|
+
branchStatus,
|
|
20
|
+
branchReadOnly,
|
|
21
|
+
branchWriteBlocked,
|
|
22
|
+
});
|
|
23
|
+
// Why the branch takes no writes, with the way forward; shared by Save, Save all and the chip.
|
|
24
|
+
const branchLockReason = branchReadOnly
|
|
47
25
|
? 'The base branch is read-only. Create a branch to make changes.'
|
|
48
26
|
: branchDataUnavailable
|
|
49
27
|
? 'Branch details could not be loaded. Use "Switch branch…" to retry.'
|
|
@@ -53,102 +31,103 @@ export const EditorHeader = forwardRef(function EditorHeader({ siteTitle, siteSu
|
|
|
53
31
|
: branchStatus === 'approved'
|
|
54
32
|
? 'This branch is approved. Withdraw it to make changes.'
|
|
55
33
|
: `This branch is ${branchStatus}. Its content is read-only.`
|
|
56
|
-
:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
});
|
|
34
|
+
: undefined;
|
|
35
|
+
const entryLockReason = entryReadOnly
|
|
36
|
+
? "You don't have edit access to this entry. Ask a CanopyCMS admin to grant it."
|
|
37
|
+
: undefined;
|
|
38
|
+
// The Tooltip hides it while the editor is busy.
|
|
39
|
+
const saveBlockedReason = branchLockReason ??
|
|
40
|
+
(!branchName
|
|
41
|
+
? 'Open a branch first.'
|
|
42
|
+
: !currentEntry
|
|
43
|
+
? 'Open an entry to save it.'
|
|
44
|
+
: (entryLockReason ?? (!hasUnsavedChanges ? 'No changes to save.' : '')));
|
|
45
|
+
const presentation = branchStatusPresentation(branchStatus, branchIsProtected);
|
|
46
|
+
const status = !branchLoading &&
|
|
47
|
+
clientOperatingStrategy(operatingMode ?? 'prod').supportsStatusBadge() &&
|
|
48
|
+
presentation
|
|
49
|
+
? {
|
|
50
|
+
label: presentation.label,
|
|
51
|
+
value: branchIsProtected ? 'protected' : String(branchStatus),
|
|
52
|
+
testId: `header-status-badge-${branchIsProtected ? 'protected' : branchStatus}`,
|
|
53
|
+
}
|
|
54
|
+
: undefined;
|
|
55
|
+
return (_jsx(TopBar, { leading: _jsx(EntryBreadcrumb, { segments: breadcrumbSegments, entryLabel: currentEntry ? (currentEntry.label ?? currentEntry.slug) : undefined, entryDirty: hasUnsavedChanges, readOnlyReason: branchLoading
|
|
56
|
+
? undefined
|
|
57
|
+
: currentEntry || branchName
|
|
58
|
+
? (branchLockReason ?? entryLockReason)
|
|
59
|
+
: undefined, onAllContent: onNavigatorOpen, onCollection: onCollectionOpen, onRefresh: onFileReload, onDiscard: onFileDiscardDraft,
|
|
60
|
+
// A path rule's refusal hides them, as the content tree hides that entry's "…" menu.
|
|
61
|
+
onRename: entryReadOnly ? undefined : onEntryRename, onDelete: entryReadOnly ? undefined : onEntryDelete, refreshBlockedReason: branchName ? undefined : 'Open a branch first.', discardBlockedReason: entryHasDraft ? undefined : 'This entry has no unsaved changes.', changeBlockedReason: branchLockReason ??
|
|
62
|
+
(entryUnavailable
|
|
63
|
+
? "This entry's content type isn't known to this editor version yet. Reload in a few minutes."
|
|
64
|
+
: undefined), entryMenuRef: navigatorToggleRef, allContentRef: allContentRef }), trailing: _jsxs(_Fragment, { children: [_jsx(BranchPill, { branchName: branchName, loading: busy, status: status, unsavedCount: modifiedCount, unsavedEntries: editedFiles, onSwitchBranch: onBranchManagerOpen, onRefreshBranch: onBranchReloadData, onDiscardAll: onBranchDiscardDrafts, onEntrySelect: onEntrySelect, pillRef: branchToggleRef }), _jsx(WorkflowButton, { branchName: branchName, busy: busy, branchStatus: branchStatus, userContext: userContext, branchCreatedBy: branchCreatedBy, branchAccess: branchAccess, branchIsProtected: branchIsProtected, onSubmit: onSubmit, onWithdraw: onWithdraw, savingAll: !!saveAllProgress }), _jsx(SaveSplitButton, { onSave: onSave, saveBlockedReason: saveBlockedReason, busy: busy, onSaveAll: onSaveAll, unsavedCount: modifiedCount, saveAllBlockedReason: branchLockReason ??
|
|
65
|
+
(!branchName
|
|
66
|
+
? 'Open a branch first.'
|
|
67
|
+
: entryListFailed && modifiedCount > 0
|
|
68
|
+
? '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.'
|
|
69
|
+
: undefined), saveAllProgress: saveAllProgress, saving: saving, savedSignal: savedSignal, savedHere: savedHere, entryDirty: hasUnsavedChanges, draftStorageFailed: draftStorageFailed })] }) }));
|
|
70
|
+
}
|
|
71
|
+
/** Submit for review, or Withdraw from review once submitted or approved. */
|
|
72
|
+
function WorkflowButton({ branchName, busy, branchStatus, userContext, branchCreatedBy, branchAccess, branchIsProtected, onSubmit, onWithdraw, savingAll, }) {
|
|
73
|
+
const isSubmitted = branchStatus === 'submitted';
|
|
74
|
+
const isEditing = branchStatus === 'editing';
|
|
75
|
+
// 'approved' withdraws too -- it is that status's only
|
|
76
|
+
// non-destructive exit now that the submit status gate refuses a
|
|
77
|
+
// non-editing branch (api/branch-status.ts, api/branch-withdraw.ts).
|
|
78
|
+
// This button is a two-state toggle, so every withdrawable status
|
|
79
|
+
// has to drive the withdraw side of it, not just 'submitted'.
|
|
80
|
+
const isWithdrawable = isSubmitted || branchStatus === 'approved';
|
|
81
|
+
// The protected base branch can never be submitted for review (both
|
|
82
|
+
// modes); hide the button entirely rather than showing it disabled.
|
|
83
|
+
// Withdraw stays available as the recovery path for a base branch
|
|
84
|
+
// wrongly stuck in 'submitted'.
|
|
85
|
+
if (branchIsProtected && !isWithdrawable)
|
|
86
|
+
return null;
|
|
87
|
+
// Who may submit/withdraw: creator, ACL access, the system branch, or an
|
|
88
|
+
// admin/reviewer (alone on a locked-down branch, and even without ACL access
|
|
89
|
+
// on a protected base branch stuck in 'submitted'). Mirrors the server's
|
|
90
|
+
// canPerformWorkflowAction (BranchManager.tsx), which is the actual
|
|
91
|
+
// enforcer; this only decides whether to show the control disabled.
|
|
92
|
+
const userIsCreator = userContext?.userId === branchCreatedBy;
|
|
93
|
+
const isSystemBranch = branchCreatedBy === SYSTEM_USER_ID && !branchIsProtected;
|
|
94
|
+
const userInACL = userContext &&
|
|
95
|
+
branchAccess &&
|
|
96
|
+
(branchAccess.allowedUsers?.includes(userContext.userId) ||
|
|
97
|
+
userContext.groups?.some((g) => branchAccess.allowedGroups?.includes(g)));
|
|
98
|
+
const userIsPrivileged = !!userContext && (isAdmin(userContext.groups ?? []) || isReviewer(userContext.groups ?? []));
|
|
99
|
+
const userHasPermission = !isLockedOutOfBranch(branchAccess, userContext?.groups) &&
|
|
100
|
+
(userIsCreator || userInACL || isSystemBranch || userIsPrivileged);
|
|
101
|
+
// A status with no available transition (today: 'archived') is a
|
|
102
|
+
// separate condition from lacking permission, and conflating them
|
|
103
|
+
// sent people to an admin to fix a non-permissions problem.
|
|
104
|
+
const statusHasAction = isEditing || isWithdrawable;
|
|
105
|
+
const canPerformAction = userHasPermission && statusHasAction;
|
|
106
|
+
// Blocked, it stays focusable (`aria-disabled`), so its reason opens on focus.
|
|
107
|
+
// `busy` alone can't hold it: an entry load clears that shared flag mid-run.
|
|
108
|
+
const actionBlocked = !branchName || busy || savingAll || !canPerformAction;
|
|
109
|
+
const waitReason = savingAll ? 'Wait for Save all to finish.' : undefined;
|
|
110
|
+
return (_jsx(Tooltip, { label:
|
|
111
|
+
// No branch data (`branchStatus === undefined`) must not be
|
|
112
|
+
// interpolated raw into this label. Guarded explicitly here rather
|
|
113
|
+
// than left to the early return above, which hides this button
|
|
114
|
+
// today only as a side effect of `branchIsProtected`'s `?? true`
|
|
115
|
+
// default -- not a guarantee this arm stays unreachable.
|
|
116
|
+
waitReason ??
|
|
117
|
+
(branchStatus === undefined
|
|
118
|
+
? 'Branch details could not be loaded. Use "Switch branch…" to retry.'
|
|
119
|
+
: !statusHasAction
|
|
120
|
+
? `This branch is ${branchStatus}, so it can't be submitted for review or withdrawn from review.`
|
|
121
|
+
: !userHasPermission
|
|
122
|
+
? 'You do not have permission to submit this branch for review or withdraw it from review. Ask an admin for access.'
|
|
123
|
+
: ''), disabled: canPerformAction && !waitReason, multiline: true, maw: 320, children: _jsx(Button, { size: "sm",
|
|
124
|
+
// Save is the top bar's one filled primary; the rest take their colour's light variant.
|
|
125
|
+
variant: "light", color: isWithdrawable ? 'orange' : undefined, style: { flexShrink: 0 }, onClick: () => {
|
|
126
|
+
if (actionBlocked)
|
|
127
|
+
return;
|
|
128
|
+
if (isWithdrawable)
|
|
129
|
+
onWithdraw();
|
|
130
|
+
else
|
|
131
|
+
onSubmit();
|
|
132
|
+
}, "data-disabled": actionBlocked ? true : undefined, "aria-disabled": actionBlocked ? true : undefined, "data-testid": isWithdrawable ? 'withdraw-button' : 'submit-button', children: isWithdrawable ? EDITOR_ACTIONS.withdrawFromReview : EDITOR_ACTIONS.submitForReview }) }));
|
|
133
|
+
}
|