@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.
- package/CONFIG.md +77 -30
- package/LICENSE +201 -0
- package/dist/cdcdashboard.js +49936 -49166
- package/examples/dashboard-conditions-filters-incomplete.json +221 -0
- package/examples/dashboard-missing-datasets-multi.json +174 -0
- package/examples/dashboard-missing-datasets-single.json +121 -0
- package/examples/dashboard-multi-dashboard-version-regression.json +146 -0
- package/examples/dashboard-shared-filter-row-delete-cleanup.json +186 -0
- package/examples/dashboard-stale-dataset-keys.json +181 -0
- package/examples/dashboard-tiered-filter-regression.json +190 -0
- package/examples/private/cfa-dashboard.json +651 -0
- package/examples/private/data-bite-wrap.json +6936 -0
- package/examples/private/multi-dash-fix.json +16963 -0
- package/examples/private/versions.json +41612 -0
- package/examples/us-map-filter-example.json +1074 -0
- package/package.json +9 -9
- package/src/CdcDashboard.tsx +6 -2
- package/src/CdcDashboardComponent.tsx +178 -87
- package/src/DashboardCopyPasteContext.test.tsx +33 -0
- package/src/DashboardCopyPasteContext.tsx +48 -0
- package/src/_stories/Dashboard.EditorRegression.stories.tsx +72 -0
- package/src/_stories/Dashboard.Regression.stories.tsx +196 -0
- package/src/_stories/Dashboard.Zoom.stories.tsx +88 -0
- package/src/_stories/Dashboard.stories.tsx +294 -0
- package/src/_stories/FilteredTextMigrationComparison.stories.tsx +87 -0
- package/src/components/Column.test.tsx +176 -0
- package/src/components/Column.tsx +214 -13
- package/src/components/DashboardConditionModal.test.tsx +420 -0
- package/src/components/DashboardConditionModal.tsx +367 -0
- package/src/components/DashboardConditionSummary.tsx +59 -0
- package/src/components/DashboardEditors.tsx +8 -0
- package/src/components/DashboardFilters/DashboardFilters.test.tsx +139 -1
- package/src/components/DashboardFilters/DashboardFilters.tsx +192 -174
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.test.tsx +164 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +41 -2
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.test.tsx +180 -3
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +15 -32
- package/src/components/DashboardFilters/DashboardFiltersWrapper.test.tsx +142 -0
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +32 -27
- package/src/components/DashboardFilters/dashboardfilter.styles.css +42 -27
- package/src/components/DataDesignerModal.tsx +2 -1
- package/src/components/Grid.tsx +8 -4
- package/src/components/Header/Header.tsx +36 -17
- package/src/components/Row.test.tsx +228 -0
- package/src/components/Row.tsx +93 -18
- package/src/components/VisualizationRow.test.tsx +396 -0
- package/src/components/VisualizationRow.tsx +110 -35
- package/src/components/VisualizationsPanel/VisualizationsPanel.test.tsx +49 -0
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +14 -13
- package/src/components/Widget/Widget.test.tsx +218 -0
- package/src/components/Widget/Widget.tsx +119 -17
- package/src/components/Widget/widget.styles.css +31 -18
- package/src/components/dashboard-condition-modal.css +76 -0
- package/src/components/dashboard-condition-summary.css +87 -0
- package/src/helpers/addValuesToDashboardFilters.ts +3 -5
- package/src/helpers/addVisualization.ts +15 -4
- package/src/helpers/cloneDashboardWidget.ts +127 -0
- package/src/helpers/dashboardColumnWidgets.ts +99 -0
- package/src/helpers/dashboardConditionUi.ts +47 -0
- package/src/helpers/dashboardConditions.ts +200 -0
- package/src/helpers/dashboardFilterTargets.ts +156 -0
- package/src/helpers/filterData.ts +4 -9
- package/src/helpers/filterVisibility.ts +20 -0
- package/src/helpers/formatConfigBeforeSave.ts +2 -2
- package/src/helpers/getFilteredData.ts +18 -5
- package/src/helpers/getUpdateConfig.ts +43 -12
- package/src/helpers/getVizRowColumnLocator.ts +11 -1
- package/src/helpers/iconHash.tsx +9 -3
- package/src/helpers/mapDataToConfig.ts +31 -29
- package/src/helpers/reloadURLHelpers.ts +25 -5
- package/src/helpers/removeDashboardFilter.ts +33 -33
- package/src/helpers/tests/addVisualization.test.ts +53 -9
- package/src/helpers/tests/cloneDashboardWidget.test.ts +136 -0
- package/src/helpers/tests/dashboardColumnWidgets.test.ts +99 -0
- package/src/helpers/tests/dashboardConditionUi.test.ts +41 -0
- package/src/helpers/tests/dashboardConditions.test.ts +428 -0
- package/src/helpers/tests/formatConfigBeforeSave.test.ts +51 -0
- package/src/helpers/tests/getFilteredData.test.ts +265 -86
- package/src/helpers/tests/getUpdateConfig.test.ts +338 -0
- package/src/helpers/tests/reloadURLHelpers.test.ts +394 -238
- package/src/index.tsx +6 -3
- package/src/scss/grid.scss +249 -20
- package/src/scss/main.scss +108 -29
- package/src/store/dashboard.actions.ts +17 -4
- package/src/store/dashboard.reducer.test.ts +538 -0
- package/src/store/dashboard.reducer.ts +135 -22
- package/src/test/CdcDashboard.test.tsx +148 -0
- package/src/test/CdcDashboardComponent.test.tsx +935 -2
- package/src/types/ConfigRow.ts +15 -0
- package/src/types/DashboardFilters.ts +4 -0
- package/src/types/SharedFilter.ts +1 -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: {
|