@firecms/core 3.0.0-beta.14 → 3.0.0-beta.15
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/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +2 -1
- package/dist/components/HomePage/DefaultHomePage.d.ts +2 -15
- package/dist/components/HomePage/HomePageDnD.d.ts +76 -0
- package/dist/components/HomePage/NavigationCard.d.ts +3 -1
- package/dist/components/HomePage/NavigationCardBinding.d.ts +3 -2
- package/dist/components/HomePage/NavigationGroup.d.ts +7 -1
- package/dist/components/HomePage/RenameGroupDialog.d.ts +9 -0
- package/dist/core/EntityEditView.d.ts +3 -0
- package/dist/core/EntityEditViewFormActions.d.ts +1 -1
- package/dist/core/field_configs.d.ts +1 -1
- package/dist/form/EntityForm.d.ts +2 -1
- package/dist/form/EntityFormActions.d.ts +6 -2
- package/dist/form/field_bindings/ReferenceAsStringFieldBinding.d.ts +9 -0
- package/dist/form/index.d.ts +1 -0
- package/dist/hooks/useBuildNavigationController.d.ts +51 -2
- package/dist/index.es.js +2184 -1052
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2181 -1049
- package/dist/index.umd.js.map +1 -1
- package/dist/types/analytics.d.ts +1 -1
- package/dist/types/collections.d.ts +8 -2
- package/dist/types/customization_controller.d.ts +8 -0
- package/dist/types/entity_actions.d.ts +46 -6
- package/dist/types/firecms.d.ts +8 -0
- package/dist/types/navigation.d.ts +21 -5
- package/dist/types/plugins.d.ts +20 -1
- package/dist/types/properties.d.ts +7 -0
- package/dist/types/property_config.d.ts +1 -1
- package/dist/types/side_entity_controller.d.ts +4 -0
- package/dist/util/icons.d.ts +2 -2
- package/dist/util/navigation_from_path.d.ts +4 -0
- 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 +6 -0
- package/src/components/EntityCollectionTable/PropertyTableCell.tsx +25 -3
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +2 -2
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +7 -4
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +3 -2
- package/src/components/FireCMSLogo.tsx +7 -51
- package/src/components/HomePage/DefaultHomePage.tsx +491 -157
- package/src/components/HomePage/FavouritesView.tsx +3 -3
- package/src/components/HomePage/HomePageDnD.tsx +599 -0
- package/src/components/HomePage/NavigationCard.tsx +47 -38
- package/src/components/HomePage/NavigationCardBinding.tsx +10 -6
- package/src/components/HomePage/NavigationGroup.tsx +63 -29
- package/src/components/HomePage/RenameGroupDialog.tsx +117 -0
- package/src/components/UnsavedChangesDialog.tsx +6 -2
- package/src/components/common/default_entity_actions.tsx +25 -9
- package/src/components/common/useDataSourceTableController.tsx +2 -2
- package/src/core/DefaultDrawer.tsx +8 -8
- package/src/core/DrawerNavigationItem.tsx +1 -1
- package/src/core/EntityEditView.tsx +41 -6
- package/src/core/EntityEditViewFormActions.tsx +154 -29
- package/src/core/EntitySidePanel.tsx +5 -2
- package/src/core/FireCMS.tsx +2 -0
- package/src/core/field_configs.tsx +15 -1
- package/src/form/EntityForm.tsx +36 -4
- package/src/form/EntityFormActions.tsx +51 -9
- package/src/form/components/StorageItemPreview.tsx +1 -1
- package/src/form/components/StorageUploadProgress.tsx +3 -3
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +4 -2
- package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +135 -0
- package/src/form/field_bindings/RepeatFieldBinding.tsx +0 -1
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +12 -17
- package/src/form/index.tsx +1 -0
- package/src/hooks/useBuildNavigationController.tsx +273 -84
- package/src/internal/useBuildSideEntityController.tsx +7 -4
- package/src/preview/PropertyPreview.tsx +14 -0
- package/src/routes/FireCMSRoute.tsx +3 -3
- package/src/types/analytics.ts +3 -0
- package/src/types/collections.ts +8 -2
- package/src/types/customization_controller.tsx +9 -0
- package/src/types/entity_actions.tsx +57 -6
- package/src/types/firecms.tsx +9 -0
- package/src/types/navigation.ts +28 -6
- package/src/types/plugins.tsx +24 -1
- package/src/types/properties.ts +8 -0
- package/src/types/property_config.tsx +1 -0
- package/src/types/side_entity_controller.tsx +5 -0
- package/src/util/icons.tsx +22 -7
- package/src/util/join_collections.ts +3 -1
- package/src/util/navigation_from_path.ts +15 -5
- package/src/util/navigation_utils.ts +2 -2
- package/src/util/resolutions.ts +13 -1
|
@@ -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 {
|
|
@@ -41,9 +49,13 @@ export type OnTabChangeParams<M extends Record<string, any>> = {
|
|
|
41
49
|
entityId?: string;
|
|
42
50
|
selectedTab?: string;
|
|
43
51
|
collection: EntityCollection<M>;
|
|
52
|
+
|
|
44
53
|
};
|
|
45
54
|
|
|
46
55
|
export interface EntityEditViewProps<M extends Record<string, any>> {
|
|
56
|
+
/**
|
|
57
|
+
* The database path of the entity, e.g. "users" or "products".
|
|
58
|
+
*/
|
|
47
59
|
path: string;
|
|
48
60
|
/**
|
|
49
61
|
* The navigation path to the entity.
|
|
@@ -158,12 +170,33 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
158
170
|
setUsedEntity(entity);
|
|
159
171
|
}, [entity]);
|
|
160
172
|
|
|
161
|
-
// Instead of using a ref (which does not trigger re-render), we use state for the form context.
|
|
162
173
|
const [formContext, setFormContext] = useState<FormContext<M> | undefined>(undefined);
|
|
163
174
|
|
|
164
175
|
const largeLayout = useLargeLayout();
|
|
165
176
|
|
|
166
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
|
+
}
|
|
167
200
|
|
|
168
201
|
const defaultSelectedView = useMemo(() => resolveDefaultSelectedView(
|
|
169
202
|
collection ? collection.defaultSelectedView : undefined,
|
|
@@ -187,8 +220,6 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
187
220
|
const includeJsonView = collection.includeJsonView === undefined ? true : collection.includeJsonView;
|
|
188
221
|
const hasAdditionalViews = customViewsCount > 0 || subcollectionsCount > 0 || includeJsonView;
|
|
189
222
|
|
|
190
|
-
const plugins = customizationController.plugins;
|
|
191
|
-
|
|
192
223
|
const {
|
|
193
224
|
resolvedEntityViews,
|
|
194
225
|
selectedEntityView,
|
|
@@ -279,7 +310,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
279
310
|
|
|
280
311
|
const subCollectionsViews = subcollections && subcollections.map((subcollection) => {
|
|
281
312
|
const subcollectionId = subcollection.id ?? subcollection.path;
|
|
282
|
-
const newFullPath = usedEntity ? `${path}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(
|
|
313
|
+
const newFullPath = usedEntity ? `${path}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollection.path)}` : undefined;
|
|
283
314
|
const newFullIdPath = fullIdPath ? `${fullIdPath}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollectionId)}` : undefined;
|
|
284
315
|
|
|
285
316
|
if (selectedTab !== subcollectionId) return null;
|
|
@@ -342,6 +373,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
342
373
|
</div> : null;
|
|
343
374
|
|
|
344
375
|
const entityView = <EntityForm<M>
|
|
376
|
+
fullIdPath={fullIdPath}
|
|
345
377
|
collection={collection}
|
|
346
378
|
path={path}
|
|
347
379
|
entityId={entityId ?? usedEntity?.id}
|
|
@@ -411,17 +443,20 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
411
443
|
let result = <div className="relative flex flex-col h-full w-full bg-white dark:bg-surface-900">
|
|
412
444
|
|
|
413
445
|
{shouldShowTopBar && <div
|
|
414
|
-
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)}>
|
|
415
447
|
|
|
416
448
|
{barActions}
|
|
417
449
|
|
|
418
450
|
<div className={"flex-grow"}/>
|
|
419
451
|
|
|
452
|
+
{pluginActionsTop}
|
|
453
|
+
|
|
420
454
|
{globalLoading && <div className="self-center">
|
|
421
455
|
<CircularProgress size={"small"}/>
|
|
422
456
|
</div>}
|
|
423
457
|
|
|
424
458
|
{hasAdditionalViews && <Tabs
|
|
459
|
+
className={"self-end"}
|
|
425
460
|
value={selectedTab}
|
|
426
461
|
onValueChange={(value) => {
|
|
427
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
|
+
}
|
|
@@ -23,6 +23,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
23
23
|
allowFullScreen = true,
|
|
24
24
|
path,
|
|
25
25
|
entityId,
|
|
26
|
+
fullIdPath,
|
|
26
27
|
formProps,
|
|
27
28
|
} = props;
|
|
28
29
|
|
|
@@ -57,6 +58,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
57
58
|
sideEntityController.replace({
|
|
58
59
|
path: params.path,
|
|
59
60
|
entityId: params.entityId,
|
|
61
|
+
fullIdPath: props.fullIdPath,
|
|
60
62
|
selectedTab: params.selectedTab,
|
|
61
63
|
updateUrl: true,
|
|
62
64
|
collection: params.collection,
|
|
@@ -74,7 +76,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
74
76
|
return navigationController.getParentCollectionIds(path);
|
|
75
77
|
}, [navigationController, path]);
|
|
76
78
|
|
|
77
|
-
const collection =
|
|
79
|
+
const collection = navigationController.getCollection(fullIdPath ?? path) ?? props.collection;
|
|
78
80
|
|
|
79
81
|
useEffect(() => {
|
|
80
82
|
function beforeunload(e: any) {
|
|
@@ -110,6 +112,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
110
112
|
<ErrorBoundary>
|
|
111
113
|
<EntityEditView
|
|
112
114
|
{...props}
|
|
115
|
+
fullIdPath={fullIdPath}
|
|
113
116
|
layout={"side_panel"}
|
|
114
117
|
collection={collection}
|
|
115
118
|
parentCollectionIds={parentCollectionIds}
|
|
@@ -133,7 +136,6 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
133
136
|
</IconButton>}
|
|
134
137
|
</>}
|
|
135
138
|
onTabChange={({
|
|
136
|
-
path,
|
|
137
139
|
entityId,
|
|
138
140
|
selectedTab,
|
|
139
141
|
collection,
|
|
@@ -141,6 +143,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
141
143
|
sideEntityController.replace({
|
|
142
144
|
path,
|
|
143
145
|
entityId,
|
|
146
|
+
fullIdPath: props.fullIdPath,
|
|
144
147
|
selectedTab,
|
|
145
148
|
updateUrl: true,
|
|
146
149
|
collection,
|
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
|
};
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
MapFieldBinding,
|
|
11
11
|
MarkdownEditorFieldBinding,
|
|
12
12
|
MultiSelectFieldBinding,
|
|
13
|
+
ReferenceAsStringFieldBinding,
|
|
13
14
|
ReferenceFieldBinding,
|
|
14
15
|
RepeatFieldBinding,
|
|
15
16
|
SelectFieldBinding,
|
|
@@ -211,10 +212,21 @@ export const DEFAULT_FIELD_CONFIGS: Record<string, PropertyConfig<any>> = {
|
|
|
211
212
|
Field: StorageUploadFieldBinding
|
|
212
213
|
}
|
|
213
214
|
},
|
|
215
|
+
reference_as_string: {
|
|
216
|
+
key: "reference_as_string",
|
|
217
|
+
name: "Reference (as string)",
|
|
218
|
+
description: "The value refers to a different collection (it is saved as a string)",
|
|
219
|
+
Icon: LinkIcon,
|
|
220
|
+
color: "#154fb3",
|
|
221
|
+
property: {
|
|
222
|
+
dataType: "string",
|
|
223
|
+
Field: ReferenceAsStringFieldBinding
|
|
224
|
+
}
|
|
225
|
+
},
|
|
214
226
|
reference: {
|
|
215
227
|
key: "reference",
|
|
216
228
|
name: "Reference",
|
|
217
|
-
description: "The value refers to a different collection",
|
|
229
|
+
description: "The value refers to a different collection (it is saved as a reference)",
|
|
218
230
|
Icon: LinkIcon,
|
|
219
231
|
color: "#ff0042",
|
|
220
232
|
property: {
|
|
@@ -348,6 +360,8 @@ export function getDefaultFieldId(property: Property | ResolvedProperty) {
|
|
|
348
360
|
return "email";
|
|
349
361
|
} else if (property.enumValues) {
|
|
350
362
|
return "select";
|
|
363
|
+
} else if (property.reference) {
|
|
364
|
+
return "reference_as_string";
|
|
351
365
|
} else {
|
|
352
366
|
return "text_field";
|
|
353
367
|
}
|
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";
|
|
@@ -57,6 +58,7 @@ export type OnUpdateParams = {
|
|
|
57
58
|
|
|
58
59
|
export type EntityFormProps<M extends Record<string, any>> = {
|
|
59
60
|
path: string;
|
|
61
|
+
fullIdPath?: string;
|
|
60
62
|
collection: EntityCollection<M>;
|
|
61
63
|
entityId?: string;
|
|
62
64
|
entity?: Entity<M>;
|
|
@@ -96,6 +98,7 @@ export type EntityFormProps<M extends Record<string, any>> = {
|
|
|
96
98
|
|
|
97
99
|
export function EntityForm<M extends Record<string, any>>({
|
|
98
100
|
path,
|
|
101
|
+
fullIdPath,
|
|
99
102
|
entityId: entityIdProp,
|
|
100
103
|
collection,
|
|
101
104
|
onValuesModified,
|
|
@@ -123,6 +126,19 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
123
126
|
console.warn(`The collection ${collection.path} has customId and formAutoSave enabled. This is not supported and formAutoSave will be ignored`);
|
|
124
127
|
}
|
|
125
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
|
+
|
|
126
142
|
const authController = useAuthController();
|
|
127
143
|
const [status, setStatus] = useState<EntityStatus>(initialStatus);
|
|
128
144
|
|
|
@@ -433,14 +449,16 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
433
449
|
const plugins = customizationController.plugins;
|
|
434
450
|
|
|
435
451
|
const actionsDisabled = disabled || formex.isSubmitting || (status === "existing" && !formex.dirty) || Boolean(disabledProp);
|
|
452
|
+
const parentCollectionIds = navigationController.getParentCollectionIds(path);
|
|
453
|
+
|
|
436
454
|
if (plugins && collection) {
|
|
437
455
|
const actionProps: PluginFormActionProps = {
|
|
438
456
|
entityId,
|
|
457
|
+
parentCollectionIds,
|
|
439
458
|
path,
|
|
440
459
|
status,
|
|
441
|
-
collection
|
|
460
|
+
collection,
|
|
442
461
|
context,
|
|
443
|
-
currentEntityId: entityId,
|
|
444
462
|
formContext,
|
|
445
463
|
openEntityMode,
|
|
446
464
|
disabled: actionsDisabled,
|
|
@@ -625,7 +643,13 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
625
643
|
variant={"h4"}>
|
|
626
644
|
{title ?? collection.singularName ?? collection.name}
|
|
627
645
|
</Typography>
|
|
628
|
-
|
|
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"}>
|
|
629
653
|
<code
|
|
630
654
|
className={"text-xs select-all text-text-secondary dark:text-text-secondary-dark"}>
|
|
631
655
|
{entity?.path ?? path}/{entityId}
|
|
@@ -635,6 +659,10 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
635
659
|
|
|
636
660
|
{children}
|
|
637
661
|
|
|
662
|
+
{initialEntityId && !entity && initialStatus !== "new" && <Alert color={"info"} size={"small"}>
|
|
663
|
+
This entity does not exist in the database
|
|
664
|
+
</Alert>}
|
|
665
|
+
|
|
638
666
|
{!Builder && !collection.hideIdFromForm &&
|
|
639
667
|
<CustomIdField customId={collection.customId}
|
|
640
668
|
entityId={entityId}
|
|
@@ -668,6 +696,8 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
668
696
|
const dialogActions = <EntityFormActionsComponent
|
|
669
697
|
collection={resolvedCollection}
|
|
670
698
|
path={path}
|
|
699
|
+
fullPath={path}
|
|
700
|
+
fullIdPath={fullIdPath}
|
|
671
701
|
entity={entity}
|
|
672
702
|
layout={forceActionsAtTheBottom ? "bottom" : "side"}
|
|
673
703
|
savingError={savingError}
|
|
@@ -677,6 +707,8 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
677
707
|
pluginActions={pluginActions ?? []}
|
|
678
708
|
openEntityMode={openEntityMode}
|
|
679
709
|
showDefaultActions={showDefaultActions}
|
|
710
|
+
navigateBack={navigateBack}
|
|
711
|
+
formContext={formContext}
|
|
680
712
|
/>;
|
|
681
713
|
|
|
682
714
|
return (
|