@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.
Files changed (74) hide show
  1. package/assets/icon-combo-chart.svg +1 -0
  2. package/assets/icon-epi-chart.svg +27 -0
  3. package/components/BlurStrokeText.tsx +44 -0
  4. package/components/DataTable/DataTable.tsx +51 -35
  5. package/components/DataTable/DataTableStandAlone.tsx +37 -6
  6. package/components/DataTable/components/ChartHeader.tsx +31 -26
  7. package/components/DataTable/components/MapHeader.tsx +19 -10
  8. package/components/DataTable/components/SortIcon/index.tsx +25 -0
  9. package/components/DataTable/components/SortIcon/sort-icon.css +21 -0
  10. package/{styles/_data-table.scss → components/DataTable/data-table.css} +268 -298
  11. package/components/DataTable/helpers/customSort.ts +11 -15
  12. package/components/DataTable/helpers/getDataSeriesColumns.ts +5 -1
  13. package/components/DataTable/helpers/getNewSortBy.ts +35 -0
  14. package/components/DataTable/helpers/tests/customSort.test.ts +52 -0
  15. package/components/DataTable/helpers/tests/getNewSortBy.test.ts +26 -0
  16. package/components/EditorPanel/DataTableEditor.tsx +132 -26
  17. package/components/EditorPanel/Inputs.tsx +42 -4
  18. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +25 -7
  19. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +1 -1
  20. package/components/{Filters.tsx → Filters/Filters.tsx} +48 -39
  21. package/components/Filters/helpers/applyQueuedActive.ts +12 -0
  22. package/components/Filters/helpers/getNestedOptions.ts +29 -0
  23. package/components/Filters/helpers/handleSorting.ts +18 -0
  24. package/components/Filters/helpers/tests/applyQueuedActive.test.ts +49 -0
  25. package/components/Filters/helpers/tests/getNestedOptions.test.ts +93 -0
  26. package/components/Filters/helpers/tests/handleSorting.test.ts +68 -0
  27. package/components/Filters/index.ts +5 -0
  28. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +1 -3
  29. package/components/Legend/Legend.Gradient.tsx +2 -9
  30. package/components/Loader/Loader.tsx +33 -0
  31. package/components/Loader/index.ts +1 -0
  32. package/components/Loader/loader.styles.css +13 -0
  33. package/components/NestedDropdown/NestedDropdown.tsx +90 -48
  34. package/components/NestedDropdown/nestedDropdownHelpers.ts +34 -0
  35. package/components/NestedDropdown/nesteddropdown.styles.css +7 -0
  36. package/components/NestedDropdown/tests/nestedDropdownHelpers.test.ts +58 -0
  37. package/components/Table/components/GroupRow.tsx +1 -1
  38. package/components/_stories/BlurStrokeTest.stories.tsx +27 -0
  39. package/components/_stories/NestedDropdown.stories.tsx +22 -46
  40. package/components/_stories/_mocks/nested-dropdown.json +30 -0
  41. package/components/_stories/styles.scss +0 -1
  42. package/components/ui/{Tooltip.jsx → Tooltip.tsx} +38 -14
  43. package/data/colorPalettes.js +107 -10
  44. package/dist/cove-main.css +6114 -0
  45. package/dist/cove-main.css.map +1 -0
  46. package/helpers/addValuesToFilters.ts +8 -3
  47. package/helpers/cove/number.js +46 -25
  48. package/helpers/coveUpdateWorker.ts +6 -7
  49. package/helpers/pivotData.ts +52 -11
  50. package/helpers/tests/gatherQueryParams.test.ts +13 -1
  51. package/helpers/tests/pivotData.test.ts +50 -0
  52. package/helpers/ver/4.24.10.ts +47 -0
  53. package/helpers/ver/4.24.9.ts +0 -3
  54. package/helpers/ver/tests/4.24.10.test.ts +45 -0
  55. package/helpers/viewports.ts +9 -0
  56. package/package.json +7 -3
  57. package/styles/_button-section.scss +4 -0
  58. package/styles/_global-variables.scss +19 -1
  59. package/styles/_global.scss +1 -8
  60. package/styles/_reset.scss +2 -15
  61. package/styles/base.scss +0 -1
  62. package/styles/cove-main.scss +6 -0
  63. package/styles/filters.scss +6 -4
  64. package/styles/v2/components/ui/tooltip.scss +42 -40
  65. package/styles/v2/layout/_component.scss +0 -6
  66. package/styles/v2/layout/index.scss +0 -1
  67. package/types/Axis.ts +2 -0
  68. package/types/General.ts +1 -0
  69. package/types/Table.ts +2 -1
  70. package/types/Visualization.ts +13 -1
  71. package/types/VizFilter.ts +2 -1
  72. package/components/DataTable/components/Icons.tsx +0 -10
  73. package/components/_stories/EditorPanel.stories.tsx +0 -54
  74. 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 display='question' style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }} />
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>Hiding the data table may affect accessibility. An alternate form of accessing visualization data is a 508 requirement.</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 display='question' style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }} />
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>To comply with 508 standards, if the first column in the data table has no header, enter a brief one here.</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 value={config.table.limitHeight} section='table' fieldName='limitHeight' label=' Limit Table Height' updateField={updateField} />
164
- {config.table.limitHeight && <TextField value={config.table.height} section='table' fieldName='height' label='Data Table Height' type='number' min={0} max={500} placeholder='Height(px)' updateField={updateField} />}
165
- {config?.visualizationType !== 'Sankey' && <MultiSelect key={excludedColumns.join('') + 'excluded'} options={dataColumns.map(c => ({ label: c, value: c }))} selected={excludedColumns} fieldName='dataTable' label='Exclude Columns' section='columns' updateField={excludeColumns} />}
166
- <CheckBox value={config.table.collapsible} fieldName='collapsible' label=' Collapsible' section='table' updateField={updateField} />
167
- {config.table.collapsible !== false && <CheckBox value={config.table.expanded} fieldName='expanded' label=' Expanded by Default' section='table' updateField={updateField} />}
168
- {isDashboard && config.type !== 'table' && <CheckBox value={config.table.showDataTableLink} fieldName='showDataTableLink' label='Show Data Table Name & Link' section='table' updateField={updateField} />}
169
- {isLoadedFromUrl && <CheckBox value={config.table.showDownloadUrl} fieldName='showDownloadUrl' label='Show URL to Automatically Updated Data' section='table' updateField={updateField} />}
170
- {config.type !== 'table' && <CheckBox value={config.table.showDownloadImgButton} fieldName='showDownloadImgButton' label='Display Image Button' section='table' updateField={updateField} />}
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 type='number' value={config.table.cellMinWidth ? config.table.cellMinWidth : 0} onChange={e => updateField('table', null, 'cellMinWidth', e.target.value)} />
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>Choose a column to use for grouping data rows. The selected column will not be shown in the data table. You will only be able to choose a column which does not have a column configuration.</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
- {[PLACEHOLDER, ...groupPivotColumns.filter(col => col !== config.table.pivot?.columnName && col !== config.table.pivot?.valueColumn)].map(option => (
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(col => col !== config.table.groupBy && col !== config.table.pivot?.valueColumn)}
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
- <Select
223
- label='Pivot Value Column: '
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 { display = true, label, tooltip, section = null, subsection = null, fieldName, updateField, value: stateValue, type = 'text', i = null, min = null, ...attributes } = props
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 { display = true, label, value, fieldName, section = null, subsection = null, tooltip, updateField, ...attributes } = props
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 { display = true, label, value, options, fieldName, section = null, subsection = null, required = false, tooltip, updateField, initial: initialValue, ...attributes } = props
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={!!filter.setByQueryParameter}
198
+ checked={useParameters}
179
199
  aria-label='Create query parameters'
180
- onChange={e => {
181
- updateGroupingFilterProp('setByQueryParameter', filter.columnName)
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
- {!!filter.setByQueryParameter && (
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.setByQueryParameter}
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 './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'
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 filterStyleOptions = ['dropdown', 'nested-dropdown', 'pill', 'tab', 'tab bar', 'multi-select']
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.active = newFilter.queuedActive
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
- currentFilter={singleFilter}
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
- const filterSectionClassList = [
483
- `filters-section legend_${visualizationConfig?.legend?.hide ? 'hidden' : 'visible'}_${
484
- visualizationConfig?.legend?.position || ''
485
- }`,
486
- type === 'map' ? general.headerColor : visualizationConfig?.visualizationType === 'Spark Line' ? null : theme
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={filterSectionClassList.join(' ')}>
498
+ <section className={getClasses().join(' ')}>
490
499
  <p className='filters-section__intro-text'>
491
- {filters?.some(f => f.active && f.showDropdown) ? filterConstants.introText : ''}{' '}
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
+ }