Skip to content

Commit 7ad0c78

Browse files
committed
Autosave tweaks and updates
1 parent 20d5f18 commit 7ad0c78

File tree

4 files changed

+93
-64
lines changed

4 files changed

+93
-64
lines changed

lib/src/core/internal/EntityView.tsx

+69-46
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,14 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
22
import equal from "react-fast-compare";
33
import { Box, CircularProgress, Divider, IconButton, Tab, Tabs, Typography, useTheme } from "@mui/material";
44
import CloseIcon from "@mui/icons-material/Close";
5-
import {
6-
Entity,
7-
EntityCollection,
8-
EntityStatus,
9-
EntityValues,
10-
FireCMSPlugin,
11-
FormContext,
12-
ResolvedEntityCollection,
13-
User
14-
} from "../../types";
5+
import { Entity, EntityCollection, EntityStatus, EntityValues, FireCMSPlugin, FormContext, User } from "../../types";
156
import { CircularProgressCenter, EntityCollectionView, EntityPreview, ErrorBoundary } from "../components";
167
import {
178
canEditEntity,
189
fullPathToCollectionSegments,
1910
removeInitialAndTrailingSlashes,
20-
resolveDefaultSelectedView
11+
resolveDefaultSelectedView,
12+
useDebounce
2113
} from "../util";
2214

2315
import { ADDITIONAL_TAB_WIDTH, CONTAINER_FULL_WIDTH, FORM_CONTAINER_WIDTH } from "./common";
@@ -33,6 +25,8 @@ import {
3325
import { EntityForm } from "../../form";
3426
import { useSideDialogContext } from "../SideDialogs";
3527
import { useLargeSideLayout } from "./useLargeSideLayout";
28+
import { EntityFormSaveParams } from "../../form/EntityForm";
29+
import { setIn } from "formik";
3630

3731
export interface EntityViewProps<M extends Record<string, any>> {
3832
path: string;
@@ -69,6 +63,17 @@ export const EntityView = React.memo<EntityViewProps<any>>(
6963
console.warn(`The collection ${collection.path} has customId and formAutoSave enabled. This is not supported and formAutoSave will be ignored`);
7064
}
7165

66+
const [saving, setSaving] = useState(false);
67+
/**
68+
* These are the values that are being saved. They are debounced.
69+
* We use this only when autoSave is enabled.
70+
*/
71+
const [valuesToBeSaved, setValuesToBeSaved] = useState<EntityValues<M> | undefined>(undefined);
72+
useDebounce(valuesToBeSaved, () => {
73+
if (valuesToBeSaved)
74+
saveEntity({ values: valuesToBeSaved, closeAfterSave: false });
75+
}, false, 2000);
76+
7277
const theme = useTheme();
7378
const largeLayout = useLargeSideLayout();
7479
const largeLayoutTabSelected = useRef(!largeLayout);
@@ -85,7 +90,6 @@ export const EntityView = React.memo<EntityViewProps<any>>(
8590
const [formContext, setFormContext] = useState<FormContext<M> | undefined>(undefined);
8691

8792
const [status, setStatus] = useState<EntityStatus>(copy ? "copy" : (entityId ? "existing" : "new"));
88-
// const [currentEntityId, setCurrentEntityId] = useState<string | undefined>(entityId);
8993

9094
const modifiedValuesRef = useRef<EntityValues<M> | undefined>(undefined);
9195
const modifiedValues = modifiedValuesRef.current;
@@ -176,6 +180,7 @@ export const EntityView = React.memo<EntityViewProps<any>>(
176180
}, [defaultSelectedView, largeLayout, selectedSubPath]);
177181

178182
const onPreSaveHookError = useCallback((e: Error) => {
183+
setSaving(false);
179184
snackbarController.open({
180185
type: "error",
181186
message: "Error before saving: " + e?.message
@@ -184,6 +189,7 @@ export const EntityView = React.memo<EntityViewProps<any>>(
184189
}, [snackbarController]);
185190

186191
const onSaveSuccessHookError = useCallback((e: Error) => {
192+
setSaving(false);
187193
snackbarController.open({
188194
type: "error",
189195
message: "Error after saving (entity is saved): " + e?.message
@@ -193,10 +199,12 @@ export const EntityView = React.memo<EntityViewProps<any>>(
193199

194200
const onSaveSuccess = (updatedEntity: Entity<M>, closeAfterSave: boolean) => {
195201

196-
snackbarController.open({
197-
type: "success",
198-
message: `${collection.singularName ?? collection.name}: Saved correctly`
199-
});
202+
setSaving(false);
203+
if (!autoSave)
204+
snackbarController.open({
205+
type: "success",
206+
message: `${collection.singularName ?? collection.name}: Saved correctly`
207+
});
200208

201209
setUsedEntity(updatedEntity);
202210
setStatus("existing");
@@ -224,6 +232,7 @@ export const EntityView = React.memo<EntityViewProps<any>>(
224232

225233
const onSaveFailure = useCallback((e: Error) => {
226234

235+
setSaving(false);
227236
snackbarController.open({
228237
type: "error",
229238
message: "Error saving: " + e?.message
@@ -233,26 +242,13 @@ export const EntityView = React.memo<EntityViewProps<any>>(
233242
console.error(e);
234243
}, [entityId, path, snackbarController]);
235244

236-
const onEntitySave = useCallback(async ({
237-
collection,
238-
path,
239-
entityId,
240-
values,
241-
previousValues,
242-
closeAfterSave
243-
}: {
244-
collection: ResolvedEntityCollection<M>,
245-
path: string,
246-
entityId: string | undefined,
247-
values: EntityValues<M>,
248-
previousValues?: EntityValues<M>,
249-
closeAfterSave: boolean
250-
}): Promise<void> => {
251-
252-
if (!status)
253-
return;
254-
255-
return saveEntityWithCallbacks({
245+
function saveEntity({ values, previousValues, closeAfterSave }: {
246+
values: M,
247+
previousValues?: M,
248+
closeAfterSave: boolean,
249+
}) {
250+
setSaving(true);
251+
saveEntityWithCallbacks({
256252
path,
257253
entityId,
258254
values,
@@ -265,8 +261,29 @@ export const EntityView = React.memo<EntityViewProps<any>>(
265261
onSaveFailure,
266262
onPreSaveHookError,
267263
onSaveSuccessHookError
268-
});
269-
}, [status, collection, dataSource, context, onSaveSuccess, onSaveFailure, onPreSaveHookError, onSaveSuccessHookError]);
264+
}).then();
265+
}
266+
267+
const onSaveEntityRequest = async ({
268+
values,
269+
previousValues,
270+
closeAfterSave,
271+
autoSave
272+
}: EntityFormSaveParams<M>): Promise<void> => {
273+
274+
if (!status)
275+
return;
276+
277+
if (autoSave) {
278+
setValuesToBeSaved(values);
279+
} else {
280+
saveEntity({
281+
values,
282+
previousValues,
283+
closeAfterSave
284+
});
285+
}
286+
};
270287

271288
const customViewsView: React.ReactNode[] | undefined = customViews && customViews.map(
272289
(customView, colIndex) => {
@@ -306,7 +323,9 @@ export const EntityView = React.memo<EntityViewProps<any>>(
306323
}
307324
).filter(Boolean);
308325

309-
const loading = (dataLoading && !usedEntity) || ((!usedEntity || readOnly === undefined) && (status === "existing" || status === "copy"));
326+
const globalLoading = (dataLoading && !usedEntity) ||
327+
((!usedEntity || readOnly === undefined) && (status === "existing" || status === "copy"));
328+
const loading = globalLoading || saving;
310329

311330
const subCollectionsViews = subcollections && subcollections.map(
312331
(subcollection, colIndex) => {
@@ -331,7 +350,7 @@ export const EntityView = React.memo<EntityViewProps<any>>(
331350

332351
{loading && <CircularProgressCenter/>}
333352

334-
{!loading &&
353+
{!globalLoading &&
335354
(usedEntity && fullPath
336355
? <EntityCollectionView
337356
fullPath={fullPath}
@@ -401,16 +420,21 @@ export const EntityView = React.memo<EntityViewProps<any>>(
401420
setCurrentEntityId(id);
402421
}, []);
403422

423+
const onModified = (dirty: boolean) => {
424+
if (!autoSave)
425+
onValuesAreModified(dirty);
426+
}
427+
404428
function buildForm() {
405429
const plugins = context.plugins;
406430
let form = <EntityForm
407431
status={status}
408432
path={path}
409433
collection={collection}
410-
onEntitySave={onEntitySave}
434+
onEntitySaveRequested={onSaveEntityRequest}
411435
onDiscard={onDiscard}
412436
onValuesChanged={onValuesChanged}
413-
onModified={onValuesAreModified}
437+
onModified={onModified}
414438
entity={usedEntity}
415439
onIdChange={onIdChange}
416440
onFormContextChange={setFormContext}
@@ -425,10 +449,9 @@ export const EntityView = React.memo<EntityViewProps<any>>(
425449
status={status}
426450
path={path}
427451
collection={collection}
428-
onEntitySave={onEntitySave}
429452
onDiscard={onDiscard}
430453
onValuesChanged={onValuesChanged}
431-
onModified={onValuesAreModified}
454+
onModified={onModified}
432455
entity={usedEntity}
433456
context={context}
434457
currentEntityId={currentEntityId}
@@ -504,7 +527,7 @@ export const EntityView = React.memo<EntityViewProps<any>>(
504527

505528
<Box flexGrow={1}/>
506529

507-
{loading && <Box
530+
{globalLoading && <Box
508531
sx={{
509532
alignSelf: "center"
510533
}}>
@@ -595,7 +618,7 @@ export const EntityView = React.memo<EntityViewProps<any>>(
595618
}
596619
}}>
597620

598-
{loading
621+
{globalLoading
599622
? <CircularProgressCenter/>
600623
: form}
601624

lib/src/core/util/useDebounce.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import React from "react";
33
export function useDebounce<T>(value: T, callback: () => void, immediate: boolean, timeoutMs = 300) {
44

55
const pendingUpdate = React.useRef(false);
6-
const performUpdate = React.useCallback(() => {
6+
const performUpdate = () => {
77
callback();
88
pendingUpdate.current = false;
9-
}, [callback]);
9+
};
1010

1111
const handlerRef = React.useRef<number | undefined>(undefined);
1212

lib/src/form/EntityForm.tsx

+18-13
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { useDataSource, useFireCMSContext } from "../hooks";
2222
import { ErrorFocus } from "./components/ErrorFocus";
2323
import { CustomIdField } from "./components/CustomIdField";
2424

25+
2526
/**
2627
* @category Components
2728
*/
@@ -51,16 +52,8 @@ export interface EntityFormProps<M extends Record<string, any>> {
5152
/**
5253
* The callback function called when Save is clicked and validation is correct
5354
*/
54-
onEntitySave: (
55-
props:
56-
{
57-
collection: ResolvedEntityCollection<M>,
58-
path: string,
59-
entityId: string | undefined,
60-
values: EntityValues<M>,
61-
previousValues?: EntityValues<M>,
62-
closeAfterSave: boolean
63-
}
55+
onEntitySaveRequested: (
56+
props: EntityFormSaveParams<M>
6457
) => Promise<void>;
6558

6659
/**
@@ -95,6 +88,17 @@ export interface EntityFormProps<M extends Record<string, any>> {
9588

9689
}
9790

91+
92+
export type EntityFormSaveParams<M extends Record<string, any>> = {
93+
collection: ResolvedEntityCollection<M>,
94+
path: string,
95+
entityId: string | undefined,
96+
values: EntityValues<M>,
97+
previousValues?: EntityValues<M>,
98+
closeAfterSave: boolean,
99+
autoSave: boolean
100+
};
101+
98102
/**
99103
* This is the form used internally by the CMS
100104
* @param status
@@ -120,7 +124,7 @@ function EntityFormInternal<M extends Record<string, any>>({
120124
path,
121125
collection: inputCollection,
122126
entity,
123-
onEntitySave,
127+
onEntitySaveRequested,
124128
onDiscard,
125129
onModified,
126130
onValuesChanged,
@@ -242,13 +246,14 @@ function EntityFormInternal<M extends Record<string, any>>({
242246
}, [baseDataSourceValues, collection.properties, initialValues, status]);
243247

244248
const save = (values: EntityValues<M>) =>
245-
onEntitySave({
249+
onEntitySaveRequested({
246250
collection,
247251
path,
248252
entityId,
249253
values,
250254
previousValues: entity?.values,
251-
closeAfterSave: closeAfterSaveRef.current
255+
closeAfterSave: closeAfterSaveRef.current,
256+
autoSave: autoSave ?? false,
252257
}).then(_ => {
253258
const eventName: CMSAnalyticsEvent = status === "new"
254259
? "new_entity_saved"

lib/src/form/field_bindings/BlockFieldBinding.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -203,12 +203,13 @@ function BlockEntry({
203203
}
204204
: undefined;
205205

206-
const updateType = useCallback((newType: any) => {
206+
const updateType = (newType: any) => {
207+
const newSelectedProperty = newType ? properties[newType] : undefined;
207208
setTypeInternal(newType);
208209
formikContext.setFieldTouched(typeFieldName);
209210
formikContext.setFieldValue(typeFieldName, newType);
210-
formikContext.setFieldValue(valueFieldName, property ? getDefaultValueFor(property) : null);
211-
}, [typeFieldName, valueFieldName]);
211+
formikContext.setFieldValue(valueFieldName, newSelectedProperty ? getDefaultValueFor(newSelectedProperty) : null);
212+
};
212213

213214
return (
214215
<Paper sx={(theme) => ({

0 commit comments

Comments
 (0)