@itwin/presentation-components
Version:
React components based on iTwin.js Presentation library
261 lines • 8.73 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 Core
*/
import "../common/DisposePolyfill.js";
import * as mm from "micro-memoize";
import { useCallback, useEffect, useState } from "react";
import { PropertyRecord, PropertyValueFormat } from "@itwin/appui-abstract";
import { Guid } from "@itwin/core-bentley";
import { KeySet, LabelDefinition, parseCombinedFieldNames } from "@itwin/presentation-common";
import { createSelectionScopeProps, Presentation } from "@itwin/presentation-frontend";
import { Selectables } from "@itwin/unified-selection";
/** @internal */
export const localizationNamespaceName = "PresentationComponents";
/**
* Translate a string with the specified id from `PresentationComponents`
* localization namespace. The `stringId` should not contain namespace - it's
* prepended automatically.
*
* @internal
*/
export const translate = (stringId, options) => {
stringId = `${localizationNamespaceName}:${stringId}`;
return Presentation.localization.getLocalizedString(stringId, options);
};
/**
* Creates a display name for the supplied component
* @internal
*/
export const getDisplayName = (component) => {
if (component.displayName) {
return component.displayName;
}
if (component.name) {
return component.name;
}
return "Component";
};
/**
* Finds a field given the name of property record created from that field.
* @internal
*/
export const findField = (descriptor, recordPropertyName) => {
let fieldsSource = descriptor;
const fieldNames = parseCombinedFieldNames(recordPropertyName);
while (fieldsSource && fieldNames.length) {
const field = fieldsSource.getFieldByName(fieldNames.shift());
fieldsSource = field && field.isNestedContentField() ? field : undefined;
if (!fieldNames.length) {
return field;
}
}
return undefined;
};
/**
* Creates property record for label using label definition.
* @internal
*/
export const createLabelRecord = (label, name) => {
const value = {
displayValue: label.displayValue,
value: createPrimitiveLabelValue(label),
valueFormat: PropertyValueFormat.Primitive,
};
const property = {
displayLabel: "Label",
typename: label.typeName,
name,
};
return new PropertyRecord(value, property);
};
const createPrimitiveLabelValue = (label) => {
return LabelDefinition.isCompositeDefinition(label) ? createPrimitiveCompositeValue(label.rawValue) : label.rawValue;
};
const createPrimitiveCompositeValue = (compositeValue) => {
return {
separator: compositeValue.separator,
parts: compositeValue.values.map((part) => ({
displayValue: part.displayValue,
typeName: part.typeName,
rawValue: createPrimitiveLabelValue(part),
})),
};
};
/**
* Returns ruleset id from `RulesetOrId`.
* @internal
*/
export function getRulesetId(ruleset) {
return typeof ruleset === "string" ? ruleset : ruleset.id;
}
/**
* A helper to track ongoing async tasks. Usage:
* ```
* {
* using _r = tracker.trackAsyncTask();
* await doSomethingAsync();
* }
* ```
*
* Can be used with `waitForPendingAsyncs` in test helpers to wait for all
* async tasks to complete.
*
* @internal
*/
export class AsyncTasksTracker {
_asyncsInProgress = new Set();
get pendingAsyncs() {
return this._asyncsInProgress;
}
trackAsyncTask() {
const id = Guid.createValue();
this._asyncsInProgress.add(id);
return {
[Symbol.dispose]: () => this._asyncsInProgress.delete(id),
};
}
}
/** @internal */
/* c8 ignore start */
export function useMergedRefs(...refs) {
return useCallback((instance) => {
refs.forEach((ref) => {
if (typeof ref === "function") {
ref(instance);
}
else if (ref) {
ref.current = instance;
}
});
}, [...refs]);
}
/* c8 ignore end */
/**
* A hook that helps components throw errors in React's render loop so they can be captured by React error
* boundaries.
*
* Usage: simply call the returned function with an error and it will be re-thrown in React render loop.
*
* @internal
*/
export function useErrorState() {
const [_, setError] = useState(undefined);
const setErrorState = useCallback((e) => {
setError(() => {
throw e instanceof Error ? e : /* c8 ignore next */ new Error();
});
}, []);
return setErrorState;
}
/**
* A hook that rerenders component after some time.
* @param delayMilliseconds - milliseconds to delay. Default is 250.
* @internal
*/
export function useDelay(delayMilliseconds = 250) {
const [passed, setPassed] = useState(false);
useEffect(() => {
const timeout = setTimeout(() => {
setPassed(true);
}, delayMilliseconds);
return () => {
clearTimeout(timeout);
};
}, [delayMilliseconds]);
return passed;
}
/**
* Function for serializing `UniqueValue`.
* Returns an object, which consists of `displayValues` and `groupedRawValues`.
*/
export function serializeUniqueValues(values) {
const displayValues = [];
const groupedRawValues = {};
values.forEach((item) => {
displayValues.push(item.displayValue);
groupedRawValues[item.displayValue] = [...item.groupedRawValues];
});
return { displayValues: JSON.stringify(displayValues), groupedRawValues: JSON.stringify(groupedRawValues) };
}
/**
* Function for deserializing `displayValues` and `groupedRawValues`.
* Returns an array of `UniqueValue` or undefined if parsing fails.
*/
export function deserializeUniqueValues(serializedDisplayValues, serializedGroupedRawValues) {
const tryParseJSON = (value) => {
try {
return JSON.parse(value);
}
catch {
return false;
}
};
const displayValues = tryParseJSON(serializedDisplayValues);
const groupedRawValues = tryParseJSON(serializedGroupedRawValues);
if (!displayValues || !groupedRawValues || !Array.isArray(displayValues) || Object.keys(groupedRawValues).length !== displayValues.length) {
return undefined;
}
const uniqueValues = [];
for (const displayValue of displayValues) {
uniqueValues.push({ displayValue, groupedRawValues: groupedRawValues[displayValue] });
}
return uniqueValues;
}
export function memoize(fn, options) {
const microMemoize = mm.default;
return microMemoize(fn, options);
}
export async function createKeySetFromSelectables(selectables) {
const keys = new KeySet();
for await (const instanceKey of Selectables.load(selectables)) {
keys.add(instanceKey);
}
return keys;
}
export function mapPresentationFrontendSelectionScopeToUnifiedSelectionScope(
// eslint-disable-next-line @typescript-eslint/no-deprecated
scope) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
const scopeProps = createSelectionScopeProps(scope);
switch (scopeProps.id) {
case "functional-element":
return { id: "functional" };
case "functional-assembly":
return { id: "functional", ancestorLevel: 1 };
case "functional-top-assembly":
return { id: "functional", ancestorLevel: -1 };
case "element":
return { id: "element" };
case "assembly":
return { id: "element", ancestorLevel: 1 };
case "top-assembly":
return { id: "element", ancestorLevel: -1 };
case "category":
return { id: "category" };
case "model":
return { id: "model" };
}
throw new Error(`Unknown selection scope: "${scopeProps.id}"`);
}
/**
* A helper that disposes the given object, if it's disposable.
*
* The first option is to dispose using the deprecated `dispose` method if it exists on the object.
* If not, we use the new `Symbol.dispose` method. If that doesn't exist either, the object is
* considered as non-disposable and nothing is done with it.
*
* @internal
*/
export function safeDispose(disposable) {
if ("dispose" in disposable) {
disposable.dispose();
}
else if (Symbol.dispose in disposable) {
disposable[Symbol.dispose]();
}
}
//# sourceMappingURL=Utils.js.map