@carto/ps-react-ui 4.3.7 → 4.3.8

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 (27) 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/widgets/actions.js +714 -671
  10. package/dist/widgets/actions.js.map +1 -1
  11. package/dist/widgets/loader.js +58 -49
  12. package/dist/widgets/loader.js.map +1 -1
  13. package/package.json +3 -3
  14. package/src/widgets/actions/change-column/change-column.test.tsx +129 -2
  15. package/src/widgets/actions/change-column/change-column.tsx +79 -2
  16. package/src/widgets/actions/fullscreen/fullscreen.tsx +3 -0
  17. package/src/widgets/actions/fullscreen/types.ts +6 -1
  18. package/src/widgets/actions/index.ts +9 -3
  19. package/src/widgets/actions/lock-selection/lock-selection.tsx +26 -25
  20. package/src/widgets/actions/lock-selection/types.ts +17 -0
  21. package/src/widgets/actions/relative-data/relative-data.tsx +21 -18
  22. package/src/widgets/actions/relative-data/types.ts +7 -0
  23. package/src/widgets/actions/searcher/searcher.tsx +22 -40
  24. package/src/widgets/actions/searcher/types.ts +2 -0
  25. package/src/widgets/actions/stack-toggle/stack-toggle.tsx +22 -32
  26. package/src/widgets/actions/stack-toggle/types.ts +8 -0
  27. package/src/widgets/loader/loader.tsx +25 -24
@@ -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
  }