Skip to main content
Glama
ResourceArrayInput.tsx6.63 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { Group, Stack, Text } from '@mantine/core'; import type { ExtendedInternalSchemaElement, SliceDefinitionWithTypes } from '@medplum/core'; import { getPathDisplayName } from '@medplum/core'; import { useMedplum } from '@medplum/react-hooks'; import type { JSX, MouseEvent } from 'react'; import { useContext, useEffect, useState } from 'react'; import { ElementsContext } from '../ElementsInput/ElementsInput.utils'; import { ResourcePropertyInput } from '../ResourcePropertyInput/ResourcePropertyInput'; import type { BaseInputProps } from '../ResourcePropertyInput/ResourcePropertyInput.utils'; import { getValuePath } from '../ResourcePropertyInput/ResourcePropertyInput.utils'; import { SliceInput } from '../SliceInput/SliceInput'; import { ArrayAddButton } from '../buttons/ArrayAddButton'; import { ArrayRemoveButton } from '../buttons/ArrayRemoveButton'; import { killEvent } from '../utils/dom'; import classes from './ResourceArrayInput.module.css'; import { assignValuesIntoSlices, prepareSlices } from './ResourceArrayInput.utils'; export interface ResourceArrayInputProps extends BaseInputProps { readonly property: ExtendedInternalSchemaElement; readonly name: string; readonly defaultValue?: any[]; readonly indent?: boolean; readonly onChange?: (value: any[]) => void; readonly hideNonSliceValues?: boolean; } export function ResourceArrayInput(props: ResourceArrayInputProps): JSX.Element | null { const { property } = props; const medplum = useMedplum(); const [loading, setLoading] = useState(true); const [slices, setSlices] = useState<SliceDefinitionWithTypes[]>([]); // props.defaultValue should NOT be used after this; prefer the defaultValue state const [defaultValue] = useState<any[]>(() => (Array.isArray(props.defaultValue) ? props.defaultValue : [])); const [slicedValues, setSlicedValues] = useState<any[][]>(() => [defaultValue]); const ctx = useContext(ElementsContext); const propertyTypeCode = property.type[0]?.code; useEffect(() => { prepareSlices({ medplum, property, }) .then((slices) => { setSlices(slices); const slicedValues = assignValuesIntoSlices(defaultValue, slices, property.slicing, ctx.profileUrl); addPlaceholderValues(slicedValues, slices); setSlicedValues(slicedValues); setLoading(false); }) .catch((reason) => { console.error(reason); setLoading(false); }); }, [medplum, property, defaultValue, ctx.profileUrl, setSlicedValues]); function setValuesWrapper(newValues: any[], sliceIndex: number): void { const newSlicedValues = [...slicedValues]; newSlicedValues[sliceIndex] = newValues; setSlicedValues(newSlicedValues); if (props.onChange) { // Remove any placeholder (i.e. undefined) values before propagating const cleaned = newSlicedValues.flat().filter((val) => val !== undefined); props.onChange(cleaned); } } if (loading) { return <div>Loading...</div>; } const nonSliceIndex = slices.length; const nonSliceValues = slicedValues[nonSliceIndex]; // Hide non-sliced values when handling sliced extensions const showNonSliceValues = !(props.hideNonSliceValues ?? (propertyTypeCode === 'Extension' && slices.length > 0)); const propertyDisplayName = getPathDisplayName(property.path); const showEmptyMessage = props.property.readonly && slices.length === 0 && defaultValue.length === 0; return ( <Stack className={props.indent ? classes.indented : undefined}> {showEmptyMessage && <Text c="dimmed">(empty)</Text>} {slices.map((slice, sliceIndex) => { return ( <SliceInput slice={slice} key={slice.name} path={props.path} valuePath={props.valuePath} property={property} defaultValue={slicedValues[sliceIndex]} onChange={(newValue: any[]) => { setValuesWrapper(newValue, sliceIndex); }} testId={`slice-${slice.name}`} /> ); })} {showNonSliceValues && nonSliceValues.map((value, valueIndex) => ( <Group key={`${valueIndex}-${nonSliceValues.length}`} wrap="nowrap" style={{ flexGrow: 1 }}> <div style={{ flexGrow: 1 }}> <ResourcePropertyInput arrayElement={true} property={props.property} name={props.name + '.' + valueIndex} path={props.path} valuePath={getValuePath(props.path, props.valuePath, valueIndex)} defaultValue={value} onChange={(newValue: any) => { const newNonSliceValues = [...nonSliceValues]; newNonSliceValues[valueIndex] = newValue; setValuesWrapper(newNonSliceValues, nonSliceIndex); }} defaultPropertyType={undefined} outcome={props.outcome} /> </div> {!props.property.readonly && ( <ArrayRemoveButton propertyDisplayName={propertyDisplayName} testId={`nonsliced-remove-${valueIndex}`} onClick={(e: MouseEvent) => { killEvent(e); const newNonSliceValues = [...nonSliceValues]; newNonSliceValues.splice(valueIndex, 1); setValuesWrapper(newNonSliceValues, nonSliceIndex); }} /> )} </Group> ))} {!props.property.readonly && showNonSliceValues && slicedValues.flat().length < property.max && ( <Group wrap="nowrap" style={{ justifyContent: 'flex-start' }}> <ArrayAddButton propertyDisplayName={propertyDisplayName} onClick={(e: MouseEvent) => { killEvent(e); const newNonSliceValues = [...nonSliceValues]; newNonSliceValues.push(undefined); setValuesWrapper(newNonSliceValues, nonSliceIndex); }} testId="nonsliced-add" /> </Group> )} </Stack> ); } function addPlaceholderValues(slicedValues: any[][], slices: SliceDefinitionWithTypes[]): void { for (let sliceIndex = 0; sliceIndex < slices.length; sliceIndex++) { const slice = slices[sliceIndex]; const sliceValues = slicedValues[sliceIndex]; while (sliceValues.length < slice.min) { sliceValues.push(undefined); } } }

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