@carto/ps-react-ui 4.5.0 → 4.6.0

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 (99) hide show
  1. package/dist/{download-config-DemuQ3Jm.js → download-config-C3I0jWIL.js} +2 -2
  2. package/dist/{download-config-DemuQ3Jm.js.map → download-config-C3I0jWIL.js.map} +1 -1
  3. package/dist/{row-D4VOhcNI.js → row-DZSP99LW.js} +2 -2
  4. package/dist/{row-D4VOhcNI.js.map → row-DZSP99LW.js.map} +1 -1
  5. package/dist/{series-Bola3CmD.js → series-DLNHDWs0.js} +3 -3
  6. package/dist/{series-Bola3CmD.js.map → series-DLNHDWs0.js.map} +1 -1
  7. package/dist/types/hooks/index.d.ts +0 -1
  8. package/dist/types/widgets/actions/brush-toggle/brush-toggle.d.ts +3 -0
  9. package/dist/types/widgets/actions/index.d.ts +4 -4
  10. package/dist/types/widgets/actions/lock-selection/types.d.ts +2 -0
  11. package/dist/types/widgets/actions/relative-data/relative-data.d.ts +7 -2
  12. package/dist/types/widgets/actions/relative-data/types.d.ts +2 -0
  13. package/dist/types/widgets/actions/zoom-toggle/zoom-toggle.d.ts +4 -0
  14. package/dist/types/widgets/category/index.d.ts +10 -2
  15. package/dist/types/widgets/category/style.d.ts +1 -0
  16. package/dist/types/widgets/no-data/no-data.d.ts +3 -2
  17. package/dist/types/widgets/no-data/types.d.ts +5 -1
  18. package/dist/types/widgets/stores/index.d.ts +1 -1
  19. package/dist/types/widgets/stores/types.d.ts +10 -10
  20. package/dist/types/widgets/stores/widget-store.d.ts +2 -3
  21. package/dist/types/widgets/table/index.d.ts +6 -2
  22. package/dist/{use-widget-ref-BFazQvJK.js → use-widget-ref-Ddr_SlJJ.js} +2 -2
  23. package/dist/{use-widget-ref-BFazQvJK.js.map → use-widget-ref-Ddr_SlJJ.js.map} +1 -1
  24. package/dist/{use-widget-selector-DqRmWQ1K.js → use-widget-selector-DFl2hW0R.js} +2 -2
  25. package/dist/{use-widget-selector-DqRmWQ1K.js.map → use-widget-selector-DFl2hW0R.js.map} +1 -1
  26. package/dist/{widget-store-CIrb9RKP.js → widget-store-Bw5zRUGg.js} +93 -95
  27. package/dist/widget-store-Bw5zRUGg.js.map +1 -0
  28. package/dist/widgets/actions.js +770 -755
  29. package/dist/widgets/actions.js.map +1 -1
  30. package/dist/widgets/bar.js +2 -2
  31. package/dist/widgets/category.js +187 -183
  32. package/dist/widgets/category.js.map +1 -1
  33. package/dist/widgets/echart.js +2 -2
  34. package/dist/widgets/error.js +37 -2
  35. package/dist/widgets/error.js.map +1 -1
  36. package/dist/widgets/formula.js +5 -5
  37. package/dist/widgets/histogram.js +1 -1
  38. package/dist/widgets/loader.js +1 -1
  39. package/dist/widgets/markdown.js +2 -2
  40. package/dist/widgets/no-data.js +58 -2
  41. package/dist/widgets/no-data.js.map +1 -1
  42. package/dist/widgets/note.js +121 -2
  43. package/dist/widgets/note.js.map +1 -1
  44. package/dist/widgets/pie.js +2 -2
  45. package/dist/widgets/range.js +3 -3
  46. package/dist/widgets/scatterplot.js +2 -2
  47. package/dist/widgets/skeleton-loader.js +1 -1
  48. package/dist/widgets/spread.js +5 -5
  49. package/dist/widgets/stores.js +2 -2
  50. package/dist/widgets/subheader.js +29 -29
  51. package/dist/widgets/subheader.js.map +1 -1
  52. package/dist/widgets/table.js +3 -3
  53. package/dist/widgets/timeseries.js +2 -2
  54. package/dist/widgets/utils.js +1 -1
  55. package/dist/widgets/wrapper.js +2 -2
  56. package/package.json +1 -5
  57. package/src/hooks/index.ts +0 -1
  58. package/src/widgets/actions/brush-toggle/brush-toggle.tsx +18 -22
  59. package/src/widgets/actions/change-column/change-column.test.tsx +1 -1
  60. package/src/widgets/actions/download/download.test.tsx +1 -1
  61. package/src/widgets/actions/index.ts +11 -2
  62. package/src/widgets/actions/lock-selection/lock-selection.test.tsx +14 -0
  63. package/src/widgets/actions/lock-selection/lock-selection.tsx +18 -11
  64. package/src/widgets/actions/lock-selection/types.ts +2 -0
  65. package/src/widgets/actions/relative-data/relative-data.test.tsx +211 -20
  66. package/src/widgets/actions/relative-data/relative-data.tsx +65 -34
  67. package/src/widgets/actions/relative-data/types.ts +2 -0
  68. package/src/widgets/actions/searcher/searcher.tsx +28 -30
  69. package/src/widgets/actions/stack-toggle/stack-toggle.tsx +11 -2
  70. package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +53 -45
  71. package/src/widgets/category/category-ui.tsx +7 -6
  72. package/src/widgets/category/index.ts +13 -14
  73. package/src/widgets/category/style.ts +1 -0
  74. package/src/widgets/no-data/no-data.test.tsx +90 -40
  75. package/src/widgets/no-data/no-data.tsx +7 -5
  76. package/src/widgets/no-data/types.ts +5 -1
  77. package/src/widgets/stores/index.ts +2 -0
  78. package/src/widgets/stores/types.ts +10 -18
  79. package/src/widgets/stores/widget-store.test.ts +132 -13
  80. package/src/widgets/stores/widget-store.ts +29 -35
  81. package/src/widgets/subheader/subheader.tsx +11 -3
  82. package/src/widgets/table/index.ts +6 -4
  83. package/dist/error-Cj8eUMrl.js +0 -40
  84. package/dist/error-Cj8eUMrl.js.map +0 -1
  85. package/dist/no-data-DkIt7Qt1.js +0 -61
  86. package/dist/no-data-DkIt7Qt1.js.map +0 -1
  87. package/dist/note-t51drNe0.js +0 -124
  88. package/dist/note-t51drNe0.js.map +0 -1
  89. package/dist/types/hooks/use-debounce.d.ts +0 -19
  90. package/dist/types/widgets/category/components/index.d.ts +0 -10
  91. package/dist/types/widgets/index.d.ts +0 -9
  92. package/dist/types/widgets/table/hooks/index.d.ts +0 -6
  93. package/dist/widget-store-CIrb9RKP.js.map +0 -1
  94. package/dist/widgets.js +0 -13
  95. package/dist/widgets.js.map +0 -1
  96. package/src/hooks/use-debounce.ts +0 -55
  97. package/src/widgets/category/components/index.ts +0 -14
  98. package/src/widgets/index.ts +0 -25
  99. package/src/widgets/table/hooks/index.ts +0 -7
@@ -5,7 +5,7 @@ import {
5
5
  } from '@mui/icons-material'
6
6
  import { useEffect, useCallback } from 'react'
7
7
  import { widgetStoreActions } from '../../stores/widget-store'
8
- import type { ZoomToggleProps } from './types'
8
+ import type { ZoomToggleProps, ZoomState } from './types'
9
9
  import { styles } from './style'
10
10
  import { Tooltip } from '../../../components'
11
11
  import { getEChartZoomConfig } from '../../echart/utils'
@@ -20,6 +20,10 @@ export const ZOOM_TOGGLE_TOOL_ID = 'zoom-toggle'
20
20
  * Registers as a config pipeline tool so that zoom configuration is automatically
21
21
  * re-applied when the base config is updated (e.g., by WidgetLoader).
22
22
  *
23
+ * Zoom state (enabled, range) is stored in the widget store root, and the tool
24
+ * derives its `enabled` flag from the store. This keeps state accessible across
25
+ * component instances.
26
+ *
23
27
  * When zoom is active, displays an inline reset button to disable zoom.
24
28
  * Only intended for use in fullscreen ToolbarActions for bar and histogram widgets.
25
29
  *
@@ -45,19 +49,26 @@ export function ZoomToggle({
45
49
  }: ZoomToggleProps) {
46
50
  const theme = useTheme()
47
51
 
48
- const { zoom, zoomStart, zoomEnd } = useWidgetSelector(id, (w) => {
49
- const zoomTool = w?.registeredTools?.find(
50
- (tool) => tool.id === ZOOM_TOGGLE_TOOL_ID,
51
- )
52
- return {
53
- zoom: zoomTool?.enabled ?? defaultZoom,
54
- zoomStart:
55
- (zoomTool?.config?.start as number | undefined) ?? defaultZoomStart,
56
- zoomEnd: (zoomTool?.config?.end as number | undefined) ?? defaultZoomEnd,
52
+ // Read zoom state from widget store root single source of truth
53
+ const { zoom, zoomStart, zoomEnd } = useWidgetSelector(id, (w) => ({
54
+ zoom: (w as ZoomState | undefined)?.zoom ?? defaultZoom,
55
+ zoomStart: (w as ZoomState | undefined)?.zoomStart ?? defaultZoomStart,
56
+ zoomEnd: (w as ZoomState | undefined)?.zoomEnd ?? defaultZoomEnd,
57
+ }))
58
+
59
+ // Initialize store with default values on mount
60
+ useEffect(() => {
61
+ const current = widgetStoreActions.getWidget<ZoomState>(id)
62
+ if (current?.zoom === undefined) {
63
+ widgetStoreActions.setWidget(id, {
64
+ zoom: defaultZoom,
65
+ zoomStart: defaultZoomStart,
66
+ zoomEnd: defaultZoomEnd,
67
+ })
57
68
  }
58
- })
69
+ }, [id, defaultZoom, defaultZoomStart, defaultZoomEnd])
59
70
 
60
- // Handle dataZoom event to update zoom range in tool config
71
+ // Handle dataZoom event update store (source of truth)
61
72
  const handleDataZoom = useCallback(
62
73
  (event: unknown) => {
63
74
  const zoomEvent = event as {
@@ -73,41 +84,32 @@ export function ZoomToggle({
73
84
  const end = zoomEvent.end ?? zoomEvent.batch?.[0]?.end
74
85
 
75
86
  if (start !== undefined && end !== undefined) {
76
- widgetStoreActions.setToolEnabled(id, ZOOM_TOGGLE_TOOL_ID, true)
77
- widgetStoreActions.updateToolConfig(id, ZOOM_TOGGLE_TOOL_ID, {
78
- start,
79
- end,
87
+ widgetStoreActions.setWidget(id, {
88
+ zoom: true,
89
+ zoomStart: start,
90
+ zoomEnd: end,
80
91
  })
81
92
  }
82
93
  },
83
94
  [id],
84
95
  )
85
96
 
86
- // Register config tool with all reactive deps store's no-op detection handles performance
97
+ // Register config tool once fn reads zoom range from the store at execution time.
87
98
  useEffect(() => {
88
- const existingTool = widgetStoreActions
89
- .getWidget(id)
90
- ?.registeredTools?.find((tool) => tool.id === ZOOM_TOGGLE_TOOL_ID)
91
-
92
- const initialEnabled = existingTool?.enabled ?? defaultZoom
93
- const initialStart =
94
- (existingTool?.config?.start as number | undefined) ?? defaultZoomStart
95
- const initialEnd =
96
- (existingTool?.config?.end as number | undefined) ?? defaultZoomEnd
97
-
98
99
  widgetStoreActions.registerTool(id, {
99
100
  id: ZOOM_TOGGLE_TOOL_ID,
100
101
  type: 'config',
101
102
  order: 20,
102
- enabled: initialEnabled,
103
- fn: (currentConfig, toolConfig) => {
103
+ enabled: defaultZoom,
104
+ fn: (currentConfig) => {
104
105
  const config = currentConfig as Record<string, unknown>
105
106
  const option = config.option as EchartOptionsProps | undefined
106
107
  const currentOnEvents =
107
108
  (config.onEvents as Record<string, unknown> | undefined) ?? {}
108
109
 
109
- const start = (toolConfig?.start as number) ?? 0
110
- const end = (toolConfig?.end as number) ?? 100
110
+ const widget = widgetStoreActions.getWidget<ZoomState>(id)
111
+ const start = widget?.zoomStart ?? 0
112
+ const end = widget?.zoomEnd ?? 100
111
113
 
112
114
  const legend = option?.legend as { show?: boolean } | undefined
113
115
  const hasLegend = legend?.show !== false && legend !== undefined
@@ -135,8 +137,6 @@ export function ZoomToggle({
135
137
 
136
138
  const gridBottom = currentGridBottom + sliderHeight + sliderGap
137
139
 
138
- const onEventsWithoutDataZoom = { ...currentOnEvents }
139
- delete onEventsWithoutDataZoom.dataZoom
140
140
  const onEvents = { ...currentOnEvents, dataZoom: handleDataZoom }
141
141
 
142
142
  return {
@@ -149,28 +149,36 @@ export function ZoomToggle({
149
149
  onEvents,
150
150
  }
151
151
  },
152
- config: {
153
- start: initialStart,
154
- end: initialEnd,
155
- },
156
152
  })
157
153
  return () => widgetStoreActions.unregisterTool(id, ZOOM_TOGGLE_TOOL_ID)
158
154
  }, [id, theme, handleDataZoom, defaultZoom, defaultZoomStart, defaultZoomEnd])
159
155
 
156
+ // Sync enabled from store — lightweight, no re-registration
157
+ useEffect(() => {
158
+ widgetStoreActions.setToolEnabled(id, ZOOM_TOGGLE_TOOL_ID, zoom)
159
+ }, [id, zoom])
160
+
161
+ // Trigger pipeline when zoom range changes — fn reads from store at execution time,
162
+ // but the pipeline only re-runs when registeredTools reference changes.
163
+ // setToolEnabled handles the zoom on/off case, this handles range-only changes.
164
+ useEffect(() => {
165
+ widgetStoreActions.triggerToolPipeline(id)
166
+ }, [id, zoomStart, zoomEnd])
167
+
160
168
  const handleToggle = () => {
161
169
  const newZoom = !zoom
162
- widgetStoreActions.setToolEnabled(id, ZOOM_TOGGLE_TOOL_ID, newZoom)
163
- widgetStoreActions.updateToolConfig(id, ZOOM_TOGGLE_TOOL_ID, {
164
- start: newZoom ? zoomStart : 0,
165
- end: newZoom ? zoomEnd : 100,
170
+ widgetStoreActions.setWidget(id, {
171
+ zoom: newZoom,
172
+ zoomStart: newZoom ? zoomStart : 0,
173
+ zoomEnd: newZoom ? zoomEnd : 100,
166
174
  })
167
175
  }
168
176
 
169
177
  const handleReset = () => {
170
- widgetStoreActions.setToolEnabled(id, ZOOM_TOGGLE_TOOL_ID, true)
171
- widgetStoreActions.updateToolConfig(id, ZOOM_TOGGLE_TOOL_ID, {
172
- start: defaultZoomStart,
173
- end: defaultZoomEnd,
178
+ widgetStoreActions.setWidget(id, {
179
+ zoom: true,
180
+ zoomStart: defaultZoomStart,
181
+ zoomEnd: defaultZoomEnd,
174
182
  })
175
183
  }
176
184
 
@@ -2,19 +2,19 @@ import { Box, useTheme } from '@mui/material'
2
2
  import { useWidgetSelector } from '../stores/use-widget-selector'
3
3
  import { styles } from './style'
4
4
  import type { CategoryUIProps, CategoryWidgetState } from './types'
5
- import {
6
- CategoryRowSingle,
7
- CategoryRowMulti,
8
- CategoryRowOther,
9
- CategoryLegend,
10
- } from './components'
5
+ import { CategoryRowSingle } from './components/category-row-single'
6
+ import { CategoryRowMulti } from './components/category-row-multi'
7
+ import { CategoryRowOther } from './components/category-row-other'
8
+ import { CategoryLegend } from './components/category-legend'
11
9
  import { useState } from 'react'
12
10
  import { defaultFormatter, defaultLabelFormatter } from '../utils/formatter'
11
+ import { useWidgetRef } from '../../hooks'
13
12
 
14
13
  /**
15
14
  * Renders a category widget displaying horizontal bars for categorical data with support for single and multi-series layouts, selection, and overflow grouping.
16
15
  */
17
16
  export function CategoryUI({ id }: CategoryUIProps) {
17
+ const { ref } = useWidgetRef<HTMLDivElement>(id)
18
18
  const theme = useTheme()
19
19
 
20
20
  // Single consolidated subscription instead of 9 separate ones.
@@ -85,6 +85,7 @@ export function CategoryUI({ id }: CategoryUIProps) {
85
85
 
86
86
  return (
87
87
  <Box
88
+ ref={ref}
88
89
  sx={{
89
90
  ...styles.root,
90
91
  }}
@@ -2,21 +2,20 @@ export { CategoryUI } from './category-ui'
2
2
  export { CategorySkeleton } from './skeleton'
3
3
  export { categoryConfig, categoryDownloadConfig } from './config'
4
4
 
5
- export {
6
- CategoryBar,
7
- CategoryRowSingle,
8
- CategoryRowMulti,
9
- CategoryLegend,
10
- CategoryRowOther,
11
- } from './components'
5
+ export { CategoryBar } from './components/category-bar'
6
+ export type { CategoryBarProps } from './components/category-bar'
12
7
 
13
- export type {
14
- CategoryBarProps,
15
- CategoryRowSingleProps,
16
- CategoryRowMultiProps,
17
- CategoryLegendProps,
18
- CategoryRowOtherProps,
19
- } from './components'
8
+ export { CategoryRowSingle } from './components/category-row-single'
9
+ export type { CategoryRowSingleProps } from './components/category-row-single'
10
+
11
+ export { CategoryRowMulti } from './components/category-row-multi'
12
+ export type { CategoryRowMultiProps } from './components/category-row-multi'
13
+
14
+ export { CategoryRowOther } from './components/category-row-other'
15
+ export type { CategoryRowOtherProps } from './components/category-row-other'
16
+
17
+ export { CategoryLegend } from './components/category-legend'
18
+ export type { CategoryLegendProps } from './components/category-legend'
20
19
 
21
20
  export type {
22
21
  CategoryUIProps,
@@ -5,6 +5,7 @@ export const styles = {
5
5
  display: 'flex',
6
6
  flexDirection: 'column',
7
7
  position: 'relative',
8
+ width: '100%',
8
9
  },
9
10
  list: {
10
11
  display: 'flex',
@@ -10,12 +10,12 @@ describe('WidgetNoData', () => {
10
10
  useWidgetStore.getState().clearWidgets()
11
11
  })
12
12
 
13
- describe('when data is empty', () => {
13
+ describe('when sourceData is empty', () => {
14
14
  test('renders NoData UI with empty array', () => {
15
15
  useWidgetStore.getState().setWidget(widgetId, {
16
16
  isLoading: false,
17
17
  isFetching: false,
18
- data: [],
18
+ sourceData: [],
19
19
  })
20
20
 
21
21
  render(
@@ -31,11 +31,11 @@ describe('WidgetNoData', () => {
31
31
  expect(screen.queryByText('Widget Content')).toBeNull()
32
32
  })
33
33
 
34
- test('renders NoData UI with null data', () => {
34
+ test('renders NoData UI with null sourceData', () => {
35
35
  useWidgetStore.getState().setWidget(widgetId, {
36
36
  isLoading: false,
37
37
  isFetching: false,
38
- data: null,
38
+ sourceData: null,
39
39
  })
40
40
 
41
41
  render(
@@ -48,11 +48,11 @@ describe('WidgetNoData', () => {
48
48
  expect(screen.queryByText('Widget Content')).toBeNull()
49
49
  })
50
50
 
51
- test('renders NoData UI with undefined data', () => {
51
+ test('renders NoData UI with undefined sourceData', () => {
52
52
  useWidgetStore.getState().setWidget(widgetId, {
53
53
  isLoading: false,
54
54
  isFetching: false,
55
- data: undefined,
55
+ sourceData: undefined,
56
56
  })
57
57
 
58
58
  render(
@@ -69,7 +69,7 @@ describe('WidgetNoData', () => {
69
69
  useWidgetStore.getState().setWidget(widgetId, {
70
70
  isLoading: false,
71
71
  isFetching: false,
72
- data: [[], []],
72
+ sourceData: [[], []],
73
73
  })
74
74
 
75
75
  render(
@@ -86,7 +86,7 @@ describe('WidgetNoData', () => {
86
86
  useWidgetStore.getState().setWidget(widgetId, {
87
87
  isLoading: false,
88
88
  isFetching: false,
89
- data: {},
89
+ sourceData: {},
90
90
  })
91
91
 
92
92
  render(
@@ -100,12 +100,12 @@ describe('WidgetNoData', () => {
100
100
  })
101
101
  })
102
102
 
103
- describe('when data exists', () => {
104
- test('renders children with array data', () => {
103
+ describe('when sourceData exists', () => {
104
+ test('renders children with array sourceData', () => {
105
105
  useWidgetStore.getState().setWidget(widgetId, {
106
106
  isLoading: false,
107
107
  isFetching: false,
108
- data: [{ value: 1 }],
108
+ sourceData: [{ value: 1 }],
109
109
  })
110
110
 
111
111
  render(
@@ -118,11 +118,11 @@ describe('WidgetNoData', () => {
118
118
  expect(screen.queryByText('No data available')).toBeNull()
119
119
  })
120
120
 
121
- test('renders children with object data', () => {
121
+ test('renders children with object sourceData', () => {
122
122
  useWidgetStore.getState().setWidget(widgetId, {
123
123
  isLoading: false,
124
124
  isFetching: false,
125
- data: { content: 'Hello' },
125
+ sourceData: { content: 'Hello' },
126
126
  })
127
127
 
128
128
  render(
@@ -135,11 +135,11 @@ describe('WidgetNoData', () => {
135
135
  expect(screen.queryByText('No data available')).toBeNull()
136
136
  })
137
137
 
138
- test('renders children with nested array data', () => {
138
+ test('renders children with nested array sourceData', () => {
139
139
  useWidgetStore.getState().setWidget(widgetId, {
140
140
  isLoading: false,
141
141
  isFetching: false,
142
- data: [[{ name: 'A', value: 1 }]],
142
+ sourceData: [[{ name: 'A', value: 1 }]],
143
143
  })
144
144
 
145
145
  render(
@@ -152,11 +152,11 @@ describe('WidgetNoData', () => {
152
152
  expect(screen.queryByText('No data available')).toBeNull()
153
153
  })
154
154
 
155
- test('renders children with primitive data (number)', () => {
155
+ test('renders children with primitive sourceData (number)', () => {
156
156
  useWidgetStore.getState().setWidget(widgetId, {
157
157
  isLoading: false,
158
158
  isFetching: false,
159
- data: 0,
159
+ sourceData: 0,
160
160
  })
161
161
 
162
162
  render(
@@ -169,11 +169,11 @@ describe('WidgetNoData', () => {
169
169
  expect(screen.queryByText('No data available')).toBeNull()
170
170
  })
171
171
 
172
- test('renders children with primitive data (string)', () => {
172
+ test('renders children with primitive sourceData (string)', () => {
173
173
  useWidgetStore.getState().setWidget(widgetId, {
174
174
  isLoading: false,
175
175
  isFetching: false,
176
- data: 'test',
176
+ sourceData: 'test',
177
177
  })
178
178
 
179
179
  render(
@@ -186,11 +186,11 @@ describe('WidgetNoData', () => {
186
186
  expect(screen.queryByText('No data available')).toBeNull()
187
187
  })
188
188
 
189
- test('renders children with primitive data (boolean)', () => {
189
+ test('renders children with primitive sourceData (boolean)', () => {
190
190
  useWidgetStore.getState().setWidget(widgetId, {
191
191
  isLoading: false,
192
192
  isFetching: false,
193
- data: false,
193
+ sourceData: false,
194
194
  })
195
195
 
196
196
  render(
@@ -205,11 +205,10 @@ describe('WidgetNoData', () => {
205
205
  })
206
206
 
207
207
  describe('when loading or fetching', () => {
208
- test('renders children when isLoading=true even if data is empty', () => {
208
+ test('renders children when isLoading=true even if sourceData is empty', () => {
209
209
  useWidgetStore.getState().setWidget(widgetId, {
210
210
  isLoading: true,
211
211
  isFetching: false,
212
- data: [],
213
212
  })
214
213
 
215
214
  render(
@@ -222,11 +221,10 @@ describe('WidgetNoData', () => {
222
221
  expect(screen.queryByText('No data available')).toBeNull()
223
222
  })
224
223
 
225
- test('renders children when isFetching=true even if data is empty', () => {
224
+ test('renders children when isFetching=true even if sourceData is empty', () => {
226
225
  useWidgetStore.getState().setWidget(widgetId, {
227
226
  isLoading: false,
228
227
  isFetching: true,
229
- data: [],
230
228
  })
231
229
 
232
230
  render(
@@ -243,7 +241,6 @@ describe('WidgetNoData', () => {
243
241
  useWidgetStore.getState().setWidget(widgetId, {
244
242
  isLoading: true,
245
243
  isFetching: true,
246
- data: [],
247
244
  })
248
245
 
249
246
  render(
@@ -262,7 +259,7 @@ describe('WidgetNoData', () => {
262
259
  useWidgetStore.getState().setWidget(widgetId, {
263
260
  isLoading: false,
264
261
  isFetching: false,
265
- data: [],
262
+ sourceData: [],
266
263
  })
267
264
 
268
265
  render(
@@ -279,7 +276,7 @@ describe('WidgetNoData', () => {
279
276
  useWidgetStore.getState().setWidget(widgetId, {
280
277
  isLoading: false,
281
278
  isFetching: false,
282
- data: [],
279
+ sourceData: [],
283
280
  })
284
281
 
285
282
  render(
@@ -298,10 +295,10 @@ describe('WidgetNoData', () => {
298
295
  useWidgetStore.getState().setWidget(widgetId, {
299
296
  isLoading: false,
300
297
  isFetching: false,
301
- data: { customField: 'value' },
298
+ sourceData: { customField: 'value' },
302
299
  })
303
300
 
304
- // Custom isEmpty that treats this object as empty
301
+ // Custom isEmpty that treats this sourceData as empty
305
302
  const customIsEmpty = (data: unknown) => {
306
303
  const d = data as { customField: string }
307
304
  return d?.customField === 'value'
@@ -321,7 +318,7 @@ describe('WidgetNoData', () => {
321
318
  useWidgetStore.getState().setWidget(widgetId, {
322
319
  isLoading: false,
323
320
  isFetching: false,
324
- data: [],
321
+ sourceData: [],
325
322
  })
326
323
 
327
324
  // Custom isEmpty that treats empty array as having data
@@ -352,12 +349,66 @@ describe('WidgetNoData', () => {
352
349
  })
353
350
  })
354
351
 
352
+ describe('sourceData vs pipeline-transformed data', () => {
353
+ test('renders children when sourceData has data but pipeline data is empty', () => {
354
+ useWidgetStore.getState().setWidget(widgetId, {
355
+ isLoading: false,
356
+ isFetching: false,
357
+ sourceData: [{ value: 1 }, { value: 2 }],
358
+ data: [], // pipeline filtered everything out
359
+ })
360
+
361
+ render(
362
+ <WidgetNoData id={widgetId}>
363
+ <div>Widget Content</div>
364
+ </WidgetNoData>,
365
+ )
366
+
367
+ // Should show children because sourceData is not empty
368
+ expect(screen.getByText('Widget Content')).toBeTruthy()
369
+ expect(screen.queryByText('No data available')).toBeNull()
370
+ })
371
+
372
+ test('renders NoData when both sourceData and data are empty', () => {
373
+ useWidgetStore.getState().setWidget(widgetId, {
374
+ isLoading: false,
375
+ isFetching: false,
376
+ sourceData: [],
377
+ })
378
+
379
+ render(
380
+ <WidgetNoData id={widgetId}>
381
+ <div>Widget Content</div>
382
+ </WidgetNoData>,
383
+ )
384
+
385
+ expect(screen.getByText('No data available')).toBeTruthy()
386
+ expect(screen.queryByText('Widget Content')).toBeNull()
387
+ })
388
+
389
+ test('renders NoData when sourceData is null (API returned nothing)', () => {
390
+ useWidgetStore.getState().setWidget(widgetId, {
391
+ isLoading: false,
392
+ isFetching: false,
393
+ sourceData: null,
394
+ })
395
+
396
+ render(
397
+ <WidgetNoData id={widgetId}>
398
+ <div>Widget Content</div>
399
+ </WidgetNoData>,
400
+ )
401
+
402
+ expect(screen.getByText('No data available')).toBeTruthy()
403
+ expect(screen.queryByText('Widget Content')).toBeNull()
404
+ })
405
+ })
406
+
355
407
  describe('reactivity to store changes', () => {
356
- test('updates from NoData to content when data arrives', () => {
408
+ test('updates from NoData to content when sourceData arrives', () => {
357
409
  useWidgetStore.getState().setWidget(widgetId, {
358
410
  isLoading: false,
359
411
  isFetching: false,
360
- data: [],
361
412
  })
362
413
 
363
414
  const { rerender } = render(
@@ -369,9 +420,9 @@ describe('WidgetNoData', () => {
369
420
  expect(screen.getByText('No data available')).toBeTruthy()
370
421
  expect(screen.queryByText('Widget Content')).toBeNull()
371
422
 
372
- // Update with data
423
+ // Update with sourceData
373
424
  useWidgetStore.getState().setWidget(widgetId, {
374
- data: [{ value: 1 }],
425
+ sourceData: [{ value: 1 }],
375
426
  })
376
427
 
377
428
  // Force rerender to pick up store changes
@@ -385,11 +436,11 @@ describe('WidgetNoData', () => {
385
436
  expect(screen.queryByText('No data available')).toBeNull()
386
437
  })
387
438
 
388
- test('updates from content to NoData when data becomes empty', () => {
439
+ test('updates from content to NoData when sourceData becomes empty', () => {
389
440
  useWidgetStore.getState().setWidget(widgetId, {
390
441
  isLoading: false,
391
442
  isFetching: false,
392
- data: [{ value: 1 }],
443
+ sourceData: [{ value: 1 }],
393
444
  })
394
445
 
395
446
  const { rerender } = render(
@@ -401,8 +452,8 @@ describe('WidgetNoData', () => {
401
452
  expect(screen.getByText('Widget Content')).toBeTruthy()
402
453
  expect(screen.queryByText('No data available')).toBeNull()
403
454
 
404
- // Clear data
405
- useWidgetStore.getState().setWidget(widgetId, { data: [] })
455
+ // Clear sourceData
456
+ useWidgetStore.getState().setWidget(widgetId, { sourceData: [] })
406
457
 
407
458
  // Force rerender to pick up store changes
408
459
  rerender(
@@ -419,7 +470,6 @@ describe('WidgetNoData', () => {
419
470
  useWidgetStore.getState().setWidget(widgetId, {
420
471
  isLoading: false,
421
472
  isFetching: false,
422
- data: [],
423
473
  })
424
474
 
425
475
  const { rerender } = render(
@@ -6,8 +6,9 @@ import { styles } from './style'
6
6
  /**
7
7
  * NoData wrapper component that displays empty state UI when widget has no data
8
8
  *
9
- * Integrates with widget store to check loading/fetching state and data availability.
10
- * Works in conjunction with SkeletonLoader for complete loading/empty state handling.
9
+ * Integrates with widget store to check loading/fetching state and source data availability.
10
+ * Uses `sourceData` (pre-pipeline data) instead of `data` (post-pipeline) to distinguish
11
+ * "API returned nothing" from "pipeline tools filtered everything out".
11
12
  *
12
13
  * @example Basic usage
13
14
  * ```tsx
@@ -44,10 +45,11 @@ export function WidgetNoData({
44
45
  isEmpty = defaultIsEmpty,
45
46
  }: WidgetNoDataProps) {
46
47
  // Single consolidated subscription instead of 3 separate ones.
47
- const { isLoading, isFetching, data } = useWidgetSelector(id, (w) => ({
48
+ // Reads sourceData (pre-pipeline) to check emptiness, not data (post-pipeline).
49
+ const { isLoading, isFetching, sourceData } = useWidgetSelector(id, (w) => ({
48
50
  isLoading: w?.isLoading,
49
51
  isFetching: w?.isFetching,
50
- data: w?.data,
52
+ sourceData: w?.sourceData,
51
53
  }))
52
54
 
53
55
  // If loading or fetching, show children
@@ -57,7 +59,7 @@ export function WidgetNoData({
57
59
  }
58
60
 
59
61
  // Check if data is empty
60
- if (isEmpty(data)) {
62
+ if (isEmpty(sourceData)) {
61
63
  return (
62
64
  <Box sx={styles.root}>
63
65
  <Typography variant='body2' color='text.primary'>
@@ -59,7 +59,11 @@ export interface WidgetNoDataProps {
59
59
  description?: string
60
60
 
61
61
  /**
62
- * Optional custom function to determine if data is empty
62
+ * Optional custom function to determine if source data is empty.
63
+ * Receives `sourceData` (pre-pipeline data from the API), not `data`
64
+ * (post-pipeline). This allows distinguishing "API returned nothing"
65
+ * from "pipeline tools filtered everything out".
66
+ *
63
67
  * If not provided, uses default isEmpty logic that handles:
64
68
  * - null/undefined → empty
65
69
  * - [] (empty array) → empty
@@ -2,6 +2,8 @@ export { useWidgetStore, widgetStoreActions } from './widget-store'
2
2
  export { useWidgetSelector } from './use-widget-selector'
3
3
  export type {
4
4
  BaseWidgetState,
5
+ ToolRegistration,
6
+ ToolTransformFunction,
5
7
  ToolType,
6
8
  WidgetsStoreProps,
7
9
  WidgetState,