@rebasepro/ui 0.17.3 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +4 -0
  2. package/dist/components/MultiSelect.d.ts +10 -0
  3. package/dist/index.es.js +2 -1
  4. package/dist/index.es.js.map +1 -1
  5. package/package.json +32 -23
  6. package/src/components/Alert.tsx +0 -118
  7. package/src/components/Autocomplete.tsx +0 -93
  8. package/src/components/Avatar.tsx +0 -73
  9. package/src/components/Badge.tsx +0 -44
  10. package/src/components/BooleanSwitch.tsx +0 -116
  11. package/src/components/BooleanSwitchWithLabel.tsx +0 -120
  12. package/src/components/Button.tsx +0 -110
  13. package/src/components/Card.tsx +0 -43
  14. package/src/components/CenteredView.tsx +0 -35
  15. package/src/components/Checkbox.tsx +0 -129
  16. package/src/components/Chip.tsx +0 -131
  17. package/src/components/CircularProgress.tsx +0 -53
  18. package/src/components/CircularProgressCenter.tsx +0 -27
  19. package/src/components/Collapse.tsx +0 -66
  20. package/src/components/ColorPicker.tsx +0 -140
  21. package/src/components/Container.tsx +0 -51
  22. package/src/components/DateTimeField.tsx +0 -363
  23. package/src/components/DebouncedTextField.tsx +0 -76
  24. package/src/components/Dialog.tsx +0 -159
  25. package/src/components/DialogActions.tsx +0 -29
  26. package/src/components/DialogContent.tsx +0 -27
  27. package/src/components/DialogTitle.tsx +0 -41
  28. package/src/components/ErrorBoundary.tsx +0 -236
  29. package/src/components/ExpandablePanel.tsx +0 -130
  30. package/src/components/FileUpload.tsx +0 -103
  31. package/src/components/FilterChip.tsx +0 -89
  32. package/src/components/IconButton.tsx +0 -101
  33. package/src/components/InfoLabel.tsx +0 -23
  34. package/src/components/InputLabel.tsx +0 -40
  35. package/src/components/Label.tsx +0 -32
  36. package/src/components/LoadingButton.tsx +0 -32
  37. package/src/components/Markdown.tsx +0 -55
  38. package/src/components/Menu.tsx +0 -114
  39. package/src/components/Menubar.tsx +0 -355
  40. package/src/components/MultiSelect.tsx +0 -433
  41. package/src/components/Paper.tsx +0 -23
  42. package/src/components/Popover.tsx +0 -162
  43. package/src/components/RadioGroup.tsx +0 -74
  44. package/src/components/ResizablePanels.tsx +0 -189
  45. package/src/components/SearchBar.tsx +0 -136
  46. package/src/components/Select.tsx +0 -326
  47. package/src/components/Separator.tsx +0 -28
  48. package/src/components/Sheet.tsx +0 -135
  49. package/src/components/Skeleton.tsx +0 -54
  50. package/src/components/Slider.tsx +0 -110
  51. package/src/components/Table.tsx +0 -178
  52. package/src/components/Tabs.tsx +0 -185
  53. package/src/components/TextField.tsx +0 -270
  54. package/src/components/TextareaAutosize.tsx +0 -368
  55. package/src/components/ToggleButtonGroup.tsx +0 -69
  56. package/src/components/Tooltip.tsx +0 -153
  57. package/src/components/Typography.tsx +0 -132
  58. package/src/components/VirtualTable/VirtualTable.performance.test.tsx +0 -90
  59. package/src/components/VirtualTable/VirtualTable.tsx +0 -700
  60. package/src/components/VirtualTable/VirtualTableCell.tsx +0 -59
  61. package/src/components/VirtualTable/VirtualTableHeader.tsx +0 -327
  62. package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +0 -250
  63. package/src/components/VirtualTable/VirtualTableProps.tsx +0 -339
  64. package/src/components/VirtualTable/VirtualTableRow.tsx +0 -51
  65. package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +0 -44
  66. package/src/components/VirtualTable/fields/VirtualTableInput.tsx +0 -89
  67. package/src/components/VirtualTable/fields/VirtualTableNumberInput.tsx +0 -71
  68. package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +0 -107
  69. package/src/components/VirtualTable/fields/VirtualTableSwitch.tsx +0 -28
  70. package/src/components/VirtualTable/index.tsx +0 -10
  71. package/src/components/VirtualTable/selection/SelectionContext.tsx +0 -36
  72. package/src/components/VirtualTable/selection/SelectionStore.ts +0 -54
  73. package/src/components/VirtualTable/types.tsx +0 -47
  74. package/src/components/common/SelectInputLabel.tsx +0 -23
  75. package/src/components/index.tsx +0 -62
  76. package/src/hooks/PortalContainerContext.tsx +0 -50
  77. package/src/hooks/index.ts +0 -7
  78. package/src/hooks/useDebounceCallback.tsx +0 -21
  79. package/src/hooks/useDebounceValue.tsx +0 -20
  80. package/src/hooks/useDebouncedCallback.ts +0 -39
  81. package/src/hooks/useInjectStyles.tsx +0 -31
  82. package/src/hooks/useOutsideAlerter.tsx +0 -46
  83. package/src/icons/GitHubIcon.tsx +0 -21
  84. package/src/icons/HandleIcon.tsx +0 -23
  85. package/src/icons/Icon.tsx +0 -33
  86. package/src/icons/LucideIconByName.tsx +0 -117
  87. package/src/icons/cool_icon_keys.ts +0 -12
  88. package/src/icons/icon_keys.ts +0 -1936
  89. package/src/icons/index.ts +0 -179
  90. package/src/index.css +0 -169
  91. package/src/index.ts +0 -47
  92. package/src/scripts/fixIconSizes.cjs +0 -92
  93. package/src/scripts/fixIconSizesToConst.cjs +0 -106
  94. package/src/scripts/generateIconKeys.ts +0 -41
  95. package/src/scripts/generateIcons.ts +0 -4
  96. package/src/scripts/saveIconFiles.ts +0 -89
  97. package/src/styles.ts +0 -141
  98. package/src/theme.css +0 -168
  99. package/src/util/chip_colors.ts +0 -242
  100. package/src/util/cls.ts +0 -8
  101. package/src/util/debounce.ts +0 -28
  102. package/src/util/hash.ts +0 -11
  103. package/src/util/index.ts +0 -4
  104. package/src/util/key_to_icon_component.ts +0 -11
  105. package/src/util/lazy_chunk.ts +0 -101
  106. package/src/views/CardView.tsx +0 -281
  107. package/src/views/CollectionView/CollectionCardView.tsx +0 -265
  108. package/src/views/CollectionView/CollectionKanbanView.tsx +0 -246
  109. package/src/views/CollectionView/CollectionListView.tsx +0 -240
  110. package/src/views/CollectionView/CollectionTableView.tsx +0 -229
  111. package/src/views/CollectionView/CollectionView.tsx +0 -450
  112. package/src/views/CollectionView/CollectionViewToolbar.tsx +0 -242
  113. package/src/views/CollectionView/CollectionViewTypes.ts +0 -181
  114. package/src/views/CollectionView/DefaultCellRenderer.tsx +0 -404
  115. package/src/views/CollectionView/index.ts +0 -39
  116. package/src/views/CollectionView/resolveTitleProperty.ts +0 -52
  117. package/src/views/CollectionView/utils.ts +0 -14
  118. package/src/views/Kanban/Board.tsx +0 -384
  119. package/src/views/Kanban/BoardColumn.tsx +0 -138
  120. package/src/views/Kanban/BoardColumnTitle.tsx +0 -47
  121. package/src/views/Kanban/BoardSortableList.tsx +0 -182
  122. package/src/views/Kanban/board_types.ts +0 -80
  123. package/src/views/Kanban/index.tsx +0 -5
  124. package/src/views/Kanban/placement.ts +0 -62
  125. package/src/views/KanbanView.tsx +0 -32
  126. package/src/views/ListView.tsx +0 -296
  127. 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
- };