@cdc/dashboard 4.26.3 → 4.26.5

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 (151) hide show
  1. package/CONFIG.md +219 -0
  2. package/README.md +60 -20
  3. package/dist/cdcdashboard-CY9IcPSi.es.js +6 -0
  4. package/dist/cdcdashboard-DlpiY3fQ.es.js +4 -0
  5. package/dist/cdcdashboard.js +61559 -58048
  6. package/examples/__data__/data-2.json +6 -0
  7. package/examples/__data__/data.json +6 -0
  8. package/examples/dashboard-conditions-filters-incomplete.json +221 -0
  9. package/examples/dashboard-missing-datasets-multi.json +174 -0
  10. package/examples/dashboard-missing-datasets-single.json +121 -0
  11. package/examples/dashboard-multi-dashboard-version-regression.json +146 -0
  12. package/examples/dashboard-shared-filter-row-delete-cleanup.json +186 -0
  13. package/examples/dashboard-stale-dataset-keys.json +181 -0
  14. package/examples/dashboard-tiered-filter-regression.json +190 -0
  15. package/examples/legend-issue.json +1 -1
  16. package/examples/minimal-example.json +34 -0
  17. package/examples/private/cfa-dashboard.json +651 -0
  18. package/examples/private/data-bite-wrap.json +6936 -0
  19. package/examples/private/dengue.json +4640 -0
  20. package/examples/private/link_to_file.json +16662 -0
  21. package/examples/private/multi-dash-fix.json +16963 -0
  22. package/examples/private/versions.json +41612 -0
  23. package/examples/sankey.json +3 -3
  24. package/examples/test-api-filter-reset.json +4 -4
  25. package/examples/tp5-test.json +86 -4
  26. package/examples/us-map-filter-example.json +1074 -0
  27. package/package.json +9 -9
  28. package/src/CdcDashboard.tsx +6 -2
  29. package/src/CdcDashboardComponent.tsx +179 -88
  30. package/src/DashboardCopyPasteContext.test.tsx +33 -0
  31. package/src/DashboardCopyPasteContext.tsx +48 -0
  32. package/src/_stories/Dashboard.EditorRegression.stories.tsx +72 -0
  33. package/src/_stories/Dashboard.Regression.stories.tsx +196 -0
  34. package/src/_stories/Dashboard.Zoom.stories.tsx +88 -0
  35. package/src/_stories/Dashboard.smoke.stories.tsx +33 -0
  36. package/src/_stories/Dashboard.stories.tsx +337 -2
  37. package/src/_stories/FilteredTextMigrationComparison.stories.tsx +87 -0
  38. package/src/_stories/_mock/dashboard-data-driven-colors.json +171 -0
  39. package/src/_stories/_mock/tp5-test.json +86 -5
  40. package/src/components/Column.test.tsx +176 -0
  41. package/src/components/Column.tsx +214 -13
  42. package/src/components/DashboardConditionModal.test.tsx +420 -0
  43. package/src/components/DashboardConditionModal.tsx +367 -0
  44. package/src/components/DashboardConditionSummary.tsx +59 -0
  45. package/src/components/DashboardEditors.tsx +23 -0
  46. package/src/components/DashboardFilters/DashboardFilters.test.tsx +267 -0
  47. package/src/components/DashboardFilters/DashboardFilters.tsx +193 -172
  48. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.test.tsx +164 -0
  49. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +46 -6
  50. package/src/components/DashboardFilters/DashboardFiltersEditor/components/APIModal.tsx +5 -3
  51. package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +59 -58
  52. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.test.tsx +304 -0
  53. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +43 -36
  54. package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +2 -2
  55. package/src/components/DashboardFilters/DashboardFiltersWrapper.test.tsx +142 -0
  56. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +32 -27
  57. package/src/components/DashboardFilters/dashboardfilter.styles.css +42 -27
  58. package/src/components/DataDesignerModal.tsx +2 -1
  59. package/src/components/ExpandCollapseButtons.tsx +6 -4
  60. package/src/components/Grid.tsx +12 -7
  61. package/src/components/Header/Header.tsx +36 -17
  62. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +141 -140
  63. package/src/components/Row.test.tsx +228 -0
  64. package/src/components/Row.tsx +104 -28
  65. package/src/components/VisualizationRow.test.tsx +396 -0
  66. package/src/components/VisualizationRow.tsx +177 -51
  67. package/src/components/VisualizationsPanel/VisualizationsPanel.test.tsx +49 -0
  68. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +14 -13
  69. package/src/components/Widget/Widget.test.tsx +218 -0
  70. package/src/components/Widget/Widget.tsx +123 -20
  71. package/src/components/Widget/widget.styles.css +58 -14
  72. package/src/components/dashboard-condition-modal.css +76 -0
  73. package/src/components/dashboard-condition-summary.css +87 -0
  74. package/src/data/initial-state.js +1 -0
  75. package/src/helpers/addValuesToDashboardFilters.ts +3 -5
  76. package/src/helpers/addVisualization.ts +17 -4
  77. package/src/helpers/cloneDashboardWidget.ts +127 -0
  78. package/src/helpers/dashboardColumnWidgets.ts +99 -0
  79. package/src/helpers/dashboardConditionUi.ts +47 -0
  80. package/src/helpers/dashboardConditions.ts +200 -0
  81. package/src/helpers/dashboardFilterTargets.ts +156 -0
  82. package/src/helpers/filterData.ts +4 -9
  83. package/src/helpers/filterVisibility.ts +20 -0
  84. package/src/helpers/formatConfigBeforeSave.ts +2 -2
  85. package/src/helpers/getFilteredData.ts +18 -5
  86. package/src/helpers/getUpdateConfig.ts +43 -12
  87. package/src/helpers/getVizRowColumnLocator.ts +11 -1
  88. package/src/helpers/iconHash.tsx +9 -3
  89. package/src/helpers/mapDataToConfig.ts +31 -29
  90. package/src/helpers/reloadURLHelpers.ts +25 -5
  91. package/src/helpers/removeDashboardFilter.ts +33 -33
  92. package/src/helpers/tests/addVisualization.test.ts +53 -9
  93. package/src/helpers/tests/cloneDashboardWidget.test.ts +136 -0
  94. package/src/helpers/tests/dashboardColumnWidgets.test.ts +99 -0
  95. package/src/helpers/tests/dashboardConditionUi.test.ts +41 -0
  96. package/src/helpers/tests/dashboardConditions.test.ts +428 -0
  97. package/src/helpers/tests/formatConfigBeforeSave.test.ts +51 -0
  98. package/src/helpers/tests/getFilteredData.test.ts +265 -86
  99. package/src/helpers/tests/getUpdateConfig.test.ts +338 -0
  100. package/src/helpers/tests/reloadURLHelpers.test.ts +394 -238
  101. package/src/index.tsx +6 -3
  102. package/src/scss/grid.scss +281 -22
  103. package/src/scss/main.scss +215 -64
  104. package/src/store/dashboard.actions.ts +17 -4
  105. package/src/store/dashboard.reducer.test.ts +538 -0
  106. package/src/store/dashboard.reducer.ts +136 -22
  107. package/src/test/CdcDashboard.test.jsx +24 -0
  108. package/src/test/CdcDashboard.test.tsx +148 -0
  109. package/src/test/CdcDashboardComponent.test.tsx +935 -2
  110. package/src/types/ConfigRow.ts +15 -0
  111. package/src/types/DashboardFilters.ts +4 -0
  112. package/src/types/SharedFilter.ts +2 -0
  113. package/tests/fixtures/dashboard-config-with-metadata.json +1 -1
  114. package/dist/cdcdashboard-vr9HZwRt.es.js +0 -6
  115. package/examples/DEV-6574.json +0 -2224
  116. package/examples/api-dashboard-data.json +0 -272
  117. package/examples/api-dashboard-years.json +0 -11
  118. package/examples/api-geographies-data.json +0 -11
  119. package/examples/chart-data.json +0 -5409
  120. package/examples/custom/css/respiratory.css +0 -236
  121. package/examples/custom/js/respiratory.js +0 -242
  122. package/examples/default-data.json +0 -368
  123. package/examples/default-filter-control.json +0 -209
  124. package/examples/default-multi-dataset-shared-filter.json +0 -1729
  125. package/examples/default-multi-dataset.json +0 -506
  126. package/examples/ed-visits-county-file.json +0 -402
  127. package/examples/filters/Alabama.json +0 -72
  128. package/examples/filters/Alaska.json +0 -1737
  129. package/examples/filters/Arkansas.json +0 -4713
  130. package/examples/filters/California.json +0 -212
  131. package/examples/filters/Colorado.json +0 -1500
  132. package/examples/filters/Connecticut.json +0 -559
  133. package/examples/filters/Delaware.json +0 -63
  134. package/examples/filters/DistrictofColumbia.json +0 -63
  135. package/examples/filters/Florida.json +0 -4217
  136. package/examples/filters/States.json +0 -146
  137. package/examples/state-level.json +0 -90136
  138. package/examples/state-points.json +0 -10474
  139. package/examples/temp-example-data.json +0 -130
  140. package/examples/test-dashboard-simple.json +0 -503
  141. package/examples/test-example.json +0 -752
  142. package/examples/test-file.json +0 -147
  143. package/examples/test.json +0 -752
  144. package/examples/testing.json +0 -94456
  145. /package/examples/{data → __data__}/data-with-metadata.json +0 -0
  146. /package/examples/{legend-issue-data.json → __data__/legend-issue-data.json} +0 -0
  147. /package/examples/api-test/{categories.json → __data__/categories.json} +0 -0
  148. /package/examples/api-test/{chart-data.json → __data__/chart-data.json} +0 -0
  149. /package/examples/api-test/{topics.json → __data__/topics.json} +0 -0
  150. /package/examples/api-test/{years.json → __data__/years.json} +0 -0
  151. /package/src/_stories/{Dashboard.Pages.stories.tsx → Dashboard.Pages.smoke.stories.tsx} +0 -0
@@ -14,10 +14,10 @@ import { hasDashboardApplyBehavior } from '../../helpers/hasDashboardApplyBehavi
14
14
  import * as apiFilterHelpers from '../../helpers/apiFilterHelpers'
15
15
  import * as filterResetHelpers from '../../helpers/filterResetHelpers'
16
16
  import { applyQueuedActive } from '@cdc/core/components/Filters/helpers/applyQueuedActive'
17
- import './dashboardfilter.styles.css'
18
17
  import { updateChildFilters } from '../../helpers/updateChildFilters'
19
18
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
20
19
  import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
20
+ import { hasVisibleDashboardFiltersForIndexes } from '../../helpers/filterVisibility'
21
21
 
22
22
  type SubOptions = { subOptions?: Record<'value' | 'text', string>[] }
23
23
 
@@ -193,7 +193,7 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
193
193
  }
194
194
  }
195
195
  const newFilteredData = getFilteredData(clonedState)
196
- dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
196
+ dispatch({ type: 'SET_FILTERED_DATA', payload: { filteredData: newFilteredData } })
197
197
 
198
198
  publishAnalyticsEvent({
199
199
  vizType: dashboardConfig.type,
@@ -260,7 +260,7 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
260
260
  newSharedFilters[index].queuedActive = value
261
261
 
262
262
  // Don't clear data immediately - keep existing data until new data loads
263
- // Only update the filter dropdowns and prepare for reload
263
+ // Only update the dashboard filters and prepare for reload
264
264
  setAPIFilterDropdowns(loadingFilterMemo)
265
265
  loadAPIFilters(newSharedFilters, loadingFilterMemo, undefined, undefined, isStale)
266
266
  }
@@ -280,7 +280,7 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
280
280
  }
281
281
  }
282
282
  const newFilteredData = getFilteredData(clonedState)
283
- dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
283
+ dispatch({ type: 'SET_FILTERED_DATA', payload: { filteredData: newFilteredData } })
284
284
  dispatch({ type: 'SET_SHARED_FILTERS', payload: updatedFilters })
285
285
  }
286
286
  }
@@ -294,13 +294,28 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
294
294
  })
295
295
  }
296
296
 
297
- // if all of the filters are hidden filters don't display the VisualizationWrapper
298
- const filters = visualizationConfig?.sharedFilterIndexes
299
- ?.map(Number)
300
- ?.map(filterIndex => dashboardConfig.dashboard.sharedFilters[filterIndex])
301
-
302
- const displayNone = filters?.length ? filters.every(filter => filter.showDropdown === false) : false
303
- if (displayNone && !isEditor) return <></>
297
+ const hasVisibleFilterControls = hasVisibleDashboardFiltersForIndexes(
298
+ dashboardConfig.dashboard.sharedFilters,
299
+ visualizationConfig?.sharedFilterIndexes
300
+ )
301
+ const filterControls = (
302
+ <Filters
303
+ show={visualizationConfig?.sharedFilterIndexes?.map(Number)}
304
+ filters={updateChildFilters(dashboardConfig.dashboard.sharedFilters, state.data) || []}
305
+ apiFilterDropdowns={apiFilterDropdowns}
306
+ handleOnChange={handleOnChange}
307
+ showSubmit={visualizationConfig.filterBehavior === FilterBehavior.Apply && !visualizationConfig.autoLoad}
308
+ filterIntro={visualizationConfig.filterIntro}
309
+ applyFilters={applyFilters}
310
+ applyFiltersButtonText={visualizationConfig.applyFiltersButtonText}
311
+ handleReset={
312
+ visualizationConfig.filterBehavior === FilterBehavior.Apply && (visualizationConfig.showClearButton ?? true)
313
+ ? handleReset
314
+ : undefined
315
+ }
316
+ />
317
+ )
318
+ if (!hasVisibleFilterControls && !isEditor) return <></>
304
319
  return (
305
320
  <VisualizationWrapper config={visualizationConfig} isEditor={isEditor} currentViewport={currentViewport}>
306
321
  {isEditor && (
@@ -314,28 +329,18 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
314
329
  </Sidebar>
315
330
  )}
316
331
 
317
- {!displayNone && (
332
+ {hasVisibleFilterControls && (
318
333
  <Responsive isEditor={isEditor}>
319
334
  <div
320
335
  className={`${
321
336
  isEditor ? ' is-editor' : ''
322
337
  } cove-visualization__inner cove-visualization__body col-12 cove-dashboard-filters-container`}
323
338
  >
324
- <Filters
325
- show={visualizationConfig?.sharedFilterIndexes?.map(Number)}
326
- filters={updateChildFilters(dashboardConfig.dashboard.sharedFilters, state.data) || []}
327
- apiFilterDropdowns={apiFilterDropdowns}
328
- handleOnChange={handleOnChange}
329
- showSubmit={visualizationConfig.filterBehavior === FilterBehavior.Apply && !visualizationConfig.autoLoad}
330
- applyFilters={applyFilters}
331
- applyFiltersButtonText={visualizationConfig.applyFiltersButtonText}
332
- handleReset={
333
- visualizationConfig.filterBehavior === FilterBehavior.Apply &&
334
- (visualizationConfig.showClearButton ?? true)
335
- ? handleReset
336
- : undefined
337
- }
338
- />
339
+ {visualizationConfig.visual?.grayBackground ? (
340
+ <div className='cdc-callout cdc-callout--dashboard-filters'>{filterControls}</div>
341
+ ) : (
342
+ filterControls
343
+ )}
339
344
  </div>
340
345
  </Responsive>
341
346
  )}
@@ -1,27 +1,42 @@
1
- .cove-dashboard-filters-container {
2
- :is(label) {
3
- font-size: var(--filter-label-font-size);
4
- font-weight: 700;
5
- }
6
- .btn {
7
- align-self: flex-end;
8
- /* this is the height that is defined for the .form-control class in _forms.scss in bootstrap. */
9
- height: calc(1.5em + 0.75rem + 2px);
10
- }
11
- .loading-filter {
12
- position: relative;
13
- .spinner-border {
14
- height: 1.5rem;
15
- position: absolute;
16
- right: 10%;
17
- top: 55%;
18
- width: 1.5rem;
19
- }
20
- }
21
- :is(select):disabled {
22
- background-color: var(--lightestGray);
23
- & > :is(option) {
24
- color: var(--darkGray);
25
- }
26
- }
27
- }
1
+ .dashboard-filters__form {
2
+ align-items: flex-end;
3
+ display: flex;
4
+ flex-wrap: wrap;
5
+ gap: 1rem 1.5rem;
6
+ }
7
+
8
+ .dashboard-filters__field {
9
+ margin: 0;
10
+ }
11
+
12
+ .cove-dashboard-filters-container {
13
+ .cdc-callout--dashboard-filters {
14
+ --cdc-callout-background: #f4f8fa;
15
+ }
16
+
17
+ :is(label) {
18
+ font-size: var(--filter-label-font-size);
19
+ font-weight: 700;
20
+ }
21
+ .btn {
22
+ align-self: flex-end;
23
+ /* this is the height that is defined for the .form-control class in _forms.scss in bootstrap. */
24
+ height: calc(1.5em + 0.75rem + 2px);
25
+ }
26
+ .loading-filter {
27
+ position: relative;
28
+ .spinner-border {
29
+ height: 1.5rem;
30
+ position: absolute;
31
+ right: 10%;
32
+ top: 55%;
33
+ width: 1.5rem;
34
+ }
35
+ }
36
+ :is(select):disabled {
37
+ background-color: var(--lightestGray);
38
+ & > :is(option) {
39
+ color: var(--darkGray);
40
+ }
41
+ }
42
+ }
@@ -11,6 +11,7 @@ import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
11
11
  import DataTransform from '@cdc/core/helpers/DataTransform'
12
12
  import { ConfigureData } from '@cdc/core/types/ConfigureData'
13
13
  import Icon from '@cdc/core/components/ui/Icon'
14
+ import { getColumnWidgetKeys } from '../helpers/dashboardColumnWidgets'
14
15
 
15
16
  type DataDesignerModalProps = {
16
17
  rowIndex: number
@@ -79,7 +80,7 @@ export const DataDesignerModal: React.FC<DataDesignerModalProps> = ({ vizKey, ro
79
80
  }
80
81
 
81
82
  const removeDatasetsFromVisualizations = () => {
82
- const columnVisualizations = config.rows[rowIndex].columns.map(column => column.widget).filter(Boolean)
83
+ const columnVisualizations = config.rows[rowIndex].columns.flatMap(column => getColumnWidgetKeys(column))
83
84
  columnVisualizations.forEach(currentVisualizationKey => {
84
85
  dispatch({ type: 'RESET_VISUALIZATION', payload: { vizKey: currentVisualizationKey } })
85
86
  })
@@ -1,3 +1,5 @@
1
+ import Button from '@cdc/core/components/elements/Button'
2
+
1
3
  type ExpandCollapseButtonsProps = {
2
4
  setAllExpanded: Function
3
5
  }
@@ -6,12 +8,12 @@ const ExpandCollapseButtons: React.FC<ExpandCollapseButtonsProps> = ({ setAllExp
6
8
  return (
7
9
  <div className='d-block '>
8
10
  <div className='d-flex flex-row-reverse mb-2'>
9
- <button className='btn expand-collapse-buttons' onClick={() => setAllExpanded(false)}>
11
+ <Button variant='light' onClick={() => setAllExpanded(false)}>
10
12
  - Collapse All
11
- </button>
12
- <button className='btn expand-collapse-buttons me-2' onClick={() => setAllExpanded(true)}>
13
+ </Button>
14
+ <Button variant='light' className='me-2' onClick={() => setAllExpanded(true)}>
13
15
  + Expand All
14
- </button>
16
+ </Button>
15
17
  </div>
16
18
  </div>
17
19
  )
@@ -1,32 +1,37 @@
1
1
  import React, { useContext } from 'react'
2
2
  import Row from './Row'
3
+ import Button from '@cdc/core/components/elements/Button'
3
4
 
4
5
  import { DashboardContext, DashboardDispatchContext } from '../DashboardContext'
5
6
  import { ConfigRow } from '../types/ConfigRow'
7
+ import { createCoveId } from '@cdc/core/helpers/createCoveId'
6
8
 
7
9
  const Grid = () => {
8
10
  const { config } = useContext(DashboardContext)
11
+ const dispatch = useContext(DashboardDispatchContext)
9
12
  if (!config) return null
10
13
  const rows = config.rows
11
- const dispatch = useContext(DashboardDispatchContext)
12
14
  const updateConfig = config => dispatch({ type: 'UPDATE_CONFIG', payload: [config] })
13
15
  const addRow = () => {
14
- const blankRow: Partial<ConfigRow> = { columns: [{ width: 12 }] }
16
+ const existingRowUuids = rows?.flatMap(row => (row.uuid === undefined ? [] : [row.uuid]))
17
+ const blankRow: Partial<ConfigRow> = {
18
+ columns: [{ width: 12 }],
19
+ uuid: createCoveId('row', { existingIds: existingRowUuids })
20
+ }
15
21
  updateConfig({
16
22
  ...config,
17
- rows: [...rows, blankRow],
18
- uuid: Date.now()
23
+ rows: [...rows, blankRow]
19
24
  })
20
25
  }
21
26
 
22
27
  return (
23
28
  <div className='builder-grid'>
24
29
  {(rows || []).map((row, idx) => (
25
- <Row row={row} idx={idx} uuid={row.uuid} key={idx} />
30
+ <Row row={row} idx={idx} uuid={row.uuid ?? idx} key={idx} />
26
31
  ))}
27
- <button className='btn btn-primary col' onClick={addRow}>
32
+ <Button variant='primary' className='col' onClick={addRow}>
28
33
  Add Row
29
- </button>
34
+ </Button>
30
35
  </div>
31
36
  )
32
37
  }
@@ -13,6 +13,8 @@ type HeaderProps = {
13
13
  visualizationKey?: string
14
14
  }
15
15
 
16
+ type DownloadImageMode = 'off' | 'button' | 'link'
17
+
16
18
  const Header = (props: HeaderProps) => {
17
19
  const tabs: Tab[] = ['Dashboard Description', 'Data Table Settings', 'Dashboard Preview']
18
20
  const { visualizationKey, subEditor } = props
@@ -51,7 +53,7 @@ const Header = (props: HeaderProps) => {
51
53
  return acc
52
54
  }, {})
53
55
 
54
- dispatch({ type: 'SET_DATA', payload: sampleDataRemoved })
56
+ dispatch({ type: 'SET_DATA', payload: { data: sampleDataRemoved } })
55
57
  }
56
58
  }
57
59
 
@@ -62,6 +64,18 @@ const Header = (props: HeaderProps) => {
62
64
  dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig] })
63
65
  }
64
66
 
67
+ const getDownloadImageMode = (): DownloadImageMode => {
68
+ if (!config.table?.downloadImageButton) return 'off'
69
+ return config.table.downloadImageButtonStyle === 'link' ? 'link' : 'button'
70
+ }
71
+
72
+ const changeDownloadImageMode = (mode: DownloadImageMode) => {
73
+ const newConfig = { ...config, table: { ...(config.table || {}) } }
74
+ newConfig.table.downloadImageButton = mode !== 'off'
75
+ if (mode !== 'off') newConfig.table.downloadImageButtonStyle = mode
76
+ dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig] })
77
+ }
78
+
65
79
  const convertStateToConfig = () => {
66
80
  const strippedState = cloneConfig(config)
67
81
  delete strippedState.newViz
@@ -231,22 +245,27 @@ const Header = (props: HeaderProps) => {
231
245
  />
232
246
  Show URL to Automatically Updated Data
233
247
  </label>
234
- <label>
235
- <input
236
- type='checkbox'
237
- defaultChecked={config.table.downloadImageButton}
238
- onChange={e => changeConfigValue('table', 'downloadImageButton', e.target.checked)}
239
- />
240
- Show Download Image Button
241
- </label>
242
- {config.table.downloadImageButton && (
243
- <input
244
- type='text'
245
- placeholder='Customize label'
246
- defaultValue={config.table.downloadImageLabel}
247
- onChange={e => changeConfigValue('table', 'downloadImageLabel', e.target.value)}
248
- />
249
- )}
248
+ <div className='download-image-controls'>
249
+ <select
250
+ aria-label='Download image display'
251
+ className='download-image-mode-select'
252
+ value={getDownloadImageMode()}
253
+ onChange={e => changeDownloadImageMode(e.target.value as DownloadImageMode)}
254
+ >
255
+ <option value='off'>Download Image Off</option>
256
+ <option value='button'>Download Image Button</option>
257
+ <option value='link'>Download Image Link</option>
258
+ </select>
259
+ {getDownloadImageMode() !== 'off' && (
260
+ <input
261
+ className='download-image-label-input'
262
+ type='text'
263
+ placeholder='Customize label'
264
+ defaultValue={config.table.downloadImageLabel}
265
+ onChange={e => changeConfigValue('table', 'downloadImageLabel', e.target.value)}
266
+ />
267
+ )}
268
+ </div>
250
269
  </div>
251
270
  </>
252
271
  )}
@@ -1,140 +1,141 @@
1
- import { createRef, useContext, useMemo, useState } from 'react'
2
- import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
3
- import Modal from '@cdc/core/components/ui/Modal'
4
- import { useGlobalContext } from '@cdc/core/components/GlobalContext'
5
- import './multiconfigtabs.styles.css'
6
-
7
- const AreYouSure = deleteCallback => {
8
- return (
9
- <Modal>
10
- <Modal.Content>
11
- <p>Are you sure you want to delete this dashboard? </p>
12
- <button className='btn btn-danger' onClick={deleteCallback}>
13
- DELETE
14
- </button>
15
- </Modal.Content>
16
- </Modal>
17
- )
18
- }
19
-
20
- const Tab = ({ name, handleClick, tabs, index, active }) => {
21
- const [editing, setEditing] = useState(false)
22
- const dispatch = useContext(DashboardDispatchContext)
23
- const { overlay } = useGlobalContext()
24
- const inputRef = createRef<HTMLInputElement>()
25
-
26
- const saveName = e => {
27
- e.stopPropagation()
28
- const newVal = inputRef.current.value
29
- const sameName = newVal === name
30
- const blankName = !newVal
31
- const duplicateName = tabs.includes(newVal)
32
- if (!sameName && !blankName && !duplicateName) {
33
- dispatch({ type: 'RENAME_DASHBOARD_TAB', payload: { current: name, new: newVal } })
34
- }
35
- setEditing(false)
36
- }
37
-
38
- const onClick = e => {
39
- // ignore click on delete button
40
- if (e.target.className === 'remove') return
41
- if (active) {
42
- setEditing(true)
43
- } else {
44
- handleClick()
45
- }
46
- }
47
-
48
- const handleRemove = () => {
49
- const deleteCallback = () => {
50
- dispatch({ type: 'REMOVE_MULTIDASHBOARD_AT_INDEX', payload: index })
51
- overlay?.actions.toggleOverlay(false)
52
- }
53
- overlay?.actions.openOverlay(AreYouSure(deleteCallback))
54
- }
55
-
56
- const handleReorder = (index: number, moveTo: -1 | 1) => {
57
- const newIndex = index + moveTo
58
- const inbounds = newIndex > -1 && newIndex <= tabs.length - 1
59
- if (inbounds) {
60
- dispatch({ type: 'REORDER_MULTIDASHBOARDS', payload: { currentIndex: index, newIndex: index + moveTo } })
61
- }
62
- }
63
-
64
- const canMoveLeft = index !== 0
65
- const canMoveRight = index <= tabs.length - 2
66
-
67
- return (
68
- <li className='nav-item d-flex mt-0'>
69
- {canMoveLeft && editing && (
70
- <button className='border-0' onClick={() => handleReorder(index, -1)}>
71
- {'<'}
72
- </button>
73
- )}
74
- <div
75
- className={`edit nav-link${active ? ' active' : ''}`}
76
- aria-current={active ? 'page' : null}
77
- onClick={onClick}
78
- >
79
- {editing ? (
80
- <div className='d-flex'>
81
- <input type='text' defaultValue={name} onBlur={saveName} ref={inputRef} />
82
- <button className='btn btn-link save' onClick={saveName}>
83
- save
84
- </button>
85
- </div>
86
- ) : (
87
- <>
88
- {name}
89
- <button className='btn btn-danger border-0 ms-1' onClick={handleRemove}>
90
- X
91
- </button>
92
- </>
93
- )}
94
- </div>
95
- {canMoveRight && editing && (
96
- <button className='border-0' onClick={() => handleReorder(index, 1)}>
97
- {'>'}
98
- </button>
99
- )}
100
- </li>
101
- )
102
- }
103
-
104
- const MultiConfigTabs = () => {
105
- const { config } = useContext(DashboardContext)
106
- const dispatch = useContext(DashboardDispatchContext)
107
- const tabs = useMemo<string[]>(
108
- () => (config.multiDashboards || []).map(({ label }) => label),
109
- [config.multiDashboards]
110
- )
111
- const activeTab = useMemo<number>(() => config.activeDashboard, [config.activeDashboard])
112
-
113
- const saveAndLoad = (indexToSwitchTo: number) => {
114
- dispatch({ type: 'SAVE_CURRENT_CHANGES' })
115
- dispatch({ type: 'SWITCH_CONFIG', payload: indexToSwitchTo })
116
- }
117
-
118
- if (!config.multiDashboards) return null
119
- return (
120
- <ul className='nav nav-tabs multi-config-tabs mb-4'>
121
- {tabs.map((tab, index) => (
122
- <Tab
123
- key={tab + index}
124
- name={tab}
125
- tabs={tabs}
126
- index={index}
127
- handleClick={() => saveAndLoad(index)}
128
- active={index === activeTab}
129
- />
130
- ))}
131
- <li className='nav-item'>
132
- <button className='nav-link add' onClick={() => dispatch({ type: 'ADD_NEW_DASHBOARD' })}>
133
- +
134
- </button>
135
- </li>
136
- </ul>
137
- )
138
- }
139
-
140
- export default MultiConfigTabs
1
+ import { createRef, useContext, useMemo, useState } from 'react'
2
+ import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
3
+ import Modal from '@cdc/core/components/ui/Modal'
4
+ import { useGlobalContext } from '@cdc/core/components/GlobalContext'
5
+ import Button from '@cdc/core/components/elements/Button'
6
+ import './multiconfigtabs.styles.css'
7
+
8
+ const AreYouSure = deleteCallback => {
9
+ return (
10
+ <Modal>
11
+ <Modal.Content>
12
+ <p>Are you sure you want to delete this dashboard? </p>
13
+ <Button variant='danger' onClick={deleteCallback}>
14
+ DELETE
15
+ </Button>
16
+ </Modal.Content>
17
+ </Modal>
18
+ )
19
+ }
20
+
21
+ const Tab = ({ name, handleClick, tabs, index, active }) => {
22
+ const [editing, setEditing] = useState(false)
23
+ const dispatch = useContext(DashboardDispatchContext)
24
+ const { overlay } = useGlobalContext()
25
+ const inputRef = createRef<HTMLInputElement>()
26
+
27
+ const saveName = e => {
28
+ e.stopPropagation()
29
+ const newVal = inputRef.current.value
30
+ const sameName = newVal === name
31
+ const blankName = !newVal
32
+ const duplicateName = tabs.includes(newVal)
33
+ if (!sameName && !blankName && !duplicateName) {
34
+ dispatch({ type: 'RENAME_DASHBOARD_TAB', payload: { current: name, new: newVal } })
35
+ }
36
+ setEditing(false)
37
+ }
38
+
39
+ const onClick = e => {
40
+ // ignore click on delete button
41
+ if (e.target.className === 'remove') return
42
+ if (active) {
43
+ setEditing(true)
44
+ } else {
45
+ handleClick()
46
+ }
47
+ }
48
+
49
+ const handleRemove = () => {
50
+ const deleteCallback = () => {
51
+ dispatch({ type: 'REMOVE_MULTIDASHBOARD_AT_INDEX', payload: index })
52
+ overlay?.actions.toggleOverlay(false)
53
+ }
54
+ overlay?.actions.openOverlay(AreYouSure(deleteCallback))
55
+ }
56
+
57
+ const handleReorder = (index: number, moveTo: -1 | 1) => {
58
+ const newIndex = index + moveTo
59
+ const inbounds = newIndex > -1 && newIndex <= tabs.length - 1
60
+ if (inbounds) {
61
+ dispatch({ type: 'REORDER_MULTIDASHBOARDS', payload: { currentIndex: index, newIndex: index + moveTo } })
62
+ }
63
+ }
64
+
65
+ const canMoveLeft = index !== 0
66
+ const canMoveRight = index <= tabs.length - 2
67
+
68
+ return (
69
+ <li className='nav-item d-flex mt-0'>
70
+ {canMoveLeft && editing && (
71
+ <button className='border-0' onClick={() => handleReorder(index, -1)}>
72
+ {'<'}
73
+ </button>
74
+ )}
75
+ <div
76
+ className={`edit nav-link${active ? ' active' : ''}`}
77
+ aria-current={active ? 'page' : null}
78
+ onClick={onClick}
79
+ >
80
+ {editing ? (
81
+ <div className='d-flex'>
82
+ <input type='text' defaultValue={name} onBlur={saveName} ref={inputRef} />
83
+ <Button variant='link' className='save' onClick={saveName}>
84
+ save
85
+ </Button>
86
+ </div>
87
+ ) : (
88
+ <>
89
+ {name}
90
+ <Button variant='danger' className='border-0 ms-1' onClick={handleRemove}>
91
+ X
92
+ </Button>
93
+ </>
94
+ )}
95
+ </div>
96
+ {canMoveRight && editing && (
97
+ <button className='border-0' onClick={() => handleReorder(index, 1)}>
98
+ {'>'}
99
+ </button>
100
+ )}
101
+ </li>
102
+ )
103
+ }
104
+
105
+ const MultiConfigTabs = () => {
106
+ const { config } = useContext(DashboardContext)
107
+ const dispatch = useContext(DashboardDispatchContext)
108
+ const tabs = useMemo<string[]>(
109
+ () => (config.multiDashboards || []).map(({ label }) => label),
110
+ [config.multiDashboards]
111
+ )
112
+ const activeTab = useMemo<number>(() => config.activeDashboard, [config.activeDashboard])
113
+
114
+ const saveAndLoad = (indexToSwitchTo: number) => {
115
+ dispatch({ type: 'SAVE_CURRENT_CHANGES' })
116
+ dispatch({ type: 'SWITCH_CONFIG', payload: indexToSwitchTo })
117
+ }
118
+
119
+ if (!config.multiDashboards) return null
120
+ return (
121
+ <ul className='nav nav-tabs multi-config-tabs mb-4'>
122
+ {tabs.map((tab, index) => (
123
+ <Tab
124
+ key={tab + index}
125
+ name={tab}
126
+ tabs={tabs}
127
+ index={index}
128
+ handleClick={() => saveAndLoad(index)}
129
+ active={index === activeTab}
130
+ />
131
+ ))}
132
+ <li className='nav-item'>
133
+ <button className='nav-link add' onClick={() => dispatch({ type: 'ADD_NEW_DASHBOARD' })}>
134
+ +
135
+ </button>
136
+ </li>
137
+ </ul>
138
+ )
139
+ }
140
+
141
+ export default MultiConfigTabs