@rebasepro/ui 0.17.3 → 0.18.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 +4 -0
- package/dist/components/MultiSelect.d.ts +10 -0
- package/dist/index.es.js +2 -1
- package/dist/index.es.js.map +1 -1
- package/package.json +32 -23
- package/src/components/Alert.tsx +0 -118
- package/src/components/Autocomplete.tsx +0 -93
- package/src/components/Avatar.tsx +0 -73
- package/src/components/Badge.tsx +0 -44
- package/src/components/BooleanSwitch.tsx +0 -116
- package/src/components/BooleanSwitchWithLabel.tsx +0 -120
- package/src/components/Button.tsx +0 -110
- package/src/components/Card.tsx +0 -43
- package/src/components/CenteredView.tsx +0 -35
- package/src/components/Checkbox.tsx +0 -129
- package/src/components/Chip.tsx +0 -131
- package/src/components/CircularProgress.tsx +0 -53
- package/src/components/CircularProgressCenter.tsx +0 -27
- package/src/components/Collapse.tsx +0 -66
- package/src/components/ColorPicker.tsx +0 -140
- package/src/components/Container.tsx +0 -51
- package/src/components/DateTimeField.tsx +0 -363
- package/src/components/DebouncedTextField.tsx +0 -76
- package/src/components/Dialog.tsx +0 -159
- package/src/components/DialogActions.tsx +0 -29
- package/src/components/DialogContent.tsx +0 -27
- package/src/components/DialogTitle.tsx +0 -41
- package/src/components/ErrorBoundary.tsx +0 -236
- package/src/components/ExpandablePanel.tsx +0 -130
- package/src/components/FileUpload.tsx +0 -103
- package/src/components/FilterChip.tsx +0 -89
- package/src/components/IconButton.tsx +0 -101
- package/src/components/InfoLabel.tsx +0 -23
- package/src/components/InputLabel.tsx +0 -40
- package/src/components/Label.tsx +0 -32
- package/src/components/LoadingButton.tsx +0 -32
- package/src/components/Markdown.tsx +0 -55
- package/src/components/Menu.tsx +0 -114
- package/src/components/Menubar.tsx +0 -355
- package/src/components/MultiSelect.tsx +0 -433
- package/src/components/Paper.tsx +0 -23
- package/src/components/Popover.tsx +0 -162
- package/src/components/RadioGroup.tsx +0 -74
- package/src/components/ResizablePanels.tsx +0 -189
- package/src/components/SearchBar.tsx +0 -136
- package/src/components/Select.tsx +0 -326
- package/src/components/Separator.tsx +0 -28
- package/src/components/Sheet.tsx +0 -135
- package/src/components/Skeleton.tsx +0 -54
- package/src/components/Slider.tsx +0 -110
- package/src/components/Table.tsx +0 -178
- package/src/components/Tabs.tsx +0 -185
- package/src/components/TextField.tsx +0 -270
- package/src/components/TextareaAutosize.tsx +0 -368
- package/src/components/ToggleButtonGroup.tsx +0 -69
- package/src/components/Tooltip.tsx +0 -153
- package/src/components/Typography.tsx +0 -132
- package/src/components/VirtualTable/VirtualTable.performance.test.tsx +0 -90
- package/src/components/VirtualTable/VirtualTable.tsx +0 -700
- package/src/components/VirtualTable/VirtualTableCell.tsx +0 -59
- package/src/components/VirtualTable/VirtualTableHeader.tsx +0 -327
- package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +0 -250
- package/src/components/VirtualTable/VirtualTableProps.tsx +0 -339
- package/src/components/VirtualTable/VirtualTableRow.tsx +0 -51
- package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +0 -44
- package/src/components/VirtualTable/fields/VirtualTableInput.tsx +0 -89
- package/src/components/VirtualTable/fields/VirtualTableNumberInput.tsx +0 -71
- package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +0 -107
- package/src/components/VirtualTable/fields/VirtualTableSwitch.tsx +0 -28
- package/src/components/VirtualTable/index.tsx +0 -10
- package/src/components/VirtualTable/selection/SelectionContext.tsx +0 -36
- package/src/components/VirtualTable/selection/SelectionStore.ts +0 -54
- package/src/components/VirtualTable/types.tsx +0 -47
- package/src/components/common/SelectInputLabel.tsx +0 -23
- package/src/components/index.tsx +0 -62
- package/src/hooks/PortalContainerContext.tsx +0 -50
- package/src/hooks/index.ts +0 -7
- package/src/hooks/useDebounceCallback.tsx +0 -21
- package/src/hooks/useDebounceValue.tsx +0 -20
- package/src/hooks/useDebouncedCallback.ts +0 -39
- package/src/hooks/useInjectStyles.tsx +0 -31
- package/src/hooks/useOutsideAlerter.tsx +0 -46
- package/src/icons/GitHubIcon.tsx +0 -21
- package/src/icons/HandleIcon.tsx +0 -23
- package/src/icons/Icon.tsx +0 -33
- package/src/icons/LucideIconByName.tsx +0 -117
- package/src/icons/cool_icon_keys.ts +0 -12
- package/src/icons/icon_keys.ts +0 -1936
- package/src/icons/index.ts +0 -179
- package/src/index.css +0 -169
- package/src/index.ts +0 -47
- package/src/scripts/fixIconSizes.cjs +0 -92
- package/src/scripts/fixIconSizesToConst.cjs +0 -106
- package/src/scripts/generateIconKeys.ts +0 -41
- package/src/scripts/generateIcons.ts +0 -4
- package/src/scripts/saveIconFiles.ts +0 -89
- package/src/styles.ts +0 -141
- package/src/theme.css +0 -168
- package/src/util/chip_colors.ts +0 -242
- package/src/util/cls.ts +0 -8
- package/src/util/debounce.ts +0 -28
- package/src/util/hash.ts +0 -11
- package/src/util/index.ts +0 -4
- package/src/util/key_to_icon_component.ts +0 -11
- package/src/util/lazy_chunk.ts +0 -101
- package/src/views/CardView.tsx +0 -281
- package/src/views/CollectionView/CollectionCardView.tsx +0 -265
- package/src/views/CollectionView/CollectionKanbanView.tsx +0 -246
- package/src/views/CollectionView/CollectionListView.tsx +0 -240
- package/src/views/CollectionView/CollectionTableView.tsx +0 -229
- package/src/views/CollectionView/CollectionView.tsx +0 -450
- package/src/views/CollectionView/CollectionViewToolbar.tsx +0 -242
- package/src/views/CollectionView/CollectionViewTypes.ts +0 -181
- package/src/views/CollectionView/DefaultCellRenderer.tsx +0 -404
- package/src/views/CollectionView/index.ts +0 -39
- package/src/views/CollectionView/resolveTitleProperty.ts +0 -52
- package/src/views/CollectionView/utils.ts +0 -14
- package/src/views/Kanban/Board.tsx +0 -384
- package/src/views/Kanban/BoardColumn.tsx +0 -138
- package/src/views/Kanban/BoardColumnTitle.tsx +0 -47
- package/src/views/Kanban/BoardSortableList.tsx +0 -182
- package/src/views/Kanban/board_types.ts +0 -80
- package/src/views/Kanban/index.tsx +0 -5
- package/src/views/Kanban/placement.ts +0 -62
- package/src/views/KanbanView.tsx +0 -32
- package/src/views/ListView.tsx +0 -296
- package/src/views/index.ts +0 -5
|
@@ -1,700 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import React, { createContext, forwardRef, RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
-
|
|
5
|
-
import { deepEqual as equal } from "fast-equals"
|
|
6
|
-
|
|
7
|
-
import { FixedSizeList as List } from "react-window";
|
|
8
|
-
import useMeasure from "react-use-measure";
|
|
9
|
-
|
|
10
|
-
import { CircularProgress } from "../CircularProgress";
|
|
11
|
-
import {
|
|
12
|
-
OnVirtualTableColumnResizeParams,
|
|
13
|
-
VirtualTableColumn,
|
|
14
|
-
VirtualTableFilterValues,
|
|
15
|
-
VirtualTableProps,
|
|
16
|
-
VirtualTableSortKey,
|
|
17
|
-
VirtualTableWhereFilterOp
|
|
18
|
-
} from "./VirtualTableProps";
|
|
19
|
-
|
|
20
|
-
import { VirtualTableContextProps } from "./types";
|
|
21
|
-
import { VirtualTableHeaderRow } from "./VirtualTableHeaderRow";
|
|
22
|
-
import { VirtualTableRow } from "./VirtualTableRow";
|
|
23
|
-
import { VirtualTableCell } from "./VirtualTableCell";
|
|
24
|
-
import { CenteredView } from "../CenteredView";
|
|
25
|
-
import { Typography } from "../Typography";
|
|
26
|
-
import { cls } from "../../util/cls";
|
|
27
|
-
import { useDebounceCallback } from "../../hooks/useDebounceCallback";
|
|
28
|
-
import {
|
|
29
|
-
closestCenter,
|
|
30
|
-
DndContext,
|
|
31
|
-
DragEndEvent,
|
|
32
|
-
DragStartEvent,
|
|
33
|
-
PointerSensor,
|
|
34
|
-
useSensor,
|
|
35
|
-
useSensors
|
|
36
|
-
} from "@dnd-kit/core";
|
|
37
|
-
import { arrayMove, horizontalListSortingStrategy, SortableContext, useSortable } from "@dnd-kit/sortable";
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const VirtualListContext = createContext<VirtualTableContextProps<Record<string, unknown>>>(null! as VirtualTableContextProps<Record<string, unknown>>);
|
|
41
|
-
VirtualListContext.displayName = "VirtualListContext";
|
|
42
|
-
|
|
43
|
-
type InnerElementProps = {
|
|
44
|
-
children: React.ReactNode,
|
|
45
|
-
style: React.CSSProperties
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// eslint-disable-next-line react/display-name
|
|
49
|
-
const innerElementType = forwardRef<HTMLDivElement, InnerElementProps>(({
|
|
50
|
-
children,
|
|
51
|
-
...rest
|
|
52
|
-
}: InnerElementProps, ref) => {
|
|
53
|
-
|
|
54
|
-
return (
|
|
55
|
-
<VirtualListContext.Consumer>
|
|
56
|
-
{(virtualTableProps) => {
|
|
57
|
-
const customView = virtualTableProps.customView;
|
|
58
|
-
const headerHeight = virtualTableProps.headerHeight ?? 48;
|
|
59
|
-
return (
|
|
60
|
-
<>
|
|
61
|
-
<div
|
|
62
|
-
id={"virtual-table"}
|
|
63
|
-
style={{
|
|
64
|
-
position: "relative",
|
|
65
|
-
height: "100%",
|
|
66
|
-
zIndex: 0
|
|
67
|
-
}}>
|
|
68
|
-
<div
|
|
69
|
-
ref={ref}
|
|
70
|
-
{...rest}
|
|
71
|
-
style={{
|
|
72
|
-
...rest?.style,
|
|
73
|
-
height: (typeof rest?.style?.height === "number" ? rest.style.height : parseFloat((rest?.style?.height as string) || "0")) + headerHeight,
|
|
74
|
-
minHeight: "100%",
|
|
75
|
-
position: "relative"
|
|
76
|
-
}}>
|
|
77
|
-
<div style={{ position: "sticky",
|
|
78
|
-
top: 0,
|
|
79
|
-
zIndex: 20 }}>
|
|
80
|
-
<VirtualTableHeaderRow {...virtualTableProps}/>
|
|
81
|
-
</div>
|
|
82
|
-
{!customView && children}
|
|
83
|
-
</div>
|
|
84
|
-
|
|
85
|
-
</div>
|
|
86
|
-
|
|
87
|
-
{customView && <div style={{
|
|
88
|
-
position: "sticky",
|
|
89
|
-
top: virtualTableProps.headerHeight ?? 48,
|
|
90
|
-
flexGrow: 1,
|
|
91
|
-
height: `calc(100% - ${virtualTableProps.headerHeight ?? 48}px)`,
|
|
92
|
-
marginTop: `calc(${(virtualTableProps.headerHeight ?? 48)}px - 100vh)`,
|
|
93
|
-
left: 0
|
|
94
|
-
}}>{customView}</div>}
|
|
95
|
-
|
|
96
|
-
</>
|
|
97
|
-
);
|
|
98
|
-
}}
|
|
99
|
-
</VirtualListContext.Consumer>
|
|
100
|
-
);
|
|
101
|
-
})
|
|
102
|
-
;
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* This is a Table component that allows displaying arbitrary data, not
|
|
106
|
-
* necessarily related to entities or properties. It is the component
|
|
107
|
-
* that powers the entity collections but has a generic API, so it
|
|
108
|
-
* can be reused.
|
|
109
|
-
*
|
|
110
|
-
* @group Components
|
|
111
|
-
*/
|
|
112
|
-
const VirtualTableInner = React.memo<VirtualTableProps<Record<string, unknown>>>(
|
|
113
|
-
function VirtualTable<T extends Record<string, unknown>>({
|
|
114
|
-
data,
|
|
115
|
-
onResetPagination,
|
|
116
|
-
onEndReached,
|
|
117
|
-
endOffset = 600,
|
|
118
|
-
rowHeight = 54,
|
|
119
|
-
headerHeight = 48,
|
|
120
|
-
columns: columnsProp,
|
|
121
|
-
onRowClick,
|
|
122
|
-
onColumnResize,
|
|
123
|
-
onColumnResizeEnd,
|
|
124
|
-
filter: filterInput,
|
|
125
|
-
checkFilterCombination,
|
|
126
|
-
onFilterUpdate,
|
|
127
|
-
sortBy,
|
|
128
|
-
error,
|
|
129
|
-
emptyComponent,
|
|
130
|
-
onSortByUpdate,
|
|
131
|
-
onScroll: onScrollProp,
|
|
132
|
-
loading,
|
|
133
|
-
cellRenderer,
|
|
134
|
-
hoverRow,
|
|
135
|
-
createFilterField,
|
|
136
|
-
rowClassName,
|
|
137
|
-
style,
|
|
138
|
-
className,
|
|
139
|
-
endAdornment,
|
|
140
|
-
AddColumnComponent,
|
|
141
|
-
initialScroll = 0,
|
|
142
|
-
onColumnsOrderChange,
|
|
143
|
-
extraData
|
|
144
|
-
}: VirtualTableProps<T>) {
|
|
145
|
-
|
|
146
|
-
// Where each sorted column sits in the order, so a header can show both
|
|
147
|
-
// which way it runs and how much it counts: `roles ↑1, created_at ↓2`
|
|
148
|
-
// reads as "by role, newest first within each role". Without the rank a
|
|
149
|
-
// multi-key sort shows two arrows and no way to tell which one wins.
|
|
150
|
-
const sortIndex = useMemo(() => {
|
|
151
|
-
const index = new Map<string, { direction: "asc" | "desc"; position: number }>();
|
|
152
|
-
(sortBy ?? []).forEach(([key, direction], position) => {
|
|
153
|
-
if (!index.has(key)) index.set(key, { direction,
|
|
154
|
-
position });
|
|
155
|
-
});
|
|
156
|
-
return index;
|
|
157
|
-
}, [sortBy]);
|
|
158
|
-
|
|
159
|
-
const [columns, setColumns] = useState(columnsProp);
|
|
160
|
-
|
|
161
|
-
const tableRef = useRef<HTMLDivElement>(null);
|
|
162
|
-
const endReachCallbackThreshold = useRef<number>(0);
|
|
163
|
-
|
|
164
|
-
const debouncedScroll = useDebounceCallback(onScrollProp, 200);
|
|
165
|
-
|
|
166
|
-
// Drag and drop state
|
|
167
|
-
const [draggingColumnId, setDraggingColumnId] = useState<string | null>(null);
|
|
168
|
-
|
|
169
|
-
const sensors = useSensors(
|
|
170
|
-
useSensor(PointerSensor, {
|
|
171
|
-
activationConstraint: {
|
|
172
|
-
distance: 5
|
|
173
|
-
}
|
|
174
|
-
})
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
178
|
-
setDraggingColumnId(event.active.id as string);
|
|
179
|
-
}, []);
|
|
180
|
-
|
|
181
|
-
const handleDragEnd = useCallback((event: DragEndEvent) => {
|
|
182
|
-
const {
|
|
183
|
-
active,
|
|
184
|
-
over
|
|
185
|
-
} = event;
|
|
186
|
-
setDraggingColumnId(null);
|
|
187
|
-
|
|
188
|
-
if (over && active.id !== over.id && onColumnsOrderChange) {
|
|
189
|
-
const oldIndex = columns.findIndex((col) => col.key === active.id);
|
|
190
|
-
const newIndex = columns.findIndex((col) => col.key === over.id);
|
|
191
|
-
|
|
192
|
-
if (oldIndex !== -1 && newIndex !== -1) {
|
|
193
|
-
const newColumns = arrayMove(columns, oldIndex, newIndex);
|
|
194
|
-
setColumns(newColumns);
|
|
195
|
-
onColumnsOrderChange(newColumns);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}, [columns, onColumnsOrderChange]);
|
|
199
|
-
|
|
200
|
-
const handleDragCancel = useCallback(() => {
|
|
201
|
-
setDraggingColumnId(null);
|
|
202
|
-
}, []);
|
|
203
|
-
|
|
204
|
-
// Set initial scroll position
|
|
205
|
-
useEffect(() => {
|
|
206
|
-
if (tableRef.current && initialScroll) {
|
|
207
|
-
const { scrollLeft } = tableRef.current;
|
|
208
|
-
tableRef.current.scrollTo(scrollLeft, initialScroll);
|
|
209
|
-
}
|
|
210
|
-
}, [tableRef, initialScroll]);
|
|
211
|
-
|
|
212
|
-
useEffect(() => {
|
|
213
|
-
setColumns(columnsProp);
|
|
214
|
-
}, [columnsProp]);
|
|
215
|
-
|
|
216
|
-
// Removed redundant ResizeObserver to prevent performance issues and UI flickering.
|
|
217
|
-
|
|
218
|
-
const [measureRef, bounds] = useMeasure({
|
|
219
|
-
polyfill: ResizeObserver,
|
|
220
|
-
scroll: true,
|
|
221
|
-
// This is important for handling zooming in react-flow
|
|
222
|
-
offsetSize: true
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
const onColumnResizeInternal = useCallback((params: OnVirtualTableColumnResizeParams) => {
|
|
226
|
-
setColumns(prevColumns =>
|
|
227
|
-
prevColumns.map((column) =>
|
|
228
|
-
column.key === params.column.key ? params.column : column
|
|
229
|
-
)
|
|
230
|
-
);
|
|
231
|
-
}, []);
|
|
232
|
-
|
|
233
|
-
const onColumnResizeEndInternal = useCallback((params: OnVirtualTableColumnResizeParams) => {
|
|
234
|
-
setColumns(columns.map((column) => column.key === params.column.key ? params.column : column));
|
|
235
|
-
if (onColumnResize) {
|
|
236
|
-
onColumnResize(params);
|
|
237
|
-
}
|
|
238
|
-
if (onColumnResizeEnd) {
|
|
239
|
-
onColumnResizeEnd(params);
|
|
240
|
-
}
|
|
241
|
-
}, [columns, onColumnResize, onColumnResizeEnd]);
|
|
242
|
-
|
|
243
|
-
// saving the current filter as a ref as a workaround for header closure
|
|
244
|
-
const filterRef = useRef<VirtualTableFilterValues<string> | undefined>(undefined);
|
|
245
|
-
|
|
246
|
-
useEffect(() => {
|
|
247
|
-
filterRef.current = filterInput;
|
|
248
|
-
}, [filterInput]);
|
|
249
|
-
|
|
250
|
-
const scrollToTop = useCallback(() => {
|
|
251
|
-
endReachCallbackThreshold.current = 0;
|
|
252
|
-
if (tableRef.current) {
|
|
253
|
-
tableRef.current.scrollTo(tableRef.current?.scrollLeft, 0);
|
|
254
|
-
}
|
|
255
|
-
}, []);
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Click a header to sort by that column: ascending, descending, then
|
|
259
|
-
* off. Shift-click to keep the sort you already have and add this
|
|
260
|
-
* column under it — the same idiom spreadsheets use, and the only way
|
|
261
|
-
* to build "by role, newest first" out of two clicks.
|
|
262
|
-
*
|
|
263
|
-
* Shift cycles the column it lands on through the same three states, so
|
|
264
|
-
* a key added by mistake is removed by shift-clicking it twice more,
|
|
265
|
-
* without disturbing the keys around it.
|
|
266
|
-
*/
|
|
267
|
-
const onColumnSort = useCallback((key: string, additive = false) => {
|
|
268
|
-
|
|
269
|
-
const current = sortIndex.get(key);
|
|
270
|
-
const next: "asc" | "desc" | undefined = current === undefined
|
|
271
|
-
? "asc"
|
|
272
|
-
: current.direction === "asc" ? "desc" : undefined;
|
|
273
|
-
|
|
274
|
-
const existing = sortBy ?? [];
|
|
275
|
-
let newSortBy: VirtualTableSortKey[] | undefined;
|
|
276
|
-
if (!additive) {
|
|
277
|
-
newSortBy = next ? [[key, next]] : undefined;
|
|
278
|
-
} else if (next === undefined) {
|
|
279
|
-
newSortBy = existing.filter(([existingKey]) => existingKey !== key);
|
|
280
|
-
} else if (current === undefined) {
|
|
281
|
-
newSortBy = [...existing, [key, next]];
|
|
282
|
-
} else {
|
|
283
|
-
newSortBy = existing.map((entry) => entry[0] === key ? [key, next] as VirtualTableSortKey : entry);
|
|
284
|
-
}
|
|
285
|
-
if (newSortBy?.length === 0) newSortBy = undefined;
|
|
286
|
-
|
|
287
|
-
const filter = filterRef.current;
|
|
288
|
-
if (filter) {
|
|
289
|
-
if (checkFilterCombination && !checkFilterCombination(filter, newSortBy)) {
|
|
290
|
-
if (onFilterUpdate)
|
|
291
|
-
onFilterUpdate(undefined);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (onResetPagination) {
|
|
296
|
-
onResetPagination();
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (onSortByUpdate) {
|
|
300
|
-
onSortByUpdate(newSortBy);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
scrollToTop();
|
|
304
|
-
}, [checkFilterCombination, onFilterUpdate, onResetPagination, onSortByUpdate, scrollToTop, sortBy, sortIndex]);
|
|
305
|
-
|
|
306
|
-
const maxScroll = Math.max((data?.length ?? 0) * rowHeight - bounds.height, 0);
|
|
307
|
-
|
|
308
|
-
const onEndReachedInternal = useCallback((scrollOffset: number) => {
|
|
309
|
-
if (onEndReached && (data?.length ?? 0) > 0 && scrollOffset > endReachCallbackThreshold.current + endOffset) {
|
|
310
|
-
endReachCallbackThreshold.current = scrollOffset;
|
|
311
|
-
onEndReached();
|
|
312
|
-
}
|
|
313
|
-
}, [data?.length, onEndReached]);
|
|
314
|
-
|
|
315
|
-
const onScroll = useCallback(({
|
|
316
|
-
scrollDirection,
|
|
317
|
-
scrollOffset,
|
|
318
|
-
scrollUpdateWasRequested
|
|
319
|
-
}: {
|
|
320
|
-
scrollDirection: "forward" | "backward",
|
|
321
|
-
scrollOffset: number,
|
|
322
|
-
scrollUpdateWasRequested: boolean;
|
|
323
|
-
}) => {
|
|
324
|
-
if (onScrollProp) {
|
|
325
|
-
debouncedScroll({
|
|
326
|
-
scrollDirection,
|
|
327
|
-
scrollOffset,
|
|
328
|
-
scrollUpdateWasRequested
|
|
329
|
-
})
|
|
330
|
-
}
|
|
331
|
-
if (!scrollUpdateWasRequested && (scrollOffset >= maxScroll - endOffset))
|
|
332
|
-
onEndReachedInternal(scrollOffset);
|
|
333
|
-
}, [maxScroll, onEndReachedInternal]);
|
|
334
|
-
|
|
335
|
-
const onFilterUpdateInternal = useCallback((column: VirtualTableColumn, filterForProperty?: [VirtualTableWhereFilterOp, unknown]) => {
|
|
336
|
-
|
|
337
|
-
endReachCallbackThreshold.current = 0;
|
|
338
|
-
const filter = filterRef.current;
|
|
339
|
-
let newFilterValue: VirtualTableFilterValues<string> = filter ? { ...filter } : {};
|
|
340
|
-
|
|
341
|
-
if (!filterForProperty) {
|
|
342
|
-
delete newFilterValue[column.key];
|
|
343
|
-
} else {
|
|
344
|
-
newFilterValue[column.key] = filterForProperty;
|
|
345
|
-
}
|
|
346
|
-
const isNewFilterCombinationValid = !checkFilterCombination || checkFilterCombination(newFilterValue, sortBy);
|
|
347
|
-
if (!isNewFilterCombinationValid) {
|
|
348
|
-
newFilterValue = filterForProperty ? { [column.key]: filterForProperty } as VirtualTableFilterValues<Extract<keyof T, string>> : {};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (onFilterUpdate) onFilterUpdate(newFilterValue);
|
|
352
|
-
}, [checkFilterCombination, onFilterUpdate, sortBy]);
|
|
353
|
-
|
|
354
|
-
const empty = !loading && (data?.length ?? 0) === 0;
|
|
355
|
-
const customView = (error && (data?.length ?? 0) === 0)
|
|
356
|
-
? <CenteredView maxWidth={"2xl"}
|
|
357
|
-
className="flex flex-col gap-2">
|
|
358
|
-
|
|
359
|
-
<Typography variant={"h6"}>
|
|
360
|
-
{"Error"}
|
|
361
|
-
</Typography>
|
|
362
|
-
|
|
363
|
-
{error?.message && <SafeLinkRenderer text={error.message}/>}
|
|
364
|
-
|
|
365
|
-
</CenteredView>
|
|
366
|
-
: (empty
|
|
367
|
-
? (loading
|
|
368
|
-
? <div className="flex items-center justify-center py-12 px-8">
|
|
369
|
-
<CircularProgress size="small"/>
|
|
370
|
-
</div>
|
|
371
|
-
: <div
|
|
372
|
-
className="flex flex-col overflow-auto items-center justify-center py-12 px-8">
|
|
373
|
-
{emptyComponent}
|
|
374
|
-
</div>)
|
|
375
|
-
: undefined);
|
|
376
|
-
|
|
377
|
-
const virtualListController = useMemo(() => ({
|
|
378
|
-
data,
|
|
379
|
-
rowHeight: rowHeight,
|
|
380
|
-
headerHeight: headerHeight,
|
|
381
|
-
cellRenderer,
|
|
382
|
-
columns,
|
|
383
|
-
onRowClick,
|
|
384
|
-
customView,
|
|
385
|
-
onColumnResize: onColumnResizeInternal,
|
|
386
|
-
onColumnResizeEnd: onColumnResizeEndInternal,
|
|
387
|
-
filter: filterRef.current,
|
|
388
|
-
onColumnSort,
|
|
389
|
-
onFilterUpdate: onFilterUpdateInternal,
|
|
390
|
-
sortIndex,
|
|
391
|
-
hoverRow: hoverRow ?? false,
|
|
392
|
-
createFilterField,
|
|
393
|
-
rowClassName,
|
|
394
|
-
endAdornment,
|
|
395
|
-
AddColumnComponent,
|
|
396
|
-
onColumnsOrderChange: onColumnsOrderChange ? (newColumns: VirtualTableColumn[]) => {
|
|
397
|
-
setColumns(newColumns);
|
|
398
|
-
onColumnsOrderChange(newColumns);
|
|
399
|
-
} : undefined,
|
|
400
|
-
draggingColumnId,
|
|
401
|
-
extraData
|
|
402
|
-
}), [data, rowHeight, cellRenderer, columns, onRowClick, customView, onColumnResizeInternal, onColumnResizeEndInternal, filterInput, onColumnSort, onFilterUpdateInternal, sortIndex, hoverRow, createFilterField, rowClassName, endAdornment, AddColumnComponent, onColumnsOrderChange, draggingColumnId, extraData]);
|
|
403
|
-
|
|
404
|
-
// Get sortable column keys (excluding frozen columns)
|
|
405
|
-
const sortableColumnKeys = columns
|
|
406
|
-
.filter(col => !col.frozen)
|
|
407
|
-
.map(col => col.key);
|
|
408
|
-
|
|
409
|
-
const tableContent = (
|
|
410
|
-
<div
|
|
411
|
-
ref={measureRef}
|
|
412
|
-
style={style}
|
|
413
|
-
className={cls(
|
|
414
|
-
"h-full w-full",
|
|
415
|
-
className,
|
|
416
|
-
draggingColumnId && "overflow-hidden"
|
|
417
|
-
)}>
|
|
418
|
-
{/* SAFETY: T extends Record<string, any> is structurally assignable to Record<string, unknown> */}
|
|
419
|
-
<VirtualListContext.Provider
|
|
420
|
-
value={virtualListController as VirtualTableContextProps<Record<string, unknown>>}>
|
|
421
|
-
|
|
422
|
-
<MemoizedList
|
|
423
|
-
outerRef={tableRef}
|
|
424
|
-
key={rowHeight}
|
|
425
|
-
width={bounds.width}
|
|
426
|
-
height={bounds.height}
|
|
427
|
-
itemCount={(data?.length ?? 0) + (endAdornment ? 1 : 0)}
|
|
428
|
-
onScroll={draggingColumnId ? undefined : onScroll}
|
|
429
|
-
includeAddColumn={Boolean(AddColumnComponent)}
|
|
430
|
-
itemSize={rowHeight}/>
|
|
431
|
-
|
|
432
|
-
</VirtualListContext.Provider>
|
|
433
|
-
</div>
|
|
434
|
-
);
|
|
435
|
-
|
|
436
|
-
// Wrap with DndContext if column reorder is enabled
|
|
437
|
-
if (onColumnsOrderChange) {
|
|
438
|
-
return (
|
|
439
|
-
<DndContext
|
|
440
|
-
sensors={sensors}
|
|
441
|
-
collisionDetection={closestCenter}
|
|
442
|
-
onDragStart={handleDragStart}
|
|
443
|
-
onDragEnd={handleDragEnd}
|
|
444
|
-
onDragCancel={handleDragCancel}
|
|
445
|
-
>
|
|
446
|
-
<SortableContext
|
|
447
|
-
items={sortableColumnKeys}
|
|
448
|
-
strategy={horizontalListSortingStrategy}
|
|
449
|
-
>
|
|
450
|
-
{tableContent}
|
|
451
|
-
</SortableContext>
|
|
452
|
-
</DndContext>
|
|
453
|
-
);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
return tableContent;
|
|
457
|
-
},
|
|
458
|
-
equal
|
|
459
|
-
);
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* `React.memo` is not generic-preserving: it takes the props type as its own
|
|
463
|
-
* type argument, so annotating the call with
|
|
464
|
-
* `VirtualTableProps<Record<string, unknown>>` pinned `T` at the boundary and
|
|
465
|
-
* discarded the `<T>` the inner function declares. The exported component was
|
|
466
|
-
* therefore not generic at all — every consumer got `Record<string, unknown>`,
|
|
467
|
-
* `columns` and `cellRenderer` were unrelated to `data`, and a caller writing
|
|
468
|
-
* `<VirtualTable<Post> …>` was told the component expects no type arguments.
|
|
469
|
-
*
|
|
470
|
-
* Re-asserting the call signature is the standard way to carry a type parameter
|
|
471
|
-
* across `memo`; it is the only cast here, and it restores the generic the
|
|
472
|
-
* implementation always had. Runtime behaviour is unchanged — this is the same
|
|
473
|
-
* memoized component under a type that describes it.
|
|
474
|
-
*/
|
|
475
|
-
export const VirtualTable = VirtualTableInner as <T extends Record<string, unknown>>(
|
|
476
|
-
props: VirtualTableProps<T>
|
|
477
|
-
) => React.ReactElement | null;
|
|
478
|
-
// Wrapper that applies sortable transforms to cells
|
|
479
|
-
const SortableCellWrapper = ({
|
|
480
|
-
columnKey,
|
|
481
|
-
width,
|
|
482
|
-
isDragging,
|
|
483
|
-
isDraggable,
|
|
484
|
-
frozen,
|
|
485
|
-
children
|
|
486
|
-
}: {
|
|
487
|
-
columnKey: string;
|
|
488
|
-
width: number;
|
|
489
|
-
isDragging: boolean;
|
|
490
|
-
isDraggable: boolean;
|
|
491
|
-
frozen?: boolean;
|
|
492
|
-
children: React.ReactNode;
|
|
493
|
-
}) => {
|
|
494
|
-
const {
|
|
495
|
-
attributes,
|
|
496
|
-
listeners,
|
|
497
|
-
setNodeRef,
|
|
498
|
-
transform,
|
|
499
|
-
transition
|
|
500
|
-
} = useSortable({
|
|
501
|
-
id: columnKey,
|
|
502
|
-
disabled: !isDraggable || frozen
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
// Remove tabIndex from attributes to avoid capturing focus before cell content
|
|
506
|
-
const { tabIndex: _tabIndex, ...attrsWithoutTabIndex } = attributes;
|
|
507
|
-
|
|
508
|
-
const style = {
|
|
509
|
-
// Only use translate, ignore any scale transforms
|
|
510
|
-
transform: transform ? `translateX(${transform.x}px)` : undefined,
|
|
511
|
-
// Don't transition the dragged item - only other items should animate
|
|
512
|
-
transition: isDragging ? undefined : transition,
|
|
513
|
-
minWidth: width,
|
|
514
|
-
maxWidth: width,
|
|
515
|
-
width: width
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
return (
|
|
519
|
-
<div
|
|
520
|
-
ref={setNodeRef}
|
|
521
|
-
style={style}
|
|
522
|
-
className={cls(
|
|
523
|
-
"flex-shrink-0",
|
|
524
|
-
frozen && "sticky left-0 z-10 bg-white dark:bg-surface-900"
|
|
525
|
-
)}
|
|
526
|
-
{...attrsWithoutTabIndex}
|
|
527
|
-
>
|
|
528
|
-
{children}
|
|
529
|
-
</div>
|
|
530
|
-
);
|
|
531
|
-
};
|
|
532
|
-
|
|
533
|
-
function MemoizedList({
|
|
534
|
-
outerRef,
|
|
535
|
-
width,
|
|
536
|
-
height,
|
|
537
|
-
itemCount,
|
|
538
|
-
onScroll,
|
|
539
|
-
itemSize,
|
|
540
|
-
includeAddColumn
|
|
541
|
-
}: {
|
|
542
|
-
outerRef: RefObject<HTMLDivElement | null>;
|
|
543
|
-
width: number;
|
|
544
|
-
height: number;
|
|
545
|
-
itemCount: number;
|
|
546
|
-
onScroll?: (params: {
|
|
547
|
-
scrollDirection: "forward" | "backward",
|
|
548
|
-
scrollOffset: number,
|
|
549
|
-
scrollUpdateWasRequested: boolean;
|
|
550
|
-
}) => void;
|
|
551
|
-
itemSize: number;
|
|
552
|
-
includeAddColumn?: boolean;
|
|
553
|
-
}) {
|
|
554
|
-
|
|
555
|
-
const Row = useCallback(({
|
|
556
|
-
index,
|
|
557
|
-
style
|
|
558
|
-
}: { index: number; style: React.CSSProperties }) => {
|
|
559
|
-
return <VirtualListContext.Consumer>
|
|
560
|
-
{({
|
|
561
|
-
onRowClick,
|
|
562
|
-
data,
|
|
563
|
-
columns,
|
|
564
|
-
rowHeight = 54,
|
|
565
|
-
headerHeight = 48,
|
|
566
|
-
cellRenderer,
|
|
567
|
-
hoverRow,
|
|
568
|
-
rowClassName,
|
|
569
|
-
endAdornment,
|
|
570
|
-
draggingColumnId,
|
|
571
|
-
onColumnsOrderChange,
|
|
572
|
-
extraData
|
|
573
|
-
}) => {
|
|
574
|
-
|
|
575
|
-
if (endAdornment && index === (data ?? []).length) {
|
|
576
|
-
return <div style={{
|
|
577
|
-
...style,
|
|
578
|
-
height: "auto",
|
|
579
|
-
position: "sticky",
|
|
580
|
-
bottom: 0,
|
|
581
|
-
zIndex: 1
|
|
582
|
-
}}>
|
|
583
|
-
{endAdornment}
|
|
584
|
-
</div>;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
const rowData = (data ? data[index] : undefined) as Record<string, unknown>;
|
|
588
|
-
return (
|
|
589
|
-
<VirtualTableRow
|
|
590
|
-
key={`row_${index}`}
|
|
591
|
-
rowData={rowData}
|
|
592
|
-
rowIndex={index}
|
|
593
|
-
onRowClick={onRowClick}
|
|
594
|
-
columns={columns}
|
|
595
|
-
hoverRow={hoverRow}
|
|
596
|
-
rowClassName={rowClassName}
|
|
597
|
-
style={{
|
|
598
|
-
...style,
|
|
599
|
-
top: `calc(${style.top}px + ${headerHeight ?? 48}px)`
|
|
600
|
-
}}
|
|
601
|
-
rowHeight={rowHeight}>
|
|
602
|
-
|
|
603
|
-
{columns.map((column: VirtualTableColumn, columnIndex: number) => {
|
|
604
|
-
const cellData = rowData && rowData[column.key];
|
|
605
|
-
const isDragging = draggingColumnId === column.key;
|
|
606
|
-
const isDraggable = !column.frozen && !!onColumnsOrderChange;
|
|
607
|
-
|
|
608
|
-
return onColumnsOrderChange ? (
|
|
609
|
-
<SortableCellWrapper
|
|
610
|
-
key={`cell_wrapper_${column.key}`}
|
|
611
|
-
columnKey={column.key}
|
|
612
|
-
width={column.width}
|
|
613
|
-
isDragging={isDragging}
|
|
614
|
-
isDraggable={isDraggable}
|
|
615
|
-
frozen={column.frozen}
|
|
616
|
-
>
|
|
617
|
-
<VirtualTableCell
|
|
618
|
-
dataKey={column.key}
|
|
619
|
-
// @ts-expect-error -- React.memo<VirtualTableProps<any>> erases the generic T, so cellRenderer's parameter type widens to `any` losing structural compatibility with VirtualTableCell<T>. The types are structurally correct at runtime.
|
|
620
|
-
cellRenderer={cellRenderer}
|
|
621
|
-
column={column}
|
|
622
|
-
columns={columns}
|
|
623
|
-
rowData={rowData}
|
|
624
|
-
cellData={cellData}
|
|
625
|
-
rowIndex={index}
|
|
626
|
-
columnIndex={columnIndex}
|
|
627
|
-
extraData={extraData}/>
|
|
628
|
-
</SortableCellWrapper>
|
|
629
|
-
) : (
|
|
630
|
-
<div
|
|
631
|
-
key={`cell_wrapper_${column.key}`}
|
|
632
|
-
className={cls(
|
|
633
|
-
"flex-shrink-0 relative",
|
|
634
|
-
column.frozen && "sticky left-0 z-10 bg-white dark:bg-surface-900"
|
|
635
|
-
)}
|
|
636
|
-
style={{
|
|
637
|
-
minWidth: column.width,
|
|
638
|
-
maxWidth: column.width,
|
|
639
|
-
width: column.width
|
|
640
|
-
}}
|
|
641
|
-
>
|
|
642
|
-
<VirtualTableCell
|
|
643
|
-
dataKey={column.key}
|
|
644
|
-
// @ts-expect-error -- React.memo<VirtualTableProps<any>> erases the generic T, so cellRenderer's parameter type widens to `any` losing structural compatibility with VirtualTableCell<T>. The types are structurally correct at runtime.
|
|
645
|
-
cellRenderer={cellRenderer}
|
|
646
|
-
column={column}
|
|
647
|
-
columns={columns}
|
|
648
|
-
rowData={rowData}
|
|
649
|
-
cellData={cellData}
|
|
650
|
-
rowIndex={index}
|
|
651
|
-
columnIndex={columnIndex}
|
|
652
|
-
extraData={extraData}/>
|
|
653
|
-
</div>
|
|
654
|
-
);
|
|
655
|
-
})}
|
|
656
|
-
|
|
657
|
-
{includeAddColumn && <div className={"w-20"}/>}
|
|
658
|
-
|
|
659
|
-
</VirtualTableRow>
|
|
660
|
-
);
|
|
661
|
-
}}
|
|
662
|
-
</VirtualListContext.Consumer>;
|
|
663
|
-
}, []);
|
|
664
|
-
|
|
665
|
-
return <List
|
|
666
|
-
outerRef={outerRef}
|
|
667
|
-
innerElementType={innerElementType}
|
|
668
|
-
width={width}
|
|
669
|
-
height={height}
|
|
670
|
-
overscanCount={4}
|
|
671
|
-
itemCount={itemCount}
|
|
672
|
-
onScroll={onScroll}
|
|
673
|
-
itemSize={itemSize}>
|
|
674
|
-
{Row}
|
|
675
|
-
</List>;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
const SafeLinkRenderer: React.FC<{
|
|
679
|
-
text: string;
|
|
680
|
-
}> = ({ text }) => {
|
|
681
|
-
const urlRegex = /https?:\/\/[^\s]+/g;
|
|
682
|
-
const urls = text.match(urlRegex) || [];
|
|
683
|
-
const parts = text.split(urlRegex);
|
|
684
|
-
|
|
685
|
-
return (
|
|
686
|
-
<div className={"break-all"}>
|
|
687
|
-
{parts.map((part, i) => (
|
|
688
|
-
<React.Fragment key={i}>
|
|
689
|
-
{part}
|
|
690
|
-
{urls[i] && (
|
|
691
|
-
<>
|
|
692
|
-
<a href={urls[i]} className="underline" target="_blank" rel="noopener noreferrer">Link</a>
|
|
693
|
-
<br/>
|
|
694
|
-
</>
|
|
695
|
-
)}
|
|
696
|
-
</React.Fragment>
|
|
697
|
-
))}
|
|
698
|
-
</div>
|
|
699
|
-
);
|
|
700
|
-
};
|