@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,431 @@
1
+ import { Node } from 'clarity-pattern-parser';
2
+ import { NumberFieldFilterPresenter } from './number_field_filter_presenter.js';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ describe('NumberFieldFilterPresenter', () => {
6
+ let presenter: NumberFieldFilterPresenter;
7
+ let mockFieldRegistry: any;
8
+
9
+ beforeEach(() => {
10
+ presenter = new NumberFieldFilterPresenter('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 NumberFieldFilterPresenter('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('setValue', () => {
29
+ it('should set the value when setValue is called with a number', () => {
30
+ presenter.setValue(42);
31
+ presenter.setOperator('=');
32
+ expect(presenter.getFilterString()).toBe('test_field = 42');
33
+ });
34
+
35
+ it('should set the value when setValue is called with null', () => {
36
+ presenter.setValue(42);
37
+ presenter.setValue(null);
38
+ expect(presenter.getFilterString()).toBe('');
39
+ });
40
+
41
+ it('should handle zero values', () => {
42
+ presenter.setValue(0);
43
+ presenter.setOperator('=');
44
+ expect(presenter.getFilterString()).toBe('test_field = 0');
45
+ });
46
+
47
+ it('should handle negative values', () => {
48
+ presenter.setValue(-42);
49
+ presenter.setOperator('>');
50
+ expect(presenter.getFilterString()).toBe('test_field > -42');
51
+ });
52
+
53
+ it('should handle decimal values', () => {
54
+ presenter.setValue(3.14);
55
+ presenter.setOperator('>=');
56
+ expect(presenter.getFilterString()).toBe('test_field >= 3.14');
57
+ });
58
+
59
+ it('should trigger onChange handlers when value is set', () => {
60
+ const onChangeHandler = vi.fn();
61
+ presenter.onChange(onChangeHandler);
62
+
63
+ presenter.setValue(42);
64
+
65
+ expect(onChangeHandler).toHaveBeenCalledTimes(1);
66
+ });
67
+ });
68
+
69
+ describe('setOperator', () => {
70
+ it('should set the operator when setOperator is called', () => {
71
+ presenter.setValue(42);
72
+ presenter.setOperator('>');
73
+ expect(presenter.getFilterString()).toBe('test_field > 42');
74
+ });
75
+
76
+ it('should work with all comparison operators', () => {
77
+ const operators = ['>=', '>', '<=', '<', '=', ':', '!='] as const;
78
+ presenter.setValue(42);
79
+
80
+ operators.forEach(operator => {
81
+ presenter.setOperator(operator);
82
+ expect(presenter.getFilterString()).toBe(`test_field ${operator} 42`);
83
+ });
84
+ });
85
+
86
+ it('should trigger onChange handlers when operator is set', () => {
87
+ const onChangeHandler = vi.fn();
88
+ presenter.onChange(onChangeHandler);
89
+
90
+ presenter.setOperator('>');
91
+
92
+ expect(onChangeHandler).toHaveBeenCalledTimes(1);
93
+ });
94
+ });
95
+
96
+ describe('getFilterString', () => {
97
+ it('should return empty string when value is null', () => {
98
+ expect(presenter.getFilterString()).toBe('');
99
+ });
100
+
101
+ it('should return empty string when value is set to null', () => {
102
+ presenter.setValue(42);
103
+ presenter.setValue(null);
104
+ expect(presenter.getFilterString()).toBe('');
105
+ });
106
+
107
+ it('should return formatted filter string with value and operator', () => {
108
+ presenter.setValue(42);
109
+ presenter.setOperator('=');
110
+ expect(presenter.getFilterString()).toBe('test_field = 42');
111
+ });
112
+
113
+ it('should handle zero values correctly', () => {
114
+ presenter.setValue(0);
115
+ presenter.setOperator('=');
116
+ expect(presenter.getFilterString()).toBe('test_field = 0');
117
+ });
118
+
119
+ it('should handle negative values correctly', () => {
120
+ presenter.setValue(-42);
121
+ presenter.setOperator('>');
122
+ expect(presenter.getFilterString()).toBe('test_field > -42');
123
+ });
124
+
125
+ it('should handle decimal values correctly', () => {
126
+ presenter.setValue(3.14159);
127
+ presenter.setOperator('>=');
128
+ expect(presenter.getFilterString()).toBe('test_field >= 3.14159');
129
+ });
130
+
131
+ it('should handle large numbers', () => {
132
+ presenter.setValue(1000000);
133
+ presenter.setOperator('<');
134
+ expect(presenter.getFilterString()).toBe('test_field < 1000000');
135
+ });
136
+ });
137
+
138
+ describe('onChange', () => {
139
+ it('should register onChange handler and return unsubscribe function', () => {
140
+ const onChangeHandler = vi.fn();
141
+ const unsubscribe = presenter.onChange(onChangeHandler);
142
+
143
+ presenter.setValue(42);
144
+ expect(onChangeHandler).toHaveBeenCalledTimes(1);
145
+
146
+ unsubscribe();
147
+ presenter.setValue(100);
148
+ expect(onChangeHandler).toHaveBeenCalledTimes(1); // Should not be called again
149
+ });
150
+
151
+ it('should handle multiple onChange handlers', () => {
152
+ const handler1 = vi.fn();
153
+ const handler2 = vi.fn();
154
+
155
+ presenter.onChange(handler1);
156
+ presenter.onChange(handler2);
157
+
158
+ presenter.setValue(42);
159
+
160
+ expect(handler1).toHaveBeenCalledTimes(1);
161
+ expect(handler2).toHaveBeenCalledTimes(1);
162
+ });
163
+
164
+ it('should handle removing onChange handlers', () => {
165
+ const handler1 = vi.fn();
166
+ const handler2 = vi.fn();
167
+
168
+ const unsubscribe1 = presenter.onChange(handler1);
169
+ presenter.onChange(handler2);
170
+
171
+ unsubscribe1();
172
+ presenter.setValue(42);
173
+
174
+ expect(handler1).not.toHaveBeenCalled();
175
+ expect(handler2).toHaveBeenCalledTimes(1);
176
+ });
177
+ });
178
+
179
+ describe('setFieldRegistry', () => {
180
+ it('should register the field filter with the registry', () => {
181
+ presenter.setFieldRegistry(mockFieldRegistry);
182
+
183
+ expect(mockFieldRegistry.registerFieldFilter).toHaveBeenCalledWith(
184
+ 'test_field',
185
+ presenter
186
+ );
187
+ });
188
+
189
+ it('should store the field registry reference', () => {
190
+ presenter.setFieldRegistry(mockFieldRegistry);
191
+
192
+ // Test that dispose can call unregisterFieldFilter
193
+ presenter.dispose();
194
+ expect(mockFieldRegistry.unregisterFieldFilter).toHaveBeenCalledWith('test_field');
195
+ });
196
+ });
197
+
198
+ describe('setFilterState', () => {
199
+ it('should parse filter AST and set operator and value', () => {
200
+ // Create a mock AST node structure
201
+ const mockAst = {
202
+ findAll: vi.fn().mockReturnValue([
203
+ {
204
+ name: 'plain-field',
205
+ value: 'test_field',
206
+ parent: {
207
+ name: 'infix-expression',
208
+ children: [
209
+ { name: 'plain-field', value: 'test_field' },
210
+ { name: 'operator', value: '=' },
211
+ { name: 'number', value: '42' },
212
+ ],
213
+ },
214
+ },
215
+ ]),
216
+ } as unknown as Node;
217
+
218
+ presenter.setFilterState(mockAst);
219
+
220
+ expect(presenter.getFilterString()).toBe('test_field = 42');
221
+ });
222
+
223
+ it('should handle case when field is not found in AST', () => {
224
+ const mockAst = {
225
+ findAll: vi.fn().mockReturnValue([]),
226
+ } as unknown as Node;
227
+
228
+ presenter.setValue(42);
229
+ presenter.setOperator('>');
230
+
231
+ presenter.setFilterState(mockAst);
232
+
233
+ // Should not change existing state
234
+ expect(presenter.getFilterString()).toBe('test_field > 42');
235
+ });
236
+
237
+ it('should handle case when field node has no parent', () => {
238
+ const mockAst = {
239
+ findAll: vi.fn().mockReturnValue([
240
+ {
241
+ name: 'plain-field',
242
+ value: 'test_field',
243
+ parent: null,
244
+ },
245
+ ]),
246
+ } as unknown as Node;
247
+
248
+ presenter.setValue(42);
249
+ presenter.setOperator('>');
250
+
251
+ presenter.setFilterState(mockAst);
252
+
253
+ // Should not change existing state
254
+ expect(presenter.getFilterString()).toBe('test_field > 42');
255
+ });
256
+
257
+ it('should parse numeric values from AST', () => {
258
+ const mockAst = {
259
+ findAll: vi.fn().mockReturnValue([
260
+ {
261
+ name: 'plain-field',
262
+ value: 'test_field',
263
+ parent: {
264
+ name: 'infix-expression',
265
+ children: [
266
+ { name: 'plain-field', value: 'test_field' },
267
+ { name: 'operator', value: '!=' },
268
+ { name: 'number', value: '100' },
269
+ ],
270
+ },
271
+ },
272
+ ]),
273
+ } as unknown as Node;
274
+
275
+ presenter.setFilterState(mockAst);
276
+
277
+ expect(presenter.getFilterString()).toBe('test_field != 100');
278
+ });
279
+
280
+ it('should handle decimal values from AST', () => {
281
+ const mockAst = {
282
+ findAll: vi.fn().mockReturnValue([
283
+ {
284
+ name: 'plain-field',
285
+ value: 'test_field',
286
+ parent: {
287
+ name: 'infix-expression',
288
+ children: [
289
+ { name: 'plain-field', value: 'test_field' },
290
+ { name: 'operator', value: '>=' },
291
+ { name: 'number', value: '3.14' },
292
+ ],
293
+ },
294
+ },
295
+ ]),
296
+ } as unknown as Node;
297
+
298
+ presenter.setFilterState(mockAst);
299
+
300
+ expect(presenter.getFilterString()).toBe('test_field >= 3.14');
301
+ });
302
+
303
+ it('should handle negative values from AST', () => {
304
+ const mockAst = {
305
+ findAll: vi.fn().mockReturnValue([
306
+ {
307
+ name: 'plain-field',
308
+ value: 'test_field',
309
+ parent: {
310
+ name: 'infix-expression',
311
+ children: [
312
+ { name: 'plain-field', value: 'test_field' },
313
+ { name: 'operator', value: '<' },
314
+ { name: 'number', value: '-42' },
315
+ ],
316
+ },
317
+ },
318
+ ]),
319
+ } as unknown as Node;
320
+
321
+ presenter.setFilterState(mockAst);
322
+
323
+ expect(presenter.getFilterString()).toBe('test_field < -42');
324
+ });
325
+
326
+ it('should handle zero values from AST', () => {
327
+ const mockAst = {
328
+ findAll: vi.fn().mockReturnValue([
329
+ {
330
+ name: 'plain-field',
331
+ value: 'test_field',
332
+ parent: {
333
+ name: 'infix-expression',
334
+ children: [
335
+ { name: 'plain-field', value: 'test_field' },
336
+ { name: 'operator', value: '=' },
337
+ { name: 'number', value: '0' },
338
+ ],
339
+ },
340
+ },
341
+ ]),
342
+ } as unknown as Node;
343
+
344
+ presenter.setFilterState(mockAst);
345
+
346
+ expect(presenter.getFilterString()).toBe('test_field = 0');
347
+ });
348
+ });
349
+
350
+ describe('dispose', () => {
351
+ it('should unregister field filter from registry when registry is set', () => {
352
+ presenter.setFieldRegistry(mockFieldRegistry);
353
+ presenter.dispose();
354
+
355
+ expect(mockFieldRegistry.unregisterFieldFilter).toHaveBeenCalledWith('test_field');
356
+ });
357
+
358
+ it('should not call unregisterFieldFilter when no registry is set', () => {
359
+ presenter.dispose();
360
+
361
+ expect(mockFieldRegistry.unregisterFieldFilter).not.toHaveBeenCalled();
362
+ });
363
+
364
+ it('should clear onChange handlers', () => {
365
+ const onChangeHandler = vi.fn();
366
+ presenter.onChange(onChangeHandler);
367
+
368
+ presenter.dispose();
369
+ presenter.setValue(42);
370
+
371
+ expect(onChangeHandler).not.toHaveBeenCalled();
372
+ });
373
+ });
374
+
375
+ describe('edge cases and error handling', () => {
376
+ it('should handle rapid successive value changes', () => {
377
+ const onChangeHandler = vi.fn();
378
+ presenter.onChange(onChangeHandler);
379
+
380
+ presenter.setValue(10);
381
+ presenter.setValue(20);
382
+ presenter.setValue(30);
383
+
384
+ expect(onChangeHandler).toHaveBeenCalledTimes(3);
385
+ expect(presenter.getFilterString()).toBe('test_field = 30');
386
+ });
387
+
388
+ it('should handle rapid successive operator changes', () => {
389
+ const onChangeHandler = vi.fn();
390
+ presenter.onChange(onChangeHandler);
391
+
392
+ presenter.setValue(42);
393
+ presenter.setOperator('=');
394
+ presenter.setOperator('>');
395
+ presenter.setOperator('!=');
396
+
397
+ expect(onChangeHandler).toHaveBeenCalledTimes(4); // 1 for value + 3 for operators
398
+ expect(presenter.getFilterString()).toBe('test_field != 42');
399
+ });
400
+
401
+ it('should handle very large numbers', () => {
402
+ presenter.setValue(Number.MAX_SAFE_INTEGER);
403
+ presenter.setOperator('>');
404
+ expect(presenter.getFilterString()).toBe(`test_field > ${Number.MAX_SAFE_INTEGER}`);
405
+ });
406
+
407
+ it('should handle very small numbers', () => {
408
+ presenter.setValue(Number.MIN_SAFE_INTEGER);
409
+ presenter.setOperator('<');
410
+ expect(presenter.getFilterString()).toBe(`test_field < ${Number.MIN_SAFE_INTEGER}`);
411
+ });
412
+
413
+ it('should handle NaN values gracefully', () => {
414
+ presenter.setValue(NaN);
415
+ presenter.setOperator('=');
416
+ expect(presenter.getFilterString()).toBe('test_field = NaN');
417
+ });
418
+
419
+ it('should handle Infinity values', () => {
420
+ presenter.setValue(Infinity);
421
+ presenter.setOperator('>');
422
+ expect(presenter.getFilterString()).toBe('test_field > Infinity');
423
+ });
424
+
425
+ it('should handle negative Infinity values', () => {
426
+ presenter.setValue(-Infinity);
427
+ presenter.setOperator('<');
428
+ expect(presenter.getFilterString()).toBe('test_field < -Infinity');
429
+ });
430
+ });
431
+ });
@@ -0,0 +1,80 @@
1
+ import { Signal } from '@tcn/state';
2
+ import { Node } from 'clarity-pattern-parser';
3
+ import { FieldFilterRegistry, FieldFilterStrategy } from './field_filter_strategy.js';
4
+ import { ComparisonOperator } from '../types.js';
5
+
6
+ export class NumberFieldFilterPresenter implements FieldFilterStrategy {
7
+ private _fieldName: string;
8
+ private _value = new Signal<number | null>(null);
9
+ private _operator = new Signal<ComparisonOperator>('=');
10
+ private _fieldFilterRegistry: FieldFilterRegistry | null = null;
11
+ private _onChangeHandlers = new Set<() => void>();
12
+
13
+ private _broadcasts = {
14
+ value: this._value.broadcast,
15
+ operator: this._operator.broadcast,
16
+ };
17
+
18
+ constructor(fieldName: string) {
19
+ this._fieldName = fieldName;
20
+ }
21
+
22
+ get broadcasts() {
23
+ return this._broadcasts;
24
+ }
25
+
26
+ onChange(handler: () => void) {
27
+ this._onChangeHandlers.add(handler);
28
+ return () => {
29
+ this._onChangeHandlers.delete(handler);
30
+ };
31
+ }
32
+
33
+ setValue(value: number | null) {
34
+ this._value.set(value);
35
+ this._onChangeHandlers.forEach(handler => handler());
36
+ }
37
+
38
+ setOperator(operator: ComparisonOperator) {
39
+ this._operator.set(operator);
40
+ this._onChangeHandlers.forEach(handler => handler());
41
+ }
42
+
43
+ getFilterString() {
44
+ if (this._value.get() === null) {
45
+ return '';
46
+ }
47
+ return `${this._fieldName} ${this._operator.get()} ${this._value.get()}`;
48
+ }
49
+
50
+ setFieldRegistry(fieldFilterRegistry: FieldFilterRegistry) {
51
+ this._fieldFilterRegistry = fieldFilterRegistry;
52
+ this._fieldFilterRegistry.registerFieldFilter(this._fieldName, this);
53
+ }
54
+
55
+ setFilterState(filterAst: Node) {
56
+ const fieldNodes = filterAst.findAll(
57
+ (n: Node) => n.name === 'plain-field' && n.value === this._fieldName
58
+ );
59
+
60
+ const fieldNode = fieldNodes.filter(
61
+ (n: Node) => n.parent !== null && n.parent.name === 'infix-expression'
62
+ )[0];
63
+
64
+ if (fieldNode == null) {
65
+ return;
66
+ }
67
+
68
+ const parent = fieldNode.parent as Node;
69
+ const operatorNode = parent.children[1] as Node;
70
+ const valueNode = parent.children[2] as Node;
71
+ this._operator.set(operatorNode.value as ComparisonOperator);
72
+ const value = Number(valueNode.value);
73
+ this._value.set(value);
74
+ }
75
+
76
+ dispose() {
77
+ this._fieldFilterRegistry?.unregisterFieldFilter(this._fieldName);
78
+ this._onChangeHandlers.clear();
79
+ }
80
+ }
@@ -0,0 +1,68 @@
1
+ import { CrossCircleIcon } from '@tcn/icons/cross_circle_icon.js';
2
+ import { useSignalValue } from '@tcn/state';
3
+ import { Button } from '@tcn/ui/actions';
4
+ import { Input } from '@tcn/ui/inputs';
5
+ import { Box, HStack, VStack } from '@tcn/ui/stacks';
6
+ import { BodyText, Title } from '@tcn/ui/typography';
7
+ import React from 'react';
8
+ import { useFieldFilterStrategy } from './use_field_filter_strategy.js';
9
+ import { NumberRangeFieldFilterPresenter } from './number_range_field_filter_presenter.js';
10
+
11
+ export function NumberRangeFieldFilter({
12
+ fieldName,
13
+ label,
14
+ }: {
15
+ fieldName: string;
16
+ label: string;
17
+ }) {
18
+ const presenter = useFieldFilterStrategy(NumberRangeFieldFilterPresenter, fieldName);
19
+
20
+ const minValue = useSignalValue(presenter.broadcasts.minValue);
21
+ const maxValue = useSignalValue(presenter.broadcasts.maxValue);
22
+
23
+ return (
24
+ <VStack gap="4px">
25
+ <Box width="flex">
26
+ <Title size="md">{label}</Title>
27
+ </Box>
28
+ <HStack gap="4px">
29
+ <Box width="auto">
30
+ <BodyText size="lg">Min</BodyText>
31
+ </Box>
32
+ <Box width="flex">
33
+ <Input
34
+ type="number"
35
+ value={String(minValue ?? '')}
36
+ onChange={value => presenter.setMinValue(Number(value))}
37
+ />
38
+ </Box>
39
+ <Button
40
+ onClick={() => presenter.setMinValue(null)}
41
+ hierarchy="tertiary"
42
+ disabled={minValue == null}
43
+ >
44
+ <CrossCircleIcon />
45
+ </Button>
46
+ </HStack>
47
+ <HStack gap="4px">
48
+ <Box width="auto">
49
+ <BodyText size="lg">Max</BodyText>
50
+ </Box>
51
+ <Box width="flex">
52
+ <Input
53
+ type="number"
54
+ value={String(maxValue ?? '')}
55
+ onChange={value => presenter.setMaxValue(Number(value))}
56
+ />
57
+ </Box>
58
+ <Button
59
+ onClick={() => presenter.setMaxValue(null)}
60
+ hierarchy="tertiary"
61
+ disabled={maxValue == null}
62
+ >
63
+ <CrossCircleIcon />
64
+ </Button>
65
+ </HStack>
66
+ </VStack>
67
+ );
68
+ }