@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.
- package/README.md +135 -0
- package/dist/{CollectionEditorDialog-D0VqpLPO.js → CollectionEditorDialog-C8E440oK.js} +22 -5
- package/dist/CollectionEditorDialog-C8E440oK.js.map +1 -0
- package/dist/{CollectionsStudioView-Bc3Rxxc2.js → CollectionsStudioView-Brr_b06v.js} +4 -4
- package/dist/{CollectionsStudioView-Bc3Rxxc2.js.map → CollectionsStudioView-Brr_b06v.js.map} +1 -1
- package/dist/{ExportCollectionAction-Ckc-09BQ.js → ExportCollectionAction-D3TTYXrq.js} +35 -27
- package/dist/ExportCollectionAction-D3TTYXrq.js.map +1 -0
- package/dist/{ImportCollectionAction-BqjIrC3Z.js → ImportCollectionAction-Dn7lya6E.js} +2 -2
- package/dist/{ImportCollectionAction-BqjIrC3Z.js.map → ImportCollectionAction-Dn7lya6E.js.map} +1 -1
- package/dist/{PropertyEditView-CvRSV-A2.js → PropertyEditView-ChBleIbN.js} +4 -4
- package/dist/PropertyEditView-ChBleIbN.js.map +1 -0
- package/dist/collection_editor/ui/collection_editor/PropertyEditView.d.ts +1 -0
- package/dist/collection_editor/ui/collection_editor/templates/blog_template.d.ts +1 -1
- package/dist/collection_editor/ui/collection_editor/templates/pages_template.d.ts +1 -1
- package/dist/collection_editor/ui/collection_editor/templates/products_template.d.ts +1 -1
- package/dist/collection_editor/ui/collection_editor/templates/users_template.d.ts +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/RebaseRouteDefs.d.ts +1 -1
- 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/admin/index.d.ts +1 -3
- package/dist/components/index.d.ts +1 -0
- package/dist/editor.js +15 -5
- package/dist/editor.js.map +1 -1
- package/dist/hooks/navigation/useBuildNavigationStateController.d.ts +1 -1
- package/dist/hooks/navigation/useResolvedViews.d.ts +2 -5
- package/dist/{index-BCcLwgfe.js → index-D3S9x_RP.js} +2 -2
- package/dist/{index-BCcLwgfe.js.map → index-D3S9x_RP.js.map} +1 -1
- package/dist/{index-DY2k5TtG.js → index-D9RNzm01.js} +3 -3
- package/dist/index-D9RNzm01.js.map +1 -0
- package/dist/{index-UQOMHwt1.js → index-DrrT332R.js} +3 -3
- package/dist/index-DrrT332R.js.map +1 -0
- package/dist/index.d.ts +2 -4
- package/dist/index.js +228 -295
- 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-ZM9gQuCv.js → util-COYYD8zr.js} +473 -880
- package/dist/util-COYYD8zr.js.map +1 -0
- package/package.json +9 -10
- package/src/collection_editor/pgColumnToProperty.ts +19 -2
- package/src/collection_editor/ui/collection_editor/CollectionRelationsTab.tsx +2 -2
- package/src/collection_editor/ui/collection_editor/PropertyEditView.tsx +2 -1
- package/src/collection_editor/ui/collection_editor/PropertyFieldPreview.tsx +2 -2
- package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +2 -1
- package/src/collection_editor/ui/collection_editor/properties/conditions/EnumConditionsEditor.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/templates/blog_template.ts +1 -1
- package/src/collection_editor/ui/collection_editor/templates/pages_template.ts +1 -1
- package/src/collection_editor/ui/collection_editor/templates/products_template.ts +1 -1
- package/src/collection_editor/ui/collection_editor/templates/users_template.ts +1 -1
- package/src/collection_editor/validateCollectionJson.ts +12 -12
- package/src/components/CollectionPanel.tsx +155 -0
- package/src/components/DefaultDrawer.tsx +2 -2
- package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +5 -2
- package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +64 -34
- package/src/components/EntityCollectionView/EntityCollectionListView.tsx +8 -1
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +4 -1
- package/src/components/EntityCollectionView/hooks/useCollectionInlineEditor.ts +1 -0
- package/src/components/EntityCollectionView/useEntityPreviewSlots.ts +68 -18
- package/src/components/EntityDetailView.tsx +32 -36
- package/src/components/EntityEditView.tsx +101 -50
- package/src/components/EntityEditViewFormActions.tsx +4 -4
- package/src/components/EntitySidePanel.tsx +50 -14
- package/src/components/PropertyCollectionView.tsx +1 -0
- package/src/components/RebaseRouteDefs.tsx +4 -6
- 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/admin/index.ts +1 -3
- package/src/components/index.ts +3 -3
- 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/PropertyFieldBinding.tsx +1 -1
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +1 -1
- package/src/form/field_bindings/MapFieldBinding.tsx +3 -45
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +3 -1
- package/src/hooks/navigation/useBuildNavigationStateController.tsx +2 -3
- package/src/hooks/navigation/useResolvedCollections.ts +2 -10
- package/src/hooks/navigation/useResolvedViews.tsx +6 -48
- package/src/hooks/useBuildSideEntityController.tsx +20 -3
- package/src/index.ts +6 -5
- package/src/util/entity_view_constants.ts +6 -0
- package/src/util/previews.ts +9 -1
- package/src/util/resolutions.ts +2 -6
- package/dist/CollectionEditorDialog-D0VqpLPO.js.map +0 -1
- package/dist/ExportCollectionAction-Ckc-09BQ.js.map +0 -1
- package/dist/PropertyEditView-CvRSV-A2.js.map +0 -1
- package/dist/components/admin/RoleChip.d.ts +0 -4
- package/dist/components/admin/RolesFilterSelect.d.ts +0 -2
- package/dist/components/admin/RolesView.d.ts +0 -4
- package/dist/components/admin/UserRolesSelectField.d.ts +0 -2
- package/dist/components/admin/UsersView.d.ts +0 -4
- package/dist/index-DY2k5TtG.js.map +0 -1
- package/dist/index-UQOMHwt1.js.map +0 -1
- package/dist/util-ZM9gQuCv.js.map +0 -1
- package/src/components/admin/RoleChip.tsx +0 -23
- package/src/components/admin/RolesFilterSelect.tsx +0 -45
- package/src/components/admin/RolesView.tsx +0 -470
- package/src/components/admin/UserRolesSelectField.tsx +0 -50
- 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.
|
|
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
|
-
"
|
|
83
|
+
"zod": "^3.25.76",
|
|
84
84
|
"react-dropzone": "^14.4.1",
|
|
85
85
|
"react-use-measure": "^2.1.7",
|
|
86
|
-
"
|
|
87
|
-
"@rebasepro/
|
|
88
|
-
"@rebasepro/
|
|
89
|
-
"@rebasepro/
|
|
90
|
-
"@rebasepro/
|
|
91
|
-
"@rebasepro/types": "0.
|
|
92
|
-
"@rebasepro/
|
|
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
|
-
|
|
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: ""
|
|
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: ""
|
|
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
|
-
{
|
|
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
|
-
{
|
|
149
|
+
{"columnType" in property ? (property as { columnType?: string }).columnType ?? property.type : property.type}
|
|
150
150
|
</Typography>
|
|
151
151
|
</ErrorBoundary>}
|
|
152
152
|
</Paper>
|
package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsEditor.tsx
CHANGED
|
@@ -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:
|
|
821
|
+
const conditions: PropertyConditions = values.conditions ?? {};
|
|
821
822
|
|
|
822
823
|
const activeConditions: { type: ConditionType; group: ConditionGroup }[] = [];
|
|
823
824
|
|
package/src/collection_editor/ui/collection_editor/properties/conditions/EnumConditionsEditor.tsx
CHANGED
|
@@ -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 =
|
|
531
|
+
const conditions = values.conditions ?? {};
|
|
532
532
|
|
|
533
533
|
// Parse existing enum conditions
|
|
534
534
|
const activeConditions: { type: EnumConditionType; config: EnumConditionConfig }[] = [];
|
|
@@ -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
|
+
}
|
|
@@ -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-
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
147
|
+
const onScrollEvent = () => {
|
|
148
|
+
if (rafId === null) rafId = requestAnimationFrame(update);
|
|
149
|
+
};
|
|
130
150
|
|
|
131
|
-
|
|
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
|
-
|
|
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 && (
|
|
@@ -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=
|
|
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(
|