@carto/ps-react-ui 4.3.5 → 4.3.6
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/components.js +2 -2
- package/dist/{error-B2IJ9d2h.js → error-piB8FwYO.js} +2 -2
- package/dist/{error-B2IJ9d2h.js.map → error-piB8FwYO.js.map} +1 -1
- package/dist/{lasso-tool-wFqOD6wk.js → lasso-tool-BctzdzBu.js} +185 -160
- package/dist/lasso-tool-BctzdzBu.js.map +1 -0
- package/dist/{no-data-C54XJt13.js → no-data-jdlbMef0.js} +2 -2
- package/dist/{no-data-C54XJt13.js.map → no-data-jdlbMef0.js.map} +1 -1
- package/dist/{row-DrHwXNvF.js → row-D3uVFImu.js} +2 -2
- package/dist/{row-DrHwXNvF.js.map → row-D3uVFImu.js.map} +1 -1
- package/dist/{series-D3Pc-kYX.js → series-BAImrSBo.js} +3 -3
- package/dist/{series-D3Pc-kYX.js.map → series-BAImrSBo.js.map} +1 -1
- package/dist/types/widgets/actions/index.d.ts +2 -2
- package/dist/types/widgets/actions/stack-toggle/stack-toggle.d.ts +3 -2
- package/dist/types/widgets/actions/zoom-toggle/zoom-toggle.d.ts +4 -0
- package/dist/types/widgets/loader/loader.d.ts +1 -1
- package/dist/types/widgets/loader/types.d.ts +1 -1
- package/dist/types/widgets/stores/index.d.ts +1 -1
- package/dist/types/widgets/stores/types.d.ts +15 -0
- package/dist/{use-widget-ref-B0aNCANx.js → use-widget-ref-B8x4sHIj.js} +2 -2
- package/dist/{use-widget-ref-B0aNCANx.js.map → use-widget-ref-B8x4sHIj.js.map} +1 -1
- package/dist/widget-store-Dn0Bnc4h.js +178 -0
- package/dist/widget-store-Dn0Bnc4h.js.map +1 -0
- package/dist/widgets/actions.js +698 -617
- package/dist/widgets/actions.js.map +1 -1
- package/dist/widgets/bar.js +2 -2
- package/dist/widgets/category.js +2 -2
- package/dist/widgets/echart.js +2 -2
- package/dist/widgets/error.js +1 -1
- package/dist/widgets/formula.js +5 -5
- package/dist/widgets/histogram.js +2 -2
- package/dist/widgets/loader.js +41 -40
- package/dist/widgets/loader.js.map +1 -1
- package/dist/widgets/markdown.js +2 -2
- package/dist/widgets/no-data.js +1 -1
- package/dist/widgets/pie.js +2 -2
- package/dist/widgets/range.js +2 -2
- 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 +1 -1
- package/dist/widgets/table.js +3 -3
- package/dist/widgets/timeseries.js +2 -2
- package/dist/widgets/wrapper.js +2 -2
- package/dist/widgets.js +4 -4
- package/package.json +1 -1
- package/src/components/lasso-tool/lasso-tool.tsx +5 -2
- package/src/widgets/actions/index.ts +2 -2
- package/src/widgets/actions/stack-toggle/stack-toggle.test.tsx +143 -9
- package/src/widgets/actions/stack-toggle/stack-toggle.tsx +61 -70
- package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +85 -53
- package/src/widgets/loader/loader.tsx +18 -8
- package/src/widgets/loader/types.ts +1 -1
- package/src/widgets/stores/index.ts +1 -0
- package/src/widgets/stores/types.ts +20 -0
- package/src/widgets/stores/widget-store.test.ts +141 -0
- package/src/widgets/stores/widget-store.ts +99 -2
- package/dist/lasso-tool-wFqOD6wk.js.map +0 -1
- package/dist/widget-store-CB6Trp_0.js +0 -131
- package/dist/widget-store-CB6Trp_0.js.map +0 -1
|
@@ -8,13 +8,16 @@ import { GroupedBarChartIcon } from './grouped-bar-chart-icon'
|
|
|
8
8
|
import { getEChartStackConfig } from '../../echart/utils'
|
|
9
9
|
import { DEFAULT_STACK_GROUP } from '../../echart/const'
|
|
10
10
|
import type { EchartWidgetState } from '../../echart/types'
|
|
11
|
+
import type { EchartOptionsProps } from '../../echart/types'
|
|
11
12
|
import { useShallow } from 'zustand/shallow'
|
|
12
13
|
|
|
14
|
+
export const STACK_TOGGLE_TOOL_ID = 'stack-toggle'
|
|
15
|
+
|
|
13
16
|
/**
|
|
14
17
|
* Widget action to toggle stacking behavior in ECharts bar and histogram widgets.
|
|
15
18
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
19
|
+
* Registers as a config pipeline tool so that stack configuration is automatically
|
|
20
|
+
* re-applied when the base config is updated (e.g., by WidgetLoader).
|
|
18
21
|
*
|
|
19
22
|
* @example
|
|
20
23
|
* ```tsx
|
|
@@ -32,93 +35,81 @@ export function StackToggle({
|
|
|
32
35
|
IconButtonProps,
|
|
33
36
|
}: StackToggleProps) {
|
|
34
37
|
const setWidget = useWidgetStore((state) => state.setWidget)
|
|
38
|
+
const registerTool = useWidgetStore((state) => state.registerTool)
|
|
39
|
+
const unregisterTool = useWidgetStore((state) => state.unregisterTool)
|
|
40
|
+
const setToolEnabled = useWidgetStore((state) => state.setToolEnabled)
|
|
41
|
+
const updateToolConfig = useWidgetStore((state) => state.updateToolConfig)
|
|
42
|
+
|
|
35
43
|
const storeIsStacked = useWidgetStore(
|
|
36
44
|
useShallow((state) => state.getWidget<StackToggleState>(id)?.isStacked),
|
|
37
45
|
)
|
|
38
46
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
* Checks if there are multiple series in the chart
|
|
43
|
-
*/
|
|
44
|
-
const hasMultiSeries = useMemo(() => {
|
|
45
|
-
const option = getWidget<EchartWidgetState>(id)?.option
|
|
46
|
-
if (!option) return false
|
|
47
|
-
|
|
48
|
-
const series = Array.isArray(option.series)
|
|
49
|
-
? option.series
|
|
50
|
-
: [option.series]
|
|
51
|
-
|
|
52
|
-
return series.length > 1
|
|
53
|
-
}, [getWidget, id])
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Checks if any series in the option has a stack property defined
|
|
57
|
-
*/
|
|
58
|
-
const hasStackInSeries = useMemo(() => {
|
|
59
|
-
const option = getWidget<EchartWidgetState>(id)?.option
|
|
60
|
-
if (!option) return false
|
|
47
|
+
const option = useWidgetStore(
|
|
48
|
+
useShallow((state) => state.getWidget<EchartWidgetState>(id)?.option),
|
|
49
|
+
)
|
|
61
50
|
|
|
51
|
+
const { hasMultiSeries, hasStackInSeries } = useMemo(() => {
|
|
52
|
+
if (!option) return { hasMultiSeries: false, hasStackInSeries: false }
|
|
62
53
|
const series = Array.isArray(option.series)
|
|
63
54
|
? option.series
|
|
64
55
|
: [option.series]
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
56
|
+
return {
|
|
57
|
+
hasMultiSeries: series.length > 1,
|
|
58
|
+
hasStackInSeries: series.some((s) => (s as { stack?: string })?.stack),
|
|
59
|
+
}
|
|
60
|
+
}, [option])
|
|
68
61
|
|
|
69
62
|
// If series already has stack defined, default to stacked=true
|
|
70
63
|
const effectiveDefaultIsStacked = hasStackInSeries || defaultIsStacked
|
|
71
64
|
const isStacked = storeIsStacked ?? effectiveDefaultIsStacked
|
|
72
65
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
)
|
|
66
|
+
// Register config tool on mount
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
registerTool(id, {
|
|
69
|
+
id: STACK_TOGGLE_TOOL_ID,
|
|
70
|
+
type: 'config',
|
|
71
|
+
order: 10,
|
|
72
|
+
enabled: isStacked && hasMultiSeries,
|
|
73
|
+
fn: (currentConfig, toolConfig) => {
|
|
74
|
+
const config = currentConfig as Record<string, unknown>
|
|
75
|
+
const option = config.option as EchartOptionsProps | undefined
|
|
76
|
+
if (!option) return currentConfig
|
|
77
|
+
|
|
78
|
+
const stacked = (toolConfig?.stacked as boolean) ?? false
|
|
79
|
+
const series = Array.isArray(option.series)
|
|
80
|
+
? option.series
|
|
81
|
+
: [option.series]
|
|
82
|
+
const updatedSeries = series.map((s) => {
|
|
83
|
+
const existingStack = (s as { stack?: string })?.stack
|
|
84
|
+
const stackGroup =
|
|
85
|
+
typeof existingStack === 'string'
|
|
86
|
+
? existingStack
|
|
87
|
+
: DEFAULT_STACK_GROUP
|
|
88
|
+
return { ...s, ...getEChartStackConfig(stacked, stackGroup) }
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
return { ...config, option: { ...option, series: updatedSeries } }
|
|
92
|
+
},
|
|
93
|
+
config: { stacked: isStacked },
|
|
94
|
+
})
|
|
95
|
+
return () => unregisterTool(id, STACK_TOGGLE_TOOL_ID)
|
|
96
|
+
}, [id, registerTool, unregisterTool, isStacked, hasMultiSeries])
|
|
97
|
+
|
|
98
|
+
// Sync tool enabled/config when state changes
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
setToolEnabled(id, STACK_TOGGLE_TOOL_ID, isStacked && hasMultiSeries)
|
|
101
|
+
updateToolConfig(id, STACK_TOGGLE_TOOL_ID, { stacked: isStacked })
|
|
102
|
+
}, [id, isStacked, hasMultiSeries, setToolEnabled, updateToolConfig])
|
|
110
103
|
|
|
111
104
|
// Initialize store with default value only if not already configured
|
|
112
105
|
useEffect(() => {
|
|
106
|
+
if (storeIsStacked !== undefined) return
|
|
113
107
|
setWidget(id, { isStacked: effectiveDefaultIsStacked })
|
|
114
|
-
}, [effectiveDefaultIsStacked, id, setWidget])
|
|
108
|
+
}, [effectiveDefaultIsStacked, id, setWidget, storeIsStacked])
|
|
115
109
|
|
|
116
110
|
const handleToggle = useCallback(() => {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
setWidget(id, { isStacked: newIsStacked })
|
|
120
|
-
updateOptions(newIsStacked)
|
|
121
|
-
}, [isStacked, id, setWidget, updateOptions])
|
|
111
|
+
setWidget(id, { isStacked: !isStacked })
|
|
112
|
+
}, [isStacked, id, setWidget])
|
|
122
113
|
|
|
123
114
|
const tooltipLabel = isStacked
|
|
124
115
|
? (labels?.unstacked ?? 'Disable stacking')
|
|
@@ -10,11 +10,17 @@ import { styles } from './style'
|
|
|
10
10
|
import { Tooltip } from '../../../components'
|
|
11
11
|
import { getEChartZoomConfig } from '../../echart/utils'
|
|
12
12
|
import type { EchartWidgetState } from '../../echart/types'
|
|
13
|
+
import type { EchartOptionsProps } from '../../echart/types'
|
|
13
14
|
import { useShallow } from 'zustand/shallow'
|
|
14
15
|
|
|
16
|
+
export const ZOOM_TOGGLE_TOOL_ID = 'zoom-toggle'
|
|
17
|
+
|
|
15
18
|
/**
|
|
16
19
|
* Widget action to toggle EChart zoom functionality.
|
|
17
20
|
*
|
|
21
|
+
* Registers as a config pipeline tool so that zoom configuration is automatically
|
|
22
|
+
* re-applied when the base config is updated (e.g., by WidgetLoader).
|
|
23
|
+
*
|
|
18
24
|
* When zoom is active, displays an inline reset button to disable zoom.
|
|
19
25
|
* Only intended for use in fullscreen ToolbarActions for bar and histogram widgets.
|
|
20
26
|
*
|
|
@@ -41,6 +47,10 @@ export function ZoomToggle({
|
|
|
41
47
|
const theme = useTheme()
|
|
42
48
|
const setWidget = useWidgetStore((state) => state.setWidget)
|
|
43
49
|
const getWidget = useWidgetStore((state) => state.getWidget)
|
|
50
|
+
const registerTool = useWidgetStore((state) => state.registerTool)
|
|
51
|
+
const unregisterTool = useWidgetStore((state) => state.unregisterTool)
|
|
52
|
+
const setToolEnabled = useWidgetStore((state) => state.setToolEnabled)
|
|
53
|
+
const updateToolConfig = useWidgetStore((state) => state.updateToolConfig)
|
|
44
54
|
|
|
45
55
|
const zoom = useWidgetStore(
|
|
46
56
|
useShallow((state) => {
|
|
@@ -49,76 +59,94 @@ export function ZoomToggle({
|
|
|
49
59
|
}),
|
|
50
60
|
)
|
|
51
61
|
|
|
52
|
-
const
|
|
53
|
-
(
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
zoomStart: start,
|
|
65
|
-
zoomEnd: end,
|
|
66
|
-
option: {
|
|
67
|
-
...currentOption,
|
|
68
|
-
...zoomConfig,
|
|
69
|
-
},
|
|
70
|
-
})
|
|
71
|
-
},
|
|
72
|
-
[getWidget, id, setWidget, theme],
|
|
62
|
+
const zoomStart = useWidgetStore(
|
|
63
|
+
useShallow((state) => {
|
|
64
|
+
const widget = state.getWidget<ZoomState>(id)
|
|
65
|
+
return widget?.zoomStart ?? defaultZoomStart
|
|
66
|
+
}),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const zoomEnd = useWidgetStore(
|
|
70
|
+
useShallow((state) => {
|
|
71
|
+
const widget = state.getWidget<ZoomState>(id)
|
|
72
|
+
return widget?.zoomEnd ?? defaultZoomEnd
|
|
73
|
+
}),
|
|
73
74
|
)
|
|
74
75
|
|
|
76
|
+
// Register config tool on mount
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
registerTool(id, {
|
|
79
|
+
id: ZOOM_TOGGLE_TOOL_ID,
|
|
80
|
+
type: 'config',
|
|
81
|
+
order: 20,
|
|
82
|
+
enabled: zoom,
|
|
83
|
+
fn: (currentConfig, toolConfig) => {
|
|
84
|
+
const config = currentConfig as Record<string, unknown>
|
|
85
|
+
const option = config.option as EchartOptionsProps | undefined
|
|
86
|
+
const enabled = (toolConfig?.enabled as boolean) ?? false
|
|
87
|
+
const start = (toolConfig?.start as number) ?? 0
|
|
88
|
+
const end = (toolConfig?.end as number) ?? 100
|
|
89
|
+
|
|
90
|
+
const zoomConfig = getEChartZoomConfig(
|
|
91
|
+
enabled,
|
|
92
|
+
{ start, end },
|
|
93
|
+
{ inside: true, xSlider: true, ySlider: false },
|
|
94
|
+
theme,
|
|
95
|
+
)
|
|
96
|
+
return { ...config, option: { ...option, ...zoomConfig } }
|
|
97
|
+
},
|
|
98
|
+
config: { enabled: zoom, start: zoomStart, end: zoomEnd },
|
|
99
|
+
})
|
|
100
|
+
return () => unregisterTool(id, ZOOM_TOGGLE_TOOL_ID)
|
|
101
|
+
}, [id, registerTool, unregisterTool, zoom, theme, zoomStart, zoomEnd])
|
|
102
|
+
|
|
103
|
+
// Sync tool enabled/config when state changes
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
setToolEnabled(id, ZOOM_TOGGLE_TOOL_ID, zoom)
|
|
106
|
+
updateToolConfig(id, ZOOM_TOGGLE_TOOL_ID, {
|
|
107
|
+
enabled: zoom,
|
|
108
|
+
start: zoomStart,
|
|
109
|
+
end: zoomEnd,
|
|
110
|
+
})
|
|
111
|
+
}, [id, zoom, zoomStart, zoomEnd, setToolEnabled, updateToolConfig])
|
|
112
|
+
|
|
75
113
|
// Initialize zoom state from defaults only if not already set
|
|
76
114
|
useEffect(() => {
|
|
77
115
|
const existingState = getWidget<ZoomState>(id)
|
|
78
116
|
|
|
79
117
|
if (existingState?.zoom) return
|
|
80
118
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
existingState?.
|
|
84
|
-
existingState?.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
defaultZoomEnd,
|
|
90
|
-
defaultZoomStart,
|
|
91
|
-
getWidget,
|
|
92
|
-
id,
|
|
93
|
-
updateZoomOption,
|
|
94
|
-
])
|
|
95
|
-
|
|
96
|
-
// Cleanup: reset zoom when component unmounts
|
|
119
|
+
setWidget<ZoomState>(id, {
|
|
120
|
+
zoom: existingState?.zoom ?? defaultZoom,
|
|
121
|
+
zoomStart: existingState?.zoomStart ?? defaultZoomStart,
|
|
122
|
+
zoomEnd: existingState?.zoomEnd ?? defaultZoomEnd,
|
|
123
|
+
})
|
|
124
|
+
}, [defaultZoom, defaultZoomEnd, defaultZoomStart, getWidget, id, setWidget])
|
|
125
|
+
|
|
126
|
+
// Cleanup: disable zoom when component unmounts
|
|
97
127
|
useEffect(() => {
|
|
98
128
|
return () => {
|
|
99
|
-
|
|
100
|
-
updateZoomOption(
|
|
101
|
-
false,
|
|
102
|
-
existingState?.zoomStart ?? 0,
|
|
103
|
-
existingState?.zoomEnd ?? 100,
|
|
104
|
-
)
|
|
129
|
+
setWidget<ZoomState>(id, { zoom: false })
|
|
105
130
|
}
|
|
106
|
-
}, [
|
|
131
|
+
}, [id, setWidget])
|
|
107
132
|
|
|
108
133
|
const handleToggle = () => {
|
|
109
134
|
const newZoom = !zoom
|
|
110
|
-
|
|
111
135
|
const existingState = getWidget<ZoomState>(id)
|
|
112
136
|
|
|
113
|
-
|
|
114
|
-
newZoom,
|
|
115
|
-
newZoom ? (existingState?.zoomStart ?? defaultZoomStart) : 0,
|
|
116
|
-
newZoom ? (existingState?.zoomEnd ?? defaultZoomEnd) : 100,
|
|
117
|
-
)
|
|
137
|
+
setWidget<ZoomState>(id, {
|
|
138
|
+
zoom: newZoom,
|
|
139
|
+
zoomStart: newZoom ? (existingState?.zoomStart ?? defaultZoomStart) : 0,
|
|
140
|
+
zoomEnd: newZoom ? (existingState?.zoomEnd ?? defaultZoomEnd) : 100,
|
|
141
|
+
})
|
|
118
142
|
}
|
|
119
143
|
|
|
120
144
|
const handleReset = () => {
|
|
121
|
-
|
|
145
|
+
setWidget<ZoomState>(id, {
|
|
146
|
+
zoom: true,
|
|
147
|
+
zoomStart: defaultZoomStart,
|
|
148
|
+
zoomEnd: defaultZoomEnd,
|
|
149
|
+
})
|
|
122
150
|
}
|
|
123
151
|
|
|
124
152
|
// Handle dataZoom event to update zoom range in store
|
|
@@ -133,10 +161,14 @@ export function ZoomToggle({
|
|
|
133
161
|
const end = zoomEvent.end
|
|
134
162
|
|
|
135
163
|
if (start !== undefined && end !== undefined) {
|
|
136
|
-
|
|
164
|
+
setWidget<ZoomState>(id, {
|
|
165
|
+
zoom: true,
|
|
166
|
+
zoomStart: start,
|
|
167
|
+
zoomEnd: end,
|
|
168
|
+
})
|
|
137
169
|
}
|
|
138
170
|
},
|
|
139
|
-
[
|
|
171
|
+
[id, setWidget],
|
|
140
172
|
)
|
|
141
173
|
|
|
142
174
|
// Register dataZoom event handler when zoom is enabled
|
|
@@ -3,11 +3,14 @@ import type { WidgetLoaderProps } from './types'
|
|
|
3
3
|
import { useWidgetStore } from '../stores/widget-store'
|
|
4
4
|
import type { WrapperState } from '../wrapper'
|
|
5
5
|
|
|
6
|
-
export function WidgetLoader<T>(props: WidgetLoaderProps<T>) {
|
|
6
|
+
export function WidgetLoader<T extends Record<string, unknown> = Record<string, unknown>>(props: WidgetLoaderProps<T>) {
|
|
7
7
|
const setWidget = useWidgetStore((state) => state.setWidget)
|
|
8
8
|
const executeToolPipeline = useWidgetStore(
|
|
9
9
|
(state) => state.executeToolPipeline,
|
|
10
10
|
)
|
|
11
|
+
const executeConfigPipeline = useWidgetStore(
|
|
12
|
+
(state) => state.executeConfigPipeline,
|
|
13
|
+
)
|
|
11
14
|
|
|
12
15
|
// Split into 3 effects for metadata and 1 for data pipeline:
|
|
13
16
|
// Each property that can be modified independently gets its own effect to avoid
|
|
@@ -35,21 +38,19 @@ export function WidgetLoader<T>(props: WidgetLoaderProps<T>) {
|
|
|
35
38
|
})
|
|
36
39
|
}, [props.id, props.isLoading, props.isFetching, props.error, setWidget])
|
|
37
40
|
|
|
38
|
-
// Effect 3: Config updates
|
|
41
|
+
// Effect 3: Config updates — run through config pipeline
|
|
39
42
|
useEffect(() => {
|
|
40
43
|
if (props.config) {
|
|
41
|
-
|
|
42
|
-
...props.config,
|
|
43
|
-
})
|
|
44
|
+
void executeConfigPipeline(props.id, props.config)
|
|
44
45
|
}
|
|
45
|
-
}, [props.id, props.config,
|
|
46
|
+
}, [props.id, props.config, executeConfigPipeline])
|
|
46
47
|
|
|
47
48
|
// Effect 4: Execute tool pipeline when props.data changes
|
|
48
49
|
useEffect(() => {
|
|
49
50
|
void executeToolPipeline(props.id, props.data)
|
|
50
51
|
}, [props.id, props.data, executeToolPipeline])
|
|
51
52
|
|
|
52
|
-
// Effect 5: Re-execute
|
|
53
|
+
// Effect 5: Re-execute pipelines when tool state changes (enabled/config)
|
|
53
54
|
useEffect(() => {
|
|
54
55
|
let prevTools = useWidgetStore.getState().widgets[props.id]?.registeredTools
|
|
55
56
|
|
|
@@ -60,11 +61,20 @@ export function WidgetLoader<T>(props: WidgetLoaderProps<T>) {
|
|
|
60
61
|
if (currentTools !== prevTools) {
|
|
61
62
|
prevTools = currentTools
|
|
62
63
|
void executeToolPipeline(props.id, props.data)
|
|
64
|
+
if (props.config) {
|
|
65
|
+
void executeConfigPipeline(props.id, props.config)
|
|
66
|
+
}
|
|
63
67
|
}
|
|
64
68
|
})
|
|
65
69
|
|
|
66
70
|
return unsubscribe
|
|
67
|
-
}, [
|
|
71
|
+
}, [
|
|
72
|
+
props.id,
|
|
73
|
+
props.data,
|
|
74
|
+
props.config,
|
|
75
|
+
executeToolPipeline,
|
|
76
|
+
executeConfigPipeline,
|
|
77
|
+
])
|
|
68
78
|
|
|
69
79
|
return props.children
|
|
70
80
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
2
|
import type { WidgetsStoreProps, WidgetState } from '../stores/types'
|
|
3
3
|
|
|
4
|
-
export interface WidgetLoaderProps<T> extends WidgetsStoreProps {
|
|
4
|
+
export interface WidgetLoaderProps<T extends Record<string, unknown> = Record<string, unknown>> extends WidgetsStoreProps {
|
|
5
5
|
children: ReactNode
|
|
6
6
|
config?: T
|
|
7
7
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { RefObject } from 'react'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Tool type determines which pipeline a tool participates in.
|
|
5
|
+
* - 'data': transforms widget data (default)
|
|
6
|
+
* - 'config': transforms widget config/option
|
|
7
|
+
*/
|
|
8
|
+
export type ToolType = 'data' | 'config'
|
|
9
|
+
|
|
3
10
|
export interface WidgetsStoreProps {
|
|
4
11
|
/** Unique identifier for the widget */
|
|
5
12
|
id: string
|
|
@@ -77,6 +84,8 @@ export interface ToolRegistration {
|
|
|
77
84
|
fn: ToolTransformFunction
|
|
78
85
|
/** Whether tool is currently enabled */
|
|
79
86
|
enabled: boolean
|
|
87
|
+
/** 'data' (default) transforms data, 'config' transforms widget config/option */
|
|
88
|
+
type?: ToolType
|
|
80
89
|
/** Tool-specific configuration */
|
|
81
90
|
config?: Record<string, unknown>
|
|
82
91
|
/**
|
|
@@ -184,6 +193,17 @@ export interface WidgetStoreActions {
|
|
|
184
193
|
* @param sourceData - Original data to transform
|
|
185
194
|
*/
|
|
186
195
|
executeToolPipeline: (widgetId: string, sourceData: unknown) => Promise<void>
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Execute the config transformation pipeline
|
|
199
|
+
* Applies config-type tools to the base config, then sets the result on the widget
|
|
200
|
+
* @param widgetId - Widget ID
|
|
201
|
+
* @param baseConfig - Base config to transform
|
|
202
|
+
*/
|
|
203
|
+
executeConfigPipeline: (
|
|
204
|
+
widgetId: string,
|
|
205
|
+
baseConfig: Record<string, unknown>,
|
|
206
|
+
) => Promise<void>
|
|
187
207
|
}
|
|
188
208
|
|
|
189
209
|
/**
|
|
@@ -465,6 +465,147 @@ describe('WidgetStore', () => {
|
|
|
465
465
|
})
|
|
466
466
|
})
|
|
467
467
|
|
|
468
|
+
describe('Config Tool Pipeline', () => {
|
|
469
|
+
const widgetId = 'test-widget-config'
|
|
470
|
+
|
|
471
|
+
beforeEach(() => {
|
|
472
|
+
useWidgetStore.getState().clearWidgets()
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
it('executes config tools and sets transformed config', async () => {
|
|
476
|
+
useWidgetStore.getState().setWidget(widgetId, {
|
|
477
|
+
type: 'bar',
|
|
478
|
+
isLoading: false,
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
const configTool: import('./types').ToolRegistration = {
|
|
482
|
+
id: 'stack-tool',
|
|
483
|
+
type: 'config',
|
|
484
|
+
order: 10,
|
|
485
|
+
enabled: true,
|
|
486
|
+
fn: (config) => {
|
|
487
|
+
const c = config as Record<string, unknown>
|
|
488
|
+
const option = c.option as { series?: { name: string }[] }
|
|
489
|
+
const series = option?.series ?? []
|
|
490
|
+
return {
|
|
491
|
+
...c,
|
|
492
|
+
option: {
|
|
493
|
+
...option,
|
|
494
|
+
series: series.map((s) => ({ ...s, stack: 'group' })),
|
|
495
|
+
},
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
useWidgetStore.getState().registerTool(widgetId, configTool)
|
|
501
|
+
|
|
502
|
+
await useWidgetStore.getState().executeConfigPipeline(widgetId, {
|
|
503
|
+
option: {
|
|
504
|
+
series: [{ name: 'Series 1' }, { name: 'Series 2' }],
|
|
505
|
+
},
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
509
|
+
const option = (widget as { option?: { series?: { stack?: string }[] } })
|
|
510
|
+
?.option
|
|
511
|
+
expect(option?.series?.[0]?.stack).toBe('group')
|
|
512
|
+
expect(option?.series?.[1]?.stack).toBe('group')
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
it('passes base config through when no config tools registered', async () => {
|
|
516
|
+
useWidgetStore.getState().setWidget(widgetId, {
|
|
517
|
+
type: 'bar',
|
|
518
|
+
isLoading: false,
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
await useWidgetStore.getState().executeConfigPipeline(widgetId, {
|
|
522
|
+
option: { series: [{ name: 'Series 1' }] },
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
526
|
+
const option = (widget as { option?: { series?: { name: string }[] } })
|
|
527
|
+
?.option
|
|
528
|
+
expect(option?.series?.[0]?.name).toBe('Series 1')
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
it('does not include config tools in data pipeline', async () => {
|
|
532
|
+
const executionOrder: string[] = []
|
|
533
|
+
|
|
534
|
+
useWidgetStore.getState().setWidget(widgetId, {
|
|
535
|
+
type: 'bar',
|
|
536
|
+
isLoading: false,
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
const dataTool: import('./types').ToolRegistration = {
|
|
540
|
+
id: 'data-tool',
|
|
541
|
+
type: 'data',
|
|
542
|
+
order: 10,
|
|
543
|
+
enabled: true,
|
|
544
|
+
fn: (data) => {
|
|
545
|
+
executionOrder.push('data-tool')
|
|
546
|
+
return data
|
|
547
|
+
},
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const configTool: import('./types').ToolRegistration = {
|
|
551
|
+
id: 'config-tool',
|
|
552
|
+
type: 'config',
|
|
553
|
+
order: 10,
|
|
554
|
+
enabled: true,
|
|
555
|
+
fn: (config) => {
|
|
556
|
+
executionOrder.push('config-tool')
|
|
557
|
+
return config
|
|
558
|
+
},
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
useWidgetStore.getState().registerTool(widgetId, dataTool)
|
|
562
|
+
useWidgetStore.getState().registerTool(widgetId, configTool)
|
|
563
|
+
|
|
564
|
+
await useWidgetStore.getState().executeToolPipeline(widgetId, {})
|
|
565
|
+
|
|
566
|
+
expect(executionOrder).toEqual(['data-tool'])
|
|
567
|
+
expect(executionOrder).not.toContain('config-tool')
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
it('respects disables across tool types', async () => {
|
|
571
|
+
const executionOrder: string[] = []
|
|
572
|
+
|
|
573
|
+
useWidgetStore.getState().setWidget(widgetId, {
|
|
574
|
+
type: 'bar',
|
|
575
|
+
isLoading: false,
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
const configTool: import('./types').ToolRegistration = {
|
|
579
|
+
id: 'config-tool',
|
|
580
|
+
type: 'config',
|
|
581
|
+
order: 10,
|
|
582
|
+
enabled: true,
|
|
583
|
+
fn: (config) => {
|
|
584
|
+
executionOrder.push('config-tool')
|
|
585
|
+
return config
|
|
586
|
+
},
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const disablerTool: import('./types').ToolRegistration = {
|
|
590
|
+
id: 'disabler',
|
|
591
|
+
type: 'data',
|
|
592
|
+
order: 10,
|
|
593
|
+
enabled: true,
|
|
594
|
+
fn: (data) => data,
|
|
595
|
+
disables: ['config-tool'],
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
useWidgetStore.getState().registerTool(widgetId, configTool)
|
|
599
|
+
useWidgetStore.getState().registerTool(widgetId, disablerTool)
|
|
600
|
+
|
|
601
|
+
await useWidgetStore.getState().executeConfigPipeline(widgetId, {
|
|
602
|
+
option: {},
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
expect(executionOrder).not.toContain('config-tool')
|
|
606
|
+
})
|
|
607
|
+
})
|
|
608
|
+
|
|
468
609
|
describe('Tool Dependency Management', () => {
|
|
469
610
|
const widgetId = 'test-widget-deps'
|
|
470
611
|
|