import React, { useEffect, forwardRef, useReducer, type Dispatch, useState, useRef } from 'react'; import { Button, Search, Table, TableBody, TableCell, TableContainer, TableHead, TableHeader, TableRow, } from '@carbon/react'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; import fuzzy from 'fuzzy'; import { useDebounce, type ImportMap, showModal, resetRouteMapOverrides, getCurrentPageMap, getImportMapDefaultMap, getImportMapNextPageMap, getImportMapOverrideMap, getImportMapDisabledOverrides, resetImportMapOverrides, } from '@openmrs/esm-framework/src/internal'; import type { Module } from './types'; import styles from './list.scss'; interface ImportMapListState { notOverriddenMap: ImportMap; currentPageMap: ImportMap; nextPageMap: ImportMap; dialogModule: { module: Module; isNew: false } | { isNew: true } | null; dialogExternalMap: null; } type ImportMapDispatchAction = | SetDefaultMapAction | SetCurrentMapAction | SetNextDispatchAction | ShowNewModuleDialogAction | ShowEditModuleDialogAction | HideEditModuleDialogAction | ResetAllOverridesAction; interface SetDefaultMapAction { type: 'set_default_map'; notOverriddenMap: ImportMap; } interface SetCurrentMapAction { type: 'set_current_map'; currentPageMap: ImportMap; } interface SetNextDispatchAction { type: 'set_next_map'; nextPageMap: ImportMap; } interface ShowNewModuleDialogAction { type: 'show_new_module_dialog'; } interface ShowEditModuleDialogAction { type: 'show_edit_module_dialog'; module: Module; } interface HideEditModuleDialogAction { type: 'hide_edit_module_dialog'; } interface ResetAllOverridesAction { type: 'reset_all_overrides'; } const initialImportMapState: ImportMapListState = { notOverriddenMap: { imports: {} }, currentPageMap: { imports: {} }, nextPageMap: { imports: {} }, dialogModule: null, dialogExternalMap: null, }; function updateToNext(dispatch: Dispatch) { return () => getImportMapNextPageMap().then((nextPageMap) => dispatch({ type: 'set_next_map', nextPageMap })); } function reducer(state: ImportMapListState, action: ImportMapDispatchAction) { switch (action.type) { case 'set_default_map': return { ...state, notOverriddenMap: action.notOverriddenMap }; case 'set_current_map': return { ...state, currentPageMap: action.currentPageMap }; case 'set_next_map': return { ...state, nextPageMap: action.nextPageMap }; case 'show_new_module_dialog': return { ...state, dialogModule: { isNew: true }, }; case 'show_edit_module_dialog': return { ...state, dialogModule: { module: action.module, isNew: false }, }; case 'hide_edit_module_dialog': return { ...state, dialogModule: null, }; case 'reset_all_overrides': resetImportMapOverrides(); resetRouteMapOverrides(); return state; } } const ImportMapList = forwardRef((props, ref) => { const { t } = useTranslation(); const [state, dispatch] = useReducer(reducer, initialImportMapState); const inputRef = useRef(); const [searchQuery, setSearchQuery] = useState(''); const searchVal = useDebounce(searchQuery); useEffect(() => { getImportMapDefaultMap().then((notOverriddenMap) => dispatch({ type: 'set_default_map', notOverriddenMap })); getCurrentPageMap().then((currentPageMap) => dispatch({ type: 'set_current_map', currentPageMap })); // focus on the search box inputRef.current?.focus(); // add our event listener const dispatcher = updateToNext(dispatch); window.addEventListener('import-map-overrides:change', dispatcher); // clean-up event listener return () => window.removeEventListener('import-map-overrides:change', dispatcher); }, []); useEffect(() => { if (state.dialogModule) { showModal( 'importmap-override-modal', { ...state.dialogModule, }, () => { dispatch({ type: 'hide_edit_module_dialog' }); }, ); } }, [state.dialogModule]); const overriddenModules: Array = [], nextOverriddenModules: Array = [], disabledOverrides: Array = [], defaultModules: Array = [], externalOverrideModules: Array = [], pendingRefreshDefaultModules: Array = []; const overrideMap = getImportMapOverrideMap(true).imports; const notOverriddenKeys = Object.keys(state.notOverriddenMap.imports); const disabledModules = getImportMapDisabledOverrides(); const searchableKeys = [...new Set([...notOverriddenKeys, ...Object.keys(overrideMap)])]; searchableKeys.sort(); fuzzy.filter(searchVal, searchableKeys).map((searchResult) => { const moduleName = searchResult.original; const mod: Module = { moduleName, defaultUrl: state.notOverriddenMap.imports[moduleName], overrideUrl: overrideMap[moduleName], disabled: disabledModules.includes(moduleName), order: searchResult.score, }; if (mod.disabled) { disabledOverrides.push(mod); } else if (mod.overrideUrl) { if (state.currentPageMap.imports[moduleName] === mod.overrideUrl) { overriddenModules.push(mod); } else { nextOverriddenModules.push(mod); } } else if (state.currentPageMap.imports[moduleName] === mod.defaultUrl) { defaultModules.push(mod); } else if (state.nextPageMap.imports[moduleName] === mod.defaultUrl) { pendingRefreshDefaultModules.push(mod); } else { externalOverrideModules.push({ ...mod, overrideUrl: state.currentPageMap.imports[moduleName], }); } }); overriddenModules.sort(sorter); defaultModules.sort(sorter); nextOverriddenModules.sort(sorter); defaultModules.sort(sorter); disabledOverrides.sort(sorter); externalOverrideModules.sort(sorter); pendingRefreshDefaultModules.sort(sorter); return (
setSearchQuery(evt.target.value)} labelText="" size="lg" />
{t('moduleStatus', 'Module Status')} {t('moduleName', 'Module Name')} {t('domain', 'Domain')} {t('filename', 'Filename')} {nextOverriddenModules.map((mod) => ( dispatch({ type: 'show_edit_module_dialog', module: mod })} >
{ evt.preventDefault(); window.location.reload(); }} />
{t('inlineOverride', 'Inline Override')}
{mod.moduleName} {toDomain(mod)} {toFileName(mod)} ))} {pendingRefreshDefaultModules.map((mod) => ( dispatch({ type: 'show_edit_module_dialog', module: mod })} >
{ evt.preventDefault(); window.location.reload(); }} />
{t('default', 'Default')}
{mod.moduleName} {toDomain(mod)} {toFileName(mod)} ))} {disabledOverrides.map((mod) => ( dispatch({ type: 'show_edit_module_dialog', module: mod })} >
{t('overrideDisabled', 'Override Disabled')}
{mod.moduleName} {toDomain(mod)} {toFileName(mod)} ))} {overriddenModules.map((mod) => ( dispatch({ type: 'show_edit_module_dialog', module: mod })} >
{t('inlineOverride', 'Inline Override')}
{mod.moduleName} {toDomain(mod)} {toFileName(mod)} ))} {externalOverrideModules.map((mod) => ( dispatch({ type: 'show_edit_module_dialog', module: mod })} >
{t('externalOverride', 'External Override')}
{mod.moduleName} {toDomain(mod)} {toFileName(mod)} ))} {defaultModules.map((mod) => ( dispatch({ type: 'show_edit_module_dialog', module: mod })} >
{t('default', 'Default')}
{mod.moduleName} {toDomain(mod)} {toFileName(mod)} ))}
); }); function sorter(obj1: { order: number }, obj2: { order: number }) { return obj1.order - obj2.order; } const currentBase = (document.querySelector('base') && document.querySelector('base').href) || location.origin + '/'; function toDomain(mod: Module) { const urlStr = toUrlStr(mod); const url = toURL(urlStr); return url ? url.host : urlStr; } function toFileName(mod: Module) { const urlStr = toUrlStr(mod); const url = toURL(urlStr); return url ? url.pathname.slice(url.pathname.lastIndexOf('/') + 1) : urlStr; } function toUrlStr(mod: Module) { return mod.overrideUrl || mod.defaultUrl; } function toURL(urlStr: string) { try { return new URL(urlStr, currentBase); } catch { return null; } } export default ImportMapList;