@firecms/core 3.0.0-canary.186 → 3.0.0-canary.187
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.
- package/dist/core/EntityEditView.d.ts +4 -15
- package/dist/core/EntityForm.d.ts +43 -0
- package/dist/form/components/FormEntry.d.ts +6 -0
- package/dist/form/components/FormLayout.d.ts +5 -0
- package/dist/form/components/index.d.ts +2 -0
- package/dist/form/field_bindings/ArrayCustomShapedFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
- package/dist/index.es.js +11960 -11691
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +11938 -11669
- package/dist/index.umd.js.map +1 -1
- package/dist/types/fields.d.ts +11 -0
- package/dist/types/navigation.d.ts +1 -0
- package/dist/types/plugins.d.ts +1 -1
- package/dist/types/properties.d.ts +1 -1
- package/package.json +5 -5
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +4 -2
- package/src/core/EntityEditView.tsx +210 -1049
- package/src/core/EntityForm.tsx +952 -0
- package/src/core/NavigationRoutes.tsx +16 -12
- package/src/form/components/FormEntry.tsx +22 -0
- package/src/form/components/FormLayout.tsx +16 -0
- package/src/form/components/index.tsx +2 -0
- package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +0 -2
- package/src/form/field_bindings/MapFieldBinding.tsx +0 -2
- package/src/routes/FireCMSRoute.tsx +7 -2
- package/src/types/fields.tsx +16 -0
- package/src/types/navigation.ts +1 -0
- package/src/types/plugins.tsx +1 -1
- package/src/types/properties.ts +1 -1
- package/src/util/entity_cache.ts +10 -1
|
@@ -0,0 +1,952 @@
|
|
|
1
|
+
import React, { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
CMSAnalyticsEvent,
|
|
4
|
+
Entity,
|
|
5
|
+
EntityAction,
|
|
6
|
+
EntityCollection,
|
|
7
|
+
EntityStatus,
|
|
8
|
+
EntityValues,
|
|
9
|
+
FireCMSContext,
|
|
10
|
+
FormContext,
|
|
11
|
+
PluginFormActionProps,
|
|
12
|
+
PropertyFieldBindingProps,
|
|
13
|
+
ResolvedEntityCollection,
|
|
14
|
+
SideEntityController
|
|
15
|
+
} from "../types";
|
|
16
|
+
import equal from "react-fast-compare";
|
|
17
|
+
|
|
18
|
+
import { copyEntityAction, deleteEntityAction, ErrorBoundary, getFormFieldKeys } from "../components";
|
|
19
|
+
import {
|
|
20
|
+
canCreateEntity,
|
|
21
|
+
canDeleteEntity,
|
|
22
|
+
getDefaultValuesFor,
|
|
23
|
+
getEntityTitlePropertyKey,
|
|
24
|
+
getValueInPath,
|
|
25
|
+
isHidden,
|
|
26
|
+
isReadOnly,
|
|
27
|
+
mergeEntityActions,
|
|
28
|
+
resolveCollection,
|
|
29
|
+
useDebouncedCallback
|
|
30
|
+
} from "../util";
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
saveEntityWithCallbacks,
|
|
34
|
+
useAuthController,
|
|
35
|
+
useCustomizationController,
|
|
36
|
+
useDataSource,
|
|
37
|
+
useFireCMSContext,
|
|
38
|
+
useSideEntityController,
|
|
39
|
+
useSnackbarController
|
|
40
|
+
} 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";
|
|
57
|
+
import { Formex, FormexController, getIn, setIn, useCreateFormex } from "@firecms/formex";
|
|
58
|
+
import { useAnalyticsController } from "../hooks/useAnalyticsController";
|
|
59
|
+
import { FormEntry, FormLayout, LabelWithIconAndTooltip, PropertyFieldBinding } from "../form";
|
|
60
|
+
import { ValidationError } from "yup";
|
|
61
|
+
import { removeEntityFromCache, saveEntityToCache } from "../util/entity_cache";
|
|
62
|
+
import { CustomIdField } from "../form/components/CustomIdField";
|
|
63
|
+
import { ErrorFocus } from "../form/components/ErrorFocus";
|
|
64
|
+
import { CustomFieldValidator, getYupEntitySchema } from "../form/validation";
|
|
65
|
+
|
|
66
|
+
export type OnUpdateParams = {
|
|
67
|
+
entity: Entity<any>,
|
|
68
|
+
status: EntityStatus,
|
|
69
|
+
path: string,
|
|
70
|
+
entityId?: string;
|
|
71
|
+
selectedTab?: string;
|
|
72
|
+
collection: EntityCollection<any>
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type EntityFormProps<M extends Record<string, any>> = {
|
|
76
|
+
path: string;
|
|
77
|
+
collection: EntityCollection<M>;
|
|
78
|
+
entityId?: string;
|
|
79
|
+
entity?: Entity<M>;
|
|
80
|
+
databaseId?: string;
|
|
81
|
+
onIdChange?: (id: string) => void;
|
|
82
|
+
onValuesModified?: (modified: boolean) => void;
|
|
83
|
+
onSaved?: (params: OnUpdateParams) => void;
|
|
84
|
+
onClose?: () => void;
|
|
85
|
+
cachedDirtyValues?: Partial<M>; // dirty cached entity in memory
|
|
86
|
+
onFormContextReady?: (formContext: FormContext) => void;
|
|
87
|
+
forceActionsAtTheBottom?: boolean;
|
|
88
|
+
initialStatus: EntityStatus;
|
|
89
|
+
className?: string;
|
|
90
|
+
onStatusChange?: (status: EntityStatus) => void;
|
|
91
|
+
onEntityChange?: (entity: Entity<M>) => void;
|
|
92
|
+
formex?: FormexController<M>;
|
|
93
|
+
openEntityMode?: "side_panel" | "full_screen";
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export function EntityForm<M extends Record<string, any>>({
|
|
97
|
+
path,
|
|
98
|
+
entityId: entityIdProp,
|
|
99
|
+
collection,
|
|
100
|
+
onValuesModified,
|
|
101
|
+
onIdChange,
|
|
102
|
+
onSaved,
|
|
103
|
+
onClose,
|
|
104
|
+
entity,
|
|
105
|
+
cachedDirtyValues,
|
|
106
|
+
onFormContextReady,
|
|
107
|
+
forceActionsAtTheBottom,
|
|
108
|
+
initialStatus,
|
|
109
|
+
className,
|
|
110
|
+
onStatusChange,
|
|
111
|
+
onEntityChange,
|
|
112
|
+
openEntityMode = "full_screen",
|
|
113
|
+
formex: formexProp
|
|
114
|
+
}: EntityFormProps<M>) {
|
|
115
|
+
|
|
116
|
+
if (collection.customId && collection.formAutoSave) {
|
|
117
|
+
console.warn(`The collection ${collection.path} has customId and formAutoSave enabled. This is not supported and formAutoSave will be ignored`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const [status, setStatus] = useState<EntityStatus>(initialStatus);
|
|
121
|
+
const [saving, setSaving] = useState(false);
|
|
122
|
+
|
|
123
|
+
const updateStatus = (status: EntityStatus) => {
|
|
124
|
+
setStatus(status);
|
|
125
|
+
onStatusChange?.(status);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const [valuesToBeSaved, setValuesToBeSaved] = useState<EntityValues<M> | undefined>(undefined);
|
|
129
|
+
useDebouncedCallback(valuesToBeSaved, () => {
|
|
130
|
+
if (valuesToBeSaved)
|
|
131
|
+
saveEntity({
|
|
132
|
+
entityId: entityIdProp,
|
|
133
|
+
collection,
|
|
134
|
+
path,
|
|
135
|
+
values: valuesToBeSaved,
|
|
136
|
+
closeAfterSave: false
|
|
137
|
+
});
|
|
138
|
+
}, false, 2000);
|
|
139
|
+
|
|
140
|
+
const authController = useAuthController();
|
|
141
|
+
const dataSource = useDataSource(collection);
|
|
142
|
+
const sideEntityController = useSideEntityController();
|
|
143
|
+
const snackbarController = useSnackbarController();
|
|
144
|
+
const customizationController = useCustomizationController();
|
|
145
|
+
const context = useFireCMSContext();
|
|
146
|
+
const closeAfterSaveRef = useRef(false);
|
|
147
|
+
const analyticsController = useAnalyticsController();
|
|
148
|
+
|
|
149
|
+
const [underlyingChanges, setUnderlyingChanges] = useState<Partial<EntityValues<M>>>({});
|
|
150
|
+
|
|
151
|
+
const [customIdLoading, setCustomIdLoading] = useState<boolean>(false);
|
|
152
|
+
|
|
153
|
+
const mustSetCustomId: boolean = (status === "new" || status === "copy") &&
|
|
154
|
+
(Boolean(collection.customId) && collection.customId !== "optional");
|
|
155
|
+
|
|
156
|
+
const initialEntityId: string | undefined = useMemo((): string | undefined => {
|
|
157
|
+
if (status === "new" || status === "copy") {
|
|
158
|
+
if (mustSetCustomId) {
|
|
159
|
+
return undefined;
|
|
160
|
+
} else {
|
|
161
|
+
return dataSource.generateEntityId(path, collection);
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
return entityIdProp;
|
|
165
|
+
}
|
|
166
|
+
}, [entityIdProp, status]);
|
|
167
|
+
|
|
168
|
+
const [entityId, setEntityId] = useState<string | undefined>(initialEntityId);
|
|
169
|
+
const [entityIdError, setEntityIdError] = useState<boolean>(false);
|
|
170
|
+
const [savingError, setSavingError] = useState<Error | undefined>();
|
|
171
|
+
|
|
172
|
+
const autoSave = collection.formAutoSave && !collection.customId;
|
|
173
|
+
|
|
174
|
+
const onSubmit = (values: EntityValues<M>, formexController: FormexController<EntityValues<M>>) => {
|
|
175
|
+
|
|
176
|
+
if (mustSetCustomId && !entityId) {
|
|
177
|
+
console.error("Missing custom Id");
|
|
178
|
+
setEntityIdError(true);
|
|
179
|
+
formexController.setSubmitting(false);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
setSavingError(undefined);
|
|
184
|
+
setEntityIdError(false);
|
|
185
|
+
|
|
186
|
+
if (status === "existing") {
|
|
187
|
+
if (!entity?.id) throw Error("Form misconfiguration when saving, no id for existing entity");
|
|
188
|
+
} else if (status === "new" || status === "copy") {
|
|
189
|
+
if (collection.customId) {
|
|
190
|
+
if (collection.customId !== "optional" && !entityId) {
|
|
191
|
+
throw Error("Form misconfiguration when saving, entityId should be set");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
throw Error("New FormType added, check EntityForm");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return save(values)
|
|
199
|
+
?.then(_ => {
|
|
200
|
+
formexController.resetForm({
|
|
201
|
+
values,
|
|
202
|
+
submitCount: 0,
|
|
203
|
+
touched: {}
|
|
204
|
+
});
|
|
205
|
+
})
|
|
206
|
+
.finally(() => {
|
|
207
|
+
formexController.setSubmitting(false);
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const formex: FormexController<M> = formexProp ?? useCreateFormex<M>({
|
|
212
|
+
initialValues: (cachedDirtyValues ?? getInitialEntityValues(collection, path, status, entity)) as M,
|
|
213
|
+
initialDirty: Boolean(cachedDirtyValues),
|
|
214
|
+
onSubmit,
|
|
215
|
+
onReset: () => {
|
|
216
|
+
clearDirtyCache();
|
|
217
|
+
onValuesModified?.(false);
|
|
218
|
+
},
|
|
219
|
+
validation: (values) => {
|
|
220
|
+
return validationSchema?.validate(values, { abortEarly: false })
|
|
221
|
+
.then(() => {
|
|
222
|
+
return {};
|
|
223
|
+
})
|
|
224
|
+
.catch((e: any) => {
|
|
225
|
+
return yupToFormErrors(e);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const resolvedCollection = useMemo(() => resolveCollection<M>({
|
|
231
|
+
collection,
|
|
232
|
+
path,
|
|
233
|
+
entityId,
|
|
234
|
+
values: formex.values,
|
|
235
|
+
previousValues: formex.initialValues,
|
|
236
|
+
propertyConfigs: customizationController.propertyConfigs
|
|
237
|
+
}), [collection, path, entityId, formex.values, formex.initialValues, customizationController.propertyConfigs]);
|
|
238
|
+
|
|
239
|
+
const onPreSaveHookError = useCallback((e: Error) => {
|
|
240
|
+
setSaving(false);
|
|
241
|
+
snackbarController.open({
|
|
242
|
+
type: "error",
|
|
243
|
+
message: "Error before saving: " + e?.message
|
|
244
|
+
});
|
|
245
|
+
console.error(e);
|
|
246
|
+
}, [snackbarController]);
|
|
247
|
+
|
|
248
|
+
const onSaveSuccessHookError = useCallback((e: Error) => {
|
|
249
|
+
setSaving(false);
|
|
250
|
+
snackbarController.open({
|
|
251
|
+
type: "error",
|
|
252
|
+
message: "Error after saving (entity is saved): " + e?.message
|
|
253
|
+
});
|
|
254
|
+
console.error(e);
|
|
255
|
+
}, [snackbarController]);
|
|
256
|
+
|
|
257
|
+
function clearDirtyCache() {
|
|
258
|
+
if (status === "new" || status === "copy") {
|
|
259
|
+
removeEntityFromCache(path + "#new");
|
|
260
|
+
} else {
|
|
261
|
+
removeEntityFromCache(path + "/" + entityId);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const onSaveSuccess = (updatedEntity: Entity<M>, closeAfterSave: boolean) => {
|
|
266
|
+
|
|
267
|
+
clearDirtyCache();
|
|
268
|
+
onValuesModified?.(false);
|
|
269
|
+
setSaving(false);
|
|
270
|
+
if (!autoSave)
|
|
271
|
+
snackbarController.open({
|
|
272
|
+
type: "success",
|
|
273
|
+
message: `${collection.singularName ?? collection.name}: Saved correctly`
|
|
274
|
+
});
|
|
275
|
+
onEntityChange?.(updatedEntity);
|
|
276
|
+
updateStatus("existing");
|
|
277
|
+
setEntityId(updatedEntity.id);
|
|
278
|
+
|
|
279
|
+
if (onSaved) {
|
|
280
|
+
onSaved({
|
|
281
|
+
entity: updatedEntity,
|
|
282
|
+
status,
|
|
283
|
+
path,
|
|
284
|
+
entityId: updatedEntity.id,
|
|
285
|
+
collection
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (closeAfterSave) {
|
|
290
|
+
onClose?.();
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const onSaveFailure = useCallback((e: Error) => {
|
|
295
|
+
setSaving(false);
|
|
296
|
+
snackbarController.open({
|
|
297
|
+
type: "error",
|
|
298
|
+
message: "Error saving: " + e?.message
|
|
299
|
+
});
|
|
300
|
+
console.error("Error saving entity", path, entityId);
|
|
301
|
+
console.error(e);
|
|
302
|
+
}, [entityId, path, snackbarController]);
|
|
303
|
+
|
|
304
|
+
const saveEntity = ({
|
|
305
|
+
values,
|
|
306
|
+
previousValues,
|
|
307
|
+
closeAfterSave,
|
|
308
|
+
entityId,
|
|
309
|
+
collection,
|
|
310
|
+
path
|
|
311
|
+
}: {
|
|
312
|
+
collection: EntityCollection<M>,
|
|
313
|
+
path: string,
|
|
314
|
+
entityId: string | undefined,
|
|
315
|
+
values: M,
|
|
316
|
+
previousValues?: M,
|
|
317
|
+
closeAfterSave: boolean,
|
|
318
|
+
}) => {
|
|
319
|
+
setSaving(true);
|
|
320
|
+
return saveEntityWithCallbacks({
|
|
321
|
+
path,
|
|
322
|
+
entityId,
|
|
323
|
+
values,
|
|
324
|
+
previousValues,
|
|
325
|
+
collection,
|
|
326
|
+
status,
|
|
327
|
+
dataSource,
|
|
328
|
+
context,
|
|
329
|
+
onSaveSuccess: (updatedEntity: Entity<M>) => onSaveSuccess(updatedEntity, closeAfterSave),
|
|
330
|
+
onSaveFailure,
|
|
331
|
+
onPreSaveHookError,
|
|
332
|
+
onSaveSuccessHookError
|
|
333
|
+
}).then();
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
type EntityFormSaveParams<M extends Record<string, any>> = {
|
|
337
|
+
collection: ResolvedEntityCollection<M>,
|
|
338
|
+
path: string,
|
|
339
|
+
entityId: string | undefined,
|
|
340
|
+
values: EntityValues<M>,
|
|
341
|
+
previousValues?: EntityValues<M>,
|
|
342
|
+
closeAfterSave: boolean,
|
|
343
|
+
autoSave: boolean
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const onSaveEntityRequest = async ({
|
|
347
|
+
collection,
|
|
348
|
+
path,
|
|
349
|
+
entityId,
|
|
350
|
+
values,
|
|
351
|
+
previousValues,
|
|
352
|
+
closeAfterSave,
|
|
353
|
+
autoSave
|
|
354
|
+
}: EntityFormSaveParams<M>): Promise<void> => {
|
|
355
|
+
if (!status)
|
|
356
|
+
return;
|
|
357
|
+
if (autoSave) {
|
|
358
|
+
setValuesToBeSaved(values);
|
|
359
|
+
} else {
|
|
360
|
+
return saveEntity({
|
|
361
|
+
collection,
|
|
362
|
+
path,
|
|
363
|
+
entityId,
|
|
364
|
+
values,
|
|
365
|
+
previousValues,
|
|
366
|
+
closeAfterSave
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const lastSavedValues = useRef<EntityValues<M> | undefined>(entity?.values);
|
|
372
|
+
const save = (values: EntityValues<M>): Promise<void> => {
|
|
373
|
+
lastSavedValues.current = values;
|
|
374
|
+
return onSaveEntityRequest({
|
|
375
|
+
collection: resolvedCollection,
|
|
376
|
+
path,
|
|
377
|
+
entityId,
|
|
378
|
+
values,
|
|
379
|
+
previousValues: entity?.values,
|
|
380
|
+
closeAfterSave: closeAfterSaveRef.current,
|
|
381
|
+
autoSave: autoSave ?? false
|
|
382
|
+
}).then(_ => {
|
|
383
|
+
const eventName: CMSAnalyticsEvent = status === "new"
|
|
384
|
+
? "new_entity_saved"
|
|
385
|
+
: (status === "copy" ? "entity_copied" : (status === "existing" ? "entity_edited" : "unmapped_event"));
|
|
386
|
+
analyticsController.onAnalyticsEvent?.(eventName, { path });
|
|
387
|
+
}).catch(e => {
|
|
388
|
+
console.error(e);
|
|
389
|
+
setSavingError(e);
|
|
390
|
+
}).finally(() => {
|
|
391
|
+
closeAfterSaveRef.current = false;
|
|
392
|
+
});
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const formContext: FormContext<M> = {
|
|
396
|
+
// @ts-ignore
|
|
397
|
+
setFieldValue: useCallback(formex.setFieldValue, []),
|
|
398
|
+
values: formex.values,
|
|
399
|
+
collection: resolvedCollection,
|
|
400
|
+
entityId: entityId as string,
|
|
401
|
+
path,
|
|
402
|
+
save,
|
|
403
|
+
formex,
|
|
404
|
+
entity,
|
|
405
|
+
savingError,
|
|
406
|
+
status,
|
|
407
|
+
openEntityMode,
|
|
408
|
+
setPendingClose: (value: boolean) => {
|
|
409
|
+
closeAfterSaveRef.current = value;
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
useEffect(() => {
|
|
414
|
+
onFormContextReady?.(formContext);
|
|
415
|
+
}, [formex.version, resolvedCollection, entityId, path]);
|
|
416
|
+
|
|
417
|
+
const onIdUpdateError = useCallback((error: any) => {
|
|
418
|
+
snackbarController.open({
|
|
419
|
+
type: "error",
|
|
420
|
+
message: "Error updating id, check the console"
|
|
421
|
+
});
|
|
422
|
+
}, []);
|
|
423
|
+
|
|
424
|
+
const pluginActions: React.ReactNode[] = [];
|
|
425
|
+
const plugins = customizationController.plugins;
|
|
426
|
+
|
|
427
|
+
if (plugins && collection) {
|
|
428
|
+
const actionProps: PluginFormActionProps = {
|
|
429
|
+
entityId,
|
|
430
|
+
path,
|
|
431
|
+
status,
|
|
432
|
+
collection: collection,
|
|
433
|
+
context,
|
|
434
|
+
currentEntityId: entityId,
|
|
435
|
+
formContext,
|
|
436
|
+
openEntityMode
|
|
437
|
+
};
|
|
438
|
+
pluginActions.push(...plugins.map((plugin) => (
|
|
439
|
+
plugin.form?.Actions
|
|
440
|
+
? <plugin.form.Actions
|
|
441
|
+
key={`actions_${plugin.key}`} {...actionProps} />
|
|
442
|
+
: null
|
|
443
|
+
)).filter(Boolean));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const titlePropertyKey = getEntityTitlePropertyKey(resolvedCollection, customizationController.propertyConfigs);
|
|
447
|
+
const title = (formex.values && titlePropertyKey ? getValueInPath(formex.values, titlePropertyKey) : undefined)
|
|
448
|
+
?? collection.singularName
|
|
449
|
+
?? collection.name;
|
|
450
|
+
|
|
451
|
+
const onIdUpdate = collection.callbacks?.onIdUpdate;
|
|
452
|
+
const doOnIdUpdate = useCallback(async () => {
|
|
453
|
+
if (onIdUpdate && formex.values && (status === "new" || status === "copy")) {
|
|
454
|
+
setCustomIdLoading(true);
|
|
455
|
+
try {
|
|
456
|
+
const updatedId = await onIdUpdate({
|
|
457
|
+
collection: resolvedCollection,
|
|
458
|
+
path,
|
|
459
|
+
entityId,
|
|
460
|
+
values: formex.values,
|
|
461
|
+
context
|
|
462
|
+
});
|
|
463
|
+
setEntityId(updatedId);
|
|
464
|
+
} catch (e) {
|
|
465
|
+
onIdUpdateError?.(e);
|
|
466
|
+
console.error(e);
|
|
467
|
+
}
|
|
468
|
+
setCustomIdLoading(false);
|
|
469
|
+
}
|
|
470
|
+
}, [entityId, formex.values, status, onIdUpdate, resolvedCollection, path, context, onIdUpdateError]);
|
|
471
|
+
|
|
472
|
+
useEffect(() => {
|
|
473
|
+
doOnIdUpdate();
|
|
474
|
+
}, [doOnIdUpdate]);
|
|
475
|
+
|
|
476
|
+
useEffect(() => {
|
|
477
|
+
if (!autoSave) {
|
|
478
|
+
onValuesModified?.(modified);
|
|
479
|
+
}
|
|
480
|
+
}, [formex.dirty]);
|
|
481
|
+
|
|
482
|
+
const deferredValues = useDeferredValue(formex.values);
|
|
483
|
+
const modified = formex.dirty;
|
|
484
|
+
|
|
485
|
+
const uniqueFieldValidator: CustomFieldValidator = useCallback(({
|
|
486
|
+
name,
|
|
487
|
+
value,
|
|
488
|
+
property
|
|
489
|
+
}) => dataSource.checkUniqueField(path, name, value, entityId, collection),
|
|
490
|
+
[dataSource, path, entityId]);
|
|
491
|
+
|
|
492
|
+
const validationSchema = useMemo(() => entityId
|
|
493
|
+
? getYupEntitySchema(
|
|
494
|
+
entityId,
|
|
495
|
+
resolvedCollection.properties,
|
|
496
|
+
uniqueFieldValidator)
|
|
497
|
+
: undefined,
|
|
498
|
+
[entityId, resolvedCollection.properties, uniqueFieldValidator]);
|
|
499
|
+
|
|
500
|
+
useEffect(() => {
|
|
501
|
+
const key = (status === "new" || status === "copy") ? path + "#new" : path + "/" + entityId;
|
|
502
|
+
if (modified) {
|
|
503
|
+
saveEntityToCache(key, deferredValues);
|
|
504
|
+
}
|
|
505
|
+
}, [deferredValues, modified, path, entityId, status]);
|
|
506
|
+
|
|
507
|
+
useOnAutoSave(autoSave, formex, lastSavedValues, save);
|
|
508
|
+
|
|
509
|
+
useEffect(() => {
|
|
510
|
+
if (!autoSave && !formex.isSubmitting && underlyingChanges && entity) {
|
|
511
|
+
// we update the form fields from the Firestore data
|
|
512
|
+
// if they were not touched
|
|
513
|
+
Object.entries(underlyingChanges).forEach(([key, value]) => {
|
|
514
|
+
const formValue = formex.values[key];
|
|
515
|
+
if (!equal(value, formValue) && !formex.touched[key]) {
|
|
516
|
+
console.debug("Updated value from the datasource:", key, value);
|
|
517
|
+
formex.setFieldValue(key, value !== undefined ? value : null);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}, [formex.isSubmitting, autoSave, underlyingChanges, entity, formex.values, formex.touched, formex.setFieldValue]);
|
|
522
|
+
|
|
523
|
+
const formFieldKeys = getFormFieldKeys(resolvedCollection);
|
|
524
|
+
|
|
525
|
+
const formFields = () => (
|
|
526
|
+
<FormLayout>
|
|
527
|
+
{formFieldKeys.map((key) => {
|
|
528
|
+
const property = resolvedCollection.properties[key];
|
|
529
|
+
if (property) {
|
|
530
|
+
const underlyingValueHasChanged: boolean =
|
|
531
|
+
!!underlyingChanges &&
|
|
532
|
+
Object.keys(underlyingChanges).includes(key) &&
|
|
533
|
+
formex.touched[key];
|
|
534
|
+
const disabled = (!autoSave && formex.isSubmitting) || isReadOnly(property) || Boolean(property.disabled);
|
|
535
|
+
const hidden = isHidden(property);
|
|
536
|
+
if (hidden) return null;
|
|
537
|
+
const widthPercentage = property.widthPercentage ?? 100;
|
|
538
|
+
const cmsFormFieldProps: PropertyFieldBindingProps<any, M> = {
|
|
539
|
+
propertyKey: key,
|
|
540
|
+
disabled,
|
|
541
|
+
property,
|
|
542
|
+
includeDescription: property.description || property.longDescription,
|
|
543
|
+
underlyingValueHasChanged: underlyingValueHasChanged && !autoSave,
|
|
544
|
+
context: formContext,
|
|
545
|
+
partOfArray: false,
|
|
546
|
+
minimalistView: false,
|
|
547
|
+
autoFocus: false
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
return (
|
|
551
|
+
<FormEntry propertyKey={key}
|
|
552
|
+
widthPercentage={widthPercentage}
|
|
553
|
+
key={`field_${key}`}>
|
|
554
|
+
<PropertyFieldBinding {...cmsFormFieldProps} />
|
|
555
|
+
</FormEntry>
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const additionalField = resolvedCollection.additionalFields?.find(f => f.key === key);
|
|
560
|
+
if (additionalField && entity) {
|
|
561
|
+
const Builder = additionalField.Builder;
|
|
562
|
+
if (!Builder && !additionalField.value) {
|
|
563
|
+
throw new Error("When using additional fields you need to provide a Builder or a value");
|
|
564
|
+
}
|
|
565
|
+
const child = Builder
|
|
566
|
+
? <Builder entity={entity} context={context}/>
|
|
567
|
+
: <div className={"w-full"}>
|
|
568
|
+
{additionalField.value?.({
|
|
569
|
+
entity,
|
|
570
|
+
context
|
|
571
|
+
})?.toString()}
|
|
572
|
+
</div>;
|
|
573
|
+
|
|
574
|
+
return (
|
|
575
|
+
<div key={`additional_${key}`} className={"w-full"}>
|
|
576
|
+
<LabelWithIconAndTooltip
|
|
577
|
+
propertyKey={key}
|
|
578
|
+
icon={<NotesIcon size={"small"}/>}
|
|
579
|
+
title={additionalField.name}
|
|
580
|
+
className={"text-text-secondary dark:text-text-secondary-dark ml-3.5"}/>
|
|
581
|
+
<div
|
|
582
|
+
className={cls(paperMixin, "w-full min-h-14 p-4 md:p-6 overflow-x-scroll no-scrollbar")}>
|
|
583
|
+
<ErrorBoundary>
|
|
584
|
+
{child}
|
|
585
|
+
</ErrorBoundary>
|
|
586
|
+
</div>
|
|
587
|
+
</div>
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
console.warn(`Property ${key} not found in collection ${resolvedCollection.name} in properties or additional fields. Skipping.`);
|
|
592
|
+
return null;
|
|
593
|
+
}).filter(Boolean)}
|
|
594
|
+
</FormLayout>
|
|
595
|
+
)
|
|
596
|
+
;
|
|
597
|
+
|
|
598
|
+
const formRef = useRef<HTMLDivElement>(null);
|
|
599
|
+
|
|
600
|
+
const formView = <ErrorBoundary>
|
|
601
|
+
<>
|
|
602
|
+
<div className={"w-full py-2 flex flex-col items-start mt-4 lg:mt-8 mb-8"}>
|
|
603
|
+
<Typography
|
|
604
|
+
className={"py-4 flex-grow line-clamp-1 " + (collection.hideIdFromForm ? "mb-2" : "mb-0")}
|
|
605
|
+
variant={"h4"}>
|
|
606
|
+
{title ?? collection.singularName ?? collection.name}
|
|
607
|
+
</Typography>
|
|
608
|
+
<Alert color={"base"} className={"w-full"} size={"small"}>
|
|
609
|
+
<code
|
|
610
|
+
className={"text-xs select-all text-text-secondary dark:text-text-secondary-dark"}>
|
|
611
|
+
{entity?.path ?? path}/{entityId}
|
|
612
|
+
</code>
|
|
613
|
+
</Alert>
|
|
614
|
+
</div>
|
|
615
|
+
|
|
616
|
+
{!collection.hideIdFromForm &&
|
|
617
|
+
<CustomIdField customId={collection.customId}
|
|
618
|
+
entityId={entityId}
|
|
619
|
+
status={status}
|
|
620
|
+
onChange={setEntityId}
|
|
621
|
+
error={entityIdError}
|
|
622
|
+
loading={customIdLoading}
|
|
623
|
+
entity={entity}/>
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
{entityId && formContext && <>
|
|
627
|
+
<div className="mt-12 flex flex-col gap-8" ref={formRef}>
|
|
628
|
+
{formFields()}
|
|
629
|
+
<ErrorFocus containerRef={formRef}/>
|
|
630
|
+
</div>
|
|
631
|
+
</>}
|
|
632
|
+
|
|
633
|
+
{forceActionsAtTheBottom && <div className="h-16"/>}
|
|
634
|
+
</>
|
|
635
|
+
</ErrorBoundary>;
|
|
636
|
+
|
|
637
|
+
useEffect(() => {
|
|
638
|
+
if (entityId && onIdChange)
|
|
639
|
+
onIdChange(entityId);
|
|
640
|
+
}, [entityId, onIdChange]);
|
|
641
|
+
|
|
642
|
+
return (
|
|
643
|
+
<FormLayoutInner
|
|
644
|
+
className={className}
|
|
645
|
+
id={`form_${path}`}
|
|
646
|
+
pluginActions={pluginActions}
|
|
647
|
+
forceActionsAtTheBottom={forceActionsAtTheBottom}
|
|
648
|
+
formContext={formContext}>
|
|
649
|
+
{formView}
|
|
650
|
+
</FormLayoutInner>
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function getInitialEntityValues<M extends object>(
|
|
655
|
+
collection: EntityCollection,
|
|
656
|
+
path: string,
|
|
657
|
+
status: "new" | "existing" | "copy",
|
|
658
|
+
entity: Entity<M> | undefined
|
|
659
|
+
): Partial<EntityValues<M>> {
|
|
660
|
+
const resolvedCollection = resolveCollection({
|
|
661
|
+
collection,
|
|
662
|
+
path,
|
|
663
|
+
values: entity?.values,
|
|
664
|
+
});
|
|
665
|
+
const properties = resolvedCollection.properties;
|
|
666
|
+
if ((status === "existing" || status === "copy") && entity) {
|
|
667
|
+
return entity.values ?? getDefaultValuesFor(properties);
|
|
668
|
+
} else if (status === "new") {
|
|
669
|
+
return getDefaultValuesFor(properties);
|
|
670
|
+
} else {
|
|
671
|
+
console.error({
|
|
672
|
+
status,
|
|
673
|
+
entity
|
|
674
|
+
});
|
|
675
|
+
throw new Error("Form has not been initialised with the correct parameters");
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export function yupToFormErrors(yupError: ValidationError): Record<string, any> {
|
|
680
|
+
let errors: Record<string, any> = {};
|
|
681
|
+
if (yupError.inner) {
|
|
682
|
+
if (yupError.inner.length === 0) {
|
|
683
|
+
return setIn(errors, yupError.path!, yupError.message);
|
|
684
|
+
}
|
|
685
|
+
for (const err of yupError.inner) {
|
|
686
|
+
if (!getIn(errors, err.path!)) {
|
|
687
|
+
errors = setIn(errors, err.path!, err.message);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return errors;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
export function FormLayoutInner<M extends object>({
|
|
695
|
+
id,
|
|
696
|
+
formContext,
|
|
697
|
+
children,
|
|
698
|
+
className,
|
|
699
|
+
forceActionsAtTheBottom,
|
|
700
|
+
pluginActions
|
|
701
|
+
}: {
|
|
702
|
+
id?: string,
|
|
703
|
+
formContext: FormContext,
|
|
704
|
+
children: React.ReactNode,
|
|
705
|
+
className?: string,
|
|
706
|
+
forceActionsAtTheBottom?: boolean,
|
|
707
|
+
pluginActions?: React.ReactNode[],
|
|
708
|
+
}) {
|
|
709
|
+
|
|
710
|
+
const context = useFireCMSContext();
|
|
711
|
+
const sideEntityController = useSideEntityController();
|
|
712
|
+
const authController = useAuthController();
|
|
713
|
+
|
|
714
|
+
const formex = formContext.formex;
|
|
715
|
+
const collection = formContext.collection;
|
|
716
|
+
const path = formContext.path;
|
|
717
|
+
const entity = formContext.entity;
|
|
718
|
+
const savingError = formContext.savingError;
|
|
719
|
+
const status = formContext.status;
|
|
720
|
+
const openEntityMode = formContext.openEntityMode;
|
|
721
|
+
const setPendingClose = formContext.setPendingClose;
|
|
722
|
+
const disabled = formex.isSubmitting || (!formex.dirty && status === "existing");
|
|
723
|
+
|
|
724
|
+
if (!collection || !path) {
|
|
725
|
+
throw Error("INTERNAL: Collection and path must be defined in form context");
|
|
726
|
+
}
|
|
727
|
+
|
|
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
|
+
});
|
|
782
|
+
|
|
783
|
+
return (
|
|
784
|
+
<Formex value={formContext.formex}>
|
|
785
|
+
<form
|
|
786
|
+
onSubmit={formContext.formex.handleSubmit}
|
|
787
|
+
onReset={() => formex.resetForm({
|
|
788
|
+
values: getInitialEntityValues(collection, path, status, entity),
|
|
789
|
+
})}
|
|
790
|
+
noValidate
|
|
791
|
+
className={cls("flex-1 flex flex-row w-full overflow-y-auto justify-center", className)}>
|
|
792
|
+
<div
|
|
793
|
+
id={id}
|
|
794
|
+
className={cls("relative flex flex-row max-w-4xl lg:max-w-3xl xl:max-w-4xl 2xl:max-w-6xl w-full h-fit")}>
|
|
795
|
+
|
|
796
|
+
<div className={cls("flex flex-col w-full pt-12 pb-16 px-4 sm:px-8 md:px-10")}>
|
|
797
|
+
|
|
798
|
+
{formContext.formex.dirty
|
|
799
|
+
? <Tooltip title={"Unsaved changes"}
|
|
800
|
+
className={"self-end sticky top-4 z-10"}>
|
|
801
|
+
<Chip size={"small"} colorScheme={"orangeDarker"}>
|
|
802
|
+
<EditIcon size={"smallest"}/>
|
|
803
|
+
</Chip>
|
|
804
|
+
</Tooltip>
|
|
805
|
+
: <Tooltip title={"In sync with the database"}
|
|
806
|
+
className={"self-end sticky top-4 z-10"}>
|
|
807
|
+
<Chip size={"small"}>
|
|
808
|
+
<CheckIcon size={"smallest"}/>
|
|
809
|
+
</Chip>
|
|
810
|
+
</Tooltip>}
|
|
811
|
+
{children}
|
|
812
|
+
|
|
813
|
+
</div>
|
|
814
|
+
|
|
815
|
+
</div>
|
|
816
|
+
{dialogActions}
|
|
817
|
+
</form>
|
|
818
|
+
|
|
819
|
+
</Formex>
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
|
|
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" color="primary" type="submit" loading={isSubmitting}
|
|
895
|
+
disabled={disabled} onClick={() => {
|
|
896
|
+
setPendingClose?.(true);
|
|
897
|
+
}}>
|
|
898
|
+
{status === "existing" && "Save and close"}
|
|
899
|
+
{status === "copy" && "Create copy and close"}
|
|
900
|
+
{status === "new" && "Create and close"}
|
|
901
|
+
</LoadingButton>}
|
|
902
|
+
</DialogActions>;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function buildSideActions<M extends object>({
|
|
906
|
+
savingError,
|
|
907
|
+
entity,
|
|
908
|
+
formActions,
|
|
909
|
+
collection,
|
|
910
|
+
context,
|
|
911
|
+
sideEntityController,
|
|
912
|
+
isSubmitting,
|
|
913
|
+
disabled,
|
|
914
|
+
status,
|
|
915
|
+
setPendingClose,
|
|
916
|
+
pluginActions
|
|
917
|
+
}: ActionsViewProps<M>) {
|
|
918
|
+
|
|
919
|
+
return <div
|
|
920
|
+
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)}>
|
|
921
|
+
<LoadingButton fullWidth={true} variant="filled" color="primary" type="submit" size={"large"}
|
|
922
|
+
disabled={disabled || isSubmitting} onClick={() => {
|
|
923
|
+
setPendingClose?.(false);
|
|
924
|
+
}}>
|
|
925
|
+
{status === "existing" && "Save"}
|
|
926
|
+
{status === "copy" && "Create copy"}
|
|
927
|
+
{status === "new" && "Create"}
|
|
928
|
+
</LoadingButton>
|
|
929
|
+
<Button fullWidth={true} variant="text" disabled={disabled || isSubmitting} type="reset">
|
|
930
|
+
{status === "existing" ? "Discard" : "Clear"}
|
|
931
|
+
</Button>
|
|
932
|
+
|
|
933
|
+
{pluginActions}
|
|
934
|
+
|
|
935
|
+
{savingError &&
|
|
936
|
+
<div className="text-right">
|
|
937
|
+
<Typography color={"error"}>{savingError.message}</Typography>
|
|
938
|
+
</div>
|
|
939
|
+
}
|
|
940
|
+
</div>;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function useOnAutoSave(autoSave: undefined | boolean, formex: FormexController<any>, lastSavedValues: any, save: (values: EntityValues<any>) => Promise<void>) {
|
|
944
|
+
if (!autoSave) return;
|
|
945
|
+
useEffect(() => {
|
|
946
|
+
if (autoSave) {
|
|
947
|
+
if (formex.values && !equal(formex.values, lastSavedValues.current)) {
|
|
948
|
+
save(formex.values);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}, [formex.values]);
|
|
952
|
+
}
|