@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.
- package/README.md +1 -1
- package/dist/cell.css +1 -0
- package/dist/cell.module-WpHnQBVu.js +5 -0
- package/dist/cell.module-WpHnQBVu.js.map +1 -0
- package/dist/components/cells/data_cell.d.ts +3 -2
- package/dist/components/cells/data_cell.d.ts.map +1 -0
- package/dist/components/cells/data_cell.js +18 -10
- package/dist/components/cells/data_cell.js.map +1 -1
- package/dist/components/cells/footer_cell.d.ts +3 -2
- package/dist/components/cells/footer_cell.d.ts.map +1 -0
- package/dist/components/cells/footer_cell.js +18 -10
- package/dist/components/cells/footer_cell.js.map +1 -1
- package/dist/components/cells/header_cell.d.ts +3 -2
- package/dist/components/cells/header_cell.d.ts.map +1 -0
- package/dist/components/cells/header_cell.js +52 -18
- package/dist/components/cells/header_cell.js.map +1 -1
- package/dist/components/cells/sticky_row_data_cell.d.ts +3 -2
- package/dist/components/cells/sticky_row_data_cell.d.ts.map +1 -0
- package/dist/components/cells/sticky_row_data_cell.js +26 -11
- package/dist/components/cells/sticky_row_data_cell.js.map +1 -1
- package/dist/components/cells/sticky_row_fill_cell.d.ts +2 -2
- package/dist/components/cells/sticky_row_fill_cell.d.ts.map +1 -0
- package/dist/components/cells/sticky_row_fill_cell.js +15 -5
- package/dist/components/cells/sticky_row_fill_cell.js.map +1 -1
- package/dist/components/global_search.d.ts +2 -2
- package/dist/components/global_search.d.ts.map +1 -0
- package/dist/components/global_search.js +26 -9
- package/dist/components/global_search.js.map +1 -1
- package/dist/components/global_search_presenter.d.ts +2 -1
- package/dist/components/global_search_presenter.d.ts.map +1 -0
- package/dist/components/global_search_presenter.js +20 -18
- package/dist/components/global_search_presenter.js.map +1 -1
- package/dist/components/table/table.d.ts +3 -2
- package/dist/components/table/table.d.ts.map +1 -0
- package/dist/components/table/table.js +140 -77
- package/dist/components/table/table.js.map +1 -1
- package/dist/components/table/table_column.d.ts +1 -1
- package/dist/components/table/table_column.d.ts.map +1 -0
- package/dist/components/table/table_column.js +6 -5
- package/dist/components/table/table_column.js.map +1 -1
- package/dist/components/table/table_presenter.d.ts +3 -2
- package/dist/components/table/table_presenter.d.ts.map +1 -0
- package/dist/components/table/table_presenter.js +45 -62
- package/dist/components/table/table_presenter.js.map +1 -1
- package/dist/components/table_filter_panel/field_filters/date_field_filter.d.ts +2 -2
- package/dist/components/table_filter_panel/field_filters/date_field_filter.d.ts.map +1 -0
- package/dist/components/table_filter_panel/field_filters/date_field_filter.js +59 -33
- package/dist/components/table_filter_panel/field_filters/date_field_filter.js.map +1 -1
- package/dist/components/table_filter_panel/field_filters/date_field_filter_presenter.d.ts +4 -3
- package/dist/components/table_filter_panel/field_filters/date_field_filter_presenter.d.ts.map +1 -0
- package/dist/components/table_filter_panel/field_filters/date_field_filter_presenter.js +57 -91
- package/dist/components/table_filter_panel/field_filters/date_field_filter_presenter.js.map +1 -1
- package/dist/components/table_filter_panel/field_filters/field_filter_props.d.ts +1 -0
- package/dist/components/table_filter_panel/field_filters/field_filter_props.d.ts.map +1 -0
- package/dist/components/table_filter_panel/field_filters/field_filter_strategy.d.ts +1 -0
- package/dist/components/table_filter_panel/field_filters/field_filter_strategy.d.ts.map +1 -0
- package/dist/components/table_filter_panel/field_filters/mulit_select_field_filter.d.ts +3 -3
- package/dist/components/table_filter_panel/field_filters/mulit_select_field_filter.d.ts.map +1 -0
- package/dist/components/table_filter_panel/field_filters/mulit_select_field_filter.js +52 -29
- package/dist/components/table_filter_panel/field_filters/mulit_select_field_filter.js.map +1 -1
- package/dist/components/table_filter_panel/field_filters/multi_select_field_filter_presenter.d.ts +3 -2
- package/dist/components/table_filter_panel/field_filters/multi_select_field_filter_presenter.d.ts.map +1 -0
- package/dist/components/table_filter_panel/field_filters/multi_select_field_filter_presenter.js +53 -70
- package/dist/components/table_filter_panel/field_filters/multi_select_field_filter_presenter.js.map +1 -1
- package/dist/components/table_filter_panel/field_filters/number_field_filter.d.ts +3 -3
- package/dist/components/table_filter_panel/field_filters/number_field_filter.d.ts.map +1 -0
- package/dist/components/table_filter_panel/field_filters/number_field_filter.js +47 -23
- package/dist/components/table_filter_panel/field_filters/number_field_filter.js.map +1 -1
- package/dist/components/table_filter_panel/field_filters/number_field_filter_presenter.d.ts +5 -4
- package/dist/components/table_filter_panel/field_filters/number_field_filter_presenter.d.ts.map +1 -0
- package/dist/components/table_filter_panel/field_filters/number_field_filter_presenter.js +53 -58
- package/dist/components/table_filter_panel/field_filters/number_field_filter_presenter.js.map +1 -1
- package/dist/components/table_filter_panel/field_filters/number_range_field_filter.d.ts +2 -2
- package/dist/components/table_filter_panel/field_filters/number_range_field_filter.d.ts.map +1 -0
- package/dist/components/table_filter_panel/field_filters/number_range_field_filter.js +61 -31
- package/dist/components/table_filter_panel/field_filters/number_range_field_filter.js.map +1 -1
- package/dist/components/table_filter_panel/field_filters/number_range_field_filter_presenter.d.ts +4 -3
- package/dist/components/table_filter_panel/field_filters/number_range_field_filter_presenter.d.ts.map +1 -0
- package/dist/components/table_filter_panel/field_filters/number_range_field_filter_presenter.js +57 -91
- package/dist/components/table_filter_panel/field_filters/number_range_field_filter_presenter.js.map +1 -1
- package/dist/components/table_filter_panel/field_filters/select_field_filter.d.ts +3 -3
- package/dist/components/table_filter_panel/field_filters/select_field_filter.d.ts.map +1 -0
- package/dist/components/table_filter_panel/field_filters/select_field_filter.js +49 -24
- package/dist/components/table_filter_panel/field_filters/select_field_filter.js.map +1 -1
- package/dist/components/table_filter_panel/field_filters/select_field_filter_presenter.d.ts +3 -2
- package/dist/components/table_filter_panel/field_filters/select_field_filter_presenter.d.ts.map +1 -0
- package/dist/components/table_filter_panel/field_filters/select_field_filter_presenter.js +49 -53
- package/dist/components/table_filter_panel/field_filters/select_field_filter_presenter.js.map +1 -1
- package/dist/components/table_filter_panel/field_filters/string_field_filter.d.ts +3 -3
- package/dist/components/table_filter_panel/field_filters/string_field_filter.d.ts.map +1 -0
- package/dist/components/table_filter_panel/field_filters/string_field_filter.js +62 -33
- package/dist/components/table_filter_panel/field_filters/string_field_filter.js.map +1 -1
- package/dist/components/table_filter_panel/field_filters/string_field_filter_presenter.d.ts +5 -4
- package/dist/components/table_filter_panel/field_filters/string_field_filter_presenter.d.ts.map +1 -0
- package/dist/components/table_filter_panel/field_filters/string_field_filter_presenter.js +54 -59
- package/dist/components/table_filter_panel/field_filters/string_field_filter_presenter.js.map +1 -1
- package/dist/components/table_filter_panel/field_filters/use_field_filter_strategy.d.ts +2 -1
- package/dist/components/table_filter_panel/field_filters/use_field_filter_strategy.d.ts.map +1 -0
- package/dist/components/table_filter_panel/field_filters/use_field_filter_strategy.js +13 -19
- package/dist/components/table_filter_panel/field_filters/use_field_filter_strategy.js.map +1 -1
- package/dist/components/table_filter_panel/table_filter_panel.d.ts +5 -4
- package/dist/components/table_filter_panel/table_filter_panel.d.ts.map +1 -0
- package/dist/components/table_filter_panel/table_filter_panel.js +15 -11
- package/dist/components/table_filter_panel/table_filter_panel.js.map +1 -1
- package/dist/components/table_filter_panel/table_filter_panel_presenter.d.ts +2 -2
- package/dist/components/table_filter_panel/table_filter_panel_presenter.d.ts.map +1 -0
- package/dist/components/table_filter_panel/table_filter_panel_presenter.js +45 -62
- package/dist/components/table_filter_panel/table_filter_panel_presenter.js.map +1 -1
- package/dist/components/table_filter_panel/types.d.ts +1 -0
- package/dist/components/table_filter_panel/types.d.ts.map +1 -0
- package/dist/components/table_filter_panel/types.js +5 -2
- package/dist/components/table_filter_panel/types.js.map +1 -1
- package/dist/components/table_pager.d.ts +2 -2
- package/dist/components/table_pager.d.ts.map +1 -0
- package/dist/components/table_pager.js +22 -20
- package/dist/components/table_pager.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -13
- package/dist/index.js.map +1 -1
- package/dist/table.css +1 -0
- package/dist/table_pager.css +1 -0
- package/package.json +61 -61
- package/src/__stories__/aip_table.stories.tsx +190 -0
- package/src/__stories__/auth_provider.tsx +14 -0
- package/src/__stories__/demo.stories.tsx +137 -0
- package/src/__stories__/sample_data.ts +1398 -0
- package/src/__stories__/table.stories.tsx +423 -0
- package/src/__tests__/sanity.test.ts +7 -0
- package/src/components/cells/data_cell.tsx +25 -0
- package/src/components/cells/footer_cell.tsx +25 -0
- package/src/components/cells/header_cell.tsx +77 -0
- package/src/components/cells/sticky_row_data_cell.tsx +31 -0
- package/src/components/cells/sticky_row_fill_cell.tsx +16 -0
- package/src/components/global_search.tsx +33 -0
- package/src/components/global_search_presenter.ts +24 -0
- package/{dist → src}/components/table/table.module.css +3 -2
- package/src/components/table/table.tsx +183 -0
- package/src/components/table/table_column.tsx +27 -0
- package/src/components/table/table_presenter.test.ts +161 -0
- package/src/components/table/table_presenter.ts +103 -0
- package/src/components/table_filter_panel/field_filters/date_field_filter.tsx +70 -0
- package/src/components/table_filter_panel/field_filters/date_field_filter_presenter.test.ts +583 -0
- package/src/components/table_filter_panel/field_filters/date_field_filter_presenter.ts +110 -0
- package/src/components/table_filter_panel/field_filters/field_filter_props.ts +5 -0
- package/src/components/table_filter_panel/field_filters/field_filter_strategy.ts +14 -0
- package/src/components/table_filter_panel/field_filters/mulit_select_field_filter.tsx +68 -0
- package/src/components/table_filter_panel/field_filters/multi_select_field_filter_presenter.test.ts +444 -0
- package/src/components/table_filter_panel/field_filters/multi_select_field_filter_presenter.ts +90 -0
- package/src/components/table_filter_panel/field_filters/number_field_filter.tsx +53 -0
- package/src/components/table_filter_panel/field_filters/number_field_filter_presenter.test.ts +431 -0
- package/src/components/table_filter_panel/field_filters/number_field_filter_presenter.ts +80 -0
- package/src/components/table_filter_panel/field_filters/number_range_field_filter.tsx +68 -0
- package/src/components/table_filter_panel/field_filters/number_range_field_filter_presenter.test.ts +582 -0
- package/src/components/table_filter_panel/field_filters/number_range_field_filter_presenter.ts +110 -0
- package/src/components/table_filter_panel/field_filters/select_field_filter.tsx +57 -0
- package/src/components/table_filter_panel/field_filters/select_field_filter_presenter.test.ts +365 -0
- package/src/components/table_filter_panel/field_filters/select_field_filter_presenter.ts +74 -0
- package/src/components/table_filter_panel/field_filters/string_field_filter.tsx +70 -0
- package/src/components/table_filter_panel/field_filters/string_field_filter_presenter.test.ts +296 -0
- package/src/components/table_filter_panel/field_filters/string_field_filter_presenter.ts +81 -0
- package/src/components/table_filter_panel/field_filters/use_field_filter_strategy.tsx +30 -0
- package/src/components/table_filter_panel/table_filter_panel.stories.tsx +46 -0
- package/src/components/table_filter_panel/table_filter_panel.tsx +26 -0
- package/src/components/table_filter_panel/table_filter_panel_presenter.ts +77 -0
- package/src/components/table_filter_panel/types.ts +3 -0
- package/src/components/table_pager.tsx +39 -0
- package/src/index.ts +16 -0
- package/tsconfig.json +36 -0
- package/types/file_types.d.ts +54 -0
- package/types/react_color.d.ts +61 -0
- package/dist/__stories__/aip_table.stories.d.ts +0 -5
- package/dist/__stories__/aip_table.stories.js +0 -96
- package/dist/__stories__/aip_table.stories.js.map +0 -1
- package/dist/__stories__/auth_provider.d.ts +0 -4
- package/dist/__stories__/auth_provider.js +0 -10
- package/dist/__stories__/auth_provider.js.map +0 -1
- package/dist/__stories__/demo.stories.d.ts +0 -6
- package/dist/__stories__/demo.stories.js +0 -94
- package/dist/__stories__/demo.stories.js.map +0 -1
- package/dist/__stories__/sample_data.d.ts +0 -36
- package/dist/__stories__/sample_data.js +0 -1385
- package/dist/__stories__/sample_data.js.map +0 -1
- package/dist/__stories__/table.stories.d.ts +0 -12
- package/dist/__stories__/table.stories.js +0 -272
- package/dist/__stories__/table.stories.js.map +0 -1
- package/dist/components/table/table_presenter.test.d.ts +0 -1
- package/dist/components/table/table_presenter.test.js +0 -125
- package/dist/components/table/table_presenter.test.js.map +0 -1
- package/dist/components/table_filter_panel/field_filters/date_field_filter_presenter.test.d.ts +0 -1
- package/dist/components/table_filter_panel/field_filters/date_field_filter_presenter.test.js +0 -434
- package/dist/components/table_filter_panel/field_filters/date_field_filter_presenter.test.js.map +0 -1
- package/dist/components/table_filter_panel/field_filters/field_filter_props.js +0 -2
- package/dist/components/table_filter_panel/field_filters/field_filter_props.js.map +0 -1
- package/dist/components/table_filter_panel/field_filters/field_filter_strategy.js +0 -2
- package/dist/components/table_filter_panel/field_filters/field_filter_strategy.js.map +0 -1
- package/dist/components/table_filter_panel/field_filters/multi_select_field_filter_presenter.test.d.ts +0 -1
- package/dist/components/table_filter_panel/field_filters/multi_select_field_filter_presenter.test.js +0 -332
- package/dist/components/table_filter_panel/field_filters/multi_select_field_filter_presenter.test.js.map +0 -1
- package/dist/components/table_filter_panel/field_filters/number_field_filter_presenter.test.d.ts +0 -1
- package/dist/components/table_filter_panel/field_filters/number_field_filter_presenter.test.js +0 -347
- package/dist/components/table_filter_panel/field_filters/number_field_filter_presenter.test.js.map +0 -1
- package/dist/components/table_filter_panel/field_filters/number_range_field_filter_presenter.test.d.ts +0 -1
- package/dist/components/table_filter_panel/field_filters/number_range_field_filter_presenter.test.js +0 -452
- package/dist/components/table_filter_panel/field_filters/number_range_field_filter_presenter.test.js.map +0 -1
- package/dist/components/table_filter_panel/field_filters/select_field_filter_presenter.test.d.ts +0 -1
- package/dist/components/table_filter_panel/field_filters/select_field_filter_presenter.test.js +0 -285
- package/dist/components/table_filter_panel/field_filters/select_field_filter_presenter.test.js.map +0 -1
- package/dist/components/table_filter_panel/field_filters/string_field_filter_presenter.test.d.ts +0 -1
- package/dist/components/table_filter_panel/field_filters/string_field_filter_presenter.test.js +0 -232
- package/dist/components/table_filter_panel/field_filters/string_field_filter_presenter.test.js.map +0 -1
- package/dist/components/table_filter_panel/table_filter_panel.stories.d.ts +0 -6
- package/dist/components/table_filter_panel/table_filter_panel.stories.js +0 -25
- package/dist/components/table_filter_panel/table_filter_panel.stories.js.map +0 -1
- /package/{dist → src}/__stories__/table.module.css +0 -0
- /package/{dist → src}/components/cells/cell.module.css +0 -0
- /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
|
+
}
|