@medplum/react-hooks
Version:
Medplum React Hooks Library
4 lines • 80.4 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../../src/index.ts", "../../src/MedplumProvider/MedplumProvider.tsx", "../../src/MedplumProvider/MedplumProvider.context.ts", "../../src/useCachedBinaryUrl/useCachedBinaryUrl.ts", "../../src/usePrevious/usePrevious.ts", "../../src/useQuestionnaireForm/useQuestionnaireForm.ts", "../../src/useResource/useResource.ts", "../../src/useQuestionnaireForm/utils.ts", "../../src/useSearch/useSearch.ts", "../../src/useDebouncedValue/useDebouncedValue.ts", "../../src/useSubscription/useSubscription.ts"],
"sourcesContent": ["// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors\n// SPDX-License-Identifier: Apache-2.0\nexport * from './MedplumProvider/MedplumProvider';\nexport * from './MedplumProvider/MedplumProvider.context';\nexport * from './useCachedBinaryUrl/useCachedBinaryUrl';\nexport * from './usePrevious/usePrevious';\nexport * from './useQuestionnaireForm/useQuestionnaireForm';\nexport * from './useQuestionnaireForm/utils';\nexport * from './useResource/useResource';\nexport * from './useSearch/useSearch';\nexport * from './useSubscription/useSubscription';\n", "// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors\n// SPDX-License-Identifier: Apache-2.0\nimport { MedplumClient, MedplumClientEventMap } from '@medplum/core';\nimport { JSX, ReactNode, useEffect, useMemo, useState } from 'react';\nimport { MedplumNavigateFunction, reactContext } from './MedplumProvider.context';\n\nexport interface MedplumProviderProps {\n readonly medplum: MedplumClient;\n readonly navigate?: MedplumNavigateFunction;\n readonly children: ReactNode;\n}\n\nconst EVENTS_TO_TRACK = [\n 'change',\n 'storageInitialized',\n 'storageInitFailed',\n 'profileRefreshing',\n 'profileRefreshed',\n] satisfies (keyof MedplumClientEventMap)[];\n\n/**\n * The MedplumProvider component provides Medplum context state.\n *\n * Medplum context includes:\n * 1) medplum - Medplum client library\n * 2) profile - The current user profile (if signed in)\n * @param props - The MedplumProvider React props.\n * @returns The MedplumProvider React node.\n */\nexport function MedplumProvider(props: MedplumProviderProps): JSX.Element {\n const medplum = props.medplum;\n const navigate = props.navigate ?? defaultNavigate;\n\n const [state, setState] = useState({\n profile: medplum.getProfile(),\n loading: medplum.isLoading(),\n });\n\n useEffect(() => {\n function eventListener(): void {\n setState((s) => ({\n ...s,\n profile: medplum.getProfile(),\n loading: medplum.isLoading(),\n }));\n }\n\n for (const event of EVENTS_TO_TRACK) {\n medplum.addEventListener(event, eventListener);\n }\n return () => {\n for (const event of EVENTS_TO_TRACK) {\n medplum.removeEventListener(event, eventListener);\n }\n };\n }, [medplum]);\n\n const medplumContext = useMemo(\n () => ({\n ...state,\n medplum,\n navigate,\n }),\n [state, medplum, navigate]\n );\n\n return <reactContext.Provider value={medplumContext}>{props.children}</reactContext.Provider>;\n}\n\n/**\n * The default \"navigate\" function which simply uses window.location.href.\n * @param path - The path to navigate to.\n */\nfunction defaultNavigate(path: string): void {\n window.location.assign(path);\n}\n", "// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors\n// SPDX-License-Identifier: Apache-2.0\nimport { MedplumClient, ProfileResource } from '@medplum/core';\nimport { createContext, useContext } from 'react';\n\nexport const reactContext = createContext(undefined as MedplumContext | undefined);\n\nexport type MedplumNavigateFunction = (path: string) => void;\n\nexport interface MedplumContext {\n medplum: MedplumClient;\n navigate: MedplumNavigateFunction;\n profile?: ProfileResource;\n loading: boolean;\n}\n\n/**\n * Returns the MedplumContext instance.\n * @returns The MedplumContext instance.\n */\nexport function useMedplumContext(): MedplumContext {\n return useContext(reactContext) as MedplumContext;\n}\n\n/**\n * Returns the MedplumClient instance.\n * This is a shortcut for useMedplumContext().medplum.\n * @returns The MedplumClient instance.\n */\nexport function useMedplum(): MedplumClient {\n return useMedplumContext().medplum;\n}\n\n/**\n * Returns the Medplum navigate function.\n * @returns The Medplum navigate function.\n */\nexport function useMedplumNavigate(): MedplumNavigateFunction {\n return useMedplumContext().navigate;\n}\n\n/**\n * Returns the current Medplum user profile (if signed in).\n * This is a shortcut for useMedplumContext().profile.\n * @returns The current user profile.\n */\nexport function useMedplumProfile(): ProfileResource | undefined {\n return useMedplumContext().profile;\n}\n", "// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors\n// SPDX-License-Identifier: Apache-2.0\nimport { useMemo } from 'react';\n\n// Maintain a cache of urls to avoid unnecessary re-download of attachments\n// The following is a workaround for the fact that each request to a resource containing a Binary data reference\n// returns a NEW signed S3 URL for each bypassing the native browser caching mechanism\n// resulting in unnecessary bandwidth consumption.\n// https://www.medplum.com/docs/fhir-datastore/binary-data#consuming-a-fhir-binary-in-an-application\n// https://github.com/medplum/medplum/issues/3815\n\n// The S3 presigned URLs expire after 1 hour with the default configuration and hard refreshes are not uncommon even in SPAs so this\n// could be a good way to get additional cache hits\n// This would require additional logic for initialization, saving, and purging of expired keys\nconst urls = new Map<string, string>();\n\nexport const useCachedBinaryUrl = (binaryUrl: string | undefined): string | undefined => {\n return useMemo(() => {\n if (!binaryUrl) {\n return undefined;\n }\n\n const binaryResourceUrl = binaryUrl.split('?')[0];\n if (!binaryResourceUrl) {\n return binaryUrl;\n }\n\n // Check if the binaryUrl is a presigned S3 URL\n let binaryUrlSearchParams: URLSearchParams;\n try {\n binaryUrlSearchParams = new URLSearchParams(new URL(binaryUrl).search);\n } catch (_err) {\n return binaryUrl;\n }\n\n if (!binaryUrlSearchParams.has('Key-Pair-Id') || !binaryUrlSearchParams.has('Signature')) {\n return binaryUrl;\n }\n\n // https://stackoverflow.com/questions/23929145/how-to-test-if-a-given-time-stamp-is-in-seconds-or-milliseconds\n const binaryUrlExpires = binaryUrlSearchParams.get('Expires');\n if (!binaryUrlExpires || binaryUrlExpires.length > 13) {\n // Expires is expected to be in seconds, not milliseconds\n return binaryUrl;\n }\n\n const cachedUrl = urls.get(binaryResourceUrl);\n if (cachedUrl) {\n const searchParams = new URLSearchParams(new URL(cachedUrl).search);\n\n // This is fairly brittle as it relies on the current structure of the Medplum returned URL\n const expires = searchParams.get('Expires');\n\n // `expires` is in seconds, Date.now() is in ms\n // Add padding to mitigate expiration between time of check and time of use\n if (expires && parseInt(expires, 10) * 1000 - 5_000 > Date.now()) {\n return cachedUrl;\n }\n }\n\n urls.set(binaryResourceUrl, binaryUrl);\n return binaryUrl;\n }, [binaryUrl]);\n};\n", "// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors\n// SPDX-License-Identifier: Apache-2.0\nimport { useEffect, useRef } from 'react';\n\n/**\n * React Hook to keep track of the passed-in value from the previous render of the containing component.\n * @param value - The value to track.\n * @returns The value passed in from the previous render.\n */\nexport function usePrevious<T>(value: T): T | undefined {\n const ref = useRef<T>(undefined);\n useEffect(() => {\n ref.current = value;\n });\n return ref.current;\n}\n", "// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors\n// SPDX-License-Identifier: Apache-2.0\nimport { getExtension } from '@medplum/core';\nimport {\n Encounter,\n Questionnaire,\n QuestionnaireItem,\n QuestionnaireResponse,\n QuestionnaireResponseItem,\n QuestionnaireResponseItemAnswer,\n Reference,\n Signature,\n} from '@medplum/fhirtypes';\nimport { useReducer, useRef } from 'react';\nimport { useResource } from '../useResource/useResource';\nimport {\n buildInitialResponse,\n buildInitialResponseItem,\n evaluateCalculatedExpressionsInQuestionnaire,\n QUESTIONNAIRE_ITEM_CONTROL_URL,\n QUESTIONNAIRE_SIGNATURE_RESPONSE_URL,\n} from './utils';\n\n// React Hook for Questionnaire Form\n\n// Why is this hard?\n// 1. It needs to handle both initial loading of a questionnaire and updating the response as the user interacts with it.\n// 2. It needs to support pagination and navigation through the questionnaire.\n// 3. It needs to handle complex items like groups and repeatable items.\n\n// Conventions we use:\n// 1. We use `QuestionnaireResponse` to track the user's answers.\n// 2. We use `QuestionnaireItem` to define the structure of the questionnaire.\n// 3. Response items are linked to their corresponding questionnaire items by `linkId`.\n// 4. Response items will always have a `linkId` that matches the `linkId` of the questionnaire item they correspond to.\n// 5. Response items will also always have an `id` that is unique within the response, which can be used to track changes to individual items.\n// 6. Pagination is enabled by default, so current state items will only include items for the current page.\n// 7. If Pagination is disabled, all items will be included in the current state items.\n\nexport interface UseQuestionnaireFormProps {\n readonly questionnaire: Questionnaire | Reference<Questionnaire>;\n readonly defaultValue?: QuestionnaireResponse | Reference<QuestionnaireResponse>;\n readonly subject?: Reference;\n readonly encounter?: Reference<Encounter>;\n readonly source?: QuestionnaireResponse['source'];\n readonly disablePagination?: boolean;\n readonly onChange?: (response: QuestionnaireResponse) => void;\n}\n\nexport interface QuestionnaireFormPage {\n readonly linkId: string;\n readonly title: string;\n readonly group: QuestionnaireItem & { type: 'group' };\n}\n\nexport interface QuestionnaireFormLoadingState {\n /** Currently loading data such as the Questionnaire or the QuestionnaireResponse default value */\n readonly loading: true;\n}\n\nexport interface QuestionnaireFormLoadedState {\n /** Not loading */\n readonly loading: false;\n\n /** The loaded questionnaire */\n questionnaire: Questionnaire;\n\n /** The current draft questionnaire response */\n questionnaireResponse: QuestionnaireResponse;\n\n /** Optional questionnaire subject */\n subject?: Reference;\n\n /** Optional questionnaire encounter */\n encounter?: Reference<Encounter>;\n\n /** The top level items for the current page */\n items: QuestionnaireItem[];\n\n /** The response items for the current page */\n responseItems: QuestionnaireResponseItem[];\n\n /**\n * Adds a new group item to the current context.\n * @param context - The current context of the questionnaire response items.\n * @param item - The questionnaire item that is being added to the group.\n */\n onAddGroup: (context: QuestionnaireResponseItem[], item: QuestionnaireItem) => void;\n\n /**\n * Adds an answer to a repeating item.\n * @param context - The current context of the questionnaire response items.\n * @param item - The questionnaire item that is being answered.\n */\n onAddAnswer: (context: QuestionnaireResponseItem[], item: QuestionnaireItem) => void;\n\n /**\n * Changes an answer value.\n * @param context - The current context of the questionnaire response items.\n * @param item - The questionnaire item that is being answered.\n * @param answer - The answer(s) provided by the user for the questionnaire item.\n */\n onChangeAnswer: (\n context: QuestionnaireResponseItem[],\n item: QuestionnaireItem,\n answer: QuestionnaireResponseItemAnswer[]\n ) => void;\n\n /**\n * Sets or updates the signature for the questionnaire response.\n * @param signature - The signature to set, or undefined to clear the signature.\n */\n onChangeSignature: (signature: Signature | undefined) => void;\n}\n\nexport interface QuestionnaireFormSinglePageState extends QuestionnaireFormLoadedState {\n readonly pagination: false;\n}\n\nexport interface QuestionnaireFormPaginationState extends QuestionnaireFormLoadedState {\n readonly pagination: true;\n pages: QuestionnaireFormPage[];\n activePage: number;\n onNextPage: () => void;\n onPrevPage: () => void;\n}\n\nexport type QuestionnaireFormState =\n | QuestionnaireFormLoadingState\n | QuestionnaireFormSinglePageState\n | QuestionnaireFormPaginationState;\n\nexport function useQuestionnaireForm(props: UseQuestionnaireFormProps): Readonly<QuestionnaireFormState> {\n const questionnaire = useResource(props.questionnaire);\n const defaultResponse = useResource(props.defaultValue);\n const [, forceUpdate] = useReducer((x) => x + 1, 0);\n\n const state = useRef<Partial<QuestionnaireFormPaginationState>>({\n activePage: 0,\n });\n\n // If the questionnaire is loaded, we will set the current questionnaire and pages.\n if (!state.current.questionnaire && questionnaire) {\n state.current.questionnaire = questionnaire;\n state.current.pages = props.disablePagination ? undefined : getPages(questionnaire);\n }\n\n // If we are expecting a questionnaire response, and it is loaded, then use it.\n if (questionnaire && props.defaultValue && defaultResponse && !state.current.questionnaireResponse) {\n state.current.questionnaireResponse = buildInitialResponse(questionnaire, defaultResponse);\n emitChange();\n }\n\n // If we are not expecting a questionnaire response, we will create a new one.\n if (questionnaire && !props.defaultValue && !state.current.questionnaireResponse) {\n state.current.questionnaireResponse = buildInitialResponse(questionnaire);\n emitChange();\n }\n\n if (!state.current.questionnaire || !state.current.questionnaireResponse) {\n return { loading: true };\n }\n\n function getResponseItemByContext(\n context: QuestionnaireResponseItem[]\n ): QuestionnaireResponse | QuestionnaireResponseItem | undefined;\n function getResponseItemByContext(\n context: QuestionnaireResponseItem[],\n item?: QuestionnaireItem\n ): QuestionnaireResponseItem | undefined;\n function getResponseItemByContext(\n context: QuestionnaireResponseItem[],\n item?: QuestionnaireItem\n ): QuestionnaireResponse | QuestionnaireResponseItem | undefined {\n let currentItem: QuestionnaireResponse | QuestionnaireResponseItem | undefined =\n state.current.questionnaireResponse;\n for (const contextElement of context) {\n currentItem = currentItem?.item?.find((i) =>\n contextElement.id ? i.id === contextElement.id : i.linkId === contextElement.linkId\n );\n }\n if (item) {\n currentItem = currentItem?.item?.find((i) => i.linkId === item.linkId);\n }\n return currentItem;\n }\n\n function onNextPage(): void {\n state.current.activePage = (state.current.activePage ?? 0) + 1;\n forceUpdate();\n }\n\n function onPrevPage(): void {\n state.current.activePage = (state.current.activePage ?? 0) - 1;\n forceUpdate();\n }\n\n function onAddGroup(context: QuestionnaireResponseItem[], item: QuestionnaireItem): void {\n const responseItem = getResponseItemByContext(context);\n if (responseItem) {\n responseItem.item ??= [];\n responseItem.item.push(buildInitialResponseItem(item));\n emitChange();\n }\n }\n\n function onAddAnswer(context: QuestionnaireResponseItem[], item: QuestionnaireItem): void {\n const currentItem = getResponseItemByContext(context, item);\n if (currentItem) {\n currentItem.answer ??= [];\n currentItem.answer.push({});\n emitChange();\n }\n }\n\n function onChangeAnswer(\n context: QuestionnaireResponseItem[],\n item: QuestionnaireItem,\n answer: QuestionnaireResponseItemAnswer[]\n ): void {\n const currentItem = getResponseItemByContext(context, item);\n if (currentItem) {\n currentItem.answer = answer;\n emitChange();\n }\n }\n\n function onChangeSignature(signature: Signature | undefined): void {\n const currentResponse = state.current.questionnaireResponse;\n if (!currentResponse) {\n return;\n }\n if (signature) {\n currentResponse.extension = currentResponse.extension ?? [];\n currentResponse.extension = currentResponse.extension.filter(\n (ext) => ext.url !== QUESTIONNAIRE_SIGNATURE_RESPONSE_URL\n );\n currentResponse.extension.push({\n url: QUESTIONNAIRE_SIGNATURE_RESPONSE_URL,\n valueSignature: signature,\n });\n } else {\n currentResponse.extension = currentResponse.extension?.filter(\n (ext) => ext.url !== QUESTIONNAIRE_SIGNATURE_RESPONSE_URL\n );\n }\n emitChange();\n }\n\n function updateCalculatedExpressions(): void {\n const questionnaire = state.current.questionnaire;\n if (questionnaire?.item) {\n const response = state.current.questionnaireResponse as QuestionnaireResponse;\n evaluateCalculatedExpressionsInQuestionnaire(questionnaire.item, response);\n }\n }\n\n function emitChange(): void {\n const currentResponse = state.current.questionnaireResponse;\n if (!currentResponse) {\n return;\n }\n updateCalculatedExpressions();\n forceUpdate();\n props.onChange?.(currentResponse);\n }\n\n return {\n loading: false,\n pagination: !!state.current.pages,\n questionnaire: state.current.questionnaire,\n questionnaireResponse: state.current.questionnaireResponse,\n subject: props.subject,\n encounter: props.encounter,\n activePage: state.current.activePage,\n pages: state.current.pages,\n items: getItemsForPage(state.current.questionnaire, state.current.pages, state.current.activePage),\n responseItems: getResponseItemsForPage(\n state.current.questionnaireResponse,\n state.current.pages,\n state.current.activePage\n ),\n onNextPage,\n onPrevPage,\n onAddGroup,\n onAddAnswer,\n onChangeAnswer,\n onChangeSignature,\n } as QuestionnaireFormSinglePageState | QuestionnaireFormPaginationState;\n}\n\nfunction getPages(questionnaire: Questionnaire): QuestionnaireFormPage[] | undefined {\n if (!questionnaire?.item) {\n return undefined;\n }\n const extension = getExtension(questionnaire?.item?.[0], QUESTIONNAIRE_ITEM_CONTROL_URL);\n if (extension?.valueCodeableConcept?.coding?.[0]?.code !== 'page') {\n return undefined;\n }\n\n return questionnaire.item.map((item, index) => {\n return {\n linkId: item.linkId,\n title: item.text ?? `Page ${index + 1}`,\n group: item as QuestionnaireItem & { type: 'group' },\n };\n });\n}\n\nfunction getItemsForPage(\n questionnaire: Questionnaire,\n pages: QuestionnaireFormPage[] | undefined,\n activePage = 0\n): QuestionnaireItem[] {\n if (pages && questionnaire?.item?.[activePage]) {\n return [questionnaire.item[activePage]];\n }\n return questionnaire.item ?? [];\n}\n\nfunction getResponseItemsForPage(\n questionnaireResponse: QuestionnaireResponse,\n pages: QuestionnaireFormPage[] | undefined,\n activePage = 0\n): QuestionnaireResponseItem[] {\n if (pages && questionnaireResponse?.item?.[activePage]) {\n return [questionnaireResponse.item[activePage]];\n }\n return questionnaireResponse.item ?? [];\n}\n", "// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors\n// SPDX-License-Identifier: Apache-2.0\nimport { deepEquals, isReference, isResource, MedplumClient, normalizeOperationOutcome } from '@medplum/core';\nimport { OperationOutcome, Reference, Resource } from '@medplum/fhirtypes';\nimport { useCallback, useEffect, useState } from 'react';\nimport { useMedplum } from '../MedplumProvider/MedplumProvider.context';\n\n/**\n * React Hook to use a FHIR reference.\n * Handles the complexity of resolving references and caching resources.\n * @param value - The resource or reference to resource.\n * @param setOutcome - Optional callback to set the OperationOutcome.\n * @returns The resolved resource.\n */\nexport function useResource<T extends Resource>(\n value: Reference<T> | Partial<T> | undefined,\n setOutcome?: (outcome: OperationOutcome) => void\n): T | undefined {\n const medplum = useMedplum();\n const [resource, setResource] = useState<T | undefined>(() => {\n return getInitialResource(medplum, value);\n });\n\n const setResourceIfChanged = useCallback(\n (r: T | undefined) => {\n if (!deepEquals(r, resource)) {\n setResource(r);\n }\n },\n [resource]\n );\n\n useEffect(() => {\n let subscribed = true;\n\n const newValue = getInitialResource(medplum, value);\n if (!newValue && isReference(value)) {\n medplum\n .readReference(value as Reference<T>)\n .then((r) => {\n if (subscribed) {\n setResourceIfChanged(r);\n }\n })\n .catch((err) => {\n if (subscribed) {\n setResourceIfChanged(undefined);\n if (setOutcome) {\n setOutcome(normalizeOperationOutcome(err));\n }\n }\n });\n } else {\n setResourceIfChanged(newValue);\n }\n\n return (() => (subscribed = false)) as () => void;\n }, [medplum, value, setResourceIfChanged, setOutcome]);\n\n return resource;\n}\n\n/**\n * Returns the initial resource value based on the input value.\n * If the input value is a resource, returns the resource.\n * If the input value is a reference to a resource available in the cache, returns the resource.\n * Otherwise, returns undefined.\n * @param medplum - The medplum client.\n * @param value - The resource or reference to resource.\n * @returns An initial resource if available; undefined otherwise.\n */\nfunction getInitialResource<T extends Resource>(\n medplum: MedplumClient,\n value: Reference<T> | Partial<T> | undefined\n): T | undefined {\n if (value) {\n if (isResource(value)) {\n return value as T;\n }\n\n if (isReference(value)) {\n return medplum.getCachedReference(value as Reference<T>);\n }\n }\n\n return undefined;\n}\n", "// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors\n// SPDX-License-Identifier: Apache-2.0\nimport {\n HTTP_HL7_ORG,\n PropertyType,\n TypedValue,\n capitalize,\n deepClone,\n evalFhirPathTyped,\n getExtension,\n getReferenceString,\n getTypedPropertyValueWithoutSchema,\n normalizeErrorString,\n splitN,\n toJsBoolean,\n toTypedValue,\n typedValueToString,\n} from '@medplum/core';\nimport {\n Encounter,\n Questionnaire,\n QuestionnaireItem,\n QuestionnaireItemAnswerOption,\n QuestionnaireItemEnableWhen,\n QuestionnaireItemInitial,\n QuestionnaireResponse,\n QuestionnaireResponseItem,\n QuestionnaireResponseItemAnswer,\n Reference,\n ResourceType,\n} from '@medplum/fhirtypes';\n\nexport const QuestionnaireItemType = {\n group: 'group',\n display: 'display',\n question: 'question',\n boolean: 'boolean',\n decimal: 'decimal',\n integer: 'integer',\n date: 'date',\n dateTime: 'dateTime',\n time: 'time',\n string: 'string',\n text: 'text',\n url: 'url',\n choice: 'choice',\n openChoice: 'open-choice',\n attachment: 'attachment',\n reference: 'reference',\n quantity: 'quantity',\n} as const;\nexport type QuestionnaireItemType = (typeof QuestionnaireItemType)[keyof typeof QuestionnaireItemType];\n\nexport const QUESTIONNAIRE_ITEM_CONTROL_URL = `${HTTP_HL7_ORG}/fhir/StructureDefinition/questionnaire-itemControl`;\nexport const QUESTIONNAIRE_REFERENCE_FILTER_URL = `${HTTP_HL7_ORG}/fhir/StructureDefinition/questionnaire-referenceFilter`;\nexport const QUESTIONNAIRE_REFERENCE_RESOURCE_URL = `${HTTP_HL7_ORG}/fhir/StructureDefinition/questionnaire-referenceResource`;\nexport const QUESTIONNAIRE_VALIDATION_ERROR_URL = `${HTTP_HL7_ORG}/fhir/StructureDefinition/questionnaire-validationError`;\nexport const QUESTIONNAIRE_ENABLED_WHEN_EXPRESSION_URL = `${HTTP_HL7_ORG}/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression`;\nexport const QUESTIONNAIRE_CALCULATED_EXPRESSION_URL = `${HTTP_HL7_ORG}/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression`;\nexport const QUESTIONNAIRE_SIGNATURE_REQUIRED_URL = `${HTTP_HL7_ORG}/fhir/StructureDefinition/questionnaire-signatureRequired`;\nexport const QUESTIONNAIRE_SIGNATURE_RESPONSE_URL = `${HTTP_HL7_ORG}/fhir/StructureDefinition/questionnaireresponse-signature`;\n\n/**\n * Returns true if the item is a choice question.\n * @param item - The questionnaire item to check.\n * @returns True if the item is a choice question, false otherwise.\n */\nexport function isChoiceQuestion(item: QuestionnaireItem): boolean {\n return item.type === 'choice' || item.type === 'open-choice';\n}\n\n/**\n * Returns true if the questionnaire item is enabled based on the enableWhen conditions or expression.\n * @param item - The questionnaire item to check.\n * @param questionnaireResponse - The questionnaire response to check against.\n * @returns True if the question is enabled, false otherwise.\n */\nexport function isQuestionEnabled(\n item: QuestionnaireItem,\n questionnaireResponse: QuestionnaireResponse | undefined\n): boolean {\n const extensionResult = isQuestionEnabledViaExtension(item, questionnaireResponse);\n if (extensionResult !== undefined) {\n return extensionResult;\n }\n return isQuestionEnabledViaEnabledWhen(item, questionnaireResponse);\n}\n\n/**\n * Returns true if the questionnaire item is enabled via an extension expression.\n *\n * An expression that returns a boolean value for whether to enable the item.\n * If the expression does not resolve to a boolean, it is considered an error in the design of the Questionnaire.\n * Form renderer behavior is undefined.\n * Some tools may attempt to force the value to be a boolean (e.g. is it a non-empty collection, non-null, non-zero - if so, then true).\n *\n * See: https://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-enableWhenExpression.html\n *\n * @param item - The questionnaire item to check.\n * @param questionnaireResponse - The questionnaire response to check against.\n * @returns True if the question is enabled via an extension expression, false otherwise.\n */\nfunction isQuestionEnabledViaExtension(\n item: QuestionnaireItem,\n questionnaireResponse: QuestionnaireResponse | undefined\n): boolean | undefined {\n const extension = getExtension(item, QUESTIONNAIRE_ENABLED_WHEN_EXPRESSION_URL);\n if (questionnaireResponse && extension) {\n const expression = extension.valueExpression?.expression;\n if (expression) {\n const value = toTypedValue(questionnaireResponse);\n const result = evalFhirPathTyped(expression, [value], { '%resource': value });\n return toJsBoolean(result);\n }\n }\n return undefined;\n}\n\n/**\n * Returns true if the questionnaire item is enabled based on the enableWhen conditions.\n *\n * See: https://hl7.org/fhir/R4/questionnaire-definitions.html#Questionnaire.item.enableWhen\n * See: https://hl7.org/fhir/R4/questionnaire-definitions.html#Questionnaire.item.enableBehavior\n *\n * @param item - The questionnaire item to check.\n * @param questionnaireResponse - The questionnaire response to check against.\n * @returns True if the question is enabled based on the enableWhen conditions, false otherwise.\n */\nfunction isQuestionEnabledViaEnabledWhen(\n item: QuestionnaireItem,\n questionnaireResponse: QuestionnaireResponse | undefined\n): boolean {\n if (!item.enableWhen) {\n return true;\n }\n\n const enableBehavior = item.enableBehavior ?? 'any';\n for (const enableWhen of item.enableWhen) {\n const actualAnswers = getByLinkId(questionnaireResponse?.item, enableWhen.question as string);\n\n if (enableWhen.operator === 'exists' && !enableWhen.answerBoolean && !actualAnswers?.length) {\n if (enableBehavior === 'any') {\n return true;\n } else {\n continue;\n }\n }\n const { anyMatch, allMatch } = checkAnswers(enableWhen, actualAnswers, enableBehavior);\n\n if (enableBehavior === 'any' && anyMatch) {\n return true;\n }\n if (enableBehavior === 'all' && !allMatch) {\n return false;\n }\n }\n\n return enableBehavior !== 'any';\n}\n\n/**\n * Evaluates the calculated expressions in a questionnaire.\n * Updates response item answers in place with the calculated values.\n *\n * See: https://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-calculatedExpression.html\n *\n * @param items - The questionnaire items to evaluate.\n * @param response - The questionnaire response to evaluate against.\n * @param responseItems - The response items to update.\n */\nexport function evaluateCalculatedExpressionsInQuestionnaire(\n items: QuestionnaireItem[],\n response: QuestionnaireResponse,\n responseItems: QuestionnaireResponseItem[] | undefined = response.item\n): void {\n for (const item of items) {\n const responseItem = responseItems?.find((r) => r.linkId === item.linkId);\n if (responseItem) {\n evaluateQuestionnaireItemCalculatedExpressions(response, item, responseItem);\n if (item.item && responseItem.item) {\n // If the item has nested items, evaluate their calculated expressions as well\n evaluateCalculatedExpressionsInQuestionnaire(item.item, response, responseItem.item);\n }\n }\n }\n}\n\nfunction evaluateQuestionnaireItemCalculatedExpressions(\n response: QuestionnaireResponse,\n item: QuestionnaireItem,\n responseItem: QuestionnaireResponseItem\n): void {\n try {\n const calculatedValue = evaluateCalculatedExpression(item, response);\n if (!calculatedValue) {\n return;\n }\n const answer = typedValueToResponseItem(item, calculatedValue);\n if (!answer) {\n return;\n }\n responseItem.answer = [answer];\n } catch (error) {\n responseItem.extension = [\n {\n url: QUESTIONNAIRE_VALIDATION_ERROR_URL,\n valueString: `Expression evaluation failed: ${normalizeErrorString(error)}`,\n },\n ];\n }\n}\n\nconst questionnaireItemTypesAllowedPropertyTypes: Record<string, string[]> = {\n [QuestionnaireItemType.boolean]: [PropertyType.boolean],\n [QuestionnaireItemType.date]: [PropertyType.date],\n [QuestionnaireItemType.dateTime]: [PropertyType.dateTime],\n [QuestionnaireItemType.time]: [PropertyType.time],\n [QuestionnaireItemType.url]: [PropertyType.string, PropertyType.uri, PropertyType.url],\n [QuestionnaireItemType.attachment]: [PropertyType.Attachment],\n [QuestionnaireItemType.reference]: [PropertyType.Reference],\n [QuestionnaireItemType.quantity]: [PropertyType.Quantity],\n [QuestionnaireItemType.decimal]: [PropertyType.decimal, PropertyType.integer],\n [QuestionnaireItemType.integer]: [PropertyType.decimal, PropertyType.integer],\n} as const;\n\nexport function typedValueToResponseItem(\n item: QuestionnaireItem,\n value: TypedValue\n): QuestionnaireResponseItemAnswer | undefined {\n if (!item.type) {\n return undefined;\n }\n if (item.type === QuestionnaireItemType.choice || item.type === QuestionnaireItemType.openChoice) {\n // Choice and open-choice items can have multiple answer options\n return { [`value${capitalize(value.type)}`]: value.value };\n }\n if (item.type === QuestionnaireItemType.string || item.type === QuestionnaireItemType.text) {\n // Always coerce string values to valueString\n if (typeof value.value === 'string') {\n return { valueString: value.value };\n }\n return undefined;\n }\n const allowedPropertyTypes = questionnaireItemTypesAllowedPropertyTypes[item.type];\n if (allowedPropertyTypes?.includes(value.type)) {\n // Use the questionnaire item type to determine the response item type\n return { [`value${capitalize(item.type)}`]: value.value };\n }\n return undefined;\n}\n\nfunction evaluateCalculatedExpression(\n item: QuestionnaireItem,\n response: QuestionnaireResponse | undefined\n): TypedValue | undefined {\n if (!response) {\n return undefined;\n }\n\n const extension = getExtension(item, QUESTIONNAIRE_CALCULATED_EXPRESSION_URL);\n if (extension) {\n const expression = extension.valueExpression?.expression;\n if (expression) {\n const value = toTypedValue(response);\n const result = evalFhirPathTyped(expression, [value], { '%resource': value });\n return result.length !== 0 ? result[0] : undefined;\n }\n }\n return undefined;\n}\n\nexport function getNewMultiSelectValues(\n selected: string[],\n propertyName: string,\n item: QuestionnaireItem\n): QuestionnaireResponseItemAnswer[] {\n const result: QuestionnaireResponseItemAnswer[] = [];\n\n for (const selectedStr of selected) {\n const option = item.answerOption?.find(\n (candidate) => typedValueToString(getItemAnswerOptionValue(candidate)) === selectedStr\n );\n if (option) {\n const optionValue = getItemAnswerOptionValue(option);\n if (optionValue) {\n result.push({ [propertyName]: optionValue.value });\n }\n }\n }\n\n return result;\n}\n\nfunction getByLinkId(\n responseItems: QuestionnaireResponseItem[] | undefined,\n linkId: string\n): QuestionnaireResponseItemAnswer[] | undefined {\n if (!responseItems) {\n return undefined;\n }\n\n for (const response of responseItems) {\n if (response.linkId === linkId) {\n return response.answer;\n }\n if (response.item) {\n const nestedAnswer = getByLinkId(response.item, linkId);\n if (nestedAnswer) {\n return nestedAnswer;\n }\n }\n }\n\n return undefined;\n}\n\nfunction evaluateMatch(actualAnswer: TypedValue | undefined, expectedAnswer: TypedValue, operator?: string): boolean {\n // We handle exists separately since its so different in terms of comparisons than the other mathematical operators\n if (operator === 'exists') {\n // if actualAnswer is not undefined, then exists: true passes\n // if actualAnswer is undefined, then exists: false passes\n return !!actualAnswer === expectedAnswer.value;\n } else if (!actualAnswer) {\n return false;\n } else {\n // `=` and `!=` should be treated as the FHIRPath `~` and `!~`\n // All other operators should be unmodified\n const fhirPathOperator = operator === '=' || operator === '!=' ? operator?.replace('=', '~') : operator;\n const [{ value }] = evalFhirPathTyped(`%actualAnswer ${fhirPathOperator} %expectedAnswer`, [actualAnswer], {\n '%actualAnswer': actualAnswer,\n '%expectedAnswer': expectedAnswer,\n });\n return value;\n }\n}\n\nfunction checkAnswers(\n enableWhen: QuestionnaireItemEnableWhen,\n answers: QuestionnaireResponseItemAnswer[] | undefined,\n enableBehavior: 'any' | 'all'\n): { anyMatch: boolean; allMatch: boolean } {\n const actualAnswers = answers || [];\n const expectedAnswer = getItemEnableWhenValueAnswer(enableWhen);\n\n let anyMatch = false;\n let allMatch = true;\n\n for (const actualAnswerValue of actualAnswers) {\n const actualAnswer = getResponseItemAnswerValue(actualAnswerValue);\n const { operator } = enableWhen;\n const match = evaluateMatch(actualAnswer, expectedAnswer, operator);\n if (match) {\n anyMatch = true;\n } else {\n allMatch = false;\n }\n\n if (enableBehavior === 'any' && anyMatch) {\n break;\n }\n }\n\n return { anyMatch, allMatch };\n}\n\nexport function getQuestionnaireItemReferenceTargetTypes(item: QuestionnaireItem): ResourceType[] | undefined {\n const extension = getExtension(item, QUESTIONNAIRE_REFERENCE_RESOURCE_URL);\n if (!extension) {\n return undefined;\n }\n if (extension.valueCode !== undefined) {\n return [extension.valueCode] as ResourceType[];\n }\n if (extension.valueCodeableConcept) {\n return extension.valueCodeableConcept?.coding?.map((c) => c.code) as ResourceType[];\n }\n return undefined;\n}\n\nexport function setQuestionnaireItemReferenceTargetTypes(\n item: QuestionnaireItem,\n targetTypes: ResourceType[] | undefined\n): QuestionnaireItem {\n const result = deepClone(item);\n let extension = getExtension(result, QUESTIONNAIRE_REFERENCE_RESOURCE_URL);\n\n if (!targetTypes || targetTypes.length === 0) {\n if (extension) {\n result.extension = result.extension?.filter((e) => e !== extension);\n }\n return result;\n }\n\n if (!extension) {\n result.extension ??= [];\n extension = { url: QUESTIONNAIRE_REFERENCE_RESOURCE_URL };\n result.extension.push(extension);\n }\n\n if (targetTypes.length === 1) {\n extension.valueCode = targetTypes[0];\n delete extension.valueCodeableConcept;\n } else {\n extension.valueCodeableConcept = { coding: targetTypes.map((t) => ({ code: t })) };\n delete extension.valueCode;\n }\n\n return result;\n}\n\n/**\n * Returns the reference filter for the given questionnaire item.\n * @see https://build.fhir.org/ig/HL7/fhir-extensions/StructureDefinition-questionnaire-referenceFilter-definitions.html\n * @param item - The questionnaire item to get the reference filter for.\n * @param subject - Optional subject reference.\n * @param encounter - Optional encounter reference.\n * @returns The reference filter as a map of key/value pairs.\n */\nexport function getQuestionnaireItemReferenceFilter(\n item: QuestionnaireItem,\n subject: Reference | undefined,\n encounter: Reference<Encounter> | undefined\n): Record<string, string> | undefined {\n const extension = getExtension(item, QUESTIONNAIRE_REFERENCE_FILTER_URL);\n if (!extension?.valueString) {\n return undefined;\n }\n\n // Replace variables\n let filter = extension.valueString;\n if (subject?.reference) {\n filter = filter.replaceAll('$subj', subject.reference);\n }\n if (encounter?.reference) {\n filter = filter.replaceAll('$encounter', encounter.reference);\n }\n\n // Parse the valueString into a map\n const result: Record<string, string> = {};\n const parts = filter.split('&');\n for (const part of parts) {\n const [key, value] = splitN(part, '=', 2);\n result[key] = value;\n }\n return result;\n}\n\nexport function buildInitialResponse(\n questionnaire: Questionnaire,\n questionnaireResponse?: QuestionnaireResponse\n): QuestionnaireResponse {\n const response: QuestionnaireResponse = {\n resourceType: 'QuestionnaireResponse',\n questionnaire: questionnaire.url ?? getReferenceString(questionnaire),\n item: buildInitialResponseItems(questionnaire.item, questionnaireResponse?.item),\n status: 'in-progress',\n };\n\n return response;\n}\n\nfunction buildInitialResponseItems(\n items: QuestionnaireItem[] | undefined,\n responseItems: QuestionnaireResponseItem[] | undefined\n): QuestionnaireResponseItem[] | undefined {\n if (!items) {\n return undefined;\n }\n\n const result = [];\n for (const item of items) {\n if (item.type === QuestionnaireItemType.display) {\n // Display items do not have response items, so we skip them.\n continue;\n }\n\n const existingResponseItems = responseItems?.filter((responseItem) => responseItem.linkId === item.linkId);\n if (existingResponseItems && existingResponseItems?.length > 0) {\n for (const existingResponseItem of existingResponseItems) {\n // Update existing response item\n existingResponseItem.id = existingResponseItem.id ?? generateId();\n existingResponseItem.text = existingResponseItem.text ?? item.text;\n existingResponseItem.item = buildInitialResponseItems(item.item, existingResponseItem.item);\n existingResponseItem.answer = buildInitialResponseAnswer(item, existingResponseItem);\n result.push(existingResponseItem);\n }\n } else {\n // Add new response item\n result.push(buildInitialResponseItem(item));\n }\n }\n\n return result;\n}\n\nexport function buildInitialResponseItem(item: QuestionnaireItem): QuestionnaireResponseItem {\n return {\n id: generateId(),\n linkId: item.linkId,\n text: item.text,\n item: buildInitialResponseItems(item.item, undefined),\n answer: buildInitialResponseAnswer(item),\n };\n}\n\nlet nextId = 1;\nfunction generateId(): string {\n return 'id-' + nextId++;\n}\n\nfunction buildInitialResponseAnswer(\n item: QuestionnaireItem,\n responseItem?: QuestionnaireResponseItem\n): QuestionnaireResponseItemAnswer[] | undefined {\n if (item.type === QuestionnaireItemType.display || item.type === QuestionnaireItemType.group) {\n return undefined;\n }\n\n if (responseItem?.answer && responseItem.answer.length > 0) {\n // If the response item already has answers, return them as is.\n return responseItem.answer;\n }\n\n if (item.initial && item.initial.length > 0) {\n // If the item has initial values, return them as answers.\n // This works because QuestionnaireItemInitial and QuestionnaireResponseItemAnswer\n // have the same properties.\n return item.initial.map((initial) => ({ ...initial }));\n }\n\n if (item.answerOption) {\n return item.answerOption\n .filter((option) => option.initialSelected)\n .map((option) => ({ ...option, initialSelected: undefined }));\n }\n\n // Otherwise, return undefined to indicate no initial answers.\n return undefined;\n}\n\nexport function getItemInitialValue(initial: QuestionnaireItemInitial | undefined): TypedValue {\n return getTypedPropertyValueWithoutSchema(\n { type: 'QuestionnaireItemInitial', value: initial },\n 'value'\n ) as TypedValue;\n}\n\nexport function getItemAnswerOptionValue(option: QuestionnaireItemAnswerOption): TypedValue {\n return getTypedPropertyValueWithoutSchema(\n { type: 'QuestionnaireItemAnswerOption', value: option },\n 'value'\n ) as TypedValue;\n}\n\nexport function getItemEnableWhenValueAnswer(enableWhen: QuestionnaireItemEnableWhen): TypedValue {\n return getTypedPropertyValueWithoutSchema(\n { type: 'QuestionnaireItemEnableWhen', value: enableWhen },\n 'answer'\n ) as TypedValue;\n}\n\nexport function getResponseItemAnswerValue(answer: QuestionnaireResponseItemAnswer): TypedValue | undefined {\n return getTypedPropertyValueWithoutSchema({ type: 'QuestionnaireResponseItemAnswer', value: answer }, 'value') as\n | TypedValue\n | undefined;\n}\n", "// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors\n// SPDX-License-Identifier: Apache-2.0\nimport { allOk, normalizeOperationOutcome, QueryTypes, ResourceArray } from '@medplum/core';\nimport { Bundle, ExtractResource, OperationOutcome, ResourceType } from '@medplum/fhirtypes';\nimport { useEffect, useState } from 'react';\nimport { useMedplum } from '../MedplumProvider/MedplumProvider.context';\nimport { useDebouncedValue } from '../useDebouncedValue/useDebouncedValue';\n\ntype SearchFn = 'search' | 'searchOne' | 'searchResources';\nexport type SearchOptions = { debounceMs?: number };\n\nconst DEFAULT_DEBOUNCE_MS = 250;\n\n/**\n * React hook for searching FHIR resources.\n *\n * This is a convenience hook for calling the MedplumClient.search() method.\n *\n * @param resourceType - The FHIR resource type to search.\n * @param query - Optional search parameters.\n * @param options - Optional options for configuring the search.\n * @returns A 3-element tuple containing the search result, loading flag, and operation outcome.\n */\nexport function useSearch<K extends ResourceType>(\n resourceType: K,\n query?: QueryTypes,\n options?: SearchOptions\n): [Bundle<ExtractResource<K>> | undefined, boolean, OperationOutcome | undefined] {\n return useSearchImpl<K, Bundle<ExtractResource<K>>>('search', resourceType, query, options);\n}\n\n/**\n * React hook for searching for a single FHIR resource.\n *\n * This is a convenience hook for calling the MedplumClient.searchOne() method.\n *\n * @param resourceType - The FHIR resource type to search.\n * @param query - Optional search parameters.\n * @param options - Optional options for configuring the search.\n * @returns A 3-element tuple containing the search result, loading flag, and operation outcome.\n */\nexport function useSearchOne<K extends ResourceType>(\n resourceType: K,\n query?: QueryTypes,\n options?: SearchOptions\n): [ExtractResource<K> | undefined, boolean, OperationOutcome | undefined] {\n return useSearchImpl<K, ExtractResource<K>>('searchOne', resourceType, query, options);\n}\n\n/**\n * React hook for searching for an array of FHIR resources.\n *\n * This is a convenience hook for calling the MedplumClient.searchResources() method.\n *\n * @param resourceType - The FHIR resource type to search.\n * @param query - Optional search parameters.\n * @param options - Optional options for configuring the search.\n * @returns A 3-element tuple containing the search result, loading flag, and operation outcome.\n */\nexport function useSearchResources<K extends ResourceType>(\n resourceType: K,\n query?: QueryTypes,\n options?: SearchOptions\n): [ResourceArray<ExtractResource<K>> | undefined, boolean, OperationOutcome | undefined] {\n return useSearchImpl<K, ResourceArray<ExtractResource<K>>>('searchResources', resourceType, query, options);\n}\n\nfunction useSearchImpl<K extends ResourceType, SearchReturnType>(\n searchFn: SearchFn,\n resourceType: K,\n query: QueryTypes | undefined,\n options?: SearchOptions\n): [SearchReturnType | undefined, boolean, OperationOutcome | undefined] {\n const medplum = useMedplum();\n const [lastSearchKey, setLastSearchKey] = useState<string>();\n const [loading, setLoading] = useState<boolean>(true);\n const [result, setResult] = useState<SearchReturnType>();\n const [outcome, setOutcome] = useState<OperationOutcome>();\n\n const searchKey = medplum.fhirSearchUrl(resourceType, query).toString();\n const [debouncedSearchKey] = useDebouncedValue(searchKey, options?.debounceMs ?? DEFAULT_DEBOUNCE_MS, {\n leading: true,\n });\n\n useEffect(() => {\n if (debouncedSearchKey !== lastSearchKey) {\n setLastSearchKey(debouncedSearchKey);\n medplum[searchFn](resourceType, query)\n .then((res) => {\n setLoading(false);\n setResult(res as SearchReturnType);\n setOutcome(allOk);\n })\n .catch((err) => {\n setLoading(false);\n setResult(undefined);\n setOutcome(normalizeOperationOutcome(err));\n });\n }\n }, [medplum, searchFn, resourceType, query, lastSearchKey, debouncedSearchKey]);\n\n return [result, loading, outcome];\n}\n", "// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors\n// SPDX-License-Identifier: Apache-2.0\n\n/*\n This hook was forked from: https://github.com/mantinedev/mantine/blob/fbcee929e0b11782092f48c1e7af2a1d1c878823/packages/%40mantine/hooks/src/use-debounced-value/use-debounced-value.ts\n and has the following license:\n\n MIT License\n\n Copyright (c) 2021 Vitaly Rtishchev\n\n Permission is hereby granted, free of charge, to any person obtaining a copy\n of this software and associated documentation files (the \"Software\"), to deal\n in the Software without restriction, including without limitation the rights\n to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the Software is\n furnished to do so, subject to the following conditions:\n\n The above copyright notice and this permission notice shall be included in all\n copies or substantial portions of the Software.\n\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n SOFTWARE.\n*/\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nexport type UseDebouncedValueOptions = {\n /** Whether the first update to `value` should be immediate or not */\n leading?: boolean;\n};\n\n/**\n * This hook allows users to debounce an incoming value by a specified number of milliseconds.\n *\n * Users can also specify whether the first update to `value` in a sequence of rapid updates should be immediate, by specifying `leading: true` in the options.\n * The default value for `leading` is `false`.\n *\n * The return value is a tuple containing the debounced value at `arr[0]` and a function to cancel the pending debounced value change at `arr[1]`.\n *\n * @param value - The value to debounce.\n * @param waitMs - How long in milliseconds should.\n * @param options - Optional options for configuring the debounce.\n * @returns An array tuple of `[debouncedValue, cancelFn]`.\n */\nexport function useDebouncedValue<T = any>(\n value: T,\n waitMs: number,\n options: UseDebouncedValueOptions = { leading: false }\n): [T, () => void] {\n const [debouncedValue, setDebouncedValue] = useState(value);\n const mountedRef = useRef(false);\n const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);\n const cooldownRef = useRef(false);\n\n const cancel = useCallback(() => window.clearTimeout(timeoutRef.current), []);\n\n useEffect(() => {\n if (mountedRef.current) {\n if (!cooldownRef.current && optio