Skip to main content
Glama
FhirPathTable.tsx6.66 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { Button, Loader, Table } from '@mantine/core'; import { normalizeOperationOutcome } from '@medplum/core'; import type { OperationOutcome, Resource } from '@medplum/fhirtypes'; import { useMedplum } from '@medplum/react-hooks'; import type { ChangeEvent, JSX, MouseEvent } from 'react'; import { memo, useEffect, useRef, useState } from 'react'; import { FhirPathDisplay } from '../FhirPathDisplay/FhirPathDisplay'; import { SearchClickEvent } from '../SearchControl/SearchControl'; import { isCheckboxCell, killEvent } from '../utils/dom'; export interface FhirPathTableField { readonly propertyType: string; readonly name: string; readonly fhirPath: string; } export interface FhirPathTableProps { readonly resourceType: string; readonly query: string; readonly fields: FhirPathTableField[]; readonly checkboxesEnabled?: boolean; readonly onClick?: (e: SearchClickEvent) => void; readonly onAuxClick?: (e: SearchClickEvent) => void; readonly onBulk?: (ids: string[]) => void; } export interface SmartSearchResponse { readonly data: { ResourceList: Resource[]; }; } /** * The FhirPathTable component represents the embeddable search table control. * @param props - FhirPathTable React props. * @returns FhirPathTable React node. */ export function FhirPathTable(props: FhirPathTableProps): JSX.Element { const medplum = useMedplum(); const [schemaLoaded, setSchemaLoaded] = useState(false); const [outcome, setOutcome] = useState<OperationOutcome | undefined>(); const { query, fields } = props; const [response, setResponse] = useState<SmartSearchResponse | undefined>(); const [selected, setSelected] = useState<{ [id: string]: boolean }>({}); const responseRef = useRef<SmartSearchResponse>(response); responseRef.current = response; const selectedRef = useRef<{ [id: string]: boolean }>({}); selectedRef.current = selected; useEffect(() => { setOutcome(undefined); medplum .graphql(query) .then(setResponse) .catch((err) => setOutcome(normalizeOperationOutcome(err))); }, [medplum, query]); function handleSingleCheckboxClick(e: ChangeEvent, id: string): void { e.stopPropagation(); const el = e.target as HTMLInputElement; const checked = el.checked; const newSelected = { ...selectedRef.current }; if (checked) { newSelected[id] = true; } else { delete newSelected[id]; } setSelected(newSelected); } function handleAllCheckboxClick(e: ChangeEvent): void { e.stopPropagation(); const el = e.target as HTMLInputElement; const checked = el.checked; const newSelected = {} as { [id: string]: boolean }; const resources = responseRef.current?.data.ResourceList; if (checked && resources) { resources.forEach((resource) => { if (resource.id) { newSelected[resource.id] = true; } }); } setSelected(newSelected); } function isAllSelected(): boolean { const resources = responseRef.current?.data.ResourceList; if (!resources || resources.length === 0) { return false; } for (const resource of resources) { if (resource.id && !selectedRef.current[resource.id]) { return false; } } return true; } function handleRowClick(e: MouseEvent, resource: Resource): void { if (isCheckboxCell(e.target as Element)) { // Ignore clicks on checkboxes return; } killEvent(e); if (e.button !== 1 && props.onClick) { props.onClick(new SearchClickEvent(resource, e)); } if (e.button === 1 && props.onAuxClick) { props.onAuxClick(new SearchClickEvent(resource, e)); } } useEffect(() => { medplum .requestSchema(props.resourceType) .then(() => setSchemaLoaded(true)) .catch(console.log); }, [medplum, props.resourceType]); if (!schemaLoaded) { return <Loader />; } const checkboxColumn = props.checkboxesEnabled; return ( <div onContextMenu={(e) => killEvent(e)} data-testid="search-control"> <Table> <Table.Thead> <Table.Tr> {checkboxColumn && ( <Table.Th> <input type="checkbox" value="checked" aria-label="all-checkbox" data-testid="all-checkbox" checked={isAllSelected()} onChange={(e) => handleAllCheckboxClick(e)} /> </Table.Th> )} {fields.map((field) => ( <Table.Th key={field.name}>{field.name}</Table.Th> ))} </Table.Tr> </Table.Thead> <Table.Tbody> {response?.data.ResourceList.map( (resource) => resource && ( <Table.Tr key={resource.id} data-testid="search-control-row" onClick={(e) => handleRowClick(e, resource)} onAuxClick={(e) => handleRowClick(e, resource)} > {checkboxColumn && ( <Table.Td> <input type="checkbox" value="checked" data-testid="row-checkbox" aria-label={`Checkbox for ${resource.id}`} checked={!!selected[resource.id as string]} onChange={(e) => handleSingleCheckboxClick(e, resource.id as string)} /> </Table.Td> )} {fields.map((field) => { return ( <Table.Td key={field.name}> <FhirPathDisplay propertyType={field.propertyType} path={field.fhirPath} resource={resource} /> </Table.Td> ); })} </Table.Tr> ) )} </Table.Tbody> </Table> {response?.data.ResourceList.length === 0 && <div data-testid="empty-search">No results</div>} {outcome && ( <div data-testid="search-error"> <pre style={{ textAlign: 'left' }}>{JSON.stringify(outcome, undefined, 2)}</pre> </div> )} {props.onBulk && ( <Button onClick={() => (props.onBulk as (ids: string[]) => any)(Object.keys(selectedRef.current))}> Bulk... </Button> )} </div> ); } export const MemoizedFhirPathTable = memo(FhirPathTable);

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