Skip to content

Commit

Permalink
PresentationInstanceFilterDialog: Make API more convienent (#438)
Browse files Browse the repository at this point in the history
* Filtering dialog associate descriptor with keys

* changeset

* Rename
  • Loading branch information
saskliutas authored Feb 27, 2024
1 parent 8791d60 commit 115d815
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 86 deletions.
34 changes: 34 additions & 0 deletions .changeset/green-windows-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
"@itwin/presentation-components": major
---

Merged `PresentationInstanceFilterDialogProps.descriptor` and `PresentationInstanceFilterDialogProps.descriptorInputKeys` into single property `PresentationInstanceFilterDialogProps.propertiesSource`. This explicitly associates `Descriptor` with input keys. It provides more convenient API in case `Descriptor` is lazy loaded and input keys are known only after loading.

Before:

```tsx
const [inputKey, setInputKeys] = useState([]);

<PresentationInstanceFilterDialog
descriptor={async () => {
const { descriptor, keys } = loadDescriptorAndKeys();
setInputKeys(keys);
return descriptor;
}}
descriptorInputKeys={inputKeys}
/>
```

After:

```tsx
<PresentationInstanceFilterDialog
propertiesSource={async () => {
const { descriptor, keys } = loadDescriptorAndKeys();
return {
descriptor,
inputKeys: keys,
};
}}
/>
```
10 changes: 8 additions & 2 deletions packages/components/api/presentation-components.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,15 +425,15 @@ export function PresentationInstanceFilterDialog(props: PresentationInstanceFilt

// @beta
export interface PresentationInstanceFilterDialogProps {
descriptor: (() => Promise<Descriptor>) | Descriptor;
descriptorInputKeys?: Keys;
filterResultsCountRenderer?: (filter: PresentationInstanceFilterInfo) => ReactNode;
imodel: IModelConnection;
initialFilter?: PresentationInstanceFilterInfo;
isOpen: boolean;
onApply: (filter?: PresentationInstanceFilterInfo) => void;
onClose?: () => void;
onReset?: () => void;
propertiesSource: (() => Promise<PresentationInstanceFilterPropertiesSource>) | PresentationInstanceFilterPropertiesSource | undefined;
// @deprecated
ruleGroupDepthLimit?: number;
title?: React.ReactNode;
toolbarButtonsRenderer?: (toolbarHandlers: FilteringDialogToolbarHandlers) => ReactNode;
Expand All @@ -445,6 +445,12 @@ export interface PresentationInstanceFilterInfo {
usedClasses: ClassInfo[];
}

// @beta
export interface PresentationInstanceFilterPropertiesSource {
descriptor: Descriptor;
inputKeys?: Keys;
}

// @beta
export interface PresentationInstanceFilterPropertyInfo {
categoryLabel?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ beta;PresentationInstanceFilterConditionGroup
beta;PresentationInstanceFilterDialog(props: PresentationInstanceFilterDialogProps): JSX_2.Element
beta;PresentationInstanceFilterDialogProps
beta;PresentationInstanceFilterInfo
beta;PresentationInstanceFilterPropertiesSource
beta;PresentationInstanceFilterPropertyInfo
public;PresentationLabelsProvider
public;PresentationLabelsProviderProps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,33 @@ import { PresentationInstanceFilterInfo } from "./PresentationFilterBuilder";
import { PresentationInstanceFilter } from "./PresentationInstanceFilter";
import { filterRuleValidator, isFilterNonEmpty } from "./Utils";

/**
* Data structure that describes source to gather properties from.
* @beta
*/
export interface PresentationInstanceFilterPropertiesSource {
/**
* [Descriptor]($presentation-common) that will be used to get properties.
*/
descriptor: Descriptor;
/**
* [Keys]($presentation-common) of filterables on which the filter was called.
* These keys should match the keys that were used to create the descriptor.
*/
inputKeys?: Keys;
}

/**
* Props for [[PresentationInstanceFilterDialog]] component.
* @beta
*/
export interface PresentationInstanceFilterDialogProps {
/** iModel connection to pull data from. */
imodel: IModelConnection;
/** Specifies how deep rule groups can be nested. */
/**
* Specifies how deep rule groups can be nested.
* @deprecated in 5.0. Rule groups nesting was removed from [PropertyFilterBuilderRenderer]($components-react)
*/
ruleGroupDepthLimit?: number;
/** Specifies whether dialog is open or not. */
isOpen: boolean;
Expand All @@ -40,19 +59,13 @@ export interface PresentationInstanceFilterDialogProps {
/** Renderer that will be used to render a custom toolbar instead of the default one. */
toolbarButtonsRenderer?: (toolbarHandlers: FilteringDialogToolbarHandlers) => ReactNode;
/**
* [Descriptor]($presentation-common) that will be used in [[InstanceFilterBuilder]] component rendered inside this dialog.
* [[PresentationInstanceFilterPropertiesSource]] that will be used in [[InstanceFilterBuilder]] component to populate properties.
*
* This property can be set to function in order to lazy load [Descriptor]($presentation-common) when dialog is opened.
* This property can be set to function in order to lazy load [[PresentationInstanceFilterPropertiesSource]] when dialog is opened.
*/
descriptor: (() => Promise<Descriptor>) | Descriptor;
propertiesSource: (() => Promise<PresentationInstanceFilterPropertiesSource>) | PresentationInstanceFilterPropertiesSource | undefined;
/** Renders filter results count. */
filterResultsCountRenderer?: (filter: PresentationInstanceFilterInfo) => ReactNode;
/**
* [Keys]($presentation-common) of filterables on which the filter was called.
*
* These keys should match the keys that were used to create the descriptor.
*/
descriptorInputKeys?: Keys;
/** Dialog title. */
title?: React.ReactNode;
/** Initial filter that will be show when component is mounted. */
Expand Down Expand Up @@ -101,70 +114,84 @@ export function PresentationInstanceFilterDialog(props: PresentationInstanceFilt

type FilterDialogContentProps = Omit<PresentationInstanceFilterDialogProps, "isOpen" | "title">;

function FilterDialogContent({ descriptor, ...restProps }: FilterDialogContentProps) {
const loadedDescriptor = useDelayLoadedDescriptor(descriptor);
if (!loadedDescriptor) {
function FilterDialogContent({ propertiesSource, ...restProps }: FilterDialogContentProps) {
const { propertiesSource: loadedPropertiesSource, isLoading } = useDelayLoadedPropertiesSource(propertiesSource);
if (isLoading) {
return <DelayedCenteredProgressRadial />;
}

return <LoadedFilterDialogContent {...restProps} descriptor={loadedDescriptor} />;
if (!loadedPropertiesSource) {
return null;
}

return <LoadedFilterDialogContent {...restProps} descriptor={loadedPropertiesSource.descriptor} descriptorInputKeys={loadedPropertiesSource.inputKeys} />;
}

function useDelayLoadedDescriptor(descriptorOrGetter: Descriptor | (() => Promise<Descriptor>)) {
const [descriptor, setDescriptor] = useState<Descriptor | undefined>(() => (descriptorOrGetter instanceof Descriptor ? descriptorOrGetter : undefined));
function useDelayLoadedPropertiesSource(
sourceOrGetter: PresentationInstanceFilterPropertiesSource | (() => Promise<PresentationInstanceFilterPropertiesSource>) | undefined,
): {
propertiesSource: PresentationInstanceFilterPropertiesSource | undefined;
isLoading: boolean;
} {
const [{ source, isLoading }, setState] = useState(() =>
typeof sourceOrGetter === "function"
? {
source: undefined,
isLoading: false,
}
: {
source: sourceOrGetter,
isLoading: false,
},
);

useEffect(() => {
let disposed = false;

if (descriptorOrGetter instanceof Descriptor) {
setDescriptor(descriptorOrGetter);
} else {
const updateState = (...params: Parameters<typeof setDescriptor>) => {
// istanbul ignore else
if (!disposed) {
setDescriptor(...params);
}
};

void (async () => {
try {
const newDescriptor = await descriptorOrGetter();
updateState(newDescriptor);
} catch (error) {
updateState(() => {
// throw error in setSate callback for it to be caught by ErrorBoundary
throw error;
});
}
})();
if (typeof sourceOrGetter !== "function") {
setState({ source: sourceOrGetter, isLoading: false });
return;
}

const updateState = (...params: Parameters<typeof setState>) => {
// istanbul ignore else
if (!disposed) {
setState(...params);
}
};

updateState({ source: undefined, isLoading: true });

void (async () => {
try {
const newDescriptor = await sourceOrGetter();
updateState({
source: newDescriptor,
isLoading: false,
});
} catch (error) {
updateState(() => {
// throw error in setSate callback for it to be caught by ErrorBoundary
throw error;
});
}
})();

return () => {
disposed = true;
};
}, [descriptorOrGetter]);
}, [sourceOrGetter]);

return descriptor;
return { propertiesSource: source, isLoading };
}

interface LoadedFilterDialogContentProps extends Omit<PresentationInstanceFilterDialogProps, "isOpen" | "title" | "descriptor"> {
interface LoadedFilterDialogContentProps extends Omit<PresentationInstanceFilterDialogProps, "isOpen" | "title" | "propertiesSource"> {
descriptor: Descriptor;
descriptorInputKeys?: Keys;
}

function LoadedFilterDialogContent(props: LoadedFilterDialogContentProps) {
const {
initialFilter,
descriptor,
imodel,
ruleGroupDepthLimit,
filterResultsCountRenderer,
descriptorInputKeys,
onApply,
onReset,
onClose,
toolbarButtonsRenderer,
} = props;
const { initialFilter, descriptor, imodel, filterResultsCountRenderer, descriptorInputKeys, onApply, onReset, onClose, toolbarButtonsRenderer } = props;
const [initialPropertyFilter] = useState(() => {
if (!initialFilter?.filter) {
return undefined;
Expand Down Expand Up @@ -232,7 +259,6 @@ function LoadedFilterDialogContent(props: LoadedFilterDialogContentProps) {
onSelectedClassesChanged={onSelectedClassesChanged}
rootGroup={rootGroup}
actions={actions}
ruleGroupDepthLimit={ruleGroupDepthLimit}
imodel={imodel}
descriptor={descriptor}
descriptorInputKeys={descriptorInputKeys}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,17 +120,33 @@ interface TreeNodeFilterBuilderDialogProps {
function TreeNodeFilterBuilderDialog(props: TreeNodeFilterBuilderDialogProps) {
const { filterNode, dataProvider, ...restProps } = props;
const filteringInfo = filterNode.filtering;
const descriptorInputKeys = useMemo(() => [filterNode.key], [filterNode.key]);
const imodel = dataProvider.imodel;

const propertiesSource = useMemo(() => {
if (typeof filteringInfo.descriptor === "function") {
const descriptorGetter = filteringInfo.descriptor;
return async () => {
const descriptor = await descriptorGetter();
return {
descriptor,
inputKeys: [filterNode.key],
};
};
}

return {
descriptor: filteringInfo.descriptor,
inputKeys: [filterNode.key],
};
}, [filteringInfo.descriptor, filterNode.key]);

return (
<PresentationInstanceFilterDialog
{...restProps}
isOpen={true}
imodel={imodel}
descriptor={filteringInfo.descriptor}
propertiesSource={propertiesSource}
initialFilter={filteringInfo.active}
descriptorInputKeys={descriptorInputKeys}
filterResultsCountRenderer={(filter) => <MatchingInstancesCount dataProvider={dataProvider} filter={filter} parentKey={filterNode.key} />}
/>
);
Expand Down
Loading

0 comments on commit 115d815

Please sign in to comment.