@carto/ps-react-ui 4.5.1 → 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/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 +3 -3
- 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/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 +4 -6
- package/src/widgets/category/index.ts +13 -14
- 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/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
package/dist/widgets/stores.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { u as o, w as r } from "../widget-store-
|
|
2
|
-
import { u as i } from "../use-widget-selector-
|
|
1
|
+
import { u as o, w as r } from "../widget-store-Bw5zRUGg.js";
|
|
2
|
+
import { u as i } from "../use-widget-selector-DFl2hW0R.js";
|
|
3
3
|
export {
|
|
4
4
|
i as useWidgetSelector,
|
|
5
5
|
o as useWidgetStore,
|
package/dist/widgets/table.js
CHANGED
|
@@ -6,10 +6,10 @@ import se from "@mui/icons-material/FirstPage";
|
|
|
6
6
|
import ce from "@mui/icons-material/KeyboardArrowLeft";
|
|
7
7
|
import ae from "@mui/icons-material/KeyboardArrowRight";
|
|
8
8
|
import fe from "@mui/icons-material/LastPage";
|
|
9
|
+
import { u as de } from "../use-widget-ref-Ddr_SlJJ.js";
|
|
9
10
|
import "react";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { u as B } from "../use-widget-selector-DqRmWQ1K.js";
|
|
11
|
+
import { w as x } from "../widget-store-Bw5zRUGg.js";
|
|
12
|
+
import { u as B } from "../use-widget-selector-DFl2hW0R.js";
|
|
13
13
|
import "zustand/shallow";
|
|
14
14
|
import "@mui/icons-material";
|
|
15
15
|
import { d as J, a as F } from "../exports-Cr43OCul.js";
|
|
@@ -2,11 +2,11 @@ import { jsxs as m, jsx as r } from "react/jsx-runtime";
|
|
|
2
2
|
import { c as y } from "react/compiler-runtime";
|
|
3
3
|
import "react";
|
|
4
4
|
import "echarts";
|
|
5
|
-
import "../widget-store-
|
|
5
|
+
import "../widget-store-Bw5zRUGg.js";
|
|
6
6
|
import "zustand/shallow";
|
|
7
7
|
import { g as b } from "../options-D9wflre6.js";
|
|
8
8
|
import { m as k } from "../utils-BOhInag6.js";
|
|
9
|
-
import { c as v, f as w } from "../download-config-
|
|
9
|
+
import { c as v, f as w } from "../download-config-C3I0jWIL.js";
|
|
10
10
|
import { g as L, b as C, a as I, d as S, e as _, c as A } from "../styles-Y8q7Jff3.js";
|
|
11
11
|
import { Box as l, Skeleton as s } from "@mui/material";
|
|
12
12
|
const Y = v(w);
|
package/dist/widgets/utils.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { d as i, a as n } from "../formatter-B9Bxn1k7.js";
|
|
2
|
-
import { c as f, f as p, s as c } from "../download-config-
|
|
2
|
+
import { c as f, f as p, s as c } from "../download-config-C3I0jWIL.js";
|
|
3
3
|
import { a as m, b as u, c as C, d as b, e as x, f as g, g as F, h as y, n as A } from "../styles-Y8q7Jff3.js";
|
|
4
4
|
function r({
|
|
5
5
|
type: a,
|
package/dist/widgets/wrapper.js
CHANGED
|
@@ -6,8 +6,8 @@ import { useState as H, useLayoutEffect as G } from "react";
|
|
|
6
6
|
import "../lasso-tool-BYbxrJ-7.js";
|
|
7
7
|
import "../cjs-D4KH3azB.js";
|
|
8
8
|
import { S as U } from "../smart-tooltip-D4vwQpFf.js";
|
|
9
|
-
import { u as V } from "../use-widget-selector-
|
|
10
|
-
import { w as M } from "../widget-store-
|
|
9
|
+
import { u as V } from "../use-widget-selector-DFl2hW0R.js";
|
|
10
|
+
import { w as M } from "../widget-store-Bw5zRUGg.js";
|
|
11
11
|
const k = {
|
|
12
12
|
root: {
|
|
13
13
|
".Mui-disabled .MuiAccordionSummary-expandIconWrapper": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carto/ps-react-ui",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.0",
|
|
4
4
|
"description": "CARTO's Professional Service React Material library",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"devDependencies": {
|
|
@@ -38,10 +38,6 @@
|
|
|
38
38
|
"import": "./dist/components.js",
|
|
39
39
|
"types": "./dist/types/components/index.d.ts"
|
|
40
40
|
},
|
|
41
|
-
"./widgets": {
|
|
42
|
-
"import": "./dist/widgets.js",
|
|
43
|
-
"types": "./dist/types/widgets/index.d.ts"
|
|
44
|
-
},
|
|
45
41
|
"./widgets/actions": {
|
|
46
42
|
"import": "./dist/widgets/actions.js",
|
|
47
43
|
"types": "./dist/types/widgets/actions/index.d.ts"
|
package/src/hooks/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Box, IconButton } from '@mui/material'
|
|
|
2
2
|
import { HighlightAltOutlined } from '@mui/icons-material'
|
|
3
3
|
import { useEffect, useCallback, useRef } from 'react'
|
|
4
4
|
import { widgetStoreActions } from '../../stores/widget-store'
|
|
5
|
-
import type { BrushSelectedItems, BrushToggleProps } from './types'
|
|
5
|
+
import type { BrushSelectedItems, BrushToggleProps, BrushState } from './types'
|
|
6
6
|
import { styles } from './style'
|
|
7
7
|
import { Tooltip } from '../../../components'
|
|
8
8
|
import { getEChartBrushConfig } from '../../echart/utils'
|
|
@@ -17,6 +17,9 @@ export const BRUSH_TOGGLE_TOOL_ID = 'brush-toggle'
|
|
|
17
17
|
* Registers as a config pipeline tool so that brush configuration is automatically
|
|
18
18
|
* re-applied when the base config is updated (e.g., by WidgetLoader).
|
|
19
19
|
*
|
|
20
|
+
* Brush state is stored in the widget store root, and the tool derives its
|
|
21
|
+
* `enabled` flag from the store. This keeps state accessible across component instances.
|
|
22
|
+
*
|
|
20
23
|
* When brush is active, users can drag across bars to select a range.
|
|
21
24
|
* Selection clearing is handled via the chart's brush interactions/toolbox configuration.
|
|
22
25
|
* Only intended for use in fullscreen ToolbarActions for bar and histogram widgets.
|
|
@@ -38,26 +41,19 @@ export function BrushToggle({
|
|
|
38
41
|
}: BrushToggleProps) {
|
|
39
42
|
const selected = useRef<BrushSelectedItems>({ dataIndex: [], seriesIndex: 0 })
|
|
40
43
|
|
|
44
|
+
// Read brush state from widget store root — single source of truth
|
|
41
45
|
const { brush } = useWidgetSelector(id, (w) => ({
|
|
42
|
-
brush: w
|
|
43
|
-
?.enabled,
|
|
46
|
+
brush: (w as BrushState | undefined)?.brush ?? false,
|
|
44
47
|
}))
|
|
45
48
|
|
|
46
|
-
const toggleTool = useCallback(
|
|
47
|
-
(value: boolean) => {
|
|
48
|
-
widgetStoreActions.setToolEnabled(id, BRUSH_TOGGLE_TOOL_ID, value)
|
|
49
|
-
},
|
|
50
|
-
[id],
|
|
51
|
-
)
|
|
52
|
-
|
|
53
49
|
const handleToggle = useCallback(() => {
|
|
54
50
|
const newBrush = !brush
|
|
55
|
-
|
|
51
|
+
widgetStoreActions.setWidget(id, { brush: newBrush })
|
|
56
52
|
|
|
57
53
|
if (newBrush) {
|
|
58
54
|
onBrushSelected?.({ dataIndex: [], seriesIndex: 0 }) // Clear selection when enabling brush
|
|
59
55
|
}
|
|
60
|
-
}, [brush,
|
|
56
|
+
}, [brush, id, onBrushSelected])
|
|
61
57
|
|
|
62
58
|
// Re-dispatch brush action after every ECharts render while brush is active
|
|
63
59
|
useEffect(() => {
|
|
@@ -135,22 +131,17 @@ export function BrushToggle({
|
|
|
135
131
|
|
|
136
132
|
const handleBrushEnd = useCallback(() => {
|
|
137
133
|
onBrushSelected?.(selected.current)
|
|
138
|
-
|
|
139
|
-
}, [onBrushSelected,
|
|
134
|
+
widgetStoreActions.setWidget(id, { brush: false }) // Disable brush after selection is made
|
|
135
|
+
}, [onBrushSelected, id])
|
|
140
136
|
|
|
141
|
-
// Register config tool
|
|
137
|
+
// Register config tool once — fn closure depends on event handlers.
|
|
138
|
+
// Enabled is synced separately to avoid full re-registration on toggle.
|
|
142
139
|
useEffect(() => {
|
|
143
|
-
const existingTool = widgetStoreActions
|
|
144
|
-
.getWidget(id)
|
|
145
|
-
?.registeredTools?.find((tool) => tool.id === BRUSH_TOGGLE_TOOL_ID)
|
|
146
|
-
|
|
147
|
-
const initialEnabled = existingTool?.enabled ?? false
|
|
148
|
-
|
|
149
140
|
widgetStoreActions.registerTool(id, {
|
|
150
141
|
id: BRUSH_TOGGLE_TOOL_ID,
|
|
151
142
|
type: 'config',
|
|
152
143
|
order: 25,
|
|
153
|
-
enabled:
|
|
144
|
+
enabled: false,
|
|
154
145
|
fn: (currentConfig) => {
|
|
155
146
|
const config = currentConfig as Record<string, unknown>
|
|
156
147
|
const option = config.option as EchartOptionsProps | undefined
|
|
@@ -178,6 +169,11 @@ export function BrushToggle({
|
|
|
178
169
|
return () => widgetStoreActions.unregisterTool(id, BRUSH_TOGGLE_TOOL_ID)
|
|
179
170
|
}, [id, handleBrushSelected, handleBrushEnd])
|
|
180
171
|
|
|
172
|
+
// Sync enabled from store — lightweight, no re-registration
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
widgetStoreActions.setToolEnabled(id, BRUSH_TOGGLE_TOOL_ID, brush)
|
|
175
|
+
}, [id, brush])
|
|
176
|
+
|
|
181
177
|
const enableLabel = labels?.enable ?? 'Enable brush selection'
|
|
182
178
|
const disableLabel = labels?.disable ?? 'Disable brush selection'
|
|
183
179
|
const tooltipLabel = brush ? disableLabel : enableLabel
|
|
@@ -217,7 +217,7 @@ describe('ChangeColumn', () => {
|
|
|
217
217
|
|
|
218
218
|
// When config columns match widget columns, fn returns the same reference
|
|
219
219
|
const input = { columns: mockColumns }
|
|
220
|
-
const result = tool?.fn(input
|
|
220
|
+
const result = tool?.fn(input)
|
|
221
221
|
expect(result).toBe(input)
|
|
222
222
|
})
|
|
223
223
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect, beforeEach, vi } from 'vitest'
|
|
2
2
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
3
3
|
import { Download } from './download'
|
|
4
|
-
import { useWidgetStore } from '../../../widgets'
|
|
4
|
+
import { useWidgetStore } from '../../../widgets/stores'
|
|
5
5
|
import type { DownloadItem } from './types'
|
|
6
6
|
|
|
7
7
|
describe('Download', () => {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/* Fullscreen Widget */
|
|
2
2
|
export { FullScreen } from './fullscreen/fullscreen'
|
|
3
|
-
export type {
|
|
3
|
+
export type {
|
|
4
|
+
FullScreenState,
|
|
5
|
+
FullScreenConfig,
|
|
6
|
+
FullScreenProps,
|
|
7
|
+
} from './fullscreen/types'
|
|
4
8
|
|
|
5
9
|
/* Download Widget */
|
|
6
10
|
export { Download } from './download/download'
|
|
@@ -13,7 +17,10 @@ export {
|
|
|
13
17
|
RELATIVE_DATA_TOOL_ID,
|
|
14
18
|
RELATIVE_DATA_CONFIG_TOOL_ID,
|
|
15
19
|
} from './relative-data/relative-data'
|
|
16
|
-
export type {
|
|
20
|
+
export type {
|
|
21
|
+
RelativeDataProps,
|
|
22
|
+
RelativeDataState,
|
|
23
|
+
} from './relative-data/types'
|
|
17
24
|
|
|
18
25
|
/* Zoom Toggle Widget */
|
|
19
26
|
export { ZoomToggle, ZOOM_TOGGLE_TOOL_ID } from './zoom-toggle/zoom-toggle'
|
|
@@ -35,6 +42,7 @@ export type {
|
|
|
35
42
|
SearcherProps,
|
|
36
43
|
SearcherFilterFn,
|
|
37
44
|
SearcherState,
|
|
45
|
+
SearcherStateProps,
|
|
38
46
|
} from './searcher/types'
|
|
39
47
|
|
|
40
48
|
/* Change Column Widget */
|
|
@@ -52,6 +60,7 @@ export {
|
|
|
52
60
|
export type {
|
|
53
61
|
LockSelectionProps,
|
|
54
62
|
LockSelectionState,
|
|
63
|
+
LockSelectionStateProps,
|
|
55
64
|
} from './lock-selection/types'
|
|
56
65
|
|
|
57
66
|
/* Brush Toggle Widget */
|
|
@@ -3,6 +3,7 @@ import { render, screen, fireEvent } from '@testing-library/react'
|
|
|
3
3
|
import { LockSelection, LOCK_SELECTION_TOOL_ID } from './lock-selection'
|
|
4
4
|
import { useWidgetStore } from '../../stores/widget-store'
|
|
5
5
|
import type { EchartWidgetData } from '../../echart/types'
|
|
6
|
+
import type { LockSelectionState } from './types'
|
|
6
7
|
import { WidgetLoader } from '../../loader/loader'
|
|
7
8
|
|
|
8
9
|
// Test data
|
|
@@ -110,6 +111,19 @@ describe('LockSelection', () => {
|
|
|
110
111
|
expect(tool?.enabled).toBe(false)
|
|
111
112
|
})
|
|
112
113
|
|
|
114
|
+
test('clears lock state when selectedItems becomes empty while locked', () => {
|
|
115
|
+
// Simulate remount: isLocked persists in store but selectedItems is now empty
|
|
116
|
+
useWidgetStore
|
|
117
|
+
.getState()
|
|
118
|
+
.setWidget(widgetId, { isLocked: true, lockedItems: ['Electronics'] })
|
|
119
|
+
|
|
120
|
+
render(<LockSelection id={widgetId} selectedItems={[]} />)
|
|
121
|
+
|
|
122
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
123
|
+
expect((widget as LockSelectionState | undefined)?.isLocked).toBe(false)
|
|
124
|
+
expect((widget as LockSelectionState | undefined)?.lockedItems).toEqual([])
|
|
125
|
+
})
|
|
126
|
+
|
|
113
127
|
test('has active state when locked', () => {
|
|
114
128
|
render(<LockSelection id={widgetId} selectedItems={['Electronics']} />)
|
|
115
129
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { IconButton } from '@mui/material'
|
|
2
2
|
import { CheckBoxOutlined } from '@mui/icons-material'
|
|
3
|
-
import { useCallback, useEffect
|
|
3
|
+
import { useCallback, useEffect } from 'react'
|
|
4
4
|
import type { LockSelectionProps, LockSelectionState } from './types'
|
|
5
5
|
import { actionButtonStyles } from '../shared/styles'
|
|
6
6
|
import { Tooltip } from '../../../components'
|
|
@@ -39,28 +39,35 @@ export function LockSelection({
|
|
|
39
39
|
}))
|
|
40
40
|
|
|
41
41
|
const isLocked = storeIsLocked ?? false
|
|
42
|
-
const lockedItems = useMemo(
|
|
43
|
-
() => (isLocked ? selectedItems : []),
|
|
44
|
-
[isLocked, selectedItems],
|
|
45
|
-
)
|
|
46
42
|
|
|
47
|
-
// Register tool
|
|
43
|
+
// Register tool once — fn reads lockedItems from the store at execution time
|
|
48
44
|
useEffect(() => {
|
|
49
45
|
widgetStoreActions.registerTool(id, {
|
|
50
46
|
id: LOCK_SELECTION_TOOL_ID,
|
|
51
47
|
order,
|
|
52
|
-
enabled:
|
|
53
|
-
fn: (data
|
|
54
|
-
const
|
|
48
|
+
enabled: false,
|
|
49
|
+
fn: (data) => {
|
|
50
|
+
const widget = widgetStoreActions.getWidget<LockSelectionState>(id)
|
|
51
|
+
const items = widget?.lockedItems ?? []
|
|
55
52
|
if (items.length === 0) return data
|
|
56
53
|
|
|
57
54
|
return filterDataByLockedItems(data as EchartWidgetData, items)
|
|
58
55
|
},
|
|
59
|
-
config: { lockedItems },
|
|
60
56
|
})
|
|
61
57
|
|
|
62
58
|
return () => widgetStoreActions.unregisterTool(id, LOCK_SELECTION_TOOL_ID)
|
|
63
|
-
}, [id, order
|
|
59
|
+
}, [id, order])
|
|
60
|
+
|
|
61
|
+
// Sync enabled from store — lightweight, no re-registration.
|
|
62
|
+
// When selectedItems is empty (e.g., remount with no active selection),
|
|
63
|
+
// disable the tool and clear stale lock state.
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (isLocked && selectedItems.length === 0) {
|
|
66
|
+
widgetStoreActions.setWidget(id, { isLocked: false, lockedItems: [] })
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
widgetStoreActions.setToolEnabled(id, LOCK_SELECTION_TOOL_ID, isLocked)
|
|
70
|
+
}, [id, isLocked, selectedItems.length])
|
|
64
71
|
|
|
65
72
|
const handleToggle = useCallback(() => {
|
|
66
73
|
if (isLocked) {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from './relative-data'
|
|
8
8
|
import { useWidgetStore } from '../../stores/widget-store'
|
|
9
9
|
import type { EchartWidgetData } from '../../echart/types'
|
|
10
|
+
import type { RelativeDataState } from './types'
|
|
10
11
|
|
|
11
12
|
describe('RelativeData', () => {
|
|
12
13
|
const widgetId = 'test-relative-widget'
|
|
@@ -227,23 +228,28 @@ describe('RelativeData', () => {
|
|
|
227
228
|
expect(button.hasAttribute('disabled')).toBeTruthy()
|
|
228
229
|
})
|
|
229
230
|
|
|
230
|
-
test('registers config
|
|
231
|
+
test('registers both data and config tools on mount', async () => {
|
|
231
232
|
render(<RelativeData id={widgetId} />)
|
|
232
233
|
|
|
233
234
|
await waitFor(() => {
|
|
234
235
|
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
235
|
-
const
|
|
236
|
+
const dataTool = widget?.registeredTools?.find(
|
|
237
|
+
(t) => t.id === RELATIVE_DATA_TOOL_ID,
|
|
238
|
+
)
|
|
239
|
+
const configTool = widget?.registeredTools?.find(
|
|
236
240
|
(t) => t.id === RELATIVE_DATA_CONFIG_TOOL_ID,
|
|
237
241
|
)
|
|
238
|
-
expect(
|
|
239
|
-
expect(
|
|
240
|
-
expect(
|
|
242
|
+
expect(dataTool).toBeTruthy()
|
|
243
|
+
expect(configTool).toBeTruthy()
|
|
244
|
+
expect(configTool?.type).toBe('config')
|
|
245
|
+
// Data tool disabled by default (isRelative=false)
|
|
246
|
+
expect(dataTool?.enabled).toBe(false)
|
|
247
|
+
// Config tool always enabled — it reads isRelative from store internally
|
|
248
|
+
expect(configTool?.enabled).toBe(true)
|
|
241
249
|
})
|
|
242
250
|
})
|
|
243
251
|
|
|
244
|
-
test('
|
|
245
|
-
useWidgetStore.getState().setWidget(widgetId, { max: 500 })
|
|
246
|
-
|
|
252
|
+
test('data tool enabled when toggled to relative mode', async () => {
|
|
247
253
|
render(<RelativeData id={widgetId} />)
|
|
248
254
|
|
|
249
255
|
const button = screen.getByRole('button')
|
|
@@ -251,16 +257,77 @@ describe('RelativeData', () => {
|
|
|
251
257
|
|
|
252
258
|
await waitFor(() => {
|
|
253
259
|
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
254
|
-
const
|
|
255
|
-
(t) => t.id ===
|
|
260
|
+
const dataTool = widget?.registeredTools?.find(
|
|
261
|
+
(t) => t.id === RELATIVE_DATA_TOOL_ID,
|
|
256
262
|
)
|
|
257
|
-
expect(
|
|
258
|
-
expect(tool?.config?.originalFormatter).toBeUndefined()
|
|
259
|
-
expect(tool?.config?.originalMax).toBe(500)
|
|
263
|
+
expect(dataTool?.enabled).toBe(true)
|
|
260
264
|
})
|
|
261
265
|
})
|
|
262
266
|
|
|
263
|
-
test('
|
|
267
|
+
test('stores isRelative in widget root state', () => {
|
|
268
|
+
render(<RelativeData id={widgetId} />)
|
|
269
|
+
|
|
270
|
+
// Default: isRelative=false in store
|
|
271
|
+
let widget = useWidgetStore.getState().getWidget(widgetId)
|
|
272
|
+
expect((widget as RelativeDataState | undefined)?.isRelative).toBe(false)
|
|
273
|
+
|
|
274
|
+
// Toggle to relative
|
|
275
|
+
const button = screen.getByRole('button')
|
|
276
|
+
fireEvent.click(button)
|
|
277
|
+
|
|
278
|
+
widget = useWidgetStore.getState().getWidget(widgetId)
|
|
279
|
+
expect((widget as RelativeDataState | undefined)?.isRelative).toBe(true)
|
|
280
|
+
|
|
281
|
+
// Toggle back
|
|
282
|
+
fireEvent.click(button)
|
|
283
|
+
|
|
284
|
+
widget = useWidgetStore.getState().getWidget(widgetId)
|
|
285
|
+
expect((widget as RelativeDataState | undefined)?.isRelative).toBe(false)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('config tool does not apply formatter when sourceData is empty', async () => {
|
|
289
|
+
const customFormatter = (value: number) => `$${value}`
|
|
290
|
+
useWidgetStore
|
|
291
|
+
.getState()
|
|
292
|
+
.setWidget(widgetId, { formatter: customFormatter, sourceData: [] })
|
|
293
|
+
|
|
294
|
+
render(<RelativeData id={widgetId} />)
|
|
295
|
+
|
|
296
|
+
const button = screen.getByRole('button')
|
|
297
|
+
fireEvent.click(button)
|
|
298
|
+
|
|
299
|
+
// Execute config pipeline — sourceData is empty, so formatter should pass through
|
|
300
|
+
await useWidgetStore
|
|
301
|
+
.getState()
|
|
302
|
+
.executeConfigPipeline(widgetId, { formatter: customFormatter, max: 200 })
|
|
303
|
+
|
|
304
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
305
|
+
expect(widget?.formatter).toBe(customFormatter)
|
|
306
|
+
expect((widget as unknown as Record<string, unknown>)?.max).toBe(200)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
test('config pipeline applies percentage formatter when in relative mode', async () => {
|
|
310
|
+
useWidgetStore
|
|
311
|
+
.getState()
|
|
312
|
+
.setWidget(widgetId, { max: 500, sourceData: mockData })
|
|
313
|
+
|
|
314
|
+
render(<RelativeData id={widgetId} />)
|
|
315
|
+
|
|
316
|
+
const button = screen.getByRole('button')
|
|
317
|
+
fireEvent.click(button)
|
|
318
|
+
|
|
319
|
+
// Execute config pipeline
|
|
320
|
+
await useWidgetStore
|
|
321
|
+
.getState()
|
|
322
|
+
.executeConfigPipeline(widgetId, { max: 500 })
|
|
323
|
+
|
|
324
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
325
|
+
expect((widget as unknown as Record<string, unknown>)?.max).toBe(100)
|
|
326
|
+
expect(widget?.formatter).toBeDefined()
|
|
327
|
+
expect(typeof widget?.formatter).toBe('function')
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
test('config tool passes config through unchanged when not in relative mode', async () => {
|
|
264
331
|
const customFormatter = (value: number) => `$${value}`
|
|
265
332
|
useWidgetStore
|
|
266
333
|
.getState()
|
|
@@ -268,20 +335,144 @@ describe('RelativeData', () => {
|
|
|
268
335
|
|
|
269
336
|
render(<RelativeData id={widgetId} />)
|
|
270
337
|
|
|
338
|
+
// Execute config pipeline — config tool is enabled but isRelative=false in store,
|
|
339
|
+
// so it passes through the config unchanged
|
|
340
|
+
await useWidgetStore
|
|
341
|
+
.getState()
|
|
342
|
+
.executeConfigPipeline(widgetId, { formatter: customFormatter, max: 200 })
|
|
343
|
+
|
|
344
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
345
|
+
expect(widget?.formatter).toBe(customFormatter)
|
|
346
|
+
expect((widget as unknown as Record<string, unknown>)?.max).toBe(200)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
test('restores original formatter/max when toggling back to absolute', async () => {
|
|
350
|
+
const customFormatter = (value: number) => `$${value}`
|
|
351
|
+
useWidgetStore.getState().setWidget(widgetId, {
|
|
352
|
+
formatter: customFormatter,
|
|
353
|
+
max: 200,
|
|
354
|
+
sourceData: mockData,
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
render(<RelativeData id={widgetId} />)
|
|
358
|
+
|
|
271
359
|
const button = screen.getByRole('button')
|
|
272
360
|
|
|
273
|
-
// Toggle to relative
|
|
361
|
+
// Toggle to relative — captures originalFormatter/originalMax in store
|
|
274
362
|
fireEvent.click(button)
|
|
275
|
-
|
|
363
|
+
|
|
364
|
+
await useWidgetStore
|
|
365
|
+
.getState()
|
|
366
|
+
.executeConfigPipeline(widgetId, { max: 200 })
|
|
367
|
+
|
|
368
|
+
let widget = useWidgetStore.getState().getWidget(widgetId)
|
|
369
|
+
expect((widget as unknown as Record<string, unknown>)?.max).toBe(100)
|
|
370
|
+
|
|
371
|
+
// Toggle back to absolute — config tool restores originals from store
|
|
372
|
+
fireEvent.click(button)
|
|
373
|
+
|
|
374
|
+
await useWidgetStore
|
|
375
|
+
.getState()
|
|
376
|
+
.executeConfigPipeline(widgetId, { max: 200 })
|
|
377
|
+
|
|
378
|
+
widget = useWidgetStore.getState().getWidget(widgetId)
|
|
379
|
+
expect(widget?.formatter).toBe(customFormatter)
|
|
380
|
+
expect((widget as unknown as Record<string, unknown>)?.max).toBe(200)
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
test('removes percentage formatter when toggling back with no original formatter', async () => {
|
|
384
|
+
// Widget has no formatter — originalFormatter will be captured as undefined
|
|
385
|
+
useWidgetStore.getState().setWidget(widgetId, { sourceData: mockData })
|
|
386
|
+
|
|
387
|
+
render(<RelativeData id={widgetId} />)
|
|
388
|
+
|
|
389
|
+
const button = screen.getByRole('button')
|
|
390
|
+
|
|
391
|
+
// Toggle to relative — captures originalFormatter=undefined
|
|
392
|
+
fireEvent.click(button)
|
|
393
|
+
|
|
394
|
+
await useWidgetStore.getState().executeConfigPipeline(widgetId, {})
|
|
395
|
+
|
|
396
|
+
let widget = useWidgetStore.getState().getWidget(widgetId)
|
|
397
|
+
expect(widget?.formatter).toBeDefined()
|
|
398
|
+
expect(typeof widget?.formatter).toBe('function')
|
|
399
|
+
|
|
400
|
+
// Toggle back to absolute — should remove percentage formatter
|
|
401
|
+
fireEvent.click(button)
|
|
402
|
+
|
|
403
|
+
await useWidgetStore.getState().executeConfigPipeline(widgetId, {})
|
|
404
|
+
|
|
405
|
+
widget = useWidgetStore.getState().getWidget(widgetId)
|
|
406
|
+
expect(widget?.formatter).toBeUndefined()
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
test('stores originalFormatter/originalMax in widget root on toggle', () => {
|
|
410
|
+
const customFormatter = (value: number) => `$${value}`
|
|
411
|
+
useWidgetStore
|
|
412
|
+
.getState()
|
|
413
|
+
.setWidget(widgetId, { formatter: customFormatter, max: 200 })
|
|
414
|
+
|
|
415
|
+
render(<RelativeData id={widgetId} />)
|
|
416
|
+
|
|
417
|
+
const button = screen.getByRole('button')
|
|
418
|
+
fireEvent.click(button)
|
|
419
|
+
|
|
420
|
+
const widget = useWidgetStore
|
|
421
|
+
.getState()
|
|
422
|
+
.getWidget<RelativeDataState>(widgetId)
|
|
423
|
+
expect(widget?.originalFormatter).toBe(customFormatter)
|
|
424
|
+
expect(widget?.originalMax).toBe(200)
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
test('restores original formatter/max when unmounting while in relative mode', async () => {
|
|
428
|
+
const customFormatter = (value: number) => `$${value}`
|
|
429
|
+
useWidgetStore
|
|
430
|
+
.getState()
|
|
431
|
+
.setWidget(widgetId, { formatter: customFormatter, max: 200 })
|
|
432
|
+
|
|
433
|
+
const { unmount } = render(<RelativeData id={widgetId} />)
|
|
434
|
+
|
|
435
|
+
const button = screen.getByRole('button')
|
|
436
|
+
|
|
437
|
+
// Toggle to relative — captures originalFormatter/originalMax in store
|
|
276
438
|
fireEvent.click(button)
|
|
277
439
|
|
|
440
|
+
await waitFor(() => {
|
|
441
|
+
const widget = useWidgetStore
|
|
442
|
+
.getState()
|
|
443
|
+
.getWidget<RelativeDataState>(widgetId)
|
|
444
|
+
expect(widget?.isRelative).toBe(true)
|
|
445
|
+
expect(widget?.originalFormatter).toBe(customFormatter)
|
|
446
|
+
expect(widget?.originalMax).toBe(200)
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
// Unmount while still in relative mode
|
|
450
|
+
unmount()
|
|
451
|
+
|
|
452
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
453
|
+
expect(widget?.formatter).toBe(customFormatter)
|
|
454
|
+
expect((widget as unknown as Record<string, unknown>)?.max).toBe(200)
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
test('unregisters tools on unmount', async () => {
|
|
458
|
+
const { unmount } = render(<RelativeData id={widgetId} />)
|
|
459
|
+
|
|
460
|
+
await waitFor(() => {
|
|
461
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
462
|
+
expect(widget?.registeredTools?.length).toBeGreaterThan(0)
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
unmount()
|
|
466
|
+
|
|
278
467
|
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
279
|
-
const
|
|
468
|
+
const dataTool = widget?.registeredTools?.find(
|
|
469
|
+
(t) => t.id === RELATIVE_DATA_TOOL_ID,
|
|
470
|
+
)
|
|
471
|
+
const configTool = widget?.registeredTools?.find(
|
|
280
472
|
(t) => t.id === RELATIVE_DATA_CONFIG_TOOL_ID,
|
|
281
473
|
)
|
|
282
|
-
expect(
|
|
283
|
-
expect(
|
|
284
|
-
expect(tool?.config?.originalMax).toBe(200)
|
|
474
|
+
expect(dataTool).toBeUndefined()
|
|
475
|
+
expect(configTool).toBeUndefined()
|
|
285
476
|
})
|
|
286
477
|
|
|
287
478
|
test('recalculates relative values when store data changes externally while in relative mode', async () => {
|