@cdc/dashboard 4.26.3 → 4.26.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/CONFIG.md +219 -0
  2. package/README.md +60 -20
  3. package/dist/cdcdashboard-CY9IcPSi.es.js +6 -0
  4. package/dist/cdcdashboard-DlpiY3fQ.es.js +4 -0
  5. package/dist/cdcdashboard.js +61559 -58048
  6. package/examples/__data__/data-2.json +6 -0
  7. package/examples/__data__/data.json +6 -0
  8. package/examples/dashboard-conditions-filters-incomplete.json +221 -0
  9. package/examples/dashboard-missing-datasets-multi.json +174 -0
  10. package/examples/dashboard-missing-datasets-single.json +121 -0
  11. package/examples/dashboard-multi-dashboard-version-regression.json +146 -0
  12. package/examples/dashboard-shared-filter-row-delete-cleanup.json +186 -0
  13. package/examples/dashboard-stale-dataset-keys.json +181 -0
  14. package/examples/dashboard-tiered-filter-regression.json +190 -0
  15. package/examples/legend-issue.json +1 -1
  16. package/examples/minimal-example.json +34 -0
  17. package/examples/private/cfa-dashboard.json +651 -0
  18. package/examples/private/data-bite-wrap.json +6936 -0
  19. package/examples/private/dengue.json +4640 -0
  20. package/examples/private/link_to_file.json +16662 -0
  21. package/examples/private/multi-dash-fix.json +16963 -0
  22. package/examples/private/versions.json +41612 -0
  23. package/examples/sankey.json +3 -3
  24. package/examples/test-api-filter-reset.json +4 -4
  25. package/examples/tp5-test.json +86 -4
  26. package/examples/us-map-filter-example.json +1074 -0
  27. package/package.json +9 -9
  28. package/src/CdcDashboard.tsx +6 -2
  29. package/src/CdcDashboardComponent.tsx +179 -88
  30. package/src/DashboardCopyPasteContext.test.tsx +33 -0
  31. package/src/DashboardCopyPasteContext.tsx +48 -0
  32. package/src/_stories/Dashboard.EditorRegression.stories.tsx +72 -0
  33. package/src/_stories/Dashboard.Regression.stories.tsx +196 -0
  34. package/src/_stories/Dashboard.Zoom.stories.tsx +88 -0
  35. package/src/_stories/Dashboard.smoke.stories.tsx +33 -0
  36. package/src/_stories/Dashboard.stories.tsx +337 -2
  37. package/src/_stories/FilteredTextMigrationComparison.stories.tsx +87 -0
  38. package/src/_stories/_mock/dashboard-data-driven-colors.json +171 -0
  39. package/src/_stories/_mock/tp5-test.json +86 -5
  40. package/src/components/Column.test.tsx +176 -0
  41. package/src/components/Column.tsx +214 -13
  42. package/src/components/DashboardConditionModal.test.tsx +420 -0
  43. package/src/components/DashboardConditionModal.tsx +367 -0
  44. package/src/components/DashboardConditionSummary.tsx +59 -0
  45. package/src/components/DashboardEditors.tsx +23 -0
  46. package/src/components/DashboardFilters/DashboardFilters.test.tsx +267 -0
  47. package/src/components/DashboardFilters/DashboardFilters.tsx +193 -172
  48. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.test.tsx +164 -0
  49. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +46 -6
  50. package/src/components/DashboardFilters/DashboardFiltersEditor/components/APIModal.tsx +5 -3
  51. package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +59 -58
  52. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.test.tsx +304 -0
  53. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +43 -36
  54. package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +2 -2
  55. package/src/components/DashboardFilters/DashboardFiltersWrapper.test.tsx +142 -0
  56. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +32 -27
  57. package/src/components/DashboardFilters/dashboardfilter.styles.css +42 -27
  58. package/src/components/DataDesignerModal.tsx +2 -1
  59. package/src/components/ExpandCollapseButtons.tsx +6 -4
  60. package/src/components/Grid.tsx +12 -7
  61. package/src/components/Header/Header.tsx +36 -17
  62. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +141 -140
  63. package/src/components/Row.test.tsx +228 -0
  64. package/src/components/Row.tsx +104 -28
  65. package/src/components/VisualizationRow.test.tsx +396 -0
  66. package/src/components/VisualizationRow.tsx +177 -51
  67. package/src/components/VisualizationsPanel/VisualizationsPanel.test.tsx +49 -0
  68. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +14 -13
  69. package/src/components/Widget/Widget.test.tsx +218 -0
  70. package/src/components/Widget/Widget.tsx +123 -20
  71. package/src/components/Widget/widget.styles.css +58 -14
  72. package/src/components/dashboard-condition-modal.css +76 -0
  73. package/src/components/dashboard-condition-summary.css +87 -0
  74. package/src/data/initial-state.js +1 -0
  75. package/src/helpers/addValuesToDashboardFilters.ts +3 -5
  76. package/src/helpers/addVisualization.ts +17 -4
  77. package/src/helpers/cloneDashboardWidget.ts +127 -0
  78. package/src/helpers/dashboardColumnWidgets.ts +99 -0
  79. package/src/helpers/dashboardConditionUi.ts +47 -0
  80. package/src/helpers/dashboardConditions.ts +200 -0
  81. package/src/helpers/dashboardFilterTargets.ts +156 -0
  82. package/src/helpers/filterData.ts +4 -9
  83. package/src/helpers/filterVisibility.ts +20 -0
  84. package/src/helpers/formatConfigBeforeSave.ts +2 -2
  85. package/src/helpers/getFilteredData.ts +18 -5
  86. package/src/helpers/getUpdateConfig.ts +43 -12
  87. package/src/helpers/getVizRowColumnLocator.ts +11 -1
  88. package/src/helpers/iconHash.tsx +9 -3
  89. package/src/helpers/mapDataToConfig.ts +31 -29
  90. package/src/helpers/reloadURLHelpers.ts +25 -5
  91. package/src/helpers/removeDashboardFilter.ts +33 -33
  92. package/src/helpers/tests/addVisualization.test.ts +53 -9
  93. package/src/helpers/tests/cloneDashboardWidget.test.ts +136 -0
  94. package/src/helpers/tests/dashboardColumnWidgets.test.ts +99 -0
  95. package/src/helpers/tests/dashboardConditionUi.test.ts +41 -0
  96. package/src/helpers/tests/dashboardConditions.test.ts +428 -0
  97. package/src/helpers/tests/formatConfigBeforeSave.test.ts +51 -0
  98. package/src/helpers/tests/getFilteredData.test.ts +265 -86
  99. package/src/helpers/tests/getUpdateConfig.test.ts +338 -0
  100. package/src/helpers/tests/reloadURLHelpers.test.ts +394 -238
  101. package/src/index.tsx +6 -3
  102. package/src/scss/grid.scss +281 -22
  103. package/src/scss/main.scss +215 -64
  104. package/src/store/dashboard.actions.ts +17 -4
  105. package/src/store/dashboard.reducer.test.ts +538 -0
  106. package/src/store/dashboard.reducer.ts +136 -22
  107. package/src/test/CdcDashboard.test.jsx +24 -0
  108. package/src/test/CdcDashboard.test.tsx +148 -0
  109. package/src/test/CdcDashboardComponent.test.tsx +935 -2
  110. package/src/types/ConfigRow.ts +15 -0
  111. package/src/types/DashboardFilters.ts +4 -0
  112. package/src/types/SharedFilter.ts +2 -0
  113. package/tests/fixtures/dashboard-config-with-metadata.json +1 -1
  114. package/dist/cdcdashboard-vr9HZwRt.es.js +0 -6
  115. package/examples/DEV-6574.json +0 -2224
  116. package/examples/api-dashboard-data.json +0 -272
  117. package/examples/api-dashboard-years.json +0 -11
  118. package/examples/api-geographies-data.json +0 -11
  119. package/examples/chart-data.json +0 -5409
  120. package/examples/custom/css/respiratory.css +0 -236
  121. package/examples/custom/js/respiratory.js +0 -242
  122. package/examples/default-data.json +0 -368
  123. package/examples/default-filter-control.json +0 -209
  124. package/examples/default-multi-dataset-shared-filter.json +0 -1729
  125. package/examples/default-multi-dataset.json +0 -506
  126. package/examples/ed-visits-county-file.json +0 -402
  127. package/examples/filters/Alabama.json +0 -72
  128. package/examples/filters/Alaska.json +0 -1737
  129. package/examples/filters/Arkansas.json +0 -4713
  130. package/examples/filters/California.json +0 -212
  131. package/examples/filters/Colorado.json +0 -1500
  132. package/examples/filters/Connecticut.json +0 -559
  133. package/examples/filters/Delaware.json +0 -63
  134. package/examples/filters/DistrictofColumbia.json +0 -63
  135. package/examples/filters/Florida.json +0 -4217
  136. package/examples/filters/States.json +0 -146
  137. package/examples/state-level.json +0 -90136
  138. package/examples/state-points.json +0 -10474
  139. package/examples/temp-example-data.json +0 -130
  140. package/examples/test-dashboard-simple.json +0 -503
  141. package/examples/test-example.json +0 -752
  142. package/examples/test-file.json +0 -147
  143. package/examples/test.json +0 -752
  144. package/examples/testing.json +0 -94456
  145. /package/examples/{data → __data__}/data-with-metadata.json +0 -0
  146. /package/examples/{legend-issue-data.json → __data__/legend-issue-data.json} +0 -0
  147. /package/examples/api-test/{categories.json → __data__/categories.json} +0 -0
  148. /package/examples/api-test/{chart-data.json → __data__/chart-data.json} +0 -0
  149. /package/examples/api-test/{topics.json → __data__/topics.json} +0 -0
  150. /package/examples/api-test/{years.json → __data__/years.json} +0 -0
  151. /package/src/_stories/{Dashboard.Pages.stories.tsx → Dashboard.Pages.smoke.stories.tsx} +0 -0
@@ -0,0 +1,267 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+ import DashboardFilters from './DashboardFilters'
4
+
5
+ vi.mock('@cdc/core/components/ui/Icon', () => ({
6
+ default: props => <span data-testid='mock-icon' {...props} />
7
+ }))
8
+
9
+ const createDataBackedFilter = (displaySubgroupingOnly = false) =>
10
+ ({
11
+ key: 'Year and Quarter',
12
+ type: 'datafilter',
13
+ filterStyle: 'nested-dropdown',
14
+ showDropdown: true,
15
+ values: ['2023', '2024'],
16
+ columnName: 'year',
17
+ id: 0,
18
+ parents: [],
19
+ order: 'asc',
20
+ active: '2023',
21
+ displaySubgroupingOnly,
22
+ subGrouping: {
23
+ columnName: 'quarter',
24
+ active: 'Q2',
25
+ valuesLookup: {
26
+ '2023': { values: ['Q1', 'Q2'] },
27
+ '2024': { values: ['Q3', 'Q4'] }
28
+ }
29
+ }
30
+ } as any)
31
+
32
+ const createApiBackedFilter = (displaySubgroupingOnly = false) =>
33
+ ({
34
+ key: 'API Year and Quarter',
35
+ type: 'urlfilter',
36
+ filterStyle: 'nested-dropdown',
37
+ showDropdown: true,
38
+ values: [],
39
+ columnName: 'year',
40
+ id: 0,
41
+ parents: [],
42
+ order: 'asc',
43
+ active: '2023',
44
+ displaySubgroupingOnly,
45
+ apiFilter: {
46
+ apiEndpoint: '/api/nested-options',
47
+ valueSelector: 'year',
48
+ subgroupValueSelector: 'quarter'
49
+ },
50
+ subGrouping: {
51
+ columnName: 'quarter',
52
+ active: 'Q2',
53
+ valuesLookup: {}
54
+ }
55
+ } as any)
56
+
57
+ const apiFilterDropdowns = {
58
+ '/api/nested-options': [
59
+ {
60
+ value: '2023',
61
+ text: '2023',
62
+ subOptions: [
63
+ { value: 'Q1', text: 'Q1' },
64
+ { value: 'Q2', text: 'Q2' }
65
+ ]
66
+ },
67
+ {
68
+ value: '2024',
69
+ text: '2024',
70
+ subOptions: [
71
+ { value: 'Q3', text: 'Q3' },
72
+ { value: 'Q4', text: 'Q4' }
73
+ ]
74
+ }
75
+ ]
76
+ }
77
+
78
+ describe('DashboardFilters nested dropdown display', () => {
79
+ it.each([
80
+ ['data-backed', createDataBackedFilter(false), {}, '2023 - Q2'],
81
+ ['data-backed subgroup only', createDataBackedFilter(true), {}, 'Q2'],
82
+ ['api-backed', createApiBackedFilter(false), apiFilterDropdowns, '2023 - Q2'],
83
+ ['api-backed subgroup only', createApiBackedFilter(true), apiFilterDropdowns, 'Q2']
84
+ ])('shows the expected closed text for %s filters', (_label, filter, dropdowns, expectedValue) => {
85
+ const { container } = render(
86
+ <DashboardFilters
87
+ applyFilters={vi.fn()}
88
+ apiFilterDropdowns={dropdowns as any}
89
+ filters={[filter]}
90
+ handleOnChange={vi.fn()}
91
+ show={[0]}
92
+ showSubmit={false}
93
+ />
94
+ )
95
+
96
+ const input = container.querySelector('.nested-dropdown input')
97
+ expect(input).toHaveValue(expectedValue)
98
+ })
99
+
100
+ it.each([
101
+ ['data-backed', createDataBackedFilter(false), {}],
102
+ ['data-backed subgroup only', createDataBackedFilter(true), {}],
103
+ ['api-backed', createApiBackedFilter(false), apiFilterDropdowns],
104
+ ['api-backed subgroup only', createApiBackedFilter(true), apiFilterDropdowns]
105
+ ])('keeps nested dropdown selection behavior unchanged for %s filters', (_label, filter, dropdowns) => {
106
+ const handleOnChange = vi.fn()
107
+ const { container, getByText, queryByText } = render(
108
+ <DashboardFilters
109
+ applyFilters={vi.fn()}
110
+ apiFilterDropdowns={dropdowns as any}
111
+ filters={[filter]}
112
+ handleOnChange={handleOnChange}
113
+ show={[0]}
114
+ showSubmit={false}
115
+ />
116
+ )
117
+
118
+ const input = container.querySelector('.nested-dropdown input') as HTMLInputElement
119
+ fireEvent.focus(input)
120
+ fireEvent.change(input, { target: { value: 'Q3' } })
121
+
122
+ expect(getByText('2024')).toBeInTheDocument()
123
+ expect(queryByText('2023')).not.toBeInTheDocument()
124
+
125
+ fireEvent.click(getByText('Q3'))
126
+
127
+ expect(handleOnChange).toHaveBeenCalledWith(0, ['2024', 'Q3'])
128
+ })
129
+ })
130
+
131
+ describe('DashboardFilters layout', () => {
132
+ const createDropdownFilter = () =>
133
+ ({
134
+ key: 'State',
135
+ type: 'datafilter',
136
+ filterStyle: 'dropdown',
137
+ showDropdown: true,
138
+ values: ['Alabama', 'Alaska'],
139
+ columnName: 'state',
140
+ id: 0,
141
+ parents: [],
142
+ order: 'asc',
143
+ active: 'Alabama'
144
+ } as any)
145
+
146
+ it('keeps intro text outside the gapped controls form', () => {
147
+ const { container } = render(
148
+ <DashboardFilters
149
+ applyFilters={vi.fn()}
150
+ apiFilterDropdowns={{}}
151
+ filterIntro='Choose a <strong>state</strong>.'
152
+ filters={[createDropdownFilter()]}
153
+ handleOnChange={vi.fn()}
154
+ show={[0]}
155
+ showSubmit={true}
156
+ />
157
+ )
158
+
159
+ const intro = container.querySelector('.filters-section__intro-text')
160
+ const form = container.querySelector('.dashboard-filters__form')
161
+
162
+ expect(intro).toBeInTheDocument()
163
+ expect(intro?.querySelector('strong')).toHaveTextContent('state')
164
+ expect(form).toBeInTheDocument()
165
+ expect(form).not.toContainElement(intro as Element)
166
+ expect(form?.querySelector('.dashboard-filters__field')).toBeInTheDocument()
167
+ expect(form?.querySelector('.dashboard-filters__actions')).toBeInTheDocument()
168
+ expect(intro?.compareDocumentPosition(form as Element) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
169
+ })
170
+ })
171
+
172
+ describe('DashboardFilters filter notes', () => {
173
+ const createDropdownFilter = (note?: string) =>
174
+ ({
175
+ key: 'State',
176
+ type: 'datafilter',
177
+ filterStyle: 'dropdown',
178
+ showDropdown: true,
179
+ values: ['Alabama', 'Alaska'],
180
+ columnName: 'state',
181
+ id: 0,
182
+ parents: [],
183
+ order: 'asc',
184
+ active: 'Alabama',
185
+ note
186
+ } as any)
187
+
188
+ const renderDashboardFilterList = (filters, show = filters.map((_filter, index) => index)) =>
189
+ render(
190
+ <DashboardFilters
191
+ applyFilters={vi.fn()}
192
+ apiFilterDropdowns={{}}
193
+ filters={filters}
194
+ handleOnChange={vi.fn()}
195
+ show={show}
196
+ showSubmit={false}
197
+ />
198
+ )
199
+ const renderDashboardFilters = filter => renderDashboardFilterList([filter])
200
+
201
+ it('renders parsed HTML notes under the label and before dropdown controls', () => {
202
+ const { container } = renderDashboardFilters(createDropdownFilter('Choose a <strong>state</strong>.'))
203
+
204
+ const label = screen.getByText('State')
205
+ const note = container.querySelector('.filters-section__note-text')
206
+ const select = screen.getByLabelText('State')
207
+
208
+ expect(note).toBeInTheDocument()
209
+ expect(note).toHaveTextContent('Choose a state.')
210
+ expect(note?.querySelector('strong')).toHaveTextContent('state')
211
+ expect(label.compareDocumentPosition(note as Element) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
212
+ expect(note?.compareDocumentPosition(select) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
213
+ expect(select).toHaveClass('filters-section__select--fit-content')
214
+ expect(select).not.toHaveClass('w-100')
215
+ })
216
+
217
+ it('renders notes for tab-simple filters above the tab control', () => {
218
+ const { container } = renderDashboardFilters({
219
+ ...createDropdownFilter('Pick <em>status</em>.'),
220
+ key: 'Status',
221
+ filterStyle: 'tab-simple',
222
+ values: ['Current', 'Prior'],
223
+ active: 'Current'
224
+ })
225
+
226
+ const note = container.querySelector('.filters-section__note-text')
227
+ const tab = screen.getByRole('button', { name: 'Current' })
228
+
229
+ expect(note).toHaveTextContent('Pick status.')
230
+ expect(note?.querySelector('em')).toHaveTextContent('status')
231
+ expect(note?.compareDocumentPosition(tab) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
232
+ })
233
+
234
+ it('does not render note markup for empty notes', () => {
235
+ const { container } = renderDashboardFilters(createDropdownFilter(' '))
236
+ const select = screen.getByLabelText('State')
237
+
238
+ expect(container.querySelector('.filters-section__note-text')).not.toBeInTheDocument()
239
+ expect(select).toHaveClass('w-100')
240
+ expect(select).not.toHaveClass('filters-section__select--fit-content')
241
+ })
242
+
243
+ it('marks the form as single-filter layout when only one dashboard filter is visible', () => {
244
+ const hiddenFilter = { ...createDropdownFilter(), key: 'Hidden State', showDropdown: false }
245
+ const { container } = renderDashboardFilterList([createDropdownFilter('Choose a state.'), hiddenFilter])
246
+
247
+ const form = container.querySelector('.dashboard-filters__form')
248
+
249
+ expect(form).toHaveClass('filters-section__wrapper--single')
250
+ expect(form).not.toHaveClass('filters-section__wrapper--multiple')
251
+ })
252
+
253
+ it('marks the form as multiple-filter layout when more than one dashboard filter is visible', () => {
254
+ const statusFilter = {
255
+ ...createDropdownFilter('Choose a status.'),
256
+ key: 'Status',
257
+ columnName: 'status',
258
+ active: 'Current'
259
+ }
260
+ const { container } = renderDashboardFilterList([createDropdownFilter('Choose a state.'), statusFilter])
261
+
262
+ const form = container.querySelector('.dashboard-filters__form')
263
+
264
+ expect(form).toHaveClass('filters-section__wrapper--multiple')
265
+ expect(form).not.toHaveClass('filters-section__wrapper--single')
266
+ })
267
+ })
@@ -9,9 +9,14 @@ import NestedDropdown from '@cdc/core/components/NestedDropdown'
9
9
  import { getNestedOptions } from '@cdc/core/components/Filters/helpers/getNestedOptions'
10
10
  import { MouseEventHandler } from 'react'
11
11
  import Loader from '@cdc/core/components/Loader'
12
+ import Button from '@cdc/core/components/elements/Button'
12
13
  import _ from 'lodash'
13
- import { DROPDOWN_STYLES } from '@cdc/core/components/Filters/components/Dropdown'
14
+ import { getDropdownStyles } from '@cdc/core/components/Filters/components/Dropdown'
14
15
  import Tabs from '@cdc/core/components/Filters/components/Tabs'
16
+ import FilterNote from '@cdc/core/components/Filters/components/FilterNote'
17
+ import parse from 'html-react-parser'
18
+ import './dashboardfilter.styles.css'
19
+ import { isVisibleDashboardFilter } from '../../helpers/filterVisibility'
15
20
 
16
21
  type DashboardFilterProps = {
17
22
  show: number[]
@@ -19,6 +24,7 @@ type DashboardFilterProps = {
19
24
  apiFilterDropdowns: APIFilterDropdowns
20
25
  handleOnChange: (index: number, value: string | string[]) => void
21
26
  showSubmit: boolean
27
+ filterIntro?: string
22
28
  applyFilters: MouseEventHandler<HTMLButtonElement>
23
29
  applyFiltersButtonText?: string
24
30
  handleReset?: MouseEventHandler<HTMLButtonElement>
@@ -30,6 +36,7 @@ const DashboardFilters: React.FC<DashboardFilterProps> = ({
30
36
  apiFilterDropdowns,
31
37
  handleOnChange,
32
38
  showSubmit,
39
+ filterIntro,
33
40
  applyFilters,
34
41
  applyFiltersButtonText,
35
42
  handleReset
@@ -57,184 +64,198 @@ const DashboardFilters: React.FC<DashboardFilterProps> = ({
57
64
  ])
58
65
  }
59
66
 
67
+ const visibleFilterIndexes = show.filter(filterIndex => isVisibleDashboardFilter(sharedFilters[filterIndex]))
68
+ const formClasses = [
69
+ 'dashboard-filters__form',
70
+ 'filters-section__wrapper',
71
+ visibleFilterIndexes.length > 1 ? 'filters-section__wrapper--multiple' : 'filters-section__wrapper--single'
72
+ ]
73
+
60
74
  return (
61
- <form className='d-flex flex-wrap'>
62
- {show.map(filterIndex => {
63
- const filter = sharedFilters[filterIndex]
64
- const urlFilterType = filter.type === 'urlfilter'
65
- const label = stripDuplicateLabelIncrement(filter.key || '')
66
-
67
- if (
68
- !urlFilterType &&
69
- !filter.showDropdown &&
70
- filter.filterStyle !== FILTER_STYLE.nestedDropdown &&
71
- filter.filterStyle !== FILTER_STYLE.tabSimple
72
- )
73
- return <React.Fragment key={`${filter.key}-filtersection-${filterIndex}-option`} />
74
- const values: JSX.Element[] = []
75
-
76
- const _key = filter.apiFilter?.apiEndpoint
77
- const loading = apiFilterDropdowns[_key] === null
78
-
79
- const multiValues: { value; label }[] = []
80
- const nestedOptions: NestedOptions = getNestedOptions({
81
- orderedValues: filter.orderedValues,
82
- values: filter.values,
83
- subGrouping: filter.subGrouping
84
- })
85
-
86
- if (_key && apiFilterDropdowns[_key]) {
87
- // URL Filter
88
- if (filter.filterStyle !== FILTER_STYLE.nestedDropdown) {
89
- apiFilterDropdowns[_key].forEach(({ text, value }, index) => {
90
- values.push(
91
- <option key={`${value}-option-${index}`} value={value}>
92
- {text}
93
- </option>
94
- )
95
- multiValues.push({ value, label: text })
75
+ <>
76
+ {filterIntro && <p className='filters-section__intro-text cove-prose mb-3 w-100'>{parse(filterIntro)}</p>}
77
+ <form className={formClasses.join(' ')}>
78
+ {show.map(filterIndex => {
79
+ const filter = sharedFilters[filterIndex]
80
+
81
+ if (!isVisibleDashboardFilter(filter))
82
+ return <React.Fragment key={`${filter?.key || 'missing'}-filtersection-${filterIndex}-option`} />
83
+
84
+ const label = stripDuplicateLabelIncrement(filter.key || '')
85
+ const values: JSX.Element[] = []
86
+
87
+ const _key = filter.apiFilter?.apiEndpoint
88
+ const loading = apiFilterDropdowns[_key] === null
89
+
90
+ const multiValues: { value; label }[] = []
91
+ const nestedOptions: NestedOptions = getNestedOptions({
92
+ orderedValues: filter.orderedValues,
93
+ values: filter.values,
94
+ subGrouping: filter.subGrouping
95
+ })
96
+
97
+ if (_key && apiFilterDropdowns[_key]) {
98
+ // URL Filter
99
+ if (filter.filterStyle !== FILTER_STYLE.nestedDropdown) {
100
+ apiFilterDropdowns[_key].forEach(({ text, value }, index) => {
101
+ values.push(
102
+ <option key={`${value}-option-${index}`} value={value}>
103
+ {text}
104
+ </option>
105
+ )
106
+ multiValues.push({ value, label: text })
107
+ })
108
+ }
109
+ } else {
110
+ // Data Filter
111
+ const orderedFilterValues = filter.orderedValues || filter.values
112
+ orderedFilterValues?.forEach((filterOption, index) => {
113
+ const labeledOpt = filter.labels && filter.labels[filterOption]
114
+ const resetLabelHasMatch = (filterOption || labeledOpt) === filter.resetLabel
115
+
116
+ if (!resetLabelHasMatch) {
117
+ values.push(
118
+ <option key={`${filter.key}-option-${index}`} value={filterOption}>
119
+ {labeledOpt || filterOption}
120
+ </option>
121
+ )
122
+ } else {
123
+ // add label to the front of list if it matches with reset label
124
+ values.unshift(
125
+ <option key={`${filter.key}-option-${index}`} value={filterOption}>
126
+ {labeledOpt || filterOption}
127
+ </option>
128
+ )
129
+ }
130
+
131
+ multiValues.push({ value: filterOption, label: labeledOpt || filterOption })
96
132
  })
97
133
  }
98
- } else {
99
- // Data Filter
100
- const orderedFilterValues = filter.orderedValues || filter.values
101
- orderedFilterValues?.forEach((filterOption, index) => {
102
- const labeledOpt = filter.labels && filter.labels[filterOption]
103
- const resetLabelHasMatch = (filterOption || labeledOpt) === filter.resetLabel
104
-
105
- if (!resetLabelHasMatch) {
106
- values.push(
107
- <option key={`${filter.key}-option-${index}`} value={filterOption}>
108
- {labeledOpt || filterOption}
109
- </option>
110
- )
111
- } else {
112
- // add label to the front of list if it matches with reset label
113
- values.unshift(
114
- <option key={`${filter.key}-option-${index}`} value={filterOption}>
115
- {labeledOpt || filterOption}
116
- </option>
117
- )
118
- }
119
134
 
120
- multiValues.push({ value: filterOption, label: labeledOpt || filterOption })
121
- })
122
- }
123
-
124
- const isDisabled = !values.length
125
- // push reset label only if it does not includes in filter values options
126
- if (filter.resetLabel && !filter.values.includes(filter.resetLabel) && !_key) {
127
- values.unshift(
128
- <option key={`${filter.resetLabel}-option`} value={filter.resetLabel}>
129
- {filter.resetLabel}
130
- </option>
135
+ const isDisabled = !values.length
136
+ // push reset label only if it does not includes in filter values options
137
+ if (filter.resetLabel && !filter.values.includes(filter.resetLabel) && !_key) {
138
+ values.unshift(
139
+ <option key={`${filter.resetLabel}-option`} value={filter.resetLabel}>
140
+ {filter.resetLabel}
141
+ </option>
142
+ )
143
+ }
144
+
145
+ const isTabSimple = filter.filterStyle === FILTER_STYLE.tabSimple
146
+ const dropdownStyles = getDropdownStyles(Boolean(filter.note?.trim()))
147
+ const formGroupClass = [
148
+ 'dashboard-filters__field',
149
+ 'form-group',
150
+ loading ? 'loading-filter' : '',
151
+ isTabSimple ? 'w-100' : ''
152
+ ]
153
+ .filter(Boolean)
154
+ .join(' ')
155
+ return (
156
+ <div className={formGroupClass} key={`${filter.key}-filtersection-${filterIndex}`}>
157
+ {label && (
158
+ <label className='font-weight-bold mb-2' htmlFor={`filter-${filterIndex}`}>
159
+ {label}
160
+ </label>
161
+ )}
162
+ <FilterNote note={filter.note} />
163
+ {filter.filterStyle === FILTER_STYLE.tabSimple ? (
164
+ <Tabs
165
+ filter={filter}
166
+ index={filterIndex}
167
+ changeFilterActive={(index, value) => handleOnChange(index, value)}
168
+ loading={loading}
169
+ />
170
+ ) : filter.filterStyle === FILTER_STYLE.multiSelect ? (
171
+ <MultiSelect
172
+ label={label}
173
+ options={multiValues}
174
+ fieldName={filterIndex}
175
+ updateField={updateField}
176
+ selected={filter.active as string[]}
177
+ limit={filter.selectLimit || 5}
178
+ loading={loading}
179
+ />
180
+ ) : filter.filterStyle === FILTER_STYLE.nestedDropdown ? (
181
+ <NestedDropdown
182
+ activeGroup={(filter.queuedActive?.[0] || filter.active) as string}
183
+ activeSubGroup={(filter.queuedActive?.[1] || filter.subGrouping?.active) as string}
184
+ displaySubgroupingOnly={filter.displaySubgroupingOnly}
185
+ filterIndex={filterIndex}
186
+ options={_key ? getNestedDropdownOptions(apiFilterDropdowns[_key]) : nestedOptions}
187
+ listLabel={label}
188
+ handleSelectedItems={value => updateField(null, null, filterIndex, value)}
189
+ loading={loading}
190
+ />
191
+ ) : filter.filterStyle === FILTER_STYLE.combobox ? (
192
+ <ComboBox
193
+ options={multiValues}
194
+ fieldName={filterIndex}
195
+ updateField={updateField}
196
+ selected={(filter.queuedActive || filter.active) as string}
197
+ label={label}
198
+ loading={loading}
199
+ placeholder={filter.resetLabel || '- Select -'}
200
+ />
201
+ ) : (
202
+ <>
203
+ <select
204
+ id={`filter-${filterIndex}`}
205
+ className={`cove-form-select ${dropdownStyles}`}
206
+ data-index='0'
207
+ value={loading ? 'Loading...' : filter.queuedActive || filter.active}
208
+ onChange={val => {
209
+ handleOnChange(filterIndex, val.target.value)
210
+ }}
211
+ disabled={loading || isDisabled}
212
+ >
213
+ {loading && <option value='Loading...'>Loading...</option>}
214
+ {/* For API filters, show placeholder when no value is selected */}
215
+ {_key && nullVal(filter) && (
216
+ <option key={`reset-label`} value=''>
217
+ {filter.resetLabel || '- Select One -'}
218
+ </option>
219
+ )}
220
+ {/* For non-API filters or when no value is selected, show empty option */}
221
+ {!_key && nullVal(filter) && (
222
+ <option key={`select`} value=''>
223
+ {filter.resetLabel || '- Select -'}
224
+ </option>
225
+ )}
226
+ {values}
227
+ </select>
228
+ {loading && <Loader spinnerType={'text-secondary'} />}
229
+ </>
230
+ )}
231
+ </div>
131
232
  )
132
- }
133
-
134
- const isTabSimple = filter.filterStyle === FILTER_STYLE.tabSimple
135
- const formGroupClass = `form-group${isTabSimple ? '' : ' me-4'} mb-1${loading ? ' loading-filter' : ''}${
136
- isTabSimple ? ' w-100' : ''
137
- }`
138
- return (
139
- <div className={formGroupClass} key={`${filter.key}-filtersection-${filterIndex}`}>
140
- {label && (
141
- <label className='font-weight-bold mb-2' htmlFor={`filter-${filterIndex}`}>
142
- {label}
143
- </label>
144
- )}
145
- {filter.filterStyle === FILTER_STYLE.tabSimple ? (
146
- <Tabs
147
- filter={filter}
148
- index={filterIndex}
149
- changeFilterActive={(index, value) => handleOnChange(index, value)}
150
- loading={loading}
151
- />
152
- ) : filter.filterStyle === FILTER_STYLE.multiSelect ? (
153
- <MultiSelect
154
- label={label}
155
- options={multiValues}
156
- fieldName={filterIndex}
157
- updateField={updateField}
158
- selected={filter.active as string[]}
159
- limit={filter.selectLimit || 5}
160
- loading={loading}
161
- />
162
- ) : filter.filterStyle === FILTER_STYLE.nestedDropdown ? (
163
- <NestedDropdown
164
- activeGroup={(filter.queuedActive?.[0] || filter.active) as string}
165
- activeSubGroup={(filter.queuedActive?.[1] || filter.subGrouping?.active) as string}
166
- filterIndex={filterIndex}
167
- options={_key ? getNestedDropdownOptions(apiFilterDropdowns[_key]) : nestedOptions}
168
- listLabel={label}
169
- handleSelectedItems={value => updateField(null, null, filterIndex, value)}
170
- loading={loading}
171
- />
172
- ) : filter.filterStyle === FILTER_STYLE.combobox ? (
173
- <ComboBox
174
- options={multiValues}
175
- fieldName={filterIndex}
176
- updateField={updateField}
177
- selected={(filter.queuedActive || filter.active) as string}
178
- label={label}
179
- loading={loading}
180
- placeholder={filter.resetLabel || '- Select -'}
181
- />
182
- ) : (
183
- <>
184
- <select
185
- id={`filter-${filterIndex}`}
186
- className={`cove-form-select ${DROPDOWN_STYLES}`}
187
- data-index='0'
188
- value={loading ? 'Loading...' : filter.queuedActive || filter.active}
189
- onChange={val => {
190
- handleOnChange(filterIndex, val.target.value)
191
- }}
192
- disabled={loading || isDisabled}
193
- >
194
- {loading && <option value='Loading...'>Loading...</option>}
195
- {/* For API filters, show placeholder when no value is selected */}
196
- {_key && nullVal(filter) && (
197
- <option key={`reset-label`} value=''>
198
- {filter.resetLabel || '- Select One -'}
199
- </option>
200
- )}
201
- {/* For non-API filters or when no value is selected, show empty option */}
202
- {!_key && nullVal(filter) && (
203
- <option key={`select`} value=''>
204
- {filter.resetLabel || '- Select -'}
205
- </option>
206
- )}
207
- {values}
208
- </select>
209
- {loading && <Loader spinnerType={'text-secondary'} />}
210
- </>
233
+ })}
234
+ {showSubmit && (
235
+ <div className='dashboard-filters__actions'>
236
+ <Button
237
+ variant='primary'
238
+ className='mb-1 me-2'
239
+ onClick={applyFilters}
240
+ disabled={visibleFilterIndexes.some(filterIndex => {
241
+ const emptyFilterValues = [undefined, '', '- Select -']
242
+ return (
243
+ emptyFilterValues.includes(sharedFilters[filterIndex]?.queuedActive) &&
244
+ emptyFilterValues.includes(sharedFilters[filterIndex]?.active)
245
+ )
246
+ })}
247
+ >
248
+ {applyFiltersButtonText || 'GO!'}
249
+ </Button>
250
+ {handleReset && (
251
+ <Button variant='link' className='mb-1' onClick={handleReset}>
252
+ Clear Filters
253
+ </Button>
211
254
  )}
212
255
  </div>
213
- )
214
- })}
215
- {showSubmit && (
216
- <>
217
- <button
218
- className='btn btn-primary mb-1 me-2'
219
- onClick={applyFilters}
220
- disabled={show.some(filterIndex => {
221
- const emptyFilterValues = [undefined, '', '- Select -']
222
- return (
223
- emptyFilterValues.includes(sharedFilters[filterIndex].queuedActive) &&
224
- emptyFilterValues.includes(sharedFilters[filterIndex].active)
225
- )
226
- })}
227
- >
228
- {applyFiltersButtonText || 'GO!'}
229
- </button>
230
- {handleReset && (
231
- <button className='btn btn-link mb-1' onClick={handleReset}>
232
- Clear Filters
233
- </button>
234
- )}
235
- </>
236
- )}
237
- </form>
256
+ )}
257
+ </form>
258
+ </>
238
259
  )
239
260
  }
240
261