@rebasepro/admin 0.2.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{CollectionEditorDialog-Cn8-tGyL.js → CollectionEditorDialog-Bo86h-d-.js} +3 -3
- package/dist/{CollectionEditorDialog-Cn8-tGyL.js.map → CollectionEditorDialog-Bo86h-d-.js.map} +1 -1
- package/dist/{CollectionsStudioView-C-Ts1rZt.js → CollectionsStudioView-bsg4AT_Z.js} +4 -4
- package/dist/{CollectionsStudioView-C-Ts1rZt.js.map → CollectionsStudioView-bsg4AT_Z.js.map} +1 -1
- package/dist/{ExportCollectionAction-BRdKM3DF.js → ExportCollectionAction-Cf-Id5uO.js} +35 -27
- package/dist/ExportCollectionAction-Cf-Id5uO.js.map +1 -0
- package/dist/{ImportCollectionAction-U-v7lGxO.js → ImportCollectionAction-LN5sreK9.js} +2 -2
- package/dist/{ImportCollectionAction-U-v7lGxO.js.map → ImportCollectionAction-LN5sreK9.js.map} +1 -1
- package/dist/{PropertyEditView-BDNYkfNf.js → PropertyEditView-jCWCxmCz.js} +2 -2
- package/dist/{PropertyEditView-BDNYkfNf.js.map → PropertyEditView-jCWCxmCz.js.map} +1 -1
- package/dist/collection_editor_ui.js +3 -3
- package/dist/components/CollectionPanel.d.ts +83 -0
- package/dist/components/EntityDetailView.d.ts +0 -3
- package/dist/components/EntityEditView.d.ts +2 -3
- package/dist/components/SelectableTable/SelectableTable.d.ts +2 -2
- package/dist/components/SelectableTable/SelectableTableContext.d.ts +1 -1
- package/dist/components/SelectableTable/SelectionStore.d.ts +3 -3
- package/dist/components/index.d.ts +1 -0
- package/dist/editor.js +15 -5
- package/dist/editor.js.map +1 -1
- package/dist/{index-DJSL_SCr.js → index-BrqSvMG0.js} +3 -3
- package/dist/index-BrqSvMG0.js.map +1 -0
- package/dist/{index-DHaOV-7A.js → index-Cb0bPnzZ.js} +3 -3
- package/dist/index-Cb0bPnzZ.js.map +1 -0
- package/dist/{index-XMII4H3d.js → index-z3H8iqsj.js} +2 -2
- package/dist/{index-XMII4H3d.js.map → index-z3H8iqsj.js.map} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +142 -4
- package/dist/index.js.map +1 -1
- package/dist/util/entity_view_constants.d.ts +6 -0
- package/dist/util/resolutions.d.ts +1 -2
- package/dist/{util-0GYaJqL_.js → util-CvNp6A-r.js} +283 -168
- package/dist/util-CvNp6A-r.js.map +1 -0
- package/package.json +8 -8
- package/src/collection_editor/validateCollectionJson.ts +12 -12
- package/src/components/CollectionPanel.tsx +155 -0
- package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +65 -35
- package/src/components/EntityCollectionView/EntityCollectionListView.tsx +1 -1
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +1 -1
- package/src/components/EntityCollectionView/hooks/useCollectionInlineEditor.ts +1 -0
- package/src/components/EntityCollectionView/useEntityPreviewSlots.ts +69 -18
- package/src/components/EntityDetailView.tsx +36 -37
- package/src/components/EntityEditView.tsx +108 -51
- package/src/components/EntityEditViewFormActions.tsx +4 -4
- package/src/components/EntitySidePanel.tsx +47 -14
- package/src/components/PropertyCollectionView.tsx +1 -0
- package/src/components/SelectableTable/SelectableTable.tsx +24 -22
- package/src/components/SelectableTable/SelectableTableContext.tsx +2 -1
- package/src/components/SelectableTable/SelectionStore.ts +4 -4
- package/src/components/index.ts +2 -0
- package/src/data_export/export/BasicExportAction.tsx +11 -9
- package/src/data_export/export/ExportCollectionAction.tsx +15 -13
- package/src/editor/editor.tsx +20 -4
- package/src/form/EntityForm.tsx +3 -3
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +3 -1
- package/src/hooks/navigation/useResolvedCollections.ts +2 -10
- package/src/hooks/useBuildSideEntityController.tsx +20 -3
- package/src/index.ts +4 -2
- package/src/util/entity_view_constants.ts +6 -0
- package/src/util/resolutions.ts +2 -6
- package/dist/ExportCollectionAction-BRdKM3DF.js.map +0 -1
- package/dist/index-DHaOV-7A.js.map +0 -1
- package/dist/index-DJSL_SCr.js.map +0 -1
- package/dist/util-0GYaJqL_.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/admin",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"description": "Rebase CMS — content management views, forms, and routing",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -84,13 +84,13 @@
|
|
|
84
84
|
"react-dropzone": "^14.4.1",
|
|
85
85
|
"react-use-measure": "^2.1.7",
|
|
86
86
|
"zod": "^3.25.76",
|
|
87
|
-
"@rebasepro/
|
|
88
|
-
"@rebasepro/
|
|
89
|
-
"@rebasepro/formex": "0.
|
|
90
|
-
"@rebasepro/schema-inference": "0.
|
|
91
|
-
"@rebasepro/
|
|
92
|
-
"@rebasepro/
|
|
93
|
-
"@rebasepro/
|
|
87
|
+
"@rebasepro/core": "0.4.0",
|
|
88
|
+
"@rebasepro/common": "0.4.0",
|
|
89
|
+
"@rebasepro/formex": "0.4.0",
|
|
90
|
+
"@rebasepro/schema-inference": "0.4.0",
|
|
91
|
+
"@rebasepro/types": "0.4.0",
|
|
92
|
+
"@rebasepro/utils": "0.4.0",
|
|
93
|
+
"@rebasepro/ui": "0.4.0"
|
|
94
94
|
},
|
|
95
95
|
"peerDependencies": {
|
|
96
96
|
"react": ">=19.0.0",
|
|
@@ -52,16 +52,16 @@ function validateProperty(
|
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// Check
|
|
56
|
-
if (!property.
|
|
55
|
+
// Check type
|
|
56
|
+
if (!property.type) {
|
|
57
57
|
errors.push({
|
|
58
|
-
path: `${path}.
|
|
58
|
+
path: `${path}.type`,
|
|
59
59
|
message: "Required field is missing"
|
|
60
60
|
});
|
|
61
|
-
} else if (!VALID_DATA_TYPES.includes(property.
|
|
61
|
+
} else if (!VALID_DATA_TYPES.includes(property.type as DataType)) {
|
|
62
62
|
errors.push({
|
|
63
|
-
path: `${path}.
|
|
64
|
-
message: `Invalid value "${property.
|
|
63
|
+
path: `${path}.type`,
|
|
64
|
+
message: `Invalid value "${property.type}", expected one of: ${VALID_DATA_TYPES.join(", ")}`
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -74,7 +74,7 @@ function validateProperty(
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// Validate array "of" property
|
|
77
|
-
if (property.
|
|
77
|
+
if (property.type === "array") {
|
|
78
78
|
if (property.of) {
|
|
79
79
|
if (Array.isArray(property.of)) {
|
|
80
80
|
property.of.forEach((ofProp: Record<string, unknown>, index: number) => {
|
|
@@ -98,12 +98,12 @@ function validateProperty(
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
// Validate map properties
|
|
101
|
-
if (property.
|
|
101
|
+
if (property.type === "map" && property.properties) {
|
|
102
102
|
validateProperties(property.properties as Record<string, unknown>, `${path}.properties`, errors);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// Validate reference path
|
|
106
|
-
if (property.
|
|
106
|
+
if (property.type === "reference") {
|
|
107
107
|
if (property.path !== undefined && typeof property.path !== "string") {
|
|
108
108
|
errors.push({
|
|
109
109
|
path: `${path}.path`,
|
|
@@ -113,7 +113,7 @@ function validateProperty(
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
// Validate relation property
|
|
116
|
-
if (property.
|
|
116
|
+
if (property.type === "relation") {
|
|
117
117
|
if (property.target !== undefined && typeof property.target !== "string" && typeof property.target !== "function") {
|
|
118
118
|
errors.push({
|
|
119
119
|
path: `${path}.target`,
|
|
@@ -187,7 +187,7 @@ function validateProperty(
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
// Validate vector property
|
|
190
|
-
if (property.
|
|
190
|
+
if (property.type === "vector") {
|
|
191
191
|
if (property.dimensions !== undefined && (typeof property.dimensions !== "number" || isNaN(property.dimensions) || property.dimensions <= 0)) {
|
|
192
192
|
errors.push({
|
|
193
193
|
path: `${path}.dimensions`,
|
|
@@ -197,7 +197,7 @@ function validateProperty(
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
// Validate storage config for string
|
|
200
|
-
if (property.
|
|
200
|
+
if (property.type === "string" && property.storage) {
|
|
201
201
|
if (typeof property.storage !== "object") {
|
|
202
202
|
errors.push({
|
|
203
203
|
path: `${path}.storage`,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { EntityCollection, ViewMode } from "@rebasepro/types";
|
|
2
|
+
import React, { useMemo } from "react";
|
|
3
|
+
import { EntityCollectionView } from "./EntityCollectionView/EntityCollectionView";
|
|
4
|
+
import { useCollectionRegistryController } from "../hooks/navigation/contexts/CollectionRegistryContext";
|
|
5
|
+
import { Typography } from "@rebasepro/ui";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Props for the {@link CollectionPanel} component.
|
|
9
|
+
*
|
|
10
|
+
* This is a high-level, consumer-friendly wrapper around {@link EntityCollectionView}
|
|
11
|
+
* designed for embedding collection views inside custom pages (home pages,
|
|
12
|
+
* dashboards, entity detail views, etc.).
|
|
13
|
+
*
|
|
14
|
+
* At minimum, provide the `path` (collection slug). All other props are optional
|
|
15
|
+
* overrides that take precedence over the collection's default configuration.
|
|
16
|
+
*
|
|
17
|
+
* @group Components
|
|
18
|
+
*/
|
|
19
|
+
export type CollectionPanelProps = {
|
|
20
|
+
/**
|
|
21
|
+
* The collection slug / path to display (e.g. "tasks", "clients").
|
|
22
|
+
* The collection must be registered in the Rebase context.
|
|
23
|
+
*/
|
|
24
|
+
path: string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Optional title displayed above the collection.
|
|
28
|
+
* Set to `false` to hide the title entirely.
|
|
29
|
+
* Defaults to the collection's `name`.
|
|
30
|
+
*/
|
|
31
|
+
title?: string | false;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Force a specific view mode, overriding the collection's default.
|
|
35
|
+
*/
|
|
36
|
+
viewMode?: ViewMode;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Override the sort field.
|
|
40
|
+
*/
|
|
41
|
+
sort?: [string, "asc" | "desc"];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Maximum number of entities to display.
|
|
45
|
+
*/
|
|
46
|
+
limit?: number;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether to sync filter/sort state with URL query params.
|
|
50
|
+
* Defaults to `false` (embedded panels shouldn't hijack the URL).
|
|
51
|
+
*/
|
|
52
|
+
updateUrl?: boolean;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Override the entity open mode when clicking an entity.
|
|
56
|
+
*/
|
|
57
|
+
openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Additional CSS class name for the container.
|
|
61
|
+
*/
|
|
62
|
+
className?: string;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Any additional collection-level overrides (e.g. `previewProperties`,
|
|
66
|
+
* `enabledViews`, `entityActions`, `defaultFilter`, etc.).
|
|
67
|
+
*/
|
|
68
|
+
collectionOverrides?: Partial<EntityCollection>;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A high-level, reusable wrapper for embedding a Rebase collection view
|
|
73
|
+
* inside custom pages (dashboards, home pages, entity detail views, etc.).
|
|
74
|
+
*
|
|
75
|
+
* Usage:
|
|
76
|
+
* ```tsx
|
|
77
|
+
* import { CollectionPanel } from "@rebasepro/admin";
|
|
78
|
+
*
|
|
79
|
+
* function MyDashboard() {
|
|
80
|
+
* return (
|
|
81
|
+
* <div>
|
|
82
|
+
* <CollectionPanel path="tasks" title="Pending Tasks" />
|
|
83
|
+
* <CollectionPanel
|
|
84
|
+
* path="clients"
|
|
85
|
+
* viewMode="table"
|
|
86
|
+
* collectionOverrides={{
|
|
87
|
+
* defaultFilter: { status: ["!=", "completed"] }
|
|
88
|
+
* }}
|
|
89
|
+
* />
|
|
90
|
+
* </div>
|
|
91
|
+
* );
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* @group Components
|
|
96
|
+
*/
|
|
97
|
+
export function CollectionPanel({
|
|
98
|
+
path,
|
|
99
|
+
title,
|
|
100
|
+
viewMode,
|
|
101
|
+
sort,
|
|
102
|
+
limit,
|
|
103
|
+
updateUrl = false,
|
|
104
|
+
openEntityMode,
|
|
105
|
+
className,
|
|
106
|
+
collectionOverrides
|
|
107
|
+
}: CollectionPanelProps) {
|
|
108
|
+
const collectionRegistry = useCollectionRegistryController();
|
|
109
|
+
const registeredCollection = collectionRegistry.getCollection(path);
|
|
110
|
+
|
|
111
|
+
const mergedCollection = useMemo(() => {
|
|
112
|
+
if (!registeredCollection) return undefined;
|
|
113
|
+
|
|
114
|
+
// Build overrides object from shorthand props + explicit overrides
|
|
115
|
+
const propOverrides: Record<string, unknown> = {};
|
|
116
|
+
if (viewMode) propOverrides.defaultViewMode = viewMode;
|
|
117
|
+
if (sort) propOverrides.sort = sort;
|
|
118
|
+
if (limit) propOverrides.pagination = limit;
|
|
119
|
+
if (openEntityMode) propOverrides.openEntityMode = openEntityMode;
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
...registeredCollection,
|
|
123
|
+
...(collectionOverrides ?? {}),
|
|
124
|
+
...propOverrides
|
|
125
|
+
} as EntityCollection;
|
|
126
|
+
}, [registeredCollection, collectionOverrides, viewMode, sort, limit, openEntityMode]);
|
|
127
|
+
|
|
128
|
+
if (!mergedCollection) {
|
|
129
|
+
return (
|
|
130
|
+
<div className={className}>
|
|
131
|
+
<Typography variant="body2" color="secondary">
|
|
132
|
+
Collection "{path}" not found.
|
|
133
|
+
</Typography>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div className={className}>
|
|
140
|
+
{title !== false && (
|
|
141
|
+
<Typography
|
|
142
|
+
variant="subtitle2"
|
|
143
|
+
className="font-bold mb-2 text-surface-700 dark:text-surface-300"
|
|
144
|
+
>
|
|
145
|
+
{title ?? mergedCollection.name}
|
|
146
|
+
</Typography>
|
|
147
|
+
)}
|
|
148
|
+
<EntityCollectionView
|
|
149
|
+
{...mergedCollection}
|
|
150
|
+
path={path}
|
|
151
|
+
updateUrl={updateUrl}
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
@@ -60,6 +60,19 @@ function getGridColumnsClass(size: CollectionSize): string {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
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
|
+
|
|
63
76
|
/**
|
|
64
77
|
* Card grid view for displaying entities with infinite scroll.
|
|
65
78
|
* Alternative to the EntityCollectionTable for visual browsing.
|
|
@@ -78,7 +91,6 @@ export function EntityCollectionCardView<M extends Record<string, unknown> = Rec
|
|
|
78
91
|
}: EntityCollectionCardViewProps<M>) {
|
|
79
92
|
|
|
80
93
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
81
|
-
const loadMoreRef = useRef<HTMLDivElement>(null);
|
|
82
94
|
const hasRestoredScroll = useRef(false);
|
|
83
95
|
|
|
84
96
|
const {
|
|
@@ -95,8 +107,8 @@ export function EntityCollectionCardView<M extends Record<string, unknown> = Rec
|
|
|
95
107
|
// Track if we're currently loading to prevent multiple simultaneous load requests
|
|
96
108
|
const isLoadingMore = useRef(false);
|
|
97
109
|
|
|
98
|
-
// Keep mutable refs for values used in the
|
|
99
|
-
// to avoid re-
|
|
110
|
+
// Keep mutable refs for values used in the scroll listener callback
|
|
111
|
+
// to avoid re-attaching the listener every time pagination state changes.
|
|
100
112
|
const paginationStateRef = useRef({ paginationEnabled, noMoreToLoad, dataLoading, itemCount, pageSize });
|
|
101
113
|
useEffect(() => {
|
|
102
114
|
paginationStateRef.current = { paginationEnabled, noMoreToLoad, dataLoading, itemCount, pageSize };
|
|
@@ -107,43 +119,60 @@ export function EntityCollectionCardView<M extends Record<string, unknown> = Rec
|
|
|
107
119
|
if (!dataLoading) isLoadingMore.current = false;
|
|
108
120
|
}, [dataLoading]);
|
|
109
121
|
|
|
110
|
-
// Infinite scroll
|
|
122
|
+
// Infinite scroll and resize observer
|
|
111
123
|
useEffect(() => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
const el = containerRef.current;
|
|
125
|
+
if (!el) return;
|
|
126
|
+
const scrollEl = getScrollParent(el);
|
|
127
|
+
if (!scrollEl) return;
|
|
128
|
+
|
|
129
|
+
let rafId: number | null = null;
|
|
130
|
+
|
|
131
|
+
const update = () => {
|
|
132
|
+
rafId = null;
|
|
133
|
+
|
|
134
|
+
// Infinite scroll: trigger load-more when near the bottom
|
|
135
|
+
const { paginationEnabled: pe, noMoreToLoad: nm, itemCount: ic, pageSize: ps } = paginationStateRef.current;
|
|
136
|
+
if (
|
|
137
|
+
pe &&
|
|
138
|
+
!nm &&
|
|
139
|
+
!isLoadingMore.current &&
|
|
140
|
+
scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight < 400
|
|
141
|
+
) {
|
|
142
|
+
isLoadingMore.current = true;
|
|
143
|
+
setItemCount?.((ic ?? ps) + ps);
|
|
126
144
|
}
|
|
127
|
-
|
|
145
|
+
};
|
|
128
146
|
|
|
129
|
-
|
|
147
|
+
const onScrollEvent = () => {
|
|
148
|
+
if (rafId === null) rafId = requestAnimationFrame(update);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
scrollEl.addEventListener("scroll", onScrollEvent, { passive: true });
|
|
152
|
+
const ro = new ResizeObserver(() => update());
|
|
153
|
+
ro.observe(scrollEl);
|
|
154
|
+
update(); // initial measurement
|
|
130
155
|
|
|
131
|
-
return () =>
|
|
132
|
-
|
|
156
|
+
return () => {
|
|
157
|
+
scrollEl.removeEventListener("scroll", onScrollEvent);
|
|
158
|
+
ro.disconnect();
|
|
159
|
+
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
160
|
+
};
|
|
161
|
+
}, [setItemCount]);
|
|
133
162
|
|
|
134
163
|
// Scroll restoration — deferred to after layout paint
|
|
135
164
|
useEffect(() => {
|
|
136
165
|
if (!containerRef.current || !initialScroll || hasRestoredScroll.current || data.length === 0) return;
|
|
137
166
|
|
|
167
|
+
const scrollEl = getScrollParent(containerRef.current);
|
|
168
|
+
if (!scrollEl) return;
|
|
169
|
+
|
|
138
170
|
let attempts = 0;
|
|
139
171
|
const maxAttempts = 5;
|
|
140
172
|
|
|
141
173
|
const tryRestore = () => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (el.scrollHeight >= initialScroll || attempts >= maxAttempts) {
|
|
146
|
-
el.scrollTop = initialScroll;
|
|
174
|
+
if (scrollEl.scrollHeight >= initialScroll || attempts >= maxAttempts) {
|
|
175
|
+
scrollEl.scrollTop = initialScroll;
|
|
147
176
|
hasRestoredScroll.current = true;
|
|
148
177
|
} else {
|
|
149
178
|
attempts++;
|
|
@@ -157,11 +186,13 @@ export function EntityCollectionCardView<M extends Record<string, unknown> = Rec
|
|
|
157
186
|
// Scroll tracking: call onScroll when user scrolls
|
|
158
187
|
const lastScrollOffset = useRef(0);
|
|
159
188
|
useEffect(() => {
|
|
160
|
-
const
|
|
161
|
-
if (!
|
|
189
|
+
const el = containerRef.current;
|
|
190
|
+
if (!el || !onScroll) return;
|
|
191
|
+
const scrollEl = getScrollParent(el);
|
|
192
|
+
if (!scrollEl) return;
|
|
162
193
|
|
|
163
194
|
const handleScroll = () => {
|
|
164
|
-
const currentOffset =
|
|
195
|
+
const currentOffset = scrollEl.scrollTop;
|
|
165
196
|
const direction = currentOffset > lastScrollOffset.current ? "forward" : "backward";
|
|
166
197
|
lastScrollOffset.current = currentOffset;
|
|
167
198
|
onScroll({
|
|
@@ -171,8 +202,8 @@ export function EntityCollectionCardView<M extends Record<string, unknown> = Rec
|
|
|
171
202
|
});
|
|
172
203
|
};
|
|
173
204
|
|
|
174
|
-
|
|
175
|
-
return () =>
|
|
205
|
+
scrollEl.addEventListener("scroll", handleScroll, { passive: true });
|
|
206
|
+
return () => scrollEl.removeEventListener("scroll", handleScroll);
|
|
176
207
|
}, [onScroll]);
|
|
177
208
|
|
|
178
209
|
const handleEntityClick = useCallback((entity: Entity<M>) => {
|
|
@@ -201,7 +232,7 @@ export function EntityCollectionCardView<M extends Record<string, unknown> = Rec
|
|
|
201
232
|
return (
|
|
202
233
|
<div
|
|
203
234
|
ref={containerRef}
|
|
204
|
-
className="
|
|
235
|
+
className="w-full p-4"
|
|
205
236
|
>
|
|
206
237
|
{/* Error state */}
|
|
207
238
|
{dataLoadingError ? (
|
|
@@ -215,7 +246,7 @@ export function EntityCollectionCardView<M extends Record<string, unknown> = Rec
|
|
|
215
246
|
<CircularProgress size="small"/>
|
|
216
247
|
</div>
|
|
217
248
|
) : isEmpty ? (
|
|
218
|
-
<div className="flex items-center justify-center py-12 px-8">
|
|
249
|
+
<div className="w-full flex items-center justify-center py-12 px-8">
|
|
219
250
|
{emptyComponent ?? (
|
|
220
251
|
<Typography variant="label" color="secondary">
|
|
221
252
|
No entries found
|
|
@@ -247,7 +278,6 @@ export function EntityCollectionCardView<M extends Record<string, unknown> = Rec
|
|
|
247
278
|
|
|
248
279
|
{/* Load more trigger / Loading indicator */}
|
|
249
280
|
<div
|
|
250
|
-
ref={loadMoreRef}
|
|
251
281
|
className="flex items-center justify-center py-8"
|
|
252
282
|
>
|
|
253
283
|
{dataLoading && (
|
|
@@ -604,7 +604,7 @@ export function EntityCollectionListView<M extends Record<string, unknown> = Rec
|
|
|
604
604
|
<CircularProgress size="small"/>
|
|
605
605
|
</div>
|
|
606
606
|
) : isEmpty ? (
|
|
607
|
-
<div className="flex items-center justify-center py-12 px-8">
|
|
607
|
+
<div className="w-full flex items-center justify-center py-12 px-8">
|
|
608
608
|
{emptyComponent ?? (
|
|
609
609
|
<Typography variant="label" color="secondary">
|
|
610
610
|
No entries found
|
|
@@ -1094,7 +1094,7 @@ export const EntityCollectionView = React.memo(
|
|
|
1094
1094
|
{toolbarNode}
|
|
1095
1095
|
<div className={cls(
|
|
1096
1096
|
"flex-1 flex flex-col",
|
|
1097
|
-
viewMode === "list" && "overflow-y-auto"
|
|
1097
|
+
(viewMode === "list" || viewMode === "cards") && "overflow-y-auto"
|
|
1098
1098
|
)}>
|
|
1099
1099
|
{viewMode === "list" ? (
|
|
1100
1100
|
<div
|
|
@@ -2,6 +2,7 @@ import { useMemo } from "react";
|
|
|
2
2
|
import type { EntityCollection, Property, RelationProperty } from "@rebasepro/types";
|
|
3
3
|
import type { Entity, PropertyConfig, EntityRelation } from "@rebasepro/types";
|
|
4
4
|
import { getEntityImagePreviewPropertyKey } from "@rebasepro/common";
|
|
5
|
+
import { getEntityFromCache } from "@rebasepro/core";
|
|
5
6
|
import { getEntityTitlePropertyKey, getEntityPreviewKeys } from "../../util/previews";
|
|
6
7
|
import { getValueInPath } from "@rebasepro/utils";
|
|
7
8
|
import type { AuthController } from "@rebasepro/types";
|
|
@@ -428,43 +429,93 @@ function resolveRelationDisplayName(
|
|
|
428
429
|
): string | undefined {
|
|
429
430
|
// Support both EntityRelation instances and plain objects
|
|
430
431
|
const data = "data" in relation ? (relation as EntityRelation).data : undefined;
|
|
431
|
-
if (!data || !data.values) {
|
|
432
|
-
// No eagerly-loaded data — fall back to ID
|
|
433
|
-
const id = "id" in relation ? relation.id : undefined;
|
|
434
|
-
return id !== undefined ? String(id) : undefined;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const values = data.values as Record<string, unknown>;
|
|
438
432
|
|
|
439
|
-
//
|
|
433
|
+
// Resolve target collection from either `prop.relation.target()` or `prop.target()` (inline API)
|
|
434
|
+
let targetCollection: EntityCollection | undefined;
|
|
440
435
|
try {
|
|
441
|
-
const
|
|
436
|
+
const resolved = prop.relation?.target?.() ?? (typeof prop.target === "function" ? prop.target() : undefined);
|
|
437
|
+
if (resolved && typeof resolved === "object") {
|
|
438
|
+
targetCollection = resolved as EntityCollection;
|
|
439
|
+
}
|
|
440
|
+
} catch {
|
|
441
|
+
// Target collection may not be resolvable
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Helper: extract display name from entity values using the target collection
|
|
445
|
+
const extractDisplayName = (values: Record<string, unknown>): string | undefined => {
|
|
442
446
|
if (targetCollection) {
|
|
443
447
|
const targetTitleKey = targetCollection.titleProperty as string | undefined;
|
|
444
448
|
if (targetTitleKey && values[targetTitleKey] !== undefined) {
|
|
445
449
|
return String(values[targetTitleKey]);
|
|
446
450
|
}
|
|
447
|
-
|
|
451
|
+
|
|
452
|
+
// Helper to check if a property is hidden/internal
|
|
453
|
+
const isHiddenProp = (p: Property): boolean => {
|
|
454
|
+
const ui = (p as any).ui;
|
|
455
|
+
if (ui?.hideFromCollection) return true;
|
|
456
|
+
if (ui?.disabled?.hidden) return true;
|
|
457
|
+
return false;
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// Helper to check if a property is a visible, non-id string
|
|
461
|
+
const isDisplayCandidate = (p: Property): boolean => {
|
|
462
|
+
return p.type === "string" && !p.multiline && !p.markdown && !p.storage
|
|
463
|
+
&& !("isId" in p && p.isId) && !isHiddenProp(p);
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// Prioritize common title-like fields: name, title, label, displayName
|
|
467
|
+
const priorityKeys = ["name", "title", "label", "displayName"];
|
|
468
|
+
for (const pk of priorityKeys) {
|
|
469
|
+
const p = targetCollection.properties[pk] as Property | undefined;
|
|
470
|
+
if (p && isDisplayCandidate(p) && values[pk] !== undefined && values[pk] !== null) {
|
|
471
|
+
return String(values[pk]);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Fallback: find first visible, non-id string property in target
|
|
448
476
|
for (const [k, p] of Object.entries(targetCollection.properties)) {
|
|
449
477
|
const tp = p as Property;
|
|
450
|
-
if (
|
|
478
|
+
if (isDisplayCandidate(tp)) {
|
|
451
479
|
if (values[k] !== undefined && values[k] !== null) {
|
|
452
480
|
return String(values[k]);
|
|
453
481
|
}
|
|
454
482
|
}
|
|
455
483
|
}
|
|
456
484
|
}
|
|
457
|
-
|
|
458
|
-
|
|
485
|
+
// Generic fallback: walk entity values for any short string.
|
|
486
|
+
for (const [, v] of Object.entries(values)) {
|
|
487
|
+
if (typeof v === "string" && v.length > 0 && v.length < 200) {
|
|
488
|
+
return v;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return undefined;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// 1. Try eagerly-loaded data on the relation object
|
|
495
|
+
if (data && data.values) {
|
|
496
|
+
const result = extractDisplayName(data.values as Record<string, unknown>);
|
|
497
|
+
if (result) return result;
|
|
459
498
|
}
|
|
460
499
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
500
|
+
const id = "id" in relation ? relation.id : undefined;
|
|
501
|
+
|
|
502
|
+
// 2. Try the entity cache (sessionStorage) as a fallback
|
|
503
|
+
if (id !== undefined && targetCollection) {
|
|
504
|
+
try {
|
|
505
|
+
const slug = targetCollection.slug ?? (targetCollection as any).table;
|
|
506
|
+
if (slug) {
|
|
507
|
+
const cacheKey = `${slug}/${id}`;
|
|
508
|
+
const cached = getEntityFromCache(cacheKey) as { values?: Record<string, unknown> } | undefined;
|
|
509
|
+
if (cached?.values) {
|
|
510
|
+
const result = extractDisplayName(cached.values);
|
|
511
|
+
if (result) return result;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
} catch {
|
|
515
|
+
// Cache lookup failed — fall through
|
|
465
516
|
}
|
|
466
517
|
}
|
|
467
518
|
|
|
468
|
-
|
|
519
|
+
// 3. Final fallback: show the ID
|
|
469
520
|
return id !== undefined ? String(id) : undefined;
|
|
470
521
|
}
|