@cdc/core 4.26.2 → 4.26.3

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 (186) hide show
  1. package/LICENSE +201 -0
  2. package/_stories/Gallery.Charts.stories.tsx +1 -1
  3. package/_stories/Gallery.DataBite.stories.tsx +1 -1
  4. package/_stories/Gallery.Maps.stories.tsx +1 -1
  5. package/_stories/PageART.stories.tsx +1 -1
  6. package/_stories/PageBRFSS.stories.tsx +1 -1
  7. package/_stories/PageCancerRegistries.stories.tsx +1 -1
  8. package/_stories/PageEasternEquineEncephalitis.stories.tsx +3 -3
  9. package/_stories/PageExcessiveAlcoholUse.stories.tsx +1 -1
  10. package/_stories/PageMaternalMortality.stories.tsx +1 -1
  11. package/_stories/PageOralHealth.stories.tsx +1 -1
  12. package/_stories/PageRespiratory.stories.tsx +4 -4
  13. package/_stories/PageSmokingTobacco.stories.tsx +1 -1
  14. package/_stories/PageStateDiabetesProfiles.stories.tsx +1 -1
  15. package/_stories/PageWastewater.stories.tsx +4 -4
  16. package/_stories/VegaImport.stories.tsx +3 -3
  17. package/assets/callout-flag.svg +7 -0
  18. package/components/AdvancedEditor/EmbedEditor.tsx +1 -1
  19. package/components/Alert/components/Alert.styles.css +2 -2
  20. package/components/ComboBox/combobox.styles.css +48 -48
  21. package/components/CustomColorsEditor/CustomColorsEditor.css +53 -53
  22. package/components/DataTable/DataTable.tsx +46 -18
  23. package/components/DataTable/DataTableStandAlone.tsx +1 -0
  24. package/components/DataTable/components/ChartHeader.tsx +21 -12
  25. package/components/DataTable/components/MapHeader.tsx +34 -28
  26. package/components/DataTable/components/SortIcon/sort-icon.css +5 -5
  27. package/components/DataTable/data-table.css +50 -52
  28. package/components/DataTable/helpers/applyCustomOrder.ts +17 -0
  29. package/components/DataTable/helpers/getChartCellValue.ts +10 -7
  30. package/components/DataTable/helpers/getMapDataTableColumnKeys.ts +22 -0
  31. package/components/DataTable/helpers/mapCellMatrix.tsx +33 -23
  32. package/components/DataTable/helpers/tests/mapCellMatrix.test.ts +33 -0
  33. package/components/DownloadButton.tsx +14 -6
  34. package/components/EditorPanel/ColumnsEditor.tsx +38 -31
  35. package/components/EditorPanel/CustomSortOrder.tsx +94 -0
  36. package/components/EditorPanel/DataTableEditor.tsx +139 -23
  37. package/components/EditorPanel/EditorPanel.styles.css +71 -71
  38. package/components/EditorPanel/EditorPanel.tsx +3 -8
  39. package/components/EditorPanel/EditorPanelDispatch.tsx +4 -4
  40. package/components/EditorPanel/FootnotesEditor.tsx +2 -2
  41. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +7 -6
  42. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +12 -10
  43. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +160 -106
  44. package/components/EditorPanel/components/PanelMarkup.tsx +5 -1
  45. package/{styles/v2/components → components/EditorPanel}/editor.scss +67 -13
  46. package/components/EditorPanel/sections/StyleTreatmentSection.tsx +99 -0
  47. package/components/EditorPanel/sections/VisualSection.tsx +11 -0
  48. package/components/EditorWrapper/editor-wrapper.style.css +1 -1
  49. package/components/Filters/Filters.tsx +3 -5
  50. package/components/Filters/components/Tabs.tsx +19 -7
  51. package/{styles → components/Filters}/filters.scss +3 -3
  52. package/components/Footnotes/FootnotesStandAlone.tsx +4 -2
  53. package/components/HeaderThemeSelector/HeaderThemeSelector.css +61 -5
  54. package/components/Layout/components/Responsive.tsx +14 -6
  55. package/components/Layout/components/Sidebar/components/Sidebar.tsx +1 -1
  56. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +12 -18
  57. package/components/Layout/components/Visualization/index.tsx +39 -38
  58. package/components/Layout/components/Visualization/visualizations.scss +232 -15
  59. package/components/Layout/components/VisualizationContainer.test.tsx +67 -0
  60. package/components/Layout/components/VisualizationContainer.tsx +37 -0
  61. package/components/Layout/components/VisualizationContent.test.tsx +182 -0
  62. package/components/Layout/components/VisualizationContent.tsx +75 -0
  63. package/components/Layout/index.tsx +5 -5
  64. package/components/Layout/styles/editor-utils.scss +3 -3
  65. package/components/Layout/styles/editor.scss +4 -4
  66. package/components/Legend/Legend.Gradient.tsx +7 -1
  67. package/components/Loader/loader.styles.css +2 -2
  68. package/components/Loading.jsx +1 -1
  69. package/components/MediaControls.tsx +10 -2
  70. package/components/MultiSelect/multiselect.styles.css +19 -19
  71. package/components/NestedDropdown/nesteddropdown.styles.css +15 -15
  72. package/components/PaletteSelector/PaletteSelector.css +15 -15
  73. package/components/RichTooltip/richTooltip.css +6 -6
  74. package/components/Table/table.styles.css +2 -2
  75. package/components/Waiting.tsx +1 -1
  76. package/components/_stories/Filters.stories.tsx +1 -1
  77. package/components/_stories/styles.scss +0 -1
  78. package/components/elements/Button.jsx +1 -1
  79. package/components/elements/Card.jsx +1 -1
  80. package/{styles/v2/components → components/elements}/button.scss +9 -8
  81. package/components/inputs/InputCheckbox.jsx +1 -1
  82. package/components/inputs/InputSelect.tsx +1 -1
  83. package/components/inputs/InputText.jsx +1 -1
  84. package/components/inputs/InputToggle.tsx +1 -1
  85. package/{styles/v2/components/input → components/inputs}/_input-check-radio.scss +2 -2
  86. package/{styles/v2/components/input → components/inputs}/_input-group.scss +3 -3
  87. package/{styles/v2/components/input → components/inputs}/_input-slider.scss +2 -2
  88. package/{styles/v2/components/input → components/inputs}/_input.scss +5 -5
  89. package/{styles/v2/components/input → components/inputs}/index.scss +2 -2
  90. package/{styles → components}/loading.scss +1 -1
  91. package/components/managers/DataDesigner.tsx +1 -1
  92. package/{styles/v2/components → components/managers}/data-designer.scss +6 -7
  93. package/components/ui/Accordion.jsx +1 -1
  94. package/components/ui/Icon.tsx +1 -1
  95. package/components/ui/LoadSpin.jsx +1 -1
  96. package/components/ui/Modal.jsx +1 -1
  97. package/components/ui/Overlay.jsx +1 -1
  98. package/components/ui/Title/index.test.tsx +34 -0
  99. package/components/ui/Title/index.tsx +24 -7
  100. package/components/ui/Title/title.styles.css +119 -25
  101. package/components/ui/Tooltip.tsx +1 -1
  102. package/components/ui/_stories/Title.stories.tsx +1 -1
  103. package/{styles/v2/components → components/ui}/accordion.scss +3 -3
  104. package/components/ui/accordion.styles.css +11 -11
  105. package/{styles/v2/components → components/ui}/modal.scss +2 -2
  106. package/{styles/v2/components → components/ui}/overlay.scss +6 -6
  107. package/{styles/v2/components → components}/ui/tooltip.scss +1 -1
  108. package/{styles → components}/waiting.scss +9 -3
  109. package/devTemplate/dev.js +50 -0
  110. package/dist/cove-main.css +528 -231
  111. package/dist/cove-main.css.map +1 -1
  112. package/generateViteConfig.js +2 -2
  113. package/helpers/backfillDefaults.ts +35 -0
  114. package/helpers/constants.ts +12 -0
  115. package/helpers/cove/date.ts +32 -3
  116. package/helpers/cove/number.ts +29 -15
  117. package/helpers/coveUpdateWorker.ts +12 -8
  118. package/helpers/displayDataAsText.ts +1 -1
  119. package/helpers/embed/embedHelper.js +13 -2
  120. package/helpers/embed/index.ts +0 -4
  121. package/helpers/extractDataAndMetadata.ts +20 -0
  122. package/helpers/fetchRemoteData.ts +14 -8
  123. package/helpers/labelHash.ts +9 -0
  124. package/helpers/markupProcessor.ts +56 -38
  125. package/helpers/prepareScreenshot.ts +6 -3
  126. package/helpers/testing.ts +1 -1
  127. package/helpers/tests/abbreviateNumber.test.ts +59 -0
  128. package/helpers/tests/backfillDefaults.test.ts +253 -0
  129. package/helpers/tests/date.test.ts +46 -0
  130. package/helpers/tests/extractDataAndMetadata.test.ts +93 -0
  131. package/helpers/tests/markupProcessor.test.ts +315 -124
  132. package/helpers/tests/number.test.ts +42 -0
  133. package/helpers/tests/prepareScreenshot.test.ts +28 -28
  134. package/helpers/tests/testStandaloneBuild.ts +36 -26
  135. package/helpers/tests/useDataVizClasses.test.ts +66 -0
  136. package/helpers/tests/visualizationWrapperUsage.test.ts +57 -0
  137. package/helpers/useDataVizClasses.ts +13 -7
  138. package/helpers/ver/4.24.4.ts +24 -0
  139. package/helpers/ver/4.26.3.ts +44 -0
  140. package/helpers/ver/4.26.4.ts +31 -0
  141. package/helpers/ver/tests/4.26.3.test.ts +168 -0
  142. package/helpers/ver/tests/4.26.4.test.ts +88 -0
  143. package/helpers/ver/tests/coveUpdateWorker.test.ts +57 -0
  144. package/package.json +2 -2
  145. package/styles/_global.scss +7 -7
  146. package/styles/_reset.scss +2 -2
  147. package/styles/{v2/base → base}/_file-selector.scss +4 -4
  148. package/styles/{v2/base → base}/_general.scss +2 -4
  149. package/styles/{v2/base → base}/index.scss +1 -1
  150. package/styles/base.scss +107 -165
  151. package/styles/cove-main.scss +3 -6
  152. package/styles/layout/_component.scss +110 -0
  153. package/styles/{v2/layout → layout}/_data-table.scss +7 -7
  154. package/styles/layout/_wrapper-padding.scss +27 -0
  155. package/styles/{v2/main.scss → main.scss} +3 -1
  156. package/styles/{v2/themes → themes}/_color-definitions.scss +46 -41
  157. package/styles/{_accessibility.scss → utils/_accessibility.scss} +1 -1
  158. package/styles/{_global-variables.scss → utils/_properties.scss} +133 -112
  159. package/styles/{v2/utils → utils}/index.scss +2 -1
  160. package/types/Axis.ts +2 -0
  161. package/types/ComponentStyles.ts +1 -0
  162. package/types/ConfigureData.ts +1 -0
  163. package/types/MarkupInclude.ts +1 -0
  164. package/types/MarkupVariable.ts +2 -1
  165. package/types/Palette.ts +1 -0
  166. package/types/Table.ts +9 -0
  167. package/types/Visualization.ts +1 -0
  168. package/styles/_common-components.css +0 -73
  169. package/styles/_variables.scss +0 -63
  170. package/styles/v2/layout/_component.scss +0 -21
  171. package/styles/v2/utils/_variables.scss +0 -9
  172. package/{styles/v2/components/card.scss → components/elements/card.css} +2 -2
  173. /package/{styles/v2/components → components/ui}/icon.scss +0 -0
  174. /package/{styles/v2/components → components/ui}/loadspin.scss +0 -0
  175. /package/styles/{v2/base → base}/_heading.scss +0 -0
  176. /package/styles/{v2/base → base}/_reset.scss +0 -0
  177. /package/styles/{v2/layout → layout}/_alert.scss +0 -0
  178. /package/styles/{v2/layout → layout}/_progression.scss +0 -0
  179. /package/styles/{v2/layout → layout}/_tooltip.scss +0 -0
  180. /package/styles/{v2/layout → layout}/index.scss +0 -0
  181. /package/styles/{v2/themes → themes}/index.scss +0 -0
  182. /package/styles/{v2/utils → utils}/_align.scss +0 -0
  183. /package/styles/{v2/utils → utils}/_animations.scss +0 -0
  184. /package/styles/{v2/utils → utils}/_breakpoints.scss +0 -0
  185. /package/styles/{v2/utils → utils}/_grid.scss +0 -0
  186. /package/styles/{v2/utils → utils}/_mixins.scss +0 -0
@@ -1,7 +1,7 @@
1
1
  import { useState, useEffect, useCallback, useRef, ReactNode } from 'react'
2
2
  import { cloneConfig } from '../../helpers/cloneConfig'
3
3
  import ErrorBoundary from '../ErrorBoundary'
4
- import Layout from '../Layout'
4
+ import { Sidebar } from '../Layout'
5
5
  import './EditorPanel.styles.css'
6
6
 
7
7
  export interface BaseEditorPanelProps<TConfig = any> {
@@ -101,7 +101,6 @@ export function EditorPanel<TConfig = any>({
101
101
 
102
102
  /**
103
103
  * Toggle the editor panel visibility and update config to reflect the change.
104
- * In dashboard context, also sets editing to false to return to dashboard editor.
105
104
  */
106
105
  const onBackClick = () => {
107
106
  const newDisplayPanel = !displayPanel
@@ -110,10 +109,6 @@ export function EditorPanel<TConfig = any>({
110
109
  ...config,
111
110
  showEditorPanel: newDisplayPanel
112
111
  }
113
- // If in dashboard mode, set editing to false to return to dashboard editor
114
- if (isDashboard) {
115
- (newConfig as any).editing = false
116
- }
117
112
 
118
113
  // Update local config - the useEffect will handle syncing to parent
119
114
  updateConfig(newConfig)
@@ -124,7 +119,7 @@ export function EditorPanel<TConfig = any>({
124
119
 
125
120
  return (
126
121
  <ErrorBoundary component='EditorPanel'>
127
- <Layout.Sidebar
122
+ <Sidebar
128
123
  displayPanel={displayPanel}
129
124
  isDashboard={isDashboard || false}
130
125
  title={title}
@@ -136,7 +131,7 @@ export function EditorPanel<TConfig = any>({
136
131
  displayPanel,
137
132
  convertStateToConfig
138
133
  })}
139
- </Layout.Sidebar>
134
+ </Sidebar>
140
135
  </ErrorBoundary>
141
136
  )
142
137
  }
@@ -1,6 +1,6 @@
1
1
  import { ReactNode } from 'react'
2
2
  import ErrorBoundary from '../ErrorBoundary'
3
- import Layout from '../Layout'
3
+ import { Sidebar } from '../Layout'
4
4
 
5
5
  export interface EditorPanelDispatchProps<TState = any, TAction = any> {
6
6
  state: TState
@@ -22,7 +22,7 @@ export interface EditorPanelDispatchChildProps<TState = any, TAction = any> {
22
22
  *
23
23
  * Provides common wrapper functionality including:
24
24
  * - ErrorBoundary for error handling
25
- * - Layout.Sidebar for consistent panel display
25
+ * - Sidebar for consistent panel display
26
26
  * - State management for panel visibility
27
27
  *
28
28
  * @example
@@ -62,14 +62,14 @@ export function EditorPanelDispatch<TState = any, TAction = any>({
62
62
 
63
63
  return (
64
64
  <ErrorBoundary component='EditorPanel'>
65
- <Layout.Sidebar
65
+ <Sidebar
66
66
  title={title}
67
67
  onBackClick={onBackClick}
68
68
  displayPanel={showEditorPanel}
69
69
  isDashboard={isDashboard}
70
70
  >
71
71
  {children({ state, dispatch })}
72
- </Layout.Sidebar>
72
+ </Sidebar>
73
73
  </ErrorBoundary>
74
74
  )
75
75
  }
@@ -32,8 +32,8 @@ const FootnotesEditor: React.FC<FootnotesEditorProps> = ({ config, updateField,
32
32
  if (dataSetChanged || noCachedData) {
33
33
  setLoadingAPIData(true)
34
34
  try {
35
- newData = await fetchRemoteData(dataUrl)
36
- newData = transform.autoStandardize(newData)
35
+ const result = await fetchRemoteData(dataUrl)
36
+ newData = transform.autoStandardize(result.data)
37
37
  } catch (e) {
38
38
  setErrorMessage('There was an issue loading the data source. Please check the datasource URL and try again.')
39
39
  }
@@ -1,4 +1,5 @@
1
- import _ from 'lodash'
1
+ import cloneDeep from 'lodash/cloneDeep'
2
+ import uniq from 'lodash/uniq'
2
3
  import { SubGrouping, VizFilter, OrderBy } from '../../../types/VizFilter'
3
4
  import { handleSorting } from '../../Filters/helpers/handleSorting'
4
5
  import { filterOrderOptions } from '../../../helpers/filterOrderOptions'
@@ -45,12 +46,12 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
45
46
 
46
47
  const handleGroupingOrderBy = (order: OrderBy) => {
47
48
  const groupSortObject = {
48
- values: _.cloneDeep(filter.values),
49
+ values: cloneDeep(filter.values),
49
50
  order
50
51
  }
51
52
  const newOrderedValues = handleSorting(groupSortObject).values
52
53
 
53
- const newAllFilters = _.cloneDeep(config.filters)
54
+ const newAllFilters = cloneDeep(config.filters)
54
55
  newAllFilters[filterIndex] = { ...filter, values: newOrderedValues, order }
55
56
  if (order === 'cust') {
56
57
  newAllFilters[filterIndex].orderedValues = newOrderedValues
@@ -68,7 +69,7 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
68
69
  const filterGroups = filter.orderedValues?.length ? filter.orderedValues : filter.values
69
70
 
70
71
  const valuesLookup = filterGroups.reduce((acc, groupName) => {
71
- const values: string[] = _.uniq(
72
+ const values: string[] = uniq(
72
73
  rawData
73
74
  .map(d => {
74
75
  return d[filter.columnName] === groupName ? d[value] : ''
@@ -115,10 +116,10 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
115
116
  subGroupingFitlerOrder: string[],
116
117
  groupName: string
117
118
  ) => {
118
- const updatedGroupOrderedValues = _.cloneDeep(subGroupingFitlerOrder)
119
+ const updatedGroupOrderedValues = cloneDeep(subGroupingFitlerOrder)
119
120
  const [movedItem] = updatedGroupOrderedValues.splice(currentIndex, 1)
120
121
  updatedGroupOrderedValues.splice(newIndex, 0, movedItem)
121
- const newSubGrouping = _.cloneDeep(subGrouping)
122
+ const newSubGrouping = cloneDeep(subGrouping)
122
123
  newSubGrouping.valuesLookup[groupName].values = updatedGroupOrderedValues
123
124
  newSubGrouping.valuesLookup[groupName].orderedValues = updatedGroupOrderedValues
124
125
  updateSubGroupingFilterProperty({ ...newSubGrouping, order: 'cust' })
@@ -3,7 +3,9 @@ import Tooltip from '../../ui/Tooltip'
3
3
  import Icon from '../../ui/Icon'
4
4
  import { Visualization } from '../../../types/Visualization'
5
5
  import { UpdateFieldFunc } from '../../../types/UpdateFieldFunc'
6
- import _ from 'lodash'
6
+ import cloneDeep from 'lodash/cloneDeep'
7
+ import flatten from 'lodash/flatten'
8
+ import uniq from 'lodash/uniq'
7
9
  import { MultiSelectFilter, VizFilter, VizFilterStyle } from '../../../types/VizFilter'
8
10
  import { handleSorting } from '../../Filters/helpers/handleSorting'
9
11
  import { filterOrderOptions } from '../../../helpers/filterOrderOptions'
@@ -27,7 +29,7 @@ const VizFilterEditor: React.FC<VizFilterProps> = ({ config, updateField, rawDat
27
29
  const openControls = useState({})
28
30
  const [isNestedDragHovered, setIsNestedDragHovered] = useState(false)
29
31
  const dataColumns = useMemo(() => {
30
- return _.uniq(_.flatten(rawData?.map(row => Object.keys(row))))
32
+ return uniq(flatten(rawData?.map(row => Object.keys(row))))
31
33
  }, [rawData])
32
34
 
33
35
  // Helper function to get filter values from various sources
@@ -35,13 +37,13 @@ const VizFilterEditor: React.FC<VizFilterProps> = ({ config, updateField, rawDat
35
37
  if (filter.values && filter.values.length > 0) return filter.values
36
38
  if (filter.orderedValues && filter.orderedValues.length > 0) return filter.orderedValues
37
39
  if (filter.columnName && rawData && rawData.length > 0) {
38
- return _.uniq(rawData.map(row => row[filter.columnName]))
40
+ return uniq(rawData.map(row => row[filter.columnName]))
39
41
  }
40
42
  return []
41
43
  }
42
44
 
43
45
  const removeFilter = index => {
44
- let filters = _.cloneDeep(config.filters)
46
+ let filters = cloneDeep(config.filters)
45
47
 
46
48
  filters.splice(index, 1)
47
49
 
@@ -53,7 +55,7 @@ const VizFilterEditor: React.FC<VizFilterProps> = ({ config, updateField, rawDat
53
55
  }
54
56
 
55
57
  const updateFilterDefaultValue = (index, value) => {
56
- const filters = _.cloneDeep(config.filters)
58
+ const filters = cloneDeep(config.filters)
57
59
  const currentFilter = { ...filters[index], orderedValues: filters[index].values }
58
60
  currentFilter.defaultValue = value
59
61
  currentFilter.active = value
@@ -62,7 +64,7 @@ const VizFilterEditor: React.FC<VizFilterProps> = ({ config, updateField, rawDat
62
64
  }
63
65
 
64
66
  const updateFilterStyle = (index, style: VizFilterStyle) => {
65
- const filters = _.cloneDeep(config.filters)
67
+ const filters = cloneDeep(config.filters)
66
68
  const currentFilter = { ...filters[index], orderedValues: filters[index].values }
67
69
  currentFilter.filterStyle = style
68
70
  if (style === 'multi-select') {
@@ -78,8 +80,8 @@ const VizFilterEditor: React.FC<VizFilterProps> = ({ config, updateField, rawDat
78
80
  }
79
81
 
80
82
  const handleNameChange = (filterIndex, columnName) => {
81
- const values = _.uniq(rawData.map(row => row[columnName]))
82
- const copiedFilter = { ..._.cloneDeep(config.filters[filterIndex]), columnName, values }
83
+ const values = uniq(rawData.map(row => row[columnName]))
84
+ const copiedFilter = { ...cloneDeep(config.filters[filterIndex]), columnName, values }
83
85
  handleSorting(copiedFilter) // sorts dropdown values in place
84
86
  copiedFilter.active = copiedFilter.values[0]
85
87
  const newFilters = config.filters.map((filter, index) => {
@@ -109,7 +111,7 @@ const VizFilterEditor: React.FC<VizFilterProps> = ({ config, updateField, rawDat
109
111
  const [movedItem] = updatedValues.splice(idx1, 1)
110
112
  updatedValues.splice(idx2, 0, movedItem)
111
113
 
112
- const filtersCopy = _.cloneDeep(config.filters)
114
+ const filtersCopy = cloneDeep(config.filters)
113
115
  const filterItem = { ...filtersCopy[filterIndex] }
114
116
 
115
117
  // Overwrite filterItem.values since thats what we map through in the editor panel
@@ -132,7 +134,7 @@ const VizFilterEditor: React.FC<VizFilterProps> = ({ config, updateField, rawDat
132
134
 
133
135
  const handleFilterReorder = (idx1: number, idx2: number) => {
134
136
  if (idx1 === undefined || idx2 === undefined || idx1 === idx2) return
135
- const filters = _.cloneDeep(config.filters)
137
+ const filters = cloneDeep(config.filters)
136
138
  const [movedFilter] = filters.splice(idx1, 1)
137
139
  filters.splice(idx2, 0, movedFilter)
138
140
  updateField(null, null, 'filters', filters)
@@ -22,6 +22,8 @@ type MarkupVariablesEditorProps = {
22
22
  enableMarkupVariables?: boolean
23
23
  /** Callback when enable/disable toggle changes */
24
24
  onToggleEnable?: (enabled: boolean) => void
25
+ /** File-level metadata extracted from the data source */
26
+ dataMetadata?: Record<string, string>
25
27
  }
26
28
 
27
29
  export type { MarkupVariablesEditorProps }
@@ -37,7 +39,8 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
37
39
  config,
38
40
  onChange,
39
41
  enableMarkupVariables = false,
40
- onToggleEnable
42
+ onToggleEnable,
43
+ dataMetadata = {}
41
44
  }) => {
42
45
  const [editingIndex, setEditingIndex] = useState<number | null>(null)
43
46
  const [validationErrors, setValidationErrors] = useState<Record<number, string[]>>({})
@@ -63,6 +66,9 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
63
66
  return []
64
67
  }, [data, datasets, config?.dataKey])
65
68
 
69
+ const metadataKeys = useMemo(() => Object.keys(dataMetadata || {}), [dataMetadata])
70
+ const hasMetadataKeys = metadataKeys.length > 0
71
+
66
72
  // Get columns from the available data (memoized for performance)
67
73
  const getAvailableColumns = useMemo((): string[] => {
68
74
  const targetData = getTargetData()
@@ -92,18 +98,20 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
92
98
  if (!variable.tag || variable.tag.trim() === '') {
93
99
  errors.push('Variable tag is required')
94
100
  }
95
- if (!variable.columnName || variable.columnName.trim() === '') {
101
+ if (!variable.metadataKey && (!variable.columnName || variable.columnName.trim() === '')) {
96
102
  errors.push('Data column is required')
97
103
  }
98
- // Validate conditions
99
- variable.conditions?.forEach((condition, index) => {
100
- if (!condition.columnName) {
101
- errors.push(`Condition ${index + 1}: Column is required`)
102
- }
103
- if (!condition.value) {
104
- errors.push(`Condition ${index + 1}: Value is required`)
105
- }
106
- })
104
+ // Validate conditions (not applicable to metadata-sourced variables)
105
+ if (!variable.metadataKey) {
106
+ variable.conditions?.forEach((condition, index) => {
107
+ if (!condition.columnName) {
108
+ errors.push(`Condition ${index + 1}: Column is required`)
109
+ }
110
+ if (!condition.value) {
111
+ errors.push(`Condition ${index + 1}: Value is required`)
112
+ }
113
+ })
114
+ }
107
115
  return errors
108
116
  }, [])
109
117
 
@@ -240,9 +248,15 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
240
248
  {variable.tag}
241
249
  </div>
242
250
  <div style={{ fontSize: '13px', color: '#6c757d' }}>
243
- Column: <strong>{variable.columnName || 'Not selected'}</strong>
244
- {variable.conditions && variable.conditions.length > 0 && (
245
- <span> • {variable.conditions.length} condition{variable.conditions.length !== 1 ? 's' : ''}</span>
251
+ {variable.metadataKey ? (
252
+ <>Metadata: <strong>{variable.metadataKey}</strong></>
253
+ ) : (
254
+ <>
255
+ Column: <strong>{variable.columnName || 'Not selected'}</strong>
256
+ {variable.conditions && variable.conditions.length > 0 && (
257
+ <span> • {variable.conditions.length} condition{variable.conditions.length !== 1 ? 's' : ''}</span>
258
+ )}
259
+ </>
246
260
  )}
247
261
  </div>
248
262
  {validationErrors[index] && validationErrors[index].length > 0 && (
@@ -270,6 +284,60 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
270
284
  <div className='mt-3 pt-3 border-t'>
271
285
  <Accordion>
272
286
  <Accordion.Section title='Basic Settings'>
287
+ {hasMetadataKeys && (
288
+ <div className='mb-3'>
289
+ <Select
290
+ value={variable.metadataKey ? 'metadata' : 'column'}
291
+ fieldName='variableSource'
292
+ label='Source'
293
+ options={[
294
+ { value: 'column', label: 'Data Column' },
295
+ { value: 'metadata', label: 'Data File Metadata' }
296
+ ]}
297
+ updateField={(_section, _subsection, _fieldName, value) => {
298
+ if (value === 'metadata') {
299
+ updateVariable(index, { metadataKey: metadataKeys[0] || '', columnName: '', conditions: [] })
300
+ } else {
301
+ updateVariable(index, { metadataKey: undefined, columnName: '' })
302
+ }
303
+ }}
304
+ />
305
+ </div>
306
+ )}
307
+
308
+ {variable.metadataKey !== undefined && variable.metadataKey !== null && hasMetadataKeys ? (
309
+ <div className='mb-3'>
310
+ <Select
311
+ value={variable.metadataKey}
312
+ fieldName='metadataKey'
313
+ label='Metadata Field'
314
+ options={metadataKeys.map(key => ({ value: key, label: `${key}: ${dataMetadata[key]}` }))}
315
+ updateField={(_section, _subsection, _fieldName, value) => {
316
+ updateVariable(index, {
317
+ metadataKey: value,
318
+ name: variable.name || value,
319
+ tag: variable.tag || generateTag(value)
320
+ })
321
+ }}
322
+ />
323
+ </div>
324
+ ) : (
325
+ <div className='mb-3'>
326
+ <Select
327
+ value={variable.columnName}
328
+ fieldName='columnName'
329
+ label='Data Column'
330
+ options={[
331
+ { value: '', label: 'Select Column...' },
332
+ ...getAvailableColumns.map(col => ({ value: col, label: col }))
333
+ ]}
334
+ updateField={(_section, _subsection, _fieldName, value) => {
335
+ updateVariable(index, { columnName: value })
336
+ }}
337
+ />
338
+ </div>
339
+ )}
340
+
273
341
  <div className='mb-3'>
274
342
  <TextField
275
343
  value={variable.name}
@@ -296,102 +364,88 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
296
364
  />
297
365
  </label>
298
366
  </div>
299
-
300
- <div className='mb-3'>
301
- <Select
302
- value={variable.columnName}
303
- fieldName='columnName'
304
- label='Data Column'
305
- options={[
306
- { value: '', label: 'Select Column...' },
307
- ...getAvailableColumns.map(col => ({ value: col, label: col }))
308
- ]}
309
- updateField={(_section, _subsection, _fieldName, value) => {
310
- updateVariable(index, { columnName: value })
311
- }}
312
- />
313
- </div>
314
367
  </Accordion.Section>
315
368
 
316
- <Accordion.Section title='Conditions'>
317
- <div className='text-sm text-gray-500 mb-2'>
318
- Add conditions to filter when this variable should display data
319
- </div>
320
-
321
- {variable.conditions && variable.conditions.length > 0 && (
322
- <div className='conditions-list mb-2'>
323
- {variable.conditions.map((condition, conditionIndex) => (
324
- <div key={`condition-${index}-${conditionIndex}`} className='condition-item p-2 border rounded mb-2' style={{ backgroundColor: '#f8f9fa' }}>
325
- <div className='mb-2'>
326
- <Select
327
- value={condition.columnName || ''}
328
- fieldName={`condition-column-${index}-${conditionIndex}`}
329
- label='Column'
330
- options={[
331
- { value: '', label: 'Select Column...' },
332
- ...getAvailableColumns.map(col => ({ value: col, label: col }))
333
- ]}
334
- updateField={(_section, _subsection, _fieldName, newColumnName) => {
335
- // Reset value when column changes
336
- updateCondition(index, conditionIndex, {
337
- columnName: newColumnName,
338
- value: ''
339
- })
340
- }}
341
- />
342
- </div>
343
- <div className='mb-2'>
344
- <Select
345
- value={condition.isOrIsNotEqualTo || 'is'}
346
- fieldName={`condition-operator-${index}-${conditionIndex}`}
347
- label='Operator'
348
- options={[
349
- { value: 'is', label: 'is' },
350
- { value: 'is not', label: 'is not' }
351
- ]}
352
- updateField={(_section, _subsection, _fieldName, value) => {
353
- updateCondition(index, conditionIndex, { isOrIsNotEqualTo: value as 'is' | 'is not' })
354
- }}
355
- />
356
- </div>
357
- <div className='mb-2'>
358
- <Select
359
- value={condition.value || ''}
360
- fieldName={`condition-value-${index}-${conditionIndex}`}
361
- label='Value'
362
- options={[
363
- { value: '', label: 'Select Value...' },
364
- ...(condition.columnName
365
- ? getColumnValues(condition.columnName).map(val => ({
366
- value: String(val),
367
- label: String(val)
368
- }))
369
- : [])
370
- ]}
371
- updateField={(_section, _subsection, _fieldName, value) => {
372
- updateCondition(index, conditionIndex, { value })
373
- }}
374
- />
375
- </div>
376
- <Button
377
- className='btn-sm btn-danger'
378
- onClick={() => removeCondition(index, conditionIndex)}
379
- >
380
- Remove Condition
381
- </Button>
382
- </div>
383
- ))}
369
+ {!variable.metadataKey && (
370
+ <Accordion.Section title='Conditions'>
371
+ <div className='text-sm text-gray-500 mb-2'>
372
+ Add conditions to filter when this variable should display data
384
373
  </div>
385
- )}
386
374
 
387
- <Button
388
- className='btn-sm'
389
- onClick={() => addCondition(index)}
390
- >
391
- <Icon display='plus' size={14} className='mr-1' />
392
- Add Condition
393
- </Button>
394
- </Accordion.Section>
375
+ {variable.conditions && variable.conditions.length > 0 && (
376
+ <div className='conditions-list mb-2'>
377
+ {variable.conditions.map((condition, conditionIndex) => (
378
+ <div key={`condition-${index}-${conditionIndex}`} className='condition-item p-2 border rounded mb-2' style={{ backgroundColor: '#f8f9fa' }}>
379
+ <div className='mb-2'>
380
+ <Select
381
+ value={condition.columnName || ''}
382
+ fieldName={`condition-column-${index}-${conditionIndex}`}
383
+ label='Column'
384
+ options={[
385
+ { value: '', label: 'Select Column...' },
386
+ ...getAvailableColumns.map(col => ({ value: col, label: col }))
387
+ ]}
388
+ updateField={(_section, _subsection, _fieldName, newColumnName) => {
389
+ updateCondition(index, conditionIndex, {
390
+ columnName: newColumnName,
391
+ value: ''
392
+ })
393
+ }}
394
+ />
395
+ </div>
396
+ <div className='mb-2'>
397
+ <Select
398
+ value={condition.isOrIsNotEqualTo || 'is'}
399
+ fieldName={`condition-operator-${index}-${conditionIndex}`}
400
+ label='Operator'
401
+ options={[
402
+ { value: 'is', label: 'is' },
403
+ { value: 'is not', label: 'is not' }
404
+ ]}
405
+ updateField={(_section, _subsection, _fieldName, value) => {
406
+ updateCondition(index, conditionIndex, { isOrIsNotEqualTo: value as 'is' | 'is not' })
407
+ }}
408
+ />
409
+ </div>
410
+ <div className='mb-2'>
411
+ <Select
412
+ value={condition.value || ''}
413
+ fieldName={`condition-value-${index}-${conditionIndex}`}
414
+ label='Value'
415
+ options={[
416
+ { value: '', label: 'Select Value...' },
417
+ ...(condition.columnName
418
+ ? getColumnValues(condition.columnName).map(val => ({
419
+ value: String(val),
420
+ label: String(val)
421
+ }))
422
+ : [])
423
+ ]}
424
+ updateField={(_section, _subsection, _fieldName, value) => {
425
+ updateCondition(index, conditionIndex, { value })
426
+ }}
427
+ />
428
+ </div>
429
+ <Button
430
+ className='btn-sm btn-danger'
431
+ onClick={() => removeCondition(index, conditionIndex)}
432
+ >
433
+ Remove Condition
434
+ </Button>
435
+ </div>
436
+ ))}
437
+ </div>
438
+ )}
439
+
440
+ <Button
441
+ className='btn-sm'
442
+ onClick={() => addCondition(index)}
443
+ >
444
+ <Icon display='plus' size={14} className='mr-1' />
445
+ Add Condition
446
+ </Button>
447
+ </Accordion.Section>
448
+ )}
395
449
 
396
450
  <Accordion.Section title='Formatting Options'>
397
451
  <div className='mb-3'>
@@ -18,6 +18,8 @@ type PanelMarkupProps = {
18
18
  onToggleEnable: (enabled: boolean) => void
19
19
  /** Optional: wrap in accordion. Default true */
20
20
  withAccordion?: boolean
21
+ /** File-level metadata extracted from the data source */
22
+ dataMetadata?: Record<string, string>
21
23
  }
22
24
 
23
25
  /**
@@ -31,7 +33,8 @@ const PanelMarkup: React.FC<PanelMarkupProps> = ({
31
33
  enableMarkupVariables,
32
34
  onMarkupVariablesChange,
33
35
  onToggleEnable,
34
- withAccordion = true
36
+ withAccordion = true,
37
+ dataMetadata
35
38
  }) => {
36
39
  const content = (
37
40
  <MarkupVariablesEditor
@@ -40,6 +43,7 @@ const PanelMarkup: React.FC<PanelMarkupProps> = ({
40
43
  onChange={onMarkupVariablesChange}
41
44
  enableMarkupVariables={enableMarkupVariables || false}
42
45
  onToggleEnable={onToggleEnable}
46
+ dataMetadata={dataMetadata}
43
47
  />
44
48
  )
45
49