@cdc/core 4.24.4 → 4.24.7

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 (80) hide show
  1. package/components/AdvancedEditor/AdvancedEditor.tsx +93 -0
  2. package/components/AdvancedEditor/advanced-editor-styles.css +3 -0
  3. package/components/AdvancedEditor/index.ts +1 -0
  4. package/components/DataTable/DataTable.tsx +33 -15
  5. package/components/DataTable/DataTableStandAlone.tsx +30 -4
  6. package/components/DataTable/components/ChartHeader.tsx +3 -2
  7. package/components/DataTable/components/DataTableEditorPanel.tsx +23 -6
  8. package/components/DataTable/components/ExpandCollapse.tsx +1 -1
  9. package/components/DataTable/helpers/chartCellMatrix.tsx +5 -10
  10. package/components/DataTable/helpers/getChartCellValue.ts +26 -2
  11. package/components/DataTable/helpers/getDataSeriesColumns.ts +40 -16
  12. package/components/DataTable/helpers/getRowType.ts +6 -0
  13. package/components/DataTable/helpers/getSeriesName.ts +2 -1
  14. package/components/DataTable/helpers/{customColumns.ts → removeNullColumns.ts} +3 -3
  15. package/components/DataTable/types/TableConfig.ts +12 -22
  16. package/components/EditorPanel/ColumnsEditor.tsx +278 -262
  17. package/components/EditorPanel/DataTableEditor.tsx +159 -60
  18. package/components/EditorPanel/FieldSetWrapper.tsx +51 -0
  19. package/components/EditorPanel/FootnotesEditor.tsx +77 -0
  20. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +227 -0
  21. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +54 -0
  22. package/components/EditorPanel/VizFilterEditor/index.ts +1 -0
  23. package/components/EditorWrapper/EditorWrapper.tsx +47 -0
  24. package/components/EditorWrapper/editor-wrapper.style.css +14 -0
  25. package/components/EditorWrapper/index.ts +1 -0
  26. package/components/{Filters.jsx → Filters.tsx} +102 -70
  27. package/components/Footnotes/Footnotes.tsx +25 -0
  28. package/components/Footnotes/FootnotesStandAlone.tsx +45 -0
  29. package/components/Footnotes/footnotes.css +5 -0
  30. package/components/Footnotes/index.ts +1 -0
  31. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +8 -4
  32. package/components/Layout/components/Visualization/index.tsx +14 -5
  33. package/components/MediaControls.jsx +1 -1
  34. package/components/MultiSelect/MultiSelect.tsx +36 -9
  35. package/components/MultiSelect/multiselect.styles.css +0 -3
  36. package/components/_stories/DataTable.stories.tsx +1 -2
  37. package/components/_stories/EditorPanel.stories.tsx +1 -0
  38. package/components/_stories/Footnotes.stories.tsx +17 -0
  39. package/components/inputs/InputSelect.tsx +17 -6
  40. package/components/ui/Icon.tsx +1 -2
  41. package/helpers/DataTransform.ts +9 -32
  42. package/helpers/addValuesToFilters.ts +56 -0
  43. package/helpers/cove/accessibility.ts +1 -0
  44. package/helpers/cove/fontSettings.ts +2 -0
  45. package/helpers/coveUpdateWorker.ts +11 -2
  46. package/helpers/filterVizData.ts +30 -0
  47. package/helpers/footnoteSymbols.ts +11 -0
  48. package/helpers/formatConfigBeforeSave.ts +90 -0
  49. package/helpers/gatherQueryParams.ts +14 -7
  50. package/helpers/lineChartHelpers.js +2 -1
  51. package/helpers/pivotData.ts +18 -0
  52. package/helpers/queryStringUtils.ts +29 -0
  53. package/helpers/tests/updateFieldFactory.test.ts +1 -0
  54. package/helpers/updateFieldFactory.ts +1 -1
  55. package/helpers/useDataVizClasses.js +0 -4
  56. package/helpers/ver/4.23.4.ts +27 -0
  57. package/helpers/ver/4.24.5.ts +32 -0
  58. package/helpers/ver/4.24.7.ts +92 -0
  59. package/package.json +6 -4
  60. package/styles/_button-section.scss +6 -1
  61. package/styles/_data-table.scss +0 -1
  62. package/styles/_reset.scss +7 -6
  63. package/styles/base.scss +4 -0
  64. package/styles/v2/themes/_color-definitions.scss +1 -0
  65. package/types/Annotation.ts +46 -0
  66. package/types/Column.ts +1 -0
  67. package/types/ConfigureData.ts +1 -1
  68. package/types/Footnotes.ts +17 -0
  69. package/types/General.ts +5 -0
  70. package/types/Legend.ts +1 -0
  71. package/types/MarkupInclude.ts +26 -0
  72. package/types/Runtime.ts +3 -7
  73. package/types/Series.ts +1 -1
  74. package/types/Table.ts +21 -14
  75. package/types/Visualization.ts +40 -11
  76. package/types/VizFilter.ts +24 -0
  77. package/LICENSE +0 -201
  78. package/components/AdvancedEditor.jsx +0 -74
  79. package/helpers/queryStringUtils.js +0 -26
  80. package/types/BaseVisualizationType.ts +0 -1
@@ -1,21 +1,64 @@
1
- import React from 'react'
1
+ import React, { useMemo } from 'react'
2
2
  import Tooltip from '@cdc/core/components/ui/Tooltip'
3
3
  import Icon from '../ui/Icon'
4
- import { CheckBox, TextField } from './Inputs'
5
- import type { Table } from '@cdc/core/types/Table'
4
+ import { CheckBox, TextField, Select } from './Inputs'
6
5
  import MultiSelect from '../MultiSelect'
7
6
  import { UpdateFieldFunc } from '../../types/UpdateFieldFunc'
8
7
  import { Visualization } from '../../types/Visualization'
8
+ import _ from 'lodash'
9
+ import { Column } from '../../types/Column'
9
10
 
10
11
  interface DataTableProps {
11
- config: Visualization
12
- updateField: UpdateFieldFunc<string | boolean | string[] | number>
12
+ config: Partial<Visualization>
13
+ updateField: UpdateFieldFunc<string | boolean | string[] | number | Record<string, Partial<Column>>>
13
14
  isDashboard: boolean
14
15
  columns: string[]
15
16
  }
16
17
 
17
- const DataTable: React.FC<DataTableProps> = ({ config, updateField, isDashboard, columns }) => {
18
+ const PLACEHOLDER = '-Select-'
19
+
20
+ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDashboard, columns: dataColumns }) => {
18
21
  const isLoadedFromUrl = config.dataKey?.includes('http://') || config?.dataKey?.includes('https://')
22
+ const excludedColumns = useMemo(() => {
23
+ return Object.keys(config.columns)
24
+ .map<[string, boolean]>(key => [key, config.columns[key].dataTable])
25
+ .filter(([key, dataTable]) => !dataTable)
26
+ .map(([key]) => key)
27
+ }, [config.columns])
28
+
29
+ const groupPivotColumns = useMemo(() => {
30
+ const columns: string[] = config.data.flatMap(Object.keys)
31
+ const cols = _.uniq(columns).filter(key => {
32
+ return true
33
+ })
34
+ return cols
35
+ }, [config.data])
36
+
37
+ const changeGroupBy = (value: string) => {
38
+ if (value === PLACEHOLDER) value = undefined
39
+ updateField('table', null, 'groupBy', value)
40
+ }
41
+
42
+ const excludeColumns = (section, subSection, fieldName, excludedColNames: string[]) => {
43
+ const newColumns = _.cloneDeep(config.columns)
44
+ const colNames: string[] = []
45
+ for (let colKey in newColumns) {
46
+ const col = newColumns[colKey]
47
+ colNames.push(col.name) // keep track of all column names
48
+ if (excludedColNames.includes(col.name)) {
49
+ // ensure all excluded columns are set to false
50
+ newColumns[colKey].dataTable = false
51
+ }
52
+ }
53
+ excludedColNames.forEach(colName => {
54
+ if (!colNames.includes(colName)) {
55
+ // make sure there is a column config to set to dataTable: false
56
+ newColumns[colName] = { name: colName, dataTable: false }
57
+ }
58
+ })
59
+ updateField(null, null, 'columns', newColumns)
60
+ }
61
+
19
62
  return (
20
63
  <>
21
64
  <TextField
@@ -37,25 +80,28 @@ const DataTable: React.FC<DataTableProps> = ({ config, updateField, isDashboard,
37
80
  </Tooltip>
38
81
  }
39
82
  />
40
- <CheckBox
41
- value={config.table.show}
42
- fieldName='show'
43
- label='Show Data Table'
44
- section='table'
45
- updateField={updateField}
46
- className='column-heading'
47
- tooltip={
48
- <Tooltip style={{ textTransform: 'none' }}>
49
- <Tooltip.Target>
50
- <Icon display='question' style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }} />
51
- </Tooltip.Target>
52
- <Tooltip.Content>
53
- <p>Hiding the data table may affect accessibility. An alternate form of accessing visualization data is a 508 requirement.</p>
54
- </Tooltip.Content>
55
- </Tooltip>
56
- }
57
- />
58
- {config.visualizationType !== 'Box Plot' && (
83
+ {config.type !== 'table' && (
84
+ <CheckBox
85
+ value={config.table.show}
86
+ fieldName='show'
87
+ label='Show Data Table'
88
+ section='table'
89
+ updateField={updateField}
90
+ className='column-heading'
91
+ tooltip={
92
+ <Tooltip style={{ textTransform: 'none' }}>
93
+ <Tooltip.Target>
94
+ <Icon display='question' style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }} />
95
+ </Tooltip.Target>
96
+ <Tooltip.Content>
97
+ <p>Hiding the data table may affect accessibility. An alternate form of accessing visualization data is a 508 requirement.</p>
98
+ </Tooltip.Content>
99
+ </Tooltip>
100
+ }
101
+ />
102
+ )}
103
+
104
+ {config.visualizationType !== 'Box Plot' && config.type !== 'table' && (
59
105
  <CheckBox
60
106
  value={config.table.showVertical}
61
107
  fieldName='showVertical'
@@ -75,74 +121,127 @@ const DataTable: React.FC<DataTableProps> = ({ config, updateField, isDashboard,
75
121
  }
76
122
  />
77
123
  )}
124
+
125
+ {config.type !== 'table' && (
126
+ <TextField
127
+ value={config.table.indexLabel}
128
+ section='table'
129
+ fieldName='indexLabel'
130
+ label='Index Column Header'
131
+ updateField={updateField}
132
+ tooltip={
133
+ <Tooltip style={{ textTransform: 'none' }}>
134
+ <Tooltip.Target>
135
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
136
+ </Tooltip.Target>
137
+ <Tooltip.Content>
138
+ <p>To comply with 508 standards, if the first column in the data table has no header, enter a brief one here.</p>
139
+ </Tooltip.Content>
140
+ </Tooltip>
141
+ }
142
+ />
143
+ )}
78
144
  <TextField
79
- value={config.table.indexLabel}
80
- section='table'
81
- fieldName='indexLabel'
82
- label='Index Column Header'
145
+ value={config.table.caption}
83
146
  updateField={updateField}
147
+ section='table'
148
+ type='textarea'
149
+ fieldName='caption'
150
+ label='Screen Reader Description'
151
+ placeholder=' Data table'
84
152
  tooltip={
85
153
  <Tooltip style={{ textTransform: 'none' }}>
86
154
  <Tooltip.Target>
87
155
  <Icon display='question' style={{ marginLeft: '0.5rem' }} />
88
156
  </Tooltip.Target>
89
157
  <Tooltip.Content>
90
- <p>To comply with 508 standards, if the first column in the data table has no header, enter a brief one here.</p>
158
+ <p>Enter a description of the data table to be read by screen readers.</p>
91
159
  </Tooltip.Content>
92
160
  </Tooltip>
93
161
  }
94
162
  />
95
- <TextField
96
- value={config.table.caption}
97
- updateField={updateField}
98
- section='table'
99
- type='textarea'
100
- fieldName='caption'
101
- label='Screen Reader Description'
102
- placeholder=' Data table'
163
+ <CheckBox value={config.table.limitHeight} section='table' fieldName='limitHeight' label=' Limit Table Height' updateField={updateField} />
164
+ {config.table.limitHeight && <TextField value={config.table.height} section='table' fieldName='height' label='Data Table Height' type='number' min={0} max={500} placeholder='Height(px)' updateField={updateField} />}
165
+ {config?.visualizationType !== 'Sankey' && <MultiSelect key={excludedColumns.join('') + 'excluded'} options={dataColumns.map(c => ({ label: c, value: c }))} selected={excludedColumns} fieldName='dataTable' label='Exclude Columns' section='columns' updateField={excludeColumns} />}
166
+ <CheckBox value={config.table.collapsible} fieldName='collapsible' label=' Collapsible' section='table' updateField={updateField} />
167
+ {config.table.collapsible !== false && <CheckBox value={config.table.expanded} fieldName='expanded' label=' Expanded by Default' section='table' updateField={updateField} />}
168
+ {isDashboard && config.type !== 'table' && <CheckBox value={config.table.showDataTableLink} fieldName='showDataTableLink' label='Show Data Table Name & Link' section='table' updateField={updateField} />}
169
+ {isLoadedFromUrl && <CheckBox value={config.table.showDownloadUrl} fieldName='showDownloadUrl' label='Show URL to Automatically Updated Data' section='table' updateField={updateField} />}
170
+ {config.type !== 'table' && <CheckBox value={config.table.showDownloadImgButton} fieldName='showDownloadImgButton' label='Display Image Button' section='table' updateField={updateField} />}
171
+ <label>
172
+ <span className='edit-label column-heading'>Table Cell Min Width</span>
173
+ <input type='number' value={config.table.cellMinWidth ? config.table.cellMinWidth : 0} onChange={e => updateField('table', null, 'cellMinWidth', e.target.value)} />
174
+ </label>
175
+ {config?.visualizationType !== 'Sankey' && (
176
+ <label>
177
+ <span className='edit-label column-heading'>
178
+ Group By{' '}
179
+ <Tooltip style={{ textTransform: 'none' }}>
180
+ <Tooltip.Target>
181
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
182
+ </Tooltip.Target>
183
+ <Tooltip.Content>
184
+ <p>Choose a column to use for grouping data rows. The selected column will not be shown in the data table. You will only be able to choose a column which does not have a column configuration.</p>
185
+ </Tooltip.Content>
186
+ </Tooltip>
187
+ </span>
188
+
189
+ <select
190
+ value={config.table.groupBy}
191
+ onChange={event => {
192
+ changeGroupBy(event.target.value)
193
+ }}
194
+ >
195
+ {[PLACEHOLDER, ...groupPivotColumns.filter(col => col !== config.table.pivot?.columnName && col !== config.table.pivot?.valueColumn)].map(option => (
196
+ <option key={option}>{option}</option>
197
+ ))}
198
+ </select>
199
+ </label>
200
+ )}
201
+ <Select
202
+ label='Pivot Column: '
103
203
  tooltip={
104
204
  <Tooltip style={{ textTransform: 'none' }}>
105
205
  <Tooltip.Target>
106
206
  <Icon display='question' style={{ marginLeft: '0.5rem' }} />
107
207
  </Tooltip.Target>
108
208
  <Tooltip.Content>
109
- <p>Enter a description of the data table to be read by screen readers.</p>
209
+ <p>Select a Column whos data values will be pivoted to Column Values.</p>
110
210
  </Tooltip.Content>
111
211
  </Tooltip>
112
212
  }
213
+ value={config.table.pivot?.columnName}
214
+ options={groupPivotColumns.filter(col => col !== config.table.groupBy && col !== config.table.pivot?.valueColumn)}
215
+ initial='-Select-'
216
+ section='table'
217
+ subsection='pivot'
218
+ fieldName='columnName'
219
+ updateField={updateField}
113
220
  />
114
- <CheckBox value={config.table.limitHeight} section='table' fieldName='limitHeight' label='Limit Table Height' updateField={updateField} />
115
- {config.type !== 'table' && (
116
- <CheckBox
117
- value={config.table.customTableConfig}
118
- fieldName='customTableConfig'
119
- label='Customize Table Config'
120
- section='table'
121
- updateField={updateField}
221
+ {config.table.pivot?.columnName && (
222
+ <Select
223
+ label='Pivot Value Column: '
122
224
  tooltip={
123
225
  <Tooltip style={{ textTransform: 'none' }}>
124
226
  <Tooltip.Target>
125
- <Icon display='question' style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }} />
227
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
126
228
  </Tooltip.Target>
127
229
  <Tooltip.Content>
128
- <p>This will display all available columns in the data set. It will not show any columns where all of the column cells are null.</p>
230
+ <p>The column whos values will be pivoted under the column selected as the Filter.</p>
129
231
  </Tooltip.Content>
130
232
  </Tooltip>
131
233
  }
234
+ value={config.table.pivot?.valueColumn}
235
+ initial='-Select-'
236
+ section='table'
237
+ options={groupPivotColumns.filter(col => col !== config.table.pivot?.columnName && col !== config.table.groupBy)}
238
+ subsection='pivot'
239
+ fieldName='valueColumn'
240
+ updateField={updateField}
132
241
  />
133
242
  )}
134
- {config.table.customTableConfig && <MultiSelect options={columns.map(c => ({ label: c, value: c }))} fieldName='excludeColumns' label='Exclude Columns' section='table' updateField={updateField} />}
135
- {config.table.limitHeight && <TextField value={config.table.height} section='table' fieldName='height' label='Data Table Height' type='number' min={0} max={500} placeholder='Height(px)' updateField={updateField} />}
136
- <CheckBox value={config.table.expanded} fieldName='expanded' label='Expanded by Default' section='table' updateField={updateField} />
137
- {isDashboard && config.type !== 'table' && <CheckBox value={config.table.showDataTableLink} fieldName='showDataTableLink' label='Show Data Table Name & Link' section='table' updateField={updateField} />}
138
- {isLoadedFromUrl && <CheckBox value={config.table.showDownloadUrl} fieldName='showDownloadUrl' label='Show URL to Automatically Updated Data' section='table' updateField={updateField} />}
139
- {config.type !== 'table' && <CheckBox value={config.table.showDownloadImgButton} fieldName='showDownloadImgButton' label='Display Image Button' section='table' updateField={updateField} />}
140
- <label>
141
- <span className='edit-label column-heading'>Table Cell Min Width</span>
142
- <input type='number' value={config.table.cellMinWidth ? config.table.cellMinWidth : 0} onChange={e => updateField('table', null, 'cellMinWidth', e.target.value)} />
143
- </label>
144
243
  </>
145
244
  )
146
245
  }
147
246
 
148
- export default DataTable
247
+ export default DataTableEditor
@@ -0,0 +1,51 @@
1
+ import Icon from '../ui/Icon'
2
+
3
+ type OpenControls = [Record<string, boolean>, Function] // useState type
4
+
5
+ type FieldSetProps = {
6
+ fieldName: string
7
+ fieldKey: string | number
8
+ fieldType: string
9
+ controls: OpenControls
10
+ deleteField: Function
11
+ children: React.ReactNode
12
+ }
13
+
14
+ const FieldSet: React.FC<FieldSetProps> = ({ fieldName, fieldKey, fieldType, controls, deleteField, children }) => {
15
+ const [openControls, setOpenControls] = controls
16
+ const show = openControls[fieldKey]
17
+ const setShow = (key, value) => {
18
+ setOpenControls({ ...openControls, [key]: value })
19
+ }
20
+
21
+ if (!show)
22
+ return (
23
+ <div className='mb-1'>
24
+ <button onClick={() => setShow(fieldKey, true)}>
25
+ <Icon display='caretDown' />
26
+ </button>
27
+ <span> {fieldName ? `${fieldName}` : 'New ' + fieldType}</span>
28
+ </div>
29
+ )
30
+ return (
31
+ <fieldset className='edit-block mb-1' key={fieldKey}>
32
+ <div className='d-flex justify-content-between'>
33
+ <button onClick={() => setShow(fieldKey, false)}>
34
+ <Icon display='caretUp' />
35
+ </button>
36
+ <button
37
+ className='btn btn-danger btn-sm'
38
+ onClick={event => {
39
+ event.preventDefault()
40
+ deleteField()
41
+ }}
42
+ >
43
+ Remove
44
+ </button>
45
+ </div>
46
+ {children}
47
+ </fieldset>
48
+ )
49
+ }
50
+
51
+ export default FieldSet
@@ -0,0 +1,77 @@
1
+ import { UpdateFieldFunc } from '../../types/UpdateFieldFunc'
2
+ import _ from 'lodash'
3
+ import Footnotes, { Footnote } from '../../types/Footnotes'
4
+ import { footnotesSymbols } from '../../helpers/footnoteSymbols'
5
+ import InputSelect from '../inputs/InputSelect'
6
+ import { TextField } from './Inputs'
7
+ interface FootnotesEditorProps {
8
+ config: Footnotes
9
+ updateField: UpdateFieldFunc<Footnote[]>
10
+ }
11
+
12
+ const FootnotesEditor: React.FC<FootnotesEditorProps> = ({ config, updateField }) => {
13
+ const addStaticFootnote = () => {
14
+ const newStaticNotes = [...(config.staticFootnotes || []), { text: 'Add Footnote Text' }]
15
+ updateField(null, null, 'staticFootnotes', newStaticNotes)
16
+ }
17
+
18
+ const updateStaticFootnote = (footnoteIndex, footnoteUpdate: Footnote) => {
19
+ const footnoteCopy = _.cloneDeep(config.staticFootnotes)
20
+ footnoteCopy[footnoteIndex] = footnoteUpdate
21
+ updateField(null, null, 'staticFootnotes', footnoteCopy)
22
+ }
23
+
24
+ const deleteStaticFootnote = footnoteIndex => {
25
+ const footnoteCopy = _.cloneDeep(config.staticFootnotes)
26
+ footnoteCopy.splice(footnoteIndex, 1)
27
+ updateField(null, null, 'staticFootnotes', footnoteCopy)
28
+ }
29
+
30
+ const getOptions = (opts: string[]) => {
31
+ return [['', '--Select--']].concat(opts.map(key => [key, key]))
32
+ }
33
+
34
+ const datasets = config.datasets || {}
35
+
36
+ const dataColumns = config.dataKey ? getOptions(Object.keys(datasets[config.dataKey]?.data?.[0] || {})) : []
37
+ const dataSetOptions = getOptions(Object.keys(datasets))
38
+ return (
39
+ <>
40
+ <em>Dynamic Footnotes</em>
41
+ <div className='row border p-2'>
42
+ <InputSelect label='Select a Footnote Dataset' value={config.dataKey} options={dataSetOptions} fieldName='dataKey' updateField={updateField} />
43
+
44
+ {config.dataKey && (
45
+ <div className='p-3'>
46
+ <InputSelect label='Footnote Symbol Column' value={config.dynamicFootnotes?.symbolColumn} options={dataColumns} section='dynamicFootnotes' fieldName='symbolColumn' updateField={updateField} />
47
+ <InputSelect label='Footnote Text Column' value={config.dynamicFootnotes?.textColumn} options={dataColumns} section='dynamicFootnotes' fieldName='textColumn' updateField={updateField} />
48
+ <InputSelect label='Footnote Order Column' value={config.dynamicFootnotes?.orderColumn} options={dataColumns} section='dynamicFootnotes' fieldName='orderColumn' updateField={updateField} />
49
+ </div>
50
+ )}
51
+ </div>
52
+
53
+ <hr />
54
+
55
+ <em>Static Footnotes</em>
56
+
57
+ {config.staticFootnotes?.map((note, index) => (
58
+ <div key={index} className='row border p-2'>
59
+ <div className='col-8'>
60
+ <InputSelect label='Symbol' value={note.symbol} options={[['', '--Select--'], ...footnotesSymbols]} fieldName='symbol' updateField={(section, subsection, fieldName, value) => updateStaticFootnote(index, { ...note, symbol: value })} />{' '}
61
+ <TextField label='Text' value={note.text} fieldName='text' updateField={(section, subsection, fieldName, value) => updateStaticFootnote(index, { ...note, text: value })} />
62
+ </div>
63
+ <div className='col-2 ml-4'>
64
+ <button className='btn btn-danger p-1' onClick={() => deleteStaticFootnote(index)}>
65
+ Delete
66
+ </button>
67
+ </div>
68
+ </div>
69
+ ))}
70
+ <button className='btn btn-primary' onClick={addStaticFootnote}>
71
+ Add Static Footnote
72
+ </button>
73
+ </>
74
+ )
75
+ }
76
+
77
+ export default FootnotesEditor
@@ -0,0 +1,227 @@
1
+ import { Select, TextField } from '../Inputs'
2
+ import Tooltip from '../../ui/Tooltip'
3
+ import Icon from '../../ui/Icon'
4
+ import { Visualization } from '../../../types/Visualization'
5
+ import { UpdateFieldFunc } from '../../../types/UpdateFieldFunc'
6
+ import _ from 'lodash'
7
+ import { MultiSelectFilter, VizFilter } from '../../../types/VizFilter'
8
+ import { filterStyleOptions, handleSorting } from '../../Filters'
9
+ import FieldSetWrapper from '../FieldSetWrapper'
10
+
11
+ import FilterOrder from './components/FilterOrder'
12
+ import { useMemo, useState } from 'react'
13
+
14
+ type VizFilterProps = {
15
+ config: Visualization
16
+ updateField: UpdateFieldFunc<string | VizFilter[] | VizFilter>
17
+ rawData: Object[]
18
+ }
19
+
20
+ const VizFilterEditor: React.FC<VizFilterProps> = ({ config, updateField, rawData }) => {
21
+ const openControls = useState({})
22
+ const dataColumns = useMemo(() => {
23
+ return _.uniq(_.flatten(rawData?.map(row => Object.keys(row))))
24
+ }, [rawData])
25
+
26
+ const removeFilter = index => {
27
+ let filters = _.cloneDeep(config.filters)
28
+
29
+ filters.splice(index, 1)
30
+
31
+ updateField(null, null, 'filters', filters)
32
+ }
33
+
34
+ const updateFilterProp = (prop, index, value) => {
35
+ updateField('filters', index, prop, value)
36
+ }
37
+
38
+ const updateFilterStyle = (index, value) => {
39
+ const filters = _.cloneDeep(config.filters)
40
+ const currentFilter = filters[index]
41
+ currentFilter.filterStyle = value
42
+ if (value === 'multi-select') {
43
+ currentFilter.active = Array.isArray(currentFilter.active) ? currentFilter.active : [currentFilter.active]
44
+ } else if (Array.isArray(currentFilter.active)) {
45
+ currentFilter.active = currentFilter.active[0]
46
+ }
47
+ filters[index] = currentFilter
48
+ updateField(null, null, 'filters', filters)
49
+ }
50
+
51
+ const handleNameChange = (filterIndex, columnName) => {
52
+ const values = _.uniq(rawData.map(row => row[columnName]))
53
+ const copiedFilter = { ..._.cloneDeep(config.filters[filterIndex]), columnName, values }
54
+ handleSorting(copiedFilter) // sorts dropdown values in place
55
+ copiedFilter.active = copiedFilter.values[0]
56
+ const newFilters = config.filters.map((filter, index) => {
57
+ if (index === filterIndex) return copiedFilter
58
+ return filter
59
+ })
60
+ updateField(null, null, 'filters', newFilters)
61
+ }
62
+
63
+ const addNewFilter = () => {
64
+ const filters = config.filters ? [...config.filters] : []
65
+ const newVizFilter: VizFilter = { values: [], filterStyle: 'dropdown' } as VizFilter
66
+ filters.push(newVizFilter)
67
+ updateField(null, null, 'filters', filters)
68
+ }
69
+
70
+ const handleFilterOrder = (idx1, idx2, filterIndex, filter) => {
71
+ // Create a shallow copy of the filter values array & update position of the values
72
+ const updatedValues = [...filter.values]
73
+ const [movedItem] = updatedValues.splice(idx1, 1)
74
+ updatedValues.splice(idx2, 0, movedItem)
75
+
76
+ const filtersCopy = _.cloneDeep(config.filters)
77
+ const filterItem = { ...filtersCopy[filterIndex] }
78
+
79
+ // Overwrite filterItem.values since thats what we map through in the editor panel
80
+ filterItem.values = updatedValues
81
+ filterItem.orderedValues = updatedValues
82
+ filterItem.active = updatedValues[0]
83
+ filterItem.order = 'cust'
84
+
85
+ // Update the filters
86
+ filtersCopy[filterIndex] = filterItem
87
+
88
+ updateField(null, null, 'filters', filtersCopy)
89
+ }
90
+
91
+ return (
92
+ <>
93
+ {config.filters && (
94
+ <>
95
+ <Select
96
+ value={config.filterBehavior}
97
+ fieldName='filterBehavior'
98
+ label='Filter Behavior'
99
+ updateField={updateField}
100
+ options={['Apply Button', 'Filter Change']}
101
+ tooltip={
102
+ <Tooltip style={{ textTransform: 'none' }}>
103
+ <Tooltip.Target>
104
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
105
+ </Tooltip.Target>
106
+ <Tooltip.Content>
107
+ <p>The Apply Button option changes the visualization when the user clicks "apply". The Filter Change option immediately changes the visualization when the selection is changed.</p>
108
+ </Tooltip.Content>
109
+ </Tooltip>
110
+ }
111
+ />
112
+ <br />
113
+ <ul className='filters-list'>
114
+ {/* Whether filters should apply onChange or Apply Button */}
115
+
116
+ {config.filters.map((filter, index) => {
117
+ if (filter.type === 'url') return <></>
118
+
119
+ return (
120
+ <FieldSetWrapper fieldName={filter.columnName} fieldKey={index} fieldType='Filter' controls={openControls} deleteField={() => removeFilter(index)}>
121
+ <label>
122
+ <span className='edit-label column-heading'>Filter</span>
123
+ <select
124
+ value={filter.columnName}
125
+ onChange={e => {
126
+ handleNameChange(index, e.target.value)
127
+ }}
128
+ >
129
+ <option value=''>- Select Option -</option>
130
+ {dataColumns.map((dataKey, index) => (
131
+ <option value={dataKey} key={index}>
132
+ {dataKey}
133
+ </option>
134
+ ))}
135
+ </select>
136
+ </label>
137
+
138
+ <label>
139
+ <span className='edit-showDropdown column-heading'>Show Filter Input</span>
140
+ <input
141
+ type='checkbox'
142
+ checked={filter.showDropdown === undefined ? true : filter.showDropdown}
143
+ onChange={e => {
144
+ updateFilterProp('showDropdown', index, e.target.checked)
145
+ }}
146
+ />
147
+ </label>
148
+
149
+ <label>
150
+ <span className='edit-label column-heading'>Filter Style</span>
151
+
152
+ <select
153
+ value={filter.filterStyle}
154
+ onChange={e => {
155
+ updateFilterStyle(index, e.target.value)
156
+ }}
157
+ >
158
+ {filterStyleOptions.map((item, index) => {
159
+ return (
160
+ <option key={`filter-style-${index}`} value={item}>
161
+ {item}
162
+ </option>
163
+ )
164
+ })}
165
+ </select>
166
+ </label>
167
+
168
+ <label>
169
+ <span className='edit-label column-heading'>Label</span>
170
+ <input
171
+ type='text'
172
+ value={filter.label}
173
+ onChange={e => {
174
+ updateFilterProp('label', index, e.target.value)
175
+ }}
176
+ />
177
+ </label>
178
+
179
+ {filter.filterStyle === 'multi-select' && (
180
+ <TextField
181
+ label='Select Limit'
182
+ value={(filter as MultiSelectFilter).selectLimit}
183
+ updateField={updateField}
184
+ section='filters'
185
+ subsection={index}
186
+ fieldName='selectLimit'
187
+ type='number'
188
+ tooltip={
189
+ <Tooltip style={{ textTransform: 'none' }}>
190
+ <Tooltip.Target>
191
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
192
+ </Tooltip.Target>
193
+ <Tooltip.Content>
194
+ <p>The maximum number of items that can be selected.</p>
195
+ </Tooltip.Content>
196
+ </Tooltip>
197
+ }
198
+ />
199
+ )}
200
+
201
+ <label>
202
+ <span className='edit-label column-heading'>Default Value Set By Query String Parameter</span>
203
+ <input
204
+ type='text'
205
+ value={filter.setByQueryParameter}
206
+ onChange={e => {
207
+ updateFilterProp('setByQueryParameter', index, e.target.value)
208
+ }}
209
+ />
210
+ </label>
211
+
212
+ <FilterOrder filterIndex={index} filter={filter} updateFilterProp={updateFilterProp} handleFilterOrder={handleFilterOrder} />
213
+ </FieldSetWrapper>
214
+ )
215
+ })}
216
+ </ul>
217
+ </>
218
+ )}
219
+ {!config.filters && <p style={{ textAlign: 'center' }}>There are currently no filters.</p>}
220
+ <button type='button' onClick={addNewFilter} className='btn btn-primary full-width'>
221
+ Add Filter
222
+ </button>
223
+ </>
224
+ )
225
+ }
226
+
227
+ export default VizFilterEditor