import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { debounce } from 'lodash-es'; import { ComboBox, DropdownSkeleton, Layer, InlineLoading } from '@carbon/react'; import { useTranslation } from 'react-i18next'; import { useWatch } from 'react-hook-form'; import { type OpenmrsResource } from '@openmrs/esm-framework'; import { getControlTemplate } from '../../../registry/inbuilt-components/control-templates'; import { getRegisteredDataSource } from '../../../registry/registry'; import { isEmpty } from '../../../validators/form-validator'; import { isTrue } from '../../../utils/boolean-utils'; import { isViewMode } from '../../../utils/common-utils'; import { shouldUseInlineLayout } from '../../../utils/form-helper'; import { type DataSource, type FormFieldInputProps } from '../../../types'; import { useFormProviderContext } from '../../../provider/form-provider'; import useDataSourceDependentValue from '../../../hooks/useDataSourceDependentValue'; import FieldLabel from '../../field-label/field-label.component'; import FieldValueView from '../../value/view/field-value-view.component'; import styles from './ui-select-extended.scss'; const UiSelectExtended: React.FC = ({ field, errors, warnings, setFieldValue }) => { const { t } = useTranslation(); const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isSearching, setIsSearching] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const isProcessingSelection = useRef(false); const [dataSource, setDataSource] = useState(null); const [config, setConfig] = useState({}); const dataSourceDependentValue = useDataSourceDependentValue(field); const isSearchable = isTrue(field.questionOptions.isSearchable); const { layoutType, sessionMode, workspaceLayout, methods: { control, getFieldState }, } = useFormProviderContext(); const value = useWatch({ control, name: field.id, exact: true }); const { isDirty } = getFieldState(field.id); const isInline = useMemo(() => { if (isViewMode(sessionMode) || isTrue(field.readonly)) { return shouldUseInlineLayout(field.inlineRendering, layoutType, workspaceLayout, sessionMode); } return false; }, [sessionMode, field.readonly, field.inlineRendering, layoutType, workspaceLayout]); const selectedItem = useMemo(() => items.find((item) => item.uuid == value) || null, [items, value]); const debouncedSearch = debounce((searchTerm: string, dataSource: DataSource) => { setIsSearching(true); dataSource .fetchData(searchTerm, config) .then((dataItems) => { if (dataItems.length) { const currentSelectedItem = items.find((item) => item.uuid == value); const newItems = dataItems.map(dataSource.toUuidAndDisplay); if (currentSelectedItem && !newItems.some((item) => item.uuid == currentSelectedItem.uuid)) { newItems.unshift(currentSelectedItem); } setItems(newItems); } setIsSearching(false); }) .catch((err) => { console.error(err); setIsSearching(false); }); }, 300); const searchTermHasMatchingItem = useCallback( (searchTerm: string) => { return items.some((item) => item.display?.toLowerCase().includes(searchTerm.toLowerCase())); }, [items], ); useEffect(() => { const dataSource = field.questionOptions?.datasource?.name; setConfig( dataSource ? field.questionOptions.datasource?.config : getControlTemplate(field.questionOptions.rendering)?.datasource?.config, ); getRegisteredDataSource(dataSource ? dataSource : field.questionOptions.rendering).then((ds) => setDataSource(ds)); }, [field.questionOptions?.datasource]); useEffect(() => { let ignore = false; // If not searchable, preload the items if (dataSource && !isTrue(field.questionOptions.isSearchable)) { setItems([]); setIsLoading(true); dataSource .fetchData(null, { ...config, referencedValue: dataSourceDependentValue }) .then((dataItems) => { if (!ignore) { setItems(dataItems.map(dataSource.toUuidAndDisplay)); setIsLoading(false); } }) .catch((err) => { if (!ignore) { console.error(err); setIsLoading(false); setItems([]); } }); } return () => { ignore = true; }; }, [dataSource, config, dataSourceDependentValue]); useEffect(() => { if (dataSource && isSearchable && !isEmpty(searchTerm) && !searchTermHasMatchingItem(searchTerm)) { debouncedSearch(searchTerm, dataSource); } }, [dataSource, searchTerm, config]); useEffect(() => { let ignore = false; if (value && !isDirty && dataSource && isSearchable && !items.length) { // For search-based instances, fetch the initial item to resolve its display property setIsLoading(true); dataSource .fetchSingleItem(value) .then((item) => { if (!ignore) { setItems([dataSource.toUuidAndDisplay(item)]); setIsLoading(false); } }) .catch((error) => { if (!ignore) { console.error(error); setIsLoading(false); } }); } return () => { ignore = true; }; }, [value, isDirty, dataSource, isSearchable, items]); if (isLoading) { return ; } return isViewMode(sessionMode) || isTrue(field.readonly) ? ( item.uuid == value)?.display : value} conceptName={field.meta?.concept?.display} isInline={isInline} /> ) : ( !field.isHidden && (
} items={items} itemToString={(item) => item?.display} selectedItem={selectedItem} placeholder={isSearchable ? t('search', 'Search') + '...' : null} onChange={({ selectedItem }) => { isProcessingSelection.current = true; setFieldValue(selectedItem?.uuid); }} disabled={field.isDisabled} readOnly={isTrue(field.readonly)} invalid={errors.length > 0} invalidText={errors.length && errors[0].message} onInputChange={(value) => { if (isProcessingSelection.current) { // Notes: // When the user selects a value, both the onChange and onInputChange functions are invoked sequentially. // Issue: onInputChange modifies the search term, unnecessarily triggering a search. isProcessingSelection.current = false; return; } if (field.questionOptions.isSearchable) { setSearchTerm(value); } }} onBlur={(event) => { // Notes: // There is an issue with the onBlur event where the value is not persistently set to null when the user clears the input field. // This is a workaround to ensure that the value is set to null when the user clears the input field. if (!event.target.value) { setFieldValue(null); } }} /> {isSearching && }
) ); }; export default UiSelectExtended;