Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,21 @@ export interface ConfigSubtreeProps {
}

export function ConfigSubtree({ config, path = [] }: ConfigSubtreeProps) {
function setActiveItemDescriptionOnMouseEnter(thisPath, key, value) {
function setActiveItemDescriptionOnMouseEnter(thisPath: Array<string>, 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<string>) {
const state = implementerToolsStore.getState();
if (isEqual(state.activeItemDescription?.path, thisPath) && !isEqual(state.configPathBeingEdited, thisPath)) {
implementerToolsStore.setState({ activeItemDescription: undefined });
Expand All @@ -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 (
<Subtree
label={key}
leaf={isLeaf}
onMouseEnter={() => setActiveItemDescriptionOnMouseEnter(thisPath, key, value)}
onMouseLeave={() => removeActiveItemDescriptionOnMouseLeave(thisPath)}
key={`subtree-${thisPath.join('.')}`}
>
{isLeaf ? (
<EditableValue path={thisPath} element={value} />
) : (
<ConfigSubtree config={value} path={thisPath} />
)}
</Subtree>
);
})}
{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 (
<Subtree
label={key}
leaf={isLeaf}
onMouseEnter={() => setActiveItemDescriptionOnMouseEnter(thisPath, value)}
onMouseLeave={() => removeActiveItemDescriptionOnMouseLeave(thisPath)}
key={`subtree-${thisPath.join('.')}`}
>
{isLeaf ? (
<EditableValue path={thisPath} element={value} />
) : (
<ConfigSubtree config={value} path={thisPath} />
)}
</Subtree>
);
})}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Loading