@tcn/ui-table 2.2.0 → 2.3.1

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,57 @@
1
+ import React from 'react';
2
+
3
+ import { CrossCircleIcon } from '@tcn/icons/cross_circle_icon.js';
4
+ import { useSignalValue } from '@tcn/state';
5
+ import { Button } from '@tcn/ui/actions';
6
+ import { Option, Select } from '@tcn/ui/inputs';
7
+ import { Box, HStack, VStack } from '@tcn/ui/stacks';
8
+ import { Title } from '@tcn/ui/typography';
9
+ import { FieldFilterProps } from './field_filter_props.js';
10
+ import { SelectFieldFilterPresenter } from './select_field_filter_presenter.js';
11
+ import { useFieldFilterStrategy } from './use_field_filter_strategy.js';
12
+
13
+ export type SelectFieldFilterProps = FieldFilterProps & {
14
+ options: { label: string; value: string | boolean | number }[];
15
+ };
16
+
17
+ export function SelectFieldFilter({ fieldName, label, options }: SelectFieldFilterProps) {
18
+ const presenter = useFieldFilterStrategy(SelectFieldFilterPresenter, fieldName);
19
+
20
+ const value = useSignalValue(presenter.broadcasts.value);
21
+ const valueLabel = options.find(option => option.value === value)?.label ?? '';
22
+
23
+ return (
24
+ <VStack gap="4px">
25
+ <Box width="flex">
26
+ <Title size="md">{label}</Title>
27
+ </Box>
28
+ <HStack>
29
+ <Select
30
+ value={valueLabel}
31
+ onChange={value => {
32
+ const realValue = options.find(option => option.label === value)?.value;
33
+ presenter.setValue(realValue ?? null);
34
+ }}
35
+ width="flex"
36
+ >
37
+ {options.map(option => (
38
+ <Option
39
+ key={option.value.toString()}
40
+ value={option.label}
41
+ label={option.label}
42
+ >
43
+ {option.label}
44
+ </Option>
45
+ ))}
46
+ </Select>
47
+ <Button
48
+ onClick={() => presenter.setValue(null)}
49
+ hierarchy="tertiary"
50
+ disabled={value == null}
51
+ >
52
+ <CrossCircleIcon />
53
+ </Button>
54
+ </HStack>
55
+ </VStack>
56
+ );
57
+ }
@@ -0,0 +1,365 @@
1
+ import { SelectFieldFilterPresenter } from './select_field_filter_presenter.js';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ describe('SelectFieldFilterPresenter', () => {
5
+ let presenter: SelectFieldFilterPresenter;
6
+ let mockFieldRegistry: any;
7
+
8
+ beforeEach(() => {
9
+ presenter = new SelectFieldFilterPresenter('test_field');
10
+ mockFieldRegistry = {
11
+ registerFieldFilter: vi.fn(),
12
+ unregisterFieldFilter: vi.fn(),
13
+ };
14
+ });
15
+
16
+ describe('constructor', () => {
17
+ it('should initialize with the correct field name', () => {
18
+ const presenter = new SelectFieldFilterPresenter('custom_field');
19
+ expect(presenter.getFilterString()).toBe('');
20
+ });
21
+
22
+ it('should initialize with default values', () => {
23
+ expect(presenter.getFilterString()).toBe('');
24
+ });
25
+ });
26
+
27
+ describe('setValue', () => {
28
+ it('should set the value when setValue is called with a string', () => {
29
+ presenter.setValue('test_value');
30
+ expect(presenter.getFilterString()).toBe('test_field = "test_value"');
31
+ });
32
+
33
+ it('should set the value when setValue is called with a number', () => {
34
+ presenter.setValue(123);
35
+ expect(presenter.getFilterString()).toBe('test_field = 123');
36
+ });
37
+
38
+ it('should set the value when setValue is called with a boolean', () => {
39
+ presenter.setValue(true);
40
+ expect(presenter.getFilterString()).toBe('test_field = true');
41
+ });
42
+
43
+ it('should set the value when setValue is called with null', () => {
44
+ presenter.setValue('test_value');
45
+ presenter.setValue(null);
46
+ expect(presenter.getFilterString()).toBe('');
47
+ });
48
+
49
+ it('should trigger onChange handlers when value is set', () => {
50
+ const onChangeHandler = vi.fn();
51
+ presenter.onChange(onChangeHandler);
52
+
53
+ presenter.setValue('new_value');
54
+
55
+ expect(onChangeHandler).toHaveBeenCalledTimes(1);
56
+ });
57
+
58
+ it('should trigger onChange handlers when value is set to null', () => {
59
+ const onChangeHandler = vi.fn();
60
+ presenter.onChange(onChangeHandler);
61
+
62
+ presenter.setValue('test_value');
63
+ presenter.setValue(null);
64
+
65
+ expect(onChangeHandler).toHaveBeenCalledTimes(2);
66
+ });
67
+ });
68
+
69
+ describe('getFilterString', () => {
70
+ it('should return empty string when value is null', () => {
71
+ expect(presenter.getFilterString()).toBe('');
72
+ });
73
+
74
+ it('should return empty string when value is set to null', () => {
75
+ presenter.setValue('test_value');
76
+ presenter.setValue(null);
77
+ expect(presenter.getFilterString()).toBe('');
78
+ });
79
+
80
+ it('should return formatted filter string with string value', () => {
81
+ presenter.setValue('test_value');
82
+ expect(presenter.getFilterString()).toBe('test_field = "test_value"');
83
+ });
84
+
85
+ it('should return formatted filter string with numeric value', () => {
86
+ presenter.setValue(123);
87
+ expect(presenter.getFilterString()).toBe('test_field = 123');
88
+ });
89
+
90
+ it('should return formatted filter string with boolean value', () => {
91
+ presenter.setValue(true);
92
+ expect(presenter.getFilterString()).toBe('test_field = true');
93
+ });
94
+
95
+ it('should handle special characters in string values', () => {
96
+ presenter.setValue('test"value');
97
+ expect(presenter.getFilterString()).toBe('test_field = "test"value"');
98
+ });
99
+
100
+ it('should handle empty string values', () => {
101
+ presenter.setValue('');
102
+ expect(presenter.getFilterString()).toBe('test_field = ""');
103
+ });
104
+ });
105
+
106
+ describe('onChange', () => {
107
+ it('should register and return unsubscribe function', () => {
108
+ const handler = vi.fn();
109
+ const unsubscribe = presenter.onChange(handler);
110
+
111
+ expect(typeof unsubscribe).toBe('function');
112
+ });
113
+
114
+ it('should call registered handlers when value changes', () => {
115
+ const handler1 = vi.fn();
116
+ const handler2 = vi.fn();
117
+
118
+ presenter.onChange(handler1);
119
+ presenter.onChange(handler2);
120
+
121
+ presenter.setValue('new_value');
122
+
123
+ expect(handler1).toHaveBeenCalledTimes(1);
124
+ expect(handler2).toHaveBeenCalledTimes(1);
125
+ });
126
+
127
+ it('should not call unsubscribed handlers', () => {
128
+ const handler1 = vi.fn();
129
+ const handler2 = vi.fn();
130
+
131
+ const unsubscribe1 = presenter.onChange(handler1);
132
+ presenter.onChange(handler2);
133
+
134
+ unsubscribe1();
135
+ presenter.setValue('new_value');
136
+
137
+ expect(handler1).not.toHaveBeenCalled();
138
+ expect(handler2).toHaveBeenCalledTimes(1);
139
+ });
140
+
141
+ it('should handle multiple subscriptions and unsubscriptions', () => {
142
+ const handler1 = vi.fn();
143
+ const handler2 = vi.fn();
144
+ const handler3 = vi.fn();
145
+
146
+ const unsubscribe1 = presenter.onChange(handler1);
147
+ const unsubscribe2 = presenter.onChange(handler2);
148
+ const unsubscribe3 = presenter.onChange(handler3);
149
+
150
+ unsubscribe2();
151
+ presenter.setValue('value1');
152
+
153
+ expect(handler1).toHaveBeenCalledTimes(1);
154
+ expect(handler2).not.toHaveBeenCalled();
155
+ expect(handler3).toHaveBeenCalledTimes(1);
156
+
157
+ unsubscribe1();
158
+ unsubscribe3();
159
+ presenter.setValue('value2');
160
+
161
+ expect(handler1).toHaveBeenCalledTimes(1);
162
+ expect(handler2).not.toHaveBeenCalled();
163
+ expect(handler3).toHaveBeenCalledTimes(1);
164
+ });
165
+ });
166
+
167
+ describe('setFieldRegistry', () => {
168
+ it('should register the field filter with the registry', () => {
169
+ presenter.setFieldRegistry(mockFieldRegistry);
170
+
171
+ expect(mockFieldRegistry.registerFieldFilter).toHaveBeenCalledWith(
172
+ 'test_field',
173
+ presenter
174
+ );
175
+ });
176
+
177
+ it('should store the field registry reference', () => {
178
+ presenter.setFieldRegistry(mockFieldRegistry);
179
+ // This is tested indirectly through the dispose method
180
+ presenter.dispose();
181
+ expect(mockFieldRegistry.unregisterFieldFilter).toHaveBeenCalledWith('test_field');
182
+ });
183
+ });
184
+
185
+ describe('setFilterState', () => {
186
+ it('should parse string value from filter AST', () => {
187
+ const mockNode = {
188
+ name: 'plain-field',
189
+ value: 'test_field',
190
+ parent: {
191
+ name: 'infix-expression',
192
+ children: [null, null, { name: 'double-quote-str', value: '"test_value"' }],
193
+ },
194
+ };
195
+
196
+ const mockAst = {
197
+ findAll: vi.fn().mockReturnValue([mockNode]),
198
+ };
199
+
200
+ presenter.setFilterState(mockAst as any);
201
+
202
+ expect(presenter.getFilterString()).toBe('test_field = "test_value"');
203
+ });
204
+
205
+ it('should parse single-quoted string value from filter AST', () => {
206
+ const mockNode = {
207
+ name: 'plain-field',
208
+ value: 'test_field',
209
+ parent: {
210
+ name: 'infix-expression',
211
+ children: [null, null, { name: 'single-quote-string', value: "'test_value'" }],
212
+ },
213
+ };
214
+
215
+ const mockAst = {
216
+ findAll: vi.fn().mockReturnValue([mockNode]),
217
+ };
218
+
219
+ presenter.setFilterState(mockAst as any);
220
+
221
+ expect(presenter.getFilterString()).toBe('test_field = "test_value"');
222
+ });
223
+
224
+ it('should handle no matching field nodes', () => {
225
+ const mockAst = {
226
+ findAll: vi.fn().mockReturnValue([]),
227
+ };
228
+
229
+ presenter.setFilterState(mockAst as any);
230
+
231
+ expect(presenter.getFilterString()).toBe('');
232
+ });
233
+
234
+ it('should handle field nodes without proper parent structure', () => {
235
+ const mockNode = {
236
+ name: 'plain-field',
237
+ value: 'test_field',
238
+ parent: null,
239
+ };
240
+
241
+ const mockAst = {
242
+ findAll: vi.fn().mockReturnValue([mockNode]),
243
+ };
244
+
245
+ presenter.setFilterState(mockAst as any);
246
+
247
+ expect(presenter.getFilterString()).toBe('');
248
+ });
249
+
250
+ it('should handle field nodes with non-infix-expression parent', () => {
251
+ const mockNode = {
252
+ name: 'plain-field',
253
+ value: 'test_field',
254
+ parent: {
255
+ name: 'other-expression',
256
+ children: [null, null, { name: 'double-quote-str', value: '"test_value"' }],
257
+ },
258
+ };
259
+
260
+ const mockAst = {
261
+ findAll: vi.fn().mockReturnValue([mockNode]),
262
+ };
263
+
264
+ presenter.setFilterState(mockAst as any);
265
+
266
+ expect(presenter.getFilterString()).toBe('');
267
+ });
268
+
269
+ it('should handle multiple field nodes and use the first valid one', () => {
270
+ const mockNode1 = {
271
+ name: 'plain-field',
272
+ value: 'test_field',
273
+ parent: {
274
+ name: 'infix-expression',
275
+ children: [null, null, { name: 'double-quote-str', value: '"first_value"' }],
276
+ },
277
+ };
278
+
279
+ const mockNode2 = {
280
+ name: 'plain-field',
281
+ value: 'test_field',
282
+ parent: {
283
+ name: 'infix-expression',
284
+ children: [null, null, { name: 'double-quote-str', value: '"second_value"' }],
285
+ },
286
+ };
287
+
288
+ const mockAst = {
289
+ findAll: vi.fn().mockReturnValue([mockNode1, mockNode2]),
290
+ };
291
+
292
+ presenter.setFilterState(mockAst as any);
293
+
294
+ expect(presenter.getFilterString()).toBe('test_field = "first_value"');
295
+ });
296
+
297
+ it('should handle empty string values', () => {
298
+ const mockNode = {
299
+ name: 'plain-field',
300
+ value: 'test_field',
301
+ parent: {
302
+ name: 'infix-expression',
303
+ children: [null, null, { name: 'double-quote-str', value: '""' }],
304
+ },
305
+ };
306
+
307
+ const mockAst = {
308
+ findAll: vi.fn().mockReturnValue([mockNode]),
309
+ };
310
+
311
+ presenter.setFilterState(mockAst as any);
312
+
313
+ expect(presenter.getFilterString()).toBe('test_field = ""');
314
+ });
315
+
316
+ it('should handle special characters in string values', () => {
317
+ const mockNode = {
318
+ name: 'plain-field',
319
+ value: 'test_field',
320
+ parent: {
321
+ name: 'infix-expression',
322
+ children: [null, null, { name: 'double-quote-str', value: '"test\\"value"' }],
323
+ },
324
+ };
325
+
326
+ const mockAst = {
327
+ findAll: vi.fn().mockReturnValue([mockNode]),
328
+ };
329
+
330
+ presenter.setFilterState(mockAst as any);
331
+
332
+ expect(presenter.getFilterString()).toBe('test_field = "test\\"value"');
333
+ });
334
+ });
335
+
336
+ describe('dispose', () => {
337
+ it('should unregister field filter from registry', () => {
338
+ presenter.setFieldRegistry(mockFieldRegistry);
339
+ presenter.dispose();
340
+
341
+ expect(mockFieldRegistry.unregisterFieldFilter).toHaveBeenCalledWith('test_field');
342
+ });
343
+
344
+ it('should clear onChange handlers', () => {
345
+ const handler = vi.fn();
346
+ presenter.onChange(handler);
347
+ presenter.dispose();
348
+ presenter.setValue('value1');
349
+
350
+ expect(handler).not.toHaveBeenCalled();
351
+ });
352
+
353
+ it('should handle disposal when no registry is set', () => {
354
+ expect(() => presenter.dispose()).not.toThrow();
355
+ });
356
+ });
357
+
358
+ describe('broadcasts', () => {
359
+ it('should provide value broadcast', () => {
360
+ expect(presenter.broadcasts).toHaveProperty('value');
361
+ expect(typeof presenter.broadcasts.value).toBe('object');
362
+ expect(typeof presenter.broadcasts.value.subscribe).toBe('function');
363
+ });
364
+ });
365
+ });
@@ -0,0 +1,74 @@
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 SelectFieldFilterPresenter implements FieldFilterStrategy {
6
+ private _fieldName: string;
7
+ private _value = new Signal<string | boolean | number | null>(null);
8
+ private _fieldFilterRegistry: FieldFilterRegistry | null = null;
9
+ private _onChangeHandlers = new Set<() => void>();
10
+
11
+ private _broadcasts = {
12
+ value: this._value.broadcast,
13
+ };
14
+
15
+ constructor(fieldName: string) {
16
+ this._fieldName = fieldName;
17
+ }
18
+
19
+ get broadcasts() {
20
+ return this._broadcasts;
21
+ }
22
+
23
+ setValue(value: string | boolean | number | null) {
24
+ this._value.set(value);
25
+ this._onChangeHandlers.forEach(handler => handler());
26
+ }
27
+
28
+ onChange(handler: () => void) {
29
+ this._onChangeHandlers.add(handler);
30
+ return () => {
31
+ this._onChangeHandlers.delete(handler);
32
+ };
33
+ }
34
+
35
+ setFieldRegistry(fieldRegistry: FieldFilterRegistry) {
36
+ this._fieldFilterRegistry = fieldRegistry;
37
+ fieldRegistry.registerFieldFilter(this._fieldName, this);
38
+ }
39
+
40
+ getFilterString() {
41
+ if (this._value.get() === null) {
42
+ return '';
43
+ }
44
+ const wrappedValue =
45
+ typeof this._value.get() === 'string'
46
+ ? `"${this._value.get()}"`
47
+ : this._value.get();
48
+ return `${this._fieldName} = ${wrappedValue}`;
49
+ }
50
+
51
+ setFilterState(filterAst: Node) {
52
+ const fieldNodes = filterAst.findAll(
53
+ (n: Node) => n.name === 'plain-field' && n.value === this._fieldName
54
+ );
55
+
56
+ const fieldNode = fieldNodes.filter(
57
+ (n: Node) => n.parent !== null && n.parent.name === 'infix-expression'
58
+ )[0];
59
+
60
+ if (fieldNode == null) {
61
+ return;
62
+ }
63
+
64
+ const parent = fieldNode.parent as Node;
65
+ const valueNode = parent.children[2] as Node;
66
+ const value = valueNode.value as string;
67
+ this._value.set(value.slice(1, -1));
68
+ }
69
+
70
+ dispose() {
71
+ this._fieldFilterRegistry?.unregisterFieldFilter(this._fieldName);
72
+ this._onChangeHandlers.clear();
73
+ }
74
+ }
@@ -0,0 +1,70 @@
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, Option, Select } from '@tcn/ui/inputs';
5
+ import { Box, HStack, VStack } from '@tcn/ui/stacks';
6
+ import { Title } from '@tcn/ui/typography';
7
+ import React from 'react';
8
+ import { FieldFilterProps } from './field_filter_props.js';
9
+ import { StringFieldFilterPresenter } from './string_field_filter_presenter.js';
10
+ import { ComparisonOperator } from '../types.js';
11
+
12
+ import { useFieldFilterStrategy } from './use_field_filter_strategy.js';
13
+
14
+ const allOperators: ('is' | 'isNot' | 'has')[] = ['is', 'isNot', 'has'];
15
+ const operatorSymbols: Record<'is' | 'isNot' | 'has', string> = {
16
+ is: '=',
17
+ isNot: '!=',
18
+ has: ':',
19
+ };
20
+ const operatorLabels: Record<'is' | 'isNot' | 'has', string> = {
21
+ is: 'is',
22
+ isNot: 'is not',
23
+ has: 'has',
24
+ };
25
+
26
+ export function StringFieldFilter({ fieldName, label, operators }: FieldFilterProps) {
27
+ const presenter = useFieldFilterStrategy(StringFieldFilterPresenter, fieldName);
28
+
29
+ const value = useSignalValue(presenter.broadcasts.value);
30
+ const operator = useSignalValue(presenter.broadcasts.operator);
31
+
32
+ const availableOperators = operators || allOperators;
33
+
34
+ return (
35
+ <VStack gap="4px">
36
+ <Box width="flex">
37
+ <Title size="md">{label}</Title>
38
+ </Box>
39
+ <HStack width="flex">
40
+ <Select
41
+ value={operator}
42
+ onChange={value => presenter.setOperator(value as ComparisonOperator)}
43
+ width="65px"
44
+ >
45
+ {availableOperators.map(operator => (
46
+ <Option
47
+ key={operator}
48
+ value={operatorSymbols[operator]}
49
+ label={operatorLabels[operator]}
50
+ >
51
+ {operatorLabels[operator]}
52
+ </Option>
53
+ ))}
54
+ </Select>
55
+ <Input
56
+ type="text"
57
+ value={value ?? ''}
58
+ onChange={value => presenter.setValue(value)}
59
+ />
60
+ <Button
61
+ onClick={() => presenter.setValue(null)}
62
+ hierarchy="tertiary"
63
+ disabled={value == null}
64
+ >
65
+ <CrossCircleIcon />
66
+ </Button>
67
+ </HStack>
68
+ </VStack>
69
+ );
70
+ }