@tcn/ui-transfer 1.0.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/AI_USAGE.md +16 -0
- package/README.md +3 -0
- package/dist/components/available_list/available_list.d.ts +11 -0
- package/dist/components/available_list/available_list.d.ts.map +1 -0
- package/dist/components/available_list/available_list.js +22 -0
- package/dist/components/available_list/available_list.js.map +1 -0
- package/dist/components/available_list/index.d.ts +2 -0
- package/dist/components/available_list/index.d.ts.map +1 -0
- package/dist/components/available_list/index.js +5 -0
- package/dist/components/available_list/index.js.map +1 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +13 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/manage_list_item/addable_item.d.ts +5 -0
- package/dist/components/manage_list_item/addable_item.d.ts.map +1 -0
- package/dist/components/manage_list_item/addable_item.js +19 -0
- package/dist/components/manage_list_item/addable_item.js.map +1 -0
- package/dist/components/manage_list_item/index.d.ts +4 -0
- package/dist/components/manage_list_item/index.d.ts.map +1 -0
- package/dist/components/manage_list_item/index.js +9 -0
- package/dist/components/manage_list_item/index.js.map +1 -0
- package/dist/components/manage_list_item/manage_list_item.d.ts +11 -0
- package/dist/components/manage_list_item/manage_list_item.d.ts.map +1 -0
- package/dist/components/manage_list_item/manage_list_item.js +33 -0
- package/dist/components/manage_list_item/manage_list_item.js.map +1 -0
- package/dist/components/manage_list_item/removable_item.d.ts +5 -0
- package/dist/components/manage_list_item/removable_item.d.ts.map +1 -0
- package/dist/components/manage_list_item/removable_item.js +22 -0
- package/dist/components/manage_list_item/removable_item.js.map +1 -0
- package/dist/components/selected_list/index.d.ts +2 -0
- package/dist/components/selected_list/index.d.ts.map +1 -0
- package/dist/components/selected_list/index.js +5 -0
- package/dist/components/selected_list/index.js.map +1 -0
- package/dist/components/selected_list/selected_list.d.ts +13 -0
- package/dist/components/selected_list/selected_list.d.ts.map +1 -0
- package/dist/components/selected_list/selected_list.js +28 -0
- package/dist/components/selected_list/selected_list.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/removable_item.css +1 -0
- package/dist/transfer_list/index.d.ts +2 -0
- package/dist/transfer_list/index.d.ts.map +1 -0
- package/dist/transfer_list/index.js +5 -0
- package/dist/transfer_list/index.js.map +1 -0
- package/dist/transfer_list/transfer_list.d.ts +6 -0
- package/dist/transfer_list/transfer_list.d.ts.map +1 -0
- package/dist/transfer_list/transfer_list.js +12 -0
- package/dist/transfer_list/transfer_list.js.map +1 -0
- package/dist/transfer_table/components/available_table.d.ts +17 -0
- package/dist/transfer_table/components/available_table.d.ts.map +1 -0
- package/dist/transfer_table/components/available_table.js +68 -0
- package/dist/transfer_table/components/available_table.js.map +1 -0
- package/dist/transfer_table/components/available_table_filter_panel.d.ts +13 -0
- package/dist/transfer_table/components/available_table_filter_panel.d.ts.map +1 -0
- package/dist/transfer_table/components/available_table_filter_panel.js +39 -0
- package/dist/transfer_table/components/available_table_filter_panel.js.map +1 -0
- package/dist/transfer_table/components/available_table_header.d.ts +13 -0
- package/dist/transfer_table/components/available_table_header.d.ts.map +1 -0
- package/dist/transfer_table/components/available_table_header.js +43 -0
- package/dist/transfer_table/components/available_table_header.js.map +1 -0
- package/dist/transfer_table/components/selected_column.d.ts +15 -0
- package/dist/transfer_table/components/selected_column.d.ts.map +1 -0
- package/dist/transfer_table/components/selected_column.js +93 -0
- package/dist/transfer_table/components/selected_column.js.map +1 -0
- package/dist/transfer_table/components/selected_item.d.ts +8 -0
- package/dist/transfer_table/components/selected_item.d.ts.map +1 -0
- package/dist/transfer_table/components/selected_item.js +39 -0
- package/dist/transfer_table/components/selected_item.js.map +1 -0
- package/dist/transfer_table/index.d.ts +4 -0
- package/dist/transfer_table/index.d.ts.map +1 -0
- package/dist/transfer_table/index.js +7 -0
- package/dist/transfer_table/index.js.map +1 -0
- package/dist/transfer_table/transfer_table.d.ts +4 -0
- package/dist/transfer_table/transfer_table.d.ts.map +1 -0
- package/dist/transfer_table/transfer_table.js +82 -0
- package/dist/transfer_table/transfer_table.js.map +1 -0
- package/dist/transfer_table/transfer_table_presenter.d.ts +46 -0
- package/dist/transfer_table/transfer_table_presenter.d.ts.map +1 -0
- package/dist/transfer_table/transfer_table_presenter.js +98 -0
- package/dist/transfer_table/transfer_table_presenter.js.map +1 -0
- package/dist/transfer_table/types.d.ts +30 -0
- package/dist/transfer_table/types.d.ts.map +1 -0
- package/dist/transfer_table/types.js +2 -0
- package/dist/transfer_table/types.js.map +1 -0
- package/dist/transfer_table.css +1 -0
- package/dist/transfer_table.module-CI4PvlY3.js +5 -0
- package/dist/transfer_table.module-CI4PvlY3.js.map +1 -0
- package/package.json +81 -0
- package/src/__stories__/available_list.stories.tsx +41 -0
- package/src/__stories__/sample_data.ts +101 -0
- package/src/__stories__/selected_list.stories.tsx +41 -0
- package/src/__stories__/transfer_list.stories.tsx +13 -0
- package/src/__stories__/transfer_table.stories.tsx +128 -0
- package/src/__tests__/sanity.test.ts +7 -0
- package/src/components/available_list/available_list.tsx +39 -0
- package/src/components/available_list/index.ts +5 -0
- package/src/components/index.ts +19 -0
- package/src/components/manage_list_item/addable_item.tsx +22 -0
- package/src/components/manage_list_item/index.ts +7 -0
- package/src/components/manage_list_item/manage_list_item.tsx +43 -0
- package/src/components/manage_list_item/removable_item.module.css +3 -0
- package/src/components/manage_list_item/removable_item.tsx +25 -0
- package/src/components/selected_list/index.ts +5 -0
- package/src/components/selected_list/selected_list.tsx +50 -0
- package/src/index.ts +22 -0
- package/src/transfer_list/index.ts +1 -0
- package/src/transfer_list/transfer_list.tsx +14 -0
- package/src/transfer_table/components/available_table.tsx +80 -0
- package/src/transfer_table/components/available_table_filter_panel.tsx +62 -0
- package/src/transfer_table/components/available_table_header.tsx +60 -0
- package/src/transfer_table/components/selected_column.tsx +120 -0
- package/src/transfer_table/components/selected_item.tsx +58 -0
- package/src/transfer_table/index.ts +6 -0
- package/src/transfer_table/transfer_table.module.css +41 -0
- package/src/transfer_table/transfer_table.tsx +77 -0
- package/src/transfer_table/transfer_table_presenter.ts +164 -0
- package/src/transfer_table/types.ts +35 -0
- package/tsconfig.json +7 -0
- package/types/file_types.d.ts +106 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TransferList, type TransferListProps } from './transfer_list.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Scaffold, type ScaffoldProps } from '@tcn/ui/layouts';
|
|
3
|
+
|
|
4
|
+
export interface TransferListProps extends ScaffoldProps {}
|
|
5
|
+
|
|
6
|
+
export const TransferList = React.forwardRef<HTMLElement, TransferListProps>(
|
|
7
|
+
function TransferList({ children, ...props }: TransferListProps, ref) {
|
|
8
|
+
return (
|
|
9
|
+
<Scaffold ref={ref} {...props}>
|
|
10
|
+
{children}
|
|
11
|
+
</Scaffold>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { clsx } from 'clsx';
|
|
2
|
+
import { ArrowRightIcon } from '@tcn/icons/arrow_right_icon.js';
|
|
3
|
+
import { DataSource } from '@tcn/resource-store';
|
|
4
|
+
import { IBroadcast } from '@tcn/state';
|
|
5
|
+
import { FieldFilterProps, Table, TableColumn, TableColumnProps } from '@tcn/ui-table';
|
|
6
|
+
import { Button } from '@tcn/ui/actions';
|
|
7
|
+
import { Rail, Scaffold, type ScaffoldProps } from '@tcn/ui/layouts';
|
|
8
|
+
import React, { ReactElement } from 'react';
|
|
9
|
+
import styles from '../transfer_table.module.css';
|
|
10
|
+
import { AvailableTableFilterPanel } from './available_table_filter_panel.js';
|
|
11
|
+
import { AvailableTableHeader } from './available_table_header.js';
|
|
12
|
+
|
|
13
|
+
export interface AvailableTableOwnProps<T> {
|
|
14
|
+
dataSource: DataSource<T>;
|
|
15
|
+
columns: TableColumnProps<T>[];
|
|
16
|
+
filterPanelIsOpenBroadcast: IBroadcast<boolean>;
|
|
17
|
+
filterChildren?: ReactElement<FieldFilterProps> | ReactElement<FieldFilterProps>[];
|
|
18
|
+
onClickMove: (item: T) => void;
|
|
19
|
+
onClickFilterPanelToggle: () => void;
|
|
20
|
+
onClickMoveAll: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type AvailableTableProps<T> = AvailableTableOwnProps<T> & ScaffoldProps;
|
|
24
|
+
|
|
25
|
+
export const AvailableTable = <T,>({
|
|
26
|
+
dataSource,
|
|
27
|
+
columns,
|
|
28
|
+
filterPanelIsOpenBroadcast,
|
|
29
|
+
filterChildren,
|
|
30
|
+
onClickMove,
|
|
31
|
+
onClickFilterPanelToggle,
|
|
32
|
+
onClickMoveAll,
|
|
33
|
+
className,
|
|
34
|
+
...props
|
|
35
|
+
}: AvailableTableProps<T>) => {
|
|
36
|
+
return (
|
|
37
|
+
<Scaffold
|
|
38
|
+
className={clsx(
|
|
39
|
+
styles['tcn-table-available-table'],
|
|
40
|
+
'tcn-table-available-table',
|
|
41
|
+
className
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
|
+
<AvailableTableHeader
|
|
46
|
+
dataSource={dataSource}
|
|
47
|
+
filterChildren={filterChildren}
|
|
48
|
+
onClickFilterPanelToggle={onClickFilterPanelToggle}
|
|
49
|
+
onClickMoveAll={onClickMoveAll}
|
|
50
|
+
/>
|
|
51
|
+
<Rail>
|
|
52
|
+
<AvailableTableFilterPanel
|
|
53
|
+
dataSource={dataSource}
|
|
54
|
+
filterPanelIsOpenBroadcast={filterPanelIsOpenBroadcast}
|
|
55
|
+
filterChildren={filterChildren}
|
|
56
|
+
onClickFilterPanelToggle={onClickFilterPanelToggle}
|
|
57
|
+
/>
|
|
58
|
+
{columns?.length > 0 ? (
|
|
59
|
+
<Table minWidth={200} dataSource={dataSource} className={styles['table-body']}>
|
|
60
|
+
{[
|
|
61
|
+
...columns.map((columnProps, index) => (
|
|
62
|
+
<TableColumn key={columnProps.fieldName ?? index} {...columnProps} />
|
|
63
|
+
)),
|
|
64
|
+
<TableColumn
|
|
65
|
+
key="actions"
|
|
66
|
+
heading="Actions"
|
|
67
|
+
sticky="end"
|
|
68
|
+
render={(item: T) => (
|
|
69
|
+
<Button utility hierarchy="tertiary" onClick={() => onClickMove(item)}>
|
|
70
|
+
<ArrowRightIcon />
|
|
71
|
+
</Button>
|
|
72
|
+
)}
|
|
73
|
+
/>,
|
|
74
|
+
]}
|
|
75
|
+
</Table>
|
|
76
|
+
) : null}
|
|
77
|
+
</Rail>
|
|
78
|
+
</Scaffold>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { DataSource } from '@tcn/resource-store';
|
|
2
|
+
import { IBroadcast, useSignalValue } from '@tcn/state';
|
|
3
|
+
import {
|
|
4
|
+
FieldFilterProps,
|
|
5
|
+
TableFilterPanel,
|
|
6
|
+
type TableFilterPanelProps,
|
|
7
|
+
} from '@tcn/ui-table';
|
|
8
|
+
import { Resizable, ResizeHandle } from '@tcn/ui/utils';
|
|
9
|
+
import React, { ReactElement } from 'react';
|
|
10
|
+
|
|
11
|
+
export interface AvailableTableFilterPanelOwnProps<T> {
|
|
12
|
+
dataSource: DataSource<T>;
|
|
13
|
+
filterPanelIsOpenBroadcast: IBroadcast<boolean>;
|
|
14
|
+
filterChildren?: ReactElement<FieldFilterProps> | ReactElement<FieldFilterProps>[];
|
|
15
|
+
onClickFilterPanelToggle: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type AvailableTableFilterPanelProps<T> = AvailableTableFilterPanelOwnProps<T> &
|
|
19
|
+
Omit<TableFilterPanelProps, 'children' | 'dataSource' | 'onClose'>;
|
|
20
|
+
|
|
21
|
+
function AvailableTableFilterPanelInner<T>(
|
|
22
|
+
{
|
|
23
|
+
dataSource,
|
|
24
|
+
filterPanelIsOpenBroadcast,
|
|
25
|
+
filterChildren,
|
|
26
|
+
onClickFilterPanelToggle,
|
|
27
|
+
minWidth = '200px',
|
|
28
|
+
maxWidth = '400px',
|
|
29
|
+
width = '296px',
|
|
30
|
+
...props
|
|
31
|
+
}: AvailableTableFilterPanelProps<T>,
|
|
32
|
+
ref: React.ForwardedRef<HTMLElement>
|
|
33
|
+
) {
|
|
34
|
+
const filterPanelIsOpen = useSignalValue(filterPanelIsOpenBroadcast);
|
|
35
|
+
|
|
36
|
+
if (!filterPanelIsOpen || !filterChildren) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Resizable>
|
|
42
|
+
<TableFilterPanel
|
|
43
|
+
ref={ref}
|
|
44
|
+
dataSource={dataSource}
|
|
45
|
+
onClose={onClickFilterPanelToggle}
|
|
46
|
+
minWidth={minWidth}
|
|
47
|
+
maxWidth={maxWidth}
|
|
48
|
+
width={width}
|
|
49
|
+
{...props}
|
|
50
|
+
>
|
|
51
|
+
{filterChildren}
|
|
52
|
+
</TableFilterPanel>
|
|
53
|
+
<ResizeHandle position="end" />
|
|
54
|
+
</Resizable>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const AvailableTableFilterPanel = React.forwardRef(
|
|
59
|
+
AvailableTableFilterPanelInner
|
|
60
|
+
) as <T>(
|
|
61
|
+
props: AvailableTableFilterPanelProps<T> & React.RefAttributes<HTMLElement>
|
|
62
|
+
) => React.ReactElement | null;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { clsx } from 'clsx';
|
|
2
|
+
import { ArrowRightIcon } from '@tcn/icons/arrow_right_icon.js';
|
|
3
|
+
import { FilterTwoIcon } from '@tcn/icons/filter_two_icon.js';
|
|
4
|
+
import { DataSource } from '@tcn/resource-store';
|
|
5
|
+
import { FieldFilterProps, GlobalSearch } from '@tcn/ui-table';
|
|
6
|
+
import { Button } from '@tcn/ui/actions';
|
|
7
|
+
import { Group, UtilityBar, type HeaderProps } from '@tcn/ui/layouts';
|
|
8
|
+
import { Spacer } from '@tcn/ui/stacks';
|
|
9
|
+
import { Title } from '@tcn/ui/typography';
|
|
10
|
+
import React, { ReactElement } from 'react';
|
|
11
|
+
import styles from '../transfer_table.module.css';
|
|
12
|
+
|
|
13
|
+
export interface AvailableTableHeaderOwnProps<T> {
|
|
14
|
+
dataSource: DataSource<T>;
|
|
15
|
+
onClickFilterPanelToggle: () => void;
|
|
16
|
+
onClickMoveAll: () => void;
|
|
17
|
+
filterChildren?: ReactElement<FieldFilterProps> | ReactElement<FieldFilterProps>[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type AvailableTableHeaderProps<T> = AvailableTableHeaderOwnProps<T> & HeaderProps;
|
|
21
|
+
|
|
22
|
+
function AvailableTableHeaderInner<T>(
|
|
23
|
+
{
|
|
24
|
+
dataSource,
|
|
25
|
+
onClickFilterPanelToggle,
|
|
26
|
+
onClickMoveAll,
|
|
27
|
+
filterChildren,
|
|
28
|
+
className,
|
|
29
|
+
...props
|
|
30
|
+
}: AvailableTableHeaderProps<T>,
|
|
31
|
+
ref: React.ForwardedRef<HTMLElement>
|
|
32
|
+
) {
|
|
33
|
+
return (
|
|
34
|
+
<UtilityBar
|
|
35
|
+
ref={ref}
|
|
36
|
+
className={clsx(styles['available-table-header'], className)}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
<Group>
|
|
40
|
+
<Title emphasis="strong">Available</Title>
|
|
41
|
+
</Group>
|
|
42
|
+
{filterChildren && (
|
|
43
|
+
<Group>
|
|
44
|
+
<Button utility hierarchy="tertiary" onClick={onClickFilterPanelToggle}>
|
|
45
|
+
<FilterTwoIcon />
|
|
46
|
+
</Button>
|
|
47
|
+
</Group>
|
|
48
|
+
)}
|
|
49
|
+
<Spacer />
|
|
50
|
+
<GlobalSearch dataSource={dataSource} />
|
|
51
|
+
<Button hierarchy="tertiary" onClick={onClickMoveAll}>
|
|
52
|
+
Move All <ArrowRightIcon />
|
|
53
|
+
</Button>
|
|
54
|
+
</UtilityBar>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const AvailableTableHeader = React.forwardRef(AvailableTableHeaderInner) as <T>(
|
|
59
|
+
props: AvailableTableHeaderProps<T> & React.RefAttributes<HTMLElement>
|
|
60
|
+
) => React.ReactElement | null;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { clsx } from 'clsx';
|
|
2
|
+
import { CrossCircleIcon } from '@tcn/icons/cross_circle_icon.js';
|
|
3
|
+
import { IBroadcast, useSignalValue } from '@tcn/state';
|
|
4
|
+
import { Button } from '@tcn/ui/actions';
|
|
5
|
+
import { Column, type ColumnProps, Detail, Scaffold, UtilityBar } from '@tcn/ui/layouts';
|
|
6
|
+
import { Spacer } from '@tcn/ui/stacks';
|
|
7
|
+
import { Title } from '@tcn/ui/typography';
|
|
8
|
+
import React, { useEffect, useRef } from 'react';
|
|
9
|
+
import { SelectedList } from '../../components/index.js';
|
|
10
|
+
import { SelectedItemsUpdate } from '../types.js';
|
|
11
|
+
import styles from '../transfer_table.module.css';
|
|
12
|
+
import { SelectedItem } from './selected_item.js';
|
|
13
|
+
import { Resizable, ResizeHandle } from '@tcn/ui/utils';
|
|
14
|
+
|
|
15
|
+
export interface SelectedColumnOwnProps<T> {
|
|
16
|
+
selectedItemsBroadcast: IBroadcast<SelectedItemsUpdate<T>>;
|
|
17
|
+
getSelectedItemKey: (item: T) => string | number;
|
|
18
|
+
selectedItemRender: (item: T) => React.ReactNode;
|
|
19
|
+
/** Receives the items that were removed (snapshot before the list is cleared). */
|
|
20
|
+
onRemoveAll: (removedItems: T[]) => void;
|
|
21
|
+
onRemove: (item: T) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type SelectedColumnProps<T> = SelectedColumnOwnProps<T> & ColumnProps;
|
|
25
|
+
|
|
26
|
+
function SelectedColumnInner<T>(
|
|
27
|
+
{
|
|
28
|
+
selectedItemsBroadcast,
|
|
29
|
+
getSelectedItemKey,
|
|
30
|
+
selectedItemRender,
|
|
31
|
+
onRemoveAll,
|
|
32
|
+
onRemove,
|
|
33
|
+
className,
|
|
34
|
+
width = '296px',
|
|
35
|
+
minWidth = '200px',
|
|
36
|
+
maxWidth = '50%',
|
|
37
|
+
...props
|
|
38
|
+
}: SelectedColumnProps<T>,
|
|
39
|
+
ref: React.ForwardedRef<HTMLElement>
|
|
40
|
+
) {
|
|
41
|
+
const { selectedItems, highlight } = useSignalValue(selectedItemsBroadcast);
|
|
42
|
+
|
|
43
|
+
// TODO: Remove highlight tracking once Presence is done (https://git.tcncloud.net/blackcat-ui/blackcat/-/issues/386)
|
|
44
|
+
const knownKeysRef = useRef<Set<string | number>>(new Set());
|
|
45
|
+
const newKeys = new Set<string | number>();
|
|
46
|
+
if (highlight) {
|
|
47
|
+
for (const item of selectedItems) {
|
|
48
|
+
const key = getSelectedItemKey(item);
|
|
49
|
+
if (!knownKeysRef.current.has(key)) {
|
|
50
|
+
newKeys.add(key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const currentKeys = new Set(selectedItems.map(getSelectedItemKey));
|
|
57
|
+
for (const key of currentKeys) {
|
|
58
|
+
knownKeysRef.current.add(key);
|
|
59
|
+
}
|
|
60
|
+
for (const key of [...knownKeysRef.current]) {
|
|
61
|
+
if (!currentKeys.has(key)) {
|
|
62
|
+
knownKeysRef.current.delete(key);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}, [selectedItems, getSelectedItemKey]);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Resizable>
|
|
69
|
+
<ResizeHandle position="start" />
|
|
70
|
+
<Column
|
|
71
|
+
ref={ref}
|
|
72
|
+
className={clsx(
|
|
73
|
+
styles['selected-list'],
|
|
74
|
+
'tcn-transfer-table-selected-list',
|
|
75
|
+
className
|
|
76
|
+
)}
|
|
77
|
+
width={width}
|
|
78
|
+
minWidth={minWidth}
|
|
79
|
+
maxWidth={maxWidth}
|
|
80
|
+
{...props}
|
|
81
|
+
>
|
|
82
|
+
<Scaffold height="100%" width="100%">
|
|
83
|
+
<UtilityBar>
|
|
84
|
+
<Title emphasis="strong">Selected</Title>
|
|
85
|
+
<Spacer />
|
|
86
|
+
<Button hierarchy="tertiary" onClick={() => onRemoveAll(selectedItems)}>
|
|
87
|
+
Remove All <CrossCircleIcon />
|
|
88
|
+
</Button>
|
|
89
|
+
</UtilityBar>
|
|
90
|
+
<Scaffold>
|
|
91
|
+
<Detail>
|
|
92
|
+
<SelectedList
|
|
93
|
+
items={selectedItems}
|
|
94
|
+
getItemKey={getSelectedItemKey}
|
|
95
|
+
renderItem={selectedItemRender}
|
|
96
|
+
onClickItem={onRemove}
|
|
97
|
+
renderListItem={(item, content) => (
|
|
98
|
+
<SelectedItem
|
|
99
|
+
item={item}
|
|
100
|
+
onClickItem={onRemove}
|
|
101
|
+
isNew={newKeys.has(getSelectedItemKey(item))}
|
|
102
|
+
>
|
|
103
|
+
{content}
|
|
104
|
+
</SelectedItem>
|
|
105
|
+
)}
|
|
106
|
+
/>
|
|
107
|
+
</Detail>
|
|
108
|
+
</Scaffold>
|
|
109
|
+
</Scaffold>
|
|
110
|
+
</Column>
|
|
111
|
+
</Resizable>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// React.forwardRef erases the generic <T> from SelectedColumnInner, returning
|
|
116
|
+
// ForwardRefExoticComponent<SelectedColumnProps<unknown>>. The cast restores the
|
|
117
|
+
// generic signature so consumers get proper type inference for their item type.
|
|
118
|
+
export const SelectedColumn = React.forwardRef(SelectedColumnInner) as <T>(
|
|
119
|
+
props: SelectedColumnProps<T> & React.RefAttributes<HTMLElement>
|
|
120
|
+
) => React.ReactElement | null;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { clsx } from 'clsx';
|
|
2
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { RemovableItem, type RemovableItemProps } from '../../components/index.js';
|
|
4
|
+
import styles from '../transfer_table.module.css';
|
|
5
|
+
|
|
6
|
+
export interface SelectedItemOwnProps {
|
|
7
|
+
isNew: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type SelectedItemProps<T> = SelectedItemOwnProps & RemovableItemProps<T>;
|
|
11
|
+
|
|
12
|
+
function SelectedItemInner<T>(
|
|
13
|
+
{ isNew: isNewProp, className, ...props }: SelectedItemProps<T>,
|
|
14
|
+
ref: React.ForwardedRef<HTMLDivElement>
|
|
15
|
+
) {
|
|
16
|
+
const [isNew, setIsNew] = useState(isNewProp);
|
|
17
|
+
const timeoutIdRef = useRef<number | null>(null);
|
|
18
|
+
|
|
19
|
+
// TODO: Remove once Presence is done (https://git.tcncloud.net/blackcat-ui/blackcat/-/issues/386)
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (isNewProp) {
|
|
22
|
+
setIsNew(true);
|
|
23
|
+
if (timeoutIdRef.current !== null) {
|
|
24
|
+
window.clearTimeout(timeoutIdRef.current);
|
|
25
|
+
}
|
|
26
|
+
const HIGHLIGHT_HOLD_MS = 1000;
|
|
27
|
+
timeoutIdRef.current = window.setTimeout(() => {
|
|
28
|
+
setIsNew(false);
|
|
29
|
+
timeoutIdRef.current = null;
|
|
30
|
+
}, HIGHLIGHT_HOLD_MS);
|
|
31
|
+
}
|
|
32
|
+
}, [isNewProp]);
|
|
33
|
+
|
|
34
|
+
// TODO: Remove once Presence is done (https://git.tcncloud.net/blackcat-ui/blackcat/-/issues/386)
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
return () => {
|
|
37
|
+
if (timeoutIdRef.current !== null) {
|
|
38
|
+
window.clearTimeout(timeoutIdRef.current);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<RemovableItem
|
|
45
|
+
ref={ref}
|
|
46
|
+
className={clsx(
|
|
47
|
+
styles['selected-item'],
|
|
48
|
+
isNew && styles['selected-item-new'],
|
|
49
|
+
className
|
|
50
|
+
)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const SelectedItem = React.forwardRef(SelectedItemInner) as <T>(
|
|
57
|
+
props: SelectedItemProps<T> & React.RefAttributes<HTMLDivElement>
|
|
58
|
+
) => React.ReactElement | null;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* Transfer Table */
|
|
2
|
+
.tcn-transfer-table {
|
|
3
|
+
--material: var(--background-color-primary);
|
|
4
|
+
:where(.tcn-table-available-table) :global(thead tr) {
|
|
5
|
+
--material: #e2e2e2;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* TODO: should be on Theme - Column not direct child of page has border */
|
|
9
|
+
:global(.tcn-rail.tcn-columns) {
|
|
10
|
+
--resize-offset: -4px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* TODO: remove once theme provides a way to opt out of .tcn-panel .tcn-columns layout */
|
|
14
|
+
:global(.tcn-rail.tcn-columns) {
|
|
15
|
+
padding: 0;
|
|
16
|
+
|
|
17
|
+
:global(.tcn-column) {
|
|
18
|
+
gap: 0;
|
|
19
|
+
border-right: none;
|
|
20
|
+
padding-inline-end: 0;
|
|
21
|
+
padding-inline-start: 0;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.tcn-table-available-table {
|
|
27
|
+
/* TODO: should be on Theme - Column not direct child of page has border */
|
|
28
|
+
border-right: 1px solid #aaaaaa;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Selected Item
|
|
32
|
+
TODO: Remove highlight CSS once Presence is done (https://git.tcncloud.net/blackcat-ui/blackcat/-/issues/386).
|
|
33
|
+
--highlight-hold-ms (1000ms) must match the JS timeout in selected_item.tsx.
|
|
34
|
+
The fade-out duration (0.35s) kicks in after the JS class is removed. */
|
|
35
|
+
:where(.tcn-transfer-table) :where(.selected-item) {
|
|
36
|
+
transition: background-color 0.35s ease;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:where(.tcn-transfer-table) :where(.selected-item-new) {
|
|
40
|
+
background-color: var(--secondary-color-faint);
|
|
41
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { clsx } from 'clsx';
|
|
2
|
+
import { PlusIcon } from '@tcn/icons/plus_icon.js';
|
|
3
|
+
import { Button } from '@tcn/ui/actions';
|
|
4
|
+
import { Column, Columns, Footer, Scaffold } from '@tcn/ui/layouts';
|
|
5
|
+
import { Spacer } from '@tcn/ui/stacks';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { AvailableTable } from './components/available_table.js';
|
|
8
|
+
import { SelectedColumn } from './components/selected_column.js';
|
|
9
|
+
import styles from './transfer_table.module.css';
|
|
10
|
+
import { TransferTableProps } from './types.js';
|
|
11
|
+
|
|
12
|
+
function TransferTableInner<T>(
|
|
13
|
+
{
|
|
14
|
+
dataSource,
|
|
15
|
+
columns,
|
|
16
|
+
onClickMoveAll,
|
|
17
|
+
onClickMove,
|
|
18
|
+
selectedItemsBroadcast,
|
|
19
|
+
getSelectedItemKey,
|
|
20
|
+
selectedItemRender,
|
|
21
|
+
onRemoveAll,
|
|
22
|
+
onRemove,
|
|
23
|
+
filterPanelIsOpenBroadcast,
|
|
24
|
+
filterChildren,
|
|
25
|
+
onClickFilterPanelToggle,
|
|
26
|
+
onClickCancel,
|
|
27
|
+
onClickAdd,
|
|
28
|
+
...props
|
|
29
|
+
}: TransferTableProps<T>,
|
|
30
|
+
ref: React.ForwardedRef<HTMLElement>
|
|
31
|
+
) {
|
|
32
|
+
return (
|
|
33
|
+
<Scaffold
|
|
34
|
+
ref={ref}
|
|
35
|
+
className={clsx(styles['tcn-transfer-table'], 'tcn-transfer-table', 'tcn-material')}
|
|
36
|
+
{...props}
|
|
37
|
+
>
|
|
38
|
+
<Columns>
|
|
39
|
+
<Column width="fill">
|
|
40
|
+
<AvailableTable
|
|
41
|
+
dataSource={dataSource}
|
|
42
|
+
columns={columns}
|
|
43
|
+
onClickMove={onClickMove}
|
|
44
|
+
onClickMoveAll={onClickMoveAll}
|
|
45
|
+
filterPanelIsOpenBroadcast={filterPanelIsOpenBroadcast}
|
|
46
|
+
filterChildren={filterChildren}
|
|
47
|
+
onClickFilterPanelToggle={onClickFilterPanelToggle}
|
|
48
|
+
/>
|
|
49
|
+
</Column>
|
|
50
|
+
|
|
51
|
+
<SelectedColumn
|
|
52
|
+
selectedItemRender={selectedItemRender}
|
|
53
|
+
selectedItemsBroadcast={selectedItemsBroadcast}
|
|
54
|
+
getSelectedItemKey={getSelectedItemKey}
|
|
55
|
+
onRemoveAll={onRemoveAll}
|
|
56
|
+
onRemove={onRemove}
|
|
57
|
+
/>
|
|
58
|
+
</Columns>
|
|
59
|
+
<Footer>
|
|
60
|
+
<Spacer />
|
|
61
|
+
<Button hierarchy="tertiary" onClick={onClickCancel}>
|
|
62
|
+
Cancel
|
|
63
|
+
</Button>
|
|
64
|
+
<Button
|
|
65
|
+
hierarchy="primary"
|
|
66
|
+
onClick={() => onClickAdd(selectedItemsBroadcast.get().selectedItems)}
|
|
67
|
+
>
|
|
68
|
+
Add <PlusIcon />
|
|
69
|
+
</Button>
|
|
70
|
+
</Footer>
|
|
71
|
+
</Scaffold>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const TransferTable = React.forwardRef(TransferTableInner) as <T>(
|
|
76
|
+
props: TransferTableProps<T> & React.RefAttributes<HTMLElement>
|
|
77
|
+
) => React.ReactElement | null;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { StaticDataSource } from '@tcn/resource-store';
|
|
2
|
+
import { ISubscription, Signal } from '@tcn/state';
|
|
3
|
+
import { FieldFilterProps, TableColumnProps } from '@tcn/ui-table';
|
|
4
|
+
import { ReactElement } from 'react';
|
|
5
|
+
import { SelectedItemsUpdate, TransferTableProps } from './types.js';
|
|
6
|
+
|
|
7
|
+
export interface TransferTablePresenterArgs<T> {
|
|
8
|
+
dataSource: StaticDataSource<T>;
|
|
9
|
+
columns: TableColumnProps<T>[];
|
|
10
|
+
filterChildren?: ReactElement<FieldFilterProps> | ReactElement<FieldFilterProps>[];
|
|
11
|
+
getItemKey: (item: T) => string | number;
|
|
12
|
+
selectedItemRender: (item: T) => React.ReactNode;
|
|
13
|
+
onClickMoveAll?: (items: T[]) => void;
|
|
14
|
+
onClickMove?: (item: T) => void;
|
|
15
|
+
onRemoveAll?: (items: T[]) => void;
|
|
16
|
+
onRemove?: (item: T) => void;
|
|
17
|
+
onClickFilterPanelToggle?: () => void;
|
|
18
|
+
onClickCancel?: () => void;
|
|
19
|
+
onClickAdd?: (items: T[]) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class TransferTablePresenter<T> {
|
|
23
|
+
dataSource: StaticDataSource<T>;
|
|
24
|
+
columns: TableColumnProps<T>[];
|
|
25
|
+
getSelectedItemKey: (item: T) => string | number;
|
|
26
|
+
|
|
27
|
+
private selectedItemsSignal = new Signal<SelectedItemsUpdate<T>>({
|
|
28
|
+
selectedItems: [],
|
|
29
|
+
highlight: false,
|
|
30
|
+
});
|
|
31
|
+
selectedItemsBroadcast = this.selectedItemsSignal.broadcast;
|
|
32
|
+
|
|
33
|
+
selectedItemRender: (item: T) => React.ReactNode;
|
|
34
|
+
filterChildren?: ReactElement<FieldFilterProps> | ReactElement<FieldFilterProps>[];
|
|
35
|
+
|
|
36
|
+
private availableItems: T[] = [];
|
|
37
|
+
private _initialItemsSubscription: ISubscription<T[]> | null = null;
|
|
38
|
+
|
|
39
|
+
private filterPanelIsOpenSignal = new Signal<boolean>(false);
|
|
40
|
+
filterPanelIsOpenBroadcast = this.filterPanelIsOpenSignal.broadcast;
|
|
41
|
+
|
|
42
|
+
private callbacks: TransferTablePresenterArgs<T>;
|
|
43
|
+
|
|
44
|
+
constructor(args: TransferTablePresenterArgs<T>) {
|
|
45
|
+
this.dataSource = args.dataSource;
|
|
46
|
+
this.columns = args.columns;
|
|
47
|
+
this.selectedItemRender = args.selectedItemRender;
|
|
48
|
+
this.getSelectedItemKey = args.getItemKey;
|
|
49
|
+
this.filterChildren = args.filterChildren;
|
|
50
|
+
this.callbacks = args;
|
|
51
|
+
|
|
52
|
+
this._initialItemsSubscription = this.dataSource.broadcasts.currentResults.subscribe(
|
|
53
|
+
items => {
|
|
54
|
+
if (this.availableItems.length === 0) {
|
|
55
|
+
this.availableItems = [...items] as unknown as T[];
|
|
56
|
+
}
|
|
57
|
+
this._initialItemsSubscription?.unsubscribe();
|
|
58
|
+
this._initialItemsSubscription = null;
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Load pre-selected items without triggering the highlight animation. */
|
|
64
|
+
loadItems = (items: T[]) => {
|
|
65
|
+
this.selectedItemsSignal.set({ selectedItems: items, highlight: false });
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
onClickMoveAll = () => {
|
|
69
|
+
const visibleItems = this.dataSource.broadcasts.currentResults.get();
|
|
70
|
+
const visibleKeys = new Set(visibleItems.map(this.getSelectedItemKey));
|
|
71
|
+
const currentSelectedItems = this.selectedItemsSignal.get().selectedItems;
|
|
72
|
+
|
|
73
|
+
this.availableItems = this.availableItems.filter(
|
|
74
|
+
i => !visibleKeys.has(this.getSelectedItemKey(i))
|
|
75
|
+
);
|
|
76
|
+
this.dataSource.setItems(this.availableItems);
|
|
77
|
+
this.selectedItemsSignal.set({
|
|
78
|
+
selectedItems: [...currentSelectedItems, ...visibleItems],
|
|
79
|
+
highlight: true,
|
|
80
|
+
});
|
|
81
|
+
this.callbacks.onClickMoveAll?.(visibleItems);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
onClickMove = (item: T) => {
|
|
85
|
+
const itemKey = this.getSelectedItemKey(item);
|
|
86
|
+
this.availableItems = this.availableItems.filter(
|
|
87
|
+
i => this.getSelectedItemKey(i) !== itemKey
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const currentSelectedItems = this.selectedItemsSignal.get().selectedItems;
|
|
91
|
+
|
|
92
|
+
this.dataSource.setItems(this.availableItems);
|
|
93
|
+
this.selectedItemsSignal.set({
|
|
94
|
+
selectedItems: [...currentSelectedItems, item],
|
|
95
|
+
highlight: true,
|
|
96
|
+
});
|
|
97
|
+
this.callbacks.onClickMove?.(item);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
onRemoveAll = () => {
|
|
101
|
+
const currentSelectedItems = this.selectedItemsSignal.get().selectedItems;
|
|
102
|
+
|
|
103
|
+
this.availableItems = [...this.availableItems, ...currentSelectedItems];
|
|
104
|
+
this.dataSource.setItems(this.availableItems);
|
|
105
|
+
this.selectedItemsSignal.set({ selectedItems: [], highlight: false });
|
|
106
|
+
this.callbacks.onRemoveAll?.(currentSelectedItems);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
onRemove = (removeItem: T) => {
|
|
110
|
+
const removeKey = this.getSelectedItemKey(removeItem);
|
|
111
|
+
const currentSelectedItems = this.selectedItemsSignal.get().selectedItems;
|
|
112
|
+
const mutatedSelectedItems = currentSelectedItems.filter(
|
|
113
|
+
item => this.getSelectedItemKey(item) !== removeKey
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
this.availableItems = [...this.availableItems, removeItem];
|
|
117
|
+
this.dataSource.setItems(this.availableItems);
|
|
118
|
+
this.selectedItemsSignal.set({
|
|
119
|
+
selectedItems: mutatedSelectedItems,
|
|
120
|
+
highlight: false,
|
|
121
|
+
});
|
|
122
|
+
this.callbacks.onRemove?.(removeItem);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
onClickFilterPanelToggle = () => {
|
|
126
|
+
const currentOpenValue = this.filterPanelIsOpenSignal.get();
|
|
127
|
+
this.filterPanelIsOpenSignal.set(!currentOpenValue);
|
|
128
|
+
this.callbacks.onClickFilterPanelToggle?.();
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
onClickCancel = () => {
|
|
132
|
+
this.callbacks.onClickCancel?.();
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
onClickAdd = (items: T[]) => {
|
|
136
|
+
this.callbacks.onClickAdd?.(items);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** Returns only the props the TransferTable component needs. */
|
|
140
|
+
getProps(): TransferTableProps<T> {
|
|
141
|
+
return {
|
|
142
|
+
dataSource: this.dataSource,
|
|
143
|
+
columns: this.columns,
|
|
144
|
+
onClickMoveAll: this.onClickMoveAll,
|
|
145
|
+
onClickMove: this.onClickMove,
|
|
146
|
+
selectedItemsBroadcast: this.selectedItemsBroadcast,
|
|
147
|
+
getSelectedItemKey: this.getSelectedItemKey,
|
|
148
|
+
selectedItemRender: this.selectedItemRender,
|
|
149
|
+
onRemoveAll: this.onRemoveAll,
|
|
150
|
+
onRemove: this.onRemove,
|
|
151
|
+
filterPanelIsOpenBroadcast: this.filterPanelIsOpenBroadcast,
|
|
152
|
+
filterChildren: this.filterChildren,
|
|
153
|
+
onClickFilterPanelToggle: this.onClickFilterPanelToggle,
|
|
154
|
+
onClickCancel: this.onClickCancel,
|
|
155
|
+
onClickAdd: this.onClickAdd,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
dispose(): void {
|
|
160
|
+
this._initialItemsSubscription?.unsubscribe();
|
|
161
|
+
this.selectedItemsSignal.dispose();
|
|
162
|
+
this.filterPanelIsOpenSignal.dispose();
|
|
163
|
+
}
|
|
164
|
+
}
|