diff --git a/packages/apps/esm-implementer-tools-app/src/configuration/configuration.test.tsx b/packages/apps/esm-implementer-tools-app/src/configuration/configuration.test.tsx index f5e4bf179..ad8f2720f 100644 --- a/packages/apps/esm-implementer-tools-app/src/configuration/configuration.test.tsx +++ b/packages/apps/esm-implementer-tools-app/src/configuration/configuration.test.tsx @@ -361,4 +361,38 @@ describe('Configuration', () => { // expect(mockSetTemporaryConfigValue).toHaveBeenCalledWith(["@openmrs/luigi", "favoriteNumbers"], [5, 11, 13]); } }); + + it('handles hovering over config tree items without crashing', async () => { + const user = userEvent.setup(); + + implementerToolsConfigStore.setState({ + config: { + '@openmrs/mario': { + hasHat: mockImplToolsConfig['@openmrs/mario'].hasHat, + weapons: { + gloves: { + _type: Type.Number, + _default: 0, + _value: 2, + _source: 'provided', + }, + }, + }, + }, + }); + + renderConfiguration(); + + // Find and hover over a leaf node (hasHat) + const hasHatElement = await screen.findByText('hasHat'); + await user.hover(hasHatElement); + + // Find and hover over a branch node (weapons) - this should not crash + const weaponsElement = await screen.findByText('weapons'); + await user.hover(weaponsElement); + + // Both elements should still be in the document (no crash occurred) + expect(hasHatElement).toBeInTheDocument(); + expect(weaponsElement).toBeInTheDocument(); + }); }); diff --git a/packages/apps/esm-implementer-tools-app/src/configuration/interactive-editor/config-subtree.component.tsx b/packages/apps/esm-implementer-tools-app/src/configuration/interactive-editor/config-subtree.component.tsx index 574029031..09afa7251 100644 --- a/packages/apps/esm-implementer-tools-app/src/configuration/interactive-editor/config-subtree.component.tsx +++ b/packages/apps/esm-implementer-tools-app/src/configuration/interactive-editor/config-subtree.component.tsx @@ -10,20 +10,21 @@ export interface ConfigSubtreeProps { } export function ConfigSubtree({ config, path = [] }: ConfigSubtreeProps) { - function setActiveItemDescriptionOnMouseEnter(thisPath, key, value) { + function setActiveItemDescriptionOnMouseEnter(thisPath: Array, value: any) { if (!implementerToolsStore.getState().configPathBeingEdited) { + const isLeaf = value && typeof value === 'object' && Object.hasOwn(value, '_value'); implementerToolsStore.setState({ activeItemDescription: { path: thisPath, - source: value._source, - description: value._description, - value: JSON.stringify(value._value), + source: isLeaf ? value._source : undefined, + description: isLeaf ? value._description : undefined, + value: isLeaf ? JSON.stringify(value._value) : undefined, }, }); } } - function removeActiveItemDescriptionOnMouseLeave(thisPath) { + function removeActiveItemDescriptionOnMouseLeave(thisPath: Array) { const state = implementerToolsStore.getState(); if (isEqual(state.activeItemDescription?.path, thisPath) && !isEqual(state.configPathBeingEdited, thisPath)) { implementerToolsStore.setState({ activeItemDescription: undefined }); @@ -32,25 +33,27 @@ export function ConfigSubtree({ config, path = [] }: ConfigSubtreeProps) { return ( <> - {Object.entries(config).map(([key, value], i) => { - const thisPath = path.concat([key]); - const isLeaf = value.hasOwnProperty('_value') || value.hasOwnProperty('_type'); - return ( - setActiveItemDescriptionOnMouseEnter(thisPath, key, value)} - onMouseLeave={() => removeActiveItemDescriptionOnMouseLeave(thisPath)} - key={`subtree-${thisPath.join('.')}`} - > - {isLeaf ? ( - - ) : ( - - )} - - ); - })} + {Object.entries(config) + .filter(([key]) => !key.startsWith('_')) + .map(([key, value]) => { + const thisPath = path.concat([key]); + const isLeaf = value && typeof value === 'object' && Object.hasOwn(value, '_value'); + return ( + setActiveItemDescriptionOnMouseEnter(thisPath, value)} + onMouseLeave={() => removeActiveItemDescriptionOnMouseLeave(thisPath)} + key={`subtree-${thisPath.join('.')}`} + > + {isLeaf ? ( + + ) : ( + + )} + + ); + })} ); } diff --git a/packages/apps/esm-implementer-tools-app/src/configuration/interactive-editor/editable-value.component.tsx b/packages/apps/esm-implementer-tools-app/src/configuration/interactive-editor/editable-value.component.tsx index 46ac91cf0..93e2dff86 100644 --- a/packages/apps/esm-implementer-tools-app/src/configuration/interactive-editor/editable-value.component.tsx +++ b/packages/apps/esm-implementer-tools-app/src/configuration/interactive-editor/editable-value.component.tsx @@ -125,7 +125,9 @@ export default function EditableValue({ path, element, customType }: EditableVal hasIconOnly onClick={() => { clearConfigErrors(path.join('.')); - temporaryConfigStore.setState(unset(temporaryConfigStore.getState(), ['config', ...path]) as any); + const state = cloneDeep(temporaryConfigStore.getState()); + unset(state, ['config', ...path]); + temporaryConfigStore.setState(state); }} /> ) : null}