@openmrs/esm-reports-app 4.2.2-pre.356 → 4.2.2-pre.394
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.
- package/.turbo/turbo-build.log +17 -17
- package/dist/1104.js +1 -0
- package/dist/1104.js.map +1 -0
- package/dist/1119.js +1 -1
- package/dist/1197.js +1 -1
- package/dist/1650.js +1 -1
- package/dist/1650.js.map +1 -1
- package/dist/2146.js +1 -1
- package/dist/2252.js +2 -0
- package/dist/2252.js.map +1 -0
- package/dist/2690.js +1 -1
- package/dist/2820.js +1 -0
- package/dist/289.js +1 -0
- package/dist/289.js.map +1 -0
- package/dist/3099.js +1 -1
- package/dist/3203.js +2 -0
- package/dist/3203.js.map +1 -0
- package/dist/3584.js +1 -1
- package/dist/3715.js +1 -1
- package/dist/3877.js +1 -0
- package/dist/3877.js.map +1 -0
- package/dist/4055.js +1 -1
- package/dist/4132.js +1 -1
- package/dist/4300.js +1 -1
- package/dist/4335.js +1 -1
- package/dist/439.js +2 -0
- package/dist/439.js.map +1 -0
- package/dist/4584.js +1 -1
- package/dist/4618.js +1 -1
- package/dist/4652.js +1 -1
- package/dist/4944.js +1 -1
- package/dist/5173.js +1 -1
- package/dist/5241.js +1 -1
- package/dist/5442.js +1 -1
- package/dist/5661.js +1 -1
- package/dist/6022.js +1 -1
- package/dist/6468.js +1 -1
- package/dist/6589.js +1 -0
- package/dist/6679.js +1 -1
- package/dist/6840.js +1 -1
- package/dist/6859.js +1 -1
- package/dist/7097.js +1 -1
- package/dist/7159.js +1 -1
- package/dist/723.js +1 -1
- package/dist/7617.js +1 -1
- package/dist/795.js +1 -1
- package/dist/8163.js +1 -1
- package/dist/826.js +1 -1
- package/dist/826.js.map +1 -1
- package/dist/8349.js +1 -1
- package/dist/8371.js +1 -0
- package/dist/8390.js +1 -1
- package/dist/8488.js +2 -0
- package/dist/8488.js.map +1 -0
- package/dist/8618.js +1 -1
- package/dist/890.js +1 -1
- package/dist/9090.js +1 -1
- package/dist/9090.js.map +1 -1
- package/dist/9214.js +1 -1
- package/dist/9538.js +1 -1
- package/dist/9569.js +1 -1
- package/dist/986.js +1 -1
- package/dist/9879.js +1 -1
- package/dist/9895.js +1 -1
- package/dist/9900.js +1 -1
- package/dist/9913.js +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-reports-app.js +1 -1
- package/dist/openmrs-esm-reports-app.js.buildmanifest.json +325 -234
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/components/concept-search/concept-search-input.component.tsx +59 -0
- package/src/components/concept-search/concept-search-input.scss +13 -0
- package/src/components/concept-search/concept-search-input.test.tsx +119 -0
- package/src/components/concept-search/concept-search-results.scss +107 -0
- package/src/components/concept-search/concept-search-results.tsx +135 -0
- package/src/components/report-parameter-input.component.tsx +84 -19
- package/src/components/report-parameter.component.tsx +23 -0
- package/src/hooks/useConceptSearch.ts +20 -0
- package/src/hooks/useConceptSearchInput.test.ts +118 -0
- package/src/hooks/useConceptSearchInput.ts +65 -0
- package/translations/am.json +1 -0
- package/translations/ar.json +1 -0
- package/translations/ar_SY.json +1 -0
- package/translations/bn.json +1 -0
- package/translations/cs.json +87 -0
- package/translations/de.json +1 -0
- package/translations/en.json +1 -0
- package/translations/en_US.json +1 -0
- package/translations/es.json +1 -0
- package/translations/es_MX.json +1 -0
- package/translations/fr.json +1 -0
- package/translations/he.json +1 -0
- package/translations/hi.json +1 -0
- package/translations/hi_IN.json +1 -0
- package/translations/id.json +1 -0
- package/translations/it.json +1 -0
- package/translations/ka.json +1 -0
- package/translations/km.json +1 -0
- package/translations/ku.json +1 -0
- package/translations/ky.json +1 -0
- package/translations/lg.json +1 -0
- package/translations/ne.json +1 -0
- package/translations/pl.json +1 -0
- package/translations/pt.json +1 -0
- package/translations/pt_BR.json +1 -0
- package/translations/qu.json +1 -0
- package/translations/ro_RO.json +1 -0
- package/translations/ru_RU.json +1 -0
- package/translations/si.json +1 -0
- package/translations/sq.json +87 -0
- package/translations/sw.json +1 -0
- package/translations/sw_KE.json +1 -0
- package/translations/tr.json +1 -0
- package/translations/tr_TR.json +1 -0
- package/translations/uk.json +1 -0
- package/translations/uz.json +1 -0
- package/translations/uz@Latn.json +1 -0
- package/translations/uz_UZ.json +1 -0
- package/translations/vi.json +1 -0
- package/translations/zh.json +1 -0
- package/translations/zh_CN.json +1 -0
- package/translations/zh_TW.json +87 -0
- package/dist/1877.js +0 -1
- package/dist/1877.js.map +0 -1
- package/dist/2316.js +0 -2
- package/dist/2316.js.map +0 -1
- package/dist/2345.js +0 -2
- package/dist/2345.js.map +0 -1
- package/dist/2358.js +0 -1
- package/dist/2358.js.map +0 -1
- package/dist/5520.js +0 -2
- package/dist/5520.js.map +0 -1
- package/dist/8178.js +0 -2
- package/dist/8178.js.map +0 -1
- /package/dist/{2345.js.LICENSE.txt → 2252.js.LICENSE.txt} +0 -0
- /package/dist/{2316.js.LICENSE.txt → 3203.js.LICENSE.txt} +0 -0
- /package/dist/{5520.js.LICENSE.txt → 439.js.LICENSE.txt} +0 -0
- /package/dist/{8178.js.LICENSE.txt → 8488.js.LICENSE.txt} +0 -0
package/dist/routes.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"fhir2":">=1.2","webservices.rest":">=2.2.0","reportingrest":">=1.15.0","reporting":">=1.27.0"},"extensions":[{"name":"admin-report-link","slot":"system-admin-page-card-link-slot","component":"reportsLink"},{"name":"run-report","component":"runReport"}],"pages":[{"component":"root","route":"reports"},{"component":"overview","route":"overview"},{"component":"scheduled-overview","route":"scheduled-overview"},{"component":"reports-data-overview","route":"reports-data-overview"}],"modals":[{"name":"cancel-report-modal","component":"cancelReportModal"}],"version":"4.2.2-pre.
|
|
1
|
+
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"fhir2":">=1.2","webservices.rest":">=2.2.0","reportingrest":">=1.15.0","reporting":">=1.27.0"},"extensions":[{"name":"admin-report-link","slot":"system-admin-page-card-link-slot","component":"reportsLink"},{"name":"run-report","component":"runReport"}],"pages":[{"component":"root","route":"reports"},{"component":"overview","route":"overview"},{"component":"scheduled-overview","route":"scheduled-overview"},{"component":"reports-data-overview","route":"reports-data-overview"}],"modals":[{"name":"cancel-report-modal","component":"cancelReportModal"}],"version":"4.2.2-pre.394"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Search } from '@carbon/react';
|
|
4
|
+
import ConceptSearchResults from './concept-search-results';
|
|
5
|
+
import { useConceptSearchInput } from '../../hooks/useConceptSearchInput';
|
|
6
|
+
import styles from './concept-search-input.scss';
|
|
7
|
+
|
|
8
|
+
interface ConceptSearchInputProps {
|
|
9
|
+
parameterName: string;
|
|
10
|
+
labelText?: string;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
onConceptSelect: (conceptUuid: string, conceptDisplay: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ConceptSearchInput: React.FC<ConceptSearchInputProps> = ({
|
|
16
|
+
parameterName,
|
|
17
|
+
labelText,
|
|
18
|
+
placeholder,
|
|
19
|
+
onConceptSelect,
|
|
20
|
+
}) => {
|
|
21
|
+
const { t } = useTranslation();
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
searchInputRef,
|
|
25
|
+
searchTerm,
|
|
26
|
+
selectedConcept,
|
|
27
|
+
debouncedSearchTerm,
|
|
28
|
+
handleSearchTermChange,
|
|
29
|
+
focusAndClearSearchInput,
|
|
30
|
+
handleConceptSelect,
|
|
31
|
+
} = useConceptSearchInput({
|
|
32
|
+
parameterName,
|
|
33
|
+
onConceptSelect,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className={styles.conceptSearchContainer}>
|
|
38
|
+
<Search
|
|
39
|
+
size="lg"
|
|
40
|
+
placeholder={placeholder || t('searchFieldPlaceholder', 'Search for a concept')}
|
|
41
|
+
labelText={labelText || t('searchFieldPlaceholder', 'Search for a concept')}
|
|
42
|
+
onChange={handleSearchTermChange}
|
|
43
|
+
ref={searchInputRef}
|
|
44
|
+
value={searchTerm}
|
|
45
|
+
/>
|
|
46
|
+
{!selectedConcept && (
|
|
47
|
+
<div className={styles.conceptSearchResultsDropdown}>
|
|
48
|
+
<ConceptSearchResults
|
|
49
|
+
searchTerm={debouncedSearchTerm}
|
|
50
|
+
focusAndClearSearchInput={focusAndClearSearchInput}
|
|
51
|
+
onConceptSelect={handleConceptSelect}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default ConceptSearchInput;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import ConceptSearchInput from './concept-search-input.component';
|
|
5
|
+
|
|
6
|
+
// Mock the dependencies
|
|
7
|
+
jest.mock('react-i18next', () => ({
|
|
8
|
+
useTranslation: () => ({
|
|
9
|
+
t: (key: string, defaultValue: string) => defaultValue,
|
|
10
|
+
}),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
jest.mock('@openmrs/esm-framework', () => ({
|
|
14
|
+
useDebounce: jest.fn((value) => value),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
jest.mock('./concept-search-results', () => {
|
|
18
|
+
return function MockConceptSearchResults({ searchTerm, onConceptSelect }: any) {
|
|
19
|
+
if (!searchTerm) return null;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div data-testid="concept-search-results">
|
|
23
|
+
<div
|
|
24
|
+
data-testid="concept-result"
|
|
25
|
+
onClick={() => onConceptSelect({ uuid: 'test-uuid', display: 'Test Concept' })}
|
|
26
|
+
>
|
|
27
|
+
Test Concept
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('ConceptSearchInput', () => {
|
|
35
|
+
const mockOnConceptSelect = jest.fn();
|
|
36
|
+
const defaultProps = {
|
|
37
|
+
parameterName: 'testParameter',
|
|
38
|
+
onConceptSelect: mockOnConceptSelect,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
jest.clearAllMocks();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should render search input with default placeholder', () => {
|
|
46
|
+
render(<ConceptSearchInput {...defaultProps} />);
|
|
47
|
+
|
|
48
|
+
expect(screen.getByRole('searchbox')).toBeInTheDocument();
|
|
49
|
+
expect(screen.getByRole('search', { name: 'Search for a concept' })).toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should render search input with custom placeholder and label', () => {
|
|
53
|
+
render(<ConceptSearchInput {...defaultProps} placeholder="Custom placeholder" labelText="Custom label" />);
|
|
54
|
+
|
|
55
|
+
expect(screen.getByRole('searchbox')).toHaveAttribute('placeholder', 'Custom placeholder');
|
|
56
|
+
expect(screen.getByRole('search', { name: 'Custom placeholder' })).toBeInTheDocument();
|
|
57
|
+
expect(screen.getByLabelText('Custom label')).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should show search results when typing', async () => {
|
|
61
|
+
render(<ConceptSearchInput {...defaultProps} />);
|
|
62
|
+
|
|
63
|
+
const searchInput = screen.getByRole('searchbox');
|
|
64
|
+
fireEvent.change(searchInput, { target: { value: 'test' } });
|
|
65
|
+
|
|
66
|
+
await waitFor(() => {
|
|
67
|
+
expect(screen.getByTestId('concept-search-results')).toBeInTheDocument();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should call onConceptSelect when a concept is selected', async () => {
|
|
72
|
+
render(<ConceptSearchInput {...defaultProps} />);
|
|
73
|
+
|
|
74
|
+
const searchInput = screen.getByRole('searchbox');
|
|
75
|
+
fireEvent.change(searchInput, { target: { value: 'test' } });
|
|
76
|
+
|
|
77
|
+
await waitFor(() => {
|
|
78
|
+
expect(screen.getByTestId('concept-search-results')).toBeInTheDocument();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
fireEvent.click(screen.getByTestId('concept-result'));
|
|
82
|
+
|
|
83
|
+
expect(mockOnConceptSelect).toHaveBeenCalledWith('test-uuid', 'Test Concept');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should hide search results after concept selection', async () => {
|
|
87
|
+
render(<ConceptSearchInput {...defaultProps} />);
|
|
88
|
+
|
|
89
|
+
const searchInput = screen.getByRole('searchbox');
|
|
90
|
+
fireEvent.change(searchInput, { target: { value: 'test' } });
|
|
91
|
+
|
|
92
|
+
await waitFor(() => {
|
|
93
|
+
expect(screen.getByTestId('concept-search-results')).toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
fireEvent.click(screen.getByTestId('concept-result'));
|
|
97
|
+
|
|
98
|
+
await waitFor(() => {
|
|
99
|
+
expect(screen.queryByTestId('concept-search-results')).not.toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should update input value with selected concept display name', async () => {
|
|
104
|
+
render(<ConceptSearchInput {...defaultProps} />);
|
|
105
|
+
|
|
106
|
+
const searchInput = screen.getByRole('searchbox') as HTMLInputElement;
|
|
107
|
+
fireEvent.change(searchInput, { target: { value: 'test' } });
|
|
108
|
+
|
|
109
|
+
await waitFor(() => {
|
|
110
|
+
expect(screen.getByTestId('concept-search-results')).toBeInTheDocument();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
fireEvent.click(screen.getByTestId('concept-result'));
|
|
114
|
+
|
|
115
|
+
await waitFor(() => {
|
|
116
|
+
expect(searchInput.value).toBe('Test Concept');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
5
|
+
|
|
6
|
+
.container {
|
|
7
|
+
position: relative;
|
|
8
|
+
z-index: 1000;
|
|
9
|
+
margin: layout.$spacing-03 0 0 0;
|
|
10
|
+
background: $ui-01;
|
|
11
|
+
border: 1px solid $ui-03;
|
|
12
|
+
border-radius: layout.$spacing-02;
|
|
13
|
+
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2);
|
|
14
|
+
max-height: 300px;
|
|
15
|
+
overflow-y: auto;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.searchResultsCount {
|
|
19
|
+
@include type.type-style('body-compact-01');
|
|
20
|
+
color: $text-02;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.conceptBasketSearchResultsHeader {
|
|
24
|
+
display: flex;
|
|
25
|
+
justify-content: space-between;
|
|
26
|
+
margin-bottom: layout.$spacing-03;
|
|
27
|
+
align-items: center;
|
|
28
|
+
padding: layout.$spacing-03 layout.$spacing-03 0 layout.$spacing-03;
|
|
29
|
+
border-bottom: 1px solid $ui-03;
|
|
30
|
+
padding-bottom: layout.$spacing-03;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.tabletSearchResultTile {
|
|
34
|
+
&:not(:last-of-type) {
|
|
35
|
+
margin-bottom: layout.$spacing-04;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.searchResultSkeletonWrapper {
|
|
40
|
+
margin: layout.$spacing-03 0;
|
|
41
|
+
padding: layout.$spacing-03;
|
|
42
|
+
|
|
43
|
+
:global(.cds--skeleton__text) {
|
|
44
|
+
margin: 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.searchResultCntSkeleton {
|
|
48
|
+
margin-right: layout.$spacing-07;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.skeletonTile {
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.emptyState {
|
|
58
|
+
margin: layout.$spacing-03;
|
|
59
|
+
display: flex;
|
|
60
|
+
justify-content: center;
|
|
61
|
+
align-items: center;
|
|
62
|
+
padding: layout.$spacing-05;
|
|
63
|
+
text-align: center;
|
|
64
|
+
background-color: $ui-01;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:global(.omrs-breakpoint-lt-small-desktop) .emptyState {
|
|
68
|
+
background-color: $ui-02;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.link {
|
|
72
|
+
color: $interactive-01;
|
|
73
|
+
text-decoration: underline;
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.resultsContainer {
|
|
78
|
+
padding: layout.$spacing-03;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.resultsContainer div[role='option'] {
|
|
82
|
+
margin: 0;
|
|
83
|
+
padding: layout.$spacing-03;
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
border-radius: layout.$spacing-02;
|
|
86
|
+
@include type.type-style('body-compact-01');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.resultsContainer div[role='option']:hover,
|
|
90
|
+
.resultsContainer div[role='option']:focus {
|
|
91
|
+
background-color: $ui-03;
|
|
92
|
+
outline: 2px solid $interactive-01;
|
|
93
|
+
outline-offset: -2px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.resultsContainer::-webkit-scrollbar-thumb {
|
|
97
|
+
border-radius: layout.$spacing-02;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.productiveHeading01 {
|
|
101
|
+
@include type.type-style('heading-compact-01');
|
|
102
|
+
margin-bottom: layout.$spacing-03;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.bodyShort01 {
|
|
106
|
+
@include type.type-style('body-compact-01');
|
|
107
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styles from './concept-search-results.scss';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { useLayoutType } from '@openmrs/esm-framework';
|
|
5
|
+
import { Button, ButtonSkeleton, SkeletonText, Tile } from '@carbon/react';
|
|
6
|
+
import classNames from 'classnames';
|
|
7
|
+
import { useConceptSearch } from '../../hooks/useConceptSearch';
|
|
8
|
+
|
|
9
|
+
export interface ConceptSearchResultsProps {
|
|
10
|
+
searchTerm: string;
|
|
11
|
+
focusAndClearSearchInput: () => void;
|
|
12
|
+
onConceptSelect?: (concept: { uuid: string; display?: string }) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function ConceptSearchResults({
|
|
16
|
+
searchTerm,
|
|
17
|
+
focusAndClearSearchInput,
|
|
18
|
+
onConceptSelect,
|
|
19
|
+
}: ConceptSearchResultsProps) {
|
|
20
|
+
const { t } = useTranslation();
|
|
21
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
22
|
+
const { concepts, isLoading, error } = useConceptSearch(searchTerm);
|
|
23
|
+
|
|
24
|
+
if (!searchTerm) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (isLoading) {
|
|
29
|
+
return <ConceptSearchSkeleton />;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (error) {
|
|
33
|
+
return (
|
|
34
|
+
<Tile className={styles.emptyState}>
|
|
35
|
+
<div>
|
|
36
|
+
<h4 className={styles.productiveHeading01}>
|
|
37
|
+
{t('errorFetchingConceptResults', 'Error fetching results for "{{searchTerm}}"', {
|
|
38
|
+
searchTerm,
|
|
39
|
+
})}
|
|
40
|
+
</h4>
|
|
41
|
+
<p className={styles.bodyShort01}>
|
|
42
|
+
<span>{t('trySearchingAgain', 'Please try searching again')}</span>
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
</Tile>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (concepts?.length === 0) {
|
|
50
|
+
return (
|
|
51
|
+
<Tile className={styles.emptyState}>
|
|
52
|
+
<div>
|
|
53
|
+
<h4 className={styles.productiveHeading01}>
|
|
54
|
+
{t('noResultsForConceptSearch', 'No results to display for "{{searchTerm}}"', {
|
|
55
|
+
searchTerm,
|
|
56
|
+
})}
|
|
57
|
+
</h4>
|
|
58
|
+
<p className={styles.bodyShort01}>
|
|
59
|
+
<span>{t('tryTo', 'Try to')}</span>{' '}
|
|
60
|
+
<span className={styles.link} role="link" tabIndex={0} onClick={focusAndClearSearchInput}>
|
|
61
|
+
{t('searchAgain', 'search again')}
|
|
62
|
+
</span>{' '}
|
|
63
|
+
<span>{t('usingADifferentTerm', 'using a different term')}</span>
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
</Tile>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className={styles.container}>
|
|
72
|
+
<div className={styles.conceptBasketSearchResultsHeader}>
|
|
73
|
+
<span className={styles.searchResultsCount}>
|
|
74
|
+
{t('searchResultsMatchesForTerm', '{{count}} results for "{{searchTerm}}"', {
|
|
75
|
+
count: concepts?.length,
|
|
76
|
+
searchTerm,
|
|
77
|
+
})}
|
|
78
|
+
</span>
|
|
79
|
+
<Button kind="ghost" onClick={focusAndClearSearchInput} size={isTablet ? 'md' : 'sm'}>
|
|
80
|
+
{t('clearSearchResults', 'Clear Results')}
|
|
81
|
+
</Button>
|
|
82
|
+
</div>
|
|
83
|
+
<div
|
|
84
|
+
className={styles.resultsContainer}
|
|
85
|
+
role="listbox"
|
|
86
|
+
aria-label={t('conceptSearchResults', 'Concept search results')}
|
|
87
|
+
>
|
|
88
|
+
{concepts?.map((concept, index) => (
|
|
89
|
+
<div
|
|
90
|
+
key={concept.uuid}
|
|
91
|
+
role="option"
|
|
92
|
+
tabIndex={0}
|
|
93
|
+
aria-selected={false}
|
|
94
|
+
onClick={() => {
|
|
95
|
+
onConceptSelect?.(concept);
|
|
96
|
+
}}
|
|
97
|
+
onKeyDown={(e) => {
|
|
98
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
onConceptSelect?.(concept);
|
|
101
|
+
}
|
|
102
|
+
}}
|
|
103
|
+
style={{ cursor: 'pointer' }}
|
|
104
|
+
>
|
|
105
|
+
{concept.display}
|
|
106
|
+
</div>
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const ConceptSearchSkeleton = () => {
|
|
114
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
115
|
+
const tileClassName = classNames({
|
|
116
|
+
[styles.tabletSearchResultTile]: isTablet,
|
|
117
|
+
[styles.desktopSearchResultTile]: !isTablet,
|
|
118
|
+
[styles.skeletonTile]: true,
|
|
119
|
+
});
|
|
120
|
+
const buttonSize = isTablet ? 'md' : 'sm';
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div className={styles.searchResultSkeletonWrapper}>
|
|
124
|
+
<div className={styles.conceptBasketSearchResultsHeader}>
|
|
125
|
+
<SkeletonText className={styles.searchResultCntSkeleton} />
|
|
126
|
+
<ButtonSkeleton size={buttonSize} />
|
|
127
|
+
</div>
|
|
128
|
+
{[...Array(4)].map((_, index) => (
|
|
129
|
+
<Tile key={index} className={tileClassName}>
|
|
130
|
+
<SkeletonText />
|
|
131
|
+
</Tile>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useState } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { isEqual } from 'lodash-es';
|
|
3
|
-
import { DatePicker, DatePickerInput, Select, SelectItem, TextInput } from '@carbon/react';
|
|
3
|
+
import { DatePicker, DatePickerInput, Select, SelectItem, TextInput, Search } from '@carbon/react';
|
|
4
4
|
import { useLocations } from './reports.resource';
|
|
5
5
|
import styles from './run-report/run-report-form.scss';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { useDebounce } from '@openmrs/esm-framework';
|
|
8
|
+
import ConceptSearchResults from './concept-search/concept-search-results';
|
|
6
9
|
|
|
7
10
|
interface ReportParameterInputProps {
|
|
8
11
|
parameter: any;
|
|
@@ -21,8 +24,56 @@ function getInitialValue(parameter: any, value: any) {
|
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
const ReportParameterInput: React.FC<ReportParameterInputProps> = ({ parameter, value, onChange }) => {
|
|
27
|
+
const { t } = useTranslation();
|
|
24
28
|
const { locations } = useLocations();
|
|
25
29
|
const [valueInternal, setValueInternal] = useState<string | number>(getInitialValue(parameter, value));
|
|
30
|
+
const searchInputRef = useRef(null);
|
|
31
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
32
|
+
const [selectedConcept, setSelectedConcept] = useState<{ uuid: string; display?: string } | null>(null);
|
|
33
|
+
const debouncedSearchTerm = useDebounce(searchTerm, 300);
|
|
34
|
+
|
|
35
|
+
const focusAndClearSearchInput = useCallback(() => {
|
|
36
|
+
setSearchTerm('');
|
|
37
|
+
searchInputRef.current?.focus();
|
|
38
|
+
}, [setSearchTerm]);
|
|
39
|
+
|
|
40
|
+
const handleOnChange = useCallback(
|
|
41
|
+
(event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
|
42
|
+
let eventValue = null;
|
|
43
|
+
const target = event.target as HTMLInputElement | HTMLSelectElement;
|
|
44
|
+
if ((target as HTMLInputElement).type === 'checkbox') {
|
|
45
|
+
eventValue = (target as HTMLInputElement).checked;
|
|
46
|
+
} else {
|
|
47
|
+
eventValue = target.value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setValueInternal(eventValue);
|
|
51
|
+
|
|
52
|
+
if (parameter.type === 'java.util.Date') {
|
|
53
|
+
onChange(new Date(eventValue).toLocaleDateString());
|
|
54
|
+
} else {
|
|
55
|
+
onChange(eventValue);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
[onChange, parameter.type],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const handleConceptSelect = useCallback(
|
|
62
|
+
(concept: { uuid: string; display?: string }) => {
|
|
63
|
+
setSelectedConcept(concept);
|
|
64
|
+
setSearchTerm(concept.display || '');
|
|
65
|
+
if (handleOnChange) {
|
|
66
|
+
const syntheticEvent = {
|
|
67
|
+
target: {
|
|
68
|
+
name: parameter.name,
|
|
69
|
+
value: concept.uuid,
|
|
70
|
+
},
|
|
71
|
+
} as React.ChangeEvent<HTMLInputElement>;
|
|
72
|
+
handleOnChange(syntheticEvent);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
[handleOnChange, parameter.name],
|
|
76
|
+
);
|
|
26
77
|
|
|
27
78
|
const isValueEqual = useCallback(
|
|
28
79
|
(valueA, valueB) => {
|
|
@@ -35,6 +86,17 @@ const ReportParameterInput: React.FC<ReportParameterInputProps> = ({ parameter,
|
|
|
35
86
|
[parameter.type],
|
|
36
87
|
);
|
|
37
88
|
|
|
89
|
+
const handleSearchTermChange = useCallback(
|
|
90
|
+
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
91
|
+
setSearchTerm(event.target.value ?? '');
|
|
92
|
+
// Clear selected concept when user starts typing again
|
|
93
|
+
if (selectedConcept) {
|
|
94
|
+
setSelectedConcept(null);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
[setSearchTerm, selectedConcept],
|
|
98
|
+
);
|
|
99
|
+
|
|
38
100
|
useEffect(() => {
|
|
39
101
|
const newInternalValue = getInitialValue(parameter, value);
|
|
40
102
|
setValueInternal((prevValue) => {
|
|
@@ -88,6 +150,26 @@ const ReportParameterInput: React.FC<ReportParameterInputProps> = ({ parameter,
|
|
|
88
150
|
))}
|
|
89
151
|
</Select>
|
|
90
152
|
);
|
|
153
|
+
case 'org.openmrs.Concept':
|
|
154
|
+
return (
|
|
155
|
+
<div>
|
|
156
|
+
<Search
|
|
157
|
+
size="lg"
|
|
158
|
+
placeholder={t('searchFieldPlaceholder', 'Search for a concept')}
|
|
159
|
+
labelText={t('searchFieldPlaceholder', 'Search for a concept')}
|
|
160
|
+
onChange={handleSearchTermChange}
|
|
161
|
+
ref={searchInputRef}
|
|
162
|
+
value={searchTerm}
|
|
163
|
+
/>
|
|
164
|
+
{!selectedConcept && (
|
|
165
|
+
<ConceptSearchResults
|
|
166
|
+
searchTerm={debouncedSearchTerm}
|
|
167
|
+
focusAndClearSearchInput={focusAndClearSearchInput}
|
|
168
|
+
onConceptSelect={handleConceptSelect}
|
|
169
|
+
/>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
91
173
|
default:
|
|
92
174
|
return (
|
|
93
175
|
<span className={styles.unknownParameterTypeSpan}>
|
|
@@ -97,23 +179,6 @@ const ReportParameterInput: React.FC<ReportParameterInputProps> = ({ parameter,
|
|
|
97
179
|
}
|
|
98
180
|
};
|
|
99
181
|
|
|
100
|
-
function handleOnChange(event) {
|
|
101
|
-
let eventValue = null;
|
|
102
|
-
if (event.target.type === 'checkbox') {
|
|
103
|
-
eventValue = event.target.checked;
|
|
104
|
-
} else {
|
|
105
|
-
eventValue = event.target.value;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
setValueInternal(eventValue);
|
|
109
|
-
|
|
110
|
-
if (parameter.type === 'java.util.Date') {
|
|
111
|
-
onChange(new Date(eventValue).toLocaleDateString());
|
|
112
|
-
} else {
|
|
113
|
-
onChange(eventValue);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
182
|
function handleOnDateChange(dateValue) {
|
|
118
183
|
const newDate = new Date(dateValue);
|
|
119
184
|
setValueInternal(newDate.toISOString());
|
|
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
3
3
|
import { TextInput, Select, SelectItem } from '@carbon/react';
|
|
4
4
|
import { OpenmrsDatePicker } from '@openmrs/esm-framework';
|
|
5
5
|
import styles from './report-parameter.scss';
|
|
6
|
+
import ConceptSearchInput from './concept-search/concept-search-input.component';
|
|
6
7
|
|
|
7
8
|
type DateReportParameter = 'java.util.Date';
|
|
8
9
|
type InputReportParameter = 'java.lang.String' | 'java.lang.Integer';
|
|
@@ -42,6 +43,19 @@ const ReportParameter: React.FC<ReportParameterProps> = ({
|
|
|
42
43
|
handleOnDateChange,
|
|
43
44
|
}) => {
|
|
44
45
|
const { t } = useTranslation();
|
|
46
|
+
|
|
47
|
+
const handleConceptSelect = (conceptUuid: string, conceptDisplay: string) => {
|
|
48
|
+
if (handleOnChange) {
|
|
49
|
+
const syntheticEvent = {
|
|
50
|
+
target: {
|
|
51
|
+
name: parameter.name,
|
|
52
|
+
value: conceptUuid,
|
|
53
|
+
},
|
|
54
|
+
} as React.ChangeEvent<HTMLInputElement>;
|
|
55
|
+
handleOnChange(syntheticEvent);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
45
59
|
switch (parameter.type) {
|
|
46
60
|
case 'java.util.Date':
|
|
47
61
|
return (
|
|
@@ -90,6 +104,15 @@ const ReportParameter: React.FC<ReportParameterProps> = ({
|
|
|
90
104
|
</Select>
|
|
91
105
|
</div>
|
|
92
106
|
);
|
|
107
|
+
case 'org.openmrs.Concept':
|
|
108
|
+
return (
|
|
109
|
+
<ConceptSearchInput
|
|
110
|
+
parameterName={parameter.name}
|
|
111
|
+
labelText={parameter.label}
|
|
112
|
+
placeholder={t('searchFieldPlaceholder', 'Search for a concept')}
|
|
113
|
+
onConceptSelect={handleConceptSelect}
|
|
114
|
+
/>
|
|
115
|
+
);
|
|
93
116
|
default:
|
|
94
117
|
return (
|
|
95
118
|
<div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Concept, type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import useSWRImmutable from 'swr/immutable';
|
|
4
|
+
|
|
5
|
+
export function useConceptSearch(query: string) {
|
|
6
|
+
const { data, ...rest } = useSWRImmutable<FetchResponse<{ results: Array<Concept> }>, Error>(
|
|
7
|
+
query ? `${restBaseUrl}/concept?q=${query}&v=full` : null,
|
|
8
|
+
openmrsFetch,
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const results = useMemo(
|
|
12
|
+
() => ({
|
|
13
|
+
concepts: data?.data?.results,
|
|
14
|
+
...rest,
|
|
15
|
+
}),
|
|
16
|
+
[data, rest],
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
return results;
|
|
20
|
+
}
|