@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
package/src/util/hash.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export function hashString(str: string): number {
|
|
2
|
-
let hash = 0;
|
|
3
|
-
let i;
|
|
4
|
-
let chr;
|
|
5
|
-
for (i = 0; i < str.length; i++) {
|
|
6
|
-
chr = str.charCodeAt(i);
|
|
7
|
-
hash = ((hash << 5) - hash) + chr;
|
|
8
|
-
hash |= 0; // Convert to 32bit integer
|
|
9
|
-
}
|
|
10
|
-
return Math.abs(hash);
|
|
11
|
-
}
|
package/src/util/index.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export function keyToIconComponent(key: string) {
|
|
2
|
-
const startsWithNumber = key.match(/^\d/);
|
|
3
|
-
|
|
4
|
-
// convert key to came case
|
|
5
|
-
const componentName = (startsWithNumber ? "_" : "") +
|
|
6
|
-
key.split("_").map((word: string) => {
|
|
7
|
-
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
8
|
-
}).join("") +
|
|
9
|
-
"Icon";
|
|
10
|
-
return componentName;
|
|
11
|
-
}
|
package/src/util/lazy_chunk.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Dynamic imports that survive a redeploy.
|
|
5
|
-
*
|
|
6
|
-
* A built SPA names its chunks by content hash and a deploy replaces the whole
|
|
7
|
-
* `assets/` directory, so a tab opened before the deploy still holds the *old*
|
|
8
|
-
* entry chunk. The first lazy route that tab opens asks for a hash that no
|
|
9
|
-
* longer exists on the server, the import rejects, and the view is dead until
|
|
10
|
-
* the user thinks to reload — which nothing on screen tells them to do. The
|
|
11
|
-
* browser's own wording ("Failed to fetch dynamically imported module: …") reads
|
|
12
|
-
* like a build bug, so this is usually reported as one.
|
|
13
|
-
*
|
|
14
|
-
* `loadChunk` retries once — that covers a transient network failure — and then
|
|
15
|
-
* gives up with an error tagged as a chunk load failure, so `ErrorBoundary` can
|
|
16
|
-
* offer a reload instead of printing the browser's message.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Marker property set on the error thrown after a failed retry. Read by
|
|
21
|
-
* `isChunkLoadError`, which is what the UI matches on.
|
|
22
|
-
*/
|
|
23
|
-
const CHUNK_LOAD_ERROR_FLAG = "rebaseChunkLoadError";
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Each engine words this differently, and the message is the only signal — none
|
|
27
|
-
* of them use a distinguishable error type. The MIME variants matter as much as
|
|
28
|
-
* the fetch ones: a server whose SPA fallback answers a missing `/assets/x.js`
|
|
29
|
-
* with `index.html` returns 200 HTML, and the browser rejects it as a module.
|
|
30
|
-
*/
|
|
31
|
-
const CHUNK_ERROR_PATTERNS = [
|
|
32
|
-
"failed to fetch dynamically imported module",
|
|
33
|
-
"error loading dynamically imported module",
|
|
34
|
-
"importing a module script failed",
|
|
35
|
-
"failed to load module script",
|
|
36
|
-
"expected a javascript",
|
|
37
|
-
"unable to preload css"
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Does this error mean a chunk could not be loaded — rather than the chunk's
|
|
42
|
-
* own code throwing once it did load?
|
|
43
|
-
*
|
|
44
|
-
* Errors raised anywhere are matched, not just ones `loadChunk` produced: React
|
|
45
|
-
* `lazy()` calls that still import directly, and Vite's own CSS preloads, fail
|
|
46
|
-
* the same way and deserve the same recovery.
|
|
47
|
-
*/
|
|
48
|
-
export function isChunkLoadError(error: unknown): boolean {
|
|
49
|
-
if (!error || typeof error !== "object") return false;
|
|
50
|
-
if ((error as Record<string, unknown>)[CHUNK_LOAD_ERROR_FLAG] === true) return true;
|
|
51
|
-
const message = (error as { message?: unknown }).message;
|
|
52
|
-
if (typeof message !== "string") return false;
|
|
53
|
-
const lower = message.toLowerCase();
|
|
54
|
-
return CHUNK_ERROR_PATTERNS.some(pattern => lower.includes(pattern));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function chunkLoadError(cause: unknown): Error {
|
|
58
|
-
const error = new Error(
|
|
59
|
-
"This app was updated while the tab was open, so part of it could not be loaded. Reload to continue."
|
|
60
|
-
);
|
|
61
|
-
Object.assign(error, { [CHUNK_LOAD_ERROR_FLAG]: true,
|
|
62
|
-
cause });
|
|
63
|
-
return error;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Run a dynamic import, retrying once before declaring the chunk unreachable.
|
|
68
|
-
*
|
|
69
|
-
* Errors that are not chunk load failures — the imported module throwing while
|
|
70
|
-
* it evaluates, say — are re-thrown untouched and never retried: running a
|
|
71
|
-
* module's side effects twice is worse than the original failure.
|
|
72
|
-
*/
|
|
73
|
-
export async function loadChunk<T>(loader: () => Promise<T>): Promise<T> {
|
|
74
|
-
try {
|
|
75
|
-
return await loader();
|
|
76
|
-
} catch (error) {
|
|
77
|
-
if (!isChunkLoadError(error)) throw error;
|
|
78
|
-
// A blip resolves inside a few hundred ms; a stale hash never will.
|
|
79
|
-
await new Promise(resolve => setTimeout(resolve, 250));
|
|
80
|
-
try {
|
|
81
|
-
return await loader();
|
|
82
|
-
} catch (retryError) {
|
|
83
|
-
throw chunkLoadError(retryError);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* `React.lazy` with the retry above. A drop-in replacement — use it for every
|
|
90
|
-
* lazy route or dialog, since any one of them can be the first chunk a stale
|
|
91
|
-
* tab reaches for.
|
|
92
|
-
*
|
|
93
|
-
* Note that React caches the rejection: once a lazy component has failed, later
|
|
94
|
-
* renders re-throw without calling the loader again. Resetting an error boundary
|
|
95
|
-
* therefore cannot recover it, which is why the boundary offers a reload.
|
|
96
|
-
*/
|
|
97
|
-
export function lazyChunk<T extends React.ComponentType<any>>(
|
|
98
|
-
loader: () => Promise<{ default: T }>
|
|
99
|
-
): React.LazyExoticComponent<T> {
|
|
100
|
-
return React.lazy(() => loadChunk(loader));
|
|
101
|
-
}
|
package/src/views/CardView.tsx
DELETED
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useRef } from "react";
|
|
2
|
-
import {
|
|
3
|
-
CircularProgress,
|
|
4
|
-
Typography
|
|
5
|
-
} from "../components";
|
|
6
|
-
import { cls } from "../util";
|
|
7
|
-
|
|
8
|
-
import { CollectionSize } from "./ListView";
|
|
9
|
-
|
|
10
|
-
export type CardViewProps<T> = {
|
|
11
|
-
data: T[];
|
|
12
|
-
dataLoading?: boolean;
|
|
13
|
-
noMoreToLoad?: boolean;
|
|
14
|
-
dataLoadingError?: Error;
|
|
15
|
-
itemCount?: number;
|
|
16
|
-
setItemCount?: (itemCount: number) => void;
|
|
17
|
-
pageSize?: number;
|
|
18
|
-
paginationEnabled?: boolean;
|
|
19
|
-
|
|
20
|
-
onItemClick?: (item: T) => void;
|
|
21
|
-
selectedIds?: Set<string | number>;
|
|
22
|
-
highlightedIds?: Set<string | number>;
|
|
23
|
-
selectionEnabled?: boolean;
|
|
24
|
-
onSelectionChange?: (item: T, selected: boolean) => void;
|
|
25
|
-
|
|
26
|
-
onScroll?: (props: {
|
|
27
|
-
scrollDirection: "forward" | "backward";
|
|
28
|
-
scrollOffset: number;
|
|
29
|
-
scrollUpdateWasRequested: boolean;
|
|
30
|
-
}) => void;
|
|
31
|
-
initialScroll?: number;
|
|
32
|
-
|
|
33
|
-
size?: CollectionSize;
|
|
34
|
-
renderCard: (
|
|
35
|
-
item: T,
|
|
36
|
-
extra: {
|
|
37
|
-
selected: boolean;
|
|
38
|
-
highlighted: boolean;
|
|
39
|
-
onSelectionChange: (selected: boolean) => void;
|
|
40
|
-
onClick: (e: React.MouseEvent) => void;
|
|
41
|
-
}
|
|
42
|
-
) => React.ReactNode;
|
|
43
|
-
emptyComponent?: React.ReactNode;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
function getGridColumnsClass(size: CollectionSize): string {
|
|
47
|
-
switch (size) {
|
|
48
|
-
case "xs":
|
|
49
|
-
return "grid-cols-4 sm:grid-cols-5 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10";
|
|
50
|
-
case "s":
|
|
51
|
-
return "grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-8";
|
|
52
|
-
case "m":
|
|
53
|
-
return "grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6";
|
|
54
|
-
case "l":
|
|
55
|
-
return "grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-4";
|
|
56
|
-
case "xl":
|
|
57
|
-
return "grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3";
|
|
58
|
-
default:
|
|
59
|
-
return "grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6";
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function getScrollParent(element: HTMLElement | null): HTMLElement | null {
|
|
64
|
-
if (!element) return null;
|
|
65
|
-
let parent = element.parentElement;
|
|
66
|
-
while (parent) {
|
|
67
|
-
const overflowY = window.getComputedStyle(parent).overflowY;
|
|
68
|
-
if (overflowY === "auto" || overflowY === "scroll") {
|
|
69
|
-
return parent;
|
|
70
|
-
}
|
|
71
|
-
parent = parent.parentElement;
|
|
72
|
-
}
|
|
73
|
-
return document.documentElement;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function CardView<T>({
|
|
77
|
-
data,
|
|
78
|
-
dataLoading = false,
|
|
79
|
-
noMoreToLoad = false,
|
|
80
|
-
dataLoadingError,
|
|
81
|
-
itemCount,
|
|
82
|
-
setItemCount,
|
|
83
|
-
pageSize = 50,
|
|
84
|
-
paginationEnabled = true,
|
|
85
|
-
|
|
86
|
-
onItemClick,
|
|
87
|
-
selectedIds,
|
|
88
|
-
highlightedIds,
|
|
89
|
-
selectionEnabled = true,
|
|
90
|
-
onSelectionChange,
|
|
91
|
-
|
|
92
|
-
onScroll,
|
|
93
|
-
initialScroll,
|
|
94
|
-
size = "m",
|
|
95
|
-
renderCard,
|
|
96
|
-
emptyComponent
|
|
97
|
-
}: CardViewProps<T>) {
|
|
98
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
99
|
-
const hasRestoredScroll = useRef(false);
|
|
100
|
-
const isLoadingMore = useRef(false);
|
|
101
|
-
|
|
102
|
-
// Sync mutable ref with pagination settings to avoid resetting listeners
|
|
103
|
-
const paginationStateRef = useRef({ paginationEnabled, noMoreToLoad, itemCount, pageSize });
|
|
104
|
-
useEffect(() => {
|
|
105
|
-
paginationStateRef.current = { paginationEnabled, noMoreToLoad, itemCount, pageSize };
|
|
106
|
-
}, [paginationEnabled, noMoreToLoad, itemCount, pageSize]);
|
|
107
|
-
|
|
108
|
-
useEffect(() => {
|
|
109
|
-
if (!dataLoading) isLoadingMore.current = false;
|
|
110
|
-
}, [dataLoading]);
|
|
111
|
-
|
|
112
|
-
// Infinite scroll and resize observer
|
|
113
|
-
useEffect(() => {
|
|
114
|
-
const el = containerRef.current;
|
|
115
|
-
if (!el) return;
|
|
116
|
-
const scrollEl = getScrollParent(el);
|
|
117
|
-
if (!scrollEl) return;
|
|
118
|
-
|
|
119
|
-
let rafId: number | null = null;
|
|
120
|
-
|
|
121
|
-
const update = () => {
|
|
122
|
-
rafId = null;
|
|
123
|
-
const { paginationEnabled: pe, noMoreToLoad: nm, itemCount: ic, pageSize: ps } = paginationStateRef.current;
|
|
124
|
-
if (
|
|
125
|
-
pe &&
|
|
126
|
-
!nm &&
|
|
127
|
-
!isLoadingMore.current &&
|
|
128
|
-
scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight < 400
|
|
129
|
-
) {
|
|
130
|
-
isLoadingMore.current = true;
|
|
131
|
-
setItemCount?.((ic ?? ps) + ps);
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const onScrollEvent = () => {
|
|
136
|
-
if (rafId === null) rafId = requestAnimationFrame(update);
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
scrollEl.addEventListener("scroll", onScrollEvent, { passive: true });
|
|
140
|
-
const ro = new ResizeObserver(() => update());
|
|
141
|
-
ro.observe(scrollEl);
|
|
142
|
-
update();
|
|
143
|
-
|
|
144
|
-
return () => {
|
|
145
|
-
scrollEl.removeEventListener("scroll", onScrollEvent);
|
|
146
|
-
ro.disconnect();
|
|
147
|
-
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
148
|
-
};
|
|
149
|
-
}, [setItemCount]);
|
|
150
|
-
|
|
151
|
-
// Scroll restoration
|
|
152
|
-
useEffect(() => {
|
|
153
|
-
if (!containerRef.current || !initialScroll || hasRestoredScroll.current || data.length === 0) return;
|
|
154
|
-
|
|
155
|
-
const scrollEl = getScrollParent(containerRef.current);
|
|
156
|
-
if (!scrollEl) return;
|
|
157
|
-
|
|
158
|
-
let attempts = 0;
|
|
159
|
-
const maxAttempts = 5;
|
|
160
|
-
|
|
161
|
-
const tryRestore = () => {
|
|
162
|
-
if (scrollEl.scrollHeight >= initialScroll || attempts >= maxAttempts) {
|
|
163
|
-
scrollEl.scrollTop = initialScroll;
|
|
164
|
-
hasRestoredScroll.current = true;
|
|
165
|
-
} else {
|
|
166
|
-
attempts++;
|
|
167
|
-
requestAnimationFrame(tryRestore);
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
requestAnimationFrame(tryRestore);
|
|
172
|
-
}, [initialScroll, data.length]);
|
|
173
|
-
|
|
174
|
-
// Scroll tracking: call onScroll callback
|
|
175
|
-
const lastScrollOffset = useRef(0);
|
|
176
|
-
useEffect(() => {
|
|
177
|
-
const el = containerRef.current;
|
|
178
|
-
if (!el || !onScroll) return;
|
|
179
|
-
const scrollEl = getScrollParent(el);
|
|
180
|
-
if (!scrollEl) return;
|
|
181
|
-
|
|
182
|
-
const handleScroll = () => {
|
|
183
|
-
const currentOffset = scrollEl.scrollTop;
|
|
184
|
-
const direction = currentOffset > lastScrollOffset.current ? "forward" : "backward";
|
|
185
|
-
lastScrollOffset.current = currentOffset;
|
|
186
|
-
onScroll({
|
|
187
|
-
scrollDirection: direction,
|
|
188
|
-
scrollOffset: currentOffset,
|
|
189
|
-
scrollUpdateWasRequested: false
|
|
190
|
-
});
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
scrollEl.addEventListener("scroll", handleScroll, { passive: true });
|
|
194
|
-
return () => scrollEl.removeEventListener("scroll", handleScroll);
|
|
195
|
-
}, [onScroll]);
|
|
196
|
-
|
|
197
|
-
const getItemId = useCallback((item: T): string | number => {
|
|
198
|
-
if (item && typeof item === "object" && "id" in item) {
|
|
199
|
-
const id = (item as Record<"id", unknown>).id;
|
|
200
|
-
return typeof id === "number" ? id : String(id);
|
|
201
|
-
}
|
|
202
|
-
return String(item);
|
|
203
|
-
}, []);
|
|
204
|
-
|
|
205
|
-
const gridColumnsClass = getGridColumnsClass(size);
|
|
206
|
-
|
|
207
|
-
const isInitialLoading = dataLoading && data.length === 0 && !dataLoadingError;
|
|
208
|
-
const isEmpty = !dataLoading && data.length === 0 && !dataLoadingError;
|
|
209
|
-
|
|
210
|
-
return (
|
|
211
|
-
<div
|
|
212
|
-
ref={containerRef}
|
|
213
|
-
className="w-full p-4"
|
|
214
|
-
>
|
|
215
|
-
{dataLoadingError && data.length === 0 ? (
|
|
216
|
-
<div className="h-full flex items-center justify-center p-8">
|
|
217
|
-
<Typography className="text-red-500">
|
|
218
|
-
Error loading data: {dataLoadingError.message}
|
|
219
|
-
</Typography>
|
|
220
|
-
</div>
|
|
221
|
-
) : isInitialLoading ? (
|
|
222
|
-
<div className="flex items-center justify-center py-12 px-8">
|
|
223
|
-
<CircularProgress size="small"/>
|
|
224
|
-
</div>
|
|
225
|
-
) : isEmpty ? (
|
|
226
|
-
<div className="w-full flex items-center justify-center py-12 px-8">
|
|
227
|
-
{emptyComponent ?? (
|
|
228
|
-
<Typography variant="label" color="secondary">
|
|
229
|
-
No entries found
|
|
230
|
-
</Typography>
|
|
231
|
-
)}
|
|
232
|
-
</div>
|
|
233
|
-
) : (
|
|
234
|
-
<div className="max-w-7xl mx-auto">
|
|
235
|
-
<div className={cls("grid gap-4", gridColumnsClass)}>
|
|
236
|
-
{data.map((item, index) => {
|
|
237
|
-
const id = getItemId(item);
|
|
238
|
-
const selected = selectedIds?.has(id) ?? false;
|
|
239
|
-
const highlighted = highlightedIds?.has(id) ?? false;
|
|
240
|
-
|
|
241
|
-
const handleClick = (e: React.MouseEvent) => {
|
|
242
|
-
if ((e.metaKey || e.ctrlKey) && selectionEnabled) {
|
|
243
|
-
e.preventDefault();
|
|
244
|
-
onSelectionChange?.(item, !selected);
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
onItemClick?.(item);
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
const handleSelectionChange = (val: boolean) => {
|
|
251
|
-
onSelectionChange?.(item, val);
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
return (
|
|
255
|
-
<React.Fragment key={id || index}>
|
|
256
|
-
{renderCard(item, {
|
|
257
|
-
selected,
|
|
258
|
-
highlighted,
|
|
259
|
-
onSelectionChange: handleSelectionChange,
|
|
260
|
-
onClick: handleClick
|
|
261
|
-
})}
|
|
262
|
-
</React.Fragment>
|
|
263
|
-
);
|
|
264
|
-
})}
|
|
265
|
-
</div>
|
|
266
|
-
|
|
267
|
-
<div className="flex items-center justify-center py-8">
|
|
268
|
-
{dataLoading && (
|
|
269
|
-
<CircularProgress size="small"/>
|
|
270
|
-
)}
|
|
271
|
-
{!dataLoading && noMoreToLoad && data.length > 0 && (
|
|
272
|
-
<Typography variant="caption" color="secondary">
|
|
273
|
-
All {data.length} entries loaded
|
|
274
|
-
</Typography>
|
|
275
|
-
)}
|
|
276
|
-
</div>
|
|
277
|
-
</div>
|
|
278
|
-
)}
|
|
279
|
-
</div>
|
|
280
|
-
);
|
|
281
|
-
}
|
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import React, { useCallback, useMemo } from "react";
|
|
3
|
-
import { CardView } from "../CardView";
|
|
4
|
-
import { Card } from "../../components/Card";
|
|
5
|
-
import { Typography } from "../../components/Typography";
|
|
6
|
-
import { Checkbox } from "../../components/Checkbox";
|
|
7
|
-
import { DefaultCellRenderer } from "./DefaultCellRenderer";
|
|
8
|
-
import { cls } from "../../util";
|
|
9
|
-
import { cardSelectedMixin, fieldBackgroundMixin } from "../../styles";
|
|
10
|
-
import type {
|
|
11
|
-
CollectionPropertyConfig,
|
|
12
|
-
CollectionDataController,
|
|
13
|
-
CellRendererOverride,
|
|
14
|
-
CollectionViewSize,
|
|
15
|
-
CollectionSelectionController,
|
|
16
|
-
} from "./CollectionViewTypes";
|
|
17
|
-
import { resolveTitlePropertyKey } from "./resolveTitleProperty";
|
|
18
|
-
|
|
19
|
-
import { getValueInPath } from "./utils";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Props for the {@link CollectionCardView} component.
|
|
23
|
-
* @group Collection View
|
|
24
|
-
*/
|
|
25
|
-
export type CollectionCardViewProps<T = Record<string, unknown>> = {
|
|
26
|
-
dataController: CollectionDataController<T>;
|
|
27
|
-
properties: Record<string, CollectionPropertyConfig>;
|
|
28
|
-
propertiesOrder?: string[];
|
|
29
|
-
idProperty?: string;
|
|
30
|
-
titleProperty?: string;
|
|
31
|
-
onRowClick?: (row: T) => void;
|
|
32
|
-
cellRenderer?: CellRendererOverride<T>;
|
|
33
|
-
size?: CollectionViewSize;
|
|
34
|
-
selectionEnabled?: boolean;
|
|
35
|
-
selectionController?: CollectionSelectionController<T>;
|
|
36
|
-
highlightedItems?: T[];
|
|
37
|
-
emptyComponent?: React.ReactNode;
|
|
38
|
-
className?: string;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Headless card grid view that wraps {@link CardView}.
|
|
43
|
-
*
|
|
44
|
-
* Renders each item as a card with an optional image, title,
|
|
45
|
-
* preview properties, and selection support.
|
|
46
|
-
*
|
|
47
|
-
* @group Collection View
|
|
48
|
-
*/
|
|
49
|
-
export function CollectionCardView<T extends Record<string, unknown> = Record<string, unknown>>({
|
|
50
|
-
dataController,
|
|
51
|
-
properties,
|
|
52
|
-
propertiesOrder,
|
|
53
|
-
idProperty = "id",
|
|
54
|
-
titleProperty,
|
|
55
|
-
onRowClick,
|
|
56
|
-
cellRenderer,
|
|
57
|
-
size = "m",
|
|
58
|
-
selectionEnabled,
|
|
59
|
-
selectionController,
|
|
60
|
-
highlightedItems,
|
|
61
|
-
emptyComponent,
|
|
62
|
-
className,
|
|
63
|
-
}: CollectionCardViewProps<T>) {
|
|
64
|
-
const order = useMemo(
|
|
65
|
-
() => propertiesOrder ?? Object.keys(properties),
|
|
66
|
-
[propertiesOrder, properties]
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
// ── Resolve title property ───────────────────────────────────────
|
|
70
|
-
const titlePropertyKey = useMemo(
|
|
71
|
-
() => resolveTitlePropertyKey(order, properties, idProperty, titleProperty),
|
|
72
|
-
[titleProperty, order, properties, idProperty]
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
// ── Resolve image property ───────────────────────────────────────
|
|
76
|
-
const imageProperty = useMemo(() => {
|
|
77
|
-
for (const key of order) {
|
|
78
|
-
const prop = properties[key];
|
|
79
|
-
if (prop && prop.type === "string" && prop.storage) {
|
|
80
|
-
return key;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return undefined;
|
|
84
|
-
}, [order, properties]);
|
|
85
|
-
|
|
86
|
-
// ── Preview properties (up to 3) ─────────────────────────────────
|
|
87
|
-
const previewProperties = useMemo(() => {
|
|
88
|
-
const result: { key: string; property: CollectionPropertyConfig }[] = [];
|
|
89
|
-
for (const key of order) {
|
|
90
|
-
if (key === titlePropertyKey || key === imageProperty) continue;
|
|
91
|
-
const prop = properties[key];
|
|
92
|
-
if (prop && !prop.hideFromCollection) {
|
|
93
|
-
result.push({ key, property: prop });
|
|
94
|
-
if (result.length >= 3) break;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return result;
|
|
98
|
-
}, [order, properties, titlePropertyKey, imageProperty]);
|
|
99
|
-
|
|
100
|
-
// ── Selection IDs ────────────────────────────────────────────────
|
|
101
|
-
const selectedIds = useMemo(() => {
|
|
102
|
-
if (!selectionController?.selectedItems) return undefined;
|
|
103
|
-
const ids = new Set<string | number>();
|
|
104
|
-
for (const item of selectionController.selectedItems) {
|
|
105
|
-
const id = getValueInPath(item as Record<string, unknown>, idProperty);
|
|
106
|
-
if (id !== undefined && id !== null) {
|
|
107
|
-
ids.add(id as string | number);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return ids;
|
|
111
|
-
}, [selectionController?.selectedItems, idProperty]);
|
|
112
|
-
|
|
113
|
-
// ── Highlighted IDs ──────────────────────────────────────────────
|
|
114
|
-
const highlightedIds = useMemo(() => {
|
|
115
|
-
if (!highlightedItems) return undefined;
|
|
116
|
-
const ids = new Set<string | number>();
|
|
117
|
-
for (const item of highlightedItems) {
|
|
118
|
-
const id = getValueInPath(item as Record<string, unknown>, idProperty);
|
|
119
|
-
if (id !== undefined && id !== null) {
|
|
120
|
-
ids.add(id as string | number);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return ids;
|
|
124
|
-
}, [highlightedItems, idProperty]);
|
|
125
|
-
|
|
126
|
-
// ── Selection handler ────────────────────────────────────────────
|
|
127
|
-
const handleSelectionChange = useCallback(
|
|
128
|
-
(item: T, selected: boolean) => {
|
|
129
|
-
if (!selectionController) return;
|
|
130
|
-
selectionController.setSelectedItems((prev) => {
|
|
131
|
-
if (selected) {
|
|
132
|
-
return [...prev, item];
|
|
133
|
-
}
|
|
134
|
-
const itemId = getValueInPath(item as Record<string, unknown>, idProperty);
|
|
135
|
-
return prev.filter((existing) => {
|
|
136
|
-
const existingId = getValueInPath(existing as Record<string, unknown>, idProperty);
|
|
137
|
-
return existingId !== itemId;
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
},
|
|
141
|
-
[selectionController, idProperty]
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
// ── Render card ──────────────────────────────────────────────────
|
|
145
|
-
const renderCard = useCallback(
|
|
146
|
-
(
|
|
147
|
-
item: T,
|
|
148
|
-
extra: {
|
|
149
|
-
selected: boolean;
|
|
150
|
-
highlighted: boolean;
|
|
151
|
-
onSelectionChange: (selected: boolean) => void;
|
|
152
|
-
onClick: (e: React.MouseEvent) => void;
|
|
153
|
-
}
|
|
154
|
-
): React.ReactNode => {
|
|
155
|
-
const titleValue = getValueInPath(item as Record<string, unknown>, titlePropertyKey);
|
|
156
|
-
const imageValue = imageProperty
|
|
157
|
-
? getValueInPath(item as Record<string, unknown>, imageProperty)
|
|
158
|
-
: undefined;
|
|
159
|
-
const hasImage = typeof imageValue === "string" && imageValue.length > 0;
|
|
160
|
-
|
|
161
|
-
return (
|
|
162
|
-
<Card
|
|
163
|
-
onClick={(e) => { if (e) extra.onClick(e); }}
|
|
164
|
-
className={cls(
|
|
165
|
-
"overflow-hidden relative",
|
|
166
|
-
extra.selected && cardSelectedMixin,
|
|
167
|
-
extra.highlighted && !extra.selected && "ring-1 ring-primary/50"
|
|
168
|
-
)}
|
|
169
|
-
>
|
|
170
|
-
{/* Image */}
|
|
171
|
-
{imageProperty && (
|
|
172
|
-
hasImage ? (
|
|
173
|
-
<img
|
|
174
|
-
src={imageValue as string}
|
|
175
|
-
alt=""
|
|
176
|
-
className="w-full h-32 object-cover rounded-t-lg"
|
|
177
|
-
/>
|
|
178
|
-
) : (
|
|
179
|
-
<div className={cls("h-24 rounded-t-lg", fieldBackgroundMixin)} />
|
|
180
|
-
)
|
|
181
|
-
)}
|
|
182
|
-
|
|
183
|
-
{/* Selection checkbox */}
|
|
184
|
-
{selectionEnabled && (
|
|
185
|
-
<div
|
|
186
|
-
className="absolute top-2 right-2"
|
|
187
|
-
onClick={(e) => e.stopPropagation()}
|
|
188
|
-
>
|
|
189
|
-
<Checkbox
|
|
190
|
-
checked={extra.selected}
|
|
191
|
-
onCheckedChange={extra.onSelectionChange}
|
|
192
|
-
size="small"
|
|
193
|
-
/>
|
|
194
|
-
</div>
|
|
195
|
-
)}
|
|
196
|
-
|
|
197
|
-
{/* Title */}
|
|
198
|
-
<Typography variant="subtitle2" noWrap className="px-3 pt-2 pb-1">
|
|
199
|
-
{String(titleValue ?? "")}
|
|
200
|
-
</Typography>
|
|
201
|
-
|
|
202
|
-
{/* Preview properties */}
|
|
203
|
-
{previewProperties.map(({ key, property }) => {
|
|
204
|
-
const value = getValueInPath(item as Record<string, unknown>, key);
|
|
205
|
-
|
|
206
|
-
if (cellRenderer) {
|
|
207
|
-
const custom = cellRenderer({
|
|
208
|
-
row: item,
|
|
209
|
-
propertyKey: key,
|
|
210
|
-
property,
|
|
211
|
-
value,
|
|
212
|
-
size: "small",
|
|
213
|
-
});
|
|
214
|
-
if (custom !== undefined) {
|
|
215
|
-
return (
|
|
216
|
-
<div key={key} className="px-3 py-0.5">
|
|
217
|
-
{custom}
|
|
218
|
-
</div>
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return (
|
|
224
|
-
<div key={key} className="px-3 py-0.5">
|
|
225
|
-
<DefaultCellRenderer
|
|
226
|
-
row={item}
|
|
227
|
-
propertyKey={key}
|
|
228
|
-
property={property}
|
|
229
|
-
value={value}
|
|
230
|
-
size="small"
|
|
231
|
-
/>
|
|
232
|
-
</div>
|
|
233
|
-
);
|
|
234
|
-
})}
|
|
235
|
-
|
|
236
|
-
<div className="pb-2" />
|
|
237
|
-
</Card>
|
|
238
|
-
);
|
|
239
|
-
},
|
|
240
|
-
[titlePropertyKey, imageProperty, previewProperties, cellRenderer, selectionEnabled]
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
return (
|
|
244
|
-
<div className={cls("w-full h-full overflow-auto", className)}>
|
|
245
|
-
<CardView<T>
|
|
246
|
-
data={dataController.data}
|
|
247
|
-
dataLoading={dataController.loading}
|
|
248
|
-
noMoreToLoad={dataController.noMoreToLoad}
|
|
249
|
-
dataLoadingError={dataController.error}
|
|
250
|
-
itemCount={dataController.itemCount}
|
|
251
|
-
setItemCount={dataController.setItemCount}
|
|
252
|
-
pageSize={dataController.pageSize}
|
|
253
|
-
paginationEnabled={dataController.paginationEnabled}
|
|
254
|
-
onItemClick={onRowClick}
|
|
255
|
-
selectedIds={selectedIds}
|
|
256
|
-
highlightedIds={highlightedIds}
|
|
257
|
-
selectionEnabled={!!selectionEnabled}
|
|
258
|
-
onSelectionChange={handleSelectionChange}
|
|
259
|
-
size={size}
|
|
260
|
-
renderCard={renderCard}
|
|
261
|
-
emptyComponent={emptyComponent}
|
|
262
|
-
/>
|
|
263
|
-
</div>
|
|
264
|
-
);
|
|
265
|
-
}
|