Skip to main content
Glama
Spotlight.tsx5.93 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { SpotlightActionData, SpotlightActionGroupData } from '@mantine/spotlight'; import { Spotlight as MantineSpotlight } from '@mantine/spotlight'; import { formatHumanName, isUUID } from '@medplum/core'; import type { Patient, ServiceRequest } from '@medplum/fhirtypes'; import type { MedplumNavigateFunction } from '@medplum/react-hooks'; import { useMedplum, useMedplumNavigate } from '@medplum/react-hooks'; import { IconSearch } from '@tabler/icons-react'; import type { JSX } from 'react'; import { useState } from 'react'; export type HeaderSearchTypes = Patient | ServiceRequest; interface SearchGraphQLResponse { readonly data: { readonly Patients1: Patient[] | undefined; readonly Patients2: Patient[] | undefined; readonly ServiceRequestList: ServiceRequest[] | undefined; }; } export function Spotlight(): JSX.Element { const medplum = useMedplum(); const navigate = useMedplumNavigate(); const [nothingFoundMessage, setNothingFoundMessage] = useState('Type to search...'); const [actions, setActions] = useState<SpotlightActionGroupData[]>([]); const handleQueryChange = (query: string): void => { if (!query) { setNothingFoundMessage('Type to search...'); setActions([]); return; } setNothingFoundMessage('Searching...'); const graphqlQuery = buildGraphQLQuery(query); medplum .graphql(graphqlQuery) .then((response: SearchGraphQLResponse) => { const resources = getResourcesFromResponse(response); const newActions = resourcesToActions(resources, navigate); setActions(newActions); }) .catch((error) => { console.error('GraphQL query failed:', error); }) .finally(() => { setNothingFoundMessage('No results found'); }); }; return ( <MantineSpotlight actions={actions} nothingFound={nothingFoundMessage} highlightQuery searchProps={{ leftSection: <IconSearch size={20} stroke={1.5} />, placeholder: 'Search...', }} onQueryChange={handleQueryChange} /> ); } function buildGraphQLQuery(input: string): string { const escaped = JSON.stringify(input); if (isUUID(input)) { return `{ Patients1: PatientList(_id: ${escaped}, _count: 1) { resourceType id identifier { system value } name { given family } birthDate } ServiceRequestList(_id: ${escaped}, _count: 1) { resourceType id identifier { system value } subject { display } } }`.replaceAll(/\s+/g, ' '); } return `{ Patients1: PatientList(name: ${escaped}, _count: 5) { resourceType id identifier { system value } name { given family } birthDate } Patients2: PatientList(identifier: ${escaped}, _count: 5) { resourceType id identifier { system value } name { given family } birthDate } ServiceRequestList(identifier: ${escaped}, _count: 5) { resourceType id identifier { system value } subject { display } } }`.replaceAll(/\s+/g, ' '); } /** * Returns a de-duped and sorted list of resources from the search response. * The search request is actually 3+ separate searches, which can include duplicates. * This function combines the results, de-dupes, and sorts by relevance. * @param response - The response from a search query. * @returns The resources to display in the autocomplete. */ function getResourcesFromResponse(response: SearchGraphQLResponse): HeaderSearchTypes[] { const resources = []; if (response.data.Patients1) { resources.push(...response.data.Patients1); } if (response.data.Patients2) { resources.push(...response.data.Patients2); } if (response.data.ServiceRequestList) { resources.push(...response.data.ServiceRequestList); } return dedupeResources(resources); } /** * Removes duplicate resources from an array by ID. * @param resources - The array of resources with possible duplicates. * @returns The array of resources with no duplicates. */ function dedupeResources(resources: HeaderSearchTypes[]): HeaderSearchTypes[] { const ids = new Set<string>(); const result = []; for (const resource of resources) { if (!ids.has(resource.id as string)) { ids.add(resource.id as string); result.push(resource); } } return result; } function resourcesToActions( resources: HeaderSearchTypes[], navigate: MedplumNavigateFunction ): SpotlightActionGroupData[] { const patientActions: SpotlightActionData[] = []; const serviceRequestActions: SpotlightActionData[] = []; for (const resource of resources) { if (resource.resourceType === 'Patient') { patientActions.push({ id: resource.id as string, label: resource.name ? formatHumanName(resource.name[0]) : resource.id, description: resource.birthDate, onClick: () => navigate(`/Patient/${resource.id}`), }); } else if (resource.resourceType === 'ServiceRequest') { serviceRequestActions.push({ id: resource.id as string, label: resource.id, description: resource.subject?.display, onClick: () => navigate(`/ServiceRequest/${resource.id}`), }); } } const result = []; if (patientActions.length > 0) { result.push({ group: 'Patients', actions: patientActions }); } if (serviceRequestActions.length > 0) { result.push({ group: 'Service Requests', actions: serviceRequestActions }); } return result; }

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