@carto/ps-react-ui 4.3.3 → 4.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/dist/components.js +3 -3
  2. package/dist/components.js.map +1 -1
  3. package/dist/{lasso-tool-BwRzEW7k.js → lasso-tool-wFqOD6wk.js} +13 -13
  4. package/dist/lasso-tool-wFqOD6wk.js.map +1 -0
  5. package/dist/types/components/common-types.d.ts +41 -0
  6. package/dist/types/components/types.d.ts +1 -1
  7. package/dist/types/widgets/echart/echart-ui.d.ts +1 -1
  8. package/dist/types/widgets/echart/types.d.ts +4 -0
  9. package/dist/widgets/actions.js +1 -1
  10. package/dist/widgets/bar.js +1 -1
  11. package/dist/widgets/category.js +1 -1
  12. package/dist/widgets/echart.js +96 -85
  13. package/dist/widgets/echart.js.map +1 -1
  14. package/dist/widgets/formula.js +1 -1
  15. package/dist/widgets/histogram.js +1 -1
  16. package/dist/widgets/markdown.js +1 -1
  17. package/dist/widgets/pie.js +1 -1
  18. package/dist/widgets/scatterplot.js +1 -1
  19. package/dist/widgets/spread.js +1 -1
  20. package/dist/widgets/table.js +1 -1
  21. package/dist/widgets/timeseries.js +1 -1
  22. package/dist/widgets/toolbar-actions.js.map +1 -1
  23. package/dist/widgets/wrapper.js +1 -1
  24. package/package.json +8 -3
  25. package/src/components/basemaps/basemaps.test.tsx +196 -0
  26. package/src/components/basemaps/basemaps.tsx +128 -0
  27. package/src/components/basemaps/const.ts +13 -0
  28. package/src/components/basemaps/group-wrapper.test.tsx +38 -0
  29. package/src/components/basemaps/group-wrapper.tsx +28 -0
  30. package/src/components/basemaps/group.test.tsx +52 -0
  31. package/src/components/basemaps/group.tsx +42 -0
  32. package/src/components/basemaps/header.test.tsx +54 -0
  33. package/src/components/basemaps/header.tsx +36 -0
  34. package/src/components/basemaps/styles.ts +76 -0
  35. package/src/components/basemaps/types.ts +30 -0
  36. package/src/components/common-types.ts +1 -0
  37. package/src/components/geolocation-controls/const.ts +6 -0
  38. package/src/components/geolocation-controls/geolocation-controls.test.tsx +133 -0
  39. package/src/components/geolocation-controls/geolocation-controls.tsx +95 -0
  40. package/src/components/geolocation-controls/types.ts +17 -0
  41. package/src/components/index.ts +64 -0
  42. package/src/components/lasso-tool/chip.tsx +37 -0
  43. package/src/components/lasso-tool/const.tsx +70 -0
  44. package/src/components/lasso-tool/icons.tsx +91 -0
  45. package/src/components/lasso-tool/lasso-tool-inline.test.tsx +168 -0
  46. package/src/components/lasso-tool/lasso-tool-inline.tsx +245 -0
  47. package/src/components/lasso-tool/lasso-tool.test.tsx +212 -0
  48. package/src/components/lasso-tool/lasso-tool.tsx +479 -0
  49. package/src/components/lasso-tool/styles.ts +143 -0
  50. package/src/components/lasso-tool/types.ts +114 -0
  51. package/src/components/list-data/list-data-skeleton.test.tsx +10 -0
  52. package/src/components/list-data/list-data-skeleton.tsx +40 -0
  53. package/src/components/list-data/list-data.test.tsx +94 -0
  54. package/src/components/list-data/list-data.tsx +106 -0
  55. package/src/components/list-data/styles.ts +37 -0
  56. package/src/components/list-data/types.ts +25 -0
  57. package/src/components/measurement-tools/const.tsx +108 -0
  58. package/src/components/measurement-tools/icons.tsx +54 -0
  59. package/src/components/measurement-tools/measurement-tools.test.tsx +165 -0
  60. package/src/components/measurement-tools/measurement-tools.tsx +443 -0
  61. package/src/components/measurement-tools/styles.ts +91 -0
  62. package/src/components/measurement-tools/types.ts +77 -0
  63. package/src/components/no-data-alert/no-data-alert.test.tsx +31 -0
  64. package/src/components/no-data-alert/no-data-alert.tsx +59 -0
  65. package/src/components/responsive-drawer/responsive-drawer.test.tsx +91 -0
  66. package/src/components/responsive-drawer/responsive-drawer.tsx +53 -0
  67. package/src/components/smart-tooltip/smart-tooltip.test.tsx +168 -0
  68. package/src/components/smart-tooltip/smart-tooltip.tsx +40 -0
  69. package/src/components/tooltip/tooltip.test.tsx +86 -0
  70. package/src/components/tooltip/tooltip.tsx +30 -0
  71. package/src/components/types.ts +1 -0
  72. package/src/components/zoom-controls/styles.ts +27 -0
  73. package/src/components/zoom-controls/types.ts +19 -0
  74. package/src/components/zoom-controls/zoom-controls.test.tsx +101 -0
  75. package/src/components/zoom-controls/zoom-controls.tsx +114 -0
  76. package/src/hooks/index.ts +2 -0
  77. package/src/hooks/use-debounce.ts +55 -0
  78. package/src/hooks/use-skeleton.test.tsx +32 -0
  79. package/src/hooks/use-skeleton.ts +19 -0
  80. package/src/hooks/use-widget-ref.ts +33 -0
  81. package/src/widgets/README.md +277 -0
  82. package/src/widgets/_shared/chart-config/config-factory.ts +67 -0
  83. package/src/widgets/_shared/chart-config/csv-modifiers.ts +56 -0
  84. package/src/widgets/_shared/chart-config/index.ts +21 -0
  85. package/src/widgets/_shared/chart-config/option-builders.ts +203 -0
  86. package/src/widgets/_shared/skeleton/index.ts +5 -0
  87. package/src/widgets/_shared/skeleton/styles.ts +20 -0
  88. package/src/widgets/actions/change-column/change-column-icon.tsx +10 -0
  89. package/src/widgets/actions/change-column/change-column.test.tsx +163 -0
  90. package/src/widgets/actions/change-column/change-column.tsx +141 -0
  91. package/src/widgets/actions/change-column/sortable-column-item.tsx +49 -0
  92. package/src/widgets/actions/change-column/types.ts +20 -0
  93. package/src/widgets/actions/download/download.test.tsx +322 -0
  94. package/src/widgets/actions/download/download.tsx +118 -0
  95. package/src/widgets/actions/download/exports.test.tsx +275 -0
  96. package/src/widgets/actions/download/exports.tsx +103 -0
  97. package/src/widgets/actions/download/types.ts +21 -0
  98. package/src/widgets/actions/fullscreen/fullscreen.test.tsx +269 -0
  99. package/src/widgets/actions/fullscreen/fullscreen.tsx +82 -0
  100. package/src/widgets/actions/fullscreen/styles.ts +17 -0
  101. package/src/widgets/actions/fullscreen/types.ts +27 -0
  102. package/src/widgets/actions/index.ts +51 -0
  103. package/src/widgets/actions/lock-selection/lock-selection.test.tsx +186 -0
  104. package/src/widgets/actions/lock-selection/lock-selection.tsx +133 -0
  105. package/src/widgets/actions/lock-selection/types.ts +41 -0
  106. package/src/widgets/actions/relative-data/relative-data.test.tsx +267 -0
  107. package/src/widgets/actions/relative-data/relative-data.tsx +133 -0
  108. package/src/widgets/actions/relative-data/style.ts +9 -0
  109. package/src/widgets/actions/relative-data/types.ts +31 -0
  110. package/src/widgets/actions/relative-data/utils.test.ts +223 -0
  111. package/src/widgets/actions/relative-data/utils.ts +58 -0
  112. package/src/widgets/actions/searcher/searcher-toggle.test.tsx +354 -0
  113. package/src/widgets/actions/searcher/searcher-toggle.tsx +73 -0
  114. package/src/widgets/actions/searcher/searcher.tsx +205 -0
  115. package/src/widgets/actions/searcher/types.ts +72 -0
  116. package/src/widgets/actions/shared/styles.ts +12 -0
  117. package/src/widgets/actions/stack-toggle/grouped-bar-chart-icon.tsx +14 -0
  118. package/src/widgets/actions/stack-toggle/stack-toggle.test.tsx +270 -0
  119. package/src/widgets/actions/stack-toggle/stack-toggle.tsx +146 -0
  120. package/src/widgets/actions/stack-toggle/types.ts +29 -0
  121. package/src/widgets/actions/zoom-toggle/index.ts +2 -0
  122. package/src/widgets/actions/zoom-toggle/style.ts +14 -0
  123. package/src/widgets/actions/zoom-toggle/types.ts +44 -0
  124. package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +186 -0
  125. package/src/widgets/bar/config.ts +122 -0
  126. package/src/widgets/bar/index.ts +9 -0
  127. package/src/widgets/bar/skeleton.tsx +60 -0
  128. package/src/widgets/bar/style.ts +33 -0
  129. package/src/widgets/bar/types.ts +16 -0
  130. package/src/widgets/category/category-ui.test.tsx +399 -0
  131. package/src/widgets/category/category-ui.tsx +156 -0
  132. package/src/widgets/category/components/category-bar.tsx +28 -0
  133. package/src/widgets/category/components/category-legend.tsx +30 -0
  134. package/src/widgets/category/components/category-row-multi.tsx +50 -0
  135. package/src/widgets/category/components/category-row-other.tsx +23 -0
  136. package/src/widgets/category/components/category-row-single.tsx +47 -0
  137. package/src/widgets/category/components/index.ts +14 -0
  138. package/src/widgets/category/config.ts +85 -0
  139. package/src/widgets/category/index.ts +30 -0
  140. package/src/widgets/category/skeleton.tsx +24 -0
  141. package/src/widgets/category/style.ts +133 -0
  142. package/src/widgets/category/types.ts +40 -0
  143. package/src/widgets/echart/const.ts +1 -0
  144. package/src/widgets/echart/echart-ui.test.tsx +537 -0
  145. package/src/widgets/echart/echart-ui.tsx +92 -0
  146. package/src/widgets/echart/echart.test.tsx +562 -0
  147. package/src/widgets/echart/echart.tsx +68 -0
  148. package/src/widgets/echart/index.ts +16 -0
  149. package/src/widgets/echart/options.ts +53 -0
  150. package/src/widgets/echart/types.ts +45 -0
  151. package/src/widgets/echart/utils.ts +169 -0
  152. package/src/widgets/error/error.test.tsx +331 -0
  153. package/src/widgets/error/error.tsx +40 -0
  154. package/src/widgets/error/index.ts +2 -0
  155. package/src/widgets/error/types.ts +14 -0
  156. package/src/widgets/formula/components/item.test.tsx +249 -0
  157. package/src/widgets/formula/components/item.tsx +18 -0
  158. package/src/widgets/formula/components/prefix.test.tsx +341 -0
  159. package/src/widgets/formula/components/prefix.tsx +18 -0
  160. package/src/widgets/formula/components/row.test.tsx +364 -0
  161. package/src/widgets/formula/components/row.tsx +21 -0
  162. package/src/widgets/formula/components/series.tsx +34 -0
  163. package/src/widgets/formula/components/suffix.test.tsx +383 -0
  164. package/src/widgets/formula/components/suffix.tsx +28 -0
  165. package/src/widgets/formula/components/value.test.tsx +329 -0
  166. package/src/widgets/formula/components/value.tsx +29 -0
  167. package/src/widgets/formula/config.ts +27 -0
  168. package/src/widgets/formula/formula-ui.test.tsx +399 -0
  169. package/src/widgets/formula/formula-ui.tsx +27 -0
  170. package/src/widgets/formula/index.ts +24 -0
  171. package/src/widgets/formula/serializer.test.tsx +144 -0
  172. package/src/widgets/formula/serializer.ts +28 -0
  173. package/src/widgets/formula/skeleton.tsx +10 -0
  174. package/src/widgets/formula/style.ts +23 -0
  175. package/src/widgets/formula/types.ts +50 -0
  176. package/src/widgets/histogram/config.ts +143 -0
  177. package/src/widgets/histogram/index.ts +8 -0
  178. package/src/widgets/histogram/skeleton.tsx +52 -0
  179. package/src/widgets/histogram/style.ts +8 -0
  180. package/src/widgets/histogram/types.ts +17 -0
  181. package/src/widgets/index.ts +25 -0
  182. package/src/widgets/loader/index.ts +4 -0
  183. package/src/widgets/loader/loader.tsx +70 -0
  184. package/src/widgets/loader/types.ts +11 -0
  185. package/src/widgets/loader/utils.test.ts +112 -0
  186. package/src/widgets/loader/utils.ts +35 -0
  187. package/src/widgets/markdown/config.ts +18 -0
  188. package/src/widgets/markdown/index.ts +14 -0
  189. package/src/widgets/markdown/markdown-ui.test.tsx +341 -0
  190. package/src/widgets/markdown/markdown-ui.tsx +52 -0
  191. package/src/widgets/markdown/markdown.tsx +20 -0
  192. package/src/widgets/markdown/skeleton.tsx +12 -0
  193. package/src/widgets/markdown/style.ts +28 -0
  194. package/src/widgets/markdown/types.ts +28 -0
  195. package/src/widgets/no-data/index.ts +2 -0
  196. package/src/widgets/no-data/no-data.test.tsx +447 -0
  197. package/src/widgets/no-data/no-data.tsx +116 -0
  198. package/src/widgets/no-data/style.ts +18 -0
  199. package/src/widgets/no-data/types.ts +72 -0
  200. package/src/widgets/note/index.ts +2 -0
  201. package/src/widgets/note/note.test.tsx +391 -0
  202. package/src/widgets/note/note.tsx +114 -0
  203. package/src/widgets/note/style.ts +29 -0
  204. package/src/widgets/note/types.ts +9 -0
  205. package/src/widgets/pie/config.ts +177 -0
  206. package/src/widgets/pie/index.ts +8 -0
  207. package/src/widgets/pie/skeleton.tsx +70 -0
  208. package/src/widgets/pie/style.ts +8 -0
  209. package/src/widgets/pie/types.ts +16 -0
  210. package/src/widgets/range/components/range-item.tsx +213 -0
  211. package/src/widgets/range/config.ts +10 -0
  212. package/src/widgets/range/index.ts +16 -0
  213. package/src/widgets/range/range-ui.test.tsx +203 -0
  214. package/src/widgets/range/range-ui.tsx +11 -0
  215. package/src/widgets/range/serializer.test.ts +70 -0
  216. package/src/widgets/range/serializer.ts +27 -0
  217. package/src/widgets/range/skeleton.tsx +14 -0
  218. package/src/widgets/range/style.ts +37 -0
  219. package/src/widgets/range/types.ts +39 -0
  220. package/src/widgets/scatterplot/config.ts +138 -0
  221. package/src/widgets/scatterplot/index.ts +8 -0
  222. package/src/widgets/scatterplot/skeleton.tsx +59 -0
  223. package/src/widgets/scatterplot/style.ts +21 -0
  224. package/src/widgets/scatterplot/types.ts +17 -0
  225. package/src/widgets/selection-summary/index.ts +6 -0
  226. package/src/widgets/selection-summary/selection-summary.tsx +46 -0
  227. package/src/widgets/selection-summary/style.ts +10 -0
  228. package/src/widgets/selection-summary/types.ts +14 -0
  229. package/src/widgets/skeleton-loader/index.ts +2 -0
  230. package/src/widgets/skeleton-loader/skeleton-loader.test.tsx +139 -0
  231. package/src/widgets/skeleton-loader/skeleton-loader.tsx +28 -0
  232. package/src/widgets/skeleton-loader/types.ts +8 -0
  233. package/src/widgets/spread/components/max-value.tsx +29 -0
  234. package/src/widgets/spread/components/min-value.tsx +29 -0
  235. package/src/widgets/spread/components/separator.tsx +6 -0
  236. package/src/widgets/spread/config.ts +34 -0
  237. package/src/widgets/spread/index.ts +23 -0
  238. package/src/widgets/spread/skeleton.tsx +10 -0
  239. package/src/widgets/spread/spread-ui.test.tsx +368 -0
  240. package/src/widgets/spread/spread-ui.tsx +29 -0
  241. package/src/widgets/spread/style.ts +22 -0
  242. package/src/widgets/spread/types.ts +47 -0
  243. package/src/widgets/stores/index.ts +9 -0
  244. package/src/widgets/stores/types.ts +192 -0
  245. package/src/widgets/stores/widget-store.test.ts +601 -0
  246. package/src/widgets/stores/widget-store.ts +239 -0
  247. package/src/widgets/subheader/index.ts +3 -0
  248. package/src/widgets/subheader/style.ts +20 -0
  249. package/src/widgets/subheader/subheader.test.tsx +45 -0
  250. package/src/widgets/subheader/subheader.tsx +16 -0
  251. package/src/widgets/subheader/types.ts +11 -0
  252. package/src/widgets/table/components/cell-header.tsx +58 -0
  253. package/src/widgets/table/components/cell.tsx +80 -0
  254. package/src/widgets/table/components/index.ts +4 -0
  255. package/src/widgets/table/components/pagination-actions.tsx +67 -0
  256. package/src/widgets/table/components/pagination.tsx +41 -0
  257. package/src/widgets/table/components/row.tsx +60 -0
  258. package/src/widgets/table/config.ts +71 -0
  259. package/src/widgets/table/helpers.test.ts +244 -0
  260. package/src/widgets/table/helpers.ts +107 -0
  261. package/src/widgets/table/hooks/index.ts +7 -0
  262. package/src/widgets/table/hooks/use-pagination.test.ts +294 -0
  263. package/src/widgets/table/hooks/use-pagination.ts +155 -0
  264. package/src/widgets/table/hooks/use-selection.test.ts +504 -0
  265. package/src/widgets/table/hooks/use-selection.ts +189 -0
  266. package/src/widgets/table/hooks/use-sort.test.ts +296 -0
  267. package/src/widgets/table/hooks/use-sort.ts +138 -0
  268. package/src/widgets/table/index.ts +53 -0
  269. package/src/widgets/table/serializer.ts +54 -0
  270. package/src/widgets/table/skeleton.tsx +48 -0
  271. package/src/widgets/table/style.ts +34 -0
  272. package/src/widgets/table/table-ui.tsx +64 -0
  273. package/src/widgets/table/types.ts +223 -0
  274. package/src/widgets/timeseries/config.ts +135 -0
  275. package/src/widgets/timeseries/index.ts +8 -0
  276. package/src/widgets/timeseries/skeleton.tsx +55 -0
  277. package/src/widgets/timeseries/style.ts +36 -0
  278. package/src/widgets/timeseries/types.ts +17 -0
  279. package/src/widgets/toolbar-actions/index.ts +6 -0
  280. package/src/widgets/toolbar-actions/styles.ts +38 -0
  281. package/src/widgets/toolbar-actions/toolbar-actions.test.tsx +691 -0
  282. package/src/widgets/toolbar-actions/toolbar-actions.tsx +145 -0
  283. package/src/widgets/toolbar-actions/types.ts +60 -0
  284. package/src/widgets/wrapper/components/actions.test.tsx +101 -0
  285. package/src/widgets/wrapper/components/actions.tsx +30 -0
  286. package/src/widgets/wrapper/components/options.test.tsx +323 -0
  287. package/src/widgets/wrapper/components/options.tsx +73 -0
  288. package/src/widgets/wrapper/components/title.test.tsx +126 -0
  289. package/src/widgets/wrapper/components/title.tsx +32 -0
  290. package/src/widgets/wrapper/index.ts +16 -0
  291. package/src/widgets/wrapper/styles.ts +98 -0
  292. package/src/widgets/wrapper/types.ts +55 -0
  293. package/src/widgets/wrapper/wrapper-ui.test.tsx +232 -0
  294. package/src/widgets/wrapper/wrapper-ui.tsx +57 -0
  295. package/src/widgets/wrapper/wrapper.test.tsx +365 -0
  296. package/src/widgets/wrapper/wrapper.tsx +50 -0
  297. package/dist/lasso-tool-BwRzEW7k.js.map +0 -1
  298. package/dist/types/common/common.d.ts +0 -3
  299. package/dist/types/common/index.d.ts +0 -26
  300. package/dist/types/common/lasso-tools.d.ts +0 -36
  301. package/dist/types/common/measurement-tools.d.ts +0 -65
@@ -0,0 +1,186 @@
1
+ import { describe, test, expect, beforeEach } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { LockSelection } from './lock-selection'
4
+ import { useWidgetStore } from '../../stores/widget-store'
5
+ import type { EchartWidgetData } from '../../echart/types'
6
+ import type { LockSelectionState } from './types'
7
+ import { WidgetLoader } from '../../loader/loader'
8
+
9
+ // Test data
10
+ const mockData: EchartWidgetData = [
11
+ [
12
+ { name: 'Electronics', value: 440 },
13
+ { name: 'Clothing', value: 280 },
14
+ { name: 'Books', value: 100 },
15
+ ],
16
+ ]
17
+
18
+ describe('LockSelection', () => {
19
+ const widgetId = 'test-lock-selection'
20
+
21
+ beforeEach(() => {
22
+ useWidgetStore.getState().clearWidgets()
23
+ })
24
+
25
+ test('renders nothing when no selections', () => {
26
+ const { container } = render(
27
+ <LockSelection id={widgetId} selectedItems={[]} />,
28
+ )
29
+
30
+ expect(container.firstChild).toBeNull()
31
+ })
32
+
33
+ test('renders lock button when there are selections', () => {
34
+ render(<LockSelection id={widgetId} selectedItems={['Electronics']} />)
35
+
36
+ const button = screen.getByRole('button')
37
+ expect(button).toBeTruthy()
38
+ })
39
+
40
+ test('shows lock tooltip when unlocked', () => {
41
+ render(<LockSelection id={widgetId} selectedItems={['Electronics']} />)
42
+
43
+ const button = screen.getByRole('button', { name: 'Lock selection' })
44
+ expect(button).toBeTruthy()
45
+ })
46
+
47
+ test('shows unlock tooltip when locked', () => {
48
+ // Pre-set the store to locked state
49
+ useWidgetStore.getState().setWidget(widgetId, { isLocked: true })
50
+
51
+ render(<LockSelection id={widgetId} selectedItems={['Electronics']} />)
52
+
53
+ const button = screen.getByRole('button', { name: 'Unlock selection' })
54
+ expect(button).toBeTruthy()
55
+ })
56
+
57
+ test('toggles isLocked in store when clicked', () => {
58
+ render(<LockSelection id={widgetId} selectedItems={['Electronics']} />)
59
+
60
+ const button = screen.getByRole('button')
61
+ fireEvent.click(button)
62
+
63
+ const widget = useWidgetStore
64
+ .getState()
65
+ .getWidget<LockSelectionState>(widgetId)
66
+ expect(widget?.isLocked).toBe(true)
67
+ })
68
+
69
+ test('filters data in store when locking', async () => {
70
+ const selectedItems = ['Electronics']
71
+
72
+ render(
73
+ <WidgetLoader
74
+ id={widgetId}
75
+ type='test'
76
+ data={mockData}
77
+ isLoading={false}
78
+ isFetching={false}
79
+ >
80
+ <LockSelection id={widgetId} selectedItems={selectedItems} />
81
+ </WidgetLoader>,
82
+ )
83
+
84
+ const button = screen.getByRole('button')
85
+ fireEvent.click(button)
86
+
87
+ await useWidgetStore.getState().executeToolPipeline(widgetId, mockData)
88
+
89
+ const widget = useWidgetStore
90
+ .getState()
91
+ .getWidget<LockSelectionState>(widgetId)
92
+ expect(widget?.data).toEqual([[{ name: 'Electronics', value: 440 }]])
93
+ })
94
+
95
+ 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
+ render(<LockSelection id={widgetId} selectedItems={['Electronics']} />)
103
+
104
+ const button = screen.getByRole('button')
105
+ fireEvent.click(button)
106
+
107
+ const widget = useWidgetStore
108
+ .getState()
109
+ .getWidget<LockSelectionState>(widgetId)
110
+ expect(widget?.isLocked).toBe(false)
111
+ })
112
+
113
+ test('has active state when locked', () => {
114
+ useWidgetStore.getState().setWidget(widgetId, { isLocked: true })
115
+
116
+ render(<LockSelection id={widgetId} selectedItems={['Electronics']} />)
117
+
118
+ const button = screen.getByRole('button')
119
+ expect(button.getAttribute('data-active')).toBe('true')
120
+ })
121
+
122
+ test('has inactive state when unlocked', () => {
123
+ render(<LockSelection id={widgetId} selectedItems={['Electronics']} />)
124
+
125
+ const button = screen.getByRole('button')
126
+ expect(button.getAttribute('data-active')).toBe('false')
127
+ })
128
+
129
+ test('uses custom labels', () => {
130
+ render(
131
+ <LockSelection
132
+ id={widgetId}
133
+ selectedItems={['Electronics']}
134
+ labels={{
135
+ lock: 'Bloquear selección',
136
+ unlock: 'Desbloquear selección',
137
+ ariaLabel: 'Toggle lock',
138
+ }}
139
+ />,
140
+ )
141
+
142
+ const button = screen.getByRole('button', { name: 'Toggle lock' })
143
+ expect(button).toBeTruthy()
144
+ })
145
+
146
+ test('filters multiple series when locking', async () => {
147
+ const multiSeriesData: EchartWidgetData = [
148
+ [
149
+ { name: 'Electronics', value: 440 },
150
+ { name: 'Clothing', value: 280 },
151
+ ],
152
+ [
153
+ { name: 'Electronics', value: 520 },
154
+ { name: 'Clothing', value: 350 },
155
+ ],
156
+ ]
157
+ const selectedItems = ['Electronics']
158
+
159
+ render(
160
+ <WidgetLoader
161
+ id={widgetId}
162
+ type='test'
163
+ data={multiSeriesData}
164
+ isLoading={false}
165
+ isFetching={false}
166
+ >
167
+ <LockSelection id={widgetId} selectedItems={selectedItems} />
168
+ </WidgetLoader>,
169
+ )
170
+
171
+ const button = screen.getByRole('button')
172
+ fireEvent.click(button)
173
+
174
+ await useWidgetStore
175
+ .getState()
176
+ .executeToolPipeline(widgetId, multiSeriesData)
177
+
178
+ const widget = useWidgetStore
179
+ .getState()
180
+ .getWidget<LockSelectionState>(widgetId)
181
+ expect(widget?.data).toEqual([
182
+ [{ name: 'Electronics', value: 440 }],
183
+ [{ name: 'Electronics', value: 520 }],
184
+ ])
185
+ })
186
+ })
@@ -0,0 +1,133 @@
1
+ import { IconButton } from '@mui/material'
2
+ import { CheckBoxOutlined } from '@mui/icons-material'
3
+ import { useCallback, useEffect, useMemo } from 'react'
4
+ import type { LockSelectionProps, LockSelectionState } from './types'
5
+ import { actionButtonStyles } from '../shared/styles'
6
+ import { Tooltip } from '../../../components'
7
+ import { useWidgetStore } from '../../stores/widget-store'
8
+ import { useShallow } from 'zustand/shallow'
9
+ import type { EchartWidgetData } from '../../echart/types'
10
+
11
+ export const LOCK_SELECTION_TOOL_ID = 'lock-selection'
12
+
13
+ /**
14
+ * Widget action button to lock the current selection.
15
+ *
16
+ * When locked, the widget data is filtered to only show the selected items.
17
+ * The button is only visible when there are selected items.
18
+ * Registers a transformation tool in the widget pipeline when mounted.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * <LockSelection
23
+ * id="my-widget"
24
+ * order={30}
25
+ * selectedItems={selectedItems}
26
+ * />
27
+ * ```
28
+ */
29
+ export function LockSelection({
30
+ id,
31
+ selectedItems,
32
+ order = 30,
33
+ labels,
34
+ Icon,
35
+ IconButtonProps,
36
+ }: LockSelectionProps) {
37
+ const setWidget = useWidgetStore((state) => state.setWidget)
38
+ const registerTool = useWidgetStore((state) => state.registerTool)
39
+ const unregisterTool = useWidgetStore((state) => state.unregisterTool)
40
+ const setToolEnabled = useWidgetStore((state) => state.setToolEnabled)
41
+ const updateToolConfig = useWidgetStore((state) => state.updateToolConfig)
42
+
43
+ const storeIsLocked = useWidgetStore(
44
+ useShallow((state) => state.getWidget<LockSelectionState>(id)?.isLocked),
45
+ )
46
+
47
+ const isLocked = storeIsLocked ?? false
48
+ const lockedItems = useMemo(
49
+ () => (isLocked ? selectedItems : []),
50
+ [isLocked, selectedItems],
51
+ )
52
+
53
+ // Register tool on mount
54
+ useEffect(() => {
55
+ registerTool(id, {
56
+ id: LOCK_SELECTION_TOOL_ID,
57
+ order,
58
+ enabled: isLocked,
59
+ fn: (data, config) => {
60
+ const items = (config?.lockedItems as string[]) || []
61
+ if (items.length === 0) return data
62
+
63
+ return filterDataByLockedItems(data as EchartWidgetData, items)
64
+ },
65
+ config: { lockedItems },
66
+ })
67
+
68
+ return () => unregisterTool(id, LOCK_SELECTION_TOOL_ID)
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])
76
+
77
+ const handleToggle = useCallback(() => {
78
+ if (isLocked) {
79
+ // Unlock: clear locked items and disable tool
80
+ setWidget(id, {
81
+ isLocked: false,
82
+ lockedItems: [],
83
+ })
84
+ } else {
85
+ // Lock: save selected items and enable tool
86
+ setWidget(id, {
87
+ isLocked: true,
88
+ lockedItems: selectedItems,
89
+ })
90
+ }
91
+ }, [id, isLocked, selectedItems, setWidget])
92
+
93
+ // Don't render if no selections
94
+ if (selectedItems.length === 0) {
95
+ return null
96
+ }
97
+
98
+ const tooltipLabel = isLocked
99
+ ? (labels?.unlock ?? 'Unlock selection')
100
+ : (labels?.lock ?? 'Lock selection')
101
+
102
+ const ariaLabel = labels?.ariaLabel ?? tooltipLabel
103
+
104
+ return (
105
+ <Tooltip title={tooltipLabel}>
106
+ <IconButton
107
+ size='small'
108
+ aria-label={ariaLabel}
109
+ onClick={handleToggle}
110
+ sx={actionButtonStyles.trigger}
111
+ data-active={isLocked}
112
+ {...IconButtonProps}
113
+ >
114
+ {Icon ?? <CheckBoxOutlined fontSize='small' />}
115
+ </IconButton>
116
+ </Tooltip>
117
+ )
118
+ }
119
+
120
+ /**
121
+ * Filters widget data to only include items with names matching the locked items.
122
+ */
123
+ function filterDataByLockedItems(
124
+ data: EchartWidgetData,
125
+ lockedItems: string[],
126
+ ): EchartWidgetData {
127
+ return data.map((series) =>
128
+ series.filter((item) => {
129
+ const name = item.name
130
+ return typeof name === 'string' && lockedItems.includes(name)
131
+ }),
132
+ )
133
+ }
@@ -0,0 +1,41 @@
1
+ import type { IconButtonProps } from '@mui/material'
2
+ import type { ReactNode } from 'react'
3
+ import type { BaseWidgetState } from '../../stores/types'
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
+ /**
14
+ * Widget state extension for lock selection functionality.
15
+ * Extends the base widget state with lock-specific properties.
16
+ */
17
+ export type LockSelectionState<T = object> = BaseWidgetState<
18
+ T & LockSelectionStateProps
19
+ >
20
+
21
+ export interface LockSelectionProps {
22
+ /** Widget ID to store lock selection state */
23
+ id: string
24
+ /** Currently selected items (by name) */
25
+ selectedItems: string[]
26
+ /** Execution order in the tool pipeline. Lower values execute first. Defaults to 30. */
27
+ order?: number
28
+ /** Custom labels for the action */
29
+ labels?: {
30
+ /** Tooltip when unlocked (button will lock selection) */
31
+ lock?: string
32
+ /** Tooltip when locked (button will unlock selection) */
33
+ unlock?: string
34
+ /** Accessibility label */
35
+ ariaLabel?: string
36
+ }
37
+ /** Props passed to the IconButton component */
38
+ IconButtonProps?: IconButtonProps
39
+ /** Custom icon to display */
40
+ Icon?: ReactNode
41
+ }
@@ -0,0 +1,267 @@
1
+ import { describe, test, expect, beforeEach } from 'vitest'
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react'
3
+ import { RelativeData } from './relative-data'
4
+ import { useWidgetStore } from '../../stores/widget-store'
5
+ import type { EchartWidgetData } from '../../echart/types'
6
+ import type { RelativeDataState } from './types'
7
+
8
+ describe('RelativeData', () => {
9
+ const widgetId = 'test-relative-widget'
10
+
11
+ const mockData: EchartWidgetData = [
12
+ [
13
+ { category: 'A', value: 25 },
14
+ { category: 'B', value: 75 },
15
+ ],
16
+ ]
17
+
18
+ beforeEach(() => {
19
+ useWidgetStore.getState().clearWidgets()
20
+ })
21
+
22
+ test('renders relative data toggle button', () => {
23
+ render(<RelativeData id={widgetId} />)
24
+
25
+ const button = screen.getByRole('button')
26
+ expect(button).toBeTruthy()
27
+ })
28
+
29
+ test('shows relative tooltip when in absolute mode', () => {
30
+ render(<RelativeData id={widgetId} />)
31
+
32
+ const button = screen.getByRole('button', { name: 'Show relative values' })
33
+ expect(button).toBeTruthy()
34
+ })
35
+
36
+ test('toggles to relative mode and updates widget store with percentage values', async () => {
37
+ render(<RelativeData id={widgetId} />)
38
+
39
+ const button = screen.getByRole('button')
40
+ fireEvent.click(button)
41
+
42
+ // Execute the pipeline with the source data
43
+ await useWidgetStore.getState().executeToolPipeline(widgetId, mockData)
44
+
45
+ const widget = useWidgetStore.getState().getWidget(widgetId)
46
+ const data = widget?.data as EchartWidgetData
47
+
48
+ // Total is 100, so 25 becomes 25% and 75 becomes 75%
49
+ expect(data?.[0]?.[0]?.value).toBe(25)
50
+ expect(data?.[0]?.[1]?.value).toBe(75)
51
+ expect(data?.[0]?.[0]?.category).toBe('A')
52
+ expect(data?.[0]?.[1]?.category).toBe('B')
53
+ })
54
+
55
+ test('toggles back to absolute mode and restores original values', async () => {
56
+ render(<RelativeData id={widgetId} />)
57
+
58
+ const button = screen.getByRole('button')
59
+
60
+ // Toggle to relative
61
+ fireEvent.click(button)
62
+ await useWidgetStore.getState().executeToolPipeline(widgetId, mockData)
63
+
64
+ // Toggle back to absolute
65
+ fireEvent.click(button)
66
+ await useWidgetStore.getState().executeToolPipeline(widgetId, mockData)
67
+
68
+ const widget = useWidgetStore.getState().getWidget(widgetId)
69
+ const data = widget?.data as EchartWidgetData
70
+
71
+ expect(data?.[0]?.[0]?.value).toBe(25)
72
+ expect(data?.[0]?.[1]?.value).toBe(75)
73
+ })
74
+
75
+ test('has active state when in relative mode', () => {
76
+ render(<RelativeData id={widgetId} />)
77
+
78
+ const button = screen.getByRole('button')
79
+
80
+ expect(button.getAttribute('data-active')).toBe('false')
81
+
82
+ fireEvent.click(button)
83
+
84
+ expect(button.getAttribute('data-active')).toBe('true')
85
+ })
86
+
87
+ test('starts in relative mode when defaultIsRelative is true', () => {
88
+ render(<RelativeData id={widgetId} defaultIsRelative />)
89
+
90
+ const button = screen.getByRole('button')
91
+ expect(button.getAttribute('data-active')).toBe('true')
92
+
93
+ // Should show absolute tooltip when in relative mode
94
+ expect(button.getAttribute('aria-label')).toBe('Show absolute values')
95
+ })
96
+
97
+ test('initializes widget store with defaultIsRelative on mount', async () => {
98
+ render(<RelativeData id={widgetId} defaultIsRelative />)
99
+
100
+ await waitFor(() => {
101
+ const widget = useWidgetStore
102
+ .getState()
103
+ .getWidget<RelativeDataState>(widgetId)
104
+ expect(widget?.isRelative).toBe(true)
105
+ })
106
+ })
107
+
108
+ test('initializes widget store with false when defaultIsRelative is false', async () => {
109
+ render(<RelativeData id={widgetId} defaultIsRelative={false} />)
110
+
111
+ await waitFor(() => {
112
+ const widget = useWidgetStore
113
+ .getState()
114
+ .getWidget<RelativeDataState>(widgetId)
115
+ expect(widget?.isRelative).toBe(false)
116
+ })
117
+ })
118
+
119
+ test('uses custom labels when provided', () => {
120
+ const customLabels = {
121
+ relative: 'Switch to %',
122
+ absolute: 'Switch to numbers',
123
+ ariaLabel: 'Toggle data mode',
124
+ }
125
+
126
+ render(<RelativeData id={widgetId} labels={customLabels} />)
127
+
128
+ const button = screen.getByRole('button', { name: 'Toggle data mode' })
129
+ expect(button).toBeTruthy()
130
+ })
131
+
132
+ test('handles data with multiple series', async () => {
133
+ const multiSeriesData: EchartWidgetData = [
134
+ [
135
+ { name: 'X', count: 10 },
136
+ { name: 'Y', count: 20 },
137
+ ],
138
+ [
139
+ { name: 'X', count: 30 },
140
+ { name: 'Y', count: 40 },
141
+ ],
142
+ ]
143
+
144
+ render(<RelativeData id={widgetId} />)
145
+
146
+ const button = screen.getByRole('button')
147
+ fireEvent.click(button)
148
+
149
+ await useWidgetStore
150
+ .getState()
151
+ .executeToolPipeline(widgetId, multiSeriesData)
152
+
153
+ const widget = useWidgetStore.getState().getWidget(widgetId)
154
+ const data = widget?.data as EchartWidgetData
155
+
156
+ // Total is 100, so values become 10%, 20%, 30%, 40%
157
+ expect(data?.[0]?.[0]?.count).toBe(10)
158
+ expect(data?.[0]?.[1]?.count).toBe(20)
159
+ expect(data?.[1]?.[0]?.count).toBe(30)
160
+ expect(data?.[1]?.[1]?.count).toBe(40)
161
+ })
162
+
163
+ test('handles zero total gracefully', async () => {
164
+ const zeroData: EchartWidgetData = [
165
+ [
166
+ { category: 'A', value: 0 },
167
+ { category: 'B', value: 0 },
168
+ ],
169
+ ]
170
+
171
+ render(<RelativeData id={widgetId} />)
172
+
173
+ const button = screen.getByRole('button')
174
+ fireEvent.click(button)
175
+
176
+ await useWidgetStore.getState().executeToolPipeline(widgetId, zeroData)
177
+
178
+ const widget = useWidgetStore.getState().getWidget(widgetId)
179
+ const data = widget?.data as EchartWidgetData
180
+
181
+ // Should preserve original data when total is 0
182
+ expect(data?.[0]?.[0]?.value).toBe(0)
183
+ expect(data?.[0]?.[1]?.value).toBe(0)
184
+ })
185
+
186
+ test('preserves string values during transformation', async () => {
187
+ const mixedData: EchartWidgetData = [
188
+ [
189
+ { label: 'First', value: 50, extra: 'text' },
190
+ { label: 'Second', value: 50, extra: 'more' },
191
+ ],
192
+ ]
193
+
194
+ render(<RelativeData id={widgetId} />)
195
+
196
+ const button = screen.getByRole('button')
197
+ fireEvent.click(button)
198
+
199
+ await useWidgetStore.getState().executeToolPipeline(widgetId, mixedData)
200
+
201
+ const widget = useWidgetStore.getState().getWidget(widgetId)
202
+ const data = widget?.data as EchartWidgetData
203
+
204
+ expect(data?.[0]?.[0]?.label).toBe('First')
205
+ expect(data?.[0]?.[0]?.extra).toBe('text')
206
+ expect(data?.[0]?.[1]?.label).toBe('Second')
207
+ expect(data?.[0]?.[1]?.extra).toBe('more')
208
+ })
209
+
210
+ test('renders custom icon when provided', () => {
211
+ const customIcon = <span data-testid='custom-icon'>%</span>
212
+
213
+ render(<RelativeData id={widgetId} Icon={customIcon} />)
214
+
215
+ expect(screen.getByTestId('custom-icon')).toBeTruthy()
216
+ })
217
+
218
+ test('passes IconButtonProps to the button', () => {
219
+ render(<RelativeData id={widgetId} IconButtonProps={{ disabled: true }} />)
220
+
221
+ const button = screen.getByRole('button')
222
+ expect(button.hasAttribute('disabled')).toBeTruthy()
223
+ })
224
+
225
+ test('recalculates relative values when store data changes externally while in relative mode', async () => {
226
+ const initialData: EchartWidgetData = [
227
+ [
228
+ { category: 'A', value: 50 },
229
+ { category: 'B', value: 50 },
230
+ ],
231
+ ]
232
+
233
+ // Pre-populate the widget in the store with relative data
234
+ useWidgetStore.getState().setWidget(widgetId, {
235
+ data: initialData,
236
+ isRelative: true,
237
+ })
238
+
239
+ render(<RelativeData id={widgetId} defaultIsRelative />)
240
+
241
+ let widget
242
+ let data
243
+ await waitFor(() => {
244
+ widget = useWidgetStore.getState().getWidget(widgetId)
245
+ data = widget?.data as EchartWidgetData
246
+ // Initial data: 50/100 = 50%, 50/100 = 50%
247
+ expect(data?.[0]?.[0]?.value).toBe(50)
248
+ expect(data?.[0]?.[1]?.value).toBe(50)
249
+ })
250
+
251
+ // Simulate external store update with new absolute data
252
+ const newAbsoluteData: EchartWidgetData = [
253
+ [
254
+ { category: 'A', value: 25 },
255
+ { category: 'B', value: 75 },
256
+ ],
257
+ ]
258
+ useWidgetStore.getState().setWidget(widgetId, { data: newAbsoluteData })
259
+
260
+ // Component should detect external change and recalculate
261
+ // New data: 25/100 = 25%, 75/100 = 75%
262
+ widget = useWidgetStore.getState().getWidget(widgetId)
263
+ data = widget?.data as EchartWidgetData
264
+ expect(data?.[0]?.[0]?.value).toBe(25)
265
+ expect(data?.[0]?.[1]?.value).toBe(75)
266
+ })
267
+ })