@tcn/ui-table 2.2.0 → 2.3.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.
Files changed (217) hide show
  1. package/README.md +1 -1
  2. package/dist/cell.css +1 -0
  3. package/dist/cell.module-WpHnQBVu.js +5 -0
  4. package/dist/cell.module-WpHnQBVu.js.map +1 -0
  5. package/dist/components/cells/data_cell.d.ts +3 -2
  6. package/dist/components/cells/data_cell.d.ts.map +1 -0
  7. package/dist/components/cells/data_cell.js +18 -10
  8. package/dist/components/cells/data_cell.js.map +1 -1
  9. package/dist/components/cells/footer_cell.d.ts +3 -2
  10. package/dist/components/cells/footer_cell.d.ts.map +1 -0
  11. package/dist/components/cells/footer_cell.js +18 -10
  12. package/dist/components/cells/footer_cell.js.map +1 -1
  13. package/dist/components/cells/header_cell.d.ts +3 -2
  14. package/dist/components/cells/header_cell.d.ts.map +1 -0
  15. package/dist/components/cells/header_cell.js +52 -18
  16. package/dist/components/cells/header_cell.js.map +1 -1
  17. package/dist/components/cells/sticky_row_data_cell.d.ts +3 -2
  18. package/dist/components/cells/sticky_row_data_cell.d.ts.map +1 -0
  19. package/dist/components/cells/sticky_row_data_cell.js +26 -11
  20. package/dist/components/cells/sticky_row_data_cell.js.map +1 -1
  21. package/dist/components/cells/sticky_row_fill_cell.d.ts +2 -2
  22. package/dist/components/cells/sticky_row_fill_cell.d.ts.map +1 -0
  23. package/dist/components/cells/sticky_row_fill_cell.js +15 -5
  24. package/dist/components/cells/sticky_row_fill_cell.js.map +1 -1
  25. package/dist/components/global_search.d.ts +2 -2
  26. package/dist/components/global_search.d.ts.map +1 -0
  27. package/dist/components/global_search.js +26 -9
  28. package/dist/components/global_search.js.map +1 -1
  29. package/dist/components/global_search_presenter.d.ts +2 -1
  30. package/dist/components/global_search_presenter.d.ts.map +1 -0
  31. package/dist/components/global_search_presenter.js +20 -18
  32. package/dist/components/global_search_presenter.js.map +1 -1
  33. package/dist/components/table/table.d.ts +3 -2
  34. package/dist/components/table/table.d.ts.map +1 -0
  35. package/dist/components/table/table.js +140 -77
  36. package/dist/components/table/table.js.map +1 -1
  37. package/dist/components/table/table_column.d.ts +1 -1
  38. package/dist/components/table/table_column.d.ts.map +1 -0
  39. package/dist/components/table/table_column.js +6 -5
  40. package/dist/components/table/table_column.js.map +1 -1
  41. package/dist/components/table/table_presenter.d.ts +3 -2
  42. package/dist/components/table/table_presenter.d.ts.map +1 -0
  43. package/dist/components/table/table_presenter.js +45 -62
  44. package/dist/components/table/table_presenter.js.map +1 -1
  45. package/dist/components/table_filter_panel/field_filters/date_field_filter.d.ts +2 -2
  46. package/dist/components/table_filter_panel/field_filters/date_field_filter.d.ts.map +1 -0
  47. package/dist/components/table_filter_panel/field_filters/date_field_filter.js +59 -33
  48. package/dist/components/table_filter_panel/field_filters/date_field_filter.js.map +1 -1
  49. package/dist/components/table_filter_panel/field_filters/date_field_filter_presenter.d.ts +4 -3
  50. package/dist/components/table_filter_panel/field_filters/date_field_filter_presenter.d.ts.map +1 -0
  51. package/dist/components/table_filter_panel/field_filters/date_field_filter_presenter.js +57 -91
  52. package/dist/components/table_filter_panel/field_filters/date_field_filter_presenter.js.map +1 -1
  53. package/dist/components/table_filter_panel/field_filters/field_filter_props.d.ts +1 -0
  54. package/dist/components/table_filter_panel/field_filters/field_filter_props.d.ts.map +1 -0
  55. package/dist/components/table_filter_panel/field_filters/field_filter_strategy.d.ts +1 -0
  56. package/dist/components/table_filter_panel/field_filters/field_filter_strategy.d.ts.map +1 -0
  57. package/dist/components/table_filter_panel/field_filters/mulit_select_field_filter.d.ts +3 -3
  58. package/dist/components/table_filter_panel/field_filters/mulit_select_field_filter.d.ts.map +1 -0
  59. package/dist/components/table_filter_panel/field_filters/mulit_select_field_filter.js +52 -29
  60. package/dist/components/table_filter_panel/field_filters/mulit_select_field_filter.js.map +1 -1
  61. package/dist/components/table_filter_panel/field_filters/multi_select_field_filter_presenter.d.ts +3 -2
  62. package/dist/components/table_filter_panel/field_filters/multi_select_field_filter_presenter.d.ts.map +1 -0
  63. package/dist/components/table_filter_panel/field_filters/multi_select_field_filter_presenter.js +53 -70
  64. package/dist/components/table_filter_panel/field_filters/multi_select_field_filter_presenter.js.map +1 -1
  65. package/dist/components/table_filter_panel/field_filters/number_field_filter.d.ts +3 -3
  66. package/dist/components/table_filter_panel/field_filters/number_field_filter.d.ts.map +1 -0
  67. package/dist/components/table_filter_panel/field_filters/number_field_filter.js +47 -23
  68. package/dist/components/table_filter_panel/field_filters/number_field_filter.js.map +1 -1
  69. package/dist/components/table_filter_panel/field_filters/number_field_filter_presenter.d.ts +5 -4
  70. package/dist/components/table_filter_panel/field_filters/number_field_filter_presenter.d.ts.map +1 -0
  71. package/dist/components/table_filter_panel/field_filters/number_field_filter_presenter.js +53 -58
  72. package/dist/components/table_filter_panel/field_filters/number_field_filter_presenter.js.map +1 -1
  73. package/dist/components/table_filter_panel/field_filters/number_range_field_filter.d.ts +2 -2
  74. package/dist/components/table_filter_panel/field_filters/number_range_field_filter.d.ts.map +1 -0
  75. package/dist/components/table_filter_panel/field_filters/number_range_field_filter.js +61 -31
  76. package/dist/components/table_filter_panel/field_filters/number_range_field_filter.js.map +1 -1
  77. package/dist/components/table_filter_panel/field_filters/number_range_field_filter_presenter.d.ts +4 -3
  78. package/dist/components/table_filter_panel/field_filters/number_range_field_filter_presenter.d.ts.map +1 -0
  79. package/dist/components/table_filter_panel/field_filters/number_range_field_filter_presenter.js +57 -91
  80. package/dist/components/table_filter_panel/field_filters/number_range_field_filter_presenter.js.map +1 -1
  81. package/dist/components/table_filter_panel/field_filters/select_field_filter.d.ts +3 -3
  82. package/dist/components/table_filter_panel/field_filters/select_field_filter.d.ts.map +1 -0
  83. package/dist/components/table_filter_panel/field_filters/select_field_filter.js +49 -24
  84. package/dist/components/table_filter_panel/field_filters/select_field_filter.js.map +1 -1
  85. package/dist/components/table_filter_panel/field_filters/select_field_filter_presenter.d.ts +3 -2
  86. package/dist/components/table_filter_panel/field_filters/select_field_filter_presenter.d.ts.map +1 -0
  87. package/dist/components/table_filter_panel/field_filters/select_field_filter_presenter.js +49 -53
  88. package/dist/components/table_filter_panel/field_filters/select_field_filter_presenter.js.map +1 -1
  89. package/dist/components/table_filter_panel/field_filters/string_field_filter.d.ts +3 -3
  90. package/dist/components/table_filter_panel/field_filters/string_field_filter.d.ts.map +1 -0
  91. package/dist/components/table_filter_panel/field_filters/string_field_filter.js +62 -33
  92. package/dist/components/table_filter_panel/field_filters/string_field_filter.js.map +1 -1
  93. package/dist/components/table_filter_panel/field_filters/string_field_filter_presenter.d.ts +5 -4
  94. package/dist/components/table_filter_panel/field_filters/string_field_filter_presenter.d.ts.map +1 -0
  95. package/dist/components/table_filter_panel/field_filters/string_field_filter_presenter.js +54 -59
  96. package/dist/components/table_filter_panel/field_filters/string_field_filter_presenter.js.map +1 -1
  97. package/dist/components/table_filter_panel/field_filters/use_field_filter_strategy.d.ts +2 -1
  98. package/dist/components/table_filter_panel/field_filters/use_field_filter_strategy.d.ts.map +1 -0
  99. package/dist/components/table_filter_panel/field_filters/use_field_filter_strategy.js +13 -19
  100. package/dist/components/table_filter_panel/field_filters/use_field_filter_strategy.js.map +1 -1
  101. package/dist/components/table_filter_panel/table_filter_panel.d.ts +5 -4
  102. package/dist/components/table_filter_panel/table_filter_panel.d.ts.map +1 -0
  103. package/dist/components/table_filter_panel/table_filter_panel.js +15 -11
  104. package/dist/components/table_filter_panel/table_filter_panel.js.map +1 -1
  105. package/dist/components/table_filter_panel/table_filter_panel_presenter.d.ts +2 -2
  106. package/dist/components/table_filter_panel/table_filter_panel_presenter.d.ts.map +1 -0
  107. package/dist/components/table_filter_panel/table_filter_panel_presenter.js +45 -62
  108. package/dist/components/table_filter_panel/table_filter_panel_presenter.js.map +1 -1
  109. package/dist/components/table_filter_panel/types.d.ts +1 -0
  110. package/dist/components/table_filter_panel/types.d.ts.map +1 -0
  111. package/dist/components/table_filter_panel/types.js +5 -2
  112. package/dist/components/table_filter_panel/types.js.map +1 -1
  113. package/dist/components/table_pager.d.ts +2 -2
  114. package/dist/components/table_pager.d.ts.map +1 -0
  115. package/dist/components/table_pager.js +22 -20
  116. package/dist/components/table_pager.js.map +1 -1
  117. package/dist/index.d.ts +1 -0
  118. package/dist/index.d.ts.map +1 -0
  119. package/dist/index.js +27 -13
  120. package/dist/index.js.map +1 -1
  121. package/dist/table.css +1 -0
  122. package/dist/table_pager.css +1 -0
  123. package/package.json +61 -61
  124. package/src/__stories__/aip_table.stories.tsx +190 -0
  125. package/src/__stories__/auth_provider.tsx +14 -0
  126. package/src/__stories__/demo.stories.tsx +137 -0
  127. package/src/__stories__/sample_data.ts +1398 -0
  128. package/src/__stories__/table.stories.tsx +423 -0
  129. package/src/__tests__/sanity.test.ts +7 -0
  130. package/src/components/cells/data_cell.tsx +25 -0
  131. package/src/components/cells/footer_cell.tsx +25 -0
  132. package/src/components/cells/header_cell.tsx +77 -0
  133. package/src/components/cells/sticky_row_data_cell.tsx +31 -0
  134. package/src/components/cells/sticky_row_fill_cell.tsx +16 -0
  135. package/src/components/global_search.tsx +33 -0
  136. package/src/components/global_search_presenter.ts +24 -0
  137. package/{dist → src}/components/table/table.module.css +3 -2
  138. package/src/components/table/table.tsx +183 -0
  139. package/src/components/table/table_column.tsx +27 -0
  140. package/src/components/table/table_presenter.test.ts +161 -0
  141. package/src/components/table/table_presenter.ts +103 -0
  142. package/src/components/table_filter_panel/field_filters/date_field_filter.tsx +70 -0
  143. package/src/components/table_filter_panel/field_filters/date_field_filter_presenter.test.ts +583 -0
  144. package/src/components/table_filter_panel/field_filters/date_field_filter_presenter.ts +110 -0
  145. package/src/components/table_filter_panel/field_filters/field_filter_props.ts +5 -0
  146. package/src/components/table_filter_panel/field_filters/field_filter_strategy.ts +14 -0
  147. package/src/components/table_filter_panel/field_filters/mulit_select_field_filter.tsx +68 -0
  148. package/src/components/table_filter_panel/field_filters/multi_select_field_filter_presenter.test.ts +444 -0
  149. package/src/components/table_filter_panel/field_filters/multi_select_field_filter_presenter.ts +90 -0
  150. package/src/components/table_filter_panel/field_filters/number_field_filter.tsx +53 -0
  151. package/src/components/table_filter_panel/field_filters/number_field_filter_presenter.test.ts +431 -0
  152. package/src/components/table_filter_panel/field_filters/number_field_filter_presenter.ts +80 -0
  153. package/src/components/table_filter_panel/field_filters/number_range_field_filter.tsx +68 -0
  154. package/src/components/table_filter_panel/field_filters/number_range_field_filter_presenter.test.ts +582 -0
  155. package/src/components/table_filter_panel/field_filters/number_range_field_filter_presenter.ts +110 -0
  156. package/src/components/table_filter_panel/field_filters/select_field_filter.tsx +57 -0
  157. package/src/components/table_filter_panel/field_filters/select_field_filter_presenter.test.ts +365 -0
  158. package/src/components/table_filter_panel/field_filters/select_field_filter_presenter.ts +74 -0
  159. package/src/components/table_filter_panel/field_filters/string_field_filter.tsx +70 -0
  160. package/src/components/table_filter_panel/field_filters/string_field_filter_presenter.test.ts +296 -0
  161. package/src/components/table_filter_panel/field_filters/string_field_filter_presenter.ts +81 -0
  162. package/src/components/table_filter_panel/field_filters/use_field_filter_strategy.tsx +30 -0
  163. package/src/components/table_filter_panel/table_filter_panel.stories.tsx +46 -0
  164. package/src/components/table_filter_panel/table_filter_panel.tsx +26 -0
  165. package/src/components/table_filter_panel/table_filter_panel_presenter.ts +77 -0
  166. package/src/components/table_filter_panel/types.ts +3 -0
  167. package/src/components/table_pager.tsx +39 -0
  168. package/src/index.ts +16 -0
  169. package/tsconfig.json +36 -0
  170. package/types/file_types.d.ts +54 -0
  171. package/types/react_color.d.ts +61 -0
  172. package/dist/__stories__/aip_table.stories.d.ts +0 -5
  173. package/dist/__stories__/aip_table.stories.js +0 -96
  174. package/dist/__stories__/aip_table.stories.js.map +0 -1
  175. package/dist/__stories__/auth_provider.d.ts +0 -4
  176. package/dist/__stories__/auth_provider.js +0 -10
  177. package/dist/__stories__/auth_provider.js.map +0 -1
  178. package/dist/__stories__/demo.stories.d.ts +0 -6
  179. package/dist/__stories__/demo.stories.js +0 -94
  180. package/dist/__stories__/demo.stories.js.map +0 -1
  181. package/dist/__stories__/sample_data.d.ts +0 -36
  182. package/dist/__stories__/sample_data.js +0 -1385
  183. package/dist/__stories__/sample_data.js.map +0 -1
  184. package/dist/__stories__/table.stories.d.ts +0 -12
  185. package/dist/__stories__/table.stories.js +0 -272
  186. package/dist/__stories__/table.stories.js.map +0 -1
  187. package/dist/components/table/table_presenter.test.d.ts +0 -1
  188. package/dist/components/table/table_presenter.test.js +0 -125
  189. package/dist/components/table/table_presenter.test.js.map +0 -1
  190. package/dist/components/table_filter_panel/field_filters/date_field_filter_presenter.test.d.ts +0 -1
  191. package/dist/components/table_filter_panel/field_filters/date_field_filter_presenter.test.js +0 -434
  192. package/dist/components/table_filter_panel/field_filters/date_field_filter_presenter.test.js.map +0 -1
  193. package/dist/components/table_filter_panel/field_filters/field_filter_props.js +0 -2
  194. package/dist/components/table_filter_panel/field_filters/field_filter_props.js.map +0 -1
  195. package/dist/components/table_filter_panel/field_filters/field_filter_strategy.js +0 -2
  196. package/dist/components/table_filter_panel/field_filters/field_filter_strategy.js.map +0 -1
  197. package/dist/components/table_filter_panel/field_filters/multi_select_field_filter_presenter.test.d.ts +0 -1
  198. package/dist/components/table_filter_panel/field_filters/multi_select_field_filter_presenter.test.js +0 -332
  199. package/dist/components/table_filter_panel/field_filters/multi_select_field_filter_presenter.test.js.map +0 -1
  200. package/dist/components/table_filter_panel/field_filters/number_field_filter_presenter.test.d.ts +0 -1
  201. package/dist/components/table_filter_panel/field_filters/number_field_filter_presenter.test.js +0 -347
  202. package/dist/components/table_filter_panel/field_filters/number_field_filter_presenter.test.js.map +0 -1
  203. package/dist/components/table_filter_panel/field_filters/number_range_field_filter_presenter.test.d.ts +0 -1
  204. package/dist/components/table_filter_panel/field_filters/number_range_field_filter_presenter.test.js +0 -452
  205. package/dist/components/table_filter_panel/field_filters/number_range_field_filter_presenter.test.js.map +0 -1
  206. package/dist/components/table_filter_panel/field_filters/select_field_filter_presenter.test.d.ts +0 -1
  207. package/dist/components/table_filter_panel/field_filters/select_field_filter_presenter.test.js +0 -285
  208. package/dist/components/table_filter_panel/field_filters/select_field_filter_presenter.test.js.map +0 -1
  209. package/dist/components/table_filter_panel/field_filters/string_field_filter_presenter.test.d.ts +0 -1
  210. package/dist/components/table_filter_panel/field_filters/string_field_filter_presenter.test.js +0 -232
  211. package/dist/components/table_filter_panel/field_filters/string_field_filter_presenter.test.js.map +0 -1
  212. package/dist/components/table_filter_panel/table_filter_panel.stories.d.ts +0 -6
  213. package/dist/components/table_filter_panel/table_filter_panel.stories.js +0 -25
  214. package/dist/components/table_filter_panel/table_filter_panel.stories.js.map +0 -1
  215. /package/{dist → src}/__stories__/table.module.css +0 -0
  216. /package/{dist → src}/components/cells/cell.module.css +0 -0
  217. /package/{dist → src}/components/table_pager.module.css +0 -0
@@ -0,0 +1,582 @@
1
+ import { Node } from 'clarity-pattern-parser';
2
+ import { NumberRangeFieldFilterPresenter } from './number_range_field_filter_presenter.js';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ describe('NumberRangeFieldFilterPresenter', () => {
6
+ let presenter: NumberRangeFieldFilterPresenter;
7
+ let mockFieldRegistry: any;
8
+
9
+ beforeEach(() => {
10
+ presenter = new NumberRangeFieldFilterPresenter('test_field');
11
+ mockFieldRegistry = {
12
+ registerFieldFilter: vi.fn(),
13
+ unregisterFieldFilter: vi.fn(),
14
+ };
15
+ });
16
+
17
+ describe('constructor', () => {
18
+ it('should initialize with the correct field name', () => {
19
+ const presenter = new NumberRangeFieldFilterPresenter('custom_field');
20
+ expect(presenter.getFilterString()).toBe('');
21
+ });
22
+
23
+ it('should initialize with default values', () => {
24
+ expect(presenter.getFilterString()).toBe('');
25
+ });
26
+ });
27
+
28
+ describe('setMinValue', () => {
29
+ it('should set the minimum value when setMinValue is called with a number', () => {
30
+ presenter.setMinValue(10);
31
+ expect(presenter.getFilterString()).toBe('test_field >= 10');
32
+ });
33
+
34
+ it('should set the minimum value when setMinValue is called with null', () => {
35
+ presenter.setMinValue(10);
36
+ presenter.setMinValue(null);
37
+ expect(presenter.getFilterString()).toBe('');
38
+ });
39
+
40
+ it('should handle zero values', () => {
41
+ presenter.setMinValue(0);
42
+ expect(presenter.getFilterString()).toBe('test_field >= 0');
43
+ });
44
+
45
+ it('should handle negative values', () => {
46
+ presenter.setMinValue(-10);
47
+ expect(presenter.getFilterString()).toBe('test_field >= -10');
48
+ });
49
+
50
+ it('should handle decimal values', () => {
51
+ presenter.setMinValue(3.14);
52
+ expect(presenter.getFilterString()).toBe('test_field >= 3.14');
53
+ });
54
+
55
+ it('should trigger onChange handlers when min value is set', () => {
56
+ const onChangeHandler = vi.fn();
57
+ presenter.onChange(onChangeHandler);
58
+
59
+ presenter.setMinValue(10);
60
+
61
+ expect(onChangeHandler).toHaveBeenCalledTimes(1);
62
+ });
63
+ });
64
+
65
+ describe('setMaxValue', () => {
66
+ it('should set the maximum value when setMaxValue is called with a number', () => {
67
+ presenter.setMaxValue(100);
68
+ expect(presenter.getFilterString()).toBe('test_field <= 100');
69
+ });
70
+
71
+ it('should set the maximum value when setMaxValue is called with null', () => {
72
+ presenter.setMaxValue(100);
73
+ presenter.setMaxValue(null);
74
+ expect(presenter.getFilterString()).toBe('');
75
+ });
76
+
77
+ it('should handle zero values', () => {
78
+ presenter.setMaxValue(0);
79
+ expect(presenter.getFilterString()).toBe('test_field <= 0');
80
+ });
81
+
82
+ it('should handle negative values', () => {
83
+ presenter.setMaxValue(-5);
84
+ expect(presenter.getFilterString()).toBe('test_field <= -5');
85
+ });
86
+
87
+ it('should handle decimal values', () => {
88
+ presenter.setMaxValue(2.718);
89
+ expect(presenter.getFilterString()).toBe('test_field <= 2.718');
90
+ });
91
+
92
+ it('should trigger onChange handlers when max value is set', () => {
93
+ const onChangeHandler = vi.fn();
94
+ presenter.onChange(onChangeHandler);
95
+
96
+ presenter.setMaxValue(100);
97
+
98
+ expect(onChangeHandler).toHaveBeenCalledTimes(1);
99
+ });
100
+ });
101
+
102
+ describe('getFilterString', () => {
103
+ it('should return empty string when both values are null', () => {
104
+ expect(presenter.getFilterString()).toBe('');
105
+ });
106
+
107
+ it('should return min value filter when only min is set', () => {
108
+ presenter.setMinValue(10);
109
+ expect(presenter.getFilterString()).toBe('test_field >= 10');
110
+ });
111
+
112
+ it('should return max value filter when only max is set', () => {
113
+ presenter.setMaxValue(100);
114
+ expect(presenter.getFilterString()).toBe('test_field <= 100');
115
+ });
116
+
117
+ it('should return range filter when both values are set', () => {
118
+ presenter.setMinValue(10);
119
+ presenter.setMaxValue(100);
120
+ expect(presenter.getFilterString()).toBe(
121
+ '(test_field >= 10 AND test_field <= 100)'
122
+ );
123
+ });
124
+
125
+ it('should handle zero values correctly', () => {
126
+ presenter.setMinValue(0);
127
+ presenter.setMaxValue(0);
128
+ expect(presenter.getFilterString()).toBe('(test_field >= 0 AND test_field <= 0)');
129
+ });
130
+
131
+ it('should handle negative values correctly', () => {
132
+ presenter.setMinValue(-100);
133
+ presenter.setMaxValue(-10);
134
+ expect(presenter.getFilterString()).toBe(
135
+ '(test_field >= -100 AND test_field <= -10)'
136
+ );
137
+ });
138
+
139
+ it('should handle decimal values correctly', () => {
140
+ presenter.setMinValue(1.5);
141
+ presenter.setMaxValue(9.9);
142
+ expect(presenter.getFilterString()).toBe(
143
+ '(test_field >= 1.5 AND test_field <= 9.9)'
144
+ );
145
+ });
146
+
147
+ it('should handle large numbers', () => {
148
+ presenter.setMinValue(1000000);
149
+ presenter.setMaxValue(9999999);
150
+ expect(presenter.getFilterString()).toBe(
151
+ '(test_field >= 1000000 AND test_field <= 9999999)'
152
+ );
153
+ });
154
+ });
155
+
156
+ describe('onChange', () => {
157
+ it('should register onChange handler and return unsubscribe function', () => {
158
+ const onChangeHandler = vi.fn();
159
+ const unsubscribe = presenter.onChange(onChangeHandler);
160
+
161
+ presenter.setMinValue(10);
162
+ expect(onChangeHandler).toHaveBeenCalledTimes(1);
163
+
164
+ unsubscribe();
165
+ presenter.setMaxValue(100);
166
+ expect(onChangeHandler).toHaveBeenCalledTimes(1); // Should not be called again
167
+ });
168
+
169
+ it('should handle multiple onChange handlers', () => {
170
+ const handler1 = vi.fn();
171
+ const handler2 = vi.fn();
172
+
173
+ presenter.onChange(handler1);
174
+ presenter.onChange(handler2);
175
+
176
+ presenter.setMinValue(10);
177
+
178
+ expect(handler1).toHaveBeenCalledTimes(1);
179
+ expect(handler2).toHaveBeenCalledTimes(1);
180
+ });
181
+
182
+ it('should handle removing onChange handlers', () => {
183
+ const handler1 = vi.fn();
184
+ const handler2 = vi.fn();
185
+
186
+ const unsubscribe1 = presenter.onChange(handler1);
187
+ presenter.onChange(handler2);
188
+
189
+ unsubscribe1();
190
+ presenter.setMaxValue(100);
191
+
192
+ expect(handler1).not.toHaveBeenCalled();
193
+ expect(handler2).toHaveBeenCalledTimes(1);
194
+ });
195
+
196
+ it('should trigger handlers for both min and max value changes', () => {
197
+ const onChangeHandler = vi.fn();
198
+ presenter.onChange(onChangeHandler);
199
+
200
+ presenter.setMinValue(10);
201
+ presenter.setMaxValue(100);
202
+
203
+ expect(onChangeHandler).toHaveBeenCalledTimes(2);
204
+ });
205
+ });
206
+
207
+ describe('setFieldRegistry', () => {
208
+ it('should register the field filter with the registry', () => {
209
+ presenter.setFieldRegistry(mockFieldRegistry);
210
+
211
+ expect(mockFieldRegistry.registerFieldFilter).toHaveBeenCalledWith(
212
+ 'test_field',
213
+ presenter
214
+ );
215
+ });
216
+
217
+ it('should store the field registry reference', () => {
218
+ presenter.setFieldRegistry(mockFieldRegistry);
219
+
220
+ // Test that dispose can call unregisterFieldFilter
221
+ presenter.dispose();
222
+ expect(mockFieldRegistry.unregisterFieldFilter).toHaveBeenCalledWith('test_field');
223
+ });
224
+ });
225
+
226
+ describe('setFilterState', () => {
227
+ it('should parse single min value filter from AST', () => {
228
+ const mockAst = {
229
+ findAll: vi.fn().mockReturnValue([
230
+ {
231
+ name: 'plain-field',
232
+ value: 'test_field',
233
+ parent: {
234
+ name: 'infix-expression',
235
+ children: [
236
+ { name: 'plain-field', value: 'test_field' },
237
+ { name: 'operator', value: '>=' },
238
+ { name: 'number', value: '10' },
239
+ ],
240
+ },
241
+ },
242
+ ]),
243
+ } as unknown as Node;
244
+
245
+ presenter.setFilterState(mockAst);
246
+
247
+ expect(presenter.getFilterString()).toBe('test_field >= 10');
248
+ });
249
+
250
+ it('should parse single max value filter from AST', () => {
251
+ const mockAst = {
252
+ findAll: vi.fn().mockReturnValue([
253
+ {
254
+ name: 'plain-field',
255
+ value: 'test_field',
256
+ parent: {
257
+ name: 'infix-expression',
258
+ children: [
259
+ { name: 'plain-field', value: 'test_field' },
260
+ { name: 'operator', value: '<=' },
261
+ { name: 'number', value: '100' },
262
+ ],
263
+ },
264
+ },
265
+ ]),
266
+ } as unknown as Node;
267
+
268
+ presenter.setFilterState(mockAst);
269
+
270
+ expect(presenter.getFilterString()).toBe('test_field <= 100');
271
+ });
272
+
273
+ it('should parse range filter with min >= and max <= from AST', () => {
274
+ const mockAst = {
275
+ findAll: vi.fn().mockReturnValue([
276
+ {
277
+ name: 'plain-field',
278
+ value: 'test_field',
279
+ parent: {
280
+ name: 'infix-expression',
281
+ children: [
282
+ { name: 'plain-field', value: 'test_field' },
283
+ { name: 'operator', value: '>=' },
284
+ { name: 'number', value: '10' },
285
+ ],
286
+ },
287
+ },
288
+ {
289
+ name: 'plain-field',
290
+ value: 'test_field',
291
+ parent: {
292
+ name: 'infix-expression',
293
+ children: [
294
+ { name: 'plain-field', value: 'test_field' },
295
+ { name: 'operator', value: '<=' },
296
+ { name: 'number', value: '100' },
297
+ ],
298
+ },
299
+ },
300
+ ]),
301
+ } as unknown as Node;
302
+
303
+ presenter.setFilterState(mockAst);
304
+
305
+ expect(presenter.getFilterString()).toBe(
306
+ '(test_field >= 10 AND test_field <= 100)'
307
+ );
308
+ });
309
+
310
+ it('should parse range filter with max <= and min >= from AST (reversed order)', () => {
311
+ const mockAst = {
312
+ findAll: vi.fn().mockReturnValue([
313
+ {
314
+ name: 'plain-field',
315
+ value: 'test_field',
316
+ parent: {
317
+ name: 'infix-expression',
318
+ children: [
319
+ { name: 'plain-field', value: 'test_field' },
320
+ { name: 'operator', value: '<=' },
321
+ { name: 'number', value: '100' },
322
+ ],
323
+ },
324
+ },
325
+ {
326
+ name: 'plain-field',
327
+ value: 'test_field',
328
+ parent: {
329
+ name: 'infix-expression',
330
+ children: [
331
+ { name: 'plain-field', value: 'test_field' },
332
+ { name: 'operator', value: '>=' },
333
+ { name: 'number', value: '10' },
334
+ ],
335
+ },
336
+ },
337
+ ]),
338
+ } as unknown as Node;
339
+
340
+ presenter.setFilterState(mockAst);
341
+
342
+ expect(presenter.getFilterString()).toBe(
343
+ '(test_field >= 10 AND test_field <= 100)'
344
+ );
345
+ });
346
+
347
+ it('should handle case when field is not found in AST', () => {
348
+ const mockAst = {
349
+ findAll: vi.fn().mockReturnValue([]),
350
+ } as unknown as Node;
351
+
352
+ presenter.setMinValue(10);
353
+ presenter.setMaxValue(100);
354
+
355
+ presenter.setFilterState(mockAst);
356
+
357
+ // Should not change existing state
358
+ expect(presenter.getFilterString()).toBe(
359
+ '(test_field >= 10 AND test_field <= 100)'
360
+ );
361
+ });
362
+
363
+ it('should handle case when field node has no parent', () => {
364
+ const mockAst = {
365
+ findAll: vi.fn().mockReturnValue([
366
+ {
367
+ name: 'plain-field',
368
+ value: 'test_field',
369
+ parent: null,
370
+ },
371
+ ]),
372
+ } as unknown as Node;
373
+
374
+ presenter.setMinValue(10);
375
+ presenter.setMaxValue(100);
376
+
377
+ presenter.setFilterState(mockAst);
378
+
379
+ // Should not change existing state
380
+ expect(presenter.getFilterString()).toBe(
381
+ '(test_field >= 10 AND test_field <= 100)'
382
+ );
383
+ });
384
+
385
+ it('should handle decimal values from AST', () => {
386
+ const mockAst = {
387
+ findAll: vi.fn().mockReturnValue([
388
+ {
389
+ name: 'plain-field',
390
+ value: 'test_field',
391
+ parent: {
392
+ name: 'infix-expression',
393
+ children: [
394
+ { name: 'plain-field', value: 'test_field' },
395
+ { name: 'operator', value: '>=' },
396
+ { name: 'number', value: '1.5' },
397
+ ],
398
+ },
399
+ },
400
+ {
401
+ name: 'plain-field',
402
+ value: 'test_field',
403
+ parent: {
404
+ name: 'infix-expression',
405
+ children: [
406
+ { name: 'plain-field', value: 'test_field' },
407
+ { name: 'operator', value: '<=' },
408
+ { name: 'number', value: '9.9' },
409
+ ],
410
+ },
411
+ },
412
+ ]),
413
+ } as unknown as Node;
414
+
415
+ presenter.setFilterState(mockAst);
416
+
417
+ expect(presenter.getFilterString()).toBe(
418
+ '(test_field >= 1.5 AND test_field <= 9.9)'
419
+ );
420
+ });
421
+
422
+ it('should handle negative values from AST', () => {
423
+ const mockAst = {
424
+ findAll: vi.fn().mockReturnValue([
425
+ {
426
+ name: 'plain-field',
427
+ value: 'test_field',
428
+ parent: {
429
+ name: 'infix-expression',
430
+ children: [
431
+ { name: 'plain-field', value: 'test_field' },
432
+ { name: 'operator', value: '>=' },
433
+ { name: 'number', value: '-100' },
434
+ ],
435
+ },
436
+ },
437
+ {
438
+ name: 'plain-field',
439
+ value: 'test_field',
440
+ parent: {
441
+ name: 'infix-expression',
442
+ children: [
443
+ { name: 'plain-field', value: 'test_field' },
444
+ { name: 'operator', value: '<=' },
445
+ { name: 'number', value: '-10' },
446
+ ],
447
+ },
448
+ },
449
+ ]),
450
+ } as unknown as Node;
451
+
452
+ presenter.setFilterState(mockAst);
453
+
454
+ expect(presenter.getFilterString()).toBe(
455
+ '(test_field >= -100 AND test_field <= -10)'
456
+ );
457
+ });
458
+
459
+ it('should ignore non-range operators', () => {
460
+ const mockAst = {
461
+ findAll: vi.fn().mockReturnValue([
462
+ {
463
+ name: 'plain-field',
464
+ value: 'test_field',
465
+ parent: {
466
+ name: 'infix-expression',
467
+ children: [
468
+ { name: 'plain-field', value: 'test_field' },
469
+ { name: 'operator', value: '=' },
470
+ { name: 'number', value: '42' },
471
+ ],
472
+ },
473
+ },
474
+ ]),
475
+ } as unknown as Node;
476
+
477
+ presenter.setFilterState(mockAst);
478
+
479
+ // Should not change existing state
480
+ expect(presenter.getFilterString()).toBe('');
481
+ });
482
+ });
483
+
484
+ describe('dispose', () => {
485
+ it('should unregister field filter from registry when registry is set', () => {
486
+ presenter.setFieldRegistry(mockFieldRegistry);
487
+ presenter.dispose();
488
+
489
+ expect(mockFieldRegistry.unregisterFieldFilter).toHaveBeenCalledWith('test_field');
490
+ });
491
+
492
+ it('should not call unregisterFieldFilter when no registry is set', () => {
493
+ presenter.dispose();
494
+
495
+ expect(mockFieldRegistry.unregisterFieldFilter).not.toHaveBeenCalled();
496
+ });
497
+
498
+ it('should clear onChange handlers', () => {
499
+ const onChangeHandler = vi.fn();
500
+ presenter.onChange(onChangeHandler);
501
+
502
+ presenter.dispose();
503
+ presenter.setMinValue(10);
504
+
505
+ expect(onChangeHandler).not.toHaveBeenCalled();
506
+ });
507
+
508
+ it('should clear field registry reference', () => {
509
+ presenter.setFieldRegistry(mockFieldRegistry);
510
+ presenter.dispose();
511
+
512
+ // Should not call unregisterFieldFilter again
513
+ presenter.dispose();
514
+ expect(mockFieldRegistry.unregisterFieldFilter).toHaveBeenCalledTimes(1);
515
+ });
516
+ });
517
+
518
+ describe('edge cases and error handling', () => {
519
+ it('should handle rapid successive value changes', () => {
520
+ const onChangeHandler = vi.fn();
521
+ presenter.onChange(onChangeHandler);
522
+
523
+ presenter.setMinValue(10);
524
+ presenter.setMaxValue(100);
525
+ presenter.setMinValue(20);
526
+ presenter.setMaxValue(200);
527
+
528
+ expect(onChangeHandler).toHaveBeenCalledTimes(4);
529
+ expect(presenter.getFilterString()).toBe(
530
+ '(test_field >= 20 AND test_field <= 200)'
531
+ );
532
+ });
533
+
534
+ it('should handle very large numbers', () => {
535
+ presenter.setMinValue(Number.MAX_SAFE_INTEGER);
536
+ presenter.setMaxValue(Number.MAX_SAFE_INTEGER);
537
+ expect(presenter.getFilterString()).toBe(
538
+ `(test_field >= ${Number.MAX_SAFE_INTEGER} AND test_field <= ${Number.MAX_SAFE_INTEGER})`
539
+ );
540
+ });
541
+
542
+ it('should handle very small numbers', () => {
543
+ presenter.setMinValue(Number.MIN_SAFE_INTEGER);
544
+ presenter.setMaxValue(Number.MIN_SAFE_INTEGER);
545
+ expect(presenter.getFilterString()).toBe(
546
+ `(test_field >= ${Number.MIN_SAFE_INTEGER} AND test_field <= ${Number.MIN_SAFE_INTEGER})`
547
+ );
548
+ });
549
+
550
+ it('should handle NaN values gracefully', () => {
551
+ presenter.setMinValue(NaN);
552
+ presenter.setMaxValue(NaN);
553
+ expect(presenter.getFilterString()).toBe(
554
+ '(test_field >= NaN AND test_field <= NaN)'
555
+ );
556
+ });
557
+
558
+ it('should handle Infinity values', () => {
559
+ presenter.setMinValue(Infinity);
560
+ presenter.setMaxValue(Infinity);
561
+ expect(presenter.getFilterString()).toBe(
562
+ '(test_field >= Infinity AND test_field <= Infinity)'
563
+ );
564
+ });
565
+
566
+ it('should handle negative Infinity values', () => {
567
+ presenter.setMinValue(-Infinity);
568
+ presenter.setMaxValue(-Infinity);
569
+ expect(presenter.getFilterString()).toBe(
570
+ '(test_field >= -Infinity AND test_field <= -Infinity)'
571
+ );
572
+ });
573
+
574
+ it('should handle mixed edge cases', () => {
575
+ presenter.setMinValue(-Infinity);
576
+ presenter.setMaxValue(Infinity);
577
+ expect(presenter.getFilterString()).toBe(
578
+ '(test_field >= -Infinity AND test_field <= Infinity)'
579
+ );
580
+ });
581
+ });
582
+ });
@@ -0,0 +1,110 @@
1
+ import { Signal } from '@tcn/state';
2
+ import { Node } from 'clarity-pattern-parser';
3
+ import { FieldFilterRegistry, FieldFilterStrategy } from './field_filter_strategy.js';
4
+
5
+ export class NumberRangeFieldFilterPresenter implements FieldFilterStrategy {
6
+ private _fieldName: string;
7
+ private _fieldFilterRegistry: FieldFilterRegistry | null = null;
8
+ private _minValue = new Signal<number | null>(null);
9
+ private _maxValue = new Signal<number | null>(null);
10
+ private _onChangeHandlers = new Set<() => void>();
11
+
12
+ private _broadcasts = {
13
+ minValue: this._minValue.broadcast,
14
+ maxValue: this._maxValue.broadcast,
15
+ };
16
+
17
+ constructor(fieldName: string) {
18
+ this._fieldName = fieldName;
19
+ }
20
+
21
+ get broadcasts() {
22
+ return this._broadcasts;
23
+ }
24
+
25
+ getFilterString() {
26
+ const minValue = this._minValue.get();
27
+ const maxValue = this._maxValue.get();
28
+ if (minValue !== null && maxValue !== null) {
29
+ return `(${this._fieldName} >= ${minValue} AND ${this._fieldName} <= ${maxValue})`;
30
+ } else if (minValue !== null) {
31
+ return `${this._fieldName} >= ${minValue}`;
32
+ } else if (maxValue !== null) {
33
+ return `${this._fieldName} <= ${maxValue}`;
34
+ }
35
+ return '';
36
+ }
37
+
38
+ onChange(handler: () => void) {
39
+ this._onChangeHandlers.add(handler);
40
+ return () => {
41
+ this._onChangeHandlers.delete(handler);
42
+ };
43
+ }
44
+
45
+ setMinValue(minValue: number | null) {
46
+ this._minValue.set(minValue);
47
+ this._onChangeHandlers.forEach(handler => handler());
48
+ }
49
+
50
+ setMaxValue(maxValue: number | null) {
51
+ this._maxValue.set(maxValue);
52
+ this._onChangeHandlers.forEach(handler => handler());
53
+ }
54
+
55
+ setFieldRegistry(fieldRegistry: FieldFilterRegistry) {
56
+ this._fieldFilterRegistry = fieldRegistry;
57
+ fieldRegistry.registerFieldFilter(this._fieldName, this);
58
+ }
59
+
60
+ setFilterState(filterAst: Node) {
61
+ const fieldNodes = filterAst.findAll(
62
+ (n: Node) => n.name === 'plain-field' && n.value === this._fieldName
63
+ );
64
+
65
+ if (fieldNodes.length === 0) {
66
+ return;
67
+ }
68
+
69
+ if (fieldNodes.length === 1) {
70
+ const operatorNode = fieldNodes[0].parent?.children[1];
71
+ const valueNode = fieldNodes[0].parent?.children[2];
72
+ if (operatorNode?.value === '>=' && valueNode != null) {
73
+ this.setMinValue(Number(valueNode?.value));
74
+ } else if (operatorNode?.value === '<=' && valueNode != null) {
75
+ this.setMaxValue(Number(valueNode?.value));
76
+ }
77
+ return;
78
+ }
79
+
80
+ if (fieldNodes.length === 2) {
81
+ const operatorOneNode = fieldNodes[0].parent?.children[1];
82
+ const operatorTwoNode = fieldNodes[1].parent?.children[1];
83
+ const valueOneNode = fieldNodes[0].parent?.children[2];
84
+ const valueTwoNode = fieldNodes[1].parent?.children[2];
85
+ if (
86
+ operatorOneNode?.value === '>=' &&
87
+ operatorTwoNode?.value === '<=' &&
88
+ valueOneNode != null &&
89
+ valueTwoNode != null
90
+ ) {
91
+ this.setMinValue(Number(valueOneNode?.value));
92
+ this.setMaxValue(Number(valueTwoNode?.value));
93
+ } else if (
94
+ operatorOneNode?.value === '<=' &&
95
+ operatorTwoNode?.value === '>=' &&
96
+ valueOneNode != null &&
97
+ valueTwoNode != null
98
+ ) {
99
+ this.setMaxValue(Number(valueOneNode?.value));
100
+ this.setMinValue(Number(valueTwoNode?.value));
101
+ }
102
+ }
103
+ }
104
+
105
+ dispose() {
106
+ this._fieldFilterRegistry?.unregisterFieldFilter(this._fieldName);
107
+ this._fieldFilterRegistry = null;
108
+ this._onChangeHandlers.clear();
109
+ }
110
+ }