@openmrs/esm-patient-tests-app 11.3.1-pre.9435 → 11.3.1-pre.9437

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 (41) hide show
  1. package/.turbo/turbo-build.log +3 -3
  2. package/dist/1477.js +1 -1
  3. package/dist/1477.js.map +1 -1
  4. package/dist/1935.js +1 -1
  5. package/dist/1935.js.map +1 -1
  6. package/dist/3509.js +1 -1
  7. package/dist/3509.js.map +1 -1
  8. package/dist/4300.js +1 -1
  9. package/dist/6301.js +1 -1
  10. package/dist/6301.js.map +1 -1
  11. package/dist/main.js +1 -1
  12. package/dist/main.js.map +1 -1
  13. package/dist/openmrs-esm-patient-tests-app.js +1 -1
  14. package/dist/openmrs-esm-patient-tests-app.js.buildmanifest.json +15 -15
  15. package/dist/routes.json +1 -1
  16. package/package.json +2 -2
  17. package/src/index.ts +1 -1
  18. package/src/routes.json +1 -1
  19. package/src/test-orders/add-test-order/add-test-order.test.tsx +1 -1
  20. package/src/test-orders/add-test-order/add-test-order.workspace.tsx +1 -1
  21. package/src/test-orders/add-test-order/test-order-form.component.tsx +1 -1
  22. package/src/test-orders/add-test-order/test-type-search.component.tsx +1 -1
  23. package/src/test-orders/lab-order-basket-panel/lab-order-basket-panel.test.tsx +2 -2
  24. package/src/test-results/filter/filter-context.test.tsx +556 -0
  25. package/src/test-results/filter/filter-context.tsx +1 -1
  26. package/src/test-results/filter/filter-reducer.test.ts +540 -0
  27. package/src/test-results/filter/filter-reducer.ts +1 -1
  28. package/src/test-results/filter/filter-set.test.tsx +694 -0
  29. package/src/test-results/grouped-timeline/grouped-timeline.test.tsx +1 -1
  30. package/src/test-results/grouped-timeline/useObstreeData.test.ts +471 -0
  31. package/src/test-results/individual-results-table-tablet/usePanelData.tsx +40 -26
  32. package/src/test-results/loadPatientTestData/helpers.ts +29 -12
  33. package/src/test-results/loadPatientTestData/usePatientResultsData.ts +18 -7
  34. package/src/test-results/overview/external-overview.extension.tsx +1 -2
  35. package/src/test-results/print-modal/print-modal.extension.tsx +1 -1
  36. package/src/test-results/results-viewer/results-viewer.extension.tsx +7 -3
  37. package/src/test-results/tree-view/tree-view.component.tsx +6 -1
  38. package/src/test-results/tree-view/tree-view.test.tsx +117 -1
  39. package/src/test-results/trendline/trendline.component.tsx +88 -52
  40. package/src/test-results/ui-elements/reset-filters-empty-state/filter-empty-data-illustration.tsx +2 -2
  41. package/translations/en.json +1 -1
@@ -37,7 +37,6 @@ function getFilteredOverviewData(sortedObs: PatientData, filter) {
37
37
 
38
38
  function useFilteredOverviewData(patientUuid: string, filter: (filterProps: PanelFilterProps) => boolean = () => true) {
39
39
  const { sortedObs, loaded, error } = usePatientResultsData(patientUuid);
40
-
41
40
  const overviewData = useMemo(() => getFilteredOverviewData(sortedObs, filter), [filter, sortedObs]);
42
41
 
43
42
  return { overviewData, loaded, error };
@@ -46,8 +45,8 @@ function useFilteredOverviewData(patientUuid: string, filter: (filterProps: Pane
46
45
  const ExternalOverview: React.FC<ExternalOverviewProps> = ({ patientUuid, filter }) => {
47
46
  const { t } = useTranslation();
48
47
  const { overviewData, loaded } = useFilteredOverviewData(patientUuid, filter);
49
-
50
48
  const cardTitle = t('recentResults', 'Recent Results');
49
+
51
50
  const handleSeeAll = useCallback(() => {
52
51
  navigate({ to: `\${openmrsSpaBase}/patient/${patientUuid}/chart/Results` });
53
52
  }, [patientUuid]);
@@ -87,7 +87,7 @@ function PrintModal({
87
87
 
88
88
  const identifiers =
89
89
  patient?.identifier?.filter(
90
- (identifier) => !excludePatientIdentifierCodeTypes?.uuids.includes(identifier.type.coding[0].code),
90
+ (identifier) => !excludePatientIdentifierCodeTypes?.uuids.includes(identifier.type?.coding?.[0]?.code),
91
91
  ) ?? [];
92
92
 
93
93
  return {
@@ -2,7 +2,7 @@ import React, { useContext, useEffect, useRef, useState } from 'react';
2
2
  import classNames from 'classnames';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import type { TFunction } from 'i18next';
5
- import { ContentSwitcher, Switch, Button } from '@carbon/react';
5
+ import { ContentSwitcher, Switch, Button, DataTableSkeleton } from '@carbon/react';
6
6
  import { EmptyState, ErrorState } from '@openmrs/esm-patient-common-lib';
7
7
  import { RenewIcon, useConfig, useLayoutType } from '@openmrs/esm-framework';
8
8
  import { type ConfigObject } from '../../config-schema';
@@ -36,9 +36,13 @@ const RoutedResultsViewer: React.FC<ResultsViewerProps> = ({ basePath, patientUu
36
36
  return <ErrorState error={error} headerTitle={t('dataLoadError', 'Data Load Error')} />;
37
37
  }
38
38
 
39
+ if (isLoading) {
40
+ return <DataTableSkeleton role="progressbar" />;
41
+ }
42
+
39
43
  if (roots?.length) {
40
44
  return (
41
- <FilterProvider roots={!isLoading ? (roots as Roots) : []} isLoading={isLoading}>
45
+ <FilterProvider roots={roots as Roots} isLoading={isLoading}>
42
46
  <ResultsViewer patientUuid={patientUuid} basePath={basePath} />
43
47
  </FilterProvider>
44
48
  );
@@ -47,7 +51,7 @@ const RoutedResultsViewer: React.FC<ResultsViewerProps> = ({ basePath, patientUu
47
51
  return (
48
52
  <EmptyState
49
53
  headerTitle={t('testResults_title', 'Test Results')}
50
- displayText={t('testResultsData', 'Test results data')}
54
+ displayText={t('testResultsData', 'test results data')}
51
55
  />
52
56
  );
53
57
  };
@@ -99,11 +99,16 @@ const TreeView: React.FC<TreeViewProps> = ({ patientUuid, expanded, view }) => {
99
99
  return <ErrorState error={error} headerTitle={t('dataLoadError', 'Data Load Error')} />;
100
100
  }
101
101
 
102
+ // Don't show empty state while loading - wait for data to finish loading
103
+ if (isLoading) {
104
+ return <DataTableSkeleton role="progressbar" />;
105
+ }
106
+
102
107
  if (!roots || roots.length === 0) {
103
108
  return (
104
109
  <EmptyState
105
110
  headerTitle={t('testResults_title', 'Test Results')}
106
- displayText={t('testResultsData', 'Test results data')}
111
+ displayText={t('testResultsData', 'test results data')}
107
112
  />
108
113
  );
109
114
  }
@@ -1,13 +1,14 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
3
4
  import { getDefaultsFromConfigSchema, useConfig, useLayoutType } from '@openmrs/esm-framework';
4
5
  import { mockPatient } from 'tools';
5
6
  import { mockResults } from '__mocks__';
6
7
  import { type ConfigObject, configSchema } from '../../config-schema';
7
8
  import { useGetManyObstreeData } from '../grouped-timeline';
8
- import TreeView from './tree-view.component';
9
9
  import { FilterProvider, type Roots } from '../filter/filter-context';
10
10
  import { type ObsTreeNode } from '../grouped-timeline/useObstreeData';
11
+ import TreeView from './tree-view.component';
11
12
 
12
13
  const mockUseConfig = jest.mocked(useConfig<ConfigObject>);
13
14
  const mockUseLayoutType = jest.mocked(useLayoutType);
@@ -106,4 +107,119 @@ describe('TreeView', () => {
106
107
  expect(screen.getAllByText('Haemoglobin').length).toBeGreaterThan(0);
107
108
  expect(screen.getAllByText('Hematocrit').length).toBeGreaterThan(0);
108
109
  });
110
+
111
+ describe('Reset button - Tablet overlay', () => {
112
+ beforeEach(() => {
113
+ mockUseLayoutType.mockReturnValue('tablet');
114
+ mockUseGetManyObstreeData.mockReturnValue({
115
+ roots: mockResults as unknown as Array<ObsTreeNode>,
116
+ isLoading: false,
117
+ error: null,
118
+ });
119
+ });
120
+
121
+ it('should show reset tree button in tablet overlay when tree is opened', async () => {
122
+ const user = userEvent.setup();
123
+
124
+ renderTreeViewWithMockContext();
125
+
126
+ // Open the tree overlay by clicking the tree button
127
+ const treeButton = screen.getByRole('button', { name: /show tree/i });
128
+ await user.click(treeButton);
129
+
130
+ // Reset tree button should be visible in the overlay
131
+ const resetButton = screen.getByRole('button', { name: /reset tree/i });
132
+ expect(resetButton).toBeInTheDocument();
133
+ expect(resetButton).toBeEnabled();
134
+ });
135
+
136
+ it('should show both reset and view buttons in overlay', async () => {
137
+ const user = userEvent.setup();
138
+
139
+ renderTreeViewWithMockContext();
140
+
141
+ // Open the tree overlay
142
+ const treeButton = screen.getByRole('button', { name: /show tree/i });
143
+ await user.click(treeButton);
144
+
145
+ // Both buttons should be visible and enabled
146
+ const resetButton = screen.getByRole('button', { name: /reset tree/i });
147
+ const viewButton = screen.getByRole('button', { name: /view.*results/i });
148
+
149
+ expect(resetButton).toBeInTheDocument();
150
+ expect(resetButton).toBeEnabled();
151
+ expect(viewButton).toBeInTheDocument();
152
+ expect(viewButton).toBeEnabled();
153
+ });
154
+
155
+ it('should reset filters when reset tree button is clicked', async () => {
156
+ const user = userEvent.setup();
157
+
158
+ renderTreeViewWithMockContext();
159
+
160
+ // Open the tree overlay
161
+ const treeButton = screen.getByRole('button', { name: /show tree/i });
162
+ await user.click(treeButton);
163
+
164
+ // Find an enabled checkbox (one with hasData: true in mock data)
165
+ // Mock data has "Platelets" with data
166
+ const plateletsCheckboxes = screen.getAllByRole('checkbox', { name: /platelets/i });
167
+ // Find the first enabled one
168
+ const plateletsCheckbox = plateletsCheckboxes.find((cb) => !cb.hasAttribute('disabled'));
169
+
170
+ expect(plateletsCheckbox).toBeDefined();
171
+ await user.click(plateletsCheckbox);
172
+ expect(plateletsCheckbox).toBeChecked();
173
+
174
+ // Click reset tree button
175
+ const resetButton = screen.getByRole('button', { name: /reset tree/i });
176
+ await user.click(resetButton);
177
+
178
+ // Checkbox should be unchecked
179
+ expect(plateletsCheckbox).not.toBeChecked();
180
+ });
181
+
182
+ it('should show filtered results count in view button', async () => {
183
+ const user = userEvent.setup();
184
+
185
+ renderTreeViewWithMockContext();
186
+
187
+ // Open the tree overlay
188
+ const treeButton = screen.getByRole('button', { name: /show tree/i });
189
+ await user.click(treeButton);
190
+
191
+ // Initially should show total count
192
+ const viewButton = screen.getByRole('button', { name: /view.*results/i });
193
+ expect(viewButton).toBeInTheDocument();
194
+
195
+ // Check a filter - find an enabled checkbox
196
+ const plateletsCheckboxes = screen.getAllByRole('checkbox', { name: /platelets/i });
197
+ const plateletsCheckbox = plateletsCheckboxes.find((cb) => !cb.hasAttribute('disabled'));
198
+ await user.click(plateletsCheckbox);
199
+
200
+ // View button should update with filtered count
201
+ expect(screen.getByRole('button', { name: /view.*results/i })).toBeInTheDocument();
202
+ });
203
+
204
+ it('should close overlay when view results button is clicked', async () => {
205
+ const user = userEvent.setup();
206
+
207
+ renderTreeViewWithMockContext();
208
+
209
+ // Open the tree overlay
210
+ const treeButton = screen.getByRole('button', { name: /show tree/i });
211
+ await user.click(treeButton);
212
+
213
+ // Reset tree button should be visible in the overlay
214
+ const resetButton = screen.getByRole('button', { name: /reset tree/i });
215
+ expect(resetButton).toBeInTheDocument();
216
+
217
+ // Click view results button
218
+ const viewButton = screen.getByRole('button', { name: /view.*results/i });
219
+ await user.click(viewButton);
220
+
221
+ // Overlay should be closed (reset button no longer visible)
222
+ expect(screen.queryByRole('button', { name: /reset tree/i })).not.toBeInTheDocument();
223
+ });
224
+ });
109
225
  });
@@ -4,23 +4,43 @@ import { Button, InlineLoading, SkeletonText } from '@carbon/react';
4
4
  import { LineChart, ScaleTypes, TickRotations } from '@carbon/charts-react';
5
5
  import { ArrowLeftIcon, ConfigurableLink, formatDate } from '@openmrs/esm-framework';
6
6
  import { EmptyState, type OBSERVATION_INTERPRETATION } from '@openmrs/esm-patient-common-lib';
7
- import { useObstreeData } from './trendline-resource';
8
7
  import { testResultsBasePath } from '../helpers';
8
+ import { useObstreeData } from './trendline-resource';
9
9
  import CommonDataTable from '../overview/common-datatable.component';
10
10
  import RangeSelector from './range-selector.component';
11
11
  import styles from './trendline.scss';
12
12
 
13
13
  interface TrendlineProps {
14
- patientUuid: string;
15
- conceptUuid: string;
16
14
  basePath: string;
15
+ conceptUuid: string;
16
+ patientUuid: string;
17
17
  hideTrendlineHeader?: boolean;
18
18
  showBackToTimelineButton?: boolean;
19
19
  }
20
20
 
21
- const TrendLineBackground = ({ ...props }) => <div {...props} className={styles.background} />;
21
+ interface TrendLineBackgroundProps {
22
+ children?: React.ReactNode;
23
+ }
24
+
25
+ const TrendLineBackground: React.FC<TrendLineBackgroundProps> = ({ ...props }) => (
26
+ <div {...props} className={styles.background} />
27
+ );
22
28
 
23
- const TrendlineHeader = ({ patientUuid, title, referenceRange, isValidating, showBackToTimelineButton }) => {
29
+ interface TrendlineHeaderProps {
30
+ isValidating: boolean;
31
+ patientUuid: string;
32
+ referenceRange: string;
33
+ showBackToTimelineButton: boolean;
34
+ title: string;
35
+ }
36
+
37
+ const TrendlineHeader: React.FC<TrendlineHeaderProps> = ({
38
+ patientUuid,
39
+ title,
40
+ referenceRange,
41
+ isValidating,
42
+ showBackToTimelineButton,
43
+ }) => {
24
44
  const { t } = useTranslation();
25
45
  return (
26
46
  <div className={styles.header}>
@@ -28,9 +48,9 @@ const TrendlineHeader = ({ patientUuid, title, referenceRange, isValidating, sho
28
48
  {showBackToTimelineButton && (
29
49
  <ConfigurableLink to={testResultsBasePath(`/patient/${patientUuid}/chart`)}>
30
50
  <Button
51
+ iconDescription={t('returnToTimeline', 'Return to timeline')}
31
52
  kind="ghost"
32
53
  renderIcon={(props: ComponentProps<typeof ArrowLeftIcon>) => <ArrowLeftIcon size={24} {...props} />}
33
- iconDescription={t('returnToTimeline', 'Return to timeline')}
34
54
  >
35
55
  <span>{t('backToTimeline', 'Back to timeline')}</span>
36
56
  </Button>
@@ -52,8 +72,8 @@ const Trendline: React.FC<TrendlineProps> = ({
52
72
  hideTrendlineHeader = false,
53
73
  showBackToTimelineButton = false,
54
74
  }) => {
55
- const { trendlineData, isLoading, isValidating } = useObstreeData(patientUuid, conceptUuid);
56
75
  const { t } = useTranslation();
76
+ const { trendlineData, isLoading, isValidating } = useObstreeData(patientUuid, conceptUuid);
57
77
  const { obs, display: chartTitle, hiNormal, lowNormal, units: leftAxisTitle, range: referenceRange } = trendlineData;
58
78
  const bottomAxisTitle = t('date', 'Date');
59
79
  const [range, setRange] = useState<[Date, Date]>();
@@ -83,52 +103,52 @@ const Trendline: React.FC<TrendlineProps> = ({
83
103
  }
84
104
  }, [obs]);
85
105
 
86
- const data: Array<{
87
- date: Date;
88
- value: number;
89
- group: string;
90
- min?: number;
91
- max?: number;
92
- }> = [];
106
+ const { data, tableData } = useMemo(() => {
107
+ const chartData: Array<{
108
+ date: Date;
109
+ value: number;
110
+ group: string;
111
+ min?: number;
112
+ max?: number;
113
+ }> = [];
93
114
 
94
- const tableData: Array<{
95
- id: string;
96
- dateTime: string;
97
- value:
98
- | number
99
- | {
100
- value: number;
101
- interpretation: OBSERVATION_INTERPRETATION;
102
- };
103
- }> = [];
115
+ const table: Array<{
116
+ id: string;
117
+ dateTime: string;
118
+ value: {
119
+ value: number;
120
+ interpretation: OBSERVATION_INTERPRETATION;
121
+ };
122
+ }> = [];
104
123
 
105
- const dataset = chartTitle;
124
+ obs.forEach((observation, idx) => {
125
+ const normalRange =
126
+ hiNormal && lowNormal
127
+ ? {
128
+ max: hiNormal,
129
+ min: lowNormal,
130
+ }
131
+ : {};
106
132
 
107
- obs.forEach((obs, idx) => {
108
- const range =
109
- hiNormal && lowNormal
110
- ? {
111
- max: hiNormal,
112
- min: lowNormal,
113
- }
114
- : {};
133
+ chartData.push({
134
+ date: new Date(Date.parse(observation.obsDatetime)),
135
+ value: parseFloat(observation.value),
136
+ group: chartTitle,
137
+ ...normalRange,
138
+ });
115
139
 
116
- data.push({
117
- date: new Date(Date.parse(obs.obsDatetime)),
118
- value: parseFloat(obs.value),
119
- group: chartTitle,
120
- ...range,
140
+ table.push({
141
+ id: `${idx}`,
142
+ dateTime: observation.obsDatetime,
143
+ value: {
144
+ value: parseFloat(observation.value),
145
+ interpretation: observation.interpretation,
146
+ },
147
+ });
121
148
  });
122
149
 
123
- tableData.push({
124
- id: `${idx}`,
125
- dateTime: obs.obsDatetime,
126
- value: {
127
- value: parseFloat(obs.value),
128
- interpretation: obs.interpretation,
129
- },
130
- });
131
- });
150
+ return { data: chartData, tableData: table };
151
+ }, [obs, chartTitle, hiNormal, lowNormal]);
132
152
 
133
153
  const chartOptions = useMemo(
134
154
  () => ({
@@ -143,7 +163,6 @@ const Trendline: React.FC<TrendlineProps> = ({
143
163
  scaleType: ScaleTypes.TIME,
144
164
  ticks: {
145
165
  rotation: TickRotations.ALWAYS,
146
- // formatter: x => x.toLocaleDateString("en-US", TableDateFormatOption)
147
166
  },
148
167
  domain: range,
149
168
  },
@@ -239,11 +258,11 @@ const Trendline: React.FC<TrendlineProps> = ({
239
258
  <div className={styles.container}>
240
259
  {!hideTrendlineHeader && (
241
260
  <TrendlineHeader
242
- showBackToTimelineButton={showBackToTimelineButton}
243
261
  isValidating={isValidating}
244
262
  patientUuid={patientUuid}
245
- title={dataset}
246
263
  referenceRange={referenceRange}
264
+ showBackToTimelineButton={showBackToTimelineButton}
265
+ title={chartTitle}
247
266
  />
248
267
  )}
249
268
  <TrendLineBackground>
@@ -269,8 +288,25 @@ const Trendline: React.FC<TrendlineProps> = ({
269
288
  );
270
289
  };
271
290
 
272
- const DrawTable = React.memo<{ tableData; tableHeaderData }>(({ tableData, tableHeaderData }) => {
273
- return <CommonDataTable data={tableData} tableHeaders={tableHeaderData} />;
291
+ interface DrawTableProps {
292
+ tableData: Array<{
293
+ id: string;
294
+ dateTime: string;
295
+ value: {
296
+ value: number;
297
+ interpretation: OBSERVATION_INTERPRETATION;
298
+ };
299
+ }>;
300
+ tableHeaderData: Array<{
301
+ header: string;
302
+ key: string;
303
+ }>;
304
+ }
305
+
306
+ const DrawTable = React.memo<DrawTableProps>(({ tableData, tableHeaderData }) => {
307
+ return <CommonDataTable data={tableData as any} tableHeaders={tableHeaderData} />;
274
308
  });
275
309
 
310
+ DrawTable.displayName = 'DrawTable';
311
+
276
312
  export default Trendline;
@@ -14,8 +14,8 @@ const FilterEmptyDataIllustration: React.FC<FilterEmptyDataIllustrationProps> =
14
14
  <path
15
15
  d="M57 57H1V1h56v56zM1 29h56M1 15h56M1 43h56M29 1v56M15 1v56M43 1v56"
16
16
  stroke="#9ACBCA"
17
- stroke-width="1.44"
18
- stroke-linejoin="round"
17
+ strokeWidth="1.44"
18
+ strokeLinejoin="round"
19
19
  />
20
20
  </g>
21
21
  </svg>
@@ -100,7 +100,7 @@
100
100
  "testName": "Test name",
101
101
  "testResults": "test results",
102
102
  "testResults_title": "Test Results",
103
- "testResultsData": "Test results data",
103
+ "testResultsData": "test results data",
104
104
  "tests": "Tests",
105
105
  "testType": "Test type",
106
106
  "testTypeRequired": "Test type is required",