@rebasepro/admin 0.11.1-canary.gfd39654 → 0.12.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 +1 -1
- package/dist/{CollectionEditorDialog-o7F_zbGY.js → CollectionEditorDialog-Dpc75wIZ.js} +56 -260
- package/dist/CollectionEditorDialog-Dpc75wIZ.js.map +1 -0
- package/dist/{PropertyEditView-DgB2b22q.js → PropertyEditView-DH3XZC2x.js} +2 -2
- package/dist/{PropertyEditView-DgB2b22q.js.map → PropertyEditView-DH3XZC2x.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-DaLRgPkj.js → RouterCollectionsStudioView-BZLq4MGp.js} +4 -4
- package/dist/{RouterCollectionsStudioView-DaLRgPkj.js.map → RouterCollectionsStudioView-BZLq4MGp.js.map} +1 -1
- package/dist/collection_editor/CollectionEditorDialogsContext.d.ts +1 -1
- package/dist/collection_editor/_cms_internals.d.ts +1 -1
- package/dist/collection_editor/extensibility_types.d.ts +1 -1
- package/dist/collection_editor/serializable_types.d.ts +40 -12
- package/dist/collection_editor/serializable_utils.d.ts +1 -1
- package/dist/collection_editor/ui/collection_editor/CollectionRLSTab.d.ts +1 -11
- package/dist/collection_editor_ui.js +4 -4
- package/dist/components/CollectionEditorDialogs.d.ts +2 -2
- package/dist/components/CollectionTableBinding/column_utils.d.ts +7 -1
- package/dist/components/CollectionViewBinding/CollectionListViewBinding.d.ts +2 -2
- package/dist/components/DefaultDrawer.d.ts +3 -3
- package/dist/components/DrawerNavigationGroup.d.ts +8 -1
- package/dist/components/DrawerNavigationItem.d.ts +7 -1
- package/dist/components/EditViewBinding.d.ts +1 -1
- package/dist/components/RebaseAdmin.d.ts +1 -1
- package/dist/components/RebaseAuthGate.d.ts +1 -1
- package/dist/components/RebaseLayout.d.ts +5 -1
- package/dist/components/RebaseNavigation.d.ts +2 -2
- package/dist/components/RebaseRouteDefs.d.ts +1 -1
- package/dist/components/RebaseShell.d.ts +6 -1
- package/dist/components/RelationSelector.d.ts +14 -0
- package/dist/components/SelectableTable/filters/RelationFilterField.d.ts +27 -0
- package/dist/components/SelectableTable/filters/null_filter.d.ts +46 -0
- package/dist/components/SidePanelProvider.d.ts +2 -2
- package/dist/components/app/Scaffold.d.ts +11 -0
- package/dist/components/field_configs.d.ts +1 -1
- package/dist/{export-BvqhjdEb.js → export-BzIpUdSK.js} +3 -3
- package/dist/export-BzIpUdSK.js.map +1 -0
- package/dist/form/EntityForm.d.ts +2 -2
- package/dist/form/EntityFormBinding.d.ts +5 -5
- package/dist/{history-Dz20hH38.js → history-g9688IaK.js} +2 -2
- package/dist/{history-Dz20hH38.js.map → history-g9688IaK.js.map} +1 -1
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/useAdminContext.d.ts +29 -0
- package/dist/hooks/useHistory.d.ts +7 -11
- package/dist/{import-DINSB-Vu.js → import-BjUG-cIY.js} +2 -2
- package/dist/{import-DINSB-Vu.js.map → import-BjUG-cIY.js.map} +1 -1
- package/dist/index.js +61 -38
- package/dist/index.js.map +1 -1
- package/dist/types/components/EntityFormProps.d.ts +1 -1
- package/dist/types/fields.d.ts +1 -1
- package/dist/{util-BhhfHY1M.js → util-B5fJm1FA.js} +296 -169
- package/dist/util-B5fJm1FA.js.map +1 -0
- package/package.json +9 -9
- package/src/collection_editor/CollectionEditorDialogsContext.tsx +1 -1
- package/src/collection_editor/ConfigControllerProvider.tsx +1 -1
- package/src/collection_editor/_cms_internals.ts +1 -1
- package/src/collection_editor/extensibility_types.ts +1 -1
- package/src/collection_editor/index.ts +2 -2
- package/src/collection_editor/serializable_types.ts +44 -13
- package/src/collection_editor/serializable_utils.ts +68 -59
- package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +66 -41
- package/src/collection_editor/ui/collection_editor/EntityActionsEditTab.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/GetCodeDialog.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/SubcollectionsEditTab.tsx +2 -2
- package/src/components/CollectionEditorDialogs.tsx +2 -2
- package/src/components/CollectionTableBinding/CollectionRowActions.tsx +5 -4
- package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +8 -2
- package/src/components/CollectionTableBinding/column_utils.tsx +19 -5
- package/src/components/CollectionTableBinding/table_bindings.tsx +10 -10
- package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +8 -7
- package/src/components/CollectionViewBinding/CollectionViewActions.tsx +2 -2
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +2 -2
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +2 -2
- package/src/components/DefaultDrawer.tsx +19 -8
- package/src/components/DeleteEntityDialog.tsx +2 -2
- package/src/components/DetailViewBinding.tsx +21 -1
- package/src/components/DrawerNavigationGroup.tsx +47 -4
- package/src/components/DrawerNavigationItem.tsx +19 -5
- package/src/components/EditFormActions.tsx +4 -3
- package/src/components/EditViewBinding.tsx +1 -1
- package/src/components/HomePage/ContentHomePage.tsx +4 -4
- package/src/components/RebaseAdmin.tsx +3 -3
- package/src/components/RebaseAuthGate.tsx +1 -1
- package/src/components/RebaseLayout.tsx +8 -2
- package/src/components/RebaseNavigation.tsx +9 -9
- package/src/components/RebaseRouteDefs.tsx +2 -2
- package/src/components/RebaseShell.tsx +10 -1
- package/src/components/RelationSelector.tsx +18 -3
- package/src/components/SelectableTable/filters/FilterFieldBinding.tsx +1 -1
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +36 -9
- package/src/components/SelectableTable/filters/RelationFilterField.tsx +93 -17
- package/src/components/SelectableTable/filters/null_filter.ts +63 -0
- package/src/components/SidePanelProvider.tsx +2 -2
- package/src/components/app/Scaffold.tsx +16 -1
- package/src/data_export/export/ExportCollectionAction.tsx +2 -2
- package/src/form/EntityForm.tsx +2 -2
- package/src/form/EntityFormActions.tsx +2 -1
- package/src/form/EntityFormBinding.tsx +7 -7
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +3 -3
- package/src/form/field_bindings/MultipleRelationFieldBinding.tsx +16 -5
- package/src/form/field_bindings/ReferenceFieldBinding.tsx +3 -3
- package/src/form/field_bindings/RelationFieldBinding.tsx +17 -6
- package/src/form/field_bindings/RepeatFieldBinding.tsx +2 -2
- package/src/hooks/index.ts +1 -1
- package/src/hooks/navigation/useNavigationResolution.ts +10 -4
- package/src/hooks/{useCMSContext.tsx → useAdminContext.tsx} +10 -10
- package/src/hooks/useHistory.ts +7 -11
- package/src/hooks/useResolvedNavigationFrom.tsx +2 -2
- package/src/index.ts +4 -4
- package/src/preview/PropertyPreview.tsx +6 -6
- package/src/preview/components/RelationPreview.tsx +1 -1
- package/src/preview/property_previews/ArrayOfMapsPreview.tsx +1 -1
- package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +2 -2
- package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +2 -2
- package/src/preview/property_previews/SkeletonPropertyComponent.tsx +2 -2
- package/src/types/components/EntityFormProps.tsx +2 -2
- package/src/types/fields.tsx +1 -1
- package/dist/CollectionEditorDialog-o7F_zbGY.js.map +0 -1
- package/dist/collection_editor/pgColumnToProperty.d.ts +0 -7
- package/dist/editor/extensions/Image/index.d.ts +0 -6
- package/dist/export-BvqhjdEb.js.map +0 -1
- package/dist/hooks/useCMSContext.d.ts +0 -29
- package/dist/util-BhhfHY1M.js.map +0 -1
- package/src/collection_editor/pgColumnToProperty.ts +0 -291
- package/src/editor/extensions/Image/index.ts +0 -133
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.12.0",
|
|
5
5
|
"description": "Rebase CMS — content management views, forms, and routing",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -83,14 +83,14 @@
|
|
|
83
83
|
"react-dropzone": "^15.0.0",
|
|
84
84
|
"react-use-measure": "^2.1.7",
|
|
85
85
|
"zod": "^4.4.3",
|
|
86
|
-
"@rebasepro/
|
|
87
|
-
"@rebasepro/
|
|
88
|
-
"@rebasepro/
|
|
89
|
-
"@rebasepro/
|
|
90
|
-
"@rebasepro/
|
|
91
|
-
"@rebasepro/
|
|
92
|
-
"@rebasepro/
|
|
93
|
-
"@rebasepro/
|
|
86
|
+
"@rebasepro/admin-types": "0.12.0",
|
|
87
|
+
"@rebasepro/app": "0.12.0",
|
|
88
|
+
"@rebasepro/common": "0.12.0",
|
|
89
|
+
"@rebasepro/forms": "0.12.0",
|
|
90
|
+
"@rebasepro/types": "0.12.0",
|
|
91
|
+
"@rebasepro/inference": "0.12.0",
|
|
92
|
+
"@rebasepro/ui": "0.12.0",
|
|
93
|
+
"@rebasepro/utils": "0.12.0"
|
|
94
94
|
},
|
|
95
95
|
"peerDependencies": {
|
|
96
96
|
"react": ">=19.0.0",
|
|
@@ -5,7 +5,7 @@ import React, { createContext, useContext } from "react";
|
|
|
5
5
|
* sets and CollectionEditorDialogs reads.
|
|
6
6
|
*
|
|
7
7
|
* This indirection exists because ConfigControllerProvider is a root-scope
|
|
8
|
-
* plugin provider that wraps <RebaseShell>, therefore living ABOVE the
|
|
8
|
+
* plugin provider that wraps <RebaseShell>, therefore living ABOVE the admin
|
|
9
9
|
* context providers (CollectionRegistryContext, NavigationStateContext,
|
|
10
10
|
* UrlContext). The actual dialog components need those contexts, so they
|
|
11
11
|
* must render inside the shell tree.
|
|
@@ -245,7 +245,7 @@ parentEntityIds,
|
|
|
245
245
|
}, [onAnalyticsEvent, pathSuggestions]);
|
|
246
246
|
|
|
247
247
|
// Build the dialog props objects that will be consumed by
|
|
248
|
-
// CollectionEditorDialogs (rendered inside RebaseShell where
|
|
248
|
+
// CollectionEditorDialogs (rendered inside RebaseShell where admin
|
|
249
249
|
// contexts are available).
|
|
250
250
|
const collectionDialogProps = useMemo(() => {
|
|
251
251
|
if (!currentDialog) return undefined;
|
|
@@ -160,7 +160,7 @@ export interface CollectionEditorExtensionProps {
|
|
|
160
160
|
* reading from React contexts.
|
|
161
161
|
*
|
|
162
162
|
* Use this when embedding the collection editor outside of the Rebase
|
|
163
|
-
*
|
|
163
|
+
* admin, where the Rebase context providers are not available.
|
|
164
164
|
*
|
|
165
165
|
* Default: false (standard Rebase mode).
|
|
166
166
|
*/
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Collection Editor — moved from @rebasepro/studio
|
|
2
|
-
// This module provides the visual schema/collection editor for the
|
|
2
|
+
// This module provides the visual schema/collection editor for the admin.
|
|
3
3
|
//
|
|
4
4
|
// IMPORTANT: Heavy UI components (CollectionEditorDialog, CollectionStudioView,
|
|
5
5
|
// CollectionsStudioView, PropertyForm, PropertyFormDialog) are NOT re-exported
|
|
6
|
-
// here to keep the main
|
|
6
|
+
// here to keep the main admin bundle lean. Import them from
|
|
7
7
|
// "@rebasepro/admin/collection_editor_ui" instead — they live in a separate chunk
|
|
8
8
|
// that is loaded on demand.
|
|
9
9
|
|
|
@@ -23,7 +23,8 @@ import type {
|
|
|
23
23
|
OnAction,
|
|
24
24
|
PropertyConditions,
|
|
25
25
|
PropertyValidationSchema,
|
|
26
|
-
SecurityRule
|
|
26
|
+
SecurityRule,
|
|
27
|
+
WhereFilterOp
|
|
27
28
|
} from "@rebasepro/types";
|
|
28
29
|
import type {
|
|
29
30
|
CollectionSize,
|
|
@@ -47,7 +48,9 @@ export interface SerializableAdminBaseOptions {
|
|
|
47
48
|
disabled?: boolean | SerializablePropertyDisabledConfig;
|
|
48
49
|
widthPercentage?: number;
|
|
49
50
|
customProps?: unknown;
|
|
50
|
-
|
|
51
|
+
/** Narrows the filter operators offered for this property. Plain data. */
|
|
52
|
+
filterOperators?: readonly WhereFilterOp[];
|
|
53
|
+
// Field, Preview and Filter are dropped (ComponentRef is not serializable)
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
/** JSON-serializable version of `PropertyDisabledConfig`. Already serializable. */
|
|
@@ -113,7 +116,12 @@ export interface SerializableAdminStringOptions extends SerializableAdminBaseOpt
|
|
|
113
116
|
markdown?: boolean;
|
|
114
117
|
previewAsTag?: boolean;
|
|
115
118
|
clearable?: boolean;
|
|
116
|
-
|
|
119
|
+
/**
|
|
120
|
+
* How a string holding a URL is rendered. Was mirrored here as `url`, the
|
|
121
|
+
* name of the *core* flag that says the string is a URL at all — so this
|
|
122
|
+
* option had no serializable form and the core flag had two.
|
|
123
|
+
*/
|
|
124
|
+
urlPreview?: "image" | "video" | "audio" | "file";
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
/** JSON-serializable version of `AdminNumberOptions`. */
|
|
@@ -134,18 +142,26 @@ export interface SerializableAdminVectorOptions extends SerializableAdminBaseOpt
|
|
|
134
142
|
/** JSON-serializable version of `AdminReferenceOptions`. */
|
|
135
143
|
export interface SerializableAdminReferenceOptions extends SerializableAdminBaseOptions {
|
|
136
144
|
previewProperties?: string[];
|
|
145
|
+
fixedFilter?: FilterValues<string>;
|
|
146
|
+
includeId?: boolean;
|
|
147
|
+
includeEntityLink?: boolean;
|
|
137
148
|
}
|
|
138
149
|
|
|
139
150
|
/** JSON-serializable version of `AdminRelationOptions`. */
|
|
140
151
|
export interface SerializableAdminRelationOptions extends SerializableAdminBaseOptions {
|
|
141
152
|
previewProperties?: string[];
|
|
142
153
|
widget?: "select" | "dialog";
|
|
154
|
+
fixedFilter?: FilterValues<string>;
|
|
155
|
+
includeId?: boolean;
|
|
156
|
+
includeEntityLink?: boolean;
|
|
143
157
|
}
|
|
144
158
|
|
|
145
159
|
/** JSON-serializable version of `AdminArrayOptions`. */
|
|
146
160
|
export interface SerializableAdminArrayOptions extends SerializableAdminBaseOptions {
|
|
147
161
|
expanded?: boolean;
|
|
148
162
|
minimalistView?: boolean;
|
|
163
|
+
sortable?: boolean;
|
|
164
|
+
canAddElements?: boolean;
|
|
149
165
|
}
|
|
150
166
|
|
|
151
167
|
/** JSON-serializable version of `AdminMapOptions`. */
|
|
@@ -153,6 +169,7 @@ export interface SerializableAdminMapOptions extends SerializableAdminBaseOption
|
|
|
153
169
|
expanded?: boolean;
|
|
154
170
|
minimalistView?: boolean;
|
|
155
171
|
spreadChildren?: boolean;
|
|
172
|
+
previewProperties?: string[];
|
|
156
173
|
}
|
|
157
174
|
|
|
158
175
|
// ── Serializable base property ────────────────────────────────────────
|
|
@@ -168,6 +185,12 @@ export interface SerializableBaseProperty {
|
|
|
168
185
|
description?: string;
|
|
169
186
|
propertyConfig?: string;
|
|
170
187
|
columnName?: string;
|
|
188
|
+
/**
|
|
189
|
+
* Server-side guarantee that the column never reaches an API response — a
|
|
190
|
+
* password hash, a verification token. Absent from this mirror until now,
|
|
191
|
+
* so editing a collection in the panel silently unset it.
|
|
192
|
+
*/
|
|
193
|
+
excludeFromApi?: boolean;
|
|
171
194
|
validation?: PropertyValidationSchema;
|
|
172
195
|
defaultValue?: unknown;
|
|
173
196
|
/**
|
|
@@ -196,6 +219,8 @@ export interface SerializableStringProperty extends SerializableBaseProperty {
|
|
|
196
219
|
storage?: SerializableStorageConfig;
|
|
197
220
|
userSelect?: boolean;
|
|
198
221
|
email?: boolean;
|
|
222
|
+
/** The string holds a URL. Feeds the generated OpenAPI contract. */
|
|
223
|
+
url?: boolean;
|
|
199
224
|
reference?: SerializableReferenceProperty;
|
|
200
225
|
}
|
|
201
226
|
|
|
@@ -257,9 +282,6 @@ export interface SerializableReferenceProperty extends SerializableBaseProperty
|
|
|
257
282
|
type: "reference";
|
|
258
283
|
isId?: boolean;
|
|
259
284
|
path?: string;
|
|
260
|
-
fixedFilter?: FilterValues<string>;
|
|
261
|
-
includeId?: boolean;
|
|
262
|
-
includeEntityLink?: boolean;
|
|
263
285
|
}
|
|
264
286
|
|
|
265
287
|
/**
|
|
@@ -309,10 +331,6 @@ export interface SerializableRelationProperty extends SerializableBaseProperty {
|
|
|
309
331
|
* went on describing a shape nothing produced, and the cast hid it.
|
|
310
332
|
*/
|
|
311
333
|
relation?: SerializableRelation;
|
|
312
|
-
fixedFilter?: FilterValues<string>;
|
|
313
|
-
includeId?: boolean;
|
|
314
|
-
includeEntityLink?: boolean;
|
|
315
|
-
widget?: "select" | "dialog";
|
|
316
334
|
// overrides are dropped (can contain non-serializable CollectionConfig fields)
|
|
317
335
|
}
|
|
318
336
|
|
|
@@ -329,8 +347,6 @@ export interface SerializableArrayProperty extends SerializableBaseProperty {
|
|
|
329
347
|
valueField?: string;
|
|
330
348
|
};
|
|
331
349
|
validation?: ArrayPropertyValidationSchema;
|
|
332
|
-
sortable?: boolean;
|
|
333
|
-
canAddElements?: boolean;
|
|
334
350
|
}
|
|
335
351
|
|
|
336
352
|
/** JSON-serializable `MapProperty`. */
|
|
@@ -341,7 +357,6 @@ export interface SerializableMapProperty extends SerializableBaseProperty {
|
|
|
341
357
|
properties?: SerializableProperties;
|
|
342
358
|
propertiesOrder?: string[];
|
|
343
359
|
validation?: PropertyValidationSchema;
|
|
344
|
-
previewProperties?: string[];
|
|
345
360
|
keyValue?: boolean;
|
|
346
361
|
}
|
|
347
362
|
|
|
@@ -453,7 +468,23 @@ export interface SerializableCollectionConfig {
|
|
|
453
468
|
// ── SQL / Postgres-specific ───────────────────────────────────────
|
|
454
469
|
table?: string;
|
|
455
470
|
schema?: string;
|
|
471
|
+
/**
|
|
472
|
+
* Collection-level relations, `target` as a slug — a thunk does not
|
|
473
|
+
* serialize, same as on a relation property.
|
|
474
|
+
*
|
|
475
|
+
* Absent from this mirror until now, and `buildCollectionFromTableMetadata`
|
|
476
|
+
* is the reason it matters: importing a table detects its foreign keys and
|
|
477
|
+
* junctions, puts them on the form, and the save then dropped every one of
|
|
478
|
+
* them. The import looked like it worked.
|
|
479
|
+
*/
|
|
480
|
+
relations?: SerializableRelation[];
|
|
456
481
|
securityRules?: readonly SecurityRule[];
|
|
482
|
+
/** Removes the framework's injected baseline RLS policies. */
|
|
483
|
+
disableDefaultPolicies?: boolean;
|
|
484
|
+
|
|
485
|
+
// ── Write behaviour ───────────────────────────────────────────────
|
|
486
|
+
/** Whether a write naming an undeclared field is rejected with a 400. */
|
|
487
|
+
strictWrites?: boolean;
|
|
457
488
|
|
|
458
489
|
// ── Owner ─────────────────────────────────────────────────────────
|
|
459
490
|
ownerId?: string;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type
|
|
11
|
+
import { isRelationalCollectionConfig, type Relation, type ArrayProperty, type BinaryProperty, type BooleanProperty, type DateProperty, type GeopointProperty, type MapProperty, type NumberProperty, type Properties, type Property, type ReferenceProperty, type RelationProperty, type StorageConfig, type StringProperty, type VectorProperty } from "@rebasepro/types";
|
|
12
12
|
import type { AdminPropertyOptions } from "@rebasepro/admin-types";
|
|
13
13
|
import type { AdminCollection } from "@rebasepro/admin-types";
|
|
14
14
|
|
|
@@ -27,6 +27,7 @@ import type {
|
|
|
27
27
|
SerializableProperties,
|
|
28
28
|
SerializableProperty,
|
|
29
29
|
SerializableReferenceProperty,
|
|
30
|
+
SerializableRelation,
|
|
30
31
|
SerializableRelationProperty,
|
|
31
32
|
SerializableStorageConfig,
|
|
32
33
|
SerializableStringProperty,
|
|
@@ -51,13 +52,22 @@ export type CollectionLookup = (slug: string) => AdminCollection | undefined;
|
|
|
51
52
|
* Strip non-serializable fields from a `AdminPropertyOptions`.
|
|
52
53
|
* Removes `Field` and `Preview` (ComponentRef).
|
|
53
54
|
*/
|
|
55
|
+
/**
|
|
56
|
+
* Every `ComponentRef` on a property's admin block. These are components, not
|
|
57
|
+
* data, and none of them survives `JSON.stringify` — a function-valued key is
|
|
58
|
+
* dropped from the output object with no error, so a component written here
|
|
59
|
+
* would leave a key that reads back as missing rather than as itself.
|
|
60
|
+
*
|
|
61
|
+
* `Filter` was absent from this list while `Field` and `Preview` were named
|
|
62
|
+
* twice, once destructured and once compared by string.
|
|
63
|
+
*/
|
|
64
|
+
const NON_SERIALIZABLE_ADMIN_KEYS = ["Field", "Preview", "Filter"];
|
|
65
|
+
|
|
54
66
|
function toSerializableAdminOptions(ui: AdminPropertyOptions | undefined): SerializableAdminBaseOptions | undefined {
|
|
55
67
|
if (!ui) return undefined;
|
|
56
|
-
const { Field, Preview, ...rest } = ui as AdminPropertyOptions & Record<string, unknown>;
|
|
57
|
-
// Only return if there are remaining fields
|
|
58
68
|
const result: Record<string, unknown> = {};
|
|
59
|
-
for (const [key, value] of Object.entries(
|
|
60
|
-
if (key
|
|
69
|
+
for (const [key, value] of Object.entries(ui as Record<string, unknown>)) {
|
|
70
|
+
if (!NON_SERIALIZABLE_ADMIN_KEYS.includes(key) && value !== undefined) {
|
|
61
71
|
result[key] = value;
|
|
62
72
|
}
|
|
63
73
|
}
|
|
@@ -121,6 +131,48 @@ function resolveRelationTarget(target: RelationProperty["relation"] extends infe
|
|
|
121
131
|
return undefined;
|
|
122
132
|
}
|
|
123
133
|
|
|
134
|
+
/**
|
|
135
|
+
* A `Relation` as JSON: `target` resolved from its thunk down to a slug.
|
|
136
|
+
*
|
|
137
|
+
* Switched on `kind` and assigned without a cast, so the compiler checks that
|
|
138
|
+
* each branch writes the fields its kind owns and no others. This used to be
|
|
139
|
+
* built as an untyped bag and cast into place — which is how the target
|
|
140
|
+
* interface drifted a whole refactor behind the code writing to it.
|
|
141
|
+
*
|
|
142
|
+
* Shared by the relation *property* and the collection-level `relations` array,
|
|
143
|
+
* which had no serializer at all: `buildCollectionFromTableMetadata` fills that
|
|
144
|
+
* array from the table's foreign keys and junctions on import, and every one of
|
|
145
|
+
* them was dropped on save.
|
|
146
|
+
*/
|
|
147
|
+
function toSerializableRelation(link: Relation): SerializableRelation {
|
|
148
|
+
const target = resolveRelationTarget(link.target);
|
|
149
|
+
const common = {
|
|
150
|
+
...(target ? { target } : {}),
|
|
151
|
+
...(link.relationName ? { relationName: link.relationName } : {}),
|
|
152
|
+
...(link.onUpdate ? { onUpdate: link.onUpdate } : {}),
|
|
153
|
+
...(link.onDelete ? { onDelete: link.onDelete } : {})
|
|
154
|
+
};
|
|
155
|
+
switch (link.kind) {
|
|
156
|
+
case "belongsTo":
|
|
157
|
+
return { kind: "belongsTo", ...common, ...(link.localKey ? { localKey: link.localKey } : {}) };
|
|
158
|
+
case "hasOne":
|
|
159
|
+
case "hasMany":
|
|
160
|
+
return {
|
|
161
|
+
kind: link.kind,
|
|
162
|
+
...common,
|
|
163
|
+
...(link.foreignKeyOnTarget ? { foreignKeyOnTarget: link.foreignKeyOnTarget } : {})
|
|
164
|
+
};
|
|
165
|
+
case "manyToMany":
|
|
166
|
+
return { kind: "manyToMany", ...common, ...(link.through ? { through: link.through } : {}) };
|
|
167
|
+
case "via":
|
|
168
|
+
return { kind: "via", ...common, cardinality: link.cardinality, joinPath: link.joinPath };
|
|
169
|
+
default: {
|
|
170
|
+
const exhaustive: never = link;
|
|
171
|
+
throw new Error(`Unhandled relation kind: ${JSON.stringify(exhaustive)}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
124
176
|
/**
|
|
125
177
|
* Strip non-serializable base property fields.
|
|
126
178
|
* Returns a clean object with only the common serializable fields from BaseProperty.
|
|
@@ -134,6 +186,7 @@ function toSerializableBaseFields(property: Property): Omit<SerializableBaseProp
|
|
|
134
186
|
if (property.description) result.description = property.description;
|
|
135
187
|
if (property.propertyConfig) result.propertyConfig = property.propertyConfig;
|
|
136
188
|
if (property.columnName) result.columnName = property.columnName;
|
|
189
|
+
if (property.excludeFromApi !== undefined) result.excludeFromApi = property.excludeFromApi;
|
|
137
190
|
if (property.defaultValue !== undefined) result.defaultValue = property.defaultValue;
|
|
138
191
|
if (property.conditions) result.conditions = property.conditions;
|
|
139
192
|
if (property.metadata) result.metadata = property.metadata;
|
|
@@ -164,6 +217,7 @@ export function toSerializableProperty(property: Property): SerializableProperty
|
|
|
164
217
|
if (sp.enum) result.enum = sp.enum;
|
|
165
218
|
if (sp.userSelect) result.userSelect = sp.userSelect;
|
|
166
219
|
if (sp.email) result.email = sp.email;
|
|
220
|
+
if (sp.url) result.url = sp.url;
|
|
167
221
|
|
|
168
222
|
// Convert validation.matches from RegExp to string
|
|
169
223
|
if (sp.validation) {
|
|
@@ -255,9 +309,6 @@ export function toSerializableProperty(property: Property): SerializableProperty
|
|
|
255
309
|
};
|
|
256
310
|
if (rp.isId !== undefined) result.isId = rp.isId;
|
|
257
311
|
if (rp.path) result.path = rp.path;
|
|
258
|
-
if (rp.fixedFilter) result.fixedFilter = rp.fixedFilter;
|
|
259
|
-
if (rp.includeId !== undefined) result.includeId = rp.includeId;
|
|
260
|
-
if (rp.includeEntityLink !== undefined) result.includeEntityLink = rp.includeEntityLink;
|
|
261
312
|
return result;
|
|
262
313
|
}
|
|
263
314
|
|
|
@@ -278,54 +329,8 @@ export function toSerializableProperty(property: Property): SerializableProperty
|
|
|
278
329
|
// no others. This used to be built as an untyped bag and cast
|
|
279
330
|
// into place — which is how the target interface drifted a
|
|
280
331
|
// whole refactor behind the code writing to it.
|
|
281
|
-
|
|
282
|
-
const common = {
|
|
283
|
-
...(target ? { target } : {}),
|
|
284
|
-
...(link.relationName ? { relationName: link.relationName } : {}),
|
|
285
|
-
...(link.onUpdate ? { onUpdate: link.onUpdate } : {}),
|
|
286
|
-
...(link.onDelete ? { onDelete: link.onDelete } : {})
|
|
287
|
-
};
|
|
288
|
-
switch (link.kind) {
|
|
289
|
-
case "belongsTo":
|
|
290
|
-
result.relation = {
|
|
291
|
-
kind: "belongsTo",
|
|
292
|
-
...common,
|
|
293
|
-
...(link.localKey ? { localKey: link.localKey } : {})
|
|
294
|
-
};
|
|
295
|
-
break;
|
|
296
|
-
case "hasOne":
|
|
297
|
-
case "hasMany":
|
|
298
|
-
result.relation = {
|
|
299
|
-
kind: link.kind,
|
|
300
|
-
...common,
|
|
301
|
-
...(link.foreignKeyOnTarget ? { foreignKeyOnTarget: link.foreignKeyOnTarget } : {})
|
|
302
|
-
};
|
|
303
|
-
break;
|
|
304
|
-
case "manyToMany":
|
|
305
|
-
result.relation = {
|
|
306
|
-
kind: "manyToMany",
|
|
307
|
-
...common,
|
|
308
|
-
...(link.through ? { through: link.through } : {})
|
|
309
|
-
};
|
|
310
|
-
break;
|
|
311
|
-
case "via":
|
|
312
|
-
result.relation = {
|
|
313
|
-
kind: "via",
|
|
314
|
-
...common,
|
|
315
|
-
cardinality: link.cardinality,
|
|
316
|
-
joinPath: link.joinPath
|
|
317
|
-
};
|
|
318
|
-
break;
|
|
319
|
-
default: {
|
|
320
|
-
const exhaustive: never = link;
|
|
321
|
-
throw new Error(`Unhandled relation kind: ${JSON.stringify(exhaustive)}`);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
332
|
+
result.relation = toSerializableRelation(link);
|
|
324
333
|
}
|
|
325
|
-
if (rl.fixedFilter) result.fixedFilter = rl.fixedFilter;
|
|
326
|
-
if (rl.includeId !== undefined) result.includeId = rl.includeId;
|
|
327
|
-
if (rl.includeEntityLink !== undefined) result.includeEntityLink = rl.includeEntityLink;
|
|
328
|
-
if (rl.widget) result.widget = rl.widget;
|
|
329
334
|
// overrides are dropped (may hold a non-serializable CollectionConfig)
|
|
330
335
|
return result;
|
|
331
336
|
}
|
|
@@ -338,8 +343,6 @@ export function toSerializableProperty(property: Property): SerializableProperty
|
|
|
338
343
|
};
|
|
339
344
|
if (ap.columnType) result.columnType = ap.columnType;
|
|
340
345
|
if (ap.validation) result.validation = ap.validation;
|
|
341
|
-
if (ap.sortable !== undefined) result.sortable = ap.sortable;
|
|
342
|
-
if (ap.canAddElements !== undefined) result.canAddElements = ap.canAddElements;
|
|
343
346
|
|
|
344
347
|
// Recursively serialize the "of" property
|
|
345
348
|
if (ap.of) {
|
|
@@ -371,7 +374,6 @@ export function toSerializableProperty(property: Property): SerializableProperty
|
|
|
371
374
|
if (mp.columnType) result.columnType = mp.columnType;
|
|
372
375
|
if (mp.validation) result.validation = mp.validation;
|
|
373
376
|
if (mp.propertiesOrder) result.propertiesOrder = mp.propertiesOrder;
|
|
374
|
-
if (mp.previewProperties) result.previewProperties = mp.previewProperties;
|
|
375
377
|
if (mp.keyValue) result.keyValue = mp.keyValue;
|
|
376
378
|
|
|
377
379
|
// Recursively serialize nested properties
|
|
@@ -448,7 +450,10 @@ export function toSerializableCollectionConfig(collection: AdminCollection): Ser
|
|
|
448
450
|
if (collection.titleProperty) result.titleProperty = collection.titleProperty as string;
|
|
449
451
|
if (collection.ownerId) result.ownerId = collection.ownerId;
|
|
450
452
|
if (collection.metadata) result.metadata = collection.metadata;
|
|
451
|
-
if (collection.table) result.table = collection.table;
|
|
453
|
+
if (isRelationalCollectionConfig(collection) && collection.table) result.table = collection.table;
|
|
454
|
+
if (isRelationalCollectionConfig(collection) && collection.relations?.length) {
|
|
455
|
+
result.relations = collection.relations.map(toSerializableRelation);
|
|
456
|
+
}
|
|
452
457
|
if ("schema" in collection && collection.schema) result.schema = collection.schema as string;
|
|
453
458
|
if (collection.orderProperty) result.orderProperty = collection.orderProperty as string;
|
|
454
459
|
|
|
@@ -462,6 +467,10 @@ export function toSerializableCollectionConfig(collection: AdminCollection): Ser
|
|
|
462
467
|
if (collection.enabledViews) result.enabledViews = collection.enabledViews;
|
|
463
468
|
if (collection.disableDefaultActions) result.disableDefaultActions = collection.disableDefaultActions;
|
|
464
469
|
if (collection.securityRules) result.securityRules = collection.securityRules;
|
|
470
|
+
if (isRelationalCollectionConfig(collection) && collection.disableDefaultPolicies !== undefined) {
|
|
471
|
+
result.disableDefaultPolicies = collection.disableDefaultPolicies;
|
|
472
|
+
}
|
|
473
|
+
if (collection.strictWrites !== undefined) result.strictWrites = collection.strictWrites;
|
|
465
474
|
|
|
466
475
|
// Enum-like fields
|
|
467
476
|
if (collection.openEntityMode) result.openEntityMode = collection.openEntityMode;
|
|
@@ -53,7 +53,7 @@ import { AICollectionGeneratorPopover } from "./AICollectionGeneratorPopover";
|
|
|
53
53
|
import { AIModifiedPathsProvider, useAIModifiedPaths } from "./AIModifiedPathsContext";
|
|
54
54
|
import { CollectionGenerationCallback, CollectionOperation } from "../../api/generateCollectionApi";
|
|
55
55
|
import { CollectionRLSTab } from "./CollectionRLSTab";
|
|
56
|
-
import { buildCollectionFromTableMetadata } from "
|
|
56
|
+
import { buildCollectionFromTableMetadata } from "@rebasepro/common";
|
|
57
57
|
import { mergeDeep, randomString, removeUndefined } from "@rebasepro/utils";
|
|
58
58
|
import type { CollectionEditorExtensionProps, CollectionEditorTab } from "../../extensibility_types";
|
|
59
59
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
|
|
2
|
-
import { SecurityRule as GeneratedSecurityRule } from "@rebasepro/types";
|
|
3
2
|
import { getPolicyNamesForRule, getPolicyNamesForRules } from "@rebasepro/utils";
|
|
4
3
|
import { getEffectiveSecurityRules } from "@rebasepro/common";
|
|
5
4
|
import React, { useState, useEffect, useMemo } from "react";
|
|
@@ -42,27 +41,9 @@ import {
|
|
|
42
41
|
import { useFormex } from "@rebasepro/forms";
|
|
43
42
|
import { useRebaseContext } from "@rebasepro/app";
|
|
44
43
|
import type { AdminCollection } from "@rebasepro/admin-types";
|
|
44
|
+
import type { PostgresPolicy, SecurityOperation, SecurityRule } from "@rebasepro/types";
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
export interface PostgresPolicy {
|
|
48
|
-
policyname: string;
|
|
49
|
-
tablename: string;
|
|
50
|
-
permissive: "PERMISSIVE" | "RESTRICTIVE";
|
|
51
|
-
roles: string[];
|
|
52
|
-
cmd: "SELECT" | "INSERT" | "UPDATE" | "DELETE" | "ALL";
|
|
53
|
-
qual: string | null;
|
|
54
|
-
with_check: string | null;
|
|
55
|
-
status?: "live" | "code_only" | "both";
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
interface SecurityRule {
|
|
59
|
-
name: string;
|
|
60
|
-
operation?: string;
|
|
61
|
-
mode?: string;
|
|
62
|
-
using?: string;
|
|
63
|
-
withCheck?: string;
|
|
64
|
-
roles?: string[];
|
|
65
|
-
}
|
|
46
|
+
export type { PostgresPolicy } from "@rebasepro/types";
|
|
66
47
|
|
|
67
48
|
type CollectionWithSecurity = AdminCollection & {
|
|
68
49
|
securityRules?: SecurityRule[];
|
|
@@ -71,6 +52,64 @@ type CollectionWithSecurity = AdminCollection & {
|
|
|
71
52
|
alias?: string;
|
|
72
53
|
};
|
|
73
54
|
|
|
55
|
+
/**
|
|
56
|
+
* `pg_policies` reports the operation and the permissive flag in upper case;
|
|
57
|
+
* `SecurityRule` names them in lower case. These two used to be `.toLowerCase()`
|
|
58
|
+
* assigned straight into a `SecurityRule`, which typechecks only against a
|
|
59
|
+
* `string` field — and this file declared its own local `SecurityRule` with
|
|
60
|
+
* `operation?: string`, so it did. Against the real type it does not: an
|
|
61
|
+
* unrecognised `cmd` would have been written into the collection as a policy
|
|
62
|
+
* operation that compiles to nothing.
|
|
63
|
+
*/
|
|
64
|
+
function toSecurityOperation(cmd: PostgresPolicy["cmd"] | undefined): SecurityOperation | undefined {
|
|
65
|
+
switch (cmd) {
|
|
66
|
+
case "SELECT": return "select";
|
|
67
|
+
case "INSERT": return "insert";
|
|
68
|
+
case "UPDATE": return "update";
|
|
69
|
+
case "DELETE": return "delete";
|
|
70
|
+
case "ALL": return "all";
|
|
71
|
+
default: return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toSecurityMode(permissive: PostgresPolicy["permissive"] | undefined): SecurityRule["mode"] {
|
|
76
|
+
// Absent stays absent. `permissive` is the default, so writing it out
|
|
77
|
+
// changes nothing semantically — but this value is serialized into the
|
|
78
|
+
// user's collection file, and defaulting here would grow a `mode:
|
|
79
|
+
// "permissive"` on every rule the editor touches. The `.toLowerCase()`
|
|
80
|
+
// this replaced returned `undefined` for an absent value; that part was
|
|
81
|
+
// right, and only the widened type was wrong.
|
|
82
|
+
if (!permissive) return undefined;
|
|
83
|
+
return permissive === "RESTRICTIVE" ? "restrictive" : "permissive";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build a {@link SecurityRule} from a `pg_policies` row.
|
|
88
|
+
*
|
|
89
|
+
* `SecurityRule` is a discriminated union and this is the part the local shadow
|
|
90
|
+
* type flattened away: a raw-SQL rule *requires* `using`, so a policy with only
|
|
91
|
+
* a `WITH CHECK` clause — every INSERT-only policy — is not one. It is a
|
|
92
|
+
* roles-only rule carrying the check. Assembling the object field-by-field
|
|
93
|
+
* against `operation?: string` accepted all of these and told the compiler
|
|
94
|
+
* nothing.
|
|
95
|
+
*/
|
|
96
|
+
function toSecurityRule(policy: Partial<PostgresPolicy>): SecurityRule {
|
|
97
|
+
const base = {
|
|
98
|
+
name: policy.policyname ?? "",
|
|
99
|
+
operation: toSecurityOperation(policy.cmd),
|
|
100
|
+
mode: toSecurityMode(policy.permissive),
|
|
101
|
+
roles: policy.roles ? [...policy.roles] : undefined
|
|
102
|
+
};
|
|
103
|
+
if (policy.qual) {
|
|
104
|
+
return {
|
|
105
|
+
...base,
|
|
106
|
+
using: policy.qual,
|
|
107
|
+
withCheck: policy.with_check || undefined
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return base;
|
|
111
|
+
}
|
|
112
|
+
|
|
74
113
|
export function CollectionRLSTab() {
|
|
75
114
|
const { values, setFieldValue } = useFormex<CollectionWithSecurity>();
|
|
76
115
|
const [editingPolicy, setEditingPolicy] = useState<PostgresPolicy | "new" | null>(null);
|
|
@@ -186,7 +225,7 @@ export function CollectionRLSTab() {
|
|
|
186
225
|
if (!tableName) return new Set<string>();
|
|
187
226
|
const effectiveRules = getEffectiveSecurityRules(values as AdminCollection);
|
|
188
227
|
return getPolicyNamesForRules(
|
|
189
|
-
[...
|
|
228
|
+
[...rules, ...effectiveRules],
|
|
190
229
|
tableName
|
|
191
230
|
);
|
|
192
231
|
}, [rules, tableName, values]);
|
|
@@ -194,14 +233,7 @@ export function CollectionRLSTab() {
|
|
|
194
233
|
const unmappedPolicies = dbPolicies.filter(dp => !generatedPolicyNames.has(dp.policyname));
|
|
195
234
|
|
|
196
235
|
const handleSave = async (newPolicy: Partial<PostgresPolicy>) => {
|
|
197
|
-
const rule: SecurityRule =
|
|
198
|
-
name: newPolicy.policyname ?? "",
|
|
199
|
-
operation: newPolicy.cmd?.toLowerCase(),
|
|
200
|
-
mode: newPolicy.permissive?.toLowerCase(),
|
|
201
|
-
using: newPolicy.qual || undefined,
|
|
202
|
-
withCheck: newPolicy.with_check || undefined,
|
|
203
|
-
roles: newPolicy.roles
|
|
204
|
-
};
|
|
236
|
+
const rule: SecurityRule = toSecurityRule(newPolicy);
|
|
205
237
|
|
|
206
238
|
let newRules;
|
|
207
239
|
if (editingPolicy === "new") {
|
|
@@ -240,7 +272,7 @@ export function CollectionRLSTab() {
|
|
|
240
272
|
{/* Unnamed rules are still named in Postgres — show what they compile to
|
|
241
273
|
rather than an empty heading. */}
|
|
242
274
|
{rule.name || (tableName
|
|
243
|
-
? getPolicyNamesForRule(rule
|
|
275
|
+
? getPolicyNamesForRule(rule, tableName).join(", ")
|
|
244
276
|
: "Unnamed policy")}
|
|
245
277
|
</Typography>
|
|
246
278
|
</div>
|
|
@@ -251,11 +283,11 @@ export function CollectionRLSTab() {
|
|
|
251
283
|
</div>
|
|
252
284
|
<div className="flex items-center gap-1 sm:gap-2 shrink-0">
|
|
253
285
|
<Button size="small" variant="text" onClick={() => setEditingPolicy({
|
|
254
|
-
policyname: rule.name,
|
|
286
|
+
policyname: rule.name ?? "",
|
|
255
287
|
tablename: values.id || values.table || values.alias || "your_table",
|
|
256
288
|
permissive: (rule.mode || "permissive").toUpperCase() as PostgresPolicy["permissive"],
|
|
257
289
|
cmd: (rule.operation || "ALL").toUpperCase() as PostgresPolicy["cmd"],
|
|
258
|
-
roles: rule.roles
|
|
290
|
+
roles: [...(rule.roles ?? [])],
|
|
259
291
|
qual: rule.using || null,
|
|
260
292
|
with_check: rule.withCheck || null
|
|
261
293
|
})}>
|
|
@@ -305,14 +337,7 @@ export function CollectionRLSTab() {
|
|
|
305
337
|
</div>
|
|
306
338
|
<div className="flex items-center gap-1 sm:gap-2 shrink-0">
|
|
307
339
|
<Button size="small" variant="outlined" color="primary" onClick={() => {
|
|
308
|
-
const rule: SecurityRule =
|
|
309
|
-
name: dp.policyname,
|
|
310
|
-
operation: dp.cmd?.toLowerCase(),
|
|
311
|
-
mode: dp.permissive?.toLowerCase(),
|
|
312
|
-
using: dp.qual || undefined,
|
|
313
|
-
withCheck: dp.with_check || undefined,
|
|
314
|
-
roles: dp.roles
|
|
315
|
-
};
|
|
340
|
+
const rule: SecurityRule = toSecurityRule(dp);
|
|
316
341
|
setFieldValue("securityRules", [...rules, rule]);
|
|
317
342
|
}}>
|
|
318
343
|
Import to codebase
|
|
@@ -145,7 +145,7 @@ export function EntityActionsEditTab({
|
|
|
145
145
|
title={<>Remove this action?</>}
|
|
146
146
|
body={<>This will <b>not
|
|
147
147
|
delete any data</b>, only
|
|
148
|
-
the action in the
|
|
148
|
+
the action in the admin</>}/>}
|
|
149
149
|
|
|
150
150
|
<EntityActionsSelectDialog
|
|
151
151
|
open={addEntityActionDialogOpen}
|
|
@@ -27,7 +27,7 @@ export function GetCodeDialog({
|
|
|
27
27
|
<DialogContent>
|
|
28
28
|
|
|
29
29
|
<Typography variant={"body2"} className={"my-4 mb-8"}>
|
|
30
|
-
If you want to customise the collection in code, you can add this collection code to your
|
|
30
|
+
If you want to customise the collection in code, you can add this collection code to your admin
|
|
31
31
|
app configuration.
|
|
32
32
|
More info in the <a
|
|
33
33
|
rel="noopener noreferrer"
|
|
@@ -214,7 +214,7 @@ export function SubcollectionsEditTab({
|
|
|
214
214
|
title={<>Delete this subcollection?</>}
|
|
215
215
|
body={<> This will <b>not
|
|
216
216
|
delete any data</b>, only
|
|
217
|
-
the collection in the
|
|
217
|
+
the collection in the admin</>}/>}
|
|
218
218
|
{viewToDelete &&
|
|
219
219
|
<ConfirmationDialog open={Boolean(viewToDelete)}
|
|
220
220
|
onAccept={() => {
|
|
@@ -225,7 +225,7 @@ export function SubcollectionsEditTab({
|
|
|
225
225
|
title={<>Remove this view?</>}
|
|
226
226
|
body={<>This will <b>not
|
|
227
227
|
delete any data</b>, only
|
|
228
|
-
the view in the
|
|
228
|
+
the view in the admin</>}/>}
|
|
229
229
|
|
|
230
230
|
<CollectionEditorDialog
|
|
231
231
|
open={Boolean(currentDialog)}
|