UNPKG

@itwin/presentation-components

Version:

React components based on iTwin.js Presentation library

195 lines 9.06 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module Table */ import { useCallback, useEffect, useMemo, useState } from "react"; import { debounceTime, EMPTY, from, map, mergeMap, of, Subject, switchMap, tap } from "rxjs"; import { BeEvent, Guid } from "@itwin/core-bentley"; import { Key, KeySet, NodeKey } from "@itwin/presentation-common"; import { createIModelKey } from "@itwin/presentation-core-interop"; import { Presentation } from "@itwin/presentation-frontend"; import { parseFullClassName } from "@itwin/presentation-shared"; import { Selectables } from "@itwin/unified-selection"; import { useColumns } from "./UseColumns.js"; import { useRows } from "./UseRows.js"; import { useTableOptions } from "./UseTableOptions.js"; /** * Custom hook that loads data for generic table component. * @throws on failure to get table data. The error is thrown in the React's render loop, so it can be caught using an error boundary. * @public */ export function usePresentationTable(props) { const { imodel, ruleset, keys, pageSize, columnMapper, rowMapper } = props; const columns = useColumns({ imodel, ruleset, keys }); const { options, sort, filter } = useTableOptions({ columns }); const { rows, isLoading, loadMoreRows } = useRows({ imodel, ruleset, keys, pageSize, options }); return { columns: useMemo(() => columns?.map(columnMapper), [columns, columnMapper]), rows: useMemo(() => rows?.map(rowMapper), [rows, rowMapper]), isLoading, loadMoreRows, sort, filter, }; } /** * Custom hook that load data for generic table component. It uses [Unified Selection]($docs/presentation/unified-selection/index.md) to get keys defining what to load rows for. * * @throws on failure to get table data. The error is thrown in the React's render loop, so it can be caught using an error boundary. * @public */ export function usePresentationTableWithUnifiedSelection(props) { const { imodel, ruleset, pageSize, columnMapper, rowMapper, selectionStorage } = props; const [tableName] = useState(() => `UnifiedSelectionTable_${Guid.createValue()}`); const [selectedRows, setSelectedRows] = useState(); const { keys: { keys, isLoading: isLoadingKeys }, getSelection, replaceSelection, selectionChange, } = useSelectionHandler({ imodel, selectionStorage, tableName }); const columns = useColumns({ imodel, ruleset, keys }); const { options, sort, filter } = useTableOptions({ columns }); const { rows, isLoading: isLoadingRows, loadMoreRows } = useRows({ imodel, ruleset, keys, pageSize, options }); useEffect(() => { const updateSelectedRows = new Subject(); const subscription = updateSelectedRows .pipe(mergeMap((level) => { if (level > 1) { // ignore all selection changes with level > 1 return EMPTY; } if (level === 0) { // selection at level 0 defines what the table shows, so just clear the selection return of([]); } return from(getSelection({ level: 1 })).pipe(debounceTime(0), map((selectedKeys) => { const rowsToAddToSelection = []; selectedKeys.forEach((selectable) => { const selectedRow = rows.find((row) => { // table content is built using the legacy Presentation library, where full class name format is // "schema:class". In the unified selection library, the format is "schema.class". const { schemaName, className } = parseFullClassName(selectable.className); return row.key === JSON.stringify({ className: `${schemaName}:${className}`, id: selectable.id }); }); if (selectedRow !== undefined) { rowsToAddToSelection.push(selectedRow); } }); return rowsToAddToSelection; })); })) .subscribe({ next: (rowsToSelect) => { setSelectedRows(rowsToSelect); }, }); updateSelectedRows.next(1); const removeListener = selectionChange.addListener((level) => updateSelectedRows.next(level)); return () => { subscription.unsubscribe(); removeListener(); }; }, [rows, imodel, selectionChange, getSelection]); const onSelect = (selectedRowKeys) => { const selectables = []; for (const selectedRowKey of selectedRowKeys) { if (!rows.find((row) => row.key === selectedRowKey)) { continue; } const selectableKey = JSON.parse(selectedRowKey); selectables.push(selectableKey); } replaceSelection({ source: tableName, selectables, level: 1 }); }; return { columns: useMemo(() => columns?.map(columnMapper), [columns, columnMapper]), rows: useMemo(() => rows.map(rowMapper), [rows, rowMapper]), isLoading: !columns || isLoadingRows || isLoadingKeys, loadMoreRows, sort, filter, onSelect, selectedRows: useMemo(() => (selectedRows ?? []).map(rowMapper), [selectedRows, rowMapper]), }; } function useSelectionHandler({ imodel, selectionStorage, tableName }) { const [selectionChange] = useState(() => new BeEvent()); useEffect(() => { if (selectionStorage) { return selectionStorage.selectionChangeEvent.addListener((args) => { if (args.imodelKey === createIModelKey(imodel) && args.source !== tableName) { selectionChange.raiseEvent(args.level); } }); } // eslint-disable-next-line @typescript-eslint/no-deprecated return Presentation.selection.selectionChange.addListener((args) => { if (imodel === args.imodel && args.source !== tableName) { selectionChange.raiseEvent(args.level); } }); }, [imodel, selectionStorage, tableName, selectionChange]); const getSelection = useCallback(async (args) => { return selectionStorage ? loadInstanceKeysFromSelectables(selectionStorage.getSelection({ imodelKey: createIModelKey(imodel), level: args.level })) : // eslint-disable-next-line @typescript-eslint/no-deprecated loadInstanceKeysFromKeySet(Presentation.selection.getSelection(imodel, args.level)); }, [imodel, selectionStorage]); const replaceSelection = useCallback((args) => { return selectionStorage ? selectionStorage.replaceSelection({ imodelKey: createIModelKey(imodel), source: args.source, level: args.level, selectables: args.selectables, }) : // eslint-disable-next-line @typescript-eslint/no-deprecated Presentation.selection.replaceSelection(args.source, imodel, args.selectables, args.level); }, [imodel, selectionStorage]); const keys = useUnifiedSelectionKeys({ getSelection, selectionChange }); return { keys, getSelection, replaceSelection, selectionChange, }; } async function loadInstanceKeysFromSelectables(selectables) { const keys = []; for await (const selectable of Selectables.load(selectables)) { keys.push(selectable); } return keys; } async function loadInstanceKeysFromKeySet(keySet) { const keys = []; keySet.forEach((key) => { if (Key.isInstanceKey(key)) { keys.push(key); } else if (NodeKey.isInstancesNodeKey(key)) { keys.push(...key.instanceKeys); } }); return keys; } function useUnifiedSelectionKeys({ getSelection, selectionChange, }) { const [state, setState] = useState(() => ({ isLoading: false, keys: new KeySet() })); useEffect(() => { const update = new Subject(); const subscription = update .pipe(tap(() => setState((prev) => ({ ...prev, isLoading: true }))), switchMap(async () => getSelection({ level: 0 })), map((selectables) => new KeySet(selectables))) .subscribe({ next: (newKeys) => { setState({ isLoading: false, keys: newKeys }); }, }); update.next(); const removeListener = selectionChange.addListener(() => update.next()); return () => { subscription.unsubscribe(); removeListener(); }; }, [getSelection, selectionChange]); return state; } //# sourceMappingURL=UsePresentationTable.js.map