@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,5 @@
1
1
  import DataTableStandAlone from '@cdc/core/components/DataTable/DataTableStandAlone'
2
- import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
2
+ import React, { useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
3
3
  import Toggle from './Toggle'
4
4
  import cloneDeep from 'lodash/cloneDeep'
5
5
  import { ConfigRow } from '../types/ConfigRow'
@@ -11,6 +11,12 @@ import CdcFilteredText from '@cdc/filtered-text/src/CdcFilteredText'
11
11
  import DashboardSharedFilters, { APIFilterDropdowns } from './DashboardFilters'
12
12
  import { DashboardContext } from '../DashboardContext'
13
13
  import { ViewPort } from '@cdc/core/types/ViewPort'
14
+ import { evaluateDashboardCondition } from '../helpers/dashboardConditions'
15
+ import { dashboardConditionsSupportedForRow } from '../helpers/dashboardFilterTargets'
16
+ import {
17
+ hasAuthoredWidgetEntries,
18
+ resolveColumnWidgetEntry as resolveDashboardColumnWidgetEntry
19
+ } from '../helpers/dashboardColumnWidgets'
14
20
  import { getVizConfig } from '../helpers/getVizConfig'
15
21
  import { TableConfig } from '@cdc/core/components/DataTable/types/TableConfig'
16
22
  import CollapsibleVisualizationRow from './CollapsibleVisualizationRow'
@@ -21,6 +27,7 @@ import ExpandCollapseButtons from './ExpandCollapseButtons'
21
27
  import { ChartConfig } from '@cdc/chart/src/types/ChartConfig'
22
28
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
23
29
  import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
30
+ import { hasVisibleDashboardFiltersForIndexes } from '../helpers/filterVisibility'
24
31
 
25
32
  type VisualizationWrapperProps = {
26
33
  allExpanded: boolean
@@ -63,6 +70,8 @@ type VizRowProps = {
63
70
  rowIndex: number
64
71
  inNoDataState: boolean
65
72
  setSharedFilter: Function
73
+ clearSharedFilter: (key: string) => void
74
+ hasActiveSharedFilter: (key: string) => boolean
66
75
  updateChildConfig: Function
67
76
  apiFilterDropdowns: APIFilterDropdowns
68
77
  currentViewport: ViewPort
@@ -79,6 +88,8 @@ const VisualizationRow: React.FC<VizRowProps> = ({
79
88
  rowIndex: index,
80
89
  inNoDataState,
81
90
  setSharedFilter,
91
+ clearSharedFilter,
92
+ hasActiveSharedFilter,
82
93
  updateChildConfig,
83
94
  apiFilterDropdowns,
84
95
  currentViewport,
@@ -87,7 +98,7 @@ const VisualizationRow: React.FC<VizRowProps> = ({
87
98
  interactionLabel = ''
88
99
  }) => {
89
100
  const { config, filteredData: dashboardFilteredData, data: rawData } = useContext(DashboardContext)
90
- const [toggledRow, setToggled] = React.useState<number>(0)
101
+ const [toggledRow, setToggled] = useState<number>(0)
91
102
  const rowRef = useRef<HTMLDivElement>(null)
92
103
 
93
104
  useEffect(() => {
@@ -166,9 +177,27 @@ const VisualizationRow: React.FC<VizRowProps> = ({
166
177
  }
167
178
  }
168
179
 
169
- const tp5CountInRow = row.columns.reduce((count, col) => {
170
- if (!col.widget) return count
171
- const viz = config.visualizations[col.widget]
180
+ const shouldIgnoreDashboardConditions = !dashboardConditionsSupportedForRow(row)
181
+ const columnDashboardConditionEvaluations = useMemo(
182
+ () =>
183
+ row.columns.map(column =>
184
+ resolveDashboardColumnWidgetEntry(column, {
185
+ evaluateCondition: dashboardCondition => {
186
+ if (shouldIgnoreDashboardConditions || !dashboardCondition) {
187
+ return { matches: true, resolved: true }
188
+ }
189
+
190
+ return evaluateDashboardCondition(dashboardCondition, dashboardFilteredData[dashboardCondition.id])
191
+ }
192
+ })
193
+ ),
194
+ [dashboardFilteredData, row.columns, shouldIgnoreDashboardConditions]
195
+ )
196
+
197
+ const tp5CountInRow = columnDashboardConditionEvaluations.reduce((count, evaluation) => {
198
+ const widgetKey = evaluation?.widget
199
+ if (!widgetKey) return count
200
+ const viz = config.visualizations[widgetKey]
172
201
  if (!viz) return count
173
202
 
174
203
  const isTp5DataBite = viz.type === 'data-bite' && (viz as any).biteStyle === 'tp5'
@@ -182,7 +211,7 @@ const VisualizationRow: React.FC<VizRowProps> = ({
182
211
  const shouldEqualizeRow = !!row.equalHeight || needsTP5AutoEqualization
183
212
 
184
213
  // Layer TP5 equalization for row-level title consistency and same-type internals.
185
- useEffect(() => {
214
+ useLayoutEffect(() => {
186
215
  if (!shouldEqualizeRow) return
187
216
 
188
217
  const rowElement = rowRef.current
@@ -205,9 +234,10 @@ const VisualizationRow: React.FC<VizRowProps> = ({
205
234
  }
206
235
  }, [shouldEqualizeRow, row.columns, config.activeDashboard, filteredDataOverride, dashboardFilteredData[index]])
207
236
 
208
- const isFilterRow = row.columns.some(
209
- col => col.widget && config.visualizations[col.widget]?.type === 'dashboardFilters'
210
- )
237
+ const isFilterRow = columnDashboardConditionEvaluations.some(evaluation => {
238
+ const widgetKey = evaluation?.widget
239
+ return widgetKey && config.visualizations[widgetKey]?.type === 'dashboardFilters'
240
+ })
211
241
  const needsEqualHeight = shouldEqualizeRow && !isFilterRow
212
242
 
213
243
  const show = useMemo(() => {
@@ -219,6 +249,33 @@ const VisualizationRow: React.FC<VizRowProps> = ({
219
249
  }, [config.activeDashboard, toggledRow])
220
250
 
221
251
  const _data = dashboardFilteredData[index] || row.formattedData || []
252
+ const isMultiVizGroupRow = !!row.originalMultiVizColumn && !!filteredDataOverride
253
+ const rowDashboardCondition = useMemo(() => {
254
+ if (shouldIgnoreDashboardConditions || !row.dashboardCondition) {
255
+ return { matches: true, resolved: true }
256
+ }
257
+
258
+ return evaluateDashboardCondition(row.dashboardCondition, dashboardFilteredData[row.dashboardCondition.id])
259
+ }, [dashboardFilteredData, row.dashboardCondition, shouldIgnoreDashboardConditions])
260
+ const hasVisibleWidgetColumn = row.columns.some((_column, columnIndex) => {
261
+ if (!row.columns[columnIndex].width) return false
262
+
263
+ const widgetKey = columnDashboardConditionEvaluations[columnIndex]?.widget
264
+ if (!widgetKey) return false
265
+
266
+ const visualization = config.visualizations[widgetKey]
267
+ if (visualization?.type !== 'dashboardFilters') return true
268
+
269
+ return hasVisibleDashboardFiltersForIndexes(
270
+ config.dashboard.sharedFilters,
271
+ (visualization as DashboardFilters).sharedFilterIndexes
272
+ )
273
+ })
274
+
275
+ if (!rowDashboardCondition.matches || !hasVisibleWidgetColumn) {
276
+ return null
277
+ }
278
+
222
279
  const dataGroups =
223
280
  row.multiVizColumn &&
224
281
  _data.reduce((acc, dataRow) => {
@@ -235,7 +292,9 @@ const VisualizationRow: React.FC<VizRowProps> = ({
235
292
  const applyFilters = dashboardFilters.filter(v => !v.autoLoad).flatMap(v => v.sharedFilterIndexes)
236
293
  if (hasDashboardApplyBehavior(config.visualizations) && vizConfig.autoLoad) {
237
294
  return applyFilters.some(index => {
238
- const { queuedActive, active, subGrouping } = config.dashboard.sharedFilters[index]
295
+ const filter = config.dashboard.sharedFilters[index]
296
+ if (!filter) return false
297
+ const { queuedActive, active, subGrouping } = filter
239
298
  if (!active && !queuedActive) return true
240
299
  if (!queuedActive) return false
241
300
  // for nested dropdowns
@@ -294,11 +353,27 @@ const VisualizationRow: React.FC<VizRowProps> = ({
294
353
  )}
295
354
  {row.columns.map((col, colIndex) => {
296
355
  if (col.width) {
297
- if (!col.widget)
298
- return <div key={`row__${index}__col__${colIndex}`} className={`col-12 col-md-${col.width}`}></div>
356
+ const resolvedWidget = columnDashboardConditionEvaluations[colIndex]?.widget
357
+ const hasAuthoredWidgets = hasAuthoredWidgetEntries(col)
358
+ const hiddenByDashboardCondition = hasAuthoredWidgets && !resolvedWidget
359
+ if (!resolvedWidget) {
360
+ if (!hasAuthoredWidgets && (isMultiVizGroupRow || row.toggle)) {
361
+ return null
362
+ }
363
+
364
+ return (
365
+ <div
366
+ key={`row__${index}__col__${colIndex}`}
367
+ className={`col-12 col-md-${col.width}${
368
+ hiddenByDashboardCondition ? ' dashboard-condition-hidden' : ''
369
+ }`}
370
+ data-dashboard-condition-hidden={hiddenByDashboardCondition || undefined}
371
+ ></div>
372
+ )
373
+ }
299
374
 
300
375
  const visualizationConfig = getVizConfig(
301
- col.widget,
376
+ resolvedWidget,
302
377
  index,
303
378
  config,
304
379
  rawData,
@@ -311,9 +386,9 @@ const VisualizationRow: React.FC<VizRowProps> = ({
311
386
 
312
387
  const setsSharedFilter =
313
388
  config.dashboard.sharedFilters &&
314
- config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === col.widget).length > 0
389
+ config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === resolvedWidget).length > 0
315
390
  const setSharedFilterValue = setsSharedFilter
316
- ? config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === col.widget)[0].active
391
+ ? config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === resolvedWidget)[0].active
317
392
  : undefined
318
393
  const tableLink = (
319
394
  <a
@@ -349,9 +424,7 @@ const VisualizationRow: React.FC<VizRowProps> = ({
349
424
 
350
425
  const hiddenDashboardFilters =
351
426
  type === 'dashboardFilters' &&
352
- sharedFilterIndexes &&
353
- sharedFilterIndexes.filter(idx => config.dashboard.sharedFilters?.[idx]?.showDropdown === false).length ===
354
- sharedFilterIndexes.length
427
+ !hasVisibleDashboardFiltersForIndexes(config.dashboard.sharedFilters, sharedFilterIndexes)
355
428
 
356
429
  const vizWrapperClass = `col-12 col-md-${col.width}${!shouldShow ? ' d-none' : ''}${
357
430
  hideVisualization ? ' hide-parent-visualization' : ''
@@ -371,12 +444,12 @@ const VisualizationRow: React.FC<VizRowProps> = ({
371
444
  >
372
445
  {type === 'chart' && (
373
446
  <CdcChart
374
- key={col.widget}
447
+ key={resolvedWidget}
375
448
  config={visualizationConfig as ChartConfig}
376
449
  dashboardConfig={config}
377
450
  datasets={config.datasets}
378
451
  setConfig={newConfig => {
379
- updateChildConfig(col.widget, newConfig)
452
+ updateChildConfig(resolvedWidget, newConfig)
380
453
  }}
381
454
  setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
382
455
  isDashboard={true}
@@ -386,13 +459,15 @@ const VisualizationRow: React.FC<VizRowProps> = ({
386
459
  )}
387
460
  {type === 'map' && (
388
461
  <CdcMap
389
- key={col.widget}
462
+ key={resolvedWidget}
390
463
  config={visualizationConfig}
391
464
  setConfig={newConfig => {
392
- updateChildConfig(col.widget, newConfig)
465
+ updateChildConfig(resolvedWidget, newConfig)
393
466
  }}
394
467
  showLoader={false}
395
468
  setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
469
+ clearSharedFilter={setsSharedFilter ? clearSharedFilter : undefined}
470
+ hasActiveSharedFilter={setsSharedFilter ? hasActiveSharedFilter(resolvedWidget) : false}
396
471
  setSharedFilterValue={setSharedFilterValue}
397
472
  isDashboard={true}
398
473
  link={link}
@@ -402,11 +477,11 @@ const VisualizationRow: React.FC<VizRowProps> = ({
402
477
  )}
403
478
  {type === 'data-bite' && (
404
479
  <CdcDataBite
405
- key={col.widget}
480
+ key={resolvedWidget}
406
481
  config={visualizationConfig}
407
482
  rawData={rawData?.[visualizationConfig.dataKey] || []}
408
483
  setConfig={newConfig => {
409
- updateChildConfig(col.widget, newConfig)
484
+ updateChildConfig(resolvedWidget, newConfig)
410
485
  }}
411
486
  isDashboard={true}
412
487
  isEditor={config.editing === true}
@@ -415,11 +490,11 @@ const VisualizationRow: React.FC<VizRowProps> = ({
415
490
  )}
416
491
  {type === 'waffle-chart' && (
417
492
  <CdcWaffleChart
418
- key={col.widget}
493
+ key={resolvedWidget}
419
494
  config={visualizationConfig}
420
495
  rawData={rawData?.[visualizationConfig.dataKey] || []}
421
496
  setConfig={newConfig => {
422
- updateChildConfig(col.widget, newConfig)
497
+ updateChildConfig(resolvedWidget, newConfig)
423
498
  }}
424
499
  isDashboard={true}
425
500
  interactionLabel={interactionLabel}
@@ -427,22 +502,22 @@ const VisualizationRow: React.FC<VizRowProps> = ({
427
502
  )}
428
503
  {type === 'markup-include' && (
429
504
  <CdcMarkupInclude
430
- key={col.widget}
505
+ key={resolvedWidget}
431
506
  config={visualizationConfig}
432
507
  datasets={config.datasets}
433
508
  isDashboard={true}
434
509
  setConfig={newConfig => {
435
- updateChildConfig(col.widget, newConfig)
510
+ updateChildConfig(resolvedWidget, newConfig)
436
511
  }}
437
512
  interactionLabel={interactionLabel}
438
513
  />
439
514
  )}
440
515
  {type === 'filtered-text' && (
441
516
  <CdcFilteredText
442
- key={col.widget}
517
+ key={resolvedWidget}
443
518
  config={visualizationConfig}
444
519
  setConfig={newConfig => {
445
- updateChildConfig(col.widget, newConfig)
520
+ updateChildConfig(resolvedWidget, newConfig)
446
521
  }}
447
522
  isDashboard={true}
448
523
  />
@@ -450,9 +525,9 @@ const VisualizationRow: React.FC<VizRowProps> = ({
450
525
  {type === 'dashboardFilters' && (
451
526
  <DashboardSharedFilters
452
527
  setConfig={newConfig => {
453
- updateChildConfig(col.widget, newConfig)
528
+ updateChildConfig(resolvedWidget, newConfig)
454
529
  }}
455
- key={col.widget}
530
+ key={resolvedWidget}
456
531
  visualizationConfig={visualizationConfig as DashboardFilters}
457
532
  apiFilterDropdowns={apiFilterDropdowns}
458
533
  currentViewport={currentViewport}
@@ -461,11 +536,11 @@ const VisualizationRow: React.FC<VizRowProps> = ({
461
536
  )}
462
537
  {type === 'table' && (
463
538
  <DataTableStandAlone
464
- key={col.widget}
539
+ key={resolvedWidget}
465
540
  updateConfig={newConfig => {
466
- updateChildConfig(col.widget, newConfig)
541
+ updateChildConfig(resolvedWidget, newConfig)
467
542
  }}
468
- visualizationKey={col.widget}
543
+ visualizationKey={resolvedWidget}
469
544
  config={visualizationConfig as TableConfig}
470
545
  viewport={currentViewport}
471
546
  interactionLabel={interactionLabel}
@@ -0,0 +1,49 @@
1
+ import React from 'react'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { describe, expect, it, vi } from 'vitest'
4
+ import { DashboardContext, DashboardDispatchContext, initialState } from '../../DashboardContext'
5
+ import VisualizationsPanel from './VisualizationsPanel'
6
+
7
+ vi.mock('../Widget/Widget', () => ({
8
+ default: ({ type }: { type: string }) => <div data-testid='creation-widget'>{type}</div>
9
+ }))
10
+
11
+ vi.mock('@cdc/core/components/AdvancedEditor', () => ({
12
+ default: () => <div data-testid='advanced-editor' />
13
+ }))
14
+
15
+ describe('VisualizationsPanel', () => {
16
+ it('does not expose filtered-text in dashboard creation surfaces', () => {
17
+ render(
18
+ <DashboardContext.Provider
19
+ value={{
20
+ ...initialState,
21
+ config: {
22
+ type: 'dashboard',
23
+ dashboard: { sharedFilters: [] },
24
+ datasets: {},
25
+ rows: [],
26
+ visualizations: {}
27
+ } as any,
28
+ outerContainerRef: vi.fn(),
29
+ setParentConfig: vi.fn(),
30
+ isDebug: false,
31
+ isEditor: true,
32
+ reloadURLData: vi.fn(),
33
+ loadAPIFilters: vi.fn(),
34
+ setAPIFilterDropdowns: vi.fn(),
35
+ setAPILoading: vi.fn(),
36
+ data: {}
37
+ }}
38
+ >
39
+ <DashboardDispatchContext.Provider value={vi.fn()}>
40
+ <VisualizationsPanel />
41
+ </DashboardDispatchContext.Provider>
42
+ </DashboardContext.Provider>
43
+ )
44
+
45
+ const creationTypes = screen.getAllByTestId('creation-widget').map(widget => widget.textContent)
46
+ expect(creationTypes).toContain('markup-include')
47
+ expect(creationTypes).not.toContain('filtered-text')
48
+ })
49
+ })
@@ -12,6 +12,8 @@ const VisualizationsPanel = () => {
12
12
  const [advancedEditing, setAdvancedEditing] = useState(false)
13
13
  const { config, isEditor } = useContext(DashboardContext)
14
14
  const dispatch = useContext(DashboardDispatchContext)
15
+ const createVisualization = (type, subType) =>
16
+ addVisualization(type, subType, { existingIds: Object.keys(config.visualizations || {}) })
15
17
  const loadConfig = incomingConfig => {
16
18
  const newConfig = !incomingConfig.multiDashboards
17
19
  ? incomingConfig
@@ -29,25 +31,24 @@ const VisualizationsPanel = () => {
29
31
  <p style={{ fontSize: '14px' }}>Click and drag an item onto the grid to add it to your dashboard.</p>
30
32
  <span className='subheading-3'>Chart</span>
31
33
  <div className='drag-grid'>
32
- <Widget addVisualization={() => addVisualization('chart', 'Bar')} type='Bar' />
33
- <Widget addVisualization={() => addVisualization('chart', 'Line')} type='Line' />
34
- <Widget addVisualization={() => addVisualization('chart', 'Pie')} type='Pie' />
35
- <Widget addVisualization={() => addVisualization('chart', 'Sankey')} type='Sankey' />
34
+ <Widget addVisualization={() => createVisualization('chart', 'Bar')} type='Bar' />
35
+ <Widget addVisualization={() => createVisualization('chart', 'Line')} type='Line' />
36
+ <Widget addVisualization={() => createVisualization('chart', 'Pie')} type='Pie' />
37
+ <Widget addVisualization={() => createVisualization('chart', 'Sankey')} type='Sankey' />
36
38
  </div>
37
39
  <span className='subheading-3'>Map</span>
38
40
  <div className='drag-grid'>
39
- <Widget addVisualization={() => addVisualization('map', 'us')} type='us' />
40
- <Widget addVisualization={() => addVisualization('map', 'world')} type='world' />
41
- <Widget addVisualization={() => addVisualization('map', 'single-state')} type='single-state' />
41
+ <Widget addVisualization={() => createVisualization('map', 'us')} type='us' />
42
+ <Widget addVisualization={() => createVisualization('map', 'world')} type='world' />
43
+ <Widget addVisualization={() => createVisualization('map', 'single-state')} type='single-state' />
42
44
  </div>
43
45
  <span className='subheading-3'>Misc.</span>
44
46
  <div className='drag-grid'>
45
- <Widget addVisualization={() => addVisualization('data-bite', '')} type='data-bite' />
46
- <Widget addVisualization={() => addVisualization('waffle-chart', 'Waffle')} type='waffle-chart' />
47
- <Widget addVisualization={() => addVisualization('markup-include', '')} type='markup-include' />
48
- <Widget addVisualization={() => addVisualization('filtered-text', '')} type='filtered-text' />
49
- <Widget addVisualization={() => addVisualization('dashboardFilters', '')} type='dashboardFilters' />
50
- <Widget addVisualization={() => addVisualization('table', '')} type='table' />
47
+ <Widget addVisualization={() => createVisualization('data-bite', '')} type='data-bite' />
48
+ <Widget addVisualization={() => createVisualization('waffle-chart', 'Waffle')} type='waffle-chart' />
49
+ <Widget addVisualization={() => createVisualization('markup-include', '')} type='markup-include' />
50
+ <Widget addVisualization={() => createVisualization('dashboardFilters', '')} type='dashboardFilters' />
51
+ <Widget addVisualization={() => createVisualization('table', '')} type='table' />
51
52
  </div>
52
53
  <AdvancedEditor
53
54
  loadConfig={loadConfig}
@@ -0,0 +1,218 @@
1
+ import React from 'react'
2
+ import { fireEvent, render, screen } from '@testing-library/react'
3
+ import { describe, expect, it, vi } from 'vitest'
4
+ import { DashboardContext, DashboardDispatchContext, initialState } from '../../DashboardContext'
5
+ import { DashboardCopyPasteContext } from '../../DashboardCopyPasteContext'
6
+ import { GlobalContext } from '@cdc/core/components/GlobalContext'
7
+ import Widget from './Widget'
8
+
9
+ vi.mock('@cdc/core/components/ui/Icon', () => ({
10
+ default: props => <span data-testid='mock-icon' {...props} />
11
+ }))
12
+
13
+ let latestDragSpec: any
14
+
15
+ vi.mock('react-dnd', () => ({
16
+ useDrag: spec => {
17
+ latestDragSpec = spec
18
+ return [{ isDragging: false }, () => {}]
19
+ }
20
+ }))
21
+
22
+ const renderWidget = (
23
+ options: {
24
+ copiedWidget?: any
25
+ copyWidget?: any
26
+ clearCopiedWidget?: any
27
+ dispatch?: any
28
+ type?: string
29
+ title?: string
30
+ widgetConfig?: any
31
+ } = {}
32
+ ) => {
33
+ const openOverlay = vi.fn()
34
+ const copyWidget = options.copyWidget || vi.fn()
35
+ const clearCopiedWidget = options.clearCopiedWidget || vi.fn()
36
+ const dispatch = options.dispatch || vi.fn()
37
+ const type = options.type || 'markup-include'
38
+ const title = options.title ?? 'Example'
39
+ const widgetConfig = options.widgetConfig || {
40
+ uid: 'markup-1',
41
+ rowIdx: 0,
42
+ colIdx: 0,
43
+ entryIdx: 0,
44
+ type: 'markup-include',
45
+ visualizationType: 'markup-include',
46
+ contentEditor: { title: 'Example' }
47
+ }
48
+
49
+ const renderResult = render(
50
+ <GlobalContext.Provider
51
+ value={{
52
+ overlay: {
53
+ object: null,
54
+ show: false,
55
+ disableBgClose: false,
56
+ actions: {
57
+ openOverlay,
58
+ toggleOverlay: vi.fn()
59
+ }
60
+ }
61
+ }}
62
+ >
63
+ <DashboardContext.Provider
64
+ value={{
65
+ ...initialState,
66
+ config: {
67
+ type: 'dashboard',
68
+ activeDashboard: 0,
69
+ dashboard: { sharedFilters: [] },
70
+ datasets: {},
71
+ rows: [
72
+ {
73
+ columns: [
74
+ {
75
+ width: 12,
76
+ conditionalWidgets: [
77
+ {
78
+ widget: 'markup-1',
79
+ dashboardCondition: {
80
+ id: 'column-condition-1',
81
+ datasetKey: 'condition-data',
82
+ operator: 'hasData'
83
+ }
84
+ }
85
+ ]
86
+ }
87
+ ],
88
+ expandCollapseAllButtons: false
89
+ }
90
+ ],
91
+ visualizations: {
92
+ 'markup-1': {
93
+ uid: 'markup-1',
94
+ type: 'markup-include',
95
+ visualizationType: 'markup-include',
96
+ contentEditor: { title: 'Example' }
97
+ }
98
+ }
99
+ } as any,
100
+ outerContainerRef: vi.fn(),
101
+ setParentConfig: vi.fn(),
102
+ isDebug: false,
103
+ isEditor: true,
104
+ reloadURLData: vi.fn(),
105
+ loadAPIFilters: vi.fn(),
106
+ setAPIFilterDropdowns: vi.fn(),
107
+ setAPILoading: vi.fn(),
108
+ data: {}
109
+ }}
110
+ >
111
+ <DashboardDispatchContext.Provider value={dispatch}>
112
+ <DashboardCopyPasteContext.Provider
113
+ value={{ copiedWidget: options.copiedWidget, copyWidget, clearCopiedWidget }}
114
+ >
115
+ <Widget title={title} toggleRow={false} type={type} widgetConfig={widgetConfig} widgetInRow />
116
+ </DashboardCopyPasteContext.Provider>
117
+ </DashboardDispatchContext.Provider>
118
+ </DashboardContext.Provider>
119
+ </GlobalContext.Provider>
120
+ )
121
+
122
+ return { ...renderResult, openOverlay, copyWidget, clearCopiedWidget, dispatch }
123
+ }
124
+
125
+ describe('Widget', () => {
126
+ it('shows the active widget condition button and summary strip when a column condition exists', () => {
127
+ renderWidget()
128
+
129
+ expect(screen.getByTitle('Configure Dashboard Condition')).toHaveClass('is-active')
130
+ expect(screen.getByRole('button', { name: "Configure Dashboard Condition: Show when there's data" })).toHaveClass(
131
+ 'dashboard-condition-summary'
132
+ )
133
+ })
134
+
135
+ it('opens the condition modal from the widget condition button or summary strip', () => {
136
+ const { openOverlay } = renderWidget()
137
+
138
+ fireEvent.click(screen.getByTitle('Configure Dashboard Condition'))
139
+ fireEvent.click(screen.getByRole('button', { name: "Configure Dashboard Condition: Show when there's data" }))
140
+
141
+ expect(openOverlay).toHaveBeenCalledTimes(2)
142
+ })
143
+
144
+ it('shows a copy button for populated widgets and stores the copied widget label', () => {
145
+ const { copyWidget } = renderWidget()
146
+
147
+ fireEvent.click(screen.getByTitle('Copy Component'))
148
+
149
+ expect(copyWidget).toHaveBeenCalledWith({ sourceWidgetKey: 'markup-1', label: 'Example' })
150
+ })
151
+
152
+ it('uses a delete button for removing widgets', () => {
153
+ const { dispatch } = renderWidget()
154
+
155
+ fireEvent.click(screen.getByRole('button', { name: 'Delete Component' }))
156
+
157
+ expect(dispatch).toHaveBeenCalledWith({
158
+ type: 'DELETE_WIDGET',
159
+ payload: { uid: 'markup-1' }
160
+ })
161
+ })
162
+
163
+ it('shows labels for waffle and gauge visualization type aliases', () => {
164
+ renderWidget({
165
+ type: 'TP5 Waffle',
166
+ title: '',
167
+ widgetConfig: {
168
+ uid: 'waffle-1',
169
+ rowIdx: 0,
170
+ colIdx: 0,
171
+ type: 'waffle-chart',
172
+ visualizationType: 'TP5 Waffle'
173
+ }
174
+ })
175
+
176
+ expect(screen.getByText('Waffle Chart')).toBeInTheDocument()
177
+ })
178
+
179
+ it('marks the copied widget and lets the active copy button cancel copy mode', () => {
180
+ const clearCopiedWidget = vi.fn()
181
+ const { container, copyWidget } = renderWidget({
182
+ copiedWidget: { sourceWidgetKey: 'markup-1', label: 'Example' },
183
+ clearCopiedWidget
184
+ })
185
+
186
+ expect(container.querySelector('.widget')).toHaveClass('widget--copied-source')
187
+ expect(screen.getByTitle('Copy Component')).toHaveClass('is-active')
188
+ expect(screen.getByRole('button', { name: 'Clear copied component' })).toHaveTextContent('Copied')
189
+
190
+ fireEvent.click(screen.getByRole('button', { name: 'Clear copied component' }))
191
+
192
+ expect(clearCopiedWidget).toHaveBeenCalledTimes(1)
193
+
194
+ fireEvent.click(screen.getByTitle('Copy Component'))
195
+
196
+ expect(clearCopiedWidget).toHaveBeenCalledTimes(2)
197
+ expect(copyWidget).not.toHaveBeenCalled()
198
+ })
199
+
200
+ it('clears copy mode after a successful widget move', () => {
201
+ const clearCopiedWidget = vi.fn()
202
+ const dispatch = vi.fn()
203
+ renderWidget({ clearCopiedWidget, dispatch })
204
+
205
+ latestDragSpec.end(null, { getDropResult: () => ({ rowIdx: 0, colIdx: 0 }) })
206
+
207
+ expect(dispatch).toHaveBeenCalledWith({
208
+ type: 'MOVE_VISUALIZATION',
209
+ payload: {
210
+ rowIdx: 0,
211
+ colIdx: 0,
212
+ entryIdx: undefined,
213
+ widget: expect.objectContaining({ uid: 'markup-1' })
214
+ }
215
+ })
216
+ expect(clearCopiedWidget).toHaveBeenCalled()
217
+ })
218
+ })