@ampath/esm-reports-app 1.0.0-next.6 → 1.0.0-next.60
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/dist/197.js +1 -1
- package/dist/282.js +1 -1
- package/dist/282.js.map +1 -1
- package/dist/295.css +1 -0
- package/dist/295.css.map +1 -0
- package/dist/295.js +1 -0
- package/dist/295.js.map +1 -0
- package/dist/300.js +1 -1
- package/dist/311.js +1 -1
- package/dist/311.js.map +1 -1
- package/dist/335.js +1 -1
- package/dist/353.js +1 -1
- package/dist/353.js.map +1 -1
- package/dist/410.js +1 -0
- package/dist/410.js.map +1 -0
- package/dist/478.js +1 -1
- package/dist/478.js.map +1 -1
- package/dist/540.js +1 -1
- package/dist/540.js.map +1 -1
- package/dist/55.js +1 -1
- package/dist/580.js +1 -0
- package/dist/580.js.map +1 -0
- package/dist/652.js +1 -1
- package/dist/876.css +1 -0
- package/dist/876.css.map +1 -0
- package/dist/876.js +1 -0
- package/dist/876.js.map +1 -0
- package/dist/907.js +2 -0
- package/dist/907.js.LICENSE.txt +35 -0
- package/dist/907.js.map +1 -0
- package/dist/961.js +1 -1
- package/dist/961.js.map +1 -1
- package/dist/99.js +1 -1
- package/dist/ampath-esm-reports-app.js +1 -1
- package/dist/ampath-esm-reports-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +2 -2
- package/src/common/daily-summary-table.tsx +55 -0
- package/src/common/linelist-table/index.ts +3 -0
- package/src/common/linelist-table/linelist-columns.ts +48 -0
- package/src/common/linelist-table/linelist-table.component.tsx +442 -0
- package/src/common/linelist-table/linelist-table.scss +306 -0
- package/src/common/linelist-table/linelist-table.test.tsx +120 -0
- package/src/common/linelist-table/use-all-rows.ts +93 -0
- package/src/common/linelist-table/use-column-layout.ts +123 -0
- package/src/common/report-error/index.ts +1 -0
- package/src/common/report-error/show-report-error.ts +25 -0
- package/src/common/report-filters/date-range.ts +137 -0
- package/src/common/report-filters/index.ts +4 -0
- package/src/common/report-filters/report-filters.component.tsx +297 -100
- package/src/common/report-filters/report-filters.scss +255 -32
- package/src/common/report-filters/types.ts +66 -0
- package/src/common/report-filters/use-restore-period.ts +43 -0
- package/src/common/report-header/index.ts +1 -0
- package/src/common/report-header/use-location-details.ts +58 -0
- package/src/common/report-page/index.ts +2 -0
- package/src/common/report-page/report-page.component.tsx +32 -0
- package/src/common/report-page/report-page.scss +45 -0
- package/src/common/report-page/use-page-scale.ts +60 -0
- package/src/common/report-placeholder/has-report-data.ts +15 -0
- package/src/common/report-placeholder/index.ts +3 -0
- package/src/common/report-placeholder/report-placeholder.component.tsx +81 -0
- package/src/common/report-placeholder/report-placeholder.scss +48 -0
- package/src/common/report-placeholder/use-generated-location.ts +62 -0
- package/src/common/report-register/index.ts +6 -0
- package/src/common/report-register/register-columns.ts +46 -0
- package/src/common/report-register/register-layout.component.tsx +265 -0
- package/src/common/report-register/register-layout.scss +150 -0
- package/src/common/report-register/register-table.scss +71 -0
- package/src/common/report-register/report-parents.ts +27 -0
- package/src/common/report-register/rows-to-csv.ts +89 -0
- package/src/common/report-register/use-location-name.ts +54 -0
- package/src/common/report-register/use-patient-list.ts +92 -0
- package/src/common/report-skeleton/index.ts +1 -0
- package/src/common/report-skeleton/report-skeleton.component.tsx +46 -0
- package/src/common/report-skeleton/report-skeleton.scss +29 -0
- package/src/config-schema.ts +9 -1
- package/src/dashboard/report-catalog.ts +173 -0
- package/src/dashboard/reports-dasboard.tsx +101 -17
- package/src/dashboard/reports-dashboard.module.scss +269 -11
- package/src/index.ts +13 -1
- package/src/reports/datatable-wrapper/datatable-wrapper.component.tsx +69 -0
- package/src/reports/moh-240/moh-240-header.component.tsx +62 -0
- package/src/reports/moh-240/moh-240.columns.ts +113 -0
- package/src/reports/moh-240/moh-240.component.tsx +75 -0
- package/src/reports/moh-240/moh-240.scss +40 -0
- package/src/reports/moh-240/sub-reports/moh-240-register.component.tsx +36 -0
- package/src/reports/moh-240/sub-reports/page-summary.component.tsx +148 -0
- package/src/reports/moh-505/moh-505-header.component.tsx +67 -0
- package/src/reports/moh-505/moh-505.component.tsx +441 -0
- package/src/reports/moh-505/moh-505.scss +117 -0
- package/src/reports/moh-705B/moh-204b-register.component.tsx +244 -0
- package/src/reports/moh-705B/moh-204b.columns.ts +39 -0
- package/src/reports/moh-705B/moh-705b.component.tsx +658 -0
- package/src/reports/moh-705B/moh-705b.scss +172 -0
- package/src/reports/moh-705a/moh-705a.component.tsx +660 -0
- package/src/reports/moh-705a/moh-705a.scss +172 -0
- package/src/reports/moh-705a/registers/moh-204a-register.component.tsx +329 -0
- package/src/reports/moh-705a/registers/moh-204a.columns.ts +57 -0
- package/src/reports/moh-705a/type.ts +6 -0
- package/src/reports/moh-706/moh-706-header.component.tsx +66 -0
- package/src/reports/moh-706/moh-706.component.tsx +150 -24
- package/src/reports/moh-706/moh-706.scss +119 -11
- package/src/reports/moh-706/sub-reports/bacteriology/bacteriology.component.tsx +36 -44
- package/src/reports/moh-706/sub-reports/blood-chemistry/blood-chemistry.component.tsx +232 -318
- package/src/reports/moh-706/sub-reports/drug-susceptibility-testing/drug-susceptibility-testing.component.tsx +32 -47
- package/src/reports/moh-706/sub-reports/haematology/haematology.component.tsx +44 -52
- package/src/reports/moh-706/sub-reports/histology-and-cytology/histology-and-cytology.component.tsx +25 -40
- package/src/reports/moh-706/sub-reports/parasitology/parasitology.component.tsx +31 -39
- package/src/reports/moh-706/sub-reports/serology/serology.component.tsx +26 -34
- package/src/reports/moh-706/sub-reports/specimen-referral-to-higher-levels/specimen-referral-to-higher-levels.component.tsx +10 -25
- package/src/reports/moh-706/sub-reports/urine-analysis/urine-analysis.component.tsx +42 -50
- package/src/reports/moh-710/moh-710.component.tsx +799 -632
- package/src/reports/moh-710/moh-710.scss +142 -21
- package/src/reports/moh-710/registers/moh-510/moh-510.columns.ts +40 -0
- package/src/reports/moh-710/registers/moh-510/moh-510.component.tsx +144 -0
- package/src/reports/moh-710/registers/moh-510/moh-510.scss +130 -0
- package/src/reports/moh-710/registers/moh-511-710-register.component.tsx +268 -0
- package/src/reports/moh-710/registers/moh-511-710.columns.ts +52 -0
- package/src/reports/moh-710/registers/type.ts +31 -0
- package/src/reports/moh-711/moh-711.component.tsx +138 -1269
- package/src/reports/moh-711/moh711.scss +135 -20
- package/src/reports/moh-711/registers/moh-333-register.component.tsx +637 -0
- package/src/reports/moh-711/registers/moh-333.columns.ts +119 -0
- package/src/reports/moh-711/registers/moh-405-register.component.tsx +520 -0
- package/src/reports/moh-711/registers/moh-405.columns.ts +120 -0
- package/src/reports/moh-711/registers/moh-406-register.component.tsx +530 -0
- package/src/reports/moh-711/registers/moh-406.columns.ts +133 -0
- package/src/reports/moh-711/registers/moh-510-register.component.tsx +55 -0
- package/src/reports/moh-711/registers/moh-511-register.component.tsx +270 -0
- package/src/reports/moh-711/registers/moh-511.columns.ts +59 -0
- package/src/reports/moh-711/registers/type.ts +192 -0
- package/src/reports/moh-711/sections/anc.component.tsx +159 -0
- package/src/reports/moh-711/sections/cervical-cancer.component.tsx +148 -0
- package/src/reports/moh-711/sections/chanis.component.tsx +366 -0
- package/src/reports/moh-711/sections/family-planning.component.tsx +221 -0
- package/src/reports/moh-711/sections/gbv.component.tsx +115 -0
- package/src/reports/moh-711/sections/maternity.component.tsx +321 -0
- package/src/reports/moh-711/sections/medical-social-work.component.tsx +73 -0
- package/src/reports/moh-711/sections/other.component.tsx +47 -0
- package/src/reports/moh-711/sections/physiotherapy.component.tsx +61 -0
- package/src/reports/moh-711/sections/pnc.component.tsx +127 -0
- package/src/reports/moh-711/sections/post-abortion.component.tsx +42 -0
- package/src/reports/moh-711/sections/rehabilitation.component.tsx +57 -0
- package/src/reports/moh-711/sections/report-compiled-by.component.tsx +50 -0
- package/src/reports/moh-711/sections/tb-screening.component.tsx +57 -0
- package/src/reports/moh-717/moh-717.component.tsx +149 -985
- package/src/reports/moh-717/moh717.scss +217 -21
- package/src/reports/moh-717/registers/nutrition-register.component.tsx +48 -0
- package/src/reports/moh-717/sections/finance.component.tsx +42 -0
- package/src/reports/moh-717/sections/inpatient.component.tsx +374 -0
- package/src/reports/moh-717/sections/maternity.component.tsx +126 -0
- package/src/reports/moh-717/sections/medical-records.component.tsx +35 -0
- package/src/reports/moh-717/sections/mortuary.component.tsx +43 -0
- package/src/reports/moh-717/sections/operations.component.tsx +53 -0
- package/src/reports/moh-717/sections/orthopaedic-trauma.component.tsx +98 -0
- package/src/reports/moh-717/sections/outpatient.component.tsx +597 -0
- package/src/reports/moh-717/sections/pharmacy.component.tsx +43 -0
- package/src/reports/moh-717/sections/preparedby.component.tsx +52 -0
- package/src/reports/moh-717/sections/special-services.component.tsx +114 -0
- package/src/reports/moh-731/linelist/moh-731-indicators.ts +1040 -0
- package/src/reports/moh-731/linelist/moh-731-linelist-sections.ts +423 -0
- package/src/reports/moh-731/linelist/moh-731-linelist.component.tsx +102 -0
- package/src/reports/moh-731/linelist/use-open-moh-731-linelist.ts +50 -0
- package/src/reports/moh-731/moh-731.component.tsx +251 -0
- package/src/reports/moh-731/moh-731.scss +493 -0
- package/src/reports/moh-731/registers/moh-366.columns.ts +38 -0
- package/src/reports/moh-731/registers/moh-366.component.tsx +471 -0
- package/src/reports/moh-731/registers/prep-register.component.tsx +284 -0
- package/src/reports/moh-731/registers/prep.columns.ts +34 -0
- package/src/reports/moh-731/sections/hiv-and-tb-treatment.component.tsx +1209 -0
- package/src/reports/moh-731/sections/hiv-testing-and-pre-exposure.component.tsx +727 -0
- package/src/reports/moh-731/sections/pmtct.component.tsx +572 -0
- package/src/reports/moh-731/summary/moh-731-summary.component.tsx +313 -0
- package/src/reports/moh-731/summary/moh-731-summary.config.ts +520 -0
- package/src/reports/moh-731/summary/moh-731-summary.scss +242 -0
- package/src/reports/moh-740/moh-740.component.scss +202 -0
- package/src/reports/moh-740/moh-740.component.tsx +1312 -0
- package/src/reports/moh-740/moh-740.resource.ts +49 -0
- package/src/reports/moh-740/registers/moh-222-daily-register/moh-222-daily-register.tsx +89 -0
- package/src/reports/moh-740/registers/moh-270-permanent-register/moh-270-permanent-register.tsx +61 -0
- package/src/reports/moh-740/registers/moh-740-patient-list.scss +0 -0
- package/src/reports/moh-740/registers/moh-740-patient-list.tsx +95 -0
- package/src/reports/moh-740/registers/moh-740.columns.ts +52 -0
- package/src/reports/moh-740/shared/data-cell/data-cell.scss +6 -0
- package/src/reports/moh-740/shared/data-cell/data-cell.tsx +23 -0
- package/src/reports/moh-740/shared/utils/indicator-register-map.ts +157 -0
- package/src/reports/moh-740/types/index.ts +220 -0
- package/src/reports/moh-745/moh-745.component.tsx +1021 -0
- package/src/reports/moh-745/moh-745.scss +132 -0
- package/src/reports/moh-745/registers/moh-412-register.component.tsx +199 -0
- package/src/reports/moh-745/registers/moh-412.columns.ts +52 -0
- package/src/reports/moh-745/registers/type.ts +181 -0
- package/src/reports/table-wrapper/table-row-mapper.component.tsx +53 -0
- package/src/reports/table-wrapper/table-wrapper.component.tsx +30 -0
- package/src/reports/{moh-706/table-wrapper → table-wrapper}/table-wrapper.scss +1 -16
- package/src/resources/moh-505.resource.ts +69 -0
- package/src/resources/moh-705.resource.ts +106 -0
- package/src/resources/moh-706.resource.ts +80 -0
- package/src/resources/moh-710.resource.ts +32 -9
- package/src/resources/moh-711.resource.ts +137 -9
- package/src/resources/moh-717.resource.ts +26 -9
- package/src/resources/moh-731.resource.ts +90 -0
- package/src/resources/moh-745.resource.ts +76 -0
- package/src/resources/patient-list-page.ts +65 -0
- package/src/root.component.tsx +50 -7
- package/src/root.scss +36 -0
- package/src/routes.json +3 -1
- package/src/utils/format-indicator.ts +22 -0
- package/src/utils/get-base-url.ts +39 -7
- package/src/utils/utils.ts +36 -0
- package/tsconfig.json +1 -1
- package/webpack.config.js +22 -1
- package/dist/184.js +0 -2
- package/dist/184.js.LICENSE.txt +0 -14
- package/dist/184.js.map +0 -1
- package/dist/260.js +0 -1
- package/dist/260.js.map +0 -1
- package/dist/402.js +0 -1
- package/dist/402.js.map +0 -1
- package/dist/812.js +0 -1
- package/dist/812.js.map +0 -1
- package/dist/959.js +0 -2
- package/dist/959.js.LICENSE.txt +0 -20
- package/dist/959.js.map +0 -1
- package/dist/ampath-esm-reports-app.js.buildmanifest.json +0 -539
- package/dist/main.js +0 -1
- package/dist/main.js.map +0 -1
- package/src/reports/moh-706/table-wrapper/table-wrapper.component.tsx +0 -21
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
2
|
+
|
|
3
|
+
/* A screen table for reading and analysing a list: compact rows, a header
|
|
4
|
+
* that stays in view while scrolling, and cells that truncate (full value on
|
|
5
|
+
* hover) so one long name cannot stretch every row. */
|
|
6
|
+
|
|
7
|
+
.linelist {
|
|
8
|
+
/* Light gridlines: just darker than the striping, so columns stay distinct on
|
|
9
|
+
both white and grey rows without the grid competing with the values */
|
|
10
|
+
--linelist-grid: #e2e2e2;
|
|
11
|
+
--linelist-grid-header: #cfcfcf;
|
|
12
|
+
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
gap: 0.5rem;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.toolbar {
|
|
19
|
+
position: relative;
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-wrap: wrap;
|
|
22
|
+
align-items: center;
|
|
23
|
+
gap: 0.25rem 0.5rem;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.search {
|
|
27
|
+
flex: 1 1 16rem;
|
|
28
|
+
max-width: 24rem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.count {
|
|
32
|
+
margin-left: auto;
|
|
33
|
+
font-size: 0.75rem;
|
|
34
|
+
color: var(--cds-text-secondary, #525252);
|
|
35
|
+
white-space: nowrap;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.activeFilters {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-wrap: wrap;
|
|
41
|
+
gap: 0.25rem;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* Bounded height so the sticky header has something to stick within */
|
|
45
|
+
.scroll {
|
|
46
|
+
max-height: calc(100vh - 18rem);
|
|
47
|
+
min-height: 12rem;
|
|
48
|
+
overflow: auto;
|
|
49
|
+
border: 1px solid var(--linelist-grid);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.table {
|
|
53
|
+
width: 100%;
|
|
54
|
+
border-collapse: separate;
|
|
55
|
+
border-spacing: 0;
|
|
56
|
+
font-size: 0.8125rem;
|
|
57
|
+
line-height: 1.25;
|
|
58
|
+
|
|
59
|
+
th,
|
|
60
|
+
td {
|
|
61
|
+
padding: 0.3125rem 0.625rem;
|
|
62
|
+
text-align: left;
|
|
63
|
+
vertical-align: middle;
|
|
64
|
+
border-right: 1px solid var(--linelist-grid);
|
|
65
|
+
border-bottom: 1px solid var(--linelist-grid);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* The scroll frame already draws the outer edge */
|
|
69
|
+
th:last-child,
|
|
70
|
+
td:last-child {
|
|
71
|
+
border-right: none;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
thead th {
|
|
75
|
+
border-right-color: var(--linelist-grid-header);
|
|
76
|
+
border-bottom-color: var(--linelist-grid-header);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
td {
|
|
80
|
+
max-width: 18rem;
|
|
81
|
+
overflow: hidden;
|
|
82
|
+
text-overflow: ellipsis;
|
|
83
|
+
white-space: nowrap;
|
|
84
|
+
color: var(--cds-text-primary, #161616);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
thead th {
|
|
88
|
+
position: sticky;
|
|
89
|
+
top: 0;
|
|
90
|
+
z-index: 2;
|
|
91
|
+
padding: 0;
|
|
92
|
+
font-weight: 600;
|
|
93
|
+
white-space: nowrap;
|
|
94
|
+
background-color: var(--cds-layer-accent-01, #e0e0e0);
|
|
95
|
+
cursor: grab;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
tbody tr:nth-of-type(even) td {
|
|
99
|
+
background-color: var(--cds-layer-01, #f4f4f4);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
tbody tr:hover td {
|
|
103
|
+
background-color: $brand-primary-10;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* The filter row sits under the headings and sticks with them */
|
|
108
|
+
.filterRow th {
|
|
109
|
+
top: 2rem;
|
|
110
|
+
padding: 0.25rem 0.375rem;
|
|
111
|
+
cursor: default;
|
|
112
|
+
background-color: var(--cds-layer-01, #f4f4f4);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.filterInput {
|
|
116
|
+
width: 100%;
|
|
117
|
+
min-width: 5rem;
|
|
118
|
+
height: 1.5rem;
|
|
119
|
+
padding: 0 0.375rem;
|
|
120
|
+
font: inherit;
|
|
121
|
+
font-size: 0.75rem;
|
|
122
|
+
font-weight: 400;
|
|
123
|
+
background-color: var(--cds-field-01, #fff);
|
|
124
|
+
border: 1px solid var(--cds-border-strong-01, #8d8d8d);
|
|
125
|
+
border-width: 0 0 1px;
|
|
126
|
+
|
|
127
|
+
&:focus {
|
|
128
|
+
outline: 2px solid var(--cds-focus, #0f62fe);
|
|
129
|
+
outline-offset: -2px;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.sortButton {
|
|
134
|
+
display: flex;
|
|
135
|
+
width: 100%;
|
|
136
|
+
height: 2rem;
|
|
137
|
+
align-items: center;
|
|
138
|
+
justify-content: space-between;
|
|
139
|
+
gap: 0.5rem;
|
|
140
|
+
padding: 0 0.625rem;
|
|
141
|
+
font: inherit;
|
|
142
|
+
font-weight: 600;
|
|
143
|
+
color: var(--cds-text-primary, #161616);
|
|
144
|
+
text-align: left;
|
|
145
|
+
background: none;
|
|
146
|
+
border: none;
|
|
147
|
+
cursor: pointer;
|
|
148
|
+
|
|
149
|
+
&:hover {
|
|
150
|
+
background-color: var(--cds-layer-accent-hover-01, #d1d1d1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
&:hover .sortIdle {
|
|
154
|
+
opacity: 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
&:focus-visible {
|
|
158
|
+
outline: 2px solid var(--cds-focus, #0f62fe);
|
|
159
|
+
outline-offset: -2px;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.sortIdle {
|
|
164
|
+
flex-shrink: 0;
|
|
165
|
+
opacity: 0.3;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.sortActive {
|
|
169
|
+
flex-shrink: 0;
|
|
170
|
+
fill: var(--cds-link-primary, #0f62fe);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.dragging {
|
|
174
|
+
opacity: 0.4;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.dropTarget {
|
|
178
|
+
box-shadow: inset 3px 0 0 var(--cds-focus, #0f62fe);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* The running row number stays in view when scrolling sideways */
|
|
182
|
+
.table .rowNumber {
|
|
183
|
+
position: sticky;
|
|
184
|
+
left: 0;
|
|
185
|
+
z-index: 1;
|
|
186
|
+
width: 3rem;
|
|
187
|
+
padding: 0.3125rem 0.5rem;
|
|
188
|
+
color: var(--cds-text-secondary, #525252);
|
|
189
|
+
text-align: right;
|
|
190
|
+
font-variant-numeric: tabular-nums;
|
|
191
|
+
background-color: var(--cds-layer-accent-01, #e0e0e0);
|
|
192
|
+
border-right: 1px solid var(--linelist-grid);
|
|
193
|
+
cursor: default;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.table thead .rowNumber {
|
|
197
|
+
z-index: 3;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* In the body the row number takes its row's striping and hover like any other
|
|
201
|
+
cell; it only needs an opaque background so cells slide beneath it. */
|
|
202
|
+
.table tbody td.rowNumber {
|
|
203
|
+
background-color: var(--cds-background, #fff);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.table td.wrap {
|
|
207
|
+
min-width: 10rem;
|
|
208
|
+
white-space: normal;
|
|
209
|
+
overflow-wrap: anywhere;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.noMatches {
|
|
213
|
+
padding: 2rem 1rem !important;
|
|
214
|
+
color: var(--cds-text-secondary, #525252);
|
|
215
|
+
text-align: center !important;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.linkButton {
|
|
219
|
+
padding: 0;
|
|
220
|
+
font: inherit;
|
|
221
|
+
color: var(--cds-link-primary, #0f62fe);
|
|
222
|
+
text-decoration: underline;
|
|
223
|
+
background: none;
|
|
224
|
+
border: none;
|
|
225
|
+
cursor: pointer;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.pagination {
|
|
229
|
+
border-top: none;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.columnsPanel {
|
|
233
|
+
position: absolute;
|
|
234
|
+
z-index: 10;
|
|
235
|
+
width: min(20rem, 100%);
|
|
236
|
+
padding: 0.75rem;
|
|
237
|
+
background-color: var(--cds-layer-01, #f4f4f4);
|
|
238
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.columnsHint {
|
|
242
|
+
margin-bottom: 0.5rem;
|
|
243
|
+
font-size: 0.75rem;
|
|
244
|
+
color: var(--cds-text-secondary, #525252);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.columnsList {
|
|
248
|
+
max-height: 22rem;
|
|
249
|
+
overflow-y: auto;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.columnsItem {
|
|
253
|
+
display: flex;
|
|
254
|
+
align-items: center;
|
|
255
|
+
gap: 0.25rem;
|
|
256
|
+
min-height: 2rem;
|
|
257
|
+
|
|
258
|
+
:global(.cds--form-item) {
|
|
259
|
+
flex: 1;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.dragHandle {
|
|
264
|
+
flex-shrink: 0;
|
|
265
|
+
fill: var(--cds-icon-secondary, #525252);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.columnsMove {
|
|
269
|
+
display: flex;
|
|
270
|
+
flex-shrink: 0;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.columnsReset {
|
|
274
|
+
margin-top: 0.5rem;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/* Touch screens: headings and filter boxes get finger-sized targets. Rows stay
|
|
278
|
+
compact -- they are read, not tapped. Headings cannot be dragged on touch
|
|
279
|
+
(no native drag and drop), so reordering there is done from the Columns menu. */
|
|
280
|
+
@media (pointer: coarse) {
|
|
281
|
+
.sortButton {
|
|
282
|
+
height: 2.5rem;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.filterRow th {
|
|
286
|
+
top: 2.5rem;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.filterInput {
|
|
290
|
+
height: 2rem;
|
|
291
|
+
font-size: 1rem; /* below 16px, iOS zooms the page on focus */
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* Phones: the search takes a line of its own, and the table leaves room for the
|
|
296
|
+
toolbar above it. */
|
|
297
|
+
@media (max-width: 42rem) {
|
|
298
|
+
.search {
|
|
299
|
+
flex-basis: 100%;
|
|
300
|
+
max-width: none;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.scroll {
|
|
304
|
+
max-height: calc(100vh - 14rem);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, within } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
|
|
5
|
+
import LinelistTable from './linelist-table.component';
|
|
6
|
+
import { type LinelistColumn } from './linelist-columns';
|
|
7
|
+
import { downloadCsv } from '../report-register';
|
|
8
|
+
|
|
9
|
+
jest.mock('../report-register', () => ({
|
|
10
|
+
...jest.requireActual('../report-register'),
|
|
11
|
+
downloadCsv: jest.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const columns: LinelistColumn[] = [
|
|
15
|
+
{ header: 'Name', field: 'name' },
|
|
16
|
+
{ header: 'Sex', field: 'sex' },
|
|
17
|
+
{ header: 'Age', field: 'age' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const rows = [
|
|
21
|
+
{ name: 'Achieng', sex: 'F', age: 34 },
|
|
22
|
+
{ name: 'Kiprop', sex: 'M', age: 9 },
|
|
23
|
+
{ name: 'Wanjiru', sex: 'F', age: null },
|
|
24
|
+
{ name: 'Otieno', sex: 'M', age: 41 },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const renderTable = () =>
|
|
28
|
+
render(<LinelistTable rows={rows} columns={columns} layoutKey="test" exportName="Test linelist" />);
|
|
29
|
+
|
|
30
|
+
const headings = () =>
|
|
31
|
+
within(screen.getAllByRole('rowgroup')[0])
|
|
32
|
+
.getAllByRole('columnheader')
|
|
33
|
+
.map((cell) => cell.textContent);
|
|
34
|
+
|
|
35
|
+
const names = () =>
|
|
36
|
+
within(screen.getAllByRole('rowgroup')[1])
|
|
37
|
+
.getAllByRole('row')
|
|
38
|
+
.map((row) => within(row).getAllByRole('cell')[1].textContent);
|
|
39
|
+
|
|
40
|
+
describe('LinelistTable', () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
localStorage.clear();
|
|
43
|
+
jest.mocked(downloadCsv).mockClear();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('lists every row under the configured columns', () => {
|
|
47
|
+
renderTable();
|
|
48
|
+
|
|
49
|
+
expect(headings()).toEqual(['#', 'Name', 'Sex', 'Age']);
|
|
50
|
+
expect(names()).toEqual(['Achieng', 'Kiprop', 'Wanjiru', 'Otieno']);
|
|
51
|
+
expect(screen.getByText('4 records')).toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('searches across columns', async () => {
|
|
55
|
+
renderTable();
|
|
56
|
+
|
|
57
|
+
await userEvent.type(screen.getByRole('searchbox', { name: /search the linelist/i }), 'kip');
|
|
58
|
+
|
|
59
|
+
expect(names()).toEqual(['Kiprop']);
|
|
60
|
+
expect(screen.getByText('1 of 4 records')).toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('filters by a single column', async () => {
|
|
64
|
+
renderTable();
|
|
65
|
+
|
|
66
|
+
await userEvent.click(screen.getByRole('button', { name: /filters/i }));
|
|
67
|
+
await userEvent.type(screen.getByRole('searchbox', { name: 'Filter Sex' }), 'm');
|
|
68
|
+
|
|
69
|
+
expect(names()).toEqual(['Kiprop', 'Otieno']);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('sorts numerically, both ways, with blanks last', async () => {
|
|
73
|
+
renderTable();
|
|
74
|
+
const age = screen.getByRole('button', { name: /^age/i });
|
|
75
|
+
|
|
76
|
+
await userEvent.click(age);
|
|
77
|
+
expect(names()).toEqual(['Kiprop', 'Achieng', 'Otieno', 'Wanjiru']);
|
|
78
|
+
|
|
79
|
+
await userEvent.click(age);
|
|
80
|
+
expect(names()).toEqual(['Otieno', 'Achieng', 'Kiprop', 'Wanjiru']);
|
|
81
|
+
|
|
82
|
+
await userEvent.click(age);
|
|
83
|
+
expect(names()).toEqual(['Achieng', 'Kiprop', 'Wanjiru', 'Otieno']);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('hides and reorders columns, and remembers the layout', async () => {
|
|
87
|
+
const { unmount } = renderTable();
|
|
88
|
+
|
|
89
|
+
await userEvent.click(screen.getByRole('button', { name: /^columns/i }));
|
|
90
|
+
await userEvent.click(screen.getByRole('checkbox', { name: 'Sex' }));
|
|
91
|
+
// Age is last in the menu; two steps left puts it before Name
|
|
92
|
+
await userEvent.click(screen.getAllByRole('button', { name: 'Move left' })[2]);
|
|
93
|
+
await userEvent.click(screen.getAllByRole('button', { name: 'Move left' })[1]);
|
|
94
|
+
|
|
95
|
+
expect(headings()).toEqual(['#', 'Age', 'Name']);
|
|
96
|
+
|
|
97
|
+
unmount();
|
|
98
|
+
renderTable();
|
|
99
|
+
expect(headings()).toEqual(['#', 'Age', 'Name']);
|
|
100
|
+
|
|
101
|
+
await userEvent.click(screen.getByRole('button', { name: /^columns/i }));
|
|
102
|
+
await userEvent.click(screen.getByRole('button', { name: /reset columns/i }));
|
|
103
|
+
expect(headings()).toEqual(['#', 'Name', 'Sex', 'Age']);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('exports what is on screen: visible columns in order, filtered and sorted rows', async () => {
|
|
107
|
+
renderTable();
|
|
108
|
+
|
|
109
|
+
await userEvent.click(screen.getByRole('button', { name: /^columns/i }));
|
|
110
|
+
await userEvent.click(screen.getByRole('checkbox', { name: 'Sex' }));
|
|
111
|
+
await userEvent.type(screen.getByRole('searchbox', { name: /search the linelist/i }), 'o');
|
|
112
|
+
await userEvent.click(screen.getByRole('button', { name: /^age/i }));
|
|
113
|
+
await userEvent.click(screen.getByRole('button', { name: /export csv/i }));
|
|
114
|
+
|
|
115
|
+
const [csv, filename] = jest.mocked(downloadCsv).mock.calls[0];
|
|
116
|
+
expect(filename).toBe('test-linelist.csv');
|
|
117
|
+
// "o" matches Kiprop and Otieno; the hidden Sex column is neither searched nor exported
|
|
118
|
+
expect(csv.split('\r\n')).toEqual(['No,Name,Age', '1,Kiprop,9', '2,Otieno,41']);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { type PatientListPage } from '../../resources/patient-list-page';
|
|
4
|
+
import { showReportError } from '../report-error';
|
|
5
|
+
|
|
6
|
+
type Fetcher = (paging: { startIndex: number; limit: number }) => Promise<PatientListPage>;
|
|
7
|
+
|
|
8
|
+
const BATCH_SIZE = 500;
|
|
9
|
+
|
|
10
|
+
/* Figures of a total loaded at once, so a large total is not one long queue
|
|
11
|
+
yet the ETL server is not sent every figure's query together */
|
|
12
|
+
const PARALLEL_FIGURES = 4;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Every row of a linelist, fetched in batches.
|
|
16
|
+
*
|
|
17
|
+
* Sorting, filtering and export all have to see the whole list: sorting one
|
|
18
|
+
* server page of 50 would present the top of that page as the top of the list.
|
|
19
|
+
* A figure's linelist is one facility's clients for one month, so holding it
|
|
20
|
+
* in memory is cheap; only the page on screen is rendered.
|
|
21
|
+
*
|
|
22
|
+
* Given several fetchers -- a total made of separate figures -- they are read a
|
|
23
|
+
* few at a time, and their rows listed one figure after another.
|
|
24
|
+
*/
|
|
25
|
+
export function useAllRows(fetcher: Fetcher | Fetcher[], deps: React.DependencyList) {
|
|
26
|
+
const [rows, setRows] = useState<Array<Record<string, any>>>([]);
|
|
27
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
28
|
+
const [loadedCount, setLoadedCount] = useState(0);
|
|
29
|
+
|
|
30
|
+
/* A newer query supersedes one still loading, so its batches are discarded */
|
|
31
|
+
const request = useRef(0);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const id = ++request.current;
|
|
35
|
+
|
|
36
|
+
setRows([]);
|
|
37
|
+
setLoadedCount(0);
|
|
38
|
+
setIsLoading(true);
|
|
39
|
+
|
|
40
|
+
(async () => {
|
|
41
|
+
const fetchers = Array.isArray(fetcher) ? fetcher : [fetcher];
|
|
42
|
+
/* each figure's rows kept apart, so the list keeps the figures' order */
|
|
43
|
+
const byFigure: Array<Array<Record<string, any>>> = fetchers.map(() => []);
|
|
44
|
+
let loaded = 0;
|
|
45
|
+
let next = 0;
|
|
46
|
+
|
|
47
|
+
const readFigure = async (index: number) => {
|
|
48
|
+
for (let startIndex = 0; ; startIndex += BATCH_SIZE) {
|
|
49
|
+
const page = await fetchers[index]({ startIndex, limit: BATCH_SIZE });
|
|
50
|
+
|
|
51
|
+
if (id !== request.current) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
byFigure[index].push(...page.results);
|
|
56
|
+
loaded += page.results.length;
|
|
57
|
+
setLoadedCount(loaded);
|
|
58
|
+
|
|
59
|
+
if (page.results.length < BATCH_SIZE) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const worker = async () => {
|
|
66
|
+
while (next < fetchers.length && id === request.current) {
|
|
67
|
+
await readFigure(next++);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await Promise.all(Array.from({ length: Math.min(PARALLEL_FIGURES, fetchers.length) }, worker));
|
|
73
|
+
|
|
74
|
+
if (id !== request.current) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
setRows(byFigure.flat());
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (id === request.current) {
|
|
81
|
+
showReportError('the linelist', error);
|
|
82
|
+
}
|
|
83
|
+
} finally {
|
|
84
|
+
if (id === request.current) {
|
|
85
|
+
setIsLoading(false);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
})();
|
|
89
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
90
|
+
}, deps);
|
|
91
|
+
|
|
92
|
+
return { rows, isLoading, loadedCount };
|
|
93
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { type LinelistColumn } from './linelist-columns';
|
|
4
|
+
|
|
5
|
+
interface ColumnLayout {
|
|
6
|
+
/** Column headers in display order */
|
|
7
|
+
order: string[];
|
|
8
|
+
hidden: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const storageKeyFor = (key: string) => `ampath-linelist-layout:${key}`;
|
|
12
|
+
|
|
13
|
+
function readLayout(key: string): ColumnLayout | null {
|
|
14
|
+
try {
|
|
15
|
+
const saved = JSON.parse(localStorage.getItem(storageKeyFor(key)) ?? 'null');
|
|
16
|
+
return saved && Array.isArray(saved.order) && Array.isArray(saved.hidden) ? saved : null;
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeLayout(key: string, layout: ColumnLayout | null) {
|
|
23
|
+
try {
|
|
24
|
+
if (layout) {
|
|
25
|
+
localStorage.setItem(storageKeyFor(key), JSON.stringify(layout));
|
|
26
|
+
} else {
|
|
27
|
+
localStorage.removeItem(storageKeyFor(key));
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
/* storage unavailable (private window, blocked site data): the layout just isn't remembered */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const defaultLayout = (columns: LinelistColumn[]): ColumnLayout => ({
|
|
35
|
+
order: columns.map((column) => column.header),
|
|
36
|
+
hidden: columns.filter((column) => column.hidden).map((column) => column.header),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The order and visibility of a linelist's columns, remembered per linelist in
|
|
41
|
+
* this browser.
|
|
42
|
+
*
|
|
43
|
+
* The saved layout is reconciled with the configured columns every time, so a
|
|
44
|
+
* column added to the configuration later still appears (at the end), and one
|
|
45
|
+
* removed from it is dropped from the layout rather than breaking it.
|
|
46
|
+
*/
|
|
47
|
+
export function useColumnLayout(columns: LinelistColumn[], storageKey: string) {
|
|
48
|
+
const [layout, setLayout] = useState<ColumnLayout>(() => readLayout(storageKey) ?? defaultLayout(columns));
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
setLayout(readLayout(storageKey) ?? defaultLayout(columns));
|
|
52
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
53
|
+
}, [storageKey]);
|
|
54
|
+
|
|
55
|
+
const byHeader = useMemo(() => new Map(columns.map((column) => [column.header, column])), [columns]);
|
|
56
|
+
|
|
57
|
+
/** Every configured column, in the user's order */
|
|
58
|
+
const ordered = useMemo(() => {
|
|
59
|
+
const known = layout.order.filter((header) => byHeader.has(header));
|
|
60
|
+
const added = columns.map((column) => column.header).filter((header) => !known.includes(header));
|
|
61
|
+
return [...known, ...added].map((header) => byHeader.get(header)!);
|
|
62
|
+
}, [layout.order, byHeader, columns]);
|
|
63
|
+
|
|
64
|
+
const isVisible = useCallback((header: string) => !layout.hidden.includes(header), [layout.hidden]);
|
|
65
|
+
const visible = useMemo(() => ordered.filter((column) => isVisible(column.header)), [ordered, isVisible]);
|
|
66
|
+
|
|
67
|
+
const update = useCallback(
|
|
68
|
+
(next: ColumnLayout) => {
|
|
69
|
+
setLayout(next);
|
|
70
|
+
writeLayout(storageKey, next);
|
|
71
|
+
},
|
|
72
|
+
[storageKey],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const toggle = useCallback(
|
|
76
|
+
(header: string) => {
|
|
77
|
+
const hidden = layout.hidden.includes(header)
|
|
78
|
+
? layout.hidden.filter((item) => item !== header)
|
|
79
|
+
: [...layout.hidden, header];
|
|
80
|
+
update({ order: ordered.map((column) => column.header), hidden });
|
|
81
|
+
},
|
|
82
|
+
[layout.hidden, ordered, update],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
/** Moves `header` to where `target` is, shifting the columns between them */
|
|
86
|
+
const move = useCallback(
|
|
87
|
+
(header: string, target: string) => {
|
|
88
|
+
const order = ordered.map((column) => column.header).filter((item) => item !== header);
|
|
89
|
+
const at = order.indexOf(target);
|
|
90
|
+
if (at === -1 || header === target) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const from = ordered.findIndex((column) => column.header === header);
|
|
94
|
+
const to = ordered.findIndex((column) => column.header === target);
|
|
95
|
+
order.splice(from < to ? at + 1 : at, 0, header);
|
|
96
|
+
update({ order, hidden: layout.hidden });
|
|
97
|
+
},
|
|
98
|
+
[ordered, layout.hidden, update],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
/** Moves a column one place left (-1) or right (1) */
|
|
102
|
+
const shift = useCallback(
|
|
103
|
+
(header: string, step: -1 | 1) => {
|
|
104
|
+
const index = ordered.findIndex((column) => column.header === header);
|
|
105
|
+
const target = ordered[index + step];
|
|
106
|
+
if (target) {
|
|
107
|
+
move(header, target.header);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
[ordered, move],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const reset = useCallback(() => {
|
|
114
|
+
writeLayout(storageKey, null);
|
|
115
|
+
setLayout(defaultLayout(columns));
|
|
116
|
+
}, [storageKey, columns]);
|
|
117
|
+
|
|
118
|
+
const isCustomised =
|
|
119
|
+
JSON.stringify({ order: ordered.map((column) => column.header), hidden: layout.hidden }) !==
|
|
120
|
+
JSON.stringify(defaultLayout(columns));
|
|
121
|
+
|
|
122
|
+
return { ordered, visible, isVisible, toggle, move, shift, reset, isCustomised };
|
|
123
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './show-report-error';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { showSnackbar } from '@openmrs/esm-framework';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The single way a report reports a failure.
|
|
5
|
+
*
|
|
6
|
+
* Reports previously each rendered their own error markup driven by local
|
|
7
|
+
* state, which is easy to get wrong and easy to leave out. A snackbar keeps the
|
|
8
|
+
* report body showing whatever was last generated instead of replacing it, and
|
|
9
|
+
* behaves the same whether the failure came from the fetch, the ETL
|
|
10
|
+
* configuration or a malformed response.
|
|
11
|
+
*/
|
|
12
|
+
export function showReportError(reportName: string, error: unknown): void {
|
|
13
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
14
|
+
|
|
15
|
+
// still worth a console entry: the snackbar is transient and truncates
|
|
16
|
+
console.error(`Failed to generate ${reportName}`, error);
|
|
17
|
+
|
|
18
|
+
showSnackbar({
|
|
19
|
+
kind: 'error',
|
|
20
|
+
title: `Could not generate ${reportName}`,
|
|
21
|
+
subtitle: message,
|
|
22
|
+
isLowContrast: false,
|
|
23
|
+
autoClose: false,
|
|
24
|
+
});
|
|
25
|
+
}
|