@cdc/dashboard 4.26.4 → 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 (91) hide show
  1. package/CONFIG.md +77 -30
  2. package/LICENSE +201 -0
  3. package/dist/cdcdashboard.js +49936 -49166
  4. package/examples/dashboard-conditions-filters-incomplete.json +221 -0
  5. package/examples/dashboard-missing-datasets-multi.json +174 -0
  6. package/examples/dashboard-missing-datasets-single.json +121 -0
  7. package/examples/dashboard-multi-dashboard-version-regression.json +146 -0
  8. package/examples/dashboard-shared-filter-row-delete-cleanup.json +186 -0
  9. package/examples/dashboard-stale-dataset-keys.json +181 -0
  10. package/examples/dashboard-tiered-filter-regression.json +190 -0
  11. package/examples/private/cfa-dashboard.json +651 -0
  12. package/examples/private/data-bite-wrap.json +6936 -0
  13. package/examples/private/multi-dash-fix.json +16963 -0
  14. package/examples/private/versions.json +41612 -0
  15. package/examples/us-map-filter-example.json +1074 -0
  16. package/package.json +9 -9
  17. package/src/CdcDashboard.tsx +6 -2
  18. package/src/CdcDashboardComponent.tsx +178 -87
  19. package/src/DashboardCopyPasteContext.test.tsx +33 -0
  20. package/src/DashboardCopyPasteContext.tsx +48 -0
  21. package/src/_stories/Dashboard.EditorRegression.stories.tsx +72 -0
  22. package/src/_stories/Dashboard.Regression.stories.tsx +196 -0
  23. package/src/_stories/Dashboard.Zoom.stories.tsx +88 -0
  24. package/src/_stories/Dashboard.stories.tsx +294 -0
  25. package/src/_stories/FilteredTextMigrationComparison.stories.tsx +87 -0
  26. package/src/components/Column.test.tsx +176 -0
  27. package/src/components/Column.tsx +214 -13
  28. package/src/components/DashboardConditionModal.test.tsx +420 -0
  29. package/src/components/DashboardConditionModal.tsx +367 -0
  30. package/src/components/DashboardConditionSummary.tsx +59 -0
  31. package/src/components/DashboardEditors.tsx +8 -0
  32. package/src/components/DashboardFilters/DashboardFilters.test.tsx +139 -1
  33. package/src/components/DashboardFilters/DashboardFilters.tsx +192 -174
  34. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.test.tsx +164 -0
  35. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +41 -2
  36. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.test.tsx +180 -3
  37. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +15 -32
  38. package/src/components/DashboardFilters/DashboardFiltersWrapper.test.tsx +142 -0
  39. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +32 -27
  40. package/src/components/DashboardFilters/dashboardfilter.styles.css +42 -27
  41. package/src/components/DataDesignerModal.tsx +2 -1
  42. package/src/components/Grid.tsx +8 -4
  43. package/src/components/Header/Header.tsx +36 -17
  44. package/src/components/Row.test.tsx +228 -0
  45. package/src/components/Row.tsx +93 -18
  46. package/src/components/VisualizationRow.test.tsx +396 -0
  47. package/src/components/VisualizationRow.tsx +110 -35
  48. package/src/components/VisualizationsPanel/VisualizationsPanel.test.tsx +49 -0
  49. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +14 -13
  50. package/src/components/Widget/Widget.test.tsx +218 -0
  51. package/src/components/Widget/Widget.tsx +119 -17
  52. package/src/components/Widget/widget.styles.css +31 -18
  53. package/src/components/dashboard-condition-modal.css +76 -0
  54. package/src/components/dashboard-condition-summary.css +87 -0
  55. package/src/helpers/addValuesToDashboardFilters.ts +3 -5
  56. package/src/helpers/addVisualization.ts +15 -4
  57. package/src/helpers/cloneDashboardWidget.ts +127 -0
  58. package/src/helpers/dashboardColumnWidgets.ts +99 -0
  59. package/src/helpers/dashboardConditionUi.ts +47 -0
  60. package/src/helpers/dashboardConditions.ts +200 -0
  61. package/src/helpers/dashboardFilterTargets.ts +156 -0
  62. package/src/helpers/filterData.ts +4 -9
  63. package/src/helpers/filterVisibility.ts +20 -0
  64. package/src/helpers/formatConfigBeforeSave.ts +2 -2
  65. package/src/helpers/getFilteredData.ts +18 -5
  66. package/src/helpers/getUpdateConfig.ts +43 -12
  67. package/src/helpers/getVizRowColumnLocator.ts +11 -1
  68. package/src/helpers/iconHash.tsx +9 -3
  69. package/src/helpers/mapDataToConfig.ts +31 -29
  70. package/src/helpers/reloadURLHelpers.ts +25 -5
  71. package/src/helpers/removeDashboardFilter.ts +33 -33
  72. package/src/helpers/tests/addVisualization.test.ts +53 -9
  73. package/src/helpers/tests/cloneDashboardWidget.test.ts +136 -0
  74. package/src/helpers/tests/dashboardColumnWidgets.test.ts +99 -0
  75. package/src/helpers/tests/dashboardConditionUi.test.ts +41 -0
  76. package/src/helpers/tests/dashboardConditions.test.ts +428 -0
  77. package/src/helpers/tests/formatConfigBeforeSave.test.ts +51 -0
  78. package/src/helpers/tests/getFilteredData.test.ts +265 -86
  79. package/src/helpers/tests/getUpdateConfig.test.ts +338 -0
  80. package/src/helpers/tests/reloadURLHelpers.test.ts +394 -238
  81. package/src/index.tsx +6 -3
  82. package/src/scss/grid.scss +249 -20
  83. package/src/scss/main.scss +108 -29
  84. package/src/store/dashboard.actions.ts +17 -4
  85. package/src/store/dashboard.reducer.test.ts +538 -0
  86. package/src/store/dashboard.reducer.ts +135 -22
  87. package/src/test/CdcDashboard.test.tsx +148 -0
  88. package/src/test/CdcDashboardComponent.test.tsx +935 -2
  89. package/src/types/ConfigRow.ts +15 -0
  90. package/src/types/DashboardFilters.ts +4 -0
  91. package/src/types/SharedFilter.ts +1 -0
@@ -1,5 +1,6 @@
1
1
  import _ from 'lodash'
2
2
  import { getUpdateConfig } from '../helpers/getUpdateConfig'
3
+ import { getFilteredData } from '../helpers/getFilteredData'
3
4
  import { MultiDashboard, MultiDashboardConfig } from '../types/MultiDashboard'
4
5
  import DashboardActions from './dashboard.actions'
5
6
  import { devToolsWrapper } from '@cdc/core/helpers/withDevTools'
@@ -8,6 +9,8 @@ import { Dashboard } from '../types/Dashboard'
8
9
  import { ConfigRow } from '../types/ConfigRow'
9
10
  import { AnyVisualization } from '@cdc/core/types/Visualization'
10
11
  import { initialState } from '../DashboardContext'
12
+ import { hasConditionalWidgets, normalizeConditionalColumn } from '../helpers/dashboardColumnWidgets'
13
+ import { cloneDashboardWidget } from '../helpers/cloneDashboardWidget'
11
14
 
12
15
  type BlankMultiConfig = {
13
16
  dashboard: Partial<Dashboard>
@@ -76,10 +79,22 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
76
79
  } else return state // ignore SET_CONFIG calls that have the wrong activeDashboard due to async api fetching
77
80
  }
78
81
  case 'SET_DATA': {
79
- return { ...state, data: action.payload }
82
+ if (
83
+ action.payload.activeDashboard !== undefined &&
84
+ state.config.activeDashboard !== action.payload.activeDashboard
85
+ ) {
86
+ return state
87
+ }
88
+ return { ...state, data: action.payload.data }
80
89
  }
81
90
  case 'SET_FILTERED_DATA': {
82
- return { ...state, filteredData: action.payload }
91
+ if (
92
+ action.payload.activeDashboard !== undefined &&
93
+ state.config.activeDashboard !== action.payload.activeDashboard
94
+ ) {
95
+ return state
96
+ }
97
+ return { ...state, filteredData: action.payload.filteredData }
83
98
  }
84
99
  case 'SET_LOADING': {
85
100
  return { ...state, loading: action.payload }
@@ -90,9 +105,15 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
90
105
  case 'SET_SHARED_FILTERS': {
91
106
  const newSharedFilters = action.payload
92
107
  const newDashboardConfig = { ...state.config.dashboard, sharedFilters: newSharedFilters }
108
+ const nextConfig = saveMultiChanges(
109
+ { ...state.config, dashboard: newDashboardConfig },
110
+ state.config.activeDashboard
111
+ )
112
+ const filteredData = getFilteredData({ ...state, config: nextConfig })
93
113
  return {
94
114
  ...state,
95
- config: saveMultiChanges({ ...state.config, dashboard: newDashboardConfig }, state.config.activeDashboard)
115
+ config: nextConfig,
116
+ filteredData
96
117
  }
97
118
  }
98
119
  case 'SET_TAB_SELECTED': {
@@ -115,12 +136,12 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
115
136
  case 'RENAME_DASHBOARD_TAB': {
116
137
  const newMultiDashboards = state.config.multiDashboards.map(dashboard => {
117
138
  if (dashboard.label === action.payload.current) {
118
- dashboard.label = action.payload.new
139
+ return { ...dashboard, label: action.payload.new }
119
140
  }
120
141
  return dashboard
121
142
  })
122
143
  const newConfig = { ...state.config, label: action.payload.new } // make sure active label is updated
123
- return applyMultiDashboards({ ...state, newConfig }, newMultiDashboards)
144
+ return applyMultiDashboards({ ...state, config: newConfig }, newMultiDashboards)
124
145
  }
125
146
  case 'REORDER_MULTIDASHBOARDS': {
126
147
  const { newIndex, currentIndex } = action.payload
@@ -164,10 +185,24 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
164
185
  return { ...state, config: { ...state.config, rows: newRows } }
165
186
  }
166
187
  case 'ADD_VISUALIZATION': {
167
- const { newViz, rowIdx, colIdx } = action.payload
188
+ const { newViz, rowIdx, colIdx, entryIdx } = action.payload
168
189
  const vizKey = newViz.uid
169
190
  const newRows = _.cloneDeep(state.config.rows)
170
- newRows[rowIdx].columns[colIdx].widget = vizKey
191
+ const column = newRows[rowIdx].columns[colIdx]
192
+
193
+ if (entryIdx !== undefined || hasConditionalWidgets(column)) {
194
+ const nextConditionalWidgets = [...(column.conditionalWidgets || [])]
195
+ const targetEntryIndex = entryIdx ?? nextConditionalWidgets.length
196
+ nextConditionalWidgets[targetEntryIndex] = { widget: vizKey }
197
+ newRows[rowIdx].columns[colIdx] = normalizeConditionalColumn({
198
+ ...column,
199
+ widget: undefined,
200
+ conditionalWidgets: nextConditionalWidgets.filter(entry => !!entry?.widget)
201
+ })
202
+ } else {
203
+ newRows[rowIdx].columns[colIdx].widget = vizKey
204
+ }
205
+
171
206
  return {
172
207
  ...state,
173
208
  config: saveMultiChanges(
@@ -176,14 +211,73 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
176
211
  )
177
212
  }
178
213
  }
214
+ case 'CLONE_VISUALIZATION': {
215
+ const { sourceWidgetKey, rowIdx, colIdx, entryIdx } = action.payload
216
+ const nextConfig = cloneDashboardWidget(state.config, sourceWidgetKey, { rowIdx, colIdx, entryIdx })
217
+
218
+ if (nextConfig === state.config) return state
219
+
220
+ const config = saveMultiChanges(nextConfig, state.config.activeDashboard)
221
+ const filteredData = getFilteredData({ ...state, config })
222
+
223
+ return {
224
+ ...state,
225
+ config,
226
+ filteredData
227
+ }
228
+ }
179
229
  case 'MOVE_VISUALIZATION': {
180
- const { rowIdx, colIdx, widget } = action.payload
230
+ const { rowIdx, colIdx, entryIdx, widget } = action.payload
181
231
  const newRows = _.cloneDeep(state.config.rows)
182
- newRows[widget.rowIdx].columns[widget.colIdx].widget = null
183
- newRows[rowIdx].columns[colIdx].widget = widget.uid
232
+ const sourceColumn = newRows[widget.rowIdx].columns[widget.colIdx]
233
+ let widgetEntry
234
+
235
+ if (hasConditionalWidgets(sourceColumn)) {
236
+ widgetEntry =
237
+ widget.entryIdx !== undefined
238
+ ? sourceColumn.conditionalWidgets[widget.entryIdx]
239
+ : sourceColumn.conditionalWidgets.find(entry => entry.widget === widget.uid)
240
+ } else if (sourceColumn.widget === widget.uid) {
241
+ widgetEntry = { widget: widget.uid }
242
+ }
243
+
244
+ if (!widgetEntry) {
245
+ return state
246
+ }
247
+
248
+ if (hasConditionalWidgets(sourceColumn)) {
249
+ newRows[widget.rowIdx].columns[widget.colIdx] = normalizeConditionalColumn({
250
+ ...sourceColumn,
251
+ conditionalWidgets: sourceColumn.conditionalWidgets.filter((entry, index) => {
252
+ if (widget.entryIdx !== undefined) return index !== widget.entryIdx
253
+ return entry.widget !== widget.uid
254
+ })
255
+ })
256
+ } else {
257
+ newRows[widget.rowIdx].columns[widget.colIdx].widget = undefined
258
+ }
259
+
260
+ const targetColumn = newRows[rowIdx].columns[colIdx]
261
+ if (entryIdx !== undefined || hasConditionalWidgets(targetColumn)) {
262
+ const nextConditionalWidgets = [...(targetColumn.conditionalWidgets || [])]
263
+ const targetEntryIndex = entryIdx ?? nextConditionalWidgets.length
264
+ nextConditionalWidgets[targetEntryIndex] = widgetEntry
265
+ newRows[rowIdx].columns[colIdx] = normalizeConditionalColumn({
266
+ ...targetColumn,
267
+ widget: undefined,
268
+ conditionalWidgets: nextConditionalWidgets.filter(entry => !!entry?.widget)
269
+ })
270
+ } else {
271
+ newRows[rowIdx].columns[colIdx].widget = widgetEntry.widget
272
+ }
273
+
274
+ const nextConfig = saveMultiChanges({ ...state.config, rows: newRows }, state.config.activeDashboard)
275
+ const filteredData = getFilteredData({ ...state, config: nextConfig })
276
+
184
277
  return {
185
278
  ...state,
186
- config: saveMultiChanges({ ...state.config, rows: newRows }, state.config.activeDashboard)
279
+ config: nextConfig,
280
+ filteredData
187
281
  }
188
282
  }
189
283
  case 'RESET_VISUALIZATION': {
@@ -220,7 +314,15 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
220
314
  }
221
315
  return row
222
316
  })
223
- return { ...state, config: saveMultiChanges({ ...state.config, rows: newRows }, state.config.activeDashboard) }
317
+ const nextConfig = {
318
+ ...state.config,
319
+ rows: newRows
320
+ }
321
+
322
+ return {
323
+ ...state,
324
+ config: saveMultiChanges(nextConfig, state.config.activeDashboard)
325
+ }
224
326
  }
225
327
  case 'DELETE_WIDGET': {
226
328
  const { uid } = action.payload
@@ -238,20 +340,31 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
238
340
 
239
341
  const filteredRows = _.map(newRows, row => ({
240
342
  ...row,
241
- columns: row.columns.map(column => (column.widget === uid ? _.omit(column, 'widget') : column))
343
+ columns: row.columns.map(column => {
344
+ if (hasConditionalWidgets(column)) {
345
+ return normalizeConditionalColumn({
346
+ ...column,
347
+ conditionalWidgets: column.conditionalWidgets.filter(entry => entry.widget !== uid)
348
+ })
349
+ }
350
+
351
+ return column.widget === uid ? _.omit(column, 'widget') : column
352
+ })
242
353
  }))
243
354
 
355
+ const nextConfig = {
356
+ ...state.config,
357
+ dashboard: {
358
+ ...state.config.dashboard,
359
+ sharedFilters: newSharedFilters
360
+ },
361
+ visualizations: newVisualizations,
362
+ rows: filteredRows
363
+ }
364
+
244
365
  return {
245
366
  ...state,
246
- config: saveMultiChanges(
247
- {
248
- ...state.config,
249
- dashboard: { ...state.config.dashboard, sharedFilters: newSharedFilters },
250
- visualizations: newVisualizations,
251
- rows: filteredRows
252
- },
253
- state.config.activeDashboard
254
- )
367
+ config: saveMultiChanges(nextConfig, state.config.activeDashboard)
255
368
  }
256
369
  }
257
370
  case 'UPDATE_TOGGLE_NAME': {
@@ -0,0 +1,148 @@
1
+ import React from 'react'
2
+ import { render, screen, waitFor } from '@testing-library/react'
3
+ import userEvent from '@testing-library/user-event'
4
+ import { afterEach, describe, expect, it, vi } from 'vitest'
5
+ import CdcDashboard from '../CdcDashboard'
6
+
7
+ vi.mock('resize-observer-polyfill', () => ({
8
+ default: vi.fn(() => ({
9
+ observe: vi.fn(),
10
+ unobserve: vi.fn(),
11
+ disconnect: vi.fn()
12
+ }))
13
+ }))
14
+
15
+ const createDashboardConfig = () => ({
16
+ type: 'dashboard',
17
+ dashboard: {
18
+ theme: 'theme-blue',
19
+ titleStyle: 'small',
20
+ sharedFilters: [
21
+ {
22
+ key: 'Year',
23
+ showDropdown: true,
24
+ values: ['2024', '2025'],
25
+ orderedValues: ['2024', '2025'],
26
+ type: 'datafilter',
27
+ filterStyle: 'dropdown',
28
+ columnName: 'year',
29
+ defaultValue: '2025',
30
+ active: '2025',
31
+ usedBy: ['waffle']
32
+ }
33
+ ]
34
+ },
35
+ rows: [{ columns: [{ width: 12, widget: 'filters' }] }, { columns: [{ width: 12, widget: 'waffle' }] }],
36
+ visualizations: {
37
+ filters: {
38
+ type: 'dashboardFilters',
39
+ visualizationType: 'dashboardFilters',
40
+ filterBehavior: 'Filter Change',
41
+ sharedFilterIndexes: [0],
42
+ uid: 'filters'
43
+ },
44
+ waffle: {
45
+ type: 'waffle-chart',
46
+ uid: 'waffle',
47
+ title: 'Year Waffle',
48
+ showTitle: false,
49
+ visualizationType: 'Waffle',
50
+ visualizationSubType: 'linear',
51
+ showPercent: false,
52
+ showDenominator: false,
53
+ valueDescription: 'out of 100',
54
+ content: 'during {{year}}',
55
+ subtext: '',
56
+ orientation: 'horizontal',
57
+ filters: [],
58
+ fontSize: '',
59
+ overallFontSize: 'medium',
60
+ dataColumn: 'value',
61
+ dataFunction: 'Max',
62
+ dataConditionalColumn: '',
63
+ dataConditionalOperator: '',
64
+ dataConditionalComparate: '',
65
+ invalidComparate: false,
66
+ customDenom: false,
67
+ dataDenom: '100',
68
+ dataDenomColumn: '',
69
+ dataDenomFunction: '',
70
+ suffix: '',
71
+ roundToPlace: '0',
72
+ shape: 'circle',
73
+ nodeWidth: '10',
74
+ nodeSpacer: '2',
75
+ theme: 'theme-blue',
76
+ gauge: {
77
+ height: 35,
78
+ width: '100%'
79
+ },
80
+ visual: {
81
+ border: true,
82
+ accent: false,
83
+ background: false,
84
+ useWrap: false,
85
+ hideBackgroundColor: false,
86
+ borderColorTheme: false,
87
+ colors: {
88
+ 'theme-blue': '#005eaa'
89
+ }
90
+ },
91
+ markupVariables: [
92
+ {
93
+ sourceType: 'column',
94
+ name: 'year',
95
+ tag: '{{year}}',
96
+ columnName: 'year',
97
+ conditions: [],
98
+ addCommas: false,
99
+ hideOnNull: false,
100
+ outputType: 'value'
101
+ }
102
+ ],
103
+ enableMarkupVariables: true,
104
+ filterBehavior: 'Filter Change',
105
+ dataKey: 'waffle-data.json',
106
+ version: '4.26.4-1'
107
+ }
108
+ },
109
+ datasets: {
110
+ 'waffle-data.json': {
111
+ data: [
112
+ { year: '2024', value: 24 },
113
+ { year: '2025', value: 25 }
114
+ ]
115
+ }
116
+ },
117
+ table: {
118
+ show: false
119
+ }
120
+ })
121
+
122
+ afterEach(() => {
123
+ vi.restoreAllMocks()
124
+ vi.unstubAllGlobals()
125
+ })
126
+
127
+ describe('CdcDashboard', () => {
128
+ it('updates waffle chart markup when a dashboard loaded through configUrl changes data filters', async () => {
129
+ const fetchMock = vi.fn().mockResolvedValue({
130
+ json: vi.fn().mockResolvedValue(createDashboardConfig())
131
+ })
132
+ vi.stubGlobal('fetch', fetchMock)
133
+
134
+ render(<CdcDashboard configUrl='/dashboard-with-waffle.json' interactionLabel='dashboard-test' />)
135
+
136
+ await waitFor(() => {
137
+ expect(screen.getByText(/during 2025/)).toBeInTheDocument()
138
+ })
139
+
140
+ await userEvent.selectOptions(screen.getByLabelText('Year'), '2024')
141
+
142
+ await waitFor(() => {
143
+ expect(screen.getByText(/during 2024/)).toBeInTheDocument()
144
+ })
145
+
146
+ expect(screen.queryByText(/during 2025/)).not.toBeInTheDocument()
147
+ })
148
+ })