@rebasepro/admin 0.12.0 → 0.12.1-canary.g009ed95
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 +1 -1
- package/dist/{CollectionEditorDialog-Dpc75wIZ.js → CollectionEditorDialog-B-iVE9y3.js} +75 -62
- package/dist/{CollectionEditorDialog-Dpc75wIZ.js.map → CollectionEditorDialog-B-iVE9y3.js.map} +1 -1
- package/dist/{PropertyEditView-DH3XZC2x.js → PropertyEditView-DoLEH_bQ.js} +36 -16
- package/dist/{PropertyEditView-DH3XZC2x.js.map → PropertyEditView-DoLEH_bQ.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-BZLq4MGp.js → RouterCollectionsStudioView-C3669Ezm.js} +5 -5
- package/dist/{RouterCollectionsStudioView-BZLq4MGp.js.map → RouterCollectionsStudioView-C3669Ezm.js.map} +1 -1
- package/dist/collection_editor/serializable_types.d.ts +4 -1
- package/dist/collection_editor/ui/collection_editor/CollectionRelationsTab.d.ts +1 -0
- package/dist/collection_editor/useLocalCollectionsConfigController.d.ts +8 -0
- package/dist/collection_editor_ui.js +5 -5
- package/dist/collection_editor_ui.js.map +1 -1
- package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +10 -0
- package/dist/components/CollectionViewBinding/SplitViewContext.d.ts +23 -0
- package/dist/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.d.ts +26 -0
- package/dist/components/DetailViewBinding.d.ts +5 -0
- package/dist/components/EditFormActions.d.ts +12 -1
- package/dist/components/EditViewBinding.d.ts +6 -1
- package/dist/components/EntityIdentityBar.d.ts +53 -0
- package/dist/components/EntityInspector.d.ts +29 -0
- package/dist/components/EntityPreviewNesting.d.ts +25 -0
- package/dist/components/EntityViewBinding.d.ts +9 -0
- package/dist/components/InlineEntityPreview.d.ts +47 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/editor.js +18 -10
- package/dist/editor.js.map +1 -1
- package/dist/{export-BzIpUdSK.js → export-B8DIn-u8.js} +2 -2
- package/dist/{export-BzIpUdSK.js.map → export-B8DIn-u8.js.map} +1 -1
- package/dist/form/EntityForm.d.ts +1 -1
- package/dist/form/EntityFormActions.d.ts +11 -1
- package/dist/form/PropertyFieldBinding.d.ts +1 -1
- package/dist/form/components/FieldBlock.d.ts +30 -0
- package/dist/form/components/FormRail.d.ts +18 -0
- package/dist/form/components/FormSections.d.ts +16 -0
- package/dist/form/components/index.d.ts +3 -2
- package/dist/form/components/useRailVisible.d.ts +24 -0
- package/dist/form/field_bindings/BinaryFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/DateTimeFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/GeopointFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MultiSelectFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/ReadOnlyFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/SelectFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/SwitchFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/TextFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/UserSelectFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/VectorFieldBinding.d.ts +1 -1
- package/dist/{history-g9688IaK.js → history-ClwDEtqN.js} +32 -31
- package/dist/history-ClwDEtqN.js.map +1 -0
- package/dist/hooks/navigation/utils.d.ts +16 -0
- package/dist/hooks/useBuildSidePanel.d.ts +14 -0
- package/dist/hooks/useEntityDisplayTitle.d.ts +18 -0
- package/dist/hooks/useRecordActions.d.ts +17 -0
- package/dist/{import-BjUG-cIY.js → import-Cwzf4RW5.js} +2 -2
- package/dist/{import-BjUG-cIY.js.map → import-Cwzf4RW5.js.map} +1 -1
- package/dist/index.js +106 -35
- package/dist/index.js.map +1 -1
- package/dist/markdown-CklalUUk.js.map +1 -1
- package/dist/preview/components/InlineEntityListPreview.d.ts +15 -0
- package/dist/preview/index.d.ts +1 -0
- package/dist/preview/property_previews/ArrayOfReferencesPreview.d.ts +1 -1
- package/dist/preview/property_previews/ArrayOfRelationsPreview.d.ts +1 -1
- package/dist/types/components/EntityFormActionsProps.d.ts +0 -1
- package/dist/types/components/EntityFormProps.d.ts +0 -5
- package/dist/types/fields.d.ts +24 -0
- package/dist/{util-B5fJm1FA.js → util-DSh3HRzK.js} +1974 -1130
- package/dist/util-DSh3HRzK.js.map +1 -0
- package/package.json +32 -34
- package/src/collection_editor/ConfigControllerProvider.tsx +1 -1
- package/src/collection_editor/serializable_types.ts +4 -3
- package/src/collection_editor/serializable_utils.ts +51 -17
- package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +65 -22
- package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +38 -30
- package/src/collection_editor/ui/collection_editor/CollectionRelationsTab.tsx +3 -1
- package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/properties/RelationPropertyField.tsx +16 -0
- package/src/collection_editor/useLocalCollectionsConfigController.tsx +127 -15
- package/src/collection_editor/validateCollectionJson.ts +3 -3
- package/src/components/AdminModeSyncer.tsx +1 -1
- package/src/components/CollectionTableBinding/PropertyTableCell.tsx +10 -2
- package/src/components/CollectionTableBinding/column_utils.tsx +26 -2
- package/src/components/CollectionTableBinding/table_bindings.tsx +2 -1
- package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +23 -16
- package/src/components/CollectionViewBinding/CollectionViewActions.tsx +7 -1
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +8 -2
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +22 -11
- package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +33 -0
- package/src/components/CollectionViewBinding/SplitListView.tsx +18 -3
- package/src/components/CollectionViewBinding/SplitViewContext.tsx +34 -0
- package/src/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.ts +48 -9
- package/src/components/CollectionViewBinding/useBoardDataController.tsx +80 -25
- package/src/components/CollectionViewBinding/usePreviewSlots.ts +18 -6
- package/src/components/DefaultAppBar.tsx +1 -1
- package/src/components/DefaultDrawer.tsx +1 -1
- package/src/components/DetailViewBinding.tsx +100 -143
- package/src/components/DrawerNavigationItem.tsx +1 -1
- package/src/components/EditFormActions.tsx +54 -129
- package/src/components/EditViewBinding.tsx +182 -93
- package/src/components/EntityIdentityBar.tsx +288 -0
- package/src/components/EntityInspector.tsx +132 -0
- package/src/components/EntityPreviewBinding.tsx +6 -2
- package/src/components/EntityPreviewNesting.tsx +50 -0
- package/src/components/EntityViewBinding.tsx +35 -32
- package/src/components/HomePage/FavouritesView.tsx +1 -1
- package/src/components/HomePage/NavigationCardBinding.tsx +1 -1
- package/src/components/HomePage/SmallNavigationCard.tsx +1 -1
- package/src/components/InlineEntityPreview.tsx +149 -0
- package/src/components/RebaseLayout.tsx +1 -1
- package/src/components/RebaseNavigation.tsx +45 -5
- package/src/components/RebaseRouteDefs.tsx +1 -1
- package/src/components/SelectableTable/SelectableTable.tsx +2 -2
- package/src/components/SidePanelBinding.tsx +19 -13
- package/src/components/UserSelector.tsx +12 -6
- package/src/components/common/default_entity_actions.tsx +4 -3
- package/src/components/history/EntityHistoryEntry.tsx +28 -7
- package/src/components/history/EntityHistoryView.tsx +20 -20
- package/src/components/history/LastEditedByIndicator.tsx +4 -3
- package/src/components/index.ts +2 -0
- package/src/data_import/utils/get_import_inference_type.ts +6 -0
- package/src/form/EntityForm.tsx +267 -179
- package/src/form/EntityFormActions.tsx +50 -99
- package/src/form/EntityFormBinding.tsx +4 -2
- package/src/form/PropertyFieldBinding.tsx +4 -0
- package/src/form/components/FieldBlock.tsx +119 -0
- package/src/form/components/FormRail.tsx +125 -0
- package/src/form/components/FormSections.tsx +118 -0
- package/src/form/components/index.tsx +3 -2
- package/src/form/components/useRailVisible.tsx +46 -0
- package/src/form/field_bindings/BinaryFieldBinding.tsx +3 -2
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +3 -2
- package/src/form/field_bindings/GeopointFieldBinding.tsx +3 -2
- package/src/form/field_bindings/MapFieldBinding.tsx +1 -2
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +3 -2
- package/src/form/field_bindings/MultiSelectFieldBinding.tsx +2 -1
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +2 -1
- package/src/form/field_bindings/ReferenceFieldBinding.tsx +2 -1
- package/src/form/field_bindings/RelationFieldBinding.tsx +6 -4
- package/src/form/field_bindings/RepeatFieldBinding.tsx +4 -1
- package/src/form/field_bindings/SelectFieldBinding.tsx +3 -2
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +31 -8
- package/src/form/field_bindings/SwitchFieldBinding.tsx +7 -3
- package/src/form/field_bindings/TextFieldBinding.tsx +18 -16
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +3 -2
- package/src/form/field_bindings/VectorFieldBinding.tsx +2 -1
- package/src/hooks/navigation/useBuildUrlController.tsx +1 -1
- package/src/hooks/navigation/useTopLevelNavigation.ts +18 -7
- package/src/hooks/navigation/utils.ts +20 -0
- package/src/hooks/useBuildSideDialogsController.tsx +2 -2
- package/src/hooks/useBuildSidePanel.tsx +22 -61
- package/src/hooks/useEntityDisplayTitle.tsx +60 -0
- package/src/hooks/useHistory.ts +6 -5
- package/src/hooks/useRecordActions.tsx +69 -0
- package/src/hooks/useResolvedUsers.ts +10 -5
- package/src/preview/PropertyPreview.tsx +48 -22
- package/src/preview/components/InlineEntityListPreview.tsx +42 -0
- package/src/preview/components/ReferencePreview.tsx +34 -14
- package/src/preview/components/RelationPreview.tsx +34 -14
- package/src/preview/index.ts +1 -0
- package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +24 -1
- package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +25 -1
- package/src/routes/RebaseRoute.tsx +2 -2
- package/src/types/components/EntityFormActionsProps.tsx +0 -1
- package/src/types/components/EntityFormProps.tsx +0 -6
- package/src/types/fields.tsx +27 -0
- package/dist/form/components/FormEntry.d.ts +0 -6
- package/dist/form/components/FormLayout.d.ts +0 -5
- package/dist/history-g9688IaK.js.map +0 -1
- package/dist/util-B5fJm1FA.js.map +0 -1
- package/src/form/components/FormEntry.tsx +0 -22
- package/src/form/components/FormLayout.tsx +0 -16
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/admin",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.12.
|
|
4
|
+
"version": "0.12.1-canary.g009ed95",
|
|
5
5
|
"description": "Rebase CMS — content management views, forms, and routing",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
19
19
|
"source": "src/index.ts",
|
|
20
20
|
"engines": {
|
|
21
|
-
"node": ">=
|
|
21
|
+
"node": ">=22.22.0"
|
|
22
22
|
},
|
|
23
23
|
"keywords": [
|
|
24
24
|
"rebase",
|
|
@@ -54,66 +54,64 @@
|
|
|
54
54
|
"@dnd-kit/modifiers": "^9.0.0",
|
|
55
55
|
"@dnd-kit/sortable": "^10.0.0",
|
|
56
56
|
"@dnd-kit/utilities": "^3.2.2",
|
|
57
|
-
"@floating-ui/dom": "^1.
|
|
57
|
+
"@floating-ui/dom": "^1.8.0",
|
|
58
58
|
"cmdk": "^1.1.1",
|
|
59
59
|
"date-fns": "^4.4.0",
|
|
60
60
|
"exceljs": "^4.4.0",
|
|
61
|
-
"fast-equals": "6.0.
|
|
62
|
-
"fractional-indexing": "
|
|
63
|
-
"fuse.js": "^7.
|
|
61
|
+
"fast-equals": "6.0.2",
|
|
62
|
+
"fractional-indexing": "4.0.0",
|
|
63
|
+
"fuse.js": "^7.5.0",
|
|
64
64
|
"json5": "^2.2.3",
|
|
65
|
-
"markdown-it": "^14.
|
|
65
|
+
"markdown-it": "^14.3.0",
|
|
66
66
|
"markdown-it-ins": "^4.0.0",
|
|
67
67
|
"markdown-it-mark": "^4.0.0",
|
|
68
68
|
"markdown-it-task-lists": "^2.1.1",
|
|
69
69
|
"prism-react-renderer": "^2.4.1",
|
|
70
70
|
"prosemirror-commands": "^1.7.1",
|
|
71
|
-
"prosemirror-dropcursor": "^1.8.
|
|
71
|
+
"prosemirror-dropcursor": "^1.8.3",
|
|
72
72
|
"prosemirror-gapcursor": "^1.4.1",
|
|
73
73
|
"prosemirror-history": "^1.5.0",
|
|
74
74
|
"prosemirror-inputrules": "^1.5.1",
|
|
75
75
|
"prosemirror-keymap": "^1.2.3",
|
|
76
|
-
"prosemirror-markdown": "^1.13.
|
|
77
|
-
"prosemirror-model": "^1.25.
|
|
76
|
+
"prosemirror-markdown": "^1.13.5",
|
|
77
|
+
"prosemirror-model": "^1.25.11",
|
|
78
78
|
"prosemirror-schema-list": "^1.5.1",
|
|
79
79
|
"prosemirror-state": "^1.4.4",
|
|
80
80
|
"prosemirror-tables": "^1.8.5",
|
|
81
81
|
"prosemirror-transform": "^1.12.0",
|
|
82
|
-
"prosemirror-view": "^1.
|
|
83
|
-
"react-dropzone": "^
|
|
82
|
+
"prosemirror-view": "^1.42.2",
|
|
83
|
+
"react-dropzone": "^19.1.1",
|
|
84
84
|
"react-use-measure": "^2.1.7",
|
|
85
85
|
"zod": "^4.4.3",
|
|
86
|
-
"@rebasepro/admin-types": "0.12.
|
|
87
|
-
"@rebasepro/app": "0.12.
|
|
88
|
-
"@rebasepro/
|
|
89
|
-
"@rebasepro/forms": "0.12.
|
|
90
|
-
"@rebasepro/types": "0.12.
|
|
91
|
-
"@rebasepro/
|
|
92
|
-
"@rebasepro/
|
|
93
|
-
"@rebasepro/
|
|
86
|
+
"@rebasepro/admin-types": "0.12.1-canary.g009ed95",
|
|
87
|
+
"@rebasepro/app": "0.12.1-canary.g009ed95",
|
|
88
|
+
"@rebasepro/inference": "0.12.1-canary.g009ed95",
|
|
89
|
+
"@rebasepro/forms": "0.12.1-canary.g009ed95",
|
|
90
|
+
"@rebasepro/types": "0.12.1-canary.g009ed95",
|
|
91
|
+
"@rebasepro/ui": "0.12.1-canary.g009ed95",
|
|
92
|
+
"@rebasepro/utils": "0.12.1-canary.g009ed95",
|
|
93
|
+
"@rebasepro/common": "0.12.1-canary.g009ed95"
|
|
94
94
|
},
|
|
95
95
|
"peerDependencies": {
|
|
96
|
-
"react": ">=19.
|
|
97
|
-
"react-dom": ">=19.
|
|
98
|
-
"react-router": "^
|
|
99
|
-
"react-router-dom": "^7.0.0"
|
|
96
|
+
"react": ">=19.2.7",
|
|
97
|
+
"react-dom": ">=19.2.7",
|
|
98
|
+
"react-router": "^8.3.0"
|
|
100
99
|
},
|
|
101
100
|
"devDependencies": {
|
|
102
101
|
"@jest/globals": "^30.4.1",
|
|
103
102
|
"@testing-library/react": "^16.3.2",
|
|
104
103
|
"@types/jest": "^30.0.0",
|
|
105
104
|
"@types/markdown-it": "^14.1.2",
|
|
106
|
-
"@types/node": "^
|
|
105
|
+
"@types/node": "^26.1.2",
|
|
107
106
|
"@types/react": "^19.2.17",
|
|
108
107
|
"@types/react-dom": "^19.2.3",
|
|
109
108
|
"@types/react-measure": "^2.0.12",
|
|
110
|
-
"@vitejs/plugin-react": "^6.0.
|
|
109
|
+
"@vitejs/plugin-react": "^6.0.4",
|
|
111
110
|
"jest": "^30.4.2",
|
|
112
|
-
"react-router": "^
|
|
113
|
-
"
|
|
114
|
-
"ts-jest": "^29.4.11",
|
|
111
|
+
"react-router": "^8.3.0",
|
|
112
|
+
"ts-jest": "^29.4.12",
|
|
115
113
|
"typescript": "^6.0.3",
|
|
116
|
-
"vite": "^8.
|
|
114
|
+
"vite": "^8.1.5"
|
|
117
115
|
},
|
|
118
116
|
"files": [
|
|
119
117
|
"dist",
|
|
@@ -131,13 +129,15 @@
|
|
|
131
129
|
},
|
|
132
130
|
"jest": {
|
|
133
131
|
"transform": {
|
|
134
|
-
"^.+\\.
|
|
132
|
+
"^.+\\.tsx?$": "ts-jest",
|
|
133
|
+
"^.+\\.m?jsx?$": "<rootDir>/../../scripts/jest/react-router-esm-transform.cjs"
|
|
135
134
|
},
|
|
136
135
|
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
|
|
137
136
|
"moduleFileExtensions": [
|
|
138
137
|
"ts",
|
|
139
138
|
"tsx",
|
|
140
139
|
"js",
|
|
140
|
+
"mjs",
|
|
141
141
|
"jsx",
|
|
142
142
|
"json",
|
|
143
143
|
"node"
|
|
@@ -146,9 +146,7 @@
|
|
|
146
146
|
"testPathIgnorePatterns": [
|
|
147
147
|
"/node_modules/"
|
|
148
148
|
],
|
|
149
|
-
"transformIgnorePatterns": [
|
|
150
|
-
"node_modules/(?!.*fractional-indexing)/"
|
|
151
|
-
],
|
|
149
|
+
"transformIgnorePatterns": [],
|
|
152
150
|
"setupFilesAfterEnv": [
|
|
153
151
|
"<rootDir>/test/setupTests.ts"
|
|
154
152
|
],
|
|
@@ -6,7 +6,7 @@ import { deepEqual as equal } from "fast-equals";
|
|
|
6
6
|
import { CollectionsConfigController } from "./types/config_controller";
|
|
7
7
|
import { useCustomizationController, useRebaseContext, useAuthController, useSnackbarController } from "@rebasepro/app";
|
|
8
8
|
import { getTableName } from "@rebasepro/common";
|
|
9
|
-
import { useNavigate } from "react-router
|
|
9
|
+
import { useNavigate } from "react-router";
|
|
10
10
|
import { CollectionEditorController } from "./types/collection_editor_controller";
|
|
11
11
|
import { CollectionInference } from "./types/collection_inference";
|
|
12
12
|
import { CollectionGenerationCallback } from "./api/generateCollectionApi";
|
|
@@ -46,7 +46,8 @@ export interface SerializableAdminBaseOptions {
|
|
|
46
46
|
hideFromCollection?: boolean;
|
|
47
47
|
readOnly?: boolean;
|
|
48
48
|
disabled?: boolean | SerializablePropertyDisabledConfig;
|
|
49
|
-
|
|
49
|
+
span?: 1 | 2 | 3 | 4;
|
|
50
|
+
/** Opaque props handed to a custom `Field` / `Preview`. Round-tripped as-is. */
|
|
50
51
|
customProps?: unknown;
|
|
51
52
|
/** Narrows the filter operators offered for this property. Plain data. */
|
|
52
53
|
filterOperators?: readonly WhereFilterOp[];
|
|
@@ -291,8 +292,8 @@ export interface SerializableReferenceProperty extends SerializableBaseProperty
|
|
|
291
292
|
*/
|
|
292
293
|
export type SerializableRelation =
|
|
293
294
|
| { kind: "belongsTo"; target?: string; relationName?: string; localKey?: string; onUpdate?: OnAction; onDelete?: OnAction }
|
|
294
|
-
| { kind: "hasOne"; target?: string; relationName?: string; foreignKeyOnTarget?: string; onUpdate?: OnAction; onDelete?: OnAction }
|
|
295
|
-
| { kind: "hasMany"; target?: string; relationName?: string; foreignKeyOnTarget?: string; onUpdate?: OnAction; onDelete?: OnAction }
|
|
295
|
+
| { kind: "hasOne"; target?: string; relationName?: string; foreignKeyOnTarget?: string; sourceKey?: string; onUpdate?: OnAction; onDelete?: OnAction }
|
|
296
|
+
| { kind: "hasMany"; target?: string; relationName?: string; foreignKeyOnTarget?: string; sourceKey?: string; onUpdate?: OnAction; onDelete?: OnAction }
|
|
296
297
|
| {
|
|
297
298
|
kind: "manyToMany";
|
|
298
299
|
target?: string;
|
|
@@ -44,6 +44,41 @@ import type {
|
|
|
44
44
|
*/
|
|
45
45
|
export type CollectionLookup = (slug: string) => AdminCollection | undefined;
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Rebuild a serialized relation's `target` slug into the thunk consumers call.
|
|
49
|
+
*
|
|
50
|
+
* Shared by the relation *property* and the collection-level `relations` array.
|
|
51
|
+
* They are the same shape on the wire and they need the same treatment coming
|
|
52
|
+
* back, but only the property path had it — so a collection imported from an
|
|
53
|
+
* existing table came off disk with `relations[n].target` still a string, which
|
|
54
|
+
* typechecks (the serializable shape erases the difference) and throws "target
|
|
55
|
+
* is not a function" at the first consumer.
|
|
56
|
+
*
|
|
57
|
+
* The lookup is consulted lazily, inside the thunk, so collections may
|
|
58
|
+
* reference each other in any order, including circularly.
|
|
59
|
+
*/
|
|
60
|
+
function fromSerializableRelation<R extends { target?: string; relationName?: string }>(
|
|
61
|
+
relation: R,
|
|
62
|
+
label: string,
|
|
63
|
+
lookup?: CollectionLookup
|
|
64
|
+
): R & { target: () => AdminCollection } {
|
|
65
|
+
const slug = relation.target;
|
|
66
|
+
return {
|
|
67
|
+
...relation,
|
|
68
|
+
target: () => {
|
|
69
|
+
const found = slug ? lookup?.(slug) : undefined;
|
|
70
|
+
if (!found) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Relation "${relation.relationName ?? label}" targets collection "${slug ?? "(none)"}", ` +
|
|
73
|
+
"which is not among the collections it was deserialized with. Pass the whole set to " +
|
|
74
|
+
"`fromSerializableCollectionConfigs` so slugs can be resolved against each other."
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return found;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
47
82
|
// ═══════════════════════════════════════════════════════════════════════
|
|
48
83
|
// PROPERTY CONVERSION: Original → Serializable
|
|
49
84
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -160,7 +195,8 @@ function toSerializableRelation(link: Relation): SerializableRelation {
|
|
|
160
195
|
return {
|
|
161
196
|
kind: link.kind,
|
|
162
197
|
...common,
|
|
163
|
-
...(link.foreignKeyOnTarget ? { foreignKeyOnTarget: link.foreignKeyOnTarget } : {})
|
|
198
|
+
...(link.foreignKeyOnTarget ? { foreignKeyOnTarget: link.foreignKeyOnTarget } : {}),
|
|
199
|
+
...(link.sourceKey ? { sourceKey: link.sourceKey } : {})
|
|
164
200
|
};
|
|
165
201
|
case "manyToMany":
|
|
166
202
|
return { kind: "manyToMany", ...common, ...(link.through ? { through: link.through } : {}) };
|
|
@@ -600,21 +636,11 @@ type: "date" };
|
|
|
600
636
|
const result = { ...rest,
|
|
601
637
|
type: "relation" } as unknown as RelationProperty;
|
|
602
638
|
if (relation) {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
if (!found) {
|
|
609
|
-
throw new Error(
|
|
610
|
-
`Relation "${relation.relationName ?? sp.name}" targets collection "${slug ?? "(none)"}", ` +
|
|
611
|
-
"which is not among the collections it was deserialized with. Pass the whole set to " +
|
|
612
|
-
"`fromSerializableCollectionConfigs` so slugs can be resolved against each other."
|
|
613
|
-
);
|
|
614
|
-
}
|
|
615
|
-
return found;
|
|
616
|
-
}
|
|
617
|
-
} as unknown as RelationProperty["relation"];
|
|
639
|
+
result.relation = fromSerializableRelation(
|
|
640
|
+
relation,
|
|
641
|
+
sp.name ?? "",
|
|
642
|
+
lookup
|
|
643
|
+
) as unknown as RelationProperty["relation"];
|
|
618
644
|
}
|
|
619
645
|
return result as Property;
|
|
620
646
|
}
|
|
@@ -654,10 +680,18 @@ export function fromSerializableCollectionConfig(
|
|
|
654
680
|
serialized: SerializableCollectionConfig,
|
|
655
681
|
lookup?: CollectionLookup
|
|
656
682
|
): AdminCollection {
|
|
657
|
-
const { properties, ...rest } = serialized
|
|
683
|
+
const { properties, relations, ...rest } = serialized as SerializableCollectionConfig & {
|
|
684
|
+
relations?: SerializableRelation[]
|
|
685
|
+
};
|
|
658
686
|
|
|
659
687
|
return {
|
|
660
688
|
...rest,
|
|
689
|
+
...(relations
|
|
690
|
+
? {
|
|
691
|
+
relations: relations.map((relation, index) =>
|
|
692
|
+
fromSerializableRelation(relation, `relations[${index}]`, lookup))
|
|
693
|
+
}
|
|
694
|
+
: {}),
|
|
661
695
|
properties: fromSerializableProperties(properties, lookup)
|
|
662
696
|
} as AdminCollection;
|
|
663
697
|
}
|
|
@@ -30,7 +30,8 @@ import {
|
|
|
30
30
|
IconButton,
|
|
31
31
|
LoadingButton,
|
|
32
32
|
Tab,
|
|
33
|
-
Tabs
|
|
33
|
+
Tabs,
|
|
34
|
+
Tooltip
|
|
34
35
|
} from "@rebasepro/ui";
|
|
35
36
|
import { EngineProperties, Entity, getDataSourceCapabilities, MapProperty, Properties, Property, TableMetadata, User } from "@rebasepro/types";
|
|
36
37
|
import { PropertyConfig, AdminCollection } from "@rebasepro/admin-types";
|
|
@@ -254,11 +255,38 @@ export function CollectionEditor(props: CollectionEditorDialogProps & {
|
|
|
254
255
|
? applyPropertyConfigs(initialCollection, propertyConfigs)
|
|
255
256
|
: copyFromProp
|
|
256
257
|
? (() => {
|
|
257
|
-
// When duplicating, copy all properties but clear identifiers
|
|
258
|
-
|
|
258
|
+
// When duplicating, copy all properties but clear identifiers.
|
|
259
|
+
//
|
|
260
|
+
// "Identifiers" has to include the ones this dialog cannot
|
|
261
|
+
// show. `schema` has no field anywhere in the editor, so a
|
|
262
|
+
// duplicate of the Users collection — the one collection
|
|
263
|
+
// scaffolded with `schema: "rebase"` — silently produced a
|
|
264
|
+
// new collection whose table is created in `rebase` rather
|
|
265
|
+
// than `public`, with nothing on screen to explain why.
|
|
266
|
+
// Panel-authored collections are `public` collections.
|
|
267
|
+
//
|
|
268
|
+
// `slug`/`table` are cleared rather than carried: both are
|
|
269
|
+
// re-derived from the name the user types (see
|
|
270
|
+
// `updateName` in GeneralSettingsForm, which fills untouched
|
|
271
|
+
// fields while `isNewCollection`), and carrying them meant a
|
|
272
|
+
// duplicate pointed at the source's table until then.
|
|
273
|
+
//
|
|
274
|
+
// `auth` goes too — a duplicate must not become a second
|
|
275
|
+
// auth collection wired to the same user store.
|
|
276
|
+
const {
|
|
277
|
+
subcollections: _sub,
|
|
278
|
+
schema: _schema,
|
|
279
|
+
auth: _auth,
|
|
280
|
+
...rest
|
|
281
|
+
} = copyFromProp as unknown as Record<string, unknown>;
|
|
259
282
|
return {
|
|
260
283
|
...rest,
|
|
261
284
|
name: "",
|
|
285
|
+
// Same shape the "new collection" branch below starts
|
|
286
|
+
// from, so validation and the registry lookups never see
|
|
287
|
+
// an undefined slug mid-edit.
|
|
288
|
+
slug: fallbackSlug,
|
|
289
|
+
table: "",
|
|
262
290
|
ownerId: authController.user?.uid ?? ""
|
|
263
291
|
} as AdminCollection<any>;
|
|
264
292
|
})()
|
|
@@ -341,6 +369,10 @@ function CollectionEditorInternal<M extends Record<string, unknown>>({
|
|
|
341
369
|
}
|
|
342
370
|
) {
|
|
343
371
|
|
|
372
|
+
/** Why the submit buttons below are dead, straight from the backend. */
|
|
373
|
+
const readOnlyTitle = configController?.readOnlyReason
|
|
374
|
+
|| "This backend does not accept collection edits.";
|
|
375
|
+
|
|
344
376
|
const importConfigRaw = useImportConfig();
|
|
345
377
|
const urlControllerRaw = useUrlController();
|
|
346
378
|
const collectionRegistryRaw = useCollectionRegistryController();
|
|
@@ -720,16 +752,23 @@ function CollectionEditorInternal<M extends Record<string, unknown>>({
|
|
|
720
752
|
)}
|
|
721
753
|
{fullScreen && !isNewCollection && (
|
|
722
754
|
<div className="flex items-center gap-2">
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
755
|
+
{/* "(Read-only)" says the button will not work but not why.
|
|
756
|
+
The controller now carries the backend's own reason —
|
|
757
|
+
production, `baas` mode, no `collectionsDir` — so show it. */}
|
|
758
|
+
<Tooltip title={configController?.readOnly ? readOnlyTitle : undefined}>
|
|
759
|
+
<div>
|
|
760
|
+
<LoadingButton
|
|
761
|
+
variant="filled"
|
|
762
|
+
color="primary"
|
|
763
|
+
type="submit"
|
|
764
|
+
onClick={() => formController.handleSubmit()}
|
|
765
|
+
disabled={!dirty || isSubmitting || configController?.readOnly}
|
|
766
|
+
loading={isSubmitting}
|
|
767
|
+
>
|
|
768
|
+
{configController?.readOnly ? "Update (Read-only)" : "Update"}
|
|
769
|
+
</LoadingButton>
|
|
770
|
+
</div>
|
|
771
|
+
</Tooltip>
|
|
733
772
|
</div>
|
|
734
773
|
)}
|
|
735
774
|
</div>
|
|
@@ -935,15 +974,19 @@ function CollectionEditorInternal<M extends Record<string, unknown>>({
|
|
|
935
974
|
{currentView === "properties" && "Create collection"}
|
|
936
975
|
</LoadingButton>}
|
|
937
976
|
|
|
938
|
-
{!isNewCollection && !fullScreen && <
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
977
|
+
{!isNewCollection && !fullScreen && <Tooltip title={configController?.readOnly ? readOnlyTitle : undefined}>
|
|
978
|
+
<div>
|
|
979
|
+
<LoadingButton
|
|
980
|
+
variant="filled"
|
|
981
|
+
color="primary"
|
|
982
|
+
type="submit"
|
|
983
|
+
disabled={isSubmitting || configController?.readOnly}
|
|
984
|
+
loading={isSubmitting}
|
|
985
|
+
>
|
|
986
|
+
{configController?.readOnly ? "Update collection (Read-only)" : "Update collection"}
|
|
987
|
+
</LoadingButton>
|
|
988
|
+
</div>
|
|
989
|
+
</Tooltip>}
|
|
947
990
|
|
|
948
991
|
</div>
|
|
949
992
|
)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import { getPolicyNamesForRule, getPolicyNamesForRules } from "@rebasepro/utils";
|
|
3
|
-
import {
|
|
3
|
+
import { getGeneratedPolicyNames } from "@rebasepro/common";
|
|
4
4
|
import React, { useState, useEffect, useMemo } from "react";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
Typography
|
|
40
40
|
} from "@rebasepro/ui";
|
|
41
41
|
import { useFormex } from "@rebasepro/forms";
|
|
42
|
+
import { useCollectionsConfigController } from "../../useCollectionsConfigController";
|
|
42
43
|
import { useRebaseContext } from "@rebasepro/app";
|
|
43
44
|
import type { AdminCollection } from "@rebasepro/admin-types";
|
|
44
45
|
import type { PostgresPolicy, SecurityOperation, SecurityRule } from "@rebasepro/types";
|
|
@@ -114,6 +115,14 @@ export function CollectionRLSTab() {
|
|
|
114
115
|
const { values, setFieldValue } = useFormex<CollectionWithSecurity>();
|
|
115
116
|
const [editingPolicy, setEditingPolicy] = useState<PostgresPolicy | "new" | null>(null);
|
|
116
117
|
|
|
118
|
+
// Every other tab in this dialog disables its inputs when the backend will
|
|
119
|
+
// not accept an edit — the general and display forms behind a `fieldset
|
|
120
|
+
// disabled`, the properties editor per control. This one did not, so a
|
|
121
|
+
// read-only panel still invited you to write a policy, delete one, and
|
|
122
|
+
// reorder the lot, and only said no at the submit button on the way out.
|
|
123
|
+
const { readOnly, readOnlyReason } = useCollectionsConfigController();
|
|
124
|
+
const readOnlyTitle = readOnlyReason || "This backend does not accept collection edits.";
|
|
125
|
+
|
|
117
126
|
const rules: SecurityRule[] = values.securityRules || [];
|
|
118
127
|
|
|
119
128
|
const { databaseAdmin } = useRebaseContext();
|
|
@@ -209,26 +218,14 @@ export function CollectionRLSTab() {
|
|
|
209
218
|
|
|
210
219
|
const tableName = values.id || values.table || values.alias;
|
|
211
220
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
* - a rule without an explicit `name` becomes `<table>_<op>_<hash>`, so
|
|
217
|
-
* comparing `rule.name` to `policyname` never matches for it;
|
|
218
|
-
* - the generator also injects the safe-by-default baseline
|
|
219
|
-
* (`<table>_default_admin_*`), which is in no collection's `securityRules`.
|
|
220
|
-
*
|
|
221
|
-
* Missing either made Rebase's own policies look hand-written, and offered to
|
|
222
|
-
* import them back into the codebase that produced them.
|
|
223
|
-
*/
|
|
221
|
+
// Every policy name `rebase db push` would write for this collection — see
|
|
222
|
+
// `getGeneratedPolicyNames`. This used to be derived here by hand, and the
|
|
223
|
+
// Studio's RLS editor derived a different subset of it by hand, which is
|
|
224
|
+
// exactly how the two came to disagree about which policies were drift.
|
|
224
225
|
const generatedPolicyNames = useMemo(() => {
|
|
225
226
|
if (!tableName) return new Set<string>();
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
[...rules, ...effectiveRules],
|
|
229
|
-
tableName
|
|
230
|
-
);
|
|
231
|
-
}, [rules, tableName, values]);
|
|
227
|
+
return getGeneratedPolicyNames(values as AdminCollection);
|
|
228
|
+
}, [tableName, values]);
|
|
232
229
|
|
|
233
230
|
const unmappedPolicies = dbPolicies.filter(dp => !generatedPolicyNames.has(dp.policyname));
|
|
234
231
|
|
|
@@ -251,9 +248,13 @@ export function CollectionRLSTab() {
|
|
|
251
248
|
<div className="w-full flex flex-col">
|
|
252
249
|
<div className="flex items-center justify-between mb-8">
|
|
253
250
|
<Typography variant="h5">Row Level Security</Typography>
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
|
|
251
|
+
<Tooltip title={readOnly ? readOnlyTitle : undefined}>
|
|
252
|
+
<div>
|
|
253
|
+
<Button variant="filled" color="neutral" disabled={readOnly} onClick={() => setEditingPolicy("new")}>
|
|
254
|
+
CREATE POLICY
|
|
255
|
+
</Button>
|
|
256
|
+
</div>
|
|
257
|
+
</Tooltip>
|
|
257
258
|
</div>
|
|
258
259
|
|
|
259
260
|
{rules.length === 0 ? (
|
|
@@ -282,7 +283,7 @@ export function CollectionRLSTab() {
|
|
|
282
283
|
</div>
|
|
283
284
|
</div>
|
|
284
285
|
<div className="flex items-center gap-1 sm:gap-2 shrink-0">
|
|
285
|
-
<Button size="small" variant="text" onClick={() => setEditingPolicy({
|
|
286
|
+
<Button size="small" variant="text" disabled={readOnly} onClick={() => setEditingPolicy({
|
|
286
287
|
policyname: rule.name ?? "",
|
|
287
288
|
tablename: values.id || values.table || values.alias || "your_table",
|
|
288
289
|
permissive: (rule.mode || "permissive").toUpperCase() as PostgresPolicy["permissive"],
|
|
@@ -293,7 +294,7 @@ export function CollectionRLSTab() {
|
|
|
293
294
|
})}>
|
|
294
295
|
EDIT
|
|
295
296
|
</Button>
|
|
296
|
-
<IconButton size="small" onClick={() => {
|
|
297
|
+
<IconButton size="small" disabled={readOnly} onClick={() => {
|
|
297
298
|
setFieldValue("securityRules", rules.filter((r: SecurityRule) => r.name !== rule.name));
|
|
298
299
|
}}>
|
|
299
300
|
<Trash2Icon size={iconSize.smallest} className="text-text-secondary dark:text-text-secondary-dark hover:text-red-500 dark:hover:text-red-500 transition-colors"/>
|
|
@@ -336,12 +337,19 @@ export function CollectionRLSTab() {
|
|
|
336
337
|
</div>
|
|
337
338
|
</div>
|
|
338
339
|
<div className="flex items-center gap-1 sm:gap-2 shrink-0">
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
340
|
+
{/* "Import to codebase" is the one action here that
|
|
341
|
+
names its destination, and it is the one the
|
|
342
|
+
backend refuses when it has no codebase to write. */}
|
|
343
|
+
<Tooltip title={readOnly ? readOnlyTitle : undefined}>
|
|
344
|
+
<div>
|
|
345
|
+
<Button size="small" variant="outlined" color="primary" disabled={readOnly} onClick={() => {
|
|
346
|
+
const rule: SecurityRule = toSecurityRule(dp);
|
|
347
|
+
setFieldValue("securityRules", [...rules, rule]);
|
|
348
|
+
}}>
|
|
349
|
+
Import to codebase
|
|
350
|
+
</Button>
|
|
351
|
+
</div>
|
|
352
|
+
</Tooltip>
|
|
345
353
|
</div>
|
|
346
354
|
</Paper>
|
|
347
355
|
))}
|
|
@@ -41,6 +41,7 @@ export type RelationDraft = {
|
|
|
41
41
|
target?: string;
|
|
42
42
|
localKey?: string;
|
|
43
43
|
foreignKeyOnTarget?: string;
|
|
44
|
+
sourceKey?: string;
|
|
44
45
|
through?: { table?: string; sourceColumn?: string; targetColumn?: string };
|
|
45
46
|
joinPath?: JoinStep[];
|
|
46
47
|
cardinality?: "one" | "many";
|
|
@@ -119,7 +120,8 @@ kind: "belongsTo",
|
|
|
119
120
|
return {
|
|
120
121
|
...common,
|
|
121
122
|
kind: draft.kind,
|
|
122
|
-
...(draft.foreignKeyOnTarget ? { foreignKeyOnTarget: draft.foreignKeyOnTarget } : {})
|
|
123
|
+
...(draft.foreignKeyOnTarget ? { foreignKeyOnTarget: draft.foreignKeyOnTarget } : {}),
|
|
124
|
+
...(draft.sourceKey ? { sourceKey: draft.sourceKey } : {})
|
|
123
125
|
};
|
|
124
126
|
case "manyToMany":
|
|
125
127
|
return { ...common,
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* (which has no react-router dependency).
|
|
11
11
|
*/
|
|
12
12
|
import React from "react";
|
|
13
|
-
import { useNavigate } from "react-router
|
|
13
|
+
import { useNavigate } from "react-router";
|
|
14
14
|
import { useUrlController } from "../../_cms_internals";
|
|
15
15
|
import { useSnackbarController } from "@rebasepro/app";
|
|
16
16
|
import { CollectionStudioView, CollectionStudioViewProps } from "./CollectionStudioView";
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* (which has no react-router dependency).
|
|
10
10
|
*/
|
|
11
11
|
import React from "react";
|
|
12
|
-
import { useLocation, useNavigate } from "react-router
|
|
12
|
+
import { useLocation, useNavigate } from "react-router";
|
|
13
13
|
import { useUrlController } from "../../_cms_internals";
|
|
14
14
|
import { CollectionsStudioView, CollectionsStudioViewProps } from "./CollectionsStudioView";
|
|
15
15
|
|
|
@@ -67,6 +67,7 @@ export function RelationPropertyField({
|
|
|
67
67
|
const targetSlug = getTargetSlug(link.target);
|
|
68
68
|
const localKey = link.localKey ?? "";
|
|
69
69
|
const foreignKeyOnTarget = link.foreignKeyOnTarget ?? "";
|
|
70
|
+
const sourceKey = link.sourceKey ?? "";
|
|
70
71
|
const through = link.through;
|
|
71
72
|
const throughTable = through?.table ?? "";
|
|
72
73
|
const throughSourceColumn = through?.sourceColumn ?? "";
|
|
@@ -200,6 +201,21 @@ targetColumn: "" };
|
|
|
200
201
|
<FieldCaption>
|
|
201
202
|
Column on the target table that references this table's primary key
|
|
202
203
|
</FieldCaption>
|
|
204
|
+
<TextField
|
|
205
|
+
className={"mt-4"}
|
|
206
|
+
value={sourceKey}
|
|
207
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
|
|
208
|
+
setFieldValue("relation.sourceKey", e.target.value)
|
|
209
|
+
}
|
|
210
|
+
label={"Source key (optional)"}
|
|
211
|
+
disabled={disabled}
|
|
212
|
+
placeholder={"defaults to this table's primary key"}
|
|
213
|
+
/>
|
|
214
|
+
<FieldCaption>
|
|
215
|
+
Column on <em>this</em> table that the foreign key above points at. Leave empty
|
|
216
|
+
unless the two sides are joined on a natural key — an external identity id, a
|
|
217
|
+
SKU — rather than on the row id. It must be unique.
|
|
218
|
+
</FieldCaption>
|
|
203
219
|
</div>
|
|
204
220
|
)}
|
|
205
221
|
|