@firecms/core 3.0.0-canary.190 → 3.0.0-canary.192

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.
@@ -2,29 +2,23 @@ import React, { useCallback, useDeferredValue, useEffect, useMemo, useRef, useSt
2
2
  import {
3
3
  CMSAnalyticsEvent,
4
4
  Entity,
5
- EntityAction,
6
5
  EntityCollection,
7
6
  EntityStatus,
8
7
  EntityValues,
9
- FireCMSContext,
10
8
  FormContext,
11
9
  PluginFormActionProps,
12
10
  PropertyFieldBindingProps,
13
- ResolvedEntityCollection,
14
- SideEntityController
11
+ ResolvedEntityCollection
15
12
  } from "../types";
16
13
  import equal from "react-fast-compare";
17
14
 
18
- import { copyEntityAction, deleteEntityAction, ErrorBoundary, getFormFieldKeys } from "../components";
15
+ import { ErrorBoundary, getFormFieldKeys } from "../components";
19
16
  import {
20
- canCreateEntity,
21
- canDeleteEntity,
22
17
  getDefaultValuesFor,
23
18
  getEntityTitlePropertyKey,
24
19
  getValueInPath,
25
20
  isHidden,
26
21
  isReadOnly,
27
- mergeEntityActions,
28
22
  resolveCollection,
29
23
  useDebouncedCallback
30
24
  } from "../util";
@@ -34,26 +28,11 @@ import {
34
28
  useAuthController,
35
29
  useCustomizationController,
36
30
  useDataSource,
37
- useFireCMSContext,
31
+ useFireCMSContext, useSideDialogsController,
38
32
  useSideEntityController,
39
33
  useSnackbarController
40
34
  } from "../hooks";
41
- import {
42
- Alert,
43
- Button,
44
- CheckIcon,
45
- Chip,
46
- cls,
47
- defaultBorderMixin,
48
- DialogActions,
49
- EditIcon,
50
- IconButton,
51
- LoadingButton,
52
- NotesIcon,
53
- paperMixin,
54
- Tooltip,
55
- Typography
56
- } from "@firecms/ui";
35
+ import { Alert, CheckIcon, Chip, cls, EditIcon, NotesIcon, paperMixin, Tooltip, Typography } from "@firecms/ui";
57
36
  import { Formex, FormexController, getIn, setIn, useCreateFormex } from "@firecms/formex";
58
37
  import { useAnalyticsController } from "../hooks/useAnalyticsController";
59
38
  import { FormEntry, FormLayout, LabelWithIconAndTooltip, PropertyFieldBinding } from "../form";
@@ -62,6 +41,8 @@ import { removeEntityFromCache, saveEntityToCache } from "../util/entity_cache";
62
41
  import { CustomIdField } from "../form/components/CustomIdField";
63
42
  import { ErrorFocus } from "../form/components/ErrorFocus";
64
43
  import { CustomFieldValidator, getYupEntitySchema } from "../form/validation";
44
+ import { EntityFormActions, EntityFormActionsProps } from "./EntityFormActions";
45
+ import { useSideDialogContext } from "../core";
65
46
 
66
47
  export type OnUpdateParams = {
67
48
  entity: Entity<any>,
@@ -81,16 +62,16 @@ type EntityFormProps<M extends Record<string, any>> = {
81
62
  onIdChange?: (id: string) => void;
82
63
  onValuesModified?: (modified: boolean) => void;
83
64
  onSaved?: (params: OnUpdateParams) => void;
84
- onClose?: () => void;
85
65
  cachedDirtyValues?: Partial<M>; // dirty cached entity in memory
86
66
  onFormContextReady?: (formContext: FormContext) => void;
87
67
  forceActionsAtTheBottom?: boolean;
88
- initialStatus: EntityStatus;
89
68
  className?: string;
69
+ initialStatus: EntityStatus;
90
70
  onStatusChange?: (status: EntityStatus) => void;
91
71
  onEntityChange?: (entity: Entity<M>) => void;
92
72
  formex?: FormexController<M>;
93
73
  openEntityMode?: "side_panel" | "full_screen";
74
+ EntityFormActionsComponent?: React.FC<EntityFormActionsProps>;
94
75
  };
95
76
 
96
77
  export function EntityForm<M extends Record<string, any>>({
@@ -100,7 +81,6 @@ export function EntityForm<M extends Record<string, any>>({
100
81
  onValuesModified,
101
82
  onIdChange,
102
83
  onSaved,
103
- onClose,
104
84
  entity,
105
85
  cachedDirtyValues,
106
86
  onFormContextReady,
@@ -110,7 +90,8 @@ export function EntityForm<M extends Record<string, any>>({
110
90
  onStatusChange,
111
91
  onEntityChange,
112
92
  openEntityMode = "full_screen",
113
- formex: formexProp
93
+ formex: formexProp,
94
+ EntityFormActionsComponent = EntityFormActions
114
95
  }: EntityFormProps<M>) {
115
96
 
116
97
  if (collection.customId && collection.formAutoSave) {
@@ -133,17 +114,13 @@ export function EntityForm<M extends Record<string, any>>({
133
114
  collection,
134
115
  path,
135
116
  values: valuesToBeSaved,
136
- closeAfterSave: false
137
117
  });
138
118
  }, false, 2000);
139
119
 
140
- const authController = useAuthController();
141
120
  const dataSource = useDataSource(collection);
142
- const sideEntityController = useSideEntityController();
143
121
  const snackbarController = useSnackbarController();
144
122
  const customizationController = useCustomizationController();
145
123
  const context = useFireCMSContext();
146
- const closeAfterSaveRef = useRef(false);
147
124
  const analyticsController = useAnalyticsController();
148
125
 
149
126
  const [underlyingChanges, setUnderlyingChanges] = useState<Partial<EntityValues<M>>>({});
@@ -262,7 +239,7 @@ export function EntityForm<M extends Record<string, any>>({
262
239
  }
263
240
  }
264
241
 
265
- const onSaveSuccess = (updatedEntity: Entity<M>, closeAfterSave: boolean) => {
242
+ const onSaveSuccess = (updatedEntity: Entity<M>) => {
266
243
 
267
244
  clearDirtyCache();
268
245
  onValuesModified?.(false);
@@ -285,10 +262,6 @@ export function EntityForm<M extends Record<string, any>>({
285
262
  collection
286
263
  });
287
264
  }
288
-
289
- if (closeAfterSave) {
290
- onClose?.();
291
- }
292
265
  };
293
266
 
294
267
  const onSaveFailure = useCallback((e: Error) => {
@@ -304,7 +277,6 @@ export function EntityForm<M extends Record<string, any>>({
304
277
  const saveEntity = ({
305
278
  values,
306
279
  previousValues,
307
- closeAfterSave,
308
280
  entityId,
309
281
  collection,
310
282
  path
@@ -314,7 +286,6 @@ export function EntityForm<M extends Record<string, any>>({
314
286
  entityId: string | undefined,
315
287
  values: M,
316
288
  previousValues?: M,
317
- closeAfterSave: boolean,
318
289
  }) => {
319
290
  setSaving(true);
320
291
  return saveEntityWithCallbacks({
@@ -326,7 +297,7 @@ export function EntityForm<M extends Record<string, any>>({
326
297
  status,
327
298
  dataSource,
328
299
  context,
329
- onSaveSuccess: (updatedEntity: Entity<M>) => onSaveSuccess(updatedEntity, closeAfterSave),
300
+ onSaveSuccess: (updatedEntity: Entity<M>) => onSaveSuccess(updatedEntity),
330
301
  onSaveFailure,
331
302
  onPreSaveHookError,
332
303
  onSaveSuccessHookError
@@ -339,7 +310,6 @@ export function EntityForm<M extends Record<string, any>>({
339
310
  entityId: string | undefined,
340
311
  values: EntityValues<M>,
341
312
  previousValues?: EntityValues<M>,
342
- closeAfterSave: boolean,
343
313
  autoSave: boolean
344
314
  };
345
315
 
@@ -349,7 +319,6 @@ export function EntityForm<M extends Record<string, any>>({
349
319
  entityId,
350
320
  values,
351
321
  previousValues,
352
- closeAfterSave,
353
322
  autoSave
354
323
  }: EntityFormSaveParams<M>): Promise<void> => {
355
324
  if (!status)
@@ -363,7 +332,6 @@ export function EntityForm<M extends Record<string, any>>({
363
332
  entityId,
364
333
  values,
365
334
  previousValues,
366
- closeAfterSave
367
335
  });
368
336
  }
369
337
  };
@@ -377,9 +345,8 @@ export function EntityForm<M extends Record<string, any>>({
377
345
  entityId,
378
346
  values,
379
347
  previousValues: entity?.values,
380
- closeAfterSave: closeAfterSaveRef.current,
381
348
  autoSave: autoSave ?? false
382
- }).then(_ => {
349
+ }).then((res) => {
383
350
  const eventName: CMSAnalyticsEvent = status === "new"
384
351
  ? "new_entity_saved"
385
352
  : (status === "copy" ? "entity_copied" : (status === "existing" ? "entity_edited" : "unmapped_event"));
@@ -387,8 +354,6 @@ export function EntityForm<M extends Record<string, any>>({
387
354
  }).catch(e => {
388
355
  console.error(e);
389
356
  setSavingError(e);
390
- }).finally(() => {
391
- closeAfterSaveRef.current = false;
392
357
  });
393
358
  };
394
359
 
@@ -404,10 +369,7 @@ export function EntityForm<M extends Record<string, any>>({
404
369
  entity,
405
370
  savingError,
406
371
  status,
407
- openEntityMode,
408
- setPendingClose: (value: boolean) => {
409
- closeAfterSaveRef.current = value;
410
- }
372
+ openEntityMode
411
373
  };
412
374
 
413
375
  useEffect(() => {
@@ -645,6 +607,7 @@ export function EntityForm<M extends Record<string, any>>({
645
607
  id={`form_${path}`}
646
608
  pluginActions={pluginActions}
647
609
  forceActionsAtTheBottom={forceActionsAtTheBottom}
610
+ EntityFormActionsComponent={EntityFormActionsComponent}
648
611
  formContext={formContext}>
649
612
  {formView}
650
613
  </FormLayoutInner>
@@ -691,26 +654,24 @@ export function yupToFormErrors(yupError: ValidationError): Record<string, any>
691
654
  return errors;
692
655
  }
693
656
 
694
- export function FormLayoutInner<M extends object>({
695
- id,
696
- formContext,
697
- children,
698
- className,
699
- forceActionsAtTheBottom,
700
- pluginActions
701
- }: {
657
+ export function FormLayoutInner({
658
+ id,
659
+ formContext,
660
+ children,
661
+ className,
662
+ forceActionsAtTheBottom,
663
+ pluginActions,
664
+ EntityFormActionsComponent
665
+ }: {
702
666
  id?: string,
703
667
  formContext: FormContext,
704
668
  children: React.ReactNode,
705
669
  className?: string,
706
670
  forceActionsAtTheBottom?: boolean,
707
671
  pluginActions?: React.ReactNode[],
672
+ EntityFormActionsComponent: React.FC<EntityFormActionsProps>;
708
673
  }) {
709
674
 
710
- const context = useFireCMSContext();
711
- const sideEntityController = useSideEntityController();
712
- const authController = useAuthController();
713
-
714
675
  const formex = formContext.formex;
715
676
  const collection = formContext.collection;
716
677
  const path = formContext.path;
@@ -718,67 +679,24 @@ export function FormLayoutInner<M extends object>({
718
679
  const savingError = formContext.savingError;
719
680
  const status = formContext.status;
720
681
  const openEntityMode = formContext.openEntityMode;
721
- const setPendingClose = formContext.setPendingClose;
722
682
  const disabled = formex.isSubmitting || (!formex.dirty && status === "existing");
723
683
 
724
684
  if (!collection || !path) {
725
685
  throw Error("INTERNAL: Collection and path must be defined in form context");
726
686
  }
727
687
 
728
- const getActionsForEntity = useCallback(({
729
- entity,
730
- customEntityActions
731
- }: {
732
- entity?: Entity<M>,
733
- customEntityActions?: EntityAction[]
734
- }): EntityAction[] => {
735
-
736
- const createEnabled = canCreateEntity(collection, authController, path, null);
737
- const deleteEnabled = entity ? canDeleteEntity(collection, authController, path, entity) : false;
738
- const actions: EntityAction[] = [];
739
- if (createEnabled)
740
- actions.push(copyEntityAction);
741
- if (deleteEnabled)
742
- actions.push(deleteEntityAction);
743
- if (customEntityActions)
744
- return mergeEntityActions(actions, customEntityActions);
745
- return actions;
746
- }, [authController, collection, path]);
747
-
748
- const entityActions = getActionsForEntity({
749
- entity,
750
- customEntityActions: collection.entityActions
751
- });
752
- const formActions = entityActions.filter(a => a.includeInForm === undefined || a.includeInForm);
753
-
754
- const dialogActions = forceActionsAtTheBottom
755
- ? buildBottomActions({
756
- savingError,
757
- entity,
758
- formActions,
759
- collection,
760
- context,
761
- sideEntityController,
762
- isSubmitting: formex.isSubmitting,
763
- disabled,
764
- status,
765
- setPendingClose,
766
- pluginActions,
767
- openEntityMode
768
- })
769
- : buildSideActions({
770
- savingError,
771
- entity,
772
- formActions,
773
- collection,
774
- context,
775
- sideEntityController,
776
- isSubmitting: formex.isSubmitting,
777
- disabled,
778
- status,
779
- pluginActions,
780
- openEntityMode
781
- });
688
+ const dialogActions = <EntityFormActionsComponent
689
+ collection={collection}
690
+ path={path}
691
+ entity={entity}
692
+ layout={forceActionsAtTheBottom ? "bottom" : "side"}
693
+ savingError={savingError}
694
+ formex={formex}
695
+ disabled={disabled}
696
+ status={status}
697
+ pluginActions={pluginActions ?? []}
698
+ openEntityMode={openEntityMode}
699
+ />;
782
700
 
783
701
  return (
784
702
  <Formex value={formContext.formex}>
@@ -820,130 +738,6 @@ export function FormLayoutInner<M extends object>({
820
738
  );
821
739
  }
822
740
 
823
- type ActionsViewProps<M extends object> = {
824
- savingError: Error | undefined,
825
- entity: Entity<M> | undefined,
826
- formActions: EntityAction[],
827
- collection: ResolvedEntityCollection,
828
- context: FireCMSContext,
829
- sideEntityController: SideEntityController,
830
- isSubmitting: boolean,
831
- disabled: boolean,
832
- status: "new" | "existing" | "copy",
833
- setPendingClose?: (value: boolean) => void,
834
- pluginActions?: React.ReactNode[],
835
- openEntityMode: "side_panel" | "full_screen";
836
- };
837
-
838
- function buildBottomActions<M extends object>({
839
- savingError,
840
- entity,
841
- formActions,
842
- collection,
843
- context,
844
- sideEntityController,
845
- isSubmitting,
846
- disabled,
847
- status,
848
- setPendingClose,
849
- pluginActions,
850
- openEntityMode
851
- }: ActionsViewProps<M>) {
852
-
853
- const canClose = openEntityMode === "side_panel";
854
- return <DialogActions position={"absolute"}>
855
- {savingError &&
856
- <div className="text-right">
857
- <Typography color={"error"}>{savingError.message}</Typography>
858
- </div>
859
- }
860
- {entity && formActions.length > 0 && <div className="flex-grow flex overflow-auto no-scrollbar">
861
- {formActions.map(action => (
862
- <IconButton
863
- key={action.name}
864
- color="primary"
865
- onClick={(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
866
- event.stopPropagation();
867
- if (entity)
868
- action.onClick({
869
- entity,
870
- fullPath: collection.path,
871
- collection: collection,
872
- context,
873
- sideEntityController,
874
- openEntityMode: openEntityMode
875
- });
876
- }}>
877
- {action.icon}
878
- </IconButton>
879
- ))}
880
- </div>}
881
- {pluginActions}
882
- <Button variant="text" disabled={disabled || isSubmitting} type="reset">
883
- {status === "existing" ? "Discard" : "Clear"}
884
- </Button>
885
- <Button variant={canClose ? "text" : "filled"} color="primary" type="submit"
886
- disabled={disabled || isSubmitting}
887
- onClick={() => {
888
- setPendingClose?.(false);
889
- }}>
890
- {status === "existing" && "Save"}
891
- {status === "copy" && "Create copy"}
892
- {status === "new" && "Create"}
893
- </Button>
894
- {canClose && <LoadingButton variant="filled"
895
- color="primary"
896
- type="submit"
897
- loading={isSubmitting}
898
- disabled={disabled}
899
- onClick={() => {
900
- setPendingClose?.(true);
901
- }}>
902
- {status === "existing" && "Save and close"}
903
- {status === "copy" && "Create copy and close"}
904
- {status === "new" && "Create and close"}
905
- </LoadingButton>}
906
- </DialogActions>;
907
- }
908
-
909
- function buildSideActions<M extends object>({
910
- savingError,
911
- entity,
912
- formActions,
913
- collection,
914
- context,
915
- sideEntityController,
916
- isSubmitting,
917
- disabled,
918
- status,
919
- setPendingClose,
920
- pluginActions
921
- }: ActionsViewProps<M>) {
922
-
923
- return <div
924
- className={cls("overflow-auto h-full flex flex-col gap-2 w-80 2xl:w-96 px-4 py-16 sticky top-0 border-l", defaultBorderMixin)}>
925
- <LoadingButton fullWidth={true} variant="filled" color="primary" type="submit" size={"large"}
926
- disabled={disabled || isSubmitting} onClick={() => {
927
- setPendingClose?.(false);
928
- }}>
929
- {status === "existing" && "Save"}
930
- {status === "copy" && "Create copy"}
931
- {status === "new" && "Create"}
932
- </LoadingButton>
933
- <Button fullWidth={true} variant="text" disabled={disabled || isSubmitting} type="reset">
934
- {status === "existing" ? "Discard" : "Clear"}
935
- </Button>
936
-
937
- {pluginActions}
938
-
939
- {savingError &&
940
- <div className="text-right">
941
- <Typography color={"error"}>{savingError.message}</Typography>
942
- </div>
943
- }
944
- </div>;
945
- }
946
-
947
741
  function useOnAutoSave(autoSave: undefined | boolean, formex: FormexController<any>, lastSavedValues: any, save: (values: EntityValues<any>) => Promise<void>) {
948
742
  if (!autoSave) return;
949
743
  useEffect(() => {
@@ -0,0 +1,168 @@
1
+ import React from "react";
2
+ import { Entity, EntityAction, FireCMSContext, ResolvedEntityCollection, SideEntityController } from "../types";
3
+ import { Button, cls, defaultBorderMixin, DialogActions, IconButton, LoadingButton, Typography } from "@firecms/ui";
4
+ import { FormexController } from "@firecms/formex";
5
+ import { useFireCMSContext, useSideEntityController } from "../hooks";
6
+
7
+ export interface EntityFormActionsProps {
8
+ collection: ResolvedEntityCollection;
9
+ path: string;
10
+ entity?: Entity;
11
+ layout: "bottom" | "side";
12
+ savingError?: Error;
13
+ formex: FormexController<any>;
14
+ disabled: boolean;
15
+ status: "new" | "existing" | "copy";
16
+ pluginActions: React.ReactNode[];
17
+ openEntityMode: "side_panel" | "full_screen";
18
+ }
19
+
20
+ export function EntityFormActions({
21
+ collection,
22
+ entity,
23
+ layout,
24
+ savingError,
25
+ formex,
26
+ disabled,
27
+ status,
28
+ pluginActions,
29
+ openEntityMode
30
+ }: EntityFormActionsProps) {
31
+
32
+ const context = useFireCMSContext();
33
+ const sideEntityController = useSideEntityController();
34
+
35
+ return layout === "bottom"
36
+ ? buildBottomActions({
37
+ savingError,
38
+ entity,
39
+ collection,
40
+ context,
41
+ sideEntityController,
42
+ isSubmitting: formex.isSubmitting,
43
+ disabled,
44
+ status,
45
+ pluginActions,
46
+ openEntityMode
47
+ })
48
+ : buildSideActions({
49
+ savingError,
50
+ entity,
51
+ collection,
52
+ context,
53
+ sideEntityController,
54
+ isSubmitting: formex.isSubmitting,
55
+ disabled,
56
+ status,
57
+ pluginActions,
58
+ openEntityMode
59
+ });
60
+ }
61
+
62
+ type ActionsViewProps<M extends object> = {
63
+ savingError: Error | undefined,
64
+ entity: Entity<M> | undefined,
65
+ formActions?: EntityAction[],
66
+ collection: ResolvedEntityCollection,
67
+ context: FireCMSContext,
68
+ sideEntityController: SideEntityController,
69
+ isSubmitting: boolean,
70
+ disabled: boolean,
71
+ status: "new" | "existing" | "copy",
72
+ pluginActions?: React.ReactNode[],
73
+ openEntityMode: "side_panel" | "full_screen";
74
+ };
75
+
76
+ function buildBottomActions<M extends object>({
77
+ savingError,
78
+ entity,
79
+ formActions,
80
+ collection,
81
+ context,
82
+ sideEntityController,
83
+ isSubmitting,
84
+ disabled,
85
+ status,
86
+ pluginActions,
87
+ openEntityMode
88
+ }: ActionsViewProps<M>) {
89
+
90
+ return <DialogActions position={"absolute"}>
91
+ {savingError &&
92
+ <div className="text-right">
93
+ <Typography color={"error"}>{savingError.message}</Typography>
94
+ </div>
95
+ }
96
+ {entity && (formActions ?? []).length > 0 && <div className="flex-grow flex overflow-auto no-scrollbar">
97
+ {(formActions ?? []).map(action => (
98
+ <IconButton
99
+ key={action.name}
100
+ color="primary"
101
+ onClick={(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
102
+ event.stopPropagation();
103
+ if (entity)
104
+ action.onClick({
105
+ entity,
106
+ fullPath: collection.path,
107
+ collection: collection,
108
+ context,
109
+ sideEntityController,
110
+ openEntityMode: openEntityMode
111
+ });
112
+ }}>
113
+ {action.icon}
114
+ </IconButton>
115
+ ))}
116
+ </div>}
117
+ {pluginActions}
118
+ <Button variant="text" disabled={disabled || isSubmitting} type="reset">
119
+ {status === "existing" ? "Discard" : "Clear"}
120
+ </Button>
121
+ <Button variant={"filled"} color="primary" type="submit"
122
+ disabled={disabled || isSubmitting}>
123
+ {status === "existing" && "Save"}
124
+ {status === "copy" && "Create copy"}
125
+ {status === "new" && "Create"}
126
+ </Button>
127
+
128
+ </DialogActions>;
129
+ }
130
+
131
+ function buildSideActions<M extends object>({
132
+ savingError,
133
+ entity,
134
+ formActions,
135
+ collection,
136
+ context,
137
+ sideEntityController,
138
+ isSubmitting,
139
+ disabled,
140
+ status,
141
+ pluginActions
142
+ }: ActionsViewProps<M>) {
143
+
144
+ return <div
145
+ className={cls("overflow-auto h-full flex flex-col gap-2 w-80 2xl:w-96 px-4 py-16 sticky top-0 border-l", defaultBorderMixin)}>
146
+ <LoadingButton fullWidth={true}
147
+ variant="filled"
148
+ color="primary"
149
+ type="submit"
150
+ size={"large"}
151
+ disabled={disabled || isSubmitting}>
152
+ {status === "existing" && "Save"}
153
+ {status === "copy" && "Create copy"}
154
+ {status === "new" && "Create"}
155
+ </LoadingButton>
156
+ <Button fullWidth={true} variant="text" disabled={disabled || isSubmitting} type="reset">
157
+ {status === "existing" ? "Discard" : "Clear"}
158
+ </Button>
159
+
160
+ {pluginActions}
161
+
162
+ {savingError &&
163
+ <div className="text-right">
164
+ <Typography color={"error"}>{savingError.message}</Typography>
165
+ </div>
166
+ }
167
+ </div>;
168
+ }