UNPKG

@itwin/presentation-hierarchies-react

Version:

React components based on `@itwin/presentation-hierarchies`

244 lines 9.67 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { useCallback, useEffect, useRef, useState } from "react"; import { HierarchyNode } from "@itwin/presentation-hierarchies"; import { TreeActions } from "./internal/TreeActions.js"; import { isTreeModelHierarchyNode, isTreeModelInfoNode, TreeModel } from "./internal/TreeModel.js"; import { useUnifiedTreeSelection } from "./internal/UseUnifiedSelection.js"; import { safeDispose } from "./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 */ export 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 */ export function useUnifiedSelectionTree({ sourceName, selectionStorage, ...props }) { const { getTreeModelNode, ...rest } = useTreeInternal(props); return { ...rest, ...useUnifiedTreeSelection({ sourceName, selectionStorage, getTreeModelNode }), }; } function useTreeInternal({ getHierarchyProvider, getFilteredPaths, onPerformanceMeasured, onHierarchyLimitExceeded, onHierarchyLoadError, }) { const [state, setState] = 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] = useState(() => new 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 = useRef(); const [hierarchyProvider, setHierarchyProvider] = useState(); 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(); safeDispose(provider); }; }, [actions, getHierarchyProvider]); const [isFiltering, setIsFiltering] = useState(false); 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 = useCallback((nodeId) => { return actions.getNode(nodeId); }, [actions]); const getNode = useCallback((nodeId) => { const node = actions.getNode(nodeId); if (!node || !isTreeModelHierarchyNode(node)) { return undefined; } return createPresentationHierarchyNode(node, state.model); }, [actions, state.model]); const expandNode = useCallback((nodeId, isExpanded) => { actions.expandNode(nodeId, isExpanded); }, [actions]); const reloadTree = useCallback((options) => { actions.reloadTree(options); }, [actions]); const selectNodes = useCallback((nodeIds, changeType) => { actions.selectNodes(nodeIds, changeType); }, [actions]); const isNodeSelected = useCallback((nodeId) => TreeModel.isNodeSelected(state.model, nodeId), [state]); const setFormatter = useCallback((formatter) => { currentFormatter.current = formatter; /* c8 ignore next 3 */ if (!hierarchyProvider) { return; } hierarchyProvider.setFormatter(formatter); }, [hierarchyProvider]); const getHierarchyLevelDetails = useCallback((nodeId) => { const node = actions.getNode(nodeId); if (!hierarchyProvider || !node || isTreeModelInfoNode(node)) { return undefined; } const hierarchyNode = node.nodeData; if (hierarchyNode && 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 (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: !HierarchyNode.isGroupingNode(node.nodeData) && !!node.nodeData.supportsFiltering && node.children, isFiltered: !!node.instanceFilter, extendedData: node.nodeData.extendedData, }; } function useLatest(value) { const ref = useRef(value); useEffect(() => { ref.current = value; }, [value]); return ref; } //# sourceMappingURL=UseTree.js.map