@rebasepro/ui 0.13.0 → 0.13.1-canary.g06dbe5b
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/components/VirtualTable/VirtualTableProps.d.ts +17 -2
- package/dist/icons/index.d.ts +1 -1
- package/dist/index.es.js +34 -21
- package/dist/index.es.js.map +1 -1
- package/dist/views/ListView.d.ts +7 -1
- package/package.json +1 -1
- package/src/components/Checkbox.tsx +10 -1
- package/src/components/Dialog.tsx +15 -2
- package/src/components/FilterChip.tsx +12 -2
- package/src/components/Menu.tsx +9 -1
- package/src/components/Menubar.tsx +5 -1
- package/src/components/MultiSelect.tsx +5 -2
- package/src/components/Popover.tsx +4 -1
- package/src/components/Skeleton.tsx +9 -2
- package/src/components/VirtualTable/VirtualTableProps.tsx +17 -2
- package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +7 -0
- package/src/components/VirtualTable/fields/VirtualTableInput.tsx +7 -0
- package/src/icons/index.ts +2 -0
- package/src/views/CollectionView/CollectionView.tsx +7 -1
- package/src/views/ListView.tsx +77 -62
package/dist/views/ListView.d.ts
CHANGED
|
@@ -25,6 +25,12 @@ export type ListViewProps<T> = {
|
|
|
25
25
|
emptyComponent?: React.ReactNode;
|
|
26
26
|
size?: CollectionSize;
|
|
27
27
|
selectedEntityId?: string | number;
|
|
28
|
+
/**
|
|
29
|
+
* Rendered above the rows, inside the list's own frame, and only when there
|
|
30
|
+
* are rows to head. A column header over an empty state or a spinner labels
|
|
31
|
+
* nothing.
|
|
32
|
+
*/
|
|
33
|
+
header?: React.ReactNode;
|
|
28
34
|
renderRow: (params: {
|
|
29
35
|
item: T;
|
|
30
36
|
index: number;
|
|
@@ -37,4 +43,4 @@ export type ListViewProps<T> = {
|
|
|
37
43
|
onSelectionChange: (selected: boolean) => void;
|
|
38
44
|
}) => React.ReactNode;
|
|
39
45
|
};
|
|
40
|
-
export declare function ListView<T>({ data, dataLoading, noMoreToLoad, dataLoadingError, itemCount, setItemCount, pageSize, paginationEnabled, onItemClick, selectedIds, highlightedIds, selectionEnabled, onSelectionChange, emptyComponent, size, selectedEntityId, renderRow }: ListViewProps<T>): React.JSX.Element;
|
|
46
|
+
export declare function ListView<T>({ data, dataLoading, noMoreToLoad, dataLoadingError, itemCount, setItemCount, pageSize, paginationEnabled, onItemClick, selectedIds, highlightedIds, selectionEnabled, onSelectionChange, emptyComponent, size, selectedEntityId, header, renderRow }: ListViewProps<T>): React.JSX.Element;
|
package/package.json
CHANGED
|
@@ -31,6 +31,15 @@ const sizeClasses = {
|
|
|
31
31
|
// Outer box is the hit area and sets the control's rendered height, so it
|
|
32
32
|
// tracks the shared 28/32/40/48 scale. `smallest` used to reuse `small`'s
|
|
33
33
|
// 32px box, which made the two sizes identical.
|
|
34
|
+
//
|
|
35
|
+
// A checkbox with no `onCheckedChange` is not a hit area — it is a tick mark
|
|
36
|
+
// standing in for a value, as in `BooleanPreview`. There the box was 10px of
|
|
37
|
+
// dead space on each side of a `medium` tick: on top of a read-only field's
|
|
38
|
+
// own padding, it started the tick 10px further in than the text of the field
|
|
39
|
+
// beside it. `padding={false}` reads as the request to drop it, and until now
|
|
40
|
+
// did nothing at all, because it only removed `p-2` from a box whose size was
|
|
41
|
+
// fixed regardless. Kept for anything clickable — the same box is the click
|
|
42
|
+
// target for table row selection and for editor task items.
|
|
34
43
|
const outerSizeClasses = {
|
|
35
44
|
medium: "w-10 h-10",
|
|
36
45
|
small: "w-8 h-8",
|
|
@@ -85,7 +94,7 @@ export const Checkbox = React.memo(({
|
|
|
85
94
|
|
|
86
95
|
<div className={cls(
|
|
87
96
|
padding ? paddingClasses[size] : "",
|
|
88
|
-
outerSizeClasses[size],
|
|
97
|
+
padding || onCheckedChange ? outerSizeClasses[size] : "",
|
|
89
98
|
"inline-flex items-center justify-center text-sm font-medium focus:outline-none transition-colors ease-in-out duration-150",
|
|
90
99
|
onCheckedChange ? "rounded-full hover:bg-surface-accent-200 hover:bg-opacity-75 hover:bg-surface-accent-200/75 dark:hover:bg-surface-accent-700 dark:hover:bg-opacity-75 dark:hover:bg-surface-accent-700/75" : "",
|
|
91
100
|
onCheckedChange ? "cursor-pointer" : "cursor-default"
|
|
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react";
|
|
|
3
3
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
4
4
|
import { paperMixin } from "../styles";
|
|
5
5
|
import { cls } from "../util";
|
|
6
|
-
import { usePortalContainer } from "../hooks/PortalContainerContext";
|
|
6
|
+
import { PortalContainerProvider, usePortalContainer } from "../hooks/PortalContainerContext";
|
|
7
7
|
|
|
8
8
|
export type DialogProps = {
|
|
9
9
|
open?: boolean;
|
|
@@ -65,6 +65,12 @@ export const Dialog = ({
|
|
|
65
65
|
"aria-describedby": ariaDescribedby
|
|
66
66
|
}: DialogProps) => {
|
|
67
67
|
const [displayed, setDisplayed] = useState(false);
|
|
68
|
+
// Where popups rendered *inside* this dialog get portaled to. A modal
|
|
69
|
+
// dialog locks scrolling everywhere but its own content, so a popup
|
|
70
|
+
// portaled to `document.body` renders fine and then refuses to scroll:
|
|
71
|
+
// `react-remove-scroll` cancels every wheel event outside the lock.
|
|
72
|
+
// Handing descendants this host keeps them inside it.
|
|
73
|
+
const [popupHost, setPopupHost] = useState<HTMLDivElement | null>(null);
|
|
68
74
|
|
|
69
75
|
// Get the portal container from context
|
|
70
76
|
const contextContainer = usePortalContainer();
|
|
@@ -134,9 +140,16 @@ export const Dialog = ({
|
|
|
134
140
|
maxWidth && !fullScreen ? widthClasses[maxWidth] : undefined,
|
|
135
141
|
className
|
|
136
142
|
)}>
|
|
137
|
-
{
|
|
143
|
+
<PortalContainerProvider container={popupHost}>
|
|
144
|
+
{children}
|
|
145
|
+
</PortalContainerProvider>
|
|
138
146
|
</div>
|
|
139
147
|
|
|
148
|
+
{/* Sized to nothing so it changes no layout, but positioned and
|
|
149
|
+
stacked above the paper so popups portaled here are not
|
|
150
|
+
painted behind it. */}
|
|
151
|
+
<div ref={setPopupHost} className={"relative z-70 w-0 h-0"}/>
|
|
152
|
+
|
|
140
153
|
</DialogPrimitive.Content>
|
|
141
154
|
</div>
|
|
142
155
|
|
|
@@ -61,11 +61,21 @@ export const FilterChip = React.forwardRef<HTMLButtonElement, FilterChipProps>(f
|
|
|
61
61
|
"transition-colors duration-150",
|
|
62
62
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50",
|
|
63
63
|
sizeClasses[size],
|
|
64
|
+
!disabled && "cursor-pointer",
|
|
65
|
+
// Hover is one step along the same ramp the chip already sits
|
|
66
|
+
// on, the way every other hoverable surface here moves. It used
|
|
67
|
+
// to be `hover:bg-primary/5` for the inactive chip, which
|
|
68
|
+
// *replaces* the resting fill with a tint fainter than it — so
|
|
69
|
+
// pointing at a chip made its background vanish. The active chip
|
|
70
|
+
// had no hover state at all.
|
|
64
71
|
active
|
|
65
|
-
?
|
|
72
|
+
? cls(
|
|
73
|
+
"bg-primary/12 text-primary dark:bg-primary/20 dark:text-primary shadow-[inset_0_0_0_1.5px_var(--color-primary)]",
|
|
74
|
+
!disabled && "hover:bg-primary/20 dark:hover:bg-primary/30"
|
|
75
|
+
)
|
|
66
76
|
: cls(
|
|
67
77
|
"bg-surface-accent-100 text-text-secondary dark:bg-surface-accent-800 dark:text-text-secondary-dark",
|
|
68
|
-
!disabled && "
|
|
78
|
+
!disabled && "hover:bg-surface-accent-200 dark:hover:bg-surface-accent-700"
|
|
69
79
|
),
|
|
70
80
|
disabled && "opacity-50 cursor-not-allowed",
|
|
71
81
|
className
|
package/src/components/Menu.tsx
CHANGED
|
@@ -57,7 +57,15 @@ const Menu = React.forwardRef<
|
|
|
57
57
|
side={side}
|
|
58
58
|
sideOffset={sideOffset}
|
|
59
59
|
align={align}
|
|
60
|
-
|
|
60
|
+
// `z-50` is the floating layer — the tier `Dialog`, `Sheet`,
|
|
61
|
+
// `Select` and `Tooltip` all sit on. Radix copies this value
|
|
62
|
+
// onto the popper wrapper it portals next to them, so at any
|
|
63
|
+
// lower value the menu opens *behind* the dialog or sheet it
|
|
64
|
+
// was opened from: visible in the DOM, painted under the
|
|
65
|
+
// overlay, and unclickable. Peers on this tier stack by mount
|
|
66
|
+
// order, which is what puts a menu above the dialog holding
|
|
67
|
+
// its trigger, and a dialog that menu opens above them both.
|
|
68
|
+
className={cls(paperMixin, focusedDisabled, "py-2 z-50", className)}>
|
|
61
69
|
{children}
|
|
62
70
|
</DropdownMenu.Content>
|
|
63
71
|
</DropdownMenu.Portal>
|
|
@@ -109,7 +109,11 @@ export function MenubarContent({
|
|
|
109
109
|
return (
|
|
110
110
|
<MenubarPrimitive.Content
|
|
111
111
|
onSelect={onSelect}
|
|
112
|
-
|
|
112
|
+
// `z-50`, the floating layer — see the note in `Menu`. This carried
|
|
113
|
+
// no z-index at all, so the wrapper Radix portals out to the body
|
|
114
|
+
// got `auto` and the menu opened underneath any dialog or panel it
|
|
115
|
+
// was hosted in.
|
|
116
|
+
className={cls("z-50 min-w-[220px] bg-white dark:bg-surface-900 rounded-md p-[6px] shadow-[0px_10px_38px_-10px_rgba(22,_23,_24,_0.35),_0px_10px_20px_-15px_rgba(22,_23,_24,_0.2)] [animation-duration:_400ms] [animation-timing-function:_cubic-bezier(0.16,_1,_0.3,_1)] will-change-[transform,opacity]", className)}
|
|
113
117
|
align={align ?? "start"}
|
|
114
118
|
sideOffset={sideOffset ?? 5}
|
|
115
119
|
alignOffset={alignOffset ?? -3}
|
|
@@ -194,11 +194,13 @@ export const MultiSelect = React.forwardRef<
|
|
|
194
194
|
onPopoverOpenChange(false);
|
|
195
195
|
};
|
|
196
196
|
|
|
197
|
+
// Scoped to this popover. Unscoped, it reached every other cmdk list on
|
|
198
|
+
// the page — the relation and user selectors set their own height on
|
|
199
|
+
// the list and got a second, competing scroll container inside it.
|
|
197
200
|
useInjectStyles("MultiSelect", `
|
|
198
|
-
[cmdk-group] {
|
|
201
|
+
[data-multi-select-content] [cmdk-group] {
|
|
199
202
|
max-height: 45vh;
|
|
200
203
|
overflow-y: auto;
|
|
201
|
-
// width: 400px;
|
|
202
204
|
} `)
|
|
203
205
|
|
|
204
206
|
const contextValue = React.useMemo(() => ({
|
|
@@ -306,6 +308,7 @@ export const MultiSelect = React.forwardRef<
|
|
|
306
308
|
</PopoverPrimitive.Trigger>
|
|
307
309
|
<PopoverPrimitive.Portal container={finalContainer}>
|
|
308
310
|
<PopoverPrimitive.Content
|
|
311
|
+
data-multi-select-content
|
|
309
312
|
className={cls("z-50 overflow-hidden border bg-white dark:bg-surface-800 rounded-lg w-[400px]", defaultBorderMixin)}
|
|
310
313
|
align="start"
|
|
311
314
|
sideOffset={8}
|
|
@@ -73,8 +73,11 @@ export function Popover({
|
|
|
73
73
|
|
|
74
74
|
<PopoverPrimitive.Portal container={finalContainer}>
|
|
75
75
|
<PopoverPrimitive.Content
|
|
76
|
+
// `z-50`, the floating layer — see the note in `Menu`. Below it
|
|
77
|
+
// a popover opened from inside a dialog or a side panel is
|
|
78
|
+
// painted under the thing that owns its trigger.
|
|
76
79
|
className={cls(paperMixin,
|
|
77
|
-
"PopoverContent z-
|
|
80
|
+
"PopoverContent z-50", className)}
|
|
78
81
|
side={side}
|
|
79
82
|
sideOffset={sideOffset}
|
|
80
83
|
align={align}
|
|
@@ -29,14 +29,21 @@ export function Skeleton({
|
|
|
29
29
|
|
|
30
30
|
return <span
|
|
31
31
|
style={{
|
|
32
|
-
width: width ? `${width}px
|
|
33
|
-
height: height ? `${height}px`
|
|
32
|
+
width: width === undefined ? undefined : `${width}px`,
|
|
33
|
+
height: height === undefined ? undefined : `${height}px`
|
|
34
34
|
}}
|
|
35
35
|
className={
|
|
36
36
|
cls(
|
|
37
37
|
"block relative overflow-hidden",
|
|
38
38
|
"bg-surface-accent-50 dark:bg-surface-accent-800 rounded-md",
|
|
39
39
|
"max-w-full max-h-full",
|
|
40
|
+
// The defaults are classes, not inline styles, so that a caller can
|
|
41
|
+
// override them: an inline style beats every utility, which is why
|
|
42
|
+
// `<Skeleton className="w-full h-full"/>` inside a 40px thumbnail
|
|
43
|
+
// box used to render a 12px bar. The `width`/`height` props still
|
|
44
|
+
// win over any class, since those do land inline.
|
|
45
|
+
width === undefined ? "w-full" : undefined,
|
|
46
|
+
height === undefined ? "h-3" : undefined,
|
|
40
47
|
className)
|
|
41
48
|
}>
|
|
42
49
|
<span
|
|
@@ -275,6 +275,16 @@ export type OnVirtualTableColumnResizeParams = {
|
|
|
275
275
|
};
|
|
276
276
|
|
|
277
277
|
/**
|
|
278
|
+
* The filter operators this table can express.
|
|
279
|
+
*
|
|
280
|
+
* A deliberate copy of `WhereFilterOp` in `@rebasepro/types`, not an import:
|
|
281
|
+
* `@rebasepro/ui` has no dependency on the core types and is meant to stay
|
|
282
|
+
* usable on its own. The cost is that two published packages export the same
|
|
283
|
+
* name, so the two must be kept identical by hand —
|
|
284
|
+
* `packages/types/test/filter-operators-duplication.test.ts` fails if they
|
|
285
|
+
* diverge, because a table that cannot express an operator the query layer
|
|
286
|
+
* supports is a silent gap: the filter simply is not offered.
|
|
287
|
+
*
|
|
278
288
|
* @see Table
|
|
279
289
|
* @group Components
|
|
280
290
|
*/
|
|
@@ -312,8 +322,13 @@ export type VirtualTableSort = "asc" | "desc" | undefined;
|
|
|
312
322
|
export type VirtualTableFilterValues<Key extends string> = FilterValues<Key>;
|
|
313
323
|
|
|
314
324
|
/**
|
|
315
|
-
* Filter conditions in a `Query.where()` clause
|
|
316
|
-
*
|
|
325
|
+
* Filter conditions in a `Query.where()` clause, named by {@link WhereFilterOp}.
|
|
326
|
+
*
|
|
327
|
+
* The list is not repeated here: this comment used to name eight operators when
|
|
328
|
+
* the type had sixteen, having been written before the SQL pattern and null
|
|
329
|
+
* checks (`like`, `ilike`, `not-like`, `not-ilike`, `is-null`, `is-not-null`)
|
|
330
|
+
* were added. A prose copy of a union drifts from it silently.
|
|
331
|
+
*
|
|
317
332
|
* @see Table
|
|
318
333
|
* @group Models
|
|
319
334
|
*/
|
|
@@ -22,6 +22,13 @@ export function VirtualTableDateField(props: {
|
|
|
22
22
|
<DateTimeField
|
|
23
23
|
value={internalValue ?? undefined}
|
|
24
24
|
onChange={(dateValue) => updateValue(dateValue ?? null)}
|
|
25
|
+
// Same omission as `VirtualTableInput`: destructured and forwarded
|
|
26
|
+
// nowhere, so a disabled date cell was still editable. `error` went
|
|
27
|
+
// the same way — it drives `DateTimeField`'s invalid styling, and a
|
|
28
|
+
// prop a caller passes should reach something rather than sit in a
|
|
29
|
+
// destructure.
|
|
30
|
+
disabled={disabled}
|
|
31
|
+
error={Boolean(error)}
|
|
25
32
|
invisible={true}
|
|
26
33
|
// Without this the field falls back to DateTimeField's own default of
|
|
27
34
|
// `size="large"` (min-h-64px), which overflows a table cell and makes
|
|
@@ -50,6 +50,13 @@ export function VirtualTableInput(props: {
|
|
|
50
50
|
<TextareaAutosize
|
|
51
51
|
className={focusedDisabled}
|
|
52
52
|
ref={ref}
|
|
53
|
+
// Destructured above and passed to nothing, so a property carrying
|
|
54
|
+
// `admin: { disabled: true }` stayed typeable in the table and the
|
|
55
|
+
// debounced write fired on blur. `readOnly` as well as `disabled`:
|
|
56
|
+
// a disabled textarea is skipped by keyboard navigation, and a cell
|
|
57
|
+
// you cannot tab through is worse than one you cannot edit.
|
|
58
|
+
disabled={disabled}
|
|
59
|
+
readOnly={disabled}
|
|
53
60
|
style={{
|
|
54
61
|
padding: 0,
|
|
55
62
|
margin: 0,
|
package/src/icons/index.ts
CHANGED
|
@@ -323,7 +323,13 @@ export function CollectionView<T extends Record<string, unknown> = Record<string
|
|
|
323
323
|
<CollectionKanbanView<T>
|
|
324
324
|
{...sharedProps}
|
|
325
325
|
kanbanProperty={resolvedKanbanProperty}
|
|
326
|
-
|
|
326
|
+
// `canCreate` is documented as "whether creation is
|
|
327
|
+
// allowed (shows the + button)" and was destructured
|
|
328
|
+
// and read nowhere, so the button showed regardless.
|
|
329
|
+
// The kanban view draws its add affordance exactly when
|
|
330
|
+
// it has a callback, so withholding the callback is
|
|
331
|
+
// what the prop means.
|
|
332
|
+
onRowCreate={canCreate ? onRowCreate : undefined}
|
|
327
333
|
/>
|
|
328
334
|
) : (
|
|
329
335
|
<div className="flex items-center justify-center p-8">
|
package/src/views/ListView.tsx
CHANGED
|
@@ -36,6 +36,13 @@ export type ListViewProps<T> = {
|
|
|
36
36
|
size?: CollectionSize;
|
|
37
37
|
selectedEntityId?: string | number;
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Rendered above the rows, inside the list's own frame, and only when there
|
|
41
|
+
* are rows to head. A column header over an empty state or a spinner labels
|
|
42
|
+
* nothing.
|
|
43
|
+
*/
|
|
44
|
+
header?: React.ReactNode;
|
|
45
|
+
|
|
39
46
|
renderRow: (params: {
|
|
40
47
|
item: T;
|
|
41
48
|
index: number;
|
|
@@ -97,9 +104,14 @@ export function ListView<T>({
|
|
|
97
104
|
|
|
98
105
|
size = "m",
|
|
99
106
|
selectedEntityId,
|
|
107
|
+
header,
|
|
100
108
|
renderRow
|
|
101
109
|
}: ListViewProps<T>) {
|
|
102
110
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
111
|
+
// The rows' own box, which a header shifts down from the container's top.
|
|
112
|
+
// Virtualization measures against it rather than against the container, so
|
|
113
|
+
// the window of rendered rows stays the one the viewport is actually over.
|
|
114
|
+
const rowsRef = useRef<HTMLDivElement>(null);
|
|
103
115
|
const isLoadingMore = useRef(false);
|
|
104
116
|
|
|
105
117
|
// Reset loading gate when loading stops
|
|
@@ -128,7 +140,7 @@ export function ListView<T>({
|
|
|
128
140
|
const update = () => {
|
|
129
141
|
rafId = null;
|
|
130
142
|
const scrollRect = scrollEl.getBoundingClientRect();
|
|
131
|
-
const listRect = el.getBoundingClientRect();
|
|
143
|
+
const listRect = (rowsRef.current ?? el).getBoundingClientRect();
|
|
132
144
|
|
|
133
145
|
const listTopRelative = listRect.top - scrollRect.top;
|
|
134
146
|
setEffectiveScrollTop(Math.max(0, -listTopRelative));
|
|
@@ -213,68 +225,71 @@ export function ListView<T>({
|
|
|
213
225
|
)}
|
|
214
226
|
</div>
|
|
215
227
|
) : (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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>
|
|
228
|
+
<>
|
|
229
|
+
{header}
|
|
230
|
+
<div ref={rowsRef} style={{ height: totalHeight + footerHeight, position: "relative" }}>
|
|
231
|
+
<div style={{ position: "absolute", top: offsetY, left: 0, right: 0 }}>
|
|
232
|
+
{visibleData.map((item, i) => {
|
|
233
|
+
const actualIndex = startIndex + i;
|
|
234
|
+
const isLast = actualIndex === data.length - 1;
|
|
235
|
+
const id = getItemId(item);
|
|
236
|
+
const selected = selectedIds?.has(id) ?? false;
|
|
237
|
+
const highlighted = highlightedIds?.has(id) ?? false;
|
|
238
|
+
|
|
239
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
240
|
+
if ((e.metaKey || e.ctrlKey) && selectionEnabled) {
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
onSelectionChange?.(item, !selected);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
onItemClick?.(item);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const handleSelectionChange = (val: boolean) => {
|
|
249
|
+
onSelectionChange?.(item, val);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<React.Fragment key={id || actualIndex}>
|
|
254
|
+
{renderRow({
|
|
255
|
+
item,
|
|
256
|
+
index: actualIndex,
|
|
257
|
+
style: { height: estimatedRowHeight, overflow: "hidden" },
|
|
258
|
+
className: cls(
|
|
259
|
+
!isLast && "border-b",
|
|
260
|
+
!isLast && borderMixinClass
|
|
261
|
+
),
|
|
262
|
+
selected,
|
|
263
|
+
highlighted,
|
|
264
|
+
isLast,
|
|
265
|
+
onClick: handleClick,
|
|
266
|
+
onSelectionChange: handleSelectionChange
|
|
267
|
+
})}
|
|
268
|
+
</React.Fragment>
|
|
269
|
+
);
|
|
270
|
+
})}
|
|
275
271
|
</div>
|
|
276
|
-
|
|
277
|
-
|
|
272
|
+
|
|
273
|
+
{dataLoading && (
|
|
274
|
+
<div
|
|
275
|
+
className="flex items-center justify-center py-3"
|
|
276
|
+
style={{ position: "absolute", top: totalHeight, left: 0, right: 0 }}
|
|
277
|
+
>
|
|
278
|
+
<CircularProgress size="small" />
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
{!dataLoading && noMoreToLoad && data.length > 0 && (
|
|
282
|
+
<div
|
|
283
|
+
className="flex items-center justify-center py-2 dark:bg-surface-900"
|
|
284
|
+
style={{ position: "absolute", top: totalHeight, left: 0, right: 0 }}
|
|
285
|
+
>
|
|
286
|
+
<Typography variant="caption" color="secondary">
|
|
287
|
+
All {data.length} entries loaded
|
|
288
|
+
</Typography>
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
</div>
|
|
292
|
+
</>
|
|
278
293
|
)}
|
|
279
294
|
</div>
|
|
280
295
|
);
|