@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 +1 @@
1
- {"version":3,"file":"loader.js","sources":["../../src/widgets/loader/loader.tsx","../../src/widgets/loader/utils.ts"],"sourcesContent":["import { useEffect } from 'react'\nimport type { WidgetLoaderProps } from './types'\nimport { useWidgetStore } from '../stores/widget-store'\nimport type { WrapperState } from '../wrapper'\n\nexport function WidgetLoader<T extends object = Record<string, unknown>>(\n props: WidgetLoaderProps<T>,\n) {\n const setWidget = useWidgetStore((state) => state.setWidget)\n const executeToolPipeline = useWidgetStore(\n (state) => state.executeToolPipeline,\n )\n const executeConfigPipeline = useWidgetStore(\n (state) => state.executeConfigPipeline,\n )\n\n // Split into 3 effects for metadata and 1 for data pipeline:\n // Each property that can be modified independently gets its own effect to avoid\n // accidentally resetting other properties.\n //\n // - Effect 1: Type (can be modified by tools that change visualization type)\n // - Effect 2: Loading/Error states (change during fetch lifecycle)\n // - Effect 3: Config (can be modified by tools that change widget configuration)\n // - Effect 4: Data pipeline execution (transforms data through registered tools)\n // - Effect 5: Re-execute pipeline when tool state changes\n\n // Effect 1: Type updates\n useEffect(() => {\n setWidget<WrapperState>(props.id, {\n type: props.type,\n })\n }, [props.id, props.type, setWidget])\n\n // Effect 2: Loading and error states\n useEffect(() => {\n setWidget<WrapperState>(props.id, {\n isLoading: props.isLoading ?? false,\n isFetching: props.isFetching ?? false,\n error: props.error,\n })\n }, [props.id, props.isLoading, props.isFetching, props.error, setWidget])\n\n // Effect 3: Config updates — run through config pipeline\n useEffect(() => {\n if (props.config) {\n void executeConfigPipeline(props.id, props.config)\n }\n }, [props.id, props.config, executeConfigPipeline])\n\n // Effect 4: Execute tool pipeline when props.data changes\n useEffect(() => {\n void executeToolPipeline(props.id, props.data)\n }, [props.id, props.data, executeToolPipeline])\n\n // Effect 5: Re-execute pipelines when tool state changes (enabled/config)\n useEffect(() => {\n let prevTools = useWidgetStore.getState().widgets[props.id]?.registeredTools\n\n const unsubscribe = useWidgetStore.subscribe((state) => {\n const currentTools = state.widgets[props.id]?.registeredTools\n\n // Only re-execute if tools array changed\n if (currentTools !== prevTools) {\n prevTools = currentTools\n void executeToolPipeline(props.id, props.data)\n if (props.config) {\n void executeConfigPipeline(props.id, props.config)\n }\n }\n })\n\n return unsubscribe\n }, [\n props.id,\n props.data,\n props.config,\n executeToolPipeline,\n executeConfigPipeline,\n ])\n\n return props.children\n}\n","import deepmerge from 'deepmerge'\n\n/**\n * Config can be either an object or a function that receives baseConfig and data\n * and returns a partial config to be merged with the base.\n */\nexport type ConfigOrFn<TConfig, TData = unknown> =\n | Partial<TConfig>\n | ((baseConfig: TConfig, data: TData) => Partial<TConfig>)\n\n/**\n * Resolves a config that may be either an object or a function.\n * If it's a function, calls it with baseConfig and data.\n * If it's an object (or undefined), returns it as-is.\n */\nexport function resolveConfig<TConfig, TData>(\n config: ConfigOrFn<TConfig, TData> | undefined,\n baseConfig: TConfig,\n data: TData,\n): Partial<TConfig> | undefined {\n if (typeof config === 'function') {\n return config(baseConfig, data)\n }\n return config\n}\n\nexport function mergeWidgetConfig<T>(\n ...options: [Partial<T> | undefined, Partial<T> | undefined]\n): T {\n return deepmerge(options[0] ?? {}, options[1] ?? {}, {\n arrayMerge(_, source) {\n return source as T[keyof T][]\n },\n })\n}\n"],"names":["WidgetLoader","props","$","_c","setWidget","useWidgetStore","_temp","executeToolPipeline","_temp2","executeConfigPipeline","_temp3","t0","t1","id","type","useEffect","t2","t3","error","isFetching","isLoading","t4","t5","config","t6","t7","data","t8","t9","prevTools","getState","widgets","registeredTools","subscribe","state_2","currentTools","state","children","state_1","state_0","resolveConfig","baseConfig","mergeWidgetConfig","options","deepmerge","arrayMerge","_","source"],"mappings":"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"}
1
+ {"version":3,"file":"loader.js","sources":["../../src/widgets/loader/loader.tsx","../../src/widgets/loader/utils.ts"],"sourcesContent":["import { useEffect, useRef, useSyncExternalStore } from 'react'\nimport type { WidgetLoaderProps } from './types'\nimport { useWidgetStore } from '../stores/widget-store'\nimport type { WrapperState } from '../wrapper'\n\nexport function WidgetLoader<T extends object = Record<string, unknown>>(\n props: WidgetLoaderProps<T>,\n) {\n const setWidget = useWidgetStore((state) => state.setWidget)\n const executeToolPipeline = useWidgetStore(\n (state) => state.executeToolPipeline,\n )\n const executeConfigPipeline = useWidgetStore(\n (state) => state.executeConfigPipeline,\n )\n\n const registeredTools = useSyncExternalStore(\n useWidgetStore.subscribe,\n () => useWidgetStore.getState().widgets[props.id]?.registeredTools,\n )\n\n const dataRef = useRef(props.data)\n const configRef = useRef(props.config)\n const isMountedRef = useRef(false)\n\n useEffect(() => {\n dataRef.current = props.data\n configRef.current = props.config\n })\n\n // Split into 3 effects for metadata and 1 for data pipeline:\n // Each property that can be modified independently gets its own effect to avoid\n // accidentally resetting other properties.\n //\n // - Effect 1: Type (can be modified by tools that change visualization type)\n // - Effect 2: Loading/Error states (change during fetch lifecycle)\n // - Effect 3: Config (can be modified by tools that change widget configuration)\n // - Effect 4: Data pipeline execution (transforms data through registered tools)\n // - Effect 5: Re-execute pipeline when tool state changes\n\n // Effect 1: Type updates\n useEffect(() => {\n setWidget<WrapperState>(props.id, {\n type: props.type,\n })\n }, [props.id, props.type, setWidget])\n\n // Effect 2: Loading and error states\n useEffect(() => {\n setWidget<WrapperState>(props.id, {\n isLoading: props.isLoading ?? false,\n isFetching: props.isFetching ?? false,\n error: props.error,\n })\n }, [props.id, props.isLoading, props.isFetching, props.error, setWidget])\n\n // Effect 3: Config updates — run through config pipeline\n useEffect(() => {\n if (props.config) {\n void executeConfigPipeline(props.id, props.config)\n }\n }, [props.id, props.config, executeConfigPipeline])\n\n // Effect 4: Execute tool pipeline when props.data changes\n useEffect(() => {\n void executeToolPipeline(props.id, props.data)\n }, [props.id, props.data, executeToolPipeline])\n\n // Effect 5: Re-execute pipelines when registered tools change\n useEffect(() => {\n if (!isMountedRef.current) {\n isMountedRef.current = true\n return\n }\n\n void executeToolPipeline(props.id, dataRef.current)\n if (configRef.current) {\n void executeConfigPipeline(props.id, configRef.current)\n }\n }, [registeredTools, props.id, executeToolPipeline, executeConfigPipeline])\n\n return props.children\n}\n","import deepmerge from 'deepmerge'\n\n/**\n * Config can be either an object or a function that receives baseConfig and data\n * and returns a partial config to be merged with the base.\n */\nexport type ConfigOrFn<TConfig, TData = unknown> =\n | Partial<TConfig>\n | ((baseConfig: TConfig, data: TData) => Partial<TConfig>)\n\n/**\n * Resolves a config that may be either an object or a function.\n * If it's a function, calls it with baseConfig and data.\n * If it's an object (or undefined), returns it as-is.\n */\nexport function resolveConfig<TConfig, TData>(\n config: ConfigOrFn<TConfig, TData> | undefined,\n baseConfig: TConfig,\n data: TData,\n): Partial<TConfig> | undefined {\n if (typeof config === 'function') {\n return config(baseConfig, data)\n }\n return config\n}\n\nexport function mergeWidgetConfig<T>(\n ...options: [Partial<T> | undefined, Partial<T> | undefined]\n): T {\n return deepmerge(options[0] ?? {}, options[1] ?? {}, {\n arrayMerge(_, source) {\n return source as T[keyof T][]\n },\n })\n}\n"],"names":["WidgetLoader","props","$","_c","setWidget","useWidgetStore","_temp","executeToolPipeline","_temp2","executeConfigPipeline","_temp3","t0","id","getState","widgets","registeredTools","useSyncExternalStore","subscribe","dataRef","useRef","data","configRef","config","isMountedRef","t1","current","useEffect","t2","t3","type","t4","t5","error","isFetching","isLoading","t6","t7","t8","t9","t10","t11","children","state_1","state","state_0","resolveConfig","baseConfig","mergeWidgetConfig","options","deepmerge","arrayMerge","_","source"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carto/ps-react-ui",
3
- "version": "4.3.7",
3
+ "version": "4.3.8",
4
4
  "description": "CARTO's Professional Service React Material library",
5
5
  "type": "module",
6
6
  "devDependencies": {
@@ -15,8 +15,8 @@
15
15
  "html2canvas": "1.4.1",
16
16
  "react-markdown": "10.1.0",
17
17
  "zustand": "5.0.11",
18
- "@carto/ps-utils": "2.0.1",
19
- "@carto/ps-common-types": "1.0.0"
18
+ "@carto/ps-common-types": "1.0.0",
19
+ "@carto/ps-utils": "2.0.1"
20
20
  },
21
21
  "peerDependencies": {
22
22
  "@dnd-kit/core": "^6.0.0",
@@ -1,6 +1,6 @@
1
1
  import { describe, test, expect, beforeEach, vi } from 'vitest'
2
- import { render, screen, fireEvent } from '@testing-library/react'
3
- import { ChangeColumn } from './change-column'
2
+ import { render, screen, fireEvent, cleanup } from '@testing-library/react'
3
+ import { ChangeColumn, CHANGE_COLUMN_TOOL_ID } from './change-column'
4
4
  import { useWidgetStore } from '../../stores/widget-store'
5
5
  import type { TableColumn } from '../../table/types'
6
6
 
@@ -160,4 +160,131 @@ describe('ChangeColumn', () => {
160
160
  expect(item.getAttribute('tabindex')).toBe('0')
161
161
  })
162
162
  })
163
+
164
+ describe('config tool registration', () => {
165
+ test('registers config tool on mount', () => {
166
+ useWidgetStore.getState().setWidget(widgetId, {
167
+ columns: mockColumns,
168
+ })
169
+
170
+ render(<ChangeColumn id={widgetId} />)
171
+
172
+ const widget = useWidgetStore.getState().getWidget(widgetId)
173
+ const tool = widget?.registeredTools?.find(
174
+ (t) => t.id === CHANGE_COLUMN_TOOL_ID,
175
+ )
176
+
177
+ expect(tool).toBeDefined()
178
+ expect(tool?.type).toBe('config')
179
+ expect(tool?.order).toBe(100)
180
+ expect(tool?.enabled).toBe(true)
181
+ })
182
+
183
+ test('unregisters config tool on unmount', () => {
184
+ useWidgetStore.getState().setWidget(widgetId, {
185
+ columns: mockColumns,
186
+ })
187
+
188
+ render(<ChangeColumn id={widgetId} />)
189
+
190
+ // Verify tool is registered
191
+ let widget = useWidgetStore.getState().getWidget(widgetId)
192
+ expect(
193
+ widget?.registeredTools?.find((t) => t.id === CHANGE_COLUMN_TOOL_ID),
194
+ ).toBeDefined()
195
+
196
+ // Unmount
197
+ cleanup()
198
+
199
+ // Verify tool is unregistered
200
+ widget = useWidgetStore.getState().getWidget(widgetId)
201
+ expect(
202
+ widget?.registeredTools?.find((t) => t.id === CHANGE_COLUMN_TOOL_ID),
203
+ ).toBeUndefined()
204
+ })
205
+
206
+ test('config tool returns currentConfig unchanged when columns match widget state', () => {
207
+ useWidgetStore.getState().setWidget(widgetId, {
208
+ columns: mockColumns,
209
+ })
210
+
211
+ render(<ChangeColumn id={widgetId} />)
212
+
213
+ const widget = useWidgetStore.getState().getWidget(widgetId)
214
+ const tool = widget?.registeredTools?.find(
215
+ (t) => t.id === CHANGE_COLUMN_TOOL_ID,
216
+ )
217
+
218
+ // When config columns match widget columns, fn returns the same reference
219
+ const input = { columns: mockColumns }
220
+ const result = tool?.fn(input, tool.config)
221
+ expect(result).toBe(input)
222
+ })
223
+
224
+ test('column order persists when config pipeline re-runs with original config', async () => {
225
+ useWidgetStore.getState().setWidget(widgetId, {
226
+ columns: mockColumns,
227
+ })
228
+
229
+ render(<ChangeColumn id={widgetId} />)
230
+
231
+ // Simulate a drag that reorders columns via setWidget
232
+ const reorderedColumns: TableColumn[] = [
233
+ { id: 'population', label: 'Population' },
234
+ { id: 'name', label: 'Name' },
235
+ { id: 'country', label: 'Country' },
236
+ ]
237
+ useWidgetStore.getState().setWidget(widgetId, {
238
+ columns: reorderedColumns,
239
+ })
240
+
241
+ // Simulate config pipeline re-running with original column order
242
+ const originalConfig = { columns: mockColumns }
243
+ await useWidgetStore
244
+ .getState()
245
+ .executeConfigPipeline(widgetId, originalConfig)
246
+
247
+ // Verify the columns are in the reordered order
248
+ const widget = useWidgetStore.getState().getWidget(widgetId)
249
+ const resultColumns = (widget as unknown as Record<string, unknown>)
250
+ ?.columns as TableColumn[] | undefined
251
+ expect(resultColumns?.map((c) => c.id)).toEqual([
252
+ 'population',
253
+ 'name',
254
+ 'country',
255
+ ])
256
+ })
257
+
258
+ test('new columns appended at the end when not in widget order', async () => {
259
+ useWidgetStore.getState().setWidget(widgetId, {
260
+ columns: mockColumns,
261
+ })
262
+
263
+ render(<ChangeColumn id={widgetId} />)
264
+
265
+ // Simulate widget columns having only a partial order (e.g., user dragged 2 of 3)
266
+ const partialOrder: TableColumn[] = [
267
+ { id: 'country', label: 'Country' },
268
+ { id: 'name', label: 'Name' },
269
+ ]
270
+ useWidgetStore.getState().setWidget(widgetId, {
271
+ columns: partialOrder,
272
+ })
273
+
274
+ // Config pipeline runs with all columns (includes 'population' not in widget order)
275
+ const originalConfig = { columns: mockColumns }
276
+ await useWidgetStore
277
+ .getState()
278
+ .executeConfigPipeline(widgetId, originalConfig)
279
+
280
+ const widget = useWidgetStore.getState().getWidget(widgetId)
281
+ const resultColumns = (widget as unknown as Record<string, unknown>)
282
+ ?.columns as TableColumn[] | undefined
283
+ expect(resultColumns?.map((c) => c.id)).toEqual([
284
+ 'country',
285
+ 'name',
286
+ 'population',
287
+ ])
288
+ })
289
+ })
163
290
  })
@@ -14,16 +14,24 @@ import {
14
14
  verticalListSortingStrategy,
15
15
  } from '@dnd-kit/sortable'
16
16
  import { IconButton, Menu, SvgIcon } from '@mui/material'
17
- import { useCallback, useMemo, useState, type MouseEvent } from 'react'
17
+ import {
18
+ useCallback,
19
+ useEffect,
20
+ useMemo,
21
+ useState,
22
+ type MouseEvent,
23
+ } from 'react'
18
24
  import { useWidgetStore } from '../../stores/widget-store'
19
25
  import type { ChangeColumnProps } from './types'
20
26
  import { actionButtonStyles } from '../shared/styles'
21
27
  import { Tooltip } from '../../../components'
22
- import type { TableWidgetState } from '../../table/types'
28
+ import type { TableColumn, TableWidgetState } from '../../table/types'
23
29
  import { ChangeColumnIcon } from './change-column-icon'
24
30
  import { SortableColumnItem } from './sortable-column-item'
25
31
  import { useShallow } from 'zustand/shallow'
26
32
 
33
+ export const CHANGE_COLUMN_TOOL_ID = 'change-column'
34
+
27
35
  /**
28
36
  * Widget action to reorder columns in a table widget via drag-and-drop.
29
37
  *
@@ -31,6 +39,9 @@ import { useShallow } from 'zustand/shallow'
31
39
  * drag and drop columns to reorder them. All columns are displayed and
32
40
  * can be reordered.
33
41
  *
42
+ * Registers as a config pipeline tool so that column order is automatically
43
+ * re-applied when the base config is updated (e.g., by WidgetLoader).
44
+ *
34
45
  * Returns null if there are fewer than 2 columns.
35
46
  *
36
47
  * @example
@@ -47,10 +58,64 @@ export function ChangeColumn({
47
58
  }: ChangeColumnProps) {
48
59
  const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
49
60
  const setWidget = useWidgetStore((state) => state.setWidget)
61
+ const registerTool = useWidgetStore((state) => state.registerTool)
62
+ const unregisterTool = useWidgetStore((state) => state.unregisterTool)
50
63
  const columns = useWidgetStore(
51
64
  useShallow((state) => state.getWidget<TableWidgetState>(id)?.columns),
52
65
  )
53
66
 
67
+ /**
68
+ * Config tool function that reorders columns to match the current widget state.
69
+ * Reads desired order from the widget store (set by handleDragEnd via setWidget).
70
+ * Preserves referential identity when the order already matches to prevent
71
+ * re-render loops in the config pipeline.
72
+ */
73
+ const reorderFn = useCallback(
74
+ (currentConfig: unknown): unknown => {
75
+ const widgetState = useWidgetStore
76
+ .getState()
77
+ .getWidget<TableWidgetState>(id)
78
+ const currentColumns = widgetState?.columns
79
+ if (!currentColumns || currentColumns.length === 0) return currentConfig
80
+
81
+ const config = currentConfig as Record<string, unknown>
82
+ const configColumns = config.columns as TableColumn[] | undefined
83
+ if (!configColumns || configColumns.length === 0) return currentConfig
84
+
85
+ // Check if config columns are already in the same order as widget columns
86
+ const alreadyMatches =
87
+ configColumns.length === currentColumns.length &&
88
+ configColumns.every((col, i) => col.id === currentColumns[i]?.id)
89
+ if (alreadyMatches) return currentConfig
90
+
91
+ // Reorder config columns to match widget column order
92
+ const columnMap = new Map(configColumns.map((col) => [col.id, col]))
93
+ const reordered: TableColumn[] = []
94
+
95
+ for (const widgetCol of currentColumns) {
96
+ const col = columnMap.get(widgetCol.id)
97
+ if (col) {
98
+ reordered.push(col)
99
+ columnMap.delete(widgetCol.id)
100
+ }
101
+ }
102
+
103
+ // Append any new columns not in the widget order
104
+ for (const col of columnMap.values()) {
105
+ reordered.push(col)
106
+ }
107
+
108
+ // If result matches current widget columns, reuse the same array reference
109
+ // to prevent downstream subscribers from detecting a change
110
+ const matchesWidget =
111
+ reordered.length === currentColumns.length &&
112
+ reordered.every((col, i) => col.id === currentColumns[i]?.id)
113
+
114
+ return { ...config, columns: matchesWidget ? currentColumns : reordered }
115
+ },
116
+ [id],
117
+ )
118
+
54
119
  const sensors = useSensors(
55
120
  useSensor(PointerSensor),
56
121
  useSensor(KeyboardSensor, {
@@ -63,6 +128,18 @@ export function ChangeColumn({
63
128
  [columns],
64
129
  )
65
130
 
131
+ // Register config tool on mount
132
+ useEffect(() => {
133
+ registerTool(id, {
134
+ id: CHANGE_COLUMN_TOOL_ID,
135
+ type: 'config',
136
+ order: 100,
137
+ enabled: true,
138
+ fn: reorderFn,
139
+ })
140
+ return () => unregisterTool(id, CHANGE_COLUMN_TOOL_ID)
141
+ }, [id, registerTool, unregisterTool, reorderFn])
142
+
66
143
  const handleToggle = useCallback((event: MouseEvent<HTMLElement>) => {
67
144
  event.stopPropagation()
68
145
  setAnchorEl(event.currentTarget)
@@ -22,6 +22,7 @@ export function FullScreen({
22
22
  Icon,
23
23
  IconButtonProps,
24
24
  DialogContentProps: { sx, ...DialogContentProps } = {},
25
+ DialogProps,
25
26
  }: FullScreenProps) {
26
27
  const isFullScreen = useWidgetStore(
27
28
  useShallow((state) => state.getWidget<FullScreenState>(id)?.isFullScreen),
@@ -48,7 +49,9 @@ export function FullScreen({
48
49
  <Dialog
49
50
  maxWidth={false}
50
51
  open={!!isFullScreen}
52
+ keepMounted
51
53
  aria-labelledby={labels?.ariaLabel ?? `fullscreen-dialog-title-${id}`}
54
+ {...DialogProps}
52
55
  onClose={() => updateFullScreenConfig({ isFullScreen: false })}
53
56
  >
54
57
  <DialogTitle
@@ -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
+ >