@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.
- package/dist/{download-config-DemuQ3Jm.js → download-config-C3I0jWIL.js} +2 -2
- package/dist/{download-config-DemuQ3Jm.js.map → download-config-C3I0jWIL.js.map} +1 -1
- package/dist/{row-D4VOhcNI.js → row-DZSP99LW.js} +2 -2
- package/dist/{row-D4VOhcNI.js.map → row-DZSP99LW.js.map} +1 -1
- package/dist/{series-Bola3CmD.js → series-DLNHDWs0.js} +3 -3
- package/dist/{series-Bola3CmD.js.map → series-DLNHDWs0.js.map} +1 -1
- package/dist/types/hooks/index.d.ts +0 -1
- package/dist/types/widgets/actions/brush-toggle/brush-toggle.d.ts +3 -0
- package/dist/types/widgets/actions/index.d.ts +4 -4
- package/dist/types/widgets/actions/lock-selection/types.d.ts +2 -0
- package/dist/types/widgets/actions/relative-data/relative-data.d.ts +7 -2
- package/dist/types/widgets/actions/relative-data/types.d.ts +2 -0
- package/dist/types/widgets/actions/zoom-toggle/zoom-toggle.d.ts +4 -0
- package/dist/types/widgets/category/index.d.ts +10 -2
- package/dist/types/widgets/category/style.d.ts +1 -0
- package/dist/types/widgets/no-data/no-data.d.ts +3 -2
- package/dist/types/widgets/no-data/types.d.ts +5 -1
- package/dist/types/widgets/stores/index.d.ts +1 -1
- package/dist/types/widgets/stores/types.d.ts +10 -10
- package/dist/types/widgets/stores/widget-store.d.ts +2 -3
- package/dist/types/widgets/table/index.d.ts +6 -2
- package/dist/{use-widget-ref-BFazQvJK.js → use-widget-ref-Ddr_SlJJ.js} +2 -2
- package/dist/{use-widget-ref-BFazQvJK.js.map → use-widget-ref-Ddr_SlJJ.js.map} +1 -1
- package/dist/{use-widget-selector-DqRmWQ1K.js → use-widget-selector-DFl2hW0R.js} +2 -2
- package/dist/{use-widget-selector-DqRmWQ1K.js.map → use-widget-selector-DFl2hW0R.js.map} +1 -1
- package/dist/{widget-store-CIrb9RKP.js → widget-store-Bw5zRUGg.js} +93 -95
- package/dist/widget-store-Bw5zRUGg.js.map +1 -0
- package/dist/widgets/actions.js +770 -755
- package/dist/widgets/actions.js.map +1 -1
- package/dist/widgets/bar.js +2 -2
- package/dist/widgets/category.js +187 -183
- package/dist/widgets/category.js.map +1 -1
- package/dist/widgets/echart.js +2 -2
- package/dist/widgets/error.js +37 -2
- package/dist/widgets/error.js.map +1 -1
- package/dist/widgets/formula.js +5 -5
- package/dist/widgets/histogram.js +1 -1
- package/dist/widgets/loader.js +1 -1
- package/dist/widgets/markdown.js +2 -2
- package/dist/widgets/no-data.js +58 -2
- package/dist/widgets/no-data.js.map +1 -1
- package/dist/widgets/note.js +121 -2
- package/dist/widgets/note.js.map +1 -1
- package/dist/widgets/pie.js +2 -2
- package/dist/widgets/range.js +3 -3
- package/dist/widgets/scatterplot.js +2 -2
- package/dist/widgets/skeleton-loader.js +1 -1
- package/dist/widgets/spread.js +5 -5
- package/dist/widgets/stores.js +2 -2
- package/dist/widgets/subheader.js +29 -29
- package/dist/widgets/subheader.js.map +1 -1
- package/dist/widgets/table.js +3 -3
- package/dist/widgets/timeseries.js +2 -2
- package/dist/widgets/utils.js +1 -1
- package/dist/widgets/wrapper.js +2 -2
- package/package.json +1 -5
- package/src/hooks/index.ts +0 -1
- package/src/widgets/actions/brush-toggle/brush-toggle.tsx +18 -22
- package/src/widgets/actions/change-column/change-column.test.tsx +1 -1
- package/src/widgets/actions/download/download.test.tsx +1 -1
- package/src/widgets/actions/index.ts +11 -2
- package/src/widgets/actions/lock-selection/lock-selection.test.tsx +14 -0
- package/src/widgets/actions/lock-selection/lock-selection.tsx +18 -11
- package/src/widgets/actions/lock-selection/types.ts +2 -0
- package/src/widgets/actions/relative-data/relative-data.test.tsx +211 -20
- package/src/widgets/actions/relative-data/relative-data.tsx +65 -34
- package/src/widgets/actions/relative-data/types.ts +2 -0
- package/src/widgets/actions/searcher/searcher.tsx +28 -30
- package/src/widgets/actions/stack-toggle/stack-toggle.tsx +11 -2
- package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +53 -45
- package/src/widgets/category/category-ui.tsx +7 -6
- package/src/widgets/category/index.ts +13 -14
- package/src/widgets/category/style.ts +1 -0
- package/src/widgets/no-data/no-data.test.tsx +90 -40
- package/src/widgets/no-data/no-data.tsx +7 -5
- package/src/widgets/no-data/types.ts +5 -1
- package/src/widgets/stores/index.ts +2 -0
- package/src/widgets/stores/types.ts +10 -18
- package/src/widgets/stores/widget-store.test.ts +132 -13
- package/src/widgets/stores/widget-store.ts +29 -35
- package/src/widgets/subheader/subheader.tsx +11 -3
- package/src/widgets/table/index.ts +6 -4
- package/dist/error-Cj8eUMrl.js +0 -40
- package/dist/error-Cj8eUMrl.js.map +0 -1
- package/dist/no-data-DkIt7Qt1.js +0 -61
- package/dist/no-data-DkIt7Qt1.js.map +0 -1
- package/dist/note-t51drNe0.js +0 -124
- package/dist/note-t51drNe0.js.map +0 -1
- package/dist/types/hooks/use-debounce.d.ts +0 -19
- package/dist/types/widgets/category/components/index.d.ts +0 -10
- package/dist/types/widgets/index.d.ts +0 -9
- package/dist/types/widgets/table/hooks/index.d.ts +0 -6
- package/dist/widget-store-CIrb9RKP.js.map +0 -1
- package/dist/widgets.js +0 -13
- package/dist/widgets.js.map +0 -1
- package/src/hooks/use-debounce.ts +0 -55
- package/src/widgets/category/components/index.ts +0 -14
- package/src/widgets/index.ts +0 -25
- 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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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.
|
|
77
|
-
|
|
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
|
|
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:
|
|
103
|
-
fn: (currentConfig
|
|
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
|
|
110
|
-
const
|
|
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.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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,
|
|
@@ -10,12 +10,12 @@ describe('WidgetNoData', () => {
|
|
|
10
10
|
useWidgetStore.getState().clearWidgets()
|
|
11
11
|
})
|
|
12
12
|
|
|
13
|
-
describe('when
|
|
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
|
-
|
|
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
|
|
34
|
+
test('renders NoData UI with null sourceData', () => {
|
|
35
35
|
useWidgetStore.getState().setWidget(widgetId, {
|
|
36
36
|
isLoading: false,
|
|
37
37
|
isFetching: false,
|
|
38
|
-
|
|
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
|
|
51
|
+
test('renders NoData UI with undefined sourceData', () => {
|
|
52
52
|
useWidgetStore.getState().setWidget(widgetId, {
|
|
53
53
|
isLoading: false,
|
|
54
54
|
isFetching: false,
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
+
sourceData: {},
|
|
90
90
|
})
|
|
91
91
|
|
|
92
92
|
render(
|
|
@@ -100,12 +100,12 @@ describe('WidgetNoData', () => {
|
|
|
100
100
|
})
|
|
101
101
|
})
|
|
102
102
|
|
|
103
|
-
describe('when
|
|
104
|
-
test('renders children with array
|
|
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
|
-
|
|
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
|
|
121
|
+
test('renders children with object sourceData', () => {
|
|
122
122
|
useWidgetStore.getState().setWidget(widgetId, {
|
|
123
123
|
isLoading: false,
|
|
124
124
|
isFetching: false,
|
|
125
|
-
|
|
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
|
|
138
|
+
test('renders children with nested array sourceData', () => {
|
|
139
139
|
useWidgetStore.getState().setWidget(widgetId, {
|
|
140
140
|
isLoading: false,
|
|
141
141
|
isFetching: false,
|
|
142
|
-
|
|
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
|
|
155
|
+
test('renders children with primitive sourceData (number)', () => {
|
|
156
156
|
useWidgetStore.getState().setWidget(widgetId, {
|
|
157
157
|
isLoading: false,
|
|
158
158
|
isFetching: false,
|
|
159
|
-
|
|
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
|
|
172
|
+
test('renders children with primitive sourceData (string)', () => {
|
|
173
173
|
useWidgetStore.getState().setWidget(widgetId, {
|
|
174
174
|
isLoading: false,
|
|
175
175
|
isFetching: false,
|
|
176
|
-
|
|
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
|
|
189
|
+
test('renders children with primitive sourceData (boolean)', () => {
|
|
190
190
|
useWidgetStore.getState().setWidget(widgetId, {
|
|
191
191
|
isLoading: false,
|
|
192
192
|
isFetching: false,
|
|
193
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
298
|
+
sourceData: { customField: 'value' },
|
|
302
299
|
})
|
|
303
300
|
|
|
304
|
-
// Custom isEmpty that treats this
|
|
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
|
-
|
|
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
|
|
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
|
|
423
|
+
// Update with sourceData
|
|
373
424
|
useWidgetStore.getState().setWidget(widgetId, {
|
|
374
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
405
|
-
useWidgetStore.getState().setWidget(widgetId, {
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|