@cdc/dashboard 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 (70) hide show
  1. package/dist/cdcdashboard.js +122872 -112065
  2. package/examples/custom/css/respiratory.css +236 -0
  3. package/examples/custom/js/respiratory.js +242 -0
  4. package/examples/default-multi-dataset-shared-filter.json +1729 -0
  5. package/examples/ed-visits-county-file.json +618 -0
  6. package/examples/filtered-dash.json +6 -21
  7. package/index.html +10 -1
  8. package/package.json +12 -11
  9. package/src/CdcDashboard.tsx +5 -1
  10. package/src/CdcDashboardComponent.tsx +165 -306
  11. package/src/DashboardContext.tsx +9 -1
  12. package/src/_stories/Dashboard.stories.tsx +38 -34
  13. package/src/_stories/_mock/api-filter-chart.json +11 -35
  14. package/src/_stories/_mock/api-filter-map.json +17 -31
  15. package/src/_stories/_mock/multi-viz.json +2 -3
  16. package/src/_stories/_mock/pivot-filter.json +14 -12
  17. package/src/components/CollapsibleVisualizationRow.tsx +44 -0
  18. package/src/components/Column.tsx +1 -1
  19. package/src/components/DashboardFilters/DashboardFilters.tsx +80 -0
  20. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +218 -0
  21. package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +48 -0
  22. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +367 -0
  23. package/src/components/DashboardFilters/DashboardFiltersEditor/index.ts +1 -0
  24. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +143 -0
  25. package/src/components/DashboardFilters/index.ts +3 -0
  26. package/src/components/DataDesignerModal.tsx +9 -9
  27. package/src/components/ExpandCollapseButtons.tsx +20 -0
  28. package/src/components/Header/Header.tsx +1 -97
  29. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +4 -4
  30. package/src/components/Row.tsx +52 -19
  31. package/src/components/Toggle/Toggle.tsx +2 -4
  32. package/src/components/VisualizationRow.tsx +82 -24
  33. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +116 -0
  34. package/src/components/VisualizationsPanel/index.ts +1 -0
  35. package/src/components/VisualizationsPanel/visualizations-panel-styles.css +12 -0
  36. package/src/components/Widget.tsx +26 -90
  37. package/src/helpers/apiFilterHelpers.ts +51 -0
  38. package/src/helpers/changeFilterActive.ts +30 -0
  39. package/src/helpers/filterData.ts +10 -48
  40. package/src/helpers/generateValuesForFilter.ts +1 -1
  41. package/src/helpers/getAutoLoadVisualization.ts +11 -0
  42. package/src/helpers/getFilteredData.ts +4 -2
  43. package/src/helpers/getVizConfig.ts +23 -2
  44. package/src/helpers/getVizRowColumnLocator.ts +2 -1
  45. package/src/helpers/hasDashboardApplyBehavior.ts +5 -0
  46. package/src/helpers/iconHash.tsx +3 -3
  47. package/src/helpers/mapDataToConfig.ts +29 -0
  48. package/src/helpers/processData.ts +2 -3
  49. package/src/helpers/reloadURLHelpers.ts +68 -0
  50. package/src/helpers/tests/filterData.test.ts +1 -93
  51. package/src/scss/editor-panel.scss +1 -1
  52. package/src/scss/grid.scss +34 -27
  53. package/src/scss/main.scss +41 -3
  54. package/src/scss/variables.scss +4 -0
  55. package/src/store/dashboard.actions.ts +12 -4
  56. package/src/store/dashboard.reducer.ts +30 -4
  57. package/src/types/APIFilter.ts +1 -5
  58. package/src/types/ConfigRow.ts +2 -0
  59. package/src/types/Dashboard.ts +1 -1
  60. package/src/types/DashboardConfig.ts +2 -4
  61. package/src/types/DashboardFilters.ts +7 -0
  62. package/src/types/InitialState.ts +1 -1
  63. package/src/types/MultiDashboard.ts +2 -2
  64. package/src/types/SharedFilter.ts +2 -5
  65. package/src/types/Tab.ts +1 -1
  66. package/LICENSE +0 -201
  67. package/src/components/Filters.tsx +0 -88
  68. package/src/components/Header/FilterModal.tsx +0 -510
  69. package/src/components/VisualizationsPanel.tsx +0 -95
  70. package/src/helpers/getApiFilterKey.ts +0 -5
@@ -0,0 +1,143 @@
1
+ import { useContext, useState } from 'react'
2
+ import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
3
+ import Filters from './DashboardFilters'
4
+ import { changeFilterActive } from '../../helpers/changeFilterActive'
5
+ import _ from 'lodash'
6
+ import { FilterBehavior } from '../Header/Header'
7
+ import { getFilteredData } from '../../helpers/getFilteredData'
8
+ import { DashboardFilters } from '../../types/DashboardFilters'
9
+ import { getQueryParams, updateQueryString } from '@cdc/core/helpers/queryStringUtils'
10
+ import Layout from '@cdc/core/components/Layout'
11
+ import DashboardFiltersEditor from './DashboardFiltersEditor'
12
+ import { ViewPort } from '@cdc/core/types/ViewPort'
13
+ import { hasDashboardApplyBehavior } from '../../helpers/hasDashboardApplyBehavior'
14
+
15
+ export type DropdownOptions = Record<'value' | 'text', string>[]
16
+
17
+ export type APIFilterDropdowns = {
18
+ // null means still loading
19
+ [filtername: string]: null | DropdownOptions
20
+ }
21
+
22
+ type DashboardFiltersProps = {
23
+ apiFilterDropdowns: APIFilterDropdowns
24
+ visualizationConfig: DashboardFilters
25
+ isEditor?: boolean
26
+ setConfig: (config: DashboardFilters) => void
27
+ currentViewport?: ViewPort
28
+ }
29
+
30
+ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({ apiFilterDropdowns, visualizationConfig, setConfig: updateConfig, currentViewport, isEditor = false }) => {
31
+ const state = useContext(DashboardContext)
32
+ const { config: dashboardConfig, reloadURLData, loadAPIFilters } = state
33
+ const dispatch = useContext(DashboardDispatchContext)
34
+
35
+ const applyFilters = () => {
36
+ const dashboardConfig = _.cloneDeep(state.config.dashboard)
37
+ const nonAutoLoadFilterIndexes = Object.values(state.config.visualizations)
38
+ .filter(v => v.type === 'dashboardFilters')
39
+ .reduce((acc, viz: DashboardFilters) => (!viz.autoLoad ? [...acc, viz.sharedFilterIndexes] : acc), [])
40
+ const allRequiredFiltersSelected = !dashboardConfig.sharedFilters.some((filter, filterIndex) => {
41
+ if (nonAutoLoadFilterIndexes.includes(filterIndex)) {
42
+ return !filter.active && !filter.queuedActive
43
+ } else {
44
+ // autoload filters don't need to be selected to apply filters
45
+ return false
46
+ }
47
+ })
48
+ if (allRequiredFiltersSelected) {
49
+ if (hasDashboardApplyBehavior(state.config.visualizations)) {
50
+ const queryParams = getQueryParams()
51
+ let needsQueryUpdate = false
52
+ dashboardConfig.sharedFilters.forEach((sharedFilter, index) => {
53
+ if (sharedFilter.queuedActive) {
54
+ dashboardConfig.sharedFilters[index].active = sharedFilter.queuedActive
55
+ delete dashboardConfig.sharedFilters[index].queuedActive
56
+
57
+ if (sharedFilter.setByQueryParameter && queryParams[sharedFilter.setByQueryParameter] !== sharedFilter.active) {
58
+ queryParams[sharedFilter.setByQueryParameter] = sharedFilter.active
59
+ needsQueryUpdate = true
60
+ }
61
+ }
62
+ })
63
+
64
+ if (needsQueryUpdate) {
65
+ updateQueryString(queryParams)
66
+ }
67
+ }
68
+
69
+ dispatch({ type: 'SET_SHARED_FILTERS', payload: dashboardConfig.sharedFilters })
70
+ dispatch({ type: 'SET_FILTERED_DATA', payload: getFilteredData(_.cloneDeep(state)) })
71
+ loadAPIFilters(dashboardConfig.sharedFilters)
72
+ .then(newFilters => {
73
+ reloadURLData(newFilters)
74
+ })
75
+ .catch(e => {
76
+ console.error(e)
77
+ })
78
+ } else {
79
+ // TODO noftify of required fields
80
+ }
81
+ }
82
+
83
+ const handleOnChange = (index: number, value: string | string[]) => {
84
+ const newConfig = _.cloneDeep(dashboardConfig)
85
+ let newSharedFilters = changeFilterActive(index, value, newConfig.dashboard.sharedFilters, visualizationConfig)
86
+
87
+ if (hasDashboardApplyBehavior(dashboardConfig.visualizations)) {
88
+ const isAutoSelectFilter = visualizationConfig.autoLoad
89
+ const missingFilterSelections = newConfig.dashboard.sharedFilters.some(f => !f.active)
90
+ if (isAutoSelectFilter && !missingFilterSelections) {
91
+ // a dropdown has been selected that doesn't
92
+ // require the Go Button
93
+ loadAPIFilters(newSharedFilters).then(filters => {
94
+ reloadURLData(filters)
95
+ })
96
+ } else {
97
+ if (Array.isArray(value)) throw Error(`Cannot set active values on urlfilters. expected: ${JSON.stringify(value)} to be a single value.`)
98
+ newSharedFilters[index].queuedActive = value
99
+ // setData to empty object because we no longer have a data state.
100
+ dispatch({ type: 'SET_DATA', payload: {} })
101
+ dispatch({ type: 'SET_FILTERED_DATA', payload: {} })
102
+ loadAPIFilters(newSharedFilters)
103
+ }
104
+ } else {
105
+ if (newSharedFilters[index].apiFilter) {
106
+ reloadURLData(newSharedFilters)
107
+ } else {
108
+ const clonedState = _.cloneDeep(state)
109
+ clonedState.config.dashboard.sharedFilters = newSharedFilters
110
+ const newFilteredData = getFilteredData(clonedState)
111
+ dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
112
+ dispatch({ type: 'SET_SHARED_FILTERS', payload: newSharedFilters })
113
+ }
114
+ }
115
+ }
116
+ const [displayPanel, setDisplayPanel] = useState(true)
117
+ const onBackClick = () => {
118
+ setDisplayPanel(!displayPanel)
119
+ updateConfig({
120
+ ...visualizationConfig,
121
+ showEditorPanel: !displayPanel
122
+ })
123
+ }
124
+
125
+ return (
126
+ <Layout.VisualizationWrapper config={visualizationConfig} isEditor={isEditor} currentViewport={currentViewport}>
127
+ {isEditor && (
128
+ <Layout.Sidebar displayPanel={displayPanel} isDashboard={true} title={'Configure Dashboard Filters'} onBackClick={onBackClick}>
129
+ <DashboardFiltersEditor updateConfig={updateConfig} vizConfig={visualizationConfig} />
130
+ </Layout.Sidebar>
131
+ )}
132
+
133
+ <Layout.Responsive isEditor={isEditor}>
134
+ <div className={`cdc-dashboard-inner-container${isEditor ? ' is-editor' : ''} cove-component__content col-12`}>
135
+ <Filters show={visualizationConfig?.sharedFilterIndexes?.map(Number)} filters={dashboardConfig.dashboard.sharedFilters || []} apiFilterDropdowns={apiFilterDropdowns} handleOnChange={handleOnChange} />
136
+ {visualizationConfig.filterBehavior === FilterBehavior.Apply && !visualizationConfig.autoLoad && <button onClick={applyFilters}>GO!</button>}
137
+ </div>
138
+ </Layout.Responsive>
139
+ </Layout.VisualizationWrapper>
140
+ )
141
+ }
142
+
143
+ export default DashboardFiltersWrapper
@@ -0,0 +1,3 @@
1
+ export { default } from './DashboardFiltersWrapper'
2
+
3
+ export type { APIFilterDropdowns, DropdownOptions } from './DashboardFiltersWrapper'
@@ -71,6 +71,11 @@ export const DataDesignerModal: React.FC<DataDesignerModalProps> = ({ vizKey, ro
71
71
  }
72
72
  }
73
73
 
74
+ const setExpandCollapseAllButtons = (selection: boolean) => {
75
+ dispatch({ type: 'UPDATE_ROW', payload: { rowIndex, rowData: { expandCollapseAllButtons: selection } } })
76
+ setCanContinue(true)
77
+ }
78
+
74
79
  return (
75
80
  <Modal>
76
81
  <Modal.Content>
@@ -121,15 +126,10 @@ export const DataDesignerModal: React.FC<DataDesignerModalProps> = ({ vizKey, ro
121
126
  }}
122
127
  />
123
128
  ) : (
124
- <InputSelect
125
- options={Object.keys(config.datasets[configureData.dataKey]?.data[0] || {})}
126
- value={config.rows[rowIndex].multiVizColumn}
127
- label='Multi-Visualization Column'
128
- initial='--Select--'
129
- fieldName=''
130
- updateField={(section, subsection, fieldName, value) => setMultiVizColumn(value)}
131
- required
132
- />
129
+ <>
130
+ <InputSelect options={Object.keys(config.datasets[configureData.dataKey]?.data[0] || {})} value={config.rows[rowIndex].multiVizColumn} label='Multi-Visualization Column' initial='--Select--' updateField={(section, subsection, fieldName, value) => setMultiVizColumn(value)} required />
131
+ <CheckBox value={config.rows[rowIndex].expandCollapseAllButtons} label=' Add Expand/Collapse All buttons' fieldName='' updateField={(section, subsection, fieldName, value) => setExpandCollapseAllButtons(value)} />
132
+ </>
133
133
  )
134
134
  ) : (
135
135
  <></>
@@ -0,0 +1,20 @@
1
+ type ExpandCollapseButtonsProps = {
2
+ setAllExpanded: Function
3
+ }
4
+
5
+ const ExpandCollapseButtons: React.FC<ExpandCollapseButtonsProps> = ({ setAllExpanded }) => {
6
+ return (
7
+ <div className='d-block '>
8
+ <div className='d-flex flex-row-reverse mb-2'>
9
+ <button className='btn expand-collapse-buttons' onClick={() => setAllExpanded(false)}>
10
+ - Collapse All
11
+ </button>
12
+ <button className='btn expand-collapse-buttons mr-2' onClick={() => setAllExpanded(true)}>
13
+ + Expand All
14
+ </button>
15
+ </div>
16
+ </div>
17
+ )
18
+ }
19
+
20
+ export default ExpandCollapseButtons
@@ -2,19 +2,9 @@ import { useEffect, useContext } from 'react'
2
2
 
3
3
  import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
4
4
 
5
- // types
6
- import { type SharedFilter } from '../../types/SharedFilter'
7
- import { type DashboardConfig as Config } from '../../types/DashboardConfig'
8
- import { useGlobalContext } from '@cdc/core/components/GlobalContext'
9
-
10
- import Tooltip from '@cdc/core/components/ui/Tooltip'
11
- import Icon from '@cdc/core/components/ui/Icon'
12
- import Select from '@cdc/core/components/ui/Select'
13
-
14
5
  import './index.scss'
15
6
  import MultiConfigTabs from '../MultiConfigTabs'
16
7
  import { Tab } from '../../types/Tab'
17
- import FilterModal from './FilterModal'
18
8
  import _ from 'lodash'
19
9
 
20
10
  type HeaderProps = {
@@ -29,7 +19,7 @@ export const FilterBehavior = {
29
19
  }
30
20
 
31
21
  const Header = (props: HeaderProps) => {
32
- const tabs: Tab[] = ['Dashboard Description', 'Dashboard Filters', 'Data Table Settings', 'Dashboard Preview']
22
+ const tabs: Tab[] = ['Dashboard Description', 'Data Table Settings', 'Dashboard Preview']
33
23
  const { visualizationKey, subEditor } = props
34
24
  const { config, setParentConfig, tabSelected } = useContext(DashboardContext)
35
25
  if (!config) return null
@@ -41,8 +31,6 @@ const Header = (props: HeaderProps) => {
41
31
  dispatch({ type: 'SET_CONFIG', payload: newConfig })
42
32
  }
43
33
 
44
- const { overlay } = useGlobalContext()
45
-
46
34
  const changeConfigValue = (parentObj, key, value) => {
47
35
  let newConfig = { ...config }
48
36
  if (!newConfig[parentObj]) newConfig[parentObj] = {}
@@ -50,48 +38,6 @@ const Header = (props: HeaderProps) => {
50
38
  dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig] })
51
39
  }
52
40
 
53
- const addNewFilter = () => {
54
- let dashboardConfig = { ...config.dashboard }
55
-
56
- dashboardConfig.sharedFilters = dashboardConfig.sharedFilters || []
57
- const newFilter: SharedFilter = { key: 'Dashboard Filter ' + (dashboardConfig.sharedFilters.length + 1) }
58
- dashboardConfig.sharedFilters.push(newFilter)
59
-
60
- dispatch({ type: 'UPDATE_CONFIG', payload: [{ ...config, dashboard: dashboardConfig }] })
61
- }
62
-
63
- const removeFilter = index => {
64
- let dashboardConfig = { ...config.dashboard }
65
- let visualizations = { ...config.visualizations }
66
-
67
- dashboardConfig.sharedFilters?.splice(index, 1)
68
-
69
- Object.keys(visualizations).forEach(vizKey => {
70
- if (visualizations[vizKey].visualizationType === 'filter-dropdowns' && visualizations[vizKey].hide && visualizations[vizKey].hide.length > 0) {
71
- if (visualizations[vizKey].hide.indexOf(index) !== -1) {
72
- visualizations[vizKey].hide.splice(visualizations[vizKey].hide.indexOf(index), 1)
73
- }
74
- visualizations[vizKey].hide.forEach((hideIndex, i) => {
75
- if (hideIndex > index) {
76
- visualizations[vizKey].hide[i] = hideIndex - 1
77
- }
78
- })
79
- }
80
- })
81
-
82
- // Ensures URL filters refresh after filter removal
83
- if (dashboardConfig.datasets) {
84
- Object.keys(dashboardConfig.datasets).forEach(datasetKey => {
85
- dashboardConfig.datasets![datasetKey].runtimeDataUrl = ''
86
- })
87
- }
88
-
89
- const newConfig = { ...config, visualizations, dashboard: dashboardConfig }
90
- dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig] })
91
-
92
- overlay?.actions.toggleOverlay()
93
- }
94
-
95
41
  const convertStateToConfig = (type = 'JSON') => {
96
42
  let strippedState = JSON.parse(JSON.stringify(config))
97
43
  delete strippedState.newViz
@@ -166,48 +112,6 @@ const Header = (props: HeaderProps) => {
166
112
  </ul>
167
113
  <div className='heading-body'>
168
114
  {tabSelected === 'Dashboard Description' && <input type='text' className='description-input' placeholder='Type a dashboard description here.' defaultValue={config.dashboard?.description} onChange={e => changeConfigValue('dashboard', 'description', e.target.value)} />}
169
- {tabSelected === 'Dashboard Filters' && (
170
- <>
171
- {config.dashboard.sharedFilters &&
172
- config.dashboard.sharedFilters.map((sharedFilter, index) => (
173
- <span className='shared-filter-button' key={`shared-filter-${sharedFilter.key}`}>
174
- <a
175
- href='#'
176
- onClick={e => {
177
- e.preventDefault()
178
- overlay?.actions.openOverlay(<FilterModal index={index} config={config} filterState={sharedFilter} removeFilter={removeFilter} />)
179
- }}
180
- >
181
- {sharedFilter.key}
182
- </a>
183
- <button onClick={() => removeFilter(index)}>X</button>
184
- </span>
185
- ))}
186
- <button onClick={addNewFilter}>Add New Filter</button>
187
-
188
- <Select
189
- value={config.filterBehavior}
190
- fieldName='filterBehavior'
191
- label='Filter Behavior'
192
- initial='- Select Option -'
193
- onchange={e => {
194
- const newConfig = { ...config, filterBehavior: e.target.value }
195
- dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig] })
196
- }}
197
- options={Object.values(FilterBehavior)}
198
- tooltip={
199
- <Tooltip style={{ textTransform: 'none' }}>
200
- <Tooltip.Target>
201
- <Icon display='question' color='' style={{ marginLeft: '0.5rem' }} />
202
- </Tooltip.Target>
203
- <Tooltip.Content>
204
- <p>The Apply Button option changes the visualization when the user clicks "apply". The Filter Change option immediately changes the visualization when the selection is changed.</p>
205
- </Tooltip.Content>
206
- </Tooltip>
207
- }
208
- />
209
- </>
210
- )}
211
115
  {tabSelected === 'Data Table Settings' && (
212
116
  <>
213
117
  <div className='wrap'>
@@ -65,14 +65,14 @@ const Tab = ({ name, handleClick, tabs, index, active }) => {
65
65
 
66
66
  return (
67
67
  <li className='nav-item'>
68
- <a className={`edit nav-link${active ? ' active' : ''}`} aria-current={active ? 'page' : null} href='#' onClick={onClick}>
68
+ <div className={`edit nav-link${active ? ' active' : ''}`} aria-current={active ? 'page' : null} onClick={onClick}>
69
69
  {canMoveLeft && <button onClick={() => handleReorder(index, -1)}>{'<'}</button>}
70
70
  {editing ? <input type='text' defaultValue={name} onBlur={onBlur} ref={inputRef} /> : <>{name}</>}
71
71
  {canMoveRight && <button onClick={() => handleReorder(index, 1)}>{'>'}</button>}
72
72
  <button className='remove' onClick={handleRemove}>
73
73
  X
74
74
  </button>
75
- </a>
75
+ </div>
76
76
  </li>
77
77
  )
78
78
  }
@@ -95,9 +95,9 @@ const MultiConfigTabs = () => {
95
95
  <Tab key={tab + index} name={tab} tabs={tabs} index={index} handleClick={() => saveAndLoad(index)} active={index === activeTab} />
96
96
  ))}
97
97
  <li className='nav-item'>
98
- <a className='nav-link add' href='#' onClick={() => dispatch({ type: 'ADD_NEW_DASHBOARD' })}>
98
+ <button className='nav-link add' onClick={() => dispatch({ type: 'ADD_NEW_DASHBOARD' })}>
99
99
  +
100
- </a>
100
+ </button>
101
101
  </li>
102
102
  </ul>
103
103
  )
@@ -17,6 +17,7 @@ import { DataDesignerModal } from './DataDesignerModal'
17
17
  import { useGlobalContext } from '@cdc/core/components/GlobalContext'
18
18
  import { iconHash } from '../helpers/iconHash'
19
19
  import _ from 'lodash'
20
+ import { Visualization } from '@cdc/core/types/Visualization'
20
21
 
21
22
  type RowMenuProps = {
22
23
  rowIdx: number
@@ -104,9 +105,20 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
104
105
  }
105
106
 
106
107
  const deleteRow = () => {
107
- rows.splice(rowIdx, 1) // Just delete the row. Don't delete the instantiated widgets for now.
108
+ let newVisualizations = { ...config.visualizations }
109
+
110
+ //delete the instantiated widgets
111
+ if (rows[rowIdx] && rows[rowIdx].columns && rows[rowIdx].columns.length && config.visualizations) {
112
+ rows[rowIdx].columns.forEach(column => {
113
+ if (column.widget) {
114
+ delete newVisualizations[column.widget]
115
+ }
116
+ })
117
+ }
108
118
 
109
- updateConfig({ ...config, rows })
119
+ rows.splice(rowIdx, 1) // delete the row
120
+
121
+ updateConfig({ ...config, rows, visualizations: newVisualizations })
110
122
  }
111
123
 
112
124
  const layoutList = [
@@ -149,31 +161,52 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
149
161
  )
150
162
  }
151
163
 
152
- const Row = ({ row, idx: rowIdx, uuid }) => {
164
+ type RowProps = { row: ConfigRow; idx: number; uuid: number | string }
165
+
166
+ const Row: React.FC<RowProps> = ({ row, idx: rowIdx, uuid }) => {
153
167
  const { overlay } = useGlobalContext()
154
- return (
155
- <div className='builder-row' data-row-id={rowIdx}>
156
- <RowMenu rowIdx={rowIdx} />
157
- <div className='column-container'>
158
- <>
159
- <button
160
- title='Configure Data'
161
- className='btn btn-configure-row'
162
- onClick={() => {
163
- overlay?.actions.openOverlay(<DataDesignerModal rowIndex={rowIdx} />)
164
- }}
165
- >
166
- {iconHash['gear']}
167
- </button>
168
+ const dispatch = useContext(DashboardDispatchContext)
168
169
 
170
+ const configureFootnotes = () => {
171
+ if (!row.footnotesId) {
172
+ const type = 'footnotes'
173
+ const uid = type + Date.now()
174
+ const newVisualizationConfig = {
175
+ uid,
176
+ type,
177
+ visualizationType: type,
178
+ editing: true
179
+ }
180
+ dispatch({ type: 'ADD_FOOTNOTE', payload: { id: uid, rowIndex: rowIdx, config: newVisualizationConfig as Visualization } })
181
+ } else {
182
+ dispatch({ type: 'UPDATE_VISUALIZATION', payload: { vizKey: row.footnotesId, configureData: { editing: true } } })
183
+ }
184
+ }
185
+ return (
186
+ <>
187
+ <div className='builder-row' data-row-id={rowIdx}>
188
+ <RowMenu rowIdx={rowIdx} />
189
+ <button
190
+ title='Configure Data'
191
+ className='btn btn-configure-row'
192
+ onClick={() => {
193
+ overlay?.actions.openOverlay(<DataDesignerModal rowIndex={rowIdx} />)
194
+ }}
195
+ >
196
+ {iconHash['gear']}
197
+ </button>
198
+ <div className='column-container'>
169
199
  {row.columns
170
200
  .filter(column => column.width)
171
201
  .map((column, colIdx) => (
172
202
  <Column data={column} key={`row-${uuid}-col-${colIdx}`} rowIdx={rowIdx} colIdx={colIdx} />
173
203
  ))}
174
- </>
204
+ </div>
205
+ <button className='btn btn-primary footnotes' onClick={configureFootnotes}>
206
+ {row.footnotesId ? 'Edit' : 'Add'} Footnotes
207
+ </button>
175
208
  </div>
176
- </div>
209
+ </>
177
210
  )
178
211
  }
179
212
 
@@ -1,7 +1,5 @@
1
- import { useContext } from 'react'
2
- import { DashboardDispatchContext } from '../../DashboardContext'
3
1
  import { ConfigRow } from '../../types/ConfigRow'
4
- import { Visualization } from '@cdc/core/types/Visualization'
2
+ import { AnyVisualization } from '@cdc/core/types/Visualization'
5
3
  import { getIcon } from '../../helpers/iconHash'
6
4
  import './toggle-style.css'
7
5
  import _ from 'lodash'
@@ -9,7 +7,7 @@ import _ from 'lodash'
9
7
  type ToggleProps = {
10
8
  active: number
11
9
  row: ConfigRow
12
- visualizations: Record<string, Visualization>
10
+ visualizations: Record<string, AnyVisualization>
13
11
  setToggled: (colIndex: number) => void
14
12
  }
15
13
  const Toggle: React.FC<ToggleProps> = ({ active, row, visualizations, setToggled }) => {
@@ -1,56 +1,104 @@
1
1
  import DataTableStandAlone from '@cdc/core/components/DataTable/DataTableStandAlone'
2
- import React, { MouseEventHandler, useContext, useMemo } from 'react'
2
+ import React, { useContext, useMemo } from 'react'
3
3
  import Toggle from './Toggle'
4
4
  import _ from 'lodash'
5
5
  import { ConfigRow } from '../types/ConfigRow'
6
- import CdcMap from '@cdc/map'
7
6
  import CdcChart from '@cdc/chart'
8
7
  import CdcDataBite from '@cdc/data-bite'
8
+ import CdcMap from '@cdc/map'
9
9
  import CdcWaffleChart from '@cdc/waffle-chart'
10
10
  import CdcMarkupInclude from '@cdc/markup-include'
11
11
  import CdcFilteredText from '@cdc/filtered-text'
12
- import Filters, { APIFilterDropdowns } from './Filters'
13
- import { FilterBehavior } from './Header/Header'
12
+ import DashboardSharedFilters, { APIFilterDropdowns } from './DashboardFilters'
14
13
  import { DashboardContext } from '../DashboardContext'
15
14
  import { ViewPort } from '@cdc/core/types/ViewPort'
16
- import { getVizConfig } from '../helpers/getVizConfig'
15
+ import { getFootnotesVizConfig, getVizConfig } from '../helpers/getVizConfig'
17
16
  import { TableConfig } from '@cdc/core/components/DataTable/types/TableConfig'
17
+ import FootnotesStandAlone from '@cdc/core/components/Footnotes/FootnotesStandAlone'
18
+ import CollapsibleVisualizationRow from './CollapsibleVisualizationRow'
19
+ import { DashboardFilters } from '../types/DashboardFilters'
20
+ import { hasDashboardApplyBehavior } from '../helpers/hasDashboardApplyBehavior'
21
+
22
+ type VisualizationWrapperProps = {
23
+ allExpanded: boolean
24
+ children: React.ReactNode
25
+ currentViewport: ViewPort
26
+ groupName: string
27
+ row: ConfigRow
28
+ }
29
+
30
+ const VisualizationWrapper: React.FC<VisualizationWrapperProps> = ({ allExpanded, currentViewport, groupName, row, children }) => {
31
+ return row.expandCollapseAllButtons ? (
32
+ <div className='collapsable-multiviz-container'>
33
+ <CollapsibleVisualizationRow allExpanded={allExpanded} fontSize={'26px'} groupName={groupName} currentViewport={currentViewport}>
34
+ {children}
35
+ </CollapsibleVisualizationRow>
36
+ </div>
37
+ ) : (
38
+ <>
39
+ <h3>{groupName}</h3>
40
+ {children}
41
+ </>
42
+ )
43
+ }
18
44
 
19
45
  type VizRowProps = {
46
+ allExpanded: boolean
20
47
  filteredDataOverride?: Object[]
48
+ groupName: string
21
49
  row: ConfigRow
22
50
  rowIndex: number
23
51
  setSharedFilter: Function
24
52
  updateChildConfig: Function
25
- applyFilters: MouseEventHandler<HTMLButtonElement>
26
53
  apiFilterDropdowns: APIFilterDropdowns
27
- handleOnChange: Function
28
54
  currentViewport: ViewPort
29
55
  }
30
56
 
31
- const VisualizationRow: React.FC<VizRowProps> = ({ filteredDataOverride, row, rowIndex: index, setSharedFilter, updateChildConfig, applyFilters, apiFilterDropdowns, handleOnChange, currentViewport }) => {
57
+ const VisualizationRow: React.FC<VizRowProps> = ({ allExpanded, filteredDataOverride, groupName, row, rowIndex: index, setSharedFilter, updateChildConfig, applyFilters, apiFilterDropdowns, handleOnChange, currentViewport }) => {
32
58
  const { config, filteredData: dashboardFilteredData, data: rawData } = useContext(DashboardContext)
33
59
  const [show, setShow] = React.useState(row.columns.map((col, i) => i === 0))
34
60
  const setToggled = (colIndex: number) => {
35
61
  setShow(show.map((_, i) => i === colIndex))
36
62
  }
37
63
  const inNoDataState = useMemo(() => {
38
- const vals = Object.values(rawData)
64
+ const vals = Object.values(rawData).flatMap(val => val)
39
65
  if (!vals.length) return true
40
66
  return vals.some(val => val === undefined)
41
67
  }, [rawData])
42
- const GoButton = ({ autoLoad }: { autoLoad?: boolean }) => {
43
- if (config.filterBehavior === FilterBehavior.Apply && !autoLoad) {
44
- return <button onClick={applyFilters}>GO!</button>
68
+
69
+ const footnotesConfig = useMemo(() => {
70
+ if (row.footnotesId) {
71
+ const footnoteConfig = getFootnotesVizConfig(row.footnotesId, index, config)
72
+ if (row.multiVizColumn && filteredDataOverride) {
73
+ const vizCategory = filteredDataOverride[0][row.multiVizColumn]
74
+ // the multiViz filtering filtering is applied after the dashboard filters
75
+ const categoryFootnote = footnoteConfig.formattedData.filter(d => d[row.multiVizColumn] === vizCategory)
76
+ footnoteConfig.formattedData = categoryFootnote
77
+ }
78
+ return footnoteConfig
45
79
  }
46
80
  return null
81
+ }, [config, row, rawData, dashboardFilteredData])
82
+
83
+ const applyButtonNotClicked = (vizConfig: DashboardFilters): boolean => {
84
+ const dashboardFilters = Object.values(config.visualizations).filter(v => v.type === 'dashboardFilters') as DashboardFilters[]
85
+ const applyFilters = dashboardFilters.filter(v => !v.autoLoad).flatMap(v => v.sharedFilterIndexes)
86
+ if (hasDashboardApplyBehavior(config.visualizations) && vizConfig.autoLoad) {
87
+ return applyFilters.some(index => {
88
+ const { queuedActive, active } = config.dashboard.sharedFilters[index]
89
+ if (!active && !queuedActive) return true
90
+ if (!queuedActive) return false
91
+ return queuedActive !== active
92
+ })
93
+ }
94
+ return false
47
95
  }
48
96
  return (
49
- <div className={`dashboard-row ${row.equalHeight ? 'equal-height' : ''} ${row.toggle ? 'toggle' : ''}`} key={`row__${index}`}>
97
+ <div className={`row mb-5 ${row.equalHeight ? 'equal-height' : ''} ${row.toggle ? 'toggle' : ''}`} key={`row__${index}`}>
50
98
  {row.toggle && <Toggle row={row} visualizations={config.visualizations} active={show.indexOf(true)} setToggled={setToggled} />}
51
99
  {row.columns.map((col, colIndex) => {
52
100
  if (col.width) {
53
- if (!col.widget) return <div key={`row__${index}__col__${colIndex}`} className={`dashboard-col dashboard-col-${col.width}`}></div>
101
+ if (!col.widget) return <div key={`row__${index}__col__${colIndex}`} className={`col-md-${col.width}`}></div>
54
102
 
55
103
  const visualizationConfig = getVizConfig(col.widget, index, config, rawData, dashboardFilteredData)
56
104
  if (filteredDataOverride) {
@@ -67,12 +115,15 @@ const VisualizationRow: React.FC<VizRowProps> = ({ filteredDataOverride, row, ro
67
115
  {visualizationConfig.dataKey} (Go to Table)
68
116
  </a>
69
117
  )
70
- const hideFilter = visualizationConfig.autoLoad && inNoDataState
118
+ const hideFilter = inNoDataState && visualizationConfig.type === 'dashboardFilters' && applyButtonNotClicked(visualizationConfig)
71
119
 
72
120
  const shouldShow = row.toggle === undefined || (row.toggle && show[colIndex])
121
+
122
+ const body = <></>
123
+
73
124
  return (
74
- <React.Fragment key={`vis__${index}__${colIndex}`}>
75
- <div className={`dashboard-col dashboard-col-${col.width} ${!shouldShow ? 'hidden-toggle' : ''}`}>
125
+ <div key={`vis__${index}__${colIndex}`} className={`col-${col.width} ${!shouldShow ? 'd-none' : ''}`}>
126
+ <VisualizationWrapper allExpanded={allExpanded} currentViewport={currentViewport} groupName={groupName} row={row}>
76
127
  {visualizationConfig.type === 'chart' && (
77
128
  <CdcChart
78
129
  key={col.widget}
@@ -153,11 +204,16 @@ const VisualizationRow: React.FC<VizRowProps> = ({ filteredDataOverride, row, ro
153
204
  configUrl={undefined}
154
205
  />
155
206
  )}
156
- {visualizationConfig.type === 'filter-dropdowns' && !hideFilter && (
157
- <React.Fragment key={col.widget}>
158
- <Filters hide={visualizationConfig.hide} filters={config.dashboard.sharedFilters} apiFilterDropdowns={apiFilterDropdowns} handleOnChange={handleOnChange} />
159
- <GoButton autoLoad={visualizationConfig.autoLoad} />
160
- </React.Fragment>
207
+ {visualizationConfig.type === 'dashboardFilters' && !hideFilter && (
208
+ <DashboardSharedFilters
209
+ setConfig={newConfig => {
210
+ updateChildConfig(col.widget, newConfig)
211
+ }}
212
+ key={col.widget}
213
+ visualizationConfig={visualizationConfig as DashboardFilters}
214
+ apiFilterDropdowns={apiFilterDropdowns}
215
+ currentViewport={currentViewport}
216
+ />
161
217
  )}
162
218
  {visualizationConfig.type === 'table' && (
163
219
  <DataTableStandAlone
@@ -170,12 +226,14 @@ const VisualizationRow: React.FC<VizRowProps> = ({ filteredDataOverride, row, ro
170
226
  viewport={currentViewport}
171
227
  />
172
228
  )}
173
- </div>
174
- </React.Fragment>
229
+ {visualizationConfig.type === 'footnotes' && <FootnotesStandAlone key={col.widget} visualizationKey={col.widget} config={visualizationConfig} viewport={currentViewport} />}
230
+ </VisualizationWrapper>
231
+ </div>
175
232
  )
176
233
  }
177
234
  return <React.Fragment key={`vis__${index}__${colIndex}`}></React.Fragment>
178
235
  })}
236
+ {row.footnotesId ? <FootnotesStandAlone isEditor={false} visualizationKey={row.footnotesId} config={footnotesConfig} viewport={currentViewport} /> : null}
179
237
  </div>
180
238
  )
181
239
  }