@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
|
@@ -5,10 +5,11 @@ import { addRecentId } from "../EntityCollectionView/utils";
|
|
|
5
5
|
import { navigateToEntity, resolveDefaultSelectedView } from "../../util";
|
|
6
6
|
|
|
7
7
|
export const editEntityAction: EntityAction = {
|
|
8
|
-
icon: <EditIcon/>,
|
|
8
|
+
icon: <EditIcon size={"small"}/>,
|
|
9
9
|
key: "edit",
|
|
10
10
|
name: "Edit",
|
|
11
11
|
collapsed: false,
|
|
12
|
+
isEnabled: ({ entity }) => Boolean(entity),
|
|
12
13
|
onClick({
|
|
13
14
|
entity,
|
|
14
15
|
collection,
|
|
@@ -20,6 +21,10 @@ export const editEntityAction: EntityAction = {
|
|
|
20
21
|
openEntityMode
|
|
21
22
|
}): Promise<void> {
|
|
22
23
|
|
|
24
|
+
if (!entity) {
|
|
25
|
+
throw new Error("INTERNAL: editEntityAction: Entity is undefined");
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
highlightEntity?.(entity);
|
|
24
29
|
|
|
25
30
|
context.analyticsController?.onAnalyticsEvent?.("entity_click", {
|
|
@@ -57,9 +62,10 @@ export const editEntityAction: EntityAction = {
|
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
export const copyEntityAction: EntityAction = {
|
|
60
|
-
icon: <FileCopyIcon/>,
|
|
65
|
+
icon: <FileCopyIcon size={"small"}/>,
|
|
61
66
|
name: "Copy",
|
|
62
67
|
key: "copy",
|
|
68
|
+
isEnabled: ({ entity }) => Boolean(entity),
|
|
63
69
|
onClick({
|
|
64
70
|
entity,
|
|
65
71
|
collection,
|
|
@@ -69,6 +75,9 @@ export const copyEntityAction: EntityAction = {
|
|
|
69
75
|
unhighlightEntity,
|
|
70
76
|
openEntityMode
|
|
71
77
|
}): Promise<void> {
|
|
78
|
+
if (!entity) {
|
|
79
|
+
throw new Error("INTERNAL: copyEntityAction: Entity is undefined");
|
|
80
|
+
}
|
|
72
81
|
highlightEntity?.(entity);
|
|
73
82
|
context.analyticsController?.onAnalyticsEvent?.("copy_entity_click", {
|
|
74
83
|
path: entity.path,
|
|
@@ -94,9 +103,10 @@ export const copyEntityAction: EntityAction = {
|
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
export const deleteEntityAction: EntityAction = {
|
|
97
|
-
icon: <DeleteIcon/>,
|
|
106
|
+
icon: <DeleteIcon size={"small"}/>,
|
|
98
107
|
name: "Delete",
|
|
99
108
|
key: "delete",
|
|
109
|
+
isEnabled: ({ entity }) => Boolean(entity),
|
|
100
110
|
onClick({
|
|
101
111
|
entity,
|
|
102
112
|
fullPath,
|
|
@@ -104,8 +114,11 @@ export const deleteEntityAction: EntityAction = {
|
|
|
104
114
|
context,
|
|
105
115
|
selectionController,
|
|
106
116
|
onCollectionChange,
|
|
107
|
-
|
|
117
|
+
navigateBack
|
|
108
118
|
}): Promise<void> {
|
|
119
|
+
if (!entity) {
|
|
120
|
+
throw new Error("INTERNAL: deleteEntityAction: Entity is undefined");
|
|
121
|
+
}
|
|
109
122
|
const { closeDialog } = context.dialogsController.open({
|
|
110
123
|
key: "delete_entity_dialog_" + entity.id,
|
|
111
124
|
Component: ({ open }) => {
|
|
@@ -123,7 +136,7 @@ export const deleteEntityAction: EntityAction = {
|
|
|
123
136
|
});
|
|
124
137
|
selectionController?.setSelectedEntities(selectionController.selectedEntities.filter(e => e.id !== entity.id));
|
|
125
138
|
onCollectionChange?.();
|
|
126
|
-
|
|
139
|
+
navigateBack?.();
|
|
127
140
|
}}
|
|
128
141
|
onClose={closeDialog}/>;
|
|
129
142
|
}
|
|
@@ -334,7 +334,7 @@ function encodeFilterAndSort(filterValues?: FilterValues<string>, sortBy?: [stri
|
|
|
334
334
|
}
|
|
335
335
|
if (encodedValue !== undefined) {
|
|
336
336
|
entries[encodeURIComponent(`${key}_op`)] = encodeURIComponent(op);
|
|
337
|
-
entries[encodeURIComponent(`${key}_value`)] = encodeURIComponent(encodedValue.toString());
|
|
337
|
+
entries[encodeURIComponent(`${key}_value`)] = encodedValue ? encodeURIComponent(encodedValue.toString()) : "null";
|
|
338
338
|
}
|
|
339
339
|
}
|
|
340
340
|
});
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useState } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Entity,
|
|
4
|
+
EntityCollection,
|
|
5
|
+
EntityStatus,
|
|
6
|
+
FireCMSPlugin,
|
|
7
|
+
FormContext,
|
|
8
|
+
PluginFormActionProps,
|
|
9
|
+
User
|
|
10
|
+
} from "../types";
|
|
3
11
|
|
|
4
12
|
import { CircularProgressCenter, EntityCollectionView, EntityView, ErrorBoundary } from "../components";
|
|
5
13
|
import {
|
|
@@ -162,12 +170,33 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
162
170
|
setUsedEntity(entity);
|
|
163
171
|
}, [entity]);
|
|
164
172
|
|
|
165
|
-
// Instead of using a ref (which does not trigger re-render), we use state for the form context.
|
|
166
173
|
const [formContext, setFormContext] = useState<FormContext<M> | undefined>(undefined);
|
|
167
174
|
|
|
168
175
|
const largeLayout = useLargeLayout();
|
|
169
176
|
|
|
170
177
|
const customizationController = useCustomizationController();
|
|
178
|
+
const plugins = customizationController.plugins;
|
|
179
|
+
const pluginActionsTop: React.ReactNode[] = [];
|
|
180
|
+
|
|
181
|
+
if (plugins && collection) {
|
|
182
|
+
const actionProps: PluginFormActionProps = {
|
|
183
|
+
entityId,
|
|
184
|
+
parentCollectionIds,
|
|
185
|
+
path,
|
|
186
|
+
status,
|
|
187
|
+
collection,
|
|
188
|
+
context,
|
|
189
|
+
formContext,
|
|
190
|
+
openEntityMode: layout,
|
|
191
|
+
disabled: false
|
|
192
|
+
};
|
|
193
|
+
pluginActionsTop.push(...plugins.map((plugin) => (
|
|
194
|
+
plugin.form?.ActionsTop
|
|
195
|
+
? <plugin.form.ActionsTop
|
|
196
|
+
key={`actions_${plugin.key}`} {...actionProps} />
|
|
197
|
+
: null
|
|
198
|
+
)).filter(Boolean));
|
|
199
|
+
}
|
|
171
200
|
|
|
172
201
|
const defaultSelectedView = useMemo(() => resolveDefaultSelectedView(
|
|
173
202
|
collection ? collection.defaultSelectedView : undefined,
|
|
@@ -191,8 +220,6 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
191
220
|
const includeJsonView = collection.includeJsonView === undefined ? true : collection.includeJsonView;
|
|
192
221
|
const hasAdditionalViews = customViewsCount > 0 || subcollectionsCount > 0 || includeJsonView;
|
|
193
222
|
|
|
194
|
-
const plugins = customizationController.plugins;
|
|
195
|
-
|
|
196
223
|
const {
|
|
197
224
|
resolvedEntityViews,
|
|
198
225
|
selectedEntityView,
|
|
@@ -285,13 +312,6 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
285
312
|
const subcollectionId = subcollection.id ?? subcollection.path;
|
|
286
313
|
const newFullPath = usedEntity ? `${path}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollection.path)}` : undefined;
|
|
287
314
|
const newFullIdPath = fullIdPath ? `${fullIdPath}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollectionId)}` : undefined;
|
|
288
|
-
console.debug("Rendering subcollection", {
|
|
289
|
-
subcollectionId,
|
|
290
|
-
fullIdPath,
|
|
291
|
-
newFullPath,
|
|
292
|
-
newFullIdPath,
|
|
293
|
-
selectedTab
|
|
294
|
-
});
|
|
295
315
|
|
|
296
316
|
if (selectedTab !== subcollectionId) return null;
|
|
297
317
|
return (
|
|
@@ -423,17 +443,20 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
423
443
|
let result = <div className="relative flex flex-col h-full w-full bg-white dark:bg-surface-900">
|
|
424
444
|
|
|
425
445
|
{shouldShowTopBar && <div
|
|
426
|
-
className={cls("h-14 flex overflow-visible overflow-x-scroll w-full no-scrollbar h-14 border-b pl-2 pr-2 pt-1 flex
|
|
446
|
+
className={cls("h-14 items-center flex overflow-visible overflow-x-scroll w-full no-scrollbar h-14 border-b pl-2 pr-2 pt-1 flex bg-surface-50 dark:bg-surface-900", defaultBorderMixin)}>
|
|
427
447
|
|
|
428
448
|
{barActions}
|
|
429
449
|
|
|
430
450
|
<div className={"flex-grow"}/>
|
|
431
451
|
|
|
452
|
+
{pluginActionsTop}
|
|
453
|
+
|
|
432
454
|
{globalLoading && <div className="self-center">
|
|
433
455
|
<CircularProgress size={"small"}/>
|
|
434
456
|
</div>}
|
|
435
457
|
|
|
436
458
|
{hasAdditionalViews && <Tabs
|
|
459
|
+
className={"self-end"}
|
|
437
460
|
value={selectedTab}
|
|
438
461
|
onValueChange={(value) => {
|
|
439
462
|
onSideTabClick(value);
|
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
import React, { useMemo } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Entity,
|
|
4
|
+
EntityAction,
|
|
5
|
+
EntityActionClickProps,
|
|
6
|
+
FireCMSContext,
|
|
7
|
+
FormContext,
|
|
8
|
+
ResolvedEntityCollection,
|
|
9
|
+
SideEntityController
|
|
10
|
+
} from "../types";
|
|
3
11
|
|
|
4
12
|
import { copyEntityAction, deleteEntityAction } from "../components";
|
|
5
|
-
import { canCreateEntity, canDeleteEntity, mergeEntityActions } from "../util";
|
|
6
|
-
import {
|
|
7
|
-
|
|
13
|
+
import { canCreateEntity, canDeleteEntity, mergeEntityActions, resolveEntityAction } from "../util";
|
|
14
|
+
import {
|
|
15
|
+
Button,
|
|
16
|
+
CircularProgress,
|
|
17
|
+
cls,
|
|
18
|
+
defaultBorderMixin,
|
|
19
|
+
DialogActions,
|
|
20
|
+
IconButton,
|
|
21
|
+
LoadingButton,
|
|
22
|
+
Tooltip,
|
|
23
|
+
Typography
|
|
24
|
+
} from "@firecms/ui";
|
|
25
|
+
import {
|
|
26
|
+
useAuthController,
|
|
27
|
+
useCustomizationController,
|
|
28
|
+
useFireCMSContext,
|
|
29
|
+
useSideEntityController,
|
|
30
|
+
useSnackbarController
|
|
31
|
+
} from "../hooks";
|
|
8
32
|
import { EntityFormActionsProps } from "../form/EntityFormActions";
|
|
9
33
|
import { SideDialogController, useSideDialogContext } from "./SideDialogs";
|
|
10
34
|
|
|
@@ -19,16 +43,21 @@ export function EntityEditViewFormActions({
|
|
|
19
43
|
status,
|
|
20
44
|
pluginActions,
|
|
21
45
|
openEntityMode,
|
|
22
|
-
showDefaultActions = true
|
|
46
|
+
showDefaultActions = true,
|
|
47
|
+
navigateBack,
|
|
48
|
+
formContext
|
|
23
49
|
}: EntityFormActionsProps) {
|
|
24
50
|
|
|
25
51
|
const authController = useAuthController();
|
|
26
52
|
const context = useFireCMSContext();
|
|
27
53
|
const sideEntityController = useSideEntityController();
|
|
28
54
|
const sideDialogContext = useSideDialogContext();
|
|
55
|
+
const customizationController = useCustomizationController();
|
|
29
56
|
|
|
30
57
|
const entityActions = useMemo((): EntityAction[] => {
|
|
31
|
-
const customEntityActions = collection.entityActions
|
|
58
|
+
const customEntityActions = (collection.entityActions ?? [])
|
|
59
|
+
.map(action => resolveEntityAction(action, customizationController.entityActions))
|
|
60
|
+
.filter(Boolean) as EntityAction[];
|
|
32
61
|
const createEnabled = canCreateEntity(collection, authController, path, null);
|
|
33
62
|
const deleteEnabled = entity ? canDeleteEntity(collection, authController, path, entity) : false;
|
|
34
63
|
const actions: EntityAction[] = [];
|
|
@@ -39,7 +68,7 @@ export function EntityEditViewFormActions({
|
|
|
39
68
|
if (customEntityActions)
|
|
40
69
|
return mergeEntityActions(actions, customEntityActions);
|
|
41
70
|
return actions;
|
|
42
|
-
}, [authController, collection, path]);
|
|
71
|
+
}, [authController, collection, path, customizationController.entityActions?.length]);
|
|
43
72
|
|
|
44
73
|
const formActions = showDefaultActions ? entityActions.filter(a => a.includeInForm === undefined || a.includeInForm) : [];
|
|
45
74
|
|
|
@@ -57,6 +86,8 @@ export function EntityEditViewFormActions({
|
|
|
57
86
|
sideDialogContext,
|
|
58
87
|
pluginActions,
|
|
59
88
|
openEntityMode,
|
|
89
|
+
navigateBack,
|
|
90
|
+
formContext
|
|
60
91
|
})
|
|
61
92
|
: buildSideActions({
|
|
62
93
|
savingError,
|
|
@@ -71,6 +102,8 @@ export function EntityEditViewFormActions({
|
|
|
71
102
|
status,
|
|
72
103
|
pluginActions,
|
|
73
104
|
openEntityMode,
|
|
105
|
+
navigateBack,
|
|
106
|
+
formContext
|
|
74
107
|
});
|
|
75
108
|
}
|
|
76
109
|
|
|
@@ -87,6 +120,8 @@ type ActionsViewProps<M extends object> = {
|
|
|
87
120
|
sideDialogContext: SideDialogController,
|
|
88
121
|
pluginActions?: React.ReactNode[],
|
|
89
122
|
openEntityMode: "side_panel" | "full_screen";
|
|
123
|
+
navigateBack: () => void;
|
|
124
|
+
formContext: FormContext
|
|
90
125
|
};
|
|
91
126
|
|
|
92
127
|
function buildBottomActions<M extends object>({
|
|
@@ -102,37 +137,48 @@ function buildBottomActions<M extends object>({
|
|
|
102
137
|
sideDialogContext,
|
|
103
138
|
pluginActions,
|
|
104
139
|
openEntityMode,
|
|
140
|
+
navigateBack,
|
|
141
|
+
formContext
|
|
105
142
|
}: ActionsViewProps<M>) {
|
|
106
143
|
|
|
107
144
|
const canClose = openEntityMode === "side_panel";
|
|
108
|
-
return <DialogActions
|
|
145
|
+
return <DialogActions
|
|
146
|
+
position={"absolute"}>
|
|
109
147
|
{savingError &&
|
|
110
148
|
<div className="text-right">
|
|
111
149
|
<Typography color={"error"}>{savingError.message}</Typography>
|
|
112
150
|
</div>
|
|
113
151
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
152
|
+
|
|
153
|
+
{formActions.length > 0 && <div className="flex-grow flex overflow-auto no-scrollbar">
|
|
154
|
+
{formActions.map(action => {
|
|
155
|
+
|
|
156
|
+
const props = {
|
|
157
|
+
view: "form",
|
|
158
|
+
entity,
|
|
159
|
+
fullPath: collection.path,
|
|
160
|
+
collection: collection,
|
|
161
|
+
context,
|
|
162
|
+
sideEntityController,
|
|
163
|
+
openEntityMode,
|
|
164
|
+
navigateBack,
|
|
165
|
+
formContext
|
|
166
|
+
} satisfies EntityActionClickProps<any>;
|
|
167
|
+
|
|
168
|
+
const isEnabled = !action.isEnabled || action.isEnabled(props);
|
|
169
|
+
return (
|
|
170
|
+
<EntityActionButton
|
|
171
|
+
key={action.key}
|
|
172
|
+
action={action}
|
|
173
|
+
enabled={isEnabled}
|
|
174
|
+
props={props}
|
|
175
|
+
/>
|
|
176
|
+
);
|
|
177
|
+
})}
|
|
134
178
|
</div>}
|
|
179
|
+
|
|
135
180
|
{pluginActions}
|
|
181
|
+
|
|
136
182
|
<Button variant="text" disabled={disabled || isSubmitting} type="reset">
|
|
137
183
|
{status === "existing" ? "Discard" : "Clear"}
|
|
138
184
|
</Button>
|
|
@@ -171,7 +217,10 @@ function buildSideActions<M extends object>({
|
|
|
171
217
|
disabled,
|
|
172
218
|
status,
|
|
173
219
|
sideDialogContext,
|
|
174
|
-
pluginActions
|
|
220
|
+
pluginActions,
|
|
221
|
+
openEntityMode,
|
|
222
|
+
navigateBack,
|
|
223
|
+
formContext
|
|
175
224
|
}: ActionsViewProps<M>) {
|
|
176
225
|
|
|
177
226
|
return <div
|
|
@@ -184,12 +233,33 @@ function buildSideActions<M extends object>({
|
|
|
184
233
|
{status === "copy" && "Create copy"}
|
|
185
234
|
{status === "new" && "Create"}
|
|
186
235
|
</LoadingButton>
|
|
236
|
+
|
|
187
237
|
<Button fullWidth={true} variant="text" disabled={disabled || isSubmitting} type="reset">
|
|
188
238
|
{status === "existing" ? "Discard" : "Clear"}
|
|
189
239
|
</Button>
|
|
190
240
|
|
|
191
241
|
{pluginActions}
|
|
192
242
|
|
|
243
|
+
{formActions.length > 0 && <div className="flex flex-row flex-wrap mt-2">
|
|
244
|
+
{formActions.map(action => {
|
|
245
|
+
const props = {
|
|
246
|
+
view: "form",
|
|
247
|
+
entity,
|
|
248
|
+
fullPath: collection.path,
|
|
249
|
+
collection: collection,
|
|
250
|
+
context,
|
|
251
|
+
sideEntityController,
|
|
252
|
+
openEntityMode,
|
|
253
|
+
navigateBack,
|
|
254
|
+
formContext
|
|
255
|
+
} satisfies EntityActionClickProps<any>;
|
|
256
|
+
const isEnabled = !action.isEnabled || action.isEnabled(props);
|
|
257
|
+
return (
|
|
258
|
+
<EntityActionButton key={action.key} action={action} enabled={isEnabled} props={props}/>
|
|
259
|
+
);
|
|
260
|
+
})}
|
|
261
|
+
</div>}
|
|
262
|
+
|
|
193
263
|
{savingError &&
|
|
194
264
|
<div className="text-right">
|
|
195
265
|
<Typography color={"error"}>{savingError.message}</Typography>
|
|
@@ -197,3 +267,58 @@ function buildSideActions<M extends object>({
|
|
|
197
267
|
}
|
|
198
268
|
</div>;
|
|
199
269
|
}
|
|
270
|
+
|
|
271
|
+
function EntityActionButton({
|
|
272
|
+
action,
|
|
273
|
+
enabled,
|
|
274
|
+
props
|
|
275
|
+
}: {
|
|
276
|
+
action: EntityAction,
|
|
277
|
+
enabled: boolean,
|
|
278
|
+
props: EntityActionClickProps<any, any>
|
|
279
|
+
}) {
|
|
280
|
+
const snackbarController = useSnackbarController();
|
|
281
|
+
const [loading, setLoading] = React.useState(false);
|
|
282
|
+
return <Tooltip
|
|
283
|
+
title={action.name}>
|
|
284
|
+
<IconButton
|
|
285
|
+
color="primary"
|
|
286
|
+
disabled={!enabled}
|
|
287
|
+
onClick={(event) => {
|
|
288
|
+
console.debug("Executing action", action.key, props);
|
|
289
|
+
try {
|
|
290
|
+
event.stopPropagation();
|
|
291
|
+
if (props.entity) {
|
|
292
|
+
const onClick = action.onClick(props);
|
|
293
|
+
// If the action returns a promise, we can handle it
|
|
294
|
+
if (onClick instanceof Promise) {
|
|
295
|
+
setLoading(true);
|
|
296
|
+
onClick
|
|
297
|
+
.catch((error) => {
|
|
298
|
+
console.error("Error executing action", action.key, error);
|
|
299
|
+
snackbarController.open({
|
|
300
|
+
message: `Error executing action: ${error.message}`,
|
|
301
|
+
type: "error"
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
.finally(() => setLoading(false));
|
|
305
|
+
} else {
|
|
306
|
+
snackbarController.open({
|
|
307
|
+
message: `Action ${action.name} executed successfully`,
|
|
308
|
+
type: "success"
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
}
|
|
313
|
+
} catch (e: any) {
|
|
314
|
+
console.error("Error executing action", action.key, e);
|
|
315
|
+
snackbarController.open({
|
|
316
|
+
message: `Error executing action: ${e.message}`,
|
|
317
|
+
type: "error"
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}}>
|
|
321
|
+
{loading ? <CircularProgress size={"smallest"}/> : action.icon}
|
|
322
|
+
</IconButton>
|
|
323
|
+
</Tooltip>;
|
|
324
|
+
}
|
|
@@ -76,7 +76,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
76
76
|
return navigationController.getParentCollectionIds(path);
|
|
77
77
|
}, [navigationController, path]);
|
|
78
78
|
|
|
79
|
-
const collection =
|
|
79
|
+
const collection = navigationController.getCollection(fullIdPath ?? path) ?? props.collection;
|
|
80
80
|
|
|
81
81
|
useEffect(() => {
|
|
82
82
|
function beforeunload(e: any) {
|
package/src/core/FireCMS.tsx
CHANGED
|
@@ -48,6 +48,7 @@ export function FireCMS<USER extends User>(props: FireCMSProps<USER>) {
|
|
|
48
48
|
onAnalyticsEvent,
|
|
49
49
|
propertyConfigs,
|
|
50
50
|
entityViews,
|
|
51
|
+
entityActions,
|
|
51
52
|
components,
|
|
52
53
|
navigationController,
|
|
53
54
|
apiKey
|
|
@@ -72,6 +73,7 @@ export function FireCMS<USER extends User>(props: FireCMSProps<USER>) {
|
|
|
72
73
|
entityLinkBuilder,
|
|
73
74
|
plugins,
|
|
74
75
|
entityViews: entityViews ?? [],
|
|
76
|
+
entityActions: entityActions ?? [],
|
|
75
77
|
propertyConfigs: propertyConfigs ?? {},
|
|
76
78
|
components
|
|
77
79
|
};
|
package/src/form/EntityForm.tsx
CHANGED
|
@@ -32,7 +32,8 @@ import {
|
|
|
32
32
|
useAuthController,
|
|
33
33
|
useCustomizationController,
|
|
34
34
|
useDataSource,
|
|
35
|
-
useFireCMSContext,
|
|
35
|
+
useFireCMSContext, useNavigationController,
|
|
36
|
+
useSideEntityController,
|
|
36
37
|
useSnackbarController
|
|
37
38
|
} from "../hooks";
|
|
38
39
|
import { Alert, CheckIcon, Chip, cls, EditIcon, NotesIcon, paperMixin, Tooltip, Typography } from "@firecms/ui";
|
|
@@ -121,11 +122,23 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
121
122
|
children
|
|
122
123
|
}: EntityFormProps<M>) {
|
|
123
124
|
|
|
124
|
-
|
|
125
125
|
if (collection.customId && collection.formAutoSave) {
|
|
126
126
|
console.warn(`The collection ${collection.path} has customId and formAutoSave enabled. This is not supported and formAutoSave will be ignored`);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
|
|
130
|
+
const sideEntityController = useSideEntityController();
|
|
131
|
+
const navigationController = useNavigationController();
|
|
132
|
+
|
|
133
|
+
const navigateBack = useCallback(() => {
|
|
134
|
+
if (openEntityMode === "side_panel") {
|
|
135
|
+
// If we are in side panel mode, we close the side panel
|
|
136
|
+
sideEntityController.close();
|
|
137
|
+
} else {
|
|
138
|
+
window.history.back();
|
|
139
|
+
}
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
129
142
|
const authController = useAuthController();
|
|
130
143
|
const [status, setStatus] = useState<EntityStatus>(initialStatus);
|
|
131
144
|
|
|
@@ -436,14 +449,16 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
436
449
|
const plugins = customizationController.plugins;
|
|
437
450
|
|
|
438
451
|
const actionsDisabled = disabled || formex.isSubmitting || (status === "existing" && !formex.dirty) || Boolean(disabledProp);
|
|
452
|
+
const parentCollectionIds = navigationController.getParentCollectionIds(path);
|
|
453
|
+
|
|
439
454
|
if (plugins && collection) {
|
|
440
455
|
const actionProps: PluginFormActionProps = {
|
|
441
456
|
entityId,
|
|
457
|
+
parentCollectionIds,
|
|
442
458
|
path,
|
|
443
459
|
status,
|
|
444
|
-
collection
|
|
460
|
+
collection,
|
|
445
461
|
context,
|
|
446
|
-
currentEntityId: entityId,
|
|
447
462
|
formContext,
|
|
448
463
|
openEntityMode,
|
|
449
464
|
disabled: actionsDisabled,
|
|
@@ -628,7 +643,13 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
628
643
|
variant={"h4"}>
|
|
629
644
|
{title ?? collection.singularName ?? collection.name}
|
|
630
645
|
</Typography>
|
|
631
|
-
|
|
646
|
+
|
|
647
|
+
{!entity?.values && initialStatus === "existing" &&
|
|
648
|
+
<Alert color={"warning"} size={"small"} outerClassName={"w-full mb-4 text-xs"}>
|
|
649
|
+
This entity does not exist in the database
|
|
650
|
+
</Alert>}
|
|
651
|
+
|
|
652
|
+
{showEntityPath && <Alert color={"base"} outerClassName={"w-full"} size={"small"}>
|
|
632
653
|
<code
|
|
633
654
|
className={"text-xs select-all text-text-secondary dark:text-text-secondary-dark"}>
|
|
634
655
|
{entity?.path ?? path}/{entityId}
|
|
@@ -638,6 +659,10 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
638
659
|
|
|
639
660
|
{children}
|
|
640
661
|
|
|
662
|
+
{initialEntityId && !entity && initialStatus !== "new" && <Alert color={"info"} size={"small"}>
|
|
663
|
+
This entity does not exist in the database
|
|
664
|
+
</Alert>}
|
|
665
|
+
|
|
641
666
|
{!Builder && !collection.hideIdFromForm &&
|
|
642
667
|
<CustomIdField customId={collection.customId}
|
|
643
668
|
entityId={entityId}
|
|
@@ -668,7 +693,6 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
668
693
|
throw Error("INTERNAL: Collection and path must be defined in form context");
|
|
669
694
|
}
|
|
670
695
|
|
|
671
|
-
|
|
672
696
|
const dialogActions = <EntityFormActionsComponent
|
|
673
697
|
collection={resolvedCollection}
|
|
674
698
|
path={path}
|
|
@@ -683,6 +707,8 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
683
707
|
pluginActions={pluginActions ?? []}
|
|
684
708
|
openEntityMode={openEntityMode}
|
|
685
709
|
showDefaultActions={showDefaultActions}
|
|
710
|
+
navigateBack={navigateBack}
|
|
711
|
+
formContext={formContext}
|
|
686
712
|
/>;
|
|
687
713
|
|
|
688
714
|
return (
|