@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
@@ -0,0 +1,420 @@
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 { GlobalContext } from '@cdc/core/components/GlobalContext'
6
+ import { DashboardConditionModal } from './DashboardConditionModal'
7
+
8
+ vi.mock('@cdc/core/components/ui/Icon', () => ({
9
+ default: props => <span data-testid='mock-icon' {...props} />
10
+ }))
11
+
12
+ vi.mock('@cdc/core/components/ui/Tooltip', () => {
13
+ const Tooltip = ({ children }) => <div data-testid='tooltip'>{children}</div>
14
+ Tooltip.Target = ({ children }) => <>{children}</>
15
+ Tooltip.Content = ({ children }) => <>{children}</>
16
+ return { default: Tooltip }
17
+ })
18
+
19
+ const buildConfig = () =>
20
+ ({
21
+ type: 'dashboard',
22
+ dashboard: {
23
+ sharedFilters: []
24
+ },
25
+ datasets: {
26
+ 'condition-data': {
27
+ data: [
28
+ { region: 'North', status: 'visible' },
29
+ { region: 'South', status: 'hidden' }
30
+ ]
31
+ }
32
+ },
33
+ rows: [
34
+ {
35
+ columns: [{ width: 12, widget: 'markup-1' }],
36
+ expandCollapseAllButtons: false
37
+ }
38
+ ],
39
+ visualizations: {
40
+ 'markup-1': {
41
+ uid: 'markup-1',
42
+ type: 'markup-include',
43
+ visualizationType: 'markup-include',
44
+ contentEditor: {
45
+ inlineHTML: '<p>Example</p>',
46
+ showHeader: true,
47
+ srcUrl: '',
48
+ title: 'Example',
49
+ useInlineHTML: true
50
+ }
51
+ }
52
+ }
53
+ } as any)
54
+
55
+ const buildConditionalConfig = (dashboardCondition = undefined) =>
56
+ ({
57
+ ...buildConfig(),
58
+ rows: [
59
+ {
60
+ columns: [
61
+ {
62
+ width: 12,
63
+ conditionalWidgets: [{ widget: 'markup-1', dashboardCondition }]
64
+ }
65
+ ],
66
+ expandCollapseAllButtons: false
67
+ }
68
+ ]
69
+ } as any)
70
+
71
+ const renderModal = () => {
72
+ const dispatch = vi.fn()
73
+ const toggleOverlay = vi.fn()
74
+
75
+ render(
76
+ <GlobalContext.Provider
77
+ value={{
78
+ overlay: {
79
+ object: null,
80
+ show: true,
81
+ disableBgClose: false,
82
+ actions: {
83
+ openOverlay: vi.fn(),
84
+ toggleOverlay
85
+ }
86
+ }
87
+ }}
88
+ >
89
+ <DashboardContext.Provider
90
+ value={{
91
+ ...initialState,
92
+ config: buildConfig(),
93
+ outerContainerRef: vi.fn(),
94
+ setParentConfig: vi.fn(),
95
+ isDebug: false,
96
+ isEditor: true,
97
+ reloadURLData: vi.fn(),
98
+ loadAPIFilters: vi.fn(),
99
+ setAPIFilterDropdowns: vi.fn(),
100
+ setAPILoading: vi.fn()
101
+ }}
102
+ >
103
+ <DashboardDispatchContext.Provider value={dispatch}>
104
+ <DashboardConditionModal rowIndex={0} columnIndex={0} />
105
+ </DashboardDispatchContext.Provider>
106
+ </DashboardContext.Provider>
107
+ </GlobalContext.Provider>
108
+ )
109
+
110
+ return { dispatch, toggleOverlay }
111
+ }
112
+
113
+ const renderRowModal = (dashboardCondition = undefined) => {
114
+ const dispatch = vi.fn()
115
+ const toggleOverlay = vi.fn()
116
+
117
+ render(
118
+ <GlobalContext.Provider
119
+ value={{
120
+ overlay: {
121
+ object: null,
122
+ show: true,
123
+ disableBgClose: false,
124
+ actions: {
125
+ openOverlay: vi.fn(),
126
+ toggleOverlay
127
+ }
128
+ }
129
+ }}
130
+ >
131
+ <DashboardContext.Provider
132
+ value={{
133
+ ...initialState,
134
+ config: {
135
+ ...buildConfig(),
136
+ rows: [
137
+ {
138
+ columns: [{ width: 12, widget: 'markup-1' }],
139
+ dashboardCondition,
140
+ expandCollapseAllButtons: false
141
+ }
142
+ ]
143
+ },
144
+ outerContainerRef: vi.fn(),
145
+ setParentConfig: vi.fn(),
146
+ isDebug: false,
147
+ isEditor: true,
148
+ reloadURLData: vi.fn(),
149
+ loadAPIFilters: vi.fn(),
150
+ setAPIFilterDropdowns: vi.fn(),
151
+ setAPILoading: vi.fn()
152
+ }}
153
+ >
154
+ <DashboardDispatchContext.Provider value={dispatch}>
155
+ <DashboardConditionModal rowIndex={0} />
156
+ </DashboardDispatchContext.Provider>
157
+ </DashboardContext.Provider>
158
+ </GlobalContext.Provider>
159
+ )
160
+
161
+ return { dispatch, toggleOverlay }
162
+ }
163
+
164
+ const renderConditionalEntryModal = (dashboardCondition = undefined) => {
165
+ const dispatch = vi.fn()
166
+
167
+ render(
168
+ <GlobalContext.Provider
169
+ value={{
170
+ overlay: {
171
+ object: null,
172
+ show: true,
173
+ disableBgClose: false,
174
+ actions: {
175
+ openOverlay: vi.fn(),
176
+ toggleOverlay: vi.fn()
177
+ }
178
+ }
179
+ }}
180
+ >
181
+ <DashboardContext.Provider
182
+ value={{
183
+ ...initialState,
184
+ config: buildConditionalConfig(dashboardCondition),
185
+ outerContainerRef: vi.fn(),
186
+ setParentConfig: vi.fn(),
187
+ isDebug: false,
188
+ isEditor: true,
189
+ reloadURLData: vi.fn(),
190
+ loadAPIFilters: vi.fn(),
191
+ setAPIFilterDropdowns: vi.fn(),
192
+ setAPILoading: vi.fn()
193
+ }}
194
+ >
195
+ <DashboardDispatchContext.Provider value={dispatch}>
196
+ <DashboardConditionModal rowIndex={0} columnIndex={0} entryIndex={0} />
197
+ </DashboardDispatchContext.Provider>
198
+ </DashboardContext.Provider>
199
+ </GlobalContext.Provider>
200
+ )
201
+
202
+ return { dispatch }
203
+ }
204
+
205
+ describe('DashboardConditionModal', () => {
206
+ it('shows component-specific condition type tooltip copy and delayed field guidance', () => {
207
+ renderModal()
208
+
209
+ expect(screen.getByRole('combobox', { name: /Condition Type/i })).toBeInTheDocument()
210
+ expect(screen.getByText(/Choose whether this component should appear/)).toBeInTheDocument()
211
+
212
+ fireEvent.change(screen.getByRole('combobox', { name: /Condition Type/i }), {
213
+ target: { value: 'columnHasAnyValue' }
214
+ })
215
+
216
+ expect(screen.queryByText(/Select the dataset column to inspect for this condition/)).not.toBeInTheDocument()
217
+ expect(screen.queryByText(/Choose one or more matching values from the selected column/)).not.toBeInTheDocument()
218
+ expect(screen.queryByText('Column Values')).not.toBeInTheDocument()
219
+
220
+ fireEvent.change(screen.getAllByRole('combobox')[1], {
221
+ target: { value: 'condition-data' }
222
+ })
223
+
224
+ expect(screen.getByText(/Select the dataset column to inspect for this condition/)).toBeInTheDocument()
225
+ expect(screen.queryByText(/Choose one or more matching values from the selected column/)).not.toBeInTheDocument()
226
+
227
+ fireEvent.change(screen.getAllByRole('combobox')[2], { target: { value: 'region' } })
228
+
229
+ expect(screen.getByText('Column Values')).toBeInTheDocument()
230
+ expect(screen.getByText(/Choose one or more matching values from the selected column/)).toBeInTheDocument()
231
+ })
232
+
233
+ it('shows row-specific condition type tooltip copy for row editing', () => {
234
+ renderRowModal()
235
+
236
+ expect(screen.getByText(/Choose whether this row should appear/)).toBeInTheDocument()
237
+ expect(screen.queryByText(/Choose whether this component should appear/)).not.toBeInTheDocument()
238
+ })
239
+
240
+ it('saves a hasData condition', () => {
241
+ const { dispatch, toggleOverlay } = renderModal()
242
+
243
+ fireEvent.change(screen.getByRole('combobox', { name: /Condition Type/i }), { target: { value: 'hasData' } })
244
+ fireEvent.change(screen.getAllByRole('combobox')[1], {
245
+ target: { value: 'condition-data' }
246
+ })
247
+ fireEvent.click(screen.getByRole('button', { name: 'Save' }))
248
+
249
+ expect(dispatch).toHaveBeenCalledWith({
250
+ type: 'UPDATE_ROW',
251
+ payload: {
252
+ rowIndex: 0,
253
+ rowData: {
254
+ columns: [
255
+ expect.objectContaining({
256
+ conditionalWidgets: [
257
+ expect.objectContaining({
258
+ widget: 'markup-1',
259
+ dashboardCondition: expect.objectContaining({
260
+ datasetKey: 'condition-data',
261
+ operator: 'hasData'
262
+ })
263
+ })
264
+ ]
265
+ })
266
+ ]
267
+ }
268
+ }
269
+ })
270
+ expect(toggleOverlay).toHaveBeenCalledWith(false)
271
+ })
272
+
273
+ it('converts a simple column into conditional mode only after save', () => {
274
+ const { dispatch } = renderModal()
275
+
276
+ fireEvent.change(screen.getByRole('combobox', { name: /Condition Type/i }), { target: { value: 'hasData' } })
277
+ fireEvent.change(screen.getAllByRole('combobox')[1], {
278
+ target: { value: 'condition-data' }
279
+ })
280
+
281
+ expect(dispatch).not.toHaveBeenCalled()
282
+
283
+ fireEvent.click(screen.getByRole('button', { name: 'Save' }))
284
+
285
+ expect(dispatch).toHaveBeenCalledWith({
286
+ type: 'UPDATE_ROW',
287
+ payload: {
288
+ rowIndex: 0,
289
+ rowData: {
290
+ columns: [
291
+ expect.objectContaining({
292
+ widget: undefined,
293
+ conditionalWidgets: [
294
+ expect.objectContaining({
295
+ widget: 'markup-1',
296
+ dashboardCondition: expect.objectContaining({
297
+ datasetKey: 'condition-data',
298
+ operator: 'hasData'
299
+ })
300
+ })
301
+ ]
302
+ })
303
+ ]
304
+ }
305
+ }
306
+ })
307
+ })
308
+
309
+ it('collapses a one-entry conditional column back to simple mode when its condition is removed', () => {
310
+ const { dispatch } = renderConditionalEntryModal({
311
+ id: 'existing-condition',
312
+ datasetKey: 'condition-data',
313
+ operator: 'hasData'
314
+ })
315
+
316
+ fireEvent.change(screen.getByRole('combobox', { name: /Condition Type/i }), { target: { value: '' } })
317
+ fireEvent.click(screen.getByRole('button', { name: 'Save' }))
318
+
319
+ expect(dispatch).toHaveBeenCalledWith({
320
+ type: 'UPDATE_ROW',
321
+ payload: {
322
+ rowIndex: 0,
323
+ rowData: {
324
+ columns: [
325
+ expect.objectContaining({
326
+ widget: 'markup-1',
327
+ conditionalWidgets: undefined
328
+ })
329
+ ]
330
+ }
331
+ }
332
+ })
333
+ })
334
+
335
+ it('collapses conditional mode back to simple mode when the last condition is removed', () => {
336
+ const { dispatch } = renderConditionalEntryModal({
337
+ id: 'existing-condition',
338
+ datasetKey: 'condition-data',
339
+ operator: 'hasData'
340
+ })
341
+
342
+ fireEvent.change(screen.getByRole('combobox', { name: /Condition Type/i }), { target: { value: '' } })
343
+ fireEvent.click(screen.getByRole('button', { name: 'Save' }))
344
+
345
+ expect(dispatch).toHaveBeenCalledWith({
346
+ type: 'UPDATE_ROW',
347
+ payload: {
348
+ rowIndex: 0,
349
+ rowData: {
350
+ columns: [expect.objectContaining({ widget: 'markup-1', conditionalWidgets: undefined })]
351
+ }
352
+ }
353
+ })
354
+ })
355
+
356
+ it('requires dataset, column, and values for Column Has Any Value', () => {
357
+ renderModal()
358
+
359
+ fireEvent.change(screen.getByRole('combobox', { name: /Condition Type/i }), {
360
+ target: { value: 'columnHasAnyValue' }
361
+ })
362
+
363
+ const saveButton = screen.getByRole('button', { name: 'Save' })
364
+ expect(saveButton).toBeDisabled()
365
+
366
+ fireEvent.change(screen.getAllByRole('combobox')[1], {
367
+ target: { value: 'condition-data' }
368
+ })
369
+ expect(saveButton).toBeDisabled()
370
+
371
+ fireEvent.change(screen.getAllByRole('combobox')[2], { target: { value: 'region' } })
372
+ expect(saveButton).toBeDisabled()
373
+
374
+ fireEvent.click(screen.getByLabelText('Expand'))
375
+ fireEvent.click(screen.getByText('North'))
376
+
377
+ expect(saveButton).not.toBeDisabled()
378
+ })
379
+
380
+ it('allows filtersIncomplete without dataset, column, or values', () => {
381
+ const { dispatch } = renderModal()
382
+
383
+ fireEvent.change(screen.getByRole('combobox', { name: /Condition Type/i }), {
384
+ target: { value: 'filtersIncomplete' }
385
+ })
386
+
387
+ expect(screen.getByRole('option', { name: 'Show when filters are incomplete' })).toBeInTheDocument()
388
+ expect(screen.getAllByRole('combobox')).toHaveLength(1)
389
+ expect(screen.queryByText('Column Values')).not.toBeInTheDocument()
390
+
391
+ const saveButton = screen.getByRole('button', { name: 'Save' })
392
+ expect(saveButton).not.toBeDisabled()
393
+
394
+ fireEvent.click(saveButton)
395
+
396
+ expect(dispatch).toHaveBeenCalledWith({
397
+ type: 'UPDATE_ROW',
398
+ payload: {
399
+ rowIndex: 0,
400
+ rowData: {
401
+ columns: [
402
+ expect.objectContaining({
403
+ conditionalWidgets: [
404
+ expect.objectContaining({
405
+ widget: 'markup-1',
406
+ dashboardCondition: expect.objectContaining({
407
+ operator: 'filtersIncomplete'
408
+ })
409
+ })
410
+ ]
411
+ })
412
+ ]
413
+ }
414
+ }
415
+ })
416
+ expect(
417
+ dispatch.mock.calls[0][0].payload.rowData.columns[0].conditionalWidgets[0].dashboardCondition
418
+ ).not.toHaveProperty('datasetKey')
419
+ })
420
+ })