@itwin/presentation-components
Version:
React components based on iTwin.js Presentation library
195 lines • 9.06 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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