canopycms 0.2.0 → 0.3.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 +199 -41
- package/dist/api/branch.d.ts +2 -2
- package/dist/cli/cli.js +0 -18
- package/dist/cli/generate-ai-content.js +0 -6
- package/dist/cli/init.js +0 -6
- package/dist/editor/BranchManager.d.ts +2 -0
- package/dist/editor/BranchManager.js +3 -3
- package/dist/editor/Editor.js +253 -128
- package/dist/editor/EntryNavigator.d.ts +44 -9
- package/dist/editor/EntryNavigator.js +275 -103
- package/dist/editor/branch-lock.d.ts +42 -0
- package/dist/editor/branch-lock.js +79 -0
- package/dist/editor/branch-status.d.ts +0 -6
- package/dist/editor/branch-status.js +0 -10
- package/dist/editor/components/BranchNotAvailableNotice.d.ts +11 -0
- package/dist/editor/components/BranchNotAvailableNotice.js +10 -0
- package/dist/editor/components/BranchPill.d.ts +3 -18
- package/dist/editor/components/BranchPill.js +7 -10
- package/dist/editor/components/EditorHeader.d.ts +24 -52
- package/dist/editor/components/EditorHeader.js +151 -71
- package/dist/editor/components/EditorRail.d.ts +8 -8
- package/dist/editor/components/EditorRail.js +7 -5
- package/dist/editor/components/EntryBreadcrumb.d.ts +5 -1
- package/dist/editor/components/EntryBreadcrumb.js +3 -3
- package/dist/editor/components/EntryHeader.d.ts +60 -0
- package/dist/editor/components/EntryHeader.js +45 -0
- package/dist/editor/components/EntrySaveButton.d.ts +24 -0
- package/dist/editor/components/EntrySaveButton.js +101 -0
- package/dist/editor/components/NoEditPermissionNotice.d.ts +1 -1
- package/dist/editor/components/NoEditPermissionNotice.js +1 -1
- package/dist/editor/components/RetryToastMessage.d.ts +3 -1
- package/dist/editor/components/RetryToastMessage.js +8 -4
- package/dist/editor/components/SaveAllButton.d.ts +25 -0
- package/dist/editor/components/SaveAllButton.js +81 -0
- package/dist/editor/components/UnsavedMark.d.ts +1 -1
- package/dist/editor/components/UnsavedMark.js +1 -1
- package/dist/editor/components/index.d.ts +1 -1
- package/dist/editor/components/index.js +1 -1
- package/dist/editor/copy.d.ts +0 -1
- package/dist/editor/copy.js +0 -1
- package/dist/editor/editor-utils.d.ts +2 -2
- package/dist/editor/editor-utils.js +2 -2
- package/dist/editor/entry-search.d.ts +10 -16
- package/dist/editor/entry-search.js +4 -24
- package/dist/editor/hooks/useBranchManager.d.ts +7 -3
- package/dist/editor/hooks/useBranchManager.js +13 -9
- package/dist/editor/hooks/useEntryManager.d.ts +3 -0
- package/dist/editor/hooks/useEntryManager.js +10 -3
- package/dist/editor/shell/ActivityRail.d.ts +4 -0
- package/dist/editor/shell/ActivityRail.js +13 -4
- package/dist/editor/shell/EditorShell.d.ts +6 -7
- package/dist/editor/shell/EditorShell.js +9 -13
- package/dist/editor/shell/PaneLayout.d.ts +1 -0
- package/dist/editor/shell/PaneLayout.js +23 -5
- package/dist/editor/shell/SidePanelHost.d.ts +11 -4
- package/dist/editor/shell/SidePanelHost.js +95 -7
- package/dist/editor/shell/TopBar.d.ts +4 -4
- package/dist/editor/shell/TopBar.js +6 -3
- package/dist/editor/utils/focus-return.d.ts +10 -2
- package/dist/editor/utils/focus-return.js +40 -6
- package/dist/operating-mode/client-safe-strategy.d.ts +1 -6
- package/dist/operating-mode/client-safe-strategy.js +1 -10
- package/dist/operating-mode/types.d.ts +0 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/editor/components/BranchLockBanner.d.ts +0 -23
- package/dist/editor/components/BranchLockBanner.js +0 -66
- package/dist/editor/components/QuickOpenPalette.d.ts +0 -28
- package/dist/editor/components/QuickOpenPalette.js +0 -71
- package/dist/editor/components/SaveSplitButton.d.ts +0 -31
- package/dist/editor/components/SaveSplitButton.js +0 -121
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
-
import { ActionIcon, Badge, Box, Button, CloseButton, Group, Highlight, Loader, Menu, ScrollArea, Stack, Text, TextInput, Tooltip, Tree, useTree, rem, } from '@mantine/core';
|
|
4
|
+
import { ActionIcon, Badge, Box, Button, CloseButton, Group, Highlight, Loader, Menu, ScrollArea, SegmentedControl, Stack, Text, TextInput, Tooltip, Tree, useTree, rem, } from '@mantine/core';
|
|
5
|
+
import { useMergedRef } from '@mantine/hooks';
|
|
5
6
|
import { IconArrowDown, IconAlertTriangle, IconArrowUp, IconDots, IconEdit, IconFolderPlus, IconLock, IconPlus, IconSearch, IconTrash, } from '@tabler/icons-react';
|
|
6
7
|
import { calculatePathToEntry } from './editor-utils.js';
|
|
7
|
-
import { matchesEntryQuery } from './entry-search.js';
|
|
8
|
+
import { entryDetailLine, matchesEntryQuery } from './entry-search.js';
|
|
8
9
|
import { EDITOR_ACTIONS } from './copy.js';
|
|
9
10
|
import { DestructiveMenuItem } from './components/DestructiveMenuItem.js';
|
|
10
11
|
import { UnsavedMark } from './components/UnsavedMark.js';
|
|
11
12
|
import { BranchRowLock } from './components/BranchRowLock.js';
|
|
13
|
+
import { BranchNotAvailableNotice } from './components/BranchNotAvailableNotice.js';
|
|
12
14
|
import { ReasonedMenuItem } from './components/ReasonedMenuItem.js';
|
|
13
15
|
import { UnavailableTypeMessage } from './components/UnavailableTypeMessage.js';
|
|
14
16
|
const UnavailableTypeRow = ({ schemaRefs }) => (_jsxs(Group, { gap: 6, wrap: "nowrap", align: "flex-start", mt: 4, "data-testid": "unavailable-type-message", children: [_jsx(Box, { c: "orange", style: { display: 'flex', flexShrink: 0, paddingTop: 2 }, children: _jsx(IconAlertTriangle, { size: 14, "aria-hidden": true }) }), _jsx(Text, { size: "xs", c: "orange", children: _jsx(UnavailableTypeMessage, { schemaRefs: schemaRefs }) })] }));
|
|
@@ -38,31 +40,37 @@ const expandedStateFor = (nodes, acc = {}) => {
|
|
|
38
40
|
return acc;
|
|
39
41
|
};
|
|
40
42
|
/**
|
|
41
|
-
* The nodes of `nodes` that match `query
|
|
42
|
-
* label matches keeps its whole subtree,
|
|
43
|
-
*
|
|
43
|
+
* The nodes of `nodes` that match `query` and, given `changed`, are in it. An entry matches on
|
|
44
|
+
* label, title or slug; a collection whose label matches keeps its whole subtree, or under `changed` its
|
|
45
|
+
* changed entries, and any other collection survives only with its matching descendants. Nodes keep
|
|
46
|
+
* the position data (`childIndex`, `totalChildrenCount`) of the full tree.
|
|
44
47
|
*/
|
|
45
|
-
const filterTree = (nodes, query) => {
|
|
48
|
+
const filterTree = (nodes, query, changed) => {
|
|
46
49
|
const kept = [];
|
|
47
50
|
for (const node of nodes) {
|
|
48
51
|
const label = String(node.label ?? '');
|
|
49
52
|
if (node.nodeProps?.isEntry) {
|
|
50
53
|
const { slug, title } = node.nodeProps;
|
|
51
|
-
if (matchesEntryQuery({ label, title, slug }, query)
|
|
54
|
+
if (matchesEntryQuery({ label, title, slug }, query) &&
|
|
55
|
+
(!changed || changed.has(node.value))) {
|
|
52
56
|
kept.push(node);
|
|
53
57
|
}
|
|
58
|
+
continue;
|
|
54
59
|
}
|
|
55
|
-
|
|
60
|
+
// An empty query matches every label, so under Changed a collection is never kept whole.
|
|
61
|
+
const labelMatches = matchesEntryQuery({ label }, query);
|
|
62
|
+
if (labelMatches && !changed) {
|
|
56
63
|
kept.push(node);
|
|
64
|
+
continue;
|
|
57
65
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
kept.push({ ...node, children });
|
|
62
|
-
}
|
|
66
|
+
const children = filterTree(node.children ?? [], labelMatches ? '' : query, changed);
|
|
67
|
+
if (children.length > 0)
|
|
68
|
+
kept.push({ ...node, children });
|
|
63
69
|
}
|
|
64
70
|
return kept;
|
|
65
71
|
};
|
|
72
|
+
/** Whether `value` is a node of `nodes`, at any depth. */
|
|
73
|
+
const hasNode = (nodes, value) => nodes.some((node) => node.value === value || hasNode(node.children ?? [], value));
|
|
66
74
|
/** The first entry in tree order that matches `query`, else the first entry of any kind. */
|
|
67
75
|
const firstEntryPath = (nodes, query) => {
|
|
68
76
|
const entries = [];
|
|
@@ -82,10 +90,11 @@ const firstEntryPath = (nodes, query) => {
|
|
|
82
90
|
}, query));
|
|
83
91
|
return (match ?? entries[0])?.value;
|
|
84
92
|
};
|
|
93
|
+
const TOOLTIP_DELAY = 400;
|
|
85
94
|
/**
|
|
86
|
-
* A line of a row, truncated with an ellipsis. Its full text shows in a tooltip, and only
|
|
87
|
-
* the text is cut: the width is measured when the pointer or focus enters the row, not on
|
|
88
|
-
* render.
|
|
95
|
+
* A line of a row, truncated with an ellipsis. Its full text shows in a tooltip on hover, and only
|
|
96
|
+
* while the text is cut: the width is measured when the pointer or focus enters the row, not on
|
|
97
|
+
* every render.
|
|
89
98
|
*/
|
|
90
99
|
const TruncatedLabel = ({ label, highlight = '', fw, size = 'sm', c }) => {
|
|
91
100
|
const elementRef = useRef(null);
|
|
@@ -102,14 +111,88 @@ const TruncatedLabel = ({ label, highlight = '', fw, size = 'sm', c }) => {
|
|
|
102
111
|
const element = elementRef.current;
|
|
103
112
|
setTruncated(!!element && element.scrollWidth > element.clientWidth);
|
|
104
113
|
};
|
|
114
|
+
// The tree focuses the row's treeitem, which also holds the rows nested under it.
|
|
115
|
+
const item = row.closest('[role="treeitem"]') ?? row;
|
|
116
|
+
const focusIn = (event) => {
|
|
117
|
+
const target = event.target;
|
|
118
|
+
if (target === item || (target instanceof Node && row.contains(target)))
|
|
119
|
+
measure();
|
|
120
|
+
};
|
|
105
121
|
row.addEventListener('mouseenter', measure);
|
|
106
|
-
|
|
122
|
+
item.addEventListener('focusin', focusIn);
|
|
107
123
|
return () => {
|
|
108
124
|
row.removeEventListener('mouseenter', measure);
|
|
109
|
-
|
|
125
|
+
item.removeEventListener('focusin', focusIn);
|
|
110
126
|
};
|
|
111
127
|
}, []);
|
|
112
|
-
return (_jsx(Tooltip, { label: label, openDelay:
|
|
128
|
+
return (_jsx(Tooltip, { label: label, openDelay: TOOLTIP_DELAY, withinPortal: true, disabled: !truncated, children: highlight ? (_jsx(Highlight, { ref: setElement, highlight: highlight, size: size, fw: fw, truncate: "end", style: { minWidth: 0 }, children: label })) : (_jsx(Text, { ref: setElement, size: size, fw: fw, c: c, truncate: "end", style: { minWidth: 0 }, children: label })) }));
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* A row's cut-off detail line in full once keyboard focus has rested on the row for
|
|
132
|
+
* `TOOLTIP_DELAY`. Anchored to the whole row, below its focus ring or, without room, above it, so
|
|
133
|
+
* it never covers the row's title; Escape hides it until focus next arrives (WCAG 1.4.13).
|
|
134
|
+
*/
|
|
135
|
+
const RowDetailTip = ({ detail, children, }) => {
|
|
136
|
+
const rowRef = useRef(null);
|
|
137
|
+
const [shown, setShown] = useState(false);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
const row = rowRef.current;
|
|
140
|
+
const item = row?.closest('[role="treeitem"]');
|
|
141
|
+
if (!row || !item || !detail)
|
|
142
|
+
return;
|
|
143
|
+
let timer;
|
|
144
|
+
let open = false;
|
|
145
|
+
// While shown it also marks the row, so PreviewPane's Escape leaves the key to it.
|
|
146
|
+
const show = (next) => {
|
|
147
|
+
open = next;
|
|
148
|
+
setShown(next);
|
|
149
|
+
if (next)
|
|
150
|
+
item.setAttribute('data-mantine-stop-propagation', 'true');
|
|
151
|
+
else
|
|
152
|
+
item.removeAttribute('data-mantine-stop-propagation');
|
|
153
|
+
};
|
|
154
|
+
const cutOff = () => {
|
|
155
|
+
const line = row.querySelector('[data-testid="entry-nav-detail"] > *');
|
|
156
|
+
return !!line && line.scrollWidth > line.clientWidth;
|
|
157
|
+
};
|
|
158
|
+
const onFocusIn = (event) => {
|
|
159
|
+
if (event.target !== item)
|
|
160
|
+
return;
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
// Not after a click, which focuses the treeitem too and would pin it over the next row.
|
|
163
|
+
timer = setTimeout(() => show(item.matches(':focus-visible') && cutOff()), TOOLTIP_DELAY);
|
|
164
|
+
};
|
|
165
|
+
const onFocusOut = (event) => {
|
|
166
|
+
if (event.target !== item)
|
|
167
|
+
return;
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
show(false);
|
|
170
|
+
};
|
|
171
|
+
// Handled, so a panel's own Escape waits for the next press.
|
|
172
|
+
const onKeyDown = (event) => {
|
|
173
|
+
if (event.key !== 'Escape' || event.target !== item)
|
|
174
|
+
return;
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
if (!open)
|
|
177
|
+
return;
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
show(false);
|
|
180
|
+
};
|
|
181
|
+
item.addEventListener('focusin', onFocusIn);
|
|
182
|
+
item.addEventListener('focusout', onFocusOut);
|
|
183
|
+
item.addEventListener('keydown', onKeyDown);
|
|
184
|
+
return () => {
|
|
185
|
+
item.removeEventListener('focusin', onFocusIn);
|
|
186
|
+
item.removeEventListener('focusout', onFocusOut);
|
|
187
|
+
item.removeEventListener('keydown', onKeyDown);
|
|
188
|
+
clearTimeout(timer);
|
|
189
|
+
show(false);
|
|
190
|
+
};
|
|
191
|
+
}, [detail]);
|
|
192
|
+
// Rendered without a detail too, so the row keeps its place in the tree as one arrives.
|
|
193
|
+
return (_jsx(Tooltip, { ref: rowRef, label: detail ?? '', disabled: !detail, opened: shown, position: "bottom-start",
|
|
194
|
+
// Clears the row's 2px focus ring and its 2px offset.
|
|
195
|
+
offset: 8, withinPortal: true, children: children }));
|
|
113
196
|
};
|
|
114
197
|
/** The keys Mantine's tree acts on, matched as it matches them, by `code`. */
|
|
115
198
|
const TREE_KEYS = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Space']);
|
|
@@ -124,7 +207,7 @@ const stopTreeKeys = (event) => {
|
|
|
124
207
|
event.stopPropagation();
|
|
125
208
|
};
|
|
126
209
|
const UnsavedDot = ({ label }) => (_jsx(Tooltip, { label: label, withinPortal: true, children: _jsx(UnsavedMark, { role: "img", "aria-label": label, "data-testid": "unsaved-dot" }) }));
|
|
127
|
-
export const EntryNavigator = ({ items, collections, selectedPath, onSelect, onTreeControllerReady, expandedStateRef, onExpandedStateChange, onDeleteEntry: onDeleteEntryProp, onRenameEntry: onRenameEntryProp, onReorderEntry: onReorderEntryProp, hiddenRootPath, loading, readOnly = false, readOnlyReason, dirtyPaths,
|
|
210
|
+
export const EntryNavigator = ({ items, collections, selectedPath, onSelect, onTreeControllerReady, expandedStateRef, onExpandedStateChange, onDeleteEntry: onDeleteEntryProp, onRenameEntry: onRenameEntryProp, onReorderEntry: onReorderEntryProp, hiddenRootPath, loading, readOnly = false, readOnlyReason, dirtyPaths, reveal, onRevealed, onChangedOnlyChange, changedOnly: changedOnlyProp = false, unsavedCount: unsavedCountProp = 0, unsavedEntries, entryListFailed = false, branchNotAvailable = false, onSwitchBranch, onFilterEnter, onFilterEscape, onRowMenuOpen, filterRef, filterShortcut, }) => {
|
|
128
211
|
// Shadow the entry-mutation callbacks with undefined when readOnly, so every
|
|
129
212
|
// downstream reference (hasEntryMenu, canReorder, the menu items themselves)
|
|
130
213
|
// sees them as absent rather than threading a readOnly check through each
|
|
@@ -138,7 +221,9 @@ export const EntryNavigator = ({ items, collections, selectedPath, onSelect, onT
|
|
|
138
221
|
const localExpandedStateRef = useRef(expandedStateRef?.current ?? {});
|
|
139
222
|
const [query, setQuery] = useState('');
|
|
140
223
|
const filterInputRef = useRef(null);
|
|
141
|
-
const
|
|
224
|
+
const mergedFilterRef = useMergedRef(filterInputRef, filterRef);
|
|
225
|
+
const changedOnly = changedOnlyProp && !!onChangedOnlyChange;
|
|
226
|
+
const filtering = query.trim() !== '' || changedOnly;
|
|
142
227
|
// Read from the tree's expand/collapse callbacks, which fire from state updaters.
|
|
143
228
|
const filteringRef = useRef(false);
|
|
144
229
|
useEffect(() => {
|
|
@@ -150,6 +235,7 @@ export const EntryNavigator = ({ items, collections, selectedPath, onSelect, onT
|
|
|
150
235
|
label: entry.label,
|
|
151
236
|
nodeProps: {
|
|
152
237
|
status: entry.status,
|
|
238
|
+
route: entry.route,
|
|
153
239
|
title: entry.title,
|
|
154
240
|
missing: entry.missing,
|
|
155
241
|
noPage: entry.noPage,
|
|
@@ -174,6 +260,7 @@ export const EntryNavigator = ({ items, collections, selectedPath, onSelect, onT
|
|
|
174
260
|
label: entry?.label ?? col.label,
|
|
175
261
|
nodeProps: {
|
|
176
262
|
status: entry?.status,
|
|
263
|
+
route: entry?.route,
|
|
177
264
|
title: entry?.title,
|
|
178
265
|
missing: entry?.missing,
|
|
179
266
|
noPage: entry?.noPage,
|
|
@@ -284,6 +371,7 @@ export const EntryNavigator = ({ items, collections, selectedPath, onSelect, onT
|
|
|
284
371
|
label: item.label,
|
|
285
372
|
nodeProps: {
|
|
286
373
|
status: item.status,
|
|
374
|
+
route: item.route,
|
|
287
375
|
title: item.title,
|
|
288
376
|
missing: item.missing,
|
|
289
377
|
noPage: item.noPage,
|
|
@@ -296,19 +384,28 @@ export const EntryNavigator = ({ items, collections, selectedPath, onSelect, onT
|
|
|
296
384
|
}, [collections, items, hiddenRootPath]);
|
|
297
385
|
// Filtered from the finished tree, so each node keeps its position in the whole collection and
|
|
298
386
|
// Move up / Move down stay correct while a filter is on.
|
|
299
|
-
const filteredTree = useMemo(() =>
|
|
387
|
+
const filteredTree = useMemo(() => filtering
|
|
388
|
+
? filterTree(treeData, query, changedOnly ? (dirtyPaths ?? new Set()) : undefined)
|
|
389
|
+
: treeData, [filtering, treeData, query, changedOnly, dirtyPaths]);
|
|
390
|
+
// A caller may pass a new but equal `dirtyPaths` as drafts change: that alone never reopens a
|
|
391
|
+
// folder the user closed, while a new query or new folders re-expand.
|
|
392
|
+
const filteredFolders = useMemo(() => Object.keys(expandedStateFor(filteredTree)).join('\n'), [filteredTree]);
|
|
300
393
|
// Collections that hold an entry with unsaved changes, found over the whole tree so a filter
|
|
301
394
|
// never hides the reason for a dot.
|
|
302
|
-
|
|
395
|
+
// `listedUnsaved` counts the unsaved entries the tree has rows for.
|
|
396
|
+
const { dirtyCollections, listedUnsaved } = useMemo(() => {
|
|
303
397
|
const dirty = new Set();
|
|
398
|
+
let listed = 0;
|
|
304
399
|
if (!dirtyPaths?.size)
|
|
305
|
-
return dirty;
|
|
400
|
+
return { dirtyCollections: dirty, listedUnsaved: listed };
|
|
306
401
|
const walk = (nodes) => {
|
|
307
402
|
let any = false;
|
|
308
403
|
for (const node of nodes) {
|
|
309
404
|
if (node.nodeProps?.isEntry) {
|
|
310
|
-
if (dirtyPaths.has(node.value))
|
|
405
|
+
if (dirtyPaths.has(node.value)) {
|
|
311
406
|
any = true;
|
|
407
|
+
listed += 1;
|
|
408
|
+
}
|
|
312
409
|
}
|
|
313
410
|
else if (walk(node.children ?? [])) {
|
|
314
411
|
dirty.add(node.value);
|
|
@@ -318,8 +415,21 @@ export const EntryNavigator = ({ items, collections, selectedPath, onSelect, onT
|
|
|
318
415
|
return any;
|
|
319
416
|
};
|
|
320
417
|
walk(treeData);
|
|
321
|
-
return dirty;
|
|
418
|
+
return { dirtyCollections: dirty, listedUnsaved: listed };
|
|
322
419
|
}, [treeData, dirtyPaths]);
|
|
420
|
+
// Unsaved entries the tree has no row for, which Changed offers beneath it.
|
|
421
|
+
const unlistedNamed = useMemo(() => {
|
|
422
|
+
if (!changedOnly || !unsavedEntries?.length)
|
|
423
|
+
return [];
|
|
424
|
+
const rows = new Set();
|
|
425
|
+
const walk = (nodes) => nodes.forEach((node) => {
|
|
426
|
+
rows.add(node.value);
|
|
427
|
+
walk(node.children ?? []);
|
|
428
|
+
});
|
|
429
|
+
walk(treeData);
|
|
430
|
+
return unsavedEntries.filter((entry) => !rows.has(entry.path));
|
|
431
|
+
}, [changedOnly, unsavedEntries, treeData]);
|
|
432
|
+
const unlistedMatches = useMemo(() => unlistedNamed.filter((entry) => matchesEntryQuery({ label: entry.label }, query)), [unlistedNamed, query]);
|
|
323
433
|
// The hidden root's own row never renders, so its message is shown once above the tree.
|
|
324
434
|
const hiddenRootAddRefused = !readOnly &&
|
|
325
435
|
!!hiddenRootPath &&
|
|
@@ -365,6 +475,8 @@ export const EntryNavigator = ({ items, collections, selectedPath, onSelect, onT
|
|
|
365
475
|
// The expanded state from before the filter started, put back exactly when it clears. The tree
|
|
366
476
|
// drops the state of nodes it no longer renders, so it cannot be recovered from the tree itself.
|
|
367
477
|
const expandedBeforeFilterRef = useRef(null);
|
|
478
|
+
const servedRevealRef = useRef();
|
|
479
|
+
const revealAwaitsRestoreRef = useRef(false);
|
|
368
480
|
useEffect(() => {
|
|
369
481
|
if (filtering) {
|
|
370
482
|
expandedBeforeFilterRef.current ??= tree.expandedState;
|
|
@@ -372,14 +484,21 @@ export const EntryNavigator = ({ items, collections, selectedPath, onSelect, onT
|
|
|
372
484
|
}
|
|
373
485
|
else if (expandedBeforeFilterRef.current) {
|
|
374
486
|
// Plus the ancestors of an entry picked while filtering, so it stays in view.
|
|
375
|
-
|
|
487
|
+
const next = {
|
|
376
488
|
...expandedBeforeFilterRef.current,
|
|
377
489
|
...calculatePathToEntry(selectedPath, treeData),
|
|
378
|
-
}
|
|
490
|
+
};
|
|
491
|
+
tree.setExpandedState(next);
|
|
379
492
|
expandedBeforeFilterRef.current = null;
|
|
493
|
+
// A reveal that cleared the filter added its row's folders, kept as its own expansion is.
|
|
494
|
+
if (reveal && servedRevealRef.current !== reveal.seq) {
|
|
495
|
+
revealAwaitsRestoreRef.current = true;
|
|
496
|
+
localExpandedStateRef.current = next;
|
|
497
|
+
onExpandedStateChange?.(next);
|
|
498
|
+
}
|
|
380
499
|
}
|
|
381
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- runs when the filter or
|
|
382
|
-
}, [filtering,
|
|
500
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- runs when the filter, its query or its folders change; `tree` is a fresh object each render
|
|
501
|
+
}, [filtering, filteredFolders, query.trim()]);
|
|
383
502
|
useEffect(() => {
|
|
384
503
|
if (!selectedPath || !treeData || filtering)
|
|
385
504
|
return;
|
|
@@ -415,25 +534,53 @@ export const EntryNavigator = ({ items, collections, selectedPath, onSelect, onT
|
|
|
415
534
|
hasScrolledRef.current = false;
|
|
416
535
|
}, []);
|
|
417
536
|
const treeBoxRef = useRef(null);
|
|
418
|
-
const servedRevealRef = useRef();
|
|
419
537
|
// Runs again as the filter clears and the expansion lands, then focuses the row a frame later.
|
|
420
538
|
useEffect(() => {
|
|
421
|
-
if (!
|
|
539
|
+
if (!reveal || !treeData || servedRevealRef.current === reveal.seq) {
|
|
540
|
+
revealAwaitsRestoreRef.current = false;
|
|
422
541
|
return;
|
|
542
|
+
}
|
|
423
543
|
const served = () => {
|
|
424
|
-
servedRevealRef.current =
|
|
544
|
+
servedRevealRef.current = reveal.seq;
|
|
425
545
|
onRevealed?.();
|
|
426
546
|
};
|
|
427
|
-
if (
|
|
547
|
+
if (reveal.kind === 'changed') {
|
|
428
548
|
setQuery('');
|
|
549
|
+
served();
|
|
429
550
|
return;
|
|
430
551
|
}
|
|
431
|
-
|
|
552
|
+
// An empty tree that is still loading gets its rows; this runs again when they arrive.
|
|
553
|
+
if (treeData.length === 0 && loading)
|
|
554
|
+
return;
|
|
555
|
+
// This commit's expanded state predates the restore's, which re-runs this effect.
|
|
556
|
+
if (revealAwaitsRestoreRef.current) {
|
|
557
|
+
revealAwaitsRestoreRef.current = false;
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const value = reveal.kind === 'collection'
|
|
561
|
+
? collectionNodeValue(treeData, reveal.path)
|
|
562
|
+
: hasNode(treeData, reveal.path)
|
|
563
|
+
? reveal.path
|
|
564
|
+
: undefined;
|
|
432
565
|
if (!value) {
|
|
433
566
|
served();
|
|
434
567
|
return;
|
|
435
568
|
}
|
|
436
|
-
const wanted = {
|
|
569
|
+
const wanted = {
|
|
570
|
+
...calculatePathToEntry(value, treeData),
|
|
571
|
+
...(reveal.kind === 'collection' ? { [value]: true } : {}),
|
|
572
|
+
};
|
|
573
|
+
if (filtering) {
|
|
574
|
+
// The filter's restore expands the row's folders along with what was open before it.
|
|
575
|
+
expandedBeforeFilterRef.current = {
|
|
576
|
+
...(expandedBeforeFilterRef.current ?? tree.expandedState),
|
|
577
|
+
...wanted,
|
|
578
|
+
};
|
|
579
|
+
setQuery('');
|
|
580
|
+
if (changedOnly)
|
|
581
|
+
onChangedOnlyChange?.(false);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
437
584
|
if (Object.keys(wanted).some((key) => !tree.expandedState[key])) {
|
|
438
585
|
const next = { ...tree.expandedState, ...wanted };
|
|
439
586
|
tree.setExpandedState(next);
|
|
@@ -449,7 +596,7 @@ export const EntryNavigator = ({ items, collections, selectedPath, onSelect, onT
|
|
|
449
596
|
});
|
|
450
597
|
return () => cancelAnimationFrame(frame);
|
|
451
598
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- `tree` and the callbacks are fresh each render
|
|
452
|
-
}, [
|
|
599
|
+
}, [reveal, treeData, filtering, loading, tree.expandedState]);
|
|
453
600
|
const clearFilter = () => {
|
|
454
601
|
setQuery('');
|
|
455
602
|
filterInputRef.current?.focus();
|
|
@@ -458,17 +605,20 @@ export const EntryNavigator = ({ items, collections, selectedPath, onSelect, onT
|
|
|
458
605
|
// Enter and Escape during IME composition commit or cancel the composition, nothing more.
|
|
459
606
|
if (event.nativeEvent.isComposing)
|
|
460
607
|
return;
|
|
461
|
-
if (event.key === 'Escape' && query) {
|
|
462
|
-
//
|
|
608
|
+
if (event.key === 'Escape' && (query || onFilterEscape)) {
|
|
609
|
+
// A query clears and nothing else: the surrounding drawer stays open.
|
|
463
610
|
event.preventDefault();
|
|
464
611
|
event.stopPropagation();
|
|
465
|
-
|
|
612
|
+
if (query)
|
|
613
|
+
setQuery('');
|
|
614
|
+
else
|
|
615
|
+
onFilterEscape?.();
|
|
466
616
|
}
|
|
467
617
|
else if (event.key === 'Enter' && filtering) {
|
|
468
|
-
const path = firstEntryPath(filteredTree, query);
|
|
618
|
+
const path = firstEntryPath(filteredTree, query) ?? unlistedMatches[0]?.path;
|
|
469
619
|
if (path) {
|
|
470
620
|
event.preventDefault();
|
|
471
|
-
onSelect(path);
|
|
621
|
+
(onFilterEnter ?? onSelect)(path);
|
|
472
622
|
}
|
|
473
623
|
}
|
|
474
624
|
};
|
|
@@ -477,6 +627,10 @@ export const EntryNavigator = ({ items, collections, selectedPath, onSelect, onT
|
|
|
477
627
|
const status = node.nodeProps?.status;
|
|
478
628
|
const entryMissing = !!node.nodeProps?.missing;
|
|
479
629
|
const entryNoPage = !!node.nodeProps?.noPage;
|
|
630
|
+
const route = node.nodeProps?.route;
|
|
631
|
+
const detail = entryMissing
|
|
632
|
+
? status
|
|
633
|
+
: entryDetailLine({ typeLabel: status, route: entryNoPage ? undefined : route });
|
|
480
634
|
// Collection-level mutation callbacks are nulled when readOnly, same as the entry-level ones
|
|
481
635
|
// above, so hasCollectionMenu is false; a row that would have had a menu shows its lock.
|
|
482
636
|
const rawOnAdd = node.nodeProps?.onAdd;
|
|
@@ -541,67 +695,85 @@ export const EntryNavigator = ({ items, collections, selectedPath, onSelect, onT
|
|
|
541
695
|
const showCollectionDirtyDot = !isEntry && dirtyCollections.has(node.value);
|
|
542
696
|
const adminItems = onAddSubCollection || onEdit || onDelete;
|
|
543
697
|
const hasCollectionContentItems = onAdd || canReorder;
|
|
544
|
-
return (_jsxs(Box, { ...elementProps, ref: selected ? selectedNodeRef : undefined, "data-nav-row": true, "data-nav-selected": selected ? '' : undefined, "data-testid": `entry-nav-item-${testIdSuffix}`, onClick: (event) => {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
698
|
+
return (_jsx(RowDetailTip, { detail: detail, children: _jsxs(Box, { ...elementProps, ref: selected ? selectedNodeRef : undefined, "data-nav-row": true, "data-nav-selected": selected ? '' : undefined, "data-testid": `entry-nav-item-${testIdSuffix}`, onClick: (event) => {
|
|
699
|
+
elementProps.onClick(event);
|
|
700
|
+
if (isLeaf && isEntry) {
|
|
701
|
+
onSelect(node.value);
|
|
702
|
+
}
|
|
703
|
+
}, style: {
|
|
704
|
+
...elementProps.style,
|
|
705
|
+
marginBottom: 4,
|
|
706
|
+
borderRadius: 10,
|
|
707
|
+
paddingInline: 10,
|
|
708
|
+
paddingBlock: 6,
|
|
709
|
+
paddingLeft: `calc(${rem(8)} + ${rem(level * 12)})`,
|
|
710
|
+
backgroundColor: selected ? 'var(--mantine-primary-color-light)' : undefined,
|
|
711
|
+
color: selected ? 'var(--mantine-primary-color-light-color)' : undefined,
|
|
712
|
+
border: selected
|
|
713
|
+
? '1px solid var(--mantine-primary-color-light-color)'
|
|
714
|
+
: '1px solid transparent',
|
|
715
|
+
transition: 'background-color 120ms ease, border-color 120ms ease',
|
|
716
|
+
}, children: [_jsxs(Group, { gap: "xs", justify: "space-between", wrap: "nowrap", style: { width: '100%' }, children: [_jsxs(Group, { gap: 6, wrap: "nowrap", style: { flex: 1, minWidth: 0 }, children: [_jsx(Box, { w: 18, h: 18, style: {
|
|
717
|
+
display: 'flex',
|
|
718
|
+
flexShrink: 0,
|
|
719
|
+
alignItems: 'center',
|
|
720
|
+
justifyContent: 'center',
|
|
721
|
+
}, children: _jsx(Chevron, { expanded: expanded, visible: showChevron }) }), _jsxs(Stack, { gap: 0, style: { flex: 1, minWidth: 0 }, children: [_jsxs(Group, { gap: 6, wrap: "nowrap", style: { minWidth: 0 }, children: [_jsx(TruncatedLabel, { label: label, highlight: filtering ? query.trim() : '', fw: selected ? 600 : 500 }), showEntryDirtyDot && _jsx(UnsavedDot, { label: "Unsaved changes" }), showCollectionDirtyDot && _jsx(UnsavedDot, { label: "Contains unsaved changes" }), conflictNotice && (_jsx(Tooltip, { label: "This content was updated on the base branch \u2014 a reviewer will reconcile", withArrow: true, children: _jsx(Badge, { size: "xs", variant: "light", color: "orange", "data-testid": "conflict-badge", style: { flexShrink: 0 }, children: "Conflict" }) })), entryLocked && (_jsx(Tooltip, { label: "You can't edit this entry", withinPortal: true, children: _jsx(Box, { component: "span", role: "img", "aria-label": "You can't edit this entry", "data-testid": "entry-lock", c: "dimmed", style: { display: 'inline-flex', flexShrink: 0 }, children: _jsx(IconLock, { size: 14, "aria-hidden": true }) }) }))] }), detail && (_jsx(Box, { "data-testid": "entry-nav-detail", style: { minWidth: 0 }, children: _jsx(TruncatedLabel, { label: detail, size: "xs",
|
|
722
|
+
// A missing file is an error, in error text, not a dimmed type.
|
|
723
|
+
c: entryMissing ? 'var(--canopy-danger-text)' : 'dimmed' }) }))] })] }), _jsxs(Group, { gap: 4, wrap: "nowrap", style: { flexShrink: 0 }, children: [hasCollectionMenu && (_jsxs(Menu, { shadow: "md", width: 200, withinPortal: true, position: "bottom-end", onOpen: onRowMenuOpen, children: [_jsx(Menu.Target, { children: _jsx(ActionIcon, { size: "xs", variant: "subtle", color: "gray", onClick: (event) => event.stopPropagation(), "aria-label": "Collection actions", "data-testid": `collection-menu-${testIdSuffix}`, children: _jsx(IconDots, { size: 14 }) }) }), _jsxs(Menu.Dropdown, { onKeyDown: stopTreeKeys, children: [onAdd && (_jsx(Menu.Item, { leftSection: _jsx(IconPlus, { size: 14 }), onClick: (event) => {
|
|
724
|
+
event.stopPropagation();
|
|
725
|
+
onAdd();
|
|
726
|
+
}, "data-testid": "add-entry-menu-item", children: "Add entry\u2026" })), canReorder && (_jsxs(_Fragment, { children: [_jsx(ReasonedMenuItem, { leftSection: _jsx(IconArrowUp, { size: 14 }), blockedReason: canMoveUp ? undefined : 'Already first', onClick: (event) => {
|
|
727
|
+
event.stopPropagation();
|
|
728
|
+
onReorderEntry?.(parentCollectionPath, contentId, 'up');
|
|
729
|
+
}, children: "Move up" }), _jsx(ReasonedMenuItem, { leftSection: _jsx(IconArrowDown, { size: 14 }), blockedReason: canMoveDown ? undefined : 'Already last', onClick: (event) => {
|
|
730
|
+
event.stopPropagation();
|
|
731
|
+
onReorderEntry?.(parentCollectionPath, contentId, 'down');
|
|
732
|
+
}, children: "Move down" })] })), adminItems && (_jsxs(_Fragment, { children: [hasCollectionContentItems && _jsx(Menu.Divider, {}), _jsx(Menu.Label, { children: "Admin" })] })), onAddSubCollection && (_jsx(Menu.Item, { leftSection: _jsx(IconFolderPlus, { size: 14 }), onClick: (event) => {
|
|
733
|
+
event.stopPropagation();
|
|
734
|
+
onAddSubCollection();
|
|
735
|
+
}, children: "Add sub-collection\u2026" })), onEdit && (_jsx(Menu.Item, { leftSection: _jsx(IconEdit, { size: 14 }), onClick: (event) => {
|
|
736
|
+
event.stopPropagation();
|
|
737
|
+
onEdit();
|
|
738
|
+
}, children: "Edit collection\u2026" })), onDelete && (_jsx(DestructiveMenuItem, { leftSection: _jsx(IconTrash, { size: 14 }), onClick: (event) => {
|
|
739
|
+
event.stopPropagation();
|
|
740
|
+
onDelete();
|
|
741
|
+
}, children: "Delete collection" }))] })] })), showsLockSlot &&
|
|
742
|
+
(readOnlyReason ? (_jsx(BranchRowLock, { label: isCollection ? 'Collection actions' : 'Entry actions', reason: readOnlyReason })) : (
|
|
743
|
+
// The branch's state is still loading: hold the slot so rows do not shift. It draws
|
|
744
|
+
// nothing, so it needs no reveal.
|
|
745
|
+
_jsx(Box, { w: 18, h: 18, "aria-hidden": true, "data-testid": "branch-lock-pending" }))), hasEntryMenu && (_jsxs(Menu, { shadow: "md", width: 180, withinPortal: true, position: "bottom-end", onOpen: onRowMenuOpen, children: [_jsx(Menu.Target, { children: _jsx(ActionIcon, { size: "xs", variant: "subtle", color: "gray", onClick: (event) => event.stopPropagation(), "aria-label": "Entry actions", "data-testid": `entry-menu-${testIdSuffix}`, children: _jsx(IconDots, { size: 14 }) }) }), _jsxs(Menu.Dropdown, { onKeyDown: stopTreeKeys, children: [onRenameEntry && (_jsxs(Menu.Item, { leftSection: _jsx(IconEdit, { size: 14 }), onClick: (event) => {
|
|
746
|
+
event.stopPropagation();
|
|
747
|
+
onRenameEntry(entryPath);
|
|
748
|
+
}, "data-testid": "rename-entry-menu-item", children: [entryNoPage ? EDITOR_ACTIONS.changeSlug : EDITOR_ACTIONS.changeUrl, "\u2026"] })), onReorderEntry && contentId && parentCollectionPath && (_jsxs(_Fragment, { children: [_jsx(ReasonedMenuItem, { leftSection: _jsx(IconArrowUp, { size: 14 }), blockedReason: canMoveUp ? undefined : 'Already first', onClick: (event) => {
|
|
749
|
+
event.stopPropagation();
|
|
750
|
+
onReorderEntry(parentCollectionPath, contentId, 'up');
|
|
751
|
+
}, children: "Move up" }), _jsx(ReasonedMenuItem, { leftSection: _jsx(IconArrowDown, { size: 14 }), blockedReason: canMoveDown ? undefined : 'Already last', onClick: (event) => {
|
|
752
|
+
event.stopPropagation();
|
|
753
|
+
onReorderEntry(parentCollectionPath, contentId, 'down');
|
|
754
|
+
}, children: "Move down" })] })), onDeleteEntry && (_jsxs(_Fragment, { children: [(onRenameEntry ||
|
|
755
|
+
(onReorderEntry && contentId && parentCollectionPath)) && (_jsx(Menu.Divider, {})), _jsx(DestructiveMenuItem, { leftSection: _jsx(IconTrash, { size: 14 }), onClick: (event) => {
|
|
756
|
+
event.stopPropagation();
|
|
757
|
+
onDeleteEntry(entryPath);
|
|
758
|
+
}, "data-testid": "delete-entry-menu-item", children: "Delete entry" })] }))] })] }))] })] }), isCollection && unavailableSchemaRefs && unavailableSchemaRefs.length > 0 && (_jsx(UnavailableTypeRow, { schemaRefs: unavailableSchemaRefs })), isCollection && addRefused && _jsx(AddRefusedRow, {})] }) }));
|
|
759
|
+
};
|
|
760
|
+
// Changed can't list every entry it counts: those with no row are offered beneath the tree, and
|
|
761
|
+
// drafts whose entries aren't named yet are counted there.
|
|
762
|
+
const unsavedCount = Math.max(unsavedCountProp, listedUnsaved + unlistedNamed.length);
|
|
763
|
+
const unnamedUnsaved = unsavedCount - listedUnsaved - unlistedNamed.length;
|
|
764
|
+
const showAll = () => {
|
|
765
|
+
onChangedOnlyChange?.(false);
|
|
766
|
+
filterInputRef.current?.focus();
|
|
604
767
|
};
|
|
605
|
-
|
|
768
|
+
const outsideRows = unlistedMatches.length > 0 && (_jsxs(Stack, { gap: 2, py: 4, align: "flex-start", "data-testid": "entry-nav-changed-outside", children: [_jsx(Text, { size: "xs", c: "dimmed", children: "Not in the content tree" }), unlistedMatches.map((entry) => (_jsx(Button, { size: "compact-sm", variant: "subtle", color: "gray", leftSection: _jsx(UnsavedMark, { "aria-hidden": "true" }), onClick: () => onSelect(entry.path), children: entry.label }, entry.path)))] }));
|
|
769
|
+
const changedNotice = !changedOnly ? null : entryListFailed ? (_jsx(Text, { size: "sm", c: "dimmed", py: "sm", "data-testid": "entry-nav-changed-failed", children: "The list of entries couldn't be loaded, so unsaved entries can't be shown. Choose \"Refresh branch\" in the branch menu to try again." })) : query.trim() ? (_jsxs(_Fragment, { children: [filteredTree.length === 0 && !outsideRows && (_jsxs(Stack, { gap: 4, py: "sm", align: "flex-start", "data-testid": "entry-nav-changed-no-match", children: [_jsxs(Text, { size: "sm", c: "dimmed", children: ["No unsaved entries match \u201C", query, "\u201D."] }), _jsx(Button, { size: "compact-sm", variant: "subtle", onClick: showAll, children: "Search all entries" })] })), outsideRows] })) : unsavedCount === 0 ? (treeData.length > 0 && (_jsxs(Stack, { gap: 4, py: "sm", align: "flex-start", "data-testid": "entry-nav-changed-none", children: [_jsx(Text, { size: "sm", c: "dimmed", children: "No unsaved changes on this branch." }), _jsx(Button, { size: "compact-sm", variant: "subtle", onClick: showAll, children: "Show all entries" })] }))) : (_jsxs(_Fragment, { children: [outsideRows, unnamedUnsaved > 0 && (_jsxs(Text, { size: "sm", c: "dimmed", py: 4, "data-testid": "entry-nav-changed-unlisted", children: [listedUnsaved + unlistedNamed.length > 0
|
|
770
|
+
? `${unnamedUnsaved} more: `
|
|
771
|
+
: `${unnamedUnsaved} unsaved ${unnamedUnsaved === 1 ? 'entry' : 'entries'}: `, unnamedUnsaved === 1 ? 'loading its name…' : 'loading their names…'] }))] }));
|
|
772
|
+
return (_jsxs(Stack, { h: "100%", style: { display: 'flex', flexDirection: 'column' }, gap: 0, "data-testid": "entry-navigator", children: [treeData.length > 0 && (_jsx(TextInput, { ref: mergedFilterRef, size: "sm", py: 4, "aria-label": "Find entries", "aria-keyshortcuts": filterShortcut?.keys, spellCheck: false, "data-autofocus": true, placeholder: "Find entries", value: query, onChange: (event) => setQuery(event.currentTarget.value), onKeyDown: handleFilterKeyDown, leftSection: _jsx(IconSearch, { size: 14, "aria-hidden": true }), rightSection: query ? (_jsx(Tooltip, { label: "Clear search", withinPortal: true, children: _jsx(CloseButton, { size: "sm", "aria-label": "Clear search", onClick: clearFilter }) })) : filterShortcut ? (
|
|
773
|
+
// The placeholder's colour, so it never reads brighter than the placeholder beside it.
|
|
774
|
+
_jsx(Text, { size: "xs", c: "var(--mantine-color-placeholder)", "aria-hidden": true, "data-testid": "entry-nav-filter-shortcut", children: filterShortcut.label })) : null, rightSectionWidth: query || !filterShortcut ? undefined : 'auto', rightSectionPointerEvents: query ? undefined : 'none', rightSectionProps: query || !filterShortcut ? undefined : { style: { paddingInline: 8 } }, "data-mantine-stop-propagation": query || onFilterEscape ? 'true' : undefined, "data-testid": "entry-nav-filter" })), onChangedOnlyChange && (treeData.length > 0 || unsavedCount > 0) && (_jsx(SegmentedControl, { size: "xs", fullWidth: true, mb: 4, "aria-label": "Entries shown", value: changedOnly ? 'changed' : 'all', onChange: (value) => onChangedOnlyChange(value === 'changed'), data: [
|
|
775
|
+
{ value: 'all', label: 'All' },
|
|
776
|
+
{ value: 'changed', label: unsavedCount > 0 ? `Changed ${unsavedCount}` : 'Changed' },
|
|
777
|
+
], "data-testid": "entry-nav-changed-filter" })), _jsxs(ScrollArea, { type: "auto", scrollbars: "y", offsetScrollbars: true, style: { flex: 1 }, children: [hiddenRootUnavailableRefs && hiddenRootUnavailableRefs.length > 0 && (_jsx(Box, { pt: "sm", children: _jsx(UnavailableTypeRow, { schemaRefs: hiddenRootUnavailableRefs }) })), hiddenRootAddRefused && (_jsx(Box, { pt: "sm", children: _jsx(AddRefusedRow, {}) })), treeData.length === 0 ? (branchNotAvailable ? (_jsx(BranchNotAvailableNotice, { onSwitchBranch: onSwitchBranch, "data-testid": "entry-nav-branch-not-available" })) : loading ? (_jsxs(Group, { gap: "xs", py: "sm", children: [_jsx(Loader, { size: "xs" }), _jsx(Text, { size: "xs", c: "dimmed", children: "Loading content\u2026" })] })) : changedNotice ? (changedNotice) : (_jsx(Text, { size: "xs", c: "dimmed", py: "sm", children: "No content" }))) : filteredTree.length === 0 && changedOnly ? (changedNotice) : filteredTree.length === 0 ? (_jsxs(Stack, { gap: 4, py: "sm", align: "flex-start", "data-testid": "entry-nav-filter-empty", children: [_jsxs(Text, { size: "sm", c: "dimmed", children: ["No entries match \u201C", query, "\u201D."] }), _jsx(Button, { size: "compact-sm", variant: "subtle", onClick: clearFilter, children: "Clear search" })] })) : (_jsxs(Box, { py: "sm", ref: treeBoxRef, children: [_jsx(Tree, { data: filteredTree, tree: tree, renderNode: renderNode, selectOnClick: false, levelOffset: "sm" }), changedNotice] }))] })] }));
|
|
606
778
|
};
|
|
607
779
|
export default EntryNavigator;
|