@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,296 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest'
2
+ import { renderHook, act } from '@testing-library/react'
3
+ import { useSort } from './use-sort'
4
+ import { useWidgetStore } from '../../stores/widget-store'
5
+ import type { TableRow } from '../types'
6
+
7
+ describe('useSort', () => {
8
+ const testData: TableRow[] = [
9
+ { id: 1, name: 'Charlie', age: 30 },
10
+ { id: 2, name: 'Alice', age: 25 },
11
+ { id: 3, name: 'Bob', age: 35 },
12
+ ]
13
+
14
+ // Clear widget store before each test
15
+ beforeEach(() => {
16
+ useWidgetStore.getState().clearWidgets()
17
+ })
18
+
19
+ describe('initialization', () => {
20
+ it('should initialize with default values', () => {
21
+ const { result } = renderHook(() => useSort('test-widget', testData))
22
+
23
+ expect(result.current.columnId).toBeNull()
24
+ expect(result.current.direction).toBe('asc')
25
+ expect(result.current.sortedData).toEqual(testData)
26
+ })
27
+
28
+ it('should initialize with custom values', () => {
29
+ const { result } = renderHook(() => useSort('test-widget', testData))
30
+
31
+ expect(result.current.columnId).toBeNull()
32
+ expect(result.current.direction).toBe('asc')
33
+ })
34
+
35
+ it('should sort data when column is set', () => {
36
+ const { result } = renderHook(() => useSort('test-widget', testData))
37
+
38
+ act(() => {
39
+ result.current.setSort('name', 'asc')
40
+ })
41
+
42
+ expect(result.current.sortedData.map((r) => r.name)).toEqual([
43
+ 'Alice',
44
+ 'Bob',
45
+ 'Charlie',
46
+ ])
47
+ })
48
+ })
49
+
50
+ describe('local mode', () => {
51
+ it('should toggle sort direction on same column', () => {
52
+ const { result } = renderHook(() => useSort('test-widget', testData))
53
+
54
+ // First toggle - set to asc
55
+ act(() => {
56
+ result.current.toggleSort('name')
57
+ })
58
+
59
+ expect(result.current.columnId).toBe('name')
60
+ expect(result.current.direction).toBe('asc')
61
+
62
+ // Second toggle - change to desc
63
+ act(() => {
64
+ result.current.toggleSort('name')
65
+ })
66
+
67
+ expect(result.current.columnId).toBe('name')
68
+ expect(result.current.direction).toBe('desc')
69
+
70
+ // Third toggle - back to asc
71
+ act(() => {
72
+ result.current.toggleSort('name')
73
+ })
74
+
75
+ expect(result.current.direction).toBe('asc')
76
+ })
77
+
78
+ it('should reset to asc when changing column', () => {
79
+ const { result } = renderHook(() => useSort('test-widget', testData))
80
+
81
+ // First set name column to desc
82
+ act(() => {
83
+ result.current.setSort('name', 'desc')
84
+ })
85
+
86
+ // Then toggle to age column
87
+ act(() => {
88
+ result.current.toggleSort('age')
89
+ })
90
+
91
+ expect(result.current.columnId).toBe('age')
92
+ expect(result.current.direction).toBe('asc')
93
+ })
94
+
95
+ it('should sort data ascending', () => {
96
+ const { result } = renderHook(() => useSort('test-widget', testData))
97
+
98
+ act(() => {
99
+ result.current.toggleSort('name')
100
+ })
101
+
102
+ expect(result.current.sortedData.map((r) => r.name)).toEqual([
103
+ 'Alice',
104
+ 'Bob',
105
+ 'Charlie',
106
+ ])
107
+ })
108
+
109
+ it('should sort data descending', () => {
110
+ const { result } = renderHook(() => useSort('test-widget', testData))
111
+
112
+ // First toggle sets to asc
113
+ act(() => {
114
+ result.current.toggleSort('name')
115
+ })
116
+
117
+ // Second toggle sets to desc
118
+ act(() => {
119
+ result.current.toggleSort('name')
120
+ })
121
+
122
+ expect(result.current.sortedData.map((r) => r.name)).toEqual([
123
+ 'Charlie',
124
+ 'Bob',
125
+ 'Alice',
126
+ ])
127
+ })
128
+
129
+ it('should sort numbers correctly', () => {
130
+ const { result } = renderHook(() => useSort('test-widget', testData))
131
+
132
+ act(() => {
133
+ result.current.toggleSort('age')
134
+ })
135
+
136
+ expect(result.current.sortedData.map((r) => r.age)).toEqual([25, 30, 35])
137
+ })
138
+
139
+ it('should set sort state directly', () => {
140
+ const { result } = renderHook(() => useSort('test-widget', testData))
141
+
142
+ act(() => {
143
+ result.current.setSort('age', 'desc')
144
+ })
145
+
146
+ expect(result.current.columnId).toBe('age')
147
+ expect(result.current.direction).toBe('desc')
148
+ expect(result.current.sortedData.map((r) => r.age)).toEqual([35, 30, 25])
149
+ })
150
+
151
+ it('should clear sort', () => {
152
+ const { result } = renderHook(() => useSort('test-widget', testData))
153
+
154
+ // First set a sort
155
+ act(() => {
156
+ result.current.setSort('name', 'desc')
157
+ })
158
+
159
+ // Then clear it
160
+ act(() => {
161
+ result.current.clearSort()
162
+ })
163
+
164
+ expect(result.current.columnId).toBeNull()
165
+ expect(result.current.direction).toBe('asc')
166
+ expect(result.current.sortedData).toEqual(testData)
167
+ })
168
+
169
+ it('should persist state in widget store', () => {
170
+ const { result, unmount } = renderHook(() =>
171
+ useSort('test-widget', testData),
172
+ )
173
+
174
+ act(() => {
175
+ result.current.toggleSort('name')
176
+ result.current.toggleSort('name') // toggle to desc
177
+ })
178
+
179
+ expect(result.current.columnId).toBe('name')
180
+ expect(result.current.direction).toBe('desc')
181
+
182
+ // Unmount and remount - state should persist
183
+ unmount()
184
+
185
+ const { result: newResult } = renderHook(() =>
186
+ useSort('test-widget', testData),
187
+ )
188
+
189
+ expect(newResult.current.columnId).toBe('name')
190
+ expect(newResult.current.direction).toBe('desc')
191
+ })
192
+ })
193
+
194
+ describe('remote mode', () => {
195
+ it('should return data as-is in remote mode', () => {
196
+ useWidgetStore.getState().setWidget('test-widget', {
197
+ mode: 'remote',
198
+ sort: { columnId: null, direction: 'asc' },
199
+ })
200
+ const { result } = renderHook(() => useSort('test-widget', testData))
201
+
202
+ // In remote mode, sortedData should be the original data
203
+ // since server handles sorting
204
+ expect(result.current.sortedData).toEqual(testData)
205
+ })
206
+
207
+ it('should update store state when toggling sort in remote mode', () => {
208
+ useWidgetStore.getState().setWidget('test-widget', {
209
+ mode: 'remote',
210
+ sort: { columnId: null, direction: 'asc' },
211
+ })
212
+ const { result } = renderHook(() => useSort('test-widget', testData))
213
+
214
+ act(() => {
215
+ result.current.toggleSort('name')
216
+ })
217
+
218
+ expect(result.current.columnId).toBe('name')
219
+ expect(result.current.direction).toBe('asc')
220
+ })
221
+
222
+ it('should update store state when setting sort in remote mode', () => {
223
+ useWidgetStore.getState().setWidget('test-widget', {
224
+ mode: 'remote',
225
+ sort: { columnId: null, direction: 'asc' },
226
+ })
227
+ const { result } = renderHook(() => useSort('test-widget', testData))
228
+
229
+ act(() => {
230
+ result.current.setSort('age', 'desc')
231
+ })
232
+
233
+ expect(result.current.columnId).toBe('age')
234
+ expect(result.current.direction).toBe('desc')
235
+ // Data should still be unmodified in remote mode
236
+ expect(result.current.sortedData).toEqual(testData)
237
+ })
238
+ })
239
+
240
+ describe('edge cases', () => {
241
+ it('should handle empty data', () => {
242
+ const { result } = renderHook(() => useSort('test-widget', []))
243
+
244
+ expect(result.current.sortedData).toEqual([])
245
+
246
+ act(() => {
247
+ result.current.toggleSort('name')
248
+ })
249
+
250
+ expect(result.current.sortedData).toEqual([])
251
+ })
252
+
253
+ it('should handle single item data', () => {
254
+ const singleItem = [{ id: 1, name: 'Alice' }]
255
+ const { result } = renderHook(() => useSort('test-widget', singleItem))
256
+
257
+ act(() => {
258
+ result.current.toggleSort('name')
259
+ })
260
+
261
+ expect(result.current.sortedData).toEqual(singleItem)
262
+ })
263
+
264
+ it('should preserve sort when data changes', () => {
265
+ const { result, rerender } = renderHook(
266
+ ({ data }) => useSort('test-widget', data),
267
+ { initialProps: { data: testData } },
268
+ )
269
+
270
+ act(() => {
271
+ result.current.toggleSort('name')
272
+ })
273
+
274
+ expect(result.current.sortedData.map((r) => r.name)).toEqual([
275
+ 'Alice',
276
+ 'Bob',
277
+ 'Charlie',
278
+ ])
279
+
280
+ // Add new data
281
+ const newData: TableRow[] = [
282
+ ...testData,
283
+ { id: 4, name: 'David', age: 28 },
284
+ ]
285
+
286
+ rerender({ data: newData })
287
+
288
+ expect(result.current.sortedData.map((r) => r.name)).toEqual([
289
+ 'Alice',
290
+ 'Bob',
291
+ 'Charlie',
292
+ 'David',
293
+ ])
294
+ })
295
+ })
296
+ })
@@ -0,0 +1,138 @@
1
+ import { useCallback, useMemo } from 'react'
2
+ import type {
3
+ TableRow,
4
+ SortDirection,
5
+ SortState,
6
+ TableWidgetState,
7
+ } from '../types'
8
+ import { sortData } from '../helpers'
9
+ import { useWidgetStore } from '../../stores/widget-store'
10
+ import { useShallow } from 'zustand/shallow'
11
+ import { DEFAULT_COLUMN_ID, DEFAULT_DIRECTION } from '../config'
12
+
13
+ export interface UseSortResult<T> extends SortState {
14
+ /** Whether sorting is enabled */
15
+ sortEnabled: boolean
16
+ /** Current sort state */
17
+ columnId: string | null
18
+ direction: SortDirection
19
+ /** Sorted data (for local mode) */
20
+ sortedData: T[]
21
+ /** Toggle sort on a column */
22
+ toggleSort: (columnId: string) => void
23
+ /** Set sort state directly */
24
+ setSort: (columnId: string | null, direction: SortDirection) => void
25
+ /** Clear sort */
26
+ clearSort: () => void
27
+ }
28
+
29
+ /**
30
+ * Hook for managing table sorting
31
+ * Supports both local (client-side) and remote (server-side) sorting
32
+ * State is persisted in the widget store for the given widgetId
33
+ */
34
+ export function useSort<T extends TableRow>(
35
+ widgetId: string,
36
+ data: T[],
37
+ ): UseSortResult<T> {
38
+ // Get store actions and state
39
+ const setWidget = useWidgetStore((state) => state.setWidget)
40
+ const getWidget = useWidgetStore((state) => state.getWidget)
41
+ const sortStoreState = useWidgetStore(
42
+ useShallow((state) => {
43
+ const widget = state.widgets[widgetId] as TableWidgetState | undefined
44
+ return {
45
+ sortEnabled: !!widget?.sort,
46
+ columnId: widget?.sort?.columnId ?? DEFAULT_COLUMN_ID,
47
+ direction: widget?.sort?.direction ?? DEFAULT_DIRECTION,
48
+ }
49
+ }),
50
+ )
51
+
52
+ const mode = useWidgetStore(
53
+ useShallow((state) => {
54
+ const widget = state.widgets[widgetId] as TableWidgetState | undefined
55
+ return widget?.mode
56
+ }),
57
+ )
58
+
59
+ // Toggle sort on a column
60
+ const toggleSort = useCallback(
61
+ (toggleColumnId: string) => {
62
+ // Read current state directly from store to avoid stale closures
63
+ const widget = getWidget<TableWidgetState>(widgetId)
64
+ const currentColumnId = widget?.sort?.columnId ?? DEFAULT_COLUMN_ID
65
+ const currentDirection = widget?.sort?.direction ?? DEFAULT_DIRECTION
66
+
67
+ let newDirection: SortDirection = 'asc'
68
+
69
+ if (currentColumnId === toggleColumnId) {
70
+ // Same column - toggle direction
71
+ newDirection = currentDirection === 'asc' ? 'desc' : 'asc'
72
+ }
73
+
74
+ setWidget<TableWidgetState>(widgetId, {
75
+ sort: {
76
+ columnId: toggleColumnId,
77
+ direction: newDirection,
78
+ },
79
+ })
80
+ },
81
+ [getWidget, setWidget, widgetId],
82
+ )
83
+
84
+ // Set sort state directly
85
+ const setSort = useCallback(
86
+ (newColumnId: string | null, newDirection: SortDirection) => {
87
+ setWidget<TableWidgetState>(widgetId, {
88
+ sort: {
89
+ columnId: newColumnId,
90
+ direction: newDirection,
91
+ },
92
+ })
93
+ },
94
+ [setWidget, widgetId],
95
+ )
96
+
97
+ // Clear sort
98
+ const clearSort = useCallback(() => {
99
+ const widget = getWidget<TableWidgetState>(widgetId)
100
+ if (!widget?.sort) return
101
+
102
+ setWidget<TableWidgetState>(widgetId, {
103
+ sort: {
104
+ ...widget.sort,
105
+ columnId: null,
106
+ direction: 'asc',
107
+ },
108
+ })
109
+ }, [setWidget, getWidget, widgetId])
110
+
111
+ // Sorted data for local mode
112
+ const sortedData = useMemo(() => {
113
+ if (
114
+ mode === 'remote' ||
115
+ !sortStoreState.columnId ||
116
+ !sortStoreState.sortEnabled
117
+ ) {
118
+ // For remote mode, data is already sorted from server
119
+ // If no sort column, return original data
120
+ return data
121
+ }
122
+ return sortData(data, sortStoreState.columnId, sortStoreState.direction)
123
+ }, [
124
+ data,
125
+ mode,
126
+ sortStoreState.columnId,
127
+ sortStoreState.direction,
128
+ sortStoreState.sortEnabled,
129
+ ])
130
+
131
+ return {
132
+ ...sortStoreState,
133
+ sortedData,
134
+ toggleSort,
135
+ setSort,
136
+ clearSort,
137
+ }
138
+ }
@@ -0,0 +1,53 @@
1
+ // Components
2
+ export { CellHeader } from './components/cell-header'
3
+ export { Row } from './components/row'
4
+ export { Pagination } from './components/pagination'
5
+ export { PaginationActions } from './components/pagination-actions'
6
+
7
+ // Main components
8
+ export { Table } from './table-ui'
9
+
10
+ // Hooks
11
+ export { usePagination, useSort, useSelection } from './hooks'
12
+
13
+ // Config
14
+ export { tableConfig, tableDownloadConfig } from './config'
15
+
16
+ // Skeleton
17
+ export { TableSkeleton } from './skeleton'
18
+
19
+ // Serializer
20
+ export { sanitizeTableRow, sanitizeTableData } from './serializer'
21
+
22
+ // Helpers
23
+ export { getCellValue, compareValues, sortData, paginateData } from './helpers'
24
+
25
+ // Styles
26
+ export { styles as tableStyles } from './style'
27
+
28
+ // Types
29
+ export type {
30
+ TableUIProps,
31
+ TableColumn,
32
+ TableRow,
33
+ TableWidgetData,
34
+ TablePaginationState,
35
+ SortDirection,
36
+ SortState,
37
+ Mode,
38
+ TableWidgetConfig,
39
+ TableWidgetState,
40
+ TableDownloadConfig,
41
+ TableProps,
42
+ CellHeaderProps,
43
+ RowProps,
44
+ PaginationProps,
45
+ PaginationActionsProps,
46
+ } from './types'
47
+
48
+ export type {
49
+ UsePaginationResult,
50
+ UseSortResult,
51
+ UseSelectionOptions,
52
+ UseSelectionResult,
53
+ } from './hooks'
@@ -0,0 +1,54 @@
1
+ import type { TableRow, TableWidgetData } from './types'
2
+
3
+ /**
4
+ * Sanitizes a table row by ensuring all values are serializable.
5
+ * Converts ReactNode and function values to undefined.
6
+ *
7
+ * @param row - The table row to sanitize
8
+ * @returns A sanitized table row
9
+ */
10
+ export function sanitizeTableRow(row: TableRow): TableRow {
11
+ const sanitized: TableRow = { id: row.id }
12
+
13
+ for (const [key, value] of Object.entries(row)) {
14
+ if (key === 'id') continue
15
+
16
+ // Keep primitive values
17
+ if (
18
+ typeof value === 'string' ||
19
+ typeof value === 'number' ||
20
+ typeof value === 'boolean' ||
21
+ value === null
22
+ ) {
23
+ sanitized[key] = value
24
+ }
25
+ // Serialize arrays and plain objects
26
+ else if (Array.isArray(value) || typeof value === 'object') {
27
+ try {
28
+ // Attempt to serialize - if it fails, the value contains non-serializable content
29
+ JSON.stringify(value)
30
+ sanitized[key] = value
31
+ } catch {
32
+ sanitized[key] = undefined
33
+ }
34
+ }
35
+ // Skip functions and other non-serializable values
36
+ else {
37
+ sanitized[key] = undefined
38
+ }
39
+ }
40
+
41
+ return sanitized
42
+ }
43
+
44
+ /**
45
+ * Sanitizes table widget data by ensuring all rows are serializable.
46
+ *
47
+ * @param data - The table data to sanitize
48
+ * @returns Sanitized table data
49
+ */
50
+ export function sanitizeTableData(
51
+ data: TableWidgetData | undefined,
52
+ ): TableWidgetData | undefined {
53
+ return data?.map(sanitizeTableRow)
54
+ }
@@ -0,0 +1,48 @@
1
+ import {
2
+ Box,
3
+ Skeleton,
4
+ Table,
5
+ TableBody,
6
+ TableCell,
7
+ TableHead,
8
+ TableRow,
9
+ } from '@mui/material'
10
+
11
+ export interface TableSkeletonProps {
12
+ /** Number of columns to display */
13
+ columns?: number
14
+ /** Number of rows to display */
15
+ rows?: number
16
+ }
17
+
18
+ /**
19
+ * Table skeleton loader component
20
+ */
21
+ export function TableSkeleton({ columns = 4, rows = 5 }: TableSkeletonProps) {
22
+ return (
23
+ <Box aria-label='Table skeleton'>
24
+ <Table>
25
+ <TableHead>
26
+ <TableRow>
27
+ {Array.from({ length: columns }).map((_, index) => (
28
+ <TableCell key={index}>
29
+ <Skeleton width='80%' height={24} />
30
+ </TableCell>
31
+ ))}
32
+ </TableRow>
33
+ </TableHead>
34
+ <TableBody>
35
+ {Array.from({ length: rows }).map((_, rowIndex) => (
36
+ <TableRow key={rowIndex}>
37
+ {Array.from({ length: columns }).map((_, colIndex) => (
38
+ <TableCell key={colIndex}>
39
+ <Skeleton width='70%' height={20} />
40
+ </TableCell>
41
+ ))}
42
+ </TableRow>
43
+ ))}
44
+ </TableBody>
45
+ </Table>
46
+ </Box>
47
+ )
48
+ }
@@ -0,0 +1,34 @@
1
+ import type { SxProps, Theme } from '@mui/material'
2
+
3
+ export const styles = {
4
+ container: {
5
+ width: '100%',
6
+ overflow: 'auto',
7
+ },
8
+ table: {
9
+ minWidth: 650,
10
+ },
11
+ headerCell: {
12
+ fontWeight: 600,
13
+ backgroundColor: (theme: Theme) => theme.palette.background.paper,
14
+ },
15
+ row: {
16
+ '&:last-child td, &:last-child th': {
17
+ border: 0,
18
+ },
19
+ },
20
+ rowHover: {
21
+ '&:hover': {
22
+ backgroundColor: (theme: Theme) => theme.palette.action.hover,
23
+ },
24
+ },
25
+ rowSelected: {
26
+ backgroundColor: (theme: Theme) => theme.palette.action.selected,
27
+ '&:hover': {
28
+ backgroundColor: (theme: Theme) => theme.palette.action.selected,
29
+ },
30
+ },
31
+ pagination: {
32
+ borderTop: (theme: Theme) => `1px solid ${theme.palette.divider}`,
33
+ },
34
+ } satisfies Record<string, SxProps<Theme>>
@@ -0,0 +1,64 @@
1
+ import {
2
+ Table as MuiTable,
3
+ TableBody,
4
+ TableCell,
5
+ TableHead,
6
+ TableContainer as MuiTableContainer,
7
+ Paper,
8
+ type TableProps as MuiTableProps,
9
+ } from '@mui/material'
10
+ import type { Ref } from 'react'
11
+ import { CellHeader } from './components/cell-header'
12
+ import { Row } from './components/row'
13
+ import { Pagination } from './components/pagination'
14
+ import { PaginationActions } from './components/pagination-actions'
15
+ import { useWidgetRef } from '../../hooks'
16
+ import type { TableUIProps } from './types'
17
+
18
+ /**
19
+ * Props for the base Table component
20
+ */
21
+ export interface TableComponentProps extends MuiTableProps {
22
+ /** Table element ref */
23
+ ref?: Ref<HTMLTableElement>
24
+ }
25
+
26
+ /**
27
+ * Base Table component with compound component pattern
28
+ * Provides Table.Head, Table.Body, Table.Row, Table.Cell, Table.CellHeader, Table.Pagination
29
+ */
30
+ function Table({
31
+ children,
32
+ ref,
33
+ stickyHeader = true,
34
+ ...props
35
+ }: TableComponentProps) {
36
+ return (
37
+ <MuiTable stickyHeader={stickyHeader} ref={ref} {...props}>
38
+ {children}
39
+ </MuiTable>
40
+ )
41
+ }
42
+
43
+ // Attach sub-components as static properties
44
+ Table.Head = TableHead
45
+ Table.CellHeader = CellHeader
46
+ Table.Body = TableBody
47
+ Table.Row = Row
48
+ Table.Cell = TableCell
49
+ Table.Container = TableContainer
50
+ Table.Paper = Paper
51
+ Table.Pagination = Pagination
52
+ Table.PaginationActions = PaginationActions
53
+
54
+ export { Table }
55
+
56
+ /**
57
+ * TableContainer widget component
58
+ * Integrates with widget store and provides ref registration using custom hook
59
+ */
60
+ function TableContainer(props: TableUIProps) {
61
+ const ref = useWidgetRef<HTMLDivElement>(props.id)
62
+
63
+ return <MuiTableContainer ref={ref}>{props.children}</MuiTableContainer>
64
+ }