@cdc/dashboard 4.24.11 → 4.24.12

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 (32) hide show
  1. package/dist/cdcdashboard.js +46791 -46403
  2. package/examples/ed-visits-county-file.json +141 -357
  3. package/examples/private/DEV-9199.json +606 -0
  4. package/examples/private/DEV-9684.json +2135 -0
  5. package/examples/private/art-dashboard.json +18174 -0
  6. package/examples/private/art-scratch.json +2406 -0
  7. package/examples/private/gaza-issue.json +1214 -0
  8. package/package.json +9 -9
  9. package/src/CdcDashboard.tsx +8 -15
  10. package/src/CdcDashboardComponent.tsx +53 -38
  11. package/src/DashboardContext.tsx +2 -0
  12. package/src/components/CollapsibleVisualizationRow.tsx +8 -2
  13. package/src/components/DashboardFilters/DashboardFilters.tsx +107 -59
  14. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +2 -0
  15. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +54 -50
  16. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +6 -2
  17. package/src/components/DashboardFilters/dashboardfilter.styles.css +16 -0
  18. package/src/components/VisualizationRow.tsx +30 -20
  19. package/src/components/Widget.tsx +1 -1
  20. package/src/data/initial-state.js +2 -1
  21. package/src/helpers/addValuesToDashboardFilters.ts +4 -2
  22. package/src/helpers/apiFilterHelpers.ts +55 -20
  23. package/src/helpers/changeFilterActive.ts +3 -0
  24. package/src/helpers/filterData.ts +1 -1
  25. package/src/helpers/loadAPIFilters.ts +25 -8
  26. package/src/helpers/reloadURLHelpers.ts +9 -2
  27. package/src/helpers/shouldLoadAllFilters.ts +30 -0
  28. package/src/helpers/tests/apiFilterHelpers.test.ts +85 -4
  29. package/src/helpers/tests/loadAPIFiltersWrapper.test.ts +8 -3
  30. package/src/helpers/tests/reloadURLHelpers.test.ts +11 -5
  31. package/src/helpers/tests/shouldLoadAllFilters.test.ts +117 -0
  32. package/src/store/dashboard.reducer.ts +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cdc/dashboard",
3
- "version": "4.24.11",
3
+ "version": "4.24.12",
4
4
  "description": "React component for combining multiple visualizations into a single dashboard",
5
5
  "moduleName": "CdcDashboard",
6
6
  "main": "dist/cdcdashboard",
@@ -27,13 +27,13 @@
27
27
  },
28
28
  "license": "Apache-2.0",
29
29
  "dependencies": {
30
- "@cdc/chart": "^4.24.11",
31
- "@cdc/core": "^4.24.11",
32
- "@cdc/data-bite": "^4.24.11",
33
- "@cdc/filtered-text": "^4.24.11",
34
- "@cdc/map": "^4.24.11",
35
- "@cdc/markup-include": "^4.24.11",
36
- "@cdc/waffle-chart": "^4.24.11",
30
+ "@cdc/chart": "^4.24.12",
31
+ "@cdc/core": "^4.24.12",
32
+ "@cdc/data-bite": "^4.24.12",
33
+ "@cdc/filtered-text": "^4.24.12",
34
+ "@cdc/map": "^4.24.12",
35
+ "@cdc/markup-include": "^4.24.12",
36
+ "@cdc/waffle-chart": "^4.24.12",
37
37
  "html-react-parser": "^3.0.8",
38
38
  "js-base64": "^2.5.2",
39
39
  "papaparse": "^5.3.0",
@@ -49,5 +49,5 @@
49
49
  "react": "^18.2.0",
50
50
  "react-dom": "^18.2.0"
51
51
  },
52
- "gitHead": "9ab5ee9b2b0ef7321a66a2104be6ce8899ec3808"
52
+ "gitHead": "347414f1da4b0e9bf2f22a7b59335deccf0b2d9c"
53
53
  }
@@ -3,7 +3,6 @@ import CdcDashboard from './CdcDashboardComponent'
3
3
  import { MultiDashboardConfig } from './types/MultiDashboard'
4
4
  import Loading from '@cdc/core/components/Loading'
5
5
  import defaults from './data/initial-state'
6
- import { processData } from './helpers/processData'
7
6
  import { getVizKeys } from './helpers/getVizKeys'
8
7
  import { processDataLegacy } from './helpers/processDataLegacy'
9
8
  import { WCMSProps } from '@cdc/core/types/WCMSProps'
@@ -13,7 +12,6 @@ import { InitialState } from './types/InitialState'
13
12
  import { DashboardConfig } from './types/DashboardConfig'
14
13
  import { coveUpdateWorker } from '@cdc/core/helpers/coveUpdateWorker'
15
14
  import _ from 'lodash'
16
- import { hasDashboardApplyBehavior } from './helpers/hasDashboardApplyBehavior'
17
15
  import { getQueryParams } from '@cdc/core/helpers/queryStringUtils'
18
16
 
19
17
  type MultiDashboardProps = Omit<WCMSProps, 'configUrl'> & {
@@ -62,18 +60,13 @@ const MultiDashboardWrapper: React.FC<MultiDashboardProps> = ({
62
60
  loadConfig()
63
61
  }, [])
64
62
 
65
- const loadData = async (initialConfig: DashboardConfig | MultiDashboardConfig) => {
63
+ const prepareDatasets = (initialConfig: DashboardConfig | MultiDashboardConfig) => {
66
64
  let newConfig = { ...initialConfig }
67
- let datasets: Record<string, Object[]> = {}
68
- await Promise.all(
69
- Object.keys(initialConfig.datasets).map(async key => {
70
- const legacySupportApply = (initialConfig as any).filterBehavior === 'Apply Button'
71
- const hasApplyBehavior = hasDashboardApplyBehavior(initialConfig.visualizations) || legacySupportApply
72
- const data = await processData(initialConfig.datasets[key], !hasApplyBehavior)
73
- datasets[key] = data || []
74
- })
75
- )
76
-
65
+ const datasets: Record<string, Object[]> = Object.keys(initialConfig.datasets).reduce((acc, key) => {
66
+ const dataset = initialConfig.datasets[key]
67
+ acc[key] = dataset.formattedData || dataset.data
68
+ return acc
69
+ }, {})
77
70
  getVizKeys(newConfig).forEach(vizKey => {
78
71
  const formattedData = datasets[newConfig.visualizations[vizKey].dataKey]
79
72
  if (formattedData) {
@@ -92,7 +85,7 @@ const MultiDashboardWrapper: React.FC<MultiDashboardProps> = ({
92
85
  let newConfig = { ...defaults, ...config } as DashboardConfig
93
86
 
94
87
  if (config.datasets) {
95
- return await loadData(newConfig)
88
+ return prepareDatasets(newConfig)
96
89
  } else {
97
90
  const dataKey = newConfig.dataFileName || 'backwards-compatibility'
98
91
  const data = await processDataLegacy(config)
@@ -144,7 +137,7 @@ const MultiDashboardWrapper: React.FC<MultiDashboardProps> = ({
144
137
  multiDashboards: multiConfig.multiDashboards,
145
138
  activeDashboard: selectedConfig
146
139
  } as MultiDashboardConfig
147
- return await loadData(newConfig)
140
+ return prepareDatasets(newConfig)
148
141
  }
149
142
 
150
143
  if (!initial) return <Loading />
@@ -43,13 +43,12 @@ import { type TableConfig } from '@cdc/core/components/DataTable/types/TableConf
43
43
 
44
44
  // types
45
45
  import { type SharedFilter } from './types/SharedFilter'
46
- import { type APIFilter } from './types/APIFilter'
47
46
  import { type WCMSProps } from '@cdc/core/types/WCMSProps'
48
47
  import { type InitialState } from './types/InitialState'
49
48
  import MultiTabs from './components/MultiConfigTabs'
50
49
  import _ from 'lodash'
51
50
  import EditorContext from '../../editor/src/ConfigContext'
52
- import { APIFilterDropdowns, DropdownOptions } from './components/DashboardFilters'
51
+ import { APIFilterDropdowns } from './components/DashboardFilters'
53
52
  import DataTableStandAlone from '@cdc/core/components/DataTable/DataTableStandAlone'
54
53
  import { ViewPort } from '@cdc/core/types/ViewPort'
55
54
  import VisualizationRow from './components/VisualizationRow'
@@ -63,10 +62,10 @@ import { addValuesToDashboardFilters } from './helpers/addValuesToDashboardFilte
63
62
  import { DashboardFilters } from './types/DashboardFilters'
64
63
  import DashboardSharedFilters from './components/DashboardFilters'
65
64
  import ExpandCollapseButtons from './components/ExpandCollapseButtons'
66
- import { hasDashboardApplyBehavior } from './helpers/hasDashboardApplyBehavior'
67
65
  import { loadAPIFiltersFactory } from './helpers/loadAPIFilters'
68
66
  import Loader from '@cdc/core/components/Loader'
69
67
  import Alert from '@cdc/core/components/Alert'
68
+ import { shouldLoadAllFilters } from './helpers/shouldLoadAllFilters'
70
69
 
71
70
  type DashboardProps = Omit<WCMSProps, 'configUrl'> & {
72
71
  initialState: InitialState
@@ -85,7 +84,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
85
84
  const isPreview = state.tabSelected === 'Dashboard Preview'
86
85
 
87
86
  const inNoDataState = useMemo(() => {
88
- const vals = Object.values(state.data)
87
+ const vals = reloadURLHelpers.getDatasetKeys(state.config).map(key => state.data[key])
89
88
  if (!vals.length) return true
90
89
  return vals.some(val => val === undefined)
91
90
  }, [state.data])
@@ -129,7 +128,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
129
128
  filters.forEach(filter => {
130
129
  if (
131
130
  filter.type === 'urlfilter' &&
132
- reloadURLHelpers.filterUsedByDataUrl(filter, datasetKey, config.visualizations)
131
+ reloadURLHelpers.filterUsedByDataUrl(filter, datasetKey, config.visualizations, config.rows)
133
132
  ) {
134
133
  if (filter.filterBy === 'File Name') {
135
134
  newFileName = reloadURLHelpers.getNewFileName(newFileName, filter, datasetKey)
@@ -146,10 +145,9 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
146
145
  if (!!filter.setByQueryParameter) {
147
146
  const windowQueryParams = Object.fromEntries(new URLSearchParams(window.location.search))
148
147
  const filterValue = windowQueryParams[filter.setByQueryParameter]
149
- if (filter.apiFilter) {
150
- updatedQSParams[filter.apiFilter.valueSelector] = filterValue
151
- } else {
152
- updatedQSParams[filter.setByQueryParameter] = filterValue
148
+ const queryParam = filter.apiFilter?.valueSelector || filter.setByQueryParameter
149
+ if (filterValue) {
150
+ updatedQSParams[queryParam] = filterValue
153
151
  }
154
152
  }
155
153
 
@@ -162,7 +160,9 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
162
160
  }
163
161
  })
164
162
 
165
- if (!!newFilters || reloadURLHelpers.isUpdateNeeded(filters, currentQSParams, updatedQSParams)) {
163
+ const dataNeedsUpdate = reloadURLHelpers.isUpdateNeeded(filters, currentQSParams, updatedQSParams)
164
+
165
+ if (!!newFilters || dataNeedsUpdate) {
166
166
  dataWasFetched = true
167
167
  const dataUrlFinal = reloadURLHelpers.getDataURL(
168
168
  { ...currentQSParams, ...updatedQSParams },
@@ -189,6 +189,10 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
189
189
  })
190
190
  .catch(e => {
191
191
  console.error(e)
192
+ dispatchErrorMessages({
193
+ type: 'ADD_ERROR_MESSAGE',
194
+ payload: 'There was a problem returning data. Please try again.'
195
+ })
192
196
  newDatasets[datasetKey].data = []
193
197
  newDatasets[datasetKey].runtimeDataUrl = dataUrlFinal
194
198
  newData[datasetKey] = []
@@ -197,16 +201,30 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
197
201
  }
198
202
  }
199
203
 
200
- if (dataWasFetched) {
201
- dispatch({ type: 'SET_DATA', payload: newData })
202
- const filtersWithNewValues = addValuesToDashboardFilters(filters, newData)
203
- const dashboardConfig = newFilters
204
- ? { ...config.dashboard, sharedFilters: filtersWithNewValues }
205
- : config.dashboard
204
+ const datasetsWithFiles = _.pickBy(newDatasets, dataset => !dataset.dataUrl)
205
+
206
+ if (dataWasFetched || Object.keys(datasetsWithFiles).length) {
207
+ const dataFiles = Object.keys(datasetsWithFiles).reduce((acc, key) => {
208
+ acc[key] = datasetsWithFiles[key].data
209
+ return acc
210
+ }, {})
211
+ const _newData = { ...newData, ...dataFiles }
212
+ dispatch({ type: 'SET_DATA', payload: _newData })
213
+ const dataFilterIndexes = config.dashboard.sharedFilters.reduce((acc, filter, index) => {
214
+ if (filter.type === 'datafilter') acc.push(index)
215
+ return acc
216
+ }, [])
217
+ const appliedFilterIndexes = Object.values(config.visualizations)
218
+ .filter(viz => viz.type === 'dashboardFilters')
219
+ .flatMap(viz => viz.sharedFilterIndexes)
220
+ .filter(index => !dataFilterIndexes.includes(index))
221
+ const filtersWithNewValues = addValuesToDashboardFilters(filters, _newData, appliedFilterIndexes)
222
+
223
+ const dashboardConfig = { ...config.dashboard, sharedFilters: filtersWithNewValues }
206
224
  const filteredData = getFilteredData(
207
225
  { ...state, config: { ...state.config, dashboard: dashboardConfig } },
208
226
  {},
209
- newData
227
+ _newData
210
228
  )
211
229
  dispatch({ type: 'SET_FILTERED_DATA', payload: filteredData })
212
230
  const visualizations = reloadURLHelpers.getVisualizationsWithFormattedData(config.visualizations, newData)
@@ -246,15 +264,16 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
246
264
  }
247
265
 
248
266
  useEffect(() => {
249
- if (isEditor && !isPreview) return
250
267
  const { config } = state
251
- if (!hasDashboardApplyBehavior(config.visualizations)) {
252
- reloadURLData()
253
- }
254
-
268
+ const loadAllFilters = shouldLoadAllFilters(config, isEditor && !isPreview)
255
269
  const sharedFiltersWithValues = addValuesToDashboardFilters(config.dashboard.sharedFilters, state.data)
256
- loadAPIFilters(sharedFiltersWithValues, apiFilterDropdowns)
257
- updateFilteredData(sharedFiltersWithValues)
270
+
271
+ loadAPIFilters(sharedFiltersWithValues, apiFilterDropdowns, loadAllFilters).then(newFilters => {
272
+ const allValuesSelected = newFilters.every(filter => {
273
+ return filter.type === 'datafilter' || filter.active
274
+ })
275
+ if (allValuesSelected) reloadURLData(newFilters)
276
+ })
258
277
  }, [isEditor, isPreview, state.config?.activeDashboard])
259
278
 
260
279
  const updateChildConfig = (visualizationKey, newConfig) => {
@@ -278,13 +297,6 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
278
297
  }
279
298
  }
280
299
 
281
- const updateFilteredData = (sharedFilters = undefined) => {
282
- const clonedState = _.cloneDeep(state)
283
- if (sharedFilters) clonedState.config.dashboard.sharedFilters = sharedFilters
284
- const newFilteredData = getFilteredData(clonedState)
285
- dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
286
- }
287
-
288
300
  const resizeObserver = new ResizeObserver(entries => {
289
301
  for (let entry of entries) {
290
302
  let newViewport = getViewport(entry.contentRect.width)
@@ -437,8 +449,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
437
449
  )
438
450
  break
439
451
  case 'dashboardFilters':
440
- const hideFilter = visualizationConfig.autoLoad && inNoDataState
441
- body = !hideFilter ? (
452
+ body = (
442
453
  <>
443
454
  <Header visualizationKey={visualizationKey} subEditor='Filter Dropdowns' />
444
455
  <DashboardSharedFilters
@@ -448,8 +459,6 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
448
459
  setConfig={_updateConfig}
449
460
  />
450
461
  </>
451
- ) : (
452
- <></>
453
462
  )
454
463
  break
455
464
  case 'table':
@@ -469,11 +478,12 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
469
478
  body = (
470
479
  <>
471
480
  <Header visualizationKey={visualizationKey} subEditor='Footnotes' />
481
+ {/* Datasets are passed in just for reference and need to be removed */}
472
482
  <FootnotesStandAlone
473
483
  visualizationKey={visualizationKey}
474
484
  config={{ ...visualizationConfig, datasets: state.config.datasets }}
475
485
  isEditor={true}
476
- updateConfig={_updateConfig}
486
+ updateConfig={conf => _updateConfig(_.omit(conf, 'datasets'))}
477
487
  />
478
488
  </>
479
489
  )
@@ -542,7 +552,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
542
552
  return (
543
553
  <>
544
554
  {/* Expand/Collapse All */}
545
- {row.expandCollapseAllButtons === true && (
555
+ {!inNoDataState && row.expandCollapseAllButtons === true && (
546
556
  <ExpandCollapseButtons setAllExpanded={setAllExpanded} />
547
557
  )}
548
558
  {Object.keys(dataGroups).map(groupName => {
@@ -559,6 +569,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
559
569
  updateChildConfig={updateChildConfig}
560
570
  apiFilterDropdowns={apiFilterDropdowns}
561
571
  currentViewport={currentViewport}
572
+ inNoDataState={inNoDataState}
562
573
  />
563
574
  )
564
575
  })}
@@ -576,11 +587,14 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
576
587
  updateChildConfig={updateChildConfig}
577
588
  apiFilterDropdowns={apiFilterDropdowns}
578
589
  currentViewport={currentViewport}
590
+ inNoDataState={inNoDataState}
579
591
  />
580
592
  )
581
593
  }
582
594
  })}
583
595
 
596
+ {inNoDataState ? <div className='mt-5'>Please complete your selection to continue.</div> : <></>}
597
+
584
598
  {/* Image or PDF Inserts */}
585
599
  <section className='download-buttons'>
586
600
  {config.table?.downloadImageButton && (
@@ -691,7 +705,8 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
691
705
  isDebug,
692
706
  loadAPIFilters,
693
707
  setAPIFilterDropdowns,
694
- reloadURLData
708
+ reloadURLData,
709
+ setAPILoading
695
710
  }}
696
711
  >
697
712
  <DashboardDispatchContext.Provider value={dispatch}>
@@ -17,6 +17,7 @@ type ConfigCTX = DashboardState & {
17
17
  recursiveLimit?: number
18
18
  ) => Promise<SharedFilter[]>
19
19
  setAPIFilterDropdowns: (dropdowns: APIFilterDropdowns) => void
20
+ setAPILoading: (loading: boolean) => void
20
21
  }
21
22
 
22
23
  const firstTab: Tab = 'Dashboard Description'
@@ -33,6 +34,7 @@ const initialContext: ConfigCTX = {
33
34
  outerContainerRef: () => {},
34
35
  setParentConfig: () => {},
35
36
  setAPIFilterDropdowns: () => {},
37
+ setAPILoading: () => {},
36
38
  reloadURLData: () => {},
37
39
  loadAPIFilters: () => Promise.resolve([]),
38
40
  isDebug: false,
@@ -8,10 +8,16 @@ type CollapsableVizRow = {
8
8
  groupName: string
9
9
  currentViewport: string
10
10
  }
11
- const CollapsibleVisualizationRow: React.FC<CollapsableVizRow> = ({ allExpanded, fontSize, groupName, currentViewport, children }) => {
11
+ const CollapsibleVisualizationRow: React.FC<CollapsableVizRow> = ({
12
+ allExpanded,
13
+ fontSize,
14
+ groupName,
15
+ currentViewport,
16
+ children
17
+ }) => {
12
18
  const [isExpanded, setIsExpanded] = useState(allExpanded)
13
19
  const fontSizes = { small: 16, medium: 18, large: 20 }
14
- const titleFontSize = ['sm', 'xs', 'xxs'].includes(currentViewport) ? '13px' : `${fontSizes[fontSize]}px`
20
+ const titleFontSize = ['xs', 'xxs'].includes(currentViewport) ? '13px' : `${fontSizes[fontSize]}px`
15
21
 
16
22
  useEffect(() => {
17
23
  setIsExpanded(allExpanded)
@@ -6,6 +6,8 @@ import { FILTER_STYLE } from '../../types/FilterStyles'
6
6
  import { NestedOptions, ValueTextPair } from '@cdc/core/components/NestedDropdown/nestedDropdownHelpers'
7
7
  import NestedDropdown from '@cdc/core/components/NestedDropdown'
8
8
  import { MouseEventHandler } from 'react'
9
+ import Loader from '@cdc/core/components/Loader'
10
+ import _ from 'lodash'
9
11
 
10
12
  type DashboardFilterProps = {
11
13
  show: number[]
@@ -48,25 +50,31 @@ const DashboardFilters: React.FC<DashboardFilterProps> = ({
48
50
  <form className='d-flex flex-wrap'>
49
51
  {sharedFilters.map((filter, filterIndex) => {
50
52
  const urlFilterType = filter.type === 'urlfilter'
53
+ const label = filter.key
54
+
51
55
  if (
52
56
  (!urlFilterType && !filter.showDropdown && filter.filterStyle !== FILTER_STYLE.nestedDropdown) ||
53
57
  (show && !show.includes(filterIndex))
54
58
  )
55
- return <React.Fragment key={`${filter.key}-filtersection-${filterIndex}-option`} />
59
+ return <React.Fragment key={`${label}-filtersection-${filterIndex}-option`} />
56
60
  const values: JSX.Element[] = []
57
61
 
58
- if (filter.resetLabel) {
59
- values.push(
60
- <option key={`${filter.resetLabel}-option`} value={filter.resetLabel}>
61
- {filter.resetLabel}
62
- </option>
63
- )
64
- }
65
-
66
62
  const _key = filter.apiFilter?.apiEndpoint
63
+ const loading = apiFilterDropdowns[_key] === null
67
64
 
68
65
  const multiValues: { value; label }[] = []
66
+ const nestedOptions: NestedOptions = Object.entries(filter?.subGrouping?.valuesLookup || {}).map(
67
+ ([key, data]) => [
68
+ [key, key], // Main option: [value, text]
69
+ Array.isArray(data?.values) ? data.values.map(value => [value, value]) : [] // Ensure `values` is an array
70
+ ]
71
+ )
69
72
 
73
+ const activeSubGroupValue = _.get(
74
+ filter?.subGrouping?.valuesLookup,
75
+ [filter?.active as string, 'values', 0],
76
+ null // Default to null if the path is invalid
77
+ )
70
78
  if (_key && apiFilterDropdowns[_key]) {
71
79
  // URL Filter
72
80
  if (filter.filterStyle !== FILTER_STYLE.nestedDropdown) {
@@ -83,64 +91,104 @@ const DashboardFilters: React.FC<DashboardFilterProps> = ({
83
91
  // Data Filter
84
92
  filter.values?.forEach((filterOption, index) => {
85
93
  const labeledOpt = filter.labels && filter.labels[filterOption]
86
- values.push(
87
- <option key={`${filter.key}-option-${index}`} value={filterOption}>
88
- {labeledOpt || filterOption}
89
- </option>
90
- )
94
+ const resetLabelHasMatch = (filterOption || labeledOpt) === filter.resetLabel
95
+
96
+ if (!resetLabelHasMatch) {
97
+ values.push(
98
+ <option key={`${filter.key}-option-${index}`} value={filterOption}>
99
+ {labeledOpt || filterOption}
100
+ </option>
101
+ )
102
+ } else {
103
+ // add label to the front of list if it matches with reset label
104
+ values.unshift(
105
+ <option key={`${filter.key}-option-${index}`} value={filterOption}>
106
+ {labeledOpt || filterOption}
107
+ </option>
108
+ )
109
+ }
110
+
91
111
  multiValues.push({ value: filterOption, label: labeledOpt || filterOption })
92
112
  })
93
113
  }
94
114
 
95
- return filter.filterStyle === FILTER_STYLE.multiSelect ? (
96
- <div className='form-group mr-3 mb-1' key={`${filter.key}-filtersection-${filterIndex}`}>
97
- <MultiSelect
98
- label={filter.key}
99
- options={multiValues}
100
- fieldName={filterIndex}
101
- updateField={updateField}
102
- selected={filter.active as string[]}
103
- limit={filter.selectLimit || 5}
104
- />
105
- </div>
106
- ) : filter.filterStyle === FILTER_STYLE.nestedDropdown ? (
107
- <div className='form-group mr-3 mb-1' key={`${filter.key}-filtersection-${filterIndex}`}>
108
- <NestedDropdown
109
- activeGroup={filter.active as string}
110
- activeSubGroup={filter.subGrouping?.active}
111
- filterIndex={filterIndex}
112
- options={getNestedDropdownOptions(apiFilterDropdowns[_key])}
113
- listLabel={filter.key}
114
- handleSelectedItems={value => updateField(null, null, filterIndex, value)}
115
- />
116
- </div>
117
- ) : (
118
- <div className='form-group mr-3 mb-1' key={`${filter.key}-filtersection-${filterIndex}`}>
119
- <label className='text-capitalize font-weight-bold' htmlFor={`filter-${filterIndex}`}>
120
- {filter.key}
121
- </label>
122
- <select
123
- id={`filter-${filterIndex}`}
124
- className='cove-form-select'
125
- data-index='0'
126
- value={filter.queuedActive || filter.active}
127
- onChange={val => {
128
- handleOnChange(filterIndex, val.target.value)
129
- }}
130
- disabled={values.length === 1 && !nullVal(filter)}
131
- >
132
- {nullVal(filter) && (
133
- <option key={`select`} value=''>
134
- {filter.resetLabel || '- Select -'}
135
- </option>
136
- )}
137
- {values}
138
- </select>
115
+ const isDisabled = !values.length
116
+ // push reset label only if it does not includes in filter values options
117
+ if (filter.resetLabel && !filter.values.includes(filter.resetLabel)) {
118
+ values.unshift(
119
+ <option key={`${filter.resetLabel}-option`} value={filter.resetLabel}>
120
+ {filter.resetLabel}
121
+ </option>
122
+ )
123
+ }
124
+
125
+ const formGroupClass = `form-group mr-3 mb-1${loading ? ' loading-filter' : ''}`
126
+
127
+ return (
128
+ <div className={formGroupClass} key={`${label}-filtersection-${filterIndex}`}>
129
+ {label && (
130
+ <label className='font-weight-bold mt-1 mb-0' htmlFor={`filter-${filterIndex}`}>
131
+ {label}
132
+ </label>
133
+ )}
134
+ {filter.filterStyle === FILTER_STYLE.multiSelect ? (
135
+ <MultiSelect
136
+ label={label}
137
+ options={multiValues}
138
+ fieldName={filterIndex}
139
+ updateField={updateField}
140
+ selected={filter.active as string[]}
141
+ limit={filter.selectLimit || 5}
142
+ loading={loading}
143
+ />
144
+ ) : filter.filterStyle === FILTER_STYLE.nestedDropdown ? (
145
+ <NestedDropdown
146
+ activeGroup={filter.active as string}
147
+ activeSubGroup={_key ? filter.subGrouping?.active : activeSubGroupValue}
148
+ filterIndex={filterIndex}
149
+ options={_key ? getNestedDropdownOptions(apiFilterDropdowns[_key]) : nestedOptions}
150
+ listLabel={label}
151
+ handleSelectedItems={value => updateField(null, null, filterIndex, value)}
152
+ loading={loading}
153
+ />
154
+ ) : (
155
+ <>
156
+ <select
157
+ id={`filter-${filterIndex}`}
158
+ className='cove-form-select'
159
+ data-index='0'
160
+ value={loading ? 'Loading...' : filter.queuedActive || filter.active}
161
+ onChange={val => {
162
+ handleOnChange(filterIndex, val.target.value)
163
+ }}
164
+ disabled={loading || isDisabled}
165
+ >
166
+ {loading && <option value='Loading...'>Loading...</option>}
167
+ {nullVal(filter) && (
168
+ <option key={`select`} value=''>
169
+ {filter.resetLabel || '- Select -'}
170
+ </option>
171
+ )}
172
+ {values}
173
+ </select>
174
+ {loading && <Loader spinnerType={'text-secondary'} />}
175
+ </>
176
+ )}
139
177
  </div>
140
178
  )
141
179
  })}
142
180
  {showSubmit && (
143
- <button className='btn btn-primary mb-1' onClick={applyFilters}>
181
+ <button
182
+ className='btn btn-primary mb-1'
183
+ onClick={applyFilters}
184
+ disabled={show.some(filterIndex => {
185
+ const emptyFilterValues = [undefined, '', '- Select -']
186
+ return (
187
+ emptyFilterValues.includes(sharedFilters[filterIndex].queuedActive) &&
188
+ emptyFilterValues.includes(sharedFilters[filterIndex].active)
189
+ )
190
+ })}
191
+ >
144
192
  {applyFiltersButtonText || 'GO!'}
145
193
  </button>
146
194
  )}
@@ -274,7 +274,9 @@ const DashboardFiltersEditor: React.FC<DashboardFitlersEditorProps> = ({ vizConf
274
274
  </select>
275
275
  </label>
276
276
  ) : (
277
+
277
278
  <button onClick={() => setCanAddExisting(true)} className='btn btn-primary full-width mt-2'>
279
+
278
280
  Add Existing Dashboard Filter
279
281
  </button>
280
282
  )}