@firecms/core 3.0.0-beta.13 → 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/ArrayContainer.d.ts +7 -12
- package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +2 -1
- package/dist/components/EntityPreview.d.ts +4 -2
- 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/components/SelectableTable/SelectableTable.d.ts +1 -1
- package/dist/components/VirtualTable/VirtualTableProps.d.ts +0 -4
- package/dist/components/index.d.ts +1 -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/MapFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/ReferenceAsStringFieldBinding.d.ts +9 -0
- package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +3 -9
- package/dist/form/index.d.ts +1 -0
- package/dist/hooks/useBuildNavigationController.d.ts +50 -8
- package/dist/index.es.js +3259 -1649
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +3265 -1658
- package/dist/index.umd.js.map +1 -1
- package/dist/types/analytics.d.ts +1 -1
- package/dist/types/collections.d.ts +28 -4
- package/dist/types/customization_controller.d.ts +8 -0
- package/dist/types/entity_actions.d.ts +46 -6
- package/dist/types/firecms.d.ts +10 -1
- package/dist/types/navigation.d.ts +26 -5
- package/dist/types/plugins.d.ts +32 -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 +8 -0
- package/dist/util/callbacks.d.ts +2 -0
- package/dist/util/icons.d.ts +2 -2
- package/dist/util/index.d.ts +1 -0
- package/dist/util/navigation_from_path.d.ts +4 -0
- package/dist/util/resolutions.d.ts +2 -1
- package/package.json +8 -6
- package/src/components/ArrayContainer.tsx +409 -294
- package/src/components/ClearFilterSortButton.tsx +1 -1
- package/src/components/ConfirmationDialog.tsx +10 -9
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +6 -0
- package/src/components/EntityCollectionTable/PropertyTableCell.tsx +26 -4
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -3
- package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +13 -2
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +53 -27
- package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +9 -9
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +11 -4
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +3 -2
- package/src/components/EntityPreview.tsx +18 -14
- package/src/components/ErrorView.tsx +1 -1
- package/src/components/FireCMSLogo.tsx +7 -51
- package/src/components/HomePage/DefaultHomePage.tsx +491 -156
- 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 +13 -7
- package/src/components/HomePage/NavigationGroup.tsx +63 -29
- package/src/components/HomePage/RenameGroupDialog.tsx +117 -0
- package/src/components/SelectableTable/SelectableTable.tsx +140 -143
- package/src/components/UnsavedChangesDialog.tsx +6 -2
- package/src/components/VirtualTable/VirtualTable.tsx +8 -30
- package/src/components/VirtualTable/VirtualTableProps.tsx +0 -5
- package/src/components/VirtualTable/fields/VirtualTableInput.tsx +0 -1
- package/src/components/common/default_entity_actions.tsx +25 -9
- package/src/components/common/useDataSourceTableController.tsx +2 -2
- package/src/components/index.tsx +2 -0
- package/src/core/DefaultDrawer.tsx +8 -8
- package/src/core/DrawerNavigationItem.tsx +1 -1
- package/src/core/EntityEditView.tsx +65 -18
- package/src/core/EntityEditViewFormActions.tsx +154 -29
- package/src/core/EntitySidePanel.tsx +19 -21
- package/src/core/FireCMS.tsx +9 -1
- package/src/core/field_configs.tsx +15 -1
- package/src/form/EntityForm.tsx +37 -5
- package/src/form/EntityFormActions.tsx +51 -9
- package/src/form/PropertyFieldBinding.tsx +0 -1
- package/src/form/components/CustomIdField.tsx +3 -1
- package/src/form/components/LabelWithIcon.tsx +1 -1
- package/src/form/components/StorageItemPreview.tsx +1 -1
- package/src/form/components/StorageUploadProgress.tsx +3 -3
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +3 -2
- package/src/form/field_bindings/MapFieldBinding.tsx +5 -4
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +4 -2
- package/src/form/field_bindings/MultiSelectFieldBinding.tsx +1 -1
- package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +135 -0
- package/src/form/field_bindings/SelectFieldBinding.tsx +2 -1
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +216 -153
- package/src/form/index.tsx +1 -0
- package/src/hooks/data/save.ts +0 -6
- package/src/hooks/useBuildNavigationController.tsx +299 -96
- package/src/internal/useBuildSideEntityController.tsx +8 -5
- package/src/preview/PropertyPreview.tsx +14 -0
- package/src/preview/components/ImagePreview.tsx +2 -2
- package/src/preview/components/ReferencePreview.tsx +1 -1
- package/src/preview/property_previews/ArrayOfMapsPreview.tsx +1 -1
- package/src/preview/property_previews/MapPropertyPreview.tsx +1 -1
- package/src/preview/property_previews/SkeletonPropertyComponent.tsx +1 -1
- package/src/preview/property_previews/StringPropertyPreview.tsx +1 -1
- package/src/routes/FireCMSRoute.tsx +3 -3
- package/src/types/analytics.ts +3 -0
- package/src/types/collections.ts +32 -4
- package/src/types/customization_controller.tsx +9 -0
- package/src/types/entity_actions.tsx +57 -6
- package/src/types/firecms.tsx +11 -2
- package/src/types/navigation.ts +34 -6
- package/src/types/plugins.tsx +40 -1
- package/src/types/properties.ts +8 -0
- package/src/types/property_config.tsx +1 -0
- package/src/types/side_entity_controller.tsx +10 -5
- package/src/util/callbacks.ts +119 -0
- package/src/util/icons.tsx +22 -7
- package/src/util/index.ts +1 -0
- package/src/util/join_collections.ts +3 -1
- package/src/util/navigation_from_path.ts +15 -5
- package/src/util/navigation_utils.ts +72 -57
- package/src/util/objects.ts +53 -20
- package/src/util/resolutions.ts +13 -1
- package/dist/components/EntityCollectionTable/internal/popup_field/ElementResizeListener.d.ts +0 -5
- package/src/components/EntityCollectionTable/internal/popup_field/ElementResizeListener.tsx +0 -59
|
@@ -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,
|
|
@@ -182,7 +215,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
182
215
|
|
|
183
216
|
const subcollections = (collection.subcollections ?? []).filter(c => !c.hideFromNavigation);
|
|
184
217
|
const subcollectionsCount = subcollections?.length ?? 0;
|
|
185
|
-
const customViews = collection.entityViews;
|
|
218
|
+
const customViews = collection.entityViews ?? [];
|
|
186
219
|
const customViewsCount = customViews?.length ?? 0;
|
|
187
220
|
const includeJsonView = collection.includeJsonView === undefined ? true : collection.includeJsonView;
|
|
188
221
|
const hasAdditionalViews = customViewsCount > 0 || subcollectionsCount > 0 || includeJsonView;
|
|
@@ -212,7 +245,6 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
212
245
|
}
|
|
213
246
|
|
|
214
247
|
if (!entityId) {
|
|
215
|
-
console.error("INTERNAL: entityId is not defined");
|
|
216
248
|
return null;
|
|
217
249
|
}
|
|
218
250
|
|
|
@@ -254,6 +286,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
254
286
|
<ErrorBoundary>
|
|
255
287
|
{usedFormContext && <Builder
|
|
256
288
|
collection={collection}
|
|
289
|
+
parentCollectionIds={parentCollectionIds}
|
|
257
290
|
entity={usedEntity}
|
|
258
291
|
modifiedValues={usedFormContext?.formex?.values ?? usedEntity?.values}
|
|
259
292
|
formContext={usedFormContext}
|
|
@@ -277,7 +310,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
277
310
|
|
|
278
311
|
const subCollectionsViews = subcollections && subcollections.map((subcollection) => {
|
|
279
312
|
const subcollectionId = subcollection.id ?? subcollection.path;
|
|
280
|
-
const newFullPath = usedEntity ? `${path}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(
|
|
313
|
+
const newFullPath = usedEntity ? `${path}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollection.path)}` : undefined;
|
|
281
314
|
const newFullIdPath = fullIdPath ? `${fullIdPath}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollectionId)}` : undefined;
|
|
282
315
|
|
|
283
316
|
if (selectedTab !== subcollectionId) return null;
|
|
@@ -335,10 +368,12 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
335
368
|
entity={entity}
|
|
336
369
|
path={path}
|
|
337
370
|
collection={collection}/>
|
|
371
|
+
<div className="h-16"/>
|
|
338
372
|
</div>
|
|
339
373
|
</div> : null;
|
|
340
374
|
|
|
341
375
|
const entityView = <EntityForm<M>
|
|
376
|
+
fullIdPath={fullIdPath}
|
|
342
377
|
collection={collection}
|
|
343
378
|
path={path}
|
|
344
379
|
entityId={entityId ?? usedEntity?.id}
|
|
@@ -384,31 +419,44 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
384
419
|
</Tab>
|
|
385
420
|
);
|
|
386
421
|
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
422
|
+
const customViewTabsStart = resolvedEntityViews.filter(view => view.position === "start")
|
|
423
|
+
.map((view) =>
|
|
424
|
+
<Tab
|
|
425
|
+
className={!view.tabComponent ? "text-sm min-w-[120px]" : undefined}
|
|
426
|
+
value={view.key}
|
|
427
|
+
key={`entity_detail_collection_tab_${view.name}`}>
|
|
428
|
+
{view.tabComponent ?? view.name}
|
|
429
|
+
</Tab>
|
|
430
|
+
);
|
|
431
|
+
const customViewTabsEnd = resolvedEntityViews.filter(view => !view.position || view.position === "end")
|
|
432
|
+
.map((view) =>
|
|
433
|
+
<Tab
|
|
434
|
+
className={!view.tabComponent ? "text-sm min-w-[120px]" : undefined}
|
|
435
|
+
value={view.key}
|
|
436
|
+
key={`entity_detail_collection_tab_${view.name}`}>
|
|
437
|
+
{view.tabComponent ?? view.name}
|
|
438
|
+
</Tab>
|
|
439
|
+
);
|
|
395
440
|
|
|
396
441
|
const shouldShowTopBar = Boolean(barActions) || hasAdditionalViews;
|
|
397
442
|
|
|
398
443
|
let result = <div className="relative flex flex-col h-full w-full bg-white dark:bg-surface-900">
|
|
399
444
|
|
|
400
445
|
{shouldShowTopBar && <div
|
|
401
|
-
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)}>
|
|
402
447
|
|
|
403
448
|
{barActions}
|
|
404
449
|
|
|
405
450
|
<div className={"flex-grow"}/>
|
|
406
451
|
|
|
452
|
+
{pluginActionsTop}
|
|
453
|
+
|
|
407
454
|
{globalLoading && <div className="self-center">
|
|
408
455
|
<CircularProgress size={"small"}/>
|
|
409
456
|
</div>}
|
|
410
457
|
|
|
411
458
|
{hasAdditionalViews && <Tabs
|
|
459
|
+
className={"self-end"}
|
|
412
460
|
value={selectedTab}
|
|
413
461
|
onValueChange={(value) => {
|
|
414
462
|
onSideTabClick(value);
|
|
@@ -417,11 +465,12 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
417
465
|
{includeJsonView && <Tab
|
|
418
466
|
disabled={!hasAdditionalViews}
|
|
419
467
|
value={JSON_TAB_VALUE}
|
|
420
|
-
innerClassName={"block"}
|
|
421
468
|
className={"text-sm"}>
|
|
422
469
|
<CodeIcon size={"small"}/>
|
|
423
470
|
</Tab>}
|
|
424
471
|
|
|
472
|
+
{customViewTabsStart}
|
|
473
|
+
|
|
425
474
|
<Tab
|
|
426
475
|
disabled={!hasAdditionalViews}
|
|
427
476
|
value={MAIN_TAB_VALUE}
|
|
@@ -430,7 +479,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
430
479
|
</Tab>
|
|
431
480
|
|
|
432
481
|
|
|
433
|
-
{
|
|
482
|
+
{customViewTabsEnd}
|
|
434
483
|
|
|
435
484
|
{subcollectionTabs}
|
|
436
485
|
</Tabs>}
|
|
@@ -453,8 +502,6 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
453
502
|
|
|
454
503
|
</div>;
|
|
455
504
|
|
|
456
|
-
const plugins = customizationController.plugins;
|
|
457
|
-
|
|
458
505
|
if (plugins) {
|
|
459
506
|
plugins.forEach((plugin: FireCMSPlugin) => {
|
|
460
507
|
if (plugin.form?.provider) {
|
|
@@ -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
|
+
}
|
|
@@ -19,11 +19,19 @@ import { useLocation, useNavigate } from "react-router-dom";
|
|
|
19
19
|
*/
|
|
20
20
|
export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
21
21
|
|
|
22
|
+
const {
|
|
23
|
+
allowFullScreen = true,
|
|
24
|
+
path,
|
|
25
|
+
entityId,
|
|
26
|
+
fullIdPath,
|
|
27
|
+
formProps,
|
|
28
|
+
} = props;
|
|
29
|
+
|
|
22
30
|
const {
|
|
23
31
|
blocked,
|
|
24
32
|
setBlocked,
|
|
25
33
|
setBlockedNavigationMessage,
|
|
26
|
-
close
|
|
34
|
+
close,
|
|
27
35
|
} = useSideDialogContext();
|
|
28
36
|
|
|
29
37
|
const navigate = useNavigate();
|
|
@@ -50,6 +58,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
50
58
|
sideEntityController.replace({
|
|
51
59
|
path: params.path,
|
|
52
60
|
entityId: params.entityId,
|
|
61
|
+
fullIdPath: props.fullIdPath,
|
|
53
62
|
selectedTab: params.selectedTab,
|
|
54
63
|
updateUrl: true,
|
|
55
64
|
collection: params.collection,
|
|
@@ -64,22 +73,10 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
const parentCollectionIds = useMemo(() => {
|
|
67
|
-
return navigationController.getParentCollectionIds(
|
|
68
|
-
}, [navigationController,
|
|
69
|
-
|
|
70
|
-
const collection = useMemo(() => {
|
|
71
|
-
if (props.collection) {
|
|
72
|
-
return props.collection;
|
|
73
|
-
}
|
|
76
|
+
return navigationController.getParentCollectionIds(path);
|
|
77
|
+
}, [navigationController, path]);
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
if (registryCollection) {
|
|
77
|
-
return registryCollection;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
console.error("ERROR: No collection found in path `", props.path, "`. Entity id: ", props.entityId);
|
|
81
|
-
throw Error("ERROR: No collection found in path `" + props.path + "`. Make sure you have defined a collection for this path in the root navigation.");
|
|
82
|
-
}, [navigationController, props.collection]);
|
|
79
|
+
const collection = navigationController.getCollection(fullIdPath ?? path) ?? props.collection;
|
|
83
80
|
|
|
84
81
|
useEffect(() => {
|
|
85
82
|
function beforeunload(e: any) {
|
|
@@ -115,6 +112,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
115
112
|
<ErrorBoundary>
|
|
116
113
|
<EntityEditView
|
|
117
114
|
{...props}
|
|
115
|
+
fullIdPath={fullIdPath}
|
|
118
116
|
layout={"side_panel"}
|
|
119
117
|
collection={collection}
|
|
120
118
|
parentCollectionIds={parentCollectionIds}
|
|
@@ -126,19 +124,18 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
126
124
|
onClick={onClose}>
|
|
127
125
|
<CloseIcon size={"small"}/>
|
|
128
126
|
</IconButton>
|
|
129
|
-
<IconButton
|
|
127
|
+
{allowFullScreen && <IconButton
|
|
130
128
|
className="self-center"
|
|
131
129
|
onClick={() => {
|
|
132
|
-
if (
|
|
130
|
+
if (entityId)
|
|
133
131
|
navigate(location.pathname);
|
|
134
132
|
else
|
|
135
133
|
navigate(location.pathname + "#new");
|
|
136
134
|
}}>
|
|
137
135
|
<OpenInFullIcon size={"small"}/>
|
|
138
|
-
</IconButton>
|
|
136
|
+
</IconButton>}
|
|
139
137
|
</>}
|
|
140
138
|
onTabChange={({
|
|
141
|
-
path,
|
|
142
139
|
entityId,
|
|
143
140
|
selectedTab,
|
|
144
141
|
collection,
|
|
@@ -146,12 +143,13 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
146
143
|
sideEntityController.replace({
|
|
147
144
|
path,
|
|
148
145
|
entityId,
|
|
146
|
+
fullIdPath: props.fullIdPath,
|
|
149
147
|
selectedTab,
|
|
150
148
|
updateUrl: true,
|
|
151
149
|
collection,
|
|
152
150
|
});
|
|
153
151
|
}}
|
|
154
|
-
formProps={
|
|
152
|
+
formProps={formProps}
|
|
155
153
|
/>
|
|
156
154
|
</ErrorBoundary>
|
|
157
155
|
|
package/src/core/FireCMS.tsx
CHANGED
|
@@ -44,15 +44,22 @@ export function FireCMS<USER extends User>(props: FireCMSProps<USER>) {
|
|
|
44
44
|
authController,
|
|
45
45
|
storageSource,
|
|
46
46
|
dataSourceDelegate,
|
|
47
|
-
plugins,
|
|
47
|
+
plugins: pluginsProp,
|
|
48
48
|
onAnalyticsEvent,
|
|
49
49
|
propertyConfigs,
|
|
50
50
|
entityViews,
|
|
51
|
+
entityActions,
|
|
51
52
|
components,
|
|
52
53
|
navigationController,
|
|
53
54
|
apiKey
|
|
54
55
|
} = props;
|
|
55
56
|
|
|
57
|
+
if (pluginsProp) {
|
|
58
|
+
console.warn("The `plugins` prop is deprecated in the FireCMS component. You should pass your plugins to `useBuildNavigationController` instead.");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const plugins = navigationController.plugins ?? pluginsProp;
|
|
62
|
+
|
|
56
63
|
const sideDialogsController = useBuildSideDialogsController();
|
|
57
64
|
const sideEntityController = useBuildSideEntityController(navigationController, sideDialogsController, authController);
|
|
58
65
|
|
|
@@ -66,6 +73,7 @@ export function FireCMS<USER extends User>(props: FireCMSProps<USER>) {
|
|
|
66
73
|
entityLinkBuilder,
|
|
67
74
|
plugins,
|
|
68
75
|
entityViews: entityViews ?? [],
|
|
76
|
+
entityActions: entityActions ?? [],
|
|
69
77
|
propertyConfigs: propertyConfigs ?? {},
|
|
70
78
|
components
|
|
71
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
|
}
|