@cdc/dashboard 4.25.11 → 4.26.1
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/Dynamic_Data.md +66 -0
- package/dist/cdcdashboard.js +78783 -76370
- package/examples/api-dashboard-data.json +272 -0
- package/examples/api-dashboard-years.json +11 -0
- package/examples/api-geographies-data.json +11 -0
- package/examples/private/cat-y.json +1235 -0
- package/examples/private/chronic-dash.json +1584 -0
- package/examples/private/map-issue.json +2260 -0
- package/examples/private/mpinc-state-reports.json +2260 -0
- package/examples/private/nwss/rsv.json +1240 -0
- package/examples/private/simple-dash.json +490 -0
- package/examples/private/test-dash.json +0 -0
- package/examples/private/test123.json +491 -0
- package/examples/test-dashboard-simple.json +503 -0
- package/index.html +24 -25
- package/package.json +12 -11
- package/src/CdcDashboardComponent.tsx +18 -2
- package/src/_stories/Dashboard.DataSetup.stories.tsx +203 -0
- package/src/_stories/Dashboard.stories.tsx +385 -1
- package/src/_stories/_mock/filter-cascade.json +3350 -0
- package/src/_stories/_mock/gallery-data-bite-dashboard.json +3500 -0
- package/src/_stories/_mock/nested-parent-child-filters.json +392 -0
- package/src/_stories/_mock/parent-child-filters.json +233 -0
- package/src/components/DashboardFilters/DashboardFilters.tsx +20 -11
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +89 -38
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +51 -29
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +146 -9
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +11 -7
- package/src/components/DataDesignerModal.tsx +6 -1
- package/src/components/Header/Header.tsx +51 -20
- package/src/components/VisualizationRow.tsx +71 -5
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +2 -3
- package/src/components/Widget/Widget.tsx +1 -1
- package/src/data/initial-state.js +1 -0
- package/src/helpers/addValuesToDashboardFilters.ts +15 -22
- package/src/helpers/changeFilterActive.ts +67 -65
- package/src/helpers/formatConfigBeforeSave.ts +6 -5
- package/src/helpers/getUpdateConfig.ts +91 -91
- package/src/helpers/tests/updatesChildFilters.test.ts +53 -22
- package/src/helpers/updateChildFilters.ts +50 -27
- package/src/scss/main.scss +141 -1
- package/src/test/CdcDashboard.test.jsx +9 -4
- package/src/types/Dashboard.ts +1 -0
- package/src/types/FilterStyles.ts +8 -7
- package/src/types/SharedFilter.ts +13 -0
- package/LICENSE +0 -201
- package/examples/private/DEV-10538.json +0 -407
- package/examples/private/DEV-11072.json +0 -7591
- package/examples/private/DEV-11405.json +0 -39112
- package/examples/private/delete.json +0 -32919
- package/examples/private/pedro.json +0 -1
|
@@ -21,6 +21,7 @@ import { addValuesToDashboardFilters } from '../../../helpers/addValuesToDashboa
|
|
|
21
21
|
import { FILTER_STYLE } from '../../../types/FilterStyles'
|
|
22
22
|
import { handleSorting } from '@cdc/core/components/Filters'
|
|
23
23
|
import { removeDashboardFilter } from '../../../helpers/removeDashboardFilter'
|
|
24
|
+
import { DragDropContext, Droppable, Draggable, DropResult } from '@hello-pangea/dnd'
|
|
24
25
|
|
|
25
26
|
type DashboardFitlersEditorProps = {
|
|
26
27
|
vizConfig: DashboardFilters
|
|
@@ -71,6 +72,7 @@ const DashboardFiltersEditor: React.FC<DashboardFitlersEditorProps> = ({ vizConf
|
|
|
71
72
|
newSharedFilters[index][prop] = value
|
|
72
73
|
if (prop === 'columnName') {
|
|
73
74
|
if (newSharedFilters[index].subGrouping) delete newSharedFilters[index].subGrouping
|
|
75
|
+
newSharedFilters[index].defaultValue = ''
|
|
74
76
|
// changing a data column and want to load the data into the preview options
|
|
75
77
|
const sharedFiltersWithValues = addValuesToDashboardFilters(newSharedFilters, data)
|
|
76
78
|
dispatch({ type: 'SET_SHARED_FILTERS', payload: sharedFiltersWithValues })
|
|
@@ -122,6 +124,29 @@ const DashboardFiltersEditor: React.FC<DashboardFitlersEditorProps> = ({ vizConf
|
|
|
122
124
|
dispatch({ type: 'SET_CONFIG', payload: { dashboard, visualizations: newVisualizations } })
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
const handleFilterReorder = (result: DropResult) => {
|
|
128
|
+
const { source, destination } = result
|
|
129
|
+
if (!destination || source.index === destination.index) return
|
|
130
|
+
|
|
131
|
+
const newIndexes = [...vizConfig.sharedFilterIndexes]
|
|
132
|
+
const [movedIndex] = newIndexes.splice(source.index, 1)
|
|
133
|
+
newIndexes.splice(destination.index, 0, movedIndex)
|
|
134
|
+
|
|
135
|
+
updateConfig({
|
|
136
|
+
...vizConfig,
|
|
137
|
+
sharedFilterIndexes: newIndexes
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const getItemStyle = (isDragging, draggableStyle) => ({
|
|
142
|
+
...draggableStyle,
|
|
143
|
+
...(isDragging && sortableItemStyles)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const sortableItemStyles = {
|
|
147
|
+
background: 'rgba(0, 0, 0, 0.1)'
|
|
148
|
+
}
|
|
149
|
+
|
|
125
150
|
const addNewFilter = () => {
|
|
126
151
|
const _sharedFilters = _.cloneDeep(sharedFilters) || []
|
|
127
152
|
const columnName = 'New Dashboard Filter ' + (_sharedFilters.length + 1)
|
|
@@ -219,44 +244,70 @@ const DashboardFiltersEditor: React.FC<DashboardFitlersEditorProps> = ({ vizConf
|
|
|
219
244
|
<AccordionItemButton>Filters</AccordionItemButton>
|
|
220
245
|
</AccordionItemHeading>
|
|
221
246
|
<AccordionItemPanel>
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
247
|
+
<DragDropContext onDragEnd={handleFilterReorder}>
|
|
248
|
+
<Droppable droppableId='dashboard_filters_list'>
|
|
249
|
+
{provided => (
|
|
250
|
+
<ul {...provided.droppableProps} ref={provided.innerRef} className='draggable-field-list'>
|
|
251
|
+
{vizConfig.sharedFilterIndexes.map((index, filterIndex) => {
|
|
252
|
+
const filter = sharedFilters[index]
|
|
253
|
+
return (
|
|
254
|
+
<Draggable
|
|
255
|
+
key={filter.key + index}
|
|
256
|
+
draggableId={`filter-${filter.key}-${index}`}
|
|
257
|
+
index={filterIndex}
|
|
258
|
+
>
|
|
259
|
+
{(provided, snapshot) => (
|
|
260
|
+
<div
|
|
261
|
+
ref={provided.innerRef}
|
|
262
|
+
{...provided.draggableProps}
|
|
263
|
+
{...provided.dragHandleProps}
|
|
264
|
+
style={getItemStyle(snapshot.isDragging, provided.draggableProps.style)}
|
|
265
|
+
className={snapshot.isDragging ? 'currently-dragging' : ''}
|
|
266
|
+
>
|
|
267
|
+
<FieldSetWrapper
|
|
268
|
+
key={filter.key + index}
|
|
269
|
+
fieldName={filter.key}
|
|
270
|
+
fieldKey={index}
|
|
271
|
+
fieldType='Dashboard Filter'
|
|
272
|
+
controls={openControls}
|
|
273
|
+
draggable={true}
|
|
274
|
+
deleteField={() => {
|
|
275
|
+
overlay?.actions.openOverlay(
|
|
276
|
+
<DeleteFilterModal
|
|
277
|
+
removeFilterCompletely={removeFilter}
|
|
278
|
+
removeFilterFromViz={index => {
|
|
279
|
+
updateConfig({
|
|
280
|
+
...vizConfig,
|
|
281
|
+
sharedFilterIndexes: vizConfig.sharedFilterIndexes.filter(i => i !== index)
|
|
282
|
+
})
|
|
283
|
+
}}
|
|
284
|
+
filterIndex={index}
|
|
285
|
+
/>
|
|
286
|
+
)
|
|
287
|
+
}}
|
|
288
|
+
>
|
|
289
|
+
<FilterEditor
|
|
290
|
+
filter={filter}
|
|
291
|
+
filterIndex={index}
|
|
292
|
+
updateFilterProp={(name, value) => {
|
|
293
|
+
updateFilterProp(name, index, value)
|
|
294
|
+
}}
|
|
295
|
+
toggleNestedQueryParameters={checked => {
|
|
296
|
+
toggleNestedQueryParameters(index, checked)
|
|
297
|
+
}}
|
|
298
|
+
config={config}
|
|
299
|
+
/>
|
|
300
|
+
</FieldSetWrapper>
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
</Draggable>
|
|
304
|
+
)
|
|
305
|
+
})}
|
|
306
|
+
{provided.placeholder}
|
|
307
|
+
</ul>
|
|
308
|
+
)}
|
|
309
|
+
</Droppable>
|
|
310
|
+
</DragDropContext>
|
|
260
311
|
<button onClick={addNewFilter} className='btn btn-primary full-width'>
|
|
261
312
|
Add Filter
|
|
262
313
|
</button>
|
|
@@ -43,7 +43,11 @@ const FilterEditor: React.FC<FilterEditorProps> = ({
|
|
|
43
43
|
const filterStyles = Object.values(FILTER_STYLE)
|
|
44
44
|
|
|
45
45
|
const parentFilters: string[] = (config.dashboard.sharedFilters || [])
|
|
46
|
-
.filter(({ key
|
|
46
|
+
.filter(({ key }) => key !== filter.key)
|
|
47
|
+
.map(({ key }) => key)
|
|
48
|
+
|
|
49
|
+
const dataFilterParents: string[] = (config.dashboard.sharedFilters || [])
|
|
50
|
+
.filter(({ key }) => key !== filter.key)
|
|
47
51
|
.map(({ key }) => key)
|
|
48
52
|
|
|
49
53
|
const vizRowColumnLocator = getVizRowColumnLocator(config.rows)
|
|
@@ -246,21 +250,6 @@ const FilterEditor: React.FC<FilterEditorProps> = ({
|
|
|
246
250
|
<>
|
|
247
251
|
{!hasDashboardApplyBehavior(config.visualizations) && (
|
|
248
252
|
<>
|
|
249
|
-
<Select
|
|
250
|
-
label='URL to Filter'
|
|
251
|
-
value={filter.datasetKey || ''}
|
|
252
|
-
options={[
|
|
253
|
-
{ value: '', label: '- Select Option -' },
|
|
254
|
-
...Object.keys(config.datasets)
|
|
255
|
-
.filter(datasetKey => config.datasets[datasetKey].dataUrl)
|
|
256
|
-
.map(datasetKey => ({
|
|
257
|
-
value: datasetKey,
|
|
258
|
-
label: config.datasets[datasetKey].dataUrl
|
|
259
|
-
}))
|
|
260
|
-
]}
|
|
261
|
-
onChange={e => updateFilterProp('datasetKey', e.target.value)}
|
|
262
|
-
/>
|
|
263
|
-
|
|
264
253
|
<Select
|
|
265
254
|
label='Filter By'
|
|
266
255
|
value={filter.filterBy || ''}
|
|
@@ -271,6 +260,40 @@ const FilterEditor: React.FC<FilterEditorProps> = ({
|
|
|
271
260
|
]}
|
|
272
261
|
onChange={e => updateFilterProp('filterBy', e.target.value)}
|
|
273
262
|
/>
|
|
263
|
+
|
|
264
|
+
{filter.filterBy === 'File Name' && (
|
|
265
|
+
<Select
|
|
266
|
+
label='URL to Filter'
|
|
267
|
+
value={filter.datasetKey || ''}
|
|
268
|
+
options={[
|
|
269
|
+
{ value: '', label: '- Select Option -' },
|
|
270
|
+
...Object.keys(config.datasets)
|
|
271
|
+
.filter(datasetKey => config.datasets[datasetKey].dataUrl)
|
|
272
|
+
.map(datasetKey => ({
|
|
273
|
+
value: datasetKey,
|
|
274
|
+
label: config.datasets[datasetKey].dataUrl
|
|
275
|
+
}))
|
|
276
|
+
]}
|
|
277
|
+
onChange={e => updateFilterProp('datasetKey', e.target.value)}
|
|
278
|
+
tooltip={
|
|
279
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
280
|
+
<Tooltip.Target>
|
|
281
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
282
|
+
</Tooltip.Target>
|
|
283
|
+
<Tooltip.Content>
|
|
284
|
+
<p>Select which dataset URL's filename should be modified by this filter.</p>
|
|
285
|
+
</Tooltip.Content>
|
|
286
|
+
</Tooltip>
|
|
287
|
+
}
|
|
288
|
+
/>
|
|
289
|
+
)}
|
|
290
|
+
|
|
291
|
+
{filter.filterBy === 'Query String' && filter.usedBy && filter.usedBy.length > 0 && (
|
|
292
|
+
<div className='bg-info-subtle p-2 my-2' style={{ fontSize: '0.9em' }}>
|
|
293
|
+
<Icon display='info' style={{ marginRight: '0.5rem' }} />
|
|
294
|
+
Will apply to datasets used by selected widgets
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
274
297
|
{filter.filterBy === 'File Name' && (
|
|
275
298
|
<>
|
|
276
299
|
<TextField
|
|
@@ -617,19 +640,18 @@ const FilterEditor: React.FC<FilterEditorProps> = ({
|
|
|
617
640
|
updateField={(_section, _subSection, _key, value) => updateFilterProp('resetLabel', value)}
|
|
618
641
|
/>
|
|
619
642
|
|
|
620
|
-
<
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
{ value:
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
/>
|
|
643
|
+
<label>
|
|
644
|
+
<span className='edit-label column-heading mt-1'>Parent Filter(s): </span>
|
|
645
|
+
<MultiSelect
|
|
646
|
+
label='Parent Filter(s): '
|
|
647
|
+
options={dataFilterParents.map(key => ({ value: key, label: key }))}
|
|
648
|
+
fieldName='parents'
|
|
649
|
+
selected={filter.parents}
|
|
650
|
+
updateField={(_section, _subsection, _fieldname, newItems) => {
|
|
651
|
+
updateFilterProp('parents', newItems)
|
|
652
|
+
}}
|
|
653
|
+
/>
|
|
654
|
+
</label>
|
|
633
655
|
|
|
634
656
|
{!isNestedDropdown && (
|
|
635
657
|
<TextField
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { DashboardConfig } from '../../../../types/DashboardConfig'
|
|
2
2
|
import { SharedFilter } from '../../../../types/SharedFilter'
|
|
3
3
|
import _ from 'lodash'
|
|
4
|
-
import { SubGrouping } from '@cdc/core/types/VizFilter'
|
|
4
|
+
import { SubGrouping, OrderBy } from '@cdc/core/types/VizFilter'
|
|
5
5
|
import { TextField, Select } from '@cdc/core/components/EditorPanel/Inputs'
|
|
6
|
+
import { handleSorting } from '@cdc/core/components/Filters/helpers/handleSorting'
|
|
7
|
+
import { filterOrderOptions } from '@cdc/core/helpers/filterOrderOptions'
|
|
8
|
+
import FilterOrder from '@cdc/core/components/EditorPanel/VizFilterEditor/components/FilterOrder'
|
|
6
9
|
|
|
7
10
|
type NestedDropDownEditorDashboardProps = {
|
|
8
11
|
config: DashboardConfig
|
|
@@ -50,12 +53,10 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
|
|
|
50
53
|
const handleFitlerGroupColumnNameChange = (value: string) => {
|
|
51
54
|
if (!value) {
|
|
52
55
|
updateFilterProp('columnName', '')
|
|
53
|
-
updateFilterProp('defaultValue', '')
|
|
54
56
|
return
|
|
55
57
|
}
|
|
56
58
|
const [newColumnName, selectedOptionDatasetName] = value.split('|')
|
|
57
59
|
updateFilterProp('columnName', newColumnName)
|
|
58
|
-
updateFilterProp('defaultValue', '') // Reset default value when column changes
|
|
59
60
|
populateSubGroupingOptions(selectedOptionDatasetName, newColumnName)
|
|
60
61
|
}
|
|
61
62
|
|
|
@@ -66,18 +67,23 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
|
|
|
66
67
|
}
|
|
67
68
|
const [newColumnName, selectedOptionDatasetName] = value.split('|')
|
|
68
69
|
|
|
70
|
+
const order = subGrouping?.order || 'asc'
|
|
71
|
+
|
|
69
72
|
const valuesLookup = filter.values.reduce((acc, groupName) => {
|
|
70
|
-
const
|
|
73
|
+
const rawValues: string[] = _.uniq(
|
|
71
74
|
config.datasets[selectedOptionDatasetName].data
|
|
72
75
|
.map(d => {
|
|
73
76
|
return d[filter.columnName] === groupName ? d[newColumnName] : ''
|
|
74
77
|
})
|
|
75
78
|
.filter(value => value !== '')
|
|
76
|
-
)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
// Sort values according to the order setting
|
|
82
|
+
const { values: sortedValues } = handleSorting({ values: rawValues, order })
|
|
77
83
|
|
|
78
84
|
acc[groupName] = {
|
|
79
|
-
values,
|
|
80
|
-
orderedValues:
|
|
85
|
+
values: sortedValues,
|
|
86
|
+
orderedValues: sortedValues
|
|
81
87
|
}
|
|
82
88
|
return acc
|
|
83
89
|
}, {})
|
|
@@ -86,12 +92,94 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
|
|
|
86
92
|
...subGrouping,
|
|
87
93
|
columnName: newColumnName,
|
|
88
94
|
valuesLookup,
|
|
95
|
+
order,
|
|
89
96
|
defaultValue: '' // Reset default value when column changes
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
updateFilterProp('subGrouping', newSubGrouping)
|
|
93
100
|
}
|
|
94
101
|
|
|
102
|
+
// Handle group order change (asc/desc/cust)
|
|
103
|
+
const handleGroupingOrderBy = (order: OrderBy) => {
|
|
104
|
+
const groupSortObject = {
|
|
105
|
+
values: _.cloneDeep(filter.values),
|
|
106
|
+
order
|
|
107
|
+
}
|
|
108
|
+
const { values: newOrderedValues } = handleSorting(groupSortObject)
|
|
109
|
+
|
|
110
|
+
const updates: Partial<SharedFilter> = {
|
|
111
|
+
values: newOrderedValues,
|
|
112
|
+
order
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (order === 'cust') {
|
|
116
|
+
updates.orderedValues = newOrderedValues
|
|
117
|
+
} else {
|
|
118
|
+
updates.orderedValues = undefined
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Update filter with new order and values
|
|
122
|
+
updateFilterProp('order', order)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Handle drag-drop reorder for group values
|
|
126
|
+
const handleGroupingCustomOrder = (sourceIndex: number, destinationIndex: number) => {
|
|
127
|
+
if (sourceIndex === undefined || destinationIndex === undefined || sourceIndex === destinationIndex) return
|
|
128
|
+
|
|
129
|
+
const orderedValues = _.cloneDeep(filter.orderedValues || filter.values)
|
|
130
|
+
const [movedItem] = orderedValues.splice(sourceIndex, 1)
|
|
131
|
+
orderedValues.splice(destinationIndex, 0, movedItem)
|
|
132
|
+
|
|
133
|
+
// Update both values and orderedValues, and ensure order is 'cust'
|
|
134
|
+
updateFilterProp('orderedValues', orderedValues)
|
|
135
|
+
if (filter.order !== 'cust') {
|
|
136
|
+
updateFilterProp('order', 'cust')
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Handle subgroup order change (asc/desc/cust)
|
|
141
|
+
const handleSubGroupingOrderBy = (order: OrderBy) => {
|
|
142
|
+
const newValuesLookup = Object.keys(subGrouping.valuesLookup).reduce((acc, groupName) => {
|
|
143
|
+
const subGroup = subGrouping.valuesLookup[groupName]
|
|
144
|
+
const { values: sortedValues } = handleSorting({ values: _.cloneDeep(subGroup.values), order })
|
|
145
|
+
|
|
146
|
+
acc[groupName] = {
|
|
147
|
+
values: sortedValues,
|
|
148
|
+
orderedValues: order === 'cust' ? sortedValues : undefined
|
|
149
|
+
}
|
|
150
|
+
return acc
|
|
151
|
+
}, {})
|
|
152
|
+
|
|
153
|
+
const newSubGrouping: SubGrouping = {
|
|
154
|
+
...subGrouping,
|
|
155
|
+
order,
|
|
156
|
+
valuesLookup: newValuesLookup
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
updateFilterProp('subGrouping', newSubGrouping)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Handle drag-drop reorder for subgroup values within a specific group
|
|
163
|
+
const handleSubGroupingCustomOrder = (
|
|
164
|
+
sourceIndex: number,
|
|
165
|
+
destinationIndex: number,
|
|
166
|
+
currentOrderedValues: string[],
|
|
167
|
+
groupName: string
|
|
168
|
+
) => {
|
|
169
|
+
if (sourceIndex === undefined || destinationIndex === undefined || sourceIndex === destinationIndex) return
|
|
170
|
+
|
|
171
|
+
const updatedGroupOrderedValues = _.cloneDeep(currentOrderedValues)
|
|
172
|
+
const [movedItem] = updatedGroupOrderedValues.splice(sourceIndex, 1)
|
|
173
|
+
updatedGroupOrderedValues.splice(destinationIndex, 0, movedItem)
|
|
174
|
+
|
|
175
|
+
const newSubGrouping = _.cloneDeep(subGrouping)
|
|
176
|
+
newSubGrouping.valuesLookup[groupName].values = updatedGroupOrderedValues
|
|
177
|
+
newSubGrouping.valuesLookup[groupName].orderedValues = updatedGroupOrderedValues
|
|
178
|
+
newSubGrouping.order = 'cust'
|
|
179
|
+
|
|
180
|
+
updateFilterProp('subGrouping', newSubGrouping)
|
|
181
|
+
}
|
|
182
|
+
|
|
95
183
|
return (
|
|
96
184
|
<div className='nesteddropdown-editor'>
|
|
97
185
|
{!isDashboard && (
|
|
@@ -144,7 +232,7 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
|
|
|
144
232
|
{filter.columnName && filter.values && filter.values.length > 0 && (
|
|
145
233
|
<Select
|
|
146
234
|
value={filter.defaultValue}
|
|
147
|
-
options={filter.values}
|
|
235
|
+
options={filter.orderedValues || filter.values}
|
|
148
236
|
updateField={(_section, _subSection, _key, value) => updateFilterProp('defaultValue', value)}
|
|
149
237
|
label={'Group Default Value'}
|
|
150
238
|
initial={'Select'}
|
|
@@ -157,7 +245,8 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
|
|
|
157
245
|
value={subGrouping.defaultValue}
|
|
158
246
|
options={(() => {
|
|
159
247
|
const groupKey = filter.defaultValue || (Array.isArray(filter.active) ? filter.active[0] : filter.active)
|
|
160
|
-
|
|
248
|
+
const lookup = subGrouping.valuesLookup[groupKey as string]
|
|
249
|
+
return lookup?.orderedValues || lookup?.values || []
|
|
161
250
|
})()}
|
|
162
251
|
updateField={(_section, _subSection, _key, value) => {
|
|
163
252
|
const newSubGrouping = { ...subGrouping, defaultValue: value }
|
|
@@ -167,6 +256,54 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
|
|
|
167
256
|
initial={'Select'}
|
|
168
257
|
/>
|
|
169
258
|
)}
|
|
259
|
+
|
|
260
|
+
{/* Group Order */}
|
|
261
|
+
{filter.columnName && filter.values && filter.values.length > 0 && (
|
|
262
|
+
<div className='mt-2'>
|
|
263
|
+
<Select
|
|
264
|
+
label='Group Order'
|
|
265
|
+
value={filter.order || 'asc'}
|
|
266
|
+
options={filterOrderOptions}
|
|
267
|
+
onChange={e => handleGroupingOrderBy(e.target.value as OrderBy)}
|
|
268
|
+
/>
|
|
269
|
+
{filter.order === 'cust' && (
|
|
270
|
+
<FilterOrder
|
|
271
|
+
orderedValues={filter.orderedValues || filter.values}
|
|
272
|
+
handleFilterOrder={handleGroupingCustomOrder}
|
|
273
|
+
/>
|
|
274
|
+
)}
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
|
|
278
|
+
{/* SubGrouping Order */}
|
|
279
|
+
{subGrouping?.columnName && subGrouping.valuesLookup && Object.keys(subGrouping.valuesLookup).length > 0 && (
|
|
280
|
+
<div className='mt-2'>
|
|
281
|
+
<Select
|
|
282
|
+
label='SubGrouping Order'
|
|
283
|
+
value={subGrouping.order || 'asc'}
|
|
284
|
+
options={filterOrderOptions}
|
|
285
|
+
onChange={e => handleSubGroupingOrderBy(e.target.value as OrderBy)}
|
|
286
|
+
/>
|
|
287
|
+
{subGrouping.order === 'cust' &&
|
|
288
|
+
(filter.orderedValues || filter.values)?.map((groupName, i) => {
|
|
289
|
+
const lookup = subGrouping.valuesLookup[groupName]
|
|
290
|
+
if (!lookup) return null
|
|
291
|
+
const orderedSubGroupValues = lookup.orderedValues || lookup.values
|
|
292
|
+
return (
|
|
293
|
+
<div key={`group-subgroup-values-${groupName}-${i}`}>
|
|
294
|
+
<span className='font-weight-bold fw-bold'>{groupName}</span>
|
|
295
|
+
<FilterOrder
|
|
296
|
+
key={`subgroup-values-${groupName}-${i}`}
|
|
297
|
+
orderedValues={orderedSubGroupValues}
|
|
298
|
+
handleFilterOrder={(sourceIndex, destinationIndex) => {
|
|
299
|
+
handleSubGroupingCustomOrder(sourceIndex, destinationIndex, orderedSubGroupValues, groupName)
|
|
300
|
+
}}
|
|
301
|
+
/>
|
|
302
|
+
</div>
|
|
303
|
+
)
|
|
304
|
+
})}
|
|
305
|
+
</div>
|
|
306
|
+
)}
|
|
170
307
|
</div>
|
|
171
308
|
)
|
|
172
309
|
}
|
|
@@ -177,15 +177,18 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
|
177
177
|
// Reset filtersApplied state to false when clearing filters
|
|
178
178
|
dispatch({ type: 'SET_FILTERS_APPLIED', payload: false })
|
|
179
179
|
|
|
180
|
+
// Update child filter values before filtering data
|
|
181
|
+
const updatedFilters = updateChildFilters(dashboardConfig.sharedFilters, state.data)
|
|
182
|
+
|
|
180
183
|
// Update filtered data immediately after resetting filters
|
|
181
|
-
// Use the updated
|
|
184
|
+
// Use the updated filters instead of state
|
|
182
185
|
const clonedState = {
|
|
183
186
|
...state,
|
|
184
187
|
config: {
|
|
185
188
|
...state.config,
|
|
186
189
|
dashboard: {
|
|
187
190
|
...state.config.dashboard,
|
|
188
|
-
sharedFilters:
|
|
191
|
+
sharedFilters: updatedFilters
|
|
189
192
|
}
|
|
190
193
|
}
|
|
191
194
|
}
|
|
@@ -262,8 +265,9 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
|
262
265
|
loadAPIFilters(newSharedFilters, loadingFilterMemo, undefined, undefined, isStale)
|
|
263
266
|
}
|
|
264
267
|
} else {
|
|
268
|
+
const updatedFilters = updateChildFilters(newSharedFilters, state.data)
|
|
265
269
|
if (newSharedFilters[index].type === 'urlfilter' && newSharedFilters[index].apiFilter) {
|
|
266
|
-
reloadURLData(
|
|
270
|
+
reloadURLData(updatedFilters)
|
|
267
271
|
} else {
|
|
268
272
|
const clonedState = {
|
|
269
273
|
...state,
|
|
@@ -271,13 +275,13 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
|
271
275
|
...state.config,
|
|
272
276
|
dashboard: {
|
|
273
277
|
...state.config.dashboard,
|
|
274
|
-
sharedFilters:
|
|
278
|
+
sharedFilters: updatedFilters
|
|
275
279
|
}
|
|
276
280
|
}
|
|
277
281
|
}
|
|
278
282
|
const newFilteredData = getFilteredData(clonedState)
|
|
279
283
|
dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
|
|
280
|
-
dispatch({ type: 'SET_SHARED_FILTERS', payload:
|
|
284
|
+
dispatch({ type: 'SET_SHARED_FILTERS', payload: updatedFilters })
|
|
281
285
|
}
|
|
282
286
|
}
|
|
283
287
|
}
|
|
@@ -293,9 +297,9 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
|
293
297
|
// if all of the filters are hidden filters don't display the VisualizationWrapper
|
|
294
298
|
const filters = visualizationConfig?.sharedFilterIndexes
|
|
295
299
|
?.map(Number)
|
|
296
|
-
|
|
300
|
+
?.map(filterIndex => dashboardConfig.dashboard.sharedFilters[filterIndex])
|
|
297
301
|
|
|
298
|
-
const displayNone = filters
|
|
302
|
+
const displayNone = filters?.length ? filters.every(filter => filter.showDropdown === false) : false
|
|
299
303
|
if (displayNone && !isEditor) return <></>
|
|
300
304
|
return (
|
|
301
305
|
<Layout.VisualizationWrapper config={visualizationConfig} isEditor={isEditor} currentViewport={currentViewport}>
|
|
@@ -194,7 +194,12 @@ export const DataDesignerModal: React.FC<DataDesignerModalProps> = ({ vizKey, ro
|
|
|
194
194
|
) : (
|
|
195
195
|
<>
|
|
196
196
|
<Select
|
|
197
|
-
options={Object.keys(
|
|
197
|
+
options={Object.keys(
|
|
198
|
+
config.rows[rowIndex]?.data?.[0] ||
|
|
199
|
+
configureData.data?.[0] ||
|
|
200
|
+
config.datasets[configureData.dataKey]?.data?.[0] ||
|
|
201
|
+
{}
|
|
202
|
+
)}
|
|
198
203
|
value={config.rows[rowIndex].multiVizColumn}
|
|
199
204
|
label='Multi-Visualization Column'
|
|
200
205
|
initial='--Select--'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useContext, useRef } from 'react'
|
|
1
|
+
import { useContext, useRef, useEffect } from 'react'
|
|
2
2
|
import cloneConfig from '@cdc/core/helpers/cloneConfig'
|
|
3
3
|
import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
|
|
4
4
|
|
|
@@ -20,8 +20,22 @@ const Header = (props: HeaderProps) => {
|
|
|
20
20
|
const dispatch = useContext(DashboardDispatchContext)
|
|
21
21
|
const back = () => {
|
|
22
22
|
if (!visualizationKey) return
|
|
23
|
+
|
|
23
24
|
const newConfig = cloneConfig(config)
|
|
24
|
-
|
|
25
|
+
|
|
26
|
+
// Ensure visualizations object exists
|
|
27
|
+
if (!newConfig.visualizations || !newConfig.visualizations[visualizationKey]) {
|
|
28
|
+
console.error(`Visualization ${visualizationKey} not found in config`)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Explicitly set editing to false
|
|
33
|
+
newConfig.visualizations[visualizationKey] = {
|
|
34
|
+
...newConfig.visualizations[visualizationKey],
|
|
35
|
+
editing: false,
|
|
36
|
+
showEditorPanel: false
|
|
37
|
+
}
|
|
38
|
+
|
|
25
39
|
dispatch({ type: 'SET_CONFIG', payload: newConfig })
|
|
26
40
|
|
|
27
41
|
// the Widget component will do a data fetch if no data is available for the visualization
|
|
@@ -59,19 +73,21 @@ const Header = (props: HeaderProps) => {
|
|
|
59
73
|
const configStringRef = useRef<string>()
|
|
60
74
|
|
|
61
75
|
// Only update parent when config content actually changes (not just reference)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
configStringRef.current
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
setParentConfig
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const configString = JSON.stringify(convertStateToConfig())
|
|
78
|
+
if (configStringRef.current !== configString) {
|
|
79
|
+
configStringRef.current = configString
|
|
80
|
+
|
|
81
|
+
// Emit the data in a regular JS event so it can be consumed by anything.
|
|
82
|
+
const event = new CustomEvent('updateVizConfig', { detail: configString })
|
|
83
|
+
window.dispatchEvent(event)
|
|
84
|
+
|
|
85
|
+
// Pass up to Editor if needed
|
|
86
|
+
if (setParentConfig) {
|
|
87
|
+
setParentConfig(JSON.parse(configString))
|
|
88
|
+
}
|
|
73
89
|
}
|
|
74
|
-
}
|
|
90
|
+
}, [config, setParentConfig])
|
|
75
91
|
|
|
76
92
|
const handleCheck = e => {
|
|
77
93
|
const { checked } = e.currentTarget
|
|
@@ -97,12 +113,27 @@ const Header = (props: HeaderProps) => {
|
|
|
97
113
|
multidashboard
|
|
98
114
|
</span>
|
|
99
115
|
<br />
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
<div style={{ display: 'flex', alignItems: 'flex-end', gap: '10px' }}>
|
|
117
|
+
<input
|
|
118
|
+
type='text'
|
|
119
|
+
placeholder='Enter Dashboard Name Here'
|
|
120
|
+
defaultValue={config.dashboard?.title}
|
|
121
|
+
onChange={e => changeConfigValue('dashboard', 'title', e.target.value)}
|
|
122
|
+
style={{ flex: 1 }}
|
|
123
|
+
/>
|
|
124
|
+
<label style={{ display: 'flex', flexDirection: 'column', gap: '3px', fontSize: '0.85em' }}>
|
|
125
|
+
<span style={{ fontSize: '0.8em' }}>Title Style</span>
|
|
126
|
+
<select
|
|
127
|
+
value={config.dashboard.titleStyle}
|
|
128
|
+
onChange={e => changeConfigValue('dashboard', 'titleStyle', e.target.value)}
|
|
129
|
+
style={{ fontSize: '0.9em' }}
|
|
130
|
+
>
|
|
131
|
+
<option value='small'>Small</option>
|
|
132
|
+
<option value='large'>Large</option>
|
|
133
|
+
<option value='legacy'>Legacy</option>
|
|
134
|
+
</select>
|
|
135
|
+
</label>
|
|
136
|
+
</div>
|
|
106
137
|
</div>
|
|
107
138
|
)}
|
|
108
139
|
{!subEditor && (
|