@firecms/core 3.0.0-beta.11 → 3.0.0-beta.13

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 (201) hide show
  1. package/dist/app/Scaffold.d.ts +4 -0
  2. package/dist/components/ArrayContainer.d.ts +14 -3
  3. package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +2 -1
  4. package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +2 -2
  5. package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +15 -1
  6. package/dist/components/EntityCollectionTable/index.d.ts +1 -1
  7. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +8 -0
  8. package/dist/components/EntityJsonPreview.d.ts +3 -0
  9. package/dist/components/PropertyIdCopyTooltip.d.ts +1 -1
  10. package/dist/components/SelectableTable/SelectableTable.d.ts +12 -2
  11. package/dist/components/SelectableTable/filters/ReferenceFilterField.d.ts +1 -1
  12. package/dist/components/UnsavedChangesDialog.d.ts +8 -0
  13. package/dist/components/VirtualTable/VirtualTableProps.d.ts +13 -0
  14. package/dist/components/common/default_entity_actions.d.ts +0 -2
  15. package/dist/components/common/index.d.ts +1 -1
  16. package/dist/components/common/useColumnsIds.d.ts +1 -0
  17. package/dist/components/common/{useDataSourceEntityCollectionTableController.d.ts → useDataSourceTableController.d.ts} +10 -2
  18. package/dist/components/common/useDebouncedCallback.d.ts +1 -0
  19. package/dist/components/common/useScrollRestoration.d.ts +14 -0
  20. package/dist/contexts/BreacrumbsContext.d.ts +8 -0
  21. package/dist/core/DefaultAppBar.d.ts +8 -2
  22. package/dist/core/EntityEditView.d.ts +36 -21
  23. package/dist/core/EntityEditViewFormActions.d.ts +2 -0
  24. package/dist/core/FireCMS.d.ts +2 -2
  25. package/dist/core/FireCMSRouter.d.ts +4 -0
  26. package/dist/core/NavigationRoutes.d.ts +0 -1
  27. package/dist/core/SideDialogs.d.ts +4 -2
  28. package/dist/core/index.d.ts +2 -1
  29. package/dist/form/EntityForm.d.ts +49 -0
  30. package/dist/form/EntityFormActions.d.ts +17 -0
  31. package/dist/form/PropertyFieldBinding.d.ts +1 -1
  32. package/dist/form/components/FormEntry.d.ts +6 -0
  33. package/dist/form/components/FormLayout.d.ts +5 -0
  34. package/dist/form/components/index.d.ts +2 -0
  35. package/dist/form/field_bindings/ArrayCustomShapedFieldBinding.d.ts +1 -1
  36. package/dist/form/field_bindings/ArrayOfReferencesFieldBinding.d.ts +1 -1
  37. package/dist/form/field_bindings/BlockFieldBinding.d.ts +1 -1
  38. package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
  39. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
  40. package/dist/form/field_bindings/RepeatFieldBinding.d.ts +1 -1
  41. package/dist/form/index.d.ts +16 -16
  42. package/dist/hooks/data/save.d.ts +1 -1
  43. package/dist/hooks/data/useEntityFetch.d.ts +2 -1
  44. package/dist/hooks/useBreadcrumbsController.d.ts +26 -0
  45. package/dist/hooks/useBuildNavigationController.d.ts +4 -1
  46. package/dist/hooks/useModeController.d.ts +1 -2
  47. package/dist/index.es.js +15842 -14396
  48. package/dist/index.es.js.map +1 -1
  49. package/dist/index.umd.js +15833 -14389
  50. package/dist/index.umd.js.map +1 -1
  51. package/dist/internal/useBuildDataSource.d.ts +3 -2
  52. package/dist/internal/useBuildSideEntityController.d.ts +3 -3
  53. package/dist/internal/useUnsavedChangesDialog.d.ts +7 -9
  54. package/dist/routes/CustomCMSRoute.d.ts +4 -0
  55. package/dist/routes/FireCMSRoute.d.ts +1 -0
  56. package/dist/routes/HomePageRoute.d.ts +3 -0
  57. package/dist/types/collections.d.ts +36 -10
  58. package/dist/types/datasource.d.ts +2 -2
  59. package/dist/types/dialogs_controller.d.ts +7 -3
  60. package/dist/types/entities.d.ts +1 -1
  61. package/dist/types/entity_actions.d.ts +10 -0
  62. package/dist/types/fields.d.ts +13 -3
  63. package/dist/types/firecms.d.ts +1 -1
  64. package/dist/types/navigation.d.ts +33 -12
  65. package/dist/types/plugins.d.ts +2 -0
  66. package/dist/types/properties.d.ts +40 -4
  67. package/dist/types/side_entity_controller.d.ts +6 -1
  68. package/dist/util/builders.d.ts +1 -1
  69. package/dist/util/createFormexStub.d.ts +2 -0
  70. package/dist/util/entity_actions.d.ts +2 -0
  71. package/dist/util/entity_cache.d.ts +23 -0
  72. package/dist/util/index.d.ts +1 -0
  73. package/dist/util/navigation_from_path.d.ts +6 -1
  74. package/dist/util/navigation_utils.d.ts +13 -1
  75. package/dist/util/objects.d.ts +1 -1
  76. package/dist/util/property_utils.d.ts +2 -2
  77. package/dist/util/references.d.ts +2 -2
  78. package/dist/util/resolutions.d.ts +12 -2
  79. package/package.json +20 -21
  80. package/src/app/Scaffold.tsx +13 -3
  81. package/src/components/ArrayContainer.tsx +60 -24
  82. package/src/components/CircularProgressCenter.tsx +1 -1
  83. package/src/components/DeleteEntityDialog.tsx +5 -3
  84. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +46 -23
  85. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +10 -4
  86. package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +18 -1
  87. package/src/components/EntityCollectionTable/index.tsx +1 -1
  88. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -3
  89. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +10 -13
  90. package/src/components/EntityCollectionView/EntityCollectionView.tsx +105 -47
  91. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +4 -2
  92. package/src/components/EntityJsonPreview.tsx +66 -0
  93. package/src/components/EntityPreview.tsx +10 -3
  94. package/src/components/EntityView.tsx +4 -1
  95. package/src/components/NotFoundPage.tsx +2 -2
  96. package/src/components/PropertyIdCopyTooltip.tsx +2 -3
  97. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +14 -7
  98. package/src/components/SelectableTable/SelectableTable.tsx +18 -3
  99. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +7 -7
  100. package/src/components/UnsavedChangesDialog.tsx +42 -0
  101. package/src/components/VirtualTable/VirtualTable.tsx +48 -3
  102. package/src/components/VirtualTable/VirtualTableProps.tsx +15 -0
  103. package/src/components/VirtualTable/fields/VirtualTableInput.tsx +1 -1
  104. package/src/components/common/default_entity_actions.tsx +37 -37
  105. package/src/components/common/index.ts +1 -1
  106. package/src/components/common/useColumnsIds.tsx +1 -1
  107. package/src/components/common/useDataSourceTableController.tsx +420 -0
  108. package/src/components/common/useDebouncedCallback.tsx +20 -0
  109. package/src/components/common/useScrollRestoration.tsx +68 -0
  110. package/src/contexts/BreacrumbsContext.tsx +38 -0
  111. package/src/contexts/DialogsProvider.tsx +3 -2
  112. package/src/contexts/ModeController.tsx +1 -3
  113. package/src/core/DefaultAppBar.tsx +59 -17
  114. package/src/core/DefaultDrawer.tsx +1 -1
  115. package/src/core/EntityEditView.tsx +354 -1000
  116. package/src/core/EntityEditViewFormActions.tsx +199 -0
  117. package/src/core/EntitySidePanel.tsx +85 -15
  118. package/src/core/FireCMS.tsx +20 -16
  119. package/src/core/FireCMSRouter.tsx +17 -0
  120. package/src/core/NavigationRoutes.tsx +23 -32
  121. package/src/core/SideDialogs.tsx +20 -11
  122. package/src/core/index.tsx +4 -2
  123. package/src/form/EntityForm.tsx +782 -0
  124. package/src/form/EntityFormActions.tsx +169 -0
  125. package/src/form/PropertyFieldBinding.tsx +21 -16
  126. package/src/form/components/FormEntry.tsx +22 -0
  127. package/src/form/components/FormLayout.tsx +16 -0
  128. package/src/form/components/LabelWithIcon.tsx +6 -3
  129. package/src/form/components/StorageUploadProgress.tsx +1 -1
  130. package/src/form/components/index.tsx +2 -0
  131. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +8 -5
  132. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +5 -2
  133. package/src/form/field_bindings/BlockFieldBinding.tsx +5 -3
  134. package/src/form/field_bindings/DateTimeFieldBinding.tsx +1 -0
  135. package/src/form/field_bindings/KeyValueFieldBinding.tsx +1 -1
  136. package/src/form/field_bindings/MapFieldBinding.tsx +14 -16
  137. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +15 -8
  138. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +3 -3
  139. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +2 -2
  140. package/src/form/field_bindings/ReferenceFieldBinding.tsx +1 -1
  141. package/src/form/field_bindings/RepeatFieldBinding.tsx +13 -4
  142. package/src/form/field_bindings/SelectFieldBinding.tsx +2 -2
  143. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +11 -6
  144. package/src/form/field_bindings/SwitchFieldBinding.tsx +1 -0
  145. package/src/form/field_bindings/TextFieldBinding.tsx +1 -0
  146. package/src/form/index.tsx +16 -32
  147. package/src/form/validation.ts +12 -6
  148. package/src/hooks/data/save.ts +25 -30
  149. package/src/hooks/data/useCollectionFetch.tsx +1 -1
  150. package/src/hooks/data/useEntityFetch.tsx +5 -1
  151. package/src/hooks/useBreadcrumbsController.tsx +31 -0
  152. package/src/hooks/useBuildModeController.tsx +15 -28
  153. package/src/hooks/useBuildNavigationController.tsx +81 -24
  154. package/src/hooks/useFireCMSContext.tsx +0 -30
  155. package/src/hooks/useLargeLayout.tsx +0 -35
  156. package/src/hooks/useModeController.tsx +1 -2
  157. package/src/hooks/useResolvedNavigationFrom.tsx +4 -6
  158. package/src/internal/useBuildDataSource.ts +9 -5
  159. package/src/internal/useBuildSideDialogsController.tsx +3 -2
  160. package/src/internal/useBuildSideEntityController.tsx +135 -88
  161. package/src/internal/useUnsavedChangesDialog.tsx +126 -92
  162. package/src/preview/PropertyPreview.tsx +12 -10
  163. package/src/preview/components/UrlComponentPreview.tsx +17 -18
  164. package/src/preview/property_previews/ArrayOfMapsPreview.tsx +4 -3
  165. package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +4 -3
  166. package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +4 -2
  167. package/src/preview/property_previews/ArrayOfStringsPreview.tsx +4 -3
  168. package/src/preview/property_previews/ArrayOneOfPreview.tsx +4 -2
  169. package/src/preview/property_previews/ArrayPropertyPreview.tsx +4 -2
  170. package/src/preview/property_previews/MapPropertyPreview.tsx +3 -2
  171. package/src/routes/CustomCMSRoute.tsx +21 -0
  172. package/src/routes/FireCMSRoute.tsx +246 -0
  173. package/src/routes/HomePageRoute.tsx +17 -0
  174. package/src/types/collections.ts +45 -13
  175. package/src/types/datasource.ts +2 -1
  176. package/src/types/dialogs_controller.tsx +7 -3
  177. package/src/types/entities.ts +1 -1
  178. package/src/types/entity_actions.tsx +12 -0
  179. package/src/types/fields.tsx +18 -3
  180. package/src/types/firecms.tsx +1 -1
  181. package/src/types/navigation.ts +41 -16
  182. package/src/types/plugins.tsx +2 -0
  183. package/src/types/properties.ts +48 -4
  184. package/src/types/side_dialogs_controller.tsx +2 -0
  185. package/src/types/side_entity_controller.tsx +11 -1
  186. package/src/util/builders.ts +5 -3
  187. package/src/util/createFormexStub.tsx +62 -0
  188. package/src/util/entity_actions.ts +28 -0
  189. package/src/util/entity_cache.ts +204 -0
  190. package/src/util/index.ts +1 -0
  191. package/src/util/join_collections.ts +6 -1
  192. package/src/util/navigation_from_path.ts +18 -7
  193. package/src/util/navigation_utils.ts +115 -14
  194. package/src/util/objects.ts +9 -2
  195. package/src/util/parent_references_from_path.ts +3 -3
  196. package/src/util/property_utils.tsx +7 -3
  197. package/src/util/references.ts +8 -6
  198. package/src/util/resolutions.ts +41 -12
  199. package/src/util/useStorageUploadController.tsx +60 -23
  200. package/src/components/common/useDataSourceEntityCollectionTableController.tsx +0 -236
  201. /package/src/util/{common.tsx → common.ts} +0 -0
@@ -1,104 +1,77 @@
1
- import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import {
3
- CMSAnalyticsEvent,
4
- Entity,
5
- EntityAction,
6
- EntityCollection,
7
- EntityCustomView,
8
- EntityStatus,
9
- EntityValues,
10
- FireCMSPlugin,
11
- FormContext,
12
- PluginFormActionProps,
13
- PropertyFieldBindingProps,
14
- ResolvedEntityCollection,
15
- User
16
- } from "../types";
17
- import equal from "react-fast-compare"
1
+ import React, { useEffect, useMemo, useState } from "react";
2
+ import { Entity, EntityCollection, EntityStatus, FireCMSPlugin, FormContext, User } from "../types";
18
3
 
4
+ import { CircularProgressCenter, EntityCollectionView, EntityView, ErrorBoundary } from "../components";
19
5
  import {
20
- CircularProgressCenter,
21
- copyEntityAction,
22
- deleteEntityAction,
23
- EntityCollectionView,
24
- EntityView,
25
- ErrorBoundary,
26
- getFormFieldKeys,
27
- } from "../components";
28
- import {
29
- canCreateEntity,
30
- canDeleteEntity,
31
6
  canEditEntity,
32
- getDefaultValuesFor,
33
- getEntityTitlePropertyKey,
34
- getValueInPath,
35
- isHidden,
36
- isReadOnly,
37
7
  removeInitialAndTrailingSlashes,
38
8
  resolveCollection,
39
9
  resolveDefaultSelectedView,
40
- resolveEntityView,
41
- useDebouncedCallback
10
+ resolvedSelectedEntityView
42
11
  } from "../util";
43
12
 
44
13
  import {
45
- saveEntityWithCallbacks,
46
14
  useAuthController,
47
15
  useCustomizationController,
48
- useDataSource,
49
16
  useEntityFetch,
50
17
  useFireCMSContext,
51
- useSideEntityController,
52
- useSnackbarController
18
+ useLargeLayout
53
19
  } from "../hooks";
54
- import {
55
- Alert,
56
- Button,
57
- CircularProgress,
58
- CloseIcon,
59
- cls,
60
- defaultBorderMixin,
61
- DialogActions,
62
- IconButton,
63
- NotesIcon,
64
- paperMixin,
65
- Tab,
66
- Tabs,
67
- Typography
68
- } from "@firecms/ui";
69
- import { useSideDialogContext } from "./index";
70
- import { Formex, FormexController, getIn, setIn, useCreateFormex } from "@firecms/formex";
71
- import { useAnalyticsController } from "../hooks/useAnalyticsController";
72
- import { CustomIdField } from "../form/components/CustomIdField";
73
- import { CustomFieldValidator, getYupEntitySchema } from "../form/validation";
74
- import { ErrorFocus } from "../form/components/ErrorFocus";
75
- import { LabelWithIconAndTooltip, PropertyFieldBinding } from "../form";
76
- import { ValidationError } from "yup";
77
-
78
- const MAIN_TAB_VALUE = "main_##Q$SC^#S6";
20
+ import { CircularProgress, cls, CodeIcon, defaultBorderMixin, Tab, Tabs, Typography } from "@firecms/ui";
21
+ import { getEntityFromCache } from "../util/entity_cache";
22
+ import { EntityForm, EntityFormProps } from "../form";
23
+ import { EntityEditViewFormActions } from "./EntityEditViewFormActions";
24
+ import { EntityJsonPreview } from "../components/EntityJsonPreview";
25
+ import { createFormexStub } from "../util/createFormexStub";
26
+
27
+ export const MAIN_TAB_VALUE = "__main_##Q$SC^#S6";
28
+ export const JSON_TAB_VALUE = "__json";
29
+
30
+ export type OnUpdateParams = {
31
+ entity: Entity<any>,
32
+ status: EntityStatus,
33
+ path: string,
34
+ entityId?: string;
35
+ selectedTab?: string;
36
+ collection: EntityCollection<any>
37
+ };
38
+
39
+ export type OnTabChangeParams<M extends Record<string, any>> = {
40
+ path: string;
41
+ entityId?: string;
42
+ selectedTab?: string;
43
+ collection: EntityCollection<M>;
44
+ };
79
45
 
80
46
  export interface EntityEditViewProps<M extends Record<string, any>> {
81
47
  path: string;
48
+ /**
49
+ * The navigation path to the entity.
50
+ */
51
+ fullIdPath?: string;
82
52
  collection: EntityCollection<M>;
83
53
  entityId?: string;
54
+ databaseId?: string;
84
55
  copy?: boolean;
85
- selectedSubPath?: string;
56
+ selectedTab?: string;
86
57
  parentCollectionIds: string[];
87
- onValuesAreModified: (modified: boolean) => void;
88
- onUpdate?: (params: { entity: Entity<any> }) => void;
89
- onClose?: () => void;
58
+ onValuesModified?: (modified: boolean) => void;
59
+ onSaved?: (params: OnUpdateParams) => void;
60
+ onTabChange?: (props: OnTabChangeParams<M>) => void;
61
+ layout?: "side_panel" | "full_screen";
62
+ barActions?: React.ReactNode;
63
+ formProps?: Partial<EntityFormProps<M>>,
90
64
  }
91
65
 
92
66
  /**
93
67
  * This is the default view that is used as the content of a side panel when
94
68
  * an entity is opened.
95
- * You probably don't want to use this view directly since it is bound to the
96
- * side panel.
97
69
  */
98
70
  export function EntityEditView<M extends Record<string, any>, USER extends User>({
99
71
  entityId,
100
72
  ...props
101
73
  }: EntityEditViewProps<M>) {
74
+
102
75
  const {
103
76
  entity,
104
77
  dataLoading,
@@ -108,1018 +81,399 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
108
81
  path: props.path,
109
82
  entityId: entityId,
110
83
  collection: props.collection,
84
+ databaseId: props.databaseId,
111
85
  useCache: false
112
86
  });
113
87
 
114
- if (dataLoading) {
115
- return <CircularProgressCenter/>
88
+ const cachedValues = entityId
89
+ ? getEntityFromCache(props.path + "/" + entityId)
90
+ : getEntityFromCache(props.path + "#new");
91
+
92
+ const authController = useAuthController();
93
+
94
+ const initialStatus = props.copy ? "copy" : (entityId ? "existing" : "new");
95
+ const [status, setStatus] = useState<EntityStatus>(initialStatus);
96
+
97
+ const canEdit = useMemo(() => {
98
+ if (status === "new" || status === "copy") {
99
+ return true;
100
+ } else {
101
+ return entity ? canEditEntity(props.collection, authController, props.path, entity ?? null) : undefined;
102
+ }
103
+ }, [authController, entity, status]);
104
+
105
+ if ((dataLoading && !cachedValues) || (!entity || canEdit === undefined) && (status === "existing" || status === "copy")) {
106
+ return <CircularProgressCenter/>;
116
107
  }
117
108
 
118
- if (entityId && !entity) {
119
- console.error(`Entity with id ${entityId} not found in collection ${props.collection.path}`);
109
+ if (entityId && !entity && !cachedValues) {
110
+ console.error(`Entity with id ${entityId} not found in collection ${props.path}`);
120
111
  }
121
112
 
122
113
  return <EntityEditViewInner<M> {...props}
123
114
  entityId={entityId}
124
115
  entity={entity}
125
- dataLoading={dataLoading}/>;
116
+ cachedDirtyValues={cachedValues as Partial<M>}
117
+ dataLoading={dataLoading}
118
+ status={status}
119
+ setStatus={setStatus}
120
+ canEdit={canEdit}
121
+ />;
126
122
  }
127
123
 
128
124
  export function EntityEditViewInner<M extends Record<string, any>>({
129
125
  path,
130
- entityId: entityIdProp,
131
- selectedSubPath: selectedSubPathProp,
132
- copy,
126
+ fullIdPath,
127
+ entityId,
128
+ selectedTab: selectedTabProp,
133
129
  collection,
134
130
  parentCollectionIds,
135
- onValuesAreModified,
136
- onUpdate,
137
- onClose,
131
+ onValuesModified,
132
+ onSaved,
133
+ onTabChange,
138
134
  entity,
139
- dataLoading
135
+ cachedDirtyValues,
136
+ dataLoading,
137
+ layout = "side_panel",
138
+ barActions,
139
+ status,
140
+ setStatus,
141
+ formProps,
142
+ canEdit
140
143
  }: EntityEditViewProps<M> & {
141
144
  entity?: Entity<M>,
142
- dataLoading: boolean
145
+ cachedDirtyValues?: Partial<M>, // dirty cached entity in memory
146
+ dataLoading: boolean,
147
+ status: EntityStatus,
148
+ setStatus: (status: EntityStatus) => void,
149
+ canEdit?: boolean,
143
150
  }) {
144
151
 
145
- if (collection.customId && collection.formAutoSave) {
146
- console.warn(`The collection ${collection.path} has customId and formAutoSave enabled. This is not supported and formAutoSave will be ignored`);
147
- }
148
-
149
- const [saving, setSaving] = useState(false);
150
- /**
151
- * These are the values that are being saved. They are debounced.
152
- * We use this only when autoSave is enabled.
153
- */
154
- const [valuesToBeSaved, setValuesToBeSaved] = useState<EntityValues<M> | undefined>(undefined);
155
- useDebouncedCallback(valuesToBeSaved, () => {
156
- if (valuesToBeSaved)
157
- saveEntity({
158
- entityId: usedEntity?.id,
159
- collection,
160
- path,
161
- values: valuesToBeSaved,
162
- closeAfterSave: false
163
- });
164
- }, false, 2000);
165
-
166
- // const largeLayout = useLargeLayout();
167
- // const largeLayoutTabSelected = useRef(!largeLayout);
168
- // const resolvedFormWidth: string = typeof formWidth === "number" ? `${formWidth}px` : formWidth ?? FORM_CONTAINER_WIDTH;
169
-
170
- const inputCollection = collection;
171
-
172
- const authController = useAuthController();
173
- const dataSource = useDataSource(collection);
174
- const sideDialogContext = useSideDialogContext();
175
- const sideEntityController = useSideEntityController();
176
- const snackbarController = useSnackbarController();
177
- const customizationController = useCustomizationController();
178
152
  const context = useFireCMSContext();
179
153
 
180
- const closeAfterSaveRef = useRef(false);
181
-
182
- const analyticsController = useAnalyticsController();
183
-
184
- const initialResolvedCollection = useMemo(() => resolveCollection({
185
- collection: inputCollection,
186
- path,
187
- values: entity?.values,
188
- propertyConfigs: customizationController.propertyConfigs
189
- }), [entity?.values, path, customizationController.propertyConfigs]);
190
-
191
- const initialStatus = copy ? "copy" : (entityIdProp ? "existing" : "new");
192
- const [status, setStatus] = useState<EntityStatus>(initialStatus);
193
-
194
- const mustSetCustomId: boolean = (status === "new" || status === "copy") &&
195
- (Boolean(initialResolvedCollection.customId) && initialResolvedCollection.customId !== "optional");
196
-
197
- const initialEntityId: string | undefined = useMemo((): string | undefined => {
198
- if (status === "new" || status === "copy") {
199
- if (mustSetCustomId) {
200
- return undefined;
201
- } else {
202
- return dataSource.generateEntityId(path, collection);
203
- }
204
- } else {
205
- return entityIdProp;
206
- }
207
- }, [entityIdProp, status]);
154
+ const [usedEntity, setUsedEntity] = useState<Entity<M> | undefined>(entity);
208
155
 
209
- const [entityId, setEntityId] = React.useState<string | undefined>(initialEntityId);
156
+ useEffect(() => {
157
+ if (entity)
158
+ setUsedEntity(entity);
159
+ }, [entity]);
210
160
 
211
- // const doOnValuesChanges = (values?: EntityValues<M>) => {
212
- // const initialValues = formex.initialValues;
213
- // setInternalValues(values);
214
- // if (onValuesChanged)
215
- // onValuesChanged(values);
216
- // if (autoSave && values && !equal(values, initialValues)) {
217
- // save(values);
218
- // }
219
- // };
161
+ // Instead of using a ref (which does not trigger re-render), we use state for the form context.
162
+ const [formContext, setFormContext] = useState<FormContext<M> | undefined>(undefined);
220
163
 
221
- const [entityIdError, setEntityIdError] = React.useState<boolean>(false);
222
- const [savingError, setSavingError] = React.useState<Error | undefined>();
164
+ const largeLayout = useLargeLayout();
223
165
 
224
- const [customIdLoading, setCustomIdLoading] = React.useState<boolean>(false);
166
+ const customizationController = useCustomizationController();
225
167
 
226
- const defaultSelectedView = selectedSubPathProp ?? resolveDefaultSelectedView(
168
+ const defaultSelectedView = useMemo(() => resolveDefaultSelectedView(
227
169
  collection ? collection.defaultSelectedView : undefined,
228
170
  {
229
171
  status,
230
172
  entityId
231
173
  }
232
- );
233
-
234
- const [selectedTab, setSelectedTab] = useState<string>(defaultSelectedView ?? MAIN_TAB_VALUE);
174
+ ), []);
235
175
 
236
- const mainViewVisible = selectedTab === MAIN_TAB_VALUE;
176
+ const [selectedTab, setSelectedTab] = useState<string>(selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE);
177
+ useEffect(() => {
178
+ if ((selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE) !== selectedTab) {
179
+ setSelectedTab(selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE);
180
+ }
181
+ }, [selectedTabProp]);
237
182
 
238
183
  const subcollections = (collection.subcollections ?? []).filter(c => !c.hideFromNavigation);
239
184
  const subcollectionsCount = subcollections?.length ?? 0;
240
185
  const customViews = collection.entityViews;
241
186
  const customViewsCount = customViews?.length ?? 0;
242
- const autoSave = collection.formAutoSave && !collection.customId;
243
-
244
- const hasAdditionalViews = customViewsCount > 0 || subcollectionsCount > 0;
245
-
246
- const [usedEntity, setUsedEntity] = useState<Entity<M> | undefined>(entity);
247
- const [readOnly, setReadOnly] = useState<boolean | undefined>(undefined);
248
-
249
- const baseDataSourceValuesRef = useRef<Partial<EntityValues<M>>>(getDataSourceEntityValues(initialResolvedCollection, status, usedEntity));
250
-
251
- useEffect(() => {
252
- if (entity)
253
- setUsedEntity(entity);
254
- }, [entity]);
255
-
256
- useEffect(() => {
257
- if (status === "new") {
258
- setReadOnly(false);
259
- } else {
260
- const editEnabled = usedEntity ? canEditEntity(collection, authController, path, usedEntity ?? null) : false;
261
- if (usedEntity)
262
- setReadOnly(!editEnabled);
263
- }
264
- }, [authController, usedEntity, status]);
265
-
266
- const onPreSaveHookError = useCallback((e: Error) => {
267
- setSaving(false);
268
- snackbarController.open({
269
- type: "error",
270
- message: "Error before saving: " + e?.message
271
- });
272
- console.error(e);
273
- }, [snackbarController]);
274
-
275
- const onSaveSuccessHookError = useCallback((e: Error) => {
276
- setSaving(false);
277
- snackbarController.open({
278
- type: "error",
279
- message: "Error after saving (entity is saved): " + e?.message
280
- });
281
- console.error(e);
282
- }, [snackbarController]);
283
-
284
- const onSaveSuccess = (updatedEntity: Entity<M>, closeAfterSave: boolean) => {
285
-
286
- setSaving(false);
287
- if (!autoSave)
288
- snackbarController.open({
289
- type: "success",
290
- message: `${collection.singularName ?? collection.name}: Saved correctly`
291
- });
292
-
293
- setUsedEntity(updatedEntity);
294
- setStatus("existing");
295
-
296
- onValuesAreModified(false);
297
-
298
- if (onUpdate)
299
- onUpdate({ entity: updatedEntity });
300
-
301
- if (closeAfterSave) {
302
- sideDialogContext.setBlocked(false);
303
- sideDialogContext.close(true);
304
- onClose?.();
305
- } else if (status !== "existing") {
306
- sideEntityController.replace({
307
- path,
308
- entityId: updatedEntity.id,
309
- selectedSubPath: MAIN_TAB_VALUE === selectedTab ? undefined : selectedTab,
310
- updateUrl: true,
311
- collection,
312
- });
313
- }
187
+ const includeJsonView = collection.includeJsonView === undefined ? true : collection.includeJsonView;
188
+ const hasAdditionalViews = customViewsCount > 0 || subcollectionsCount > 0 || includeJsonView;
314
189
 
315
- };
316
-
317
- const onSaveFailure = useCallback((e: Error) => {
318
-
319
- setSaving(false);
320
- snackbarController.open({
321
- type: "error",
322
- message: "Error saving: " + e?.message
323
- });
324
-
325
- console.error("Error saving entity", path, entityId);
326
- console.error(e);
327
- }, [entityId, path, snackbarController]);
328
-
329
- const saveEntity = ({
330
- values,
331
- previousValues,
332
- closeAfterSave,
333
- entityId,
334
- collection,
335
- path
336
- }: {
337
- collection: EntityCollection<M>,
338
- path: string,
339
- entityId: string | undefined,
340
- values: M,
341
- previousValues?: M,
342
- closeAfterSave: boolean,
343
- }) => {
344
- setSaving(true);
345
- return saveEntityWithCallbacks({
346
- path,
347
- entityId,
348
- values,
349
- previousValues,
350
- collection,
351
- status,
352
- dataSource,
353
- context,
354
- onSaveSuccess: (updatedEntity: Entity<M>) => onSaveSuccess(updatedEntity, closeAfterSave),
355
- onSaveFailure,
356
- onPreSaveHookError,
357
- onSaveSuccessHookError
358
- }).then();
359
- };
190
+ const {
191
+ resolvedEntityViews,
192
+ selectedEntityView,
193
+ selectedSecondaryForm
194
+ } = resolvedSelectedEntityView(customViews, customizationController, selectedTab, canEdit);
360
195
 
361
- const onSaveEntityRequest = async ({
362
- collection,
363
- path,
364
- entityId,
365
- values,
366
- previousValues,
367
- closeAfterSave,
368
- autoSave
369
- }: EntityFormSaveParams<M>): Promise<void> => {
370
- if (!status)
371
- return;
372
-
373
- if (autoSave) {
374
- setValuesToBeSaved(values);
375
- } else {
376
- return saveEntity({
377
- collection,
378
- path,
379
- entityId,
380
- values,
381
- previousValues,
382
- closeAfterSave
383
- });
384
- }
385
- };
196
+ const actionsAtTheBottom = !largeLayout || layout === "side_panel" || selectedEntityView?.includeActions === "bottom";
386
197
 
387
- const onSubmit = (values: EntityValues<M>, formexController: FormexController<EntityValues<M>>) => {
198
+ const mainViewVisible = selectedTab === MAIN_TAB_VALUE || Boolean(selectedSecondaryForm);
388
199
 
389
- if (mustSetCustomId && !entityId) {
390
- console.error("Missing custom Id");
391
- setEntityIdError(true);
392
- formexController.setSubmitting(false);
393
- return;
394
- }
200
+ const authController = useAuthController();
395
201
 
396
- setSavingError(undefined);
397
- setEntityIdError(false);
202
+ const customViewsView: React.ReactNode[] | undefined = customViews && resolvedEntityViews
203
+ .filter(e => !e.includeActions)
204
+ .map((customView) => {
398
205
 
399
- if (status === "existing") {
400
- if (!entity?.id) throw Error("Form misconfiguration when saving, no id for existing entity");
401
- } else if (status === "new" || status === "copy") {
402
- if (inputCollection.customId) {
403
- if (inputCollection.customId !== "optional" && !entityId) {
404
- throw Error("Form misconfiguration when saving, entityId should be set");
405
- }
206
+ if (!customView)
207
+ return null;
208
+ const Builder = customView.Builder;
209
+ if (!Builder) {
210
+ console.error("INTERNAL: customView.Builder is not defined");
211
+ return null;
406
212
  }
407
- } else {
408
- throw Error("New FormType added, check EntityForm");
409
- }
410
-
411
- return save(values)
412
- ?.then(_ => {
413
- formexController.resetForm({
414
- values,
415
- submitCount: 0,
416
- touched: {}
417
- });
418
- })
419
- .finally(() => {
420
- formexController.setSubmitting(false);
421
- });
422
-
423
- };
424
-
425
- const formex: FormexController<M> = useCreateFormex<M>({
426
- initialValues: baseDataSourceValuesRef.current as M,
427
- onSubmit,
428
- validation: (values) => {
429
- return validationSchema?.validate(values, { abortEarly: false })
430
- .then(() => {
431
- return {};
432
- })
433
- .catch((e: any) => {
434
- // const errors: Record<string, string> = {};
435
- // e.inner.forEach((error: any) => {
436
- // errors[error.path] = error.message;
437
- // });
438
- return yupToFormErrors(e);
439
- });
440
- }
441
- });
442
-
443
- const resolvedCollection = resolveCollection<M>({
444
- collection: inputCollection,
445
- path,
446
- entityId,
447
- values: formex.values,
448
- previousValues: formex.initialValues,
449
- propertyConfigs: customizationController.propertyConfigs
450
- });
451
-
452
- const lastSavedValues = useRef<EntityValues<M> | undefined>(entity?.values);
453
-
454
- const save = (values: EntityValues<M>): Promise<void> => {
455
- lastSavedValues.current = values;
456
- return onSaveEntityRequest({
457
- collection: resolvedCollection,
458
- path,
459
- entityId,
460
- values,
461
- previousValues: entity?.values,
462
- closeAfterSave: closeAfterSaveRef.current,
463
- autoSave: autoSave ?? false
464
- }).then(_ => {
465
- const eventName: CMSAnalyticsEvent = status === "new"
466
- ? "new_entity_saved"
467
- : (status === "copy" ? "entity_copied" : (status === "existing" ? "entity_edited" : "unmapped_event"));
468
- analyticsController.onAnalyticsEvent?.(eventName, { path });
469
- }).catch(e => {
470
- console.error(e);
471
- setSavingError(e);
472
- }).finally(() => {
473
- closeAfterSaveRef.current = false;
474
- });
475
- };
476
-
477
- const formContext: FormContext<M> = {
478
- // @ts-ignore
479
- setFieldValue: useCallback(formex.setFieldValue, []),
480
- values: formex.values,
481
- collection: resolvedCollection,
482
- entityId: entityId as string,
483
- path,
484
- save,
485
- formex
486
- };
487
213
 
488
- const resolvedEntityViews = customViews ? customViews
489
- .map(e => resolveEntityView(e, customizationController.entityViews))
490
- .filter(Boolean) as EntityCustomView[]
491
- : [];
492
-
493
- const selectedEntityView = resolvedEntityViews.find(e => e.key === selectedTab);
494
- const shouldShowEntityActions = !autoSave && (selectedTab === MAIN_TAB_VALUE || selectedEntityView?.includeActions);
495
-
496
- const customViewsView: React.ReactNode[] | undefined = customViews && resolvedEntityViews
497
- .map(
498
- (customView, colIndex) => {
499
- if (!customView)
500
- return null;
501
- if (selectedTab !== customView.key)
502
- return null;
503
- const Builder = customView.Builder;
504
- if (!Builder) {
505
- console.error("customView.Builder is not defined");
506
- return null;
507
- }
508
- return <div
509
- className={cls(defaultBorderMixin,
510
- "relative flex-grow w-full h-full overflow-auto ")}
511
- key={`custom_view_${customView.key}`}
512
- role="tabpanel">
513
- <ErrorBoundary>
514
- {formContext && <Builder
515
- collection={collection}
516
- entity={usedEntity}
517
- modifiedValues={formex.values ?? usedEntity?.values}
518
- formContext={formContext}
519
- />}
520
- </ErrorBoundary>
521
- </div>;
214
+ if (!entityId) {
215
+ console.error("INTERNAL: entityId is not defined");
216
+ return null;
522
217
  }
523
- ).filter(Boolean);
524
-
525
- const globalLoading = (dataLoading && !usedEntity) ||
526
- ((!usedEntity || readOnly === undefined) && (status === "existing" || status === "copy"));
527
-
528
- const loading = globalLoading || saving;
529
218
 
530
- const subCollectionsViews = subcollections && subcollections.map(
531
- (subcollection, colIndex) => {
532
- const subcollectionId = subcollection.id ?? subcollection.path;
533
- const fullPath = usedEntity ? `${path}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollectionId)}` : undefined;
534
- if (selectedTab !== subcollectionId)
535
- return null;
536
- return (
537
- <div
538
- className={"relative flex-grow h-full overflow-auto w-full"}
539
- key={`subcol_${subcollectionId}`}
540
- role="tabpanel">
541
-
542
- {loading && <CircularProgressCenter/>}
543
-
544
- {!globalLoading &&
545
- (usedEntity && fullPath
546
- ? <EntityCollectionView
547
- fullPath={fullPath}
548
- parentCollectionIds={[...parentCollectionIds, collection.id]}
549
- isSubCollection={true}
550
- {...subcollection}/>
551
- : <div
552
- className="flex items-center justify-center w-full h-full p-3">
553
- <Typography variant={"label"}>
554
- You need to save your entity before
555
- adding additional collections
556
- </Typography>
557
- </div>)
558
- }
559
-
560
- </div>
561
- );
562
- }
563
- ).filter(Boolean);
219
+ const formexStub = createFormexStub<M>(usedEntity?.values ?? {} as M);
220
+ const usedFormContext: FormContext = formContext ?? {
221
+ entityId,
222
+ disabled: false,
223
+ openEntityMode: layout,
224
+ status: status,
225
+ values: usedEntity?.values ?? {},
226
+ setFieldValue: (key: string, value: any) => {
227
+ throw new Error("You can't update values in read only mode");
228
+ },
229
+ save: () => {
230
+ throw new Error("You can't save in read only mode");
231
+ },
232
+ collection: resolveCollection<M>({
233
+ collection,
234
+ path,
235
+ entityId,
236
+ values: usedEntity?.values ?? {},
237
+ previousValues: usedEntity?.values ?? {},
238
+ propertyConfigs: customizationController.propertyConfigs,
239
+ authController
240
+ }),
241
+ path,
242
+ entity: usedEntity,
243
+ savingError: undefined,
244
+ formex: formexStub
245
+ };
246
+
247
+ return <div
248
+ className={cls(defaultBorderMixin,
249
+ "relative flex-1 w-full h-full overflow-auto",
250
+ { "hidden": selectedTab !== customView.key }
251
+ )}
252
+ key={`custom_view_${customView.key}`}
253
+ role="tabpanel">
254
+ <ErrorBoundary>
255
+ {usedFormContext && <Builder
256
+ collection={collection}
257
+ entity={usedEntity}
258
+ modifiedValues={usedFormContext?.formex?.values ?? usedEntity?.values}
259
+ formContext={usedFormContext}
260
+ />}
261
+ </ErrorBoundary>
262
+ </div>;
263
+ }).filter(Boolean);
264
+
265
+ const globalLoading = dataLoading && !usedEntity;
266
+
267
+ const jsonView = <div
268
+ className={cls("relative flex-1 h-full overflow-auto w-full",
269
+ { "hidden": selectedTab !== JSON_TAB_VALUE })}
270
+ key={"json_view"}
271
+ role="tabpanel">
272
+ <ErrorBoundary>
273
+ <EntityJsonPreview
274
+ values={formContext?.values ?? entity?.values ?? {}}/>
275
+ </ErrorBoundary>
276
+ </div>;
277
+
278
+ const subCollectionsViews = subcollections && subcollections.map((subcollection) => {
279
+ const subcollectionId = subcollection.id ?? subcollection.path;
280
+ const newFullPath = usedEntity ? `${path}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollectionId)}` : undefined;
281
+ const newFullIdPath = fullIdPath ? `${fullIdPath}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollectionId)}` : undefined;
282
+
283
+ if (selectedTab !== subcollectionId) return null;
284
+ return (
285
+ <div
286
+ className={"relative flex-1 h-full overflow-auto w-full"}
287
+ key={`subcol_${subcollectionId}`}
288
+ role="tabpanel">
289
+
290
+ {globalLoading && <CircularProgressCenter/>}
291
+
292
+ {!globalLoading &&
293
+ (usedEntity && newFullPath
294
+ ? <EntityCollectionView
295
+ fullPath={newFullPath}
296
+ fullIdPath={newFullIdPath}
297
+ parentCollectionIds={[...parentCollectionIds, collection.id]}
298
+ isSubCollection={true}
299
+ updateUrl={false}
300
+ {...subcollection}
301
+ openEntityMode={layout}/>
302
+ : <div className="flex items-center justify-center w-full h-full p-3">
303
+ <Typography variant={"label"}>
304
+ You need to save your entity before
305
+ adding additional collections
306
+ </Typography>
307
+ </div>)
308
+ }
564
309
 
565
- const onDiscard = useCallback(() => {
566
- onValuesAreModified(false);
567
- }, []);
310
+ </div>
311
+ );
312
+ }).filter(Boolean);
568
313
 
569
314
  const onSideTabClick = (value: string) => {
570
315
  setSelectedTab(value);
571
316
  if (status === "existing") {
572
- sideEntityController.replace({
573
- path,
317
+ onTabChange?.({
318
+ path: fullIdPath ?? path,
574
319
  entityId,
575
- selectedSubPath: value === MAIN_TAB_VALUE ? undefined : value,
576
- updateUrl: true,
577
- collection,
320
+ selectedTab: value === MAIN_TAB_VALUE ? undefined : value,
321
+ collection
578
322
  });
579
323
  }
580
324
  };
581
325
 
582
- const onIdUpdateError = useCallback((error: any) => {
583
- snackbarController.open({
584
- type: "error",
585
- message: "Error updating id, check the console"
586
- });
587
- }, []);
588
-
589
- const onIdChange = useCallback((id: string) => {
590
- setUsedEntity((prevEntity) => prevEntity
591
- ? {
592
- ...prevEntity,
593
- id
594
- }
595
- : undefined);
596
- }, []);
597
-
598
- // useEffect(() => {
599
- // baseDataSourceValuesRef.current = getDataSourceEntityValues(initialResolvedCollection, status, entity);
600
- // const initialValues = formex.initialValues;
601
- // if (!formex.isSubmitting && initialValues && status === "existing") {
602
- // setUnderlyingChanges(
603
- // Object.entries(resolvedCollection.properties)
604
- // .map(([key, property]) => {
605
- // if (isHidden(property)) {
606
- // return {};
607
- // }
608
- // const initialValue = initialValues[key];
609
- // const latestValue = baseDataSourceValuesRef.current[key];
610
- // if (!equal(initialValue, latestValue)) {
611
- // return { [key]: latestValue };
612
- // }
613
- // return {};
614
- // })
615
- // .reduce((a, b) => ({ ...a, ...b }), {}) as Partial<EntityValues<M>>
616
- // );
617
- // } else {
618
- // setUnderlyingChanges({});
619
- // }
620
- // }, [entity, initialResolvedCollection, status]);
621
-
622
- const pluginActions: React.ReactNode[] = [];
623
-
624
- const plugins = customizationController.plugins;
625
-
626
- if (plugins && inputCollection) {
627
- const actionProps: PluginFormActionProps = {
628
- entityId,
629
- path,
630
- status,
631
- collection: inputCollection,
632
- context,
633
- currentEntityId: entityId,
634
- formContext
635
- };
636
- pluginActions.push(...plugins.map((plugin, i) => (
637
- plugin.form?.Actions
638
- ? <plugin.form.Actions
639
- key={`actions_${plugin.key}`} {...actionProps}/>
640
- : null
641
- )).filter(Boolean));
642
- }
643
-
644
- const titlePropertyKey = getEntityTitlePropertyKey(resolvedCollection, customizationController.propertyConfigs);
645
- const title = formex.values && titlePropertyKey ? getValueInPath(formex.values, titlePropertyKey) : undefined;
646
-
647
- const onIdUpdate = inputCollection.callbacks?.onIdUpdate;
648
-
649
- const doOnIdUpdate = useCallback(async () => {
650
- if (onIdUpdate && formex.values && (status === "new" || status === "copy")) {
651
- setCustomIdLoading(true);
652
- try {
653
- const updatedId = await onIdUpdate({
654
- collection: resolvedCollection,
655
- path,
656
- entityId,
657
- values: formex.values,
658
- context
659
- });
660
- setEntityId(updatedId);
661
- } catch (e) {
662
- onIdUpdateError?.(e);
663
- console.error(e);
664
- }
665
- setCustomIdLoading(false);
666
- }
667
- }, [entityId, formex.values, status]);
668
-
669
- useEffect(() => {
670
- doOnIdUpdate();
671
- }, [doOnIdUpdate]);
672
-
673
- const [underlyingChanges, setUnderlyingChanges] = useState<Partial<EntityValues<M>>>({});
674
-
675
- const uniqueFieldValidator: CustomFieldValidator = useCallback(({
676
- name,
677
- value,
678
- property
679
- }) => dataSource.checkUniqueField(path, name, value, entityId, collection),
680
- [dataSource, path, entityId]);
681
-
682
- const validationSchema = useMemo(() => entityId
683
- ? getYupEntitySchema(
684
- entityId,
685
- resolvedCollection.properties,
686
- uniqueFieldValidator)
687
- : undefined,
688
- [entityId, resolvedCollection.properties, uniqueFieldValidator]);
689
-
690
- const getActionsForEntity = useCallback(({
691
- entity,
692
- customEntityActions
693
- }: {
694
- entity?: Entity<M>,
695
- customEntityActions?: EntityAction[]
696
- }): EntityAction[] => {
697
- const createEnabled = canCreateEntity(inputCollection, authController, path, null);
698
- const deleteEnabled = entity ? canDeleteEntity(inputCollection, authController, path, entity) : true;
699
- const actions: EntityAction[] = [];
700
- if (createEnabled)
701
- actions.push(copyEntityAction);
702
- if (deleteEnabled)
703
- actions.push(deleteEntityAction);
704
- if (customEntityActions)
705
- actions.push(...customEntityActions);
706
- return actions;
707
- }, [authController, inputCollection, path]);
708
-
709
- const modified = formex.dirty;
710
- useEffect(() => {
711
- if (!autoSave) {
712
- onValuesAreModified(modified);
713
- } else {
714
- if (formex.values && !equal(formex.values, lastSavedValues.current)) {
715
- save(formex.values);
716
- }
717
- }
718
- }, [modified, formex.values]);
719
-
720
- useEffect(() => {
721
- if (!autoSave && !formex.isSubmitting && underlyingChanges && entity) {
722
- // we update the form fields from the Firestore data
723
- // if they were not touched
724
- Object.entries(underlyingChanges).forEach(([key, value]) => {
725
- const formValue = formex.values[key];
726
- if (!equal(value, formValue) && !formex.touched[key]) {
727
- console.debug("Updated value from the datasource:", key, value);
728
- formex.setFieldValue(key, value !== undefined ? value : null);
729
- }
730
- });
731
- }
732
- }, [formex.isSubmitting, autoSave, underlyingChanges, entity, formex.values, formex.touched, formex.setFieldValue]);
733
-
734
- const formFields = (
735
- <>
736
- {(getFormFieldKeys(resolvedCollection))
737
- .map((key) => {
738
-
739
- const property = resolvedCollection.properties[key];
740
- if (property) {
741
-
742
- const underlyingValueHasChanged: boolean =
743
- !!underlyingChanges &&
744
- Object.keys(underlyingChanges).includes(key) &&
745
- formex.touched[key];
746
-
747
- const disabled = (!autoSave && formex.isSubmitting) || isReadOnly(property) || Boolean(property.disabled);
748
- const hidden = isHidden(property);
749
- if (hidden) return null;
750
- const cmsFormFieldProps: PropertyFieldBindingProps<any, M> = {
751
- propertyKey: key,
752
- disabled,
753
- property,
754
- includeDescription: property.description || property.longDescription,
755
- underlyingValueHasChanged: underlyingValueHasChanged && !autoSave,
756
- context: formContext,
757
- partOfArray: false,
758
- minimalistView: false,
759
- autoFocus: false
760
- };
761
-
762
- return (
763
- <div id={`form_field_${key}`}
764
- key={`field_${resolvedCollection.name}_${key}`}>
765
- <ErrorBoundary>
766
- <PropertyFieldBinding {...cmsFormFieldProps}/>
767
- </ErrorBoundary>
768
- </div>
769
- );
770
- }
771
-
772
- const additionalField = resolvedCollection.additionalFields?.find(f => f.key === key);
773
- if (additionalField && entity) {
774
- const Builder = additionalField.Builder;
775
- if (!Builder && !additionalField.value) {
776
- throw new Error("When using additional fields you need to provide a Builder or a value");
777
- }
778
-
779
- const child = Builder
780
- ? <Builder entity={entity} context={context}/>
781
- : <>
782
- {additionalField.value?.({
783
- entity,
784
- context
785
- })?.toString()}
786
- </>;
787
- return (
788
- <div key={`additional_${key}`}>
789
- <LabelWithIconAndTooltip
790
- propertyKey={key}
791
- icon={<NotesIcon size={"small"}/>}
792
- title={additionalField.name}
793
- className={"text-text-secondary dark:text-text-secondary-dark ml-3.5"}/>
794
- <div
795
- className={cls(paperMixin, "min-h-14 p-4 md:p-6 overflow-x-scroll no-scrollbar")}>
796
-
797
- <ErrorBoundary>
798
- {child}
799
- </ErrorBoundary>
800
-
801
- </div>
802
- </div>
803
- );
804
- }
805
-
806
- console.warn(`Property ${key} not found in collection ${resolvedCollection.name} in properties or additional fields. Skipping.`);
807
- return null;
808
- })
809
- .filter(Boolean)}
810
-
811
- </>);
812
-
813
- const disabled = formex.isSubmitting || (!modified && status === "existing");
814
- const formRef = React.useRef<HTMLDivElement>(null);
815
-
816
- const entityActions = getActionsForEntity({
817
- entity,
818
- customEntityActions: inputCollection.entityActions
819
- });
820
- const formActions = entityActions.filter(a => a.includeInForm === undefined || a.includeInForm);
821
-
822
- const dialogActions = <DialogActions position={"absolute"}>
823
-
824
- {savingError &&
825
- <div className="text-right">
826
- <Typography color={"error"}>
827
- {savingError.message}
828
- </Typography>
829
- </div>}
830
-
831
- {entity && formActions.length > 0 && <div className="flex-grow flex overflow-auto no-scrollbar">
832
- {formActions.map(action => (
833
- <IconButton
834
- key={action.name}
835
- color="primary"
836
- onClick={(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
837
- event.stopPropagation();
838
- if (entity)
839
- action.onClick({
840
- entity,
841
- fullPath: resolvedCollection.path,
842
- collection: resolvedCollection,
843
- context,
844
- sideEntityController
845
- });
846
- }}>
847
- {action.icon}
848
- </IconButton>
849
- ))}
850
- </div>}
851
- {formex.isSubmitting && <CircularProgress size={"small"}/>}
852
- <Button
853
- variant="text"
854
- disabled={disabled || formex.isSubmitting}
855
- type="reset">
856
- {status === "existing" ? "Discard" : "Clear"}
857
- </Button>
858
-
859
- <Button
860
- variant="text"
861
- color="primary"
862
- type="submit"
863
- disabled={disabled || formex.isSubmitting}
864
- onClick={() => {
865
- closeAfterSaveRef.current = false;
866
- }}>
867
- {status === "existing" && "Save"}
868
- {status === "copy" && "Create copy"}
869
- {status === "new" && "Create"}
870
- </Button>
871
-
872
- <Button
873
- variant="filled"
874
- color="primary"
875
- type="submit"
876
- disabled={disabled || formex.isSubmitting}
877
- onClick={() => {
878
- closeAfterSaveRef.current = true;
879
- }}>
880
- {status === "existing" && "Save and close"}
881
- {status === "copy" && "Create copy and close"}
882
- {status === "new" && "Create and close"}
883
- </Button>
884
-
885
- </DialogActions>;
886
-
887
- function buildForm() {
888
-
889
- let form = <>
890
-
891
- {pluginActions.length > 0 && <div
892
- className={cls("w-full flex justify-end items-center sticky top-0 right-0 left-0 z-10 bg-opacity-60 bg-surface-accent-200 dark:bg-opacity-60 dark:bg-surface-accent-800 backdrop-blur-md")}>
893
- {pluginActions}
894
- </div>}
895
-
896
- <div className="pt-12 pb-16 pl-4 sm:px-8 md:px-10">
897
- <div
898
- className={`w-full py-2 flex flex-col items-start mt-${4 + (pluginActions ? 8 : 0)} lg:mt-${8 + (pluginActions ? 8 : 0)} mb-8`}>
899
-
900
- <Typography
901
- className={"mt-4 flex-grow line-clamp-1 " + inputCollection.hideIdFromForm ? "mb-2" : "mb-0"}
902
- variant={"h4"}>{title ?? inputCollection.singularName ?? inputCollection.name}
903
- </Typography>
904
- <Alert color={"base"} className={"w-full"} size={"small"}>
905
- <code className={"text-xs select-all"}>{path}/{entityId}</code>
906
- </Alert>
907
- </div>
908
-
909
- {!collection.hideIdFromForm &&
910
- <CustomIdField customId={inputCollection.customId}
911
- entityId={entityId}
912
- status={status}
913
- onChange={setEntityId}
914
- error={entityIdError}
915
- loading={customIdLoading}
916
- entity={entity}/>}
917
-
918
- {entityId && formContext && <>
919
- <div className="mt-12 flex flex-col gap-8"
920
- ref={formRef}>
921
-
922
- {formFields}
923
-
924
- <ErrorFocus containerRef={formRef}/>
925
-
926
- </div>
927
-
928
- <div className="h-14"/>
929
-
930
- </>}
931
-
932
- </div>
933
- </>;
934
-
935
- if (plugins) {
936
- plugins.forEach((plugin: FireCMSPlugin) => {
937
- if (plugin.form?.provider) {
938
- form = (
939
- <plugin.form.provider.Component
940
- status={status}
941
- path={path}
942
- collection={collection}
943
- onDiscard={onDiscard}
944
- entity={usedEntity}
945
- context={context}
946
- formContext={formContext}
947
- {...plugin.form.provider.props}>
948
- {form}
949
- </plugin.form.provider.Component>
950
- );
951
- }
952
- });
953
- }
954
- return <ErrorBoundary>{form}</ErrorBoundary>;
955
- }
956
-
957
- const entityView = (readOnly === undefined)
958
- ? <></>
959
- : (!readOnly
960
- ? buildForm()
961
- : (
962
- <>
963
- <Typography
964
- className={"mt-16 mb-8 mx-8"}
965
- variant={"h4"}>{collection.singularName ?? collection.name}
966
- </Typography>
967
- <EntityView
968
- className={"px-12"}
969
- entity={usedEntity as Entity<M>}
970
- path={path}
971
- collection={collection}/>
972
-
973
- </>
974
- ));
975
-
976
- const subcollectionTabs = subcollections && subcollections.map(
977
- (subcollection) =>
978
- <Tab
979
- className="text-sm min-w-[140px]"
980
- value={subcollection.id}
981
- key={`entity_detail_collection_tab_${subcollection.name}`}>
982
- {subcollection.name}
983
- </Tab>
326
+ const entityReadOnlyView = !canEdit && entity ? <div
327
+ className={cls("flex-1 flex flex-row w-full overflow-y-auto justify-center", (canEdit || !mainViewVisible || selectedSecondaryForm) ? "hidden" : "")}>
328
+ <div
329
+ className={cls("relative flex flex-col max-w-4xl lg:max-w-3xl xl:max-w-4xl 2xl:max-w-6xl w-full h-fit")}>
330
+ <Typography className={"mt-16 mb-8 mx-8"} variant={"h4"}>
331
+ {collection.singularName ?? collection.name}
332
+ </Typography>
333
+ <EntityView
334
+ className={"px-8 h-full overflow-auto"}
335
+ entity={entity}
336
+ path={path}
337
+ collection={collection}/>
338
+ </div>
339
+ </div> : null;
340
+
341
+ const entityView = <EntityForm<M>
342
+ collection={collection}
343
+ path={path}
344
+ entityId={entityId ?? usedEntity?.id}
345
+ onValuesModified={onValuesModified}
346
+ entity={entity}
347
+ initialDirtyValues={cachedDirtyValues}
348
+ openEntityMode={layout}
349
+ forceActionsAtTheBottom={actionsAtTheBottom}
350
+ initialStatus={status}
351
+ className={cls((!mainViewVisible || !canEdit) && !selectedSecondaryForm ? "hidden" : "", formProps?.className)}
352
+ EntityFormActionsComponent={EntityEditViewFormActions}
353
+ disabled={!canEdit}
354
+ {...formProps}
355
+ onEntityChange={(entity) => {
356
+ setUsedEntity(entity);
357
+ formProps?.onEntityChange?.(entity);
358
+ }}
359
+ onStatusChange={(status) => {
360
+ setStatus(status);
361
+ formProps?.onStatusChange?.(status);
362
+ }}
363
+ onFormContextReady={(formContext) => {
364
+ setFormContext(formContext);
365
+ formProps?.onFormContextReady?.(formContext);
366
+ }}
367
+ onSaved={(params) => {
368
+ const res = {
369
+ ...params,
370
+ selectedTab: MAIN_TAB_VALUE === selectedTab ? undefined : selectedTab
371
+ };
372
+ onSaved?.(res);
373
+ formProps?.onSaved?.(res);
374
+ }}
375
+ Builder={selectedSecondaryForm?.Builder}
376
+ />;
377
+
378
+ const subcollectionTabs = subcollections && subcollections.map((subcollection) =>
379
+ <Tab
380
+ className="text-sm min-w-[120px]"
381
+ value={subcollection.id}
382
+ key={`entity_detail_collection_tab_${subcollection.name}`}>
383
+ {subcollection.name}
384
+ </Tab>
984
385
  );
985
386
 
986
- const customViewTabs = resolvedEntityViews.map(
987
- (view) =>
988
-
989
- <Tab
990
- className="text-sm min-w-[140px]"
991
- value={view.key}
992
- key={`entity_detail_collection_tab_${view.name}`}>
993
- {view.name}
994
- </Tab>
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>
995
394
  );
996
395
 
997
- useEffect(() => {
998
- if (entityId && onIdChange)
999
- onIdChange(entityId);
1000
- }, [entityId, onIdChange]);
1001
-
1002
- return (
1003
- <Formex value={formex}>
1004
-
1005
- <div className="flex flex-col h-full w-full">
1006
-
1007
- <div
1008
- className={cls(defaultBorderMixin, "no-scrollbar h-16 border-b pl-2 pr-2 pt-1 flex items-end overflow-scroll bg-surface-50 dark:bg-surface-950")}>
396
+ const shouldShowTopBar = Boolean(barActions) || hasAdditionalViews;
1009
397
 
1010
- <div
1011
- className="pb-1 self-center">
1012
- <IconButton
1013
- onClick={() => {
1014
- onClose?.();
1015
- return sideDialogContext.close(false);
1016
- }}>
1017
- <CloseIcon size={"small"}/>
1018
- </IconButton>
1019
- </div>
398
+ let result = <div className="relative flex flex-col h-full w-full bg-white dark:bg-surface-900">
1020
399
 
1021
- <div className={"flex-grow"}/>
400
+ {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)}>
1022
402
 
1023
- {globalLoading && <div
1024
- className="self-center">
1025
- <CircularProgress size={"small"}/>
1026
- </div>}
403
+ {barActions}
1027
404
 
1028
- <Tabs
1029
- value={selectedTab}
1030
- onValueChange={(value) => {
1031
- onSideTabClick(value);
1032
- }}
1033
- innerClassName="pl-4 pr-4 pt-0">
405
+ <div className={"flex-grow"}/>
1034
406
 
1035
- <Tab
1036
- disabled={!hasAdditionalViews}
1037
- value={MAIN_TAB_VALUE}
1038
- className={`${
1039
- !hasAdditionalViews ? "hidden" : ""
1040
- } text-sm min-w-[140px]`}
1041
- >{collection.singularName ?? collection.name}</Tab>
407
+ {globalLoading && <div className="self-center">
408
+ <CircularProgress size={"small"}/>
409
+ </div>}
1042
410
 
1043
- {customViewTabs}
411
+ {hasAdditionalViews && <Tabs
412
+ value={selectedTab}
413
+ onValueChange={(value) => {
414
+ onSideTabClick(value);
415
+ }}>
1044
416
 
1045
- {subcollectionTabs}
1046
- </Tabs>
417
+ {includeJsonView && <Tab
418
+ disabled={!hasAdditionalViews}
419
+ value={JSON_TAB_VALUE}
420
+ innerClassName={"block"}
421
+ className={"text-sm"}>
422
+ <CodeIcon size={"small"}/>
423
+ </Tab>}
1047
424
 
1048
- </div>
425
+ <Tab
426
+ disabled={!hasAdditionalViews}
427
+ value={MAIN_TAB_VALUE}
428
+ className={"text-sm min-w-[120px]"}>
429
+ {collection.singularName ?? collection.name}
430
+ </Tab>
1049
431
 
1050
- <form
1051
- onSubmit={formex.handleSubmit}
1052
- onReset={() => {
1053
- formex.resetForm();
1054
- return onDiscard && onDiscard();
1055
- }}
1056
- noValidate
1057
- className={"flex-grow h-full flex overflow-auto flex-col w-full"}>
1058
432
 
1059
- <div
1060
- role="tabpanel"
1061
- hidden={!mainViewVisible}
1062
- id={`form_${path}`}
1063
- className={"relative w-full"}>
433
+ {customViewTabs}
1064
434
 
1065
- {globalLoading
1066
- ? <CircularProgressCenter/>
1067
- : entityView}
435
+ {subcollectionTabs}
436
+ </Tabs>}
437
+ </div>}
1068
438
 
1069
- </div>
439
+ {globalLoading
440
+ ? <div className="w-full pt-12 pb-16 px-4 sm:px-8 md:px-10">
441
+ <CircularProgressCenter/>
442
+ </div>
443
+ : <>
444
+ {entityReadOnlyView}
445
+ {entityView}
446
+ </>}
1070
447
 
1071
- {customViewsView}
448
+ {jsonView}
1072
449
 
1073
- {subCollectionsViews}
450
+ {customViewsView}
1074
451
 
1075
- {shouldShowEntityActions && dialogActions}
452
+ {subCollectionsViews}
1076
453
 
1077
- </form>
454
+ </div>;
1078
455
 
1079
- </div>
1080
- </Formex>
1081
- );
1082
- }
456
+ const plugins = customizationController.plugins;
1083
457
 
1084
- function getDataSourceEntityValues<M extends object>(initialResolvedCollection: ResolvedEntityCollection,
1085
- status: "new" | "existing" | "copy",
1086
- entity: Entity<M> | undefined): Partial<EntityValues<M>> {
1087
-
1088
- const properties = initialResolvedCollection.properties;
1089
- if ((status === "existing" || status === "copy") && entity) {
1090
- return entity.values ?? getDefaultValuesFor(properties);
1091
- } else if (status === "new") {
1092
- return getDefaultValuesFor(properties);
1093
- } else {
1094
- console.error({
1095
- status,
1096
- entity
458
+ if (plugins) {
459
+ plugins.forEach((plugin: FireCMSPlugin) => {
460
+ if (plugin.form?.provider) {
461
+ result = (
462
+ <plugin.form.provider.Component
463
+ status={status}
464
+ path={path}
465
+ collection={collection}
466
+ entity={usedEntity}
467
+ context={context}
468
+ formContext={formContext}
469
+ {...plugin.form.provider.props}>
470
+ {result}
471
+ </plugin.form.provider.Component>
472
+ );
473
+ }
1097
474
  });
1098
- throw new Error("Form has not been initialised with the correct parameters");
1099
475
  }
1100
- }
1101
476
 
1102
- export type EntityFormSaveParams<M extends Record<string, any>> = {
1103
- collection: ResolvedEntityCollection<M>,
1104
- path: string,
1105
- entityId: string | undefined,
1106
- values: EntityValues<M>,
1107
- previousValues?: EntityValues<M>,
1108
- closeAfterSave: boolean,
1109
- autoSave: boolean
1110
- };
1111
-
1112
- export function yupToFormErrors(yupError: ValidationError): Record<string, any> {
1113
- let errors: Record<string, any> = {};
1114
- if (yupError.inner) {
1115
- if (yupError.inner.length === 0) {
1116
- return setIn(errors, yupError.path!, yupError.message);
1117
- }
1118
- for (const err of yupError.inner) {
1119
- if (!getIn(errors, err.path!)) {
1120
- errors = setIn(errors, err.path!, err.message);
1121
- }
1122
- }
1123
- }
1124
- return errors;
477
+ return result;
1125
478
  }
479
+