Skip to content

Commit

Permalink
Tree widget: Add selectionPredicate to models tree building blocks (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
grigasp authored Dec 18, 2024
1 parent 5005c67 commit db4ec4a
Show file tree
Hide file tree
Showing 11 changed files with 173 additions and 39 deletions.
29 changes: 19 additions & 10 deletions apps/test-viewer/src/UiProvidersConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,21 @@ const configuredUiItems = new Map<string, UiItem>([
{
id: ModelsTreeComponent.id,
getLabel: () => ModelsTreeComponent.getLabel(),
render: (props) => (
<ModelsTreeComponent
getSchemaContext={getSchemaContext}
density={props.density}
selectionStorage={unifiedSelectionStorage}
selectionMode={"extended"}
onPerformanceMeasured={props.onPerformanceMeasured}
onFeatureUsed={props.onFeatureUsed}
/>
),
render: (props) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { disableNodesSelection } = useViewerOptionsContext();
return (
<ModelsTreeComponent
getSchemaContext={getSchemaContext}
density={props.density}
selectionStorage={unifiedSelectionStorage}
selectionMode={"extended"}
selectionPredicate={disableNodesSelection ? disabledSelectionPredicate : undefined}
onPerformanceMeasured={props.onPerformanceMeasured}
onFeatureUsed={props.onFeatureUsed}
/>
);
},
},
{
id: CategoriesTreeComponent.id,
Expand Down Expand Up @@ -309,3 +314,7 @@ function TreeWidgetWithOptions(props: { trees: SelectableTreeDefinition[] }) {
/>
);
}

function disabledSelectionPredicate() {
return false;
}
38 changes: 25 additions & 13 deletions apps/test-viewer/src/components/ViewerOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,31 @@

import { createContext, useContext, useState } from "react";
import { StatusBarSection } from "@itwin/appui-react";
import { SvgVisibilityShow } from "@itwin/itwinui-icons-react";
import { SvgSelection, SvgVisibilityShow } from "@itwin/itwinui-icons-react";
import { IconButton } from "@itwin/itwinui-react";

import type { PropsWithChildren } from "react";
import type { UiItemsProvider } from "@itwin/appui-react";

export interface ViewerOptionsContext {
density: "default" | "enlarged";
disableNodesSelection: boolean;
}

export interface ViewerActionsContext {
setDensity: React.Dispatch<React.SetStateAction<"default" | "enlarged">>;
setDisableNodesSelection: React.Dispatch<React.SetStateAction<boolean>>;
}

const viewerOptionsContext = createContext<ViewerOptionsContext>({} as ViewerOptionsContext);
const viewerActionsContext = createContext<ViewerActionsContext>({} as ViewerActionsContext);

export function ViewerOptionsProvider(props: PropsWithChildren<unknown>) {
const [density, setDensity] = useState<"default" | "enlarged">("default");

const [disableNodesSelection, setDisableNodesSelection] = useState<boolean>(false);
return (
<viewerActionsContext.Provider value={{ setDensity }}>
<viewerOptionsContext.Provider value={{ density }}>{props.children}</viewerOptionsContext.Provider>
<viewerActionsContext.Provider value={{ setDensity, setDisableNodesSelection }}>
<viewerOptionsContext.Provider value={{ density, disableNodesSelection }}>{props.children}</viewerOptionsContext.Provider>
</viewerActionsContext.Provider>
);
}
Expand All @@ -44,24 +46,34 @@ export const statusBarActionsProvider: UiItemsProvider = {
id: "ViewerOptionsUiItemsProvider",
getStatusBarItems: () => [
{
id: `expandedLayoutButton`,
content: <ToggleLayoutButton />,
id: `toggleExpandedLayoutButton`,
content: <ToggleExpandedLayoutButton />,
itemPriority: 1,
section: StatusBarSection.Left,
},
{
id: `toggleTreeNodesSelectionButton`,
content: <ToggleTreeNodesSelectionButton />,
itemPriority: 2,
section: StatusBarSection.Left,
},
],
};

function ToggleLayoutButton() {
function ToggleExpandedLayoutButton() {
const { setDensity } = useViewerActionsContext();
return (
<IconButton
// aria-label="Toggle expanded layout"
label="Toggle expanded layout"
styleType="borderless"
onClick={() => setDensity((prev) => (prev === "default" ? "enlarged" : "default"))}
>
<IconButton label="Toggle expanded layout" styleType="borderless" onClick={() => setDensity((prev) => (prev === "default" ? "enlarged" : "default"))}>
<SvgVisibilityShow />
</IconButton>
);
}

function ToggleTreeNodesSelectionButton() {
const { setDisableNodesSelection } = useViewerActionsContext();
return (
<IconButton label="Toggle tree nodes' selection" styleType="borderless" onClick={() => setDisableNodesSelection((prev) => !prev)}>
<SvgSelection />
</IconButton>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Added an optional `selectionPredicate` function prop to `ModelsTreeComponent`, `ModelsTree`, `useModelsTree` and `Tree` components. When provided, it allows consumers to conditionally enable/disable selection of tree nodes.",
"packageName": "@itwin/tree-widget-react",
"email": "[email protected]",
"dependentChangeType": "patch"
}
11 changes: 8 additions & 3 deletions packages/itwin/tree-widget/api/tree-widget-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ export const ModelsTreeComponent: {
};

// @public (undocumented)
interface ModelsTreeComponentProps extends Pick<ModelsTreeProps, "getSchemaContext" | "selectionStorage" | "density" | "hierarchyLevelConfig" | "selectionMode" | "hierarchyConfig" | "visibilityHandlerOverrides" | "getFilteredPaths"> {
interface ModelsTreeComponentProps extends Pick<ModelsTreeProps, "getSchemaContext" | "selectionStorage" | "density" | "hierarchyLevelConfig" | "selectionMode" | "selectionPredicate" | "hierarchyConfig" | "visibilityHandlerOverrides" | "getFilteredPaths"> {
headerButtons?: Array<(props: ModelsTreeHeaderButtonProps) => React.ReactNode>;
// (undocumented)
onFeatureUsed?: (feature: string) => void;
Expand Down Expand Up @@ -403,6 +403,7 @@ type TreeProps = Pick<FunctionProps<typeof useIModelTree>, "getFilteredPaths" |
getSchemaContext: (imodel: IModelConnection) => SchemaContext;
treeName: string;
selectionStorage: SelectionStorage;
selectionPredicate?: (node: PresentationHierarchyNode) => boolean;
treeRenderer: (treeProps: Required<Pick<TreeRendererProps, "rootNodes" | "expandNode" | "onNodeClick" | "onNodeKeyDown" | "onFilterClick" | "isNodeSelected" | "getHierarchyLevelDetails" | "size" | "getLabel">>) => ReactNode;
imodelAccess?: FunctionProps<typeof useIModelTree>["imodelAccess"];
hierarchyLevelSizeLimit?: number;
Expand Down Expand Up @@ -485,7 +486,7 @@ interface UseCategoriesTreeResult {
}

// @beta
export function useModelsTree({ activeView, filter, hierarchyConfig, visibilityHandlerOverrides, getFilteredPaths, onModelsFiltered, }: UseModelsTreeProps): UseModelsTreeResult;
export function useModelsTree({ activeView, filter, hierarchyConfig, visibilityHandlerOverrides, getFilteredPaths, onModelsFiltered, selectionPredicate: nodeTypeSelectionPredicate, }: UseModelsTreeProps): UseModelsTreeResult;

// @public
export function useModelsTreeButtonProps({ imodel, viewport }: {
Expand Down Expand Up @@ -514,14 +515,18 @@ interface UseModelsTreeProps {
hierarchyConfig?: Partial<ModelsTreeHierarchyConfiguration>;
// (undocumented)
onModelsFiltered?: (modelIds: Id64String[] | undefined) => void;
selectionPredicate?: (props: {
node: PresentationHierarchyNode;
type: "subject" | "model" | "category" | "element" | "elements-class-group";
}) => boolean;
// (undocumented)
visibilityHandlerOverrides?: ModelsTreeVisibilityHandlerOverrides;
}

// @beta (undocumented)
interface UseModelsTreeResult {
// (undocumented)
modelsTreeProps: Pick<VisibilityTreeProps, "treeName" | "getHierarchyDefinition" | "getFilteredPaths" | "visibilityHandlerFactory" | "highlight" | "noDataMessage">;
modelsTreeProps: Pick<VisibilityTreeProps, "treeName" | "getHierarchyDefinition" | "getFilteredPaths" | "visibilityHandlerFactory" | "highlight" | "noDataMessage" | "selectionPredicate">;
// (undocumented)
rendererProps: Required<Pick<VisibilityTreeRendererProps, "getIcon" | "onNodeDoubleClick">>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import type { FunctionProps } from "../Utils";
import type { ReactNode } from "react";
import type { IModelConnection } from "@itwin/core-frontend";
import type { SchemaContext } from "@itwin/ecschema-metadata";
import type { SelectionStorage, useIModelTree } from "@itwin/presentation-hierarchies-react";
import type { PresentationHierarchyNode, SelectionStorage, useIModelTree } from "@itwin/presentation-hierarchies-react";
import type { HighlightInfo } from "../UseNodeHighlighting";
import type { TreeRendererProps } from "./TreeRenderer";

Expand All @@ -38,6 +38,11 @@ export type TreeProps = Pick<FunctionProps<typeof useIModelTree>, "getFilteredPa
treeName: string;
/** Unified selection storage that should be used by tree to handle tree selection changes. */
selectionStorage: SelectionStorage;
/**
* An optional predicate to allow or prohibit selection of a node.
* When not supplied, all nodes are selectable.
*/
selectionPredicate?: (node: PresentationHierarchyNode) => boolean;
/** Tree renderer that should be used to render tree data. */
treeRenderer: (
treeProps: Required<
Expand Down Expand Up @@ -89,6 +94,7 @@ function TreeImpl({
getFilteredPaths,
defaultHierarchyLevelSizeLimit,
getHierarchyDefinition,
selectionPredicate,
selectionMode,
onReload,
treeRenderer,
Expand All @@ -100,8 +106,9 @@ function TreeImpl({
const [imodelChanged] = useState(new BeEvent<() => void>());
const {
rootNodes,
getNode,
isLoading,
selectNodes,
selectNodes: selectNodesAction,
setFormatter: _setFormatter,
expandNode,
...treeProps
Expand All @@ -123,8 +130,12 @@ function TreeImpl({
});
useIModelChangeListener({ imodel, action: useCallback(() => imodelChanged.raiseEvent(), [imodelChanged]) });

const reportingSelectNodes = useReportingAction({ action: selectNodes });
const { onNodeClick, onNodeKeyDown } = useSelectionHandler({ rootNodes, selectNodes: reportingSelectNodes, selectionMode: selectionMode ?? "single" });
const selectNodes = useSelectionPredicate({
action: useReportingAction({ action: selectNodesAction }),
predicate: selectionPredicate,
getNode,
});
const { onNodeClick, onNodeKeyDown } = useSelectionHandler({ rootNodes, selectNodes, selectionMode: selectionMode ?? "single" });
const { filteringDialog, onFilterClick } = useHierarchyLevelFiltering({
imodel,
defaultHierarchyLevelSizeLimit,
Expand Down Expand Up @@ -174,3 +185,25 @@ function TreeImpl({
</div>
);
}

function useSelectionPredicate({
action,
predicate,
getNode,
}: {
action: (...args: any[]) => void;
predicate?: (node: PresentationHierarchyNode) => boolean;
getNode: (nodeId: string) => PresentationHierarchyNode | undefined;
}): ReturnType<typeof useIModelUnifiedSelectionTree>["selectNodes"] {
return useCallback(
(nodeIds, changeType) =>
action(
nodeIds.filter((nodeId) => {
const node = getNode(nodeId);
return node && (!predicate || predicate(node));
}),
changeType,
),
[action, getNode, predicate],
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function ModelsTree({
hierarchyLevelConfig,
hierarchyConfig,
selectionMode,
selectionPredicate,
visibilityHandlerOverrides,
getFilteredPaths,
onModelsFiltered,
Expand All @@ -40,6 +41,7 @@ export function ModelsTree({
visibilityHandlerOverrides,
getFilteredPaths,
onModelsFiltered,
selectionPredicate,
});

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ interface ModelsTreeComponentProps
| "density"
| "hierarchyLevelConfig"
| "selectionMode"
| "selectionPredicate"
| "hierarchyConfig"
| "visibilityHandlerOverrides"
| "getFilteredPaths"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { FilterLimitExceededError } from "../common/TreeErrors";
import { useIModelChangeListener } from "../common/UseIModelChangeListener";
import { useTelemetryContext } from "../common/UseTelemetryContext";
import { ModelsTreeIdsCache } from "./internal/ModelsTreeIdsCache";
import { ModelsTreeNode } from "./internal/ModelsTreeNode";
import { createModelsTreeVisibilityHandler } from "./internal/ModelsTreeVisibilityHandler";
import { defaultHierarchyConfiguration, ModelsTreeDefinition } from "./ModelsTreeDefinition";

Expand All @@ -41,13 +42,18 @@ export interface UseModelsTreeProps {
createInstanceKeyPaths: (props: { targetItems: Array<InstanceKey | ElementsGroupInfo> } | { label: string }) => Promise<HierarchyFilteringPath[]>;
}) => Promise<HierarchyFilteringPath[]>;
onModelsFiltered?: (modelIds: Id64String[] | undefined) => void;
/**
* An optional predicate to allow or prohibit selection of a node.
* When not supplied, all nodes are selectable.
*/
selectionPredicate?: (props: { node: PresentationHierarchyNode; type: "subject" | "model" | "category" | "element" | "elements-class-group" }) => boolean;
}

/** @beta */
interface UseModelsTreeResult {
modelsTreeProps: Pick<
VisibilityTreeProps,
"treeName" | "getHierarchyDefinition" | "getFilteredPaths" | "visibilityHandlerFactory" | "highlight" | "noDataMessage"
"treeName" | "getHierarchyDefinition" | "getFilteredPaths" | "visibilityHandlerFactory" | "highlight" | "noDataMessage" | "selectionPredicate"
>;
rendererProps: Required<Pick<VisibilityTreeRendererProps, "getIcon" | "onNodeDoubleClick">>;
}
Expand All @@ -63,6 +69,7 @@ export function useModelsTree({
visibilityHandlerOverrides,
getFilteredPaths,
onModelsFiltered,
selectionPredicate: nodeTypeSelectionPredicate,
}: UseModelsTreeProps): UseModelsTreeResult {
const [filteringError, setFilteringError] = useState<ModelsTreeFilteringError | undefined>(undefined);
const hierarchyConfiguration = useMemo<ModelsTreeHierarchyConfiguration>(
Expand Down Expand Up @@ -195,6 +202,16 @@ export function useModelsTree({
return undefined;
}, [filter, loadFocusedItems, getModelsTreeIdsCache, onFeatureUsed, getFilteredPaths, hierarchyConfiguration, onModelsFiltered, onFilteredPathsChanged]);

const nodeSelectionPredicate = useCallback<NonNullable<VisibilityTreeProps["selectionPredicate"]>>(
(node) => {
if (!nodeTypeSelectionPredicate) {
return true;
}
return nodeTypeSelectionPredicate({ node, type: ModelsTreeNode.getType(node.nodeData) });
},
[nodeTypeSelectionPredicate],
);

return {
modelsTreeProps: {
treeName: "models-tree-v2",
Expand All @@ -203,6 +220,7 @@ export function useModelsTree({
getFilteredPaths: getPaths,
noDataMessage: getNoDataMessage(filter, filteringError),
highlight: filter ? { text: filter } : undefined,
selectionPredicate: nodeSelectionPredicate,
},
rendererProps: {
onNodeDoubleClick,
Expand Down
Loading

0 comments on commit db4ec4a

Please sign in to comment.