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