@carto/ps-react-ui 4.5.0 → 4.6.0
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/{download-config-DemuQ3Jm.js → download-config-C3I0jWIL.js} +2 -2
- package/dist/{download-config-DemuQ3Jm.js.map → download-config-C3I0jWIL.js.map} +1 -1
- package/dist/{row-D4VOhcNI.js → row-DZSP99LW.js} +2 -2
- package/dist/{row-D4VOhcNI.js.map → row-DZSP99LW.js.map} +1 -1
- package/dist/{series-Bola3CmD.js → series-DLNHDWs0.js} +3 -3
- package/dist/{series-Bola3CmD.js.map → series-DLNHDWs0.js.map} +1 -1
- package/dist/types/hooks/index.d.ts +0 -1
- package/dist/types/widgets/actions/brush-toggle/brush-toggle.d.ts +3 -0
- package/dist/types/widgets/actions/index.d.ts +4 -4
- package/dist/types/widgets/actions/lock-selection/types.d.ts +2 -0
- package/dist/types/widgets/actions/relative-data/relative-data.d.ts +7 -2
- package/dist/types/widgets/actions/relative-data/types.d.ts +2 -0
- package/dist/types/widgets/actions/zoom-toggle/zoom-toggle.d.ts +4 -0
- package/dist/types/widgets/category/index.d.ts +10 -2
- package/dist/types/widgets/category/style.d.ts +1 -0
- package/dist/types/widgets/no-data/no-data.d.ts +3 -2
- package/dist/types/widgets/no-data/types.d.ts +5 -1
- package/dist/types/widgets/stores/index.d.ts +1 -1
- package/dist/types/widgets/stores/types.d.ts +10 -10
- package/dist/types/widgets/stores/widget-store.d.ts +2 -3
- package/dist/types/widgets/table/index.d.ts +6 -2
- package/dist/{use-widget-ref-BFazQvJK.js → use-widget-ref-Ddr_SlJJ.js} +2 -2
- package/dist/{use-widget-ref-BFazQvJK.js.map → use-widget-ref-Ddr_SlJJ.js.map} +1 -1
- package/dist/{use-widget-selector-DqRmWQ1K.js → use-widget-selector-DFl2hW0R.js} +2 -2
- package/dist/{use-widget-selector-DqRmWQ1K.js.map → use-widget-selector-DFl2hW0R.js.map} +1 -1
- package/dist/{widget-store-CIrb9RKP.js → widget-store-Bw5zRUGg.js} +93 -95
- package/dist/widget-store-Bw5zRUGg.js.map +1 -0
- package/dist/widgets/actions.js +770 -755
- package/dist/widgets/actions.js.map +1 -1
- package/dist/widgets/bar.js +2 -2
- package/dist/widgets/category.js +187 -183
- package/dist/widgets/category.js.map +1 -1
- package/dist/widgets/echart.js +2 -2
- package/dist/widgets/error.js +37 -2
- package/dist/widgets/error.js.map +1 -1
- package/dist/widgets/formula.js +5 -5
- package/dist/widgets/histogram.js +1 -1
- package/dist/widgets/loader.js +1 -1
- package/dist/widgets/markdown.js +2 -2
- package/dist/widgets/no-data.js +58 -2
- package/dist/widgets/no-data.js.map +1 -1
- package/dist/widgets/note.js +121 -2
- package/dist/widgets/note.js.map +1 -1
- package/dist/widgets/pie.js +2 -2
- package/dist/widgets/range.js +3 -3
- package/dist/widgets/scatterplot.js +2 -2
- package/dist/widgets/skeleton-loader.js +1 -1
- package/dist/widgets/spread.js +5 -5
- package/dist/widgets/stores.js +2 -2
- package/dist/widgets/subheader.js +29 -29
- package/dist/widgets/subheader.js.map +1 -1
- package/dist/widgets/table.js +3 -3
- package/dist/widgets/timeseries.js +2 -2
- package/dist/widgets/utils.js +1 -1
- package/dist/widgets/wrapper.js +2 -2
- package/package.json +1 -5
- package/src/hooks/index.ts +0 -1
- package/src/widgets/actions/brush-toggle/brush-toggle.tsx +18 -22
- package/src/widgets/actions/change-column/change-column.test.tsx +1 -1
- package/src/widgets/actions/download/download.test.tsx +1 -1
- package/src/widgets/actions/index.ts +11 -2
- package/src/widgets/actions/lock-selection/lock-selection.test.tsx +14 -0
- package/src/widgets/actions/lock-selection/lock-selection.tsx +18 -11
- package/src/widgets/actions/lock-selection/types.ts +2 -0
- package/src/widgets/actions/relative-data/relative-data.test.tsx +211 -20
- package/src/widgets/actions/relative-data/relative-data.tsx +65 -34
- package/src/widgets/actions/relative-data/types.ts +2 -0
- package/src/widgets/actions/searcher/searcher.tsx +28 -30
- package/src/widgets/actions/stack-toggle/stack-toggle.tsx +11 -2
- package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +53 -45
- package/src/widgets/category/category-ui.tsx +7 -6
- package/src/widgets/category/index.ts +13 -14
- package/src/widgets/category/style.ts +1 -0
- package/src/widgets/no-data/no-data.test.tsx +90 -40
- package/src/widgets/no-data/no-data.tsx +7 -5
- package/src/widgets/no-data/types.ts +5 -1
- package/src/widgets/stores/index.ts +2 -0
- package/src/widgets/stores/types.ts +10 -18
- package/src/widgets/stores/widget-store.test.ts +132 -13
- package/src/widgets/stores/widget-store.ts +29 -35
- package/src/widgets/subheader/subheader.tsx +11 -3
- package/src/widgets/table/index.ts +6 -4
- package/dist/error-Cj8eUMrl.js +0 -40
- package/dist/error-Cj8eUMrl.js.map +0 -1
- package/dist/no-data-DkIt7Qt1.js +0 -61
- package/dist/no-data-DkIt7Qt1.js.map +0 -1
- package/dist/note-t51drNe0.js +0 -124
- package/dist/note-t51drNe0.js.map +0 -1
- package/dist/types/hooks/use-debounce.d.ts +0 -19
- package/dist/types/widgets/category/components/index.d.ts +0 -10
- package/dist/types/widgets/index.d.ts +0 -9
- package/dist/types/widgets/table/hooks/index.d.ts +0 -6
- package/dist/widget-store-CIrb9RKP.js.map +0 -1
- package/dist/widgets.js +0 -13
- package/dist/widgets.js.map +0 -1
- package/src/hooks/use-debounce.ts +0 -55
- package/src/widgets/category/components/index.ts +0 -14
- package/src/widgets/index.ts +0 -25
- package/src/widgets/table/hooks/index.ts +0 -7
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from './relative-data'
|
|
8
8
|
import { useWidgetStore } from '../../stores/widget-store'
|
|
9
9
|
import type { EchartWidgetData } from '../../echart/types'
|
|
10
|
+
import type { RelativeDataState } from './types'
|
|
10
11
|
|
|
11
12
|
describe('RelativeData', () => {
|
|
12
13
|
const widgetId = 'test-relative-widget'
|
|
@@ -227,23 +228,28 @@ describe('RelativeData', () => {
|
|
|
227
228
|
expect(button.hasAttribute('disabled')).toBeTruthy()
|
|
228
229
|
})
|
|
229
230
|
|
|
230
|
-
test('registers config
|
|
231
|
+
test('registers both data and config tools on mount', async () => {
|
|
231
232
|
render(<RelativeData id={widgetId} />)
|
|
232
233
|
|
|
233
234
|
await waitFor(() => {
|
|
234
235
|
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
235
|
-
const
|
|
236
|
+
const dataTool = widget?.registeredTools?.find(
|
|
237
|
+
(t) => t.id === RELATIVE_DATA_TOOL_ID,
|
|
238
|
+
)
|
|
239
|
+
const configTool = widget?.registeredTools?.find(
|
|
236
240
|
(t) => t.id === RELATIVE_DATA_CONFIG_TOOL_ID,
|
|
237
241
|
)
|
|
238
|
-
expect(
|
|
239
|
-
expect(
|
|
240
|
-
expect(
|
|
242
|
+
expect(dataTool).toBeTruthy()
|
|
243
|
+
expect(configTool).toBeTruthy()
|
|
244
|
+
expect(configTool?.type).toBe('config')
|
|
245
|
+
// Data tool disabled by default (isRelative=false)
|
|
246
|
+
expect(dataTool?.enabled).toBe(false)
|
|
247
|
+
// Config tool always enabled — it reads isRelative from store internally
|
|
248
|
+
expect(configTool?.enabled).toBe(true)
|
|
241
249
|
})
|
|
242
250
|
})
|
|
243
251
|
|
|
244
|
-
test('
|
|
245
|
-
useWidgetStore.getState().setWidget(widgetId, { max: 500 })
|
|
246
|
-
|
|
252
|
+
test('data tool enabled when toggled to relative mode', async () => {
|
|
247
253
|
render(<RelativeData id={widgetId} />)
|
|
248
254
|
|
|
249
255
|
const button = screen.getByRole('button')
|
|
@@ -251,16 +257,77 @@ describe('RelativeData', () => {
|
|
|
251
257
|
|
|
252
258
|
await waitFor(() => {
|
|
253
259
|
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
254
|
-
const
|
|
255
|
-
(t) => t.id ===
|
|
260
|
+
const dataTool = widget?.registeredTools?.find(
|
|
261
|
+
(t) => t.id === RELATIVE_DATA_TOOL_ID,
|
|
256
262
|
)
|
|
257
|
-
expect(
|
|
258
|
-
expect(tool?.config?.originalFormatter).toBeUndefined()
|
|
259
|
-
expect(tool?.config?.originalMax).toBe(500)
|
|
263
|
+
expect(dataTool?.enabled).toBe(true)
|
|
260
264
|
})
|
|
261
265
|
})
|
|
262
266
|
|
|
263
|
-
test('
|
|
267
|
+
test('stores isRelative in widget root state', () => {
|
|
268
|
+
render(<RelativeData id={widgetId} />)
|
|
269
|
+
|
|
270
|
+
// Default: isRelative=false in store
|
|
271
|
+
let widget = useWidgetStore.getState().getWidget(widgetId)
|
|
272
|
+
expect((widget as RelativeDataState | undefined)?.isRelative).toBe(false)
|
|
273
|
+
|
|
274
|
+
// Toggle to relative
|
|
275
|
+
const button = screen.getByRole('button')
|
|
276
|
+
fireEvent.click(button)
|
|
277
|
+
|
|
278
|
+
widget = useWidgetStore.getState().getWidget(widgetId)
|
|
279
|
+
expect((widget as RelativeDataState | undefined)?.isRelative).toBe(true)
|
|
280
|
+
|
|
281
|
+
// Toggle back
|
|
282
|
+
fireEvent.click(button)
|
|
283
|
+
|
|
284
|
+
widget = useWidgetStore.getState().getWidget(widgetId)
|
|
285
|
+
expect((widget as RelativeDataState | undefined)?.isRelative).toBe(false)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('config tool does not apply formatter when sourceData is empty', async () => {
|
|
289
|
+
const customFormatter = (value: number) => `$${value}`
|
|
290
|
+
useWidgetStore
|
|
291
|
+
.getState()
|
|
292
|
+
.setWidget(widgetId, { formatter: customFormatter, sourceData: [] })
|
|
293
|
+
|
|
294
|
+
render(<RelativeData id={widgetId} />)
|
|
295
|
+
|
|
296
|
+
const button = screen.getByRole('button')
|
|
297
|
+
fireEvent.click(button)
|
|
298
|
+
|
|
299
|
+
// Execute config pipeline — sourceData is empty, so formatter should pass through
|
|
300
|
+
await useWidgetStore
|
|
301
|
+
.getState()
|
|
302
|
+
.executeConfigPipeline(widgetId, { formatter: customFormatter, max: 200 })
|
|
303
|
+
|
|
304
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
305
|
+
expect(widget?.formatter).toBe(customFormatter)
|
|
306
|
+
expect((widget as unknown as Record<string, unknown>)?.max).toBe(200)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
test('config pipeline applies percentage formatter when in relative mode', async () => {
|
|
310
|
+
useWidgetStore
|
|
311
|
+
.getState()
|
|
312
|
+
.setWidget(widgetId, { max: 500, sourceData: mockData })
|
|
313
|
+
|
|
314
|
+
render(<RelativeData id={widgetId} />)
|
|
315
|
+
|
|
316
|
+
const button = screen.getByRole('button')
|
|
317
|
+
fireEvent.click(button)
|
|
318
|
+
|
|
319
|
+
// Execute config pipeline
|
|
320
|
+
await useWidgetStore
|
|
321
|
+
.getState()
|
|
322
|
+
.executeConfigPipeline(widgetId, { max: 500 })
|
|
323
|
+
|
|
324
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
325
|
+
expect((widget as unknown as Record<string, unknown>)?.max).toBe(100)
|
|
326
|
+
expect(widget?.formatter).toBeDefined()
|
|
327
|
+
expect(typeof widget?.formatter).toBe('function')
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
test('config tool passes config through unchanged when not in relative mode', async () => {
|
|
264
331
|
const customFormatter = (value: number) => `$${value}`
|
|
265
332
|
useWidgetStore
|
|
266
333
|
.getState()
|
|
@@ -268,20 +335,144 @@ describe('RelativeData', () => {
|
|
|
268
335
|
|
|
269
336
|
render(<RelativeData id={widgetId} />)
|
|
270
337
|
|
|
338
|
+
// Execute config pipeline — config tool is enabled but isRelative=false in store,
|
|
339
|
+
// so it passes through the config unchanged
|
|
340
|
+
await useWidgetStore
|
|
341
|
+
.getState()
|
|
342
|
+
.executeConfigPipeline(widgetId, { formatter: customFormatter, max: 200 })
|
|
343
|
+
|
|
344
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
345
|
+
expect(widget?.formatter).toBe(customFormatter)
|
|
346
|
+
expect((widget as unknown as Record<string, unknown>)?.max).toBe(200)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
test('restores original formatter/max when toggling back to absolute', async () => {
|
|
350
|
+
const customFormatter = (value: number) => `$${value}`
|
|
351
|
+
useWidgetStore.getState().setWidget(widgetId, {
|
|
352
|
+
formatter: customFormatter,
|
|
353
|
+
max: 200,
|
|
354
|
+
sourceData: mockData,
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
render(<RelativeData id={widgetId} />)
|
|
358
|
+
|
|
271
359
|
const button = screen.getByRole('button')
|
|
272
360
|
|
|
273
|
-
// Toggle to relative
|
|
361
|
+
// Toggle to relative — captures originalFormatter/originalMax in store
|
|
274
362
|
fireEvent.click(button)
|
|
275
|
-
|
|
363
|
+
|
|
364
|
+
await useWidgetStore
|
|
365
|
+
.getState()
|
|
366
|
+
.executeConfigPipeline(widgetId, { max: 200 })
|
|
367
|
+
|
|
368
|
+
let widget = useWidgetStore.getState().getWidget(widgetId)
|
|
369
|
+
expect((widget as unknown as Record<string, unknown>)?.max).toBe(100)
|
|
370
|
+
|
|
371
|
+
// Toggle back to absolute — config tool restores originals from store
|
|
372
|
+
fireEvent.click(button)
|
|
373
|
+
|
|
374
|
+
await useWidgetStore
|
|
375
|
+
.getState()
|
|
376
|
+
.executeConfigPipeline(widgetId, { max: 200 })
|
|
377
|
+
|
|
378
|
+
widget = useWidgetStore.getState().getWidget(widgetId)
|
|
379
|
+
expect(widget?.formatter).toBe(customFormatter)
|
|
380
|
+
expect((widget as unknown as Record<string, unknown>)?.max).toBe(200)
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
test('removes percentage formatter when toggling back with no original formatter', async () => {
|
|
384
|
+
// Widget has no formatter — originalFormatter will be captured as undefined
|
|
385
|
+
useWidgetStore.getState().setWidget(widgetId, { sourceData: mockData })
|
|
386
|
+
|
|
387
|
+
render(<RelativeData id={widgetId} />)
|
|
388
|
+
|
|
389
|
+
const button = screen.getByRole('button')
|
|
390
|
+
|
|
391
|
+
// Toggle to relative — captures originalFormatter=undefined
|
|
392
|
+
fireEvent.click(button)
|
|
393
|
+
|
|
394
|
+
await useWidgetStore.getState().executeConfigPipeline(widgetId, {})
|
|
395
|
+
|
|
396
|
+
let widget = useWidgetStore.getState().getWidget(widgetId)
|
|
397
|
+
expect(widget?.formatter).toBeDefined()
|
|
398
|
+
expect(typeof widget?.formatter).toBe('function')
|
|
399
|
+
|
|
400
|
+
// Toggle back to absolute — should remove percentage formatter
|
|
401
|
+
fireEvent.click(button)
|
|
402
|
+
|
|
403
|
+
await useWidgetStore.getState().executeConfigPipeline(widgetId, {})
|
|
404
|
+
|
|
405
|
+
widget = useWidgetStore.getState().getWidget(widgetId)
|
|
406
|
+
expect(widget?.formatter).toBeUndefined()
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
test('stores originalFormatter/originalMax in widget root on toggle', () => {
|
|
410
|
+
const customFormatter = (value: number) => `$${value}`
|
|
411
|
+
useWidgetStore
|
|
412
|
+
.getState()
|
|
413
|
+
.setWidget(widgetId, { formatter: customFormatter, max: 200 })
|
|
414
|
+
|
|
415
|
+
render(<RelativeData id={widgetId} />)
|
|
416
|
+
|
|
417
|
+
const button = screen.getByRole('button')
|
|
418
|
+
fireEvent.click(button)
|
|
419
|
+
|
|
420
|
+
const widget = useWidgetStore
|
|
421
|
+
.getState()
|
|
422
|
+
.getWidget<RelativeDataState>(widgetId)
|
|
423
|
+
expect(widget?.originalFormatter).toBe(customFormatter)
|
|
424
|
+
expect(widget?.originalMax).toBe(200)
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
test('restores original formatter/max when unmounting while in relative mode', async () => {
|
|
428
|
+
const customFormatter = (value: number) => `$${value}`
|
|
429
|
+
useWidgetStore
|
|
430
|
+
.getState()
|
|
431
|
+
.setWidget(widgetId, { formatter: customFormatter, max: 200 })
|
|
432
|
+
|
|
433
|
+
const { unmount } = render(<RelativeData id={widgetId} />)
|
|
434
|
+
|
|
435
|
+
const button = screen.getByRole('button')
|
|
436
|
+
|
|
437
|
+
// Toggle to relative — captures originalFormatter/originalMax in store
|
|
276
438
|
fireEvent.click(button)
|
|
277
439
|
|
|
440
|
+
await waitFor(() => {
|
|
441
|
+
const widget = useWidgetStore
|
|
442
|
+
.getState()
|
|
443
|
+
.getWidget<RelativeDataState>(widgetId)
|
|
444
|
+
expect(widget?.isRelative).toBe(true)
|
|
445
|
+
expect(widget?.originalFormatter).toBe(customFormatter)
|
|
446
|
+
expect(widget?.originalMax).toBe(200)
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
// Unmount while still in relative mode
|
|
450
|
+
unmount()
|
|
451
|
+
|
|
452
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
453
|
+
expect(widget?.formatter).toBe(customFormatter)
|
|
454
|
+
expect((widget as unknown as Record<string, unknown>)?.max).toBe(200)
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
test('unregisters tools on unmount', async () => {
|
|
458
|
+
const { unmount } = render(<RelativeData id={widgetId} />)
|
|
459
|
+
|
|
460
|
+
await waitFor(() => {
|
|
461
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
462
|
+
expect(widget?.registeredTools?.length).toBeGreaterThan(0)
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
unmount()
|
|
466
|
+
|
|
278
467
|
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
279
|
-
const
|
|
468
|
+
const dataTool = widget?.registeredTools?.find(
|
|
469
|
+
(t) => t.id === RELATIVE_DATA_TOOL_ID,
|
|
470
|
+
)
|
|
471
|
+
const configTool = widget?.registeredTools?.find(
|
|
280
472
|
(t) => t.id === RELATIVE_DATA_CONFIG_TOOL_ID,
|
|
281
473
|
)
|
|
282
|
-
expect(
|
|
283
|
-
expect(
|
|
284
|
-
expect(tool?.config?.originalMax).toBe(200)
|
|
474
|
+
expect(dataTool).toBeUndefined()
|
|
475
|
+
expect(configTool).toBeUndefined()
|
|
285
476
|
})
|
|
286
477
|
|
|
287
478
|
test('recalculates relative values when store data changes externally while in relative mode', async () => {
|
|
@@ -3,7 +3,7 @@ import { PercentOutlined } from '@mui/icons-material'
|
|
|
3
3
|
import { useCallback, useEffect, useRef } from 'react'
|
|
4
4
|
import { widgetStoreActions } from '../../stores/widget-store'
|
|
5
5
|
import { useWidgetSelector } from '../../stores/use-widget-selector'
|
|
6
|
-
import type { RelativeDataProps } from './types'
|
|
6
|
+
import type { RelativeDataProps, RelativeDataState } from './types'
|
|
7
7
|
import { actionButtonStyles } from '../shared/styles'
|
|
8
8
|
import { Tooltip } from '../../../components'
|
|
9
9
|
import { calculateTotal, toRelativeData } from './utils'
|
|
@@ -15,8 +15,13 @@ export const RELATIVE_DATA_CONFIG_TOOL_ID = 'relative-data-config'
|
|
|
15
15
|
/**
|
|
16
16
|
* Widget action to toggle between relative (percentage) and absolute data display.
|
|
17
17
|
*
|
|
18
|
-
* Registers
|
|
19
|
-
*
|
|
18
|
+
* Registers two transformation tools in the widget pipeline when mounted:
|
|
19
|
+
* - A data tool that converts values to percentages (enabled/disabled via store)
|
|
20
|
+
* - A config tool that is **always enabled** and reads `isRelative` from the store
|
|
21
|
+
* to decide whether to apply the percentage formatter or restore the original one.
|
|
22
|
+
* The config tool must always participate because the original formatter may have
|
|
23
|
+
* been set via `setWidget` (not in the base config), so disabling the tool would
|
|
24
|
+
* leave the percentage formatter stuck on the widget.
|
|
20
25
|
*
|
|
21
26
|
* @example
|
|
22
27
|
* ```tsx
|
|
@@ -39,15 +44,21 @@ export function RelativeData({
|
|
|
39
44
|
undefined,
|
|
40
45
|
)
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
// Read isRelative from widget store root — single source of truth
|
|
48
|
+
const { isRelative } = useWidgetSelector(id, (w) => ({
|
|
49
|
+
isRelative:
|
|
50
|
+
(w as RelativeDataState | undefined)?.isRelative ?? defaultIsRelative,
|
|
46
51
|
}))
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
// Initialize store with default value on mount
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const current = widgetStoreActions.getWidget<RelativeDataState>(id)
|
|
56
|
+
if (current?.isRelative === undefined) {
|
|
57
|
+
widgetStoreActions.setWidget(id, { isRelative: defaultIsRelative })
|
|
58
|
+
}
|
|
59
|
+
}, [id, defaultIsRelative])
|
|
49
60
|
|
|
50
|
-
// Register data tool
|
|
61
|
+
// Register data tool once — fn has no closure deps
|
|
51
62
|
useEffect(() => {
|
|
52
63
|
widgetStoreActions.registerTool(id, {
|
|
53
64
|
id: RELATIVE_DATA_TOOL_ID,
|
|
@@ -63,18 +74,35 @@ export function RelativeData({
|
|
|
63
74
|
return () => widgetStoreActions.unregisterTool(id, RELATIVE_DATA_TOOL_ID)
|
|
64
75
|
}, [id, order, defaultIsRelative])
|
|
65
76
|
|
|
66
|
-
//
|
|
77
|
+
// Sync data tool enabled — lightweight, no re-registration
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
widgetStoreActions.setToolEnabled(id, RELATIVE_DATA_TOOL_ID, isRelative)
|
|
80
|
+
}, [id, isRelative])
|
|
81
|
+
|
|
82
|
+
// Register config tool — ALWAYS enabled.
|
|
83
|
+
// Reads isRelative, originalFormatter, originalMax from the store at execution time:
|
|
84
|
+
// - isRelative=true → applies percentage formatter and max=100
|
|
85
|
+
// - isRelative=false → restores original formatter/max from store
|
|
67
86
|
useEffect(() => {
|
|
68
87
|
widgetStoreActions.registerTool(id, {
|
|
69
88
|
id: RELATIVE_DATA_CONFIG_TOOL_ID,
|
|
70
89
|
type: 'config',
|
|
71
90
|
order,
|
|
72
91
|
enabled: true,
|
|
73
|
-
fn: (currentConfig: unknown
|
|
92
|
+
fn: (currentConfig: unknown) => {
|
|
74
93
|
const config = currentConfig as Record<string, unknown>
|
|
75
|
-
|
|
94
|
+
const widget = widgetStoreActions.getWidget<RelativeDataState>(id)
|
|
95
|
+
|
|
96
|
+
const hasSourceData =
|
|
97
|
+
widget?.sourceData != null &&
|
|
98
|
+
!(Array.isArray(widget.sourceData) && widget.sourceData.length === 0)
|
|
99
|
+
|
|
100
|
+
if (widget?.isRelative) {
|
|
101
|
+
// Don't apply percentage formatter when there's no source data
|
|
102
|
+
if (!hasSourceData) return config
|
|
103
|
+
|
|
76
104
|
if (!percentFormatterRef.current) {
|
|
77
|
-
const locale =
|
|
105
|
+
const locale = widget?.locale
|
|
78
106
|
percentFormatterRef.current = (value: number) =>
|
|
79
107
|
new Intl.NumberFormat(locale, {
|
|
80
108
|
style: 'percent',
|
|
@@ -84,46 +112,49 @@ export function RelativeData({
|
|
|
84
112
|
}
|
|
85
113
|
return { ...config, formatter: percentFormatterRef.current, max: 100 }
|
|
86
114
|
}
|
|
87
|
-
|
|
115
|
+
|
|
116
|
+
// Not in relative mode — restore originals from store if captured.
|
|
117
|
+
// Use `in` because originalFormatter may have been captured as undefined
|
|
118
|
+
// (widget had no formatter before toggling relative on).
|
|
88
119
|
percentFormatterRef.current = undefined
|
|
89
|
-
if (
|
|
120
|
+
if (widget != null && 'originalFormatter' in widget) {
|
|
90
121
|
return {
|
|
91
122
|
...config,
|
|
92
|
-
formatter:
|
|
93
|
-
max:
|
|
123
|
+
formatter: widget.originalFormatter,
|
|
124
|
+
max: widget.originalMax,
|
|
94
125
|
}
|
|
95
126
|
}
|
|
96
127
|
return config
|
|
97
128
|
},
|
|
98
|
-
config: {
|
|
99
|
-
isRelative: defaultIsRelative,
|
|
100
|
-
},
|
|
101
129
|
})
|
|
102
|
-
return () =>
|
|
130
|
+
return () => {
|
|
131
|
+
// Restore original formatter/max if unmounting while in relative mode
|
|
132
|
+
const widget = widgetStoreActions.getWidget<RelativeDataState>(id)
|
|
133
|
+
if (widget?.isRelative && 'originalFormatter' in widget) {
|
|
134
|
+
widgetStoreActions.setWidget(id, {
|
|
135
|
+
formatter: widget.originalFormatter,
|
|
136
|
+
max: widget.originalMax,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
percentFormatterRef.current = undefined
|
|
103
140
|
widgetStoreActions.unregisterTool(id, RELATIVE_DATA_CONFIG_TOOL_ID)
|
|
141
|
+
}
|
|
104
142
|
}, [id, order, defaultIsRelative])
|
|
105
143
|
|
|
106
144
|
const handleToggle = useCallback(() => {
|
|
107
145
|
const newIsRelative = !isRelative
|
|
108
|
-
|
|
146
|
+
percentFormatterRef.current = undefined
|
|
109
147
|
|
|
110
148
|
if (newIsRelative) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
max?: number
|
|
115
|
-
}
|
|
116
|
-
widgetStoreActions.updateToolConfig(id, RELATIVE_DATA_CONFIG_TOOL_ID, {
|
|
149
|
+
// Capture current formatter/max in store before switching to relative
|
|
150
|
+
const widget = widgetStoreActions.getWidget(id)
|
|
151
|
+
widgetStoreActions.setWidget(id, {
|
|
117
152
|
isRelative: true,
|
|
118
153
|
originalFormatter: widget?.formatter,
|
|
119
|
-
originalMax: widget?.max,
|
|
120
|
-
locale: widget?.locale,
|
|
154
|
+
originalMax: (widget as unknown as Record<string, unknown>)?.max,
|
|
121
155
|
})
|
|
122
156
|
} else {
|
|
123
|
-
|
|
124
|
-
widgetStoreActions.updateToolConfig(id, RELATIVE_DATA_CONFIG_TOOL_ID, {
|
|
125
|
-
isRelative: false,
|
|
126
|
-
})
|
|
157
|
+
widgetStoreActions.setWidget(id, { isRelative: false })
|
|
127
158
|
}
|
|
128
159
|
}, [isRelative, id])
|
|
129
160
|
|
|
@@ -57,42 +57,44 @@ export function Searcher({
|
|
|
57
57
|
[id],
|
|
58
58
|
)
|
|
59
59
|
|
|
60
|
-
// Register tool
|
|
60
|
+
// Register tool once — fn reads searchText from the store at execution time.
|
|
61
|
+
// Enabled is synced separately to avoid full re-registration on toggle.
|
|
61
62
|
useEffect(() => {
|
|
62
63
|
widgetStoreActions.registerTool(id, {
|
|
63
64
|
id: SEARCHER_TOOL_ID,
|
|
64
65
|
order,
|
|
65
|
-
enabled,
|
|
66
|
-
fn: async (data
|
|
67
|
-
const
|
|
66
|
+
enabled: false,
|
|
67
|
+
fn: async (data) => {
|
|
68
|
+
const widget = widgetStoreActions.getWidget<SearcherState>(id)
|
|
69
|
+
const currentSearchText = widget?.searchText ?? ''
|
|
68
70
|
|
|
69
71
|
// Execute filter (can be sync or async)
|
|
70
|
-
const result = filter(data as EchartWidgetData,
|
|
72
|
+
const result = filter(data as EchartWidgetData, currentSearchText)
|
|
71
73
|
|
|
72
74
|
// Return result directly (pipeline will handle Promise)
|
|
73
75
|
return result
|
|
74
76
|
},
|
|
75
|
-
config: { searchText },
|
|
76
77
|
disables: [LOCK_SELECTION_TOOL_ID],
|
|
77
78
|
})
|
|
78
79
|
|
|
79
80
|
return () => widgetStoreActions.unregisterTool(id, SEARCHER_TOOL_ID)
|
|
80
|
-
}, [id, order,
|
|
81
|
+
}, [id, order, filter])
|
|
81
82
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
83
|
+
// Sync enabled from store — lightweight, no re-registration
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
widgetStoreActions.setToolEnabled(id, SEARCHER_TOOL_ID, enabled)
|
|
86
|
+
}, [id, enabled])
|
|
87
|
+
|
|
88
|
+
// Trigger pipeline re-execution when search text changes (debounced).
|
|
89
|
+
// The fn reads searchText from the store, so we just need to trigger the pipeline.
|
|
90
|
+
const debouncedTriggerPipeline = useCallback(() => {
|
|
91
|
+
if (debounceTimeoutRef.current) {
|
|
92
|
+
clearTimeout(debounceTimeoutRef.current)
|
|
93
|
+
}
|
|
94
|
+
debounceTimeoutRef.current = setTimeout(() => {
|
|
95
|
+
widgetStoreActions.triggerToolPipeline(id)
|
|
96
|
+
}, debounceDelay)
|
|
97
|
+
}, [id, debounceDelay])
|
|
96
98
|
|
|
97
99
|
// Auto-focus when enabled becomes true
|
|
98
100
|
useEffect(() => {
|
|
@@ -117,16 +119,14 @@ export function Searcher({
|
|
|
117
119
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
118
120
|
const newValue = event.target.value
|
|
119
121
|
setSearchText(newValue)
|
|
120
|
-
|
|
122
|
+
debouncedTriggerPipeline()
|
|
121
123
|
},
|
|
122
|
-
[
|
|
124
|
+
[debouncedTriggerPipeline, setSearchText],
|
|
123
125
|
)
|
|
124
126
|
|
|
125
127
|
const handleClear = useCallback(() => {
|
|
126
128
|
setSearchText('')
|
|
127
|
-
widgetStoreActions.
|
|
128
|
-
searchText: '',
|
|
129
|
-
})
|
|
129
|
+
widgetStoreActions.triggerToolPipeline(id)
|
|
130
130
|
if (inputRef.current) {
|
|
131
131
|
inputRef.current.focus()
|
|
132
132
|
}
|
|
@@ -187,10 +187,8 @@ const defaultFilterFn: SearcherFilterFn = (
|
|
|
187
187
|
return Promise.resolve(
|
|
188
188
|
data.map((series) =>
|
|
189
189
|
series.filter((item) =>
|
|
190
|
-
Object.values(item).some(
|
|
191
|
-
(value)
|
|
192
|
-
typeof value === 'string' &&
|
|
193
|
-
value.toLowerCase().includes(lowerSearch),
|
|
190
|
+
Object.values(item).some((value) =>
|
|
191
|
+
String(value).toLowerCase().includes(lowerSearch),
|
|
194
192
|
),
|
|
195
193
|
),
|
|
196
194
|
),
|
|
@@ -54,13 +54,13 @@ export function StackToggle({
|
|
|
54
54
|
const effectiveDefaultIsStacked = hasStackInSeries || defaultIsStacked
|
|
55
55
|
const isStacked = storeIsStacked ?? effectiveDefaultIsStacked
|
|
56
56
|
|
|
57
|
-
// Register config tool
|
|
57
|
+
// Register config tool once — fn has no closure deps
|
|
58
58
|
useEffect(() => {
|
|
59
59
|
widgetStoreActions.registerTool(id, {
|
|
60
60
|
id: STACK_TOGGLE_TOOL_ID,
|
|
61
61
|
type: 'config',
|
|
62
62
|
order: 10,
|
|
63
|
-
enabled:
|
|
63
|
+
enabled: false,
|
|
64
64
|
fn: (currentConfig: unknown) => {
|
|
65
65
|
const config = currentConfig as Record<string, unknown>
|
|
66
66
|
const option = config.option as EchartOptionsProps | undefined
|
|
@@ -82,6 +82,15 @@ export function StackToggle({
|
|
|
82
82
|
},
|
|
83
83
|
})
|
|
84
84
|
return () => widgetStoreActions.unregisterTool(id, STACK_TOGGLE_TOOL_ID)
|
|
85
|
+
}, [id])
|
|
86
|
+
|
|
87
|
+
// Sync enabled from store — lightweight, no re-registration
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
widgetStoreActions.setToolEnabled(
|
|
90
|
+
id,
|
|
91
|
+
STACK_TOGGLE_TOOL_ID,
|
|
92
|
+
isStacked && hasMultiSeries,
|
|
93
|
+
)
|
|
85
94
|
}, [id, isStacked, hasMultiSeries])
|
|
86
95
|
|
|
87
96
|
// Initialize store with default value only if not already configured
|