@rebasepro/admin 0.14.1 → 0.14.2-canary.g27a129e
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/dist/{CollectionEditorDialog-BeS4lxGh.js → CollectionEditorDialog-Dxx0ZaDk.js} +11 -5
- package/dist/CollectionEditorDialog-Dxx0ZaDk.js.map +1 -0
- package/dist/{PropertyEditView-BFJ6H9-Q.js → PropertyEditView-D6xNTm0d.js} +2 -2
- package/dist/{PropertyEditView-BFJ6H9-Q.js.map → PropertyEditView-D6xNTm0d.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-sfsUpEJz.js → RouterCollectionsStudioView-C4vSn9Pu.js} +4 -4
- package/dist/{RouterCollectionsStudioView-sfsUpEJz.js.map → RouterCollectionsStudioView-C4vSn9Pu.js.map} +1 -1
- package/dist/collection_editor/serializable_types.d.ts +6 -0
- package/dist/collection_editor_ui.js +4 -4
- package/dist/components/CollectionViewBinding/ViewModeToggle.d.ts +11 -5
- package/dist/components/RebaseAdmin.d.ts +1 -1
- package/dist/{export-KEf57Jso.js → export-BZi1MzQ5.js} +2 -2
- package/dist/{export-KEf57Jso.js.map → export-BZi1MzQ5.js.map} +1 -1
- package/dist/{history-Ctd2cRn_.js → history-B5pkfldE.js} +2 -2
- package/dist/{history-Ctd2cRn_.js.map → history-B5pkfldE.js.map} +1 -1
- package/dist/{import-BujhycMq.js → import-CXpuYo-T.js} +2 -2
- package/dist/{import-BujhycMq.js.map → import-CXpuYo-T.js.map} +1 -1
- package/dist/index.js +19 -14
- package/dist/index.js.map +1 -1
- package/dist/util/resolutions.d.ts +10 -1
- package/dist/util/view_mode.d.ts +24 -6
- package/dist/{util-bL-fk6C6.js → util-BKtHPLe4.js} +141 -52
- package/dist/util-BKtHPLe4.js.map +1 -0
- package/package.json +9 -9
- package/src/collection_editor/serializable_types.ts +6 -0
- package/src/collection_editor/serializable_utils.ts +8 -0
- package/src/collection_editor/ui/collection_editor/ViewModeSwitch.tsx +16 -2
- package/src/components/CollectionEditorDialogs.tsx +4 -3
- package/src/components/CollectionViewBinding/CollectionViewActions.tsx +4 -4
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +99 -22
- package/src/components/CollectionViewBinding/ViewModeToggle.tsx +66 -32
- package/src/components/EntityInspector.tsx +3 -2
- package/src/components/HomePage/NavigationCardBinding.tsx +2 -2
- package/src/components/RebaseAdmin.tsx +3 -2
- package/src/components/RebaseNavigation.tsx +13 -6
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +3 -2
- package/src/util/resolutions.ts +30 -1
- package/src/util/view_mode.ts +49 -12
- package/dist/CollectionEditorDialog-BeS4lxGh.js.map +0 -1
- package/dist/util-bL-fk6C6.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/admin",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.14.
|
|
4
|
+
"version": "0.14.2-canary.g27a129e",
|
|
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": "^19.1.1",
|
|
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/ui": "0.14.
|
|
93
|
-
"@rebasepro/
|
|
86
|
+
"@rebasepro/forms": "0.14.2-canary.g27a129e",
|
|
87
|
+
"@rebasepro/common": "0.14.2-canary.g27a129e",
|
|
88
|
+
"@rebasepro/admin-types": "0.14.2-canary.g27a129e",
|
|
89
|
+
"@rebasepro/app": "0.14.2-canary.g27a129e",
|
|
90
|
+
"@rebasepro/types": "0.14.2-canary.g27a129e",
|
|
91
|
+
"@rebasepro/utils": "0.14.2-canary.g27a129e",
|
|
92
|
+
"@rebasepro/ui": "0.14.2-canary.g27a129e",
|
|
93
|
+
"@rebasepro/inference": "0.14.2-canary.g27a129e"
|
|
94
94
|
},
|
|
95
95
|
"peerDependencies": {
|
|
96
96
|
"react": ">=19.2.7",
|
|
@@ -432,6 +432,12 @@ export interface SerializableCollectionConfig {
|
|
|
432
432
|
defaultEntityAction?: "view" | "edit";
|
|
433
433
|
defaultViewMode?: ViewMode;
|
|
434
434
|
enabledViews?: ViewMode[];
|
|
435
|
+
/**
|
|
436
|
+
* Keys only. A custom view declared inline carries a `Builder` function,
|
|
437
|
+
* which does not survive a round trip through a config file — the editor
|
|
438
|
+
* can reference app-registered views, not author new ones.
|
|
439
|
+
*/
|
|
440
|
+
customViews?: string[];
|
|
435
441
|
kanban?: KanbanConfig<Record<string, unknown>>;
|
|
436
442
|
defaultSize?: CollectionSize;
|
|
437
443
|
sideDialogWidth?: number | string;
|
|
@@ -501,6 +501,14 @@ export function toSerializableCollectionConfig(collection: AdminCollection): Ser
|
|
|
501
501
|
if (collection.previewProperties) result.previewProperties = collection.previewProperties;
|
|
502
502
|
if (collection.listProperties) result.listProperties = collection.listProperties;
|
|
503
503
|
if (collection.enabledViews) result.enabledViews = collection.enabledViews;
|
|
504
|
+
// Inline views are dropped rather than mangled: their `Builder` cannot be
|
|
505
|
+
// serialized, and writing back a half of one would delete it from the
|
|
506
|
+
// running config. Keys survive, so a collection edited in the panel keeps
|
|
507
|
+
// whichever registered views it named.
|
|
508
|
+
if (collection.customViews) {
|
|
509
|
+
const keys = collection.customViews.filter((v): v is string => typeof v === "string");
|
|
510
|
+
if (keys.length > 0) result.customViews = keys;
|
|
511
|
+
}
|
|
504
512
|
if (collection.disableDefaultActions) result.disableDefaultActions = collection.disableDefaultActions;
|
|
505
513
|
if (collection.securityRules) result.securityRules = collection.securityRules;
|
|
506
514
|
if (isRelationalCollectionConfig(collection) && collection.disableDefaultPolicies !== undefined) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ViewMode } from "@rebasepro/admin-types";
|
|
1
|
+
import { CollectionCustomView, ViewMode } from "@rebasepro/admin-types";
|
|
2
2
|
import {
|
|
3
3
|
cls,
|
|
4
4
|
KanbanIcon,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
ToggleButtonGroup,
|
|
9
9
|
Typography
|
|
10
10
|
} from "@rebasepro/ui";
|
|
11
|
+
import { getIcon, useCustomizationController } from "@rebasepro/app";
|
|
11
12
|
|
|
12
13
|
export function ViewModeSwitch({
|
|
13
14
|
value,
|
|
@@ -19,6 +20,14 @@ export function ViewModeSwitch({
|
|
|
19
20
|
className?: string;
|
|
20
21
|
}) {
|
|
21
22
|
|
|
23
|
+
const customizationController = useCustomizationController();
|
|
24
|
+
|
|
25
|
+
// Only app-registered views are offerable here. A view declared inline on
|
|
26
|
+
// a collection carries a `Builder` function, which the editor cannot write
|
|
27
|
+
// back into a config file — registering it on `<RebaseAdmin>` and naming
|
|
28
|
+
// the key is what makes it editable, the same bargain `entityViews` makes.
|
|
29
|
+
const registeredViews: CollectionCustomView[] = customizationController.collectionViews ?? [];
|
|
30
|
+
|
|
22
31
|
return <div className={cls(className)}>
|
|
23
32
|
<Typography variant={"label"} color={"secondary"} className={"ml-3.5"}>Default collection view</Typography>
|
|
24
33
|
<div className={"my-2"}>
|
|
@@ -45,7 +54,12 @@ export function ViewModeSwitch({
|
|
|
45
54
|
value: "kanban",
|
|
46
55
|
label: "Kanban",
|
|
47
56
|
icon: <KanbanIcon/>
|
|
48
|
-
}
|
|
57
|
+
},
|
|
58
|
+
...registeredViews.map((view) => ({
|
|
59
|
+
value: view.key,
|
|
60
|
+
label: view.name,
|
|
61
|
+
icon: getIcon(view.icon) ?? <ListIcon/>
|
|
62
|
+
}))
|
|
49
63
|
]}
|
|
50
64
|
/>
|
|
51
65
|
</div>
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { Suspense } from "react";
|
|
2
|
+
import { lazyChunk } from "@rebasepro/ui";
|
|
2
3
|
import { useCollectionEditorDialogsState } from "../collection_editor/CollectionEditorDialogsContext";
|
|
3
4
|
|
|
4
5
|
// Dynamic imports targeting the separate collection_editor_ui entry point.
|
|
5
6
|
// This ensures the heavy editor UI lands in its own chunk and is only fetched
|
|
6
7
|
// when a dialog is actually opened.
|
|
7
|
-
const CollectionEditorDialog =
|
|
8
|
+
const CollectionEditorDialog = lazyChunk(() =>
|
|
8
9
|
import("../collection_editor/ui/collection_editor/CollectionEditorDialog")
|
|
9
10
|
.then(m => ({ default: m.CollectionEditorDialog }))
|
|
10
11
|
);
|
|
11
12
|
|
|
12
|
-
const PropertyFormDialog =
|
|
13
|
+
const PropertyFormDialog = lazyChunk(() =>
|
|
13
14
|
import("../collection_editor/ui/collection_editor/PropertyEditView")
|
|
14
15
|
.then(m => ({ default: m.PropertyFormDialog }))
|
|
15
16
|
);
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
import React, {
|
|
3
|
+
import React, { Suspense } from "react";
|
|
4
4
|
|
|
5
5
|
import { useLargeLayout, useTranslation, useSlot, resolveComponentRef } from "@rebasepro/app";
|
|
6
6
|
import { CollectionActionsProps, EntityTableController, SelectionController, AdminCollection } from "@rebasepro/admin-types";
|
|
7
|
-
import { Button, IconButton, Tooltip, Popover, iconSize } from "@rebasepro/ui";
|
|
7
|
+
import { Button, IconButton, Tooltip, Popover, iconSize, lazyChunk } from "@rebasepro/ui";
|
|
8
8
|
import { ErrorBoundary, Link2Icon, MoreVerticalIcon, PlusIcon, Trash2Icon } from "@rebasepro/ui";
|
|
9
9
|
import { usePermissions } from "@rebasepro/app";
|
|
10
10
|
import { toArray } from "@rebasepro/utils";
|
|
11
11
|
// Lazy-load import/export — pulls in exceljs only on demand
|
|
12
|
-
const ImportCollectionAction =
|
|
13
|
-
const ExportCollectionAction =
|
|
12
|
+
const ImportCollectionAction = lazyChunk(() => import("../../data_import/import").then(m => ({ default: m.ImportCollectionAction })));
|
|
13
|
+
const ExportCollectionAction = lazyChunk(() => 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
16
|
import { useAdminContext } from "../../hooks/useAdminContext";
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
getCollectionDataPath
|
|
9
9
|
} from "@rebasepro/types";
|
|
10
10
|
import {
|
|
11
|
+
CollectionCustomViewParams,
|
|
11
12
|
CollectionSize,
|
|
12
13
|
EntityTableController,
|
|
13
14
|
PartialCollectionConfig,
|
|
@@ -23,7 +24,7 @@ import { getEntityChildViews } from "@rebasepro/common";
|
|
|
23
24
|
import { useCollectionInlineEditor } from "./hooks/useCollectionInlineEditor";
|
|
24
25
|
import { navigateToEntity } from "../../util/navigation_utils";
|
|
25
26
|
import { mergeEntityActions } from "../../util/entity_actions";
|
|
26
|
-
import { resolveEntityAction } from "../../util/resolutions";
|
|
27
|
+
import { resolveCollectionViews, resolveEntityAction } from "../../util/resolutions";
|
|
27
28
|
import { getPropertyInPath } from "../../util/property_utils";
|
|
28
29
|
import { ReferencePreview } from "../../preview";
|
|
29
30
|
import {
|
|
@@ -74,7 +75,7 @@ import { DeleteEntityDialog } from "../DeleteEntityDialog";
|
|
|
74
75
|
import { useSelectionController } from "./useSelectionController";
|
|
75
76
|
import { CollectionViewStartActions } from "./CollectionViewStartActions";
|
|
76
77
|
import { addRecentId, getRecentIds } from "./utils";
|
|
77
|
-
import { isViewMode, OpenEntityMode, resolveOpenEntityMode, VIEW_MODE_PARAM } from "../../util/view_mode";
|
|
78
|
+
import { isViewMode, OpenEntityMode, resolveOpenEntityMode, VIEW_MODE_PARAM, VIEW_MODES } from "../../util/view_mode";
|
|
78
79
|
import { mergeDeep } from "@rebasepro/utils";
|
|
79
80
|
import { useBreadcrumbsController } from "../../hooks/useBreadcrumbsController";
|
|
80
81
|
import { useAdminContext } from "../../hooks/useAdminContext";
|
|
@@ -83,7 +84,7 @@ import { useSidePanel } from "../../hooks/useSidePanel";
|
|
|
83
84
|
import { useUrlController } from "../../hooks/navigation/contexts/UrlContext";
|
|
84
85
|
import { useChildViewSource } from "../../hooks/useChildViewSource";
|
|
85
86
|
import { useSelectionDialog } from "../../hooks/useSelectionDialog";
|
|
86
|
-
import { saveEntityWithCallbacks } from "@rebasepro/app";
|
|
87
|
+
import { resolveComponentRef, saveEntityWithCallbacks } from "@rebasepro/app";
|
|
87
88
|
|
|
88
89
|
const EMPTY_ARRAY: never[] = [];
|
|
89
90
|
|
|
@@ -250,21 +251,47 @@ const CollectionViewBindingInner = React.memo(
|
|
|
250
251
|
|
|
251
252
|
const [popOverOpen, setPopOverOpen] = useState(false);
|
|
252
253
|
|
|
254
|
+
// Custom view modes this collection can render: the ones declared
|
|
255
|
+
// inline plus the ones it names by key from the app-level registry.
|
|
256
|
+
// Anything unresolvable is dropped here, once, so every consumer below
|
|
257
|
+
// — the switcher, `enabledViews`, the URL and saved-config checks —
|
|
258
|
+
// agrees on what exists.
|
|
259
|
+
const resolvedCustomViews = useMemo(
|
|
260
|
+
() => resolveCollectionViews(collection.customViews, customizationController.collectionViews),
|
|
261
|
+
[collection.customViews, customizationController]
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const customViewKeys = useMemo(
|
|
265
|
+
() => resolvedCustomViews.map((v) => v.key),
|
|
266
|
+
[resolvedCustomViews]
|
|
267
|
+
);
|
|
268
|
+
|
|
253
269
|
// View mode priority: URL > saved user config > collection.defaultViewMode
|
|
254
|
-
const defaultViewMode = collection.defaultViewMode
|
|
270
|
+
const defaultViewMode = isViewMode(collection.defaultViewMode, customViewKeys)
|
|
271
|
+
? collection.defaultViewMode
|
|
272
|
+
// Covers a `defaultViewMode` naming a custom view that has since
|
|
273
|
+
// been removed from config; without this the collection would open
|
|
274
|
+
// on a mode nothing renders.
|
|
275
|
+
: "list";
|
|
255
276
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
256
277
|
|
|
257
278
|
// Read view from React Router's searchParams (reactive on back/forward)
|
|
258
279
|
const urlView = useMemo((): ViewMode | null => {
|
|
259
280
|
const v = searchParams.get(VIEW_MODE_PARAM);
|
|
260
|
-
return isViewMode(v) ? v : null;
|
|
261
|
-
}, [searchParams]);
|
|
262
|
-
|
|
263
|
-
// Get saved view from local persistence
|
|
281
|
+
return isViewMode(v, customViewKeys) ? v : null;
|
|
282
|
+
}, [searchParams, customViewKeys]);
|
|
283
|
+
|
|
284
|
+
// Get saved view from local persistence.
|
|
285
|
+
//
|
|
286
|
+
// Validated rather than cast: what is stored is whatever the user last
|
|
287
|
+
// picked, and a custom view removed from config since then would come
|
|
288
|
+
// back as a mode nothing renders — a blank panel with a working
|
|
289
|
+
// toolbar above it. An unrecognised value reads as "nothing saved",
|
|
290
|
+
// which falls through to the collection default.
|
|
264
291
|
const getSavedView = useCallback((): ViewMode | null => {
|
|
265
292
|
const saved = userConfigPersistence?.getCollectionConfig<M>(path)?.defaultViewMode;
|
|
266
|
-
return (saved
|
|
267
|
-
}, [userConfigPersistence, path]);
|
|
293
|
+
return isViewMode(saved, customViewKeys) ? saved : null;
|
|
294
|
+
}, [userConfigPersistence, path, customViewKeys]);
|
|
268
295
|
|
|
269
296
|
const [viewMode, setViewModeState] = useState<ViewMode>(() => {
|
|
270
297
|
// Priority: URL > saved config > collection default
|
|
@@ -274,9 +301,15 @@ const CollectionViewBindingInner = React.memo(
|
|
|
274
301
|
return defaultViewMode;
|
|
275
302
|
});
|
|
276
303
|
|
|
304
|
+
const activeCustomView = useMemo(
|
|
305
|
+
() => resolvedCustomViews.find((v) => v.key === viewMode),
|
|
306
|
+
[resolvedCustomViews, viewMode]
|
|
307
|
+
);
|
|
308
|
+
|
|
277
309
|
const openEntityMode = resolveOpenEntityMode({
|
|
278
310
|
collection,
|
|
279
|
-
viewMode
|
|
311
|
+
viewMode,
|
|
312
|
+
customView: activeCustomView
|
|
280
313
|
});
|
|
281
314
|
|
|
282
315
|
// Sync URL with current view on init (if view came from saved config)
|
|
@@ -335,6 +368,12 @@ const CollectionViewBindingInner = React.memo(
|
|
|
335
368
|
// Table view size state - controls row height
|
|
336
369
|
const [tableSize, setTableSize] = useState<CollectionSize>(collection.defaultSize ?? "m");
|
|
337
370
|
|
|
371
|
+
// Size for custom views that opted in with `sizeable`. One piece of
|
|
372
|
+
// state for all of them: only the active view reads it, and switching
|
|
373
|
+
// between two sizeable custom views keeping the same density is the
|
|
374
|
+
// less surprising behaviour.
|
|
375
|
+
const [customViewSize, setCustomViewSize] = useState<CollectionSize>(collection.defaultSize ?? "m");
|
|
376
|
+
|
|
338
377
|
const selectionController = useSelectionController<M>();
|
|
339
378
|
const usedSelectionController = collection.selectionController ?? selectionController;
|
|
340
379
|
const {
|
|
@@ -513,7 +552,7 @@ parentEntityIds: parentEntityIds ?? EMPTY_ARRAY,
|
|
|
513
552
|
onNewClick
|
|
514
553
|
});
|
|
515
554
|
|
|
516
|
-
const
|
|
555
|
+
const pluginWidgets = useSlot("collection.widgets", {
|
|
517
556
|
path,
|
|
518
557
|
parentCollectionSlugs: parentCollectionSlugs ?? EMPTY_ARRAY,
|
|
519
558
|
parentEntityIds: parentEntityIds ?? EMPTY_ARRAY,
|
|
@@ -587,19 +626,26 @@ parentEntityIds: parentEntityIds ?? EMPTY_ARRAY,
|
|
|
587
626
|
}, [collection.kanban?.columnProperty, resolvedCollection.properties]);
|
|
588
627
|
|
|
589
628
|
// Compute the effective enabled views:
|
|
590
|
-
// - Start from collection.enabledViews (defaults to
|
|
629
|
+
// - Start from collection.enabledViews (defaults to every built-in
|
|
630
|
+
// plus every custom view the collection declared)
|
|
631
|
+
// - Drop anything that resolves to nothing renderable
|
|
591
632
|
// - Filter out kanban if no enum properties exist
|
|
592
633
|
const hasEnumProperty = useMemo(() => {
|
|
593
634
|
return Object.values(resolvedCollection.properties).some((p: Property) => p.type === "string" && "enum" in p && p.enum);
|
|
594
635
|
}, [resolvedCollection.properties]);
|
|
595
636
|
|
|
596
637
|
const enabledViews: ViewMode[] = useMemo(() => {
|
|
597
|
-
|
|
638
|
+
// Declaring a custom view is enough to offer it — a collection
|
|
639
|
+
// should not have to restate the built-ins in `enabledViews` just
|
|
640
|
+
// to add one. Setting `enabledViews` explicitly still wins.
|
|
641
|
+
const configured = collection.enabledViews
|
|
642
|
+
?? [...VIEW_MODES, ...customViewKeys];
|
|
643
|
+
const renderable = configured.filter(v => isViewMode(v, customViewKeys));
|
|
598
644
|
if (!hasEnumProperty) {
|
|
599
|
-
return
|
|
645
|
+
return renderable.filter(v => v !== "kanban");
|
|
600
646
|
}
|
|
601
|
-
return
|
|
602
|
-
}, [collection.enabledViews, hasEnumProperty]);
|
|
647
|
+
return renderable;
|
|
648
|
+
}, [collection.enabledViews, hasEnumProperty, customViewKeys]);
|
|
603
649
|
|
|
604
650
|
// Compute available enum properties for kanban column selection
|
|
605
651
|
const kanbanPropertyOptions: KanbanPropertyOption[] = useMemo(() => {
|
|
@@ -938,8 +984,9 @@ parentEntityIds: parentEntityIds ?? EMPTY_ARRAY
|
|
|
938
984
|
viewMode={viewMode}
|
|
939
985
|
onViewModeChange={onViewModeChange}
|
|
940
986
|
enabledViews={enabledViews}
|
|
941
|
-
|
|
942
|
-
|
|
987
|
+
customViews={resolvedCustomViews}
|
|
988
|
+
size={activeCustomView ? (activeCustomView.sizeable ? customViewSize : undefined) : viewMode === "list" ? listSize : viewMode === "table" ? tableSize : viewMode === "cards" ? cardSize : undefined}
|
|
989
|
+
onSizeChanged={activeCustomView ? (activeCustomView.sizeable ? setCustomViewSize : undefined) : viewMode === "list" ? onListSizeChanged : viewMode === "table" ? onTableSizeChanged : viewMode === "cards" ? setCardSize : undefined}
|
|
943
990
|
open={viewModePopoverOpen}
|
|
944
991
|
onOpenChange={setViewModePopoverOpen}
|
|
945
992
|
kanbanPropertyOptions={kanbanPropertyOptions}
|
|
@@ -1014,7 +1061,37 @@ parentEntityIds,
|
|
|
1014
1061
|
</ResolvedCollectionActions>
|
|
1015
1062
|
}
|
|
1016
1063
|
/>
|
|
1017
|
-
);
|
|
1064
|
+
);
|
|
1065
|
+
|
|
1066
|
+
// A custom view wins over the built-in chain, but only after the
|
|
1067
|
+
// built-in keys have been ruled out — `resolveCollectionViews` refuses
|
|
1068
|
+
// a view keyed "table", so this cannot shadow one.
|
|
1069
|
+
const CustomViewBuilder = activeCustomView
|
|
1070
|
+
? resolveComponentRef<CollectionCustomViewParams<M>>(activeCustomView.Builder)
|
|
1071
|
+
: undefined;
|
|
1072
|
+
|
|
1073
|
+
const innerView = CustomViewBuilder ? (
|
|
1074
|
+
<ErrorBoundary>
|
|
1075
|
+
<CustomViewBuilder
|
|
1076
|
+
key={`custom-view-${activeCustomView!.key}-${path}`}
|
|
1077
|
+
collection={collection}
|
|
1078
|
+
tableController={tableController}
|
|
1079
|
+
path={path}
|
|
1080
|
+
parentCollectionSlugs={parentCollectionSlugs}
|
|
1081
|
+
parentEntityIds={parentEntityIds}
|
|
1082
|
+
onEntityClick={onEntityClick}
|
|
1083
|
+
onNewClick={canCreateEntities ? onNewClick : undefined}
|
|
1084
|
+
canCreate={canCreateEntities}
|
|
1085
|
+
selectionController={usedSelectionController}
|
|
1086
|
+
selectionEnabled={selectionEnabled}
|
|
1087
|
+
highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
|
|
1088
|
+
deletedEntities={deletedEntities}
|
|
1089
|
+
emptyComponent={emptyComponent}
|
|
1090
|
+
size={activeCustomView!.sizeable ? customViewSize : undefined}
|
|
1091
|
+
viewMode={viewMode}
|
|
1092
|
+
/>
|
|
1093
|
+
</ErrorBoundary>
|
|
1094
|
+
) : viewMode === "kanban" ? (
|
|
1018
1095
|
<CollectionBoardViewBinding
|
|
1019
1096
|
key={`kanban-view-${path}-${selectedKanbanProperty}`}
|
|
1020
1097
|
collection={collection}
|
|
@@ -1124,7 +1201,7 @@ parentEntityIds,
|
|
|
1124
1201
|
</Typography>
|
|
1125
1202
|
</div>
|
|
1126
1203
|
</div>
|
|
1127
|
-
{
|
|
1204
|
+
{pluginWidgets.length > 0 && (
|
|
1128
1205
|
<div
|
|
1129
1206
|
className={cls(
|
|
1130
1207
|
"grid transition-[grid-template-rows] duration-150 ease-out",
|
|
@@ -1132,7 +1209,7 @@ parentEntityIds,
|
|
|
1132
1209
|
)}
|
|
1133
1210
|
>
|
|
1134
1211
|
<div className="overflow-hidden flex-shrink-0">
|
|
1135
|
-
{
|
|
1212
|
+
{pluginWidgets}
|
|
1136
1213
|
</div>
|
|
1137
1214
|
</div>
|
|
1138
1215
|
)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import React, { useMemo } from "react";
|
|
3
|
-
import { CollectionSize, ViewMode } from "@rebasepro/admin-types";
|
|
3
|
+
import { CollectionCustomView, CollectionSize, ViewMode } from "@rebasepro/admin-types";
|
|
4
4
|
import {
|
|
5
5
|
Button,
|
|
6
6
|
ColumnsIcon,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
ToggleButtonGroup,
|
|
16
16
|
ToggleButtonOption
|
|
17
17
|
} from "@rebasepro/ui";
|
|
18
|
-
import { useTranslation } from "@rebasepro/app";
|
|
18
|
+
import { getIcon, useTranslation } from "@rebasepro/app";
|
|
19
19
|
|
|
20
20
|
export type KanbanPropertyOption = {
|
|
21
21
|
key: string;
|
|
@@ -26,11 +26,17 @@ export type ViewModeToggleProps = {
|
|
|
26
26
|
viewMode?: ViewMode;
|
|
27
27
|
onViewModeChange?: (mode: ViewMode) => void;
|
|
28
28
|
/**
|
|
29
|
-
* Which view modes are enabled for this collection
|
|
30
|
-
* Only these modes
|
|
31
|
-
* Defaults to
|
|
29
|
+
* Which view modes are enabled for this collection, in the order they
|
|
30
|
+
* should appear. Only these modes appear in the toggle.
|
|
31
|
+
* Defaults to the four built-ins.
|
|
32
32
|
*/
|
|
33
33
|
enabledViews?: ViewMode[];
|
|
34
|
+
/**
|
|
35
|
+
* Custom views this collection can render, already resolved. Their `key`s
|
|
36
|
+
* appear in `enabledViews` alongside the built-ins, and each supplies the
|
|
37
|
+
* name and icon its entry is drawn with.
|
|
38
|
+
*/
|
|
39
|
+
customViews?: CollectionCustomView<any>[];
|
|
34
40
|
/**
|
|
35
41
|
* Current size for card/table views
|
|
36
42
|
*/
|
|
@@ -67,6 +73,7 @@ export function ViewModeToggle({
|
|
|
67
73
|
viewMode = "table",
|
|
68
74
|
onViewModeChange,
|
|
69
75
|
enabledViews = ALL_VIEW_MODES,
|
|
76
|
+
customViews,
|
|
70
77
|
size,
|
|
71
78
|
onSizeChanged,
|
|
72
79
|
open,
|
|
@@ -85,7 +92,16 @@ export function ViewModeToggle({
|
|
|
85
92
|
// labelled buttons open the toolbar together, and at 16 against its 20 this
|
|
86
93
|
// one read as the smaller of the pair. The options inside the popover stay
|
|
87
94
|
// at `smallest`, which is the size every other menu icon uses.
|
|
95
|
+
const activeCustomView = customViews?.find((v) => v.key === viewMode);
|
|
96
|
+
|
|
88
97
|
const getViewModeIcon = () => {
|
|
98
|
+
if (activeCustomView) {
|
|
99
|
+
// Falls back to the list glyph rather than rendering nothing, so a
|
|
100
|
+
// custom view that declared no icon still gets a trigger the same
|
|
101
|
+
// width as every other mode's.
|
|
102
|
+
return getIcon(activeCustomView.icon, undefined, undefined, "small")
|
|
103
|
+
?? <ListIcon size={iconSize.small}/>;
|
|
104
|
+
}
|
|
89
105
|
if (viewMode === "kanban") return <KanbanIcon size={iconSize.small}/>;
|
|
90
106
|
if (viewMode === "cards") return <LayoutGridIcon size={iconSize.small}/>;
|
|
91
107
|
if (viewMode === "table") return <TableIcon size={iconSize.small}/>;
|
|
@@ -93,51 +109,69 @@ export function ViewModeToggle({
|
|
|
93
109
|
};
|
|
94
110
|
|
|
95
111
|
const getViewModeName = () => {
|
|
112
|
+
// The declared name, untranslated — it is app-supplied copy, and there
|
|
113
|
+
// is no key for it in the admin's bundles.
|
|
114
|
+
if (activeCustomView) return activeCustomView.name;
|
|
96
115
|
if (viewMode === "kanban") return t("board");
|
|
97
116
|
if (viewMode === "cards") return t("cards");
|
|
98
117
|
if (viewMode === "table") return t("table_view_mode");
|
|
99
118
|
return t("list");
|
|
100
119
|
};
|
|
101
120
|
|
|
102
|
-
const showSizeSelector = size && onSizeChanged &&
|
|
121
|
+
const showSizeSelector = size && onSizeChanged &&
|
|
122
|
+
(activeCustomView
|
|
123
|
+
? activeCustomView.sizeable
|
|
124
|
+
: viewMode === "list" || viewMode === "table" || viewMode === "cards");
|
|
103
125
|
const showKanbanPropertySelector = viewMode === "kanban" &&
|
|
104
126
|
kanbanPropertyOptions &&
|
|
105
127
|
kanbanPropertyOptions.length > 0 &&
|
|
106
128
|
onKanbanPropertyChange;
|
|
107
129
|
|
|
108
|
-
// Build toggle options
|
|
130
|
+
// Build toggle options, in `enabledViews` order.
|
|
131
|
+
//
|
|
132
|
+
// This used to return all four built-ins unconditionally: `enabledViews`
|
|
133
|
+
// was destructured and then read nowhere, so a collection that asked for
|
|
134
|
+
// `["list", "table"]` still offered cards and kanban, and picking one
|
|
135
|
+
// rendered it. Honouring the prop is what the doc comment always claimed,
|
|
136
|
+
// and it is what lets a custom-only `enabledViews` hide the switcher.
|
|
109
137
|
const viewModeOptions: ToggleButtonOption<ViewMode>[] = useMemo(() => {
|
|
110
|
-
const
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return allOptions;
|
|
134
|
-
}, [t]);
|
|
138
|
+
const builtIns: Record<string, { label: string, icon: React.ReactNode }> = {
|
|
139
|
+
list: { label: t("list"), icon: <ListIcon size={iconSize.smallest}/> },
|
|
140
|
+
table: { label: t("table_view_mode"), icon: <TableIcon size={iconSize.smallest}/> },
|
|
141
|
+
cards: { label: t("cards"), icon: <LayoutGridIcon size={iconSize.smallest}/> },
|
|
142
|
+
kanban: { label: t("board"), icon: <KanbanIcon size={iconSize.smallest}/> }
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return enabledViews.flatMap((mode) => {
|
|
146
|
+
const builtIn = builtIns[mode];
|
|
147
|
+
if (builtIn) return [{ value: mode, ...builtIn }];
|
|
148
|
+
|
|
149
|
+
const custom = customViews?.find((v) => v.key === mode);
|
|
150
|
+
// An `enabledViews` entry naming nothing registered is skipped
|
|
151
|
+
// rather than drawn as a nameless button.
|
|
152
|
+
if (!custom) return [];
|
|
153
|
+
return [{
|
|
154
|
+
value: mode,
|
|
155
|
+
label: custom.name,
|
|
156
|
+
icon: getIcon(custom.icon, undefined, undefined, "smallest")
|
|
157
|
+
?? <ListIcon size={iconSize.smallest}/>
|
|
158
|
+
}];
|
|
159
|
+
});
|
|
160
|
+
}, [t, enabledViews, customViews]);
|
|
135
161
|
|
|
136
162
|
// ── Guard (after hooks to preserve Rules of Hooks) ──────────────
|
|
137
163
|
if (!onViewModeChange) {
|
|
138
164
|
return null;
|
|
139
165
|
}
|
|
140
166
|
|
|
167
|
+
// Nothing to switch between and nothing else in the popover: a trigger
|
|
168
|
+
// that opens an empty panel is worse than no trigger. The size and
|
|
169
|
+
// group-by selectors live behind the same button, so a single-mode
|
|
170
|
+
// collection that still has one of those keeps it.
|
|
171
|
+
if (viewModeOptions.length <= 1 && !showSizeSelector && !showKanbanPropertySelector) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
141
175
|
return (
|
|
142
176
|
<div className="overflow-visible">
|
|
143
177
|
<Popover
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AdminCollection } from "@rebasepro/admin-types";
|
|
2
2
|
import type { Entity } from "@rebasepro/types";
|
|
3
3
|
import type { FormContext } from "../types/fields";
|
|
4
|
-
import React, {
|
|
4
|
+
import React, { Suspense, useEffect } from "react";
|
|
5
5
|
import {
|
|
6
6
|
CircularProgressCenter,
|
|
7
7
|
cls,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
ErrorBoundary,
|
|
10
10
|
IconButton,
|
|
11
11
|
iconSize,
|
|
12
|
+
lazyChunk,
|
|
12
13
|
Tab,
|
|
13
14
|
Tabs,
|
|
14
15
|
XIcon
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
import { useTranslation } from "@rebasepro/app";
|
|
17
18
|
import { JsonPreviewBinding } from "./JsonPreviewBinding";
|
|
18
19
|
|
|
19
|
-
const EntityHistoryView =
|
|
20
|
+
const EntityHistoryView = lazyChunk(() => import("./history").then(m => ({ default: m.EntityHistoryView })));
|
|
20
21
|
|
|
21
22
|
export type InspectorTab = "json" | "history";
|
|
22
23
|
|
|
@@ -76,7 +76,7 @@ export function NavigationCardBinding({
|
|
|
76
76
|
context
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
-
const
|
|
79
|
+
const pluginCardWidgets = useSlot("home.card.widget", {
|
|
80
80
|
slug: slug,
|
|
81
81
|
collection: collection!,
|
|
82
82
|
context
|
|
@@ -92,7 +92,7 @@ export function NavigationCardBinding({
|
|
|
92
92
|
name={name}
|
|
93
93
|
description={description}
|
|
94
94
|
actions={actions}
|
|
95
|
-
additionalContent={
|
|
95
|
+
additionalContent={pluginCardWidgets.length > 0 ? <>{pluginCardWidgets}</> : undefined}
|
|
96
96
|
onClick={() => {
|
|
97
97
|
onClick?.();
|
|
98
98
|
navigate(url);
|
|
@@ -10,7 +10,7 @@ import type { RebaseAdminConfig } from "@rebasepro/admin-types";
|
|
|
10
10
|
* is auto-wired as a native feature (slots, provider, Studio view) without
|
|
11
11
|
* needing any external plugin.
|
|
12
12
|
*/
|
|
13
|
-
export function RebaseAdmin({ collections, views, homePage, entityViews, entityActions, collectionEditor, navigationGroupMappings, basePath }: RebaseAdminConfig) {
|
|
13
|
+
export function RebaseAdmin({ collections, views, homePage, entityViews, collectionViews, entityActions, collectionEditor, navigationGroupMappings, basePath }: RebaseAdminConfig) {
|
|
14
14
|
const dispatch = useRebaseRegistryDispatch();
|
|
15
15
|
|
|
16
16
|
useLayoutEffect(() => {
|
|
@@ -18,12 +18,13 @@ export function RebaseAdmin({ collections, views, homePage, entityViews, entityA
|
|
|
18
18
|
views,
|
|
19
19
|
homePage,
|
|
20
20
|
entityViews,
|
|
21
|
+
collectionViews,
|
|
21
22
|
entityActions,
|
|
22
23
|
collectionEditor,
|
|
23
24
|
navigationGroupMappings,
|
|
24
25
|
basePath });
|
|
25
26
|
return () => dispatch.unregisterAdmin();
|
|
26
|
-
}, [dispatch, collections, views, homePage, entityViews, entityActions, collectionEditor, navigationGroupMappings, basePath]);
|
|
27
|
+
}, [dispatch, collections, views, homePage, entityViews, collectionViews, entityActions, collectionEditor, navigationGroupMappings, basePath]);
|
|
27
28
|
|
|
28
29
|
return null;
|
|
29
30
|
}
|