@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,538 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import reducer, { DashboardState } from './dashboard.reducer'
3
+ import { initialState } from '../DashboardContext'
4
+
5
+ const makeState = (overrides: Partial<DashboardState['config']> = {}): DashboardState => ({
6
+ ...initialState,
7
+ config: {
8
+ type: 'dashboard',
9
+ label: 'Tab A',
10
+ activeDashboard: 0,
11
+ datasets: {},
12
+ visualizations: {},
13
+ rows: [],
14
+ dashboard: { sharedFilters: [] },
15
+ multiDashboards: [
16
+ { label: 'Tab A', dashboard: { sharedFilters: [] }, visualizations: {}, rows: [] },
17
+ { label: 'Tab B', dashboard: { sharedFilters: [] }, visualizations: {}, rows: [] }
18
+ ],
19
+ ...overrides
20
+ } as any
21
+ })
22
+
23
+ describe('RENAME_DASHBOARD_TAB', () => {
24
+ it('updates config.label for the active tab', () => {
25
+ const state = makeState()
26
+ const next = reducer(state, { type: 'RENAME_DASHBOARD_TAB', payload: { current: 'Tab A', new: 'Renamed' } })
27
+ expect(next.config.label).toBe('Renamed')
28
+ })
29
+
30
+ it('updates the matching entry in multiDashboards', () => {
31
+ const state = makeState()
32
+ const next = reducer(state, { type: 'RENAME_DASHBOARD_TAB', payload: { current: 'Tab A', new: 'Renamed' } })
33
+ expect(next.config.multiDashboards[0].label).toBe('Renamed')
34
+ })
35
+
36
+ it('does not rename non-matching tabs', () => {
37
+ const state = makeState()
38
+ const next = reducer(state, { type: 'RENAME_DASHBOARD_TAB', payload: { current: 'Tab A', new: 'Renamed' } })
39
+ expect(next.config.multiDashboards[1].label).toBe('Tab B')
40
+ })
41
+
42
+ it('does not mutate the previous multiDashboards state', () => {
43
+ const state = makeState()
44
+ const next = reducer(state, { type: 'RENAME_DASHBOARD_TAB', payload: { current: 'Tab A', new: 'Renamed' } })
45
+
46
+ expect(state.config.multiDashboards[0].label).toBe('Tab A')
47
+ expect(next.config.multiDashboards[0].label).toBe('Renamed')
48
+ })
49
+ })
50
+
51
+ describe('tab-scoped async updates', () => {
52
+ it('ignores stale SET_DATA updates from a previously active dashboard', () => {
53
+ const state = makeState()
54
+ state.data = { fresh: [{ value: 'tab-b' }] }
55
+ state.config.activeDashboard = 1
56
+
57
+ const next = reducer(state, {
58
+ type: 'SET_DATA',
59
+ payload: { data: { stale: [{ value: 'tab-a' }] }, activeDashboard: 0 }
60
+ })
61
+
62
+ expect(next).toBe(state)
63
+ expect(next.data).toEqual({ fresh: [{ value: 'tab-b' }] })
64
+ })
65
+
66
+ it('ignores stale SET_FILTERED_DATA updates from a previously active dashboard', () => {
67
+ const state = makeState()
68
+ state.filteredData = { fresh: [{ value: 'tab-b' }] }
69
+ state.config.activeDashboard = 1
70
+
71
+ const next = reducer(state, {
72
+ type: 'SET_FILTERED_DATA',
73
+ payload: { filteredData: { stale: [{ value: 'tab-a' }] }, activeDashboard: 0 }
74
+ })
75
+
76
+ expect(next).toBe(state)
77
+ expect(next.filteredData).toEqual({ fresh: [{ value: 'tab-b' }] })
78
+ })
79
+ })
80
+
81
+ const baseState = (): DashboardState =>
82
+ ({
83
+ config: {
84
+ type: 'dashboard',
85
+ activeDashboard: 0,
86
+ dashboard: { sharedFilters: [] },
87
+ datasets: {},
88
+ rows: [
89
+ {
90
+ columns: [
91
+ {
92
+ width: 12,
93
+ conditionalWidgets: [
94
+ {
95
+ widget: 'viz-1',
96
+ dashboardCondition: {
97
+ id: 'condition-1',
98
+ datasetKey: 'condition-data',
99
+ operator: 'hasData'
100
+ }
101
+ },
102
+ {
103
+ widget: 'viz-2',
104
+ dashboardCondition: {
105
+ id: 'condition-2',
106
+ datasetKey: 'condition-data',
107
+ operator: 'hasNoData'
108
+ }
109
+ }
110
+ ]
111
+ }
112
+ ],
113
+ expandCollapseAllButtons: false
114
+ }
115
+ ],
116
+ visualizations: {
117
+ 'viz-1': { uid: 'viz-1', type: 'markup-include', visualizationType: 'markup-include' },
118
+ 'viz-2': { uid: 'viz-2', type: 'markup-include', visualizationType: 'markup-include' },
119
+ 'viz-3': { uid: 'viz-3', type: 'markup-include', visualizationType: 'markup-include' }
120
+ },
121
+ table: {},
122
+ runtime: {}
123
+ } as any,
124
+ data: {},
125
+ filteredData: {},
126
+ loading: false,
127
+ preview: false,
128
+ tabSelected: undefined as any,
129
+ filtersApplied: false
130
+ } as DashboardState)
131
+
132
+ describe('dashboard reducer conditional columns', () => {
133
+ it('collapses back to simple mode when deleting alternates leaves one unconditioned entry', () => {
134
+ const state = baseState()
135
+ state.config.rows[0].columns[0].conditionalWidgets = [
136
+ { widget: 'viz-1' },
137
+ {
138
+ widget: 'viz-2',
139
+ dashboardCondition: {
140
+ id: 'condition-2',
141
+ datasetKey: 'condition-data',
142
+ operator: 'hasData'
143
+ }
144
+ }
145
+ ]
146
+
147
+ const nextState = reducer(state, {
148
+ type: 'DELETE_WIDGET',
149
+ payload: { uid: 'viz-2' }
150
+ })
151
+
152
+ expect(nextState.config.rows[0].columns[0]).toMatchObject({
153
+ width: 12,
154
+ widget: 'viz-1',
155
+ conditionalWidgets: undefined
156
+ })
157
+ })
158
+
159
+ it('moves a conditional entry into a new conditional slot without dropping its condition', () => {
160
+ const state = baseState()
161
+ state.config.rows[0].columns.push({
162
+ width: 12,
163
+ conditionalWidgets: [
164
+ {
165
+ widget: 'viz-3',
166
+ dashboardCondition: {
167
+ id: 'condition-3',
168
+ datasetKey: 'condition-data',
169
+ operator: 'hasData'
170
+ }
171
+ }
172
+ ]
173
+ })
174
+
175
+ const nextState = reducer(state, {
176
+ type: 'MOVE_VISUALIZATION',
177
+ payload: {
178
+ rowIdx: 0,
179
+ colIdx: 1,
180
+ entryIdx: 1,
181
+ widget: {
182
+ uid: 'viz-1',
183
+ rowIdx: 0,
184
+ colIdx: 0,
185
+ entryIdx: 0
186
+ }
187
+ }
188
+ } as any)
189
+
190
+ expect(nextState.config.rows[0].columns[1].conditionalWidgets).toEqual([
191
+ {
192
+ widget: 'viz-3',
193
+ dashboardCondition: {
194
+ id: 'condition-3',
195
+ datasetKey: 'condition-data',
196
+ operator: 'hasData'
197
+ }
198
+ },
199
+ {
200
+ widget: 'viz-1',
201
+ dashboardCondition: {
202
+ id: 'condition-1',
203
+ datasetKey: 'condition-data',
204
+ operator: 'hasData'
205
+ }
206
+ }
207
+ ])
208
+ expect(nextState.config.rows[0].columns[0].conditionalWidgets).toEqual([
209
+ {
210
+ widget: 'viz-2',
211
+ dashboardCondition: {
212
+ id: 'condition-2',
213
+ datasetKey: 'condition-data',
214
+ operator: 'hasNoData'
215
+ }
216
+ }
217
+ ])
218
+ })
219
+
220
+ it('recomputes condition filtered data when moving a conditional widget to a row-data target', () => {
221
+ const state = baseState()
222
+ const data = [
223
+ { name: 'Alice', value: 1 },
224
+ { name: 'Bob', value: 2 }
225
+ ]
226
+ state.data = {
227
+ 'condition-data': data,
228
+ 'row-data': data
229
+ }
230
+ state.config.datasets = {
231
+ 'condition-data': { data },
232
+ 'row-data': { data }
233
+ } as any
234
+ state.config.rows = [
235
+ state.config.rows[0],
236
+ {
237
+ dataKey: 'row-data',
238
+ columns: [{ width: 12, conditionalWidgets: [{ widget: 'viz-3' }] }],
239
+ expandCollapseAllButtons: false
240
+ }
241
+ ] as any
242
+ state.config.dashboard.sharedFilters = [
243
+ {
244
+ key: 'Source Name',
245
+ type: 'datafilter',
246
+ columnName: 'name',
247
+ active: 'Alice',
248
+ usedBy: ['viz-1']
249
+ },
250
+ {
251
+ key: 'Target Row Name',
252
+ type: 'datafilter',
253
+ columnName: 'name',
254
+ active: 'Bob',
255
+ usedBy: [1]
256
+ }
257
+ ] as any
258
+ state.filteredData = {
259
+ 'condition-1': [data[0]]
260
+ }
261
+
262
+ const nextState = reducer(state, {
263
+ type: 'MOVE_VISUALIZATION',
264
+ payload: {
265
+ rowIdx: 1,
266
+ colIdx: 0,
267
+ entryIdx: 1,
268
+ widget: {
269
+ uid: 'viz-1',
270
+ rowIdx: 0,
271
+ colIdx: 0,
272
+ entryIdx: 0
273
+ }
274
+ }
275
+ } as any)
276
+
277
+ expect(nextState.config.rows[1].columns[0].conditionalWidgets?.[1].widget).toBe('viz-1')
278
+ expect(nextState.filteredData).toMatchObject({
279
+ '1': [data[1]],
280
+ 'condition-1': [data[1]]
281
+ })
282
+ })
283
+
284
+ it('preserves shared filter widget and unknown targets when a condition edit replaces an id', () => {
285
+ const state = baseState()
286
+ state.config.dashboard.sharedFilters = [
287
+ {
288
+ key: 'County',
289
+ type: 'datafilter',
290
+ columnName: 'county',
291
+ usedBy: ['legacy-footnote-target', 'viz-1']
292
+ }
293
+ ] as any
294
+
295
+ const nextState = reducer(state, {
296
+ type: 'UPDATE_ROW',
297
+ payload: {
298
+ rowIndex: 0,
299
+ rowData: {
300
+ columns: [
301
+ {
302
+ width: 12,
303
+ conditionalWidgets: [
304
+ {
305
+ widget: 'viz-1',
306
+ dashboardCondition: {
307
+ id: 'condition-1-replacement',
308
+ datasetKey: 'condition-data',
309
+ operator: 'hasData'
310
+ }
311
+ },
312
+ {
313
+ widget: 'viz-2',
314
+ dashboardCondition: {
315
+ id: 'condition-2',
316
+ datasetKey: 'condition-data',
317
+ operator: 'hasNoData'
318
+ }
319
+ }
320
+ ]
321
+ }
322
+ ]
323
+ }
324
+ }
325
+ } as any)
326
+
327
+ expect(nextState.config.dashboard.sharedFilters[0].usedBy).toEqual(['legacy-footnote-target', 'viz-1'])
328
+ })
329
+
330
+ it('preserves shared filter widget and unknown targets when a row condition is cleared', () => {
331
+ const state = baseState()
332
+ state.config.rows[0].dashboardCondition = {
333
+ id: 'row-condition-1',
334
+ datasetKey: 'condition-data',
335
+ operator: 'hasData'
336
+ }
337
+ state.config.dashboard.sharedFilters = [
338
+ {
339
+ key: 'County',
340
+ type: 'datafilter',
341
+ columnName: 'county',
342
+ usedBy: ['legacy-footnote-target', 'viz-1']
343
+ }
344
+ ] as any
345
+
346
+ const nextState = reducer(state, {
347
+ type: 'UPDATE_ROW',
348
+ payload: {
349
+ rowIndex: 0,
350
+ rowData: {
351
+ dashboardCondition: undefined
352
+ }
353
+ }
354
+ } as any)
355
+
356
+ expect(nextState.config.dashboard.sharedFilters[0].usedBy).toEqual(['legacy-footnote-target', 'viz-1'])
357
+ })
358
+
359
+ it('removes deleted widget targets while preserving unknown targets', () => {
360
+ const state = baseState()
361
+ state.config.dashboard.sharedFilters = [
362
+ {
363
+ key: 'County',
364
+ type: 'datafilter',
365
+ columnName: 'county',
366
+ usedBy: ['legacy-footnote-target', 'viz-2']
367
+ }
368
+ ] as any
369
+
370
+ const nextState = reducer(state, {
371
+ type: 'DELETE_WIDGET',
372
+ payload: { uid: 'viz-2' }
373
+ })
374
+
375
+ expect(nextState.config.dashboard.sharedFilters[0].usedBy).toEqual(['legacy-footnote-target'])
376
+ })
377
+
378
+ it('clones a visualization through the multi-dashboard save path', () => {
379
+ const state = baseState()
380
+ delete state.config.rows[0].columns[0].conditionalWidgets[0].dashboardCondition
381
+ state.config.rows[0].columns.push({ width: 12 })
382
+ state.config.multiDashboards = [
383
+ {
384
+ label: 'Tab A',
385
+ dashboard: state.config.dashboard,
386
+ visualizations: state.config.visualizations,
387
+ rows: state.config.rows
388
+ },
389
+ { label: 'Tab B', dashboard: { sharedFilters: [] }, visualizations: {}, rows: [] }
390
+ ] as any
391
+ state.config.activeDashboard = 0
392
+
393
+ const nextState = reducer(state, {
394
+ type: 'CLONE_VISUALIZATION',
395
+ payload: { sourceWidgetKey: 'viz-1', rowIdx: 0, colIdx: 1 }
396
+ })
397
+ const clonedWidgetKey = nextState.config.rows[0].columns[1].widget
398
+
399
+ expect(clonedWidgetKey).toBeTruthy()
400
+ expect(clonedWidgetKey).not.toBe('viz-1')
401
+ expect(nextState.config.visualizations[clonedWidgetKey].uid).toBe(clonedWidgetKey)
402
+ expect(nextState.config.multiDashboards[0].rows[0].columns[1].widget).toBe(clonedWidgetKey)
403
+ expect(nextState.config.multiDashboards[1].label).toBe('Tab B')
404
+ })
405
+
406
+ it('precomputes filtered data for a cloned visualization targeted by shared filters', () => {
407
+ const state = baseState()
408
+ const data = [
409
+ { name: 'Alice', value: 1 },
410
+ { name: 'Bob', value: 2 }
411
+ ]
412
+
413
+ delete state.config.rows[0].columns[0].conditionalWidgets[0].dashboardCondition
414
+ state.config.rows[0].columns.push({ width: 12 })
415
+ state.config.datasets = {
416
+ data1: { data }
417
+ } as any
418
+ state.data = { data1: data }
419
+ state.config.visualizations['viz-1'] = {
420
+ uid: 'viz-1',
421
+ type: 'chart',
422
+ visualizationType: 'Bar',
423
+ dataKey: 'data1'
424
+ } as any
425
+ state.config.dashboard.sharedFilters = [
426
+ {
427
+ key: 'name',
428
+ id: 1,
429
+ type: 'datafilter',
430
+ filterStyle: 'dropdown',
431
+ showDropdown: true,
432
+ parents: [],
433
+ values: ['Alice', 'Bob'],
434
+ active: 'Alice',
435
+ columnName: 'name',
436
+ usedBy: ['viz-1']
437
+ }
438
+ ] as any
439
+
440
+ const nextState = reducer(state, {
441
+ type: 'CLONE_VISUALIZATION',
442
+ payload: { sourceWidgetKey: 'viz-1', rowIdx: 0, colIdx: 1 }
443
+ })
444
+ const clonedWidgetKey = nextState.config.rows[0].columns[1].widget
445
+
446
+ expect(nextState.config.dashboard.sharedFilters[0].usedBy).toEqual(['viz-1', clonedWidgetKey])
447
+ expect(nextState.filteredData).toMatchObject({
448
+ 'viz-1': [data[0]],
449
+ [clonedWidgetKey]: [data[0]]
450
+ })
451
+ })
452
+
453
+ it('uses the destination row as the cloned filter target when cloning a conditioned widget into row data', () => {
454
+ const state = baseState()
455
+ const data = [
456
+ { name: 'Alice', value: 1 },
457
+ { name: 'Bob', value: 2 }
458
+ ]
459
+
460
+ state.config.rows.push({
461
+ dataKey: 'row-data',
462
+ columns: [{ width: 12 }],
463
+ expandCollapseAllButtons: false
464
+ } as any)
465
+ state.config.datasets = {
466
+ 'condition-data': { data },
467
+ 'row-data': { data }
468
+ } as any
469
+ state.data = {
470
+ 'condition-data': data,
471
+ 'row-data': data
472
+ }
473
+ state.config.dashboard.sharedFilters = [
474
+ {
475
+ key: 'name',
476
+ id: 1,
477
+ type: 'datafilter',
478
+ filterStyle: 'dropdown',
479
+ showDropdown: true,
480
+ parents: [],
481
+ values: ['Alice', 'Bob'],
482
+ active: 'Alice',
483
+ columnName: 'name',
484
+ usedBy: ['viz-1']
485
+ }
486
+ ] as any
487
+
488
+ const nextState = reducer(state, {
489
+ type: 'CLONE_VISUALIZATION',
490
+ payload: { sourceWidgetKey: 'viz-1', rowIdx: 1, colIdx: 0 }
491
+ })
492
+ const clonedEntry = nextState.config.rows[1].columns[0].conditionalWidgets?.[0]
493
+ const clonedConditionId = clonedEntry?.dashboardCondition?.id
494
+
495
+ expect(clonedEntry?.widget).toBeTruthy()
496
+ expect(clonedConditionId).toBeTruthy()
497
+ expect(nextState.config.dashboard.sharedFilters[0].usedBy).toEqual(['viz-1', 1])
498
+ expect(nextState.config.dashboard.sharedFilters[0].usedBy).not.toContain(clonedEntry?.widget)
499
+ expect(nextState.filteredData[clonedConditionId as string]).toEqual([data[0]])
500
+ })
501
+ })
502
+
503
+ describe('SET_SHARED_FILTERS', () => {
504
+ it('recomputes filteredData when shared filter targets change', () => {
505
+ const state = {
506
+ ...makeState({
507
+ datasets: {
508
+ data1: { data: [{ name: 'Alice' }, { name: 'Bob' }] }
509
+ },
510
+ rows: [{ columns: [{ width: 12, widget: 'vizA' }] }],
511
+ visualizations: {
512
+ vizA: { uid: 'vizA', type: 'data-bite', visualizationType: 'data-bite', dataKey: 'data1' }
513
+ }
514
+ }),
515
+ data: {
516
+ data1: [{ name: 'Alice' }, { name: 'Bob' }]
517
+ },
518
+ filteredData: { vizA: [{ name: 'Bob' }] }
519
+ } as DashboardState
520
+
521
+ const next = reducer(state, {
522
+ type: 'SET_SHARED_FILTERS',
523
+ payload: [
524
+ {
525
+ key: 'Name',
526
+ type: 'datafilter',
527
+ columnName: 'name',
528
+ active: 'Alice',
529
+ usedBy: ['vizA']
530
+ }
531
+ ]
532
+ } as any)
533
+
534
+ expect(next.filteredData).toEqual({
535
+ vizA: [{ name: 'Alice' }]
536
+ })
537
+ })
538
+ })