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

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 (173) hide show
  1. package/dist/components/ArrayContainer.d.ts +7 -3
  2. package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +2 -1
  3. package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +2 -2
  4. package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +15 -1
  5. package/dist/components/EntityCollectionTable/index.d.ts +1 -1
  6. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +4 -0
  7. package/dist/components/PropertyIdCopyTooltip.d.ts +1 -1
  8. package/dist/components/SelectableTable/SelectableTable.d.ts +12 -2
  9. package/dist/components/SelectableTable/filters/ReferenceFilterField.d.ts +1 -1
  10. package/dist/components/UnsavedChangesDialog.d.ts +8 -0
  11. package/dist/components/VirtualTable/VirtualTableProps.d.ts +13 -0
  12. package/dist/components/common/default_entity_actions.d.ts +0 -2
  13. package/dist/components/common/index.d.ts +1 -1
  14. package/dist/components/common/useColumnsIds.d.ts +1 -0
  15. package/dist/components/common/{useDataSourceEntityCollectionTableController.d.ts → useDataSourceTableController.d.ts} +10 -2
  16. package/dist/components/common/useDebouncedCallback.d.ts +1 -0
  17. package/dist/components/common/useScrollRestoration.d.ts +14 -0
  18. package/dist/contexts/BreacrumbsContext.d.ts +8 -0
  19. package/dist/core/EntityEditView.d.ts +29 -21
  20. package/dist/core/EntityEditViewFormActions.d.ts +2 -0
  21. package/dist/core/FireCMSRouter.d.ts +4 -0
  22. package/dist/core/NavigationRoutes.d.ts +0 -1
  23. package/dist/core/SideDialogs.d.ts +4 -2
  24. package/dist/core/index.d.ts +2 -1
  25. package/dist/form/EntityForm.d.ts +45 -0
  26. package/dist/form/EntityFormActions.d.ts +17 -0
  27. package/dist/form/PropertyFieldBinding.d.ts +1 -1
  28. package/dist/form/components/FormEntry.d.ts +6 -0
  29. package/dist/form/components/FormLayout.d.ts +5 -0
  30. package/dist/form/components/index.d.ts +2 -0
  31. package/dist/form/field_bindings/ArrayCustomShapedFieldBinding.d.ts +1 -1
  32. package/dist/form/field_bindings/ArrayOfReferencesFieldBinding.d.ts +1 -1
  33. package/dist/form/field_bindings/BlockFieldBinding.d.ts +1 -1
  34. package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
  35. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
  36. package/dist/form/field_bindings/RepeatFieldBinding.d.ts +1 -1
  37. package/dist/form/index.d.ts +16 -16
  38. package/dist/hooks/data/useEntityFetch.d.ts +2 -1
  39. package/dist/hooks/useBreadcrumbsController.d.ts +26 -0
  40. package/dist/hooks/useBuildNavigationController.d.ts +4 -1
  41. package/dist/hooks/useModeController.d.ts +1 -2
  42. package/dist/index.es.js +18213 -16819
  43. package/dist/index.es.js.map +1 -1
  44. package/dist/index.umd.js +18164 -16771
  45. package/dist/index.umd.js.map +1 -1
  46. package/dist/internal/useBuildSideEntityController.d.ts +2 -2
  47. package/dist/internal/useUnsavedChangesDialog.d.ts +7 -9
  48. package/dist/routes/CustomCMSRoute.d.ts +4 -0
  49. package/dist/routes/FireCMSRoute.d.ts +1 -0
  50. package/dist/routes/HomePageRoute.d.ts +3 -0
  51. package/dist/types/collections.d.ts +30 -10
  52. package/dist/types/datasource.d.ts +2 -2
  53. package/dist/types/dialogs_controller.d.ts +7 -3
  54. package/dist/types/entities.d.ts +1 -1
  55. package/dist/types/entity_actions.d.ts +10 -0
  56. package/dist/types/fields.d.ts +9 -2
  57. package/dist/types/navigation.d.ts +29 -12
  58. package/dist/types/plugins.d.ts +1 -0
  59. package/dist/types/properties.d.ts +18 -3
  60. package/dist/types/side_entity_controller.d.ts +6 -1
  61. package/dist/util/builders.d.ts +1 -1
  62. package/dist/util/entity_actions.d.ts +2 -0
  63. package/dist/util/entity_cache.d.ts +23 -0
  64. package/dist/util/index.d.ts +1 -0
  65. package/dist/util/navigation_from_path.d.ts +6 -1
  66. package/dist/util/navigation_utils.d.ts +12 -1
  67. package/dist/util/resolutions.d.ts +6 -1
  68. package/package.json +19 -21
  69. package/src/components/ArrayContainer.tsx +48 -29
  70. package/src/components/CircularProgressCenter.tsx +1 -1
  71. package/src/components/DeleteEntityDialog.tsx +1 -1
  72. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +40 -19
  73. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +10 -4
  74. package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +18 -1
  75. package/src/components/EntityCollectionTable/index.tsx +1 -1
  76. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -3
  77. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -11
  78. package/src/components/EntityCollectionView/EntityCollectionView.tsx +83 -41
  79. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +4 -2
  80. package/src/components/NotFoundPage.tsx +2 -2
  81. package/src/components/PropertyIdCopyTooltip.tsx +2 -3
  82. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +11 -6
  83. package/src/components/SelectableTable/SelectableTable.tsx +18 -3
  84. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +7 -7
  85. package/src/components/UnsavedChangesDialog.tsx +42 -0
  86. package/src/components/VirtualTable/VirtualTable.tsx +19 -2
  87. package/src/components/VirtualTable/VirtualTableProps.tsx +15 -0
  88. package/src/components/VirtualTable/fields/VirtualTableInput.tsx +1 -1
  89. package/src/components/common/default_entity_actions.tsx +26 -37
  90. package/src/components/common/index.ts +1 -1
  91. package/src/components/common/useColumnsIds.tsx +1 -1
  92. package/src/components/common/useDataSourceTableController.tsx +420 -0
  93. package/src/components/common/useDebouncedCallback.tsx +20 -0
  94. package/src/components/common/useScrollRestoration.tsx +68 -0
  95. package/src/contexts/BreacrumbsContext.tsx +38 -0
  96. package/src/contexts/DialogsProvider.tsx +3 -2
  97. package/src/contexts/ModeController.tsx +1 -3
  98. package/src/core/DefaultAppBar.tsx +44 -14
  99. package/src/core/DefaultDrawer.tsx +1 -1
  100. package/src/core/EntityEditView.tsx +278 -1000
  101. package/src/core/EntityEditViewFormActions.tsx +199 -0
  102. package/src/core/EntitySidePanel.tsx +85 -15
  103. package/src/core/FireCMS.tsx +7 -4
  104. package/src/core/FireCMSRouter.tsx +17 -0
  105. package/src/core/NavigationRoutes.tsx +23 -32
  106. package/src/core/SideDialogs.tsx +20 -11
  107. package/src/core/index.tsx +4 -2
  108. package/src/form/EntityForm.tsx +774 -0
  109. package/src/form/EntityFormActions.tsx +169 -0
  110. package/src/form/PropertyFieldBinding.tsx +13 -11
  111. package/src/form/components/FormEntry.tsx +22 -0
  112. package/src/form/components/FormLayout.tsx +16 -0
  113. package/src/form/components/LabelWithIcon.tsx +6 -3
  114. package/src/form/components/StorageUploadProgress.tsx +1 -1
  115. package/src/form/components/index.tsx +2 -0
  116. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +4 -4
  117. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +4 -2
  118. package/src/form/field_bindings/BlockFieldBinding.tsx +4 -2
  119. package/src/form/field_bindings/DateTimeFieldBinding.tsx +1 -0
  120. package/src/form/field_bindings/MapFieldBinding.tsx +14 -16
  121. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +9 -6
  122. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +1 -1
  123. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +2 -2
  124. package/src/form/field_bindings/ReferenceFieldBinding.tsx +1 -1
  125. package/src/form/field_bindings/RepeatFieldBinding.tsx +5 -2
  126. package/src/form/field_bindings/SelectFieldBinding.tsx +1 -1
  127. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +6 -4
  128. package/src/form/field_bindings/SwitchFieldBinding.tsx +1 -0
  129. package/src/form/field_bindings/TextFieldBinding.tsx +1 -0
  130. package/src/form/index.tsx +16 -32
  131. package/src/form/validation.ts +12 -6
  132. package/src/hooks/data/save.ts +1 -1
  133. package/src/hooks/data/useCollectionFetch.tsx +1 -1
  134. package/src/hooks/data/useEntityFetch.tsx +5 -1
  135. package/src/hooks/useBreadcrumbsController.tsx +31 -0
  136. package/src/hooks/useBuildModeController.tsx +15 -28
  137. package/src/hooks/useBuildNavigationController.tsx +70 -24
  138. package/src/hooks/useLargeLayout.tsx +0 -35
  139. package/src/hooks/useModeController.tsx +1 -2
  140. package/src/hooks/useResolvedNavigationFrom.tsx +4 -6
  141. package/src/internal/useBuildSideDialogsController.tsx +3 -2
  142. package/src/internal/useBuildSideEntityController.tsx +124 -83
  143. package/src/internal/useUnsavedChangesDialog.tsx +126 -92
  144. package/src/preview/PropertyPreview.tsx +8 -8
  145. package/src/preview/components/UrlComponentPreview.tsx +17 -18
  146. package/src/preview/property_previews/MapPropertyPreview.tsx +3 -2
  147. package/src/routes/CustomCMSRoute.tsx +21 -0
  148. package/src/routes/FireCMSRoute.tsx +235 -0
  149. package/src/routes/HomePageRoute.tsx +17 -0
  150. package/src/types/collections.ts +38 -13
  151. package/src/types/datasource.ts +2 -1
  152. package/src/types/dialogs_controller.tsx +7 -3
  153. package/src/types/entities.ts +1 -1
  154. package/src/types/entity_actions.tsx +12 -0
  155. package/src/types/fields.tsx +13 -2
  156. package/src/types/navigation.ts +36 -16
  157. package/src/types/plugins.tsx +1 -0
  158. package/src/types/properties.ts +21 -3
  159. package/src/types/side_dialogs_controller.tsx +2 -0
  160. package/src/types/side_entity_controller.tsx +6 -1
  161. package/src/util/builders.ts +5 -3
  162. package/src/util/entity_actions.ts +28 -0
  163. package/src/util/entity_cache.ts +204 -0
  164. package/src/util/index.ts +1 -0
  165. package/src/util/join_collections.ts +3 -0
  166. package/src/util/navigation_from_path.ts +13 -6
  167. package/src/util/navigation_utils.ts +53 -1
  168. package/src/util/objects.ts +4 -1
  169. package/src/util/parent_references_from_path.ts +3 -3
  170. package/src/util/resolutions.ts +26 -5
  171. package/src/util/useStorageUploadController.tsx +39 -21
  172. package/src/components/common/useDataSourceEntityCollectionTableController.tsx +0 -236
  173. /package/src/util/{common.tsx → common.ts} +0 -0
@@ -1,104 +1,69 @@
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
- resolveCollection,
39
8
  resolveDefaultSelectedView,
40
- resolveEntityView,
41
- useDebouncedCallback
9
+ resolvedSelectedEntityView
42
10
  } from "../util";
43
11
 
44
12
  import {
45
- saveEntityWithCallbacks,
46
13
  useAuthController,
47
14
  useCustomizationController,
48
- useDataSource,
49
15
  useEntityFetch,
50
16
  useFireCMSContext,
51
- useSideEntityController,
52
- useSnackbarController
17
+ useLargeLayout
53
18
  } 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";
19
+ import { CircularProgress, cls, defaultBorderMixin, Tab, Tabs, Typography } from "@firecms/ui";
20
+ import { getEntityFromCache } from "../util/entity_cache";
21
+ import { EntityForm, EntityFormProps } from "../form";
22
+ import { EntityEditViewFormActions } from "./EntityEditViewFormActions";
77
23
 
78
24
  const MAIN_TAB_VALUE = "main_##Q$SC^#S6";
79
25
 
26
+ export type OnUpdateParams = {
27
+ entity: Entity<any>,
28
+ status: EntityStatus,
29
+ path: string,
30
+ entityId?: string;
31
+ selectedTab?: string;
32
+ collection: EntityCollection<any>
33
+ };
34
+
35
+ export type OnTabChangeParams<M extends Record<string, any>> = {
36
+ path: string;
37
+ entityId?: string;
38
+ selectedTab?: string;
39
+ collection: EntityCollection<M>;
40
+ };
41
+
80
42
  export interface EntityEditViewProps<M extends Record<string, any>> {
81
43
  path: string;
82
44
  collection: EntityCollection<M>;
83
45
  entityId?: string;
46
+ databaseId?: string;
84
47
  copy?: boolean;
85
- selectedSubPath?: string;
48
+ selectedTab?: string;
86
49
  parentCollectionIds: string[];
87
- onValuesAreModified: (modified: boolean) => void;
88
- onUpdate?: (params: { entity: Entity<any> }) => void;
89
- onClose?: () => void;
50
+ onValuesModified?: (modified: boolean) => void;
51
+ onSaved?: (params: OnUpdateParams) => void;
52
+ onTabChange?: (props: OnTabChangeParams<M>) => void;
53
+ layout?: "side_panel" | "full_screen";
54
+ barActions?: React.ReactNode;
55
+ formProps?: Partial<EntityFormProps<M>>
90
56
  }
91
57
 
92
58
  /**
93
59
  * This is the default view that is used as the content of a side panel when
94
60
  * 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
61
  */
98
62
  export function EntityEditView<M extends Record<string, any>, USER extends User>({
99
63
  entityId,
100
64
  ...props
101
65
  }: EntityEditViewProps<M>) {
66
+
102
67
  const {
103
68
  entity,
104
69
  dataLoading,
@@ -108,1018 +73,331 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
108
73
  path: props.path,
109
74
  entityId: entityId,
110
75
  collection: props.collection,
76
+ databaseId: props.databaseId,
111
77
  useCache: false
112
78
  });
113
79
 
114
- if (dataLoading) {
115
- return <CircularProgressCenter/>
80
+ const cachedValues = entityId
81
+ ? getEntityFromCache(props.path + "/" + entityId)
82
+ : getEntityFromCache(props.path + "#new");
83
+
84
+ const authController = useAuthController();
85
+
86
+ const initialStatus = props.copy ? "copy" : (entityId ? "existing" : "new");
87
+ const [status, setStatus] = useState<EntityStatus>(initialStatus);
88
+
89
+ const canEdit = useMemo(() => {
90
+ if (status === "new" || status === "copy") {
91
+ return true;
92
+ } else {
93
+ return entity ? canEditEntity(props.collection, authController, props.path, entity ?? null) : undefined;
94
+ }
95
+ }, [authController, entity, status]);
96
+
97
+ if ((dataLoading && !cachedValues) || (!entity || canEdit === undefined) && (status === "existing" || status === "copy")) {
98
+ return <CircularProgressCenter/>;
99
+ }
100
+
101
+ if (entityId && !entity && !cachedValues) {
102
+ console.error(`Entity with id ${entityId} not found in collection ${props.path}`);
116
103
  }
117
104
 
118
- if (entityId && !entity) {
119
- console.error(`Entity with id ${entityId} not found in collection ${props.collection.path}`);
105
+ if (!canEdit) {
106
+ return <div className={"flex flex-col"}>
107
+ <Typography className={"mt-16 mb-8 mx-8"} variant={"h4"}>
108
+ {props.collection.singularName ?? props.collection.name}
109
+ </Typography>
110
+ <EntityView
111
+ className={"px-8"}
112
+ entity={entity as Entity<M>}
113
+ path={props.path}
114
+ collection={props.collection}/>
115
+ </div>
120
116
  }
121
117
 
122
118
  return <EntityEditViewInner<M> {...props}
123
119
  entityId={entityId}
124
120
  entity={entity}
125
- dataLoading={dataLoading}/>;
121
+ cachedDirtyValues={cachedValues as Partial<M>}
122
+ dataLoading={dataLoading}
123
+ status={status}
124
+ setStatus={setStatus}
125
+ />;
126
126
  }
127
127
 
128
128
  export function EntityEditViewInner<M extends Record<string, any>>({
129
129
  path,
130
- entityId: entityIdProp,
131
- selectedSubPath: selectedSubPathProp,
132
- copy,
130
+ entityId,
131
+ selectedTab: selectedTabProp,
133
132
  collection,
134
133
  parentCollectionIds,
135
- onValuesAreModified,
136
- onUpdate,
137
- onClose,
134
+ onValuesModified,
135
+ onSaved,
136
+ onTabChange,
138
137
  entity,
139
- dataLoading
138
+ cachedDirtyValues,
139
+ dataLoading,
140
+ layout = "side_panel",
141
+ barActions,
142
+ status,
143
+ setStatus,
144
+ formProps,
140
145
  }: EntityEditViewProps<M> & {
141
146
  entity?: Entity<M>,
142
- dataLoading: boolean
147
+ cachedDirtyValues?: Partial<M>, // dirty cached entity in memory
148
+ dataLoading: boolean,
149
+ status: EntityStatus,
150
+ setStatus: (status: EntityStatus) => void,
143
151
  }) {
144
152
 
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
153
  const context = useFireCMSContext();
179
154
 
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]);
155
+ const [usedEntity, setUsedEntity] = useState<Entity<M> | undefined>(entity);
208
156
 
209
- const [entityId, setEntityId] = React.useState<string | undefined>(initialEntityId);
157
+ useEffect(() => {
158
+ if (entity)
159
+ setUsedEntity(entity);
160
+ }, [entity]);
210
161
 
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
- // };
162
+ // Instead of using a ref (which does not trigger re-render), we use state for the form context.
163
+ const [formContext, setFormContext] = useState<FormContext<M> | undefined>(undefined);
220
164
 
221
- const [entityIdError, setEntityIdError] = React.useState<boolean>(false);
222
- const [savingError, setSavingError] = React.useState<Error | undefined>();
165
+ const largeLayout = useLargeLayout();
223
166
 
224
- const [customIdLoading, setCustomIdLoading] = React.useState<boolean>(false);
167
+ const customizationController = useCustomizationController();
225
168
 
226
- const defaultSelectedView = selectedSubPathProp ?? resolveDefaultSelectedView(
169
+ const defaultSelectedView = useMemo(() => resolveDefaultSelectedView(
227
170
  collection ? collection.defaultSelectedView : undefined,
228
171
  {
229
172
  status,
230
173
  entityId
231
174
  }
232
- );
175
+ ), []);
233
176
 
234
- const [selectedTab, setSelectedTab] = useState<string>(defaultSelectedView ?? MAIN_TAB_VALUE);
177
+ const [selectedTab, setSelectedTab] = useState<string>(selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE);
235
178
 
236
- const mainViewVisible = selectedTab === MAIN_TAB_VALUE;
179
+ useEffect(() => {
180
+ if ((selectedTabProp ?? MAIN_TAB_VALUE) !== selectedTab) {
181
+ setSelectedTab(selectedTabProp ?? MAIN_TAB_VALUE);
182
+ }
183
+ }, [selectedTabProp]);
237
184
 
238
185
  const subcollections = (collection.subcollections ?? []).filter(c => !c.hideFromNavigation);
239
186
  const subcollectionsCount = subcollections?.length ?? 0;
240
187
  const customViews = collection.entityViews;
241
188
  const customViewsCount = customViews?.length ?? 0;
242
- const autoSave = collection.formAutoSave && !collection.customId;
243
-
244
189
  const hasAdditionalViews = customViewsCount > 0 || subcollectionsCount > 0;
245
190
 
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
- }
314
-
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
- };
360
-
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
- };
386
-
387
- const onSubmit = (values: EntityValues<M>, formexController: FormexController<EntityValues<M>>) => {
388
-
389
- if (mustSetCustomId && !entityId) {
390
- console.error("Missing custom Id");
391
- setEntityIdError(true);
392
- formexController.setSubmitting(false);
393
- return;
394
- }
395
-
396
- setSavingError(undefined);
397
- setEntityIdError(false);
398
-
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
- }
406
- }
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
- };
191
+ const {
192
+ resolvedEntityViews,
193
+ selectedEntityView,
194
+ selectedSecondaryForm
195
+ } = resolvedSelectedEntityView(customViews, customizationController, selectedTab);
487
196
 
488
- const resolvedEntityViews = customViews ? customViews
489
- .map(e => resolveEntityView(e, customizationController.entityViews))
490
- .filter(Boolean) as EntityCustomView[]
491
- : [];
197
+ const actionsAtTheBottom = !largeLayout || layout === "side_panel" || selectedEntityView?.includeActions === "bottom";
492
198
 
493
- const selectedEntityView = resolvedEntityViews.find(e => e.key === selectedTab);
494
- const shouldShowEntityActions = !autoSave && (selectedTab === MAIN_TAB_VALUE || selectedEntityView?.includeActions);
199
+ const mainViewVisible = selectedTab === MAIN_TAB_VALUE || Boolean(selectedSecondaryForm);
495
200
 
496
201
  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>;
522
- }
523
- ).filter(Boolean);
524
-
525
- const globalLoading = (dataLoading && !usedEntity) ||
526
- ((!usedEntity || readOnly === undefined) && (status === "existing" || status === "copy"));
527
-
528
- const loading = globalLoading || saving;
529
-
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)
202
+ .filter(e => !e.includeActions)
203
+ .map((customView) => {
204
+ if (!customView)
535
205
  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
- }
206
+ const Builder = customView.Builder;
207
+ if (!Builder) {
208
+ console.error("customView.Builder is not defined");
209
+ return null;
210
+ }
559
211
 
560
- </div>
561
- );
562
- }
563
- ).filter(Boolean);
212
+ return <div
213
+ className={cls(defaultBorderMixin,
214
+ "relative flex-1 w-full h-full overflow-auto",
215
+ { "hidden": selectedTab !== customView.key }
216
+ )}
217
+ key={`custom_view_${customView.key}`}
218
+ role="tabpanel">
219
+ <ErrorBoundary>
220
+ {formContext && <Builder
221
+ collection={collection}
222
+ entity={usedEntity}
223
+ modifiedValues={formContext.formex.values ?? usedEntity?.values}
224
+ formContext={formContext}
225
+ />}
226
+ </ErrorBoundary>
227
+ </div>;
228
+ }).filter(Boolean);
229
+
230
+ const globalLoading = dataLoading && !usedEntity;
231
+
232
+ const subCollectionsViews = subcollections && subcollections.map((subcollection) => {
233
+ const subcollectionId = subcollection.id ?? subcollection.path;
234
+ const fullPath = usedEntity ? `${path}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollectionId)}` : undefined;
235
+ if (selectedTab !== subcollectionId) return null;
236
+ return (
237
+ <div
238
+ className={"relative flex-1 h-full overflow-auto w-full"}
239
+ key={`subcol_${subcollectionId}`}
240
+ role="tabpanel">
241
+
242
+ {globalLoading && <CircularProgressCenter/>}
243
+
244
+ {!globalLoading &&
245
+ (usedEntity && fullPath
246
+ ? <EntityCollectionView
247
+ fullPath={fullPath}
248
+ parentCollectionIds={[...parentCollectionIds, collection.id]}
249
+ isSubCollection={true}
250
+ updateUrl={false}
251
+ {...subcollection}
252
+ openEntityMode={layout}/>
253
+ : <div className="flex items-center justify-center w-full h-full p-3">
254
+ <Typography variant={"label"}>
255
+ You need to save your entity before
256
+ adding additional collections
257
+ </Typography>
258
+ </div>)
259
+ }
564
260
 
565
- const onDiscard = useCallback(() => {
566
- onValuesAreModified(false);
567
- }, []);
261
+ </div>
262
+ );
263
+ }).filter(Boolean);
568
264
 
569
265
  const onSideTabClick = (value: string) => {
570
266
  setSelectedTab(value);
571
267
  if (status === "existing") {
572
- sideEntityController.replace({
268
+ onTabChange?.({
573
269
  path,
574
270
  entityId,
575
- selectedSubPath: value === MAIN_TAB_VALUE ? undefined : value,
576
- updateUrl: true,
271
+ selectedTab: value === MAIN_TAB_VALUE ? undefined : value,
577
272
  collection,
578
273
  });
579
274
  }
580
275
  };
581
276
 
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>
277
+ // Render the main entity form view (or a read-only view if the user cannot edit)
278
+ const entityView = <EntityForm<M>
279
+ collection={collection}
280
+ path={path}
281
+ entityId={entityId ?? usedEntity?.id}
282
+ onValuesModified={onValuesModified}
283
+ entity={entity}
284
+ initialDirtyValues={cachedDirtyValues}
285
+ openEntityMode={layout}
286
+ forceActionsAtTheBottom={actionsAtTheBottom}
287
+ initialStatus={status}
288
+ className={cls(!mainViewVisible ? "hidden" : "", formProps?.className)}
289
+ EntityFormActionsComponent={EntityEditViewFormActions}
290
+ {...formProps}
291
+ onEntityChange={(entity) => {
292
+ setUsedEntity(entity);
293
+ formProps?.onEntityChange?.(entity);
294
+ }}
295
+ onStatusChange={(status) => {
296
+ setStatus(status);
297
+ formProps?.onStatusChange?.(status);
298
+ }}
299
+ onFormContextReady={(formContext) => {
300
+ setFormContext(formContext);
301
+ formProps?.onFormContextReady?.(formContext);
302
+ }}
303
+ onSaved={(params) => {
304
+ const res = {
305
+ ...params,
306
+ selectedTab: MAIN_TAB_VALUE === selectedTab ? undefined : selectedTab,
307
+ };
308
+ onSaved?.(res);
309
+ formProps?.onSaved?.(res);
310
+ }}
311
+ Builder={selectedSecondaryForm?.Builder}
312
+ />;
313
+
314
+ const subcollectionTabs = subcollections && subcollections.map((subcollection) =>
315
+ <Tab
316
+ className="text-sm min-w-[120px]"
317
+ value={subcollection.id}
318
+ key={`entity_detail_collection_tab_${subcollection.name}`}>
319
+ {subcollection.name}
320
+ </Tab>
984
321
  );
985
322
 
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>
323
+ const customViewTabs = resolvedEntityViews.map((view) =>
324
+ <Tab
325
+ className="text-sm min-w-[120px]"
326
+ value={view.key}
327
+ key={`entity_detail_collection_tab_${view.name}`}>
328
+ {view.name}
329
+ </Tab>
995
330
  );
996
331
 
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")}>
332
+ const shouldShowTopBar = Boolean(barActions) || hasAdditionalViews;
1009
333
 
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>
334
+ let result = <div className="relative flex flex-col h-full w-full bg-white dark:bg-surface-900">
1020
335
 
1021
- <div className={"flex-grow"}/>
336
+ {shouldShowTopBar && <div
337
+ 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
338
 
1023
- {globalLoading && <div
1024
- className="self-center">
1025
- <CircularProgress size={"small"}/>
1026
- </div>}
339
+ {barActions}
1027
340
 
1028
- <Tabs
1029
- value={selectedTab}
1030
- onValueChange={(value) => {
1031
- onSideTabClick(value);
1032
- }}
1033
- innerClassName="pl-4 pr-4 pt-0">
341
+ <div className={"flex-grow"}/>
1034
342
 
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>
1042
-
1043
- {customViewTabs}
1044
-
1045
- {subcollectionTabs}
1046
- </Tabs>
1047
-
1048
- </div>
343
+ {globalLoading && <div className="self-center">
344
+ <CircularProgress size={"small"}/>
345
+ </div>}
1049
346
 
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"}>
347
+ {hasAdditionalViews && <Tabs
348
+ value={selectedTab}
349
+ onValueChange={(value) => {
350
+ onSideTabClick(value);
351
+ }}>
1058
352
 
1059
- <div
1060
- role="tabpanel"
1061
- hidden={!mainViewVisible}
1062
- id={`form_${path}`}
1063
- className={"relative w-full"}>
353
+ <Tab
354
+ disabled={!hasAdditionalViews}
355
+ value={MAIN_TAB_VALUE}
356
+ className={"text-sm min-w-[120px]"}>
357
+ {collection.singularName ?? collection.name}
358
+ </Tab>
1064
359
 
1065
- {globalLoading
1066
- ? <CircularProgressCenter/>
1067
- : entityView}
360
+ {customViewTabs}
1068
361
 
1069
- </div>
362
+ {subcollectionTabs}
363
+ </Tabs>}
364
+ </div>}
1070
365
 
1071
- {customViewsView}
366
+ {globalLoading
367
+ ? <div className="w-full pt-12 pb-16 px-4 sm:px-8 md:px-10">
368
+ <CircularProgressCenter/>
369
+ </div>
370
+ : entityView}
1072
371
 
1073
- {subCollectionsViews}
372
+ {/*{secondaryForms}*/}
1074
373
 
1075
- {shouldShowEntityActions && dialogActions}
374
+ {customViewsView}
1076
375
 
1077
- </form>
376
+ {subCollectionsViews}
1078
377
 
1079
- </div>
1080
- </Formex>
1081
- );
1082
- }
378
+ </div>;
1083
379
 
1084
- function getDataSourceEntityValues<M extends object>(initialResolvedCollection: ResolvedEntityCollection,
1085
- status: "new" | "existing" | "copy",
1086
- entity: Entity<M> | undefined): Partial<EntityValues<M>> {
380
+ const plugins = customizationController.plugins;
1087
381
 
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
382
+ if (plugins) {
383
+ plugins.forEach((plugin: FireCMSPlugin) => {
384
+ if (plugin.form?.provider) {
385
+ result = (
386
+ <plugin.form.provider.Component
387
+ status={status}
388
+ path={path}
389
+ collection={collection}
390
+ entity={usedEntity}
391
+ context={context}
392
+ formContext={formContext}
393
+ {...plugin.form.provider.props}>
394
+ {result}
395
+ </plugin.form.provider.Component>
396
+ );
397
+ }
1097
398
  });
1098
- throw new Error("Form has not been initialised with the correct parameters");
1099
399
  }
1100
- }
1101
400
 
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;
401
+ return result;
1125
402
  }
403
+