Skip to main content
Glama
SearchFieldEditor.tsx5.9 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { Button, Group, Modal, MultiSelect, Stack } from '@mantine/core'; import type { InternalTypeSchema, SearchRequest } from '@medplum/core'; import { getDataType, getSearchParameters, sortStringArray, stringify } from '@medplum/core'; import type { SearchParameter } from '@medplum/fhirtypes'; import type { JSX } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react'; import { buildFieldNameString } from '../SearchControl/SearchUtils'; export interface SearchFieldEditorProps { readonly visible: boolean; readonly search: SearchRequest; readonly onOk: (search: SearchRequest) => void; readonly onCancel: () => void; } export function SearchFieldEditor(props: SearchFieldEditorProps): JSX.Element | null { const wasDropdownOpen = useRef(false); const [state, setState] = useState({ search: JSON.parse(stringify(props.search)) as SearchRequest, }); const [isDropdownOpen, setIsDropdownOpen] = useState(false); useEffect(() => { setState({ search: props.search }); }, [props.search]); const allFields = useMemo(() => { if (!props.visible) { return []; } const resourceType = props.search.resourceType; const typeSchema = getDataType(resourceType); const searchParams = getSearchParameters(resourceType); return sortStringArray(getFieldsList(typeSchema, searchParams)).map((field) => { return { value: field, label: buildFieldNameString(field) }; }); }, [props.visible, props.search.resourceType]); function handleChange(newFields: string[]): void { setState({ search: { ...state.search, fields: newFields } }); } return ( <Modal title="Fields" closeButtonProps={{ 'aria-label': 'Close' }} opened={props.visible} onClose={() => { props.onCancel(); }} size="auto" /* By default, the MultiSelect dropdown does not interact well with Modal's closeOnClickOutside: When the MultiSelect's dropdown is opened and the user clicks outside of the dropdown to close it (and outside the modal, i.e. clicks on the Modal's overlay), the Modal is undesirably also closed from the same click. Due to the sequencing of the events fired during a click on the overlay and when React rerenders of various components occur, it is not possible to simply do something such as setting closeOnClickOutside={!isDropdownOpened}: * user begins a click on the overlay which triggers * mousedown event on the overlay which triggers * blur event on the MultiSelect's input element which invokes * the MultiSelect.onDropdownClose callback which calls setIsDropdownOpen(false) which causes * rerender of SearchFieldEditor with isDropdownOpen set to false * the user ends the click which triggers * click event on the Modal which activates the closeOnClickOutside logic * since isDropdownOpen is false, closeOnClickOutside is true, so the Modal closes Instead, emulate closeOnClickOutside's behavior only when the MultiSelect dropdown was not open at the beginning of the click */ withOverlay closeOnClickOutside={false} overlayProps={{ onMouseDownCapture: () => { // capture whether the MultiSelect dropdown is open when a click on the overlay begins (i.e. mousedown) wasDropdownOpen.current = isDropdownOpen; }, onClick: () => { if (!wasDropdownOpen.current) { // invoke onCancel callback since the dropdown wasn't open at the start of the click on the overlay props.onCancel(); } // not strictly needed since onMouseDownCapture should always precede onClick, but reset the ref wasDropdownOpen.current = false; }, children: <div data-testid="overlay-child" />, // can't specify testid on the overlay itself }} > <Stack> <MultiSelect // withinPortal={true} style={{ width: 550 }} placeholder="Select fields to display" data={allFields} value={state.search.fields ?? []} onChange={handleChange} onDropdownOpen={() => setIsDropdownOpen(true)} onDropdownClose={() => setIsDropdownOpen(false)} /* shows at most ~6.5 items; the extra half to provide a hint that there are more entries to scroll through */ maxDropdownHeight="250px" // dropdownPosition="bottom" clearButtonProps={{ 'aria-label': 'Clear selection' }} clearable searchable /> <Group justify="flex-end"> <Button onClick={() => props.onOk(state.search)}>OK</Button> </Group> </Stack> </Modal> ); } /** * Returns a list of fields/columns available for a type. * The result is the union of properties and search parameters. * @param typeSchema - The type definition. * @param searchParams - The search parameters. * @returns A list of fields/columns available for a resource type. */ function getFieldsList( typeSchema: InternalTypeSchema, searchParams: Record<string, SearchParameter> | undefined ): string[] { const result = [] as string[]; const keys = new Set<string>(); const names = new Set<string>(); // Add properties first for (const key of Object.keys(typeSchema.elements)) { result.push(key); keys.add(key.toLowerCase()); names.add(buildFieldNameString(key)); } // Add search parameters if unique if (searchParams) { for (const code of Object.keys(searchParams)) { const name = buildFieldNameString(code); if (!keys.has(code) && !names.has(name)) { result.push(code); keys.add(code); names.add(name); } } } 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