@openmrs/esm-implementer-tools-app 9.0.3-pre.4788 → 9.0.3-pre.4809

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { type ExtensionInfo } from '@openmrs/esm-framework/src/internal';
3
+ import { getExtensionOverlayTargets } from './ui-editor';
4
+
5
+ // `extensions[name].instances` is an Array<ExtensionInstance> in the framework store. These tests
6
+ // guard against regressing to an object-style traversal, which produced array indices as module
7
+ // names and instance property names as slot names, so no overlay ever matched a real DOM node.
8
+ const extensions = {
9
+ 'nav-item': {
10
+ instances: [
11
+ { id: 'nav-item-0', slotName: 'top-nav-slot', slotModuleName: '@openmrs/esm-nav-app' },
12
+ { id: 'nav-item-1', slotName: 'side-nav-slot', slotModuleName: '@openmrs/esm-nav-app' },
13
+ ],
14
+ },
15
+ 'patient-banner': {
16
+ instances: [{ id: 'patient-banner-0', slotName: 'patient-header-slot', slotModuleName: '@openmrs/esm-chart-app' }],
17
+ },
18
+ } as unknown as Record<string, ExtensionInfo>;
19
+
20
+ describe('getExtensionOverlayTargets', () => {
21
+ it('flattens the instances array into real slot, module, and id descriptors', () => {
22
+ const targets = getExtensionOverlayTargets(extensions);
23
+
24
+ expect(targets).toHaveLength(3);
25
+ expect(targets[0]).toMatchObject({
26
+ extensionName: 'nav-item',
27
+ slotName: 'top-nav-slot',
28
+ slotModuleName: '@openmrs/esm-nav-app',
29
+ });
30
+ expect(targets[0].extensionInstance.id).toBe('nav-item-0');
31
+ expect(targets[2]).toMatchObject({
32
+ extensionName: 'patient-banner',
33
+ slotName: 'patient-header-slot',
34
+ slotModuleName: '@openmrs/esm-chart-app',
35
+ });
36
+ });
37
+
38
+ it('does not surface array indices or instance property names (regression guard)', () => {
39
+ const targets = getExtensionOverlayTargets(extensions);
40
+
41
+ // Old object-traversal bug surfaced "0"/"1" as module names and "id"/"slotName"/"slotModuleName"
42
+ // as slot names, with an undefined instance id.
43
+ expect(targets.map((target) => target.slotModuleName)).not.toContain('0');
44
+ expect(targets.map((target) => target.slotName)).toEqual(
45
+ expect.not.arrayContaining(['id', 'slotName', 'slotModuleName']),
46
+ );
47
+ expect(targets.every((target) => typeof target.extensionInstance.id === 'string')).toBe(true);
48
+ });
49
+
50
+ it('returns an empty list when there are no extensions', () => {
51
+ expect(getExtensionOverlayTargets(undefined)).toEqual([]);
52
+ expect(getExtensionOverlayTargets({})).toEqual([]);
53
+ });
54
+ });
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
5
5
  import { Button } from '@carbon/react';
6
6
  import {
7
7
  CloseIcon,
8
+ type ExtensionInfo,
8
9
  getExtensionInternalStore,
9
10
  useStore,
10
11
  useStoreWithActions,
@@ -26,6 +27,32 @@ interface SlotOverlayProps {
26
27
  colorScheme: 'blue' | 'green';
27
28
  }
28
29
 
30
+ export interface ExtensionOverlayTarget {
31
+ extensionName: string;
32
+ slotModuleName: string;
33
+ slotName: string;
34
+ extensionInstance: ExtensionInfo['instances'][number];
35
+ }
36
+
37
+ /**
38
+ * Flattens `extensions[name].instances` into a flat list of overlay targets. `instances` is an
39
+ * `Array<ExtensionInstance>`, so it must be iterated directly. Treating it as a nested object (via
40
+ * `Object.entries`) yields array indices as the module name and instance property names as the slot
41
+ * name, so the generated selectors never match a real extension DOM node.
42
+ */
43
+ export function getExtensionOverlayTargets(
44
+ extensions: Record<string, ExtensionInfo> | undefined,
45
+ ): Array<ExtensionOverlayTarget> {
46
+ return Object.entries(extensions ?? {}).flatMap(([extensionName, extensionInfo]) =>
47
+ (extensionInfo.instances ?? []).map((extensionInstance) => ({
48
+ extensionName,
49
+ slotModuleName: extensionInstance.slotModuleName,
50
+ slotName: extensionInstance.slotName,
51
+ extensionInstance,
52
+ })),
53
+ );
54
+ }
55
+
29
56
  export default function UiEditor() {
30
57
  const { t } = useTranslation();
31
58
  const { slots, extensions } = useStore(getExtensionInternalStore());
@@ -67,25 +94,16 @@ export default function UiEditor() {
67
94
  .filter((x): x is NonNullable<typeof x> => Boolean(x));
68
95
  }, [slots]);
69
96
 
70
- const extensionElements = useMemo(() => {
71
- if (!extensions) {
72
- return [];
73
- }
74
-
75
- return Object.entries(extensions).flatMap(([extensionName, extensionInfo]) =>
76
- Object.entries(extensionInfo.instances).flatMap(([slotModuleName, bySlotName]) =>
77
- Object.entries(bySlotName).map(([slotName, extensionInstance]) => ({
78
- extensionName,
79
- slotModuleName,
80
- slotName,
81
- extensionInstance,
82
- element: document.querySelector(
83
- `*[data-extension-slot-name="${slotName}"][data-extension-slot-module-name="${slotModuleName}"] *[data-extension-id="${extensionInstance.id}"]`,
84
- ) as HTMLElement | null,
85
- })),
86
- ),
87
- );
88
- }, [extensions]);
97
+ const extensionElements = useMemo(
98
+ () =>
99
+ getExtensionOverlayTargets(extensions).map((target) => ({
100
+ ...target,
101
+ element: document.querySelector(
102
+ `*[data-extension-slot-name="${target.slotName}"][data-extension-slot-module-name="${target.slotModuleName}"] *[data-extension-id="${target.extensionInstance.id}"]`,
103
+ ) as HTMLElement | null,
104
+ })),
105
+ [extensions],
106
+ );
89
107
 
90
108
  return (
91
109
  <>