@cdc/core 4.24.5 → 4.24.7

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 (66) hide show
  1. package/components/AdvancedEditor/AdvancedEditor.tsx +93 -0
  2. package/components/AdvancedEditor/advanced-editor-styles.css +3 -0
  3. package/components/AdvancedEditor/index.ts +1 -0
  4. package/components/DataTable/DataTable.tsx +21 -2
  5. package/components/DataTable/DataTableStandAlone.tsx +4 -25
  6. package/components/DataTable/components/DataTableEditorPanel.tsx +4 -4
  7. package/components/DataTable/components/ExpandCollapse.tsx +1 -1
  8. package/components/DataTable/helpers/chartCellMatrix.tsx +3 -9
  9. package/components/DataTable/helpers/getChartCellValue.ts +8 -4
  10. package/components/DataTable/helpers/getDataSeriesColumns.ts +8 -5
  11. package/components/DataTable/helpers/getRowType.ts +6 -0
  12. package/components/DataTable/types/TableConfig.ts +1 -0
  13. package/components/EditorPanel/ColumnsEditor.tsx +3 -30
  14. package/components/EditorPanel/DataTableEditor.tsx +66 -22
  15. package/components/EditorPanel/FieldSetWrapper.tsx +51 -0
  16. package/components/EditorPanel/FootnotesEditor.tsx +77 -0
  17. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +227 -0
  18. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +54 -0
  19. package/components/EditorPanel/VizFilterEditor/index.ts +1 -0
  20. package/components/EditorWrapper/EditorWrapper.tsx +3 -4
  21. package/components/EditorWrapper/index.ts +1 -0
  22. package/components/{Filters.jsx → Filters.tsx} +40 -24
  23. package/components/Footnotes/Footnotes.tsx +25 -0
  24. package/components/Footnotes/FootnotesStandAlone.tsx +45 -0
  25. package/components/Footnotes/footnotes.css +5 -0
  26. package/components/Footnotes/index.ts +1 -0
  27. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +8 -4
  28. package/components/Layout/components/Visualization/index.tsx +12 -5
  29. package/components/MultiSelect/MultiSelect.tsx +36 -9
  30. package/components/MultiSelect/multiselect.styles.css +0 -3
  31. package/components/_stories/Footnotes.stories.tsx +17 -0
  32. package/components/_stories/styles.scss +1 -0
  33. package/components/inputs/InputSelect.tsx +17 -6
  34. package/components/ui/Icon.tsx +1 -2
  35. package/helpers/addValuesToFilters.ts +56 -0
  36. package/helpers/cove/accessibility.ts +1 -0
  37. package/helpers/cove/fontSettings.ts +2 -0
  38. package/helpers/coveUpdateWorker.ts +7 -0
  39. package/helpers/filterVizData.ts +30 -0
  40. package/helpers/formatConfigBeforeSave.ts +90 -0
  41. package/helpers/gatherQueryParams.ts +14 -7
  42. package/helpers/lineChartHelpers.js +2 -1
  43. package/helpers/pivotData.ts +18 -0
  44. package/helpers/queryStringUtils.ts +29 -0
  45. package/helpers/tests/updateFieldFactory.test.ts +1 -0
  46. package/helpers/updateFieldFactory.ts +1 -1
  47. package/helpers/ver/4.24.7.ts +92 -0
  48. package/package.json +6 -4
  49. package/styles/_button-section.scss +6 -1
  50. package/styles/_data-table.scss +0 -1
  51. package/styles/base.scss +4 -0
  52. package/styles/v2/themes/_color-definitions.scss +1 -0
  53. package/types/Annotation.ts +46 -0
  54. package/types/Axis.ts +0 -2
  55. package/types/ConfigureData.ts +1 -1
  56. package/types/Footnotes.ts +17 -0
  57. package/types/General.ts +5 -0
  58. package/types/Runtime.ts +2 -7
  59. package/types/Table.ts +6 -0
  60. package/types/Visualization.ts +31 -9
  61. package/types/VizFilter.ts +16 -5
  62. package/LICENSE +0 -201
  63. package/components/AdvancedEditor.jsx +0 -74
  64. package/components/EditorPanel/VizFilterEditor.tsx +0 -234
  65. package/helpers/queryStringUtils.js +0 -26
  66. package/types/BaseVisualizationType.ts +0 -1
@@ -601,7 +601,7 @@
601
601
  /* clicking anywhere will focus the input */
602
602
  cursor: text;
603
603
 
604
- span {
604
+ span:not(.cove-tooltip, .cove-icon) {
605
605
  display: inline;
606
606
  }
607
607
  }
@@ -723,6 +723,10 @@
723
723
  margin-right: 5px;
724
724
  }
725
725
 
726
+ .cove-tooltip {
727
+ position: relative;
728
+ }
729
+
726
730
  // tooltips
727
731
  .cove-label + .cove-tooltip {
728
732
  top: 1px;
@@ -731,9 +735,9 @@
731
735
  }
732
736
 
733
737
  .cove-accordion__button .cove-tooltip {
734
- display: inline-flex;
735
- right: 1.5rem;
736
- line-height: inherit;
738
+ // display: inline-flex;
739
+ // right: 1.5rem;
740
+ // line-height: inherit;
737
741
  }
738
742
 
739
743
  .cove-list-group__item .cove-tooltip {
@@ -1,18 +1,19 @@
1
1
  // main visualization wrapper
2
2
  import { ChartConfig } from '@cdc/chart/src/types/ChartConfig'
3
- import React, { forwardRef, useRef } from 'react'
3
+ import React, { forwardRef } from 'react'
4
4
  import { Config as DataBiteConfig } from '@cdc/data-bite/src/types/Config'
5
5
  import './visualizations.scss'
6
6
  import { Config as WaffleChartConfig } from '@cdc/waffle-chart/src/types/Config'
7
7
  import { MarkupIncludeConfig } from '@cdc/core/types/MarkupInclude'
8
+ import { DashboardFilters } from '@cdc/dashboard/src/types/DashboardFilters'
8
9
 
9
10
  type VisualizationWrapper = {
10
11
  children: React.ReactNode
11
- config: ChartConfig | DataBiteConfig | WaffleChartConfig | MarkupIncludeConfig
12
- currentViewport: string
13
- imageId: string
12
+ config: ChartConfig | DataBiteConfig | WaffleChartConfig | MarkupIncludeConfig | DashboardFilters
13
+ currentViewport?: string
14
+ imageId?: string
14
15
  isEditor: boolean
15
- showEditorPanel: boolean
16
+ showEditorPanel?: boolean
16
17
  }
17
18
 
18
19
  const Visualization: React.FC<VisualizationWrapper> = forwardRef((props, ref) => {
@@ -33,6 +34,12 @@ const Visualization: React.FC<VisualizationWrapper> = forwardRef((props, ref) =>
33
34
  classes.push('editor-panel--hidden')
34
35
  }
35
36
 
37
+ if (config.type === 'filtered-text') {
38
+ classes.push('type-filtered-text')
39
+ classes = classes.filter(item => item !== 'cove-component__content')
40
+ return classes
41
+ }
42
+
36
43
  if (config.type === 'chart') {
37
44
  classes.push('type-chart')
38
45
  config?.visualizationType === 'Spark Line' && classes.push(`type-sparkline`)
@@ -21,8 +21,8 @@ interface MultiSelectProps {
21
21
  limit?: number
22
22
  }
23
23
 
24
- const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection = null, fieldName, label, options, updateField, selected, limit }) => {
25
- const preselectedItems = options.filter(opt => selected?.includes(opt.value)).slice(0, limit)
24
+ const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection = null, fieldName, label, options, updateField, selected = [], limit }) => {
25
+ const preselectedItems = options.filter(opt => selected.includes(opt.value)).slice(0, limit)
26
26
  const [selectedItems, setSelectedItems] = useState<Option[]>(preselectedItems)
27
27
  const [expanded, setExpanded] = useState(false)
28
28
  const multiSelectRef = useRef(null)
@@ -68,22 +68,39 @@ const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection =
68
68
  return (
69
69
  <div ref={multiSelectRef} className='cove-multiselect'>
70
70
  {label && (
71
- <label id={multiID} className='cove-input__label'>
71
+ <span id={multiID} className='edit-label column-heading'>
72
72
  {label}
73
- </label>
73
+ </span>
74
74
  )}
75
75
 
76
76
  <div className='wrapper'>
77
77
  <div className='selected'>
78
78
  {selectedItems.map(item => (
79
- <div key={item.value} aria-labelledby={label ? multiID : undefined} role='button' onClick={() => handleItemRemove(item)} onKeyUp={e => handleItemRemove(item, e)}>
79
+ <div key={item.value} aria-labelledby={label ? multiID : undefined}>
80
80
  {item.label}
81
- <button aria-label='Remove' onClick={() => handleItemRemove(item)}>
81
+ <button
82
+ aria-label='Remove'
83
+ onClick={e => {
84
+ e.preventDefault()
85
+ handleItemRemove(item)
86
+ }}
87
+ onKeyUp={e => {
88
+ handleItemRemove(item, e)
89
+ }}
90
+ >
82
91
  x
83
92
  </button>
84
93
  </div>
85
94
  ))}
86
- <button aria-label={expanded ? 'Collapse' : 'Expand'} aria-labelledby={label ? multiID : undefined} className='expand' onClick={() => setExpanded(!expanded)}>
95
+ <button
96
+ aria-label={expanded ? 'Collapse' : 'Expand'}
97
+ aria-labelledby={label ? multiID : undefined}
98
+ className='expand'
99
+ onClick={e => {
100
+ e.preventDefault()
101
+ setExpanded(!expanded)
102
+ }}
103
+ >
87
104
  <Icon display={expanded ? 'caretDown' : 'caretUp'} style={{ cursor: 'pointer' }} />
88
105
  </button>
89
106
  </div>
@@ -98,11 +115,21 @@ const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection =
98
115
  </Tooltip>
99
116
  )}
100
117
  </div>
101
- <ul className={'dropdown' + (expanded ? '' : ' hide')}>
118
+ <ul className={'dropdown' + (expanded ? '' : ' d-none')}>
102
119
  {options
103
120
  .filter(option => !selectedItems.find(item => item.value === option.value))
104
121
  .map(option => (
105
- <li className='cove-multiselect-li' key={option.value} role='option' tabIndex={0} onClick={() => handleItemSelect(option)} onKeyUp={e => handleItemSelect(option, e)}>
122
+ <li
123
+ className='cove-multiselect-li'
124
+ key={option.value}
125
+ role='option'
126
+ tabIndex={0}
127
+ onClick={e => {
128
+ e.preventDefault()
129
+ handleItemSelect(option, e)
130
+ }}
131
+ onKeyUp={e => handleItemSelect(option, e)}
132
+ >
106
133
  {option.label}
107
134
  </li>
108
135
  ))}
@@ -51,9 +51,6 @@
51
51
  overflow-y: scroll;
52
52
  min-width: 200px;
53
53
  z-index: 4;
54
- &.hide {
55
- display: none;
56
- }
57
54
 
58
55
  :is(li) {
59
56
  cursor: pointer;
@@ -0,0 +1,17 @@
1
+ import { Meta, StoryObj } from '@storybook/react'
2
+ import Footnotes from '../Footnotes'
3
+
4
+ const meta: Meta<typeof Footnotes> = {
5
+ title: 'Components/Organisms/Footnotes',
6
+ component: Footnotes
7
+ }
8
+
9
+ export default meta
10
+
11
+ type Story = StoryObj<typeof Footnotes>
12
+
13
+ export const Primary: Story = {
14
+ args: {
15
+ footnotes: [{ symbol: '*', text: 'This is a footnote' }, { symbol: '†', text: 'This is another footnote' }, { text: 'This is a third footnote' }]
16
+ }
17
+ }
@@ -1,3 +1,4 @@
1
+ @import '../../styles/base.scss';
1
2
  @import '../../styles/_variables';
2
3
  @import '../../styles/_mixins';
3
4
  @import '../../styles/_data-table';
@@ -3,7 +3,7 @@ import '../../styles/v2/components/input/index.scss'
3
3
  interface InputProps {
4
4
  label?
5
5
  value?
6
- options: string[] | { [key: string]: string }
6
+ options: string[] | Record<string, any> | [any, string][]
7
7
  fieldName
8
8
  section?
9
9
  subsection?
@@ -17,11 +17,22 @@ const InputSelect = ({ label, value, options, fieldName, section = null, subsect
17
17
 
18
18
  if (Array.isArray(options)) {
19
19
  //Handle basic array
20
- optionsJsx = options.map(optionName => (
21
- <option value={optionName} key={optionName}>
22
- {optionName}
23
- </option>
24
- ))
20
+ optionsJsx = options.map(option => {
21
+ if (typeof option === 'string') {
22
+ return (
23
+ <option value={option} key={option}>
24
+ {option}
25
+ </option>
26
+ )
27
+ } else {
28
+ const [value, name] = option
29
+ return (
30
+ <option value={value} key={name}>
31
+ {name}
32
+ </option>
33
+ )
34
+ }
35
+ })
25
36
  } else {
26
37
  //Handle object with value/name pairs
27
38
  optionsJsx = []
@@ -1,4 +1,3 @@
1
- import React from 'react'
2
1
  import PropTypes from 'prop-types'
3
2
 
4
3
  import iconCaretUp from '../../assets/icon-caret-up.svg'
@@ -68,7 +67,7 @@ const iconHash = {
68
67
  plus: iconPlus,
69
68
  minus: iconMinus,
70
69
  'filtered-text': iconText,
71
- 'filter-dropdowns': iconDropdowns,
70
+ dashboardFilters: iconDropdowns,
72
71
  table: iconTable,
73
72
  sankey: iconSankey,
74
73
  rotateLeft: iconRotateLeft,
@@ -0,0 +1,56 @@
1
+ import _ from 'lodash'
2
+ import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
3
+
4
+ type Filter = {
5
+ columnName: string
6
+ values: string[]
7
+ filterStyle?: string
8
+ active?: string | string[]
9
+ }
10
+
11
+ // Gets filter values from dataset
12
+ const generateValuesForFilter = (columnName, data: any[] | Record<string, any[]>) => {
13
+ const values: string[] = []
14
+
15
+ if (Array.isArray(data)) {
16
+ data.forEach(row => {
17
+ const value = row[columnName]
18
+ if (!values.includes(value)) {
19
+ values.push(value)
20
+ }
21
+ })
22
+ } else {
23
+ // data is a dataset this loops through ALL datasets to find matching values
24
+ // not sure if this is desired behavior
25
+ Object.values(data).forEach((rows: any[]) => {
26
+ rows.forEach(row => {
27
+ const value = row[columnName]
28
+ if (!values.includes(value)) {
29
+ values.push(value)
30
+ }
31
+ })
32
+ })
33
+ }
34
+
35
+ return values
36
+ }
37
+
38
+ export const addValuesToFilters = <T>(filters: Filter[], data: any[] | Record<string, any[]>): Array<T> => {
39
+ return filters?.map(filter => {
40
+ const filterCopy = _.cloneDeep(filter)
41
+
42
+ const filterValues = generateValuesForFilter(filter.columnName, data)
43
+ filterCopy.values = filterValues
44
+ if (filterValues.length > 0) {
45
+ const defaultValues = filterCopy.filterStyle === 'multi-select' ? filterCopy.values : filterCopy.values[0]
46
+
47
+ const queryStringFilterValue = getQueryStringFilterValue(filterCopy)
48
+ if (queryStringFilterValue) {
49
+ filterCopy.active = queryStringFilterValue
50
+ } else {
51
+ filterCopy.active = filterCopy.active || defaultValues
52
+ }
53
+ }
54
+ return filterCopy
55
+ }) as Array<T>
56
+ }
@@ -9,6 +9,7 @@ import chroma from 'chroma-js'
9
9
  export const WCAG_CONTRAST_RATIO = 4.5
10
10
 
11
11
  export const getContrastColor = (textColor: string, bgColor: string) => {
12
+ if (!bgColor) return
12
13
  if (chroma.contrast(textColor, bgColor) < WCAG_CONTRAST_RATIO) {
13
14
  switch (textColor) {
14
15
  case '#FFF':
@@ -0,0 +1,2 @@
1
+ // TODO: handle with css
2
+ export const fontSizes = { small: 16, medium: 18, large: 20 }
@@ -3,13 +3,20 @@
3
3
  import update_4_24_4 from './ver/4.23.4'
4
4
  import update_4_24_3 from './ver/4.24.3'
5
5
  import update_4_24_5 from './ver/4.24.5'
6
+ import update_4_24_7 from './ver/4.24.7'
6
7
 
7
8
  export const coveUpdateWorker = config => {
9
+ if (config.multiDashboards) {
10
+ config.multiDashboards.forEach((dashboard, index) => {
11
+ config.multiDashboards[index] = coveUpdateWorker(dashboard)
12
+ })
13
+ }
8
14
  let genConfig = config
9
15
 
10
16
  genConfig = update_4_24_3(genConfig)
11
17
  genConfig = update_4_24_4(genConfig)
12
18
  genConfig = update_4_24_5(genConfig)
19
+ genConfig = update_4_24_7(genConfig)
13
20
 
14
21
  return genConfig
15
22
  }
@@ -0,0 +1,30 @@
1
+ export const filterVizData = (filters, data) => {
2
+ if (!data) {
3
+ console.warn('COVE: No data to filter')
4
+ return []
5
+ }
6
+
7
+ if (!filters) return data
8
+ const filteredData: any[] = []
9
+
10
+ data?.forEach(row => {
11
+ let add = true
12
+ filters
13
+ .filter(filter => filter.type !== 'url')
14
+ .forEach(filter => {
15
+ const value = row[filter.columnName]
16
+ if (filter.active === undefined) return
17
+ if (Array.isArray(filter.active)) {
18
+ if (!filter.active.includes(value)) {
19
+ add = false
20
+ }
21
+ } else if (value != filter.active) {
22
+ add = false
23
+ }
24
+ })
25
+
26
+ if (add) filteredData.push(row)
27
+ })
28
+
29
+ return filteredData
30
+ }
@@ -0,0 +1,90 @@
1
+ import Footnotes from '@cdc/core/types/Footnotes'
2
+ import { Visualization } from '@cdc/core/types/Visualization'
3
+ import { DashboardConfig } from '@cdc/dashboard/src/types/DashboardConfig'
4
+ import _ from 'lodash'
5
+
6
+ const cleanDashboardFootnotes = (config: DashboardConfig) => {
7
+ // strip any blank footnote visualizations
8
+ const footnoteIds: string[] = []
9
+
10
+ if (config.rows) {
11
+ config.rows.forEach(row => {
12
+ if (row.footnotesId) {
13
+ const { dataKey, staticFootnotes } = config.visualizations[row.footnotesId] as Footnotes
14
+ if (!dataKey && !staticFootnotes?.length) {
15
+ delete config.visualizations[row.footnotesId]
16
+ } else {
17
+ footnoteIds.push(row.footnotesId)
18
+ }
19
+ }
20
+ })
21
+ }
22
+
23
+ if (config.visualizations) {
24
+ Object.keys(config.visualizations).forEach(vizKey => {
25
+ const viz: Visualization = config.visualizations[vizKey]
26
+ if (viz.type === 'footnotes' && !footnoteIds.includes(vizKey)) {
27
+ // if footnote isn't being used by any rows, remove it
28
+ delete config.visualizations[vizKey]
29
+ }
30
+ })
31
+ }
32
+ }
33
+
34
+ const cleanDashboardData = (config: DashboardConfig) => {
35
+ if (config.datasets) {
36
+ Object.keys(config.datasets).forEach(datasetKey => {
37
+ delete config.datasets[datasetKey].formattedData
38
+ if (config.datasets[datasetKey].dataUrl) {
39
+ delete config.datasets[datasetKey].data
40
+ }
41
+ })
42
+ }
43
+ if (config.visualizations) {
44
+ Object.keys(config.visualizations).forEach(vizKey => {
45
+ config.visualizations[vizKey] = _.omit(config.visualizations[vizKey], ['runtime', 'formattedData', 'data'])
46
+ })
47
+ }
48
+ if (config.rows) {
49
+ config.rows.forEach((row, i) => {
50
+ if (row.dataKey) {
51
+ config.rows[i] = _.omit(row, ['data', 'formattedData'])
52
+ }
53
+ })
54
+ }
55
+ }
56
+
57
+ const cleanSharedFilters = (config: DashboardConfig) => {
58
+ if (config.dashboard?.sharedFilters) {
59
+ config.dashboard.sharedFilters.forEach((filter, index) => {
60
+ delete config.dashboard.sharedFilters[index].active
61
+ if (filter.type === 'urlfilter') {
62
+ delete config.dashboard.sharedFilters[index].values
63
+ }
64
+ })
65
+ }
66
+ }
67
+
68
+ export const formatConfigBeforeSave = configToStrip => {
69
+ let strippedConfig = _.cloneDeep(configToStrip)
70
+ if (strippedConfig.type === 'dashboard') {
71
+ if (strippedConfig.multiDashboards) {
72
+ strippedConfig.multiDashboards.forEach((multiDashboard, i) => {
73
+ cleanDashboardData(strippedConfig.multiDashboards[i])
74
+ cleanSharedFilters(strippedConfig.multiDashboards[i])
75
+ cleanDashboardFootnotes(strippedConfig.multiDashboards[i])
76
+ })
77
+ }
78
+ cleanDashboardData(strippedConfig)
79
+ cleanSharedFilters(strippedConfig)
80
+ cleanDashboardFootnotes(strippedConfig)
81
+ } else {
82
+ delete strippedConfig.runtime
83
+ delete strippedConfig.formattedData
84
+ if (strippedConfig.dataUrl) {
85
+ delete strippedConfig.data
86
+ }
87
+ }
88
+
89
+ return strippedConfig
90
+ }
@@ -1,7 +1,14 @@
1
- export const gatherQueryParams = (params: {key: string, value: string}[]) => {
2
- return params.map(({key, value}, i) => {
3
- const leadingCharacter = i === 0 ? '?' : '&'
4
- return leadingCharacter + key + '=' + value
5
- })
6
- .join('')
7
- }
1
+ import _ from 'lodash'
2
+
3
+ export const gatherQueryParams = (baseEndpoint: string, params: { key: string; value: string }[]) => {
4
+ const baseEndpointHasQueryParams = baseEndpoint.includes('?')
5
+ return params
6
+ .filter(({ value }) => value !== '')
7
+ .map(({ key, value }, i) => {
8
+ const leadingCharacter = i === 0 && !baseEndpointHasQueryParams ? '?' : '&'
9
+ const isStatementParam = key.match(/\$.*/)
10
+ if (!_.isNaN(parseInt(value)) || isStatementParam) return leadingCharacter + key + '=' + value
11
+ return leadingCharacter + key + '=' + `"${value}"`
12
+ })
13
+ .join('')
14
+ }
@@ -3,5 +3,6 @@ export const approvedCurveTypes = {
3
3
  Cardinal: 'curveCardinal',
4
4
  Natural: 'curveNatural',
5
5
  'Monotone X': 'curveMonotoneX',
6
- Step: 'curveStep'
6
+ Step: 'curveStep',
7
+ 'Curve Basis': 'curveBasis'
7
8
  }
@@ -0,0 +1,18 @@
1
+ import _ from 'lodash'
2
+
3
+ /** columnName is the column you'd like to select data values from to show as column headers.
4
+ * Pivot is the value column who's data you'd like to show under those respective columns*/
5
+ export const pivotData = (data: Record<string, any>[], columnName: string, pivot: string) => {
6
+ const grouped = _.groupBy(data, val => val[columnName])
7
+ const newData = []
8
+ for (const key in grouped) {
9
+ const group = grouped[key]
10
+ group.forEach((val, index) => {
11
+ const row = newData[index] || {}
12
+ row[key] = val[pivot]
13
+ const toAdd = _.omit(val, [columnName, pivot])
14
+ newData[index] = { ...toAdd, ...row }
15
+ })
16
+ }
17
+ return newData
18
+ }
@@ -0,0 +1,29 @@
1
+ export function getQueryStringFilterValue(filter) {
2
+ const urlParams = new URLSearchParams(window.location.search)
3
+ if (filter.setByQueryParameter) {
4
+ // Only check the query string if the filter is supposed to be set by QS param
5
+ const filterValue = urlParams.get(filter.setByQueryParameter)
6
+ if (filterValue && filter.values) {
7
+ for (let i = 0; i < filter.values.length; i++) {
8
+ if (filter.values[i] && filter.values[i].toLowerCase() === filterValue.toLowerCase()) {
9
+ return filter.values[i]
10
+ }
11
+ }
12
+ }
13
+ }
14
+ }
15
+
16
+ export function getQueryParams() {
17
+ const queryParams = {}
18
+ for (const [key, value] of Array.from(new URLSearchParams(window.location.search).entries())) {
19
+ queryParams[key] = value
20
+ }
21
+ return queryParams
22
+ }
23
+
24
+ export function updateQueryString(queryParams) {
25
+ const updateUrl = `${window.location.origin}${window.location.pathname}?${Object.keys(queryParams)
26
+ .map(queryParam => `${queryParam}=${encodeURIComponent(queryParams[queryParam])}`)
27
+ .join('&')}`
28
+ window.history.pushState({ path: updateUrl }, '', updateUrl)
29
+ }
@@ -1,4 +1,5 @@
1
1
  import { updateFieldFactory } from '../updateFieldFactory'
2
+ import { expect, describe, it } from 'vitest'
2
3
 
3
4
  describe('updateFieldFactory', () => {
4
5
  it('should update the top level field when section and subsection are null', () => {
@@ -1,7 +1,7 @@
1
1
  import { UpdateFieldFunc } from '../types/UpdateFieldFunc'
2
2
 
3
3
  export const updateFieldFactory =
4
- (config, updateConfig, legacy = false): UpdateFieldFunc<any> =>
4
+ <T>(config, updateConfig, legacy = false): UpdateFieldFunc<T> =>
5
5
  (section, subsection, fieldName, newValue) => {
6
6
  // Top level
7
7
  if (null === section && null === subsection) {
@@ -0,0 +1,92 @@
1
+ import _ from 'lodash'
2
+ import { DashboardFilters } from '@cdc/dashboard/src/types/DashboardFilters'
3
+ import { MultiDashboardConfig } from '@cdc/dashboard/src/types/MultiDashboard'
4
+ import { AnyVisualization } from '../../types/Visualization'
5
+
6
+ export const dashboardFiltersMigrate = config => {
7
+ if (!config.dashboard) return config
8
+ const dashboardConfig = config as MultiDashboardConfig
9
+ const newVisualizations = {}
10
+ // autoload was removed from APIFilter type
11
+ const newSharedFilters = (dashboardConfig.dashboard.sharedFilters || []).map(sf => {
12
+ if (sf.apiFilter?.autoLoad !== undefined) {
13
+ delete sf.apiFilter.autoLoad
14
+ }
15
+ if (sf.apiFilter?.defaultValue !== undefined) {
16
+ delete sf.apiFilter.defaultValue
17
+ }
18
+ return sf
19
+ })
20
+ config.dashboard.sharedFilters = newSharedFilters
21
+
22
+ Object.keys(dashboardConfig.visualizations).forEach(vizKey => {
23
+ const viz = dashboardConfig.visualizations[vizKey] as DashboardFilters
24
+ // hide was removed from visualizations
25
+ if (viz.hide !== undefined) {
26
+ viz.sharedFilterIndexes = newSharedFilters.map((_sf, i) => i).filter(i => !viz.hide.includes(i))
27
+ viz.type = 'dashboardFilters'
28
+ if (viz.autoLoad) {
29
+ viz.filterBehavior = 'Filter Change'
30
+ } else {
31
+ viz.filterBehavior = 'Apply Button'
32
+ }
33
+
34
+ delete viz.hide
35
+ }
36
+ // 'filter-dropdowns' was renamed to 'dashboardFilters' for clarity
37
+ if (viz.type === 'filter-dropdowns') viz.type = 'dashboardFilters'
38
+ if (viz.visualizationType === 'filter-dropdowns') viz.visualizationType = 'dashboardFilters'
39
+ newVisualizations[vizKey] = viz
40
+ })
41
+
42
+ if (config.dashboard.sharedFilters.length && !Object.values(newVisualizations).find((v: AnyVisualization) => v.type === 'dashboardFilters')) {
43
+ const newViz = {
44
+ type: 'dashboardFilters',
45
+ visualizationType: 'dashboardFilters',
46
+ sharedFilterIndexes: config.dashboard.sharedFilters.map((_sf, i) => i),
47
+ filterBehavior: config.filterBehavior || 'Filter Change'
48
+ }
49
+ const key = 'legacySharedFilters'
50
+ newVisualizations[key] = newViz
51
+ const newRow = {
52
+ columns: [
53
+ {
54
+ width: 12,
55
+ widget: key
56
+ }
57
+ ]
58
+ }
59
+ config.rows = [newRow, ...config.rows]
60
+ }
61
+ // if there's no dashboardFilters visualization but there are sharedFilters create a visualization and update rows.
62
+
63
+ config.visualizations = newVisualizations
64
+ }
65
+
66
+ const mapUpdates = newConfig => {
67
+ // When switching between old version of equal number, and the revised equal number opt in, roundToPlace needs to be set.
68
+ // There wasn't an initial value set for this, and legends would return NaN if it wasn't set. ie. 0 - NAN instead of 0 - 1
69
+ const equalNumberRoundingPatch = newConfig => {
70
+ if (newConfig.type === 'map') {
71
+ if (newConfig.columns.primary.roundToPlace === undefined) {
72
+ newConfig.columns.primary.roundToPlace = 0
73
+ }
74
+ }
75
+ }
76
+
77
+ equalNumberRoundingPatch(newConfig)
78
+
79
+ return newConfig
80
+ }
81
+
82
+ const update_4_24_7 = config => {
83
+ const ver = '4.24.7'
84
+
85
+ const newConfig = _.cloneDeep(config)
86
+
87
+ mapUpdates(newConfig)
88
+ dashboardFiltersMigrate(newConfig)
89
+ newConfig.version = ver
90
+ return newConfig
91
+ }
92
+ export default update_4_24_7