@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.
- package/dist/types/widgets/actions/change-column/change-column.d.ts +4 -0
- package/dist/types/widgets/actions/fullscreen/fullscreen.d.ts +1 -1
- package/dist/types/widgets/actions/fullscreen/types.d.ts +2 -1
- package/dist/types/widgets/actions/index.d.ts +3 -3
- package/dist/types/widgets/actions/lock-selection/types.d.ts +13 -0
- package/dist/types/widgets/actions/relative-data/types.d.ts +4 -0
- package/dist/types/widgets/actions/searcher/types.d.ts +2 -0
- package/dist/types/widgets/actions/stack-toggle/types.d.ts +4 -0
- package/dist/widgets/actions.js +714 -671
- package/dist/widgets/actions.js.map +1 -1
- package/dist/widgets/loader.js +58 -49
- package/dist/widgets/loader.js.map +1 -1
- package/package.json +3 -3
- package/src/widgets/actions/change-column/change-column.test.tsx +129 -2
- package/src/widgets/actions/change-column/change-column.tsx +79 -2
- package/src/widgets/actions/fullscreen/fullscreen.tsx +3 -0
- package/src/widgets/actions/fullscreen/types.ts +6 -1
- package/src/widgets/actions/index.ts +9 -3
- package/src/widgets/actions/lock-selection/lock-selection.tsx +26 -25
- package/src/widgets/actions/lock-selection/types.ts +17 -0
- package/src/widgets/actions/relative-data/relative-data.tsx +21 -18
- package/src/widgets/actions/relative-data/types.ts +7 -0
- package/src/widgets/actions/searcher/searcher.tsx +22 -40
- package/src/widgets/actions/searcher/types.ts +2 -0
- package/src/widgets/actions/stack-toggle/stack-toggle.tsx +22 -32
- package/src/widgets/actions/stack-toggle/types.ts +8 -0
- 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
|
|
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.
|
|
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-
|
|
19
|
-
"@carto/ps-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
|
65
|
+
config: { lockedItems },
|
|
73
66
|
})
|
|
74
67
|
|
|
75
68
|
return () => unregisterTool(id, LOCK_SELECTION_TOOL_ID)
|
|
76
|
-
}, [id, order, registerTool, unregisterTool,
|
|
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
|
-
|
|
82
|
-
|
|
80
|
+
setWidget(id, {
|
|
81
|
+
isLocked: false,
|
|
82
|
+
lockedItems: [],
|
|
83
|
+
})
|
|
83
84
|
} else {
|
|
84
85
|
// Lock: save selected items and enable tool
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
setWidget(id, {
|
|
87
|
+
isLocked: true,
|
|
87
88
|
lockedItems: selectedItems,
|
|
88
89
|
})
|
|
89
90
|
}
|
|
90
|
-
}, [id, isLocked, selectedItems,
|
|
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
|
|
48
|
-
|
|
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 =
|
|
50
|
+
const isRelative = storeIsRelative ?? defaultIsRelative
|
|
55
51
|
|
|
56
|
-
//
|
|
52
|
+
// Initialize store with default value on mount
|
|
57
53
|
useEffect(() => {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
+
>
|