UNPKG

@itwin/presentation-components

Version:

React components based on iTwin.js Presentation library

171 lines 9.56 kB
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