UNPKG

@itwin/presentation-components

Version:

React components based on iTwin.js Presentation library

261 lines 8.73 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 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