@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
|
@@ -16,12 +16,12 @@ const PropertyFormDialog = lazy(() =>
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Renders the CollectionEditorDialog and PropertyFormDialog inside
|
|
19
|
-
* the RebaseShell tree where
|
|
19
|
+
* the RebaseShell tree where admin-internal contexts
|
|
20
20
|
* (CollectionRegistryContext, NavigationStateContext, UrlContext)
|
|
21
21
|
* are available.
|
|
22
22
|
*
|
|
23
23
|
* Both dialog components are loaded dynamically via React.lazy so that
|
|
24
|
-
* production
|
|
24
|
+
* production admin bundles that never open the collection editor don't pay
|
|
25
25
|
* the download/parse cost up-front.
|
|
26
26
|
*
|
|
27
27
|
* The dialog state (open/close, props) is managed by ConfigControllerProvider
|
|
@@ -16,9 +16,10 @@ import {
|
|
|
16
16
|
Tooltip
|
|
17
17
|
} from "@rebasepro/ui";
|
|
18
18
|
import { useTranslation } from "@rebasepro/app";
|
|
19
|
+
import { getIcon } from "@rebasepro/app";
|
|
19
20
|
import { getEntityFromCache } from "@rebasepro/app";
|
|
20
21
|
import { getLocalChangesBackup } from "@rebasepro/app";
|
|
21
|
-
import {
|
|
22
|
+
import { useAdminContext } from "../../hooks/useAdminContext";
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
*
|
|
@@ -79,7 +80,7 @@ export const CollectionRowActions = function CollectionRowActions({
|
|
|
79
80
|
isDraggable?: boolean;
|
|
80
81
|
}) {
|
|
81
82
|
|
|
82
|
-
const context =
|
|
83
|
+
const context = useAdminContext();
|
|
83
84
|
const sidePanelCtrl = context.sidePanelController;
|
|
84
85
|
const { t } = useTranslation();
|
|
85
86
|
|
|
@@ -140,7 +141,7 @@ export const CollectionRowActions = function CollectionRowActions({
|
|
|
140
141
|
});
|
|
141
142
|
}}
|
|
142
143
|
size={iconSize}>
|
|
143
|
-
{action.icon}
|
|
144
|
+
{getIcon(action.icon, undefined, undefined, "smallest")}
|
|
144
145
|
</IconButton>;
|
|
145
146
|
if (isEditAction && hasDraft) {
|
|
146
147
|
iconButton = (
|
|
@@ -183,7 +184,7 @@ export const CollectionRowActions = function CollectionRowActions({
|
|
|
183
184
|
openEntityMode: openEntityMode ?? collection?.openEntityMode
|
|
184
185
|
});
|
|
185
186
|
}}>
|
|
186
|
-
{action.icon}
|
|
187
|
+
{getIcon(action.icon, undefined, undefined, "smallest")}
|
|
187
188
|
{action.name}
|
|
188
189
|
</MenuItem>
|
|
189
190
|
))}
|
|
@@ -4,7 +4,7 @@ import { Entity, User, Property } from "@rebasepro/types";
|
|
|
4
4
|
import { CollectionSize, RebaseContext } from "@rebasepro/admin-types";
|
|
5
5
|
import { PropertyTableCell } from "./PropertyTableCell";
|
|
6
6
|
import { ErrorBoundary } from "@rebasepro/ui";
|
|
7
|
-
import { useRebaseContext, useLargeLayout } from "@rebasepro/app";
|
|
7
|
+
import { useRebaseContext, useLargeLayout, useCollectionScope } from "@rebasepro/app";
|
|
8
8
|
import { CellRendererParams, VirtualTableColumn } from "@rebasepro/ui";
|
|
9
9
|
import { CollectionRowActions } from "./CollectionRowActions";
|
|
10
10
|
import { CollectionTableToolbar } from "./internal/CollectionTableToolbar";
|
|
@@ -248,11 +248,17 @@ export const CollectionTableBinding = function CollectionTableBinding<M extends
|
|
|
248
248
|
|
|
249
249
|
}, [size]);
|
|
250
250
|
|
|
251
|
+
// Same source the filter fields read their engine from, so the header's
|
|
252
|
+
// filter control and the field it opens cannot disagree about what the
|
|
253
|
+
// collection's driver can answer.
|
|
254
|
+
const engine = useCollectionScope()?.engine;
|
|
255
|
+
|
|
251
256
|
const collectionColumns: VirtualTableColumn[] = useMemo(() => {
|
|
252
257
|
const columnsResult: VirtualTableColumn[] = propertiesToColumns({
|
|
253
258
|
properties,
|
|
254
259
|
sortable,
|
|
255
260
|
fixedFilter,
|
|
261
|
+
engine,
|
|
256
262
|
AdditionalHeaderWidget
|
|
257
263
|
});
|
|
258
264
|
|
|
@@ -273,7 +279,7 @@ export const CollectionTableBinding = function CollectionTableBinding<M extends
|
|
|
273
279
|
}))
|
|
274
280
|
: [];
|
|
275
281
|
return [...columnsResult, ...additionalTableColumns];
|
|
276
|
-
}, [properties, sortable, fixedFilter, AdditionalHeaderWidget, additionalFields]);
|
|
282
|
+
}, [properties, sortable, fixedFilter, engine, AdditionalHeaderWidget, additionalFields]);
|
|
277
283
|
|
|
278
284
|
const idColumn: VirtualTableColumn = useMemo(() => ({
|
|
279
285
|
key: "id_ewcfedcswdf3",
|
|
@@ -4,7 +4,7 @@ import { getTableCellAlignment, getTablePropertyColumnWidth } from "./internal/c
|
|
|
4
4
|
import { FilterValues } from "@rebasepro/types";
|
|
5
5
|
import { VirtualTableColumn } from "@rebasepro/ui";
|
|
6
6
|
import { getIconForProperty, getResolvedPropertyInPath } from "../../util/property_utils";
|
|
7
|
-
import { getColumnKeysForProperty } from "@rebasepro/app";
|
|
7
|
+
import { getColumnKeysForProperty, isFilterableRelation } from "@rebasepro/app";
|
|
8
8
|
|
|
9
9
|
export function buildIdColumn(largeLayout?: boolean): VirtualTableColumn {
|
|
10
10
|
return {
|
|
@@ -22,6 +22,12 @@ export interface PropertiesToColumnsParams<M extends Record<string, unknown>> {
|
|
|
22
22
|
properties: Properties;
|
|
23
23
|
sortable?: boolean;
|
|
24
24
|
fixedFilter?: FilterValues<keyof M extends string ? keyof M : never>;
|
|
25
|
+
/**
|
|
26
|
+
* The engine backing the collection (`collection.engine`). Decides which
|
|
27
|
+
* relation kinds can be filtered — see `isFilterableRelation`. Omitted
|
|
28
|
+
* falls back to the default engine's capabilities.
|
|
29
|
+
*/
|
|
30
|
+
engine?: string;
|
|
25
31
|
AdditionalHeaderWidget?: React.ComponentType<{
|
|
26
32
|
property: Property,
|
|
27
33
|
propertyKey: string,
|
|
@@ -29,7 +35,7 @@ export interface PropertiesToColumnsParams<M extends Record<string, unknown>> {
|
|
|
29
35
|
}>;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
export function propertiesToColumns<M extends Record<string, unknown>>({ properties, sortable, fixedFilter, AdditionalHeaderWidget }: PropertiesToColumnsParams<M>): VirtualTableColumn[] {
|
|
38
|
+
export function propertiesToColumns<M extends Record<string, unknown>>({ properties, sortable, fixedFilter, engine, AdditionalHeaderWidget }: PropertiesToColumnsParams<M>): VirtualTableColumn[] {
|
|
33
39
|
const disabledFilter = Boolean(fixedFilter);
|
|
34
40
|
return Object.entries<Property>(properties)
|
|
35
41
|
.flatMap(([key, property]) => getColumnKeysForProperty(property, key))
|
|
@@ -40,7 +46,7 @@ export function propertiesToColumns<M extends Record<string, unknown>>({ propert
|
|
|
40
46
|
const property = getResolvedPropertyInPath(properties, key) as Property;
|
|
41
47
|
if (!property)
|
|
42
48
|
throw Error("Internal error: no property found in path " + key);
|
|
43
|
-
const filterable = filterableProperty(property);
|
|
49
|
+
const filterable = filterableProperty(property, false, engine);
|
|
44
50
|
return {
|
|
45
51
|
key: key as string,
|
|
46
52
|
align: getTableCellAlignment(property),
|
|
@@ -61,13 +67,21 @@ export function propertiesToColumns<M extends Record<string, unknown>>({ propert
|
|
|
61
67
|
});
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
function filterableProperty(property: Property, partOfArray = false): boolean {
|
|
70
|
+
function filterableProperty(property: Property, partOfArray = false, engine?: string): boolean {
|
|
71
|
+
// A relation this engine's driver cannot compile into a `WHERE` has
|
|
72
|
+
// nothing to filter on, so the header's filter control would open onto a
|
|
73
|
+
// field that renders nothing (`FilterFieldBinding` returns null on an
|
|
74
|
+
// empty operator list) — and, before the driver started failing closed,
|
|
75
|
+
// sending one silently returned every row. Same authority as the operator
|
|
76
|
+
// resolution, and engine-aware for the same reason: this table renders
|
|
77
|
+
// over Postgres, Mongo, Firestore and anything a developer registers.
|
|
78
|
+
if (!isFilterableRelation(property, engine)) return false;
|
|
65
79
|
if (partOfArray) {
|
|
66
80
|
return ["string", "number", "date", "reference", "relation"].includes(property.type);
|
|
67
81
|
}
|
|
68
82
|
if (property.type === "array") {
|
|
69
83
|
if (property.of && !Array.isArray(property.of))
|
|
70
|
-
return filterableProperty(property.of, true);
|
|
84
|
+
return filterableProperty(property.of, true, engine);
|
|
71
85
|
else
|
|
72
86
|
return false;
|
|
73
87
|
}
|
|
@@ -222,10 +222,10 @@ export function getTableBindingForProperty(
|
|
|
222
222
|
path={(property as ReferenceProperty).path!}
|
|
223
223
|
multiselect={false}
|
|
224
224
|
previewProperties={(property as ReferenceProperty).admin?.previewProperties}
|
|
225
|
-
includeId={(property as ReferenceProperty).includeId}
|
|
226
|
-
includeEntityLink={(property as ReferenceProperty).includeEntityLink}
|
|
225
|
+
includeId={(property as ReferenceProperty).admin?.includeId}
|
|
226
|
+
includeEntityLink={(property as ReferenceProperty).admin?.includeEntityLink}
|
|
227
227
|
title={property.name ?? propertyKey}
|
|
228
|
-
fixedFilter={(property as ReferenceProperty).fixedFilter}
|
|
228
|
+
fixedFilter={(property as ReferenceProperty).admin?.fixedFilter}
|
|
229
229
|
/>
|
|
230
230
|
);
|
|
231
231
|
},
|
|
@@ -290,9 +290,9 @@ export function getTableBindingForProperty(
|
|
|
290
290
|
path={refOfProp.path!}
|
|
291
291
|
previewProperties={refOfProp.admin?.previewProperties}
|
|
292
292
|
title={arrayProperty.name}
|
|
293
|
-
fixedFilter={refOfProp.fixedFilter}
|
|
294
|
-
includeId={refOfProp.includeId}
|
|
295
|
-
includeEntityLink={refOfProp.includeEntityLink}
|
|
293
|
+
fixedFilter={refOfProp.admin?.fixedFilter}
|
|
294
|
+
includeId={refOfProp.admin?.includeId}
|
|
295
|
+
includeEntityLink={refOfProp.admin?.includeEntityLink}
|
|
296
296
|
/>
|
|
297
297
|
),
|
|
298
298
|
allowScroll: false
|
|
@@ -318,10 +318,10 @@ function RelationDialogBindingComponent({ propertyKey, internalValue, updateValu
|
|
|
318
318
|
multiselect={false}
|
|
319
319
|
relation={relProp.relation!}
|
|
320
320
|
previewProperties={relProp.admin?.previewProperties}
|
|
321
|
-
includeId={relProp.includeId}
|
|
322
|
-
includeEntityLink={relProp.includeEntityLink}
|
|
321
|
+
includeId={relProp.admin?.includeId}
|
|
322
|
+
includeEntityLink={relProp.admin?.includeEntityLink}
|
|
323
323
|
title={relProp.name ?? propertyKey}
|
|
324
|
-
fixedFilter={relProp.fixedFilter}
|
|
324
|
+
fixedFilter={relProp.admin?.fixedFilter}
|
|
325
325
|
/>
|
|
326
326
|
);
|
|
327
327
|
}
|
|
@@ -337,7 +337,7 @@ function RelationSelectorBindingComponent({ propertyKey, internalValue, updateVa
|
|
|
337
337
|
disabled={disabled}
|
|
338
338
|
size={"small"}
|
|
339
339
|
relation={relProp.relation!}
|
|
340
|
-
fixedFilter={relProp.fixedFilter}
|
|
340
|
+
fixedFilter={relProp.admin?.fixedFilter}
|
|
341
341
|
/>
|
|
342
342
|
);
|
|
343
343
|
}
|
|
@@ -22,9 +22,10 @@ import {
|
|
|
22
22
|
import { useAnalyticsController } from "@rebasepro/app";
|
|
23
23
|
import { getEntityPreviewKeys } from "../../util/previews";
|
|
24
24
|
import { IconForView } from "@rebasepro/app";
|
|
25
|
+
import { getIcon } from "@rebasepro/app";
|
|
25
26
|
import { getValueInPath } from "@rebasepro/utils";
|
|
26
27
|
import { useCollectionSlotKeys, resolveEntitySlots, type CollectionSlotKeys } from "./usePreviewSlots";
|
|
27
|
-
import {
|
|
28
|
+
import { useAdminContext } from "../../hooks/useAdminContext";
|
|
28
29
|
import { resolveEntityAction } from "../../util/resolutions";
|
|
29
30
|
|
|
30
31
|
export type CollectionListViewBindingProps<M extends Record<string, unknown> = Record<string, unknown>> = {
|
|
@@ -265,8 +266,8 @@ year: diffDays > 365 ? "numeric" : undefined });
|
|
|
265
266
|
}
|
|
266
267
|
|
|
267
268
|
/**
|
|
268
|
-
* Classic
|
|
269
|
-
* Designed to be the most familiar, stereotypical
|
|
269
|
+
* Classic admin list view for displaying entities.
|
|
270
|
+
* Designed to be the most familiar, stereotypical admin content management view:
|
|
270
271
|
* - Clean rows with checkbox, icon/avatar, title, metadata, and actions
|
|
271
272
|
* - Column-sortable headers
|
|
272
273
|
* - Infinite scroll
|
|
@@ -289,7 +290,7 @@ export function CollectionListViewBinding<M extends Record<string, unknown> = Re
|
|
|
289
290
|
const authController = useAuthController();
|
|
290
291
|
const customizationController = useCustomizationController();
|
|
291
292
|
const analyticsController = useAnalyticsController();
|
|
292
|
-
const context =
|
|
293
|
+
const context = useAdminContext();
|
|
293
294
|
|
|
294
295
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
295
296
|
const [containerWidth, setContainerWidth] = useState(1200);
|
|
@@ -622,7 +623,7 @@ const ListRow = React.memo(function ListRow<M extends Record<string, unknown>>({
|
|
|
622
623
|
isLast: boolean;
|
|
623
624
|
isActive?: boolean;
|
|
624
625
|
listViewActions?: EntityAction[];
|
|
625
|
-
context?: ReturnType<typeof
|
|
626
|
+
context?: ReturnType<typeof useAdminContext>;
|
|
626
627
|
path?: string;
|
|
627
628
|
selectionController?: SelectionController<M>;
|
|
628
629
|
openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
|
|
@@ -864,7 +865,7 @@ const ListRow = React.memo(function ListRow<M extends Record<string, unknown>>({
|
|
|
864
865
|
openEntityMode: openEntityMode ?? collection?.openEntityMode ?? "full_screen"
|
|
865
866
|
});
|
|
866
867
|
}}>
|
|
867
|
-
{action.icon}
|
|
868
|
+
{getIcon(action.icon, undefined, undefined, "smallest")}
|
|
868
869
|
</IconButton>
|
|
869
870
|
</Tooltip>
|
|
870
871
|
))}
|
|
@@ -888,7 +889,7 @@ const ListRow = React.memo(function ListRow<M extends Record<string, unknown>>({
|
|
|
888
889
|
isLast: boolean;
|
|
889
890
|
isActive?: boolean;
|
|
890
891
|
listViewActions?: EntityAction[];
|
|
891
|
-
context?: ReturnType<typeof
|
|
892
|
+
context?: ReturnType<typeof useAdminContext>;
|
|
892
893
|
path?: string;
|
|
893
894
|
selectionController?: SelectionController<M>;
|
|
894
895
|
openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
|
|
@@ -13,7 +13,7 @@ const ImportCollectionAction = lazy(() => import("../../data_import/import").the
|
|
|
13
13
|
const ExportCollectionAction = lazy(() => import("../../data_export/export").then(m => ({ default: m.ExportCollectionAction })));
|
|
14
14
|
import { EditorCollectionAction } from "../../collection_editor/ui/EditorCollectionAction";
|
|
15
15
|
import { useCollectionEditorController } from "../../collection_editor/useCollectionEditorController";
|
|
16
|
-
import {
|
|
16
|
+
import { useAdminContext } from "../../hooks/useAdminContext";
|
|
17
17
|
|
|
18
18
|
export type CollectionViewActionsProps<M extends Record<string, unknown>> = {
|
|
19
19
|
collection: AdminCollection<M>;
|
|
@@ -52,7 +52,7 @@ export function CollectionViewActions<M extends Record<string, unknown>>({
|
|
|
52
52
|
children,
|
|
53
53
|
openNewDocument
|
|
54
54
|
}: CollectionViewActionsProps<M>) {
|
|
55
|
-
const context =
|
|
55
|
+
const context = useAdminContext();
|
|
56
56
|
|
|
57
57
|
const { canCreate, canDelete } = usePermissions();
|
|
58
58
|
|
|
@@ -75,7 +75,7 @@ import { addRecentId, getRecentIds } from "./utils";
|
|
|
75
75
|
import { isViewMode, OpenEntityMode, resolveOpenEntityMode, VIEW_MODE_PARAM } from "../../util/view_mode";
|
|
76
76
|
import { mergeDeep } from "@rebasepro/utils";
|
|
77
77
|
import { useBreadcrumbsController } from "../../hooks/useBreadcrumbsController";
|
|
78
|
-
import {
|
|
78
|
+
import { useAdminContext } from "../../hooks/useAdminContext";
|
|
79
79
|
import { useCollectionRegistryController } from "../../hooks/navigation/contexts/CollectionRegistryContext";
|
|
80
80
|
import { useSidePanel } from "../../hooks/useSidePanel";
|
|
81
81
|
import { useUrlController } from "../../hooks/navigation/contexts/UrlContext";
|
|
@@ -167,7 +167,7 @@ const CollectionViewBindingInner = React.memo(
|
|
|
167
167
|
) {
|
|
168
168
|
|
|
169
169
|
const { t } = useTranslation();
|
|
170
|
-
const context =
|
|
170
|
+
const context = useAdminContext();
|
|
171
171
|
const collectionRegistry = useCollectionRegistryController();
|
|
172
172
|
const urlController = useUrlController();
|
|
173
173
|
const breadcrumbs = useBreadcrumbsController();
|
|
@@ -11,7 +11,7 @@ import { FilterPresetsButton } from "./FilterPresetsButton";
|
|
|
11
11
|
import { toArray } from "@rebasepro/utils";
|
|
12
12
|
import { useNavigate } from "react-router-dom";
|
|
13
13
|
import { useUrlController } from "../../hooks/navigation/contexts/UrlContext";
|
|
14
|
-
import {
|
|
14
|
+
import { useAdminContext } from "../../hooks/useAdminContext";
|
|
15
15
|
import { withViewMode } from "../../util/view_mode";
|
|
16
16
|
|
|
17
17
|
export type CollectionViewStartActionsProps<M extends Record<string, unknown>> = {
|
|
@@ -43,7 +43,7 @@ export function CollectionViewStartActions<M extends Record<string, unknown>>({
|
|
|
43
43
|
openNewDocument
|
|
44
44
|
}: CollectionViewStartActionsProps<M>) {
|
|
45
45
|
|
|
46
|
-
const context =
|
|
46
|
+
const context = useAdminContext();
|
|
47
47
|
const largeLayout = useLargeLayout();
|
|
48
48
|
const { t } = useTranslation();
|
|
49
49
|
|
|
@@ -33,9 +33,9 @@ import { LanguageToggle, RebaseLogo } from "@rebasepro/app";
|
|
|
33
33
|
import { useApp } from "./app/useApp";
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Default drawer used in the
|
|
36
|
+
* Default drawer used in the admin.
|
|
37
37
|
*
|
|
38
|
-
* When no `children` are provided, renders the full
|
|
38
|
+
* When no `children` are provided, renders the full admin navigation
|
|
39
39
|
* (collection groups, mode switch, plugin slots).
|
|
40
40
|
*
|
|
41
41
|
* When `children` **are** provided, renders the shared drawer shell
|
|
@@ -60,7 +60,7 @@ export function DefaultDrawer({
|
|
|
60
60
|
logoDestination?: string;
|
|
61
61
|
/**
|
|
62
62
|
* Custom navigation content for the drawer.
|
|
63
|
-
* When provided, replaces the default
|
|
63
|
+
* When provided, replaces the default admin navigation (collection groups,
|
|
64
64
|
* mode switch, slots). The shared shell (logo, scroll area, footer
|
|
65
65
|
* actions, collapse toggle) is still rendered around it.
|
|
66
66
|
*/
|
|
@@ -115,7 +115,7 @@ export function DefaultDrawer({
|
|
|
115
115
|
? "linear-gradient(to bottom, transparent 0, black 20px, black calc(100% - 20px), transparent 100%)"
|
|
116
116
|
: "linear-gradient(to bottom, black 0, black calc(100% - 20px), transparent 100%)"
|
|
117
117
|
}}>
|
|
118
|
-
{children ?? <
|
|
118
|
+
{children ?? <AdminNavigationContent />}
|
|
119
119
|
</div>
|
|
120
120
|
|
|
121
121
|
{footerActions !== null && (
|
|
@@ -138,11 +138,11 @@ export function DefaultDrawer({
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
|
-
* Default
|
|
141
|
+
* Default admin navigation content — rendered inside DefaultDrawer when no
|
|
142
142
|
* custom `children` are provided. Contains the mode switch, navigation
|
|
143
143
|
* groups, and header / footer plugin slots.
|
|
144
144
|
*/
|
|
145
|
-
function
|
|
145
|
+
function AdminNavigationContent() {
|
|
146
146
|
|
|
147
147
|
const {
|
|
148
148
|
drawerHovered,
|
|
@@ -166,7 +166,7 @@ function CMSNavigationContent() {
|
|
|
166
166
|
const allNavigationEntries = navigationState.topLevelNavigation?.navigationEntries ?? [];
|
|
167
167
|
|
|
168
168
|
// Build a set of Studio-only dev view slugs so we can distinguish them from
|
|
169
|
-
//
|
|
169
|
+
// admin custom views added via <RebaseAdmin views={...}> or plugins.
|
|
170
170
|
const studioViewSlugs = useMemo(() => {
|
|
171
171
|
const slugs = new Set<string>();
|
|
172
172
|
(registry.studioConfig?.devViews ?? []).forEach(v => slugs.add(v.slug));
|
|
@@ -176,7 +176,7 @@ function CMSNavigationContent() {
|
|
|
176
176
|
}, [registry.studioConfig, registry.cmsConfig?.collectionEditor]);
|
|
177
177
|
|
|
178
178
|
// Studio mode shows Studio dev views + admin entries (Users/Roles).
|
|
179
|
-
// Content mode shows collections,
|
|
179
|
+
// Content mode shows collections, admin custom views, and admin entries — but not Studio dev views.
|
|
180
180
|
const filteredEntries = adminModeController.mode === "studio"
|
|
181
181
|
? allNavigationEntries.filter(e => (e.type === "view" && studioViewSlugs.has(e.slug)) || e.type === "admin")
|
|
182
182
|
: allNavigationEntries.filter(e => e.type !== "view" || !studioViewSlugs.has(e.slug));
|
|
@@ -189,6 +189,16 @@ function CMSNavigationContent() {
|
|
|
189
189
|
...[...entryGroups].filter(g => !orderedGroups.includes(g))
|
|
190
190
|
];
|
|
191
191
|
|
|
192
|
+
// Group header icons, declared on `navigationGroupMappings`. Keyed by name
|
|
193
|
+
// because that is what a navigation entry's `group` holds.
|
|
194
|
+
const groupIcons = useMemo(() => {
|
|
195
|
+
const icons = new Map<string, string>();
|
|
196
|
+
(registry.cmsConfig?.navigationGroupMappings ?? []).forEach(mapping => {
|
|
197
|
+
if (mapping.icon) icons.set(mapping.name, mapping.icon);
|
|
198
|
+
});
|
|
199
|
+
return icons;
|
|
200
|
+
}, [registry.cmsConfig?.navigationGroupMappings]);
|
|
201
|
+
|
|
192
202
|
// Collapsible groups state - using "drawer" namespace for independent state from home page
|
|
193
203
|
const collapsedDefaults = useMemo(
|
|
194
204
|
() => buildCollapsedDefaults(registry.cmsConfig?.navigationGroupMappings, "drawer"),
|
|
@@ -244,6 +254,7 @@ context });
|
|
|
244
254
|
tooltipsOpen={tooltipsOpen}
|
|
245
255
|
adminMenuOpen={adminMenuOpen}
|
|
246
256
|
onItemClick={onItemClick}
|
|
257
|
+
icon={groupIcons.get(group)}
|
|
247
258
|
/>
|
|
248
259
|
);
|
|
249
260
|
})}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
useSnackbarController,
|
|
11
11
|
useTranslation
|
|
12
12
|
} from "@rebasepro/app";
|
|
13
|
-
import {
|
|
13
|
+
import { useAdminContext } from "../hooks";
|
|
14
14
|
import { EntityViewBinding } from "./EntityViewBinding";
|
|
15
15
|
import type { AdminCollection } from "@rebasepro/admin-types";
|
|
16
16
|
|
|
@@ -54,7 +54,7 @@ export function DeleteEntityDialog<M extends Record<string, unknown>>({
|
|
|
54
54
|
const { t } = useTranslation();
|
|
55
55
|
const [loading, setLoading] = useState(false);
|
|
56
56
|
|
|
57
|
-
const context =
|
|
57
|
+
const context = useAdminContext();
|
|
58
58
|
const entityOrEntities = Array.isArray(entityOrEntitiesToDelete) && entityOrEntitiesToDelete.length === 1
|
|
59
59
|
? entityOrEntitiesToDelete[0]
|
|
60
60
|
: entityOrEntitiesToDelete;
|
|
@@ -11,6 +11,7 @@ import { EntityViewBinding } from "./EntityViewBinding";
|
|
|
11
11
|
import { CircularProgressCenter, iconSize } from "@rebasepro/ui";
|
|
12
12
|
import {
|
|
13
13
|
Alert,
|
|
14
|
+
ArrowLeftIcon,
|
|
14
15
|
Button,
|
|
15
16
|
CenteredView,
|
|
16
17
|
cls,
|
|
@@ -470,7 +471,24 @@ entityId }
|
|
|
470
471
|
);
|
|
471
472
|
});
|
|
472
473
|
|
|
473
|
-
|
|
474
|
+
// Full screen is the one layout with no way out of its own: a side panel and a
|
|
475
|
+
// dialog can be dismissed, and a split keeps the list beside it, but opening an
|
|
476
|
+
// entity full screen replaces the collection entirely and left browser Back as
|
|
477
|
+
// the only route back to it — which is not an affordance the page shows, and is
|
|
478
|
+
// wrong anyway once the user has moved between tabs inside the entity.
|
|
479
|
+
const backToCollectionButton = layout === "full_screen" ? (
|
|
480
|
+
<Tooltip title={t("back")}>
|
|
481
|
+
<IconButton
|
|
482
|
+
size="small"
|
|
483
|
+
aria-label={t("back")}
|
|
484
|
+
onClick={() => navigate(urlController.buildUrlCollectionPath(path))}
|
|
485
|
+
>
|
|
486
|
+
<ArrowLeftIcon size={iconSize.smallest} />
|
|
487
|
+
</IconButton>
|
|
488
|
+
</Tooltip>
|
|
489
|
+
) : null;
|
|
490
|
+
|
|
491
|
+
const shouldShowTopBar = Boolean(barActions) || hasAdditionalViews || layout === "side_panel" || layout === "dialog" || Boolean(backToCollectionButton);
|
|
474
492
|
|
|
475
493
|
const fullScreenButton = !barActions && (layout === "side_panel" || layout === "split" || layout === "dialog") && entityId ? (
|
|
476
494
|
<Tooltip title={"Open full screen"}>
|
|
@@ -562,6 +580,8 @@ entityId }
|
|
|
562
580
|
{shouldShowTopBar && <div
|
|
563
581
|
className={cls("h-[52px] items-center flex overflow-hidden w-full border-b pl-2 pr-2 flex bg-surface-50 dark:bg-surface-900", defaultBorderMixin)}>
|
|
564
582
|
|
|
583
|
+
{backToCollectionButton}
|
|
584
|
+
|
|
565
585
|
{fullScreenButton}
|
|
566
586
|
|
|
567
587
|
{barActions?.({
|
|
@@ -2,7 +2,7 @@ import type { NavigationEntry } from "@rebasepro/admin-types";
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { ChevronDownIcon, cls, iconSize, Typography } from "@rebasepro/ui";
|
|
4
4
|
|
|
5
|
-
import { IconForView } from "@rebasepro/app";
|
|
5
|
+
import { IconForView, getIcon } from "@rebasepro/app";
|
|
6
6
|
import { DrawerNavigationItem } from "./DrawerNavigationItem";
|
|
7
7
|
import { useTranslation, useComponentOverride } from "@rebasepro/app";
|
|
8
8
|
|
|
@@ -47,6 +47,13 @@ export interface DrawerNavigationGroupProps {
|
|
|
47
47
|
* Hide the group header (title and expandable panel)
|
|
48
48
|
*/
|
|
49
49
|
hideHeader?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Lucide icon name for the group header, from `NavigationGroupMapping.icon`.
|
|
52
|
+
*
|
|
53
|
+
* Supplying one moves the visual anchor from the rows to the group: the header
|
|
54
|
+
* gets the icon, and the entries below trade theirs for an indent.
|
|
55
|
+
*/
|
|
56
|
+
icon?: string;
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
/**
|
|
@@ -63,10 +70,23 @@ export function DrawerNavigationGroup({
|
|
|
63
70
|
adminMenuOpen,
|
|
64
71
|
headerActions,
|
|
65
72
|
onItemClick,
|
|
66
|
-
hideHeader
|
|
73
|
+
hideHeader,
|
|
74
|
+
icon
|
|
67
75
|
}: DrawerNavigationGroupProps) {
|
|
68
76
|
const { t } = useTranslation();
|
|
69
77
|
const ResolvedDrawerNavigationItem = useComponentOverride("Shell.DrawerNavigationItem", DrawerNavigationItem);
|
|
78
|
+
|
|
79
|
+
// Opt-in, per group: declaring `icon` on a `NavigationGroupMapping` switches
|
|
80
|
+
// this group to the categorised treatment — icon on the header, entries
|
|
81
|
+
// indented beneath it without their own. A group that declares no icon renders
|
|
82
|
+
// exactly as it did before, so an existing project sees no change at all.
|
|
83
|
+
//
|
|
84
|
+
// Two cases stay on the flat rendering regardless: with no header there is
|
|
85
|
+
// nothing to indent under, and collapsed to a rail the entry icons are the only
|
|
86
|
+
// affordance left to click.
|
|
87
|
+
const groupIcon = !hideHeader && drawerOpen ? getIcon(icon, undefined, undefined, "small") : undefined;
|
|
88
|
+
const categorised = Boolean(groupIcon);
|
|
89
|
+
const indentEntries = categorised;
|
|
70
90
|
return (
|
|
71
91
|
<div
|
|
72
92
|
className={"my-2 mx-2 flex flex-col"}
|
|
@@ -83,14 +103,36 @@ export function DrawerNavigationGroup({
|
|
|
83
103
|
<ChevronDownIcon
|
|
84
104
|
size={iconSize.small}
|
|
85
105
|
className={cls(
|
|
86
|
-
|
|
106
|
+
categorised
|
|
107
|
+
? "text-surface-500 dark:text-surface-400"
|
|
108
|
+
: "text-surface-400 dark:text-surface-400",
|
|
109
|
+
"transition-transform duration-200 mr-1",
|
|
87
110
|
collapsed ? "-rotate-90" : "rotate-0"
|
|
88
111
|
)}
|
|
89
112
|
/>
|
|
113
|
+
{groupIcon && (
|
|
114
|
+
<span className="shrink-0 mr-2 flex items-center text-surface-600 dark:text-surface-300 [&>svg]:size-4">
|
|
115
|
+
{groupIcon}
|
|
116
|
+
</span>
|
|
117
|
+
)}
|
|
90
118
|
<Typography
|
|
91
119
|
variant={"caption"}
|
|
92
120
|
color={"secondary"}
|
|
93
|
-
|
|
121
|
+
// Two treatments, and which one applies is decided by the group
|
|
122
|
+
// itself. Without an icon this is byte-for-byte the original: the
|
|
123
|
+
// drawer is a flat list of same-weight rows, and a louder header
|
|
124
|
+
// would just compete with them.
|
|
125
|
+
//
|
|
126
|
+
// With one, the group becomes the thing you scan — its rows have
|
|
127
|
+
// given up their icons to indent beneath it — so the label steps up
|
|
128
|
+
// to match. At 11px in surface-400 it sat *below* the contrast of
|
|
129
|
+
// the rows it labels, which is backwards once it is the anchor.
|
|
130
|
+
className={cls(
|
|
131
|
+
"font-semibold uppercase flex-grow line-clamp-1",
|
|
132
|
+
categorised
|
|
133
|
+
? "text-[12px] tracking-wide text-surface-600 dark:text-surface-300"
|
|
134
|
+
: "text-[11px] tracking-wider text-surface-400 dark:text-surface-400"
|
|
135
|
+
)}
|
|
94
136
|
>
|
|
95
137
|
{(group || t("views_group"))}
|
|
96
138
|
</Typography>
|
|
@@ -122,6 +164,7 @@ export function DrawerNavigationGroup({
|
|
|
122
164
|
onClick={() => onItemClick?.(entry)}
|
|
123
165
|
url={entry.url}
|
|
124
166
|
name={entry.name}
|
|
167
|
+
indented={indentEntries}
|
|
125
168
|
/>
|
|
126
169
|
))}
|
|
127
170
|
</div>
|
|
@@ -10,7 +10,8 @@ export function DrawerNavigationItem({
|
|
|
10
10
|
adminMenuOpen,
|
|
11
11
|
tooltipsOpen,
|
|
12
12
|
url,
|
|
13
|
-
onClick
|
|
13
|
+
onClick,
|
|
14
|
+
indented = false
|
|
14
15
|
}: {
|
|
15
16
|
icon: React.ReactElement,
|
|
16
17
|
name: string,
|
|
@@ -19,12 +20,25 @@ export function DrawerNavigationItem({
|
|
|
19
20
|
adminMenuOpen?: boolean,
|
|
20
21
|
url: string,
|
|
21
22
|
onClick?: () => void,
|
|
23
|
+
/**
|
|
24
|
+
* Render as a child of an icon-bearing group: no icon of its own, indented to
|
|
25
|
+
* sit under the group's label. Set by {@link DrawerNavigationGroup} only when
|
|
26
|
+
* the group actually has an icon and the drawer is expanded.
|
|
27
|
+
*/
|
|
28
|
+
indented?: boolean,
|
|
22
29
|
}) {
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
// Indented rows give up the icon rather than keeping it *and* indenting: a
|
|
32
|
+
// per-row icon repeated down a group is what flattens the hierarchy, since
|
|
33
|
+
// every row then reads at the weight of the category above it. The indent is
|
|
34
|
+
// the same 44px the icon occupied, so labels stay on the original grid and the
|
|
35
|
+
// rail width does not change.
|
|
36
|
+
const iconWrap = indented
|
|
37
|
+
? <div className={"shrink-0 w-[44px] h-[30px]"} aria-hidden={true}/>
|
|
38
|
+
: <div
|
|
39
|
+
className={"shrink-0 flex items-center justify-center w-[44px] h-[30px] text-surface-500 dark:text-text-secondary-dark [&>svg]:size-4 group-hover/nav:text-primary transition-colors duration-150"}>
|
|
40
|
+
{icon}
|
|
41
|
+
</div>;
|
|
28
42
|
|
|
29
43
|
const listItem = <div>
|
|
30
44
|
<NavLink
|
|
@@ -20,7 +20,8 @@ import { SideDialogController, useSideDialogContext } from "./SideDialogs";
|
|
|
20
20
|
import { FormexController } from "@rebasepro/forms";
|
|
21
21
|
import { ErrorTooltip } from "@rebasepro/app";
|
|
22
22
|
import { usePermissions } from "@rebasepro/app";
|
|
23
|
-
import {
|
|
23
|
+
import { getIcon } from "@rebasepro/app";
|
|
24
|
+
import { useAdminContext } from "../hooks/useAdminContext";
|
|
24
25
|
import { useChildViewSource } from "../hooks/useChildViewSource";
|
|
25
26
|
|
|
26
27
|
export function EditFormActions({
|
|
@@ -40,7 +41,7 @@ export function EditFormActions({
|
|
|
40
41
|
}: EntityFormActionsProps) {
|
|
41
42
|
|
|
42
43
|
const { canCreate, canDelete } = usePermissions();
|
|
43
|
-
const context =
|
|
44
|
+
const context = useAdminContext();
|
|
44
45
|
const sidePanelController = context.sidePanelController;
|
|
45
46
|
const sideDialogContext = useSideDialogContext();
|
|
46
47
|
const customizationController = useCustomizationController();
|
|
@@ -371,7 +372,7 @@ function EntityActionButton({
|
|
|
371
372
|
});
|
|
372
373
|
}
|
|
373
374
|
}}>
|
|
374
|
-
{loading ? <CircularProgress size={"smallest"}/> : action.icon}
|
|
375
|
+
{loading ? <CircularProgress size={"smallest"}/> : getIcon(action.icon, undefined, undefined, "smallest")}
|
|
375
376
|
</IconButton>
|
|
376
377
|
</Tooltip>;
|
|
377
378
|
}
|
|
@@ -70,7 +70,7 @@ export type OnTabChangeParams<M extends Record<string, unknown>> = {
|
|
|
70
70
|
|
|
71
71
|
export interface EditViewBindingProps<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
72
72
|
/**
|
|
73
|
-
* The
|
|
73
|
+
* The admin path of the entity, e.g. "users" or "products".
|
|
74
74
|
*/
|
|
75
75
|
path: string;
|
|
76
76
|
collection: AdminCollection<M>;
|