@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,183 @@
1
+ import React, { isValidElement, ReactElement, useState, useRef } from 'react';
2
+
3
+ import { DataSource } from '@tcn/resource-store';
4
+ import { Status, useRunnerStatus, useSignalValue } from '@tcn/state';
5
+ import { Box, BoxProps } from '@tcn/ui/stacks';
6
+ import { BodyText } from '@tcn/ui/typography';
7
+ import { DataCell } from '../cells/data_cell.js';
8
+ import { FooterCell } from '../cells/footer_cell.js';
9
+ import { HeaderCell } from '../cells/header_cell.js';
10
+ import { StickyRowDataCell } from '../cells/sticky_row_data_cell.js';
11
+ import { StickyRowFillCell } from '../cells/sticky_row_fill_cell.js';
12
+ import styles from './table.module.css';
13
+ import { TableColumn, TableColumnProps } from './table_column.js';
14
+ import { TablePresenter } from './table_presenter.js';
15
+
16
+ export type TableProps<T> = BoxProps & {
17
+ dataSource: DataSource<T>;
18
+ children: ReactElement<TableColumnProps<T>>[];
19
+ onRowClick?: (row: T, rowIndex: number) => void;
20
+ isRowHighlighted?: (row: T, rowIndex: number) => boolean;
21
+ stickyItems?: T[];
22
+ };
23
+
24
+ const HEADER_ROW_HEIGHT = 30;
25
+
26
+ // @TODO: Props for loading and error states
27
+
28
+ function wrapContent(content: React.ReactNode): React.ReactNode {
29
+ if (typeof content === 'string') {
30
+ return <BodyText>{content}</BodyText>;
31
+ }
32
+ return content;
33
+ }
34
+
35
+ export function Table<T>({
36
+ dataSource,
37
+ stickyItems,
38
+ children,
39
+ width = '100%',
40
+ height = '100%',
41
+ zIndex,
42
+ isRowHighlighted,
43
+ onRowClick,
44
+ ...props
45
+ }: TableProps<T>) {
46
+ const rows = useSignalValue(dataSource.broadcasts.currentResults);
47
+ const page = useSignalValue(dataSource.broadcasts.currentPageIndex);
48
+
49
+ const scrollerRef = useRef<HTMLDivElement>(null);
50
+
51
+ const columns = React.Children.toArray(children).filter(
52
+ (child): child is ReactElement<TableColumnProps<T>> =>
53
+ isValidElement(child) && child.type === TableColumn
54
+ );
55
+
56
+ const [presenter] = useState(
57
+ () =>
58
+ new TablePresenter({
59
+ dataSource,
60
+ columns: columns.map(column => ({
61
+ fieldName: column.props.fieldName,
62
+ width: column.props.width ?? 100,
63
+ sortMode: 'none',
64
+ canSort: column.props.canSort ?? false,
65
+ heading: column.props.heading,
66
+ footer: column.props.footer,
67
+ sticky: column.props.sticky,
68
+ render: column.props.render,
69
+ })),
70
+ scrollerRef,
71
+ })
72
+ );
73
+
74
+ const columnInfo = useSignalValue(presenter.broadcasts.columnInfo);
75
+ const itemsStatus = useRunnerStatus(dataSource.broadcasts.currentResults);
76
+ const showFooter = useSignalValue(presenter.broadcasts.showFooter);
77
+
78
+ const isClickable = typeof onRowClick === 'function';
79
+ return (
80
+ <Box
81
+ ref={scrollerRef}
82
+ zIndex={zIndex}
83
+ className={styles['table-body-wrapper']}
84
+ style={{ overflow: 'auto' }}
85
+ {...props}
86
+ >
87
+ <table
88
+ className={styles['table-body']}
89
+ data-is-clickable={isClickable}
90
+ style={{ width, height }}
91
+ >
92
+ <thead>
93
+ <tr>
94
+ {columnInfo.map((column, index) => (
95
+ <HeaderCell
96
+ key={`h-${index}`}
97
+ index={index}
98
+ heading={wrapContent(column.heading)}
99
+ sticky={column.sticky}
100
+ onResize={newSize => presenter.setColumnWidth(index, newSize)}
101
+ width={column.width}
102
+ sortMode={column.sortMode}
103
+ onSortModeChange={() => presenter.setNextColumnSortMode(index)}
104
+ canSort={column.canSort}
105
+ />
106
+ ))}
107
+ <th key="fill" className="fill"></th>
108
+ </tr>
109
+ </thead>
110
+ <tbody>
111
+ {stickyItems?.map((item, index) => (
112
+ <tr key={`sticky-${index}`} data-sticky-row>
113
+ {columnInfo.map((col, colIndex) => {
114
+ const fieldName = col.fieldName;
115
+ const render = col.render;
116
+ const content = render
117
+ ? render(item)
118
+ : fieldName
119
+ ? String((item as Record<string, unknown>)[fieldName] ?? '')
120
+ : '';
121
+ return (
122
+ <StickyRowDataCell
123
+ key={`${page}-${index}-${colIndex}`}
124
+ content={content}
125
+ sticky={col.sticky}
126
+ width={col.width}
127
+ top={HEADER_ROW_HEIGHT * (index + 1)}
128
+ />
129
+ );
130
+ })}
131
+ <StickyRowFillCell top={HEADER_ROW_HEIGHT * (index + 1) + 1} />
132
+ </tr>
133
+ ))}
134
+ {itemsStatus === Status.SUCCESS &&
135
+ rows.map((item, rowIndex) => (
136
+ <tr
137
+ key={`${page}-${rowIndex}`}
138
+ data-selected={isRowHighlighted?.(item, rowIndex)}
139
+ onClick={() => onRowClick?.(item, rowIndex)}
140
+ >
141
+ {columnInfo.map((col, colIndex) => {
142
+ const fieldName = col.fieldName;
143
+ const render = col.render;
144
+ const content = render
145
+ ? render(item)
146
+ : fieldName
147
+ ? String((item as Record<string, unknown>)[fieldName] ?? '')
148
+ : '';
149
+ return (
150
+ <DataCell
151
+ key={`${page}-${rowIndex}-${colIndex}`}
152
+ content={content}
153
+ sticky={col.sticky}
154
+ width={col.width}
155
+ />
156
+ );
157
+ })}
158
+ <td key="fill" className="fill"></td>
159
+ </tr>
160
+ ))}
161
+ {itemsStatus === Status.PENDING && 'Loading...'}
162
+ {itemsStatus === Status.ERROR && 'Error loading data'}
163
+ <tr key="fill" className="fill"></tr>
164
+ </tbody>
165
+ {showFooter && (
166
+ <tfoot>
167
+ <tr>
168
+ {columnInfo.map((col, colIndex) => (
169
+ <FooterCell
170
+ key={`footer-${colIndex}`}
171
+ content={wrapContent(col.footer)}
172
+ sticky={col.sticky}
173
+ width={col.width}
174
+ />
175
+ ))}
176
+ <td key="footer-fill" className="fill"></td>
177
+ </tr>
178
+ </tfoot>
179
+ )}
180
+ </table>
181
+ </Box>
182
+ );
183
+ }
@@ -0,0 +1,27 @@
1
+ type BaseTableColumnProps<T> = {
2
+ heading: React.ReactNode;
3
+ footer?: React.ReactNode;
4
+ render?: (item: T) => React.ReactNode;
5
+ sticky?: 'start' | 'end';
6
+ width?: number;
7
+ };
8
+
9
+ type SortableTableColumnProps<T> = BaseTableColumnProps<T> & {
10
+ fieldName: string;
11
+ canSort: true;
12
+ };
13
+
14
+ type NonSortableTableColumnProps<T> = BaseTableColumnProps<T> & {
15
+ fieldName?: string;
16
+ canSort?: false;
17
+ };
18
+
19
+ export type TableColumnProps<T> =
20
+ | SortableTableColumnProps<T>
21
+ | NonSortableTableColumnProps<T>;
22
+
23
+ // This component never gets rendered. It is given as children to the
24
+ // table body as configuration
25
+ export function TableColumn<T>(_props: TableColumnProps<T>) {
26
+ return null;
27
+ }
@@ -0,0 +1,161 @@
1
+ import { StaticDataSource, StaticStringField } from '@tcn/resource-store';
2
+ import { Signal } from '@tcn/state';
3
+ import { beforeEach, describe, expect, it } from 'vitest';
4
+
5
+ import { TablePresenter } from './table_presenter.js';
6
+
7
+ type TestItem = {
8
+ name: string;
9
+ age: string;
10
+ };
11
+
12
+ describe('TablePresenter', () => {
13
+ let dataSource: StaticDataSource<TestItem>;
14
+ let mockColumnInfo: Array<{
15
+ width: number;
16
+ sortMode: 'none' | 'asc' | 'desc';
17
+ canSort: boolean;
18
+ heading: string;
19
+ fieldName?: string;
20
+ }>;
21
+
22
+ beforeEach(() => {
23
+ dataSource = new StaticDataSource<TestItem>(
24
+ [],
25
+ [
26
+ new StaticStringField('name', i => i.name),
27
+ new StaticStringField('age', i => i.age),
28
+ ]
29
+ );
30
+
31
+ mockColumnInfo = [
32
+ {
33
+ width: 100,
34
+ sortMode: 'none' as const,
35
+ canSort: true,
36
+ heading: 'Name',
37
+ fieldName: 'name',
38
+ },
39
+ {
40
+ width: 100,
41
+ sortMode: 'none' as const,
42
+ canSort: false,
43
+ heading: 'Age',
44
+ fieldName: 'age',
45
+ },
46
+ ];
47
+ });
48
+
49
+ describe('constructor', () => {
50
+ it('initializes with provided data source and columns', () => {
51
+ const presenter = new TablePresenter({
52
+ dataSource,
53
+ columns: mockColumnInfo,
54
+ });
55
+ const columnInfo = presenter.broadcasts.columnInfo.get();
56
+
57
+ expect(columnInfo).toEqual(mockColumnInfo);
58
+ });
59
+ });
60
+
61
+ describe('setColumnWidth', () => {
62
+ it('updates the width of the specified column', () => {
63
+ const presenter = new TablePresenter({
64
+ dataSource,
65
+ columns: mockColumnInfo,
66
+ });
67
+ const newWidth = 200;
68
+
69
+ presenter.setColumnWidth(0, newWidth);
70
+ const columnInfo = presenter.broadcasts.columnInfo.get();
71
+
72
+ expect(columnInfo[0].width).toBe(newWidth);
73
+ expect(columnInfo[1].width).toBe(mockColumnInfo[1].width);
74
+ });
75
+ });
76
+
77
+ describe('setColumnSortMode', () => {
78
+ it('updates sort mode and calls setFieldSort when fieldName exists', () => {
79
+ const presenter = new TablePresenter({
80
+ dataSource,
81
+ columns: mockColumnInfo,
82
+ });
83
+ const newSortMode = 'asc' as const;
84
+
85
+ presenter.setColumnSortMode(0, newSortMode);
86
+ const columnInfo = presenter.broadcasts.columnInfo.get();
87
+
88
+ expect(columnInfo[0].sortMode).toBe(newSortMode);
89
+ expect(dataSource.broadcasts.sortStatus.get()).toEqual([['name', 'asc']]);
90
+ });
91
+
92
+ it('does not update sort mode or call setFieldSort when fieldName is missing', () => {
93
+ const columnsWithoutFieldName = [
94
+ {
95
+ width: 100,
96
+ sortMode: 'none' as const,
97
+ canSort: true,
98
+ heading: 'Name',
99
+ },
100
+ ];
101
+ const presenter = new TablePresenter({
102
+ dataSource,
103
+ columns: columnsWithoutFieldName,
104
+ });
105
+ const newSortMode = 'asc' as const;
106
+
107
+ presenter.setColumnSortMode(0, newSortMode);
108
+ const columnInfo = presenter.broadcasts.columnInfo.get();
109
+
110
+ expect(columnInfo[0].sortMode).toBe('none');
111
+ expect(dataSource.broadcasts.sortStatus.get()).toEqual([]);
112
+ });
113
+ });
114
+
115
+ describe('setNextColumnSortMode', () => {
116
+ it('cycles through sort modes in order: none -> asc -> desc -> none', () => {
117
+ const presenter = new TablePresenter({
118
+ dataSource,
119
+ columns: mockColumnInfo,
120
+ });
121
+
122
+ // First click: none -> asc
123
+ presenter.setNextColumnSortMode(0);
124
+ expect(presenter.broadcasts.columnInfo.get()[0].sortMode).toBe('asc');
125
+ expect(dataSource.broadcasts.sortStatus.get()).toEqual([['name', 'asc']]);
126
+
127
+ // Second click: asc -> desc
128
+ presenter.setNextColumnSortMode(0);
129
+ expect(presenter.broadcasts.columnInfo.get()[0].sortMode).toBe('desc');
130
+ expect(dataSource.broadcasts.sortStatus.get()).toEqual([['name', 'desc']]);
131
+
132
+ // Third click: desc -> none
133
+ presenter.setNextColumnSortMode(0);
134
+ expect(presenter.broadcasts.columnInfo.get()[0].sortMode).toBe('none');
135
+ expect(dataSource.broadcasts.sortStatus.get()).toEqual([]);
136
+ });
137
+
138
+ it('does not cycle sort modes for unsortable columns', () => {
139
+ const presenter = new TablePresenter({
140
+ dataSource,
141
+ columns: mockColumnInfo,
142
+ });
143
+
144
+ presenter.setNextColumnSortMode(1);
145
+ expect(presenter.broadcasts.columnInfo.get()[1].sortMode).toBe('none');
146
+ expect(dataSource.broadcasts.sortStatus.get()).toEqual([]);
147
+ });
148
+ });
149
+
150
+ describe('broadcasts', () => {
151
+ it('provides access to column info signal', () => {
152
+ const presenter = new TablePresenter({
153
+ dataSource,
154
+ columns: mockColumnInfo,
155
+ });
156
+
157
+ expect(presenter.broadcasts.columnInfo).toBeInstanceOf(Signal);
158
+ expect(presenter.broadcasts.columnInfo.get()).toEqual(mockColumnInfo);
159
+ });
160
+ });
161
+ });
@@ -0,0 +1,103 @@
1
+ import { ReactNode, RefObject } from 'react';
2
+
3
+ import { DataSource } from '@tcn/resource-store';
4
+ import { ISubscription, Signal } from '@tcn/state';
5
+
6
+ type SortMode = 'none' | 'asc' | 'desc';
7
+
8
+ const sortModes: SortMode[] = ['none', 'asc', 'desc'];
9
+
10
+ export type ColumnInfo = {
11
+ width: number;
12
+ render?: (item: any) => ReactNode;
13
+ sortMode: SortMode;
14
+ canSort: boolean;
15
+ heading: ReactNode;
16
+ footer?: ReactNode;
17
+ sticky?: 'start' | 'end';
18
+ fieldName?: string;
19
+ };
20
+
21
+ export class TablePresenter {
22
+ private _columnInfo = new Signal<ColumnInfo[]>([]);
23
+ private _dataSource: DataSource<any>;
24
+ private _showFooter = new Signal<boolean>(false);
25
+ private _broadcasts = {
26
+ columnInfo: this._columnInfo.broadcast,
27
+ showFooter: this._showFooter.broadcast,
28
+ };
29
+
30
+ private _scrollerRef?: RefObject<HTMLDivElement>;
31
+
32
+ // holding a reference to this subscription prevents it from being garbage collected
33
+ private _pageSubscription?: ISubscription<number>;
34
+
35
+ constructor({
36
+ dataSource,
37
+ columns,
38
+ scrollerRef,
39
+ }: {
40
+ dataSource: DataSource<any>;
41
+ columns: ColumnInfo[];
42
+ scrollerRef?: RefObject<HTMLDivElement>;
43
+ }) {
44
+ this._dataSource = dataSource;
45
+ this._columnInfo.set(columns);
46
+ this._scrollerRef = scrollerRef;
47
+
48
+ const showFooter = columns.some(column => column.footer);
49
+ this._showFooter.set(showFooter);
50
+
51
+ this._pageSubscription = this._dataSource.broadcasts.currentPageIndex.subscribe(_ => {
52
+ if (this._scrollerRef?.current) {
53
+ this._scrollerRef.current.scrollTo({ top: 0 });
54
+ }
55
+ });
56
+ }
57
+
58
+ get broadcasts() {
59
+ return this._broadcasts;
60
+ }
61
+
62
+ setColumnWidth(index: number, width: number) {
63
+ this._columnInfo.transform(columnInfo => {
64
+ columnInfo[index].width = width;
65
+ return columnInfo;
66
+ });
67
+ }
68
+
69
+ setColumnSortMode(index: number, sortMode: SortMode) {
70
+ const column = this._columnInfo.get()[index];
71
+ const fieldName = column.fieldName;
72
+ if (!column.canSort) {
73
+ return;
74
+ }
75
+
76
+ if (!fieldName) {
77
+ // can't sort a field without a name
78
+ return;
79
+ }
80
+
81
+ this._dataSource.setFieldSort(fieldName, sortMode);
82
+
83
+ this._columnInfo.transform(columnInfo => {
84
+ columnInfo[index].sortMode = sortMode;
85
+ return columnInfo;
86
+ });
87
+ }
88
+
89
+ setNextColumnSortMode(index: number) {
90
+ const column = this._columnInfo.get()[index];
91
+
92
+ const currentMode = column.sortMode;
93
+ const currentIndex = sortModes.indexOf(currentMode);
94
+ const nextIndex = (currentIndex + 1) % sortModes.length;
95
+ const nextMode = sortModes[nextIndex];
96
+
97
+ this.setColumnSortMode(index, nextMode);
98
+ }
99
+
100
+ dispose() {
101
+ this._pageSubscription?.unsubscribe();
102
+ }
103
+ }
@@ -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 { DatePickerInput } from '@tcn/ui/inputs';
5
+ import { Box, HStack, VStack } from '@tcn/ui/stacks';
6
+ import { BodyText, Title } from '@tcn/ui/typography';
7
+ import React from 'react';
8
+ import { useFieldFilterStrategy } from './use_field_filter_strategy.js';
9
+ import { DateFieldFilterPresenter } from './date_field_filter_presenter.js';
10
+
11
+ export function DateFieldFilter({
12
+ fieldName,
13
+ label,
14
+ }: {
15
+ fieldName: string;
16
+ label: string;
17
+ }) {
18
+ const presenter = useFieldFilterStrategy(DateFieldFilterPresenter, fieldName);
19
+
20
+ const startDate = useSignalValue(presenter.broadcasts.startDate);
21
+ const endDate = useSignalValue(presenter.broadcasts.endDate);
22
+
23
+ return (
24
+ <VStack gap="4px">
25
+ <Box width="flex">
26
+ <Title size="md">{label}</Title>
27
+ </Box>
28
+ <HStack gap="4px">
29
+ <Box width="auto">
30
+ <BodyText size="lg">Start</BodyText>
31
+ </Box>
32
+ <Box width="flex">
33
+ <DatePickerInput
34
+ value={startDate}
35
+ onChange={value => presenter.setStartDate(value)}
36
+ />
37
+ </Box>
38
+ <Box width="auto">
39
+ <Button
40
+ onClick={() => presenter.setStartDate(null)}
41
+ hierarchy="tertiary"
42
+ disabled={startDate == null}
43
+ >
44
+ <CrossCircleIcon />
45
+ </Button>
46
+ </Box>
47
+ </HStack>
48
+ <HStack gap="4px">
49
+ <Box width="auto">
50
+ <BodyText size="lg">End</BodyText>
51
+ </Box>
52
+ <Box width="flex">
53
+ <DatePickerInput
54
+ value={endDate}
55
+ onChange={value => presenter.setEndDate(value)}
56
+ />
57
+ </Box>
58
+ <Box width="auto">
59
+ <Button
60
+ onClick={() => presenter.setEndDate(null)}
61
+ hierarchy="tertiary"
62
+ disabled={endDate == null}
63
+ >
64
+ <CrossCircleIcon />
65
+ </Button>
66
+ </Box>
67
+ </HStack>
68
+ </VStack>
69
+ );
70
+ }