@firecms/core 3.0.0-canary.257 → 3.0.0-canary.259
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/core/EntityEditViewFormActions.d.ts +1 -1
- package/dist/form/EntityFormActions.d.ts +4 -2
- package/dist/index.es.js +340 -156
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +340 -156
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collections.d.ts +4 -1
- package/dist/types/customization_controller.d.ts +8 -0
- package/dist/types/entity_actions.d.ts +45 -6
- package/dist/types/firecms.d.ts +8 -0
- package/dist/types/plugins.d.ts +8 -1
- package/dist/util/resolutions.d.ts +2 -1
- package/package.json +5 -5
- package/src/components/ConfirmationDialog.tsx +1 -0
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -0
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +2 -2
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +5 -2
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +3 -2
- package/src/components/HomePage/DefaultHomePage.tsx +48 -33
- package/src/components/HomePage/HomePageDnD.tsx +22 -36
- package/src/components/HomePage/RenameGroupDialog.tsx +6 -2
- package/src/components/UnsavedChangesDialog.tsx +6 -2
- package/src/components/common/default_entity_actions.tsx +18 -5
- package/src/components/common/useDataSourceTableController.tsx +1 -1
- package/src/core/EntityEditView.tsx +35 -12
- package/src/core/EntityEditViewFormActions.tsx +154 -29
- package/src/core/EntitySidePanel.tsx +1 -1
- package/src/core/FireCMS.tsx +2 -0
- package/src/form/EntityForm.tsx +32 -6
- package/src/form/EntityFormActions.tsx +37 -8
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +4 -2
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +1 -1
- package/src/types/collections.ts +4 -1
- package/src/types/customization_controller.tsx +9 -0
- package/src/types/entity_actions.tsx +56 -6
- package/src/types/firecms.tsx +9 -0
- package/src/types/plugins.tsx +9 -1
- package/src/util/join_collections.ts +3 -1
- package/src/util/resolutions.ts +13 -1
|
@@ -173,8 +173,11 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
|
|
|
173
173
|
* }
|
|
174
174
|
* }
|
|
175
175
|
* ```
|
|
176
|
+
*
|
|
177
|
+
* You can also pass the action as a string that represents the `key`, in which case it will
|
|
178
|
+
* use the action defined in the main configuration under `entityActions`.
|
|
176
179
|
*/
|
|
177
|
-
entityActions?: EntityAction<M, USER>[];
|
|
180
|
+
entityActions?: (EntityAction<M, USER> | string)[];
|
|
178
181
|
/**
|
|
179
182
|
* Pass your own selection controller if you want to control selected
|
|
180
183
|
* entities externally.
|
|
@@ -4,6 +4,7 @@ import { FireCMSPlugin } from "./plugins";
|
|
|
4
4
|
import { EntityCustomView } from "./collections";
|
|
5
5
|
import { Locale } from "./locales";
|
|
6
6
|
import { PropertyConfig } from "./property_config";
|
|
7
|
+
import { EntityAction } from "./entity_actions";
|
|
7
8
|
export type CustomizationController = {
|
|
8
9
|
/**
|
|
9
10
|
* Builder for generating utility links for entities
|
|
@@ -21,6 +22,13 @@ export type CustomizationController = {
|
|
|
21
22
|
* You can also define an entity view from the UI.
|
|
22
23
|
*/
|
|
23
24
|
entityViews?: EntityCustomView[];
|
|
25
|
+
/**
|
|
26
|
+
* List of actions that can be performed on entities.
|
|
27
|
+
* These actions are displayed in the entity view and in the collection view.
|
|
28
|
+
* You can later reuse these actions in the `entityActions` prop of a collection,
|
|
29
|
+
* by specifying the `key` of the action.
|
|
30
|
+
*/
|
|
31
|
+
entityActions?: EntityAction<any, any>[];
|
|
24
32
|
/**
|
|
25
33
|
* Format of the dates in the CMS.
|
|
26
34
|
* Defaults to 'MMMM dd, yyyy, HH:mm:ss'
|
|
@@ -4,6 +4,7 @@ import { Entity } from "./entities";
|
|
|
4
4
|
import { EntityCollection, SelectionController } from "./collections";
|
|
5
5
|
import { User } from "./user";
|
|
6
6
|
import { SideEntityController } from "./side_entity_controller";
|
|
7
|
+
import { FormContext } from "./fields";
|
|
7
8
|
/**
|
|
8
9
|
* An entity action is a custom action that can be performed on an entity.
|
|
9
10
|
* They are displayed in the entity view and in the collection view.
|
|
@@ -15,7 +16,10 @@ export type EntityAction<M extends object = any, USER extends User = User> = {
|
|
|
15
16
|
name: string;
|
|
16
17
|
/**
|
|
17
18
|
* Key of the action. You only need to provide this if you want to
|
|
18
|
-
* override the default actions
|
|
19
|
+
* override the default actions, or if you are not passing the action
|
|
20
|
+
* directly to the `entityActions` prop of a collection.
|
|
21
|
+
* You can define your actions at the app level, in which case you
|
|
22
|
+
* must provide a key.
|
|
19
23
|
* The default actions are:
|
|
20
24
|
* - edit
|
|
21
25
|
* - delete
|
|
@@ -31,6 +35,11 @@ export type EntityAction<M extends object = any, USER extends User = User> = {
|
|
|
31
35
|
* @param props
|
|
32
36
|
*/
|
|
33
37
|
onClick: (props: EntityActionClickProps<M, USER>) => Promise<void> | void;
|
|
38
|
+
/**
|
|
39
|
+
* Optional callback in case you want to disable the action
|
|
40
|
+
* @param props
|
|
41
|
+
*/
|
|
42
|
+
isEnabled?: (props: EntityActionClickProps<M, USER>) => boolean;
|
|
34
43
|
/**
|
|
35
44
|
* Show this action collapsed in the menu of the collection view.
|
|
36
45
|
* Defaults to true
|
|
@@ -43,18 +52,48 @@ export type EntityAction<M extends object = any, USER extends User = User> = {
|
|
|
43
52
|
includeInForm?: boolean;
|
|
44
53
|
};
|
|
45
54
|
export type EntityActionClickProps<M extends object, USER extends User = User> = {
|
|
46
|
-
entity
|
|
55
|
+
entity?: Entity<M>;
|
|
47
56
|
context: FireCMSContext<USER>;
|
|
48
57
|
fullPath?: string;
|
|
49
58
|
fullIdPath?: string;
|
|
50
59
|
collection?: EntityCollection<M>;
|
|
60
|
+
/**
|
|
61
|
+
* Optional form context, present if the action is being called from a form.
|
|
62
|
+
* This allows you to access the form state and methods, including modifying the form values.
|
|
63
|
+
*/
|
|
64
|
+
formContext?: FormContext;
|
|
65
|
+
/**
|
|
66
|
+
* Present if this actions is being called from a side dialog only
|
|
67
|
+
*/
|
|
68
|
+
sideEntityController?: SideEntityController;
|
|
69
|
+
/**
|
|
70
|
+
* Is the action being called from the collection view or from the entity form view?
|
|
71
|
+
*/
|
|
72
|
+
view: "collection" | "form";
|
|
73
|
+
/**
|
|
74
|
+
* If the action is rendered in the form, is it open in a side panel or full screen?
|
|
75
|
+
*/
|
|
76
|
+
openEntityMode: "side_panel" | "full_screen";
|
|
77
|
+
/**
|
|
78
|
+
* Optional selection controller, present if the action is being called from a collection view
|
|
79
|
+
*/
|
|
51
80
|
selectionController?: SelectionController;
|
|
81
|
+
/**
|
|
82
|
+
* Optional highlight function to highlight the entity in the collection view
|
|
83
|
+
* @param entity
|
|
84
|
+
*/
|
|
52
85
|
highlightEntity?: (entity: Entity<any>) => void;
|
|
86
|
+
/**
|
|
87
|
+
* Optional unhighlight function to remove the highlight from the entity in the collection view
|
|
88
|
+
* @param entity
|
|
89
|
+
*/
|
|
53
90
|
unhighlightEntity?: (entity: Entity<any>) => void;
|
|
54
|
-
onCollectionChange?: () => void;
|
|
55
91
|
/**
|
|
56
|
-
*
|
|
92
|
+
* Optional function to navigate back (e.g. when deleting an entity or navigating from a form)
|
|
57
93
|
*/
|
|
58
|
-
|
|
59
|
-
|
|
94
|
+
navigateBack?: () => void;
|
|
95
|
+
/**
|
|
96
|
+
* Callback to be called when the collection changes, e.g. after an entity is deleted or created.
|
|
97
|
+
*/
|
|
98
|
+
onCollectionChange?: () => void;
|
|
60
99
|
};
|
package/dist/types/firecms.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { EntityLinkBuilder } from "./entity_link_builder";
|
|
|
12
12
|
import { UserConfigurationPersistence } from "./local_config_persistence";
|
|
13
13
|
import { FireCMSPlugin } from "./plugins";
|
|
14
14
|
import { CMSAnalyticsEvent } from "./analytics";
|
|
15
|
+
import { EntityAction } from "./entity_actions";
|
|
15
16
|
/**
|
|
16
17
|
* Use this callback to build entity collections dynamically.
|
|
17
18
|
* You can use the user to decide which collections to show.
|
|
@@ -85,6 +86,13 @@ export type FireCMSProps<USER extends User> = {
|
|
|
85
86
|
* You can also define an entity view from the UI.
|
|
86
87
|
*/
|
|
87
88
|
entityViews?: EntityCustomView[];
|
|
89
|
+
/**
|
|
90
|
+
* List of actions that can be performed on entities.
|
|
91
|
+
* These actions are displayed in the entity view and in the collection view.
|
|
92
|
+
* You can later reuse these actions in the `entityActions` prop of a collection,
|
|
93
|
+
* by specifying the `key` of the action.
|
|
94
|
+
*/
|
|
95
|
+
entityActions?: EntityAction[];
|
|
88
96
|
/**
|
|
89
97
|
* Format of the dates in the CMS.
|
|
90
98
|
* Defaults to 'MMMM dd, yyyy, HH:mm:ss'
|
package/dist/types/plugins.d.ts
CHANGED
|
@@ -139,7 +139,14 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
|
|
|
139
139
|
Component: React.ComponentType<PropsWithChildren<FORM_PROPS & PluginFormActionProps<any, EC>>>;
|
|
140
140
|
props?: FORM_PROPS;
|
|
141
141
|
};
|
|
142
|
+
/**
|
|
143
|
+
* Add custom actions to the default ones ("Save", "Discard"...)
|
|
144
|
+
*/
|
|
142
145
|
Actions?: React.ComponentType<PluginFormActionProps<any, EC>>;
|
|
146
|
+
/**
|
|
147
|
+
* Add custom actions to the top of the form
|
|
148
|
+
*/
|
|
149
|
+
ActionsTop?: React.ComponentType<PluginFormActionProps<any, EC>>;
|
|
143
150
|
fieldBuilder?: <T extends CMSType = CMSType>(props: PluginFieldBuilderParams<T, any, EC>) => React.ComponentType<FieldProps<T>> | null;
|
|
144
151
|
fieldBuilderEnabled?: <T extends CMSType = CMSType>(props: PluginFieldBuilderParams<T>) => boolean;
|
|
145
152
|
};
|
|
@@ -181,12 +188,12 @@ export interface PluginHomePageActionsProps<EP extends object = object, M extend
|
|
|
181
188
|
export interface PluginFormActionProps<USER extends User = User, EC extends EntityCollection = EntityCollection> {
|
|
182
189
|
entityId?: string;
|
|
183
190
|
path: string;
|
|
191
|
+
parentCollectionIds: string[];
|
|
184
192
|
status: EntityStatus;
|
|
185
193
|
collection: EC;
|
|
186
194
|
disabled: boolean;
|
|
187
195
|
formContext?: FormContext<any>;
|
|
188
196
|
context: FireCMSContext<USER>;
|
|
189
|
-
currentEntityId?: string;
|
|
190
197
|
openEntityMode: "side_panel" | "full_screen";
|
|
191
198
|
}
|
|
192
199
|
export type PluginFieldBuilderParams<T extends CMSType = CMSType, M extends Record<string, any> = any, EC extends EntityCollection<M> = EntityCollection<M>> = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ArrayProperty, AuthController, CMSType, CustomizationController, EntityCollection, EntityCustomView, EntityValues, EnumValueConfig, EnumValues, NumberProperty, PropertiesOrBuilders, PropertyConfig, PropertyOrBuilder, ResolvedArrayProperty, ResolvedEntityCollection, ResolvedNumberProperty, ResolvedProperties, ResolvedProperty, ResolvedStringProperty, StringProperty, UserConfigurationPersistence } from "../types";
|
|
1
|
+
import { ArrayProperty, AuthController, CMSType, CustomizationController, EntityAction, EntityCollection, EntityCustomView, EntityValues, EnumValueConfig, EnumValues, NumberProperty, PropertiesOrBuilders, PropertyConfig, PropertyOrBuilder, ResolvedArrayProperty, ResolvedEntityCollection, ResolvedNumberProperty, ResolvedProperties, ResolvedProperty, ResolvedStringProperty, StringProperty, UserConfigurationPersistence } from "../types";
|
|
2
2
|
export declare const resolveCollection: <M extends Record<string, any>>({ collection, path, entityId, values, previousValues, userConfigPersistence, propertyConfigs, ignoreMissingFields, authController }: {
|
|
3
3
|
collection: EntityCollection<M> | ResolvedEntityCollection<M>;
|
|
4
4
|
path: string;
|
|
@@ -81,6 +81,7 @@ export declare function resolveProperties<M extends Record<string, any>>({ prope
|
|
|
81
81
|
export declare function resolvePropertyEnum(property: StringProperty | NumberProperty, fromBuilder?: boolean): ResolvedStringProperty | ResolvedNumberProperty;
|
|
82
82
|
export declare function resolveEnumValues(input: EnumValues): EnumValueConfig[] | undefined;
|
|
83
83
|
export declare function resolveEntityView(entityView: string | EntityCustomView<any>, contextEntityViews?: EntityCustomView<any>[]): EntityCustomView<any> | undefined;
|
|
84
|
+
export declare function resolveEntityAction<M extends Record<string, any>>(entityAction: string | EntityAction<M>, contextEntityActions?: EntityAction<M>[]): EntityAction<M> | undefined;
|
|
84
85
|
export declare function resolvedSelectedEntityView<M extends Record<string, any>>(customViews: (string | EntityCustomView<M>)[] | undefined, customizationController: CustomizationController, selectedTab?: string, canEdit?: boolean): {
|
|
85
86
|
resolvedEntityViews: EntityCustomView<M>[];
|
|
86
87
|
selectedEntityView: EntityCustomView<M> | undefined;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firecms/core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.0.0-canary.
|
|
4
|
+
"version": "3.0.0-canary.259",
|
|
5
5
|
"description": "Awesome Firebase/Firestore-based headless open-source CMS",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/firecmsco"
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"@dnd-kit/core": "^6.3.1",
|
|
54
54
|
"@dnd-kit/modifiers": "^9.0.0",
|
|
55
55
|
"@dnd-kit/sortable": "^10.0.0",
|
|
56
|
-
"@firecms/editor": "^3.0.0-canary.
|
|
57
|
-
"@firecms/formex": "^3.0.0-canary.
|
|
58
|
-
"@firecms/ui": "^3.0.0-canary.
|
|
56
|
+
"@firecms/editor": "^3.0.0-canary.259",
|
|
57
|
+
"@firecms/formex": "^3.0.0-canary.259",
|
|
58
|
+
"@firecms/ui": "^3.0.0-canary.259",
|
|
59
59
|
"@radix-ui/react-portal": "^1.1.3",
|
|
60
60
|
"clsx": "^2.1.1",
|
|
61
61
|
"date-fns": "^3.6.0",
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
"dist",
|
|
108
108
|
"src"
|
|
109
109
|
],
|
|
110
|
-
"gitHead": "
|
|
110
|
+
"gitHead": "a4ab5e6aacfb9e10ee8c1c72354c939fb2275ab0",
|
|
111
111
|
"publishConfig": {
|
|
112
112
|
"access": "public"
|
|
113
113
|
},
|
|
@@ -107,6 +107,7 @@ export const EntityCollectionRowActions = function EntityCollectionRowActions({
|
|
|
107
107
|
onClick={(event: MouseEvent) => {
|
|
108
108
|
event.stopPropagation();
|
|
109
109
|
action.onClick({
|
|
110
|
+
view: "collection",
|
|
110
111
|
entity,
|
|
111
112
|
fullPath,
|
|
112
113
|
fullIdPath,
|
|
@@ -137,6 +138,7 @@ export const EntityCollectionRowActions = function EntityCollectionRowActions({
|
|
|
137
138
|
onClick={(e) => {
|
|
138
139
|
e.stopPropagation();
|
|
139
140
|
action.onClick({
|
|
141
|
+
view: "collection",
|
|
140
142
|
entity,
|
|
141
143
|
fullPath,
|
|
142
144
|
fullIdPath,
|
|
@@ -73,7 +73,7 @@ export function CollectionTableToolbar({
|
|
|
73
73
|
<div
|
|
74
74
|
className={cls(defaultBorderMixin, "no-scrollbar min-h-[56px] overflow-x-auto px-2 md:px-4 bg-surface-50 dark:bg-surface-900 border-b flex flex-row justify-between items-center w-full")}>
|
|
75
75
|
|
|
76
|
-
<div className="flex items-center gap-
|
|
76
|
+
<div className="flex items-center gap-1 md:mr-4 mr-2">
|
|
77
77
|
|
|
78
78
|
{title && <div className={"hidden lg:block"}>
|
|
79
79
|
{title}
|
|
@@ -85,7 +85,7 @@ export function CollectionTableToolbar({
|
|
|
85
85
|
|
|
86
86
|
</div>
|
|
87
87
|
|
|
88
|
-
<div className="flex items-center gap-
|
|
88
|
+
<div className="flex items-center gap-1">
|
|
89
89
|
|
|
90
90
|
{largeLayout && <div className="w-[22px] mr-4">
|
|
91
91
|
{loading &&
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
mergeEntityActions,
|
|
32
32
|
navigateToEntity,
|
|
33
33
|
resolveCollection,
|
|
34
|
+
resolveEntityAction,
|
|
34
35
|
resolveProperty
|
|
35
36
|
} from "../../util";
|
|
36
37
|
import { ReferencePreview } from "../../preview";
|
|
@@ -151,7 +152,6 @@ export const EntityCollectionView = React.memo(
|
|
|
151
152
|
}: EntityCollectionViewProps<M>
|
|
152
153
|
) {
|
|
153
154
|
|
|
154
|
-
|
|
155
155
|
const context = useFireCMSContext();
|
|
156
156
|
const navigation = useNavigationController();
|
|
157
157
|
const fullPath = fullPathProp ?? collectionProp.path;
|
|
@@ -521,10 +521,13 @@ export const EntityCollectionView = React.memo(
|
|
|
521
521
|
}) => {
|
|
522
522
|
|
|
523
523
|
const isSelected = Boolean(usedSelectionController.selectedEntities.find(e => e.id == entity.id && e.path == entity.path));
|
|
524
|
+
const customEntityActions = (collection.entityActions ?? [])
|
|
525
|
+
.map(action => resolveEntityAction(action, customizationController.entityActions))
|
|
526
|
+
.filter(Boolean) as EntityAction[];
|
|
524
527
|
|
|
525
528
|
const actions = getActionsForEntity({
|
|
526
529
|
entity,
|
|
527
|
-
customEntityActions
|
|
530
|
+
customEntityActions
|
|
528
531
|
});
|
|
529
532
|
|
|
530
533
|
return (
|
|
@@ -71,7 +71,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
|
|
|
71
71
|
? <Button
|
|
72
72
|
variant={"text"}
|
|
73
73
|
disabled={!(selectedEntities?.length) || !multipleDeleteEnabled}
|
|
74
|
-
startIcon={<DeleteIcon/>}
|
|
74
|
+
startIcon={<DeleteIcon size={"small"}/>}
|
|
75
75
|
onClick={onMultipleDeleteClick}
|
|
76
76
|
color={"primary"}
|
|
77
77
|
className="lg:w-20"
|
|
@@ -79,10 +79,11 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
|
|
|
79
79
|
({selectedEntities?.length})
|
|
80
80
|
</Button>
|
|
81
81
|
: <IconButton
|
|
82
|
+
size={"small"}
|
|
82
83
|
color={"primary"}
|
|
83
84
|
disabled={!(selectedEntities?.length) || !multipleDeleteEnabled}
|
|
84
85
|
onClick={onMultipleDeleteClick}>
|
|
85
|
-
<DeleteIcon/>
|
|
86
|
+
<DeleteIcon size={"small"}/>
|
|
86
87
|
</IconButton>;
|
|
87
88
|
multipleDeleteButton =
|
|
88
89
|
<Tooltip
|
|
@@ -38,6 +38,7 @@ export function DefaultHomePage({
|
|
|
38
38
|
additionalChildrenStart?: React.ReactNode;
|
|
39
39
|
additionalChildrenEnd?: React.ReactNode;
|
|
40
40
|
}) {
|
|
41
|
+
|
|
41
42
|
const context = useFireCMSContext();
|
|
42
43
|
const customizationController = useCustomizationController();
|
|
43
44
|
const navigationController = useNavigationController();
|
|
@@ -56,9 +57,12 @@ export function DefaultHomePage({
|
|
|
56
57
|
const [filteredUrls, setFilteredUrls] = useState<string[] | null>(null);
|
|
57
58
|
const performingSearch = Boolean(filteredUrls);
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
// Memoize filtered navigation entries to prevent unnecessary recalculations
|
|
61
|
+
const filteredNavigationEntries = useMemo(() => {
|
|
62
|
+
return filteredUrls
|
|
63
|
+
? rawNavigationEntries.filter((e) => filteredUrls.includes(e.url))
|
|
64
|
+
: rawNavigationEntries;
|
|
65
|
+
}, [filteredUrls, rawNavigationEntries]);
|
|
62
66
|
|
|
63
67
|
useEffect(() => {
|
|
64
68
|
fuse.current = new Fuse(rawNavigationEntries, {
|
|
@@ -67,9 +71,12 @@ export function DefaultHomePage({
|
|
|
67
71
|
}, [rawNavigationEntries]);
|
|
68
72
|
|
|
69
73
|
const updateSearch = useCallback((v?: string) => {
|
|
70
|
-
if (!v?.trim())
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
if (!v?.trim()) {
|
|
75
|
+
setFilteredUrls(null);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const results = fuse.current?.search(v.trim());
|
|
79
|
+
setFilteredUrls(results ? results.map((x) => x.item.url) : []);
|
|
73
80
|
}, []);
|
|
74
81
|
|
|
75
82
|
/* ───────────────────────────────────────────────────────────────
|
|
@@ -83,12 +90,11 @@ export function DefaultHomePage({
|
|
|
83
90
|
entries: NavigationEntry[];
|
|
84
91
|
} | null>(null);
|
|
85
92
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
: rawNavigationEntries;
|
|
90
|
-
|
|
93
|
+
// Memoize the processed groups to avoid unnecessary recalculations
|
|
94
|
+
const processedGroups = useMemo(() => {
|
|
95
|
+
const src = filteredNavigationEntries;
|
|
91
96
|
const entriesByGroup: Record<string, NavigationEntry[]> = {};
|
|
97
|
+
|
|
92
98
|
src.forEach((e) => {
|
|
93
99
|
const g =
|
|
94
100
|
e.type === "admin"
|
|
@@ -97,6 +103,9 @@ export function DefaultHomePage({
|
|
|
97
103
|
(entriesByGroup[g] ??= []).push(e);
|
|
98
104
|
});
|
|
99
105
|
|
|
106
|
+
// Check if there are custom actions from plugins that should show in the default group
|
|
107
|
+
const hasPluginAdditionalCards = customizationController.plugins?.some(p => p.homePage?.AdditionalCards);
|
|
108
|
+
|
|
100
109
|
let allProcessed: { name: string; entries: NavigationEntry[] }[];
|
|
101
110
|
|
|
102
111
|
if (performingSearch) {
|
|
@@ -121,27 +130,35 @@ export function DefaultHomePage({
|
|
|
121
130
|
entries: entriesByGroup[g]
|
|
122
131
|
});
|
|
123
132
|
});
|
|
133
|
+
|
|
134
|
+
// Ensure default group exists if there are plugin additional cards but no collections
|
|
135
|
+
if (hasPluginAdditionalCards && !allProcessed.some(g => g.name === DEFAULT_GROUP_NAME)) {
|
|
136
|
+
allProcessed.push({
|
|
137
|
+
name: DEFAULT_GROUP_NAME,
|
|
138
|
+
entries: []
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
124
142
|
allProcessed = allProcessed.filter(
|
|
125
143
|
(g) =>
|
|
126
144
|
g.entries.length ||
|
|
127
|
-
groupOrderFromNavController.includes(g.name)
|
|
145
|
+
groupOrderFromNavController.includes(g.name) ||
|
|
146
|
+
(g.name === DEFAULT_GROUP_NAME && hasPluginAdditionalCards)
|
|
128
147
|
);
|
|
129
148
|
}
|
|
130
149
|
|
|
131
150
|
const admin = allProcessed.find((g) => g.name === ADMIN_GROUP_NAME);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
groupOrderFromNavController
|
|
144
|
-
]);
|
|
151
|
+
return {
|
|
152
|
+
adminGroupData: admin || null,
|
|
153
|
+
items: allProcessed.filter((g) => g.name !== ADMIN_GROUP_NAME)
|
|
154
|
+
};
|
|
155
|
+
}, [filteredNavigationEntries, performingSearch, groupOrderFromNavController, customizationController.plugins]);
|
|
156
|
+
|
|
157
|
+
// Update state only when processedGroups actually changes
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
setAdminGroupData(processedGroups.adminGroupData);
|
|
160
|
+
setItems(processedGroups.items);
|
|
161
|
+
}, [processedGroups]);
|
|
145
162
|
|
|
146
163
|
/* ───────────────────────────────────────────────────────────────
|
|
147
164
|
Local update vs. persistence helpers
|
|
@@ -177,9 +194,9 @@ export function DefaultHomePage({
|
|
|
177
194
|
onNavigationEntriesUpdate(all);
|
|
178
195
|
};
|
|
179
196
|
|
|
180
|
-
/*
|
|
197
|
+
/* ─────────────────────────────────────────────────────���─────────
|
|
181
198
|
Hook for DnD
|
|
182
|
-
|
|
199
|
+
───�����────────────────────────────────────────────────────────── */
|
|
183
200
|
const {
|
|
184
201
|
sensors,
|
|
185
202
|
collisionDetection,
|
|
@@ -225,11 +242,9 @@ export function DefaultHomePage({
|
|
|
225
242
|
|
|
226
243
|
const dndDisabled = !allowDragAndDrop || performingSearch;
|
|
227
244
|
|
|
228
|
-
const dndModifiers =
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
return [restrictToWindowEdges];
|
|
232
|
-
}, [dndKitActiveNode]);
|
|
245
|
+
const dndModifiers = dndKitActiveNode?.data.current?.type === "group"
|
|
246
|
+
? [restrictToVerticalAxis, restrictToWindowEdges]
|
|
247
|
+
: [restrictToWindowEdges];
|
|
233
248
|
|
|
234
249
|
/* ───────────────────────────────────────────────────────────────
|
|
235
250
|
Plugin extras
|
|
@@ -288,7 +303,7 @@ export function DefaultHomePage({
|
|
|
288
303
|
|
|
289
304
|
/* ───────────────────────────────────────────────────────────────
|
|
290
305
|
Render
|
|
291
|
-
|
|
306
|
+
─────────���───────────────────────────────────────────────────── */
|
|
292
307
|
return (
|
|
293
308
|
<div ref={containerRef} className="py-2 overflow-auto h-full w-full">
|
|
294
309
|
<Container maxWidth="6xl">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useEffect,
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Active,
|
|
4
4
|
closestCenter,
|
|
@@ -84,14 +84,11 @@ export function SortableNavigationCard({
|
|
|
84
84
|
animateLayoutChanges
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
const style =
|
|
88
|
-
()
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}),
|
|
93
|
-
[transform, transition, isDragging]
|
|
94
|
-
);
|
|
87
|
+
const style = {
|
|
88
|
+
transform: transform ? CSS.Transform.toString(transform) : undefined,
|
|
89
|
+
transition,
|
|
90
|
+
opacity: isDragging ? 0 : 1
|
|
91
|
+
};
|
|
95
92
|
|
|
96
93
|
return (
|
|
97
94
|
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
|
|
@@ -153,14 +150,11 @@ export function SortableNavigationGroup({
|
|
|
153
150
|
disabled
|
|
154
151
|
});
|
|
155
152
|
|
|
156
|
-
const style =
|
|
157
|
-
()
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}),
|
|
162
|
-
[transform, transition, isDragging]
|
|
163
|
-
);
|
|
153
|
+
const style = {
|
|
154
|
+
transform: transform ? CSS.Transform.toString(transform) : undefined,
|
|
155
|
+
transition,
|
|
156
|
+
opacity: isDragging ? 0 : 1
|
|
157
|
+
};
|
|
164
158
|
|
|
165
159
|
return (
|
|
166
160
|
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
|
|
@@ -224,15 +218,10 @@ export function useHomePageDnd({
|
|
|
224
218
|
}
|
|
225
219
|
});
|
|
226
220
|
const keyboardSensor = useSensor(KeyboardSensor);
|
|
227
|
-
const sensors = useSensors(
|
|
228
|
-
...(disabled ? [] : [mouseSensor, touchSensor, keyboardSensor])
|
|
229
|
-
);
|
|
221
|
+
const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor);
|
|
230
222
|
|
|
231
223
|
/* ---------------- helpers ---------------- */
|
|
232
|
-
const dndContainers =
|
|
233
|
-
() => dndItems.map((g) => g.name),
|
|
234
|
-
[dndItems]
|
|
235
|
-
);
|
|
224
|
+
const dndContainers = dndItems.map((g) => g.name);
|
|
236
225
|
|
|
237
226
|
const findDndContainer = useCallback(
|
|
238
227
|
(id: UniqueIdentifier): string | undefined => {
|
|
@@ -518,18 +507,15 @@ export function useHomePageDnd({
|
|
|
518
507
|
};
|
|
519
508
|
|
|
520
509
|
/* ---------------- public API ---------------- */
|
|
521
|
-
const activeItemForOverlay =
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
dndItems.flatMap((g) => g.entries).find((e) => e.url === activeId) ||
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if (disabled || !activeId || !activeIsGroup) return null;
|
|
531
|
-
return dndItems.find((g) => g.name === activeId) || null;
|
|
532
|
-
}, [activeId, dndItems, disabled, activeIsGroup]);
|
|
510
|
+
const activeItemForOverlay =
|
|
511
|
+
disabled || !activeId || activeIsGroup
|
|
512
|
+
? null
|
|
513
|
+
: dndItems.flatMap((g) => g.entries).find((e) => e.url === activeId) || null;
|
|
514
|
+
|
|
515
|
+
const activeGroupData =
|
|
516
|
+
disabled || !activeId || !activeIsGroup
|
|
517
|
+
? null
|
|
518
|
+
: dndItems.find((g) => g.name === activeId) || null;
|
|
533
519
|
|
|
534
520
|
return {
|
|
535
521
|
sensors,
|
|
@@ -101,10 +101,14 @@ export function RenameGroupDialog({ open, initialName, existingGroupNames, onClo
|
|
|
101
101
|
{error && <p id="group-name-error" style={{ display: "none" }}>{error}</p>}
|
|
102
102
|
</DialogContent>
|
|
103
103
|
<DialogActions>
|
|
104
|
-
<Button onClick={onClose}
|
|
104
|
+
<Button onClick={onClose}
|
|
105
|
+
color={"primary"}
|
|
106
|
+
variant="text">
|
|
105
107
|
Cancel
|
|
106
108
|
</Button>
|
|
107
|
-
<Button onClick={handleSave}
|
|
109
|
+
<Button onClick={handleSave}
|
|
110
|
+
color={"primary"}
|
|
111
|
+
disabled={!!error || !name.trim()}>
|
|
108
112
|
Save
|
|
109
113
|
</Button>
|
|
110
114
|
</DialogActions>
|
|
@@ -34,8 +34,12 @@ export function UnsavedChangesDialog({
|
|
|
34
34
|
|
|
35
35
|
</DialogContent>
|
|
36
36
|
<DialogActions>
|
|
37
|
-
<Button variant="text"
|
|
38
|
-
|
|
37
|
+
<Button variant="text"
|
|
38
|
+
color={"primary"}
|
|
39
|
+
onClick={handleCancel} autoFocus> Cancel </Button>
|
|
40
|
+
<Button
|
|
41
|
+
color={"primary"}
|
|
42
|
+
onClick={handleOk}> Ok </Button>
|
|
39
43
|
</DialogActions>
|
|
40
44
|
</Dialog>
|
|
41
45
|
);
|