@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,296 @@
1
+ import { Node } from 'clarity-pattern-parser';
2
+ import { StringFieldFilterPresenter } from './string_field_filter_presenter.js';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ describe('StringFieldFilterPresenter', () => {
6
+ let presenter: StringFieldFilterPresenter;
7
+ let mockFieldRegistry: any;
8
+
9
+ beforeEach(() => {
10
+ presenter = new StringFieldFilterPresenter('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 StringFieldFilterPresenter('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 string', () => {
30
+ presenter.setValue('test_value');
31
+ presenter.setOperator('=');
32
+ expect(presenter.getFilterString()).toBe('test_field = "test_value"');
33
+ });
34
+
35
+ it('should set the value when setValue is called with null', () => {
36
+ presenter.setValue('test_value');
37
+ presenter.setValue(null);
38
+ expect(presenter.getFilterString()).toBe('');
39
+ });
40
+
41
+ it('should trigger onChange handlers when value is set', () => {
42
+ const onChangeHandler = vi.fn();
43
+ presenter.onChange(onChangeHandler);
44
+
45
+ presenter.setValue('new_value');
46
+
47
+ expect(onChangeHandler).toHaveBeenCalledTimes(1);
48
+ });
49
+ });
50
+
51
+ describe('setOperator', () => {
52
+ it('should set the operator when setOperator is called', () => {
53
+ presenter.setValue('test_value');
54
+ presenter.setOperator('>');
55
+ expect(presenter.getFilterString()).toBe('test_field > "test_value"');
56
+ });
57
+
58
+ it('should work with all comparison operators', () => {
59
+ const operators = ['>=', '>', '<=', '<', '=', ':', '!='] as const;
60
+ presenter.setValue('test_value');
61
+
62
+ operators.forEach(operator => {
63
+ presenter.setOperator(operator);
64
+ expect(presenter.getFilterString()).toBe(`test_field ${operator} "test_value"`);
65
+ });
66
+ });
67
+
68
+ it('should trigger onChange handlers when operator is set', () => {
69
+ const onChangeHandler = vi.fn();
70
+ presenter.onChange(onChangeHandler);
71
+
72
+ presenter.setOperator('>');
73
+
74
+ expect(onChangeHandler).toHaveBeenCalledTimes(1);
75
+ });
76
+ });
77
+
78
+ describe('getFilterString', () => {
79
+ it('should return empty string when value is null', () => {
80
+ expect(presenter.getFilterString()).toBe('');
81
+ });
82
+
83
+ it('should return empty string when value is set to null', () => {
84
+ presenter.setValue('test_value');
85
+ presenter.setValue(null);
86
+ expect(presenter.getFilterString()).toBe('');
87
+ });
88
+
89
+ it('should return formatted filter string with value and operator', () => {
90
+ presenter.setValue('test_value');
91
+ presenter.setOperator('=');
92
+ expect(presenter.getFilterString()).toBe('test_field = "test_value"');
93
+ });
94
+
95
+ it('should handle special characters in values', () => {
96
+ presenter.setValue('test"value');
97
+ presenter.setOperator('!=');
98
+ expect(presenter.getFilterString()).toBe('test_field != "test\\"value"');
99
+ });
100
+ });
101
+
102
+ describe('onChange', () => {
103
+ it('should register onChange handler and return unsubscribe function', () => {
104
+ const onChangeHandler = vi.fn();
105
+ const unsubscribe = presenter.onChange(onChangeHandler);
106
+
107
+ presenter.setValue('test_value');
108
+ expect(onChangeHandler).toHaveBeenCalledTimes(1);
109
+
110
+ unsubscribe();
111
+ presenter.setValue('another_value');
112
+ expect(onChangeHandler).toHaveBeenCalledTimes(1); // Should not be called again
113
+ });
114
+
115
+ it('should handle multiple onChange handlers', () => {
116
+ const handler1 = vi.fn();
117
+ const handler2 = vi.fn();
118
+
119
+ presenter.onChange(handler1);
120
+ presenter.onChange(handler2);
121
+
122
+ presenter.setValue('test_value');
123
+
124
+ expect(handler1).toHaveBeenCalledTimes(1);
125
+ expect(handler2).toHaveBeenCalledTimes(1);
126
+ });
127
+
128
+ it('should handle removing onChange handlers', () => {
129
+ const handler1 = vi.fn();
130
+ const handler2 = vi.fn();
131
+
132
+ const unsubscribe1 = presenter.onChange(handler1);
133
+ presenter.onChange(handler2);
134
+
135
+ unsubscribe1();
136
+ presenter.setValue('test_value');
137
+
138
+ expect(handler1).not.toHaveBeenCalled();
139
+ expect(handler2).toHaveBeenCalledTimes(1);
140
+ });
141
+ });
142
+
143
+ describe('setFieldRegistry', () => {
144
+ it('should register the field filter with the registry', () => {
145
+ presenter.setFieldRegistry(mockFieldRegistry);
146
+
147
+ expect(mockFieldRegistry.registerFieldFilter).toHaveBeenCalledWith(
148
+ 'test_field',
149
+ presenter
150
+ );
151
+ });
152
+
153
+ it('should store the field registry reference', () => {
154
+ presenter.setFieldRegistry(mockFieldRegistry);
155
+
156
+ // Test that dispose can call unregisterFieldFilter
157
+ presenter.dispose();
158
+ expect(mockFieldRegistry.unregisterFieldFilter).toHaveBeenCalledWith('test_field');
159
+ });
160
+ });
161
+
162
+ describe('setFilterState', () => {
163
+ it('should parse filter AST and set operator and value', () => {
164
+ // Create a mock AST node structure
165
+ const mockAst = {
166
+ findAll: vi.fn().mockReturnValue([
167
+ {
168
+ name: 'plain-field',
169
+ value: 'test_field',
170
+ parent: {
171
+ name: 'infix-expression',
172
+ children: [
173
+ { name: 'plain-field', value: 'test_field' },
174
+ { name: 'operator', value: '=' },
175
+ { name: 'string', value: '"test_value"' },
176
+ ],
177
+ },
178
+ },
179
+ ]),
180
+ } as unknown as Node;
181
+
182
+ presenter.setFilterState(mockAst);
183
+
184
+ expect(presenter.getFilterString()).toBe('test_field = "test_value"');
185
+ });
186
+
187
+ it('should handle case when field is not found in AST', () => {
188
+ const mockAst = {
189
+ findAll: vi.fn().mockReturnValue([]),
190
+ } as unknown as Node;
191
+
192
+ presenter.setValue('existing_value');
193
+ presenter.setOperator('>');
194
+
195
+ presenter.setFilterState(mockAst);
196
+
197
+ // Should not change existing state
198
+ expect(presenter.getFilterString()).toBe('test_field > "existing_value"');
199
+ });
200
+
201
+ it('should handle case when field node has no parent', () => {
202
+ const mockAst = {
203
+ findAll: vi.fn().mockReturnValue([
204
+ {
205
+ name: 'plain-field',
206
+ value: 'test_field',
207
+ parent: null,
208
+ },
209
+ ]),
210
+ } as unknown as Node;
211
+
212
+ presenter.setValue('existing_value');
213
+ presenter.setOperator('>');
214
+
215
+ presenter.setFilterState(mockAst);
216
+
217
+ // Should not change existing state
218
+ expect(presenter.getFilterString()).toBe('test_field > "existing_value"');
219
+ });
220
+
221
+ it('should extract value without quotes from AST', () => {
222
+ const mockAst = {
223
+ findAll: vi.fn().mockReturnValue([
224
+ {
225
+ name: 'plain-field',
226
+ value: 'test_field',
227
+ parent: {
228
+ name: 'infix-expression',
229
+ children: [
230
+ { name: 'plain-field', value: 'test_field' },
231
+ { name: 'operator', value: '!=' },
232
+ { name: 'string', value: '"quoted_value"' },
233
+ ],
234
+ },
235
+ },
236
+ ]),
237
+ } as unknown as Node;
238
+
239
+ presenter.setFilterState(mockAst);
240
+
241
+ expect(presenter.getFilterString()).toBe('test_field != "quoted_value"');
242
+ });
243
+ });
244
+
245
+ describe('dispose', () => {
246
+ it('should unregister field filter from registry when registry is set', () => {
247
+ presenter.setFieldRegistry(mockFieldRegistry);
248
+ presenter.dispose();
249
+
250
+ expect(mockFieldRegistry.unregisterFieldFilter).toHaveBeenCalledWith('test_field');
251
+ });
252
+
253
+ it('should not call unregisterFieldFilter when no registry is set', () => {
254
+ presenter.dispose();
255
+
256
+ expect(mockFieldRegistry.unregisterFieldFilter).not.toHaveBeenCalled();
257
+ });
258
+
259
+ it('should clear onChange handlers', () => {
260
+ const onChangeHandler = vi.fn();
261
+ presenter.onChange(onChangeHandler);
262
+
263
+ presenter.dispose();
264
+ presenter.setValue('test_value');
265
+
266
+ expect(onChangeHandler).not.toHaveBeenCalled();
267
+ });
268
+ });
269
+
270
+ describe('edge cases and error handling', () => {
271
+ it('should handle rapid successive value changes', () => {
272
+ const onChangeHandler = vi.fn();
273
+ presenter.onChange(onChangeHandler);
274
+
275
+ presenter.setValue('value1');
276
+ presenter.setValue('value2');
277
+ presenter.setValue('value3');
278
+
279
+ expect(onChangeHandler).toHaveBeenCalledTimes(3);
280
+ expect(presenter.getFilterString()).toBe('test_field : "value3"');
281
+ });
282
+
283
+ it('should handle rapid successive operator changes', () => {
284
+ const onChangeHandler = vi.fn();
285
+ presenter.onChange(onChangeHandler);
286
+
287
+ presenter.setValue('test_value');
288
+ presenter.setOperator('=');
289
+ presenter.setOperator('>');
290
+ presenter.setOperator('!=');
291
+
292
+ expect(onChangeHandler).toHaveBeenCalledTimes(4); // 1 for value + 3 for operators
293
+ expect(presenter.getFilterString()).toBe('test_field != "test_value"');
294
+ });
295
+ });
296
+ });
@@ -0,0 +1,81 @@
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 StringFieldFilterPresenter implements FieldFilterStrategy {
7
+ private _fieldName: string;
8
+ private _value = new Signal<string | 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
+ setFieldRegistry(fieldRegistry: FieldFilterRegistry) {
34
+ this._fieldFilterRegistry = fieldRegistry;
35
+ fieldRegistry.registerFieldFilter(this._fieldName, this);
36
+ }
37
+
38
+ getFilterString() {
39
+ if (this._value.get() === null) {
40
+ return '';
41
+ }
42
+ return `${this._fieldName} ${this._operator.get()} "${this._value.get()}"`;
43
+ }
44
+
45
+ setValue(value: string | null) {
46
+ const escapedValue = value?.replace(/"/g, '\\"') ?? null;
47
+ this._value.set(escapedValue === '' ? null : escapedValue);
48
+ this._onChangeHandlers.forEach(handler => handler());
49
+ }
50
+
51
+ setOperator(operator: ComparisonOperator) {
52
+ this._operator.set(operator);
53
+ this._onChangeHandlers.forEach(handler => handler());
54
+ }
55
+
56
+ setFilterState(filterAst: Node) {
57
+ const fieldNodes = filterAst.findAll(
58
+ (n: Node) => n.name === 'plain-field' && n.value === this._fieldName
59
+ );
60
+
61
+ const fieldNode = fieldNodes.filter(
62
+ (n: Node) => n.parent !== null && n.parent.name === 'infix-expression'
63
+ )[0];
64
+
65
+ if (fieldNode == null) {
66
+ return;
67
+ }
68
+
69
+ const parent = fieldNode.parent as Node;
70
+ const operatorNode = parent.children[1] as Node;
71
+ const valueNode = parent.children[2] as Node;
72
+ this._operator.set(operatorNode.value as ComparisonOperator);
73
+ const value = valueNode.value as string;
74
+ this._value.set(value.slice(1, -1));
75
+ }
76
+
77
+ dispose() {
78
+ this._fieldFilterRegistry?.unregisterFieldFilter(this._fieldName);
79
+ this._onChangeHandlers.clear();
80
+ }
81
+ }
@@ -0,0 +1,30 @@
1
+ import { useContext, useEffect, useState } from 'react';
2
+ import { FieldFilterStrategy } from './field_filter_strategy.js';
3
+ import { TableFilterPanelContext } from '../table_filter_panel.js';
4
+
5
+ type FieldFilterStrategyConstructor<T> = new (...args: any[]) => T;
6
+
7
+ export function useFieldFilterStrategy<T extends FieldFilterStrategy>(
8
+ filterPresenterConstructor: FieldFilterStrategyConstructor<T>,
9
+ ...args: any[]
10
+ ): T {
11
+ const panelPresenter = useContext(TableFilterPanelContext);
12
+
13
+ const [presenter] = useState<T>(() => {
14
+ return new filterPresenterConstructor(...args);
15
+ });
16
+
17
+ useEffect(() => {
18
+ if (panelPresenter) {
19
+ presenter.setFieldRegistry(panelPresenter);
20
+ }
21
+ }, [panelPresenter, presenter]);
22
+
23
+ useEffect(() => {
24
+ return () => {
25
+ presenter.dispose();
26
+ };
27
+ }, [presenter]);
28
+
29
+ return presenter;
30
+ }
@@ -0,0 +1,46 @@
1
+ import {
2
+ StaticDataSource,
3
+ StaticNumberField,
4
+ StaticStringField,
5
+ } from '@tcn/resource-store';
6
+ import React, { useState } from 'react';
7
+ import { items } from '../../__stories__/sample_data.js';
8
+ import { NumberFieldFilter } from './field_filters/number_field_filter.js';
9
+ import { TableFilterPanel } from './table_filter_panel.js';
10
+
11
+ type DataItem = {
12
+ id: string;
13
+ name: string;
14
+ age: number;
15
+ email: string;
16
+ city: string;
17
+ country: string;
18
+ occupation: string;
19
+ isActive: boolean;
20
+ };
21
+
22
+ export default {
23
+ title: 'TableFilterPanel',
24
+ };
25
+
26
+ export const Demo = () => {
27
+ const [source] = useState(() => {
28
+ return new StaticDataSource<DataItem>(items, [
29
+ new StaticStringField('name', i => i.name),
30
+ new StaticNumberField('age', i => i.age),
31
+ new StaticStringField('email', i => i.email),
32
+ new StaticStringField('city', i => i.city),
33
+ new StaticStringField('country', i => i.country),
34
+ new StaticStringField('occupation', i => i.occupation),
35
+ new StaticStringField('isActive', i => (i.isActive ? 'Yes' : 'No')),
36
+ ]);
37
+ });
38
+
39
+ return (
40
+ <div style={{ width: '300px' }}>
41
+ <TableFilterPanel dataSource={source}>
42
+ <NumberFieldFilter fieldName="age" label="Age" />
43
+ </TableFilterPanel>
44
+ </div>
45
+ );
46
+ };
@@ -0,0 +1,26 @@
1
+ import { DataSource } from '@tcn/resource-store';
2
+ import { VStack } from '@tcn/ui/stacks';
3
+ import React, { ReactElement, useState, createContext } from 'react';
4
+ import { FieldFilterProps } from './field_filters/field_filter_props.js';
5
+ import { TableFilterPanelPresenter } from './table_filter_panel_presenter.js';
6
+
7
+ export type TableFilterPanelProps = {
8
+ children: ReactElement<FieldFilterProps>[] | ReactElement<FieldFilterProps>;
9
+ dataSource: DataSource<any>;
10
+ };
11
+
12
+ export function TableFilterPanel({ children, dataSource }: TableFilterPanelProps) {
13
+ const [presenter] = useState(() => {
14
+ return new TableFilterPanelPresenter(dataSource);
15
+ });
16
+
17
+ return (
18
+ <TableFilterPanelContext.Provider value={presenter}>
19
+ <VStack gap="16px">{children}</VStack>
20
+ </TableFilterPanelContext.Provider>
21
+ );
22
+ }
23
+
24
+ export const TableFilterPanelContext = createContext<TableFilterPanelPresenter | null>(
25
+ null
26
+ );
@@ -0,0 +1,77 @@
1
+ import { parseFilterString } from '@tcn/aip-160';
2
+ import { DataSource } from '@tcn/resource-store';
3
+ import { Node } from 'clarity-pattern-parser';
4
+ import { FieldFilterStrategy } from './field_filters/field_filter_strategy.js';
5
+ import { FieldFilterRegistry } from './field_filters/field_filter_strategy.js';
6
+ export class TableFilterPanelPresenter<T = unknown> implements FieldFilterRegistry {
7
+ private _dataSource: DataSource<T>;
8
+ private _unsubscribers = new Map<string, () => void>();
9
+
10
+ private _fieldFilters = new Map<string, FieldFilterStrategy>();
11
+
12
+ // Some state to prevent infinite loops.
13
+ // Disable filtering while we're setting state of all the fields
14
+ // based on an updated filter string.
15
+ private _lastKnownFilterString = '';
16
+ private _disableObserving = false;
17
+
18
+ constructor(dataSource: DataSource<T>) {
19
+ this._dataSource = dataSource;
20
+ this._dataSource.broadcasts.filterString.subscribe(filterString => {
21
+ if (filterString !== this._lastKnownFilterString) {
22
+ this._disableObserving = true;
23
+ for (const fieldFilter of this._fieldFilters.values()) {
24
+ this._setFieldFilterState(fieldFilter);
25
+ }
26
+ this._disableObserving = false;
27
+ }
28
+ });
29
+ }
30
+
31
+ registerFieldFilter(fieldName: string, fieldFilter: FieldFilterStrategy) {
32
+ this._fieldFilters.set(fieldName, fieldFilter);
33
+ const unsubscribe = fieldFilter.onChange(() => {
34
+ if (!this._disableObserving) {
35
+ this._applyFilters();
36
+ }
37
+ });
38
+ this._unsubscribers.set(fieldName, unsubscribe);
39
+ this._setFieldFilterState(fieldFilter);
40
+ }
41
+
42
+ unregisterFieldFilter(fieldName: string) {
43
+ this._fieldFilters.delete(fieldName);
44
+ this._unsubscribers.get(fieldName)?.();
45
+ this._unsubscribers.delete(fieldName);
46
+ }
47
+
48
+ private async _applyFilters() {
49
+ const filterString = Array.from(this._fieldFilters.values())
50
+ .map(fieldFilter => fieldFilter.getFilterString())
51
+ .filter(Boolean)
52
+ .join(' AND ');
53
+
54
+ this._lastKnownFilterString = filterString;
55
+ this._dataSource.setFilterString(filterString);
56
+ }
57
+
58
+ private async _setFieldFilterState(fieldFilter: FieldFilterStrategy) {
59
+ const filterString = this._dataSource.broadcasts.filterString.get();
60
+ // TODO: Why isn't this type working?
61
+ const ast = (await parseFilterString(filterString)) as Node | null;
62
+ if (ast == null) {
63
+ return;
64
+ }
65
+
66
+ ast.findAll((n: Node) => n.name === 'space').forEach((n: Node) => n.remove());
67
+
68
+ ast.walkBreadthFirst((n: Node) => {
69
+ const firstChild = n.children[0];
70
+ if (n.children.length === 1 && firstChild.children.length === 0) {
71
+ n.replaceWith(firstChild);
72
+ }
73
+ });
74
+
75
+ fieldFilter.setFilterState(ast);
76
+ }
77
+ }
@@ -0,0 +1,3 @@
1
+ export type ComparisonOperator = '>=' | '>' | '<=' | '<' | '=' | ':' | '!=';
2
+
3
+ export const comparisonOperators = ['>=', '>', '<=', '<', '=', ':', '!='];
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+
3
+ import { ChevronLeftIcon } from '@tcn/icons/chevron_left_icon.js';
4
+ import { DataSource } from '@tcn/resource-store';
5
+ import { useSignalValue } from '@tcn/state';
6
+ import { Button } from '@tcn/ui/actions';
7
+ import { HStack, HStackOwnProps, WithDetailedHTMLProps } from '@tcn/ui/stacks';
8
+ import { BodyText } from '@tcn/ui/typography';
9
+
10
+ import { ChevronRightIcon } from '@tcn/icons/chevron_right_icon.js';
11
+ import styles from './table_pager.module.css';
12
+
13
+ export interface TablePagerOwnProps<T> {
14
+ dataSource: DataSource<T>;
15
+ }
16
+ export type TablePagerProps<T> = WithDetailedHTMLProps<
17
+ TablePagerOwnProps<T> & HStackOwnProps,
18
+ 'div'
19
+ >;
20
+
21
+ export function TablePager<T>({ dataSource, ...props }: TablePagerProps<T>) {
22
+ const pageIndex = useSignalValue(dataSource.broadcasts.currentPageIndex);
23
+ const hasNextPage = useSignalValue(dataSource.broadcasts.hasNextPage);
24
+
25
+ return (
26
+ <HStack as="div" height="auto" width="auto" gap="8px" {...props}>
27
+ <HStack gap="4px" width="auto">
28
+ <BodyText>Page:</BodyText>
29
+ <BodyText className={styles['page-count']}>{pageIndex + 1}</BodyText>
30
+ </HStack>
31
+ <Button onClick={() => dataSource.previousPage()} disabled={pageIndex === 0}>
32
+ <ChevronLeftIcon flipOnRtl />
33
+ </Button>
34
+ <Button onClick={() => dataSource.nextPage()} disabled={!hasNextPage}>
35
+ <ChevronRightIcon flipOnRtl />
36
+ </Button>
37
+ </HStack>
38
+ );
39
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ export { Table } from './components/table/table.js';
2
+ export { TableColumn } from './components/table/table_column.js';
3
+ export { TablePager } from './components/table_pager.js';
4
+ export { GlobalSearch } from './components/global_search.js';
5
+ export { TableFilterPanel } from './components/table_filter_panel/table_filter_panel.js';
6
+ export { NumberFieldFilter } from './components/table_filter_panel/field_filters/number_field_filter.js';
7
+ export { StringFieldFilter } from './components/table_filter_panel/field_filters/string_field_filter.js';
8
+ export { DateFieldFilter } from './components/table_filter_panel/field_filters/date_field_filter.js';
9
+ export { NumberRangeFieldFilter } from './components/table_filter_panel/field_filters/number_range_field_filter.js';
10
+ export { MulitSelectFieldFilter } from './components/table_filter_panel/field_filters/mulit_select_field_filter.js';
11
+ export { SelectFieldFilter } from './components/table_filter_panel/field_filters/select_field_filter.js';
12
+
13
+ // for making your own field filter
14
+ export type { FieldFilterStrategy } from './components/table_filter_panel/field_filters/field_filter_strategy.js';
15
+ export type { FieldFilterProps } from './components/table_filter_panel/field_filters/field_filter_props.js';
16
+ export { useFieldFilterStrategy as useFilterFieldStrategy } from './components/table_filter_panel/field_filters/use_field_filter_strategy.js';
package/tsconfig.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "compilerOptions": {
3
+ "esModuleInterop": true,
4
+ "jsx": "react-jsx",
5
+ "paths": {
6
+ "~aip-160/*": [
7
+ "../aip-160/src/*"
8
+ ],
9
+ "~aip-160-editor/*": [
10
+ "../aip-160-editor/src/*"
11
+ ],
12
+ "~icons/*": [
13
+ "../icons/src/*"
14
+ ],
15
+ "~resource-store/*": [
16
+ "../resource-store/src/*"
17
+ ],
18
+ "~sb-blackcat-addon/*": [
19
+ "../sb-blackcat-addon/src/*"
20
+ ],
21
+ "~state/*": [
22
+ "../state/src/*"
23
+ ],
24
+ "~ui/*": [
25
+ "../ui/src/*"
26
+ ],
27
+ "~ui-table/*": [
28
+ "./src/*"
29
+ ]
30
+ }
31
+ },
32
+ "include": [
33
+ "src/**/*",
34
+ "types/**/*"
35
+ ]
36
+ }