Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tree widget: Add selectionPredicate to models tree building blocks #1124

Merged
merged 3 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading