@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.
Files changed (119) hide show
  1. package/dist/components.js +123 -123
  2. package/dist/components.js.map +1 -1
  3. package/dist/error-CEkRPccv.js +39 -0
  4. package/dist/error-CEkRPccv.js.map +1 -0
  5. package/dist/{lasso-tool-BctzdzBu.js → lasso-tool-jl4YK02H.js} +19 -19
  6. package/dist/lasso-tool-jl4YK02H.js.map +1 -0
  7. package/dist/no-data-hR3KcJ-_.js +60 -0
  8. package/dist/no-data-hR3KcJ-_.js.map +1 -0
  9. package/dist/{row-D3uVFImu.js → row-BKmVAUN5.js} +2 -2
  10. package/dist/{row-D3uVFImu.js.map → row-BKmVAUN5.js.map} +1 -1
  11. package/dist/{series-BAImrSBo.js → series-D1pynfeh.js} +3 -3
  12. package/dist/{series-BAImrSBo.js.map → series-D1pynfeh.js.map} +1 -1
  13. package/dist/{styles-CCZnY17y.js → styles-DrPyd0y5.js} +28 -22
  14. package/dist/styles-DrPyd0y5.js.map +1 -0
  15. package/dist/types/components/lasso-tool/types.d.ts +1 -1
  16. package/dist/types/widgets/_shared/chart-config/index.d.ts +1 -1
  17. package/dist/types/widgets/_shared/chart-config/option-builders.d.ts +7 -0
  18. package/dist/types/widgets/_shared/chart-config/option-builders.test.d.ts +1 -0
  19. package/dist/types/widgets/actions/index.d.ts +2 -2
  20. package/dist/types/widgets/actions/lock-selection/types.d.ts +0 -13
  21. package/dist/types/widgets/actions/relative-data/types.d.ts +0 -4
  22. package/dist/types/widgets/actions/searcher/types.d.ts +0 -2
  23. package/dist/types/widgets/actions/stack-toggle/types.d.ts +0 -4
  24. package/dist/types/widgets/echart/types.d.ts +0 -4
  25. package/dist/types/widgets/echart/utils.d.ts +2 -1
  26. package/dist/types/widgets/error/error.d.ts +1 -1
  27. package/dist/types/widgets/error/types.d.ts +8 -0
  28. package/dist/types/widgets/loader/loader.d.ts +1 -1
  29. package/dist/types/widgets/loader/types.d.ts +1 -1
  30. package/dist/types/widgets/stores/types.d.ts +1 -1
  31. package/dist/{use-widget-ref-B8x4sHIj.js → use-widget-ref-P-2i0MJG.js} +2 -2
  32. package/dist/{use-widget-ref-B8x4sHIj.js.map → use-widget-ref-P-2i0MJG.js.map} +1 -1
  33. package/dist/{utils-D3-eQyDR.js → utils-idmvq0Oa.js} +17 -16
  34. package/dist/utils-idmvq0Oa.js.map +1 -0
  35. package/dist/{widget-store-Dn0Bnc4h.js → widget-store-CzDt8oSK.js} +31 -46
  36. package/dist/widget-store-CzDt8oSK.js.map +1 -0
  37. package/dist/widgets/actions.js +690 -716
  38. package/dist/widgets/actions.js.map +1 -1
  39. package/dist/widgets/bar.js +67 -63
  40. package/dist/widgets/bar.js.map +1 -1
  41. package/dist/widgets/category.js +250 -241
  42. package/dist/widgets/category.js.map +1 -1
  43. package/dist/widgets/echart.js +93 -100
  44. package/dist/widgets/echart.js.map +1 -1
  45. package/dist/widgets/error.js +1 -1
  46. package/dist/widgets/formula.js +64 -72
  47. package/dist/widgets/formula.js.map +1 -1
  48. package/dist/widgets/histogram.js +75 -73
  49. package/dist/widgets/histogram.js.map +1 -1
  50. package/dist/widgets/loader.js +1 -1
  51. package/dist/widgets/loader.js.map +1 -1
  52. package/dist/widgets/markdown.js +2 -2
  53. package/dist/widgets/no-data.js +1 -1
  54. package/dist/widgets/pie.js +4 -4
  55. package/dist/widgets/range.js +97 -105
  56. package/dist/widgets/range.js.map +1 -1
  57. package/dist/widgets/scatterplot.js +8 -8
  58. package/dist/widgets/skeleton-loader.js +1 -1
  59. package/dist/widgets/spread.js +84 -100
  60. package/dist/widgets/spread.js.map +1 -1
  61. package/dist/widgets/stores.js +1 -1
  62. package/dist/widgets/table.js +493 -485
  63. package/dist/widgets/table.js.map +1 -1
  64. package/dist/widgets/timeseries.js +4 -4
  65. package/dist/widgets/wrapper.js +156 -156
  66. package/dist/widgets/wrapper.js.map +1 -1
  67. package/dist/widgets.js +4 -4
  68. package/package.json +3 -3
  69. package/src/components/lasso-tool/lasso-tool-inline.tsx +19 -17
  70. package/src/components/lasso-tool/lasso-tool.tsx +22 -20
  71. package/src/components/lasso-tool/types.ts +4 -3
  72. package/src/widgets/_shared/chart-config/index.ts +1 -0
  73. package/src/widgets/_shared/chart-config/option-builders.test.ts +40 -0
  74. package/src/widgets/_shared/chart-config/option-builders.ts +12 -0
  75. package/src/widgets/actions/fullscreen/fullscreen.tsx +5 -8
  76. package/src/widgets/actions/index.ts +2 -5
  77. package/src/widgets/actions/lock-selection/lock-selection.test.tsx +28 -30
  78. package/src/widgets/actions/lock-selection/lock-selection.tsx +25 -26
  79. package/src/widgets/actions/lock-selection/types.ts +0 -17
  80. package/src/widgets/actions/relative-data/relative-data.test.tsx +13 -13
  81. package/src/widgets/actions/relative-data/relative-data.tsx +18 -21
  82. package/src/widgets/actions/relative-data/types.ts +0 -7
  83. package/src/widgets/actions/searcher/searcher.tsx +40 -22
  84. package/src/widgets/actions/searcher/types.ts +0 -2
  85. package/src/widgets/actions/stack-toggle/stack-toggle.test.tsx +19 -9
  86. package/src/widgets/actions/stack-toggle/stack-toggle.tsx +32 -22
  87. package/src/widgets/actions/stack-toggle/types.ts +0 -8
  88. package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +113 -95
  89. package/src/widgets/bar/config.ts +37 -28
  90. package/src/widgets/category/category-ui.tsx +25 -22
  91. package/src/widgets/echart/echart-ui.test.tsx +3 -18
  92. package/src/widgets/echart/echart-ui.tsx +4 -22
  93. package/src/widgets/echart/echart.test.tsx +9 -25
  94. package/src/widgets/echart/echart.tsx +36 -29
  95. package/src/widgets/echart/types.ts +0 -4
  96. package/src/widgets/echart/utils.ts +3 -1
  97. package/src/widgets/error/error.tsx +17 -14
  98. package/src/widgets/error/types.ts +10 -0
  99. package/src/widgets/formula/components/value.tsx +13 -13
  100. package/src/widgets/histogram/config.ts +36 -29
  101. package/src/widgets/loader/loader.tsx +3 -1
  102. package/src/widgets/loader/types.ts +3 -1
  103. package/src/widgets/no-data/no-data.tsx +8 -11
  104. package/src/widgets/range/components/range-item.tsx +9 -13
  105. package/src/widgets/spread/components/max-value.tsx +13 -13
  106. package/src/widgets/spread/components/min-value.tsx +13 -13
  107. package/src/widgets/stores/types.ts +1 -4
  108. package/src/widgets/stores/widget-store.ts +1 -27
  109. package/src/widgets/table/hooks/use-pagination.ts +44 -35
  110. package/src/widgets/table/hooks/use-sort.ts +25 -23
  111. package/src/widgets/wrapper/wrapper-ui.tsx +16 -17
  112. package/dist/error-piB8FwYO.js +0 -38
  113. package/dist/error-piB8FwYO.js.map +0 -1
  114. package/dist/lasso-tool-BctzdzBu.js.map +0 -1
  115. package/dist/no-data-jdlbMef0.js +0 -61
  116. package/dist/no-data-jdlbMef0.js.map +0 -1
  117. package/dist/styles-CCZnY17y.js.map +0 -1
  118. package/dist/utils-D3-eQyDR.js.map +0 -1
  119. 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 { isFullScreen, title } = useWidgetStore(
27
- useShallow((state) => {
28
- const widget = state.getWidget<FullScreenState>(id)
29
- return {
30
- isFullScreen: widget?.isFullScreen,
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, StackToggleState } from './stack-toggle/types'
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
- const button = screen.getByRole('button', { name: 'Unlock selection' })
54
- expect(button).toBeTruthy()
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 store when clicked', () => {
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
- .getState()
65
- .getWidget<LockSelectionState>(widgetId)
66
- expect(widget?.isLocked).toBe(true)
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
- const widget = useWidgetStore
108
- .getState()
109
- .getWidget<LockSelectionState>(widgetId)
110
- expect(widget?.isLocked).toBe(false)
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, useMemo } from 'react'
4
- import type { LockSelectionProps, LockSelectionState } from './types'
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 setWidget = useWidgetStore((state) => state.setWidget)
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 storeIsLocked = useWidgetStore(
44
- useShallow((state) => state.getWidget<LockSelectionState>(id)?.isLocked),
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 = storeIsLocked ?? false
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: isLocked,
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, isLocked, lockedItems])
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
- setWidget(id, {
81
- isLocked: false,
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
- setWidget(id, {
87
- isLocked: true,
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, setWidget])
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 widget store with defaultIsRelative on mount', async () => {
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
- .getState()
103
- .getWidget<RelativeDataState>(widgetId)
104
- expect(widget?.isRelative).toBe(true)
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 widget store with false when defaultIsRelative is false', async () => {
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
- .getState()
114
- .getWidget<RelativeDataState>(widgetId)
115
- expect(widget?.isRelative).toBe(false)
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, RelativeDataState } from './types'
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 storeIsRelative = useWidgetStore(
47
- (state) => state.getWidget<RelativeDataState>(id)?.isRelative,
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 = storeIsRelative ?? defaultIsRelative
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: isRelative,
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, isRelative])
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 and search text from widget store
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: { searchText },
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, searchText])
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
- // Transition from disabled to enabled - focus input
109
- if (enabled && !prevEnabledRef.current && inputRef.current) {
126
+ if (enabled && inputRef.current) {
110
127
  inputRef.current.focus()
111
128
  }
112
-
113
- prevEnabledRef.current = enabled
114
- }, [enabled])
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
- setSearchText(newValue)
146
+ setSearchTextLocal(newValue)
129
147
  debouncedUpdateConfig(newValue)
130
148
  },
131
- [debouncedUpdateConfig, setSearchText],
149
+ [debouncedUpdateConfig],
132
150
  )
133
151
 
134
152
  const handleClear = useCallback(() => {
135
- setSearchText('')
153
+ setSearchTextLocal('')
136
154
  updateToolConfig(id, SEARCHER_TOOL_ID, { searchText: '' })
137
155
  if (inputRef.current) {
138
156
  inputRef.current.focus()
139
157
  }
140
- }, [id, setSearchText, updateToolConfig])
158
+ }, [id, updateToolConfig])
141
159
 
142
160
  if (!enabled) {
143
161
  return null
@@ -19,8 +19,6 @@ export type SearcherFilterFn = (
19
19
  export interface SearcherStateProps {
20
20
  /** Whether search is currently enabled */
21
21
  isSearchEnabled?: boolean
22
- /** Current search text */
23
- searchText?: string
24
22
  }
25
23
 
26
24
  /**