@carto/ps-react-ui 4.3.6 → 4.3.7
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 +123 -123
- package/dist/components.js.map +1 -1
- package/dist/error-CEkRPccv.js +39 -0
- package/dist/error-CEkRPccv.js.map +1 -0
- package/dist/{lasso-tool-BctzdzBu.js → lasso-tool-jl4YK02H.js} +19 -19
- package/dist/lasso-tool-jl4YK02H.js.map +1 -0
- package/dist/no-data-hR3KcJ-_.js +60 -0
- package/dist/no-data-hR3KcJ-_.js.map +1 -0
- package/dist/{row-D3uVFImu.js → row-BKmVAUN5.js} +2 -2
- package/dist/{row-D3uVFImu.js.map → row-BKmVAUN5.js.map} +1 -1
- package/dist/{series-BAImrSBo.js → series-D1pynfeh.js} +3 -3
- package/dist/{series-BAImrSBo.js.map → series-D1pynfeh.js.map} +1 -1
- package/dist/{styles-CCZnY17y.js → styles-DrPyd0y5.js} +28 -22
- package/dist/styles-DrPyd0y5.js.map +1 -0
- package/dist/types/components/lasso-tool/types.d.ts +1 -1
- package/dist/types/widgets/_shared/chart-config/index.d.ts +1 -1
- package/dist/types/widgets/_shared/chart-config/option-builders.d.ts +7 -0
- package/dist/types/widgets/_shared/chart-config/option-builders.test.d.ts +1 -0
- package/dist/types/widgets/actions/index.d.ts +2 -2
- package/dist/types/widgets/actions/lock-selection/types.d.ts +0 -13
- package/dist/types/widgets/actions/relative-data/types.d.ts +0 -4
- package/dist/types/widgets/actions/searcher/types.d.ts +0 -2
- package/dist/types/widgets/actions/stack-toggle/types.d.ts +0 -4
- package/dist/types/widgets/echart/types.d.ts +0 -4
- package/dist/types/widgets/echart/utils.d.ts +2 -1
- package/dist/types/widgets/error/error.d.ts +1 -1
- package/dist/types/widgets/error/types.d.ts +8 -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/types.d.ts +1 -1
- package/dist/{use-widget-ref-B8x4sHIj.js → use-widget-ref-P-2i0MJG.js} +2 -2
- package/dist/{use-widget-ref-B8x4sHIj.js.map → use-widget-ref-P-2i0MJG.js.map} +1 -1
- package/dist/{utils-D3-eQyDR.js → utils-idmvq0Oa.js} +17 -16
- package/dist/utils-idmvq0Oa.js.map +1 -0
- package/dist/{widget-store-Dn0Bnc4h.js → widget-store-CzDt8oSK.js} +31 -46
- package/dist/widget-store-CzDt8oSK.js.map +1 -0
- package/dist/widgets/actions.js +690 -716
- package/dist/widgets/actions.js.map +1 -1
- package/dist/widgets/bar.js +67 -63
- package/dist/widgets/bar.js.map +1 -1
- package/dist/widgets/category.js +250 -241
- package/dist/widgets/category.js.map +1 -1
- package/dist/widgets/echart.js +93 -100
- package/dist/widgets/echart.js.map +1 -1
- package/dist/widgets/error.js +1 -1
- package/dist/widgets/formula.js +64 -72
- package/dist/widgets/formula.js.map +1 -1
- package/dist/widgets/histogram.js +75 -73
- package/dist/widgets/histogram.js.map +1 -1
- package/dist/widgets/loader.js +1 -1
- 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 +4 -4
- package/dist/widgets/range.js +97 -105
- package/dist/widgets/range.js.map +1 -1
- package/dist/widgets/scatterplot.js +8 -8
- package/dist/widgets/skeleton-loader.js +1 -1
- package/dist/widgets/spread.js +84 -100
- package/dist/widgets/spread.js.map +1 -1
- package/dist/widgets/stores.js +1 -1
- package/dist/widgets/table.js +493 -485
- package/dist/widgets/table.js.map +1 -1
- package/dist/widgets/timeseries.js +4 -4
- package/dist/widgets/wrapper.js +156 -156
- package/dist/widgets/wrapper.js.map +1 -1
- package/dist/widgets.js +4 -4
- package/package.json +3 -3
- package/src/components/lasso-tool/lasso-tool-inline.tsx +19 -17
- package/src/components/lasso-tool/lasso-tool.tsx +22 -20
- package/src/components/lasso-tool/types.ts +4 -3
- package/src/widgets/_shared/chart-config/index.ts +1 -0
- package/src/widgets/_shared/chart-config/option-builders.test.ts +40 -0
- package/src/widgets/_shared/chart-config/option-builders.ts +12 -0
- package/src/widgets/actions/fullscreen/fullscreen.tsx +5 -8
- package/src/widgets/actions/index.ts +2 -5
- package/src/widgets/actions/lock-selection/lock-selection.test.tsx +28 -30
- package/src/widgets/actions/lock-selection/lock-selection.tsx +25 -26
- package/src/widgets/actions/lock-selection/types.ts +0 -17
- package/src/widgets/actions/relative-data/relative-data.test.tsx +13 -13
- package/src/widgets/actions/relative-data/relative-data.tsx +18 -21
- package/src/widgets/actions/relative-data/types.ts +0 -7
- package/src/widgets/actions/searcher/searcher.tsx +40 -22
- package/src/widgets/actions/searcher/types.ts +0 -2
- package/src/widgets/actions/stack-toggle/stack-toggle.test.tsx +19 -9
- package/src/widgets/actions/stack-toggle/stack-toggle.tsx +32 -22
- package/src/widgets/actions/stack-toggle/types.ts +0 -8
- package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +113 -95
- package/src/widgets/bar/config.ts +37 -28
- package/src/widgets/category/category-ui.tsx +25 -22
- package/src/widgets/echart/echart-ui.test.tsx +3 -18
- package/src/widgets/echart/echart-ui.tsx +4 -22
- package/src/widgets/echart/echart.test.tsx +9 -25
- package/src/widgets/echart/echart.tsx +36 -29
- package/src/widgets/echart/types.ts +0 -4
- package/src/widgets/echart/utils.ts +3 -1
- package/src/widgets/error/error.tsx +17 -14
- package/src/widgets/error/types.ts +10 -0
- package/src/widgets/formula/components/value.tsx +13 -13
- package/src/widgets/histogram/config.ts +36 -29
- package/src/widgets/loader/loader.tsx +3 -1
- package/src/widgets/loader/types.ts +3 -1
- package/src/widgets/no-data/no-data.tsx +8 -11
- package/src/widgets/range/components/range-item.tsx +9 -13
- package/src/widgets/spread/components/max-value.tsx +13 -13
- package/src/widgets/spread/components/min-value.tsx +13 -13
- package/src/widgets/stores/types.ts +1 -4
- package/src/widgets/stores/widget-store.ts +1 -27
- package/src/widgets/table/hooks/use-pagination.ts +44 -35
- package/src/widgets/table/hooks/use-sort.ts +25 -23
- package/src/widgets/wrapper/wrapper-ui.tsx +16 -17
- package/dist/error-piB8FwYO.js +0 -38
- package/dist/error-piB8FwYO.js.map +0 -1
- package/dist/lasso-tool-BctzdzBu.js.map +0 -1
- package/dist/no-data-jdlbMef0.js +0 -61
- package/dist/no-data-jdlbMef0.js.map +0 -1
- package/dist/styles-CCZnY17y.js.map +0 -1
- package/dist/utils-D3-eQyDR.js.map +0 -1
- package/dist/widget-store-Dn0Bnc4h.js.map +0 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { niceNum } from './option-builders'
|
|
3
|
+
|
|
4
|
+
describe('niceNum', () => {
|
|
5
|
+
it('should return 0 for 0', () => {
|
|
6
|
+
expect(niceNum(0)).toBe(0)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('should return 0 for negative values', () => {
|
|
10
|
+
expect(niceNum(-5)).toBe(0)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('should round 547 to 600', () => {
|
|
14
|
+
expect(niceNum(547)).toBe(600)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should keep 200 as 200', () => {
|
|
18
|
+
expect(niceNum(200)).toBe(200)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should round 1200 to 2000', () => {
|
|
22
|
+
expect(niceNum(1200)).toBe(2000)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should round 18 to 20', () => {
|
|
26
|
+
expect(niceNum(18)).toBe(20)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should keep 5 as 5', () => {
|
|
30
|
+
expect(niceNum(5)).toBe(5)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should round 350 to 400', () => {
|
|
34
|
+
expect(niceNum(350)).toBe(400)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should keep 1 as 1', () => {
|
|
38
|
+
expect(niceNum(1)).toBe(1)
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -9,6 +9,18 @@ import type {
|
|
|
9
9
|
* Shared EChart configuration builders for chart widgets
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Rounds a value up to the nearest "nice" number.
|
|
14
|
+
* A nice number is a multiple of 10^floor(log10(value)).
|
|
15
|
+
*
|
|
16
|
+
* Examples: 547 → 600, 200 → 200, 1200 → 2000, 18 → 20, 5 → 5
|
|
17
|
+
*/
|
|
18
|
+
export function niceNum(value: number): number {
|
|
19
|
+
if (value <= 0) return 0
|
|
20
|
+
const base = Math.pow(10, Math.floor(Math.log10(value)))
|
|
21
|
+
return Math.ceil(value / base) * base
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
/**
|
|
13
25
|
* Builds standard legend configuration for chart widgets
|
|
14
26
|
*
|
|
@@ -23,14 +23,11 @@ export function FullScreen({
|
|
|
23
23
|
IconButtonProps,
|
|
24
24
|
DialogContentProps: { sx, ...DialogContentProps } = {},
|
|
25
25
|
}: FullScreenProps) {
|
|
26
|
-
const
|
|
27
|
-
useShallow((state) =>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
title: widget?.title,
|
|
32
|
-
}
|
|
33
|
-
}),
|
|
26
|
+
const isFullScreen = useWidgetStore(
|
|
27
|
+
useShallow((state) => state.getWidget<FullScreenState>(id)?.isFullScreen),
|
|
28
|
+
)
|
|
29
|
+
const title = useWidgetStore(
|
|
30
|
+
useShallow((state) => state.getWidget<FullScreenState>(id)?.title),
|
|
34
31
|
)
|
|
35
32
|
const setWidget = useWidgetStore((state) => state.setWidget)
|
|
36
33
|
|
|
@@ -24,7 +24,7 @@ export type {
|
|
|
24
24
|
|
|
25
25
|
/* Stack Toggle Widget */
|
|
26
26
|
export { StackToggle, STACK_TOGGLE_TOOL_ID } from './stack-toggle/stack-toggle'
|
|
27
|
-
export type { StackToggleProps
|
|
27
|
+
export type { StackToggleProps } from './stack-toggle/types'
|
|
28
28
|
|
|
29
29
|
/* Searcher Toggle Widget */
|
|
30
30
|
export { Searcher, SEARCHER_TOOL_ID } from './searcher/searcher'
|
|
@@ -45,7 +45,4 @@ export {
|
|
|
45
45
|
LockSelection,
|
|
46
46
|
LOCK_SELECTION_TOOL_ID,
|
|
47
47
|
} from './lock-selection/lock-selection'
|
|
48
|
-
export type {
|
|
49
|
-
LockSelectionProps,
|
|
50
|
-
LockSelectionState,
|
|
51
|
-
} from './lock-selection/types'
|
|
48
|
+
export type { LockSelectionProps } from './lock-selection/types'
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { describe, test, expect, beforeEach } from 'vitest'
|
|
2
2
|
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
-
import { LockSelection } from './lock-selection'
|
|
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'
|
|
7
6
|
import { WidgetLoader } from '../../loader/loader'
|
|
8
7
|
|
|
9
8
|
// Test data
|
|
@@ -45,25 +44,29 @@ describe('LockSelection', () => {
|
|
|
45
44
|
})
|
|
46
45
|
|
|
47
46
|
test('shows unlock tooltip when locked', () => {
|
|
48
|
-
// Pre-set the store to locked state
|
|
49
|
-
useWidgetStore.getState().setWidget(widgetId, { isLocked: true })
|
|
50
|
-
|
|
51
47
|
render(<LockSelection id={widgetId} selectedItems={['Electronics']} />)
|
|
52
48
|
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
// Lock by clicking the button
|
|
50
|
+
const button = screen.getByRole('button')
|
|
51
|
+
fireEvent.click(button)
|
|
52
|
+
|
|
53
|
+
const unlockedButton = screen.getByRole('button', {
|
|
54
|
+
name: 'Unlock selection',
|
|
55
|
+
})
|
|
56
|
+
expect(unlockedButton).toBeTruthy()
|
|
55
57
|
})
|
|
56
58
|
|
|
57
|
-
test('toggles isLocked in
|
|
59
|
+
test('toggles isLocked in tool state when clicked', () => {
|
|
58
60
|
render(<LockSelection id={widgetId} selectedItems={['Electronics']} />)
|
|
59
61
|
|
|
60
62
|
const button = screen.getByRole('button')
|
|
61
63
|
fireEvent.click(button)
|
|
62
64
|
|
|
63
|
-
const widget = useWidgetStore
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
66
|
+
const tool = widget?.registeredTools?.find(
|
|
67
|
+
(t) => t.id === LOCK_SELECTION_TOOL_ID,
|
|
68
|
+
)
|
|
69
|
+
expect(tool?.enabled).toBe(true)
|
|
67
70
|
})
|
|
68
71
|
|
|
69
72
|
test('filters data in store when locking', async () => {
|
|
@@ -86,36 +89,33 @@ describe('LockSelection', () => {
|
|
|
86
89
|
|
|
87
90
|
await useWidgetStore.getState().executeToolPipeline(widgetId, mockData)
|
|
88
91
|
|
|
89
|
-
const widget = useWidgetStore
|
|
90
|
-
.getState()
|
|
91
|
-
.getWidget<LockSelectionState>(widgetId)
|
|
92
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
92
93
|
expect(widget?.data).toEqual([[{ name: 'Electronics', value: 440 }]])
|
|
93
94
|
})
|
|
94
95
|
|
|
95
96
|
test('clears locked items when unlocking', () => {
|
|
96
|
-
// Pre-set the store to locked state
|
|
97
|
-
useWidgetStore.getState().setWidget(widgetId, {
|
|
98
|
-
isLocked: true,
|
|
99
|
-
lockedItems: ['Electronics'],
|
|
100
|
-
})
|
|
101
|
-
|
|
102
97
|
render(<LockSelection id={widgetId} selectedItems={['Electronics']} />)
|
|
103
98
|
|
|
99
|
+
// Lock first
|
|
104
100
|
const button = screen.getByRole('button')
|
|
105
101
|
fireEvent.click(button)
|
|
106
102
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
// Then unlock
|
|
104
|
+
fireEvent.click(button)
|
|
105
|
+
|
|
106
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
107
|
+
const tool = widget?.registeredTools?.find(
|
|
108
|
+
(t) => t.id === LOCK_SELECTION_TOOL_ID,
|
|
109
|
+
)
|
|
110
|
+
expect(tool?.enabled).toBe(false)
|
|
111
111
|
})
|
|
112
112
|
|
|
113
113
|
test('has active state when locked', () => {
|
|
114
|
-
useWidgetStore.getState().setWidget(widgetId, { isLocked: true })
|
|
115
|
-
|
|
116
114
|
render(<LockSelection id={widgetId} selectedItems={['Electronics']} />)
|
|
117
115
|
|
|
118
116
|
const button = screen.getByRole('button')
|
|
117
|
+
fireEvent.click(button)
|
|
118
|
+
|
|
119
119
|
expect(button.getAttribute('data-active')).toBe('true')
|
|
120
120
|
})
|
|
121
121
|
|
|
@@ -175,9 +175,7 @@ describe('LockSelection', () => {
|
|
|
175
175
|
.getState()
|
|
176
176
|
.executeToolPipeline(widgetId, multiSeriesData)
|
|
177
177
|
|
|
178
|
-
const widget = useWidgetStore
|
|
179
|
-
.getState()
|
|
180
|
-
.getWidget<LockSelectionState>(widgetId)
|
|
178
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
181
179
|
expect(widget?.data).toEqual([
|
|
182
180
|
[{ name: 'Electronics', value: 440 }],
|
|
183
181
|
[{ name: 'Electronics', value: 520 }],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { IconButton } from '@mui/material'
|
|
2
2
|
import { CheckBoxOutlined } from '@mui/icons-material'
|
|
3
|
-
import { useCallback, useEffect
|
|
4
|
-
import type { LockSelectionProps
|
|
3
|
+
import { useCallback, useEffect } from 'react'
|
|
4
|
+
import type { LockSelectionProps } from './types'
|
|
5
5
|
import { actionButtonStyles } from '../shared/styles'
|
|
6
6
|
import { Tooltip } from '../../../components'
|
|
7
7
|
import { useWidgetStore } from '../../stores/widget-store'
|
|
@@ -34,61 +34,60 @@ export function LockSelection({
|
|
|
34
34
|
Icon,
|
|
35
35
|
IconButtonProps,
|
|
36
36
|
}: LockSelectionProps) {
|
|
37
|
-
const
|
|
37
|
+
const getWidget = useWidgetStore((state) => state.getWidget)
|
|
38
38
|
const registerTool = useWidgetStore((state) => state.registerTool)
|
|
39
39
|
const unregisterTool = useWidgetStore((state) => state.unregisterTool)
|
|
40
40
|
const setToolEnabled = useWidgetStore((state) => state.setToolEnabled)
|
|
41
41
|
const updateToolConfig = useWidgetStore((state) => state.updateToolConfig)
|
|
42
42
|
|
|
43
|
-
const
|
|
44
|
-
useShallow((state) =>
|
|
43
|
+
const lockTool = useWidgetStore(
|
|
44
|
+
useShallow((state) => {
|
|
45
|
+
const tools = state.getWidget(id)?.registeredTools ?? []
|
|
46
|
+
return tools.find((tool) => tool.id === LOCK_SELECTION_TOOL_ID)
|
|
47
|
+
}),
|
|
45
48
|
)
|
|
46
49
|
|
|
47
|
-
const isLocked =
|
|
48
|
-
const lockedItems = useMemo(
|
|
49
|
-
() => (isLocked ? selectedItems : []),
|
|
50
|
-
[isLocked, selectedItems],
|
|
51
|
-
)
|
|
50
|
+
const isLocked = lockTool?.enabled ?? false
|
|
52
51
|
|
|
53
52
|
// Register tool on mount
|
|
54
53
|
useEffect(() => {
|
|
54
|
+
const existingTool = getWidget(id)?.registeredTools?.find(
|
|
55
|
+
(tool) => tool.id === LOCK_SELECTION_TOOL_ID,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const initialEnabled = existingTool?.enabled ?? false
|
|
59
|
+
const initialLockedItems =
|
|
60
|
+
(existingTool?.config?.lockedItems as string[]) ?? []
|
|
61
|
+
|
|
55
62
|
registerTool(id, {
|
|
56
63
|
id: LOCK_SELECTION_TOOL_ID,
|
|
57
64
|
order,
|
|
58
|
-
enabled:
|
|
65
|
+
enabled: initialEnabled,
|
|
59
66
|
fn: (data, config) => {
|
|
60
67
|
const items = (config?.lockedItems as string[]) || []
|
|
61
68
|
if (items.length === 0) return data
|
|
62
69
|
|
|
63
70
|
return filterDataByLockedItems(data as EchartWidgetData, items)
|
|
64
71
|
},
|
|
65
|
-
config: { lockedItems },
|
|
72
|
+
config: { lockedItems: initialLockedItems },
|
|
66
73
|
})
|
|
67
74
|
|
|
68
75
|
return () => unregisterTool(id, LOCK_SELECTION_TOOL_ID)
|
|
69
|
-
}, [id, order, registerTool, unregisterTool,
|
|
70
|
-
|
|
71
|
-
// Update enabled flag and config when they change
|
|
72
|
-
useEffect(() => {
|
|
73
|
-
setToolEnabled(id, LOCK_SELECTION_TOOL_ID, isLocked)
|
|
74
|
-
updateToolConfig(id, LOCK_SELECTION_TOOL_ID, { lockedItems })
|
|
75
|
-
}, [id, isLocked, lockedItems, setToolEnabled, updateToolConfig])
|
|
76
|
+
}, [id, order, registerTool, unregisterTool, getWidget])
|
|
76
77
|
|
|
77
78
|
const handleToggle = useCallback(() => {
|
|
78
79
|
if (isLocked) {
|
|
79
80
|
// Unlock: clear locked items and disable tool
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
lockedItems: [],
|
|
83
|
-
})
|
|
81
|
+
setToolEnabled(id, LOCK_SELECTION_TOOL_ID, false)
|
|
82
|
+
updateToolConfig(id, LOCK_SELECTION_TOOL_ID, { lockedItems: [] })
|
|
84
83
|
} else {
|
|
85
84
|
// Lock: save selected items and enable tool
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
setToolEnabled(id, LOCK_SELECTION_TOOL_ID, true)
|
|
86
|
+
updateToolConfig(id, LOCK_SELECTION_TOOL_ID, {
|
|
88
87
|
lockedItems: selectedItems,
|
|
89
88
|
})
|
|
90
89
|
}
|
|
91
|
-
}, [id, isLocked, selectedItems,
|
|
90
|
+
}, [id, isLocked, selectedItems, setToolEnabled, updateToolConfig])
|
|
92
91
|
|
|
93
92
|
// Don't render if no selections
|
|
94
93
|
if (selectedItems.length === 0) {
|
|
@@ -1,22 +1,5 @@
|
|
|
1
1
|
import type { IconButtonProps } from '@mui/material'
|
|
2
2
|
import type { ReactNode } from 'react'
|
|
3
|
-
import type { BaseWidgetState } from '../../stores/types'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Lock selection specific state properties.
|
|
7
|
-
*/
|
|
8
|
-
export interface LockSelectionStateProps {
|
|
9
|
-
/** Whether the selection is currently locked */
|
|
10
|
-
isLocked?: boolean
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Widget state extension for lock selection functionality.
|
|
15
|
-
* Extends the base widget state with lock-specific properties.
|
|
16
|
-
*/
|
|
17
|
-
export type LockSelectionState<T = object> = BaseWidgetState<
|
|
18
|
-
T & LockSelectionStateProps
|
|
19
|
-
>
|
|
20
3
|
|
|
21
4
|
export interface LockSelectionProps {
|
|
22
5
|
/** Widget ID to store lock selection state */
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { describe, test, expect, beforeEach } from 'vitest'
|
|
2
2
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
3
|
-
import { RelativeData } from './relative-data'
|
|
3
|
+
import { RelativeData, RELATIVE_DATA_TOOL_ID } from './relative-data'
|
|
4
4
|
import { useWidgetStore } from '../../stores/widget-store'
|
|
5
5
|
import type { EchartWidgetData } from '../../echart/types'
|
|
6
|
-
import type { RelativeDataState } from './types'
|
|
7
6
|
|
|
8
7
|
describe('RelativeData', () => {
|
|
9
8
|
const widgetId = 'test-relative-widget'
|
|
@@ -94,25 +93,27 @@ describe('RelativeData', () => {
|
|
|
94
93
|
expect(button.getAttribute('aria-label')).toBe('Show absolute values')
|
|
95
94
|
})
|
|
96
95
|
|
|
97
|
-
test('initializes
|
|
96
|
+
test('initializes tool state with defaultIsRelative on mount', async () => {
|
|
98
97
|
render(<RelativeData id={widgetId} defaultIsRelative />)
|
|
99
98
|
|
|
100
99
|
await waitFor(() => {
|
|
101
|
-
const widget = useWidgetStore
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
101
|
+
const tool = widget?.registeredTools?.find(
|
|
102
|
+
(t) => t.id === RELATIVE_DATA_TOOL_ID,
|
|
103
|
+
)
|
|
104
|
+
expect(tool?.enabled).toBe(true)
|
|
105
105
|
})
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
-
test('initializes
|
|
108
|
+
test('initializes tool state with false when defaultIsRelative is false', async () => {
|
|
109
109
|
render(<RelativeData id={widgetId} defaultIsRelative={false} />)
|
|
110
110
|
|
|
111
111
|
await waitFor(() => {
|
|
112
|
-
const widget = useWidgetStore
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
113
|
+
const tool = widget?.registeredTools?.find(
|
|
114
|
+
(t) => t.id === RELATIVE_DATA_TOOL_ID,
|
|
115
|
+
)
|
|
116
|
+
expect(tool?.enabled).toBe(false)
|
|
116
117
|
})
|
|
117
118
|
})
|
|
118
119
|
|
|
@@ -233,7 +234,6 @@ describe('RelativeData', () => {
|
|
|
233
234
|
// Pre-populate the widget in the store with relative data
|
|
234
235
|
useWidgetStore.getState().setWidget(widgetId, {
|
|
235
236
|
data: initialData,
|
|
236
|
-
isRelative: true,
|
|
237
237
|
})
|
|
238
238
|
|
|
239
239
|
render(<RelativeData id={widgetId} defaultIsRelative />)
|
|
@@ -2,11 +2,12 @@ import { IconButton } from '@mui/material'
|
|
|
2
2
|
import { PercentOutlined } from '@mui/icons-material'
|
|
3
3
|
import { useCallback, useEffect, useRef } from 'react'
|
|
4
4
|
import { useWidgetStore } from '../../stores/widget-store'
|
|
5
|
-
import type { RelativeDataProps
|
|
5
|
+
import type { RelativeDataProps } from './types'
|
|
6
6
|
import { actionButtonStyles } from '../shared/styles'
|
|
7
7
|
import { Tooltip } from '../../../components'
|
|
8
8
|
import { calculateTotal, toRelativeData } from './utils'
|
|
9
9
|
import type { EchartWidgetData } from '../../../widgets/echart'
|
|
10
|
+
import { useShallow } from 'zustand/shallow'
|
|
10
11
|
|
|
11
12
|
export const RELATIVE_DATA_TOOL_ID = 'relative-data'
|
|
12
13
|
|
|
@@ -43,26 +44,27 @@ export function RelativeData({
|
|
|
43
44
|
const unregisterTool = useWidgetStore((state) => state.unregisterTool)
|
|
44
45
|
const setToolEnabled = useWidgetStore((state) => state.setToolEnabled)
|
|
45
46
|
|
|
46
|
-
const
|
|
47
|
-
(state) =>
|
|
47
|
+
const relativeTool = useWidgetStore(
|
|
48
|
+
useShallow((state) => {
|
|
49
|
+
const tools = state.getWidget(id)?.registeredTools ?? []
|
|
50
|
+
return tools.find((tool) => tool.id === RELATIVE_DATA_TOOL_ID)
|
|
51
|
+
}),
|
|
48
52
|
)
|
|
49
53
|
|
|
50
|
-
const isRelative =
|
|
51
|
-
|
|
52
|
-
// Initialize store with default value on mount
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
const currentValue = getWidget<RelativeDataState>(id)?.isRelative
|
|
55
|
-
if (currentValue === undefined) {
|
|
56
|
-
setWidget(id, { isRelative: defaultIsRelative })
|
|
57
|
-
}
|
|
58
|
-
}, [defaultIsRelative, getWidget, id, setWidget])
|
|
54
|
+
const isRelative = relativeTool?.enabled ?? defaultIsRelative
|
|
59
55
|
|
|
60
56
|
// Register tool on mount
|
|
61
57
|
useEffect(() => {
|
|
58
|
+
const existingTool = getWidget(id)?.registeredTools?.find(
|
|
59
|
+
(tool) => tool.id === RELATIVE_DATA_TOOL_ID,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const initialEnabled = existingTool?.enabled ?? defaultIsRelative
|
|
63
|
+
|
|
62
64
|
registerTool(id, {
|
|
63
65
|
id: RELATIVE_DATA_TOOL_ID,
|
|
64
66
|
order,
|
|
65
|
-
enabled:
|
|
67
|
+
enabled: initialEnabled,
|
|
66
68
|
fn: (data) => {
|
|
67
69
|
const echartData = data as EchartWidgetData
|
|
68
70
|
const total = calculateTotal(echartData)
|
|
@@ -71,12 +73,7 @@ export function RelativeData({
|
|
|
71
73
|
})
|
|
72
74
|
|
|
73
75
|
return () => unregisterTool(id, RELATIVE_DATA_TOOL_ID)
|
|
74
|
-
}, [id, order, registerTool, unregisterTool,
|
|
75
|
-
|
|
76
|
-
// Update enabled flag when toggle changes
|
|
77
|
-
useEffect(() => {
|
|
78
|
-
setToolEnabled(id, RELATIVE_DATA_TOOL_ID, isRelative)
|
|
79
|
-
}, [id, isRelative, setToolEnabled])
|
|
76
|
+
}, [id, order, registerTool, unregisterTool, defaultIsRelative, getWidget])
|
|
80
77
|
|
|
81
78
|
const handleToggle = useCallback(() => {
|
|
82
79
|
const newIsRelative = !isRelative
|
|
@@ -96,8 +93,8 @@ export function RelativeData({
|
|
|
96
93
|
max = 100
|
|
97
94
|
}
|
|
98
95
|
|
|
96
|
+
setToolEnabled(id, RELATIVE_DATA_TOOL_ID, newIsRelative)
|
|
99
97
|
setWidget(id, {
|
|
100
|
-
isRelative: newIsRelative,
|
|
101
98
|
max,
|
|
102
99
|
formatter: newIsRelative
|
|
103
100
|
? (value: number) => {
|
|
@@ -110,7 +107,7 @@ export function RelativeData({
|
|
|
110
107
|
}
|
|
111
108
|
: originalFormatter.current,
|
|
112
109
|
})
|
|
113
|
-
}, [isRelative, setWidget, id, getWidget])
|
|
110
|
+
}, [isRelative, setWidget, setToolEnabled, id, getWidget])
|
|
114
111
|
|
|
115
112
|
const tooltipLabel = isRelative
|
|
116
113
|
? (labels?.absolute ?? 'Show absolute values')
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { IconButtonProps } from '@mui/material'
|
|
2
2
|
import type { ReactNode } from 'react'
|
|
3
|
-
import type { BaseWidgetState } from '../../stores/types'
|
|
4
3
|
|
|
5
4
|
export interface RelativeDataProps {
|
|
6
5
|
/** Widget ID to update data in the widget store */
|
|
@@ -23,9 +22,3 @@ export interface RelativeDataProps {
|
|
|
23
22
|
/** Custom icon to display */
|
|
24
23
|
Icon?: ReactNode
|
|
25
24
|
}
|
|
26
|
-
|
|
27
|
-
export type RelativeDataState<T = unknown> = BaseWidgetState<
|
|
28
|
-
T & {
|
|
29
|
-
isRelative?: boolean
|
|
30
|
-
}
|
|
31
|
-
>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TextField, InputAdornment, IconButton } from '@mui/material'
|
|
2
2
|
import { ClearOutlined, SearchOutlined } from '@mui/icons-material'
|
|
3
|
-
import { useEffect, useRef, useCallback } from 'react'
|
|
3
|
+
import { useEffect, useRef, useCallback, useState } from 'react'
|
|
4
4
|
import { useWidgetStore } from '../../stores/widget-store'
|
|
5
5
|
import type { SearcherProps, SearcherFilterFn, SearcherState } from './types'
|
|
6
6
|
import type { EchartWidgetData } from '../../echart/types'
|
|
@@ -40,29 +40,33 @@ export function Searcher({
|
|
|
40
40
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
41
41
|
const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
42
42
|
|
|
43
|
-
// Read enabled state
|
|
43
|
+
// Read enabled state from widget store (set by SearcherToggle)
|
|
44
44
|
const widgetState = useWidgetStore((state) =>
|
|
45
45
|
state.getWidget<SearcherState>(id),
|
|
46
46
|
)
|
|
47
47
|
const enabled = widgetState?.isSearchEnabled ?? false
|
|
48
|
-
const searchText = widgetState?.searchText ?? ''
|
|
49
48
|
const prevEnabledRef = useRef(enabled)
|
|
50
49
|
|
|
50
|
+
const getWidget = useWidgetStore((state) => state.getWidget)
|
|
51
|
+
|
|
52
|
+
// Local state for immediate input value — restore from tool config on remount
|
|
53
|
+
const [searchTextLocal, setSearchTextLocal] = useState(() => {
|
|
54
|
+
const existingTool = getWidget(id)?.registeredTools?.find(
|
|
55
|
+
(tool) => tool.id === SEARCHER_TOOL_ID,
|
|
56
|
+
)
|
|
57
|
+
return (existingTool?.config?.searchText as string) ?? ''
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// When disabled, display empty; when enabled, use local state
|
|
61
|
+
const searchText = enabled ? searchTextLocal : ''
|
|
62
|
+
|
|
51
63
|
const filter = filterFn ?? defaultFilterFn
|
|
52
64
|
|
|
53
|
-
const setWidget = useWidgetStore((state) => state.setWidget)
|
|
54
65
|
const registerTool = useWidgetStore((state) => state.registerTool)
|
|
55
66
|
const unregisterTool = useWidgetStore((state) => state.unregisterTool)
|
|
56
67
|
const setToolEnabled = useWidgetStore((state) => state.setToolEnabled)
|
|
57
68
|
const updateToolConfig = useWidgetStore((state) => state.updateToolConfig)
|
|
58
69
|
|
|
59
|
-
const setSearchText = useCallback(
|
|
60
|
-
(text: string) => {
|
|
61
|
-
setWidget(id, { searchText: text })
|
|
62
|
-
},
|
|
63
|
-
[id, setWidget],
|
|
64
|
-
)
|
|
65
|
-
|
|
66
70
|
// Register tool on mount
|
|
67
71
|
useEffect(() => {
|
|
68
72
|
registerTool(id, {
|
|
@@ -78,12 +82,17 @@ export function Searcher({
|
|
|
78
82
|
// Return result directly (pipeline will handle Promise)
|
|
79
83
|
return result
|
|
80
84
|
},
|
|
81
|
-
config: {
|
|
85
|
+
config: {
|
|
86
|
+
searchText:
|
|
87
|
+
(getWidget(id)?.registeredTools?.find(
|
|
88
|
+
(tool) => tool.id === SEARCHER_TOOL_ID,
|
|
89
|
+
)?.config?.searchText as string) ?? '',
|
|
90
|
+
},
|
|
82
91
|
disables: [LOCK_SELECTION_TOOL_ID],
|
|
83
92
|
})
|
|
84
93
|
|
|
85
94
|
return () => unregisterTool(id, SEARCHER_TOOL_ID)
|
|
86
|
-
}, [id, order, filter, registerTool, unregisterTool, enabled,
|
|
95
|
+
}, [id, order, filter, registerTool, unregisterTool, enabled, getWidget])
|
|
87
96
|
|
|
88
97
|
// Update enabled flag when it changes
|
|
89
98
|
useEffect(() => {
|
|
@@ -103,15 +112,24 @@ export function Searcher({
|
|
|
103
112
|
[id, updateToolConfig, debounceDelay],
|
|
104
113
|
)
|
|
105
114
|
|
|
115
|
+
// Handle enabled state transitions during render (React setState-during-render pattern)
|
|
116
|
+
if (enabled !== prevEnabledRef.current) {
|
|
117
|
+
if (enabled) {
|
|
118
|
+
// Transition from disabled to enabled - reset local state
|
|
119
|
+
setSearchTextLocal('')
|
|
120
|
+
}
|
|
121
|
+
prevEnabledRef.current = enabled
|
|
122
|
+
}
|
|
123
|
+
|
|
106
124
|
// Auto-focus when enabled becomes true
|
|
107
125
|
useEffect(() => {
|
|
108
|
-
|
|
109
|
-
if (enabled && !prevEnabledRef.current && inputRef.current) {
|
|
126
|
+
if (enabled && inputRef.current) {
|
|
110
127
|
inputRef.current.focus()
|
|
111
128
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
129
|
+
if (!enabled) {
|
|
130
|
+
updateToolConfig(id, SEARCHER_TOOL_ID, { searchText: '' })
|
|
131
|
+
}
|
|
132
|
+
}, [enabled, id, updateToolConfig])
|
|
115
133
|
|
|
116
134
|
// Cleanup debounce timeout on unmount
|
|
117
135
|
useEffect(() => {
|
|
@@ -125,19 +143,19 @@ export function Searcher({
|
|
|
125
143
|
const handleChange = useCallback(
|
|
126
144
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
127
145
|
const newValue = event.target.value
|
|
128
|
-
|
|
146
|
+
setSearchTextLocal(newValue)
|
|
129
147
|
debouncedUpdateConfig(newValue)
|
|
130
148
|
},
|
|
131
|
-
[debouncedUpdateConfig
|
|
149
|
+
[debouncedUpdateConfig],
|
|
132
150
|
)
|
|
133
151
|
|
|
134
152
|
const handleClear = useCallback(() => {
|
|
135
|
-
|
|
153
|
+
setSearchTextLocal('')
|
|
136
154
|
updateToolConfig(id, SEARCHER_TOOL_ID, { searchText: '' })
|
|
137
155
|
if (inputRef.current) {
|
|
138
156
|
inputRef.current.focus()
|
|
139
157
|
}
|
|
140
|
-
}, [id,
|
|
158
|
+
}, [id, updateToolConfig])
|
|
141
159
|
|
|
142
160
|
if (!enabled) {
|
|
143
161
|
return null
|