@carto/ps-react-ui 4.3.6 → 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/components.js +123 -123
- package/dist/components.js.map +1 -1
- package/dist/error-CEkRPccv.js +39 -0
- package/dist/error-CEkRPccv.js.map +1 -0
- package/dist/{lasso-tool-BctzdzBu.js → lasso-tool-jl4YK02H.js} +19 -19
- package/dist/lasso-tool-jl4YK02H.js.map +1 -0
- package/dist/no-data-hR3KcJ-_.js +60 -0
- package/dist/no-data-hR3KcJ-_.js.map +1 -0
- package/dist/{row-D3uVFImu.js → row-BKmVAUN5.js} +2 -2
- package/dist/{row-D3uVFImu.js.map → row-BKmVAUN5.js.map} +1 -1
- package/dist/{series-BAImrSBo.js → series-D1pynfeh.js} +3 -3
- package/dist/{series-BAImrSBo.js.map → series-D1pynfeh.js.map} +1 -1
- package/dist/{styles-CCZnY17y.js → styles-DrPyd0y5.js} +28 -22
- package/dist/styles-DrPyd0y5.js.map +1 -0
- package/dist/types/components/lasso-tool/types.d.ts +1 -1
- package/dist/types/widgets/_shared/chart-config/index.d.ts +1 -1
- package/dist/types/widgets/_shared/chart-config/option-builders.d.ts +7 -0
- package/dist/types/widgets/_shared/chart-config/option-builders.test.d.ts +1 -0
- 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 +1 -1
- package/dist/types/widgets/actions/lock-selection/types.d.ts +7 -7
- package/dist/types/widgets/actions/relative-data/types.d.ts +1 -1
- package/dist/types/widgets/echart/types.d.ts +0 -4
- package/dist/types/widgets/echart/utils.d.ts +2 -1
- package/dist/types/widgets/error/error.d.ts +1 -1
- package/dist/types/widgets/error/types.d.ts +8 -0
- package/dist/types/widgets/loader/loader.d.ts +1 -1
- package/dist/types/widgets/loader/types.d.ts +1 -1
- package/dist/types/widgets/stores/types.d.ts +1 -1
- package/dist/{use-widget-ref-B8x4sHIj.js → use-widget-ref-P-2i0MJG.js} +2 -2
- package/dist/{use-widget-ref-B8x4sHIj.js.map → use-widget-ref-P-2i0MJG.js.map} +1 -1
- package/dist/{utils-D3-eQyDR.js → utils-idmvq0Oa.js} +17 -16
- package/dist/utils-idmvq0Oa.js.map +1 -0
- package/dist/{widget-store-Dn0Bnc4h.js → widget-store-CzDt8oSK.js} +31 -46
- package/dist/widget-store-CzDt8oSK.js.map +1 -0
- package/dist/widgets/actions.js +714 -697
- package/dist/widgets/actions.js.map +1 -1
- package/dist/widgets/bar.js +67 -63
- package/dist/widgets/bar.js.map +1 -1
- package/dist/widgets/category.js +250 -241
- package/dist/widgets/category.js.map +1 -1
- package/dist/widgets/echart.js +93 -100
- package/dist/widgets/echart.js.map +1 -1
- package/dist/widgets/error.js +1 -1
- package/dist/widgets/formula.js +64 -72
- package/dist/widgets/formula.js.map +1 -1
- package/dist/widgets/histogram.js +75 -73
- package/dist/widgets/histogram.js.map +1 -1
- package/dist/widgets/loader.js +58 -49
- package/dist/widgets/loader.js.map +1 -1
- package/dist/widgets/markdown.js +2 -2
- package/dist/widgets/no-data.js +1 -1
- package/dist/widgets/pie.js +4 -4
- package/dist/widgets/range.js +97 -105
- package/dist/widgets/range.js.map +1 -1
- package/dist/widgets/scatterplot.js +8 -8
- package/dist/widgets/skeleton-loader.js +1 -1
- package/dist/widgets/spread.js +84 -100
- package/dist/widgets/spread.js.map +1 -1
- package/dist/widgets/stores.js +1 -1
- package/dist/widgets/table.js +493 -485
- package/dist/widgets/table.js.map +1 -1
- package/dist/widgets/timeseries.js +4 -4
- package/dist/widgets/wrapper.js +156 -156
- package/dist/widgets/wrapper.js.map +1 -1
- package/dist/widgets.js +4 -4
- package/package.json +1 -1
- package/src/components/lasso-tool/lasso-tool-inline.tsx +19 -17
- package/src/components/lasso-tool/lasso-tool.tsx +22 -20
- package/src/components/lasso-tool/types.ts +4 -3
- package/src/widgets/_shared/chart-config/index.ts +1 -0
- package/src/widgets/_shared/chart-config/option-builders.test.ts +40 -0
- package/src/widgets/_shared/chart-config/option-builders.ts +12 -0
- 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 +8 -8
- package/src/widgets/actions/fullscreen/types.ts +6 -1
- package/src/widgets/actions/index.ts +4 -1
- package/src/widgets/actions/lock-selection/lock-selection.test.tsx +28 -30
- package/src/widgets/actions/lock-selection/types.ts +8 -8
- package/src/widgets/actions/relative-data/relative-data.test.tsx +13 -13
- package/src/widgets/actions/relative-data/types.ts +1 -1
- package/src/widgets/actions/stack-toggle/stack-toggle.test.tsx +19 -9
- package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +113 -95
- package/src/widgets/bar/config.ts +37 -28
- package/src/widgets/category/category-ui.tsx +25 -22
- package/src/widgets/echart/echart-ui.test.tsx +3 -18
- package/src/widgets/echart/echart-ui.tsx +4 -22
- package/src/widgets/echart/echart.test.tsx +9 -25
- package/src/widgets/echart/echart.tsx +36 -29
- package/src/widgets/echart/types.ts +0 -4
- package/src/widgets/echart/utils.ts +3 -1
- package/src/widgets/error/error.tsx +17 -14
- package/src/widgets/error/types.ts +10 -0
- package/src/widgets/formula/components/value.tsx +13 -13
- package/src/widgets/histogram/config.ts +36 -29
- package/src/widgets/loader/loader.tsx +28 -25
- package/src/widgets/loader/types.ts +3 -1
- package/src/widgets/no-data/no-data.tsx +8 -11
- package/src/widgets/range/components/range-item.tsx +9 -13
- package/src/widgets/spread/components/max-value.tsx +13 -13
- package/src/widgets/spread/components/min-value.tsx +13 -13
- package/src/widgets/stores/types.ts +1 -4
- package/src/widgets/stores/widget-store.ts +1 -27
- package/src/widgets/table/hooks/use-pagination.ts +44 -35
- package/src/widgets/table/hooks/use-sort.ts +25 -23
- package/src/widgets/wrapper/wrapper-ui.tsx +16 -17
- package/dist/error-piB8FwYO.js +0 -38
- package/dist/error-piB8FwYO.js.map +0 -1
- package/dist/lasso-tool-BctzdzBu.js.map +0 -1
- package/dist/no-data-jdlbMef0.js +0 -61
- package/dist/no-data-jdlbMef0.js.map +0 -1
- package/dist/styles-CCZnY17y.js.map +0 -1
- package/dist/utils-D3-eQyDR.js.map +0 -1
- package/dist/widget-store-Dn0Bnc4h.js.map +0 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { niceNum } from './option-builders'
|
|
3
|
+
|
|
4
|
+
describe('niceNum', () => {
|
|
5
|
+
it('should return 0 for 0', () => {
|
|
6
|
+
expect(niceNum(0)).toBe(0)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('should return 0 for negative values', () => {
|
|
10
|
+
expect(niceNum(-5)).toBe(0)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('should round 547 to 600', () => {
|
|
14
|
+
expect(niceNum(547)).toBe(600)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should keep 200 as 200', () => {
|
|
18
|
+
expect(niceNum(200)).toBe(200)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should round 1200 to 2000', () => {
|
|
22
|
+
expect(niceNum(1200)).toBe(2000)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should round 18 to 20', () => {
|
|
26
|
+
expect(niceNum(18)).toBe(20)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should keep 5 as 5', () => {
|
|
30
|
+
expect(niceNum(5)).toBe(5)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should round 350 to 400', () => {
|
|
34
|
+
expect(niceNum(350)).toBe(400)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should keep 1 as 1', () => {
|
|
38
|
+
expect(niceNum(1)).toBe(1)
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -9,6 +9,18 @@ import type {
|
|
|
9
9
|
* Shared EChart configuration builders for chart widgets
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Rounds a value up to the nearest "nice" number.
|
|
14
|
+
* A nice number is a multiple of 10^floor(log10(value)).
|
|
15
|
+
*
|
|
16
|
+
* Examples: 547 → 600, 200 → 200, 1200 → 2000, 18 → 20, 5 → 5
|
|
17
|
+
*/
|
|
18
|
+
export function niceNum(value: number): number {
|
|
19
|
+
if (value <= 0) return 0
|
|
20
|
+
const base = Math.pow(10, Math.floor(Math.log10(value)))
|
|
21
|
+
return Math.ceil(value / base) * base
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
/**
|
|
13
25
|
* Builds standard legend configuration for chart widgets
|
|
14
26
|
*
|
|
@@ -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,15 +22,13 @@ export function FullScreen({
|
|
|
22
22
|
Icon,
|
|
23
23
|
IconButtonProps,
|
|
24
24
|
DialogContentProps: { sx, ...DialogContentProps } = {},
|
|
25
|
+
DialogProps,
|
|
25
26
|
}: FullScreenProps) {
|
|
26
|
-
const
|
|
27
|
-
useShallow((state) =>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
title: widget?.title,
|
|
32
|
-
}
|
|
33
|
-
}),
|
|
27
|
+
const isFullScreen = useWidgetStore(
|
|
28
|
+
useShallow((state) => state.getWidget<FullScreenState>(id)?.isFullScreen),
|
|
29
|
+
)
|
|
30
|
+
const title = useWidgetStore(
|
|
31
|
+
useShallow((state) => state.getWidget<FullScreenState>(id)?.title),
|
|
34
32
|
)
|
|
35
33
|
const setWidget = useWidgetStore((state) => state.setWidget)
|
|
36
34
|
|
|
@@ -51,7 +49,9 @@ export function FullScreen({
|
|
|
51
49
|
<Dialog
|
|
52
50
|
maxWidth={false}
|
|
53
51
|
open={!!isFullScreen}
|
|
52
|
+
keepMounted
|
|
54
53
|
aria-labelledby={labels?.ariaLabel ?? `fullscreen-dialog-title-${id}`}
|
|
54
|
+
{...DialogProps}
|
|
55
55
|
onClose={() => updateFullScreenConfig({ isFullScreen: false })}
|
|
56
56
|
>
|
|
57
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
|
}
|
|
@@ -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 */
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { describe, test, expect, beforeEach } from 'vitest'
|
|
2
2
|
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
-
import { LockSelection } from './lock-selection'
|
|
3
|
+
import { LockSelection, LOCK_SELECTION_TOOL_ID } from './lock-selection'
|
|
4
4
|
import { useWidgetStore } from '../../stores/widget-store'
|
|
5
5
|
import type { EchartWidgetData } from '../../echart/types'
|
|
6
|
-
import type { LockSelectionState } from './types'
|
|
7
6
|
import { WidgetLoader } from '../../loader/loader'
|
|
8
7
|
|
|
9
8
|
// Test data
|
|
@@ -45,25 +44,29 @@ describe('LockSelection', () => {
|
|
|
45
44
|
})
|
|
46
45
|
|
|
47
46
|
test('shows unlock tooltip when locked', () => {
|
|
48
|
-
// Pre-set the store to locked state
|
|
49
|
-
useWidgetStore.getState().setWidget(widgetId, { isLocked: true })
|
|
50
|
-
|
|
51
47
|
render(<LockSelection id={widgetId} selectedItems={['Electronics']} />)
|
|
52
48
|
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
// Lock by clicking the button
|
|
50
|
+
const button = screen.getByRole('button')
|
|
51
|
+
fireEvent.click(button)
|
|
52
|
+
|
|
53
|
+
const unlockedButton = screen.getByRole('button', {
|
|
54
|
+
name: 'Unlock selection',
|
|
55
|
+
})
|
|
56
|
+
expect(unlockedButton).toBeTruthy()
|
|
55
57
|
})
|
|
56
58
|
|
|
57
|
-
test('toggles isLocked in
|
|
59
|
+
test('toggles isLocked in tool state when clicked', () => {
|
|
58
60
|
render(<LockSelection id={widgetId} selectedItems={['Electronics']} />)
|
|
59
61
|
|
|
60
62
|
const button = screen.getByRole('button')
|
|
61
63
|
fireEvent.click(button)
|
|
62
64
|
|
|
63
|
-
const widget = useWidgetStore
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
66
|
+
const tool = widget?.registeredTools?.find(
|
|
67
|
+
(t) => t.id === LOCK_SELECTION_TOOL_ID,
|
|
68
|
+
)
|
|
69
|
+
expect(tool?.enabled).toBe(true)
|
|
67
70
|
})
|
|
68
71
|
|
|
69
72
|
test('filters data in store when locking', async () => {
|
|
@@ -86,36 +89,33 @@ describe('LockSelection', () => {
|
|
|
86
89
|
|
|
87
90
|
await useWidgetStore.getState().executeToolPipeline(widgetId, mockData)
|
|
88
91
|
|
|
89
|
-
const widget = useWidgetStore
|
|
90
|
-
.getState()
|
|
91
|
-
.getWidget<LockSelectionState>(widgetId)
|
|
92
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
92
93
|
expect(widget?.data).toEqual([[{ name: 'Electronics', value: 440 }]])
|
|
93
94
|
})
|
|
94
95
|
|
|
95
96
|
test('clears locked items when unlocking', () => {
|
|
96
|
-
// Pre-set the store to locked state
|
|
97
|
-
useWidgetStore.getState().setWidget(widgetId, {
|
|
98
|
-
isLocked: true,
|
|
99
|
-
lockedItems: ['Electronics'],
|
|
100
|
-
})
|
|
101
|
-
|
|
102
97
|
render(<LockSelection id={widgetId} selectedItems={['Electronics']} />)
|
|
103
98
|
|
|
99
|
+
// Lock first
|
|
104
100
|
const button = screen.getByRole('button')
|
|
105
101
|
fireEvent.click(button)
|
|
106
102
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
// Then unlock
|
|
104
|
+
fireEvent.click(button)
|
|
105
|
+
|
|
106
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
107
|
+
const tool = widget?.registeredTools?.find(
|
|
108
|
+
(t) => t.id === LOCK_SELECTION_TOOL_ID,
|
|
109
|
+
)
|
|
110
|
+
expect(tool?.enabled).toBe(false)
|
|
111
111
|
})
|
|
112
112
|
|
|
113
113
|
test('has active state when locked', () => {
|
|
114
|
-
useWidgetStore.getState().setWidget(widgetId, { isLocked: true })
|
|
115
|
-
|
|
116
114
|
render(<LockSelection id={widgetId} selectedItems={['Electronics']} />)
|
|
117
115
|
|
|
118
116
|
const button = screen.getByRole('button')
|
|
117
|
+
fireEvent.click(button)
|
|
118
|
+
|
|
119
119
|
expect(button.getAttribute('data-active')).toBe('true')
|
|
120
120
|
})
|
|
121
121
|
|
|
@@ -175,9 +175,7 @@ describe('LockSelection', () => {
|
|
|
175
175
|
.getState()
|
|
176
176
|
.executeToolPipeline(widgetId, multiSeriesData)
|
|
177
177
|
|
|
178
|
-
const widget = useWidgetStore
|
|
179
|
-
.getState()
|
|
180
|
-
.getWidget<LockSelectionState>(widgetId)
|
|
178
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
181
179
|
expect(widget?.data).toEqual([
|
|
182
180
|
[{ name: 'Electronics', value: 440 }],
|
|
183
181
|
[{ name: 'Electronics', value: 520 }],
|
|
@@ -2,14 +2,6 @@ import type { IconButtonProps } from '@mui/material'
|
|
|
2
2
|
import type { ReactNode } from 'react'
|
|
3
3
|
import type { BaseWidgetState } from '../../stores/types'
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Lock selection specific state properties.
|
|
7
|
-
*/
|
|
8
|
-
export interface LockSelectionStateProps {
|
|
9
|
-
/** Whether the selection is currently locked */
|
|
10
|
-
isLocked?: boolean
|
|
11
|
-
}
|
|
12
|
-
|
|
13
5
|
/**
|
|
14
6
|
* Widget state extension for lock selection functionality.
|
|
15
7
|
* Extends the base widget state with lock-specific properties.
|
|
@@ -39,3 +31,11 @@ export interface LockSelectionProps {
|
|
|
39
31
|
/** Custom icon to display */
|
|
40
32
|
Icon?: ReactNode
|
|
41
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
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { describe, test, expect, beforeEach } from 'vitest'
|
|
2
2
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
3
|
-
import { RelativeData } from './relative-data'
|
|
3
|
+
import { RelativeData, RELATIVE_DATA_TOOL_ID } from './relative-data'
|
|
4
4
|
import { useWidgetStore } from '../../stores/widget-store'
|
|
5
5
|
import type { EchartWidgetData } from '../../echart/types'
|
|
6
|
-
import type { RelativeDataState } from './types'
|
|
7
6
|
|
|
8
7
|
describe('RelativeData', () => {
|
|
9
8
|
const widgetId = 'test-relative-widget'
|
|
@@ -94,25 +93,27 @@ describe('RelativeData', () => {
|
|
|
94
93
|
expect(button.getAttribute('aria-label')).toBe('Show absolute values')
|
|
95
94
|
})
|
|
96
95
|
|
|
97
|
-
test('initializes
|
|
96
|
+
test('initializes tool state with defaultIsRelative on mount', async () => {
|
|
98
97
|
render(<RelativeData id={widgetId} defaultIsRelative />)
|
|
99
98
|
|
|
100
99
|
await waitFor(() => {
|
|
101
|
-
const widget = useWidgetStore
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
101
|
+
const tool = widget?.registeredTools?.find(
|
|
102
|
+
(t) => t.id === RELATIVE_DATA_TOOL_ID,
|
|
103
|
+
)
|
|
104
|
+
expect(tool?.enabled).toBe(true)
|
|
105
105
|
})
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
-
test('initializes
|
|
108
|
+
test('initializes tool state with false when defaultIsRelative is false', async () => {
|
|
109
109
|
render(<RelativeData id={widgetId} defaultIsRelative={false} />)
|
|
110
110
|
|
|
111
111
|
await waitFor(() => {
|
|
112
|
-
const widget = useWidgetStore
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
113
|
+
const tool = widget?.registeredTools?.find(
|
|
114
|
+
(t) => t.id === RELATIVE_DATA_TOOL_ID,
|
|
115
|
+
)
|
|
116
|
+
expect(tool?.enabled).toBe(false)
|
|
116
117
|
})
|
|
117
118
|
})
|
|
118
119
|
|
|
@@ -233,7 +234,6 @@ describe('RelativeData', () => {
|
|
|
233
234
|
// Pre-populate the widget in the store with relative data
|
|
234
235
|
useWidgetStore.getState().setWidget(widgetId, {
|
|
235
236
|
data: initialData,
|
|
236
|
-
isRelative: true,
|
|
237
237
|
})
|
|
238
238
|
|
|
239
239
|
render(<RelativeData id={widgetId} defaultIsRelative />)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IconButtonProps } from '@mui/material'
|
|
2
2
|
import type { ReactNode } from 'react'
|
|
3
|
-
import type { BaseWidgetState } from '
|
|
3
|
+
import type { BaseWidgetState } from '../../../widgets/stores'
|
|
4
4
|
|
|
5
5
|
export interface RelativeDataProps {
|
|
6
6
|
/** Widget ID to update data in the widget store */
|
|
@@ -39,14 +39,17 @@ describe('StackToggle', () => {
|
|
|
39
39
|
expect(button).toBeTruthy()
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
-
test('toggles to stacked mode and updates
|
|
42
|
+
test('toggles to stacked mode and updates tool config', () => {
|
|
43
43
|
render(<StackToggle id={widgetId} />)
|
|
44
44
|
|
|
45
45
|
const button = screen.getByRole('button')
|
|
46
46
|
fireEvent.click(button)
|
|
47
47
|
|
|
48
48
|
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
49
|
-
|
|
49
|
+
const tool = widget?.registeredTools?.find(
|
|
50
|
+
(t) => t.id === STACK_TOGGLE_TOOL_ID,
|
|
51
|
+
)
|
|
52
|
+
expect(tool?.config?.stacked).toBe(true)
|
|
50
53
|
})
|
|
51
54
|
|
|
52
55
|
test('toggles back to unstacked mode', () => {
|
|
@@ -61,7 +64,10 @@ describe('StackToggle', () => {
|
|
|
61
64
|
fireEvent.click(button)
|
|
62
65
|
|
|
63
66
|
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
64
|
-
|
|
67
|
+
const tool = widget?.registeredTools?.find(
|
|
68
|
+
(t) => t.id === STACK_TOGGLE_TOOL_ID,
|
|
69
|
+
)
|
|
70
|
+
expect(tool?.config?.stacked).toBe(false)
|
|
65
71
|
})
|
|
66
72
|
|
|
67
73
|
test('has active state when in stacked mode', () => {
|
|
@@ -99,18 +105,24 @@ describe('StackToggle', () => {
|
|
|
99
105
|
expect(button).toBeTruthy()
|
|
100
106
|
})
|
|
101
107
|
|
|
102
|
-
test('initializes
|
|
108
|
+
test('initializes tool config with default values on mount', () => {
|
|
103
109
|
render(<StackToggle id={widgetId} />)
|
|
104
110
|
|
|
105
111
|
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
106
|
-
|
|
112
|
+
const tool = widget?.registeredTools?.find(
|
|
113
|
+
(t) => t.id === STACK_TOGGLE_TOOL_ID,
|
|
114
|
+
)
|
|
115
|
+
expect(tool?.config?.stacked).toBe(false)
|
|
107
116
|
})
|
|
108
117
|
|
|
109
|
-
test('initializes
|
|
118
|
+
test('initializes tool config with stacked values when defaultIsStacked is true', () => {
|
|
110
119
|
render(<StackToggle id={widgetId} defaultIsStacked />)
|
|
111
120
|
|
|
112
121
|
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
113
|
-
|
|
122
|
+
const tool = widget?.registeredTools?.find(
|
|
123
|
+
(t) => t.id === STACK_TOGGLE_TOOL_ID,
|
|
124
|
+
)
|
|
125
|
+
expect(tool?.config?.stacked).toBe(true)
|
|
114
126
|
})
|
|
115
127
|
|
|
116
128
|
test('applies stack to EChart series via config pipeline when toggling to stacked', async () => {
|
|
@@ -279,7 +291,6 @@ describe('StackToggle', () => {
|
|
|
279
291
|
test('config tool re-applies stack when config pipeline re-runs', async () => {
|
|
280
292
|
// Start with stacked widget
|
|
281
293
|
useWidgetStore.getState().setWidget(widgetId, {
|
|
282
|
-
isStacked: true,
|
|
283
294
|
option: {
|
|
284
295
|
series: [
|
|
285
296
|
{ name: 'Series 1', type: 'bar', stack: DEFAULT_STACK_GROUP },
|
|
@@ -313,7 +324,6 @@ describe('StackToggle', () => {
|
|
|
313
324
|
test('config tool does not apply stack when unstacked and pipeline re-runs', async () => {
|
|
314
325
|
// Start with unstacked widget
|
|
315
326
|
useWidgetStore.getState().setWidget(widgetId, {
|
|
316
|
-
isStacked: false,
|
|
317
327
|
option: {
|
|
318
328
|
series: [
|
|
319
329
|
{ name: 'Series 1', type: 'bar' },
|