@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
@@ -0,0 +1,428 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+ import {
3
+ dashboardRowsUseFiltersIncomplete,
4
+ ensureRowConditionIds,
5
+ evaluateDashboardCondition,
6
+ getDashboardConditionFilteredData,
7
+ hasIncompleteFiltersForDashboardCondition
8
+ } from '../dashboardConditions'
9
+ import {
10
+ getDashboardConditionTargets,
11
+ getSharedFilterTargetOptions,
12
+ remapRowTargetsInSharedFilters
13
+ } from '../dashboardFilterTargets'
14
+
15
+ describe('dashboardConditions', () => {
16
+ afterEach(() => {
17
+ vi.restoreAllMocks()
18
+ })
19
+
20
+ it('assigns missing condition ids and preserves existing ones', () => {
21
+ vi.spyOn(Math, 'random').mockReturnValueOnce(0.123456789).mockReturnValueOnce(0.23456789)
22
+
23
+ const rows = ensureRowConditionIds([
24
+ {
25
+ columns: [
26
+ {
27
+ width: 6,
28
+ conditionalWidgets: [
29
+ { widget: 'viz-1', dashboardCondition: { datasetKey: 'dataset-1', operator: 'hasData' } }
30
+ ]
31
+ },
32
+ { width: 6, widget: 'viz-2' }
33
+ ],
34
+ dashboardCondition: { datasetKey: 'dataset-1', operator: 'hasData' },
35
+ expandCollapseAllButtons: false
36
+ }
37
+ ] as any)
38
+
39
+ expect(rows[0].dashboardCondition?.id).toMatch(/^condition-[a-z0-9]{8}$/)
40
+ expect(rows[0].columns[0].conditionalWidgets?.[0].dashboardCondition?.id).toMatch(/^condition-[a-z0-9]{8}$/)
41
+ expect(rows[0].dashboardCondition?.id).not.toBe(rows[0].columns[0].conditionalWidgets?.[0].dashboardCondition?.id)
42
+ expect(rows[0].columns[1]).toMatchObject({ widget: 'viz-2' })
43
+ })
44
+
45
+ it('assigns row condition ids without requiring normalized columns', () => {
46
+ vi.spyOn(Math, 'random').mockReturnValue(0.123456789)
47
+
48
+ const rows = ensureRowConditionIds([
49
+ {
50
+ dashboardCondition: { datasetKey: 'dataset-1', operator: 'hasData' },
51
+ expandCollapseAllButtons: false
52
+ }
53
+ ] as any)
54
+
55
+ expect(rows[0].dashboardCondition?.id).toMatch(/^condition-[a-z0-9]{8}$/)
56
+ expect(rows[0]).not.toHaveProperty('columns')
57
+ })
58
+
59
+ it('preserves legacy array-shaped rows for version migrations', () => {
60
+ const legacyRow = [{ width: 12, widget: 'viz-1' }]
61
+ const rows = ensureRowConditionIds([legacyRow] as any)
62
+
63
+ expect(rows[0]).toBe(legacyRow)
64
+ })
65
+
66
+ it('tracks dashboard condition owner filter targets without exposing condition ids as Used By options', () => {
67
+ const rows = [
68
+ {
69
+ columns: [
70
+ {
71
+ width: 12,
72
+ conditionalWidgets: [
73
+ { widget: 'viz-1', dashboardCondition: { id: 'column-condition-1', operator: 'hasData' } },
74
+ { widget: 'viz-3', dashboardCondition: { id: 'column-condition-3', operator: 'hasNoData' } }
75
+ ]
76
+ }
77
+ ],
78
+ dashboardCondition: { id: 'row-condition-1', operator: 'hasData' },
79
+ expandCollapseAllButtons: false
80
+ },
81
+ {
82
+ columns: [
83
+ {
84
+ width: 12,
85
+ conditionalWidgets: [
86
+ { widget: 'viz-row-data', dashboardCondition: { id: 'row-data-condition', operator: 'hasData' } }
87
+ ]
88
+ }
89
+ ],
90
+ dataKey: 'row-data',
91
+ expandCollapseAllButtons: false
92
+ },
93
+ {
94
+ columns: [{ width: 12, widget: 'viz-2' }],
95
+ dashboardCondition: { id: 'row-condition-2', operator: 'hasData' },
96
+ expandCollapseAllButtons: false,
97
+ toggle: true
98
+ }
99
+ ] as any
100
+ const { nameLookup, options } = getSharedFilterTargetOptions(
101
+ {
102
+ dashboard: { sharedFilters: [] },
103
+ rows,
104
+ visualizations: {
105
+ 'viz-1': { uid: 'viz-1', type: 'markup-include', visualizationType: 'markup-include' },
106
+ 'viz-2': { uid: 'viz-2', type: 'markup-include', visualizationType: 'markup-include' },
107
+ 'viz-3': { uid: 'viz-3', type: 'markup-include', visualizationType: 'markup-include' },
108
+ 'viz-row-data': { uid: 'viz-row-data', type: 'markup-include', visualizationType: 'markup-include' }
109
+ }
110
+ } as any,
111
+ {}
112
+ )
113
+
114
+ expect(
115
+ getDashboardConditionTargets(rows).map(conditionTarget => ({
116
+ id: conditionTarget.id,
117
+ filterTarget: conditionTarget.filterTarget
118
+ }))
119
+ ).toEqual([
120
+ { id: 'row-condition-1', filterTarget: 0 },
121
+ { id: 'column-condition-1', filterTarget: 'viz-1' },
122
+ { id: 'column-condition-3', filterTarget: 'viz-3' },
123
+ { id: 'row-data-condition', filterTarget: 1 }
124
+ ])
125
+ expect(options).toEqual(['viz-1', 'viz-2', 'viz-3', 0, 1])
126
+ expect(nameLookup['0']).toBe('Row 1')
127
+ expect(nameLookup['1']).toBe('Row 2')
128
+ expect(nameLookup['row-condition-1']).toBeUndefined()
129
+ expect(nameLookup['column-condition-1']).toBeUndefined()
130
+ expect(nameLookup['row-condition-2']).toBeUndefined()
131
+ })
132
+
133
+ it('preserves existing Used By row targets that are not normally selectable', () => {
134
+ const { nameLookup, options } = getSharedFilterTargetOptions(
135
+ {
136
+ dashboard: { sharedFilters: [] },
137
+ rows: [
138
+ { columns: [{ width: 12, widget: 'markup-1' }] },
139
+ { dataKey: '', columns: [{ width: 12, widget: 'markup-2' }] }
140
+ ],
141
+ visualizations: {
142
+ 'markup-1': { uid: 'markup-1', type: 'markup-include', visualizationType: 'markup-include' },
143
+ 'markup-2': { uid: 'markup-2', type: 'markup-include', visualizationType: 'markup-include' }
144
+ }
145
+ } as any,
146
+ { usedBy: [1, 'missing-target', 'row-condition-1'] }
147
+ )
148
+
149
+ expect(options).toEqual(['markup-1', 'markup-2', 1])
150
+ expect(nameLookup['1']).toBe('Row 2')
151
+ expect(nameLookup['missing-target']).toBeUndefined()
152
+ expect(nameLookup['row-condition-1']).toBeUndefined()
153
+ })
154
+
155
+ it('treats reset-state filters as unresolved instead of hasNoData', () => {
156
+ const filteredData = getDashboardConditionFilteredData(
157
+ { id: 'row-condition-1', datasetKey: 'condition-data', operator: 'hasNoData' },
158
+ {
159
+ sharedFilters: [
160
+ {
161
+ key: 'Region',
162
+ type: 'datafilter',
163
+ columnName: 'region',
164
+ showDropdown: true,
165
+ active: '',
166
+ usedBy: [0]
167
+ }
168
+ ]
169
+ } as any,
170
+ {
171
+ 'condition-data': [{ region: 'East' }]
172
+ },
173
+ 0
174
+ )
175
+
176
+ expect(filteredData).toBeUndefined()
177
+ })
178
+
179
+ it('detects filtersIncomplete conditions without requiring a dataset', () => {
180
+ expect(
181
+ dashboardRowsUseFiltersIncomplete([
182
+ {
183
+ columns: [
184
+ {
185
+ width: 12,
186
+ conditionalWidgets: [
187
+ {
188
+ widget: 'markup-1',
189
+ dashboardCondition: { id: 'column-condition-1', operator: 'filtersIncomplete' }
190
+ }
191
+ ]
192
+ }
193
+ ],
194
+ expandCollapseAllButtons: false
195
+ }
196
+ ] as any)
197
+ ).toBe(true)
198
+
199
+ const filteredData = getDashboardConditionFilteredData(
200
+ { id: 'column-condition-1', operator: 'filtersIncomplete' },
201
+ {
202
+ sharedFilters: [
203
+ {
204
+ key: 'Region',
205
+ type: 'datafilter',
206
+ columnName: 'region',
207
+ showDropdown: true,
208
+ active: '',
209
+ usedBy: ['markup-1']
210
+ }
211
+ ]
212
+ } as any,
213
+ {},
214
+ 'markup-1'
215
+ )
216
+
217
+ expect(filteredData).toEqual([{}])
218
+ expect(
219
+ evaluateDashboardCondition({ id: 'column-condition-1', operator: 'filtersIncomplete' }, filteredData)
220
+ ).toEqual({ matches: true, resolved: true })
221
+ })
222
+
223
+ it('uses owner-target filter semantics for filtersIncomplete, including unscoped filters', () => {
224
+ const dashboard = {
225
+ sharedFilters: [
226
+ {
227
+ key: 'Unscoped Region',
228
+ type: 'datafilter',
229
+ columnName: 'region',
230
+ showDropdown: true,
231
+ active: '',
232
+ usedBy: []
233
+ }
234
+ ]
235
+ } as any
236
+
237
+ expect(
238
+ hasIncompleteFiltersForDashboardCondition(
239
+ { id: 'column-condition-1', operator: 'filtersIncomplete' },
240
+ dashboard,
241
+ 'markup-1'
242
+ )
243
+ ).toBe(true)
244
+ })
245
+
246
+ it('ignores reset filters scoped only to unrelated targets for filtersIncomplete', () => {
247
+ const filteredData = getDashboardConditionFilteredData(
248
+ { id: 'column-condition-1', operator: 'filtersIncomplete' },
249
+ {
250
+ sharedFilters: [
251
+ {
252
+ key: 'Other Region',
253
+ type: 'datafilter',
254
+ columnName: 'region',
255
+ showDropdown: true,
256
+ active: '',
257
+ usedBy: ['other-widget']
258
+ }
259
+ ]
260
+ } as any,
261
+ {},
262
+ 'markup-1'
263
+ )
264
+
265
+ expect(filteredData).toEqual([])
266
+ expect(
267
+ evaluateDashboardCondition({ id: 'column-condition-1', operator: 'filtersIncomplete' }, filteredData)
268
+ ).toEqual({ matches: false, resolved: true })
269
+ })
270
+
271
+ it('ignores filters whose columns are missing from the condition dataset', () => {
272
+ const filteredData = getDashboardConditionFilteredData(
273
+ { id: 'row-condition-1', datasetKey: 'condition-data', operator: 'hasData' },
274
+ {
275
+ sharedFilters: [
276
+ {
277
+ key: 'Missing Column Filter',
278
+ type: 'datafilter',
279
+ columnName: 'missingColumn',
280
+ showDropdown: true,
281
+ active: 'x',
282
+ usedBy: [0]
283
+ }
284
+ ]
285
+ } as any,
286
+ {
287
+ 'condition-data': [{ region: 'East' }]
288
+ },
289
+ 0
290
+ )
291
+
292
+ expect(filteredData).toEqual([{ region: 'East' }])
293
+ })
294
+
295
+ it('matches columnHasAnyValue with loose string coercion', () => {
296
+ const result = evaluateDashboardCondition(
297
+ {
298
+ id: 'column-condition-1',
299
+ datasetKey: 'condition-data',
300
+ operator: 'columnHasAnyValue',
301
+ columnName: 'year',
302
+ values: ['2024', '2025']
303
+ },
304
+ [{ year: 2024 }]
305
+ )
306
+
307
+ expect(result.matches).toBe(true)
308
+ expect(result.resolved).toBe(true)
309
+ })
310
+
311
+ it('does not treat unrelated unscoped filters without matching dataset columns as unresolved', () => {
312
+ const filteredData = getDashboardConditionFilteredData(
313
+ { id: 'row-condition-1', datasetKey: 'condition-data', operator: 'hasData' },
314
+ {
315
+ sharedFilters: [
316
+ {
317
+ key: 'Unscoped URL Filter',
318
+ type: 'urlfilter',
319
+ showDropdown: true,
320
+ active: '',
321
+ usedBy: []
322
+ },
323
+ {
324
+ key: 'Different Dataset Column',
325
+ type: 'datafilter',
326
+ columnName: 'not_in_dataset',
327
+ showDropdown: true,
328
+ active: '',
329
+ usedBy: []
330
+ }
331
+ ]
332
+ } as any,
333
+ {
334
+ 'condition-data': [{ region: 'East' }]
335
+ },
336
+ 0
337
+ )
338
+
339
+ expect(filteredData).toEqual([{ region: 'East' }])
340
+ })
341
+
342
+ it('treats missing precomputed condition data as unresolved', () => {
343
+ const result = evaluateDashboardCondition(
344
+ { id: 'row-condition-1', datasetKey: 'condition-data', operator: 'hasNoData' },
345
+ undefined
346
+ )
347
+
348
+ expect(result).toEqual({ matches: false, resolved: false })
349
+ })
350
+
351
+ it('remaps row usedBy targets when rows are deleted or moved', () => {
352
+ const deletedRowTargets = remapRowTargetsInSharedFilters(
353
+ [
354
+ {
355
+ key: 'Row Filter',
356
+ type: 'datafilter',
357
+ columnName: 'region',
358
+ usedBy: [0, 1, '2', 'viz-1']
359
+ }
360
+ ] as any,
361
+ rowIndex => {
362
+ if (rowIndex === 1) return null
363
+ if (rowIndex > 1) return rowIndex - 1
364
+ return rowIndex
365
+ }
366
+ )
367
+
368
+ expect(deletedRowTargets[0].usedBy).toEqual([0, '1', 'viz-1'])
369
+
370
+ const movedRowTargets = remapRowTargetsInSharedFilters(
371
+ [
372
+ {
373
+ key: 'Row Filter',
374
+ type: 'datafilter',
375
+ columnName: 'region',
376
+ usedBy: [0, 1, 'viz-1']
377
+ }
378
+ ] as any,
379
+ rowIndex => {
380
+ if (rowIndex === 0) return 1
381
+ if (rowIndex === 1) return 0
382
+ return rowIndex
383
+ }
384
+ )
385
+
386
+ expect(movedRowTargets[0].usedBy).toEqual([1, 0, 'viz-1'])
387
+ })
388
+
389
+ it('preserves unknown string usedBy targets when remapping row targets', () => {
390
+ const remappedTargets = remapRowTargetsInSharedFilters(
391
+ [
392
+ {
393
+ key: 'Legacy Footnote Filter',
394
+ type: 'datafilter',
395
+ columnName: 'FootnoteScope',
396
+ usedBy: ['footnotes-legacy-target', 1]
397
+ }
398
+ ] as any,
399
+ rowIndex => (rowIndex === 1 ? 0 : rowIndex)
400
+ )
401
+
402
+ expect(remappedTargets[0].usedBy).toEqual(['footnotes-legacy-target', 0])
403
+ })
404
+
405
+ it('does not treat unrelated unknown targets as owner targets for data-backed conditions', () => {
406
+ const filteredData = getDashboardConditionFilteredData(
407
+ { id: 'row-condition-1', datasetKey: 'condition-data', operator: 'hasData' },
408
+ {
409
+ sharedFilters: [
410
+ {
411
+ key: 'Unknown Target Filter',
412
+ type: 'datafilter',
413
+ columnName: 'region',
414
+ showDropdown: true,
415
+ active: '',
416
+ usedBy: ['legacy-footnote-target']
417
+ }
418
+ ]
419
+ } as any,
420
+ {
421
+ 'condition-data': [{ region: 'East' }]
422
+ },
423
+ 0
424
+ )
425
+
426
+ expect(filteredData).toEqual([{ region: 'East' }])
427
+ })
428
+ })
@@ -49,6 +49,57 @@ describe('cleanSharedFilters', () => {
49
49
  ])
50
50
  })
51
51
 
52
+ it('retains filters when sharedFilterIndexes are stored as numbers', () => {
53
+ const config: DashboardConfig = {
54
+ dashboard: {
55
+ sharedFilters: [
56
+ { id: 1, type: 'filter1' },
57
+ { id: 2, type: 'filter2' }
58
+ ]
59
+ },
60
+ visualizations: {
61
+ viz1: { type: 'dashboardFilters', sharedFilterIndexes: [0, 1] }
62
+ }
63
+ }
64
+
65
+ cleanSharedFilters(config)
66
+
67
+ expect(config.dashboard.sharedFilters).toHaveLength(2)
68
+ })
69
+
70
+ it('retains filters when sharedFilterIndexes are stored as strings (malformed config)', () => {
71
+ // Documents the bug fixed when DashboardFiltersEditor started wrapping e.target.value in Number()
72
+ // so sharedFilterIndexes are stored as numbers instead of strings.
73
+ // cleanSharedFilters normalizes to numbers so malformed configs from older saves are also handled.
74
+ const config: DashboardConfig = {
75
+ dashboard: {
76
+ sharedFilters: [{ id: 1, type: 'filter1' }]
77
+ },
78
+ visualizations: {
79
+ viz1: { type: 'dashboardFilters', sharedFilterIndexes: ['0'] as any }
80
+ }
81
+ }
82
+
83
+ cleanSharedFilters(config)
84
+
85
+ expect(config.dashboard.sharedFilters).toHaveLength(1)
86
+ })
87
+
88
+ it('removes all shared filters when dashboardFilters viz has no sharedFilterIndexes', () => {
89
+ const config: DashboardConfig = {
90
+ dashboard: {
91
+ sharedFilters: [{ id: 1, type: 'filter1' }]
92
+ },
93
+ visualizations: {
94
+ viz1: { type: 'dashboardFilters' } as any
95
+ }
96
+ }
97
+
98
+ cleanSharedFilters(config)
99
+
100
+ expect(config.dashboard.sharedFilters).toEqual([])
101
+ })
102
+
52
103
  it('should remove values from urlfilter type shared filters', () => {
53
104
  const config: DashboardConfig = {
54
105
  dashboard: {