Skip to content

Commit

Permalink
Components should react to unit system change (#218)
Browse files Browse the repository at this point in the history
* Refactor test-app

* Reload data when unit system changes

* Change files

* Cleanup test

* Cleanup ContentBuilder tests
  • Loading branch information
saskliutas committed Aug 23, 2023
1 parent 585bfe0 commit 326dd33
Show file tree
Hide file tree
Showing 14 changed files with 578 additions and 286 deletions.
386 changes: 196 additions & 190 deletions apps/test-app/frontend/src/components/app/App.tsx

Large diffs are not rendered by default.

Expand Up @@ -8,8 +8,8 @@ import { UnitSystemKey } from "@itwin/core-quantity";
import { Select, SelectOption } from "@itwin/itwinui-react";

export interface UnitSystemSelectorProps {
selectedUnitSystem: UnitSystemKey | undefined;
onUnitSystemSelected: (unitSystem: UnitSystemKey | undefined) => void;
selectedUnitSystem: UnitSystemKey;
onUnitSystemSelected: (unitSystem: UnitSystemKey) => void;
}

export function UnitSystemSelector(props: UnitSystemSelectorProps) {
Expand All @@ -28,11 +28,7 @@ export function UnitSystemSelector(props: UnitSystemSelectorProps) {
);
}

const availableUnitSystems: SelectOption<UnitSystemKey | undefined>[] = [
{
value: undefined,
label: "",
},
const availableUnitSystems: SelectOption<UnitSystemKey>[] = [
{
value: "metric",
label: "Metric",
Expand Down
6 changes: 2 additions & 4 deletions apps/test-app/frontend/src/index.tsx
Expand Up @@ -16,7 +16,7 @@ import { ITwinLocalization } from "@itwin/core-i18n";
import { createFavoritePropertiesStorage, DefaultFavoritePropertiesStorageTypes, Presentation } from "@itwin/presentation-frontend";
// __PUBLISH_EXTRACT_END__
import { rpcInterfaces } from "@test-app/common";
import App from "./components/app/App";
import { App } from "./components/app/App";

// initialize logging
Logger.initializeToConsole();
Expand Down Expand Up @@ -54,6 +54,7 @@ async function initializeApp() {

readyPromises.push(initializePresentation());
readyPromises.push(UiComponents.initialize(IModelApp.localization));
readyPromises.push(IModelApp.quantityFormatter.setActiveUnitSystem("metric"));
await Promise.all(readyPromises);
}

Expand All @@ -63,9 +64,6 @@ async function initializePresentation() {
presentation: {
// specify locale for localizing presentation data, it can be changed afterwards
activeLocale: IModelApp.localization.getLanguageList()[0],

// specify the preferred unit system
activeUnitSystem: "metric",
},
favorites: {
storage: createFavoritePropertiesStorage(DefaultFavoritePropertiesStorageTypes.UserPreferencesStorage),
Expand Down
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Reload content and hierarchies when active unit system is changed.",
"packageName": "@itwin/presentation-components",
"email": "24278440+saskliutas@users.noreply.github.com",
"dependentChangeType": "patch"
}
Expand Up @@ -9,7 +9,7 @@
import memoize from "micro-memoize";
import { PropertyDescription, PropertyRecord } from "@itwin/appui-abstract";
import { Logger } from "@itwin/core-bentley";
import { IModelConnection } from "@itwin/core-frontend";
import { IModelApp, IModelConnection } from "@itwin/core-frontend";
import {
ClientDiagnosticsOptions,
Content,
Expand Down Expand Up @@ -168,6 +168,7 @@ export class ContentDataProvider implements IContentDataProvider {
private _selectionInfo?: SelectionInfo;
private _pagingSize?: number;
private _diagnosticsOptions?: ClientDiagnosticsOptions;
private _listeners: Array<() => void> = [];

/** Constructor. */
constructor(props: ContentDataProviderProps) {
Expand All @@ -179,18 +180,18 @@ export class ContentDataProvider implements IContentDataProvider {
this._pagingSize = props.pagingSize;
this._diagnosticsOptions = createDiagnosticsOptions(props);
if (props.enableContentAutoUpdate) {
Presentation.presentation.onIModelContentChanged.addListener(this.onIModelContentChanged);
Presentation.presentation.rulesets().onRulesetModified.addListener(this.onRulesetModified);
Presentation.presentation.vars(this._rulesetRegistration.rulesetId).onVariableChanged.addListener(this.onRulesetVariableChanged);
this._listeners.push(Presentation.presentation.onIModelContentChanged.addListener(this.onIModelContentChanged));
this._listeners.push(Presentation.presentation.rulesets().onRulesetModified.addListener(this.onRulesetModified));
this._listeners.push(Presentation.presentation.vars(this._rulesetRegistration.rulesetId).onVariableChanged.addListener(this.onRulesetVariableChanged));
}
this.invalidateCache(CacheInvalidationProps.full());
this._listeners.push(IModelApp.quantityFormatter.onActiveFormattingUnitSystemChanged.addListener(this.onUnitSystemChanged));
}

/** Destructor. Must be called to clean up. */
public dispose() {
Presentation.presentation.onIModelContentChanged.removeListener(this.onIModelContentChanged);
Presentation.presentation.rulesets().onRulesetModified.removeListener(this.onRulesetModified);
Presentation.presentation.vars(this._rulesetRegistration.rulesetId).onVariableChanged.removeListener(this.onRulesetVariableChanged);
for (const removeListener of this._listeners) {
removeListener();
}
this._rulesetRegistration.dispose();
}

Expand Down Expand Up @@ -438,6 +439,11 @@ export class ContentDataProvider implements IContentDataProvider {
private onRulesetVariableChanged = () => {
this.onContentUpdate();
};

// eslint-disable-next-line @typescript-eslint/naming-convention
private onUnitSystemChanged = () => {
this.invalidateCache({ content: true });
};
}

class MemoizationHelpers {
Expand Down
Expand Up @@ -129,13 +129,9 @@ export class PresentationPropertyDataProvider extends ContentDataProvider implem
*/
protected override invalidateCache(props: CacheInvalidationProps): void {
super.invalidateCache(props);
if (this.getMemoizedData) {
this.getMemoizedData.cache.keys.length = 0;
this.getMemoizedData.cache.values.length = 0;
}
if (this.onDataChanged) {
this.onDataChanged.raiseEvent();
}
this.getMemoizedData.cache.keys.length = 0;
this.getMemoizedData.cache.values.length = 0;
this.onDataChanged.raiseEvent();
}

/**
Expand Down
204 changes: 163 additions & 41 deletions packages/components/src/presentation-components/table/UseRows.ts
Expand Up @@ -6,12 +6,10 @@
* @module Internal
*/

import { useEffect, useState } from "react";
import { EMPTY, from, Subject } from "rxjs";
import { distinct } from "rxjs/internal/operators/distinct";
import { mergeMap } from "rxjs/internal/operators/mergeMap";
import { useEffect, useRef, useState } from "react";
import { EMPTY, from, mergeMap, Observable, Subject } from "rxjs";
import { assert } from "@itwin/core-bentley";
import { IModelConnection } from "@itwin/core-frontend";
import { IModelApp, IModelConnection } from "@itwin/core-frontend";
import { Content, DefaultContentDisplayTypes, KeySet, PageOptions, Ruleset, StartItemProps, traverseContent } from "@itwin/presentation-common";
import { Presentation } from "@itwin/presentation-frontend";
import { FieldHierarchyRecord, PropertyRecordsBuilder } from "../common/ContentBuilder";
Expand All @@ -37,59 +35,175 @@ export interface UseRowsResult {

/** @internal */
export function useRows(props: UseRowsProps): UseRowsResult {
interface State {
total: number;
isLoading: boolean;
rows: TableRowDefinition[];
}

const { imodel, ruleset, keys, pageSize, options } = props;
const setErrorState = useErrorState();
const [state, setState] = useState<UseRowsResult>({
const [state, setState] = useState<State>({
isLoading: false,
rows: [],
loadMoreRows: /* istanbul ignore next*/ () => {},
total: 0,
});

const loaderRef = useRef<RowsLoader>(noopRowsLoader);

useEffect(() => {
setState((prev) => ({ ...prev, rows: [] }));
const { observable, ...loader } = createRowsLoader({
imodel,
ruleset,
keys,
pageSize,
options,
onPageLoadStart: () => setState((prev) => ({ ...prev, isLoading: true })),
});
loaderRef.current = loader;

const loader = new Subject<number>();
const subscription = loader
.pipe(
distinct(),
mergeMap((pageStart) => {
if (keys.isEmpty) {
return EMPTY;
}
setState((prev) => ({ ...prev, isLoading: true }));
return from(loadRows(imodel, ruleset, keys, { start: pageStart, size: pageSize }, options));
}, 1),
)
.subscribe({
next: (loadedRows) => {
setState((prev) => ({
const subscription = observable.subscribe({
next: ({ total, rowDefinitions, offset }) => {
setState((prev) => {
const newRows = [...prev.rows];
newRows.splice(offset, rowDefinitions.length, ...rowDefinitions);

return {
...prev,
isLoading: false,
rows: [...prev.rows, ...loadedRows.rowDefinitions],
loadMoreRows: () => {
const pageStart = prev.rows.length + loadedRows.rowDefinitions.length;
if (pageStart >= loadedRows.total) {
return;
}
loader.next(pageStart);
},
}));
},
error: (err) => {
setErrorState(err);
setState((prev) => ({ ...prev, rows: [], isLoading: false }));
},
});

loader.next(0);
rows: newRows,
total,
};
});
},
error: (err) => {
setErrorState(err);
setState((prev) => ({ ...prev, rows: [], isLoading: false, total: 0 }));
},
});

loaderRef.current.loadPage(0);
return () => {
subscription.unsubscribe();
loaderRef.current = noopRowsLoader;
};
}, [imodel, ruleset, keys, pageSize, options, setErrorState]);

return state;
useEffect(() => {
return IModelApp.quantityFormatter.onActiveFormattingUnitSystemChanged.addListener(() => {
loaderRef.current.reload(state.rows.length);
});
}, [state.rows]);

return {
rows: state.rows,
isLoading: state.isLoading,
loadMoreRows: () => {
if (state.rows.length === state.total) {
return;
}

loaderRef.current.loadPage(state.rows.length);
},
};
}

interface LoadPageOptions {
action: "load-page";
pageStart: number;
}

interface ReloadOptions {
action: "reload";
loadedRowsCount: number;
}

type LoaderOptions = LoadPageOptions | ReloadOptions;

interface RowsLoaderProps {
imodel: IModelConnection;
ruleset: Ruleset | string;
keys: Readonly<KeySet>;
pageSize: number;
options: TableOptions;
onPageLoadStart: () => void;
}

interface RowsLoader {
loadPage: (pageStart: number) => void;
reload: (loadedRowsCount: number) => void;
}

function createRowsLoader({
imodel,
ruleset,
keys,
pageSize,
options,
onPageLoadStart,
}: RowsLoaderProps): RowsLoader & { observable: Observable<RowsLoadResult> } {
const loaderSubject = new Subject<LoaderOptions>();
const loaderObservable = loaderSubject.pipe(
mergeMap((loaderOptions) => {
if (keys.isEmpty) {
return EMPTY;
}

switch (loaderOptions.action) {
case "load-page": {
onPageLoadStart();
return from(loadRows(imodel, ruleset, keys, { start: loaderOptions.pageStart, size: pageSize }, options));
}
case "reload": {
return loaderOptions.loadedRowsCount === 0 ? EMPTY : createReloadObs(imodel, ruleset, keys, options, loaderOptions.loadedRowsCount);
}
}
}),
);

return {
observable: loaderObservable,
loadPage: (pageStart: number) => {
loaderSubject.next({ action: "load-page", pageStart });
},
reload: (rowsCount: number) => {
loaderSubject.next({ action: "reload", loadedRowsCount: rowsCount });
},
};
}

/** @internal */
export const ROWS_RELOAD_PAGE_SIZE = 1000;

function createReloadObs(imodel: IModelConnection, ruleset: Ruleset | string, keys: Readonly<KeySet>, options: TableOptions, loadedItemsCount: number) {
const lastPageIndex = Math.floor(loadedItemsCount / ROWS_RELOAD_PAGE_SIZE);
const lastPageSize = loadedItemsCount % ROWS_RELOAD_PAGE_SIZE;

const pages: Array<Required<PageOptions>> = [];
for (let i = 0; i <= lastPageIndex; i++) {
pages.push({
start: i * ROWS_RELOAD_PAGE_SIZE,
size: i === lastPageIndex ? lastPageSize : ROWS_RELOAD_PAGE_SIZE,
});
}

return from(pages).pipe(mergeMap((pageOptions) => from(loadRows(imodel, ruleset, keys, pageOptions, options))));
}

async function loadRows(imodel: IModelConnection, ruleset: Ruleset | string, keys: Readonly<KeySet>, paging: PageOptions, options: TableOptions) {
interface RowsLoadResult {
rowDefinitions: TableRowDefinition[];
total: number;
offset: number;
}

async function loadRows(
imodel: IModelConnection,
ruleset: Ruleset | string,
keys: Readonly<KeySet>,
paging: Required<PageOptions>,
options: TableOptions,
): Promise<RowsLoadResult> {
const result = await Presentation.presentation.getContentAndSize({
imodel,
keys: new KeySet(keys),
Expand All @@ -106,12 +220,14 @@ async function loadRows(imodel: IModelConnection, ruleset: Ruleset | string, key
return {
rowDefinitions: [],
total: 0,
offset: 0,
};
}

return {
rowDefinitions: createRows(result.content),
total: result.size,
offset: paging.start,
};
}

Expand Down Expand Up @@ -158,3 +274,9 @@ class RowsBuilder extends PropertyRecordsBuilder {
super.finishItem();
}
}

// istanbul ignore next
const noopRowsLoader: RowsLoader = {
loadPage: () => {},
reload: () => {},
};

0 comments on commit 326dd33

Please sign in to comment.