@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.
@@ -137,12 +137,6 @@ export interface FormContext<M extends Record<string, any> = any> {
137
137
  entity?: Entity<M>;
138
138
  savingError?: Error;
139
139
  openEntityMode: "side_panel" | "full_screen";
140
- /**
141
- * If pending close is set to true, the form will close when the user
142
- * saves the entity
143
- * @param pendingClose
144
- */
145
- setPendingClose?: (pendingClose: boolean) => void;
146
140
  /**
147
141
  * This is the underlying formex controller that powers the form
148
142
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@firecms/core",
3
3
  "type": "module",
4
- "version": "3.0.0-canary.190",
4
+ "version": "3.0.0-canary.192",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/firecmsco"
@@ -50,9 +50,9 @@
50
50
  "./package.json": "./package.json"
51
51
  },
52
52
  "dependencies": {
53
- "@firecms/editor": "^3.0.0-canary.190",
54
- "@firecms/formex": "^3.0.0-canary.190",
55
- "@firecms/ui": "^3.0.0-canary.190",
53
+ "@firecms/editor": "^3.0.0-canary.192",
54
+ "@firecms/formex": "^3.0.0-canary.192",
55
+ "@firecms/ui": "^3.0.0-canary.192",
56
56
  "@hello-pangea/dnd": "^17.0.0",
57
57
  "@radix-ui/react-portal": "^1.1.3",
58
58
  "clsx": "^2.1.1",
@@ -104,7 +104,7 @@
104
104
  "dist",
105
105
  "src"
106
106
  ],
107
- "gitHead": "54a510a679c4c6d63949005b879f915b23fb94b7",
107
+ "gitHead": "ec1250125ee49075f94237f0e34c82cb6d624c1c",
108
108
  "publishConfig": {
109
109
  "access": "public"
110
110
  },
@@ -20,10 +20,9 @@ import { useWindowSize } from "./useWindowSize";
20
20
  import { ElementResizeListener } from "./ElementResizeListener";
21
21
  import { getPropertyInPath, isReadOnly, resolveCollection } from "../../../../util";
22
22
  import { Button, CloseIcon, DialogActions, IconButton, Typography } from "@firecms/ui";
23
- import { PropertyFieldBinding } from "../../../../form";
23
+ import { PropertyFieldBinding, yupToFormErrors } from "../../../../form";
24
24
  import { useCustomizationController, useDataSource, useFireCMSContext } from "../../../../hooks";
25
25
  import { OnCellValueChangeParams } from "../../../common";
26
- import { yupToFormErrors } from "../../../../core/EntityForm";
27
26
 
28
27
  interface PopupFormFieldProps<M extends Record<string, any>> {
29
28
  customFieldValidator?: CustomFieldValidator;
@@ -240,14 +239,7 @@ export function PopupFormFieldInternal<M extends Record<string, any>>({
240
239
  validation: (values) => {
241
240
  return validationSchema?.validate(values, { abortEarly: false })
242
241
  .then(() => ({}))
243
- .catch((e) => {
244
- // const errors: Record<string, string> = {};
245
- // e.inner.forEach((error: any) => {
246
- // errors[error.path] = error.message;
247
- // });
248
- // return errors;
249
- return yupToFormErrors(e);
250
- });
242
+ .catch(yupToFormErrors);
251
243
  },
252
244
  validateOnInitialRender: true,
253
245
  onSubmit: (values, actions) => {
@@ -1,88 +1,20 @@
1
- import React, { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
2
- import {
3
- CMSAnalyticsEvent,
4
- Entity,
5
- EntityAction,
6
- EntityCollection,
7
- EntityCustomView,
8
- EntityStatus,
9
- EntityValues,
10
- FireCMSContext,
11
- FireCMSPlugin,
12
- FormContext,
13
- PluginFormActionProps,
14
- PropertyFieldBindingProps,
15
- ResolvedEntityCollection,
16
- SideEntityController,
17
- User
18
- } from "../types";
19
- import equal from "react-fast-compare";
1
+ import React, { useEffect, useMemo, useState } from "react";
2
+ import { Entity, EntityCollection, EntityCustomView, EntityStatus, FireCMSPlugin, FormContext, User } from "../types";
20
3
 
21
- import {
22
- CircularProgressCenter,
23
- copyEntityAction,
24
- deleteEntityAction,
25
- EntityCollectionView,
26
- EntityView,
27
- ErrorBoundary,
28
- getFormFieldKeys
29
- } from "../components";
30
- import {
31
- canCreateEntity,
32
- canDeleteEntity,
33
- canEditEntity,
34
- getDefaultValuesFor,
35
- getEntityTitlePropertyKey,
36
- getValueInPath,
37
- isHidden,
38
- isReadOnly,
39
- mergeEntityActions,
40
- removeInitialAndTrailingSlashes,
41
- resolveCollection,
42
- resolveDefaultSelectedView,
43
- resolveEntityView,
44
- useDebouncedCallback
45
- } from "../util";
4
+ import { CircularProgressCenter, EntityCollectionView, EntityView, ErrorBoundary } from "../components";
5
+ import { canEditEntity, removeInitialAndTrailingSlashes, resolveDefaultSelectedView, resolveEntityView } from "../util";
46
6
 
47
7
  import {
48
- saveEntityWithCallbacks,
49
8
  useAuthController,
50
9
  useCustomizationController,
51
- useDataSource,
52
10
  useEntityFetch,
53
11
  useFireCMSContext,
54
- useLargeLayout,
55
- useSideEntityController,
56
- useSnackbarController
12
+ useLargeLayout
57
13
  } from "../hooks";
58
- import {
59
- Alert,
60
- Button,
61
- CheckIcon,
62
- Chip,
63
- CircularProgress,
64
- cls,
65
- defaultBorderMixin,
66
- DialogActions,
67
- EditIcon,
68
- IconButton,
69
- LoadingButton,
70
- NotesIcon,
71
- paperMixin,
72
- Tab,
73
- Tabs,
74
- Tooltip,
75
- Typography
76
- } from "@firecms/ui";
77
- import { Formex, FormexController, getIn, setIn, useCreateFormex } from "@firecms/formex";
78
- import { useAnalyticsController } from "../hooks/useAnalyticsController";
79
- import { FormEntry, FormLayout, LabelWithIconAndTooltip, PropertyFieldBinding } from "../form";
80
- import { ValidationError } from "yup";
81
- import { getEntityFromCache, removeEntityFromCache, saveEntityToCache } from "../util/entity_cache";
82
- import { CustomIdField } from "../form/components/CustomIdField";
83
- import { ErrorFocus } from "../form/components/ErrorFocus";
84
- import { CustomFieldValidator, getYupEntitySchema } from "../form/validation";
85
- import { EntityForm, FormLayoutInner } from "./EntityForm";
14
+ import { CircularProgress, cls, defaultBorderMixin, Tab, Tabs, Typography } from "@firecms/ui";
15
+ import { getEntityFromCache } from "../util/entity_cache";
16
+ import { EntityForm, FormLayoutInner } from "../form";
17
+ import { EntityEditViewFormActions } from "./EntityEditViewFormActions";
86
18
 
87
19
  const MAIN_TAB_VALUE = "main_##Q$SC^#S6";
88
20
 
@@ -112,7 +44,6 @@ export interface EntityEditViewProps<M extends Record<string, any>> {
112
44
  parentCollectionIds: string[];
113
45
  onValuesModified?: (modified: boolean) => void;
114
46
  onSaved?: (params: OnUpdateParams) => void;
115
- onClose?: () => void;
116
47
  onTabChange?: (props: OnTabChangeParams<M>) => void;
117
48
  layout?: "side_panel" | "full_screen";
118
49
  barActions?: React.ReactNode;
@@ -188,21 +119,20 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
188
119
  />;
189
120
  }
190
121
 
191
-
192
122
  function SecondaryForm<M extends object>({
193
123
  collection,
194
124
  className,
195
125
  customView,
196
126
  entity,
197
127
  formContext,
198
- forceActionsAtTheBottom
128
+ forceActionsAtTheBottom,
199
129
  }: {
200
130
  className?: string,
201
131
  customView: EntityCustomView,
202
132
  formContext: FormContext<M>,
203
133
  collection: EntityCollection<M>,
204
134
  forceActionsAtTheBottom?: boolean,
205
- entity: Entity<M> | undefined
135
+ entity: Entity<M> | undefined,
206
136
  }) {
207
137
 
208
138
  if (!customView.Builder) {
@@ -213,7 +143,8 @@ function SecondaryForm<M extends object>({
213
143
  return <FormLayoutInner
214
144
  className={className}
215
145
  forceActionsAtTheBottom={forceActionsAtTheBottom}
216
- formContext={formContext}>
146
+ formContext={formContext}
147
+ EntityFormActionsComponent={EntityEditViewFormActions}>
217
148
  <ErrorBoundary>
218
149
  {formContext && <customView.Builder
219
150
  collection={collection}
@@ -229,12 +160,10 @@ export function EntityEditViewInner<M extends Record<string, any>>({
229
160
  path,
230
161
  entityId,
231
162
  selectedTab: selectedTabProp,
232
- copy,
233
163
  collection,
234
164
  parentCollectionIds,
235
165
  onValuesModified,
236
166
  onSaved,
237
- onClose,
238
167
  onTabChange,
239
168
  entity,
240
169
  cachedDirtyValues,
@@ -406,7 +335,6 @@ export function EntityEditViewInner<M extends Record<string, any>>({
406
335
  ...params,
407
336
  selectedTab: MAIN_TAB_VALUE === selectedTab ? undefined : selectedTab,
408
337
  }) : undefined}
409
- onClose={onClose}
410
338
  entity={entity}
411
339
  cachedDirtyValues={cachedDirtyValues}
412
340
  openEntityMode={layout}
@@ -415,6 +343,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
415
343
  initialStatus={status}
416
344
  onStatusChange={setStatus}
417
345
  className={!mainViewVisible ? "hidden" : ""}
346
+ EntityFormActionsComponent={EntityEditViewFormActions}
418
347
  onEntityChange={setUsedEntity}
419
348
  />;
420
349
 
@@ -0,0 +1,198 @@
1
+ import React, { useMemo } from "react";
2
+ import { Entity, EntityAction, FireCMSContext, ResolvedEntityCollection, SideEntityController } from "../types";
3
+
4
+ import { copyEntityAction, deleteEntityAction } from "../components";
5
+ import { canCreateEntity, canDeleteEntity, mergeEntityActions } from "../util";
6
+ import { Button, cls, defaultBorderMixin, DialogActions, IconButton, LoadingButton, Typography } from "@firecms/ui";
7
+ import { useAuthController, useFireCMSContext, useSideEntityController } from "../hooks";
8
+ import { EntityFormActionsProps } from "../form/EntityFormActions";
9
+ import { SideDialogController, useSideDialogContext } from "./SideDialogs";
10
+
11
+ export function EntityEditViewFormActions({
12
+ collection,
13
+ path,
14
+ entity,
15
+ layout,
16
+ savingError,
17
+ formex,
18
+ disabled,
19
+ status,
20
+ pluginActions,
21
+ openEntityMode
22
+ }: EntityFormActionsProps) {
23
+
24
+ const authController = useAuthController();
25
+ const context = useFireCMSContext();
26
+ const sideEntityController = useSideEntityController();
27
+ const sideDialogContext = useSideDialogContext();
28
+
29
+ const entityActions = useMemo((): EntityAction[] => {
30
+ const customEntityActions = collection.entityActions
31
+ const createEnabled = canCreateEntity(collection, authController, path, null);
32
+ const deleteEnabled = entity ? canDeleteEntity(collection, authController, path, entity) : false;
33
+ const actions: EntityAction[] = [];
34
+ if (createEnabled)
35
+ actions.push(copyEntityAction);
36
+ if (deleteEnabled)
37
+ actions.push(deleteEntityAction);
38
+ if (customEntityActions)
39
+ return mergeEntityActions(actions, customEntityActions);
40
+ return actions;
41
+ }, [authController, collection, path]);
42
+
43
+ const formActions = entityActions.filter(a => a.includeInForm === undefined || a.includeInForm);
44
+
45
+ return layout === "bottom"
46
+ ? buildBottomActions({
47
+ savingError,
48
+ entity,
49
+ formActions,
50
+ collection,
51
+ context,
52
+ sideEntityController,
53
+ isSubmitting: formex.isSubmitting,
54
+ disabled,
55
+ status,
56
+ sideDialogContext,
57
+ pluginActions,
58
+ openEntityMode
59
+ })
60
+ : buildSideActions({
61
+ savingError,
62
+ entity,
63
+ formActions,
64
+ collection,
65
+ context,
66
+ sideEntityController,
67
+ isSubmitting: formex.isSubmitting,
68
+ sideDialogContext,
69
+ disabled,
70
+ status,
71
+ pluginActions,
72
+ openEntityMode
73
+ });
74
+ }
75
+
76
+ type ActionsViewProps<M extends object> = {
77
+ savingError: Error | undefined,
78
+ entity: Entity<M> | undefined,
79
+ formActions: EntityAction[],
80
+ collection: ResolvedEntityCollection,
81
+ context: FireCMSContext,
82
+ sideEntityController: SideEntityController,
83
+ isSubmitting: boolean,
84
+ disabled: boolean,
85
+ status: "new" | "existing" | "copy",
86
+ sideDialogContext: SideDialogController,
87
+ pluginActions?: React.ReactNode[],
88
+ openEntityMode: "side_panel" | "full_screen";
89
+ };
90
+
91
+ function buildBottomActions<M extends object>({
92
+ savingError,
93
+ entity,
94
+ formActions,
95
+ collection,
96
+ context,
97
+ sideEntityController,
98
+ isSubmitting,
99
+ disabled,
100
+ status,
101
+ sideDialogContext,
102
+ pluginActions,
103
+ openEntityMode
104
+ }: ActionsViewProps<M>) {
105
+
106
+ const canClose = openEntityMode === "side_panel";
107
+ return <DialogActions position={"absolute"}>
108
+ {savingError &&
109
+ <div className="text-right">
110
+ <Typography color={"error"}>{savingError.message}</Typography>
111
+ </div>
112
+ }
113
+ {entity && formActions.length > 0 && <div className="flex-grow flex overflow-auto no-scrollbar">
114
+ {formActions.map(action => (
115
+ <IconButton
116
+ key={action.name}
117
+ color="primary"
118
+ onClick={(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
119
+ event.stopPropagation();
120
+ if (entity)
121
+ action.onClick({
122
+ entity,
123
+ fullPath: collection.path,
124
+ collection: collection,
125
+ context,
126
+ sideEntityController,
127
+ openEntityMode: openEntityMode
128
+ });
129
+ }}>
130
+ {action.icon}
131
+ </IconButton>
132
+ ))}
133
+ </div>}
134
+ {pluginActions}
135
+ <Button variant="text" disabled={disabled || isSubmitting} type="reset">
136
+ {status === "existing" ? "Discard" : "Clear"}
137
+ </Button>
138
+ <Button variant={canClose ? "text" : "filled"} color="primary" type="submit"
139
+ disabled={disabled || isSubmitting}
140
+ onClick={() => {
141
+ sideDialogContext.setPendingClose(false);
142
+ }}>
143
+ {status === "existing" && "Save"}
144
+ {status === "copy" && "Create copy"}
145
+ {status === "new" && "Create"}
146
+ </Button>
147
+ {canClose && <LoadingButton variant="filled"
148
+ color="primary"
149
+ type="submit"
150
+ loading={isSubmitting}
151
+ disabled={disabled}
152
+ onClick={() => {
153
+ sideDialogContext.setPendingClose?.(true);
154
+ }}>
155
+ {status === "existing" && "Save and close"}
156
+ {status === "copy" && "Create copy and close"}
157
+ {status === "new" && "Create and close"}
158
+ </LoadingButton>}
159
+ </DialogActions>;
160
+ }
161
+
162
+ function buildSideActions<M extends object>({
163
+ savingError,
164
+ entity,
165
+ formActions,
166
+ collection,
167
+ context,
168
+ sideEntityController,
169
+ isSubmitting,
170
+ disabled,
171
+ status,
172
+ sideDialogContext,
173
+ pluginActions
174
+ }: ActionsViewProps<M>) {
175
+
176
+ return <div
177
+ 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)}>
178
+ <LoadingButton fullWidth={true} variant="filled" color="primary" type="submit" size={"large"}
179
+ disabled={disabled || isSubmitting} onClick={() => {
180
+ sideDialogContext.setPendingClose?.(false);
181
+ }}>
182
+ {status === "existing" && "Save"}
183
+ {status === "copy" && "Create copy"}
184
+ {status === "new" && "Create"}
185
+ </LoadingButton>
186
+ <Button fullWidth={true} variant="text" disabled={disabled || isSubmitting} type="reset">
187
+ {status === "existing" ? "Discard" : "Clear"}
188
+ </Button>
189
+
190
+ {pluginActions}
191
+
192
+ {savingError &&
193
+ <div className="text-right">
194
+ <Typography color={"error"}>{savingError.message}</Typography>
195
+ </div>
196
+ }
197
+ </div>;
198
+ }
@@ -31,6 +31,9 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
31
31
 
32
32
  const sideEntityController = useSideEntityController();
33
33
  const navigationController = useNavigationController();
34
+ const sideDialogsController = useSideDialogContext();
35
+
36
+ console.log("sideDialogsController", sideDialogsController);
34
37
 
35
38
  const onClose = () => {
36
39
  if (props.onClose) {
@@ -55,6 +58,11 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
55
58
  });
56
59
  }
57
60
 
61
+ if (sideDialogsController.pendingClose) {
62
+ sideDialogsController.setPendingClose(false);
63
+ onClose();
64
+ }
65
+
58
66
  }
59
67
 
60
68
  const parentCollectionIds = useMemo(() => {
@@ -113,7 +121,6 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
113
121
  collection={collection}
114
122
  parentCollectionIds={parentCollectionIds}
115
123
  onValuesModified={onValuesModified}
116
- onClose={onClose}
117
124
  onSaved={onUpdate}
118
125
  barActions={<>
119
126
  <IconButton
@@ -1,4 +1,4 @@
1
- import React, { useContext, useEffect, useState } from "react";
1
+ import React, { useCallback, useContext, useEffect, useRef, useState } from "react";
2
2
  import { useSideDialogsController } from "../hooks";
3
3
  import { SideDialogPanelProps } from "../types";
4
4
  import { Sheet } from "@firecms/ui";
@@ -6,15 +6,17 @@ import { useNavigationUnsavedChangesDialog } from "../internal/useUnsavedChanges
6
6
  import { ErrorBoundary } from "../components";
7
7
  import { UnsavedChangesDialog } from "../components/UnsavedChangesDialog";
8
8
 
9
- export type SideDialogContextProps = {
9
+ export type SideDialogController = {
10
10
  blocked: boolean,
11
11
  setBlocked: (blocked: boolean) => void,
12
12
  setBlockedNavigationMessage: (message?: React.ReactNode) => void,
13
13
  width?: string,
14
- close: (force?: boolean) => void
14
+ close: (force?: boolean) => void;
15
+ pendingClose: boolean,
16
+ setPendingClose: (pendingClose: boolean) => void
15
17
  }
16
18
 
17
- const SideDialogContext = React.createContext<SideDialogContextProps>({
19
+ const SideDialogContext = React.createContext<SideDialogController>({
18
20
  width: "",
19
21
  blocked: false,
20
22
  setBlocked: (blocked: boolean) => {
@@ -22,6 +24,10 @@ const SideDialogContext = React.createContext<SideDialogContextProps>({
22
24
  setBlockedNavigationMessage: (message?: React.ReactNode) => {
23
25
  },
24
26
  close: () => {
27
+ },
28
+ pendingClose: false,
29
+ setPendingClose: () => {
30
+
25
31
  }
26
32
  });
27
33
 
@@ -30,7 +36,7 @@ const SideDialogContext = React.createContext<SideDialogContextProps>({
30
36
  * in contrast with {@link useSideDialogsController} which handles the
31
37
  * state of all the dialogs.
32
38
  */
33
- export const useSideDialogContext = () => useContext<SideDialogContextProps>(SideDialogContext);
39
+ export const useSideDialogContext = () => useContext<SideDialogController>(SideDialogContext);
34
40
 
35
41
  /**
36
42
  * This is the component in charge of rendering the side dialogs used
@@ -73,6 +79,8 @@ function SideDialogView({
73
79
  const [blocked, setBlocked] = useState(false);
74
80
  const [blockedNavigationMessage, setBlockedNavigationMessage] = useState<React.ReactNode | undefined>();
75
81
 
82
+ const [pendingClose, setPendingClose] = useState(false);
83
+
76
84
  const widthRef = React.useRef<string | undefined>(panel?.width);
77
85
  const width = widthRef.current;
78
86
 
@@ -119,19 +127,15 @@ function SideDialogView({
119
127
  setBlocked,
120
128
  setBlockedNavigationMessage,
121
129
  width,
122
- close: onCloseRequest
130
+ close: onCloseRequest,
131
+ pendingClose,
132
+ setPendingClose
123
133
  }}>
124
134
 
125
135
  <Sheet
126
136
  open={Boolean(panel)}
127
137
  onOpenChange={(open) => !open && onCloseRequest()}
128
138
  title={"Side dialog " + panel?.key}
129
- // style={{
130
- // zIndex: offsetPosition * 20 + 20,
131
- // }}
132
- // overlayStyle={{
133
- // zIndex: offsetPosition * 20 + 10,
134
- // }}
135
139
  >
136
140
  {panel &&
137
141
  <div