@firecms/core 3.0.0-canary.66 → 3.0.0-canary.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/EntityEditView.d.ts +17 -3
- package/dist/form/PropertiesForm.d.ts +8 -0
- package/dist/form/components/FieldHelperText.d.ts +3 -3
- package/dist/form/components/StorageItemPreview.d.ts +2 -4
- package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +2 -4
- package/dist/form/index.d.ts +0 -2
- package/dist/index.es.js +4271 -4322
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +5 -5
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collections.d.ts +14 -0
- package/dist/types/fields.d.ts +31 -30
- package/dist/types/plugins.d.ts +2 -2
- package/dist/types/properties.d.ts +1 -1
- package/dist/util/storage.d.ts +23 -2
- package/dist/util/useStorageUploadController.d.ts +1 -1
- package/package.json +4 -4
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +2 -1
- package/src/core/EntityEditView.tsx +662 -120
- package/src/core/EntitySidePanel.tsx +0 -1
- package/src/form/PropertiesForm.tsx +81 -0
- package/src/form/PropertyFieldBinding.tsx +28 -5
- package/src/form/components/FieldHelperText.tsx +3 -3
- package/src/form/components/StorageItemPreview.tsx +0 -4
- package/src/form/field_bindings/MapFieldBinding.tsx +10 -3
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +0 -7
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +3 -26
- package/src/form/index.tsx +4 -4
- package/src/form/validation.ts +1 -17
- package/src/types/collections.ts +14 -0
- package/src/types/customization_controller.tsx +0 -1
- package/src/types/fields.tsx +33 -33
- package/src/types/plugins.tsx +2 -2
- package/src/types/properties.ts +1 -1
- package/src/util/entities.ts +1 -0
- package/src/util/permissions.ts +1 -0
- package/src/util/storage.ts +75 -21
- package/src/util/useStorageUploadController.tsx +21 -3
- package/dist/form/EntityForm.d.ts +0 -77
- package/src/form/EntityForm.tsx +0 -735
|
@@ -1,18 +1,40 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import {
|
|
3
|
+
CMSAnalyticsEvent,
|
|
3
4
|
Entity,
|
|
5
|
+
EntityAction,
|
|
4
6
|
EntityCollection,
|
|
5
7
|
EntityCustomView,
|
|
6
8
|
EntityStatus,
|
|
7
9
|
EntityValues,
|
|
8
10
|
FireCMSPlugin,
|
|
9
11
|
FormContext,
|
|
12
|
+
PluginFormActionProps,
|
|
13
|
+
PropertyFieldBindingProps,
|
|
14
|
+
ResolvedEntityCollection,
|
|
10
15
|
User
|
|
11
16
|
} from "../types";
|
|
12
|
-
import
|
|
17
|
+
import equal from "react-fast-compare"
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
CircularProgressCenter,
|
|
21
|
+
copyEntityAction,
|
|
22
|
+
deleteEntityAction,
|
|
23
|
+
EntityCollectionView,
|
|
24
|
+
EntityView,
|
|
25
|
+
ErrorBoundary,
|
|
26
|
+
} from "../components";
|
|
13
27
|
import {
|
|
28
|
+
canCreateEntity,
|
|
29
|
+
canDeleteEntity,
|
|
14
30
|
canEditEntity,
|
|
31
|
+
getDefaultValuesFor,
|
|
32
|
+
getEntityTitlePropertyKey,
|
|
33
|
+
getValueInPath,
|
|
34
|
+
isHidden,
|
|
35
|
+
isReadOnly,
|
|
15
36
|
removeInitialAndTrailingSlashes,
|
|
37
|
+
resolveCollection,
|
|
16
38
|
resolveDefaultSelectedView,
|
|
17
39
|
resolveEntityView,
|
|
18
40
|
useDebouncedCallback
|
|
@@ -28,10 +50,29 @@ import {
|
|
|
28
50
|
useSideEntityController,
|
|
29
51
|
useSnackbarController
|
|
30
52
|
} from "../hooks";
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
|
|
53
|
+
import {
|
|
54
|
+
Alert,
|
|
55
|
+
Button,
|
|
56
|
+
CircularProgress,
|
|
57
|
+
CloseIcon,
|
|
58
|
+
cls,
|
|
59
|
+
defaultBorderMixin,
|
|
60
|
+
DialogActions,
|
|
61
|
+
IconButton,
|
|
62
|
+
Tab,
|
|
63
|
+
Tabs,
|
|
64
|
+
Tooltip,
|
|
65
|
+
Typography
|
|
66
|
+
} from "@firecms/ui";
|
|
34
67
|
import { useSideDialogContext } from "./index";
|
|
68
|
+
import { Formex, FormexController, getIn, setIn, useCreateFormex } from "@firecms/formex";
|
|
69
|
+
import { useAnalyticsController } from "../hooks/useAnalyticsController";
|
|
70
|
+
import { CustomIdField } from "../form/components/CustomIdField";
|
|
71
|
+
import { CustomFieldValidator, getYupEntitySchema } from "../form/validation";
|
|
72
|
+
import { ErrorFocus } from "../form/components/ErrorFocus";
|
|
73
|
+
import { PropertyIdCopyTooltipContent } from "../components/PropertyIdCopyTooltipContent";
|
|
74
|
+
import { PropertyFieldBinding } from "../form";
|
|
75
|
+
import { ValidationError } from "yup";
|
|
35
76
|
|
|
36
77
|
const MAIN_TAB_VALUE = "main_##Q$SC^#S6";
|
|
37
78
|
|
|
@@ -42,7 +83,6 @@ export interface EntityEditViewProps<M extends Record<string, any>> {
|
|
|
42
83
|
copy?: boolean;
|
|
43
84
|
selectedSubPath?: string;
|
|
44
85
|
parentCollectionIds: string[];
|
|
45
|
-
formWidth?: number | string;
|
|
46
86
|
onValuesAreModified: (modified: boolean) => void;
|
|
47
87
|
onUpdate?: (params: { entity: Entity<any> }) => void;
|
|
48
88
|
onClose?: () => void;
|
|
@@ -55,17 +95,47 @@ export interface EntityEditViewProps<M extends Record<string, any>> {
|
|
|
55
95
|
* side panel. Instead, you might want to use {@link EntityForm} or {@link EntityCollectionView}
|
|
56
96
|
*/
|
|
57
97
|
export function EntityEditView<M extends Record<string, any>, UserType extends User>({
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
selectedSubPath,
|
|
61
|
-
copy,
|
|
62
|
-
collection,
|
|
63
|
-
parentCollectionIds,
|
|
64
|
-
onValuesAreModified,
|
|
65
|
-
formWidth,
|
|
66
|
-
onUpdate,
|
|
67
|
-
onClose,
|
|
98
|
+
entityId: entityIdProp,
|
|
99
|
+
...props
|
|
68
100
|
}: EntityEditViewProps<M>) {
|
|
101
|
+
const {
|
|
102
|
+
entity,
|
|
103
|
+
dataLoading,
|
|
104
|
+
// eslint-disable-next-line no-unused-vars
|
|
105
|
+
dataLoadingError
|
|
106
|
+
} = useEntityFetch<M, UserType>({
|
|
107
|
+
path: props.path,
|
|
108
|
+
entityId: entityIdProp,
|
|
109
|
+
collection: props.collection,
|
|
110
|
+
useCache: false
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (dataLoading) {
|
|
114
|
+
return <CircularProgressCenter/>
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return <EntityEditViewInner<M> {...props}
|
|
118
|
+
entityId={entityIdProp}
|
|
119
|
+
entity={entity}
|
|
120
|
+
dataLoading={dataLoading}/>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function EntityEditViewInner<M extends Record<string, any>>({
|
|
124
|
+
path,
|
|
125
|
+
entityId: entityIdProp,
|
|
126
|
+
selectedSubPath: selectedSubPathProp,
|
|
127
|
+
copy,
|
|
128
|
+
collection,
|
|
129
|
+
parentCollectionIds,
|
|
130
|
+
onValuesAreModified,
|
|
131
|
+
onUpdate,
|
|
132
|
+
onClose,
|
|
133
|
+
entity,
|
|
134
|
+
dataLoading,
|
|
135
|
+
}: EntityEditViewProps<M> & {
|
|
136
|
+
entity?: Entity<M>,
|
|
137
|
+
dataLoading: boolean
|
|
138
|
+
}) {
|
|
69
139
|
|
|
70
140
|
if (collection.customId && collection.formAutoSave) {
|
|
71
141
|
console.warn(`The collection ${collection.path} has customId and formAutoSave enabled. This is not supported and formAutoSave will be ignored`);
|
|
@@ -92,30 +162,61 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
|
|
|
92
162
|
// const largeLayoutTabSelected = useRef(!largeLayout);
|
|
93
163
|
// const resolvedFormWidth: string = typeof formWidth === "number" ? `${formWidth}px` : formWidth ?? FORM_CONTAINER_WIDTH;
|
|
94
164
|
|
|
165
|
+
const inputCollection = collection;
|
|
166
|
+
|
|
167
|
+
const authController = useAuthController();
|
|
95
168
|
const dataSource = useDataSource(collection);
|
|
96
169
|
const sideDialogContext = useSideDialogContext();
|
|
97
170
|
const sideEntityController = useSideEntityController();
|
|
98
171
|
const snackbarController = useSnackbarController();
|
|
99
172
|
const customizationController = useCustomizationController();
|
|
100
173
|
const context = useFireCMSContext();
|
|
101
|
-
const authController = useAuthController<UserType>();
|
|
102
174
|
|
|
103
|
-
const
|
|
175
|
+
const closeAfterSaveRef = useRef(false);
|
|
104
176
|
|
|
105
|
-
const
|
|
177
|
+
const analyticsController = useAnalyticsController();
|
|
106
178
|
|
|
107
|
-
const
|
|
108
|
-
|
|
179
|
+
const initialResolvedCollection = useMemo(() => resolveCollection({
|
|
180
|
+
collection: inputCollection,
|
|
181
|
+
path,
|
|
182
|
+
values: entity?.values,
|
|
183
|
+
fields: customizationController.propertyConfigs
|
|
184
|
+
}), [entity?.values, path, customizationController.propertyConfigs]);
|
|
185
|
+
|
|
186
|
+
const initialStatus = copy ? "copy" : (entityIdProp ? "existing" : "new");
|
|
187
|
+
const [status, setStatus] = useState<EntityStatus>(initialStatus);
|
|
188
|
+
const mustSetCustomId: boolean = (status === "new" || status === "copy") &&
|
|
189
|
+
(Boolean(initialResolvedCollection.customId) && initialResolvedCollection.customId !== "optional");
|
|
190
|
+
const initialEntityId: string | undefined = useMemo((): string | undefined => {
|
|
191
|
+
if (status === "new" || status === "copy") {
|
|
192
|
+
if (mustSetCustomId) {
|
|
193
|
+
return undefined;
|
|
194
|
+
} else {
|
|
195
|
+
return dataSource.generateEntityId(path);
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
return entityIdProp;
|
|
199
|
+
}
|
|
200
|
+
}, [entityIdProp, status]);
|
|
109
201
|
|
|
110
|
-
const
|
|
111
|
-
const subcollectionsCount = subcollections?.length ?? 0;
|
|
112
|
-
const customViews = collection.entityViews;
|
|
113
|
-
const customViewsCount = customViews?.length ?? 0;
|
|
114
|
-
const autoSave = collection.formAutoSave && !collection.customId;
|
|
202
|
+
const [entityId, setEntityId] = React.useState<string | undefined>(initialEntityId);
|
|
115
203
|
|
|
116
|
-
const
|
|
204
|
+
const doOnValuesChanges = (values?: EntityValues<M>) => {
|
|
205
|
+
const initialValues = formex.initialValues;
|
|
206
|
+
setInternalValues(values);
|
|
207
|
+
if (onValuesChanged)
|
|
208
|
+
onValuesChanged(values);
|
|
209
|
+
if (autoSave && values && !equal(values, initialValues)) {
|
|
210
|
+
save(values);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const [entityIdError, setEntityIdError] = React.useState<boolean>(false);
|
|
215
|
+
const [savingError, setSavingError] = React.useState<Error | undefined>();
|
|
117
216
|
|
|
118
|
-
const
|
|
217
|
+
const [customIdLoading, setCustomIdLoading] = React.useState<boolean>(false);
|
|
218
|
+
|
|
219
|
+
const defaultSelectedView = selectedSubPathProp ?? resolveDefaultSelectedView(
|
|
119
220
|
collection ? collection.defaultSelectedView : undefined,
|
|
120
221
|
{
|
|
121
222
|
status,
|
|
@@ -124,20 +225,23 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
|
|
|
124
225
|
);
|
|
125
226
|
|
|
126
227
|
const selectedTabRef = useRef<string>(defaultSelectedView ?? MAIN_TAB_VALUE);
|
|
228
|
+
const baseDataSourceValuesRef = useRef<Partial<EntityValues<M>>>(getDataSourceEntityValues(initialResolvedCollection, status, entity));
|
|
127
229
|
|
|
128
230
|
const mainViewVisible = selectedTabRef.current === MAIN_TAB_VALUE;
|
|
129
231
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
232
|
+
// const initialValuesRef = useRef<EntityValues<M>>(entity?.values ?? baseDataSourceValues as EntityValues<M>);
|
|
233
|
+
const [internalValues, setInternalValues] = useState<EntityValues<M> | undefined>(entity?.values ?? baseDataSourceValuesRef.current as EntityValues<M>);
|
|
234
|
+
|
|
235
|
+
const modifiedValuesRef = useRef<EntityValues<M> | undefined>(undefined);
|
|
236
|
+
const modifiedValues = modifiedValuesRef.current;
|
|
237
|
+
|
|
238
|
+
const subcollections = (collection.subcollections ?? []).filter(c => !c.hideFromNavigation);
|
|
239
|
+
const subcollectionsCount = subcollections?.length ?? 0;
|
|
240
|
+
const customViews = collection.entityViews;
|
|
241
|
+
const customViewsCount = customViews?.length ?? 0;
|
|
242
|
+
const autoSave = collection.formAutoSave && !collection.customId;
|
|
243
|
+
|
|
244
|
+
const hasAdditionalViews = customViewsCount > 0 || subcollectionsCount > 0;
|
|
141
245
|
|
|
142
246
|
const [usedEntity, setUsedEntity] = useState<Entity<M> | undefined>(entity);
|
|
143
247
|
const [readOnly, setReadOnly] = useState<boolean | undefined>(undefined);
|
|
@@ -279,11 +383,112 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
|
|
|
279
383
|
}
|
|
280
384
|
};
|
|
281
385
|
|
|
386
|
+
const onSubmit = (values: EntityValues<M>, formexController: FormexController<EntityValues<M>>) => {
|
|
387
|
+
|
|
388
|
+
if (mustSetCustomId && !entityId) {
|
|
389
|
+
console.error("Missing custom Id");
|
|
390
|
+
setEntityIdError(true);
|
|
391
|
+
formexController.setSubmitting(false);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
setSavingError(undefined);
|
|
396
|
+
setEntityIdError(false);
|
|
397
|
+
|
|
398
|
+
if (status === "existing") {
|
|
399
|
+
if (!entity?.id) throw Error("Form misconfiguration when saving, no id for existing entity");
|
|
400
|
+
} else if (status === "new" || status === "copy") {
|
|
401
|
+
if (inputCollection.customId) {
|
|
402
|
+
if (inputCollection.customId !== "optional" && !entityId) {
|
|
403
|
+
throw Error("Form misconfiguration when saving, entityId should be set");
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
throw Error("New FormType added, check EntityForm");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return save(values)
|
|
411
|
+
?.then(_ => {
|
|
412
|
+
formexController.resetForm({
|
|
413
|
+
values,
|
|
414
|
+
submitCount: 0,
|
|
415
|
+
touched: {}
|
|
416
|
+
});
|
|
417
|
+
})
|
|
418
|
+
.finally(() => {
|
|
419
|
+
formexController.setSubmitting(false);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const formex: FormexController<M> = useCreateFormex<M>({
|
|
425
|
+
initialValues: baseDataSourceValuesRef.current as M,
|
|
426
|
+
onSubmit,
|
|
427
|
+
validation: (values) => {
|
|
428
|
+
return validationSchema?.validate(values, { abortEarly: false })
|
|
429
|
+
.then(() => {
|
|
430
|
+
return {};
|
|
431
|
+
})
|
|
432
|
+
.catch((e: any) => {
|
|
433
|
+
const errors: Record<string, string> = {};
|
|
434
|
+
e.inner.forEach((error: any) => {
|
|
435
|
+
errors[error.path] = error.message;
|
|
436
|
+
});
|
|
437
|
+
return yupToFormErrors(e);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const resolvedCollection = resolveCollection<M>({
|
|
443
|
+
collection: inputCollection,
|
|
444
|
+
path,
|
|
445
|
+
entityId,
|
|
446
|
+
values: internalValues,
|
|
447
|
+
previousValues: formex.initialValues,
|
|
448
|
+
fields: customizationController.propertyConfigs
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
const save = (values: EntityValues<M>): Promise<void> => {
|
|
452
|
+
return onSaveEntityRequest({
|
|
453
|
+
collection: resolvedCollection,
|
|
454
|
+
path,
|
|
455
|
+
entityId,
|
|
456
|
+
values,
|
|
457
|
+
previousValues: entity?.values,
|
|
458
|
+
closeAfterSave: closeAfterSaveRef.current,
|
|
459
|
+
autoSave: autoSave ?? false
|
|
460
|
+
}).then(_ => {
|
|
461
|
+
const eventName: CMSAnalyticsEvent = status === "new"
|
|
462
|
+
? "new_entity_saved"
|
|
463
|
+
: (status === "copy" ? "entity_copied" : (status === "existing" ? "entity_edited" : "unmapped_event"));
|
|
464
|
+
analyticsController.onAnalyticsEvent?.(eventName, { path });
|
|
465
|
+
}).catch(e => {
|
|
466
|
+
console.error(e);
|
|
467
|
+
setSavingError(e);
|
|
468
|
+
}).finally(() => {
|
|
469
|
+
closeAfterSaveRef.current = false;
|
|
470
|
+
});
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const formContext: FormContext<M> = {
|
|
474
|
+
// @ts-ignore
|
|
475
|
+
setFieldValue: useCallback(formex.setFieldValue, []),
|
|
476
|
+
values: formex.values,
|
|
477
|
+
collection: resolvedCollection,
|
|
478
|
+
entityId,
|
|
479
|
+
path,
|
|
480
|
+
save,
|
|
481
|
+
formex
|
|
482
|
+
};
|
|
483
|
+
|
|
282
484
|
const resolvedEntityViews = customViews ? customViews
|
|
283
485
|
.map(e => resolveEntityView(e, customizationController.entityViews))
|
|
284
486
|
.filter(Boolean) as EntityCustomView[]
|
|
285
487
|
: [];
|
|
286
488
|
|
|
489
|
+
const selectedEntityView = resolvedEntityViews.find(e => e.key === selectedTabRef.current);
|
|
490
|
+
const shouldShowEntityActions = !autoSave && (selectedTabRef.current === MAIN_TAB_VALUE || selectedEntityView?.includeActions);
|
|
491
|
+
|
|
287
492
|
const customViewsView: React.ReactNode[] | undefined = customViews && resolvedEntityViews
|
|
288
493
|
.map(
|
|
289
494
|
(customView, colIndex) => {
|
|
@@ -394,23 +599,313 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
|
|
|
394
599
|
onValuesAreModified(dirty);
|
|
395
600
|
}
|
|
396
601
|
|
|
602
|
+
// useEffect(() => {
|
|
603
|
+
// baseDataSourceValuesRef.current = getDataSourceEntityValues(initialResolvedCollection, status, entity);
|
|
604
|
+
// const initialValues = formex.initialValues;
|
|
605
|
+
// if (!formex.isSubmitting && initialValues && status === "existing") {
|
|
606
|
+
// setUnderlyingChanges(
|
|
607
|
+
// Object.entries(resolvedCollection.properties)
|
|
608
|
+
// .map(([key, property]) => {
|
|
609
|
+
// if (isHidden(property)) {
|
|
610
|
+
// return {};
|
|
611
|
+
// }
|
|
612
|
+
// const initialValue = initialValues[key];
|
|
613
|
+
// const latestValue = baseDataSourceValuesRef.current[key];
|
|
614
|
+
// if (!equal(initialValue, latestValue)) {
|
|
615
|
+
// return { [key]: latestValue };
|
|
616
|
+
// }
|
|
617
|
+
// return {};
|
|
618
|
+
// })
|
|
619
|
+
// .reduce((a, b) => ({ ...a, ...b }), {}) as Partial<EntityValues<M>>
|
|
620
|
+
// );
|
|
621
|
+
// } else {
|
|
622
|
+
// setUnderlyingChanges({});
|
|
623
|
+
// }
|
|
624
|
+
// }, [entity, initialResolvedCollection, status]);
|
|
625
|
+
|
|
626
|
+
const pluginActions: React.ReactNode[] = [];
|
|
627
|
+
|
|
628
|
+
const plugins = customizationController.plugins;
|
|
629
|
+
|
|
630
|
+
if (plugins && inputCollection) {
|
|
631
|
+
const actionProps: PluginFormActionProps = {
|
|
632
|
+
entityId,
|
|
633
|
+
path,
|
|
634
|
+
status,
|
|
635
|
+
collection: inputCollection,
|
|
636
|
+
context,
|
|
637
|
+
currentEntityId: entityId,
|
|
638
|
+
formContext
|
|
639
|
+
};
|
|
640
|
+
pluginActions.push(...plugins.map((plugin, i) => (
|
|
641
|
+
plugin.form?.Actions
|
|
642
|
+
? <plugin.form.Actions
|
|
643
|
+
key={`actions_${plugin.key}`} {...actionProps}/>
|
|
644
|
+
: null
|
|
645
|
+
)).filter(Boolean));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const titlePropertyKey = getEntityTitlePropertyKey(resolvedCollection, customizationController.propertyConfigs);
|
|
649
|
+
const title = internalValues && titlePropertyKey ? getValueInPath(internalValues, titlePropertyKey) : undefined;
|
|
650
|
+
|
|
651
|
+
const onIdUpdate = inputCollection.callbacks?.onIdUpdate;
|
|
652
|
+
|
|
653
|
+
const doOnIdUpdate = useCallback(async () => {
|
|
654
|
+
if (onIdUpdate && internalValues && (status === "new" || status === "copy")) {
|
|
655
|
+
setCustomIdLoading(true);
|
|
656
|
+
try {
|
|
657
|
+
const updatedId = await onIdUpdate({
|
|
658
|
+
collection: resolvedCollection,
|
|
659
|
+
path,
|
|
660
|
+
entityId,
|
|
661
|
+
values: internalValues,
|
|
662
|
+
context
|
|
663
|
+
});
|
|
664
|
+
setEntityId(updatedId);
|
|
665
|
+
} catch (e) {
|
|
666
|
+
onIdUpdateError && onIdUpdateError(e);
|
|
667
|
+
console.error(e);
|
|
668
|
+
}
|
|
669
|
+
setCustomIdLoading(false);
|
|
670
|
+
}
|
|
671
|
+
}, [entityId, internalValues, status]);
|
|
672
|
+
|
|
673
|
+
useEffect(() => {
|
|
674
|
+
doOnIdUpdate();
|
|
675
|
+
}, [doOnIdUpdate]);
|
|
676
|
+
|
|
677
|
+
const [underlyingChanges, setUnderlyingChanges] = useState<Partial<EntityValues<M>>>({});
|
|
678
|
+
|
|
679
|
+
const uniqueFieldValidator: CustomFieldValidator = useCallback(({
|
|
680
|
+
name,
|
|
681
|
+
value,
|
|
682
|
+
property
|
|
683
|
+
}) => dataSource.checkUniqueField(path, name, value, entityId),
|
|
684
|
+
[dataSource, path, entityId]);
|
|
685
|
+
|
|
686
|
+
const validationSchema = useMemo(() => entityId
|
|
687
|
+
? getYupEntitySchema(
|
|
688
|
+
entityId,
|
|
689
|
+
resolvedCollection.properties,
|
|
690
|
+
uniqueFieldValidator)
|
|
691
|
+
: undefined,
|
|
692
|
+
[entityId, resolvedCollection.properties, uniqueFieldValidator]);
|
|
693
|
+
|
|
694
|
+
const getActionsForEntity = useCallback(({
|
|
695
|
+
entity,
|
|
696
|
+
customEntityActions
|
|
697
|
+
}: {
|
|
698
|
+
entity?: Entity<M>,
|
|
699
|
+
customEntityActions?: EntityAction[]
|
|
700
|
+
}): EntityAction[] => {
|
|
701
|
+
const createEnabled = canCreateEntity(inputCollection, authController, path, null);
|
|
702
|
+
const deleteEnabled = entity ? canDeleteEntity(inputCollection, authController, path, entity) : true;
|
|
703
|
+
const actions: EntityAction[] = [];
|
|
704
|
+
if (createEnabled)
|
|
705
|
+
actions.push(copyEntityAction);
|
|
706
|
+
if (deleteEnabled)
|
|
707
|
+
actions.push(deleteEntityAction);
|
|
708
|
+
if (customEntityActions)
|
|
709
|
+
actions.push(...customEntityActions);
|
|
710
|
+
return actions;
|
|
711
|
+
}, [authController, inputCollection, path]);
|
|
712
|
+
|
|
713
|
+
const modified = formex.dirty;
|
|
714
|
+
useEffect(() => {
|
|
715
|
+
if (onModified)
|
|
716
|
+
onModified(modified);
|
|
717
|
+
if (onValuesChanged)
|
|
718
|
+
onValuesChanged(formex.values);
|
|
719
|
+
}, [modified, formex.values]);
|
|
720
|
+
|
|
721
|
+
useEffect(() => {
|
|
722
|
+
if (!autoSave && !formex.isSubmitting && underlyingChanges && entity) {
|
|
723
|
+
// we update the form fields from the Firestore data
|
|
724
|
+
// if they were not touched
|
|
725
|
+
Object.entries(underlyingChanges).forEach(([key, value]) => {
|
|
726
|
+
const formValue = formex.values[key];
|
|
727
|
+
if (!equal(value, formValue) && !formex.touched[key]) {
|
|
728
|
+
console.debug("Updated value from the datasource:", key, value);
|
|
729
|
+
formex.setFieldValue(key, value !== undefined ? value : null);
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}, [formex.isSubmitting, autoSave, underlyingChanges, entity, formex.values, formex.touched, formex.setFieldValue]);
|
|
734
|
+
|
|
735
|
+
const formFields = (
|
|
736
|
+
<>
|
|
737
|
+
{(resolvedCollection.propertiesOrder ?? Object.keys(resolvedCollection.properties))
|
|
738
|
+
.map((key) => {
|
|
739
|
+
|
|
740
|
+
const property = resolvedCollection.properties[key];
|
|
741
|
+
if (!property) {
|
|
742
|
+
console.warn(`Property ${key} not found in collection ${resolvedCollection.name}`);
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const underlyingValueHasChanged: boolean =
|
|
747
|
+
!!underlyingChanges &&
|
|
748
|
+
Object.keys(underlyingChanges).includes(key) &&
|
|
749
|
+
!!formex.touched[key];
|
|
750
|
+
|
|
751
|
+
const disabled = (!autoSave && formex.isSubmitting) || isReadOnly(property) || Boolean(property.disabled);
|
|
752
|
+
const hidden = isHidden(property);
|
|
753
|
+
if (hidden) return null;
|
|
754
|
+
const cmsFormFieldProps: PropertyFieldBindingProps<any, M> = {
|
|
755
|
+
propertyKey: key,
|
|
756
|
+
disabled,
|
|
757
|
+
property,
|
|
758
|
+
includeDescription: property.description || property.longDescription,
|
|
759
|
+
underlyingValueHasChanged: underlyingValueHasChanged && !autoSave,
|
|
760
|
+
context: formContext,
|
|
761
|
+
tableMode: false,
|
|
762
|
+
partOfArray: false,
|
|
763
|
+
partOfBlock: false,
|
|
764
|
+
autoFocus: false
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
return (
|
|
768
|
+
<div id={`form_field_${key}`}
|
|
769
|
+
key={`field_${resolvedCollection.name}_${key}`}>
|
|
770
|
+
<ErrorBoundary>
|
|
771
|
+
<Tooltip title={<PropertyIdCopyTooltipContent propertyId={key}/>}
|
|
772
|
+
delayDuration={800}
|
|
773
|
+
side={"left"}
|
|
774
|
+
align={"start"}
|
|
775
|
+
sideOffset={16}>
|
|
776
|
+
<PropertyFieldBinding {...cmsFormFieldProps}/>
|
|
777
|
+
</Tooltip>
|
|
778
|
+
</ErrorBoundary>
|
|
779
|
+
</div>
|
|
780
|
+
);
|
|
781
|
+
})
|
|
782
|
+
.filter(Boolean)}
|
|
783
|
+
|
|
784
|
+
</>
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
const disabled = formex.isSubmitting || (!modified && status === "existing");
|
|
788
|
+
const formRef = React.useRef<HTMLDivElement>(null);
|
|
789
|
+
|
|
790
|
+
const entityActions = getActionsForEntity({
|
|
791
|
+
entity,
|
|
792
|
+
customEntityActions: inputCollection.entityActions
|
|
793
|
+
});
|
|
794
|
+
const formActions = entityActions.filter(a => a.includeInForm === undefined || a.includeInForm);
|
|
795
|
+
|
|
796
|
+
const dialogActions = <DialogActions position={"absolute"}>
|
|
797
|
+
|
|
798
|
+
{savingError &&
|
|
799
|
+
<div className="text-right">
|
|
800
|
+
<Typography color={"error"}>
|
|
801
|
+
{savingError.message}
|
|
802
|
+
</Typography>
|
|
803
|
+
</div>}
|
|
804
|
+
|
|
805
|
+
{entity && formActions.length > 0 && <div className="flex-grow flex overflow-auto no-scrollbar">
|
|
806
|
+
{formActions.map(action => (
|
|
807
|
+
<IconButton
|
|
808
|
+
key={action.name}
|
|
809
|
+
color="primary"
|
|
810
|
+
onClick={(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
|
811
|
+
event.stopPropagation();
|
|
812
|
+
if (entity)
|
|
813
|
+
action.onClick({
|
|
814
|
+
entity,
|
|
815
|
+
fullPath: resolvedCollection.path,
|
|
816
|
+
collection: resolvedCollection,
|
|
817
|
+
context,
|
|
818
|
+
sideEntityController
|
|
819
|
+
});
|
|
820
|
+
}}>
|
|
821
|
+
{action.icon}
|
|
822
|
+
</IconButton>
|
|
823
|
+
))}
|
|
824
|
+
</div>}
|
|
825
|
+
{formex.isSubmitting && <CircularProgress size={"small"}/>}
|
|
826
|
+
<Button
|
|
827
|
+
variant="text"
|
|
828
|
+
disabled={disabled || formex.isSubmitting}
|
|
829
|
+
type="reset">
|
|
830
|
+
{status === "existing" ? "Discard" : "Clear"}
|
|
831
|
+
</Button>
|
|
832
|
+
|
|
833
|
+
<Button
|
|
834
|
+
variant="text"
|
|
835
|
+
color="primary"
|
|
836
|
+
type="submit"
|
|
837
|
+
disabled={disabled || formex.isSubmitting}
|
|
838
|
+
onClick={() => {
|
|
839
|
+
closeAfterSaveRef.current = false;
|
|
840
|
+
}}>
|
|
841
|
+
{status === "existing" && "Save"}
|
|
842
|
+
{status === "copy" && "Create copy"}
|
|
843
|
+
{status === "new" && "Create"}
|
|
844
|
+
</Button>
|
|
845
|
+
|
|
846
|
+
<Button
|
|
847
|
+
variant="filled"
|
|
848
|
+
color="primary"
|
|
849
|
+
type="submit"
|
|
850
|
+
disabled={disabled || formex.isSubmitting}
|
|
851
|
+
onClick={() => {
|
|
852
|
+
closeAfterSaveRef.current = true;
|
|
853
|
+
}}>
|
|
854
|
+
{status === "existing" && "Save and close"}
|
|
855
|
+
{status === "copy" && "Create copy and close"}
|
|
856
|
+
{status === "new" && "Create and close"}
|
|
857
|
+
</Button>
|
|
858
|
+
|
|
859
|
+
</DialogActions>;
|
|
860
|
+
|
|
397
861
|
function buildForm() {
|
|
398
|
-
|
|
399
|
-
let form = <
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
862
|
+
|
|
863
|
+
let form = <div className="h-full overflow-auto">
|
|
864
|
+
|
|
865
|
+
{pluginActions.length > 0 && <div
|
|
866
|
+
className={cls("w-full flex justify-end items-center sticky top-0 right-0 left-0 z-10 bg-opacity-60 bg-slate-200 dark:bg-opacity-60 dark:bg-slate-800 backdrop-blur-md")}>
|
|
867
|
+
{pluginActions}
|
|
868
|
+
</div>}
|
|
869
|
+
|
|
870
|
+
<div className="pt-12 pb-16 pl-8 pr-8 md:pl-10 md:pr-10">
|
|
871
|
+
<div
|
|
872
|
+
className={`w-full py-2 flex flex-col items-start mt-${4 + (pluginActions ? 8 : 0)} lg:mt-${8 + (pluginActions ? 8 : 0)} mb-8`}>
|
|
873
|
+
|
|
874
|
+
<Typography
|
|
875
|
+
className={"mt-4 flex-grow line-clamp-1 " + inputCollection.hideIdFromForm ? "mb-2" : "mb-0"}
|
|
876
|
+
variant={"h4"}>{title ?? inputCollection.singularName ?? inputCollection.name}
|
|
877
|
+
</Typography>
|
|
878
|
+
<Alert color={"base"} className={"w-full"} size={"small"}>
|
|
879
|
+
<code className={"text-xs select-all"}>{path}/{entityId}</code>
|
|
880
|
+
</Alert>
|
|
881
|
+
</div>
|
|
882
|
+
|
|
883
|
+
{!collection.hideIdFromForm &&
|
|
884
|
+
<CustomIdField customId={inputCollection.customId}
|
|
885
|
+
entityId={entityId}
|
|
886
|
+
status={status}
|
|
887
|
+
onChange={setEntityId}
|
|
888
|
+
error={entityIdError}
|
|
889
|
+
loading={customIdLoading}
|
|
890
|
+
entity={entity}/>}
|
|
891
|
+
|
|
892
|
+
{entityId && formContext && <>
|
|
893
|
+
<div className="mt-12 flex flex-col gap-8"
|
|
894
|
+
ref={formRef}>
|
|
895
|
+
|
|
896
|
+
{formFields}
|
|
897
|
+
|
|
898
|
+
<ErrorFocus containerRef={formRef}/>
|
|
899
|
+
|
|
900
|
+
</div>
|
|
901
|
+
|
|
902
|
+
<div className="h-14"/>
|
|
903
|
+
|
|
904
|
+
</>}
|
|
905
|
+
|
|
906
|
+
</div>
|
|
907
|
+
</div>;
|
|
908
|
+
|
|
414
909
|
if (plugins) {
|
|
415
910
|
plugins.forEach((plugin: FireCMSPlugin) => {
|
|
416
911
|
if (plugin.form?.provider) {
|
|
@@ -435,7 +930,7 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
|
|
|
435
930
|
return <ErrorBoundary>{form}</ErrorBoundary>;
|
|
436
931
|
}
|
|
437
932
|
|
|
438
|
-
const
|
|
933
|
+
const entityView = (readOnly === undefined)
|
|
439
934
|
? <></>
|
|
440
935
|
: (!readOnly
|
|
441
936
|
? buildForm()
|
|
@@ -450,6 +945,7 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
|
|
|
450
945
|
entity={usedEntity as Entity<M>}
|
|
451
946
|
path={path}
|
|
452
947
|
collection={collection}/>
|
|
948
|
+
|
|
453
949
|
</>
|
|
454
950
|
));
|
|
455
951
|
|
|
@@ -474,87 +970,133 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
|
|
|
474
970
|
</Tab>
|
|
475
971
|
);
|
|
476
972
|
|
|
973
|
+
useEffect(() => {
|
|
974
|
+
if (entityId && onIdChange)
|
|
975
|
+
onIdChange(entityId);
|
|
976
|
+
}, [entityId, onIdChange]);
|
|
977
|
+
|
|
477
978
|
return (
|
|
478
|
-
<
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
979
|
+
<Formex value={formex}>
|
|
980
|
+
|
|
981
|
+
<div className="flex flex-col h-full w-full transition-width duration-250 ease-in-out">
|
|
982
|
+
|
|
983
|
+
<div
|
|
984
|
+
className={cls(defaultBorderMixin, "no-scrollbar border-b pl-2 pr-2 pt-1 flex items-end overflow-scroll bg-gray-50 dark:bg-gray-950")}>
|
|
482
985
|
|
|
483
986
|
<div
|
|
484
|
-
className=
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
<CloseIcon/>
|
|
495
|
-
</IconButton>
|
|
496
|
-
</div>
|
|
987
|
+
className="pb-1 self-center">
|
|
988
|
+
<IconButton
|
|
989
|
+
onClick={() => {
|
|
990
|
+
onClose?.();
|
|
991
|
+
return sideDialogContext.close(false);
|
|
992
|
+
}}
|
|
993
|
+
size="large">
|
|
994
|
+
<CloseIcon/>
|
|
995
|
+
</IconButton>
|
|
996
|
+
</div>
|
|
497
997
|
|
|
498
|
-
|
|
998
|
+
<div className={"flex-grow"}/>
|
|
499
999
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
1000
|
+
{globalLoading && <div
|
|
1001
|
+
className="self-center">
|
|
1002
|
+
<CircularProgress size={"small"}/>
|
|
1003
|
+
</div>}
|
|
504
1004
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
1005
|
+
<Tabs
|
|
1006
|
+
value={selectedTabRef.current}
|
|
1007
|
+
onValueChange={(value) => {
|
|
1008
|
+
onSideTabClick(value);
|
|
1009
|
+
}}
|
|
1010
|
+
className="pl-4 pr-4 pt-0">
|
|
511
1011
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
1012
|
+
<Tab
|
|
1013
|
+
disabled={!hasAdditionalViews}
|
|
1014
|
+
value={MAIN_TAB_VALUE}
|
|
1015
|
+
className={`${
|
|
1016
|
+
!hasAdditionalViews ? "hidden" : ""
|
|
1017
|
+
} text-sm min-w-[140px]`}
|
|
1018
|
+
>{collection.singularName ?? collection.name}</Tab>
|
|
519
1019
|
|
|
520
|
-
|
|
1020
|
+
{customViewTabs}
|
|
521
1021
|
|
|
522
|
-
|
|
523
|
-
|
|
1022
|
+
{subcollectionTabs}
|
|
1023
|
+
</Tabs>
|
|
524
1024
|
|
|
525
|
-
|
|
1025
|
+
</div>
|
|
1026
|
+
|
|
1027
|
+
<form
|
|
1028
|
+
onSubmit={formex.handleSubmit}
|
|
1029
|
+
onReset={() => {
|
|
1030
|
+
formex.resetForm();
|
|
1031
|
+
return onDiscard && onDiscard();
|
|
1032
|
+
}}
|
|
1033
|
+
noValidate
|
|
1034
|
+
className={"flex-grow h-full flex overflow-auto flex-col w-full"}>
|
|
526
1035
|
|
|
527
1036
|
<div
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
// [`@media (max-width: ${resolvedFormWidth})`]: {
|
|
533
|
-
// width: resolvedFormWidth
|
|
534
|
-
// }
|
|
535
|
-
}}>
|
|
536
|
-
|
|
537
|
-
<div
|
|
538
|
-
role="tabpanel"
|
|
539
|
-
hidden={!mainViewVisible}
|
|
540
|
-
id={`form_${path}`}
|
|
541
|
-
className={" w-full"}>
|
|
542
|
-
|
|
543
|
-
{globalLoading
|
|
544
|
-
? <CircularProgressCenter/>
|
|
545
|
-
: form}
|
|
1037
|
+
role="tabpanel"
|
|
1038
|
+
hidden={!mainViewVisible}
|
|
1039
|
+
id={`form_${path}`}
|
|
1040
|
+
className={" w-full"}>
|
|
546
1041
|
|
|
547
|
-
|
|
1042
|
+
{globalLoading
|
|
1043
|
+
? <CircularProgressCenter/>
|
|
1044
|
+
: entityView}
|
|
548
1045
|
|
|
549
|
-
|
|
1046
|
+
</div>
|
|
550
1047
|
|
|
551
|
-
|
|
1048
|
+
{customViewsView}
|
|
552
1049
|
|
|
553
|
-
|
|
1050
|
+
{subCollectionsViews}
|
|
554
1051
|
|
|
555
|
-
|
|
556
|
-
}
|
|
1052
|
+
{shouldShowEntityActions && dialogActions}
|
|
557
1053
|
|
|
558
|
-
|
|
1054
|
+
</form>
|
|
1055
|
+
|
|
1056
|
+
</div>
|
|
1057
|
+
</Formex>
|
|
559
1058
|
);
|
|
560
1059
|
}
|
|
1060
|
+
|
|
1061
|
+
function getDataSourceEntityValues<M extends object>(initialResolvedCollection: ResolvedEntityCollection,
|
|
1062
|
+
status: "new" | "existing" | "copy",
|
|
1063
|
+
entity: Entity<M> | undefined): Partial<EntityValues<M>> {
|
|
1064
|
+
|
|
1065
|
+
const properties = initialResolvedCollection.properties;
|
|
1066
|
+
if ((status === "existing" || status === "copy") && entity) {
|
|
1067
|
+
return entity.values ?? getDefaultValuesFor(properties);
|
|
1068
|
+
} else if (status === "new") {
|
|
1069
|
+
return getDefaultValuesFor(properties);
|
|
1070
|
+
} else {
|
|
1071
|
+
console.error({
|
|
1072
|
+
status,
|
|
1073
|
+
entity
|
|
1074
|
+
});
|
|
1075
|
+
throw new Error("Form has not been initialised with the correct parameters");
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
export type EntityFormSaveParams<M extends Record<string, any>> = {
|
|
1080
|
+
collection: ResolvedEntityCollection<M>,
|
|
1081
|
+
path: string,
|
|
1082
|
+
entityId: string | undefined,
|
|
1083
|
+
values: EntityValues<M>,
|
|
1084
|
+
previousValues?: EntityValues<M>,
|
|
1085
|
+
closeAfterSave: boolean,
|
|
1086
|
+
autoSave: boolean
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
export function yupToFormErrors(yupError: ValidationError): Record<string, any> {
|
|
1090
|
+
let errors: Record<string, any> = {};
|
|
1091
|
+
if (yupError.inner) {
|
|
1092
|
+
if (yupError.inner.length === 0) {
|
|
1093
|
+
return setIn(errors, yupError.path!, yupError.message);
|
|
1094
|
+
}
|
|
1095
|
+
for (const err of yupError.inner) {
|
|
1096
|
+
if (!getIn(errors, err.path!)) {
|
|
1097
|
+
errors = setIn(errors, err.path!, err.message);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return errors;
|
|
1102
|
+
}
|