@rebasepro/ui 0.6.1 → 0.7.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/dist/icons/index.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +2058 -16
- package/dist/index.es.js.map +1 -1
- package/dist/styles.d.ts +1 -1
- package/dist/views/CardView.d.ts +32 -0
- package/dist/views/CollectionView/CollectionCardView.d.ts +30 -0
- package/dist/views/CollectionView/CollectionKanbanView.d.ts +31 -0
- package/dist/views/CollectionView/CollectionListView.d.ts +30 -0
- package/dist/views/CollectionView/CollectionTableView.d.ts +37 -0
- package/dist/views/CollectionView/CollectionView.d.ts +121 -0
- package/dist/views/CollectionView/CollectionViewToolbar.d.ts +31 -0
- package/dist/views/CollectionView/CollectionViewTypes.d.ts +106 -0
- package/dist/views/CollectionView/DefaultCellRenderer.d.ts +9 -0
- package/dist/views/CollectionView/index.d.ts +24 -0
- package/dist/views/CollectionView/utils.d.ts +5 -0
- package/dist/views/Kanban/Board.d.ts +3 -0
- package/dist/views/Kanban/BoardColumn.d.ts +20 -0
- package/dist/views/Kanban/BoardColumnTitle.d.ts +8 -0
- package/dist/views/Kanban/BoardSortableList.d.ts +14 -0
- package/dist/views/Kanban/board_types.d.ts +61 -0
- package/dist/views/Kanban/index.d.ts +5 -0
- package/dist/views/KanbanView.d.ts +24 -0
- package/dist/views/ListView.d.ts +40 -0
- package/dist/views/TableView.d.ts +6 -0
- package/dist/views/index.d.ts +5 -0
- package/package.json +1 -1
- package/src/components/Sheet.tsx +8 -4
- package/src/icons/index.ts +3 -0
- package/src/index.ts +2 -0
- package/src/styles.ts +1 -1
- package/src/views/CardView.tsx +281 -0
- package/src/views/CollectionView/CollectionCardView.tsx +270 -0
- package/src/views/CollectionView/CollectionKanbanView.tsx +251 -0
- package/src/views/CollectionView/CollectionListView.tsx +245 -0
- package/src/views/CollectionView/CollectionTableView.tsx +229 -0
- package/src/views/CollectionView/CollectionView.tsx +388 -0
- package/src/views/CollectionView/CollectionViewToolbar.tsx +229 -0
- package/src/views/CollectionView/CollectionViewTypes.ts +137 -0
- package/src/views/CollectionView/DefaultCellRenderer.tsx +404 -0
- package/src/views/CollectionView/index.ts +44 -0
- package/src/views/CollectionView/utils.ts +14 -0
- package/src/views/Kanban/Board.tsx +421 -0
- package/src/views/Kanban/BoardColumn.tsx +135 -0
- package/src/views/Kanban/BoardColumnTitle.tsx +47 -0
- package/src/views/Kanban/BoardSortableList.tsx +170 -0
- package/src/views/Kanban/board_types.ts +70 -0
- package/src/views/Kanban/index.tsx +5 -0
- package/src/views/KanbanView.tsx +32 -0
- package/src/views/ListView.tsx +281 -0
- package/src/views/TableView.tsx +7 -0
- package/src/views/index.ts +5 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import React, { memo, useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { useDroppable } from "@dnd-kit/core";
|
|
3
|
+
import { useSortable, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
4
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
5
|
+
import { CircularProgress } from "../../components";
|
|
6
|
+
import { cls } from "../../util";
|
|
7
|
+
import { BoardItem, BoardItemViewProps } from "./board_types";
|
|
8
|
+
|
|
9
|
+
interface BoardSortableListProps<T> {
|
|
10
|
+
columnId: string;
|
|
11
|
+
items: BoardItem<T>[];
|
|
12
|
+
ItemComponent: React.ComponentType<BoardItemViewProps<T>>;
|
|
13
|
+
isDragging: boolean;
|
|
14
|
+
isDragOverColumn: boolean;
|
|
15
|
+
loading?: boolean;
|
|
16
|
+
hasMore?: boolean;
|
|
17
|
+
onLoadMore?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function BoardSortableList<T>({
|
|
21
|
+
columnId,
|
|
22
|
+
items,
|
|
23
|
+
ItemComponent,
|
|
24
|
+
isDragging,
|
|
25
|
+
isDragOverColumn,
|
|
26
|
+
loading = false,
|
|
27
|
+
hasMore = false,
|
|
28
|
+
onLoadMore
|
|
29
|
+
}: BoardSortableListProps<T>) {
|
|
30
|
+
const {
|
|
31
|
+
setNodeRef
|
|
32
|
+
} = useDroppable({
|
|
33
|
+
id: columnId,
|
|
34
|
+
data: { type: "ITEM-LIST" }
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const sentinelRef = useRef<HTMLDivElement>(null);
|
|
38
|
+
const isLoadingRef = useRef(loading);
|
|
39
|
+
isLoadingRef.current = loading;
|
|
40
|
+
const lastLoadTimeRef = useRef(0);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!sentinelRef.current || !hasMore || !onLoadMore) return;
|
|
44
|
+
|
|
45
|
+
const sentinel = sentinelRef.current;
|
|
46
|
+
|
|
47
|
+
const observer = new IntersectionObserver(
|
|
48
|
+
(entries) => {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
if (
|
|
51
|
+
entries[0].isIntersecting &&
|
|
52
|
+
hasMore &&
|
|
53
|
+
!isLoadingRef.current &&
|
|
54
|
+
now - lastLoadTimeRef.current > 500
|
|
55
|
+
) {
|
|
56
|
+
lastLoadTimeRef.current = now;
|
|
57
|
+
onLoadMore();
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{ threshold: 0.1 }
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
observer.observe(sentinel);
|
|
64
|
+
|
|
65
|
+
const rect = sentinel.getBoundingClientRect();
|
|
66
|
+
const containerRect = sentinel.parentElement?.getBoundingClientRect();
|
|
67
|
+
if (containerRect && rect.top < containerRect.bottom && rect.bottom > containerRect.top) {
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
if (hasMore && !isLoadingRef.current && now - lastLoadTimeRef.current > 500) {
|
|
70
|
+
lastLoadTimeRef.current = now;
|
|
71
|
+
onLoadMore();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return () => observer.disconnect();
|
|
76
|
+
}, [hasMore, onLoadMore]);
|
|
77
|
+
|
|
78
|
+
const containerClassName = useMemo(() => cls(
|
|
79
|
+
"flex flex-col p-2 transition-opacity duration-100 transition-bg ease-linear w-full overflow-y-auto no-scrollbar flex-1 rounded-md",
|
|
80
|
+
isDragging && isDragOverColumn
|
|
81
|
+
? "bg-surface-accent-200 dark:bg-surface-900"
|
|
82
|
+
: isDragging
|
|
83
|
+
? "bg-surface-50 dark:bg-surface-900 hover:bg-surface-accent-100 dark:hover:bg-surface-800"
|
|
84
|
+
: "bg-surface-50 dark:bg-surface-900"
|
|
85
|
+
), [isDragging, isDragOverColumn]);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
ref={setNodeRef}
|
|
90
|
+
className={containerClassName}
|
|
91
|
+
style={{ minHeight: 80 }}
|
|
92
|
+
>
|
|
93
|
+
<SortableContext
|
|
94
|
+
items={items.map(i => i.id)}
|
|
95
|
+
strategy={verticalListSortingStrategy}
|
|
96
|
+
>
|
|
97
|
+
{items.length === 0 && !loading ? (
|
|
98
|
+
<div className="flex-1 flex items-center justify-center">
|
|
99
|
+
<span className="text-xs text-surface-400 dark:text-surface-500">
|
|
100
|
+
No items
|
|
101
|
+
</span>
|
|
102
|
+
</div>
|
|
103
|
+
) : (
|
|
104
|
+
<>
|
|
105
|
+
{items.map((item, index) => (
|
|
106
|
+
<SortableItem
|
|
107
|
+
key={item.id}
|
|
108
|
+
item={item}
|
|
109
|
+
index={index}
|
|
110
|
+
columnId={columnId}
|
|
111
|
+
ItemComponent={ItemComponent}
|
|
112
|
+
/>
|
|
113
|
+
))}
|
|
114
|
+
{(loading || hasMore) && (
|
|
115
|
+
<div ref={sentinelRef} className="flex items-center justify-center py-2 min-h-6">
|
|
116
|
+
{loading && <CircularProgress size="smallest"/>}
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</>
|
|
120
|
+
)}
|
|
121
|
+
</SortableContext>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface SortableItemProps<T> {
|
|
127
|
+
item: BoardItem<T>;
|
|
128
|
+
index: number;
|
|
129
|
+
columnId: string;
|
|
130
|
+
ItemComponent: React.ComponentType<BoardItemViewProps<T>>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const SortableItem = memo(function SortableItem<T>({
|
|
134
|
+
item,
|
|
135
|
+
index,
|
|
136
|
+
columnId,
|
|
137
|
+
ItemComponent
|
|
138
|
+
}: SortableItemProps<T>) {
|
|
139
|
+
const {
|
|
140
|
+
setNodeRef,
|
|
141
|
+
attributes,
|
|
142
|
+
listeners,
|
|
143
|
+
isDragging: isItemBeingDragged,
|
|
144
|
+
transform,
|
|
145
|
+
transition
|
|
146
|
+
} = useSortable({
|
|
147
|
+
id: item.id,
|
|
148
|
+
data: {
|
|
149
|
+
type: "ITEM",
|
|
150
|
+
columnId
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const sortableStyle = useMemo(() => ({
|
|
155
|
+
transform: CSS.Transform.toString(transform),
|
|
156
|
+
transition,
|
|
157
|
+
zIndex: isItemBeingDragged ? 2 : 1,
|
|
158
|
+
opacity: isItemBeingDragged ? 0 : 1
|
|
159
|
+
}), [transform, transition, isItemBeingDragged]);
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div ref={setNodeRef} style={sortableStyle} {...attributes} {...listeners}>
|
|
163
|
+
<ItemComponent
|
|
164
|
+
item={item}
|
|
165
|
+
isDragging={isItemBeingDragged}
|
|
166
|
+
index={index}
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}) as <T>(props: SortableItemProps<T>) => React.ReactElement;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { CSSProperties } from "react";
|
|
2
|
+
|
|
3
|
+
export type ChipColorKey = "primary" | "secondary" | "success" | "warning" | "error" | "info" | "neutral" | string;
|
|
4
|
+
export type ChipColorScheme = "filled" | "outlined" | "tinted" | string;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Item wrapper for elements in the Board component
|
|
8
|
+
*/
|
|
9
|
+
export interface BoardItem<T = any> {
|
|
10
|
+
id: string;
|
|
11
|
+
data: T;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Map of column keys to arrays of board items
|
|
16
|
+
*/
|
|
17
|
+
export interface BoardItemMap<T = any> {
|
|
18
|
+
[columnKey: string]: BoardItem<T>[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Props passed to custom item render components
|
|
23
|
+
*/
|
|
24
|
+
export interface BoardItemViewProps<T = any> {
|
|
25
|
+
item: BoardItem<T>;
|
|
26
|
+
isDragging: boolean;
|
|
27
|
+
isClone?: boolean;
|
|
28
|
+
isGroupedOver?: boolean;
|
|
29
|
+
style?: CSSProperties;
|
|
30
|
+
index?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Per-column loading state
|
|
35
|
+
*/
|
|
36
|
+
export interface ColumnLoadingState {
|
|
37
|
+
[columnKey: string]: {
|
|
38
|
+
loading: boolean;
|
|
39
|
+
hasMore: boolean;
|
|
40
|
+
itemCount: number;
|
|
41
|
+
totalCount?: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Props for the Board component
|
|
47
|
+
*/
|
|
48
|
+
export interface BoardProps<T, COLUMN extends string> {
|
|
49
|
+
data: BoardItem<T>[];
|
|
50
|
+
columns: COLUMN[];
|
|
51
|
+
columnLabels?: Record<COLUMN, string>;
|
|
52
|
+
columnColors?: Record<COLUMN, any>;
|
|
53
|
+
className?: string;
|
|
54
|
+
assignColumn: (item: BoardItem<T>) => COLUMN;
|
|
55
|
+
allowColumnReorder?: boolean;
|
|
56
|
+
onColumnReorder?: (columns: COLUMN[]) => void;
|
|
57
|
+
onItemsReorder?: (
|
|
58
|
+
items: BoardItem<T>[],
|
|
59
|
+
moveInfo?: {
|
|
60
|
+
itemId: string;
|
|
61
|
+
sourceColumn: COLUMN;
|
|
62
|
+
targetColumn: COLUMN;
|
|
63
|
+
}
|
|
64
|
+
) => void;
|
|
65
|
+
ItemComponent: React.ComponentType<BoardItemViewProps<T>>;
|
|
66
|
+
columnLoadingState?: ColumnLoadingState;
|
|
67
|
+
onLoadMoreColumn?: (column: COLUMN) => void;
|
|
68
|
+
onAddItemToColumn?: (column: COLUMN) => void;
|
|
69
|
+
AddColumnComponent?: React.ReactNode;
|
|
70
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Board } from "./Kanban/Board";
|
|
3
|
+
import { BoardItem, BoardItemViewProps, ColumnLoadingState } from "./Kanban/board_types";
|
|
4
|
+
|
|
5
|
+
export type KanbanViewProps<T, COLUMN extends string> = {
|
|
6
|
+
columns: COLUMN[];
|
|
7
|
+
columnLabels?: Record<COLUMN, string>;
|
|
8
|
+
columnColors?: Record<COLUMN, any>;
|
|
9
|
+
className?: string;
|
|
10
|
+
assignColumn: (item: BoardItem<T>) => COLUMN;
|
|
11
|
+
allowColumnReorder?: boolean;
|
|
12
|
+
onColumnReorder?: (columns: COLUMN[]) => void;
|
|
13
|
+
onItemsReorder?: (
|
|
14
|
+
items: BoardItem<T>[],
|
|
15
|
+
moveInfo?: {
|
|
16
|
+
itemId: string;
|
|
17
|
+
sourceColumn: COLUMN;
|
|
18
|
+
targetColumn: COLUMN;
|
|
19
|
+
}
|
|
20
|
+
) => void;
|
|
21
|
+
ItemComponent: React.ComponentType<BoardItemViewProps<T>>;
|
|
22
|
+
columnLoadingState?: ColumnLoadingState;
|
|
23
|
+
onLoadMoreColumn?: (column: COLUMN) => void;
|
|
24
|
+
onAddItemToColumn?: (column: COLUMN) => void;
|
|
25
|
+
AddColumnComponent?: React.ReactNode;
|
|
26
|
+
data: BoardItem<T>[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function KanbanView<T, COLUMN extends string>(props: KanbanViewProps<T, COLUMN>) {
|
|
30
|
+
return <Board {...props} />;
|
|
31
|
+
}
|
|
32
|
+
export type { BoardItem, BoardItemViewProps, ColumnLoadingState };
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
CircularProgress,
|
|
4
|
+
Typography
|
|
5
|
+
} from "../components";
|
|
6
|
+
import { cls } from "../util";
|
|
7
|
+
|
|
8
|
+
export type CollectionSize = "xs" | "s" | "m" | "l" | "xl";
|
|
9
|
+
|
|
10
|
+
export type ListViewColumnDef<T> = {
|
|
11
|
+
key: string;
|
|
12
|
+
label: string;
|
|
13
|
+
align?: "left" | "center" | "right";
|
|
14
|
+
width?: string;
|
|
15
|
+
renderCell: (item: T) => React.ReactNode;
|
|
16
|
+
isTitle?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type ListViewProps<T> = {
|
|
20
|
+
data: T[];
|
|
21
|
+
dataLoading?: boolean;
|
|
22
|
+
noMoreToLoad?: boolean;
|
|
23
|
+
dataLoadingError?: Error;
|
|
24
|
+
itemCount?: number;
|
|
25
|
+
setItemCount?: (itemCount: number) => void;
|
|
26
|
+
pageSize?: number;
|
|
27
|
+
paginationEnabled?: boolean;
|
|
28
|
+
|
|
29
|
+
onItemClick?: (item: T) => void;
|
|
30
|
+
selectedIds?: Set<string | number>;
|
|
31
|
+
highlightedIds?: Set<string | number>;
|
|
32
|
+
selectionEnabled?: boolean;
|
|
33
|
+
onSelectionChange?: (item: T, selected: boolean) => void;
|
|
34
|
+
emptyComponent?: React.ReactNode;
|
|
35
|
+
|
|
36
|
+
size?: CollectionSize;
|
|
37
|
+
selectedEntityId?: string | number;
|
|
38
|
+
|
|
39
|
+
renderRow: (params: {
|
|
40
|
+
item: T;
|
|
41
|
+
index: number;
|
|
42
|
+
style: React.CSSProperties;
|
|
43
|
+
className: string;
|
|
44
|
+
selected: boolean;
|
|
45
|
+
highlighted: boolean;
|
|
46
|
+
isLast: boolean;
|
|
47
|
+
onClick: (e: React.MouseEvent) => void;
|
|
48
|
+
onSelectionChange: (selected: boolean) => void;
|
|
49
|
+
}) => React.ReactNode;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function getEstimatedRowHeight(size: CollectionSize): number {
|
|
53
|
+
switch (size) {
|
|
54
|
+
case "xs": return 44;
|
|
55
|
+
case "s": return 52;
|
|
56
|
+
case "m": return 64;
|
|
57
|
+
case "l": return 76;
|
|
58
|
+
case "xl": return 88;
|
|
59
|
+
default: return 64;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const OVERSCAN_COUNT = 8;
|
|
64
|
+
const LOAD_MORE_THRESHOLD = 400;
|
|
65
|
+
|
|
66
|
+
function getScrollParent(element: HTMLElement | null): HTMLElement | null {
|
|
67
|
+
let parent = element?.parentElement ?? null;
|
|
68
|
+
while (parent) {
|
|
69
|
+
const style = getComputedStyle(parent);
|
|
70
|
+
if (
|
|
71
|
+
style.overflowY === "auto" || style.overflowY === "scroll" ||
|
|
72
|
+
style.overflow === "auto" || style.overflow === "scroll"
|
|
73
|
+
) {
|
|
74
|
+
return parent;
|
|
75
|
+
}
|
|
76
|
+
parent = parent.parentElement;
|
|
77
|
+
}
|
|
78
|
+
return document.documentElement;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function ListView<T>({
|
|
82
|
+
data,
|
|
83
|
+
dataLoading = false,
|
|
84
|
+
noMoreToLoad = false,
|
|
85
|
+
dataLoadingError,
|
|
86
|
+
itemCount,
|
|
87
|
+
setItemCount,
|
|
88
|
+
pageSize = 50,
|
|
89
|
+
paginationEnabled = true,
|
|
90
|
+
|
|
91
|
+
onItemClick,
|
|
92
|
+
selectedIds,
|
|
93
|
+
highlightedIds,
|
|
94
|
+
selectionEnabled = true,
|
|
95
|
+
onSelectionChange,
|
|
96
|
+
emptyComponent,
|
|
97
|
+
|
|
98
|
+
size = "m",
|
|
99
|
+
selectedEntityId,
|
|
100
|
+
renderRow
|
|
101
|
+
}: ListViewProps<T>) {
|
|
102
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
103
|
+
const isLoadingMore = useRef(false);
|
|
104
|
+
|
|
105
|
+
// Reset loading gate when loading stops
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!dataLoading) isLoadingMore.current = false;
|
|
108
|
+
}, [dataLoading]);
|
|
109
|
+
|
|
110
|
+
// Virtualization scroll states
|
|
111
|
+
const estimatedRowHeight = getEstimatedRowHeight(size);
|
|
112
|
+
const [effectiveScrollTop, setEffectiveScrollTop] = useState(0);
|
|
113
|
+
const [viewportHeight, setViewportHeight] = useState(800);
|
|
114
|
+
|
|
115
|
+
const paginationStateRef = useRef({ paginationEnabled, noMoreToLoad, itemCount, pageSize });
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
paginationStateRef.current = { paginationEnabled, noMoreToLoad, itemCount, pageSize };
|
|
118
|
+
}, [paginationEnabled, noMoreToLoad, itemCount, pageSize]);
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
const el = containerRef.current;
|
|
122
|
+
if (!el) return;
|
|
123
|
+
const scrollEl = getScrollParent(el);
|
|
124
|
+
if (!scrollEl) return;
|
|
125
|
+
|
|
126
|
+
let rafId: number | null = null;
|
|
127
|
+
|
|
128
|
+
const update = () => {
|
|
129
|
+
rafId = null;
|
|
130
|
+
const scrollRect = scrollEl.getBoundingClientRect();
|
|
131
|
+
const listRect = el.getBoundingClientRect();
|
|
132
|
+
|
|
133
|
+
const listTopRelative = listRect.top - scrollRect.top;
|
|
134
|
+
setEffectiveScrollTop(Math.max(0, -listTopRelative));
|
|
135
|
+
setViewportHeight(scrollRect.height);
|
|
136
|
+
|
|
137
|
+
const { paginationEnabled: pe, noMoreToLoad: nm, itemCount: ic, pageSize: ps } = paginationStateRef.current;
|
|
138
|
+
if (
|
|
139
|
+
pe &&
|
|
140
|
+
!nm &&
|
|
141
|
+
!isLoadingMore.current &&
|
|
142
|
+
scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight < LOAD_MORE_THRESHOLD
|
|
143
|
+
) {
|
|
144
|
+
isLoadingMore.current = true;
|
|
145
|
+
setItemCount?.((ic ?? ps) + ps);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const onScroll = () => {
|
|
150
|
+
if (rafId === null) rafId = requestAnimationFrame(update);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
scrollEl.addEventListener("scroll", onScroll, { passive: true });
|
|
154
|
+
const ro = new ResizeObserver(() => update());
|
|
155
|
+
ro.observe(scrollEl);
|
|
156
|
+
update();
|
|
157
|
+
|
|
158
|
+
return () => {
|
|
159
|
+
scrollEl.removeEventListener("scroll", onScroll);
|
|
160
|
+
ro.disconnect();
|
|
161
|
+
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
162
|
+
};
|
|
163
|
+
}, [setItemCount]);
|
|
164
|
+
|
|
165
|
+
const totalHeight = data.length * estimatedRowHeight;
|
|
166
|
+
const startIndex = Math.max(0, Math.floor(effectiveScrollTop / estimatedRowHeight) - OVERSCAN_COUNT);
|
|
167
|
+
const endIndex = Math.min(
|
|
168
|
+
data.length,
|
|
169
|
+
Math.ceil((effectiveScrollTop + viewportHeight) / estimatedRowHeight) + OVERSCAN_COUNT
|
|
170
|
+
);
|
|
171
|
+
const visibleData = data.slice(startIndex, endIndex);
|
|
172
|
+
const offsetY = startIndex * estimatedRowHeight;
|
|
173
|
+
|
|
174
|
+
const footerHeight = dataLoading ? 48 : (!dataLoading && noMoreToLoad && data.length > 0) ? 32 : 0;
|
|
175
|
+
|
|
176
|
+
const isInitialLoading = dataLoading && data.length === 0;
|
|
177
|
+
const isEmpty = !dataLoading && data.length === 0 && !dataLoadingError;
|
|
178
|
+
|
|
179
|
+
const getItemId = useCallback((item: T): string | number => {
|
|
180
|
+
if (item && typeof item === "object" && "id" in item) {
|
|
181
|
+
const id = (item as Record<"id", unknown>).id;
|
|
182
|
+
return typeof id === "number" ? id : String(id);
|
|
183
|
+
}
|
|
184
|
+
return String(item);
|
|
185
|
+
}, []);
|
|
186
|
+
|
|
187
|
+
const borderMixinClass = "border-surface-200 dark:border-surface-800";
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<div
|
|
191
|
+
ref={containerRef}
|
|
192
|
+
className={cls(
|
|
193
|
+
"w-full",
|
|
194
|
+
selectedEntityId === undefined && `rounded-lg overflow-hidden border ${borderMixinClass}`
|
|
195
|
+
)}
|
|
196
|
+
>
|
|
197
|
+
{dataLoadingError && data.length === 0 ? (
|
|
198
|
+
<div className="flex items-center justify-center p-8">
|
|
199
|
+
<Typography className="text-red-500">
|
|
200
|
+
Error loading data: {dataLoadingError.message}
|
|
201
|
+
</Typography>
|
|
202
|
+
</div>
|
|
203
|
+
) : isInitialLoading ? (
|
|
204
|
+
<div className="flex items-center justify-center py-12 px-8">
|
|
205
|
+
<CircularProgress size="small" />
|
|
206
|
+
</div>
|
|
207
|
+
) : isEmpty ? (
|
|
208
|
+
<div className="w-full flex items-center justify-center py-12 px-8">
|
|
209
|
+
{emptyComponent ?? (
|
|
210
|
+
<Typography variant="label" color="secondary">
|
|
211
|
+
No entries found
|
|
212
|
+
</Typography>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
) : (
|
|
216
|
+
<div style={{ height: totalHeight + footerHeight, position: "relative" }}>
|
|
217
|
+
<div style={{ position: "absolute", top: offsetY, left: 0, right: 0 }}>
|
|
218
|
+
{visibleData.map((item, i) => {
|
|
219
|
+
const actualIndex = startIndex + i;
|
|
220
|
+
const isLast = actualIndex === data.length - 1;
|
|
221
|
+
const id = getItemId(item);
|
|
222
|
+
const selected = selectedIds?.has(id) ?? false;
|
|
223
|
+
const highlighted = highlightedIds?.has(id) ?? false;
|
|
224
|
+
|
|
225
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
226
|
+
if ((e.metaKey || e.ctrlKey) && selectionEnabled) {
|
|
227
|
+
e.preventDefault();
|
|
228
|
+
onSelectionChange?.(item, !selected);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
onItemClick?.(item);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const handleSelectionChange = (val: boolean) => {
|
|
235
|
+
onSelectionChange?.(item, val);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<React.Fragment key={id || actualIndex}>
|
|
240
|
+
{renderRow({
|
|
241
|
+
item,
|
|
242
|
+
index: actualIndex,
|
|
243
|
+
style: { height: estimatedRowHeight },
|
|
244
|
+
className: cls(
|
|
245
|
+
!isLast && "border-b",
|
|
246
|
+
!isLast && borderMixinClass
|
|
247
|
+
),
|
|
248
|
+
selected,
|
|
249
|
+
highlighted,
|
|
250
|
+
isLast,
|
|
251
|
+
onClick: handleClick,
|
|
252
|
+
onSelectionChange: handleSelectionChange
|
|
253
|
+
})}
|
|
254
|
+
</React.Fragment>
|
|
255
|
+
);
|
|
256
|
+
})}
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
{dataLoading && (
|
|
260
|
+
<div
|
|
261
|
+
className="flex items-center justify-center py-3"
|
|
262
|
+
style={{ position: "absolute", top: totalHeight, left: 0, right: 0 }}
|
|
263
|
+
>
|
|
264
|
+
<CircularProgress size="small" />
|
|
265
|
+
</div>
|
|
266
|
+
)}
|
|
267
|
+
{!dataLoading && noMoreToLoad && data.length > 0 && (
|
|
268
|
+
<div
|
|
269
|
+
className="flex items-center justify-center py-2 dark:bg-surface-900"
|
|
270
|
+
style={{ position: "absolute", top: totalHeight, left: 0, right: 0 }}
|
|
271
|
+
>
|
|
272
|
+
<Typography variant="caption" color="secondary">
|
|
273
|
+
All {data.length} entries loaded
|
|
274
|
+
</Typography>
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { VirtualTable as TableView } from "../components/VirtualTable/VirtualTable";
|
|
2
|
+
export type { VirtualTableProps as TableViewProps } from "../components/VirtualTable/VirtualTableProps";
|
|
3
|
+
|
|
4
|
+
export type OnColumnResizeParams = {
|
|
5
|
+
width: number;
|
|
6
|
+
key: string;
|
|
7
|
+
};
|