@rebasepro/admin 0.3.0 → 0.5.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 (106) hide show
  1. package/README.md +135 -0
  2. package/dist/{CollectionEditorDialog-D0VqpLPO.js → CollectionEditorDialog-C8E440oK.js} +22 -5
  3. package/dist/CollectionEditorDialog-C8E440oK.js.map +1 -0
  4. package/dist/{CollectionsStudioView-Bc3Rxxc2.js → CollectionsStudioView-Brr_b06v.js} +4 -4
  5. package/dist/{CollectionsStudioView-Bc3Rxxc2.js.map → CollectionsStudioView-Brr_b06v.js.map} +1 -1
  6. package/dist/{ExportCollectionAction-Ckc-09BQ.js → ExportCollectionAction-D3TTYXrq.js} +35 -27
  7. package/dist/ExportCollectionAction-D3TTYXrq.js.map +1 -0
  8. package/dist/{ImportCollectionAction-BqjIrC3Z.js → ImportCollectionAction-Dn7lya6E.js} +2 -2
  9. package/dist/{ImportCollectionAction-BqjIrC3Z.js.map → ImportCollectionAction-Dn7lya6E.js.map} +1 -1
  10. package/dist/{PropertyEditView-CvRSV-A2.js → PropertyEditView-ChBleIbN.js} +4 -4
  11. package/dist/PropertyEditView-ChBleIbN.js.map +1 -0
  12. package/dist/collection_editor/ui/collection_editor/PropertyEditView.d.ts +1 -0
  13. package/dist/collection_editor/ui/collection_editor/templates/blog_template.d.ts +1 -1
  14. package/dist/collection_editor/ui/collection_editor/templates/pages_template.d.ts +1 -1
  15. package/dist/collection_editor/ui/collection_editor/templates/products_template.d.ts +1 -1
  16. package/dist/collection_editor/ui/collection_editor/templates/users_template.d.ts +1 -1
  17. package/dist/collection_editor_ui.js +3 -3
  18. package/dist/components/CollectionPanel.d.ts +83 -0
  19. package/dist/components/EntityDetailView.d.ts +0 -3
  20. package/dist/components/EntityEditView.d.ts +2 -3
  21. package/dist/components/RebaseRouteDefs.d.ts +1 -1
  22. package/dist/components/SelectableTable/SelectableTable.d.ts +2 -2
  23. package/dist/components/SelectableTable/SelectableTableContext.d.ts +1 -1
  24. package/dist/components/SelectableTable/SelectionStore.d.ts +3 -3
  25. package/dist/components/admin/index.d.ts +1 -3
  26. package/dist/components/index.d.ts +1 -0
  27. package/dist/editor.js +15 -5
  28. package/dist/editor.js.map +1 -1
  29. package/dist/hooks/navigation/useBuildNavigationStateController.d.ts +1 -1
  30. package/dist/hooks/navigation/useResolvedViews.d.ts +2 -5
  31. package/dist/{index-BCcLwgfe.js → index-D3S9x_RP.js} +2 -2
  32. package/dist/{index-BCcLwgfe.js.map → index-D3S9x_RP.js.map} +1 -1
  33. package/dist/{index-DY2k5TtG.js → index-D9RNzm01.js} +3 -3
  34. package/dist/index-D9RNzm01.js.map +1 -0
  35. package/dist/{index-UQOMHwt1.js → index-DrrT332R.js} +3 -3
  36. package/dist/index-DrrT332R.js.map +1 -0
  37. package/dist/index.d.ts +2 -4
  38. package/dist/index.js +228 -295
  39. package/dist/index.js.map +1 -1
  40. package/dist/util/entity_view_constants.d.ts +6 -0
  41. package/dist/util/resolutions.d.ts +1 -2
  42. package/dist/{util-ZM9gQuCv.js → util-COYYD8zr.js} +473 -880
  43. package/dist/util-COYYD8zr.js.map +1 -0
  44. package/package.json +9 -10
  45. package/src/collection_editor/pgColumnToProperty.ts +19 -2
  46. package/src/collection_editor/ui/collection_editor/CollectionRelationsTab.tsx +2 -2
  47. package/src/collection_editor/ui/collection_editor/PropertyEditView.tsx +2 -1
  48. package/src/collection_editor/ui/collection_editor/PropertyFieldPreview.tsx +2 -2
  49. package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +2 -1
  50. package/src/collection_editor/ui/collection_editor/properties/conditions/EnumConditionsEditor.tsx +1 -1
  51. package/src/collection_editor/ui/collection_editor/templates/blog_template.ts +1 -1
  52. package/src/collection_editor/ui/collection_editor/templates/pages_template.ts +1 -1
  53. package/src/collection_editor/ui/collection_editor/templates/products_template.ts +1 -1
  54. package/src/collection_editor/ui/collection_editor/templates/users_template.ts +1 -1
  55. package/src/collection_editor/validateCollectionJson.ts +12 -12
  56. package/src/components/CollectionPanel.tsx +155 -0
  57. package/src/components/DefaultDrawer.tsx +2 -2
  58. package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +5 -2
  59. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +64 -34
  60. package/src/components/EntityCollectionView/EntityCollectionListView.tsx +8 -1
  61. package/src/components/EntityCollectionView/EntityCollectionView.tsx +4 -1
  62. package/src/components/EntityCollectionView/hooks/useCollectionInlineEditor.ts +1 -0
  63. package/src/components/EntityCollectionView/useEntityPreviewSlots.ts +68 -18
  64. package/src/components/EntityDetailView.tsx +32 -36
  65. package/src/components/EntityEditView.tsx +101 -50
  66. package/src/components/EntityEditViewFormActions.tsx +4 -4
  67. package/src/components/EntitySidePanel.tsx +50 -14
  68. package/src/components/PropertyCollectionView.tsx +1 -0
  69. package/src/components/RebaseRouteDefs.tsx +4 -6
  70. package/src/components/SelectableTable/SelectableTable.tsx +24 -22
  71. package/src/components/SelectableTable/SelectableTableContext.tsx +2 -1
  72. package/src/components/SelectableTable/SelectionStore.ts +4 -4
  73. package/src/components/admin/index.ts +1 -3
  74. package/src/components/index.ts +3 -3
  75. package/src/data_export/export/BasicExportAction.tsx +11 -9
  76. package/src/data_export/export/ExportCollectionAction.tsx +15 -13
  77. package/src/editor/editor.tsx +20 -4
  78. package/src/form/EntityForm.tsx +3 -3
  79. package/src/form/PropertyFieldBinding.tsx +1 -1
  80. package/src/form/field_bindings/KeyValueFieldBinding.tsx +1 -1
  81. package/src/form/field_bindings/MapFieldBinding.tsx +3 -45
  82. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +3 -1
  83. package/src/hooks/navigation/useBuildNavigationStateController.tsx +2 -3
  84. package/src/hooks/navigation/useResolvedCollections.ts +2 -10
  85. package/src/hooks/navigation/useResolvedViews.tsx +6 -48
  86. package/src/hooks/useBuildSideEntityController.tsx +20 -3
  87. package/src/index.ts +6 -5
  88. package/src/util/entity_view_constants.ts +6 -0
  89. package/src/util/previews.ts +9 -1
  90. package/src/util/resolutions.ts +2 -6
  91. package/dist/CollectionEditorDialog-D0VqpLPO.js.map +0 -1
  92. package/dist/ExportCollectionAction-Ckc-09BQ.js.map +0 -1
  93. package/dist/PropertyEditView-CvRSV-A2.js.map +0 -1
  94. package/dist/components/admin/RoleChip.d.ts +0 -4
  95. package/dist/components/admin/RolesFilterSelect.d.ts +0 -2
  96. package/dist/components/admin/RolesView.d.ts +0 -4
  97. package/dist/components/admin/UserRolesSelectField.d.ts +0 -2
  98. package/dist/components/admin/UsersView.d.ts +0 -4
  99. package/dist/index-DY2k5TtG.js.map +0 -1
  100. package/dist/index-UQOMHwt1.js.map +0 -1
  101. package/dist/util-ZM9gQuCv.js.map +0 -1
  102. package/src/components/admin/RoleChip.tsx +0 -23
  103. package/src/components/admin/RolesFilterSelect.tsx +0 -45
  104. package/src/components/admin/RolesView.tsx +0 -470
  105. package/src/components/admin/UserRolesSelectField.tsx +0 -50
  106. 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.5.0",
5
5
  "description": "Rebase CMS — content management views, forms, and routing",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -80,17 +80,16 @@
80
80
  "prosemirror-tables": "^1.8.5",
81
81
  "prosemirror-transform": "^1.12.0",
82
82
  "prosemirror-view": "^1.41.8",
83
- "react-compiler-runtime": "1.0.0",
83
+ "zod": "^3.25.76",
84
84
  "react-dropzone": "^14.4.1",
85
85
  "react-use-measure": "^2.1.7",
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"
86
+ "@rebasepro/common": "0.5.0",
87
+ "@rebasepro/formex": "0.5.0",
88
+ "@rebasepro/core": "0.5.0",
89
+ "@rebasepro/schema-inference": "0.5.0",
90
+ "@rebasepro/utils": "0.5.0",
91
+ "@rebasepro/types": "0.5.0",
92
+ "@rebasepro/ui": "0.5.0"
94
93
  },
95
94
  "peerDependencies": {
96
95
  "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
@@ -85,7 +85,7 @@ export function CollectionRelationsTab() {
85
85
  <Button variant="filled" color="neutral" onClick={() => {
86
86
  setEditingRelationIndex(-1);
87
87
  setEditingRelationState({ relationName: "",
88
- target: "" as unknown as Relation["target"],
88
+ target: "",
89
89
  cardinality: "many",
90
90
  direction: "owning" });
91
91
  }}>
@@ -134,7 +134,7 @@ direction: "owning" });
134
134
  <Button variant="text" onClick={() => {
135
135
  setEditingRelationIndex(-1);
136
136
  setEditingRelationState({ relationName: "",
137
- target: "" as unknown as Relation["target"],
137
+ target: "",
138
138
  cardinality: "many",
139
139
  direction: "owning" });
140
140
  }}>Create your first relation</Button>
@@ -50,7 +50,8 @@ import { MarkdownPropertyField } from "./properties/MarkdownPropertyField";
50
50
  import { isValidRegExp, mergeDeep } from "@rebasepro/utils";
51
51
 
52
52
  export type PropertyWithId = Property & {
53
- id?: string
53
+ id?: string;
54
+ conditions?: import('@rebasepro/types').PropertyConditions;
54
55
  };
55
56
 
56
57
  export type OnPropertyChangedParams = {
@@ -76,7 +76,7 @@ export function PropertyFieldPreview({
76
76
 
77
77
  <ErrorBoundary>
78
78
  <Typography variant="caption" component="span" className="ml-auto shrink-0 text-text-disabled dark:text-text-disabled-dark font-mono bg-surface-100 dark:bg-surface-900 px-1.5 py-0.5 rounded mr-8">
79
- {("columnType" in property ? (property as Property & { columnType?: string }).columnType : undefined) || property.type}
79
+ {"columnType" in property ? (property as { columnType?: string }).columnType ?? property.type : property.type}
80
80
  </Typography>
81
81
  </ErrorBoundary>
82
82
 
@@ -146,7 +146,7 @@ export function NonEditablePropertyPreview({
146
146
 
147
147
  {property && !isPropertyBuilder(property) && <ErrorBoundary>
148
148
  <Typography variant="caption" component="span" className="ml-auto shrink-0 text-text-disabled dark:text-text-disabled-dark font-mono bg-surface-100 dark:bg-surface-900 px-1.5 py-0.5 rounded mr-8">
149
- {("columnType" in property ? (property as Property & { columnType?: string }).columnType : undefined) || property.type}
149
+ {"columnType" in property ? (property as { columnType?: string }).columnType ?? property.type : property.type}
150
150
  </Typography>
151
151
  </ErrorBoundary>}
152
152
  </Paper>
@@ -18,6 +18,7 @@ import {
18
18
  } from "@rebasepro/ui";
19
19
  import { getFieldConfig, DEFAULT_FIELD_CONFIGS } from "../../../../_cms_internals";
20
20
  import { Properties, Property, EnumValueConfig } from "@rebasepro/types";
21
+ import type { PropertyConditions } from "@rebasepro/types";
21
22
  import { isPropertyBuilder } from "@rebasepro/common";
22
23
  import { PropertyWithId } from "../../PropertyEditView";
23
24
  import { getPropertyPaths } from "./property_paths";
@@ -817,7 +818,7 @@ export function ConditionsEditor({ disabled, collectionProperties }: ConditionsE
817
818
  : [];
818
819
 
819
820
  // Get current conditions from form values
820
- const conditions: Record<string, unknown> = (values as PropertyWithId & { conditions?: Record<string, unknown> }).conditions ?? {};
821
+ const conditions: PropertyConditions = values.conditions ?? {};
821
822
 
822
823
  const activeConditions: { type: ConditionType; group: ConditionGroup }[] = [];
823
824
 
@@ -528,7 +528,7 @@ export function EnumConditionsEditor({ disabled, collectionProperties }: EnumCon
528
528
  const allEnumIds = propertyEnumValues.map(ev => String(ev.id));
529
529
 
530
530
  // Get conditions from the correct path: values.conditions
531
- const conditions = (values as PropertyWithId & { conditions?: Record<string, unknown> }).conditions ?? {};
531
+ const conditions = values.conditions ?? {};
532
532
 
533
533
  // Parse existing enum conditions
534
534
  const activeConditions: { type: EnumConditionType; config: EnumConditionConfig }[] = [];
@@ -1,4 +1,4 @@
1
- import { EntityCollection } from "@rebasepro/types";
1
+ import type { EntityCollection } from "@rebasepro/types";
2
2
 
3
3
  export const blogCollectionTemplate = {
4
4
  slug: "blog",
@@ -1,4 +1,4 @@
1
- import { EntityCollection } from "@rebasepro/types";
1
+ import type { EntityCollection } from "@rebasepro/types";
2
2
 
3
3
  export const pagesCollectionTemplate = {
4
4
  slug: "pages",
@@ -1,4 +1,4 @@
1
- import { EntityCollection } from "@rebasepro/types";
1
+ import type { EntityCollection } from "@rebasepro/types";
2
2
 
3
3
  export const productsCollectionTemplate = {
4
4
  slug: "products",
@@ -1,4 +1,4 @@
1
- import { EntityCollection } from "@rebasepro/types";
1
+ import type { EntityCollection } from "@rebasepro/types";
2
2
 
3
3
  export const usersCollectionTemplate = {
4
4
  slug: "users",
@@ -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
@@ -1,5 +1,8 @@
1
1
  import React, { useCallback, useEffect } from "react";
2
2
 
3
+ /** Tracks events originating inside the inner (non-draggable) area. */
4
+ const innerClicked = new WeakSet<MouseEvent>();
5
+
3
6
  interface DraggableProps {
4
7
  containerRef: React.RefObject<HTMLDivElement | null>,
5
8
  innerRef: React.RefObject<HTMLDivElement | null>,
@@ -22,7 +25,7 @@ export function useDraggable({
22
25
  const listeningRef = React.useRef(false);
23
26
 
24
27
  const onMouseDown = useCallback((event: MouseEvent) => {
25
- if (event.button !== 0 || !containerRef.current || event.defaultPrevented || (event as MouseEvent & { innerClicked?: boolean }).innerClicked) {
28
+ if (event.button !== 0 || !containerRef.current || event.defaultPrevented || innerClicked.has(event)) {
26
29
  return;
27
30
  }
28
31
 
@@ -41,7 +44,7 @@ export function useDraggable({
41
44
  }, [containerRef, onMove]);
42
45
 
43
46
  const onMouseDownInner = useCallback((event: MouseEvent) => {
44
- (event as MouseEvent & { innerClicked?: boolean }).innerClicked = true;
47
+ innerClicked.add(event);
45
48
  }, [])
46
49
 
47
50
  const onSelect = useCallback((event: Event) => {
@@ -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(