@carto/ps-react-ui 4.3.7 → 4.3.9

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 (34) hide show
  1. package/dist/types/widgets/actions/change-column/change-column.d.ts +4 -0
  2. package/dist/types/widgets/actions/fullscreen/fullscreen.d.ts +1 -1
  3. package/dist/types/widgets/actions/fullscreen/types.d.ts +2 -1
  4. package/dist/types/widgets/actions/index.d.ts +3 -3
  5. package/dist/types/widgets/actions/lock-selection/types.d.ts +13 -0
  6. package/dist/types/widgets/actions/relative-data/types.d.ts +4 -0
  7. package/dist/types/widgets/actions/searcher/types.d.ts +2 -0
  8. package/dist/types/widgets/actions/stack-toggle/types.d.ts +4 -0
  9. package/dist/types/widgets/table/components/cell.d.ts +4 -2
  10. package/dist/types/widgets/table/types.d.ts +1 -1
  11. package/dist/widgets/actions.js +714 -671
  12. package/dist/widgets/actions.js.map +1 -1
  13. package/dist/widgets/loader.js +58 -49
  14. package/dist/widgets/loader.js.map +1 -1
  15. package/dist/widgets/table.js +241 -240
  16. package/dist/widgets/table.js.map +1 -1
  17. package/package.json +3 -3
  18. package/src/widgets/actions/change-column/change-column.test.tsx +129 -2
  19. package/src/widgets/actions/change-column/change-column.tsx +79 -2
  20. package/src/widgets/actions/fullscreen/fullscreen.tsx +3 -0
  21. package/src/widgets/actions/fullscreen/types.ts +6 -1
  22. package/src/widgets/actions/index.ts +9 -3
  23. package/src/widgets/actions/lock-selection/lock-selection.tsx +26 -25
  24. package/src/widgets/actions/lock-selection/types.ts +17 -0
  25. package/src/widgets/actions/relative-data/relative-data.tsx +21 -18
  26. package/src/widgets/actions/relative-data/types.ts +7 -0
  27. package/src/widgets/actions/searcher/searcher.tsx +22 -40
  28. package/src/widgets/actions/searcher/types.ts +2 -0
  29. package/src/widgets/actions/stack-toggle/stack-toggle.tsx +22 -32
  30. package/src/widgets/actions/stack-toggle/types.ts +8 -0
  31. package/src/widgets/loader/loader.tsx +25 -24
  32. package/src/widgets/table/components/cell.tsx +5 -3
  33. package/src/widgets/table/components/row.tsx +6 -1
  34. package/src/widgets/table/types.ts +1 -1
@@ -1,4 +1,8 @@
1
- import type { DialogContentProps, IconButtonProps } from '@mui/material'
1
+ import type {
2
+ DialogContentProps,
3
+ IconButtonProps,
4
+ DialogProps,
5
+ } from '@mui/material'
2
6
  import type { ReactNode } from 'react'
3
7
  import type { BaseWidgetState } from '../../../widgets/stores/types'
4
8
 
@@ -22,6 +26,7 @@ export interface FullScreenProps {
22
26
  }
23
27
  children: ReactNode
24
28
  DialogContentProps?: DialogContentProps
29
+ DialogProps?: DialogProps
25
30
  IconButtonProps?: IconButtonProps
26
31
  Icon?: ReactNode
27
32
  }
@@ -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 } from './stack-toggle/types'
27
+ export type { StackToggleProps, StackToggleState } from './stack-toggle/types'
28
28
 
29
29
  /* Searcher Toggle Widget */
30
30
  export { Searcher, SEARCHER_TOOL_ID } from './searcher/searcher'
@@ -37,7 +37,10 @@ export type {
37
37
  } from './searcher/types'
38
38
 
39
39
  /* Change Column Widget */
40
- export { ChangeColumn } from './change-column/change-column'
40
+ export {
41
+ ChangeColumn,
42
+ CHANGE_COLUMN_TOOL_ID,
43
+ } from './change-column/change-column'
41
44
  export type { ChangeColumnProps } from './change-column/types'
42
45
 
43
46
  /* Lock Selection Widget */
@@ -45,4 +48,7 @@ export {
45
48
  LockSelection,
46
49
  LOCK_SELECTION_TOOL_ID,
47
50
  } from './lock-selection/lock-selection'
48
- export type { LockSelectionProps } from './lock-selection/types'
51
+ export type {
52
+ LockSelectionProps,
53
+ LockSelectionState,
54
+ } from './lock-selection/types'
@@ -1,7 +1,7 @@
1
1
  import { IconButton } from '@mui/material'
2
2
  import { CheckBoxOutlined } from '@mui/icons-material'
3
- import { useCallback, useEffect } from 'react'
4
- import type { LockSelectionProps } from './types'
3
+ import { useCallback, useEffect, useMemo } from 'react'
4
+ import type { LockSelectionProps, LockSelectionState } from './types'
5
5
  import { actionButtonStyles } from '../shared/styles'
6
6
  import { Tooltip } from '../../../components'
7
7
  import { useWidgetStore } from '../../stores/widget-store'
@@ -34,60 +34,61 @@ export function LockSelection({
34
34
  Icon,
35
35
  IconButtonProps,
36
36
  }: LockSelectionProps) {
37
- const getWidget = useWidgetStore((state) => state.getWidget)
37
+ const setWidget = useWidgetStore((state) => state.setWidget)
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 lockTool = useWidgetStore(
44
- useShallow((state) => {
45
- const tools = state.getWidget(id)?.registeredTools ?? []
46
- return tools.find((tool) => tool.id === LOCK_SELECTION_TOOL_ID)
47
- }),
43
+ const storeIsLocked = useWidgetStore(
44
+ useShallow((state) => state.getWidget<LockSelectionState>(id)?.isLocked),
48
45
  )
49
46
 
50
- const isLocked = lockTool?.enabled ?? false
47
+ const isLocked = storeIsLocked ?? false
48
+ const lockedItems = useMemo(
49
+ () => (isLocked ? selectedItems : []),
50
+ [isLocked, selectedItems],
51
+ )
51
52
 
52
53
  // Register tool on mount
53
54
  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
-
62
55
  registerTool(id, {
63
56
  id: LOCK_SELECTION_TOOL_ID,
64
57
  order,
65
- enabled: initialEnabled,
58
+ enabled: isLocked,
66
59
  fn: (data, config) => {
67
60
  const items = (config?.lockedItems as string[]) || []
68
61
  if (items.length === 0) return data
69
62
 
70
63
  return filterDataByLockedItems(data as EchartWidgetData, items)
71
64
  },
72
- config: { lockedItems: initialLockedItems },
65
+ config: { lockedItems },
73
66
  })
74
67
 
75
68
  return () => unregisterTool(id, LOCK_SELECTION_TOOL_ID)
76
- }, [id, order, registerTool, unregisterTool, getWidget])
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])
77
76
 
78
77
  const handleToggle = useCallback(() => {
79
78
  if (isLocked) {
80
79
  // Unlock: clear locked items and disable tool
81
- setToolEnabled(id, LOCK_SELECTION_TOOL_ID, false)
82
- updateToolConfig(id, LOCK_SELECTION_TOOL_ID, { lockedItems: [] })
80
+ setWidget(id, {
81
+ isLocked: false,
82
+ lockedItems: [],
83
+ })
83
84
  } else {
84
85
  // Lock: save selected items and enable tool
85
- setToolEnabled(id, LOCK_SELECTION_TOOL_ID, true)
86
- updateToolConfig(id, LOCK_SELECTION_TOOL_ID, {
86
+ setWidget(id, {
87
+ isLocked: true,
87
88
  lockedItems: selectedItems,
88
89
  })
89
90
  }
90
- }, [id, isLocked, selectedItems, setToolEnabled, updateToolConfig])
91
+ }, [id, isLocked, selectedItems, setWidget])
91
92
 
92
93
  // Don't render if no selections
93
94
  if (selectedItems.length === 0) {
@@ -1,5 +1,14 @@
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
+ * Widget state extension for lock selection functionality.
7
+ * Extends the base widget state with lock-specific properties.
8
+ */
9
+ export type LockSelectionState<T = object> = BaseWidgetState<
10
+ T & LockSelectionStateProps
11
+ >
3
12
 
4
13
  export interface LockSelectionProps {
5
14
  /** Widget ID to store lock selection state */
@@ -22,3 +31,11 @@ export interface LockSelectionProps {
22
31
  /** Custom icon to display */
23
32
  Icon?: ReactNode
24
33
  }
34
+
35
+ /**
36
+ * Lock selection specific state properties.
37
+ */
38
+ export interface LockSelectionStateProps {
39
+ /** Whether the selection is currently locked */
40
+ isLocked?: boolean
41
+ }
@@ -2,12 +2,11 @@ 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 } from './types'
5
+ import type { RelativeDataProps, RelativeDataState } 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'
11
10
 
12
11
  export const RELATIVE_DATA_TOOL_ID = 'relative-data'
13
12
 
@@ -44,27 +43,26 @@ export function RelativeData({
44
43
  const unregisterTool = useWidgetStore((state) => state.unregisterTool)
45
44
  const setToolEnabled = useWidgetStore((state) => state.setToolEnabled)
46
45
 
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
- }),
46
+ const storeIsRelative = useWidgetStore(
47
+ (state) => state.getWidget<RelativeDataState>(id)?.isRelative,
52
48
  )
53
49
 
54
- const isRelative = relativeTool?.enabled ?? defaultIsRelative
50
+ const isRelative = storeIsRelative ?? defaultIsRelative
55
51
 
56
- // Register tool on mount
52
+ // Initialize store with default value on mount
57
53
  useEffect(() => {
58
- const existingTool = getWidget(id)?.registeredTools?.find(
59
- (tool) => tool.id === RELATIVE_DATA_TOOL_ID,
60
- )
61
-
62
- const initialEnabled = existingTool?.enabled ?? defaultIsRelative
54
+ const currentValue = getWidget<RelativeDataState>(id)?.isRelative
55
+ if (currentValue === undefined) {
56
+ setWidget(id, { isRelative: defaultIsRelative })
57
+ }
58
+ }, [defaultIsRelative, getWidget, id, setWidget])
63
59
 
60
+ // Register tool on mount
61
+ useEffect(() => {
64
62
  registerTool(id, {
65
63
  id: RELATIVE_DATA_TOOL_ID,
66
64
  order,
67
- enabled: initialEnabled,
65
+ enabled: isRelative,
68
66
  fn: (data) => {
69
67
  const echartData = data as EchartWidgetData
70
68
  const total = calculateTotal(echartData)
@@ -73,7 +71,12 @@ export function RelativeData({
73
71
  })
74
72
 
75
73
  return () => unregisterTool(id, RELATIVE_DATA_TOOL_ID)
76
- }, [id, order, registerTool, unregisterTool, defaultIsRelative, getWidget])
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])
77
80
 
78
81
  const handleToggle = useCallback(() => {
79
82
  const newIsRelative = !isRelative
@@ -93,8 +96,8 @@ export function RelativeData({
93
96
  max = 100
94
97
  }
95
98
 
96
- setToolEnabled(id, RELATIVE_DATA_TOOL_ID, newIsRelative)
97
99
  setWidget(id, {
100
+ isRelative: newIsRelative,
98
101
  max,
99
102
  formatter: newIsRelative
100
103
  ? (value: number) => {
@@ -107,7 +110,7 @@ export function RelativeData({
107
110
  }
108
111
  : originalFormatter.current,
109
112
  })
110
- }, [isRelative, setWidget, setToolEnabled, id, getWidget])
113
+ }, [isRelative, setWidget, id, getWidget])
111
114
 
112
115
  const tooltipLabel = isRelative
113
116
  ? (labels?.absolute ?? 'Show absolute values')
@@ -1,5 +1,6 @@
1
1
  import type { IconButtonProps } from '@mui/material'
2
2
  import type { ReactNode } from 'react'
3
+ import type { BaseWidgetState } from '../../../widgets/stores'
3
4
 
4
5
  export interface RelativeDataProps {
5
6
  /** Widget ID to update data in the widget store */
@@ -22,3 +23,9 @@ export interface RelativeDataProps {
22
23
  /** Custom icon to display */
23
24
  Icon?: ReactNode
24
25
  }
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, useState } from 'react'
3
+ import { useEffect, useRef, useCallback } 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,33 +40,29 @@ 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 from widget store (set by SearcherToggle)
43
+ // Read enabled state and search text from widget store
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 ?? ''
48
49
  const prevEnabledRef = useRef(enabled)
49
50
 
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
-
63
51
  const filter = filterFn ?? defaultFilterFn
64
52
 
53
+ const setWidget = useWidgetStore((state) => state.setWidget)
65
54
  const registerTool = useWidgetStore((state) => state.registerTool)
66
55
  const unregisterTool = useWidgetStore((state) => state.unregisterTool)
67
56
  const setToolEnabled = useWidgetStore((state) => state.setToolEnabled)
68
57
  const updateToolConfig = useWidgetStore((state) => state.updateToolConfig)
69
58
 
59
+ const setSearchText = useCallback(
60
+ (text: string) => {
61
+ setWidget(id, { searchText: text })
62
+ },
63
+ [id, setWidget],
64
+ )
65
+
70
66
  // Register tool on mount
71
67
  useEffect(() => {
72
68
  registerTool(id, {
@@ -82,17 +78,12 @@ export function Searcher({
82
78
  // Return result directly (pipeline will handle Promise)
83
79
  return result
84
80
  },
85
- config: {
86
- searchText:
87
- (getWidget(id)?.registeredTools?.find(
88
- (tool) => tool.id === SEARCHER_TOOL_ID,
89
- )?.config?.searchText as string) ?? '',
90
- },
81
+ config: { searchText },
91
82
  disables: [LOCK_SELECTION_TOOL_ID],
92
83
  })
93
84
 
94
85
  return () => unregisterTool(id, SEARCHER_TOOL_ID)
95
- }, [id, order, filter, registerTool, unregisterTool, enabled, getWidget])
86
+ }, [id, order, filter, registerTool, unregisterTool, enabled, searchText])
96
87
 
97
88
  // Update enabled flag when it changes
98
89
  useEffect(() => {
@@ -112,24 +103,15 @@ export function Searcher({
112
103
  [id, updateToolConfig, debounceDelay],
113
104
  )
114
105
 
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
-
124
106
  // Auto-focus when enabled becomes true
125
107
  useEffect(() => {
126
- if (enabled && inputRef.current) {
108
+ // Transition from disabled to enabled - focus input
109
+ if (enabled && !prevEnabledRef.current && inputRef.current) {
127
110
  inputRef.current.focus()
128
111
  }
129
- if (!enabled) {
130
- updateToolConfig(id, SEARCHER_TOOL_ID, { searchText: '' })
131
- }
132
- }, [enabled, id, updateToolConfig])
112
+
113
+ prevEnabledRef.current = enabled
114
+ }, [enabled])
133
115
 
134
116
  // Cleanup debounce timeout on unmount
135
117
  useEffect(() => {
@@ -143,19 +125,19 @@ export function Searcher({
143
125
  const handleChange = useCallback(
144
126
  (event: React.ChangeEvent<HTMLInputElement>) => {
145
127
  const newValue = event.target.value
146
- setSearchTextLocal(newValue)
128
+ setSearchText(newValue)
147
129
  debouncedUpdateConfig(newValue)
148
130
  },
149
- [debouncedUpdateConfig],
131
+ [debouncedUpdateConfig, setSearchText],
150
132
  )
151
133
 
152
134
  const handleClear = useCallback(() => {
153
- setSearchTextLocal('')
135
+ setSearchText('')
154
136
  updateToolConfig(id, SEARCHER_TOOL_ID, { searchText: '' })
155
137
  if (inputRef.current) {
156
138
  inputRef.current.focus()
157
139
  }
158
- }, [id, updateToolConfig])
140
+ }, [id, setSearchText, updateToolConfig])
159
141
 
160
142
  if (!enabled) {
161
143
  return null
@@ -19,6 +19,8 @@ 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
22
24
  }
23
25
 
24
26
  /**
@@ -1,7 +1,7 @@
1
1
  import { IconButton } from '@mui/material'
2
2
  import { useCallback, useEffect, useMemo } from 'react'
3
3
  import { useWidgetStore } from '../../stores/widget-store'
4
- import type { StackToggleProps } from './types'
4
+ import type { StackToggleProps, StackToggleState } from './types'
5
5
  import { actionButtonStyles } from '../shared/styles'
6
6
  import { Tooltip } from '../../../components'
7
7
  import { GroupedBarChartIcon } from './grouped-bar-chart-icon'
@@ -34,17 +34,14 @@ export function StackToggle({
34
34
  Icon,
35
35
  IconButtonProps,
36
36
  }: StackToggleProps) {
37
- const getWidget = useWidgetStore((state) => state.getWidget)
37
+ const setWidget = useWidgetStore((state) => state.setWidget)
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 stackTool = useWidgetStore(
44
- useShallow((state) => {
45
- const tools = state.getWidget(id)?.registeredTools ?? []
46
- return tools.find((tool) => tool.id === STACK_TOGGLE_TOOL_ID)
47
- }),
43
+ const storeIsStacked = useWidgetStore(
44
+ useShallow((state) => state.getWidget<StackToggleState>(id)?.isStacked),
48
45
  )
49
46
 
50
47
  const option = useWidgetStore(
@@ -64,25 +61,15 @@ export function StackToggle({
64
61
 
65
62
  // If series already has stack defined, default to stacked=true
66
63
  const effectiveDefaultIsStacked = hasStackInSeries || defaultIsStacked
67
- const isStacked =
68
- (stackTool?.config?.stacked as boolean | undefined) ??
69
- effectiveDefaultIsStacked
64
+ const isStacked = storeIsStacked ?? effectiveDefaultIsStacked
70
65
 
71
66
  // Register config tool on mount
72
67
  useEffect(() => {
73
- const existingTool = getWidget(id)?.registeredTools?.find(
74
- (tool) => tool.id === STACK_TOGGLE_TOOL_ID,
75
- )
76
-
77
- const initialStacked =
78
- (existingTool?.config?.stacked as boolean | undefined) ??
79
- effectiveDefaultIsStacked
80
-
81
68
  registerTool(id, {
82
69
  id: STACK_TOGGLE_TOOL_ID,
83
70
  type: 'config',
84
71
  order: 10,
85
- enabled: initialStacked && hasMultiSeries,
72
+ enabled: isStacked && hasMultiSeries,
86
73
  fn: (currentConfig, toolConfig) => {
87
74
  const config = currentConfig as Record<string, unknown>
88
75
  const option = config.option as EchartOptionsProps | undefined
@@ -103,23 +90,26 @@ export function StackToggle({
103
90
 
104
91
  return { ...config, option: { ...option, series: updatedSeries } }
105
92
  },
106
- config: { stacked: initialStacked },
93
+ config: { stacked: isStacked },
107
94
  })
108
95
  return () => unregisterTool(id, STACK_TOGGLE_TOOL_ID)
109
- }, [
110
- id,
111
- registerTool,
112
- unregisterTool,
113
- effectiveDefaultIsStacked,
114
- hasMultiSeries,
115
- getWidget,
116
- ])
96
+ }, [id, registerTool, unregisterTool, isStacked, hasMultiSeries])
97
+
98
+ // Sync tool enabled/config when state changes
99
+ useEffect(() => {
100
+ setToolEnabled(id, STACK_TOGGLE_TOOL_ID, isStacked && hasMultiSeries)
101
+ updateToolConfig(id, STACK_TOGGLE_TOOL_ID, { stacked: isStacked })
102
+ }, [id, isStacked, hasMultiSeries, setToolEnabled, updateToolConfig])
103
+
104
+ // Initialize store with default value only if not already configured
105
+ useEffect(() => {
106
+ if (storeIsStacked !== undefined) return
107
+ setWidget(id, { isStacked: effectiveDefaultIsStacked })
108
+ }, [effectiveDefaultIsStacked, id, setWidget, storeIsStacked])
117
109
 
118
110
  const handleToggle = useCallback(() => {
119
- const newStacked = !isStacked
120
- setToolEnabled(id, STACK_TOGGLE_TOOL_ID, newStacked && hasMultiSeries)
121
- updateToolConfig(id, STACK_TOGGLE_TOOL_ID, { stacked: newStacked })
122
- }, [isStacked, hasMultiSeries, id, setToolEnabled, updateToolConfig])
111
+ setWidget(id, { isStacked: !isStacked })
112
+ }, [isStacked, id, setWidget])
123
113
 
124
114
  const tooltipLabel = isStacked
125
115
  ? (labels?.unstacked ?? 'Disable stacking')
@@ -1,5 +1,7 @@
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
  export interface StackToggleProps {
4
6
  /** Widget ID to update stack configuration in the widget store */
5
7
  id: string
@@ -19,3 +21,9 @@ export interface StackToggleProps {
19
21
  /** Custom icon to display */
20
22
  Icon?: ReactNode
21
23
  }
24
+
25
+ export type StackToggleState<T = unknown> = BaseWidgetState<
26
+ T & {
27
+ isStacked?: boolean
28
+ }
29
+ >
@@ -1,4 +1,4 @@
1
- import { useEffect } from 'react'
1
+ import { useEffect, useRef, useSyncExternalStore } from 'react'
2
2
  import type { WidgetLoaderProps } from './types'
3
3
  import { useWidgetStore } from '../stores/widget-store'
4
4
  import type { WrapperState } from '../wrapper'
@@ -14,6 +14,20 @@ export function WidgetLoader<T extends object = Record<string, unknown>>(
14
14
  (state) => state.executeConfigPipeline,
15
15
  )
16
16
 
17
+ const registeredTools = useSyncExternalStore(
18
+ useWidgetStore.subscribe,
19
+ () => useWidgetStore.getState().widgets[props.id]?.registeredTools,
20
+ )
21
+
22
+ const dataRef = useRef(props.data)
23
+ const configRef = useRef(props.config)
24
+ const isMountedRef = useRef(false)
25
+
26
+ useEffect(() => {
27
+ dataRef.current = props.data
28
+ configRef.current = props.config
29
+ })
30
+
17
31
  // Split into 3 effects for metadata and 1 for data pipeline:
18
32
  // Each property that can be modified independently gets its own effect to avoid
19
33
  // accidentally resetting other properties.
@@ -52,31 +66,18 @@ export function WidgetLoader<T extends object = Record<string, unknown>>(
52
66
  void executeToolPipeline(props.id, props.data)
53
67
  }, [props.id, props.data, executeToolPipeline])
54
68
 
55
- // Effect 5: Re-execute pipelines when tool state changes (enabled/config)
69
+ // Effect 5: Re-execute pipelines when registered tools change
56
70
  useEffect(() => {
57
- let prevTools = useWidgetStore.getState().widgets[props.id]?.registeredTools
58
-
59
- const unsubscribe = useWidgetStore.subscribe((state) => {
60
- const currentTools = state.widgets[props.id]?.registeredTools
61
-
62
- // Only re-execute if tools array changed
63
- if (currentTools !== prevTools) {
64
- prevTools = currentTools
65
- void executeToolPipeline(props.id, props.data)
66
- if (props.config) {
67
- void executeConfigPipeline(props.id, props.config)
68
- }
69
- }
70
- })
71
+ if (!isMountedRef.current) {
72
+ isMountedRef.current = true
73
+ return
74
+ }
71
75
 
72
- return unsubscribe
73
- }, [
74
- props.id,
75
- props.data,
76
- props.config,
77
- executeToolPipeline,
78
- executeConfigPipeline,
79
- ])
76
+ void executeToolPipeline(props.id, dataRef.current)
77
+ if (configRef.current) {
78
+ void executeConfigPipeline(props.id, configRef.current)
79
+ }
80
+ }, [registeredTools, props.id, executeToolPipeline, executeConfigPipeline])
80
81
 
81
82
  return props.children
82
83
  }
@@ -1,6 +1,6 @@
1
1
  import { TableCell as MuiTableCell, Link, Typography } from '@mui/material'
2
2
  import ReactMarkdown, { type Components } from 'react-markdown'
3
- import type { TableColumn } from '../types'
3
+ import type { TableColumn, TableRow } from '../types'
4
4
  import { getCellValue } from '../helpers'
5
5
  import type { ReactNode } from 'react'
6
6
 
@@ -10,6 +10,8 @@ import type { ReactNode } from 'react'
10
10
  export interface CellProps {
11
11
  /** Column definition */
12
12
  column: TableColumn
13
+ /** Full table row for context */
14
+ row: TableRow
13
15
  /** Cell value */
14
16
  value: unknown
15
17
  }
@@ -54,12 +56,12 @@ const CELL_MARKDOWN_COMPONENTS: Components = {
54
56
  * Markdown is rendered when the raw value is a string and no formatter is defined.
55
57
  * If a formatter is used, its output is rendered directly without markdown parsing.
56
58
  */
57
- export function Cell({ column, value }: CellProps) {
59
+ export function Cell({ column, row, value }: CellProps) {
58
60
  // If formatter is defined, use it directly without markdown
59
61
  if (column.formatter) {
60
62
  return (
61
63
  <MuiTableCell align={column.align}>
62
- {column.formatter(value)}
64
+ {column.formatter(value, row)}
63
65
  </MuiTableCell>
64
66
  )
65
67
  }
@@ -53,7 +53,12 @@ export function Row({
53
53
  </TableCell>
54
54
  )}
55
55
  {columns.map((column) => (
56
- <Cell key={column.id} column={column} value={row[column.id]} />
56
+ <Cell
57
+ key={column.id}
58
+ row={row}
59
+ column={column}
60
+ value={row[column.id]}
61
+ />
57
62
  ))}
58
63
  </MuiTableRow>
59
64
  )
@@ -27,7 +27,7 @@ export interface TableColumn {
27
27
  /** Enable sorting for this column */
28
28
  sortable?: boolean
29
29
  /** Custom cell value formatter */
30
- formatter?: (value: unknown) => ReactNode
30
+ formatter?: (value: unknown, row: TableRow) => ReactNode
31
31
  }
32
32
 
33
33
  /**