@rebasepro/admin 0.3.0 → 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.
Files changed (86) hide show
  1. package/dist/{CollectionEditorDialog-D0VqpLPO.js → CollectionEditorDialog-Bo86h-d-.js} +22 -5
  2. package/dist/CollectionEditorDialog-Bo86h-d-.js.map +1 -0
  3. package/dist/{CollectionsStudioView-Bc3Rxxc2.js → CollectionsStudioView-bsg4AT_Z.js} +4 -4
  4. package/dist/{CollectionsStudioView-Bc3Rxxc2.js.map → CollectionsStudioView-bsg4AT_Z.js.map} +1 -1
  5. package/dist/{ExportCollectionAction-Ckc-09BQ.js → ExportCollectionAction-Cf-Id5uO.js} +35 -27
  6. package/dist/ExportCollectionAction-Cf-Id5uO.js.map +1 -0
  7. package/dist/{ImportCollectionAction-BqjIrC3Z.js → ImportCollectionAction-LN5sreK9.js} +2 -2
  8. package/dist/{ImportCollectionAction-BqjIrC3Z.js.map → ImportCollectionAction-LN5sreK9.js.map} +1 -1
  9. package/dist/{PropertyEditView-CvRSV-A2.js → PropertyEditView-jCWCxmCz.js} +2 -2
  10. package/dist/{PropertyEditView-CvRSV-A2.js.map → PropertyEditView-jCWCxmCz.js.map} +1 -1
  11. package/dist/collection_editor_ui.js +3 -3
  12. package/dist/components/CollectionPanel.d.ts +83 -0
  13. package/dist/components/EntityDetailView.d.ts +0 -3
  14. package/dist/components/EntityEditView.d.ts +2 -3
  15. package/dist/components/RebaseRouteDefs.d.ts +1 -1
  16. package/dist/components/SelectableTable/SelectableTable.d.ts +2 -2
  17. package/dist/components/SelectableTable/SelectableTableContext.d.ts +1 -1
  18. package/dist/components/SelectableTable/SelectionStore.d.ts +3 -3
  19. package/dist/components/admin/index.d.ts +1 -3
  20. package/dist/components/index.d.ts +1 -0
  21. package/dist/editor.js +15 -5
  22. package/dist/editor.js.map +1 -1
  23. package/dist/hooks/navigation/useBuildNavigationStateController.d.ts +1 -1
  24. package/dist/hooks/navigation/useResolvedViews.d.ts +2 -5
  25. package/dist/{index-UQOMHwt1.js → index-BrqSvMG0.js} +3 -3
  26. package/dist/index-BrqSvMG0.js.map +1 -0
  27. package/dist/{index-DY2k5TtG.js → index-Cb0bPnzZ.js} +3 -3
  28. package/dist/index-Cb0bPnzZ.js.map +1 -0
  29. package/dist/{index-BCcLwgfe.js → index-z3H8iqsj.js} +2 -2
  30. package/dist/{index-BCcLwgfe.js.map → index-z3H8iqsj.js.map} +1 -1
  31. package/dist/index.d.ts +2 -4
  32. package/dist/index.js +228 -295
  33. package/dist/index.js.map +1 -1
  34. package/dist/util/entity_view_constants.d.ts +6 -0
  35. package/dist/util/resolutions.d.ts +1 -2
  36. package/dist/{util-ZM9gQuCv.js → util-CvNp6A-r.js} +422 -798
  37. package/dist/util-CvNp6A-r.js.map +1 -0
  38. package/package.json +8 -8
  39. package/src/collection_editor/pgColumnToProperty.ts +19 -2
  40. package/src/collection_editor/validateCollectionJson.ts +12 -12
  41. package/src/components/CollectionPanel.tsx +155 -0
  42. package/src/components/DefaultDrawer.tsx +2 -2
  43. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +64 -34
  44. package/src/components/EntityCollectionView/EntityCollectionListView.tsx +8 -1
  45. package/src/components/EntityCollectionView/EntityCollectionView.tsx +4 -1
  46. package/src/components/EntityCollectionView/hooks/useCollectionInlineEditor.ts +1 -0
  47. package/src/components/EntityCollectionView/useEntityPreviewSlots.ts +69 -18
  48. package/src/components/EntityDetailView.tsx +36 -37
  49. package/src/components/EntityEditView.tsx +108 -51
  50. package/src/components/EntityEditViewFormActions.tsx +4 -4
  51. package/src/components/EntitySidePanel.tsx +47 -14
  52. package/src/components/PropertyCollectionView.tsx +1 -0
  53. package/src/components/RebaseRouteDefs.tsx +4 -6
  54. package/src/components/SelectableTable/SelectableTable.tsx +24 -22
  55. package/src/components/SelectableTable/SelectableTableContext.tsx +2 -1
  56. package/src/components/SelectableTable/SelectionStore.ts +4 -4
  57. package/src/components/admin/index.ts +1 -3
  58. package/src/components/index.ts +3 -3
  59. package/src/data_export/export/BasicExportAction.tsx +11 -9
  60. package/src/data_export/export/ExportCollectionAction.tsx +15 -13
  61. package/src/editor/editor.tsx +20 -4
  62. package/src/form/EntityForm.tsx +3 -3
  63. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +3 -1
  64. package/src/hooks/navigation/useBuildNavigationStateController.tsx +2 -3
  65. package/src/hooks/navigation/useResolvedCollections.ts +2 -10
  66. package/src/hooks/navigation/useResolvedViews.tsx +6 -48
  67. package/src/hooks/useBuildSideEntityController.tsx +20 -3
  68. package/src/index.ts +6 -5
  69. package/src/util/entity_view_constants.ts +6 -0
  70. package/src/util/previews.ts +9 -1
  71. package/src/util/resolutions.ts +2 -6
  72. package/dist/CollectionEditorDialog-D0VqpLPO.js.map +0 -1
  73. package/dist/ExportCollectionAction-Ckc-09BQ.js.map +0 -1
  74. package/dist/components/admin/RoleChip.d.ts +0 -4
  75. package/dist/components/admin/RolesFilterSelect.d.ts +0 -2
  76. package/dist/components/admin/RolesView.d.ts +0 -4
  77. package/dist/components/admin/UserRolesSelectField.d.ts +0 -2
  78. package/dist/components/admin/UsersView.d.ts +0 -4
  79. package/dist/index-DY2k5TtG.js.map +0 -1
  80. package/dist/index-UQOMHwt1.js.map +0 -1
  81. package/dist/util-ZM9gQuCv.js.map +0 -1
  82. package/src/components/admin/RoleChip.tsx +0 -23
  83. package/src/components/admin/RolesFilterSelect.tsx +0 -45
  84. package/src/components/admin/RolesView.tsx +0 -470
  85. package/src/components/admin/UserRolesSelectField.tsx +0 -50
  86. package/src/components/admin/UsersView.tsx +0 -693
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/admin",
3
3
  "type": "module",
4
- "version": "0.3.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/common": "0.3.0",
88
- "@rebasepro/formex": "0.3.0",
89
- "@rebasepro/core": "0.3.0",
90
- "@rebasepro/schema-inference": "0.3.0",
91
- "@rebasepro/types": "0.3.0",
92
- "@rebasepro/utils": "0.3.0",
93
- "@rebasepro/ui": "0.3.0"
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",
@@ -158,12 +158,29 @@ label: prettifyIdentifier(v) })),
158
158
  };
159
159
 
160
160
  case "array":
161
- case "ARRAY":
161
+ case "ARRAY": {
162
+ let innerType = "string";
163
+ let colType: ArrayProperty["columnType"] = undefined;
164
+ if (udt_name === "_text" || udt_name === "_varchar") {
165
+ innerType = "string";
166
+ colType = "text[]";
167
+ } else if (udt_name === "_int4" || udt_name === "_int2" || udt_name === "_int8") {
168
+ innerType = "number";
169
+ colType = "integer[]";
170
+ } else if (udt_name === "_bool") {
171
+ innerType = "boolean";
172
+ colType = "boolean[]";
173
+ } else if (udt_name === "_numeric") {
174
+ innerType = "number";
175
+ colType = "numeric[]";
176
+ }
162
177
  return {
163
178
  type: "array",
164
179
  name: prettifiedName,
165
- of: { type: "string" }
180
+ columnType: colType,
181
+ of: { type: innerType }
166
182
  } as ArrayProperty;
183
+ }
167
184
 
168
185
  default:
169
186
  // Fallback: treat unknown types as string
@@ -52,16 +52,16 @@ function validateProperty(
52
52
  return;
53
53
  }
54
54
 
55
- // Check dataType
56
- if (!property.dataType) {
55
+ // Check type
56
+ if (!property.type) {
57
57
  errors.push({
58
- path: `${path}.dataType`,
58
+ path: `${path}.type`,
59
59
  message: "Required field is missing"
60
60
  });
61
- } else if (!VALID_DATA_TYPES.includes(property.dataType as DataType)) {
61
+ } else if (!VALID_DATA_TYPES.includes(property.type as DataType)) {
62
62
  errors.push({
63
- path: `${path}.dataType`,
64
- message: `Invalid value "${property.dataType}", expected one of: ${VALID_DATA_TYPES.join(", ")}`
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.dataType === "array") {
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.dataType === "map" && property.properties) {
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.dataType === "reference") {
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.dataType === "relation") {
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.dataType === "vector") {
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.dataType === "string" && property.storage) {
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 &quot;{path}&quot; 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
+ }
@@ -317,7 +317,7 @@ export function DrawerToggle({
317
317
  const { t } = useTranslation();
318
318
 
319
319
  return (
320
- <div className="shrink-0 mt-auto px-2 py-2">
320
+ <div className="shrink-0 mt-auto px-4 pt-0.5 pb-2">
321
321
  <Tooltip
322
322
  title={isExpanded ? t("collapse") : t("expand")}
323
323
  side="right"
@@ -486,7 +486,7 @@ export function DrawerFooterActions({
486
486
  }
487
487
 
488
488
  return (
489
- <div className="shrink-0 pb-1" ref={portalRef}>
489
+ <div className="shrink-0 pt-2 pb-0" ref={portalRef}>
490
490
  {avatarComponent && (
491
491
  <div className="flex items-center px-[16px] py-1">
492
492
  <Menu
@@ -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 IntersectionObserver callback
99
- // to avoid re-creating the observer every time pagination state changes.
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 with Intersection Observer — stable deps only
122
+ // Infinite scroll and resize observer
111
123
  useEffect(() => {
112
- const observer = new IntersectionObserver(
113
- (entries) => {
114
- const { paginationEnabled: pe, noMoreToLoad: nm, dataLoading: dl, itemCount: ic, pageSize: ps } = paginationStateRef.current;
115
- if (entries[0].isIntersecting && pe && !dl && !nm && !isLoadingMore.current) {
116
- isLoadingMore.current = true;
117
- setItemCount?.((ic ?? ps) + ps);
118
- }
119
- },
120
- {
121
- root: containerRef.current,
122
- rootMargin: "400px",
123
- threshold: 0
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);
124
144
  }
125
- );
145
+ };
126
146
 
127
- if (loadMoreRef.current) {
128
- observer.observe(loadMoreRef.current);
129
- }
147
+ const onScrollEvent = () => {
148
+ if (rafId === null) rafId = requestAnimationFrame(update);
149
+ };
130
150
 
131
- return () => observer.disconnect();
151
+ scrollEl.addEventListener("scroll", onScrollEvent, { passive: true });
152
+ const ro = new ResizeObserver(() => update());
153
+ ro.observe(scrollEl);
154
+ update(); // initial measurement
155
+
156
+ return () => {
157
+ scrollEl.removeEventListener("scroll", onScrollEvent);
158
+ ro.disconnect();
159
+ if (rafId !== null) cancelAnimationFrame(rafId);
160
+ };
132
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
- const el = containerRef.current;
143
- if (!el) return;
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 container = containerRef.current;
161
- if (!container || !onScroll) return;
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 = container.scrollTop;
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
- container.addEventListener("scroll", handleScroll, { passive: true });
175
- return () => container.removeEventListener("scroll", handleScroll);
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="flex-1 overflow-auto p-4"
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 && (
@@ -139,6 +139,13 @@ function getScrollParent(element: HTMLElement | null): HTMLElement | null {
139
139
  * PropertyPreview in list row columns (because it would blow up height).
140
140
  */
141
141
  function isComplexPropertyType(property: Property): boolean {
142
+ if (property.type === "array") {
143
+ const ofProp = "of" in property ? property.of : undefined;
144
+ const innerProp = ofProp ? (Array.isArray(ofProp) ? ofProp[0] : ofProp) : undefined;
145
+ if (innerProp && typeof innerProp === "object" && "enum" in innerProp && innerProp.enum) {
146
+ return false;
147
+ }
148
+ }
142
149
  return property.type === "array"
143
150
  || property.type === "map"
144
151
  || property.type === "reference"
@@ -597,7 +604,7 @@ export function EntityCollectionListView<M extends Record<string, unknown> = Rec
597
604
  <CircularProgress size="small"/>
598
605
  </div>
599
606
  ) : isEmpty ? (
600
- <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">
601
608
  {emptyComponent ?? (
602
609
  <Typography variant="label" color="secondary">
603
610
  No entries found
@@ -1092,7 +1092,10 @@ export const EntityCollectionView = React.memo(
1092
1092
  ) : (
1093
1093
  <div className="flex flex-col w-full h-full">
1094
1094
  {toolbarNode}
1095
- <div className="flex-1 flex flex-col overflow-y-auto">
1095
+ <div className={cls(
1096
+ "flex-1 flex flex-col",
1097
+ (viewMode === "list" || viewMode === "cards") && "overflow-y-auto"
1098
+ )}>
1096
1099
  {viewMode === "list" ? (
1097
1100
  <div
1098
1101
  className={cls(
@@ -42,6 +42,7 @@ export function useCollectionInlineEditor<M extends Record<string, unknown>>({
42
42
  setError,
43
43
  data: entity
44
44
  }) => {
45
+ if (!entity) return;
45
46
 
46
47
  const updatedValues = setIn({}, propertyKey, value) as Partial<Record<string, unknown>>;
47
48
 
@@ -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
- // Try using the target collection's title property for accuracy
433
+ // Resolve target collection from either `prop.relation.target()` or `prop.target()` (inline API)
434
+ let targetCollection: EntityCollection | undefined;
440
435
  try {
441
- const targetCollection = prop.relation?.target?.();
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
- // Fallback: find first non-id string property in target
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 (tp.type === "string" && !tp.multiline && !tp.markdown && !tp.storage && !("isId" in tp && tp.isId)) {
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
- } catch {
458
- // Target collection may not be resolvable — fall through
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
- // Generic fallback: walk entity values for any short string.
462
- for (const [, v] of Object.entries(values)) {
463
- if (typeof v === "string" && v.length > 0 && v.length < 200) {
464
- return v;
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
- const id = "id" in relation ? relation.id : undefined;
519
+ // 3. Final fallback: show the ID
469
520
  return id !== undefined ? String(id) : undefined;
470
521
  }