@kenyaemr/esm-admin-app 5.4.4-pre.98 → 5.4.5-pre.16

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 (285) hide show
  1. package/package.json +3 -3
  2. package/src/components/confirm-modal/confirmation-operation.test.tsx +8 -19
  3. package/src/components/dashboard/dashboard.component.tsx +4 -1
  4. package/src/components/empty-state/empty-state-log.test.tsx +3 -4
  5. package/src/components/facility-setup/constant/index.ts +3 -0
  6. package/src/components/facility-setup/facility-info.component.tsx +177 -107
  7. package/src/components/facility-setup/facility-info.scss +39 -1
  8. package/src/components/facility-setup/facility-setup.component.tsx +2 -2
  9. package/src/components/facility-setup/type/index.ts +61 -0
  10. package/src/components/facility-setup/useFacilityRegistry.ts +29 -0
  11. package/src/components/global-property/dashboard/global-property-dashboard.component.tsx +23 -0
  12. package/src/components/global-property/dashboard/global-property-dashboard.scss +6 -0
  13. package/src/components/global-property/hooks/useGlobalProperty.ts +64 -0
  14. package/src/components/global-property/hooks/useLogoUpload.ts +28 -0
  15. package/src/components/global-property/index.ts +14 -0
  16. package/src/components/global-property/modal/delete-global-property-modal.component.tsx +71 -0
  17. package/src/components/global-property/modal/delete-global-property-modal.test.tsx +131 -0
  18. package/src/components/global-property/table/global-property-table.component.tsx +241 -0
  19. package/src/components/global-property/table/global-property-table.scss +44 -0
  20. package/src/components/global-property/table/global-property-table.test.tsx +202 -0
  21. package/src/components/global-property/workspace/global-property-form-schema.ts +32 -0
  22. package/src/components/global-property/workspace/global-property.workspace.scss +40 -0
  23. package/src/components/global-property/workspace/global-property.workspace.test.tsx +172 -0
  24. package/src/components/global-property/workspace/global-property.workspace.tsx +260 -0
  25. package/src/components/global-property/workspace/upload-logo.workspace.scss +61 -0
  26. package/src/components/global-property/workspace/upload-logo.workspace.test.tsx +180 -0
  27. package/src/components/global-property/workspace/upload-logo.workspace.tsx +191 -0
  28. package/src/components/hook/healthWorkerRegistry.ts +78 -0
  29. package/src/components/hook/useProfessionalRegistryEnums.ts +59 -0
  30. package/src/components/logs-table/operation-log-table.component.tsx +87 -75
  31. package/src/components/logs-table/operation-log.test.tsx +134 -28
  32. package/src/components/modal/hwr-confirmation.modal.scss +80 -4
  33. package/src/components/modal/hwr-confirmation.modal.tsx +118 -128
  34. package/src/components/modal/hwr-sync.modal.tsx +194 -106
  35. package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-workspace/user-role-scope.workspace.tsx +1 -1
  36. package/src/components/users/manage-users/user-details/user-detail.scss +167 -39
  37. package/src/components/users/manage-users/user-details/user-details.component.tsx +130 -122
  38. package/src/components/users/manage-users/user-management.workspace.scss +233 -95
  39. package/src/components/users/manage-users/user-management.workspace.tsx +481 -375
  40. package/src/components/users/userManagementFormSchema.tsx +17 -8
  41. package/src/config-schema.ts +48 -68
  42. package/src/index.ts +21 -7
  43. package/src/left-pannel-link.component.tsx +5 -3
  44. package/src/root.component.tsx +2 -0
  45. package/src/routes.json +25 -1
  46. package/src/types/index.ts +11 -2
  47. package/translations/am.json +192 -10
  48. package/translations/en.json +205 -23
  49. package/translations/fr.json +260 -58
  50. package/translations/sw.json +329 -147
  51. package/.turbo/turbo-build.log +0 -6
  52. package/dist/1074.js +0 -1
  53. package/dist/1074.js.map +0 -1
  54. package/dist/12.js +0 -17
  55. package/dist/12.js.map +0 -1
  56. package/dist/1201.js +0 -1
  57. package/dist/1201.js.map +0 -1
  58. package/dist/1242.js +0 -1
  59. package/dist/1242.js.map +0 -1
  60. package/dist/1311.js +0 -1
  61. package/dist/1311.js.map +0 -1
  62. package/dist/1462.js +0 -1
  63. package/dist/1462.js.map +0 -1
  64. package/dist/1469.js +0 -1
  65. package/dist/1469.js.map +0 -1
  66. package/dist/1506.js +0 -13
  67. package/dist/1506.js.map +0 -1
  68. package/dist/1718.js +0 -1
  69. package/dist/1718.js.map +0 -1
  70. package/dist/1722.js +0 -1
  71. package/dist/1722.js.map +0 -1
  72. package/dist/1772.js +0 -1
  73. package/dist/1772.js.map +0 -1
  74. package/dist/1889.js +0 -1
  75. package/dist/1889.js.map +0 -1
  76. package/dist/1972.js +0 -1
  77. package/dist/1972.js.map +0 -1
  78. package/dist/1990.js +0 -1
  79. package/dist/1990.js.map +0 -1
  80. package/dist/2016.js +0 -1
  81. package/dist/2016.js.map +0 -1
  82. package/dist/2080.js +0 -1
  83. package/dist/2080.js.map +0 -1
  84. package/dist/2096.js +0 -1
  85. package/dist/2096.js.map +0 -1
  86. package/dist/2153.js +0 -1
  87. package/dist/2153.js.map +0 -1
  88. package/dist/216.js +0 -1
  89. package/dist/216.js.map +0 -1
  90. package/dist/2270.js +0 -1
  91. package/dist/2270.js.map +0 -1
  92. package/dist/2294.js +0 -1
  93. package/dist/2294.js.map +0 -1
  94. package/dist/2345.js +0 -1
  95. package/dist/2345.js.map +0 -1
  96. package/dist/2402.js +0 -1
  97. package/dist/2402.js.map +0 -1
  98. package/dist/2500.js +0 -1
  99. package/dist/2500.js.map +0 -1
  100. package/dist/251.js +0 -1
  101. package/dist/251.js.map +0 -1
  102. package/dist/257.js +0 -1
  103. package/dist/257.js.map +0 -1
  104. package/dist/2586.js +0 -1
  105. package/dist/2586.js.map +0 -1
  106. package/dist/2625.js +0 -1
  107. package/dist/2625.js.map +0 -1
  108. package/dist/2652.js +0 -1
  109. package/dist/2652.js.map +0 -1
  110. package/dist/2685.js +0 -1
  111. package/dist/2685.js.map +0 -1
  112. package/dist/2948.js +0 -1
  113. package/dist/2948.js.map +0 -1
  114. package/dist/3089.js +0 -1
  115. package/dist/3089.js.map +0 -1
  116. package/dist/3190.js +0 -1
  117. package/dist/3190.js.map +0 -1
  118. package/dist/3224.js +0 -1
  119. package/dist/3224.js.map +0 -1
  120. package/dist/3366.js +0 -1
  121. package/dist/3366.js.map +0 -1
  122. package/dist/349.js +0 -1
  123. package/dist/349.js.map +0 -1
  124. package/dist/3548.js +0 -1
  125. package/dist/3548.js.map +0 -1
  126. package/dist/3571.js +0 -1
  127. package/dist/3571.js.map +0 -1
  128. package/dist/3691.js +0 -1
  129. package/dist/3691.js.map +0 -1
  130. package/dist/3775.js +0 -1
  131. package/dist/3775.js.map +0 -1
  132. package/dist/3816.js +0 -1
  133. package/dist/3816.js.map +0 -1
  134. package/dist/3852.js +0 -1
  135. package/dist/3852.js.map +0 -1
  136. package/dist/3906.js +0 -1
  137. package/dist/3906.js.map +0 -1
  138. package/dist/3963.js +0 -1
  139. package/dist/3963.js.map +0 -1
  140. package/dist/405.js +0 -1
  141. package/dist/405.js.map +0 -1
  142. package/dist/4296.js +0 -1
  143. package/dist/4296.js.map +0 -1
  144. package/dist/4337.js +0 -1
  145. package/dist/4337.js.map +0 -1
  146. package/dist/4735.js +0 -1
  147. package/dist/4735.js.map +0 -1
  148. package/dist/4744.js +0 -1
  149. package/dist/4744.js.map +0 -1
  150. package/dist/4813.js +0 -2
  151. package/dist/4813.js.map +0 -1
  152. package/dist/4858.js +0 -1
  153. package/dist/4858.js.map +0 -1
  154. package/dist/487.js +0 -1
  155. package/dist/487.js.map +0 -1
  156. package/dist/4970.js +0 -1
  157. package/dist/4970.js.map +0 -1
  158. package/dist/5202.js +0 -1
  159. package/dist/5202.js.map +0 -1
  160. package/dist/5294.js +0 -1
  161. package/dist/5294.js.map +0 -1
  162. package/dist/545.js +0 -1
  163. package/dist/545.js.map +0 -1
  164. package/dist/552.js +0 -1
  165. package/dist/552.js.map +0 -1
  166. package/dist/5592.js +0 -1
  167. package/dist/5592.js.map +0 -1
  168. package/dist/5669.js +0 -1
  169. package/dist/5669.js.map +0 -1
  170. package/dist/5884.js +0 -1
  171. package/dist/5884.js.map +0 -1
  172. package/dist/5940.js +0 -1
  173. package/dist/5940.js.map +0 -1
  174. package/dist/6092.js +0 -1
  175. package/dist/6092.js.map +0 -1
  176. package/dist/6155.js +0 -1
  177. package/dist/6155.js.map +0 -1
  178. package/dist/6178.js +0 -1
  179. package/dist/6178.js.map +0 -1
  180. package/dist/6399.js +0 -1
  181. package/dist/6399.js.map +0 -1
  182. package/dist/6456.js +0 -1
  183. package/dist/6466.js +0 -3
  184. package/dist/6466.js.map +0 -1
  185. package/dist/6492.js +0 -1
  186. package/dist/6492.js.map +0 -1
  187. package/dist/6676.js +0 -1
  188. package/dist/6676.js.map +0 -1
  189. package/dist/6800.js +0 -1
  190. package/dist/6800.js.map +0 -1
  191. package/dist/6976.js +0 -1
  192. package/dist/6976.js.map +0 -1
  193. package/dist/7005.js +0 -1
  194. package/dist/7005.js.map +0 -1
  195. package/dist/7201.js +0 -1
  196. package/dist/7201.js.map +0 -1
  197. package/dist/7210.js +0 -1
  198. package/dist/7210.js.map +0 -1
  199. package/dist/7234.js +0 -1
  200. package/dist/7234.js.map +0 -1
  201. package/dist/7261.js +0 -1
  202. package/dist/7261.js.map +0 -1
  203. package/dist/7326.js +0 -1
  204. package/dist/7463.js +0 -1
  205. package/dist/7463.js.map +0 -1
  206. package/dist/7528.js +0 -1
  207. package/dist/7528.js.map +0 -1
  208. package/dist/7584.js +0 -1
  209. package/dist/7584.js.map +0 -1
  210. package/dist/7607.js +0 -1
  211. package/dist/7717.js +0 -1
  212. package/dist/7717.js.map +0 -1
  213. package/dist/7737.js +0 -1
  214. package/dist/7737.js.map +0 -1
  215. package/dist/7739.js +0 -1
  216. package/dist/7739.js.map +0 -1
  217. package/dist/7765.js +0 -1
  218. package/dist/7765.js.map +0 -1
  219. package/dist/7820.js +0 -1
  220. package/dist/7820.js.map +0 -1
  221. package/dist/7844.js +0 -1
  222. package/dist/7844.js.map +0 -1
  223. package/dist/7866.js +0 -1
  224. package/dist/7866.js.map +0 -1
  225. package/dist/7916.js +0 -1
  226. package/dist/7916.js.map +0 -1
  227. package/dist/7971.js +0 -1
  228. package/dist/7971.js.map +0 -1
  229. package/dist/8159.js +0 -7
  230. package/dist/8159.js.map +0 -1
  231. package/dist/8244.js +0 -1
  232. package/dist/8244.js.map +0 -1
  233. package/dist/8262.js +0 -1
  234. package/dist/8262.js.map +0 -1
  235. package/dist/834.js +0 -1
  236. package/dist/834.js.map +0 -1
  237. package/dist/8376.js +0 -1
  238. package/dist/8376.js.map +0 -1
  239. package/dist/845.js +0 -1
  240. package/dist/845.js.map +0 -1
  241. package/dist/8546.js +0 -1
  242. package/dist/8546.js.map +0 -1
  243. package/dist/8570.js +0 -1
  244. package/dist/8570.js.map +0 -1
  245. package/dist/87.js +0 -1
  246. package/dist/87.js.map +0 -1
  247. package/dist/8727.js +0 -1
  248. package/dist/8828.js +0 -1
  249. package/dist/8828.js.map +0 -1
  250. package/dist/8860.js +0 -1
  251. package/dist/8860.js.map +0 -1
  252. package/dist/9036.js +0 -1
  253. package/dist/9036.js.map +0 -1
  254. package/dist/9124.js +0 -1
  255. package/dist/9124.js.map +0 -1
  256. package/dist/9182.js +0 -1
  257. package/dist/921.js +0 -1
  258. package/dist/921.js.map +0 -1
  259. package/dist/9404.js +0 -1
  260. package/dist/9404.js.map +0 -1
  261. package/dist/9406.js +0 -1
  262. package/dist/9406.js.map +0 -1
  263. package/dist/9446.js +0 -1
  264. package/dist/9446.js.map +0 -1
  265. package/dist/9449.js +0 -1
  266. package/dist/9449.js.map +0 -1
  267. package/dist/9566.js +0 -5
  268. package/dist/9566.js.map +0 -1
  269. package/dist/9641.js +0 -1
  270. package/dist/9641.js.map +0 -1
  271. package/dist/9711.js +0 -1
  272. package/dist/9711.js.map +0 -1
  273. package/dist/9801.js +0 -1
  274. package/dist/9801.js.map +0 -1
  275. package/dist/9835.js +0 -11
  276. package/dist/9835.js.map +0 -1
  277. package/dist/kenyaemr-esm-admin-app.js +0 -5
  278. package/dist/kenyaemr-esm-admin-app.js.buildmanifest.json +0 -3173
  279. package/dist/kenyaemr-esm-admin-app.js.map +0 -1
  280. package/dist/main.js +0 -5
  281. package/dist/main.js.map +0 -1
  282. package/dist/routes.json +0 -1
  283. package/src/components/facility-setup/facility-setup.resource.tsx +0 -7
  284. package/src/components/hook/healthWorkerAdapter.ts +0 -213
  285. package/src/components/hook/useFacilityInfo.tsx +0 -37
@@ -0,0 +1,202 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
5
+ import GlobalPropertyTable from './global-property-table.component';
6
+
7
+ const mockGoTo = vi.fn();
8
+ const mockMutate = vi.fn();
9
+ const mockLaunchWorkspace2 = vi.fn();
10
+ const mockShowModal = vi.fn(() => vi.fn());
11
+
12
+ vi.mock('react-i18next', () => ({
13
+ useTranslation: () => ({ t: (_key: string, fallback: string, opts?: Record<string, unknown>) => fallback }),
14
+ }));
15
+
16
+ vi.mock('@openmrs/esm-framework', () => ({
17
+ useLayoutType: vi.fn(() => 'desktop'),
18
+ isDesktop: vi.fn(() => true),
19
+ launchWorkspace2: (...args: unknown[]) => mockLaunchWorkspace2(...args),
20
+ showModal: (...args: unknown[]) => mockShowModal(...args),
21
+ useDebounce: vi.fn((val: string) => val),
22
+ usePaginationInfo: vi.fn(() => ({ pageSizes: [10, 20, 50] })),
23
+ ErrorCard: ({ headerTitle }: { headerTitle: string }) => <div data-testid="error-card">{headerTitle}</div>,
24
+ }));
25
+
26
+ vi.mock('@carbon/react', async (importOriginal) => {
27
+ const original = await importOriginal<typeof import('@carbon/react')>();
28
+ return {
29
+ ...original,
30
+ DataTable: ({
31
+ children,
32
+ rows,
33
+ headers,
34
+ }: {
35
+ children: (props: any) => React.ReactNode;
36
+ rows: any[];
37
+ headers: any[];
38
+ }) => {
39
+ if (typeof children === 'function') {
40
+ const renderRows = rows.map((row) => ({
41
+ ...row,
42
+ cells: headers.map((h) => ({
43
+ id: `${row.id}:${h.key}`,
44
+ value: row[h.key],
45
+ info: { header: h.key },
46
+ })),
47
+ }));
48
+ return children({
49
+ rows: renderRows,
50
+ headers,
51
+ getTableProps: () => ({}),
52
+ getHeaderProps: ({ header }: any) => ({ key: header.key }),
53
+ getRowProps: ({ row }: any) => ({ key: row.id }),
54
+ getCellProps: ({ cell }: any) => ({ key: cell.id }),
55
+ getToolbarProps: () => ({}),
56
+ getTableContainerProps: () => ({}),
57
+ getBatchActionProps: () => ({ shouldShowBatchActions: false }),
58
+ });
59
+ }
60
+ return children;
61
+ },
62
+ Button: ({ children, onClick, disabled, iconDescription, hasIconOnly, type }: any) => (
63
+ <button
64
+ onClick={onClick}
65
+ disabled={disabled}
66
+ aria-label={hasIconOnly ? iconDescription : undefined}
67
+ type={type || 'button'}>
68
+ {hasIconOnly ? null : children}
69
+ </button>
70
+ ),
71
+ OverflowMenu: ({ children }: any) => <div>{children}</div>,
72
+ OverflowMenuItem: ({ itemText, onClick }: any) => <button onClick={onClick}>{itemText}</button>,
73
+ };
74
+ });
75
+
76
+ vi.mock('../hooks/useGlobalProperty', () => ({
77
+ useGlobalProperties: vi.fn(),
78
+ }));
79
+
80
+ import { useGlobalProperties } from '../hooks/useGlobalProperty';
81
+
82
+ const mockProperties = [
83
+ { uuid: 'uuid-1', property: 'setting.one', value: 'value1', description: 'First setting' },
84
+ { uuid: 'uuid-2', property: 'setting.two', value: 'value2', description: 'Second setting' },
85
+ ];
86
+
87
+ function setupMock(overrides = {}) {
88
+ (useGlobalProperties as ReturnType<typeof vi.fn>).mockReturnValue({
89
+ isLoading: false,
90
+ data: mockProperties,
91
+ error: null,
92
+ goTo: mockGoTo,
93
+ currentPage: 1,
94
+ totalCount: 2,
95
+ mutate: mockMutate,
96
+ ...overrides,
97
+ });
98
+ }
99
+
100
+ describe('GlobalPropertyTable', () => {
101
+ beforeEach(() => {
102
+ vi.clearAllMocks();
103
+ });
104
+
105
+ it('renders the table with property and value columns', () => {
106
+ setupMock();
107
+ render(<GlobalPropertyTable />);
108
+ expect(screen.getByText('Property')).toBeInTheDocument();
109
+ expect(screen.getByText('Value')).toBeInTheDocument();
110
+ });
111
+
112
+ it('renders all global property rows', () => {
113
+ setupMock();
114
+ render(<GlobalPropertyTable />);
115
+ expect(screen.getByText('setting.one')).toBeInTheDocument();
116
+ expect(screen.getByText('value1')).toBeInTheDocument();
117
+ expect(screen.getByText('setting.two')).toBeInTheDocument();
118
+ expect(screen.getByText('value2')).toBeInTheDocument();
119
+ });
120
+
121
+ it('renders the table without data rows when no properties exist', () => {
122
+ setupMock({ data: [], totalCount: 0 });
123
+ render(<GlobalPropertyTable />);
124
+ expect(screen.getByRole('table')).toBeInTheDocument();
125
+ expect(screen.queryByText('setting.one')).not.toBeInTheDocument();
126
+ });
127
+
128
+ it('opens the add workspace when "Add global property" button is clicked', () => {
129
+ setupMock();
130
+ render(<GlobalPropertyTable />);
131
+ fireEvent.click(screen.getByText('Add global property'));
132
+ expect(mockLaunchWorkspace2).toHaveBeenCalledWith(
133
+ 'global-property-workspace',
134
+ expect.objectContaining({
135
+ systemSetting: undefined,
136
+ mutateGlobalProperty: mockMutate,
137
+ }),
138
+ );
139
+ });
140
+
141
+ it('opens the upload image workspace when "Upload image" button is clicked', () => {
142
+ setupMock();
143
+ render(<GlobalPropertyTable />);
144
+ fireEvent.click(screen.getByText('Upload image'));
145
+ expect(mockLaunchWorkspace2).toHaveBeenCalledWith(
146
+ 'upload-logo-workspace',
147
+ expect.objectContaining({
148
+ mutateGlobalProperty: mockMutate,
149
+ }),
150
+ );
151
+ });
152
+
153
+ it('opens the edit workspace with the correct property when edit is clicked', () => {
154
+ setupMock();
155
+ render(<GlobalPropertyTable />);
156
+ const editButtons = screen.getAllByRole('button', { name: 'Edit' });
157
+ fireEvent.click(editButtons[0]);
158
+ expect(mockLaunchWorkspace2).toHaveBeenCalledWith(
159
+ 'global-property-workspace',
160
+ expect.objectContaining({
161
+ systemSetting: mockProperties[0],
162
+ }),
163
+ );
164
+ });
165
+
166
+ it('opens the delete modal with the correct property when delete is clicked', () => {
167
+ setupMock();
168
+ render(<GlobalPropertyTable />);
169
+ const deleteButtons = screen.getAllByRole('button', { name: 'Delete' });
170
+ fireEvent.click(deleteButtons[0]);
171
+ expect(mockShowModal).toHaveBeenCalledWith(
172
+ 'delete-global-property-modal',
173
+ expect.objectContaining({
174
+ property: 'setting.one',
175
+ uuid: 'uuid-1',
176
+ }),
177
+ );
178
+ });
179
+
180
+ it('filters results and resets to page 1 when user types in the search box', async () => {
181
+ setupMock({ currentPage: 2 });
182
+ render(<GlobalPropertyTable />);
183
+ const searchInput = screen.getByPlaceholderText('Search for global properties');
184
+ fireEvent.change(searchInput, { target: { value: 'setting' } });
185
+ await waitFor(() => {
186
+ expect(mockGoTo).toHaveBeenCalledWith(1);
187
+ });
188
+ });
189
+
190
+ it('renders a search input for filtering properties', () => {
191
+ setupMock();
192
+ render(<GlobalPropertyTable />);
193
+ expect(screen.getByPlaceholderText('Search for global properties')).toBeInTheDocument();
194
+ });
195
+
196
+ it('renders an error card when the hook returns an error', () => {
197
+ setupMock({ error: new Error('Network error'), data: [] });
198
+ render(<GlobalPropertyTable />);
199
+ expect(screen.getByTestId('error-card')).toBeInTheDocument();
200
+ expect(screen.getByText('Global property')).toBeInTheDocument();
201
+ });
202
+ });
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ import type { TFunction } from 'i18next';
3
+
4
+ export const createGlobalPropertyFormSchema = (t: TFunction) =>
5
+ z.object({
6
+ property: z.string().min(1, { message: t('gpPropertyNameRequired', 'Property name is required') }),
7
+ description: z.string().optional(),
8
+ datatypeClassname: z.string().optional(),
9
+ datatypeConfig: z.string().optional(),
10
+ preferredHandlerClassname: z.string().optional(),
11
+ handlerConfig: z.string().nullable().optional(),
12
+ value: z.string().min(1, { message: t('gpValueRequired', 'Value is required') }),
13
+ });
14
+
15
+ export type GlobalPropertyFormType = z.infer<ReturnType<typeof createGlobalPropertyFormSchema>>;
16
+
17
+ export const openmrsCustomDatatypes = [
18
+ 'org.openmrs.customdatatype.datatype.BooleanDatatype',
19
+ 'org.openmrs.customdatatype.datatype.DateDatatype',
20
+ 'org.openmrs.customdatatype.datatype.DateTimeDatatype',
21
+ 'org.openmrs.customdatatype.datatype.FloatDatatype',
22
+ 'org.openmrs.customdatatype.datatype.FreeTextDatatype',
23
+ 'org.openmrs.customdatatype.datatype.LongFreeTextDatatype',
24
+ 'org.openmrs.customdatatype.datatype.RegexValidatedTextDatatype',
25
+ 'org.openmrs.customdatatype.datatype.SpecifiedTextOptionsDatatype',
26
+ 'org.openmrs.customdatatype.datatype.ConceptDatatype',
27
+ 'org.openmrs.customdatatype.datatype.LocationDatatype',
28
+ 'org.openmrs.customdatatype.datatype.ProgramDatatype',
29
+ 'org.openmrs.customdatatype.datatype.ProviderDatatype',
30
+ ];
31
+
32
+ export type OpenmrsCustomDatatype = (typeof openmrsCustomDatatypes)[number];
@@ -0,0 +1,40 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+
4
+ .form {
5
+ display: flex;
6
+ flex-direction: column;
7
+ justify-content: space-between;
8
+ height: 100%;
9
+ }
10
+
11
+ .formContainer {
12
+ margin: layout.$spacing-05;
13
+ display: flex;
14
+ flex-direction: column;
15
+ gap: layout.$spacing-05;
16
+ overflow-y: auto;
17
+ }
18
+
19
+ .tablet {
20
+ padding: layout.$spacing-06 layout.$spacing-05;
21
+ background-color: colors.$white;
22
+ }
23
+
24
+ .desktop {
25
+ padding: 0;
26
+
27
+ & button {
28
+ max-width: 50% !important;
29
+ }
30
+ }
31
+
32
+ .buttonContainer {
33
+ max-width: 50%;
34
+ }
35
+
36
+ .inlineLoading {
37
+ display: flex;
38
+ align-items: center;
39
+ gap: layout.$spacing-03;
40
+ }
@@ -0,0 +1,172 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import '@testing-library/jest-dom';
5
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
6
+ import GlobalPropertyWorkspace from './global-property.workspace';
7
+
8
+ const mockShowSnackbar = vi.fn();
9
+ const mockSaveOrUpdateGlobalProperty = vi.fn();
10
+ const mockCloseWorkspace = vi.fn();
11
+ const mockMutateGlobalProperty = vi.fn();
12
+
13
+ vi.mock('react-i18next', () => ({
14
+ useTranslation: () => ({ t: (_key: string, fallback: string) => fallback }),
15
+ }));
16
+
17
+ vi.mock('@openmrs/esm-framework', () => ({
18
+ useLayoutType: vi.fn(() => 'desktop'),
19
+ showSnackbar: (...args: unknown[]) => mockShowSnackbar(...args),
20
+ ResponsiveWrapper: ({ children }: { children: React.ReactNode }) => <>{children}</>,
21
+ Workspace2: ({ children, title }: { children: React.ReactNode; title: string }) => (
22
+ <div>
23
+ <h2>{title}</h2>
24
+ {children}
25
+ </div>
26
+ ),
27
+ }));
28
+
29
+ vi.mock('../hooks/useGlobalProperty', () => ({
30
+ saveOrUpdateGlobalProperty: (...args: unknown[]) => mockSaveOrUpdateGlobalProperty(...args),
31
+ }));
32
+
33
+ vi.mock('@hookform/resolvers/zod', () => ({
34
+ zodResolver: () => async (values: unknown) => ({ values, errors: {} }),
35
+ }));
36
+
37
+ const baseProps = {
38
+ closeWorkspace: mockCloseWorkspace,
39
+ workspaceProps: {
40
+ systemSetting: null,
41
+ mutateGlobalProperty: mockMutateGlobalProperty,
42
+ },
43
+ };
44
+
45
+ describe('GlobalPropertyWorkspace', () => {
46
+ beforeEach(() => {
47
+ vi.clearAllMocks();
48
+ });
49
+
50
+ it('shows "Add global property" title in add mode', () => {
51
+ render(<GlobalPropertyWorkspace {...baseProps} />);
52
+ expect(screen.getByText('Add global property')).toBeInTheDocument();
53
+ });
54
+
55
+ it('shows "Edit global property" title when editing an existing property', () => {
56
+ const props = {
57
+ ...baseProps,
58
+ workspaceProps: {
59
+ systemSetting: { uuid: 'uuid-1', property: 'setting.one', value: 'value1' },
60
+ mutateGlobalProperty: mockMutateGlobalProperty,
61
+ },
62
+ };
63
+ render(<GlobalPropertyWorkspace {...props} />);
64
+ expect(screen.getByText('Edit global property')).toBeInTheDocument();
65
+ });
66
+
67
+ it('pre-fills form fields when editing an existing property', () => {
68
+ const props = {
69
+ ...baseProps,
70
+ workspaceProps: {
71
+ systemSetting: { uuid: 'uuid-1', property: 'my.setting', value: 'my-value' },
72
+ mutateGlobalProperty: mockMutateGlobalProperty,
73
+ },
74
+ };
75
+ render(<GlobalPropertyWorkspace {...props} />);
76
+ expect(screen.getByDisplayValue('my.setting')).toBeInTheDocument();
77
+ expect(screen.getByDisplayValue('my-value')).toBeInTheDocument();
78
+ });
79
+
80
+ it('disables the property field when editing an existing property', () => {
81
+ const props = {
82
+ ...baseProps,
83
+ workspaceProps: {
84
+ systemSetting: { uuid: 'uuid-1', property: 'setting.one', value: 'value1' },
85
+ mutateGlobalProperty: mockMutateGlobalProperty,
86
+ },
87
+ };
88
+ render(<GlobalPropertyWorkspace {...props} />);
89
+ expect(screen.getByLabelText(/Property/i)).toBeDisabled();
90
+ });
91
+
92
+ it('enables the property field in add mode', () => {
93
+ render(<GlobalPropertyWorkspace {...baseProps} />);
94
+ expect(screen.getByLabelText(/Property/i)).not.toBeDisabled();
95
+ });
96
+
97
+ it('renders the Cancel and Save & close buttons', () => {
98
+ render(<GlobalPropertyWorkspace {...baseProps} />);
99
+ expect(screen.getByText('Cancel')).toBeInTheDocument();
100
+ expect(screen.getByText('Save & close')).toBeInTheDocument();
101
+ });
102
+
103
+ it('calls closeWorkspace when Cancel is clicked', () => {
104
+ render(<GlobalPropertyWorkspace {...baseProps} />);
105
+ fireEvent.click(screen.getByText('Cancel'));
106
+ expect(mockCloseWorkspace).toHaveBeenCalledTimes(1);
107
+ });
108
+
109
+ it('shows validation error when form is submitted with empty required fields', async () => {
110
+ render(<GlobalPropertyWorkspace {...baseProps} />);
111
+ const saveButton = screen.getByText('Save & close');
112
+ fireEvent.click(saveButton);
113
+ await waitFor(() => {
114
+ expect(mockSaveOrUpdateGlobalProperty).not.toHaveBeenCalled();
115
+ });
116
+ });
117
+
118
+ it('saves a new property with the entered values', async () => {
119
+ mockSaveOrUpdateGlobalProperty.mockResolvedValue(undefined);
120
+ render(<GlobalPropertyWorkspace {...baseProps} />);
121
+
122
+ await userEvent.type(screen.getByLabelText(/Property/i), 'new.setting');
123
+ await userEvent.type(screen.getByLabelText(/Value/i), 'new-value');
124
+
125
+ fireEvent.submit(screen.getByLabelText(/Property/i).closest('form')!);
126
+
127
+ await waitFor(() => {
128
+ expect(mockSaveOrUpdateGlobalProperty).toHaveBeenCalledWith(
129
+ expect.objectContaining({ property: 'new.setting', value: 'new-value' }),
130
+ undefined,
131
+ );
132
+ });
133
+ });
134
+
135
+ it('shows a success snackbar and closes workspace after saving', async () => {
136
+ mockSaveOrUpdateGlobalProperty.mockResolvedValue(undefined);
137
+ render(<GlobalPropertyWorkspace {...baseProps} />);
138
+
139
+ await userEvent.type(screen.getByLabelText(/Property/i), 'new.setting');
140
+ await userEvent.type(screen.getByLabelText(/Value/i), 'new-value');
141
+
142
+ fireEvent.submit(screen.getByLabelText(/Property/i).closest('form')!);
143
+
144
+ await waitFor(() => {
145
+ expect(mockShowSnackbar).toHaveBeenCalledWith(expect.objectContaining({ kind: 'success' }));
146
+ expect(mockMutateGlobalProperty).toHaveBeenCalled();
147
+ expect(mockCloseWorkspace).toHaveBeenCalled();
148
+ });
149
+ });
150
+
151
+ it('shows an error snackbar when saving fails', async () => {
152
+ mockSaveOrUpdateGlobalProperty.mockRejectedValue(new Error('Failed to save'));
153
+ render(<GlobalPropertyWorkspace {...baseProps} />);
154
+
155
+ await userEvent.type(screen.getByLabelText(/Property/i), 'new.setting');
156
+ await userEvent.type(screen.getByLabelText(/Value/i), 'new-value');
157
+
158
+ fireEvent.submit(screen.getByLabelText(/Property/i).closest('form')!);
159
+
160
+ await waitFor(() => {
161
+ expect(mockShowSnackbar).toHaveBeenCalledWith(expect.objectContaining({ kind: 'error' }));
162
+ expect(mockCloseWorkspace).not.toHaveBeenCalled();
163
+ });
164
+ });
165
+
166
+ it('renders optional fields: description, datatype config, handler config', () => {
167
+ render(<GlobalPropertyWorkspace {...baseProps} />);
168
+ expect(screen.getByLabelText(/Description/i)).toBeInTheDocument();
169
+ expect(screen.getByLabelText(/Datatype config/i)).toBeInTheDocument();
170
+ expect(screen.getByLabelText(/Handler config/i)).toBeInTheDocument();
171
+ });
172
+ });