Skip to main content
Glama
ResourcePropertyInput.tsx14.2 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { Checkbox, Group, NativeSelect, Textarea, TextInput } from '@mantine/core'; import type { ExtendedInternalSchemaElement } from '@medplum/core'; import { applyDefaultValuesToElement, capitalize, getPathDifference, HTTP_HL7_ORG, isComplexTypeCode, isEmpty, isPopulated, PropertyType, } from '@medplum/core'; import type { ElementDefinitionBinding, ElementDefinitionType } from '@medplum/fhirtypes'; import type { JSX } from 'react'; import { useContext, useMemo, useState } from 'react'; import { AddressInput } from '../AddressInput/AddressInput'; import { AnnotationInput } from '../AnnotationInput/AnnotationInput'; import { AttachmentArrayInput } from '../AttachmentArrayInput/AttachmentArrayInput'; import { AttachmentInput } from '../AttachmentInput/AttachmentInput'; import { BackboneElementInput } from '../BackboneElementInput/BackboneElementInput'; import { CodeableConceptInput } from '../CodeableConceptInput/CodeableConceptInput'; import { CodeInput } from '../CodeInput/CodeInput'; import { CodingInput } from '../CodingInput/CodingInput'; import { ContactDetailInput } from '../ContactDetailInput/ContactDetailInput'; import { ContactPointInput } from '../ContactPointInput/ContactPointInput'; import { DateTimeInput } from '../DateTimeInput/DateTimeInput'; import { ElementsContext } from '../ElementsInput/ElementsInput.utils'; import { ExtensionInput } from '../ExtensionInput/ExtensionInput'; import { HumanNameInput } from '../HumanNameInput/HumanNameInput'; import { IdentifierInput } from '../IdentifierInput/IdentifierInput'; import { MoneyInput } from '../MoneyInput/MoneyInput'; import { PeriodInput } from '../PeriodInput/PeriodInput'; import { QuantityInput } from '../QuantityInput/QuantityInput'; import { RangeInput } from '../RangeInput/RangeInput'; import { RatioInput } from '../RatioInput/RatioInput'; import { ReferenceInput } from '../ReferenceInput/ReferenceInput'; import { ResourceArrayInput } from '../ResourceArrayInput/ResourceArrayInput'; import { SensitiveTextarea } from '../SensitiveTextarea/SensitiveTextarea'; import { TimingInput } from '../TimingInput/TimingInput'; import { getErrorsForInput } from '../utils/outcomes'; import type { BaseInputProps, ComplexTypeInputProps, PrimitiveTypeInputProps } from './ResourcePropertyInput.utils'; export interface ResourcePropertyInputProps extends BaseInputProps { readonly property: ExtendedInternalSchemaElement; readonly name: string; readonly defaultPropertyType?: string; readonly defaultValue: any; readonly arrayElement?: boolean; readonly onChange?: (value: any, propName?: string) => void; } export function ResourcePropertyInput(props: ResourcePropertyInputProps): JSX.Element { const { property, name, onChange, defaultValue } = props; const defaultPropertyType = props.defaultPropertyType && props.defaultPropertyType !== 'undefined' ? props.defaultPropertyType : property.type[0].code; const propertyTypes = property.type as ElementDefinitionType[]; if ((property.isArray || property.max > 1) && !props.arrayElement) { if (defaultPropertyType === PropertyType.Attachment) { return ( <AttachmentArrayInput name={name} defaultValue={defaultValue} onChange={onChange} disabled={property.readonly} /> ); } // Extensions are a special type of array that shouldn't be indented const indent = propertyTypes[0]?.code !== PropertyType.Extension; return ( <ResourceArrayInput property={property} name={name} path={props.path} valuePath={props.valuePath} defaultValue={defaultValue} indent={indent} onChange={onChange} outcome={props.outcome} /> ); } else if (propertyTypes.length > 1) { return <ElementDefinitionInputSelector elementDefinitionTypes={propertyTypes} {...props} />; } else { return ( <ElementDefinitionTypeInput name={name} defaultValue={defaultValue} onChange={(newValue: any) => { if (props.onChange) { const newPropName = props.name.replace('[x]', capitalize(propertyTypes[0].code as string)); props.onChange(newValue, newPropName); } }} outcome={props.outcome} elementDefinitionType={propertyTypes[0]} min={property.min} max={property.min} binding={property.binding} path={props.path} valuePath={props.valuePath} readOnly={property.readonly} /> ); } } export interface ElementDefinitionSelectorProps extends ResourcePropertyInputProps { readonly elementDefinitionTypes: ElementDefinitionType[]; } export function ElementDefinitionInputSelector(props: ElementDefinitionSelectorProps): JSX.Element { const propertyTypes = props.elementDefinitionTypes; let initialPropertyType: ElementDefinitionType | undefined = undefined; if (props.defaultPropertyType) { initialPropertyType = propertyTypes.find((t) => t.code === props.defaultPropertyType) as ElementDefinitionType; } if (!initialPropertyType) { initialPropertyType = propertyTypes[0]; } const [selectedType, setSelectedType] = useState(initialPropertyType); return ( <Group gap="xs" grow wrap="nowrap" align="flex-start"> <NativeSelect disabled={props.property.readonly} style={{ width: '200px' }} defaultValue={selectedType.code} data-testid={props.name && props.name + '-selector'} onChange={(e) => { setSelectedType( propertyTypes.find( (type: ElementDefinitionType) => type.code === e.currentTarget.value ) as ElementDefinitionType ); }} data={propertyTypes.map((type: ElementDefinitionType) => ({ value: type.code as string, label: type.code as string, }))} /> <ElementDefinitionTypeInput name={props.name} defaultValue={props.defaultValue} outcome={props.outcome} elementDefinitionType={selectedType} onChange={(newValue: any) => { if (props.onChange) { props.onChange(newValue, props.name.replace('[x]', capitalize(selectedType.code as string))); } }} min={props.property.min} max={props.property.max} binding={props.property.binding} path={props.property.path} valuePath={props.valuePath} readOnly={props.property.readonly} /> </Group> ); } // Avoiding optional props on lower-level components like to make it more difficult to misuse export interface ElementDefinitionTypeInputProps extends Pick< ResourcePropertyInputProps, 'name' | 'path' | 'valuePath' | 'defaultValue' | 'onChange' | 'outcome' > { readonly elementDefinitionType: ElementDefinitionType; readonly min: number; readonly max: number; readonly binding: ElementDefinitionBinding | undefined; readonly readOnly?: boolean; } export function ElementDefinitionTypeInput(props: ElementDefinitionTypeInputProps): JSX.Element { const { name, onChange, outcome, binding, path, valuePath, readOnly } = props; const required = props.min !== undefined && props.min > 0; const propertyType = props.elementDefinitionType.code; const elementsContext = useContext(ElementsContext); const defaultValue = useMemo(() => { if (!isComplexTypeCode(propertyType)) { return props.defaultValue; } if (!isEmpty(props.defaultValue)) { return props.defaultValue; } const withDefaults = Object.create(null); if (elementsContext.path === props.path) { applyDefaultValuesToElement(withDefaults, elementsContext.elements); } else { const key = getPathDifference(elementsContext.path, props.path); if (key === undefined) { return props.defaultValue; } applyDefaultValuesToElement(withDefaults, elementsContext.elements, key); } if (isPopulated(withDefaults)) { return withDefaults; } return props.defaultValue; }, [propertyType, elementsContext.path, elementsContext.elements, props.path, props.defaultValue]); if (!propertyType) { return <div>Property type not specified </div>; } function getComplexInputProps(): ComplexTypeInputProps<any> { return { name, defaultValue, onChange, outcome, path, valuePath, disabled: readOnly }; } function getPrimitiveInputProps(): PrimitiveTypeInputProps { const error = getErrorsForInput(props.outcome, valuePath ?? path); return { id: name, name, 'data-testid': name, defaultValue, required, error, disabled: readOnly, }; } switch (propertyType) { // 2.24.0.1 Primitive Types // https://www.hl7.org/fhir/datatypes.html#primitive case PropertyType.SystemString: case PropertyType.canonical: case PropertyType.string: case PropertyType.time: case PropertyType.uri: case PropertyType.url: if (props.path === 'Project.secret.value[x]') { return ( <SensitiveTextarea {...getPrimitiveInputProps()} onChange={(e) => { if (props.onChange) { props.onChange(e.currentTarget.value); } }} /> ); } return ( <TextInput {...getPrimitiveInputProps()} onChange={(e) => { if (onChange) { onChange(e.currentTarget.value); } }} /> ); case PropertyType.date: return ( <TextInput {...getPrimitiveInputProps()} type="date" onChange={(e) => { if (onChange) { onChange(e.currentTarget.value); } }} /> ); case PropertyType.dateTime: case PropertyType.instant: return <DateTimeInput {...getPrimitiveInputProps()} onChange={onChange} outcome={outcome} />; case PropertyType.decimal: case PropertyType.integer: case PropertyType.positiveInt: case PropertyType.unsignedInt: return ( <TextInput {...getPrimitiveInputProps()} type="number" step={propertyType === PropertyType.decimal ? 'any' : '1'} onChange={(e) => { if (onChange) { const num = e.currentTarget.valueAsNumber; onChange(Number.isNaN(num) ? undefined : num); } }} /> ); case PropertyType.code: // overwrite getPrimitiveInputProps().error since FormSection already shows errors return ( <CodeInput {...getPrimitiveInputProps()} error={undefined} onChange={onChange} binding={binding?.valueSet} creatable maxValues={1} /> ); case PropertyType.boolean: return ( <Checkbox {...getPrimitiveInputProps()} defaultChecked={Boolean(defaultValue)} onChange={(e) => { if (onChange) { onChange(e.currentTarget.checked); } }} /> ); case PropertyType.base64Binary: case PropertyType.markdown: case PropertyType.xhtml: return ( <Textarea {...getPrimitiveInputProps()} spellCheck={propertyType !== PropertyType.base64Binary} onChange={(e) => { if (onChange) { onChange(e.currentTarget.value); } }} /> ); // 2.24.0.2 Complex Types // https://www.hl7.org/fhir/datatypes.html#complex case PropertyType.Address: return <AddressInput {...getComplexInputProps()} />; case PropertyType.Annotation: return <AnnotationInput {...getComplexInputProps()} />; case PropertyType.Attachment: return <AttachmentInput {...getComplexInputProps()} />; case PropertyType.CodeableConcept: return <CodeableConceptInput binding={binding?.valueSet} {...getComplexInputProps()} />; case PropertyType.Coding: return <CodingInput binding={binding?.valueSet} {...getComplexInputProps()} />; case PropertyType.ContactDetail: return <ContactDetailInput {...getComplexInputProps()} />; case PropertyType.ContactPoint: return <ContactPointInput {...getComplexInputProps()} />; case PropertyType.Extension: return <ExtensionInput {...getComplexInputProps()} propertyType={props.elementDefinitionType} />; case PropertyType.HumanName: return <HumanNameInput {...getComplexInputProps()} />; case PropertyType.Identifier: return <IdentifierInput {...getComplexInputProps()} />; case PropertyType.Money: return <MoneyInput {...getComplexInputProps()} />; case PropertyType.Period: return <PeriodInput {...getComplexInputProps()} />; case PropertyType.Duration: case PropertyType.Quantity: return <QuantityInput {...getComplexInputProps()} />; case PropertyType.Range: return <RangeInput {...getComplexInputProps()} />; case PropertyType.Ratio: return <RatioInput {...getComplexInputProps()} />; case PropertyType.Reference: return <ReferenceInput {...getComplexInputProps()} targetTypes={getTargetTypes(props.elementDefinitionType)} />; case PropertyType.Timing: return <TimingInput {...getComplexInputProps()} />; case PropertyType.Dosage: case PropertyType.UsageContext: default: return <BackboneElementInput {...getComplexInputProps()} typeName={propertyType} />; } } const RESOURCE_TYPE_URL_PREFIXES = [ `${HTTP_HL7_ORG}/fhir/StructureDefinition/`, 'https://medplum.com/fhir/StructureDefinition/', ]; function getTargetTypes(elementDefinitionType?: ElementDefinitionType): string[] | undefined { return elementDefinitionType?.targetProfile?.map((p) => { const resourceTypePrefix = RESOURCE_TYPE_URL_PREFIXES.find((prefix) => p.startsWith(prefix)); if (resourceTypePrefix) { return p.slice(resourceTypePrefix.length); } else { return p; } }); }

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