@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.
- package/CONFIG.md +219 -0
- package/README.md +60 -20
- package/dist/cdcdashboard-CY9IcPSi.es.js +6 -0
- package/dist/cdcdashboard-DlpiY3fQ.es.js +4 -0
- package/dist/cdcdashboard.js +61559 -58048
- package/examples/__data__/data-2.json +6 -0
- package/examples/__data__/data.json +6 -0
- 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/legend-issue.json +1 -1
- package/examples/minimal-example.json +34 -0
- package/examples/private/cfa-dashboard.json +651 -0
- package/examples/private/data-bite-wrap.json +6936 -0
- package/examples/private/dengue.json +4640 -0
- package/examples/private/link_to_file.json +16662 -0
- package/examples/private/multi-dash-fix.json +16963 -0
- package/examples/private/versions.json +41612 -0
- package/examples/sankey.json +3 -3
- package/examples/test-api-filter-reset.json +4 -4
- package/examples/tp5-test.json +86 -4
- package/examples/us-map-filter-example.json +1074 -0
- package/package.json +9 -9
- package/src/CdcDashboard.tsx +6 -2
- package/src/CdcDashboardComponent.tsx +179 -88
- 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.smoke.stories.tsx +33 -0
- package/src/_stories/Dashboard.stories.tsx +337 -2
- package/src/_stories/FilteredTextMigrationComparison.stories.tsx +87 -0
- package/src/_stories/_mock/dashboard-data-driven-colors.json +171 -0
- package/src/_stories/_mock/tp5-test.json +86 -5
- 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 +23 -0
- package/src/components/DashboardFilters/DashboardFilters.test.tsx +267 -0
- package/src/components/DashboardFilters/DashboardFilters.tsx +193 -172
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.test.tsx +164 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +46 -6
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/APIModal.tsx +5 -3
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +59 -58
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.test.tsx +304 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +43 -36
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +2 -2
- 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/ExpandCollapseButtons.tsx +6 -4
- package/src/components/Grid.tsx +12 -7
- package/src/components/Header/Header.tsx +36 -17
- package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +141 -140
- package/src/components/Row.test.tsx +228 -0
- package/src/components/Row.tsx +104 -28
- package/src/components/VisualizationRow.test.tsx +396 -0
- package/src/components/VisualizationRow.tsx +177 -51
- 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 +123 -20
- package/src/components/Widget/widget.styles.css +58 -14
- package/src/components/dashboard-condition-modal.css +76 -0
- package/src/components/dashboard-condition-summary.css +87 -0
- package/src/data/initial-state.js +1 -0
- package/src/helpers/addValuesToDashboardFilters.ts +3 -5
- package/src/helpers/addVisualization.ts +17 -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 +281 -22
- package/src/scss/main.scss +215 -64
- package/src/store/dashboard.actions.ts +17 -4
- package/src/store/dashboard.reducer.test.ts +538 -0
- package/src/store/dashboard.reducer.ts +136 -22
- package/src/test/CdcDashboard.test.jsx +24 -0
- 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 +2 -0
- package/tests/fixtures/dashboard-config-with-metadata.json +1 -1
- package/dist/cdcdashboard-vr9HZwRt.es.js +0 -6
- package/examples/DEV-6574.json +0 -2224
- package/examples/api-dashboard-data.json +0 -272
- package/examples/api-dashboard-years.json +0 -11
- package/examples/api-geographies-data.json +0 -11
- package/examples/chart-data.json +0 -5409
- package/examples/custom/css/respiratory.css +0 -236
- package/examples/custom/js/respiratory.js +0 -242
- package/examples/default-data.json +0 -368
- package/examples/default-filter-control.json +0 -209
- package/examples/default-multi-dataset-shared-filter.json +0 -1729
- package/examples/default-multi-dataset.json +0 -506
- package/examples/ed-visits-county-file.json +0 -402
- package/examples/filters/Alabama.json +0 -72
- package/examples/filters/Alaska.json +0 -1737
- package/examples/filters/Arkansas.json +0 -4713
- package/examples/filters/California.json +0 -212
- package/examples/filters/Colorado.json +0 -1500
- package/examples/filters/Connecticut.json +0 -559
- package/examples/filters/Delaware.json +0 -63
- package/examples/filters/DistrictofColumbia.json +0 -63
- package/examples/filters/Florida.json +0 -4217
- package/examples/filters/States.json +0 -146
- package/examples/state-level.json +0 -90136
- package/examples/state-points.json +0 -10474
- package/examples/temp-example-data.json +0 -130
- package/examples/test-dashboard-simple.json +0 -503
- package/examples/test-example.json +0 -752
- package/examples/test-file.json +0 -147
- package/examples/test.json +0 -752
- package/examples/testing.json +0 -94456
- /package/examples/{data → __data__}/data-with-metadata.json +0 -0
- /package/examples/{legend-issue-data.json → __data__/legend-issue-data.json} +0 -0
- /package/examples/api-test/{categories.json → __data__/categories.json} +0 -0
- /package/examples/api-test/{chart-data.json → __data__/chart-data.json} +0 -0
- /package/examples/api-test/{topics.json → __data__/topics.json} +0 -0
- /package/examples/api-test/{years.json → __data__/years.json} +0 -0
- /package/src/_stories/{Dashboard.Pages.stories.tsx → Dashboard.Pages.smoke.stories.tsx} +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
.cove-visualization button.cove-button.dashboard-condition-summary {
|
|
2
|
+
align-items: center !important;
|
|
3
|
+
align-self: stretch;
|
|
4
|
+
background: var(--dashboard-condition-active-blue-bg, #f2f7fb) !important;
|
|
5
|
+
border: 1px solid var(--dashboard-condition-active-blue, var(--blue)) !important;
|
|
6
|
+
border-radius: 4px;
|
|
7
|
+
box-shadow: none;
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
color: var(--dashboard-condition-active-blue, var(--blue)) !important;
|
|
10
|
+
display: inline-flex !important;
|
|
11
|
+
flex-direction: row !important;
|
|
12
|
+
flex-wrap: nowrap !important;
|
|
13
|
+
font-family: var(--app-font-main, sans-serif) !important;
|
|
14
|
+
font-size: 0.8rem !important;
|
|
15
|
+
font-weight: 400 !important;
|
|
16
|
+
gap: 0.35rem;
|
|
17
|
+
justify-content: flex-start;
|
|
18
|
+
line-height: 1.2;
|
|
19
|
+
margin: 0 0 0.5rem;
|
|
20
|
+
min-height: 24px !important;
|
|
21
|
+
overflow: visible;
|
|
22
|
+
padding: 0.2rem 0.45rem !important;
|
|
23
|
+
text-align: left;
|
|
24
|
+
transform: none;
|
|
25
|
+
white-space: normal;
|
|
26
|
+
width: 100%;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.cove-visualization button.cove-button.dashboard-condition-summary:hover,
|
|
30
|
+
.cove-visualization button.cove-button.dashboard-condition-summary:active,
|
|
31
|
+
.cove-visualization button.cove-button.dashboard-condition-summary:focus {
|
|
32
|
+
background: var(--dashboard-condition-active-blue-bg-hover, #e6eff7) !important;
|
|
33
|
+
box-shadow: none;
|
|
34
|
+
color: var(--dashboard-condition-active-blue, var(--blue)) !important;
|
|
35
|
+
transform: none;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.cove-visualization button.cove-button.dashboard-condition-summary .dashboard-condition-summary__icon {
|
|
39
|
+
align-items: center;
|
|
40
|
+
display: inline-flex;
|
|
41
|
+
flex: 0 0 auto;
|
|
42
|
+
line-height: 1.2;
|
|
43
|
+
margin-top: 0.05rem;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.cove-visualization button.cove-button.dashboard-condition-summary .dashboard-condition-summary__icon svg {
|
|
47
|
+
display: block;
|
|
48
|
+
fill: currentColor;
|
|
49
|
+
height: 1em;
|
|
50
|
+
margin: 0 !important;
|
|
51
|
+
position: static;
|
|
52
|
+
top: auto;
|
|
53
|
+
transform: none;
|
|
54
|
+
width: 1em;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.cove-visualization button.cove-button.dashboard-condition-summary .dashboard-condition-summary__text {
|
|
58
|
+
display: block;
|
|
59
|
+
flex: 1 1 auto;
|
|
60
|
+
font-family: var(--app-font-main, sans-serif) !important;
|
|
61
|
+
font-size: 0.8rem !important;
|
|
62
|
+
font-weight: 400 !important;
|
|
63
|
+
line-height: 1.2;
|
|
64
|
+
min-width: 0;
|
|
65
|
+
overflow-wrap: break-word;
|
|
66
|
+
white-space: normal;
|
|
67
|
+
word-break: normal;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.cove-visualization button.cove-button.dashboard-condition-summary .dashboard-condition-summary__text strong {
|
|
71
|
+
font-family: inherit;
|
|
72
|
+
font-size: inherit;
|
|
73
|
+
font-weight: 700;
|
|
74
|
+
line-height: inherit;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.cove-visualization button.cove-button.dashboard-condition-summary.dashboard-condition-summary--row {
|
|
78
|
+
margin-top: 0.5rem;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.cove-visualization button.cove-button.dashboard-condition-summary.dashboard-condition-summary--widget {
|
|
82
|
+
align-items: flex-start !important;
|
|
83
|
+
flex: 0 0 auto;
|
|
84
|
+
margin: 0.5rem 0 0.4rem;
|
|
85
|
+
max-width: 100%;
|
|
86
|
+
width: 100%;
|
|
87
|
+
}
|
|
@@ -58,11 +58,9 @@ export const addValuesToDashboardFilters = (
|
|
|
58
58
|
const active: string[] = Array.isArray(filterCopy.active) ? filterCopy.active : [filterCopy.active]
|
|
59
59
|
filterCopy.active = active.filter(val => defaultValues.includes(val))
|
|
60
60
|
} else {
|
|
61
|
-
//
|
|
62
|
-
if (filterCopy.
|
|
63
|
-
filterCopy.active = filterCopy.defaultValue
|
|
64
|
-
} else if (!filterCopy.active) {
|
|
65
|
-
filterCopy.active = filterCopy.resetLabel || filterCopy.values[0]
|
|
61
|
+
// Only set active when it is not already set; prefer defaultValue, then resetLabel, then first value
|
|
62
|
+
if (!filterCopy.active) {
|
|
63
|
+
filterCopy.active = filterCopy.defaultValue || filterCopy.resetLabel || filterCopy.values[0]
|
|
66
64
|
}
|
|
67
65
|
}
|
|
68
66
|
}
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import type { AnyVisualization } from '@cdc/core/types/Visualization'
|
|
2
2
|
import type { Table } from '@cdc/core/types/Table'
|
|
3
|
+
import { createCoveId } from '@cdc/core/helpers/createCoveId'
|
|
4
|
+
import type { CreateCoveIdOptions } from '@cdc/core/helpers/createCoveId'
|
|
5
|
+
|
|
6
|
+
export const addVisualization = (type, subType, idOptions?: CreateCoveIdOptions) => {
|
|
7
|
+
if (type === 'filtered-text') {
|
|
8
|
+
throw new Error(
|
|
9
|
+
'Cannot create new filtered-text visualizations. filtered-text is deprecated; use markup-include instead.'
|
|
10
|
+
)
|
|
11
|
+
}
|
|
3
12
|
|
|
4
|
-
export const addVisualization = (type, subType) => {
|
|
5
13
|
const modalWillOpen = type !== 'markup-include'
|
|
6
14
|
const newVisualizationConfig: Partial<AnyVisualization> = {
|
|
7
15
|
filters: [],
|
|
8
16
|
filterBehavior: 'Filter Change',
|
|
9
17
|
newViz: type !== 'table',
|
|
10
18
|
openModal: modalWillOpen,
|
|
11
|
-
uid: type
|
|
19
|
+
uid: createCoveId(type, idOptions),
|
|
12
20
|
type
|
|
13
21
|
}
|
|
14
22
|
|
|
@@ -35,10 +43,12 @@ export const addVisualization = (type, subType) => {
|
|
|
35
43
|
}
|
|
36
44
|
break
|
|
37
45
|
case 'data-bite':
|
|
38
|
-
|
|
39
|
-
case 'filtered-text':
|
|
46
|
+
newVisualizationConfig.biteStyle = 'tp5'
|
|
40
47
|
newVisualizationConfig.visualizationType = type
|
|
41
48
|
break
|
|
49
|
+
case 'waffle-chart':
|
|
50
|
+
newVisualizationConfig.visualizationType = subType === 'Waffle' ? 'TP5 Waffle' : subType
|
|
51
|
+
break
|
|
42
52
|
case 'table': {
|
|
43
53
|
const tableConfig: Table = {
|
|
44
54
|
label: 'Data Table',
|
|
@@ -59,6 +69,9 @@ export const addVisualization = (type, subType) => {
|
|
|
59
69
|
break
|
|
60
70
|
case 'dashboardFilters': {
|
|
61
71
|
newVisualizationConfig.sharedFilterIndexes = []
|
|
72
|
+
newVisualizationConfig.visual = {
|
|
73
|
+
grayBackground: false
|
|
74
|
+
}
|
|
62
75
|
newVisualizationConfig.visualizationType = type
|
|
63
76
|
break
|
|
64
77
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import type { AnyVisualization } from '@cdc/core/types/Visualization'
|
|
3
|
+
import { createCoveId } from '@cdc/core/helpers/createCoveId'
|
|
4
|
+
import type { DashboardConfig } from '../types/DashboardConfig'
|
|
5
|
+
import { ConfigRow, DashboardCondition } from '../types/ConfigRow'
|
|
6
|
+
import { getDashboardConditionIds } from './dashboardConditions'
|
|
7
|
+
import { getConditionalWidgets, hasConditionalWidgets, normalizeConditionalColumn } from './dashboardColumnWidgets'
|
|
8
|
+
|
|
9
|
+
export type CloneDashboardWidgetTarget = {
|
|
10
|
+
rowIdx: number
|
|
11
|
+
colIdx: number
|
|
12
|
+
entryIdx?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const normalizeTarget = (target: string | number) => `${target}`
|
|
16
|
+
|
|
17
|
+
const appendTarget = (targets: (string | number)[], target: string | number) => {
|
|
18
|
+
if (targets.some(existingTarget => normalizeTarget(existingTarget) === normalizeTarget(target))) return targets
|
|
19
|
+
return [...targets, target]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const createClonedWidgetKey = (sourceWidgetKey: string, visualizations: Record<string, AnyVisualization>) => {
|
|
23
|
+
const sourceVisualization = visualizations[sourceWidgetKey]
|
|
24
|
+
return createCoveId(sourceVisualization.type, { existingIds: Object.keys(visualizations) })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const getSourceDashboardCondition = (rows: ConfigRow[], sourceWidgetKey: string): DashboardCondition | undefined => {
|
|
28
|
+
for (const row of rows) {
|
|
29
|
+
for (const column of row.columns || []) {
|
|
30
|
+
if (!hasConditionalWidgets(column)) continue
|
|
31
|
+
|
|
32
|
+
const sourceEntry = getConditionalWidgets(column).find(entry => entry.widget === sourceWidgetKey)
|
|
33
|
+
if (sourceEntry?.dashboardCondition) return sourceEntry.dashboardCondition
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return undefined
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const getWidgetFilterTarget = (rows: ConfigRow[], widgetKey: string): string | number => {
|
|
41
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
|
42
|
+
const row = rows[rowIndex]
|
|
43
|
+
const widgetInRow = row.columns?.some(
|
|
44
|
+
column => column.widget === widgetKey || getConditionalWidgets(column).some(entry => entry.widget === widgetKey)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if (widgetInRow) {
|
|
48
|
+
return row.dataKey ? rowIndex : widgetKey
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return widgetKey
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const cloneDashboardWidget = (
|
|
56
|
+
config: DashboardConfig,
|
|
57
|
+
sourceWidgetKey: string,
|
|
58
|
+
target: CloneDashboardWidgetTarget
|
|
59
|
+
): DashboardConfig => {
|
|
60
|
+
const sourceVisualization = config.visualizations?.[sourceWidgetKey]
|
|
61
|
+
const targetColumn = config.rows?.[target.rowIdx]?.columns?.[target.colIdx]
|
|
62
|
+
|
|
63
|
+
if (!sourceVisualization || !targetColumn) return config
|
|
64
|
+
|
|
65
|
+
if (target.entryIdx !== undefined) {
|
|
66
|
+
const existingEntry = getConditionalWidgets(targetColumn)[target.entryIdx]
|
|
67
|
+
if (existingEntry?.widget) return config
|
|
68
|
+
} else if (targetColumn.widget || hasConditionalWidgets(targetColumn)) {
|
|
69
|
+
return config
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const clonedWidgetKey = createClonedWidgetKey(sourceWidgetKey, config.visualizations)
|
|
73
|
+
const clonedVisualization = { ..._.cloneDeep(sourceVisualization), uid: clonedWidgetKey }
|
|
74
|
+
const sourceDashboardCondition = getSourceDashboardCondition(config.rows, sourceWidgetKey)
|
|
75
|
+
const clonedDashboardCondition = sourceDashboardCondition
|
|
76
|
+
? {
|
|
77
|
+
..._.cloneDeep(sourceDashboardCondition),
|
|
78
|
+
id: createCoveId('condition', { existingIds: getDashboardConditionIds(config.rows) })
|
|
79
|
+
}
|
|
80
|
+
: undefined
|
|
81
|
+
|
|
82
|
+
const nextRows = _.cloneDeep(config.rows)
|
|
83
|
+
const nextTargetColumn = nextRows[target.rowIdx].columns[target.colIdx]
|
|
84
|
+
|
|
85
|
+
if (target.entryIdx !== undefined || hasConditionalWidgets(nextTargetColumn) || clonedDashboardCondition) {
|
|
86
|
+
const nextConditionalWidgets = [...(nextTargetColumn.conditionalWidgets || [])]
|
|
87
|
+
const targetEntryIndex = target.entryIdx ?? nextConditionalWidgets.length
|
|
88
|
+
nextConditionalWidgets[targetEntryIndex] = {
|
|
89
|
+
widget: clonedWidgetKey,
|
|
90
|
+
...(clonedDashboardCondition ? { dashboardCondition: clonedDashboardCondition } : {})
|
|
91
|
+
}
|
|
92
|
+
nextRows[target.rowIdx].columns[target.colIdx] = normalizeConditionalColumn({
|
|
93
|
+
...nextTargetColumn,
|
|
94
|
+
widget: undefined,
|
|
95
|
+
conditionalWidgets: nextConditionalWidgets.filter(entry => !!entry?.widget)
|
|
96
|
+
})
|
|
97
|
+
} else {
|
|
98
|
+
nextRows[target.rowIdx].columns[target.colIdx].widget = clonedWidgetKey
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const sourceFilterTarget = getWidgetFilterTarget(config.rows, sourceWidgetKey)
|
|
102
|
+
const clonedFilterTarget = nextRows[target.rowIdx]?.dataKey ? target.rowIdx : clonedWidgetKey
|
|
103
|
+
const sharedFilters = config.dashboard.sharedFilters?.map(sharedFilter => {
|
|
104
|
+
if (!sharedFilter.usedBy?.length) return sharedFilter
|
|
105
|
+
|
|
106
|
+
let nextUsedBy = sharedFilter.usedBy
|
|
107
|
+
|
|
108
|
+
if (sharedFilter.usedBy.some(target => normalizeTarget(target) === normalizeTarget(sourceFilterTarget))) {
|
|
109
|
+
nextUsedBy = appendTarget(nextUsedBy, clonedFilterTarget)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return nextUsedBy === sharedFilter.usedBy ? sharedFilter : { ...sharedFilter, usedBy: nextUsedBy }
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
...config,
|
|
117
|
+
dashboard: {
|
|
118
|
+
...config.dashboard,
|
|
119
|
+
...(sharedFilters ? { sharedFilters } : {})
|
|
120
|
+
},
|
|
121
|
+
rows: nextRows,
|
|
122
|
+
visualizations: {
|
|
123
|
+
...config.visualizations,
|
|
124
|
+
[clonedWidgetKey]: clonedVisualization
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ConditionalWidget, DashboardCondition } from '../types/ConfigRow'
|
|
2
|
+
|
|
3
|
+
type ColumnWithWidgets = {
|
|
4
|
+
widget?: string | null
|
|
5
|
+
conditionalWidgets?: ConditionalWidget[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const hasConditionalWidgets = (column?: ColumnWithWidgets): boolean => !!column?.conditionalWidgets?.length
|
|
9
|
+
|
|
10
|
+
export const hasAuthoredWidgetEntries = (column?: ColumnWithWidgets): boolean =>
|
|
11
|
+
getColumnWidgetEntries(column).length > 0
|
|
12
|
+
|
|
13
|
+
export const getConditionalWidgets = (column?: ColumnWithWidgets): ConditionalWidget[] =>
|
|
14
|
+
column?.conditionalWidgets?.filter(entry => !!entry?.widget) || []
|
|
15
|
+
|
|
16
|
+
export const getColumnWidgetEntries = (column?: ColumnWithWidgets): ConditionalWidget[] => {
|
|
17
|
+
if (!column) return []
|
|
18
|
+
if (hasConditionalWidgets(column)) {
|
|
19
|
+
return getConditionalWidgets(column)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return column.widget ? [{ widget: column.widget }] : []
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const getColumnWidgetKeys = (column?: ColumnWithWidgets): string[] =>
|
|
26
|
+
getColumnWidgetEntries(column)
|
|
27
|
+
.map(entry => entry.widget)
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
|
|
30
|
+
export const getColumnPrimaryWidget = (column?: ColumnWithWidgets): string | undefined =>
|
|
31
|
+
getColumnWidgetEntries(column)[0]?.widget
|
|
32
|
+
|
|
33
|
+
export type DashboardConditionMatch = {
|
|
34
|
+
matches: boolean
|
|
35
|
+
resolved: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ResolvedColumnWidgetEntry = {
|
|
39
|
+
widget?: string
|
|
40
|
+
dashboardCondition?: DashboardCondition
|
|
41
|
+
} & DashboardConditionMatch
|
|
42
|
+
|
|
43
|
+
type ResolveColumnWidgetOptions = {
|
|
44
|
+
evaluateCondition?: (dashboardCondition?: DashboardCondition) => DashboardConditionMatch
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const resolveColumnWidgetEntry = (
|
|
48
|
+
column?: ColumnWithWidgets,
|
|
49
|
+
{ evaluateCondition = () => ({ matches: true, resolved: true }) }: ResolveColumnWidgetOptions = {}
|
|
50
|
+
): ResolvedColumnWidgetEntry | undefined => {
|
|
51
|
+
const widgetEntries = getColumnWidgetEntries(column)
|
|
52
|
+
|
|
53
|
+
for (const entry of widgetEntries) {
|
|
54
|
+
if (!entry.dashboardCondition) {
|
|
55
|
+
return { ...entry, matches: true, resolved: true }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const evaluation = evaluateCondition(entry.dashboardCondition)
|
|
59
|
+
if (evaluation.matches) {
|
|
60
|
+
return { ...entry, ...evaluation }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return widgetEntries.length ? { widget: undefined, matches: false, resolved: true } : undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const normalizeConditionalColumn = <T extends ColumnWithWidgets>(column: T): T => {
|
|
68
|
+
const conditionalWidgets = getConditionalWidgets(column)
|
|
69
|
+
|
|
70
|
+
if (conditionalWidgets.length > 1) {
|
|
71
|
+
return {
|
|
72
|
+
...column,
|
|
73
|
+
widget: undefined,
|
|
74
|
+
conditionalWidgets
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (conditionalWidgets.length === 1) {
|
|
79
|
+
const [entry] = conditionalWidgets
|
|
80
|
+
if (entry.dashboardCondition) {
|
|
81
|
+
return {
|
|
82
|
+
...column,
|
|
83
|
+
widget: undefined,
|
|
84
|
+
conditionalWidgets: [entry]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
...column,
|
|
90
|
+
widget: entry.widget,
|
|
91
|
+
conditionalWidgets: undefined
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
...column,
|
|
97
|
+
conditionalWidgets: undefined
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DashboardCondition } from '../types/ConfigRow'
|
|
2
|
+
|
|
3
|
+
export type DashboardConditionTypeOption = DashboardCondition['operator'] | ''
|
|
4
|
+
type DashboardConditionOperator = NonNullable<DashboardCondition['operator']>
|
|
5
|
+
|
|
6
|
+
export const DASHBOARD_CONDITION_TYPE_LABELS: Record<DashboardConditionOperator, string> = {
|
|
7
|
+
hasData: "Show when there's data",
|
|
8
|
+
hasNoData: "Show when there's no data",
|
|
9
|
+
columnHasAnyValue: 'Show when column has a value',
|
|
10
|
+
filtersIncomplete: 'Show when filters are incomplete'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DASHBOARD_CONDITION_TYPE_OPTIONS: { value: DashboardConditionTypeOption; label: string }[] = [
|
|
14
|
+
{ value: '', label: 'Always show' },
|
|
15
|
+
{ value: 'hasData', label: DASHBOARD_CONDITION_TYPE_LABELS.hasData },
|
|
16
|
+
{ value: 'hasNoData', label: DASHBOARD_CONDITION_TYPE_LABELS.hasNoData },
|
|
17
|
+
{ value: 'columnHasAnyValue', label: DASHBOARD_CONDITION_TYPE_LABELS.columnHasAnyValue },
|
|
18
|
+
{ value: 'filtersIncomplete', label: DASHBOARD_CONDITION_TYPE_LABELS.filtersIncomplete }
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
export const getColumnHasAnyValueSummaryParts = (columnName?: string) => {
|
|
22
|
+
if (!columnName) return undefined
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
prefix: 'Show based on the value in the ',
|
|
26
|
+
columnName,
|
|
27
|
+
suffix: ' column'
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const getColumnHasAnyValueSummary = (columnName?: string) => {
|
|
32
|
+
const summaryParts = getColumnHasAnyValueSummaryParts(columnName)
|
|
33
|
+
|
|
34
|
+
if (!summaryParts) return DASHBOARD_CONDITION_TYPE_LABELS.columnHasAnyValue
|
|
35
|
+
|
|
36
|
+
return `${summaryParts.prefix}${summaryParts.columnName}${summaryParts.suffix}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const getDashboardConditionSummary = (dashboardCondition?: DashboardCondition) => {
|
|
40
|
+
if (!dashboardCondition?.operator) return 'Dashboard condition configured'
|
|
41
|
+
|
|
42
|
+
if (dashboardCondition.operator === 'columnHasAnyValue') {
|
|
43
|
+
return getColumnHasAnyValueSummary(dashboardCondition.columnName)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return DASHBOARD_CONDITION_TYPE_LABELS[dashboardCondition.operator] || 'Dashboard condition configured'
|
|
47
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { Dashboard } from '../types/Dashboard'
|
|
2
|
+
import { ConfigRow, DashboardCondition } from '../types/ConfigRow'
|
|
3
|
+
import { createCoveId } from '@cdc/core/helpers/createCoveId'
|
|
4
|
+
import { getConditionalWidgets, hasConditionalWidgets } from './dashboardColumnWidgets'
|
|
5
|
+
import { filterData, isFilterAtResetState } from './filterData'
|
|
6
|
+
import {
|
|
7
|
+
dashboardConditionsSupportedForRow,
|
|
8
|
+
getApplicableFiltersForTarget,
|
|
9
|
+
SharedFilterTarget
|
|
10
|
+
} from './dashboardFilterTargets'
|
|
11
|
+
|
|
12
|
+
export type DashboardConditionEvaluation = {
|
|
13
|
+
matches: boolean
|
|
14
|
+
resolved: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const getDashboardConditionIds = (rows: ConfigRow[] = []) =>
|
|
18
|
+
rows.flatMap(row => {
|
|
19
|
+
if (Array.isArray(row)) return []
|
|
20
|
+
|
|
21
|
+
const rowConditionIds = row.dashboardCondition?.id ? [row.dashboardCondition.id] : []
|
|
22
|
+
const columnConditionIds =
|
|
23
|
+
row.columns?.flatMap(column =>
|
|
24
|
+
getConditionalWidgets(column).flatMap(entry =>
|
|
25
|
+
entry.dashboardCondition?.id ? [entry.dashboardCondition.id] : []
|
|
26
|
+
)
|
|
27
|
+
) || []
|
|
28
|
+
|
|
29
|
+
return [...rowConditionIds, ...columnConditionIds]
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// This runs during initial config preparation before coveUpdateWorker normalizes legacy dashboard rows.
|
|
33
|
+
// Preserve pre-4.24.3 array-shaped rows for packages/core/helpers/ver/4.24.3.ts, and tolerate rows
|
|
34
|
+
// without normalized columns. This constraint can be removed if CdcDashboard.tsx runs coveUpdateWorker
|
|
35
|
+
// before getUpdateConfig in formatInitialState.
|
|
36
|
+
export const ensureRowConditionIds = (rows: ConfigRow[]): ConfigRow[] => {
|
|
37
|
+
const existingConditionIds = new Set(getDashboardConditionIds(rows).map(id => String(id)))
|
|
38
|
+
|
|
39
|
+
return rows.map(row => {
|
|
40
|
+
if (Array.isArray(row)) return row
|
|
41
|
+
|
|
42
|
+
const nextRow = { ...row }
|
|
43
|
+
|
|
44
|
+
if (nextRow.dashboardCondition && !nextRow.dashboardCondition.id) {
|
|
45
|
+
const id = createCoveId('condition', { existingIds: existingConditionIds })
|
|
46
|
+
existingConditionIds.add(id)
|
|
47
|
+
nextRow.dashboardCondition = { ...nextRow.dashboardCondition, id }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (Array.isArray(nextRow.columns)) {
|
|
51
|
+
nextRow.columns = nextRow.columns.map(column => {
|
|
52
|
+
if (hasConditionalWidgets(column)) {
|
|
53
|
+
return {
|
|
54
|
+
...column,
|
|
55
|
+
conditionalWidgets: getConditionalWidgets(column).map(entry => {
|
|
56
|
+
if (!entry.dashboardCondition || entry.dashboardCondition.id) return entry
|
|
57
|
+
|
|
58
|
+
const id = createCoveId('condition', { existingIds: existingConditionIds })
|
|
59
|
+
existingConditionIds.add(id)
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
...entry,
|
|
63
|
+
dashboardCondition: {
|
|
64
|
+
...entry.dashboardCondition,
|
|
65
|
+
id
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return column
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return nextRow
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const dashboardConditionHasRequiredInputs = (dashboardCondition?: DashboardCondition) => {
|
|
80
|
+
if (!dashboardCondition?.operator) return false
|
|
81
|
+
if (dashboardCondition.operator === 'filtersIncomplete') return true
|
|
82
|
+
if (!dashboardCondition.datasetKey) return false
|
|
83
|
+
if (dashboardCondition.operator !== 'columnHasAnyValue') return true
|
|
84
|
+
|
|
85
|
+
return !!dashboardCondition.columnName && !!dashboardCondition.values?.length
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const dashboardConditionUsesFiltersIncomplete = (dashboardCondition?: DashboardCondition) =>
|
|
89
|
+
dashboardCondition?.operator === 'filtersIncomplete'
|
|
90
|
+
|
|
91
|
+
export const dashboardRowsUseFiltersIncomplete = (rows: ConfigRow[] = []) =>
|
|
92
|
+
rows.some(row => {
|
|
93
|
+
if (!dashboardConditionsSupportedForRow(row)) return false
|
|
94
|
+
if (dashboardConditionUsesFiltersIncomplete(row.dashboardCondition)) return true
|
|
95
|
+
|
|
96
|
+
return row.columns?.some(column =>
|
|
97
|
+
getConditionalWidgets(column).some(entry => dashboardConditionUsesFiltersIncomplete(entry.dashboardCondition))
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
export const getDashboardConditionDatasetKeys = (rows: ConfigRow[] = []) =>
|
|
102
|
+
rows.flatMap(row => {
|
|
103
|
+
if (!dashboardConditionsSupportedForRow(row)) return []
|
|
104
|
+
|
|
105
|
+
const rowDatasetKey = row.dashboardCondition?.datasetKey ? [row.dashboardCondition.datasetKey] : []
|
|
106
|
+
const columnDatasetKeys =
|
|
107
|
+
row.columns?.flatMap(column =>
|
|
108
|
+
getConditionalWidgets(column).flatMap(entry =>
|
|
109
|
+
entry.dashboardCondition?.datasetKey ? [entry.dashboardCondition.datasetKey] : []
|
|
110
|
+
)
|
|
111
|
+
) || []
|
|
112
|
+
|
|
113
|
+
return [...rowDatasetKey, ...columnDatasetKeys]
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const getDashboardConditionApplicableFilters = (
|
|
117
|
+
dashboard: Dashboard,
|
|
118
|
+
filterTarget: SharedFilterTarget,
|
|
119
|
+
datasetColumns: string[]
|
|
120
|
+
) => {
|
|
121
|
+
const candidateFilters = getApplicableFiltersForTarget(dashboard, filterTarget, { includeUnscoped: true })
|
|
122
|
+
if (!candidateFilters) return []
|
|
123
|
+
|
|
124
|
+
return candidateFilters.filter(filter => !!filter.columnName && datasetColumns.includes(filter.columnName))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const hasIncompleteFiltersForDashboardCondition = (
|
|
128
|
+
dashboardCondition: DashboardCondition | undefined,
|
|
129
|
+
dashboard: Dashboard,
|
|
130
|
+
filterTarget: SharedFilterTarget = ''
|
|
131
|
+
) => {
|
|
132
|
+
if (!dashboardCondition?.id) return false
|
|
133
|
+
|
|
134
|
+
const applicableFilters = getApplicableFiltersForTarget(dashboard, filterTarget, { includeUnscoped: true })
|
|
135
|
+
if (!applicableFilters) return false
|
|
136
|
+
|
|
137
|
+
return applicableFilters.some(isFilterAtResetState)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const getDashboardConditionFilteredData = (
|
|
141
|
+
dashboardCondition: DashboardCondition | undefined,
|
|
142
|
+
dashboard: Dashboard,
|
|
143
|
+
data: Record<string, any[]>,
|
|
144
|
+
filterTarget: SharedFilterTarget = ''
|
|
145
|
+
): Record<string, any>[] | undefined => {
|
|
146
|
+
if (!dashboardConditionHasRequiredInputs(dashboardCondition)) {
|
|
147
|
+
return undefined
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (dashboardCondition.operator === 'filtersIncomplete') {
|
|
151
|
+
return hasIncompleteFiltersForDashboardCondition(dashboardCondition, dashboard, filterTarget) ? [{}] : []
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const rawDataset = data[dashboardCondition.datasetKey]
|
|
155
|
+
if (!Array.isArray(rawDataset)) {
|
|
156
|
+
return undefined
|
|
157
|
+
}
|
|
158
|
+
const dataset = rawDataset || []
|
|
159
|
+
|
|
160
|
+
const datasetColumns = dataset[0] ? Object.keys(dataset[0]) : []
|
|
161
|
+
const applicableFilters = getDashboardConditionApplicableFilters(dashboard, filterTarget, datasetColumns)
|
|
162
|
+
|
|
163
|
+
if (applicableFilters.some(isFilterAtResetState)) {
|
|
164
|
+
return undefined
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return applicableFilters.length ? filterData(applicableFilters, dataset) : dataset
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export const evaluateDashboardCondition = (
|
|
171
|
+
dashboardCondition: DashboardCondition | undefined,
|
|
172
|
+
filteredData?: Record<string, any>[]
|
|
173
|
+
): DashboardConditionEvaluation => {
|
|
174
|
+
if (!dashboardConditionHasRequiredInputs(dashboardCondition) || filteredData === undefined) {
|
|
175
|
+
return { matches: false, resolved: false }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (dashboardCondition.operator === 'hasData') {
|
|
179
|
+
return { matches: filteredData.length > 0, resolved: true }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (dashboardCondition.operator === 'hasNoData') {
|
|
183
|
+
return { matches: filteredData.length === 0, resolved: true }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (dashboardCondition.operator === 'filtersIncomplete') {
|
|
187
|
+
return { matches: filteredData.length > 0, resolved: true }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const matches = filteredData.some(row => {
|
|
191
|
+
const currentValue = row?.[dashboardCondition.columnName]
|
|
192
|
+
return (
|
|
193
|
+
currentValue !== undefined &&
|
|
194
|
+
currentValue !== null &&
|
|
195
|
+
dashboardCondition.values?.some(value => String(currentValue) === String(value))
|
|
196
|
+
)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
return { matches, resolved: true }
|
|
200
|
+
}
|