@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.
Files changed (124) hide show
  1. package/dist/components/ArrayContainer.d.ts +7 -12
  2. package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +2 -1
  3. package/dist/components/EntityPreview.d.ts +4 -2
  4. package/dist/components/HomePage/DefaultHomePage.d.ts +2 -15
  5. package/dist/components/HomePage/HomePageDnD.d.ts +76 -0
  6. package/dist/components/HomePage/NavigationCard.d.ts +3 -1
  7. package/dist/components/HomePage/NavigationCardBinding.d.ts +3 -2
  8. package/dist/components/HomePage/NavigationGroup.d.ts +7 -1
  9. package/dist/components/HomePage/RenameGroupDialog.d.ts +9 -0
  10. package/dist/components/SelectableTable/SelectableTable.d.ts +1 -1
  11. package/dist/components/VirtualTable/VirtualTableProps.d.ts +0 -4
  12. package/dist/components/index.d.ts +1 -0
  13. package/dist/core/EntityEditView.d.ts +3 -0
  14. package/dist/core/EntityEditViewFormActions.d.ts +1 -1
  15. package/dist/core/field_configs.d.ts +1 -1
  16. package/dist/form/EntityForm.d.ts +2 -1
  17. package/dist/form/EntityFormActions.d.ts +6 -2
  18. package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
  19. package/dist/form/field_bindings/ReferenceAsStringFieldBinding.d.ts +9 -0
  20. package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +3 -9
  21. package/dist/form/index.d.ts +1 -0
  22. package/dist/hooks/useBuildNavigationController.d.ts +50 -8
  23. package/dist/index.es.js +3259 -1649
  24. package/dist/index.es.js.map +1 -1
  25. package/dist/index.umd.js +3265 -1658
  26. package/dist/index.umd.js.map +1 -1
  27. package/dist/types/analytics.d.ts +1 -1
  28. package/dist/types/collections.d.ts +28 -4
  29. package/dist/types/customization_controller.d.ts +8 -0
  30. package/dist/types/entity_actions.d.ts +46 -6
  31. package/dist/types/firecms.d.ts +10 -1
  32. package/dist/types/navigation.d.ts +26 -5
  33. package/dist/types/plugins.d.ts +32 -1
  34. package/dist/types/properties.d.ts +7 -0
  35. package/dist/types/property_config.d.ts +1 -1
  36. package/dist/types/side_entity_controller.d.ts +8 -0
  37. package/dist/util/callbacks.d.ts +2 -0
  38. package/dist/util/icons.d.ts +2 -2
  39. package/dist/util/index.d.ts +1 -0
  40. package/dist/util/navigation_from_path.d.ts +4 -0
  41. package/dist/util/resolutions.d.ts +2 -1
  42. package/package.json +8 -6
  43. package/src/components/ArrayContainer.tsx +409 -294
  44. package/src/components/ClearFilterSortButton.tsx +1 -1
  45. package/src/components/ConfirmationDialog.tsx +10 -9
  46. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +6 -0
  47. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +26 -4
  48. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -3
  49. package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +13 -2
  50. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +53 -27
  51. package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +9 -9
  52. package/src/components/EntityCollectionView/EntityCollectionView.tsx +11 -4
  53. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +3 -2
  54. package/src/components/EntityPreview.tsx +18 -14
  55. package/src/components/ErrorView.tsx +1 -1
  56. package/src/components/FireCMSLogo.tsx +7 -51
  57. package/src/components/HomePage/DefaultHomePage.tsx +491 -156
  58. package/src/components/HomePage/FavouritesView.tsx +3 -3
  59. package/src/components/HomePage/HomePageDnD.tsx +599 -0
  60. package/src/components/HomePage/NavigationCard.tsx +47 -38
  61. package/src/components/HomePage/NavigationCardBinding.tsx +13 -7
  62. package/src/components/HomePage/NavigationGroup.tsx +63 -29
  63. package/src/components/HomePage/RenameGroupDialog.tsx +117 -0
  64. package/src/components/SelectableTable/SelectableTable.tsx +140 -143
  65. package/src/components/UnsavedChangesDialog.tsx +6 -2
  66. package/src/components/VirtualTable/VirtualTable.tsx +8 -30
  67. package/src/components/VirtualTable/VirtualTableProps.tsx +0 -5
  68. package/src/components/VirtualTable/fields/VirtualTableInput.tsx +0 -1
  69. package/src/components/common/default_entity_actions.tsx +25 -9
  70. package/src/components/common/useDataSourceTableController.tsx +2 -2
  71. package/src/components/index.tsx +2 -0
  72. package/src/core/DefaultDrawer.tsx +8 -8
  73. package/src/core/DrawerNavigationItem.tsx +1 -1
  74. package/src/core/EntityEditView.tsx +65 -18
  75. package/src/core/EntityEditViewFormActions.tsx +154 -29
  76. package/src/core/EntitySidePanel.tsx +19 -21
  77. package/src/core/FireCMS.tsx +9 -1
  78. package/src/core/field_configs.tsx +15 -1
  79. package/src/form/EntityForm.tsx +37 -5
  80. package/src/form/EntityFormActions.tsx +51 -9
  81. package/src/form/PropertyFieldBinding.tsx +0 -1
  82. package/src/form/components/CustomIdField.tsx +3 -1
  83. package/src/form/components/LabelWithIcon.tsx +1 -1
  84. package/src/form/components/StorageItemPreview.tsx +1 -1
  85. package/src/form/components/StorageUploadProgress.tsx +3 -3
  86. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +3 -2
  87. package/src/form/field_bindings/MapFieldBinding.tsx +5 -4
  88. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +4 -2
  89. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +1 -1
  90. package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +135 -0
  91. package/src/form/field_bindings/SelectFieldBinding.tsx +2 -1
  92. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +216 -153
  93. package/src/form/index.tsx +1 -0
  94. package/src/hooks/data/save.ts +0 -6
  95. package/src/hooks/useBuildNavigationController.tsx +299 -96
  96. package/src/internal/useBuildSideEntityController.tsx +8 -5
  97. package/src/preview/PropertyPreview.tsx +14 -0
  98. package/src/preview/components/ImagePreview.tsx +2 -2
  99. package/src/preview/components/ReferencePreview.tsx +1 -1
  100. package/src/preview/property_previews/ArrayOfMapsPreview.tsx +1 -1
  101. package/src/preview/property_previews/MapPropertyPreview.tsx +1 -1
  102. package/src/preview/property_previews/SkeletonPropertyComponent.tsx +1 -1
  103. package/src/preview/property_previews/StringPropertyPreview.tsx +1 -1
  104. package/src/routes/FireCMSRoute.tsx +3 -3
  105. package/src/types/analytics.ts +3 -0
  106. package/src/types/collections.ts +32 -4
  107. package/src/types/customization_controller.tsx +9 -0
  108. package/src/types/entity_actions.tsx +57 -6
  109. package/src/types/firecms.tsx +11 -2
  110. package/src/types/navigation.ts +34 -6
  111. package/src/types/plugins.tsx +40 -1
  112. package/src/types/properties.ts +8 -0
  113. package/src/types/property_config.tsx +1 -0
  114. package/src/types/side_entity_controller.tsx +10 -5
  115. package/src/util/callbacks.ts +119 -0
  116. package/src/util/icons.tsx +22 -7
  117. package/src/util/index.ts +1 -0
  118. package/src/util/join_collections.ts +3 -1
  119. package/src/util/navigation_from_path.ts +15 -5
  120. package/src/util/navigation_utils.ts +72 -57
  121. package/src/util/objects.ts +53 -20
  122. package/src/util/resolutions.ts +13 -1
  123. package/dist/components/EntityCollectionTable/internal/popup_field/ElementResizeListener.d.ts +0 -5
  124. 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 { Entity, EntityCollection, EntityStatus, FireCMSPlugin, FormContext, User } from "../types";
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(subcollectionId)}` : undefined;
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 customViewTabs = resolvedEntityViews.map((view) =>
388
- <Tab
389
- className="text-sm min-w-[120px]"
390
- value={view.key}
391
- key={`entity_detail_collection_tab_${view.name}`}>
392
- {view.name}
393
- </Tab>
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 items-end bg-surface-50 dark:bg-surface-900", defaultBorderMixin)}>
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
- {customViewTabs}
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 { Entity, EntityAction, FireCMSContext, ResolvedEntityCollection, SideEntityController } from "../types";
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 { Button, cls, defaultBorderMixin, DialogActions, IconButton, LoadingButton, Typography } from "@firecms/ui";
7
- import { useAuthController, useFireCMSContext, useSideEntityController } from "../hooks";
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 position={"absolute"}>
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
- {entity && formActions.length > 0 && <div className="flex-grow flex overflow-auto no-scrollbar">
115
- {formActions.map(action => (
116
- <IconButton
117
- key={action.name}
118
- color="primary"
119
- onClick={(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
120
- event.stopPropagation();
121
- if (entity)
122
- action.onClick({
123
- entity,
124
- fullPath: collection.path,
125
- collection: collection,
126
- context,
127
- sideEntityController,
128
- openEntityMode: openEntityMode
129
- });
130
- }}>
131
- {action.icon}
132
- </IconButton>
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(props.path);
68
- }, [navigationController, props.path]);
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
- const registryCollection = navigationController.getCollection(props.path);
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 (props.entityId)
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={props.formProps}
152
+ formProps={formProps}
155
153
  />
156
154
  </ErrorBoundary>
157
155
 
@@ -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
  }