@cdc/dashboard 4.26.4 → 4.26.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CONFIG.md +77 -30
  2. package/LICENSE +201 -0
  3. package/dist/cdcdashboard.js +49936 -49166
  4. package/examples/dashboard-conditions-filters-incomplete.json +221 -0
  5. package/examples/dashboard-missing-datasets-multi.json +174 -0
  6. package/examples/dashboard-missing-datasets-single.json +121 -0
  7. package/examples/dashboard-multi-dashboard-version-regression.json +146 -0
  8. package/examples/dashboard-shared-filter-row-delete-cleanup.json +186 -0
  9. package/examples/dashboard-stale-dataset-keys.json +181 -0
  10. package/examples/dashboard-tiered-filter-regression.json +190 -0
  11. package/examples/private/cfa-dashboard.json +651 -0
  12. package/examples/private/data-bite-wrap.json +6936 -0
  13. package/examples/private/multi-dash-fix.json +16963 -0
  14. package/examples/private/versions.json +41612 -0
  15. package/examples/us-map-filter-example.json +1074 -0
  16. package/package.json +9 -9
  17. package/src/CdcDashboard.tsx +6 -2
  18. package/src/CdcDashboardComponent.tsx +178 -87
  19. package/src/DashboardCopyPasteContext.test.tsx +33 -0
  20. package/src/DashboardCopyPasteContext.tsx +48 -0
  21. package/src/_stories/Dashboard.EditorRegression.stories.tsx +72 -0
  22. package/src/_stories/Dashboard.Regression.stories.tsx +196 -0
  23. package/src/_stories/Dashboard.Zoom.stories.tsx +88 -0
  24. package/src/_stories/Dashboard.stories.tsx +294 -0
  25. package/src/_stories/FilteredTextMigrationComparison.stories.tsx +87 -0
  26. package/src/components/Column.test.tsx +176 -0
  27. package/src/components/Column.tsx +214 -13
  28. package/src/components/DashboardConditionModal.test.tsx +420 -0
  29. package/src/components/DashboardConditionModal.tsx +367 -0
  30. package/src/components/DashboardConditionSummary.tsx +59 -0
  31. package/src/components/DashboardEditors.tsx +8 -0
  32. package/src/components/DashboardFilters/DashboardFilters.test.tsx +139 -1
  33. package/src/components/DashboardFilters/DashboardFilters.tsx +192 -174
  34. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.test.tsx +164 -0
  35. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +41 -2
  36. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.test.tsx +180 -3
  37. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +15 -32
  38. package/src/components/DashboardFilters/DashboardFiltersWrapper.test.tsx +142 -0
  39. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +32 -27
  40. package/src/components/DashboardFilters/dashboardfilter.styles.css +42 -27
  41. package/src/components/DataDesignerModal.tsx +2 -1
  42. package/src/components/Grid.tsx +8 -4
  43. package/src/components/Header/Header.tsx +36 -17
  44. package/src/components/Row.test.tsx +228 -0
  45. package/src/components/Row.tsx +93 -18
  46. package/src/components/VisualizationRow.test.tsx +396 -0
  47. package/src/components/VisualizationRow.tsx +110 -35
  48. package/src/components/VisualizationsPanel/VisualizationsPanel.test.tsx +49 -0
  49. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +14 -13
  50. package/src/components/Widget/Widget.test.tsx +218 -0
  51. package/src/components/Widget/Widget.tsx +119 -17
  52. package/src/components/Widget/widget.styles.css +31 -18
  53. package/src/components/dashboard-condition-modal.css +76 -0
  54. package/src/components/dashboard-condition-summary.css +87 -0
  55. package/src/helpers/addValuesToDashboardFilters.ts +3 -5
  56. package/src/helpers/addVisualization.ts +15 -4
  57. package/src/helpers/cloneDashboardWidget.ts +127 -0
  58. package/src/helpers/dashboardColumnWidgets.ts +99 -0
  59. package/src/helpers/dashboardConditionUi.ts +47 -0
  60. package/src/helpers/dashboardConditions.ts +200 -0
  61. package/src/helpers/dashboardFilterTargets.ts +156 -0
  62. package/src/helpers/filterData.ts +4 -9
  63. package/src/helpers/filterVisibility.ts +20 -0
  64. package/src/helpers/formatConfigBeforeSave.ts +2 -2
  65. package/src/helpers/getFilteredData.ts +18 -5
  66. package/src/helpers/getUpdateConfig.ts +43 -12
  67. package/src/helpers/getVizRowColumnLocator.ts +11 -1
  68. package/src/helpers/iconHash.tsx +9 -3
  69. package/src/helpers/mapDataToConfig.ts +31 -29
  70. package/src/helpers/reloadURLHelpers.ts +25 -5
  71. package/src/helpers/removeDashboardFilter.ts +33 -33
  72. package/src/helpers/tests/addVisualization.test.ts +53 -9
  73. package/src/helpers/tests/cloneDashboardWidget.test.ts +136 -0
  74. package/src/helpers/tests/dashboardColumnWidgets.test.ts +99 -0
  75. package/src/helpers/tests/dashboardConditionUi.test.ts +41 -0
  76. package/src/helpers/tests/dashboardConditions.test.ts +428 -0
  77. package/src/helpers/tests/formatConfigBeforeSave.test.ts +51 -0
  78. package/src/helpers/tests/getFilteredData.test.ts +265 -86
  79. package/src/helpers/tests/getUpdateConfig.test.ts +338 -0
  80. package/src/helpers/tests/reloadURLHelpers.test.ts +394 -238
  81. package/src/index.tsx +6 -3
  82. package/src/scss/grid.scss +249 -20
  83. package/src/scss/main.scss +108 -29
  84. package/src/store/dashboard.actions.ts +17 -4
  85. package/src/store/dashboard.reducer.test.ts +538 -0
  86. package/src/store/dashboard.reducer.ts +135 -22
  87. package/src/test/CdcDashboard.test.tsx +148 -0
  88. package/src/test/CdcDashboardComponent.test.tsx +935 -2
  89. package/src/types/ConfigRow.ts +15 -0
  90. package/src/types/DashboardFilters.ts +4 -0
  91. package/src/types/SharedFilter.ts +1 -0
@@ -0,0 +1,176 @@
1
+ import React from 'react'
2
+ import { fireEvent, render, screen } from '@testing-library/react'
3
+ import { describe, expect, it, vi } from 'vitest'
4
+ import { DashboardContext, DashboardDispatchContext, initialState } from '../DashboardContext'
5
+ import { DashboardCopyPasteContext } from '../DashboardCopyPasteContext'
6
+ import Column from './Column'
7
+
8
+ vi.mock('react-dnd', () => ({
9
+ useDrop: () => [{ isOver: false, canDrop: false }, vi.fn()]
10
+ }))
11
+
12
+ vi.mock('./Widget/Widget', () => ({
13
+ default: ({ title }) => <div>{title}</div>
14
+ }))
15
+
16
+ const renderColumn = ({
17
+ data,
18
+ copiedWidget = undefined,
19
+ sharedFilters = [],
20
+ visualizations = {}
21
+ }: {
22
+ data: any
23
+ copiedWidget?: any
24
+ sharedFilters?: any[]
25
+ visualizations?: any
26
+ }) => {
27
+ const dispatch = vi.fn()
28
+ const clearCopiedWidget = vi.fn()
29
+
30
+ render(
31
+ <DashboardContext.Provider
32
+ value={
33
+ {
34
+ ...initialState,
35
+ config: {
36
+ type: 'dashboard',
37
+ activeDashboard: 0,
38
+ dashboard: { sharedFilters },
39
+ datasets: {},
40
+ rows: [{ columns: [data], expandCollapseAllButtons: false }],
41
+ visualizations: {
42
+ 'existing-widget': {
43
+ uid: 'existing-widget',
44
+ type: 'markup-include',
45
+ visualizationType: 'markup-include',
46
+ contentEditor: { title: 'Existing' }
47
+ },
48
+ ...visualizations
49
+ }
50
+ },
51
+ outerContainerRef: vi.fn(),
52
+ setParentConfig: vi.fn(),
53
+ isDebug: false,
54
+ isEditor: true,
55
+ reloadURLData: vi.fn(),
56
+ loadAPIFilters: vi.fn(),
57
+ setAPIFilterDropdowns: vi.fn(),
58
+ setAPILoading: vi.fn(),
59
+ data: {}
60
+ } as any
61
+ }
62
+ >
63
+ <DashboardDispatchContext.Provider value={dispatch}>
64
+ <DashboardCopyPasteContext.Provider value={{ copiedWidget, copyWidget: vi.fn(), clearCopiedWidget }}>
65
+ <Column data={data} rowIdx={0} colIdx={0} toggleRow={false} />
66
+ </DashboardCopyPasteContext.Provider>
67
+ </DashboardDispatchContext.Provider>
68
+ </DashboardContext.Provider>
69
+ )
70
+
71
+ return { dispatch, clearCopiedWidget }
72
+ }
73
+
74
+ describe('Column copy paste slots', () => {
75
+ it('shows paste-ready text in an empty simple column and dispatches clone on click', () => {
76
+ const copiedWidget = { sourceWidgetKey: 'source-widget', label: 'Source' }
77
+ const { dispatch, clearCopiedWidget } = renderColumn({ data: { width: 12 }, copiedWidget })
78
+
79
+ fireEvent.click(
80
+ screen.getByRole('button', { name: 'Click here to paste copied component or drag and drop a new visualization' })
81
+ )
82
+
83
+ expect(dispatch).toHaveBeenCalledWith({
84
+ type: 'CLONE_VISUALIZATION',
85
+ payload: { sourceWidgetKey: 'source-widget', rowIdx: 0, colIdx: 0 }
86
+ })
87
+ expect(clearCopiedWidget).toHaveBeenCalled()
88
+ })
89
+
90
+ it('shows paste-ready text in the empty conditional slot and dispatches with entry index', () => {
91
+ const copiedWidget = { sourceWidgetKey: 'source-widget', label: 'Source' }
92
+ const { dispatch } = renderColumn({
93
+ data: { width: 12, conditionalWidgets: [{ widget: 'existing-widget' }] },
94
+ copiedWidget
95
+ })
96
+
97
+ fireEvent.click(
98
+ screen.getByRole('button', {
99
+ name: 'Click here to paste copied alternate visualization or drag and drop a new alternate'
100
+ })
101
+ )
102
+
103
+ expect(dispatch).toHaveBeenCalledWith({
104
+ type: 'CLONE_VISUALIZATION',
105
+ payload: { sourceWidgetKey: 'source-widget', rowIdx: 0, colIdx: 0, entryIdx: 1 }
106
+ })
107
+ })
108
+
109
+ it('labels empty conditional slots as first-match alternates', () => {
110
+ renderColumn({
111
+ data: { width: 12, conditionalWidgets: [{ widget: 'existing-widget' }] }
112
+ })
113
+
114
+ expect(screen.getByText('Drag and drop an alternate visualization.')).toBeInTheDocument()
115
+ expect(
116
+ screen.getByText('If multiple conditions match, only the first match in this column is shown.')
117
+ ).toBeInTheDocument()
118
+ })
119
+ })
120
+
121
+ describe('Column widget summaries', () => {
122
+ it('shows dashboard filter labels from user-entered keys when they are configured', () => {
123
+ renderColumn({
124
+ data: { width: 12, widget: 'dashboard-filters' },
125
+ sharedFilters: [
126
+ { key: 'State', columnName: 'state' },
127
+ { key: 'Year', columnName: 'year' },
128
+ { key: 'Topic', columnName: 'topic' },
129
+ { key: 'Response', columnName: 'response' }
130
+ ],
131
+ visualizations: {
132
+ 'dashboard-filters': {
133
+ uid: 'dashboard-filters',
134
+ type: 'dashboardFilters',
135
+ visualizationType: 'dashboardFilters',
136
+ sharedFilterIndexes: [0, 1, 2, 3]
137
+ }
138
+ }
139
+ })
140
+
141
+ expect(screen.getByText('State, Year, Topic, Response')).toBeInTheDocument()
142
+ })
143
+
144
+ it('falls back to dashboard filter columns when labels are not configured', () => {
145
+ renderColumn({
146
+ data: { width: 12, widget: 'dashboard-filters' },
147
+ sharedFilters: [{ key: '', columnName: 'state' }, { columnName: 'year' }],
148
+ visualizations: {
149
+ 'dashboard-filters': {
150
+ uid: 'dashboard-filters',
151
+ type: 'dashboardFilters',
152
+ visualizationType: 'dashboardFilters',
153
+ sharedFilterIndexes: [0, 1]
154
+ }
155
+ }
156
+ })
157
+
158
+ expect(screen.getByText('state, year')).toBeInTheDocument()
159
+ })
160
+
161
+ it('uses the table label for table summaries', () => {
162
+ renderColumn({
163
+ data: { width: 12, widget: 'table-widget' },
164
+ visualizations: {
165
+ 'table-widget': {
166
+ uid: 'table-widget',
167
+ type: 'table',
168
+ visualizationType: 'table',
169
+ table: { label: 'Custom Table' }
170
+ }
171
+ }
172
+ })
173
+
174
+ expect(screen.getByText('Custom Table')).toBeInTheDocument()
175
+ })
176
+ })
@@ -1,12 +1,14 @@
1
1
  import React, { useContext } from 'react'
2
2
  import { useDrop } from 'react-dnd'
3
3
 
4
- import { DashboardContext } from '../DashboardContext'
4
+ import { DashboardContext, DashboardDispatchContext } from '../DashboardContext'
5
+ import { DashboardCopyPasteContext } from '../DashboardCopyPasteContext'
5
6
  import Widget from './Widget/Widget'
7
+ import { getColumnWidgetEntries, hasConditionalWidgets } from '../helpers/dashboardColumnWidgets'
6
8
 
7
9
  type ColumnProps = {
8
10
  // column data passed from parent
9
- data: Object[]
11
+ data: any
10
12
  // row index
11
13
  rowIdx: number
12
14
  // column index
@@ -15,8 +17,54 @@ type ColumnProps = {
15
17
  toggleRow: boolean
16
18
  }
17
19
 
18
- const Column: React.FC<ColumnProps> = ({ data, rowIdx, colIdx, toggleRow }) => {
20
+ const formatSummaryList = items => {
21
+ const summaryItems = items.filter(Boolean)
22
+ if (!summaryItems.length) return undefined
23
+
24
+ return summaryItems.join(', ')
25
+ }
26
+
27
+ const getDashboardFiltersTitle = (config, sharedFilters = []) => {
28
+ const filters = config.sharedFilterIndexes?.map(index => sharedFilters[Number(index)]).filter(Boolean) || []
29
+ return formatSummaryList(
30
+ filters.map(
31
+ filter =>
32
+ filter.key?.trim() ||
33
+ filter.columnName ||
34
+ filter.apiFilter?.textSelector ||
35
+ filter.apiFilter?.valueSelector ||
36
+ filter.queryParameter
37
+ )
38
+ )
39
+ }
40
+
41
+ const handleTitle = (config, sharedFilters = []) => {
42
+ if (!config) return
43
+ if (config.type === 'map') return config.general.title
44
+ if (config.type === 'markup-include') return config.contentEditor?.title
45
+ if (config.type === 'dashboardFilters') return getDashboardFiltersTitle(config, sharedFilters)
46
+ if (config.type === 'table') return config.table?.label
47
+ return config.title
48
+ }
49
+
50
+ type ConditionalColumnProps = {
51
+ data: any
52
+ rowIdx: number
53
+ colIdx: number
54
+ toggleRow: boolean
55
+ }
56
+
57
+ type SimpleColumnProps = {
58
+ data: any
59
+ rowIdx: number
60
+ colIdx: number
61
+ toggleRow: boolean
62
+ }
63
+
64
+ const SimpleColumn: React.FC<SimpleColumnProps> = ({ data, rowIdx, colIdx, toggleRow }) => {
19
65
  const { config } = useContext(DashboardContext)
66
+ const dispatch = useContext(DashboardDispatchContext)
67
+ const { copiedWidget, clearCopiedWidget } = useContext(DashboardCopyPasteContext)
20
68
 
21
69
  const [{ isOver, canDrop }, drop] = useDrop(
22
70
  () => ({
@@ -32,7 +80,7 @@ const Column: React.FC<ColumnProps> = ({ data, rowIdx, colIdx, toggleRow }) => {
32
80
  canDrop: !!monitor.canDrop()
33
81
  })
34
82
  }),
35
- [config.activeDashboard]
83
+ [config.activeDashboard, rowIdx, colIdx, data.widget]
36
84
  )
37
85
 
38
86
  const widget = data.widget ? config?.visualizations[data.widget] : null
@@ -46,21 +94,38 @@ const Column: React.FC<ColumnProps> = ({ data, rowIdx, colIdx, toggleRow }) => {
46
94
 
47
95
  if (widget) {
48
96
  classNames.push('column--populated')
97
+ } else if (copiedWidget) {
98
+ classNames.push('column--paste-ready')
49
99
  }
50
100
 
51
- const handleTitle = config => {
52
- if (!config) return
53
- if (config.type === 'map') return config.general.title
54
- if (config.type === 'markup-include') return config.contentEditor?.title
55
- return config.title
101
+ const pasteCopiedWidget = () => {
102
+ if (!copiedWidget || widget) return
103
+
104
+ dispatch({
105
+ type: 'CLONE_VISUALIZATION',
106
+ payload: { sourceWidgetKey: copiedWidget.sourceWidgetKey, rowIdx, colIdx }
107
+ })
108
+ clearCopiedWidget()
56
109
  }
57
110
 
58
111
  return (
59
- <div className={classNames.join(' ')} ref={drop}>
112
+ <div
113
+ className={classNames.join(' ')}
114
+ ref={drop}
115
+ onClick={pasteCopiedWidget}
116
+ onKeyDown={event => {
117
+ if (!copiedWidget || widget) return
118
+ if (event.key === 'Enter' || event.key === ' ') {
119
+ event.preventDefault()
120
+ pasteCopiedWidget()
121
+ }
122
+ }}
123
+ role={!widget && copiedWidget ? 'button' : undefined}
124
+ tabIndex={!widget && copiedWidget ? 0 : undefined}
125
+ >
60
126
  {widget ? (
61
127
  <Widget
62
- columnData={data}
63
- title={handleTitle(widget)}
128
+ title={handleTitle(widget, config.dashboard?.sharedFilters)}
64
129
  widgetConfig={{ rowIdx, colIdx, ...widget }}
65
130
  type={widget.visualizationType ?? widget.general?.geoType}
66
131
  toggleRow={toggleRow}
@@ -68,11 +133,147 @@ const Column: React.FC<ColumnProps> = ({ data, rowIdx, colIdx, toggleRow }) => {
68
133
  />
69
134
  ) : (
70
135
  <p className='builder-column__text'>
71
- Drag and drop <br /> visualization
136
+ {copiedWidget ? (
137
+ 'Click here to paste copied component or drag and drop a new visualization'
138
+ ) : (
139
+ <>
140
+ Drag and drop <br /> visualization
141
+ </>
142
+ )}
143
+ </p>
144
+ )}
145
+ </div>
146
+ )
147
+ }
148
+
149
+ type ConditionalColumnSlotProps = {
150
+ rowIdx: number
151
+ colIdx: number
152
+ toggleRow: boolean
153
+ entryIndex?: number
154
+ widgetKey?: string
155
+ }
156
+
157
+ const ConditionalColumnSlot: React.FC<ConditionalColumnSlotProps> = ({
158
+ rowIdx,
159
+ colIdx,
160
+ toggleRow,
161
+ entryIndex,
162
+ widgetKey
163
+ }) => {
164
+ const { config } = useContext(DashboardContext)
165
+ const dispatch = useContext(DashboardDispatchContext)
166
+ const { copiedWidget, clearCopiedWidget } = useContext(DashboardCopyPasteContext)
167
+
168
+ const [{ isOver, canDrop }, drop] = useDrop(
169
+ () => ({
170
+ accept: 'vis-widget',
171
+ drop: () => ({
172
+ rowIdx,
173
+ colIdx,
174
+ entryIdx: entryIndex,
175
+ canDrop
176
+ }),
177
+ canDrop: () => !widgetKey,
178
+ collect: monitor => ({
179
+ isOver: monitor.isOver(),
180
+ canDrop: !!monitor.canDrop()
181
+ })
182
+ }),
183
+ [config.activeDashboard, rowIdx, colIdx, entryIndex, widgetKey]
184
+ )
185
+
186
+ const widget = widgetKey ? config?.visualizations[widgetKey] : null
187
+ if (widget && !widget.uid) widget.uid = widgetKey
188
+ const classNames = ['builder-column--conditional__slot']
189
+
190
+ if (isOver && canDrop) {
191
+ classNames.push('column--drop')
192
+ }
193
+
194
+ if (widget) {
195
+ classNames.push('column--populated')
196
+ } else if (copiedWidget) {
197
+ classNames.push('column--paste-ready')
198
+ }
199
+
200
+ const pasteCopiedWidget = () => {
201
+ if (!copiedWidget || widget) return
202
+
203
+ dispatch({
204
+ type: 'CLONE_VISUALIZATION',
205
+ payload: { sourceWidgetKey: copiedWidget.sourceWidgetKey, rowIdx, colIdx, entryIdx: entryIndex }
206
+ })
207
+ clearCopiedWidget()
208
+ }
209
+
210
+ return (
211
+ <div
212
+ className={classNames.join(' ')}
213
+ ref={drop}
214
+ onClick={pasteCopiedWidget}
215
+ onKeyDown={event => {
216
+ if (!copiedWidget || widget) return
217
+ if (event.key === 'Enter' || event.key === ' ') {
218
+ event.preventDefault()
219
+ pasteCopiedWidget()
220
+ }
221
+ }}
222
+ role={!widget && copiedWidget ? 'button' : undefined}
223
+ tabIndex={!widget && copiedWidget ? 0 : undefined}
224
+ >
225
+ {widget ? (
226
+ <Widget
227
+ title={handleTitle(widget, config.dashboard?.sharedFilters)}
228
+ widgetConfig={{ rowIdx, colIdx, entryIdx: entryIndex, ...widget }}
229
+ type={widget.visualizationType ?? widget.general?.geoType}
230
+ toggleRow={toggleRow}
231
+ widgetInRow
232
+ />
233
+ ) : (
234
+ <p className='builder-column__text'>
235
+ {copiedWidget ? (
236
+ 'Click here to paste copied alternate visualization or drag and drop a new alternate'
237
+ ) : (
238
+ <>
239
+ Drag and drop an alternate visualization.
240
+ <span className='builder-column__hint'>
241
+ If multiple conditions match, only the first match in this column is shown.
242
+ </span>
243
+ </>
244
+ )}
72
245
  </p>
73
246
  )}
74
247
  </div>
75
248
  )
76
249
  }
77
250
 
251
+ const ConditionalColumn: React.FC<ConditionalColumnProps> = ({ data, rowIdx, colIdx, toggleRow }) => {
252
+ const widgetEntries = getColumnWidgetEntries(data)
253
+
254
+ return (
255
+ <div className={['builder-column', 'builder-column--conditional', 'column-size--' + data.width].join(' ')}>
256
+ {widgetEntries.map((entry, entryIndex) => (
257
+ <ConditionalColumnSlot
258
+ key={`${rowIdx}-${colIdx}-${entryIndex}-${entry.widget}`}
259
+ rowIdx={rowIdx}
260
+ colIdx={colIdx}
261
+ toggleRow={toggleRow}
262
+ entryIndex={entryIndex}
263
+ widgetKey={entry.widget}
264
+ />
265
+ ))}
266
+ <ConditionalColumnSlot rowIdx={rowIdx} colIdx={colIdx} toggleRow={toggleRow} entryIndex={widgetEntries.length} />
267
+ </div>
268
+ )
269
+ }
270
+
271
+ const Column: React.FC<ColumnProps> = ({ data, rowIdx, colIdx, toggleRow }) => {
272
+ if (hasConditionalWidgets(data)) {
273
+ return <ConditionalColumn data={data} rowIdx={rowIdx} colIdx={colIdx} toggleRow={toggleRow} />
274
+ }
275
+
276
+ return <SimpleColumn data={data} rowIdx={rowIdx} colIdx={colIdx} toggleRow={toggleRow} />
277
+ }
278
+
78
279
  export default Column