@firecms/core 3.0.0-beta.7 → 3.0.0-beta.8

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 (94) hide show
  1. package/dist/app/AppBar.d.ts +12 -0
  2. package/dist/app/Drawer.d.ts +17 -0
  3. package/dist/app/Scaffold.d.ts +30 -0
  4. package/dist/app/index.d.ts +4 -0
  5. package/dist/app/useApp.d.ts +16 -0
  6. package/dist/components/VirtualTable/VirtualTableProps.d.ts +4 -0
  7. package/dist/components/index.d.ts +1 -1
  8. package/dist/{components/FireCMSAppBar.d.ts → core/DefaultAppBar.d.ts} +2 -7
  9. package/dist/core/DefaultDrawer.d.ts +19 -0
  10. package/dist/core/EntityEditView.d.ts +17 -3
  11. package/dist/core/NavigationRoutes.d.ts +1 -1
  12. package/dist/core/index.d.ts +2 -2
  13. package/dist/form/PropertiesForm.d.ts +8 -0
  14. package/dist/form/components/FieldHelperText.d.ts +3 -3
  15. package/dist/form/components/StorageItemPreview.d.ts +4 -4
  16. package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
  17. package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +2 -4
  18. package/dist/form/index.d.ts +0 -2
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.es.js +5595 -5574
  21. package/dist/index.es.js.map +1 -1
  22. package/dist/index.umd.js +5 -5
  23. package/dist/index.umd.js.map +1 -1
  24. package/dist/preview/PropertyPreviewProps.d.ts +5 -0
  25. package/dist/preview/components/StorageThumbnail.d.ts +2 -1
  26. package/dist/preview/components/UrlComponentPreview.d.ts +2 -1
  27. package/dist/types/auth.d.ts +1 -1
  28. package/dist/types/collections.d.ts +14 -0
  29. package/dist/types/datasource.d.ts +18 -8
  30. package/dist/types/fields.d.ts +31 -30
  31. package/dist/types/plugins.d.ts +11 -3
  32. package/dist/types/properties.d.ts +7 -2
  33. package/dist/util/icons.d.ts +2 -2
  34. package/dist/util/storage.d.ts +23 -2
  35. package/dist/util/useStorageUploadController.d.ts +1 -1
  36. package/package.json +13 -22
  37. package/src/app/AppBar.tsx +18 -0
  38. package/src/app/Drawer.tsx +25 -0
  39. package/src/{core → app}/Scaffold.tsx +61 -107
  40. package/src/app/index.ts +4 -0
  41. package/src/app/useApp.tsx +32 -0
  42. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +7 -9
  43. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +2 -1
  44. package/src/components/EntityCollectionView/EntityCollectionView.tsx +7 -4
  45. package/src/components/VirtualTable/VirtualTable.tsx +3 -1
  46. package/src/components/VirtualTable/VirtualTableProps.tsx +5 -0
  47. package/src/components/common/useTableSearchHelper.ts +52 -12
  48. package/src/components/index.tsx +1 -1
  49. package/src/contexts/DialogsProvider.tsx +2 -2
  50. package/src/{components/FireCMSAppBar.tsx → core/DefaultAppBar.tsx} +26 -28
  51. package/src/core/DefaultDrawer.tsx +177 -0
  52. package/src/core/EntityEditView.tsx +669 -129
  53. package/src/core/EntitySidePanel.tsx +0 -1
  54. package/src/core/NavigationRoutes.tsx +3 -3
  55. package/src/core/index.tsx +2 -2
  56. package/src/form/PropertiesForm.tsx +81 -0
  57. package/src/form/PropertyFieldBinding.tsx +29 -5
  58. package/src/form/components/FieldHelperText.tsx +3 -3
  59. package/src/form/components/StorageItemPreview.tsx +19 -10
  60. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +8 -5
  61. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +5 -4
  62. package/src/form/field_bindings/KeyValueFieldBinding.tsx +40 -35
  63. package/src/form/field_bindings/MapFieldBinding.tsx +11 -3
  64. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +0 -7
  65. package/src/form/field_bindings/RepeatFieldBinding.tsx +10 -7
  66. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +6 -35
  67. package/src/form/field_bindings/SwitchFieldBinding.tsx +1 -1
  68. package/src/form/index.tsx +4 -4
  69. package/src/form/validation.ts +1 -17
  70. package/src/index.ts +1 -0
  71. package/src/internal/useBuildDataSource.ts +12 -0
  72. package/src/preview/PropertyPreview.tsx +5 -2
  73. package/src/preview/PropertyPreviewProps.tsx +6 -0
  74. package/src/preview/components/ImagePreview.tsx +21 -33
  75. package/src/preview/components/ReferencePreview.tsx +2 -1
  76. package/src/preview/components/StorageThumbnail.tsx +5 -1
  77. package/src/preview/components/UrlComponentPreview.tsx +44 -11
  78. package/src/types/auth.tsx +1 -1
  79. package/src/types/collections.ts +14 -0
  80. package/src/types/customization_controller.tsx +0 -1
  81. package/src/types/datasource.ts +20 -16
  82. package/src/types/fields.tsx +33 -33
  83. package/src/types/plugins.tsx +12 -3
  84. package/src/types/properties.ts +7 -2
  85. package/src/util/entities.ts +4 -3
  86. package/src/util/objects.ts +17 -7
  87. package/src/util/permissions.ts +1 -0
  88. package/src/util/storage.ts +75 -21
  89. package/src/util/useStorageUploadController.tsx +21 -3
  90. package/dist/core/Drawer.d.ts +0 -16
  91. package/dist/core/Scaffold.d.ts +0 -51
  92. package/dist/form/EntityForm.d.ts +0 -77
  93. package/src/core/Drawer.tsx +0 -139
  94. package/src/form/EntityForm.tsx +0 -736
@@ -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>();
216
+
217
+ const [customIdLoading, setCustomIdLoading] = React.useState<boolean>(false);
117
218
 
118
- const defaultSelectedView = selectedSubPath ?? resolveDefaultSelectedView(
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,115 @@ 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: formex.values,
447
+ previousValues: formex.initialValues,
448
+ fields: customizationController.propertyConfigs
449
+ });
450
+
451
+ const lastSavedValues = useRef<EntityValues<M> | undefined>(entity?.values);
452
+
453
+ const save = (values: EntityValues<M>): Promise<void> => {
454
+ lastSavedValues.current = values;
455
+ return onSaveEntityRequest({
456
+ collection: resolvedCollection,
457
+ path,
458
+ entityId,
459
+ values,
460
+ previousValues: entity?.values,
461
+ closeAfterSave: closeAfterSaveRef.current,
462
+ autoSave: autoSave ?? false
463
+ }).then(_ => {
464
+ const eventName: CMSAnalyticsEvent = status === "new"
465
+ ? "new_entity_saved"
466
+ : (status === "copy" ? "entity_copied" : (status === "existing" ? "entity_edited" : "unmapped_event"));
467
+ analyticsController.onAnalyticsEvent?.(eventName, { path });
468
+ }).catch(e => {
469
+ console.error(e);
470
+ setSavingError(e);
471
+ }).finally(() => {
472
+ closeAfterSaveRef.current = false;
473
+ });
474
+ };
475
+
476
+ const formContext: FormContext<M> = {
477
+ // @ts-ignore
478
+ setFieldValue: useCallback(formex.setFieldValue, []),
479
+ values: formex.values,
480
+ collection: resolvedCollection,
481
+ entityId,
482
+ path,
483
+ save,
484
+ formex
485
+ };
486
+
282
487
  const resolvedEntityViews = customViews ? customViews
283
488
  .map(e => resolveEntityView(e, customizationController.entityViews))
284
489
  .filter(Boolean) as EntityCustomView[]
285
490
  : [];
286
491
 
492
+ const selectedEntityView = resolvedEntityViews.find(e => e.key === selectedTabRef.current);
493
+ const shouldShowEntityActions = !autoSave && (selectedTabRef.current === MAIN_TAB_VALUE || selectedEntityView?.includeActions);
494
+
287
495
  const customViewsView: React.ReactNode[] | undefined = customViews && resolvedEntityViews
288
496
  .map(
289
497
  (customView, colIndex) => {
@@ -305,7 +513,7 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
305
513
  {formContext && <Builder
306
514
  collection={collection}
307
515
  entity={usedEntity}
308
- modifiedValues={modifiedValues ?? usedEntity?.values}
516
+ modifiedValues={formex.values ?? usedEntity?.values}
309
517
  formContext={formContext}
310
518
  />}
311
519
  </ErrorBoundary>
@@ -368,9 +576,9 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
368
576
  });
369
577
  };
370
578
 
371
- const onValuesChanged = useCallback((values?: EntityValues<M>) => {
372
- modifiedValuesRef.current = values;
373
- }, []);
579
+ // const onValuesChanged = useCallback((values?: EntityValues<M>) => {
580
+ // modifiedValuesRef.current = values;
581
+ // }, []);
374
582
 
375
583
  // eslint-disable-next-line n/handle-callback-err
376
584
  const onIdUpdateError = useCallback((error: any) => {
@@ -389,28 +597,316 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
389
597
  : undefined);
390
598
  }, []);
391
599
 
392
- const onModified = (dirty: boolean) => {
393
- if (!autoSave)
394
- onValuesAreModified(dirty);
600
+ // useEffect(() => {
601
+ // baseDataSourceValuesRef.current = getDataSourceEntityValues(initialResolvedCollection, status, entity);
602
+ // const initialValues = formex.initialValues;
603
+ // if (!formex.isSubmitting && initialValues && status === "existing") {
604
+ // setUnderlyingChanges(
605
+ // Object.entries(resolvedCollection.properties)
606
+ // .map(([key, property]) => {
607
+ // if (isHidden(property)) {
608
+ // return {};
609
+ // }
610
+ // const initialValue = initialValues[key];
611
+ // const latestValue = baseDataSourceValuesRef.current[key];
612
+ // if (!equal(initialValue, latestValue)) {
613
+ // return { [key]: latestValue };
614
+ // }
615
+ // return {};
616
+ // })
617
+ // .reduce((a, b) => ({ ...a, ...b }), {}) as Partial<EntityValues<M>>
618
+ // );
619
+ // } else {
620
+ // setUnderlyingChanges({});
621
+ // }
622
+ // }, [entity, initialResolvedCollection, status]);
623
+
624
+ const pluginActions: React.ReactNode[] = [];
625
+
626
+ const plugins = customizationController.plugins;
627
+
628
+ if (plugins && inputCollection) {
629
+ const actionProps: PluginFormActionProps = {
630
+ entityId,
631
+ path,
632
+ status,
633
+ collection: inputCollection,
634
+ context,
635
+ currentEntityId: entityId,
636
+ formContext
637
+ };
638
+ pluginActions.push(...plugins.map((plugin, i) => (
639
+ plugin.form?.Actions
640
+ ? <plugin.form.Actions
641
+ key={`actions_${plugin.key}`} {...actionProps}/>
642
+ : null
643
+ )).filter(Boolean));
395
644
  }
396
645
 
646
+ const titlePropertyKey = getEntityTitlePropertyKey(resolvedCollection, customizationController.propertyConfigs);
647
+ const title = formex.values && titlePropertyKey ? getValueInPath(formex.values, titlePropertyKey) : undefined;
648
+
649
+ const onIdUpdate = inputCollection.callbacks?.onIdUpdate;
650
+
651
+ const doOnIdUpdate = useCallback(async () => {
652
+ if (onIdUpdate && formex.values && (status === "new" || status === "copy")) {
653
+ setCustomIdLoading(true);
654
+ try {
655
+ const updatedId = await onIdUpdate({
656
+ collection: resolvedCollection,
657
+ path,
658
+ entityId,
659
+ values: formex.values,
660
+ context
661
+ });
662
+ setEntityId(updatedId);
663
+ } catch (e) {
664
+ onIdUpdateError && onIdUpdateError(e);
665
+ console.error(e);
666
+ }
667
+ setCustomIdLoading(false);
668
+ }
669
+ }, [entityId, formex.values, status]);
670
+
671
+ useEffect(() => {
672
+ doOnIdUpdate();
673
+ }, [doOnIdUpdate]);
674
+
675
+ const [underlyingChanges, setUnderlyingChanges] = useState<Partial<EntityValues<M>>>({});
676
+
677
+ const uniqueFieldValidator: CustomFieldValidator = useCallback(({
678
+ name,
679
+ value,
680
+ property
681
+ }) => dataSource.checkUniqueField(path, name, value, entityId),
682
+ [dataSource, path, entityId]);
683
+
684
+ const validationSchema = useMemo(() => entityId
685
+ ? getYupEntitySchema(
686
+ entityId,
687
+ resolvedCollection.properties,
688
+ uniqueFieldValidator)
689
+ : undefined,
690
+ [entityId, resolvedCollection.properties, uniqueFieldValidator]);
691
+
692
+ const getActionsForEntity = useCallback(({
693
+ entity,
694
+ customEntityActions
695
+ }: {
696
+ entity?: Entity<M>,
697
+ customEntityActions?: EntityAction[]
698
+ }): EntityAction[] => {
699
+ const createEnabled = canCreateEntity(inputCollection, authController, path, null);
700
+ const deleteEnabled = entity ? canDeleteEntity(inputCollection, authController, path, entity) : true;
701
+ const actions: EntityAction[] = [];
702
+ if (createEnabled)
703
+ actions.push(copyEntityAction);
704
+ if (deleteEnabled)
705
+ actions.push(deleteEntityAction);
706
+ if (customEntityActions)
707
+ actions.push(...customEntityActions);
708
+ return actions;
709
+ }, [authController, inputCollection, path]);
710
+
711
+ const modified = formex.dirty;
712
+ useEffect(() => {
713
+ if (!autoSave) {
714
+ onValuesAreModified(modified);
715
+ } else {
716
+ if (formex.values && !equal(formex.values, lastSavedValues.current)) {
717
+ save(formex.values);
718
+ }
719
+ }
720
+ }, [modified, formex.values]);
721
+
722
+ useEffect(() => {
723
+ if (!autoSave && !formex.isSubmitting && underlyingChanges && entity) {
724
+ // we update the form fields from the Firestore data
725
+ // if they were not touched
726
+ Object.entries(underlyingChanges).forEach(([key, value]) => {
727
+ const formValue = formex.values[key];
728
+ if (!equal(value, formValue) && !formex.touched[key]) {
729
+ console.debug("Updated value from the datasource:", key, value);
730
+ formex.setFieldValue(key, value !== undefined ? value : null);
731
+ }
732
+ });
733
+ }
734
+ }, [formex.isSubmitting, autoSave, underlyingChanges, entity, formex.values, formex.touched, formex.setFieldValue]);
735
+
736
+ const formFields = (
737
+ <>
738
+ {(resolvedCollection.propertiesOrder ?? Object.keys(resolvedCollection.properties))
739
+ .map((key) => {
740
+
741
+ const property = resolvedCollection.properties[key];
742
+ if (!property) {
743
+ console.warn(`Property ${key} not found in collection ${resolvedCollection.name}`);
744
+ return null;
745
+ }
746
+
747
+ const underlyingValueHasChanged: boolean =
748
+ !!underlyingChanges &&
749
+ Object.keys(underlyingChanges).includes(key) &&
750
+ !!formex.touched[key];
751
+
752
+ const disabled = (!autoSave && formex.isSubmitting) || isReadOnly(property) || Boolean(property.disabled);
753
+ const hidden = isHidden(property);
754
+ if (hidden) return null;
755
+ const cmsFormFieldProps: PropertyFieldBindingProps<any, M> = {
756
+ propertyKey: key,
757
+ disabled,
758
+ property,
759
+ includeDescription: property.description || property.longDescription,
760
+ underlyingValueHasChanged: underlyingValueHasChanged && !autoSave,
761
+ context: formContext,
762
+ tableMode: false,
763
+ partOfArray: false,
764
+ partOfBlock: false,
765
+ autoFocus: false
766
+ };
767
+
768
+ return (
769
+ <div id={`form_field_${key}`}
770
+ key={`field_${resolvedCollection.name}_${key}`}>
771
+ <ErrorBoundary>
772
+ <Tooltip title={<PropertyIdCopyTooltipContent propertyId={key}/>}
773
+ delayDuration={800}
774
+ side={"left"}
775
+ align={"start"}
776
+ sideOffset={16}>
777
+ <PropertyFieldBinding {...cmsFormFieldProps}/>
778
+ </Tooltip>
779
+ </ErrorBoundary>
780
+ </div>
781
+ );
782
+ })
783
+ .filter(Boolean)}
784
+
785
+ </>
786
+ );
787
+
788
+ const disabled = formex.isSubmitting || (!modified && status === "existing");
789
+ const formRef = React.useRef<HTMLDivElement>(null);
790
+
791
+ const entityActions = getActionsForEntity({
792
+ entity,
793
+ customEntityActions: inputCollection.entityActions
794
+ });
795
+ const formActions = entityActions.filter(a => a.includeInForm === undefined || a.includeInForm);
796
+
797
+ const dialogActions = <DialogActions position={"absolute"}>
798
+
799
+ {savingError &&
800
+ <div className="text-right">
801
+ <Typography color={"error"}>
802
+ {savingError.message}
803
+ </Typography>
804
+ </div>}
805
+
806
+ {entity && formActions.length > 0 && <div className="flex-grow flex overflow-auto no-scrollbar">
807
+ {formActions.map(action => (
808
+ <IconButton
809
+ key={action.name}
810
+ color="primary"
811
+ onClick={(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
812
+ event.stopPropagation();
813
+ if (entity)
814
+ action.onClick({
815
+ entity,
816
+ fullPath: resolvedCollection.path,
817
+ collection: resolvedCollection,
818
+ context,
819
+ sideEntityController
820
+ });
821
+ }}>
822
+ {action.icon}
823
+ </IconButton>
824
+ ))}
825
+ </div>}
826
+ {formex.isSubmitting && <CircularProgress size={"small"}/>}
827
+ <Button
828
+ variant="text"
829
+ disabled={disabled || formex.isSubmitting}
830
+ type="reset">
831
+ {status === "existing" ? "Discard" : "Clear"}
832
+ </Button>
833
+
834
+ <Button
835
+ variant="text"
836
+ color="primary"
837
+ type="submit"
838
+ disabled={disabled || formex.isSubmitting}
839
+ onClick={() => {
840
+ closeAfterSaveRef.current = false;
841
+ }}>
842
+ {status === "existing" && "Save"}
843
+ {status === "copy" && "Create copy"}
844
+ {status === "new" && "Create"}
845
+ </Button>
846
+
847
+ <Button
848
+ variant="filled"
849
+ color="primary"
850
+ type="submit"
851
+ disabled={disabled || formex.isSubmitting}
852
+ onClick={() => {
853
+ closeAfterSaveRef.current = true;
854
+ }}>
855
+ {status === "existing" && "Save and close"}
856
+ {status === "copy" && "Create copy and close"}
857
+ {status === "new" && "Create and close"}
858
+ </Button>
859
+
860
+ </DialogActions>;
861
+
397
862
  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
- />;
863
+
864
+ let form = <div className="h-full overflow-auto">
865
+
866
+ {pluginActions.length > 0 && <div
867
+ 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")}>
868
+ {pluginActions}
869
+ </div>}
870
+
871
+ <div className="pt-12 pb-16 pl-8 pr-8 md:pl-10 md:pr-10">
872
+ <div
873
+ className={`w-full py-2 flex flex-col items-start mt-${4 + (pluginActions ? 8 : 0)} lg:mt-${8 + (pluginActions ? 8 : 0)} mb-8`}>
874
+
875
+ <Typography
876
+ className={"mt-4 flex-grow line-clamp-1 " + inputCollection.hideIdFromForm ? "mb-2" : "mb-0"}
877
+ variant={"h4"}>{title ?? inputCollection.singularName ?? inputCollection.name}
878
+ </Typography>
879
+ <Alert color={"base"} className={"w-full"} size={"small"}>
880
+ <code className={"text-xs select-all"}>{path}/{entityId}</code>
881
+ </Alert>
882
+ </div>
883
+
884
+ {!collection.hideIdFromForm &&
885
+ <CustomIdField customId={inputCollection.customId}
886
+ entityId={entityId}
887
+ status={status}
888
+ onChange={setEntityId}
889
+ error={entityIdError}
890
+ loading={customIdLoading}
891
+ entity={entity}/>}
892
+
893
+ {entityId && formContext && <>
894
+ <div className="mt-12 flex flex-col gap-8"
895
+ ref={formRef}>
896
+
897
+ {formFields}
898
+
899
+ <ErrorFocus containerRef={formRef}/>
900
+
901
+ </div>
902
+
903
+ <div className="h-14"/>
904
+
905
+ </>}
906
+
907
+ </div>
908
+ </div>;
909
+
414
910
  if (plugins) {
415
911
  plugins.forEach((plugin: FireCMSPlugin) => {
416
912
  if (plugin.form?.provider) {
@@ -420,8 +916,6 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
420
916
  path={path}
421
917
  collection={collection}
422
918
  onDiscard={onDiscard}
423
- onValuesChanged={onValuesChanged}
424
- onModified={onModified}
425
919
  entity={usedEntity}
426
920
  context={context}
427
921
  formContext={formContext}
@@ -435,7 +929,7 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
435
929
  return <ErrorBoundary>{form}</ErrorBoundary>;
436
930
  }
437
931
 
438
- const form = (readOnly === undefined)
932
+ const entityView = (readOnly === undefined)
439
933
  ? <></>
440
934
  : (!readOnly
441
935
  ? buildForm()
@@ -450,6 +944,7 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
450
944
  entity={usedEntity as Entity<M>}
451
945
  path={path}
452
946
  collection={collection}/>
947
+
453
948
  </>
454
949
  ));
455
950
 
@@ -474,87 +969,132 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
474
969
  </Tab>
475
970
  );
476
971
 
972
+ useEffect(() => {
973
+ if (entityId && onIdChange)
974
+ onIdChange(entityId);
975
+ }, [entityId, onIdChange]);
976
+
477
977
  return (
478
- <div
479
- className="flex flex-col h-full w-full transition-width duration-250 ease-in-out">
480
- {
481
- <>
978
+ <Formex value={formex}>
979
+
980
+ <div className="flex flex-col h-full w-full transition-width duration-250 ease-in-out">
981
+
982
+ <div
983
+ 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
984
 
483
985
  <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>
986
+ className="pb-1 self-center">
987
+ <IconButton
988
+ onClick={() => {
989
+ onClose?.();
990
+ return sideDialogContext.close(false);
991
+ }}>
992
+ <CloseIcon size={"small"}/>
993
+ </IconButton>
994
+ </div>
497
995
 
498
- <div className={"flex-grow"}/>
996
+ <div className={"flex-grow"}/>
499
997
 
500
- {globalLoading && <div
501
- className="self-center">
502
- <CircularProgress size={"small"}/>
503
- </div>}
998
+ {globalLoading && <div
999
+ className="self-center">
1000
+ <CircularProgress size={"small"}/>
1001
+ </div>}
504
1002
 
505
- <Tabs
506
- value={selectedTabRef.current}
507
- onValueChange={(value) => {
508
- onSideTabClick(value);
509
- }}
510
- className="pl-4 pr-4 pt-0">
1003
+ <Tabs
1004
+ value={selectedTabRef.current}
1005
+ onValueChange={(value) => {
1006
+ onSideTabClick(value);
1007
+ }}
1008
+ className="pl-4 pr-4 pt-0">
511
1009
 
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>
1010
+ <Tab
1011
+ disabled={!hasAdditionalViews}
1012
+ value={MAIN_TAB_VALUE}
1013
+ className={`${
1014
+ !hasAdditionalViews ? "hidden" : ""
1015
+ } text-sm min-w-[140px]`}
1016
+ >{collection.singularName ?? collection.name}</Tab>
519
1017
 
520
- {customViewTabs}
1018
+ {customViewTabs}
521
1019
 
522
- {subcollectionTabs}
523
- </Tabs>
1020
+ {subcollectionTabs}
1021
+ </Tabs>
524
1022
 
525
- </div>
1023
+ </div>
1024
+
1025
+ <form
1026
+ onSubmit={formex.handleSubmit}
1027
+ onReset={() => {
1028
+ formex.resetForm();
1029
+ return onDiscard && onDiscard();
1030
+ }}
1031
+ noValidate
1032
+ className={"flex-grow h-full flex overflow-auto flex-col w-full"}>
526
1033
 
527
1034
  <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}
1035
+ role="tabpanel"
1036
+ hidden={!mainViewVisible}
1037
+ id={`form_${path}`}
1038
+ className={" w-full"}>
546
1039
 
547
- </div>
1040
+ {globalLoading
1041
+ ? <CircularProgressCenter/>
1042
+ : entityView}
1043
+
1044
+ </div>
548
1045
 
549
- {customViewsView}
1046
+ {customViewsView}
550
1047
 
551
- {subCollectionsViews}
1048
+ {subCollectionsViews}
552
1049
 
553
- </div>
1050
+ {shouldShowEntityActions && dialogActions}
554
1051
 
555
- </>
556
- }
1052
+ </form>
557
1053
 
558
- </div>
1054
+ </div>
1055
+ </Formex>
559
1056
  );
560
1057
  }
1058
+
1059
+ function getDataSourceEntityValues<M extends object>(initialResolvedCollection: ResolvedEntityCollection,
1060
+ status: "new" | "existing" | "copy",
1061
+ entity: Entity<M> | undefined): Partial<EntityValues<M>> {
1062
+
1063
+ const properties = initialResolvedCollection.properties;
1064
+ if ((status === "existing" || status === "copy") && entity) {
1065
+ return entity.values ?? getDefaultValuesFor(properties);
1066
+ } else if (status === "new") {
1067
+ return getDefaultValuesFor(properties);
1068
+ } else {
1069
+ console.error({
1070
+ status,
1071
+ entity
1072
+ });
1073
+ throw new Error("Form has not been initialised with the correct parameters");
1074
+ }
1075
+ }
1076
+
1077
+ export type EntityFormSaveParams<M extends Record<string, any>> = {
1078
+ collection: ResolvedEntityCollection<M>,
1079
+ path: string,
1080
+ entityId: string | undefined,
1081
+ values: EntityValues<M>,
1082
+ previousValues?: EntityValues<M>,
1083
+ closeAfterSave: boolean,
1084
+ autoSave: boolean
1085
+ };
1086
+
1087
+ export function yupToFormErrors(yupError: ValidationError): Record<string, any> {
1088
+ let errors: Record<string, any> = {};
1089
+ if (yupError.inner) {
1090
+ if (yupError.inner.length === 0) {
1091
+ return setIn(errors, yupError.path!, yupError.message);
1092
+ }
1093
+ for (const err of yupError.inner) {
1094
+ if (!getIn(errors, err.path!)) {
1095
+ errors = setIn(errors, err.path!, err.message);
1096
+ }
1097
+ }
1098
+ }
1099
+ return errors;
1100
+ }