@firecms/core 3.0.0-canary.66 → 3.0.0-canary.67

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 (40) hide show
  1. package/dist/core/EntityEditView.d.ts +17 -3
  2. package/dist/form/PropertiesForm.d.ts +8 -0
  3. package/dist/form/components/FieldHelperText.d.ts +3 -3
  4. package/dist/form/components/StorageItemPreview.d.ts +2 -4
  5. package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
  6. package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +2 -4
  7. package/dist/form/index.d.ts +0 -2
  8. package/dist/index.es.js +4269 -4322
  9. package/dist/index.es.js.map +1 -1
  10. package/dist/index.umd.js +5 -5
  11. package/dist/index.umd.js.map +1 -1
  12. package/dist/types/collections.d.ts +14 -0
  13. package/dist/types/fields.d.ts +31 -30
  14. package/dist/types/plugins.d.ts +2 -2
  15. package/dist/types/properties.d.ts +1 -1
  16. package/dist/util/storage.d.ts +23 -2
  17. package/dist/util/useStorageUploadController.d.ts +1 -1
  18. package/package.json +4 -4
  19. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +2 -1
  20. package/src/core/EntityEditView.tsx +662 -120
  21. package/src/core/EntitySidePanel.tsx +0 -1
  22. package/src/form/PropertiesForm.tsx +81 -0
  23. package/src/form/PropertyFieldBinding.tsx +28 -5
  24. package/src/form/components/FieldHelperText.tsx +3 -3
  25. package/src/form/components/StorageItemPreview.tsx +0 -4
  26. package/src/form/field_bindings/MapFieldBinding.tsx +10 -3
  27. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +0 -7
  28. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +3 -26
  29. package/src/form/index.tsx +4 -4
  30. package/src/form/validation.ts +1 -17
  31. package/src/types/collections.ts +14 -0
  32. package/src/types/customization_controller.tsx +0 -1
  33. package/src/types/fields.tsx +33 -33
  34. package/src/types/plugins.tsx +2 -2
  35. package/src/types/properties.ts +1 -1
  36. package/src/util/permissions.ts +1 -0
  37. package/src/util/storage.ts +75 -21
  38. package/src/util/useStorageUploadController.tsx +21 -3
  39. package/dist/form/EntityForm.d.ts +0 -77
  40. 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 { CircularProgressCenter, EntityCollectionView, EntityView, ErrorBoundary, } from "../components";
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 { EntityForm } from "../form";
32
- import { CircularProgress, CloseIcon, cls, defaultBorderMixin, IconButton, Tab, Tabs, Typography } from "@firecms/ui";
33
- import { EntityFormSaveParams } from "../form/EntityForm";
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
- path,
59
- entityId,
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 [formContext, setFormContext] = useState<FormContext<M> | undefined>(undefined);
175
+ const closeAfterSaveRef = useRef(false);
104
176
 
105
- const [status, setStatus] = useState<EntityStatus>(copy ? "copy" : (entityId ? "existing" : "new"));
177
+ const analyticsController = useAnalyticsController();
106
178
 
107
- const modifiedValuesRef = useRef<EntityValues<M> | undefined>(undefined);
108
- const modifiedValues = modifiedValuesRef.current;
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 subcollections = (collection.subcollections ?? []).filter(c => !c.hideFromNavigation);
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 hasAdditionalViews = customViewsCount > 0 || subcollectionsCount > 0;
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 defaultSelectedView = selectedSubPath ?? resolveDefaultSelectedView(
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
- entity,
132
- dataLoading,
133
- // eslint-disable-next-line no-unused-vars
134
- dataLoadingError
135
- } = useEntityFetch<M, UserType>({
136
- path,
137
- entityId,
138
- collection,
139
- useCache: false
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
- const plugins = customizationController.plugins;
399
- let form = <EntityForm
400
- status={status}
401
- path={path}
402
- collection={collection}
403
- onEntitySaveRequested={onSaveEntityRequest}
404
- onDiscard={onDiscard}
405
- onValuesChanged={onValuesChanged}
406
- onModified={onModified}
407
- entity={usedEntity}
408
- onIdChange={onIdChange}
409
- onFormContextChange={setFormContext}
410
- hideId={collection.hideIdFromForm}
411
- autoSave={autoSave}
412
- onIdUpdateError={onIdUpdateError}
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 form = (readOnly === undefined)
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
- <div
479
- className="flex flex-col h-full w-full transition-width duration-250 ease-in-out">
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={cls(defaultBorderMixin, "no-scrollbar border-b pl-2 pr-2 pt-1 flex items-end overflow-scroll bg-gray-50 dark:bg-gray-950")}>
485
-
486
- <div
487
- className="pb-1 self-center">
488
- <IconButton
489
- onClick={() => {
490
- onClose?.();
491
- return sideDialogContext.close(false);
492
- }}
493
- size="large">
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
- <div className={"flex-grow"}/>
998
+ <div className={"flex-grow"}/>
499
999
 
500
- {globalLoading && <div
501
- className="self-center">
502
- <CircularProgress size={"small"}/>
503
- </div>}
1000
+ {globalLoading && <div
1001
+ className="self-center">
1002
+ <CircularProgress size={"small"}/>
1003
+ </div>}
504
1004
 
505
- <Tabs
506
- value={selectedTabRef.current}
507
- onValueChange={(value) => {
508
- onSideTabClick(value);
509
- }}
510
- className="pl-4 pr-4 pt-0">
1005
+ <Tabs
1006
+ value={selectedTabRef.current}
1007
+ onValueChange={(value) => {
1008
+ onSideTabClick(value);
1009
+ }}
1010
+ className="pl-4 pr-4 pt-0">
511
1011
 
512
- <Tab
513
- disabled={!hasAdditionalViews}
514
- value={MAIN_TAB_VALUE}
515
- className={`${
516
- !hasAdditionalViews ? "hidden" : ""
517
- } text-sm min-w-[140px]`}
518
- >{collection.singularName ?? collection.name}</Tab>
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
- {customViewTabs}
1020
+ {customViewTabs}
521
1021
 
522
- {subcollectionTabs}
523
- </Tabs>
1022
+ {subcollectionTabs}
1023
+ </Tabs>
524
1024
 
525
- </div>
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
- className={"flex-grow h-full flex overflow-auto flex-row w-full "}
529
- style={{
530
- // width: `calc(${ADDITIONAL_TAB_WIDTH} + ${resolvedFormWidth})`,
531
- // maxWidth: "100%",
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
- </div>
1042
+ {globalLoading
1043
+ ? <CircularProgressCenter/>
1044
+ : entityView}
548
1045
 
549
- {customViewsView}
1046
+ </div>
550
1047
 
551
- {subCollectionsViews}
1048
+ {customViewsView}
552
1049
 
553
- </div>
1050
+ {subCollectionsViews}
554
1051
 
555
- </>
556
- }
1052
+ {shouldShowEntityActions && dialogActions}
557
1053
 
558
- </div>
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
+ }