ydb-ui-components 5.4.1 → 6.0.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/build/cjs/components/NavigationTree/NavigationTree.d.ts +15 -2
- package/build/cjs/components/NavigationTree/NavigationTree.js +66 -4
- package/build/cjs/components/NavigationTree/NavigationTreeNode.d.ts +6 -2
- package/build/cjs/components/NavigationTree/NavigationTreeNode.js +7 -40
- package/build/cjs/components/NavigationTree/state.d.ts +7 -0
- package/build/cjs/components/NavigationTree/state.js +32 -4
- package/build/cjs/components/NavigationTree/types.d.ts +10 -1
- package/build/cjs/components/TreeView/TreeView.d.ts +7 -1
- package/build/cjs/components/TreeView/TreeView.js +2 -2
- package/build/esm/components/NavigationTree/NavigationTree.d.ts +15 -2
- package/build/esm/components/NavigationTree/NavigationTree.js +67 -4
- package/build/esm/components/NavigationTree/NavigationTreeNode.d.ts +6 -2
- package/build/esm/components/NavigationTree/NavigationTreeNode.js +7 -40
- package/build/esm/components/NavigationTree/state.d.ts +7 -0
- package/build/esm/components/NavigationTree/state.js +32 -4
- package/build/esm/components/NavigationTree/types.d.ts +10 -1
- package/build/esm/components/TreeView/TreeView.d.ts +7 -1
- package/build/esm/components/TreeView/TreeView.js +2 -2
- package/package.json +1 -1
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
import React from 'react';
|
|
2
2
|
import type { NavigationTreeProps } from './types';
|
|
3
3
|
export type { NavigationTreeProps };
|
|
4
|
-
export
|
|
4
|
+
export interface NavigationTreeHandle {
|
|
5
|
+
/**
|
|
6
|
+
* Scrolls the currently active node into view (if any).
|
|
7
|
+
* Has no effect if there is no active node or it is not present in the tree.
|
|
8
|
+
*/
|
|
9
|
+
scrollToActive: () => void;
|
|
10
|
+
}
|
|
11
|
+
declare function NavigationTreeInner<D = any, M = any>({ rootState: partialRootState, fetchPath, getActions, renderAdditionalNodeElements, activePath, onActionsOpenToggle, onActivePathUpdate, cache, virtualize, }: NavigationTreeProps<D, M>, ref: React.ForwardedRef<NavigationTreeHandle>): JSX.Element;
|
|
12
|
+
/**
|
|
13
|
+
* `React.forwardRef` loses generics, so we cast to preserve the `<D, M>` API.
|
|
14
|
+
*/
|
|
15
|
+
export declare const NavigationTree: <D = any, M = any>(props: NavigationTreeProps<D, M> & {
|
|
16
|
+
ref?: React.ForwardedRef<NavigationTreeHandle> | undefined;
|
|
17
|
+
}) => ReturnType<typeof NavigationTreeInner>;
|
|
@@ -23,19 +23,81 @@ const renderServiceNode = (node) => {
|
|
|
23
23
|
}
|
|
24
24
|
return (0, jsx_runtime_1.jsx)(EmptyView_1.EmptyView, { level: node.level }, key);
|
|
25
25
|
};
|
|
26
|
-
function
|
|
26
|
+
function NavigationTreeInner({ rootState: partialRootState, fetchPath, getActions, renderAdditionalNodeElements, activePath, onActionsOpenToggle, onActivePathUpdate, cache = true, virtualize = false, }, ref) {
|
|
27
27
|
const [state, dispatch] = react_1.default.useReducer(state_1.reducer, {
|
|
28
28
|
[partialRootState.path]: (0, state_1.getNodeState)(partialRootState),
|
|
29
29
|
});
|
|
30
30
|
const nodesList = react_1.default.useMemo(() => (0, state_1.selectTreeAsList)(state, partialRootState.path), [partialRootState.path, state]);
|
|
31
|
+
const nodesToLoad = react_1.default.useMemo(() => nodesList.filter((node) => !(0, utils_1.isServiceNode)(node) &&
|
|
32
|
+
!node.collapsed &&
|
|
33
|
+
!node.loaded &&
|
|
34
|
+
!node.loading &&
|
|
35
|
+
!node.error), [nodesList]);
|
|
36
|
+
const reactListRef = react_1.default.useRef(null);
|
|
37
|
+
/**
|
|
38
|
+
* Monotonic counter of in-flight load requests. The current value is captured per
|
|
39
|
+
* dispatched `StartLoading` and forwarded to the resolving `FinishLoading` /
|
|
40
|
+
* `ErrorLoading`, so the reducer can drop responses that no longer match the latest
|
|
41
|
+
* request for a given path (see the stale-result guard in `reducer`).
|
|
42
|
+
*/
|
|
43
|
+
const requestIdRef = react_1.default.useRef(0);
|
|
44
|
+
const activeItemRef = react_1.default.useRef(null);
|
|
45
|
+
const activeNodeIndex = react_1.default.useMemo(() => nodesList.findIndex((node) => !(0, utils_1.isServiceNode)(node) && node.path === activePath), [activePath, nodesList]);
|
|
46
|
+
const scrolledActivePathRef = react_1.default.useRef(undefined);
|
|
47
|
+
const scrollToActive = react_1.default.useCallback(() => {
|
|
48
|
+
var _a, _b;
|
|
49
|
+
if (activeNodeIndex < 0)
|
|
50
|
+
return;
|
|
51
|
+
if (virtualize) {
|
|
52
|
+
(_a = reactListRef.current) === null || _a === void 0 ? void 0 : _a.scrollAround(activeNodeIndex);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
(_b = activeItemRef.current) === null || _b === void 0 ? void 0 : _b.scrollIntoView({ block: 'nearest' });
|
|
56
|
+
}
|
|
57
|
+
}, [activeNodeIndex, virtualize]);
|
|
58
|
+
react_1.default.useImperativeHandle(ref, () => ({ scrollToActive }), [scrollToActive]);
|
|
59
|
+
react_1.default.useEffect(() => {
|
|
60
|
+
nodesToLoad.forEach((node) => {
|
|
61
|
+
const requestId = ++requestIdRef.current;
|
|
62
|
+
dispatch({
|
|
63
|
+
type: state_1.NavigationTreeActionType.StartLoading,
|
|
64
|
+
payload: { path: node.path, requestId },
|
|
65
|
+
});
|
|
66
|
+
fetchPath(node.path)
|
|
67
|
+
.then((data) => {
|
|
68
|
+
dispatch({
|
|
69
|
+
type: state_1.NavigationTreeActionType.FinishLoading,
|
|
70
|
+
payload: { path: node.path, activePath, data, requestId },
|
|
71
|
+
});
|
|
72
|
+
})
|
|
73
|
+
.catch((error) => {
|
|
74
|
+
dispatch({
|
|
75
|
+
type: state_1.NavigationTreeActionType.ErrorLoading,
|
|
76
|
+
payload: { path: node.path, error, requestId },
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}, [activePath, dispatch, fetchPath, nodesToLoad]);
|
|
81
|
+
react_1.default.useEffect(() => {
|
|
82
|
+
if (scrolledActivePathRef.current === activePath)
|
|
83
|
+
return;
|
|
84
|
+
if (activeNodeIndex < 0)
|
|
85
|
+
return;
|
|
86
|
+
scrollToActive();
|
|
87
|
+
scrolledActivePathRef.current = activePath;
|
|
88
|
+
}, [activeNodeIndex, activePath, scrollToActive]);
|
|
31
89
|
const renderNode = (node) => {
|
|
32
|
-
|
|
90
|
+
const isActive = node.path === activePath;
|
|
91
|
+
return ((0, jsx_runtime_1.jsx)(NavigationTreeNode_1.NavigationTreeNode, { state: state, path: node.path, activePath: activePath, dispatch: dispatch, onActivate: onActivePathUpdate, getActions: getActions, onActionsOpenToggle: onActionsOpenToggle, renderAdditionalNodeElements: renderAdditionalNodeElements, cache: cache, level: node.level, activeItemRef: isActive ? activeItemRef : undefined }, node.path));
|
|
33
92
|
};
|
|
34
|
-
const renderVirtualizedTree = () => ((0, jsx_runtime_1.jsx)(react_list_1.default, { type: "uniform", length: nodesList.length, useStaticSize: true, itemRenderer: (index) => {
|
|
93
|
+
const renderVirtualizedTree = () => ((0, jsx_runtime_1.jsx)(react_list_1.default, { ref: reactListRef, type: "uniform", length: nodesList.length, useStaticSize: true, itemRenderer: (index) => {
|
|
35
94
|
const node = nodesList[index];
|
|
36
95
|
return (0, utils_1.isServiceNode)(node) ? renderServiceNode(node) : renderNode(node);
|
|
37
96
|
} }));
|
|
38
97
|
const renderSimpleTree = () => ((0, jsx_runtime_1.jsx)(react_1.default.Fragment, { children: nodesList.map((node) => (0, utils_1.isServiceNode)(node) ? renderServiceNode(node) : renderNode(node)) }));
|
|
39
98
|
return virtualize ? renderVirtualizedTree() : renderSimpleTree();
|
|
40
99
|
}
|
|
41
|
-
|
|
100
|
+
/**
|
|
101
|
+
* `React.forwardRef` loses generics, so we cast to preserve the `<D, M>` API.
|
|
102
|
+
*/
|
|
103
|
+
exports.NavigationTree = react_1.default.forwardRef(NavigationTreeInner);
|
|
@@ -3,7 +3,6 @@ import type { NavigationTreeAction } from './state';
|
|
|
3
3
|
import type { NavigationTreeProps, NavigationTreeState } from './types';
|
|
4
4
|
export interface NavigationTreeNodeProps {
|
|
5
5
|
path: string;
|
|
6
|
-
fetchPath: NavigationTreeProps['fetchPath'];
|
|
7
6
|
activePath?: string;
|
|
8
7
|
state: NavigationTreeState;
|
|
9
8
|
level?: number;
|
|
@@ -14,5 +13,10 @@ export interface NavigationTreeNodeProps {
|
|
|
14
13
|
onActionsOpenToggle?: NavigationTreeProps['onActionsOpenToggle'];
|
|
15
14
|
renderAdditionalNodeElements?: NavigationTreeProps['renderAdditionalNodeElements'];
|
|
16
15
|
cache?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Ref object attached to the item element when this node is the active one.
|
|
18
|
+
* Set only for the active node so the parent can scroll it into view.
|
|
19
|
+
*/
|
|
20
|
+
activeItemRef?: React.RefObject<HTMLDivElement>;
|
|
17
21
|
}
|
|
18
|
-
export declare function NavigationTreeNode({ path,
|
|
22
|
+
export declare function NavigationTreeNode({ path, activePath, state, level, dispatch, children, onActivate, getActions, onActionsOpenToggle, renderAdditionalNodeElements, cache, activeItemRef, }: NavigationTreeNodeProps): JSX.Element;
|
|
@@ -44,49 +44,16 @@ function renderIcon(type, collapsed) {
|
|
|
44
44
|
return null;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
function NavigationTreeNode({ path,
|
|
47
|
+
function NavigationTreeNode({ path, activePath, state, level, dispatch, children, onActivate, getActions, onActionsOpenToggle, renderAdditionalNodeElements, cache, activeItemRef, }) {
|
|
48
48
|
const nodeState = state[path];
|
|
49
49
|
react_1.default.useEffect(() => {
|
|
50
|
-
if (nodeState.collapsed) {
|
|
51
|
-
if (!cache) {
|
|
52
|
-
dispatch({
|
|
53
|
-
type: state_1.NavigationTreeActionType.ResetNode,
|
|
54
|
-
payload: { path },
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
if (nodeState.loaded || nodeState.loading || nodeState.error) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
dispatch({
|
|
63
|
-
type: state_1.NavigationTreeActionType.StartLoading,
|
|
64
|
-
payload: { path },
|
|
65
|
-
});
|
|
66
|
-
fetchPath(path)
|
|
67
|
-
.then((data) => {
|
|
50
|
+
if (nodeState.collapsed && !cache) {
|
|
68
51
|
dispatch({
|
|
69
|
-
type: state_1.NavigationTreeActionType.
|
|
70
|
-
payload: { path
|
|
52
|
+
type: state_1.NavigationTreeActionType.ResetNode,
|
|
53
|
+
payload: { path },
|
|
71
54
|
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
dispatch({
|
|
75
|
-
type: state_1.NavigationTreeActionType.ErrorLoading,
|
|
76
|
-
payload: { path, error },
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
}, [
|
|
80
|
-
activePath,
|
|
81
|
-
cache,
|
|
82
|
-
dispatch,
|
|
83
|
-
fetchPath,
|
|
84
|
-
nodeState.collapsed,
|
|
85
|
-
nodeState.loaded,
|
|
86
|
-
nodeState.loading,
|
|
87
|
-
nodeState.error,
|
|
88
|
-
path,
|
|
89
|
-
]);
|
|
55
|
+
}
|
|
56
|
+
}, [cache, dispatch, nodeState.collapsed, path]);
|
|
90
57
|
const handleClick = react_1.default.useCallback(() => {
|
|
91
58
|
if (onActivate) {
|
|
92
59
|
onActivate(path);
|
|
@@ -108,6 +75,6 @@ function NavigationTreeNode({ path, fetchPath, activePath, state, level, dispatc
|
|
|
108
75
|
isOpen,
|
|
109
76
|
});
|
|
110
77
|
}, [nodeState.path, nodeState.type, onActionsOpenToggle]);
|
|
111
|
-
return ((0, jsx_runtime_1.jsx)(TreeView_1.TreeView, { name: nodeState.name, icon: renderIcon(nodeState.type, nodeState.collapsed), collapsed: nodeState.collapsed, active: nodeState.path === activePath, actions: actions, additionalNodeElements: additionalNodeElements, hasArrow: nodeState.expandable, onClick: handleClick, onArrowClick: handleArrowClick, onActionsOpenToggle: handleActionsOpenToggle, level: level, children: children }));
|
|
78
|
+
return ((0, jsx_runtime_1.jsx)(TreeView_1.TreeView, { name: nodeState.name, icon: renderIcon(nodeState.type, nodeState.collapsed), collapsed: nodeState.collapsed, active: nodeState.path === activePath, actions: actions, additionalNodeElements: additionalNodeElements, hasArrow: nodeState.expandable, onClick: handleClick, onArrowClick: handleArrowClick, onActionsOpenToggle: handleActionsOpenToggle, level: level, itemRef: activeItemRef, children: children }));
|
|
112
79
|
}
|
|
113
80
|
exports.NavigationTreeNode = NavigationTreeNode;
|
|
@@ -15,6 +15,8 @@ export type NavigationTreeAction = {
|
|
|
15
15
|
type: NavigationTreeActionType.StartLoading;
|
|
16
16
|
payload: {
|
|
17
17
|
path: string;
|
|
18
|
+
/** Monotonic id captured at dispatch time; written into node state. */
|
|
19
|
+
requestId: number;
|
|
18
20
|
};
|
|
19
21
|
} | {
|
|
20
22
|
type: NavigationTreeActionType.FinishLoading;
|
|
@@ -22,12 +24,16 @@ export type NavigationTreeAction = {
|
|
|
22
24
|
path: string;
|
|
23
25
|
activePath?: string;
|
|
24
26
|
data: NavigationTreeDataItem[];
|
|
27
|
+
/** Id of the `StartLoading` this result belongs to; stale ones are dropped. */
|
|
28
|
+
requestId: number;
|
|
25
29
|
};
|
|
26
30
|
} | {
|
|
27
31
|
type: NavigationTreeActionType.ErrorLoading;
|
|
28
32
|
payload: {
|
|
29
33
|
path: string;
|
|
30
34
|
error: unknown;
|
|
35
|
+
/** Id of the `StartLoading` this error belongs to; stale ones are dropped. */
|
|
36
|
+
requestId: number;
|
|
31
37
|
};
|
|
32
38
|
} | {
|
|
33
39
|
type: NavigationTreeActionType.ResetNode;
|
|
@@ -41,6 +47,7 @@ export declare function getDefaultNodeState(): {
|
|
|
41
47
|
loaded: boolean;
|
|
42
48
|
error: boolean;
|
|
43
49
|
children: never[];
|
|
50
|
+
requestId: number;
|
|
44
51
|
};
|
|
45
52
|
export declare function getNodeState(partialState: NavigationTreeNodePartialState): NavigationTreeNodeState;
|
|
46
53
|
export declare function reducer(state: NavigationTreeState | undefined, action: NavigationTreeAction): NavigationTreeState;
|
|
@@ -17,6 +17,7 @@ function getDefaultNodeState() {
|
|
|
17
17
|
loaded: false,
|
|
18
18
|
error: false,
|
|
19
19
|
children: [],
|
|
20
|
+
requestId: 0,
|
|
20
21
|
};
|
|
21
22
|
}
|
|
22
23
|
exports.getDefaultNodeState = getDefaultNodeState;
|
|
@@ -30,9 +31,27 @@ function reducer(state = {}, action) {
|
|
|
30
31
|
case NavigationTreeActionType.ToggleCollapsed:
|
|
31
32
|
return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, state[action.payload.path]), { collapsed: !state[action.payload.path].collapsed }) });
|
|
32
33
|
case NavigationTreeActionType.StartLoading:
|
|
33
|
-
return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, state[action.payload.path]), { loading: true, loaded: false, error: false, children: [] }) });
|
|
34
|
+
return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, state[action.payload.path]), { loading: true, loaded: false, error: false, children: [], requestId: action.payload.requestId }) });
|
|
34
35
|
case NavigationTreeActionType.FinishLoading: {
|
|
35
|
-
const
|
|
36
|
+
const currentNode = state[action.payload.path];
|
|
37
|
+
// Ignore stale results. A result is stale when:
|
|
38
|
+
// * the node was removed entirely, OR
|
|
39
|
+
// * the node's `requestId` no longer matches the one captured at dispatch time
|
|
40
|
+
// (i.e. `ResetNode` cleared it, or a newer `StartLoading` superseded it).
|
|
41
|
+
// The previous `loading`-only guard couldn't distinguish "still loading the original
|
|
42
|
+
// request" from "loading a second, newer request", which allowed an older response
|
|
43
|
+
// to overwrite a newer one after a collapse-expand cycle with `cache: false`.
|
|
44
|
+
// Note: a plain collapse with `cache: true` keeps `requestId` intact, so the fetch
|
|
45
|
+
// is still allowed to complete and populate the cache — that's intentional.
|
|
46
|
+
if (!currentNode || currentNode.requestId !== action.payload.requestId) {
|
|
47
|
+
return state;
|
|
48
|
+
}
|
|
49
|
+
const newState = Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, currentNode), { loading: false, loaded: Boolean(action.payload.data), error: false,
|
|
50
|
+
// Clear the in-flight marker now that this request has completed.
|
|
51
|
+
// The counter in `NavigationTree` only allocates ids >= 1, so any
|
|
52
|
+
// late stale `FinishLoading` / `ErrorLoading` for this path will
|
|
53
|
+
// still fail the guard above (its id > 0 won't match this 0).
|
|
54
|
+
requestId: 0 }) });
|
|
36
55
|
if (action.payload.data) {
|
|
37
56
|
newState[action.payload.path].children = action.payload.data.map(({ name }) => name);
|
|
38
57
|
for (const item of action.payload.data) {
|
|
@@ -50,8 +69,17 @@ function reducer(state = {}, action) {
|
|
|
50
69
|
}
|
|
51
70
|
return newState;
|
|
52
71
|
}
|
|
53
|
-
case NavigationTreeActionType.ErrorLoading:
|
|
54
|
-
|
|
72
|
+
case NavigationTreeActionType.ErrorLoading: {
|
|
73
|
+
const currentNode = state[action.payload.path];
|
|
74
|
+
// Ignore stale errors — see the matching note on `FinishLoading` above.
|
|
75
|
+
if (!currentNode || currentNode.requestId !== action.payload.requestId) {
|
|
76
|
+
return state;
|
|
77
|
+
}
|
|
78
|
+
return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, currentNode), { loading: false, loaded: false, error: true,
|
|
79
|
+
// See note in `FinishLoading`: clear the in-flight id on completion
|
|
80
|
+
// to keep `requestId === 0` ⇔ "no active request".
|
|
81
|
+
requestId: 0 }) });
|
|
82
|
+
}
|
|
55
83
|
case NavigationTreeActionType.ResetNode:
|
|
56
84
|
return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, state[action.payload.path]), getDefaultNodeState()) });
|
|
57
85
|
default:
|
|
@@ -24,13 +24,22 @@ export interface NavigationTreeNodeState {
|
|
|
24
24
|
children: string[];
|
|
25
25
|
level?: number;
|
|
26
26
|
meta?: unknown;
|
|
27
|
+
/**
|
|
28
|
+
* Id of the in-flight load request for this node. Assigned by `StartLoading`,
|
|
29
|
+
* cleared back to `0` on `FinishLoading` / `ErrorLoading` / `ResetNode`, so
|
|
30
|
+
* `requestId === 0` always means "no active request". Used to discard stale
|
|
31
|
+
* results: a `FinishLoading` / `ErrorLoading` whose payload id does not match
|
|
32
|
+
* the current value is dropped (so a late response from a request superseded
|
|
33
|
+
* by `ResetNode` + a new `StartLoading` cannot overwrite the newer one).
|
|
34
|
+
*/
|
|
35
|
+
requestId: number;
|
|
27
36
|
}
|
|
28
37
|
export interface NavigationTreeServiceNode {
|
|
29
38
|
path: string;
|
|
30
39
|
status: 'loading' | 'error' | 'empty';
|
|
31
40
|
level?: number;
|
|
32
41
|
}
|
|
33
|
-
export type NavigationTreeNodePartialState = Omit<NavigationTreeNodeState, 'loading' | 'loaded' | 'error' | 'children'>;
|
|
42
|
+
export type NavigationTreeNodePartialState = Omit<NavigationTreeNodeState, 'loading' | 'loaded' | 'error' | 'children' | 'requestId'>;
|
|
34
43
|
export interface NavigationTreeProps<D = any, M = any> {
|
|
35
44
|
rootState: NavigationTreeNodePartialState;
|
|
36
45
|
fetchPath: (path: string) => Promise<NavigationTreeDataItem<M>[]>;
|
|
@@ -15,5 +15,11 @@ export interface TreeViewProps {
|
|
|
15
15
|
actions?: DropdownMenuItemMixed<any>[];
|
|
16
16
|
additionalNodeElements?: JSX.Element;
|
|
17
17
|
level?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Ref object passed to the inner item element.
|
|
20
|
+
* Use this if you need to control scroll-into-view from a parent component
|
|
21
|
+
* (e.g. `itemRef.current?.scrollIntoView({block: 'nearest'})`).
|
|
22
|
+
*/
|
|
23
|
+
itemRef?: React.Ref<HTMLDivElement>;
|
|
18
24
|
}
|
|
19
|
-
export declare function TreeView({ children, name, title, icon, collapsed, active, onClick, onArrowClick, onActionsOpenToggle, hasArrow, actions, additionalNodeElements, level, }: TreeViewProps): JSX.Element;
|
|
25
|
+
export declare function TreeView({ children, name, title, icon, collapsed, active, onClick, onArrowClick, onActionsOpenToggle, hasArrow, actions, additionalNodeElements, level, itemRef, }: TreeViewProps): JSX.Element;
|
|
@@ -11,7 +11,7 @@ const cn_1 = require("../../utils/cn");
|
|
|
11
11
|
require("./TreeView.css");
|
|
12
12
|
const TREE_LEVEL_CSS_VAR = '--ydb-tree-view-level';
|
|
13
13
|
const b = (0, cn_1.block)('ydb-tree-view');
|
|
14
|
-
function TreeView({ children, name, title, icon, collapsed = true, active = false, onClick, onArrowClick, onActionsOpenToggle, hasArrow = false, actions, additionalNodeElements, level, }) {
|
|
14
|
+
function TreeView({ children, name, title, icon, collapsed = true, active = false, onClick, onArrowClick, onActionsOpenToggle, hasArrow = false, actions, additionalNodeElements, level, itemRef, }) {
|
|
15
15
|
const handleClick = react_1.default.useCallback((event) => {
|
|
16
16
|
if (!onClick)
|
|
17
17
|
return;
|
|
@@ -37,7 +37,7 @@ function TreeView({ children, name, title, icon, collapsed = true, active = fals
|
|
|
37
37
|
}
|
|
38
38
|
const tooltipContent = title !== null && title !== void 0 ? title : (typeof name === 'string' ? name : '');
|
|
39
39
|
const isTooltipDisabled = tooltipContent.trim().length === 0;
|
|
40
|
-
return ((0, jsx_runtime_1.jsx)("div", { className: b(), style: { [TREE_LEVEL_CSS_VAR]: level }, children: (0, jsx_runtime_1.jsxs)("div", { className: "tree-view", children: [(0, jsx_runtime_1.jsxs)("div", { className: `${itemClassName} ${b('item', { active })}`, onClick: handleClick, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: `${arrowClassName} ${b('arrow', {
|
|
40
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: b(), style: { [TREE_LEVEL_CSS_VAR]: level }, children: (0, jsx_runtime_1.jsxs)("div", { className: "tree-view", children: [(0, jsx_runtime_1.jsxs)("div", { ref: itemRef, className: `${itemClassName} ${b('item', { active })}`, onClick: handleClick, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: `${arrowClassName} ${b('arrow', {
|
|
41
41
|
collapsed,
|
|
42
42
|
hidden: !hasArrow,
|
|
43
43
|
})}`, disabled: !handleArrowClick, onClick: handleArrowClick }), (0, jsx_runtime_1.jsxs)("div", { className: b('content'), children: [(0, jsx_runtime_1.jsx)(uikit_1.ActionTooltip, { title: tooltipContent, disabled: isTooltipDisabled, children: (0, jsx_runtime_1.jsxs)("div", { className: b('label'), children: [icon && (0, jsx_runtime_1.jsx)("div", { className: b('icon'), children: icon }), (0, jsx_runtime_1.jsx)("div", { className: b('text'), children: name })] }) }), actions && actions.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: b('actions'), children: [additionalNodeElements, (0, jsx_runtime_1.jsx)(uikit_1.DropdownMenu, { onOpenToggle: onActionsOpenToggle, defaultSwitcherProps: {
|
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
import React from 'react';
|
|
2
2
|
import type { NavigationTreeProps } from './types';
|
|
3
3
|
export type { NavigationTreeProps };
|
|
4
|
-
export
|
|
4
|
+
export interface NavigationTreeHandle {
|
|
5
|
+
/**
|
|
6
|
+
* Scrolls the currently active node into view (if any).
|
|
7
|
+
* Has no effect if there is no active node or it is not present in the tree.
|
|
8
|
+
*/
|
|
9
|
+
scrollToActive: () => void;
|
|
10
|
+
}
|
|
11
|
+
declare function NavigationTreeInner<D = any, M = any>({ rootState: partialRootState, fetchPath, getActions, renderAdditionalNodeElements, activePath, onActionsOpenToggle, onActivePathUpdate, cache, virtualize, }: NavigationTreeProps<D, M>, ref: React.ForwardedRef<NavigationTreeHandle>): JSX.Element;
|
|
12
|
+
/**
|
|
13
|
+
* `React.forwardRef` loses generics, so we cast to preserve the `<D, M>` API.
|
|
14
|
+
*/
|
|
15
|
+
export declare const NavigationTree: <D = any, M = any>(props: NavigationTreeProps<D, M> & {
|
|
16
|
+
ref?: React.ForwardedRef<NavigationTreeHandle> | undefined;
|
|
17
|
+
}) => ReturnType<typeof NavigationTreeInner>;
|
|
@@ -5,7 +5,7 @@ import { EmptyView } from './EmptyView/EmptyView';
|
|
|
5
5
|
import { ErrorView } from './ErrorView/ErrorView';
|
|
6
6
|
import { LoaderView } from './LoaderView/LoaderView';
|
|
7
7
|
import { NavigationTreeNode } from './NavigationTreeNode';
|
|
8
|
-
import { getNodeState, reducer, selectTreeAsList } from './state';
|
|
8
|
+
import { NavigationTreeActionType, getNodeState, reducer, selectTreeAsList } from './state';
|
|
9
9
|
import { isServiceNode } from './utils';
|
|
10
10
|
const renderServiceNode = (node) => {
|
|
11
11
|
const key = `${node.path}|${node.status}`;
|
|
@@ -17,18 +17,81 @@ const renderServiceNode = (node) => {
|
|
|
17
17
|
}
|
|
18
18
|
return _jsx(EmptyView, { level: node.level }, key);
|
|
19
19
|
};
|
|
20
|
-
|
|
20
|
+
function NavigationTreeInner({ rootState: partialRootState, fetchPath, getActions, renderAdditionalNodeElements, activePath, onActionsOpenToggle, onActivePathUpdate, cache = true, virtualize = false, }, ref) {
|
|
21
21
|
const [state, dispatch] = React.useReducer(reducer, {
|
|
22
22
|
[partialRootState.path]: getNodeState(partialRootState),
|
|
23
23
|
});
|
|
24
24
|
const nodesList = React.useMemo(() => selectTreeAsList(state, partialRootState.path), [partialRootState.path, state]);
|
|
25
|
+
const nodesToLoad = React.useMemo(() => nodesList.filter((node) => !isServiceNode(node) &&
|
|
26
|
+
!node.collapsed &&
|
|
27
|
+
!node.loaded &&
|
|
28
|
+
!node.loading &&
|
|
29
|
+
!node.error), [nodesList]);
|
|
30
|
+
const reactListRef = React.useRef(null);
|
|
31
|
+
/**
|
|
32
|
+
* Monotonic counter of in-flight load requests. The current value is captured per
|
|
33
|
+
* dispatched `StartLoading` and forwarded to the resolving `FinishLoading` /
|
|
34
|
+
* `ErrorLoading`, so the reducer can drop responses that no longer match the latest
|
|
35
|
+
* request for a given path (see the stale-result guard in `reducer`).
|
|
36
|
+
*/
|
|
37
|
+
const requestIdRef = React.useRef(0);
|
|
38
|
+
const activeItemRef = React.useRef(null);
|
|
39
|
+
const activeNodeIndex = React.useMemo(() => nodesList.findIndex((node) => !isServiceNode(node) && node.path === activePath), [activePath, nodesList]);
|
|
40
|
+
const scrolledActivePathRef = React.useRef(undefined);
|
|
41
|
+
const scrollToActive = React.useCallback(() => {
|
|
42
|
+
var _a, _b;
|
|
43
|
+
if (activeNodeIndex < 0)
|
|
44
|
+
return;
|
|
45
|
+
if (virtualize) {
|
|
46
|
+
(_a = reactListRef.current) === null || _a === void 0 ? void 0 : _a.scrollAround(activeNodeIndex);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
(_b = activeItemRef.current) === null || _b === void 0 ? void 0 : _b.scrollIntoView({ block: 'nearest' });
|
|
50
|
+
}
|
|
51
|
+
}, [activeNodeIndex, virtualize]);
|
|
52
|
+
React.useImperativeHandle(ref, () => ({ scrollToActive }), [scrollToActive]);
|
|
53
|
+
React.useEffect(() => {
|
|
54
|
+
nodesToLoad.forEach((node) => {
|
|
55
|
+
const requestId = ++requestIdRef.current;
|
|
56
|
+
dispatch({
|
|
57
|
+
type: NavigationTreeActionType.StartLoading,
|
|
58
|
+
payload: { path: node.path, requestId },
|
|
59
|
+
});
|
|
60
|
+
fetchPath(node.path)
|
|
61
|
+
.then((data) => {
|
|
62
|
+
dispatch({
|
|
63
|
+
type: NavigationTreeActionType.FinishLoading,
|
|
64
|
+
payload: { path: node.path, activePath, data, requestId },
|
|
65
|
+
});
|
|
66
|
+
})
|
|
67
|
+
.catch((error) => {
|
|
68
|
+
dispatch({
|
|
69
|
+
type: NavigationTreeActionType.ErrorLoading,
|
|
70
|
+
payload: { path: node.path, error, requestId },
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}, [activePath, dispatch, fetchPath, nodesToLoad]);
|
|
75
|
+
React.useEffect(() => {
|
|
76
|
+
if (scrolledActivePathRef.current === activePath)
|
|
77
|
+
return;
|
|
78
|
+
if (activeNodeIndex < 0)
|
|
79
|
+
return;
|
|
80
|
+
scrollToActive();
|
|
81
|
+
scrolledActivePathRef.current = activePath;
|
|
82
|
+
}, [activeNodeIndex, activePath, scrollToActive]);
|
|
25
83
|
const renderNode = (node) => {
|
|
26
|
-
|
|
84
|
+
const isActive = node.path === activePath;
|
|
85
|
+
return (_jsx(NavigationTreeNode, { state: state, path: node.path, activePath: activePath, dispatch: dispatch, onActivate: onActivePathUpdate, getActions: getActions, onActionsOpenToggle: onActionsOpenToggle, renderAdditionalNodeElements: renderAdditionalNodeElements, cache: cache, level: node.level, activeItemRef: isActive ? activeItemRef : undefined }, node.path));
|
|
27
86
|
};
|
|
28
|
-
const renderVirtualizedTree = () => (_jsx(ReactList, { type: "uniform", length: nodesList.length, useStaticSize: true, itemRenderer: (index) => {
|
|
87
|
+
const renderVirtualizedTree = () => (_jsx(ReactList, { ref: reactListRef, type: "uniform", length: nodesList.length, useStaticSize: true, itemRenderer: (index) => {
|
|
29
88
|
const node = nodesList[index];
|
|
30
89
|
return isServiceNode(node) ? renderServiceNode(node) : renderNode(node);
|
|
31
90
|
} }));
|
|
32
91
|
const renderSimpleTree = () => (_jsx(React.Fragment, { children: nodesList.map((node) => isServiceNode(node) ? renderServiceNode(node) : renderNode(node)) }));
|
|
33
92
|
return virtualize ? renderVirtualizedTree() : renderSimpleTree();
|
|
34
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* `React.forwardRef` loses generics, so we cast to preserve the `<D, M>` API.
|
|
96
|
+
*/
|
|
97
|
+
export const NavigationTree = React.forwardRef(NavigationTreeInner);
|
|
@@ -3,7 +3,6 @@ import type { NavigationTreeAction } from './state';
|
|
|
3
3
|
import type { NavigationTreeProps, NavigationTreeState } from './types';
|
|
4
4
|
export interface NavigationTreeNodeProps {
|
|
5
5
|
path: string;
|
|
6
|
-
fetchPath: NavigationTreeProps['fetchPath'];
|
|
7
6
|
activePath?: string;
|
|
8
7
|
state: NavigationTreeState;
|
|
9
8
|
level?: number;
|
|
@@ -14,5 +13,10 @@ export interface NavigationTreeNodeProps {
|
|
|
14
13
|
onActionsOpenToggle?: NavigationTreeProps['onActionsOpenToggle'];
|
|
15
14
|
renderAdditionalNodeElements?: NavigationTreeProps['renderAdditionalNodeElements'];
|
|
16
15
|
cache?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Ref object attached to the item element when this node is the active one.
|
|
18
|
+
* Set only for the active node so the parent can scroll it into view.
|
|
19
|
+
*/
|
|
20
|
+
activeItemRef?: React.RefObject<HTMLDivElement>;
|
|
17
21
|
}
|
|
18
|
-
export declare function NavigationTreeNode({ path,
|
|
22
|
+
export declare function NavigationTreeNode({ path, activePath, state, level, dispatch, children, onActivate, getActions, onActionsOpenToggle, renderAdditionalNodeElements, cache, activeItemRef, }: NavigationTreeNodeProps): JSX.Element;
|
|
@@ -38,49 +38,16 @@ function renderIcon(type, collapsed) {
|
|
|
38
38
|
return null;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
export function NavigationTreeNode({ path,
|
|
41
|
+
export function NavigationTreeNode({ path, activePath, state, level, dispatch, children, onActivate, getActions, onActionsOpenToggle, renderAdditionalNodeElements, cache, activeItemRef, }) {
|
|
42
42
|
const nodeState = state[path];
|
|
43
43
|
React.useEffect(() => {
|
|
44
|
-
if (nodeState.collapsed) {
|
|
45
|
-
if (!cache) {
|
|
46
|
-
dispatch({
|
|
47
|
-
type: NavigationTreeActionType.ResetNode,
|
|
48
|
-
payload: { path },
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
if (nodeState.loaded || nodeState.loading || nodeState.error) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
dispatch({
|
|
57
|
-
type: NavigationTreeActionType.StartLoading,
|
|
58
|
-
payload: { path },
|
|
59
|
-
});
|
|
60
|
-
fetchPath(path)
|
|
61
|
-
.then((data) => {
|
|
44
|
+
if (nodeState.collapsed && !cache) {
|
|
62
45
|
dispatch({
|
|
63
|
-
type: NavigationTreeActionType.
|
|
64
|
-
payload: { path
|
|
46
|
+
type: NavigationTreeActionType.ResetNode,
|
|
47
|
+
payload: { path },
|
|
65
48
|
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
dispatch({
|
|
69
|
-
type: NavigationTreeActionType.ErrorLoading,
|
|
70
|
-
payload: { path, error },
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
}, [
|
|
74
|
-
activePath,
|
|
75
|
-
cache,
|
|
76
|
-
dispatch,
|
|
77
|
-
fetchPath,
|
|
78
|
-
nodeState.collapsed,
|
|
79
|
-
nodeState.loaded,
|
|
80
|
-
nodeState.loading,
|
|
81
|
-
nodeState.error,
|
|
82
|
-
path,
|
|
83
|
-
]);
|
|
49
|
+
}
|
|
50
|
+
}, [cache, dispatch, nodeState.collapsed, path]);
|
|
84
51
|
const handleClick = React.useCallback(() => {
|
|
85
52
|
if (onActivate) {
|
|
86
53
|
onActivate(path);
|
|
@@ -102,5 +69,5 @@ export function NavigationTreeNode({ path, fetchPath, activePath, state, level,
|
|
|
102
69
|
isOpen,
|
|
103
70
|
});
|
|
104
71
|
}, [nodeState.path, nodeState.type, onActionsOpenToggle]);
|
|
105
|
-
return (_jsx(TreeView, { name: nodeState.name, icon: renderIcon(nodeState.type, nodeState.collapsed), collapsed: nodeState.collapsed, active: nodeState.path === activePath, actions: actions, additionalNodeElements: additionalNodeElements, hasArrow: nodeState.expandable, onClick: handleClick, onArrowClick: handleArrowClick, onActionsOpenToggle: handleActionsOpenToggle, level: level, children: children }));
|
|
72
|
+
return (_jsx(TreeView, { name: nodeState.name, icon: renderIcon(nodeState.type, nodeState.collapsed), collapsed: nodeState.collapsed, active: nodeState.path === activePath, actions: actions, additionalNodeElements: additionalNodeElements, hasArrow: nodeState.expandable, onClick: handleClick, onArrowClick: handleArrowClick, onActionsOpenToggle: handleActionsOpenToggle, level: level, itemRef: activeItemRef, children: children }));
|
|
106
73
|
}
|
|
@@ -15,6 +15,8 @@ export type NavigationTreeAction = {
|
|
|
15
15
|
type: NavigationTreeActionType.StartLoading;
|
|
16
16
|
payload: {
|
|
17
17
|
path: string;
|
|
18
|
+
/** Monotonic id captured at dispatch time; written into node state. */
|
|
19
|
+
requestId: number;
|
|
18
20
|
};
|
|
19
21
|
} | {
|
|
20
22
|
type: NavigationTreeActionType.FinishLoading;
|
|
@@ -22,12 +24,16 @@ export type NavigationTreeAction = {
|
|
|
22
24
|
path: string;
|
|
23
25
|
activePath?: string;
|
|
24
26
|
data: NavigationTreeDataItem[];
|
|
27
|
+
/** Id of the `StartLoading` this result belongs to; stale ones are dropped. */
|
|
28
|
+
requestId: number;
|
|
25
29
|
};
|
|
26
30
|
} | {
|
|
27
31
|
type: NavigationTreeActionType.ErrorLoading;
|
|
28
32
|
payload: {
|
|
29
33
|
path: string;
|
|
30
34
|
error: unknown;
|
|
35
|
+
/** Id of the `StartLoading` this error belongs to; stale ones are dropped. */
|
|
36
|
+
requestId: number;
|
|
31
37
|
};
|
|
32
38
|
} | {
|
|
33
39
|
type: NavigationTreeActionType.ResetNode;
|
|
@@ -41,6 +47,7 @@ export declare function getDefaultNodeState(): {
|
|
|
41
47
|
loaded: boolean;
|
|
42
48
|
error: boolean;
|
|
43
49
|
children: never[];
|
|
50
|
+
requestId: number;
|
|
44
51
|
};
|
|
45
52
|
export declare function getNodeState(partialState: NavigationTreeNodePartialState): NavigationTreeNodeState;
|
|
46
53
|
export declare function reducer(state: NavigationTreeState | undefined, action: NavigationTreeAction): NavigationTreeState;
|
|
@@ -14,6 +14,7 @@ export function getDefaultNodeState() {
|
|
|
14
14
|
loaded: false,
|
|
15
15
|
error: false,
|
|
16
16
|
children: [],
|
|
17
|
+
requestId: 0,
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
export function getNodeState(partialState) {
|
|
@@ -25,9 +26,27 @@ export function reducer(state = {}, action) {
|
|
|
25
26
|
case NavigationTreeActionType.ToggleCollapsed:
|
|
26
27
|
return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, state[action.payload.path]), { collapsed: !state[action.payload.path].collapsed }) });
|
|
27
28
|
case NavigationTreeActionType.StartLoading:
|
|
28
|
-
return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, state[action.payload.path]), { loading: true, loaded: false, error: false, children: [] }) });
|
|
29
|
+
return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, state[action.payload.path]), { loading: true, loaded: false, error: false, children: [], requestId: action.payload.requestId }) });
|
|
29
30
|
case NavigationTreeActionType.FinishLoading: {
|
|
30
|
-
const
|
|
31
|
+
const currentNode = state[action.payload.path];
|
|
32
|
+
// Ignore stale results. A result is stale when:
|
|
33
|
+
// * the node was removed entirely, OR
|
|
34
|
+
// * the node's `requestId` no longer matches the one captured at dispatch time
|
|
35
|
+
// (i.e. `ResetNode` cleared it, or a newer `StartLoading` superseded it).
|
|
36
|
+
// The previous `loading`-only guard couldn't distinguish "still loading the original
|
|
37
|
+
// request" from "loading a second, newer request", which allowed an older response
|
|
38
|
+
// to overwrite a newer one after a collapse-expand cycle with `cache: false`.
|
|
39
|
+
// Note: a plain collapse with `cache: true` keeps `requestId` intact, so the fetch
|
|
40
|
+
// is still allowed to complete and populate the cache — that's intentional.
|
|
41
|
+
if (!currentNode || currentNode.requestId !== action.payload.requestId) {
|
|
42
|
+
return state;
|
|
43
|
+
}
|
|
44
|
+
const newState = Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, currentNode), { loading: false, loaded: Boolean(action.payload.data), error: false,
|
|
45
|
+
// Clear the in-flight marker now that this request has completed.
|
|
46
|
+
// The counter in `NavigationTree` only allocates ids >= 1, so any
|
|
47
|
+
// late stale `FinishLoading` / `ErrorLoading` for this path will
|
|
48
|
+
// still fail the guard above (its id > 0 won't match this 0).
|
|
49
|
+
requestId: 0 }) });
|
|
31
50
|
if (action.payload.data) {
|
|
32
51
|
newState[action.payload.path].children = action.payload.data.map(({ name }) => name);
|
|
33
52
|
for (const item of action.payload.data) {
|
|
@@ -45,8 +64,17 @@ export function reducer(state = {}, action) {
|
|
|
45
64
|
}
|
|
46
65
|
return newState;
|
|
47
66
|
}
|
|
48
|
-
case NavigationTreeActionType.ErrorLoading:
|
|
49
|
-
|
|
67
|
+
case NavigationTreeActionType.ErrorLoading: {
|
|
68
|
+
const currentNode = state[action.payload.path];
|
|
69
|
+
// Ignore stale errors — see the matching note on `FinishLoading` above.
|
|
70
|
+
if (!currentNode || currentNode.requestId !== action.payload.requestId) {
|
|
71
|
+
return state;
|
|
72
|
+
}
|
|
73
|
+
return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, currentNode), { loading: false, loaded: false, error: true,
|
|
74
|
+
// See note in `FinishLoading`: clear the in-flight id on completion
|
|
75
|
+
// to keep `requestId === 0` ⇔ "no active request".
|
|
76
|
+
requestId: 0 }) });
|
|
77
|
+
}
|
|
50
78
|
case NavigationTreeActionType.ResetNode:
|
|
51
79
|
return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, state[action.payload.path]), getDefaultNodeState()) });
|
|
52
80
|
default:
|
|
@@ -24,13 +24,22 @@ export interface NavigationTreeNodeState {
|
|
|
24
24
|
children: string[];
|
|
25
25
|
level?: number;
|
|
26
26
|
meta?: unknown;
|
|
27
|
+
/**
|
|
28
|
+
* Id of the in-flight load request for this node. Assigned by `StartLoading`,
|
|
29
|
+
* cleared back to `0` on `FinishLoading` / `ErrorLoading` / `ResetNode`, so
|
|
30
|
+
* `requestId === 0` always means "no active request". Used to discard stale
|
|
31
|
+
* results: a `FinishLoading` / `ErrorLoading` whose payload id does not match
|
|
32
|
+
* the current value is dropped (so a late response from a request superseded
|
|
33
|
+
* by `ResetNode` + a new `StartLoading` cannot overwrite the newer one).
|
|
34
|
+
*/
|
|
35
|
+
requestId: number;
|
|
27
36
|
}
|
|
28
37
|
export interface NavigationTreeServiceNode {
|
|
29
38
|
path: string;
|
|
30
39
|
status: 'loading' | 'error' | 'empty';
|
|
31
40
|
level?: number;
|
|
32
41
|
}
|
|
33
|
-
export type NavigationTreeNodePartialState = Omit<NavigationTreeNodeState, 'loading' | 'loaded' | 'error' | 'children'>;
|
|
42
|
+
export type NavigationTreeNodePartialState = Omit<NavigationTreeNodeState, 'loading' | 'loaded' | 'error' | 'children' | 'requestId'>;
|
|
34
43
|
export interface NavigationTreeProps<D = any, M = any> {
|
|
35
44
|
rootState: NavigationTreeNodePartialState;
|
|
36
45
|
fetchPath: (path: string) => Promise<NavigationTreeDataItem<M>[]>;
|
|
@@ -15,5 +15,11 @@ export interface TreeViewProps {
|
|
|
15
15
|
actions?: DropdownMenuItemMixed<any>[];
|
|
16
16
|
additionalNodeElements?: JSX.Element;
|
|
17
17
|
level?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Ref object passed to the inner item element.
|
|
20
|
+
* Use this if you need to control scroll-into-view from a parent component
|
|
21
|
+
* (e.g. `itemRef.current?.scrollIntoView({block: 'nearest'})`).
|
|
22
|
+
*/
|
|
23
|
+
itemRef?: React.Ref<HTMLDivElement>;
|
|
18
24
|
}
|
|
19
|
-
export declare function TreeView({ children, name, title, icon, collapsed, active, onClick, onArrowClick, onActionsOpenToggle, hasArrow, actions, additionalNodeElements, level, }: TreeViewProps): JSX.Element;
|
|
25
|
+
export declare function TreeView({ children, name, title, icon, collapsed, active, onClick, onArrowClick, onActionsOpenToggle, hasArrow, actions, additionalNodeElements, level, itemRef, }: TreeViewProps): JSX.Element;
|
|
@@ -5,7 +5,7 @@ import { block } from '../../utils/cn';
|
|
|
5
5
|
import './TreeView.css';
|
|
6
6
|
const TREE_LEVEL_CSS_VAR = '--ydb-tree-view-level';
|
|
7
7
|
const b = block('ydb-tree-view');
|
|
8
|
-
export function TreeView({ children, name, title, icon, collapsed = true, active = false, onClick, onArrowClick, onActionsOpenToggle, hasArrow = false, actions, additionalNodeElements, level, }) {
|
|
8
|
+
export function TreeView({ children, name, title, icon, collapsed = true, active = false, onClick, onArrowClick, onActionsOpenToggle, hasArrow = false, actions, additionalNodeElements, level, itemRef, }) {
|
|
9
9
|
const handleClick = React.useCallback((event) => {
|
|
10
10
|
if (!onClick)
|
|
11
11
|
return;
|
|
@@ -31,7 +31,7 @@ export function TreeView({ children, name, title, icon, collapsed = true, active
|
|
|
31
31
|
}
|
|
32
32
|
const tooltipContent = title !== null && title !== void 0 ? title : (typeof name === 'string' ? name : '');
|
|
33
33
|
const isTooltipDisabled = tooltipContent.trim().length === 0;
|
|
34
|
-
return (_jsx("div", { className: b(), style: { [TREE_LEVEL_CSS_VAR]: level }, children: _jsxs("div", { className: "tree-view", children: [_jsxs("div", { className: `${itemClassName} ${b('item', { active })}`, onClick: handleClick, children: [_jsx("button", { type: "button", className: `${arrowClassName} ${b('arrow', {
|
|
34
|
+
return (_jsx("div", { className: b(), style: { [TREE_LEVEL_CSS_VAR]: level }, children: _jsxs("div", { className: "tree-view", children: [_jsxs("div", { ref: itemRef, className: `${itemClassName} ${b('item', { active })}`, onClick: handleClick, children: [_jsx("button", { type: "button", className: `${arrowClassName} ${b('arrow', {
|
|
35
35
|
collapsed,
|
|
36
36
|
hidden: !hasArrow,
|
|
37
37
|
})}`, disabled: !handleArrowClick, onClick: handleArrowClick }), _jsxs("div", { className: b('content'), children: [_jsx(ActionTooltip, { title: tooltipContent, disabled: isTooltipDisabled, children: _jsxs("div", { className: b('label'), children: [icon && _jsx("div", { className: b('icon'), children: icon }), _jsx("div", { className: b('text'), children: name })] }) }), actions && actions.length > 0 && (_jsxs("div", { className: b('actions'), children: [additionalNodeElements, _jsx(DropdownMenu, { onOpenToggle: onActionsOpenToggle, defaultSwitcherProps: {
|