@itwin/presentation-components
Version:
React components based on iTwin.js Presentation library
171 lines • 9.56 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module InstancesFilter
*/
import "./InstanceFilterBuilder.scss";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { BehaviorSubject, from, of } from "rxjs";
import { map } from "rxjs/internal/operators/map";
import { switchAll } from "rxjs/internal/operators/switchAll";
import { PropertyFilterBuilderRenderer } from "@itwin/components-react";
import { Alert, ComboBox } from "@itwin/itwinui-react";
import { translate } from "../common/Utils.js";
import { getIModelMetadataProvider } from "./ECMetadataProvider.js";
import { PresentationFilterBuilderValueRenderer, useInstanceFilterPropertyInfos } from "./PresentationFilterBuilder.js";
import { isFilterNonEmpty } from "./Utils.js";
/**
* Component for building complex instance filters based on instance properties. In addition to filter builder component
* it renders selector for classes that can be used to filter out available properties in filter rules.
* @internal
*/
export function InstanceFilterBuilder(props) {
const { selectedClasses, classes, onSelectedClassesChanged, imodel, descriptor, descriptorInputKeys, ...restProps } = props;
const [showClassSelectionWarning, setShowClassSelectionWarning] = useState(false);
const options = useMemo(() => classes.map(createOption), [classes]);
const selectedOptions = useMemo(() => selectedClasses.map((classInfo) => classInfo.id), [selectedClasses]);
return (_jsxs(_Fragment, { children: [showClassSelectionWarning && (_jsx(Alert, { type: "warning", className: "class-selection-warning", children: translate("instance-filter-builder.class-selection-warning") })), _jsxs("div", { className: "presentation-instance-filter", children: [_jsx(ComboBox, { enableVirtualization: true, multiple: true, options: options, value: selectedOptions, inputProps: {
placeholder: selectedClasses.length
? translate("instance-filter-builder.selected-classes")
: translate("instance-filter-builder.select-classes-optional"),
}, onShow: () => setShowClassSelectionWarning(!!selectedOptions.length && isFilterNonEmpty(props.rootGroup)), onHide: () => setShowClassSelectionWarning(false), onChange: (selectedIds) => {
onSelectedClassesChanged(selectedIds);
} }), _jsx("div", { className: "presentation-property-filter-builder", children: _jsx(PropertyFilterBuilderRenderer, { ...restProps, ruleValueRenderer: (rendererProps) => (_jsx(PresentationFilterBuilderValueRenderer, { ...rendererProps, descriptorInputKeys: descriptorInputKeys, imodel: imodel, descriptor: descriptor, selectedClasses: selectedClasses })) }) })] })] }));
}
function createOption(classInfo) {
return { label: classInfo.label, value: classInfo.id };
}
/**
* Custom hook that extracts properties and classes from [Descriptor]($presentation-common) and creates props that can be used by [[InstanceFilterBuilder]] component.
*
* This hook also makes sure that when classes are selected available properties list is updated to contain only properties found on selected classes and vice versa -
* when property is selected in one of the rules selected classes list is updated to contain only classes that has access to that property.
* @internal
*/
export function usePresentationInstanceFilteringProps(descriptor, imodel, initialActiveClasses) {
const { propertyInfos, propertyRenderer } = useInstanceFilterPropertyInfos({ descriptor });
const classes = usePropertyClasses({ descriptor });
const { activeClasses, changeActiveClasses, isFilteringClasses, filterClassesByProperty } = useActiveClasses({
imodel,
availableClasses: classes,
initialActiveClasses,
});
const { properties, isFilteringProperties } = usePropertiesFilteringByClass({ imodel, availableProperties: propertyInfos, activeClasses });
const onRulePropertySelected = useCallback((property) => {
const propertyInfo = propertyInfos.find((info) => info.propertyDescription.name === property.name);
if (propertyInfo) {
filterClassesByProperty(propertyInfo);
}
}, [propertyInfos, filterClassesByProperty]);
return {
onRulePropertySelected,
onSelectedClassesChanged: useCallback((classIds) => changeActiveClasses(classIds), [changeActiveClasses]),
propertyRenderer,
properties,
classes,
selectedClasses: activeClasses,
isDisabled: isFilteringClasses || isFilteringProperties,
};
}
function usePropertyClasses({ descriptor }) {
return useMemo(() => {
const uniqueClasses = new Map();
descriptor.selectClasses.forEach((selectClass) => uniqueClasses.set(selectClass.selectClassInfo.id, selectClass.selectClassInfo));
return [...uniqueClasses.values()];
}, [descriptor]);
}
function usePropertiesFilteringByClass({ imodel, availableProperties, activeClasses }) {
const [filteredProperties, setFilteredProperties] = useState();
const [isFilteringProperties, setIsFilteringProperties] = useState(false);
const properties = useMemo(() => (filteredProperties ?? availableProperties).map((info) => info.propertyDescription), [availableProperties, filteredProperties]);
const classChanges = useRef(new BehaviorSubject([]));
useEffect(() => {
classChanges.current.next(activeClasses);
}, [activeClasses]);
// filter properties by selected classes
useEffect(() => {
const subscription = classChanges.current
.pipe(map((classes) => {
if (classes.length === 0) {
return of(undefined);
}
setIsFilteringProperties(true);
return from(computePropertiesByClasses(availableProperties, classes, imodel));
}), switchAll())
.subscribe({
next: (infos) => {
setFilteredProperties(infos);
setIsFilteringProperties(false);
},
});
return () => {
subscription.unsubscribe();
};
}, [imodel, availableProperties]);
return {
properties,
isFilteringProperties,
};
}
function useActiveClasses({ imodel, availableClasses, initialActiveClasses }) {
const [activeClasses, setActiveClasses] = useState(initialActiveClasses ?? []);
const [isFilteringClasses, setIsFilteringClasses] = useState(false);
const availableClassesRef = useRef(availableClasses);
useEffect(() => {
if (availableClassesRef.current !== availableClasses) {
setActiveClasses([]);
availableClassesRef.current = availableClasses;
}
}, [availableClasses]);
const filterClassesByProperty = useCallback((property) => {
setIsFilteringClasses(true);
void (async () => {
const newActiveClasses = await computeClassesByProperty(activeClasses.length === 0 ? availableClasses : activeClasses, property, imodel);
setActiveClasses(newActiveClasses);
setIsFilteringClasses(false);
})();
}, [activeClasses, availableClasses, imodel]);
const changeActiveClasses = useCallback((classIds) => {
const newSelectedClasses = availableClasses.filter((availableClass) => classIds.findIndex((classId) => classId === availableClass.id) !== -1);
setActiveClasses(newSelectedClasses);
}, [availableClasses]);
return {
activeClasses,
isFilteringClasses,
changeActiveClasses,
filterClassesByProperty,
};
}
async function computePropertiesByClasses(properties, classes, imodel) {
const metadataProvider = getIModelMetadataProvider(imodel);
const ecClassInfos = await Promise.all(classes.map(async (info) => metadataProvider.getECClassInfo(info.id)));
const filteredProperties = [];
for (const prop of properties) {
// property should be shown if at least one of selected classes is derived from property source class
if (ecClassInfos.some((info) => info && prop.sourceClassIds.some((sourceClassId) => info.isDerivedFrom(sourceClassId)))) {
filteredProperties.push(prop);
}
}
return filteredProperties.length === properties.length ? undefined : filteredProperties;
}
async function computeClassesByProperty(classes, property, imodel) {
const metadataProvider = getIModelMetadataProvider(imodel);
const propertyClasses = (await Promise.all(property.sourceClassIds.map(async (sourceClassId) => {
return metadataProvider.getECClassInfo(sourceClassId);
}))).filter((propertyClass) => propertyClass !== undefined);
/* c8 ignore next 3 */
if (propertyClasses.length === 0) {
return classes;
}
const classesWithProperty = [];
for (const currentClass of classes) {
if (propertyClasses.some((propertyClass) => propertyClass.isBaseOf(currentClass.id))) {
classesWithProperty.push(currentClass);
}
}
return classesWithProperty;
}
//# sourceMappingURL=InstanceFilterBuilder.js.map