Skip to main content
Glama
ResourceForm.tsx6.25 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { ActionIcon, Alert, Button, Group, Menu, Stack, TextInput, useMantineTheme } from '@mantine/core'; import { AccessPolicyInteraction, applyDefaultValuesToResource, canWriteResourceType, isPopulated, satisfiedAccessPolicy, tryGetProfile, } from '@medplum/core'; import type { OperationOutcome, Reference, Resource, ResourceType } from '@medplum/fhirtypes'; import { useMedplum, useResource } from '@medplum/react-hooks'; import { IconAlertCircle, IconChevronDown, IconEdit, IconTrash } from '@tabler/icons-react'; import cx from 'clsx'; import type { FormEvent, JSX } from 'react'; import { useEffect, useMemo, useState } from 'react'; import { BackboneElementInput } from '../BackboneElementInput/BackboneElementInput'; import { FormSection } from '../FormSection/FormSection'; import classes from './ResourceForm.module.css'; export interface ResourceFormProps { readonly defaultValue: Partial<Resource> | Reference; readonly outcome?: OperationOutcome; readonly onSubmit: (resource: Resource) => void; readonly onPatch?: (resource: Resource) => void; readonly onDelete?: (resource: Resource) => void; /** (optional) URL of the resource profile used to display the form. Takes priority over schemaName. */ readonly profileUrl?: string; } export function ResourceForm(props: ResourceFormProps): JSX.Element { const { outcome } = props; const medplum = useMedplum(); const defaultValue = useResource(props.defaultValue); const resourceType = defaultValue?.resourceType as ResourceType; const [schemaLoaded, setSchemaLoaded] = useState(false); const [value, setValue] = useState<Resource>(); const accessPolicy = medplum.getAccessPolicy(); const theme = useMantineTheme(); useEffect(() => { if (defaultValue) { if (props.profileUrl) { const profileUrl: string = props.profileUrl; medplum .requestProfileSchema(props.profileUrl, { expandProfile: true }) .then(() => { const profile = tryGetProfile(profileUrl); if (profile) { setSchemaLoaded(true); const modifiedDefaultValue = applyDefaultValuesToResource(defaultValue, profile); setValue(modifiedDefaultValue); } else { console.error(`Schema not found for ${profileUrl}`); } }) .catch((reason) => { console.error('Error in requestProfileSchema', reason); }); } else { medplum .requestSchema(resourceType) .then(() => { setValue(defaultValue); setSchemaLoaded(true); }) .catch(console.log); } } }, [medplum, defaultValue, resourceType, props.profileUrl]); const accessPolicyResource = useMemo(() => { return defaultValue && satisfiedAccessPolicy(defaultValue, AccessPolicyInteraction.READ, accessPolicy); }, [accessPolicy, defaultValue]); const canWrite = useMemo<boolean>(() => { if (medplum.isSuperAdmin()) { return true; } if (!accessPolicy) { return true; } if (!isPopulated(value?.resourceType)) { return true; } return canWriteResourceType(accessPolicy, value?.resourceType); }, [medplum, accessPolicy, value?.resourceType]); if (!schemaLoaded || !value) { return <div>Loading...</div>; } if (!canWrite) { return ( <Alert color="red" title="Permission denied" icon={<IconAlertCircle />}> Your access level prevents you from editing and creating {value.resourceType} resources. </Alert> ); } return ( <form noValidate autoComplete="off" onSubmit={(e: FormEvent) => { e.preventDefault(); if (props.onSubmit) { props.onSubmit(value); } }} > <Stack mb="xl"> <FormSection title="Resource Type" htmlFor="resourceType" outcome={outcome}> <TextInput name="resourceType" defaultValue={value.resourceType} disabled={true} /> </FormSection> <FormSection title="ID" htmlFor="id" outcome={outcome}> <TextInput name="id" defaultValue={value.id} disabled={true} /> </FormSection> </Stack> <BackboneElementInput path={value.resourceType} valuePath={value.resourceType} typeName={resourceType} defaultValue={value} outcome={outcome} onChange={setValue} profileUrl={props.profileUrl} accessPolicyResource={accessPolicyResource} /> <Group justify="flex-end" mt="xl" wrap="nowrap" gap={0}> <Button type="submit" className={cx((props.onPatch || props.onDelete) && classes.splitButton)}> {defaultValue?.id ? 'Update' : 'Create'} </Button> {(props.onPatch || props.onDelete) && ( <Menu transitionProps={{ transition: 'pop' }} position="bottom-end" withinPortal> <Menu.Target> <ActionIcon variant="filled" color={theme.primaryColor} size={36} className={classes.menuControl} aria-label="More actions" > <IconChevronDown size={14} stroke={1.5} /> </ActionIcon> </Menu.Target> <Menu.Dropdown> {props.onPatch && ( <Menu.Item leftSection={<IconEdit size={14} stroke={1.5} />} onClick={() => { (props.onPatch as (resource: Resource) => void)(value); }} > Patch </Menu.Item> )} {props.onDelete && ( <Menu.Item color="red" leftSection={<IconTrash size={14} stroke={1.5} color="red" />} onClick={() => { (props.onDelete as (resource: Resource) => void)(value); }} > Delete </Menu.Item> )} </Menu.Dropdown> </Menu> )} </Group> </form> ); }

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