@cdc/core 4.24.9-1 → 4.24.10
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/assets/icon-combo-chart.svg +1 -0
- package/assets/icon-epi-chart.svg +27 -0
- package/components/BlurStrokeText.tsx +44 -0
- package/components/DataTable/DataTable.tsx +51 -35
- package/components/DataTable/DataTableStandAlone.tsx +37 -6
- package/components/DataTable/components/ChartHeader.tsx +31 -26
- package/components/DataTable/components/MapHeader.tsx +19 -10
- package/components/DataTable/components/SortIcon/index.tsx +25 -0
- package/components/DataTable/components/SortIcon/sort-icon.css +21 -0
- package/{styles/_data-table.scss → components/DataTable/data-table.css} +268 -298
- package/components/DataTable/helpers/customSort.ts +11 -15
- package/components/DataTable/helpers/getDataSeriesColumns.ts +5 -1
- package/components/DataTable/helpers/getNewSortBy.ts +35 -0
- package/components/DataTable/helpers/tests/customSort.test.ts +52 -0
- package/components/DataTable/helpers/tests/getNewSortBy.test.ts +26 -0
- package/components/EditorPanel/DataTableEditor.tsx +132 -26
- package/components/EditorPanel/Inputs.tsx +42 -4
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +25 -7
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +1 -1
- package/components/{Filters.tsx → Filters/Filters.tsx} +48 -39
- package/components/Filters/helpers/applyQueuedActive.ts +12 -0
- package/components/Filters/helpers/getNestedOptions.ts +29 -0
- package/components/Filters/helpers/handleSorting.ts +18 -0
- package/components/Filters/helpers/tests/applyQueuedActive.test.ts +49 -0
- package/components/Filters/helpers/tests/getNestedOptions.test.ts +93 -0
- package/components/Filters/helpers/tests/handleSorting.test.ts +68 -0
- package/components/Filters/index.ts +5 -0
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +1 -3
- package/components/Legend/Legend.Gradient.tsx +2 -9
- package/components/Loader/Loader.tsx +33 -0
- package/components/Loader/index.ts +1 -0
- package/components/Loader/loader.styles.css +13 -0
- package/components/NestedDropdown/NestedDropdown.tsx +90 -48
- package/components/NestedDropdown/nestedDropdownHelpers.ts +34 -0
- package/components/NestedDropdown/nesteddropdown.styles.css +7 -0
- package/components/NestedDropdown/tests/nestedDropdownHelpers.test.ts +58 -0
- package/components/Table/components/GroupRow.tsx +1 -1
- package/components/_stories/BlurStrokeTest.stories.tsx +27 -0
- package/components/_stories/NestedDropdown.stories.tsx +22 -46
- package/components/_stories/_mocks/nested-dropdown.json +30 -0
- package/components/_stories/styles.scss +0 -1
- package/components/ui/{Tooltip.jsx → Tooltip.tsx} +38 -14
- package/data/colorPalettes.js +107 -10
- package/dist/cove-main.css +6114 -0
- package/dist/cove-main.css.map +1 -0
- package/helpers/addValuesToFilters.ts +8 -3
- package/helpers/cove/number.js +46 -25
- package/helpers/coveUpdateWorker.ts +6 -7
- package/helpers/pivotData.ts +52 -11
- package/helpers/tests/gatherQueryParams.test.ts +13 -1
- package/helpers/tests/pivotData.test.ts +50 -0
- package/helpers/ver/4.24.10.ts +47 -0
- package/helpers/ver/4.24.9.ts +0 -3
- package/helpers/ver/tests/4.24.10.test.ts +45 -0
- package/helpers/viewports.ts +9 -0
- package/package.json +7 -3
- package/styles/_button-section.scss +4 -0
- package/styles/_global-variables.scss +19 -1
- package/styles/_global.scss +1 -8
- package/styles/_reset.scss +2 -15
- package/styles/base.scss +0 -1
- package/styles/cove-main.scss +6 -0
- package/styles/filters.scss +6 -4
- package/styles/v2/components/ui/tooltip.scss +42 -40
- package/styles/v2/layout/_component.scss +0 -6
- package/styles/v2/layout/index.scss +0 -1
- package/types/Axis.ts +2 -0
- package/types/General.ts +1 -0
- package/types/Table.ts +2 -1
- package/types/Visualization.ts +13 -1
- package/types/VizFilter.ts +2 -1
- package/components/DataTable/components/Icons.tsx +0 -10
- package/components/_stories/EditorPanel.stories.tsx +0 -54
- package/components/_stories/Layout.Debug.stories.tsx +0 -91
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { customSort } from '../customSort'
|
|
3
|
+
|
|
4
|
+
describe('customSort()', () => {
|
|
5
|
+
it('should return positive number when a > b', () => {
|
|
6
|
+
const a = 3
|
|
7
|
+
const b = 1
|
|
8
|
+
const sortBy = { column: 'someColumn', asc: true, colIndex: 0 }
|
|
9
|
+
const config = { type: 'map' }
|
|
10
|
+
expect(customSort(a, b, sortBy, config)).greaterThan(0)
|
|
11
|
+
expect(customSort(a, b, sortBy, { type: 'notMap' })).greaterThan(0)
|
|
12
|
+
})
|
|
13
|
+
it('should return negative number when a < b', () => {
|
|
14
|
+
const a = 1
|
|
15
|
+
const b = 3
|
|
16
|
+
const sortBy = { column: 'someColumn', asc: true, colIndex: 0 }
|
|
17
|
+
const config = { type: 'map' }
|
|
18
|
+
expect(customSort(a, b, sortBy, config)).lessThan(0)
|
|
19
|
+
expect(customSort(a, b, sortBy, { type: 'notMap' })).lessThan(0)
|
|
20
|
+
})
|
|
21
|
+
it('works for dates', () => {
|
|
22
|
+
const a = 2000
|
|
23
|
+
const b = 1999
|
|
24
|
+
const sortBy = { column: 'someColumn', asc: true, colIndex: 0 }
|
|
25
|
+
expect(
|
|
26
|
+
customSort(a, b, sortBy, { xAxis: { dataKey: sortBy.column, dateParseFormat: '%Y', type: 'date' } })
|
|
27
|
+
).greaterThan(0)
|
|
28
|
+
expect(
|
|
29
|
+
customSort(b, a, sortBy, { xAxis: { dataKey: sortBy.column, dateParseFormat: '%Y', type: 'date' } })
|
|
30
|
+
).lessThan(0)
|
|
31
|
+
})
|
|
32
|
+
it('works for strings', () => {
|
|
33
|
+
const a = 'banana'
|
|
34
|
+
const b = 'apple'
|
|
35
|
+
const sortBy = { column: 'someColumn', asc: true, colIndex: 0 }
|
|
36
|
+
const config = { type: 'map' }
|
|
37
|
+
expect(customSort(a, b, sortBy, config)).greaterThan(0)
|
|
38
|
+
expect(customSort(a, b, sortBy, { type: 'notMap' })).greaterThan(0)
|
|
39
|
+
expect(customSort(b, a, sortBy, config)).lessThan(0)
|
|
40
|
+
expect(customSort(b, a, sortBy, { type: 'notMap' })).lessThan(0)
|
|
41
|
+
})
|
|
42
|
+
it('works for strings after number', () => {
|
|
43
|
+
const a = 'banana'
|
|
44
|
+
const b = '1'
|
|
45
|
+
const sortBy = { column: 'someColumn', asc: true, colIndex: 0 }
|
|
46
|
+
const config = { type: 'map' }
|
|
47
|
+
expect(customSort(a, b, sortBy, config)).greaterThan(0)
|
|
48
|
+
expect(customSort(a, b, sortBy, { type: 'notMap' })).greaterThan(0)
|
|
49
|
+
expect(customSort(b, a, sortBy, config)).lessThan(0)
|
|
50
|
+
expect(customSort(b, a, sortBy, { type: 'notMap' })).lessThan(0)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getNewSortBy } from '../getNewSortBy'
|
|
3
|
+
|
|
4
|
+
describe('getNewSortBy()', () => {
|
|
5
|
+
it('should return ascending when currently undefined', () => {
|
|
6
|
+
const sortBy = { column: undefined, asc: undefined, colIndex: 0 }
|
|
7
|
+
const column = 'someColumn'
|
|
8
|
+
const index = 1
|
|
9
|
+
const result = getNewSortBy(sortBy, column, index)
|
|
10
|
+
expect(result).toEqual({ column, asc: true, colIndex: index })
|
|
11
|
+
})
|
|
12
|
+
it('should return ascending false when currently true', () => {
|
|
13
|
+
const sortBy = { column: 'someColumn', asc: true, colIndex: 0 }
|
|
14
|
+
const column = 'someColumn'
|
|
15
|
+
const index = 1
|
|
16
|
+
const result = getNewSortBy(sortBy, column, index)
|
|
17
|
+
expect(result).toEqual({ column, asc: false, colIndex: index })
|
|
18
|
+
})
|
|
19
|
+
it('should return ascending undefined when currently false', () => {
|
|
20
|
+
const sortBy = { column: 'someColumn', asc: false, colIndex: 0 }
|
|
21
|
+
const column = 'someColumn'
|
|
22
|
+
const index = 1
|
|
23
|
+
const result = getNewSortBy(sortBy, column, index)
|
|
24
|
+
expect(result).toEqual({ column: undefined, asc: undefined, colIndex: index })
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -41,6 +41,7 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
|
|
|
41
41
|
|
|
42
42
|
const excludeColumns = (section, subSection, fieldName, excludedColNames: string[]) => {
|
|
43
43
|
const newColumns = _.cloneDeep(config.columns)
|
|
44
|
+
|
|
44
45
|
const colNames: string[] = []
|
|
45
46
|
for (let colKey in newColumns) {
|
|
46
47
|
const col = newColumns[colKey]
|
|
@@ -48,6 +49,8 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
|
|
|
48
49
|
if (excludedColNames.includes(col.name)) {
|
|
49
50
|
// ensure all excluded columns are set to false
|
|
50
51
|
newColumns[colKey].dataTable = false
|
|
52
|
+
} else {
|
|
53
|
+
newColumns[colKey].dataTable = true
|
|
51
54
|
}
|
|
52
55
|
}
|
|
53
56
|
excludedColNames.forEach(colName => {
|
|
@@ -91,10 +94,16 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
|
|
|
91
94
|
tooltip={
|
|
92
95
|
<Tooltip style={{ textTransform: 'none' }}>
|
|
93
96
|
<Tooltip.Target>
|
|
94
|
-
<Icon
|
|
97
|
+
<Icon
|
|
98
|
+
display='question'
|
|
99
|
+
style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }}
|
|
100
|
+
/>
|
|
95
101
|
</Tooltip.Target>
|
|
96
102
|
<Tooltip.Content>
|
|
97
|
-
<p>
|
|
103
|
+
<p>
|
|
104
|
+
Hiding the data table may affect accessibility. An alternate form of accessing visualization data is a
|
|
105
|
+
508 requirement.
|
|
106
|
+
</p>
|
|
98
107
|
</Tooltip.Content>
|
|
99
108
|
</Tooltip>
|
|
100
109
|
}
|
|
@@ -112,7 +121,10 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
|
|
|
112
121
|
tooltip={
|
|
113
122
|
<Tooltip style={{ textTransform: 'none' }}>
|
|
114
123
|
<Tooltip.Target>
|
|
115
|
-
<Icon
|
|
124
|
+
<Icon
|
|
125
|
+
display='question'
|
|
126
|
+
style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }}
|
|
127
|
+
/>
|
|
116
128
|
</Tooltip.Target>
|
|
117
129
|
<Tooltip.Content>
|
|
118
130
|
<p>This will draw the data table with vertical data instead of horizontal.</p>
|
|
@@ -135,7 +147,10 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
|
|
|
135
147
|
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
136
148
|
</Tooltip.Target>
|
|
137
149
|
<Tooltip.Content>
|
|
138
|
-
<p>
|
|
150
|
+
<p>
|
|
151
|
+
To comply with 508 standards, if the first column in the data table has no header, enter a brief one
|
|
152
|
+
here.
|
|
153
|
+
</p>
|
|
139
154
|
</Tooltip.Content>
|
|
140
155
|
</Tooltip>
|
|
141
156
|
}
|
|
@@ -160,17 +175,96 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
|
|
|
160
175
|
</Tooltip>
|
|
161
176
|
}
|
|
162
177
|
/>
|
|
163
|
-
<CheckBox
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
{config.
|
|
178
|
+
<CheckBox
|
|
179
|
+
value={config.table.limitHeight}
|
|
180
|
+
section='table'
|
|
181
|
+
fieldName='limitHeight'
|
|
182
|
+
label=' Limit Table Height'
|
|
183
|
+
updateField={updateField}
|
|
184
|
+
/>
|
|
185
|
+
{config.table.limitHeight && (
|
|
186
|
+
<TextField
|
|
187
|
+
value={config.table.height}
|
|
188
|
+
section='table'
|
|
189
|
+
fieldName='height'
|
|
190
|
+
label='Data Table Height'
|
|
191
|
+
type='number'
|
|
192
|
+
min={0}
|
|
193
|
+
max={500}
|
|
194
|
+
placeholder='Height(px)'
|
|
195
|
+
updateField={updateField}
|
|
196
|
+
/>
|
|
197
|
+
)}
|
|
198
|
+
{config?.visualizationType !== 'Sankey' && (
|
|
199
|
+
<MultiSelect
|
|
200
|
+
key={excludedColumns.join('') + 'excluded'}
|
|
201
|
+
options={dataColumns.map(c => ({ label: c, value: c }))}
|
|
202
|
+
selected={excludedColumns}
|
|
203
|
+
fieldName='dataTable'
|
|
204
|
+
label='Exclude Columns'
|
|
205
|
+
section='columns'
|
|
206
|
+
updateField={excludeColumns}
|
|
207
|
+
/>
|
|
208
|
+
)}
|
|
209
|
+
<CheckBox
|
|
210
|
+
value={config.table.collapsible}
|
|
211
|
+
fieldName='collapsible'
|
|
212
|
+
label=' Collapsible'
|
|
213
|
+
section='table'
|
|
214
|
+
updateField={updateField}
|
|
215
|
+
/>
|
|
216
|
+
{config.table.collapsible !== false && (
|
|
217
|
+
<CheckBox
|
|
218
|
+
value={config.table.expanded}
|
|
219
|
+
fieldName='expanded'
|
|
220
|
+
label=' Expanded by Default'
|
|
221
|
+
section='table'
|
|
222
|
+
updateField={updateField}
|
|
223
|
+
/>
|
|
224
|
+
)}
|
|
225
|
+
{isDashboard && config.type !== 'table' && (
|
|
226
|
+
<CheckBox
|
|
227
|
+
value={config.table.showDataTableLink}
|
|
228
|
+
fieldName='showDataTableLink'
|
|
229
|
+
label='Show Data Table Name & Link'
|
|
230
|
+
section='table'
|
|
231
|
+
updateField={updateField}
|
|
232
|
+
/>
|
|
233
|
+
)}
|
|
234
|
+
{isLoadedFromUrl && (
|
|
235
|
+
<CheckBox
|
|
236
|
+
value={config.table.showDownloadUrl}
|
|
237
|
+
fieldName='showDownloadUrl'
|
|
238
|
+
label='Show URL to Automatically Updated Data'
|
|
239
|
+
section='table'
|
|
240
|
+
updateField={updateField}
|
|
241
|
+
/>
|
|
242
|
+
)}
|
|
243
|
+
{config.type !== 'table' && (
|
|
244
|
+
<CheckBox
|
|
245
|
+
value={config.table.showDownloadImgButton}
|
|
246
|
+
fieldName='showDownloadImgButton'
|
|
247
|
+
label='Display Image Button'
|
|
248
|
+
section='table'
|
|
249
|
+
updateField={updateField}
|
|
250
|
+
/>
|
|
251
|
+
)}
|
|
252
|
+
{config.type !== 'table' && (
|
|
253
|
+
<CheckBox
|
|
254
|
+
value={config.table.showDownloadLinkBelow}
|
|
255
|
+
fieldName='showDownloadLinkBelow'
|
|
256
|
+
label='Show Download Link Below Table'
|
|
257
|
+
section='table'
|
|
258
|
+
updateField={updateField}
|
|
259
|
+
/>
|
|
260
|
+
)}
|
|
171
261
|
<label>
|
|
172
262
|
<span className='edit-label column-heading'>Table Cell Min Width</span>
|
|
173
|
-
<input
|
|
263
|
+
<input
|
|
264
|
+
type='number'
|
|
265
|
+
value={config.table.cellMinWidth ? config.table.cellMinWidth : 0}
|
|
266
|
+
onChange={e => updateField('table', null, 'cellMinWidth', e.target.value)}
|
|
267
|
+
/>
|
|
174
268
|
</label>
|
|
175
269
|
{config?.visualizationType !== 'Sankey' && (
|
|
176
270
|
<label>
|
|
@@ -181,7 +275,10 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
|
|
|
181
275
|
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
182
276
|
</Tooltip.Target>
|
|
183
277
|
<Tooltip.Content>
|
|
184
|
-
<p>
|
|
278
|
+
<p>
|
|
279
|
+
Choose a column to use for grouping data rows. The selected column will not be shown in the data
|
|
280
|
+
table. You will only be able to choose a column which does not have a column configuration.
|
|
281
|
+
</p>
|
|
185
282
|
</Tooltip.Content>
|
|
186
283
|
</Tooltip>
|
|
187
284
|
</span>
|
|
@@ -192,7 +289,12 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
|
|
|
192
289
|
changeGroupBy(event.target.value)
|
|
193
290
|
}}
|
|
194
291
|
>
|
|
195
|
-
{[
|
|
292
|
+
{[
|
|
293
|
+
PLACEHOLDER,
|
|
294
|
+
...groupPivotColumns.filter(
|
|
295
|
+
col => col !== config.table.pivot?.columnName && col !== config.table.pivot?.valueColumn
|
|
296
|
+
)
|
|
297
|
+
].map(option => (
|
|
196
298
|
<option key={option}>{option}</option>
|
|
197
299
|
))}
|
|
198
300
|
</select>
|
|
@@ -211,7 +313,9 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
|
|
|
211
313
|
</Tooltip>
|
|
212
314
|
}
|
|
213
315
|
value={config.table.pivot?.columnName}
|
|
214
|
-
options={groupPivotColumns.filter(
|
|
316
|
+
options={groupPivotColumns.filter(
|
|
317
|
+
col => col !== config.table.groupBy && col !== config.table.pivot?.valueColumn
|
|
318
|
+
)}
|
|
215
319
|
initial='-Select-'
|
|
216
320
|
section='table'
|
|
217
321
|
subsection='pivot'
|
|
@@ -219,25 +323,27 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
|
|
|
219
323
|
updateField={updateField}
|
|
220
324
|
/>
|
|
221
325
|
{config.table.pivot?.columnName && (
|
|
222
|
-
<
|
|
223
|
-
|
|
326
|
+
<MultiSelect
|
|
327
|
+
key={config.table.pivot?.columnName}
|
|
328
|
+
options={groupPivotColumns
|
|
329
|
+
.filter(col => col !== config.table.pivot?.columnName && col !== config.table.groupBy)
|
|
330
|
+
.map(c => ({ label: c, value: c }))}
|
|
331
|
+
selected={config.table.pivot?.valueColumns}
|
|
332
|
+
label='Pivot Value Column(s) '
|
|
333
|
+
section='table'
|
|
334
|
+
subsection='pivot'
|
|
335
|
+
fieldName='valueColumns'
|
|
336
|
+
updateField={updateField}
|
|
224
337
|
tooltip={
|
|
225
338
|
<Tooltip style={{ textTransform: 'none' }}>
|
|
226
339
|
<Tooltip.Target>
|
|
227
340
|
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
228
341
|
</Tooltip.Target>
|
|
229
342
|
<Tooltip.Content>
|
|
230
|
-
<p>The column whos values will be pivoted under the column selected as the Filter.</p>
|
|
343
|
+
<p>The column(s) whos values will be pivoted under the column selected as the Filter.</p>
|
|
231
344
|
</Tooltip.Content>
|
|
232
345
|
</Tooltip>
|
|
233
346
|
}
|
|
234
|
-
value={config.table.pivot?.valueColumn}
|
|
235
|
-
initial='-Select-'
|
|
236
|
-
section='table'
|
|
237
|
-
options={groupPivotColumns.filter(col => col !== config.table.pivot?.columnName && col !== config.table.groupBy)}
|
|
238
|
-
subsection='pivot'
|
|
239
|
-
fieldName='valueColumn'
|
|
240
|
-
updateField={updateField}
|
|
241
347
|
/>
|
|
242
348
|
)}
|
|
243
349
|
</>
|
|
@@ -17,6 +17,7 @@ export type TextFieldProps = {
|
|
|
17
17
|
value: string | number
|
|
18
18
|
type?: 'text' | 'number' | 'textarea' | 'date'
|
|
19
19
|
min?: number
|
|
20
|
+
maxLength?: number
|
|
20
21
|
max?: number
|
|
21
22
|
i?: number
|
|
22
23
|
id?: string
|
|
@@ -27,7 +28,8 @@ export type CheckboxProps = {
|
|
|
27
28
|
min?: number
|
|
28
29
|
i?: number
|
|
29
30
|
className?: string
|
|
30
|
-
} & Input
|
|
31
|
+
} & Input &
|
|
32
|
+
Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value'>
|
|
31
33
|
|
|
32
34
|
export type SelectProps = {
|
|
33
35
|
value?: string
|
|
@@ -40,7 +42,20 @@ export type SelectProps = {
|
|
|
40
42
|
} & Input
|
|
41
43
|
|
|
42
44
|
const TextField = memo((props: TextFieldProps) => {
|
|
43
|
-
const {
|
|
45
|
+
const {
|
|
46
|
+
display = true,
|
|
47
|
+
label,
|
|
48
|
+
tooltip,
|
|
49
|
+
section = null,
|
|
50
|
+
subsection = null,
|
|
51
|
+
fieldName,
|
|
52
|
+
updateField,
|
|
53
|
+
value: stateValue,
|
|
54
|
+
type = 'text',
|
|
55
|
+
i = null,
|
|
56
|
+
min = null,
|
|
57
|
+
...attributes
|
|
58
|
+
} = props
|
|
44
59
|
const [value, setValue] = useState(stateValue)
|
|
45
60
|
const [debouncedValue] = useDebounce(value, 500)
|
|
46
61
|
|
|
@@ -93,7 +108,17 @@ const TextField = memo((props: TextFieldProps) => {
|
|
|
93
108
|
})
|
|
94
109
|
|
|
95
110
|
const CheckBox = memo((props: CheckboxProps) => {
|
|
96
|
-
const {
|
|
111
|
+
const {
|
|
112
|
+
display = true,
|
|
113
|
+
label,
|
|
114
|
+
value,
|
|
115
|
+
fieldName,
|
|
116
|
+
section = null,
|
|
117
|
+
subsection = null,
|
|
118
|
+
tooltip,
|
|
119
|
+
updateField,
|
|
120
|
+
...attributes
|
|
121
|
+
} = props
|
|
97
122
|
if (!display) {
|
|
98
123
|
return <></>
|
|
99
124
|
}
|
|
@@ -117,7 +142,20 @@ const CheckBox = memo((props: CheckboxProps) => {
|
|
|
117
142
|
})
|
|
118
143
|
|
|
119
144
|
const Select = memo((props: SelectProps) => {
|
|
120
|
-
const {
|
|
145
|
+
const {
|
|
146
|
+
display = true,
|
|
147
|
+
label,
|
|
148
|
+
value,
|
|
149
|
+
options,
|
|
150
|
+
fieldName,
|
|
151
|
+
section = null,
|
|
152
|
+
subsection = null,
|
|
153
|
+
required = false,
|
|
154
|
+
tooltip,
|
|
155
|
+
updateField,
|
|
156
|
+
initial: initialValue,
|
|
157
|
+
...attributes
|
|
158
|
+
} = props
|
|
121
159
|
let optionsJsx = options.map((optionName, index) => (
|
|
122
160
|
<option value={optionName} key={index}>
|
|
123
161
|
{optionName}
|
|
@@ -3,6 +3,7 @@ import { SubGrouping, VizFilter, OrderBy } from '../../../types/VizFilter'
|
|
|
3
3
|
import { filterOrderOptions, handleSorting } from '../../Filters'
|
|
4
4
|
import FilterOrder from './components/FilterOrder'
|
|
5
5
|
import { Visualization } from '../../../types/Visualization'
|
|
6
|
+
import { useMemo } from 'react'
|
|
6
7
|
|
|
7
8
|
type NestedDropdownEditorProps = {
|
|
8
9
|
config: Visualization
|
|
@@ -121,6 +122,25 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
|
|
|
121
122
|
|
|
122
123
|
const columnNameOptions = dataColumns.filter(columnName => !listOfUsedColumnNames.includes(columnName))
|
|
123
124
|
|
|
125
|
+
const useParameters = useMemo(() => {
|
|
126
|
+
const filter = config.filters[filterIndex]
|
|
127
|
+
return !!(filter.setByQueryParameter && filter.subGrouping?.setByQueryParameter)
|
|
128
|
+
}, [config, filterIndex])
|
|
129
|
+
|
|
130
|
+
const handleParametersCheckboxClick = e => {
|
|
131
|
+
const updatedFilters = config.filters
|
|
132
|
+
const { checked } = e.target
|
|
133
|
+
const groupColumnName = checked ? filter.columnName : ''
|
|
134
|
+
const subGroupColumnName = checked ? subGrouping.columnName : ''
|
|
135
|
+
updatedFilters[filterIndex] = {
|
|
136
|
+
...config.filters[filterIndex],
|
|
137
|
+
setByQueryParameter: groupColumnName,
|
|
138
|
+
subGrouping: { ...subGrouping, setByQueryParameter: subGroupColumnName }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
updateField(null, null, 'filters', updatedFilters)
|
|
142
|
+
}
|
|
143
|
+
|
|
124
144
|
return (
|
|
125
145
|
<div className='nesteddropdown-editor'>
|
|
126
146
|
<label>
|
|
@@ -175,15 +195,13 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
|
|
|
175
195
|
<label>
|
|
176
196
|
<input
|
|
177
197
|
type='checkbox'
|
|
178
|
-
checked={
|
|
198
|
+
checked={useParameters}
|
|
179
199
|
aria-label='Create query parameters'
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
updateSubGroupingFilterProperty({ ...subGrouping, setByQueryParameter: subGrouping.columnName })
|
|
183
|
-
}}
|
|
200
|
+
disabled={!filter.columnName || !subGrouping?.columnName}
|
|
201
|
+
onChange={e => handleParametersCheckboxClick(e)}
|
|
184
202
|
/>
|
|
185
203
|
<span> Create query parameters</span>
|
|
186
|
-
{
|
|
204
|
+
{useParameters && (
|
|
187
205
|
<>
|
|
188
206
|
<span className='edit-label column-heading mt-2'>
|
|
189
207
|
Grouping: Default Value Set By Query String Parameter
|
|
@@ -200,7 +218,7 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
|
|
|
200
218
|
</span>
|
|
201
219
|
<input
|
|
202
220
|
type='text'
|
|
203
|
-
value={subGrouping
|
|
221
|
+
value={subGrouping?.setByQueryParameter}
|
|
204
222
|
onChange={e => {
|
|
205
223
|
const setByQueryParameter = e.target.value
|
|
206
224
|
updateSubGroupingFilterProperty({ ...subGrouping, setByQueryParameter })
|
|
@@ -144,7 +144,7 @@ const VizFilterEditor: React.FC<VizFilterProps> = ({ config, updateField, rawDat
|
|
|
144
144
|
<span className='edit-label column-heading'>Filter Style</span>
|
|
145
145
|
|
|
146
146
|
<select
|
|
147
|
-
value={filter.filterStyle}
|
|
147
|
+
value={filter.filterStyle || 'dropdown'}
|
|
148
148
|
onChange={e => {
|
|
149
149
|
updateFilterStyle(filterIndex, e.target.value)
|
|
150
150
|
}}
|
|
@@ -2,18 +2,32 @@ import { useState, useEffect, useMemo } from 'react'
|
|
|
2
2
|
import { useId } from 'react'
|
|
3
3
|
|
|
4
4
|
// CDC
|
|
5
|
-
import Button from '
|
|
6
|
-
import { getQueryParams, updateQueryString } from '
|
|
7
|
-
import MultiSelect from '
|
|
8
|
-
import { Visualization } from '
|
|
9
|
-
import { MultiSelectFilter, OrderBy, VizFilter } from '
|
|
10
|
-
import { filterVizData } from '
|
|
11
|
-
import { addValuesToFilters } from '
|
|
12
|
-
import { DimensionsType } from '
|
|
13
|
-
import NestedDropdown from '
|
|
5
|
+
import Button from '../elements/Button'
|
|
6
|
+
import { getQueryParams, updateQueryString } from '../../helpers/queryStringUtils'
|
|
7
|
+
import MultiSelect from '../MultiSelect'
|
|
8
|
+
import { Visualization } from '../../types/Visualization'
|
|
9
|
+
import { MultiSelectFilter, OrderBy, VizFilter } from '../../types/VizFilter'
|
|
10
|
+
import { filterVizData } from '../../helpers/filterVizData'
|
|
11
|
+
import { addValuesToFilters } from '../../helpers/addValuesToFilters'
|
|
12
|
+
import { DimensionsType } from '../../types/Dimensions'
|
|
13
|
+
import NestedDropdown from '../NestedDropdown'
|
|
14
14
|
import _ from 'lodash'
|
|
15
|
+
import { getNestedOptions } from './helpers/getNestedOptions'
|
|
16
|
+
import { applyQueuedActive } from './helpers/applyQueuedActive'
|
|
17
|
+
import { handleSorting } from './helpers/handleSorting'
|
|
15
18
|
|
|
16
|
-
export const
|
|
19
|
+
export const VIZ_FILTER_STYLE = {
|
|
20
|
+
dropdown: 'dropdown',
|
|
21
|
+
nestedDropdown: 'nested-dropdown',
|
|
22
|
+
pill: 'pill',
|
|
23
|
+
tab: 'tab',
|
|
24
|
+
tabBar: 'tab bar',
|
|
25
|
+
multiSelect: 'multi-select'
|
|
26
|
+
} as const
|
|
27
|
+
|
|
28
|
+
export type VizFilterStyle = (typeof VIZ_FILTER_STYLE)[keyof typeof VIZ_FILTER_STYLE]
|
|
29
|
+
|
|
30
|
+
export const filterStyleOptions = Object.values(VIZ_FILTER_STYLE)
|
|
17
31
|
|
|
18
32
|
export const filterOrderOptions: { label: string; value: OrderBy }[] = [
|
|
19
33
|
{
|
|
@@ -30,23 +44,6 @@ export const filterOrderOptions: { label: string; value: OrderBy }[] = [
|
|
|
30
44
|
}
|
|
31
45
|
]
|
|
32
46
|
|
|
33
|
-
export const handleSorting = singleFilter => {
|
|
34
|
-
const singleFilterValues = _.cloneDeep(singleFilter.values)
|
|
35
|
-
if (singleFilter.order === 'cust' && singleFilter.filterStyle !== 'nested-dropdown') {
|
|
36
|
-
singleFilter.values = singleFilter.orderedValues?.length ? singleFilter.orderedValues : singleFilterValues
|
|
37
|
-
return singleFilter
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const sort = (a, b) => {
|
|
41
|
-
const asc = singleFilter.order !== 'desc'
|
|
42
|
-
return (asc ? a : b).toString().localeCompare((asc ? b : a).toString(), 'en', { numeric: true })
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
singleFilter.values = singleFilterValues.sort(sort)
|
|
46
|
-
|
|
47
|
-
return singleFilter
|
|
48
|
-
}
|
|
49
|
-
|
|
50
47
|
const hasStandardFilterBehavior = ['chart', 'table']
|
|
51
48
|
|
|
52
49
|
export const useFilters = props => {
|
|
@@ -116,6 +113,14 @@ export const useFilters = props => {
|
|
|
116
113
|
queryParams[newFilter.setByQueryParameter] = newFilter.active
|
|
117
114
|
updateQueryString(queryParams)
|
|
118
115
|
}
|
|
116
|
+
if (
|
|
117
|
+
newFilter?.subGrouping?.setByQueryParameter &&
|
|
118
|
+
queryParams[newFilter?.subGrouping?.setByQueryParameter] !== newFilter?.subGrouping.active
|
|
119
|
+
) {
|
|
120
|
+
queryParams[newFilter?.subGrouping?.setByQueryParameter] = newFilter.subGrouping.active
|
|
121
|
+
updateQueryString(queryParams)
|
|
122
|
+
}
|
|
123
|
+
setFilteredData(newFilters[index])
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
if (!visualizationConfig.dynamicSeries) {
|
|
@@ -186,8 +191,7 @@ export const useFilters = props => {
|
|
|
186
191
|
const queryParams = getQueryParams()
|
|
187
192
|
newFilters.forEach(newFilter => {
|
|
188
193
|
if (newFilter.queuedActive) {
|
|
189
|
-
newFilter
|
|
190
|
-
delete newFilter.queuedActive
|
|
194
|
+
applyQueuedActive(newFilter)
|
|
191
195
|
if (newFilter.setByQueryParameter && queryParams[newFilter.setByQueryParameter] !== newFilter.active) {
|
|
192
196
|
queryParams[newFilter.setByQueryParameter] = newFilter.active
|
|
193
197
|
needsQueryUpdate = true
|
|
@@ -379,7 +383,6 @@ const Filters = (props: FilterProps) => {
|
|
|
379
383
|
const { active, queuedActive, label, filterStyle } = singleFilter as VizFilter
|
|
380
384
|
|
|
381
385
|
handleSorting(singleFilter)
|
|
382
|
-
|
|
383
386
|
singleFilter.values?.forEach((filterOption, index) => {
|
|
384
387
|
const pillClassList = ['pill', active === filterOption ? 'pill--active' : null, theme && theme]
|
|
385
388
|
const tabClassList = ['tab', active === filterOption && 'tab--active', theme && theme]
|
|
@@ -458,7 +461,9 @@ const Filters = (props: FilterProps) => {
|
|
|
458
461
|
)}
|
|
459
462
|
{filterStyle === 'nested-dropdown' && (
|
|
460
463
|
<NestedDropdown
|
|
461
|
-
|
|
464
|
+
activeGroup={(singleFilter.active as string) || (singleFilter.queuedActive || [])[0]}
|
|
465
|
+
activeSubGroup={(singleFilter.subGrouping?.active as string) || (singleFilter.queuedActive || [])[1]}
|
|
466
|
+
options={getNestedOptions(singleFilter)}
|
|
462
467
|
listLabel={label}
|
|
463
468
|
handleSelectedItems={value => changeFilterActive(outerIndex, value)}
|
|
464
469
|
/>
|
|
@@ -479,16 +484,20 @@ const Filters = (props: FilterProps) => {
|
|
|
479
484
|
}
|
|
480
485
|
|
|
481
486
|
if (visualizationConfig?.filters?.length === 0) return
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
type === 'map' ? general.headerColor :
|
|
487
|
-
|
|
487
|
+
|
|
488
|
+
const getClasses = () => {
|
|
489
|
+
const { visualizationType, legend } = visualizationConfig || {}
|
|
490
|
+
const baseClass = 'filters-section'
|
|
491
|
+
const conditionalClass = type === 'map' ? general.headerColor : visualizationType === 'Spark Line' ? null : theme
|
|
492
|
+
const legendClass = legend && !legend.hide && legend.position === 'top' ? 'mb-0' : null
|
|
493
|
+
|
|
494
|
+
return [baseClass, conditionalClass, legendClass].filter(Boolean)
|
|
495
|
+
}
|
|
496
|
+
|
|
488
497
|
return (
|
|
489
|
-
<section className={
|
|
498
|
+
<section className={getClasses().join(' ')}>
|
|
490
499
|
<p className='filters-section__intro-text'>
|
|
491
|
-
{filters?.some(
|
|
500
|
+
{filters?.some(filter => filter.active && filter.columnName) ? filterConstants.introText : ''}{' '}
|
|
492
501
|
{visualizationConfig.filterBehavior === 'Apply Button' && filterConstants.applyText}
|
|
493
502
|
</p>
|
|
494
503
|
<div className='filters-section__wrapper'>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { VIZ_FILTER_STYLE } from '../Filters'
|
|
2
|
+
import { SharedFilter } from '@cdc/dashboard/src/types/SharedFilter'
|
|
3
|
+
|
|
4
|
+
export const applyQueuedActive = (sharedFilter: SharedFilter) => {
|
|
5
|
+
if (sharedFilter.filterStyle === VIZ_FILTER_STYLE.nestedDropdown) {
|
|
6
|
+
sharedFilter.active = sharedFilter.queuedActive[0]
|
|
7
|
+
sharedFilter.subGrouping.active = sharedFilter.queuedActive[1]
|
|
8
|
+
} else {
|
|
9
|
+
sharedFilter.active = sharedFilter.queuedActive
|
|
10
|
+
}
|
|
11
|
+
delete sharedFilter.queuedActive
|
|
12
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SubGrouping } from '../../../types/VizFilter'
|
|
2
|
+
import { NestedOptions, ValueTextPair } from '../../NestedDropdown/nestedDropdownHelpers'
|
|
3
|
+
|
|
4
|
+
type GetOptionsMemoParams = {
|
|
5
|
+
orderedValues?: string[]
|
|
6
|
+
values: string[]
|
|
7
|
+
subGrouping: SubGrouping
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const getNestedOptions = ({ orderedValues, values, subGrouping }: GetOptionsMemoParams): NestedOptions => {
|
|
11
|
+
// keep custom ordered value order
|
|
12
|
+
const filteredValues = orderedValues?.length
|
|
13
|
+
? orderedValues.filter(orderedValue => values.includes(orderedValue))
|
|
14
|
+
: values
|
|
15
|
+
const v: NestedOptions = filteredValues.map<[ValueTextPair, ValueTextPair[]]>(value => {
|
|
16
|
+
if (!subGrouping) return [[value], []]
|
|
17
|
+
const { orderedValues, values: filteredSubValues } = subGrouping.valuesLookup[value]
|
|
18
|
+
// keep custom subFilter order
|
|
19
|
+
const subFilterValues =
|
|
20
|
+
orderedValues?.filter(orderedValue => filteredSubValues.includes(orderedValue)) || filteredSubValues
|
|
21
|
+
const structuredNestedDropdownData: [ValueTextPair, ValueTextPair[]] = [
|
|
22
|
+
[value],
|
|
23
|
+
subFilterValues.map(subValue => [subValue])
|
|
24
|
+
]
|
|
25
|
+
return structuredNestedDropdownData
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
return v
|
|
29
|
+
}
|