@djangocfg/ui-tools 2.1.413 → 2.1.416
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/dist/file-icon/index.d.cts +1 -1
- package/dist/file-icon/index.d.ts +1 -1
- package/dist/slots-ClRpIzoh.d.cts +88 -0
- package/dist/slots-ClRpIzoh.d.ts +88 -0
- package/dist/tree/index.cjs +1994 -276
- package/dist/tree/index.cjs.map +1 -1
- package/dist/tree/index.d.cts +717 -72
- package/dist/tree/index.d.ts +717 -72
- package/dist/tree/index.mjs +1984 -279
- package/dist/tree/index.mjs.map +1 -1
- package/package.json +10 -6
- package/src/tools/chat/README.md +111 -1
- package/src/tools/chat/composer/Composer.tsx +138 -17
- package/src/tools/chat/composer/ComposerRichTextarea.tsx +25 -0
- package/src/tools/chat/composer/index.ts +22 -0
- package/src/tools/chat/composer/slash/README.md +187 -0
- package/src/tools/chat/composer/slash/SlashHighlightTextarea.tsx +144 -0
- package/src/tools/chat/composer/slash/SlashMenu.tsx +142 -0
- package/src/tools/chat/composer/slash/SlashToken.tsx +57 -0
- package/src/tools/chat/composer/slash/index.ts +44 -0
- package/src/tools/chat/composer/slash/labels.ts +19 -0
- package/src/tools/chat/composer/slash/state.ts +168 -0
- package/src/tools/chat/composer/slash/types.ts +64 -0
- package/src/tools/chat/composer/slash/useSlashCommands.ts +204 -0
- package/src/tools/chat/composer/types.ts +8 -0
- package/src/tools/chat/shell/SuggestedPrompts.tsx +194 -0
- package/src/tools/chat/shell/index.ts +6 -0
- package/src/tools/data/Listbox/lazy.tsx +1 -1
- package/src/tools/data/Masonry/lazy.tsx +1 -1
- package/src/tools/data/Timeline/lazy.tsx +1 -1
- package/src/tools/data/Tree/FinderTree.tsx +42 -0
- package/src/tools/data/Tree/README.md +337 -208
- package/src/tools/data/Tree/TreeDndProvider.tsx +137 -0
- package/src/tools/data/Tree/TreeRoot.tsx +170 -55
- package/src/tools/data/Tree/__tests__/dnd.test.ts +160 -0
- package/src/tools/data/Tree/__tests__/keyboard.test.ts +137 -0
- package/src/tools/data/Tree/__tests__/renameUtils.test.ts +52 -0
- package/src/tools/data/Tree/__tests__/selection.test.ts +227 -0
- package/src/tools/data/Tree/components/TreeDropIndicator.tsx +65 -0
- package/src/tools/data/Tree/components/TreeEmptyArea.tsx +160 -0
- package/src/tools/data/Tree/components/TreeRenameInput.tsx +114 -0
- package/src/tools/data/Tree/components/TreeRow.tsx +92 -8
- package/src/tools/data/Tree/components/index.ts +6 -0
- package/src/tools/data/Tree/context/TreeContext.tsx +204 -363
- package/src/tools/data/Tree/context/TreeContextValue.ts +139 -0
- package/src/tools/data/Tree/context/async-children/collect-ids.ts +27 -0
- package/src/tools/data/Tree/context/async-children/index.ts +8 -0
- package/src/tools/data/Tree/context/async-children/use-async-children.ts +157 -0
- package/src/tools/data/Tree/context/clipboard/index.ts +4 -0
- package/src/tools/data/Tree/context/clipboard/use-clipboard.ts +115 -0
- package/src/tools/data/Tree/context/dnd/index.ts +8 -0
- package/src/tools/data/Tree/context/dnd/use-dnd.ts +194 -0
- package/src/tools/data/Tree/context/expansion/index.ts +4 -0
- package/src/tools/data/Tree/context/expansion/use-expansion.ts +55 -0
- package/src/tools/data/Tree/context/hooks.ts +68 -1
- package/src/tools/data/Tree/context/index.ts +3 -0
- package/src/tools/data/Tree/context/menu/builtin-actions.ts +357 -0
- package/src/tools/data/Tree/context/menu/index.ts +10 -0
- package/src/tools/data/Tree/context/menu/use-resolved-menu.ts +127 -0
- package/src/tools/data/Tree/context/persist/index.ts +4 -0
- package/src/tools/data/Tree/context/persist/use-persist-sync.ts +74 -0
- package/src/tools/data/Tree/context/rename/index.ts +4 -0
- package/src/tools/data/Tree/context/rename/use-rename.ts +113 -0
- package/src/tools/data/Tree/context/selection/index.ts +4 -0
- package/src/tools/data/Tree/context/selection/use-selection.ts +146 -0
- package/src/tools/data/Tree/context/state/index.ts +6 -0
- package/src/tools/data/Tree/context/state/initial.ts +41 -0
- package/src/tools/data/Tree/context/state/reducer.ts +76 -0
- package/src/tools/data/Tree/context/state/types.ts +46 -0
- package/src/tools/data/Tree/data/clipboard.ts +33 -0
- package/src/tools/data/Tree/data/dnd.ts +123 -0
- package/src/tools/data/Tree/data/finderShortcuts.ts +67 -0
- package/src/tools/data/Tree/data/index.ts +19 -0
- package/src/tools/data/Tree/data/renameUtils.ts +51 -0
- package/src/tools/data/Tree/data/selection.ts +157 -0
- package/src/tools/data/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
- package/src/tools/data/Tree/hooks/finder-hotkeys/index.ts +8 -0
- package/src/tools/data/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
- package/src/tools/data/Tree/hooks/index.ts +23 -4
- package/src/tools/data/Tree/hooks/keyboard/activation.ts +27 -0
- package/src/tools/data/Tree/hooks/keyboard/arrow-nav.ts +26 -0
- package/src/tools/data/Tree/hooks/keyboard/expand-collapse.ts +54 -0
- package/src/tools/data/Tree/hooks/keyboard/index.ts +10 -0
- package/src/tools/data/Tree/hooks/keyboard/types.ts +39 -0
- package/src/tools/data/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
- package/src/tools/data/Tree/hooks/type-ahead/index.ts +5 -0
- package/src/tools/data/Tree/hooks/type-ahead/match-prefix.ts +42 -0
- package/src/tools/data/Tree/hooks/{useTreeTypeAhead.ts → type-ahead/use-tree-type-ahead.ts} +8 -19
- package/src/tools/data/Tree/index.tsx +25 -2
- package/src/tools/data/Tree/types/activation.ts +30 -0
- package/src/tools/data/Tree/types/adapter.ts +70 -0
- package/src/tools/data/Tree/types/index.ts +27 -0
- package/src/tools/data/Tree/types/labels.ts +97 -0
- package/src/tools/data/Tree/types/loader.ts +9 -0
- package/src/tools/data/Tree/types/node.ts +38 -0
- package/src/tools/data/Tree/types/root-props.ts +142 -0
- package/src/tools/data/Tree/types/selection.ts +3 -0
- package/src/tools/data/Tree/types/slots.ts +64 -0
- package/src/tools/dev/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseBody.tsx +1 -1
- package/src/tools/dev/OpenapiViewer/components/shared/ResponsePanel/PrettyView.tsx +1 -1
- package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +85 -0
- package/src/tools/forms/MarkdownEditor/index.ts +1 -0
- package/src/tools/forms/MarkdownEditor/lazy.tsx +6 -0
- package/src/tools/forms/MarkdownEditor/slash/SlashCommandNode.ts +162 -0
- package/src/tools/forms/MarkdownEditor/slash/index.ts +4 -0
- package/src/tools/forms/MarkdownEditor/slash/syncSlashNode.ts +97 -0
- package/src/tools/forms/MarkdownEditor/slash/types.ts +13 -0
- package/src/tools/forms/MarkdownEditor/styles.css +18 -0
- package/src/tools/index.ts +2 -2
- package/dist/types-j2vhn4Kv.d.cts +0 -241
- package/dist/types-j2vhn4Kv.d.ts +0 -241
- package/src/tools/data/Tree/hooks/useTreeKeyboard.ts +0 -171
- package/src/tools/data/Tree/types.ts +0 -217
|
@@ -1,164 +1,43 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import * as React from 'react';
|
|
4
|
-
import {
|
|
5
|
-
createContext,
|
|
6
|
-
useCallback,
|
|
7
|
-
useEffect,
|
|
8
|
-
useMemo,
|
|
9
|
-
useReducer,
|
|
10
|
-
useRef,
|
|
11
|
-
} from 'react';
|
|
4
|
+
import { createContext, useCallback, useMemo, useReducer, useRef } from 'react';
|
|
12
5
|
|
|
6
|
+
import { flattenTree } from '../data/flatten';
|
|
7
|
+
import { loadTreeState } from '../data/persist';
|
|
8
|
+
import { resolveAppearance } from '../data/appearance';
|
|
13
9
|
import {
|
|
14
10
|
DEFAULT_TREE_LABELS,
|
|
15
11
|
type FlatRow,
|
|
16
|
-
type
|
|
12
|
+
type TreeActivateOptions,
|
|
17
13
|
type TreeItemId,
|
|
18
14
|
type TreeLabels,
|
|
19
|
-
type TreeLoadChildren,
|
|
20
15
|
type TreeNode,
|
|
21
|
-
type TreeActivateOptions,
|
|
22
|
-
type TreeActivationMode,
|
|
23
16
|
type TreeRootProps,
|
|
24
|
-
type TreeRowSlot,
|
|
25
|
-
type TreeSelectionMode,
|
|
26
17
|
} from '../types';
|
|
27
|
-
import {
|
|
28
|
-
createChildCache,
|
|
29
|
-
type ChildCache,
|
|
30
|
-
type ChildEntry,
|
|
31
|
-
} from '../data/childCache';
|
|
32
|
-
import { flattenTree } from '../data/flatten';
|
|
33
|
-
import { loadTreeState, saveTreeState } from '../data/persist';
|
|
34
|
-
import {
|
|
35
|
-
resolveAppearance,
|
|
36
|
-
type ResolvedAppearance,
|
|
37
|
-
type TreeAppearance,
|
|
38
|
-
} from '../data/appearance';
|
|
39
|
-
|
|
40
|
-
// =====================================================================
|
|
41
|
-
// Reducer
|
|
42
|
-
// =====================================================================
|
|
43
|
-
|
|
44
|
-
interface State<T> {
|
|
45
|
-
expanded: Set<TreeItemId>;
|
|
46
|
-
selected: Set<TreeItemId>;
|
|
47
|
-
focused: TreeItemId | null;
|
|
48
|
-
query: string;
|
|
49
|
-
/** Bumped on every cache mutation so memos see a fresh dep. */
|
|
50
|
-
cacheTick: number;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
type Action<T> =
|
|
54
|
-
| { type: 'expand'; id: TreeItemId }
|
|
55
|
-
| { type: 'collapse'; id: TreeItemId }
|
|
56
|
-
| { type: 'toggle'; id: TreeItemId }
|
|
57
|
-
| { type: 'set-expanded'; ids: TreeItemId[] }
|
|
58
|
-
| { type: 'select'; id: TreeItemId; mode: TreeSelectionMode }
|
|
59
|
-
| { type: 'select-many'; ids: TreeItemId[] }
|
|
60
|
-
| { type: 'clear-selection' }
|
|
61
|
-
| { type: 'focus'; id: TreeItemId | null }
|
|
62
|
-
| { type: 'set-query'; q: string }
|
|
63
|
-
| { type: 'cache-tick' };
|
|
64
|
-
|
|
65
|
-
const reducer = <T,>(state: State<T>, action: Action<T>): State<T> => {
|
|
66
|
-
switch (action.type) {
|
|
67
|
-
case 'expand': {
|
|
68
|
-
if (state.expanded.has(action.id)) return state;
|
|
69
|
-
const next = new Set(state.expanded);
|
|
70
|
-
next.add(action.id);
|
|
71
|
-
return { ...state, expanded: next };
|
|
72
|
-
}
|
|
73
|
-
case 'collapse': {
|
|
74
|
-
if (!state.expanded.has(action.id)) return state;
|
|
75
|
-
const next = new Set(state.expanded);
|
|
76
|
-
next.delete(action.id);
|
|
77
|
-
return { ...state, expanded: next };
|
|
78
|
-
}
|
|
79
|
-
case 'toggle': {
|
|
80
|
-
const next = new Set(state.expanded);
|
|
81
|
-
if (next.has(action.id)) next.delete(action.id);
|
|
82
|
-
else next.add(action.id);
|
|
83
|
-
return { ...state, expanded: next };
|
|
84
|
-
}
|
|
85
|
-
case 'set-expanded':
|
|
86
|
-
return { ...state, expanded: new Set(action.ids) };
|
|
87
|
-
case 'select': {
|
|
88
|
-
if (action.mode === 'none') return state;
|
|
89
|
-
if (action.mode === 'single') {
|
|
90
|
-
return { ...state, selected: new Set([action.id]), focused: action.id };
|
|
91
|
-
}
|
|
92
|
-
const next = new Set(state.selected);
|
|
93
|
-
if (next.has(action.id)) next.delete(action.id);
|
|
94
|
-
else next.add(action.id);
|
|
95
|
-
return { ...state, selected: next, focused: action.id };
|
|
96
|
-
}
|
|
97
|
-
case 'select-many':
|
|
98
|
-
return { ...state, selected: new Set(action.ids) };
|
|
99
|
-
case 'clear-selection':
|
|
100
|
-
return { ...state, selected: new Set() };
|
|
101
|
-
case 'focus':
|
|
102
|
-
return { ...state, focused: action.id };
|
|
103
|
-
case 'set-query':
|
|
104
|
-
return { ...state, query: action.q };
|
|
105
|
-
case 'cache-tick':
|
|
106
|
-
return { ...state, cacheTick: state.cacheTick + 1 };
|
|
107
|
-
default:
|
|
108
|
-
return state;
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
18
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
select: (id: TreeItemId) => void;
|
|
135
|
-
setSelectedIds: (ids: TreeItemId[]) => void;
|
|
136
|
-
clearSelection: () => void;
|
|
137
|
-
setFocus: (id: TreeItemId | null) => void;
|
|
138
|
-
setQuery: (q: string) => void;
|
|
139
|
-
refresh: (id: TreeItemId) => Promise<void>;
|
|
140
|
-
refreshAll: () => Promise<void>;
|
|
141
|
-
activate: (node: TreeNode<T>, opts?: TreeActivateOptions) => void;
|
|
142
|
-
|
|
143
|
-
// Config / slots
|
|
144
|
-
labels: TreeLabels;
|
|
145
|
-
/** Resolved cosmetic config — never null. */
|
|
146
|
-
appearance: ResolvedAppearance;
|
|
147
|
-
/** Convenience alias for `appearance.indent`. */
|
|
148
|
-
indent: number;
|
|
149
|
-
selectionMode: TreeSelectionMode;
|
|
150
|
-
activationMode: TreeActivationMode;
|
|
151
|
-
enableSearch: boolean;
|
|
152
|
-
showIndentGuides: boolean;
|
|
153
|
-
getItemName: (node: TreeNode<T>) => string;
|
|
154
|
-
|
|
155
|
-
renderIcon?: TreeRowSlot<T>;
|
|
156
|
-
renderLabel?: TreeRowSlot<T>;
|
|
157
|
-
renderActions?: TreeRowSlot<T>;
|
|
158
|
-
renderContextMenu?: TreeContextMenuSlot<T>;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const TreeContext = createContext<TreeContextValue<unknown> | null>(null);
|
|
19
|
+
import { reducer, createInitialState } from './state';
|
|
20
|
+
import { useAsyncChildren } from './async-children';
|
|
21
|
+
import { useExpansion } from './expansion';
|
|
22
|
+
import { useSelection } from './selection';
|
|
23
|
+
import { useRename } from './rename';
|
|
24
|
+
import { useClipboard } from './clipboard';
|
|
25
|
+
import { useResolvedMenu } from './menu';
|
|
26
|
+
import { useDnd, type UseDndReturn } from './dnd';
|
|
27
|
+
import { usePersistSync } from './persist';
|
|
28
|
+
import type { TreeContextValue } from './TreeContextValue';
|
|
29
|
+
|
|
30
|
+
// Re-exported from this module: the value interface (so consumers
|
|
31
|
+
// continue to `import type { TreeContextValue }` from `./context`).
|
|
32
|
+
export type { TreeContextValue } from './TreeContextValue';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Internal context object. Exported so `TreeRoot` can wrap it with an
|
|
36
|
+
* override-provider that injects a slot-form `renderContextMenu`
|
|
37
|
+
* derived from the declarative resolver. Consumers should use
|
|
38
|
+
* `useTreeContext()` instead of touching this directly.
|
|
39
|
+
*/
|
|
40
|
+
export const TreeContext = createContext<TreeContextValue<unknown> | null>(null);
|
|
162
41
|
|
|
163
42
|
export function useTreeContext<T>(): TreeContextValue<T> {
|
|
164
43
|
const ctx = React.useContext(TreeContext);
|
|
@@ -169,7 +48,20 @@ export function useTreeContext<T>(): TreeContextValue<T> {
|
|
|
169
48
|
}
|
|
170
49
|
|
|
171
50
|
// =====================================================================
|
|
172
|
-
// Provider
|
|
51
|
+
// Provider — thin assembly. The real work lives in:
|
|
52
|
+
//
|
|
53
|
+
// state/ reducer + initial state
|
|
54
|
+
// async-children/ child cache, nodeById, fetchChildren, refresh, refreshAll
|
|
55
|
+
// expansion/ expand / collapse / toggle / expandAll / collapseAll
|
|
56
|
+
// selection/ click / move / select-all + plain select / clear
|
|
57
|
+
// rename/ start / cancel / commit
|
|
58
|
+
// clipboard/ cut / copy / paste / clear
|
|
59
|
+
// menu/ built-in actions registry + merged declarative resolver
|
|
60
|
+
// persist/ localStorage + onSelectionChange/onExpansionChange
|
|
61
|
+
//
|
|
62
|
+
// This file only stitches them together and shapes the final
|
|
63
|
+
// `TreeContextValue`. If a feature grows, add a folder above — don't
|
|
64
|
+
// extend this file.
|
|
173
65
|
// =====================================================================
|
|
174
66
|
|
|
175
67
|
export interface TreeProviderProps<T>
|
|
@@ -194,36 +86,19 @@ export interface TreeProviderProps<T>
|
|
|
194
86
|
| 'renderLabel'
|
|
195
87
|
| 'renderActions'
|
|
196
88
|
| 'renderContextMenu'
|
|
89
|
+
| 'contextMenuActions'
|
|
197
90
|
| 'labels'
|
|
198
91
|
| 'persistKey'
|
|
199
92
|
| 'persistSelection'
|
|
93
|
+
| 'adapter'
|
|
94
|
+
| 'defaultMenuItems'
|
|
95
|
+
| 'enableInlineRename'
|
|
96
|
+
| 'enableDnD'
|
|
97
|
+
| 'canDrop'
|
|
200
98
|
> {
|
|
201
99
|
children: React.ReactNode;
|
|
202
100
|
}
|
|
203
101
|
|
|
204
|
-
const setEqualsArr = (set: ReadonlySet<string>, arr: readonly string[]) => {
|
|
205
|
-
if (set.size !== arr.length) return false;
|
|
206
|
-
for (const id of arr) if (!set.has(id)) return false;
|
|
207
|
-
return true;
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
const collectAllIds = <T,>(
|
|
211
|
-
roots: TreeNode<T>[],
|
|
212
|
-
cache: ChildCache<T>,
|
|
213
|
-
out: TreeItemId[],
|
|
214
|
-
) => {
|
|
215
|
-
for (const node of roots) {
|
|
216
|
-
if (Array.isArray(node.children)) {
|
|
217
|
-
out.push(node.id);
|
|
218
|
-
collectAllIds(node.children, cache, out);
|
|
219
|
-
} else if (node.isFolder) {
|
|
220
|
-
out.push(node.id);
|
|
221
|
-
const entry = cache.get(node.id);
|
|
222
|
-
if (entry?.children) collectAllIds(entry.children, cache, out);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
|
|
227
102
|
export function TreeProvider<T>(props: TreeProviderProps<T>) {
|
|
228
103
|
const {
|
|
229
104
|
data,
|
|
@@ -245,13 +120,21 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
|
|
|
245
120
|
renderLabel,
|
|
246
121
|
renderActions,
|
|
247
122
|
renderContextMenu,
|
|
123
|
+
contextMenuActions,
|
|
248
124
|
labels: labelsOverride,
|
|
249
125
|
persistKey,
|
|
250
126
|
persistSelection = false,
|
|
127
|
+
adapter,
|
|
128
|
+
defaultMenuItems,
|
|
129
|
+
enableInlineRename = false,
|
|
130
|
+
enableDnD = false,
|
|
131
|
+
canDrop,
|
|
251
132
|
children,
|
|
252
133
|
} = props;
|
|
253
134
|
|
|
254
|
-
|
|
135
|
+
// ---- Stable config ------------------------------------------------
|
|
136
|
+
|
|
137
|
+
const labels = useMemo<TreeLabels>(
|
|
255
138
|
() => ({ ...DEFAULT_TREE_LABELS, ...labelsOverride }),
|
|
256
139
|
[labelsOverride],
|
|
257
140
|
);
|
|
@@ -261,103 +144,53 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
|
|
|
261
144
|
[appearance, indent],
|
|
262
145
|
);
|
|
263
146
|
|
|
264
|
-
//
|
|
147
|
+
// ---- Reducer ------------------------------------------------------
|
|
148
|
+
|
|
265
149
|
const persisted = useMemo(
|
|
266
150
|
() => (persistKey ? loadTreeState(persistKey) : null),
|
|
267
151
|
[persistKey],
|
|
268
152
|
);
|
|
269
153
|
|
|
270
|
-
const [state, dispatch] = useReducer(reducer
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
),
|
|
277
|
-
focused: null,
|
|
278
|
-
query: '',
|
|
279
|
-
cacheTick: 0,
|
|
280
|
-
}));
|
|
281
|
-
|
|
282
|
-
// Async cache survives provider re-renders, lives in a ref.
|
|
283
|
-
const cacheRef = useRef<ChildCache<T>>(createChildCache<T>());
|
|
284
|
-
const inflightRef = useRef<Map<TreeItemId, Promise<void>>>(new Map());
|
|
285
|
-
|
|
286
|
-
// Trigger one fetch per (folder id) — concurrent expansions are deduped.
|
|
287
|
-
const fetchChildren = useCallback(
|
|
288
|
-
async (node: TreeNode<T>) => {
|
|
289
|
-
if (!loadChildren) return;
|
|
290
|
-
if (Array.isArray(node.children)) return;
|
|
291
|
-
const existing = cacheRef.current.get(node.id);
|
|
292
|
-
if (existing?.status === 'loaded' || existing?.status === 'loading') return;
|
|
293
|
-
const inflight = inflightRef.current.get(node.id);
|
|
294
|
-
if (inflight) return inflight;
|
|
295
|
-
|
|
296
|
-
cacheRef.current.set(node.id, { status: 'loading', children: [] });
|
|
297
|
-
dispatch({ type: 'cache-tick' });
|
|
298
|
-
|
|
299
|
-
const promise = (async () => {
|
|
300
|
-
try {
|
|
301
|
-
const children = await loadChildren(node);
|
|
302
|
-
cacheRef.current.set(node.id, { status: 'loaded', children });
|
|
303
|
-
} catch (err) {
|
|
304
|
-
cacheRef.current.set(node.id, {
|
|
305
|
-
status: 'error',
|
|
306
|
-
children: [],
|
|
307
|
-
error: err instanceof Error ? err.message : String(err),
|
|
308
|
-
});
|
|
309
|
-
} finally {
|
|
310
|
-
inflightRef.current.delete(node.id);
|
|
311
|
-
dispatch({ type: 'cache-tick' });
|
|
312
|
-
}
|
|
313
|
-
})();
|
|
314
|
-
|
|
315
|
-
inflightRef.current.set(node.id, promise);
|
|
316
|
-
return promise;
|
|
317
|
-
},
|
|
318
|
-
[loadChildren],
|
|
154
|
+
const [state, dispatch] = useReducer(reducer, undefined, () =>
|
|
155
|
+
createInitialState({
|
|
156
|
+
persisted,
|
|
157
|
+
initialExpandedIds,
|
|
158
|
+
initialSelectedIds,
|
|
159
|
+
persistSelection,
|
|
160
|
+
}),
|
|
319
161
|
);
|
|
320
162
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (!loadChildren) return;
|
|
341
|
-
for (const id of state.expanded) {
|
|
342
|
-
const node = nodeById.get(id);
|
|
343
|
-
if (!node) continue;
|
|
344
|
-
void fetchChildren(node);
|
|
345
|
-
}
|
|
346
|
-
}, [loadChildren, state.expanded, state.cacheTick, nodeById, fetchChildren]);
|
|
163
|
+
const bumpCacheTick = useCallback(() => dispatch({ type: 'cache-tick' }), []);
|
|
164
|
+
|
|
165
|
+
// ---- Async children (cache + nodeById + refresh) ------------------
|
|
166
|
+
|
|
167
|
+
const {
|
|
168
|
+
nodeById,
|
|
169
|
+
refresh,
|
|
170
|
+
refreshAll,
|
|
171
|
+
collectFolderIds,
|
|
172
|
+
cache,
|
|
173
|
+
} = useAsyncChildren<T>({
|
|
174
|
+
data,
|
|
175
|
+
loadChildren,
|
|
176
|
+
expanded: state.expanded,
|
|
177
|
+
cacheTick: state.cacheTick,
|
|
178
|
+
bumpCacheTick,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ---- Flat rows (depend on cache via cacheTick) --------------------
|
|
347
182
|
|
|
348
|
-
|
|
349
|
-
const flatRows = useMemo(
|
|
183
|
+
const flatRows = useMemo<FlatRow<T>[]>(
|
|
350
184
|
() =>
|
|
351
185
|
flattenTree<T>({
|
|
352
186
|
roots: data,
|
|
353
187
|
expandedIds: state.expanded,
|
|
354
|
-
cache
|
|
188
|
+
cache,
|
|
355
189
|
filterNode,
|
|
356
190
|
}),
|
|
357
|
-
[data, state.expanded, state.cacheTick, filterNode],
|
|
191
|
+
[data, state.expanded, state.cacheTick, cache, filterNode],
|
|
358
192
|
);
|
|
359
193
|
|
|
360
|
-
// Search matches (case-insensitive substring on getItemName).
|
|
361
194
|
const matchingIds = useMemo(() => {
|
|
362
195
|
const set = new Set<TreeItemId>();
|
|
363
196
|
if (!enableSearch || state.query.trim() === '') return set;
|
|
@@ -370,127 +203,123 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
|
|
|
370
203
|
return set;
|
|
371
204
|
}, [enableSearch, state.query, flatRows, getItemName]);
|
|
372
205
|
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
const
|
|
206
|
+
// ---- Feature hooks ------------------------------------------------
|
|
207
|
+
|
|
208
|
+
const expansion = useExpansion({ dispatch, collectFolderIds });
|
|
209
|
+
const selection = useSelection<T>({
|
|
210
|
+
dispatch,
|
|
211
|
+
selectionMode,
|
|
212
|
+
flatRows,
|
|
213
|
+
selected: state.selected,
|
|
214
|
+
anchor: state.anchor,
|
|
215
|
+
focused: state.focused,
|
|
216
|
+
});
|
|
217
|
+
const rename = useRename<T>({
|
|
218
|
+
dispatch,
|
|
219
|
+
adapter,
|
|
220
|
+
enableInlineRename,
|
|
221
|
+
nodeById,
|
|
222
|
+
getItemName,
|
|
223
|
+
labels,
|
|
224
|
+
});
|
|
225
|
+
const clipboard = useClipboard<T>({
|
|
226
|
+
dispatch,
|
|
227
|
+
clipboard: state.clipboard,
|
|
228
|
+
adapter,
|
|
229
|
+
nodeById,
|
|
230
|
+
labels,
|
|
231
|
+
});
|
|
232
|
+
const dnd = useDnd<T>({
|
|
233
|
+
enabled: enableDnD,
|
|
234
|
+
adapter,
|
|
235
|
+
nodeById,
|
|
236
|
+
selected: state.selected,
|
|
237
|
+
labels,
|
|
238
|
+
canDrop,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ---- Activation ---------------------------------------------------
|
|
242
|
+
|
|
376
243
|
const onActivateRef = useRef(onActivate);
|
|
377
|
-
onSelectionChangeRef.current = onSelectionChange;
|
|
378
|
-
onExpansionChangeRef.current = onExpansionChange;
|
|
379
244
|
onActivateRef.current = onActivate;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const lastExpandedArrRef = useRef<TreeItemId[]>([...state.expanded]);
|
|
384
|
-
|
|
385
|
-
useEffect(() => {
|
|
386
|
-
const arr = [...state.expanded];
|
|
387
|
-
if (!setEqualsArr(state.expanded, lastExpandedArrRef.current)) {
|
|
388
|
-
lastExpandedArrRef.current = arr;
|
|
389
|
-
onExpansionChangeRef.current?.(arr);
|
|
390
|
-
if (persistKey) {
|
|
391
|
-
saveTreeState(persistKey, {
|
|
392
|
-
expandedItems: arr,
|
|
393
|
-
selectedItems: persistSelection ? [...state.selected] : [],
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}, [state.expanded, persistKey, persistSelection, state.selected]);
|
|
398
|
-
|
|
399
|
-
useEffect(() => {
|
|
400
|
-
const arr = [...state.selected];
|
|
401
|
-
if (!setEqualsArr(state.selected, lastSelectedArrRef.current)) {
|
|
402
|
-
lastSelectedArrRef.current = arr;
|
|
403
|
-
onSelectionChangeRef.current?.(arr);
|
|
404
|
-
if (persistKey && persistSelection) {
|
|
405
|
-
saveTreeState(persistKey, {
|
|
406
|
-
expandedItems: [...state.expanded],
|
|
407
|
-
selectedItems: arr,
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}, [state.selected, persistKey, persistSelection, state.expanded]);
|
|
412
|
-
|
|
413
|
-
// Imperative actions.
|
|
414
|
-
const expand = useCallback((id: TreeItemId) => dispatch({ type: 'expand', id }), []);
|
|
415
|
-
const collapse = useCallback((id: TreeItemId) => dispatch({ type: 'collapse', id }), []);
|
|
416
|
-
const toggle = useCallback((id: TreeItemId) => dispatch({ type: 'toggle', id }), []);
|
|
417
|
-
|
|
418
|
-
const expandAll = useCallback(() => {
|
|
419
|
-
const ids: TreeItemId[] = [];
|
|
420
|
-
collectAllIds(data, cacheRef.current, ids);
|
|
421
|
-
dispatch({ type: 'set-expanded', ids });
|
|
422
|
-
}, [data]);
|
|
423
|
-
|
|
424
|
-
const collapseAll = useCallback(
|
|
425
|
-
() => dispatch({ type: 'set-expanded', ids: [] }),
|
|
245
|
+
const activate = useCallback(
|
|
246
|
+
(node: TreeNode<T>, opts: TreeActivateOptions = { preview: false }) =>
|
|
247
|
+
onActivateRef.current?.(node, opts),
|
|
426
248
|
[],
|
|
427
249
|
);
|
|
428
250
|
|
|
429
|
-
const
|
|
430
|
-
(
|
|
431
|
-
[selectionMode],
|
|
432
|
-
);
|
|
433
|
-
const setSelectedIds = useCallback(
|
|
434
|
-
(ids: TreeItemId[]) => dispatch({ type: 'select-many', ids }),
|
|
251
|
+
const setQuery = useCallback(
|
|
252
|
+
(q: string) => dispatch({ type: 'set-query', q }),
|
|
435
253
|
[],
|
|
436
254
|
);
|
|
437
|
-
const clearSelection = useCallback(() => dispatch({ type: 'clear-selection' }), []);
|
|
438
|
-
const setFocus = useCallback(
|
|
439
|
-
(id: TreeItemId | null) => dispatch({ type: 'focus', id }),
|
|
440
|
-
[],
|
|
441
|
-
);
|
|
442
|
-
const setQuery = useCallback((q: string) => dispatch({ type: 'set-query', q }), []);
|
|
443
|
-
|
|
444
|
-
const refresh = useCallback(
|
|
445
|
-
async (id: TreeItemId) => {
|
|
446
|
-
const node = nodeById.get(id);
|
|
447
|
-
if (!node || !loadChildren) return;
|
|
448
|
-
cacheRef.current.delete(id);
|
|
449
|
-
dispatch({ type: 'cache-tick' });
|
|
450
|
-
await fetchChildren(node);
|
|
451
|
-
},
|
|
452
|
-
[nodeById, loadChildren, fetchChildren],
|
|
453
|
-
);
|
|
454
255
|
|
|
455
|
-
|
|
456
|
-
cacheRef.current.clear();
|
|
457
|
-
dispatch({ type: 'cache-tick' });
|
|
458
|
-
if (!loadChildren) return;
|
|
459
|
-
await Promise.all(
|
|
460
|
-
[...state.expanded].map((id) => {
|
|
461
|
-
const node = nodeById.get(id);
|
|
462
|
-
return node ? fetchChildren(node) : undefined;
|
|
463
|
-
}),
|
|
464
|
-
);
|
|
465
|
-
}, [loadChildren, state.expanded, nodeById, fetchChildren]);
|
|
256
|
+
// ---- Persist + notify callbacks -----------------------------------
|
|
466
257
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
258
|
+
usePersistSync({
|
|
259
|
+
expanded: state.expanded,
|
|
260
|
+
selected: state.selected,
|
|
261
|
+
persistKey,
|
|
262
|
+
persistSelection,
|
|
263
|
+
onSelectionChange,
|
|
264
|
+
onExpansionChange,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ---- Resolved context-menu resolver -------------------------------
|
|
268
|
+
|
|
269
|
+
const resolvedContextMenuActions = useResolvedMenu<T>({
|
|
270
|
+
adapter,
|
|
271
|
+
contextMenuActions,
|
|
272
|
+
defaultMenuItems,
|
|
273
|
+
labels,
|
|
274
|
+
selected: state.selected,
|
|
275
|
+
clipboard: state.clipboard,
|
|
276
|
+
nodeById,
|
|
277
|
+
getItemName,
|
|
278
|
+
enableInlineRename,
|
|
279
|
+
startRename: rename.startRename,
|
|
280
|
+
cutToClipboard: clipboard.cutToClipboard,
|
|
281
|
+
copyToClipboard: clipboard.copyToClipboard,
|
|
282
|
+
pasteFromClipboard: clipboard.pasteFromClipboard,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// ---- Final value --------------------------------------------------
|
|
472
286
|
|
|
473
287
|
const value = useMemo<TreeContextValue<T>>(
|
|
474
288
|
() => ({
|
|
289
|
+
// state
|
|
475
290
|
expanded: state.expanded,
|
|
476
291
|
selected: state.selected,
|
|
292
|
+
anchor: state.anchor,
|
|
477
293
|
focused: state.focused,
|
|
478
294
|
query: state.query,
|
|
295
|
+
renamingId: state.renaming,
|
|
296
|
+
inlineRenameEnabled: rename.enabled,
|
|
297
|
+
clipboard: state.clipboard,
|
|
479
298
|
flatRows,
|
|
480
299
|
matchingIds,
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
clearSelection,
|
|
489
|
-
setFocus,
|
|
300
|
+
|
|
301
|
+
// expansion
|
|
302
|
+
...expansion,
|
|
303
|
+
|
|
304
|
+
// selection (note: `select` is from selection hook; expansion exports no `select`)
|
|
305
|
+
...selection,
|
|
306
|
+
|
|
490
307
|
setQuery,
|
|
308
|
+
|
|
309
|
+
// clipboard
|
|
310
|
+
...clipboard,
|
|
311
|
+
|
|
312
|
+
// rename
|
|
313
|
+
startRename: rename.startRename,
|
|
314
|
+
cancelRename: rename.cancelRename,
|
|
315
|
+
commitRename: rename.commitRename,
|
|
316
|
+
|
|
317
|
+
// async
|
|
491
318
|
refresh,
|
|
492
319
|
refreshAll,
|
|
493
320
|
activate,
|
|
321
|
+
|
|
322
|
+
// config
|
|
494
323
|
labels,
|
|
495
324
|
appearance: resolvedAppearance,
|
|
496
325
|
indent: resolvedAppearance.indent,
|
|
@@ -499,28 +328,36 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
|
|
|
499
328
|
enableSearch,
|
|
500
329
|
showIndentGuides,
|
|
501
330
|
getItemName,
|
|
331
|
+
|
|
332
|
+
// slots
|
|
502
333
|
renderIcon,
|
|
503
334
|
renderLabel,
|
|
504
335
|
renderActions,
|
|
505
336
|
renderContextMenu,
|
|
337
|
+
|
|
338
|
+
adapter,
|
|
339
|
+
resolvedContextMenuActions,
|
|
340
|
+
getNodeById: (id: TreeItemId) => nodeById.get(id),
|
|
341
|
+
dnd,
|
|
506
342
|
}),
|
|
507
343
|
[
|
|
508
344
|
state.expanded,
|
|
509
345
|
state.selected,
|
|
346
|
+
state.anchor,
|
|
510
347
|
state.focused,
|
|
511
348
|
state.query,
|
|
349
|
+
state.renaming,
|
|
350
|
+
state.clipboard,
|
|
351
|
+
rename.enabled,
|
|
352
|
+
rename.startRename,
|
|
353
|
+
rename.cancelRename,
|
|
354
|
+
rename.commitRename,
|
|
512
355
|
flatRows,
|
|
513
356
|
matchingIds,
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
toggle,
|
|
517
|
-
expandAll,
|
|
518
|
-
collapseAll,
|
|
519
|
-
select,
|
|
520
|
-
setSelectedIds,
|
|
521
|
-
clearSelection,
|
|
522
|
-
setFocus,
|
|
357
|
+
expansion,
|
|
358
|
+
selection,
|
|
523
359
|
setQuery,
|
|
360
|
+
clipboard,
|
|
524
361
|
refresh,
|
|
525
362
|
refreshAll,
|
|
526
363
|
activate,
|
|
@@ -535,6 +372,10 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
|
|
|
535
372
|
renderLabel,
|
|
536
373
|
renderActions,
|
|
537
374
|
renderContextMenu,
|
|
375
|
+
adapter,
|
|
376
|
+
resolvedContextMenuActions,
|
|
377
|
+
nodeById,
|
|
378
|
+
dnd,
|
|
538
379
|
],
|
|
539
380
|
);
|
|
540
381
|
|
|
@@ -546,4 +387,4 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
|
|
|
546
387
|
}
|
|
547
388
|
|
|
548
389
|
// Re-export internal types referenced by hook consumers.
|
|
549
|
-
export type { ChildCache, ChildEntry };
|
|
390
|
+
export type { ChildCache, ChildEntry } from '../data/childCache';
|