Skip to main content
Glama
ReferenceInput.tsx9.58 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { Group, NativeSelect } from '@mantine/core'; import type { MedplumClient } from '@medplum/core'; import { LRUCache, ReadablePromise, createReference, isEmpty, isPopulated, tryGetProfile } from '@medplum/core'; import type { Reference, Resource, ResourceType, StructureDefinition } from '@medplum/fhirtypes'; import { useMedplum } from '@medplum/react-hooks'; import type { JSX } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ResourceInput } from '../ResourceInput/ResourceInput'; import { ResourceTypeInput } from '../ResourceTypeInput/ResourceTypeInput'; export interface ReferenceInputProps<T extends Resource = Resource> { readonly name: string; readonly placeholder?: string; readonly defaultValue?: Reference<T>; readonly targetTypes?: string[]; readonly searchCriteria?: Record<string, string>; readonly autoFocus?: boolean; readonly required?: boolean; readonly onChange?: (value: Reference<T> | undefined) => void; readonly disabled?: boolean; } interface BaseTargetType { readonly value: string; } type ProfileTargetType = BaseTargetType & { readonly type: 'profile'; readonly name?: string; readonly title?: string; readonly resourceType?: string; readonly error?: any; }; type ResourceTypeTargetType = BaseTargetType & { readonly type: 'resourceType'; readonly resourceType: string; }; type TargetType = ResourceTypeTargetType | ProfileTargetType; export function ReferenceInput<T extends Resource = Resource>(props: ReferenceInputProps<T>): JSX.Element { const { onChange } = props; const medplum = useMedplum(); const [value, setValue] = useState<Reference<T> | undefined>(props.defaultValue); const [targetTypes, setTargetTypes] = useState<TargetType[] | undefined>(() => createTargetTypes(props.targetTypes)); const [targetType, setTargetType] = useState<TargetType | undefined>(() => getInitialTargetType(props.defaultValue, targetTypes) ); const promiseCache = useRef(new LRUCache<ReadablePromise<TargetType>>()); const searchCriteria = useMemo<ReferenceInputProps['searchCriteria']>(() => { if (targetType?.type === 'profile') { return { ...props.searchCriteria, _profile: targetType.value }; } return props.searchCriteria; }, [props.searchCriteria, targetType]); useEffect(() => { let anyToFetch = false; const newTargetTypePromises: Promise<TargetType>[] | undefined = targetTypes?.map((tt) => { if (!shouldFetchResourceType(tt)) { return Promise.resolve(tt); } anyToFetch = true; const cacheKey = tt.value; const cached = promiseCache.current.get(cacheKey); if (cached) { return cached; } const promise = fetchResourceTypeOfProfile(medplum, tt.value) .then((profile) => { const newTargetType = { ...tt }; if (!profile) { console.error(`StructureDefinition not found for ${tt.value}`); newTargetType.error = 'StructureDefinition not found'; } else if (!isPopulated(profile.type)) { console.error(`StructureDefinition.type missing for ${tt.value}`); newTargetType.error = 'StructureDefinition.type missing'; } else { newTargetType.resourceType = profile.type; newTargetType.name = profile.name; newTargetType.title = profile.title; } return newTargetType; }) .catch((reason) => { console.error(reason); return { ...tt, error: reason }; }); const readablePromise = new ReadablePromise(promise); promiseCache.current.set(cacheKey, readablePromise); return readablePromise; }); if (!newTargetTypePromises || !anyToFetch) { return; } Promise.all(newTargetTypePromises) .then((newTargetTypes) => { setTargetTypes(newTargetTypes); if (!targetType) { return; } const index = newTargetTypes.findIndex( (tt) => tt.value === targetType.value || tt.resourceType === targetType.resourceType ); if (index === -1) { console.debug(`defaultValue had unexpected resourceType: ${targetType.resourceType}`); return; } // orphaned targetType has been resolved setTargetType(newTargetTypes[index]); }) .catch(console.error); }, [medplum, targetType, targetTypes]); const setValueHelper = useCallback( (item: T | undefined) => { const newValue = item ? createReference<T>(item) : undefined; setValue(newValue); if (onChange) { onChange(newValue); } }, [onChange] ); const typeSelectOptions = useMemo(() => { if (targetTypes) { return targetTypes.map((tt) => { return { value: tt.value, label: tt.type === 'profile' ? (tt.title ?? tt.name ?? tt.resourceType ?? tt.value) : tt.value, }; }); } return []; }, [targetTypes]); return ( <> {props.name && <input type="hidden" name={props.name} value={value?.reference ?? ''} />} <Group gap="xs" grow wrap="nowrap"> {targetTypes && targetTypes.length > 1 && ( <NativeSelect name={props.name + '-resourceType'} disabled={props.disabled} data-autofocus={props.autoFocus} data-testid="reference-input-resource-type-select" defaultValue={targetType?.resourceType} autoFocus={props.autoFocus} onChange={(e) => { const newValue = e.currentTarget.value; const newTargetType = targetTypes.find((tt) => tt.value === newValue); setTargetType(newTargetType); }} data={typeSelectOptions} /> )} {!targetTypes && ( <ResourceTypeInput disabled={props.disabled} autoFocus={props.autoFocus} testId="reference-input-resource-type-input" defaultValue={targetType?.resourceType as ResourceType} onChange={(newResourceType) => { if (newResourceType) { setTargetType({ type: 'resourceType', value: newResourceType, resourceType: newResourceType }); } else { setTargetType(undefined); } }} name={props.name + '-resourceType'} placeholder="Resource Type" /> )} <ResourceInput resourceType={targetType?.resourceType as ResourceType} name={props.name + '-id'} required={props.required} placeholder={props.placeholder} defaultValue={value} searchCriteria={searchCriteria} onChange={setValueHelper} disabled={props.disabled} /> </Group> </> ); } function createTargetTypes(resourceTypesAndProfileUrls: string[] | undefined): TargetType[] | undefined { if ( !resourceTypesAndProfileUrls || resourceTypesAndProfileUrls.length === 0 || (resourceTypesAndProfileUrls.length === 1 && resourceTypesAndProfileUrls[0] === 'Resource') ) { return undefined; } const results: TargetType[] = []; for (const value of resourceTypesAndProfileUrls) { // is there a less hacky way to distinguish resourceType from profile URLs? if (value.includes('/')) { results.push({ type: 'profile', value }); } else { results.push({ type: 'resourceType', value, resourceType: value }); } } return results; } function getInitialTargetType( defaultValue: Reference | undefined, targetTypes: TargetType[] | undefined ): TargetType | undefined { const defaultValueResourceType = defaultValue?.reference?.split('/')[0]; if (defaultValueResourceType) { const targetType = targetTypes?.find((tt) => tt.resourceType === defaultValueResourceType); if (targetType) { return targetType; } // An "orphaned" TargetType is created when defaultValue references a resourceType // that is not yet represented in targetTypes due to profile URL resolution to resource type // that has yet to occur. An orphan can also occur if a defaultValue is provided // but targetTypes is not. return { type: 'resourceType', value: defaultValueResourceType, resourceType: defaultValueResourceType, }; } if (targetTypes && targetTypes.length > 0) { return targetTypes[0]; } return undefined; } interface ResourceTypeGraphQLResponse { readonly data: { readonly StructureDefinitionList: Partial<StructureDefinition>[]; }; } async function fetchResourceTypeOfProfile( medplum: MedplumClient, profileUrl: string ): Promise<Partial<StructureDefinition> | undefined> { const profile = tryGetProfile(profileUrl); if (profile) { return { type: profile.type, name: profile.name, title: profile.title }; } const query = `{ StructureDefinitionList(url: "${profileUrl}", _sort: "_lastUpdated", _count: 1) { type, name, title, } }`.replaceAll(/\s+/g, ' '); const response = (await medplum.graphql(query)) as ResourceTypeGraphQLResponse; return response.data.StructureDefinitionList[0]; } function shouldFetchResourceType(targetType: TargetType): targetType is ProfileTargetType { return targetType.type === 'profile' && !targetType?.error && isEmpty(targetType.resourceType); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server