@itwin/presentation-hierarchies-react
Version:
React components based on `@itwin/presentation-hierarchies`
248 lines • 10.2 kB
JavaScript
;
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.useTree = useTree;
exports.useUnifiedSelectionTree = useUnifiedSelectionTree;
const react_1 = require("react");
const presentation_hierarchies_1 = require("@itwin/presentation-hierarchies");
const TreeActions_js_1 = require("./internal/TreeActions.js");
const TreeModel_js_1 = require("./internal/TreeModel.js");
const UseUnifiedSelection_js_1 = require("./internal/UseUnifiedSelection.js");
const Utils_js_1 = require("./internal/Utils.js");
/**
* A React hook that creates state for a tree component.
*
* The hook uses `@itwin/presentation-hierarchies` package to load the hierarchy data and returns a
* component-agnostic result which may be used to render the hierarchy using any UI framework.
*
* See `README.md` for an example
*
* @see `useUnifiedSelectionTree`
* @see `useIModelTree`
* @public
*/
function useTree(props) {
const { getTreeModelNode: _, ...rest } = useTreeInternal(props);
return rest;
}
/**
* A React hook that creates state for a tree component, that is integrated with unified selection
* through the given selection storage (previously the storage was provided through the, now
* deprecated, `UnifiedSelectionProvider`).
*
* The hook uses `@itwin/presentation-hierarchies` package to load the hierarchy data and returns a
* component-agnostic result which may be used to render the hierarchy using any UI framework.
*
* See `README.md` for an example
*
* @see `useTree`
* @see `useIModelUnifiedSelectionTree`
* @see `UnifiedSelectionProvider`
* @public
*/
function useUnifiedSelectionTree({ sourceName, selectionStorage, ...props }) {
const { getTreeModelNode, ...rest } = useTreeInternal(props);
return {
...rest,
...(0, UseUnifiedSelection_js_1.useUnifiedTreeSelection)({ sourceName, selectionStorage, getTreeModelNode }),
};
}
function useTreeInternal({ getHierarchyProvider, getFilteredPaths, onPerformanceMeasured, onHierarchyLimitExceeded, onHierarchyLoadError, }) {
const [state, setState] = (0, react_1.useState)({
model: { idToNode: new Map(), parentChildMap: new Map(), rootNode: { id: undefined, nodeData: undefined } },
rootNodes: undefined,
});
const onPerformanceMeasuredRef = useLatest(onPerformanceMeasured);
const onHierarchyLimitExceededRef = useLatest(onHierarchyLimitExceeded);
const onHierarchyLoadErrorRef = useLatest(onHierarchyLoadError);
const [actions] = (0, react_1.useState)(() => new TreeActions_js_1.TreeActions((model) => {
const rootNodes = model.parentChildMap.get(undefined) !== undefined ? generateTreeStructure(undefined, model) : undefined;
setState({
model,
rootNodes,
});
}, (actionType, duration) => onPerformanceMeasuredRef.current?.(actionType, duration), (props) => onHierarchyLimitExceededRef.current?.(props), (props) => onHierarchyLoadErrorRef.current?.(props)));
const currentFormatter = (0, react_1.useRef)();
const [hierarchyProvider, setHierarchyProvider] = (0, react_1.useState)();
(0, react_1.useEffect)(() => {
const provider = getHierarchyProvider();
provider.setFormatter(currentFormatter.current);
const removeHierarchyChangedListener = provider.hierarchyChanged.addListener((hierarchyChangeArgs) => {
const shouldDiscardState = hierarchyChangeArgs?.filterChange?.newFilter !== undefined;
actions.reloadTree({ state: shouldDiscardState ? "discard" : "keep" });
});
actions.setHierarchyProvider(provider);
setHierarchyProvider(provider);
return () => {
removeHierarchyChangedListener();
actions.reset();
(0, Utils_js_1.safeDispose)(provider);
};
}, [actions, getHierarchyProvider]);
const [isFiltering, setIsFiltering] = (0, react_1.useState)(false);
(0, react_1.useEffect)(() => {
let disposed = false;
const controller = new AbortController();
void (async () => {
if (!hierarchyProvider) {
return;
}
if (!getFilteredPaths) {
hierarchyProvider.setHierarchyFilter(undefined);
// reload tree in case hierarchy provider does not use hierarchy filter to load initial nodes
actions.reloadTree({ state: "keep" });
setIsFiltering(false);
return;
}
setIsFiltering(true);
let paths;
try {
paths = await getFilteredPaths({ abortSignal: controller.signal });
}
catch {
}
finally {
if (!disposed) {
hierarchyProvider.setHierarchyFilter(paths ? { paths } : undefined);
setIsFiltering(false);
}
}
})();
return () => {
controller.abort();
disposed = true;
};
}, [hierarchyProvider, getFilteredPaths, actions]);
const getTreeModelNode = (0, react_1.useCallback)((nodeId) => {
return actions.getNode(nodeId);
}, [actions]);
const getNode = (0, react_1.useCallback)((nodeId) => {
const node = actions.getNode(nodeId);
if (!node || !(0, TreeModel_js_1.isTreeModelHierarchyNode)(node)) {
return undefined;
}
return createPresentationHierarchyNode(node, state.model);
}, [actions, state.model]);
const expandNode = (0, react_1.useCallback)((nodeId, isExpanded) => {
actions.expandNode(nodeId, isExpanded);
}, [actions]);
const reloadTree = (0, react_1.useCallback)((options) => {
actions.reloadTree(options);
}, [actions]);
const selectNodes = (0, react_1.useCallback)((nodeIds, changeType) => {
actions.selectNodes(nodeIds, changeType);
}, [actions]);
const isNodeSelected = (0, react_1.useCallback)((nodeId) => TreeModel_js_1.TreeModel.isNodeSelected(state.model, nodeId), [state]);
const setFormatter = (0, react_1.useCallback)((formatter) => {
currentFormatter.current = formatter;
/* c8 ignore next 3 */
if (!hierarchyProvider) {
return;
}
hierarchyProvider.setFormatter(formatter);
}, [hierarchyProvider]);
const getHierarchyLevelDetails = (0, react_1.useCallback)((nodeId) => {
const node = actions.getNode(nodeId);
if (!hierarchyProvider || !node || (0, TreeModel_js_1.isTreeModelInfoNode)(node)) {
return undefined;
}
const hierarchyNode = node.nodeData;
if (hierarchyNode && presentation_hierarchies_1.HierarchyNode.isGroupingNode(hierarchyNode)) {
return undefined;
}
return {
hierarchyNode,
getInstanceKeysIterator: (props) => hierarchyProvider.getNodeInstanceKeys({
parentNode: hierarchyNode,
instanceFilter: props?.instanceFilter,
hierarchyLevelSizeLimit: props?.hierarchyLevelSizeLimit,
}),
instanceFilter: node.instanceFilter,
setInstanceFilter: (filter) => actions.setInstanceFilter(nodeId, filter),
sizeLimit: node.hierarchyLimit,
setSizeLimit: (value) => actions.setHierarchyLimit(nodeId, value),
};
}, [actions, hierarchyProvider]);
return {
rootNodes: state.rootNodes,
isLoading: !!state.model.rootNode.isLoading || isFiltering,
expandNode,
reloadTree,
selectNodes,
isNodeSelected,
getTreeModelNode,
getNode,
getHierarchyLevelDetails,
setFormatter,
};
}
function generateTreeStructure(parentNodeId, model) {
const currentChildren = model.parentChildMap.get(parentNodeId);
if (!currentChildren) {
return undefined;
}
return currentChildren
.map((childId) => model.idToNode.get(childId))
.filter((node) => !!node)
.map((node) => {
if ((0, TreeModel_js_1.isTreeModelHierarchyNode)(node)) {
return createPresentationHierarchyNode(node, model);
}
if (node.type === "ResultSetTooLarge") {
return {
id: node.id,
parentNodeId,
type: node.type,
resultSetSizeLimit: node.resultSetSizeLimit,
};
}
if (node.type === "NoFilterMatches") {
return {
id: node.id,
parentNodeId,
type: node.type,
};
}
return {
id: node.id,
parentNodeId,
type: node.type,
message: node.message,
};
});
}
function createPresentationHierarchyNode(modelNode, model) {
let children;
return {
...toPresentationHierarchyNodeBase(modelNode),
get children() {
if (!children) {
children = generateTreeStructure(modelNode.id, model);
}
return children ? children : modelNode.children === true ? true : [];
},
};
}
function toPresentationHierarchyNodeBase(node) {
return {
id: node.id,
label: node.label,
nodeData: node.nodeData,
isLoading: !!node.isLoading,
isExpanded: !!node.isExpanded,
isFilterable: !presentation_hierarchies_1.HierarchyNode.isGroupingNode(node.nodeData) && !!node.nodeData.supportsFiltering && node.children,
isFiltered: !!node.instanceFilter,
extendedData: node.nodeData.extendedData,
};
}
function useLatest(value) {
const ref = (0, react_1.useRef)(value);
(0, react_1.useEffect)(() => {
ref.current = value;
}, [value]);
return ref;
}
//# sourceMappingURL=UseTree.js.map