@openmrs/esm-report-builder 0.1.1-pre.92 → 0.1.1-pre.97

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.
Files changed (232) hide show
  1. package/dist/128.css +1 -0
  2. package/dist/128.css.map +1 -0
  3. package/dist/128.js +338 -0
  4. package/dist/128.js.map +1 -0
  5. package/dist/1480.js +1 -0
  6. package/dist/1480.js.map +1 -0
  7. package/dist/1646.js +1 -0
  8. package/dist/1646.js.map +1 -0
  9. package/dist/1869.js +1 -0
  10. package/dist/1869.js.map +1 -0
  11. package/dist/1877.js +1 -0
  12. package/dist/1877.js.map +1 -0
  13. package/dist/2131.js +1 -0
  14. package/dist/2131.js.map +1 -0
  15. package/dist/2317.js +1 -0
  16. package/dist/2317.js.map +1 -0
  17. package/dist/2408.js +1 -0
  18. package/dist/2408.js.map +1 -0
  19. package/dist/2416.js +1 -0
  20. package/dist/2416.js.map +1 -0
  21. package/dist/282.js +1 -0
  22. package/dist/282.js.map +1 -0
  23. package/dist/2881.js +1 -0
  24. package/dist/2881.js.map +1 -0
  25. package/dist/2997.js +1 -0
  26. package/dist/2997.js.map +1 -0
  27. package/dist/3378.js +1 -0
  28. package/dist/3378.js.map +1 -0
  29. package/dist/3963.js +1 -0
  30. package/dist/3963.js.map +1 -0
  31. package/dist/4106.js +1 -0
  32. package/dist/4106.js.map +1 -0
  33. package/dist/4111.js +1 -0
  34. package/dist/4111.js.map +1 -0
  35. package/dist/4171.css +1 -0
  36. package/dist/4171.css.map +1 -0
  37. package/dist/4171.js +43 -0
  38. package/dist/4171.js.map +1 -0
  39. package/dist/434.js +1 -0
  40. package/dist/434.js.map +1 -0
  41. package/dist/4348.js +1 -0
  42. package/dist/4348.js.map +1 -0
  43. package/dist/4383.js +1 -0
  44. package/dist/4383.js.map +1 -0
  45. package/dist/4658.js +1 -0
  46. package/dist/4658.js.map +1 -0
  47. package/dist/4668.js +1 -0
  48. package/dist/4668.js.map +1 -0
  49. package/dist/4928.js +1 -0
  50. package/dist/4928.js.map +1 -0
  51. package/dist/5098.js +1 -0
  52. package/dist/5098.js.map +1 -0
  53. package/dist/5117.js +1 -0
  54. package/dist/5117.js.map +1 -0
  55. package/dist/5132.js +1 -0
  56. package/dist/5132.js.map +1 -0
  57. package/dist/5145.js +1 -0
  58. package/dist/5145.js.map +1 -0
  59. package/dist/550.js +1 -0
  60. package/dist/550.js.map +1 -0
  61. package/dist/5503.js +1 -0
  62. package/dist/5503.js.map +1 -0
  63. package/dist/556.js +1 -0
  64. package/dist/556.js.map +1 -0
  65. package/dist/5644.js +1 -0
  66. package/dist/5644.js.map +1 -0
  67. package/dist/5940.js +1 -0
  68. package/dist/5940.js.map +1 -0
  69. package/dist/6047.js +1 -0
  70. package/dist/6047.js.map +1 -0
  71. package/dist/6237.js +1 -0
  72. package/dist/6237.js.map +1 -0
  73. package/dist/6247.js +1 -0
  74. package/dist/6247.js.map +1 -0
  75. package/dist/6371.js +1 -0
  76. package/dist/6371.js.map +1 -0
  77. package/dist/6377.js +1 -0
  78. package/dist/6377.js.map +1 -0
  79. package/dist/6444.js +1 -0
  80. package/dist/6444.js.map +1 -0
  81. package/dist/6508.js +1 -0
  82. package/dist/6508.js.map +1 -0
  83. package/dist/6652.js +1 -0
  84. package/dist/6652.js.map +1 -0
  85. package/dist/6724.js +1 -0
  86. package/dist/6724.js.map +1 -0
  87. package/dist/6867.js +19 -0
  88. package/dist/6867.js.map +1 -0
  89. package/dist/6904.js +1 -0
  90. package/dist/6904.js.map +1 -0
  91. package/dist/7045.js +1 -0
  92. package/dist/7045.js.map +1 -0
  93. package/dist/7175.js +1 -0
  94. package/dist/7175.js.map +1 -0
  95. package/dist/7182.js +1 -0
  96. package/dist/7182.js.map +1 -0
  97. package/dist/7475.js +1 -0
  98. package/dist/7475.js.map +1 -0
  99. package/dist/7742.js +1 -0
  100. package/dist/7742.js.map +1 -0
  101. package/dist/7912.js +1 -0
  102. package/dist/7912.js.map +1 -0
  103. package/dist/8251.js +1 -0
  104. package/dist/8251.js.map +1 -0
  105. package/dist/8358.js +1 -0
  106. package/dist/8358.js.map +1 -0
  107. package/dist/8359.js +1 -0
  108. package/dist/8359.js.map +1 -0
  109. package/dist/8695.js +1 -0
  110. package/dist/8695.js.map +1 -0
  111. package/dist/903.js +1 -0
  112. package/dist/903.js.map +1 -0
  113. package/dist/9072.js +1 -0
  114. package/dist/9072.js.map +1 -0
  115. package/dist/9806.js +1 -0
  116. package/dist/9806.js.map +1 -0
  117. package/dist/997.js +1 -0
  118. package/dist/997.js.map +1 -0
  119. package/dist/openmrs-esm-report-builder.js +3 -1
  120. package/dist/openmrs-esm-report-builder.js.buildmanifest.json +1099 -196
  121. package/dist/openmrs-esm-report-builder.js.map +1 -1
  122. package/dist/routes.json +1 -1
  123. package/package.json +21 -34
  124. package/rspack.config.js +2 -0
  125. package/src/__debug__/all-designs.test.tsx +7 -4
  126. package/src/components/data-visualizer/data-visualizer.scss +0 -1
  127. package/src/components/indicators/utils/composite-indicator-sql.utils.test.ts +1 -1
  128. package/src/components/indicators/utils/custom-indicator-interpreter.test.ts +320 -320
  129. package/src/components/indicators/utils/population-sql.compiler.test.ts +950 -964
  130. package/src/components/linelist/design/panels/column-category-selector.scss +7 -12
  131. package/src/components/linelist/design/panels/query-config-panel.scss +7 -8
  132. package/src/components/security/access-denied-page.component.tsx +45 -50
  133. package/src/components/security/privilege-route.component.test.tsx +59 -62
  134. package/src/components/security/privilege-route.component.tsx +3 -3
  135. package/src/constants/privileges.test.ts +1 -0
  136. package/src/features/dashboards/utils/dashboard-config.util.test.ts +37 -6
  137. package/src/globals.d.ts +0 -5
  138. package/src/hooks/use-report-builder-privileges.test.tsx +69 -75
  139. package/src/routes.json +3 -0
  140. package/src/utils/api-error.utils.test.ts +14 -18
  141. package/src/utils/etl-browser/etl-browser.util.test.ts +125 -119
  142. package/src/utils/etl-task/etl-task-card.util.test.ts +3 -2
  143. package/tools/i18next-parser.config.js +0 -0
  144. package/tools/setup-tests.ts +1 -1
  145. package/tools/update-openmrs-deps.mjs +42 -0
  146. package/translations/am.json +65 -0
  147. package/translations/ar.json +65 -0
  148. package/translations/ar_SY.json +65 -0
  149. package/translations/bn.json +65 -0
  150. package/translations/cs.json +65 -0
  151. package/translations/de.json +65 -0
  152. package/translations/en_US.json +65 -0
  153. package/translations/es.json +65 -0
  154. package/translations/es_MX.json +65 -0
  155. package/translations/fr.json +65 -0
  156. package/translations/he.json +65 -0
  157. package/translations/hi.json +65 -0
  158. package/translations/hi_IN.json +65 -0
  159. package/translations/id.json +65 -0
  160. package/translations/it.json +65 -0
  161. package/translations/ka.json +65 -0
  162. package/translations/km.json +65 -0
  163. package/translations/ku.json +65 -0
  164. package/translations/ky.json +65 -0
  165. package/translations/lg.json +65 -0
  166. package/translations/ne.json +65 -0
  167. package/translations/pl.json +65 -0
  168. package/translations/pt.json +65 -0
  169. package/translations/pt_BR.json +65 -0
  170. package/translations/qu.json +65 -0
  171. package/translations/ro_RO.json +65 -0
  172. package/translations/ru_RU.json +65 -0
  173. package/translations/si.json +65 -0
  174. package/translations/sq.json +65 -0
  175. package/translations/sw.json +65 -0
  176. package/translations/sw_KE.json +65 -0
  177. package/translations/tr.json +65 -0
  178. package/translations/tr_TR.json +65 -0
  179. package/translations/uk.json +65 -0
  180. package/translations/uz.json +65 -0
  181. package/translations/uz@Latn.json +65 -0
  182. package/translations/uz_UZ.json +65 -0
  183. package/translations/vi.json +65 -0
  184. package/translations/zh.json +65 -0
  185. package/translations/zh_CN.json +65 -0
  186. package/translations/zh_TW.json +65 -0
  187. package/tsconfig.json +9 -28
  188. package/vitest.config.mts +42 -0
  189. package/dist/162.js +0 -1
  190. package/dist/162.js.map +0 -1
  191. package/dist/184.js +0 -2
  192. package/dist/184.js.LICENSE.txt +0 -14
  193. package/dist/184.js.map +0 -1
  194. package/dist/242.js +0 -2
  195. package/dist/242.js.LICENSE.txt +0 -21
  196. package/dist/242.js.map +0 -1
  197. package/dist/253.js +0 -1
  198. package/dist/253.js.map +0 -1
  199. package/dist/300.js +0 -1
  200. package/dist/311.js +0 -1
  201. package/dist/311.js.map +0 -1
  202. package/dist/469.js +0 -2
  203. package/dist/469.js.LICENSE.txt +0 -45
  204. package/dist/469.js.map +0 -1
  205. package/dist/478.js +0 -2
  206. package/dist/478.js.LICENSE.txt +0 -9
  207. package/dist/478.js.map +0 -1
  208. package/dist/539.js +0 -1
  209. package/dist/539.js.map +0 -1
  210. package/dist/540.js +0 -2
  211. package/dist/540.js.LICENSE.txt +0 -9
  212. package/dist/540.js.map +0 -1
  213. package/dist/669.js +0 -1
  214. package/dist/669.js.map +0 -1
  215. package/dist/710.js +0 -1
  216. package/dist/710.js.map +0 -1
  217. package/dist/729.js +0 -1
  218. package/dist/729.js.map +0 -1
  219. package/dist/838.js +0 -2
  220. package/dist/838.js.LICENSE.txt +0 -9
  221. package/dist/838.js.map +0 -1
  222. package/dist/917.js +0 -1
  223. package/dist/917.js.map +0 -1
  224. package/dist/961.js +0 -2
  225. package/dist/961.js.LICENSE.txt +0 -19
  226. package/dist/961.js.map +0 -1
  227. package/dist/main.js +0 -1
  228. package/dist/main.js.map +0 -1
  229. package/jest.config.js +0 -28
  230. package/routes.json +0 -22
  231. package/tools/scss-stub.js +0 -12
  232. package/webpack.config.js +0 -30
@@ -46,10 +46,6 @@
46
46
  }
47
47
  }
48
48
 
49
- .accordion {
50
- // Override Carbon accordion defaults if needed
51
- }
52
-
53
49
  .categoryHeader {
54
50
  display: flex;
55
51
  align-items: center;
@@ -316,14 +312,13 @@
316
312
  margin: 0;
317
313
  color: #161616;
318
314
  }
319
- }
320
315
 
321
- // Debug button styling
322
- button[icondescription="View column debug info"] {
323
- svg {
324
- fill: #0f62fe;
325
- }
326
- &:hover {
327
- background-color: #e5f1ff !important;
316
+ button[icondescription="View column debug info"] {
317
+ svg {
318
+ fill: #0f62fe;
319
+ }
320
+ &:hover {
321
+ background-color: #e5f1ff !important;
322
+ }
328
323
  }
329
324
  }
@@ -11,6 +11,13 @@
11
11
  &:last-child {
12
12
  border-bottom: none;
13
13
  }
14
+
15
+ :global {
16
+ .cds--tag {
17
+ font-size: 0.75rem;
18
+ font-weight: 500;
19
+ }
20
+ }
14
21
  }
15
22
 
16
23
  .sectionHeader {
@@ -332,11 +339,3 @@
332
339
  padding-top: 1.5rem;
333
340
  border-top: 1px solid #e5e7eb;
334
341
  }
335
-
336
- /* Teal accent for interactive elements */
337
- :global {
338
- .cds--tag {
339
- font-size: 0.75rem;
340
- font-weight: 500;
341
- }
342
- }
@@ -1,69 +1,64 @@
1
1
  import React from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
- import { Button, InlineNotification, Tag } from '@carbon/react';
4
3
  import { useNavigate } from 'react-router-dom';
4
+ import { Button, InlineNotification, Tag } from '@carbon/react';
5
5
 
6
6
  import { rbDisplay, RB } from '../../constants/privileges';
7
7
  import { useReportBuilderPrivileges } from '../../hooks/use-report-builder-privileges';
8
8
  import styles from './access-denied-page.scss';
9
9
 
10
10
  interface AccessDeniedPageProps {
11
- /** Full privilege names, e.g. `Task: reportbuilder.report.edit`. */
12
- requiredPrivileges?: string[];
11
+ /** Full privilege names, e.g. `Task: reportbuilder.report.edit`. */
12
+ requiredPrivileges?: string[];
13
13
  }
14
14
 
15
15
  const AccessDeniedPage: React.FC<AccessDeniedPageProps> = ({ requiredPrivileges }) => {
16
- const { t } = useTranslation();
17
- const navigate = useNavigate();
18
- const { has } = useReportBuilderPrivileges();
19
- const privileges = requiredPrivileges ?? [];
16
+ const { t } = useTranslation();
17
+ const navigate = useNavigate();
18
+ const { has } = useReportBuilderPrivileges();
19
+ const privileges = requiredPrivileges ?? [];
20
20
 
21
- return (
22
- <div className={styles.container}>
23
- <InlineNotification
24
- lowContrast
25
- kind="error"
26
- title={t('accessDenied', 'Access denied')}
27
- subtitle={t(
28
- 'accessDeniedSubtitle',
29
- 'Your account does not have permission to open this page.',
30
- )}
31
- hideCloseButton
32
- />
21
+ return (
22
+ <div className={styles.container}>
23
+ <InlineNotification
24
+ lowContrast
25
+ kind="error"
26
+ title={t('accessDenied', 'Access denied')}
27
+ subtitle={t('accessDeniedSubtitle', 'Your account does not have permission to open this page.')}
28
+ hideCloseButton
29
+ />
33
30
 
34
- {privileges.length > 0 && (
35
- <div className={styles.section}>
36
- <p className={styles.label}>
37
- {t('requiredPrivileges', 'Required privilege(s):')}
38
- </p>
39
- <div className={styles.tags}>
40
- {privileges.map((privilege) => (
41
- <Tag key={privilege} type="red">
42
- {rbDisplay(privilege)}
43
- </Tag>
44
- ))}
45
- </div>
46
- <p className={styles.hint}>
47
- {t(
48
- 'accessDeniedHint',
49
- 'Ask your administrator to grant you the report builder role carrying these privileges.',
50
- )}
51
- </p>
52
- </div>
31
+ {privileges.length > 0 && (
32
+ <div className={styles.section}>
33
+ <p className={styles.label}>{t('requiredPrivileges', 'Required privilege(s):')}</p>
34
+ <div className={styles.tags}>
35
+ {privileges.map((privilege) => (
36
+ <Tag key={privilege} type="red">
37
+ {rbDisplay(privilege)}
38
+ </Tag>
39
+ ))}
40
+ </div>
41
+ <p className={styles.hint}>
42
+ {t(
43
+ 'accessDeniedHint',
44
+ 'Ask your administrator to grant you the report builder role carrying these privileges.',
53
45
  )}
54
-
55
- <div className={styles.actions}>
56
- {has(RB.REPORT_VIEW) && (
57
- <Button kind="primary" onClick={() => navigate('/')}>
58
- {t('goToHome', 'Go to Home')}
59
- </Button>
60
- )}
61
- <Button kind="secondary" onClick={() => navigate(-1)}>
62
- {t('goBack', 'Go back')}
63
- </Button>
64
- </div>
46
+ </p>
65
47
  </div>
66
- );
48
+ )}
49
+
50
+ <div className={styles.actions}>
51
+ {has(RB.REPORT_VIEW) && (
52
+ <Button kind="primary" onClick={() => navigate('/')}>
53
+ {t('goToHome', 'Go to Home')}
54
+ </Button>
55
+ )}
56
+ <Button kind="secondary" onClick={() => navigate(-1)}>
57
+ {t('goBack', 'Go back')}
58
+ </Button>
59
+ </div>
60
+ </div>
61
+ );
67
62
  };
68
63
 
69
64
  export default AccessDeniedPage;
@@ -1,79 +1,76 @@
1
- import React from 'react';
1
+ import React, { act } from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
- import { act } from 'react-dom/test-utils';
4
3
  import { MemoryRouter } from 'react-router-dom';
5
-
6
- jest.mock('@openmrs/esm-framework', () => ({
7
- useSession: jest.fn(),
8
- }));
9
-
10
- import { useSession } from '@openmrs/esm-framework';
4
+ import { describe, expect, it, vi } from 'vitest';
5
+ import { useSession, type Session } from '@openmrs/esm-framework';
11
6
  import { RB } from '../../constants/privileges';
12
7
  import PrivilegeRoute from './privilege-route.component';
13
8
 
9
+ const mockUseSession = vi.mocked(useSession);
10
+
14
11
  (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
15
12
 
16
13
  function sessionWith(privileges: string[], authenticated = true) {
17
- return {
18
- authenticated,
19
- user: {
20
- uuid: 'u-1',
21
- display: 'User',
22
- privileges: privileges.map((name) => ({ uuid: name, name, display: name })),
23
- roles: [],
24
- },
25
- } as any;
14
+ return {
15
+ authenticated,
16
+ user: {
17
+ uuid: 'u-1',
18
+ display: 'User',
19
+ privileges: privileges.map((name) => ({ uuid: name, name, display: name })),
20
+ roles: [],
21
+ },
22
+ } as Session;
26
23
  }
27
24
 
28
25
  function renderAt(route: string, ui: React.ReactElement): { text: () => string; unmount: () => void } {
29
- const container = document.createElement('div');
30
- document.body.appendChild(container);
31
- const root = createRoot(container);
32
- act(() => {
33
- root.render(<MemoryRouter initialEntries={[route]}>{ui}</MemoryRouter>);
34
- });
35
- return {
36
- text: () => container.textContent ?? '',
37
- unmount: () => act(() => root.unmount()),
38
- };
26
+ const container = document.createElement('div');
27
+ document.body.appendChild(container);
28
+ const root = createRoot(container);
29
+ act(() => {
30
+ root.render(<MemoryRouter initialEntries={[route]}>{ui}</MemoryRouter>);
31
+ });
32
+ return {
33
+ text: () => container.textContent ?? '',
34
+ unmount: () => act(() => root.unmount()),
35
+ };
39
36
  }
40
37
 
41
38
  describe('PrivilegeRoute', () => {
42
- it('renders children when a required privilege is held (any-of)', () => {
43
- (useSession as unknown as jest.Mock).mockReturnValue(sessionWith([RB.PACKAGE_EXPORT]));
44
- const view = renderAt(
45
- '/import-export',
46
- <PrivilegeRoute required={[RB.PACKAGE_IMPORT, RB.PACKAGE_EXPORT]}>
47
- <div>secret content</div>
48
- </PrivilegeRoute>,
49
- );
50
- expect(view.text()).toContain('secret content');
51
- view.unmount();
52
- });
39
+ it('renders children when a required privilege is held (any-of)', () => {
40
+ mockUseSession.mockReturnValue(sessionWith([RB.PACKAGE_EXPORT]));
41
+ const view = renderAt(
42
+ '/import-export',
43
+ <PrivilegeRoute required={[RB.PACKAGE_IMPORT, RB.PACKAGE_EXPORT]}>
44
+ <div>secret content</div>
45
+ </PrivilegeRoute>,
46
+ );
47
+ expect(view.text()).toContain('secret content');
48
+ view.unmount();
49
+ });
53
50
 
54
- it('renders the access denied page naming the privilege when unauthorised', () => {
55
- (useSession as unknown as jest.Mock).mockReturnValue(sessionWith([RB.REPORT_VIEW]));
56
- const view = renderAt(
57
- '/run',
58
- <PrivilegeRoute required={[RB.REPORT_RUN]}>
59
- <div>secret content</div>
60
- </PrivilegeRoute>,
61
- );
62
- expect(view.text()).not.toContain('secret content');
63
- expect(view.text()).toContain('Access denied');
64
- expect(view.text()).toContain('report.run');
65
- view.unmount();
66
- });
51
+ it('renders the access denied page naming the privilege when unauthorised', () => {
52
+ mockUseSession.mockReturnValue(sessionWith([RB.REPORT_VIEW]));
53
+ const view = renderAt(
54
+ '/run',
55
+ <PrivilegeRoute required={[RB.REPORT_RUN]}>
56
+ <div>secret content</div>
57
+ </PrivilegeRoute>,
58
+ );
59
+ expect(view.text()).not.toContain('secret content');
60
+ expect(view.text()).toContain('Access denied');
61
+ expect(view.text()).toContain('report.run');
62
+ view.unmount();
63
+ });
67
64
 
68
- it('renders nothing and does not crash for an unauthenticated visitor', () => {
69
- (useSession as unknown as jest.Mock).mockReturnValue(sessionWith([], false));
70
- const view = renderAt(
71
- '/',
72
- <PrivilegeRoute required={[RB.REPORT_VIEW]}>
73
- <div>secret content</div>
74
- </PrivilegeRoute>,
75
- );
76
- expect(view.text()).toBe('');
77
- view.unmount();
78
- });
65
+ it('renders nothing and does not crash for an unauthenticated visitor', () => {
66
+ mockUseSession.mockReturnValue(undefined);
67
+ const view = renderAt(
68
+ '/',
69
+ <PrivilegeRoute required={[RB.REPORT_VIEW]}>
70
+ <div>secret content</div>
71
+ </PrivilegeRoute>,
72
+ );
73
+ expect(view.text()).toBe('');
74
+ view.unmount();
75
+ });
79
76
  });
@@ -5,9 +5,9 @@ import { redirectToLogin } from '../../utils/api-error.utils';
5
5
  import AccessDeniedPage from './access-denied-page.component';
6
6
 
7
7
  interface PrivilegeRouteProps {
8
- /** Privileges granting access; any-of semantics, matching the backend's @Authorized. */
9
- required: string[];
10
- children: React.ReactNode;
8
+ /** Privileges granting access; any-of semantics, matching the backend's @Authorized. */
9
+ required: string[];
10
+ children: React.ReactNode;
11
11
  }
12
12
 
13
13
  /**
@@ -1,3 +1,4 @@
1
+ import { describe, expect, it } from 'vitest';
1
2
  import { RB, RB_PREFIX, ROUTE_PRIVILEGES, rbDisplay } from './privileges';
2
3
 
3
4
  describe('privilege catalog', () => {
@@ -1,3 +1,4 @@
1
+ import { describe, expect, it } from 'vitest';
1
2
  import {
2
3
  DEFAULT_SECTIONS,
3
4
  extractMonitorLayout,
@@ -22,7 +23,6 @@ function monitor(overrides: Partial<ETLMonitorDto> = {}): ETLMonitorDto {
22
23
  }),
23
24
  ...overrides,
24
25
  } as ETLMonitorDto;
25
-
26
26
  }
27
27
 
28
28
  function report(overrides: Partial<ReportDto> = {}): ReportDto {
@@ -118,7 +118,14 @@ describe('resolveDashboardLayout', () => {
118
118
  widgets: [],
119
119
  autoInclude: { etlMonitors: { enabled: true } },
120
120
  },
121
- [monitor(), monitor({ uuid: 'm2', name: 'Status', displayConfigJson: JSON.stringify({ schemaVersion: 2, component: 'STATUS_CARD', fields: [] }) })],
121
+ [
122
+ monitor(),
123
+ monitor({
124
+ uuid: 'm2',
125
+ name: 'Status',
126
+ displayConfigJson: JSON.stringify({ schemaVersion: 2, component: 'STATUS_CARD', fields: [] }),
127
+ }),
128
+ ],
122
129
  [],
123
130
  );
124
131
  const history = resolved.sections.find((s) => s.key === 'history');
@@ -132,7 +139,10 @@ describe('resolveDashboardLayout', () => {
132
139
  it('explicit widgets win over auto-include (no duplicates, by uuid and by code)', () => {
133
140
  const config = {
134
141
  schemaVersion: 1 as const,
135
- sections: [{ key: 'overview', label: 'Overview', order: 10 }, { key: 'history', label: 'History', order: 20 }],
142
+ sections: [
143
+ { key: 'overview', label: 'Overview', order: 10 },
144
+ { key: 'history', label: 'History', order: 20 },
145
+ ],
136
146
  widgets: [{ widgetType: 'ETL_MONITOR' as const, refUuid: 'm-uuid', sectionKey: 'overview' }],
137
147
  autoInclude: { etlMonitors: { enabled: true } },
138
148
  };
@@ -203,13 +213,34 @@ describe('resolveDashboardLayout', () => {
203
213
  });
204
214
 
205
215
  it('orders slots explicit-first, then by order/priority/title', () => {
206
- const autoMonitor = monitor({ uuid: 'auto', name: 'beta auto', displayConfigJson: JSON.stringify({ schemaVersion: 2, component: 'STATUS_CARD', layout: { section: 'overview', span: {}, priority: 5 }, fields: [] }) });
216
+ const autoMonitor = monitor({
217
+ uuid: 'auto',
218
+ name: 'beta auto',
219
+ displayConfigJson: JSON.stringify({
220
+ schemaVersion: 2,
221
+ component: 'STATUS_CARD',
222
+ layout: { section: 'overview', span: {}, priority: 5 },
223
+ fields: [],
224
+ }),
225
+ });
207
226
  const config = {
208
227
  schemaVersion: 1 as const,
209
228
  sections: [{ key: 'overview', label: 'Overview', order: 10 }],
210
229
  widgets: [
211
- { widgetType: 'ETL_MONITOR' as const, refUuid: 'm-uuid', sectionKey: 'overview', order: 200, titleOverride: 'second' },
212
- { widgetType: 'ETL_MONITOR' as const, refUuid: 'm2', sectionKey: 'overview', order: 100, titleOverride: 'first' },
230
+ {
231
+ widgetType: 'ETL_MONITOR' as const,
232
+ refUuid: 'm-uuid',
233
+ sectionKey: 'overview',
234
+ order: 200,
235
+ titleOverride: 'second',
236
+ },
237
+ {
238
+ widgetType: 'ETL_MONITOR' as const,
239
+ refUuid: 'm2',
240
+ sectionKey: 'overview',
241
+ order: 100,
242
+ titleOverride: 'first',
243
+ },
213
244
  ],
214
245
  autoInclude: { etlMonitors: { enabled: true } },
215
246
  };
package/src/globals.d.ts CHANGED
@@ -7,8 +7,3 @@ declare module '*.module.scss' {
7
7
  const classes: { [className: string]: string };
8
8
  export default classes;
9
9
  }
10
-
11
- // Webpack-specific typing used by OpenMRS translations loading
12
- interface Require {
13
- context(directory: string, useSubdirectories: boolean, regExp: RegExp, mode?: string): any;
14
- }
@@ -1,98 +1,92 @@
1
- import React from 'react';
1
+ import React, { act } from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
- import { act } from 'react-dom/test-utils';
4
-
5
3
  import { useSession } from '@openmrs/esm-framework';
4
+ import { describe, expect, it, vi } from 'vitest';
6
5
  import { useReportBuilderPrivileges } from './use-report-builder-privileges';
7
6
  import { RB } from '../constants/privileges';
8
7
 
9
- jest.mock('@openmrs/esm-framework', () => ({
10
- useSession: jest.fn(),
11
- }));
12
-
13
- const mockUseSession = useSession as unknown as jest.Mock;
8
+ const mockUseSession = vi.mocked(useSession);
14
9
 
15
10
  (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
16
11
 
17
12
  const priv = (name: string) => ({ uuid: name, name, display: name });
18
13
 
19
14
  function renderHook<T>(hook: () => T): { current: T | undefined; rerender: () => void; unmount: () => void } {
20
- const container = document.createElement('div');
21
- const root = createRoot(container);
22
- let current: T | undefined;
23
- const Probe = () => {
24
- current = hook();
25
- return null;
26
- };
27
- const render = () => act(() => {
28
- root.render(React.createElement(Probe));
15
+ const container = document.createElement('div');
16
+ const root = createRoot(container);
17
+ let current: T | undefined;
18
+ const Probe = () => {
19
+ current = hook();
20
+ return null;
21
+ };
22
+ const render = () =>
23
+ act(() => {
24
+ root.render(React.createElement(Probe));
29
25
  });
30
- render();
31
- return {
32
- get current() {
33
- return current;
34
- },
35
- rerender: render,
36
- unmount: () => act(() => root.unmount()),
37
- };
26
+ render();
27
+ return {
28
+ get current() {
29
+ return current;
30
+ },
31
+ rerender: render,
32
+ unmount: () => act(() => root.unmount()),
33
+ };
38
34
  }
39
35
 
40
36
  function sessionWith(privileges: string[], roles: string[] = [], authenticated = true) {
41
- return {
42
- authenticated,
43
- user: {
44
- uuid: 'u-1',
45
- display: 'User',
46
- privileges: privileges.map(priv),
47
- roles: roles.map((r) => ({ uuid: r, display: r })),
48
- },
49
- } as any;
37
+ return {
38
+ authenticated,
39
+ user: {
40
+ uuid: 'u-1',
41
+ display: 'User',
42
+ privileges: privileges.map(priv),
43
+ roles: roles.map((r) => ({ uuid: r, display: r })),
44
+ },
45
+ } as any;
50
46
  }
51
47
 
52
48
  describe('useReportBuilderPrivileges', () => {
53
- it('matches held privileges by name', () => {
54
- mockUseSession.mockReturnValue(sessionWith([RB.REPORT_VIEW]));
55
- const { current, unmount } = renderHook(() => useReportBuilderPrivileges());
56
- expect(current!.has(RB.REPORT_VIEW)).toBe(true);
57
- expect(current!.has(RB.REPORT_EDIT)).toBe(false);
58
- expect(current!.isAuthenticated).toBe(true);
59
- unmount();
60
- });
49
+ it('matches held privileges by name', () => {
50
+ mockUseSession.mockReturnValue(sessionWith([RB.REPORT_VIEW]));
51
+ const { current, unmount } = renderHook(() => useReportBuilderPrivileges());
52
+ expect(current!.has(RB.REPORT_VIEW)).toBe(true);
53
+ expect(current!.has(RB.REPORT_EDIT)).toBe(false);
54
+ expect(current!.isAuthenticated).toBe(true);
55
+ unmount();
56
+ });
61
57
 
62
- it('uses OR semantics like the backend @Authorized({a, b})', () => {
63
- mockUseSession.mockReturnValue(sessionWith([RB.REPORT_ADD]));
64
- const { current, unmount } = renderHook(() => useReportBuilderPrivileges());
65
- expect(current!.has(RB.REPORT_ADD, RB.REPORT_EDIT)).toBe(true);
66
- expect(current!.hasAll(RB.REPORT_ADD, RB.REPORT_EDIT)).toBe(false);
67
- unmount();
68
- });
58
+ it('uses OR semantics like the backend @Authorized({a, b})', () => {
59
+ mockUseSession.mockReturnValue(sessionWith([RB.REPORT_ADD]));
60
+ const { current, unmount } = renderHook(() => useReportBuilderPrivileges());
61
+ expect(current!.has(RB.REPORT_ADD, RB.REPORT_EDIT)).toBe(true);
62
+ expect(current!.hasAll(RB.REPORT_ADD, RB.REPORT_EDIT)).toBe(false);
63
+ unmount();
64
+ });
69
65
 
70
- it('treats the System Developer role as superuser', () => {
71
- mockUseSession.mockReturnValue(sessionWith([], ['System Developer']));
72
- const { current, unmount } = renderHook(() => useReportBuilderPrivileges());
73
- expect(current!.isSuperUser).toBe(true);
74
- expect(current!.has(RB.ETLMONITOR_PURGE)).toBe(true);
75
- expect(current!.hasAll(RB.PACKAGE_IMPORT, RB.PACKAGE_EXPORT)).toBe(true);
76
- unmount();
77
- });
66
+ it('treats the System Developer role as superuser', () => {
67
+ mockUseSession.mockReturnValue(sessionWith([], ['System Developer']));
68
+ const { current, unmount } = renderHook(() => useReportBuilderPrivileges());
69
+ expect(current!.isSuperUser).toBe(true);
70
+ expect(current!.has(RB.ETLMONITOR_PURGE)).toBe(true);
71
+ expect(current!.hasAll(RB.PACKAGE_IMPORT, RB.PACKAGE_EXPORT)).toBe(true);
72
+ unmount();
73
+ });
78
74
 
79
- it('exposes convenience flags', () => {
80
- mockUseSession.mockReturnValue(
81
- sessionWith([RB.REPORT_VIEW, RB.REPORT_RUN, RB.PACKAGE_EXPORT]),
82
- );
83
- const { current, unmount } = renderHook(() => useReportBuilderPrivileges());
84
- expect(current!.canAuthor).toBe(false);
85
- expect(current!.canRun).toBe(true);
86
- expect(current!.canPublish).toBe(true);
87
- expect(current!.canManageEtl).toBe(false);
88
- unmount();
89
- });
75
+ it('exposes convenience flags', () => {
76
+ mockUseSession.mockReturnValue(sessionWith([RB.REPORT_VIEW, RB.REPORT_RUN, RB.PACKAGE_EXPORT]));
77
+ const { current, unmount } = renderHook(() => useReportBuilderPrivileges());
78
+ expect(current!.canAuthor).toBe(false);
79
+ expect(current!.canRun).toBe(true);
80
+ expect(current!.canPublish).toBe(true);
81
+ expect(current!.canManageEtl).toBe(false);
82
+ unmount();
83
+ });
90
84
 
91
- it('is false on everything for an unauthenticated session', () => {
92
- mockUseSession.mockReturnValue({ authenticated: false, user: undefined });
93
- const { current, unmount } = renderHook(() => useReportBuilderPrivileges());
94
- expect(current!.isAuthenticated).toBe(false);
95
- expect(current!.has(RB.REPORT_VIEW)).toBe(false);
96
- unmount();
97
- });
85
+ it('is false on everything for an unauthenticated session', () => {
86
+ mockUseSession.mockReturnValue({ authenticated: false, user: undefined, sessionId: '' });
87
+ const { current, unmount } = renderHook(() => useReportBuilderPrivileges());
88
+ expect(current!.isAuthenticated).toBe(false);
89
+ expect(current!.has(RB.REPORT_VIEW)).toBe(false);
90
+ unmount();
91
+ });
98
92
  });
package/src/routes.json CHANGED
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "$schema": "https://json.openmrs.org/routes.schema.json",
3
+ "backendDependencies": {
4
+ "reportbuilder": "*"
5
+ },
3
6
  "pages": [
4
7
  {
5
8
  "component": "root",
@@ -1,3 +1,4 @@
1
+ import { describe, expect, it, vi } from 'vitest';
1
2
  import { RB } from '../constants/privileges';
2
3
  import { guardRejection, normalizeApiError, PrivilegeError } from './api-error.utils';
3
4
 
@@ -92,9 +93,7 @@ describe('normalizeApiError', () => {
92
93
  describe('401 (expired session)', () => {
93
94
  it('marks the error as an auth error and redirects to the SPA login page', () => {
94
95
  stubLocation();
95
- const error = normalizeApiError(
96
- axiosError(401, { error: { message: 'Session has expired' } }),
97
- ) as PrivilegeError;
96
+ const error = normalizeApiError(axiosError(401, { error: { message: 'Session has expired' } })) as PrivilegeError;
98
97
 
99
98
  expect(error).toBeInstanceOf(PrivilegeError);
100
99
  expect(error.isAuthError).toBe(true);
@@ -112,9 +111,7 @@ describe('normalizeApiError', () => {
112
111
  });
113
112
 
114
113
  it('maps other statuses to a generic error carrying the server message', () => {
115
- const error = normalizeApiError(
116
- axiosError(500, { error: { message: 'Boom' } }),
117
- ) as Error & { status?: number };
114
+ const error = normalizeApiError(axiosError(500, { error: { message: 'Boom' } })) as Error & { status?: number };
118
115
  expect(error).not.toBeInstanceOf(PrivilegeError);
119
116
  expect(error.message).toBe('Boom');
120
117
  expect(error.status).toBe(500);
@@ -134,18 +131,17 @@ describe('guardRejection', () => {
134
131
  });
135
132
 
136
133
  describe('redirectToLogin loop guard', () => {
137
- it('navigates at most once across repeated calls (module-level latch)', () => {
134
+ it('navigates at most once across repeated calls (module-level latch)', async () => {
135
+ vi.resetModules();
136
+ const fresh = await import('./api-error.utils');
137
+
138
138
  stubLocation();
139
- jest.isolateModules(() => {
140
- // eslint-disable-next-line @typescript-eslint/no-var-requires -- fresh module instance for the latch test
141
- const fresh = require('./api-error.utils');
142
- stubLocation();
143
- fresh.redirectToLogin();
144
- expect(window.location.href).toContain('/login');
145
- // The latch makes further calls no-ops even with a fresh path.
146
- stubLocation({ pathname: '/openmrs/spa/report-builder/admin', href: 'unchanged' });
147
- fresh.redirectToLogin();
148
- expect(window.location.href).toBe('unchanged');
149
- });
139
+
140
+ fresh.redirectToLogin();
141
+ expect(window.location.href).toContain('/login');
142
+ // The latch makes further calls no-ops even with a fresh path.
143
+ stubLocation({ pathname: '/openmrs/spa/report-builder/admin', href: 'unchanged' });
144
+ fresh.redirectToLogin();
145
+ expect(window.location.href).toBe('unchanged');
150
146
  });
151
147
  });