@firecms/core 3.0.0-canary.283 → 3.0.0-canary.285
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 +2 -2
- package/dist/form/EntityForm.d.ts +3 -1
- package/dist/form/index.d.ts +2 -1
- package/dist/index.es.js +123 -120
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +121 -118
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collections.d.ts +7 -0
- package/dist/util/entity_cache.d.ts +2 -1
- package/package.json +5 -5
- package/src/core/EntityEditView.tsx +11 -10
- package/src/core/EntityEditViewFormActions.tsx +33 -18
- package/src/form/EntityForm.tsx +49 -25
- package/src/form/EntityFormActions.tsx +30 -15
- package/src/form/components/ErrorFocus.tsx +22 -29
- package/src/form/index.tsx +5 -1
- package/src/types/collections.ts +8 -0
- package/src/util/entity_cache.ts +22 -34
|
@@ -307,6 +307,13 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
|
|
|
307
307
|
* This prop has no effect if the history plugin is not enabled
|
|
308
308
|
*/
|
|
309
309
|
history?: boolean;
|
|
310
|
+
/**
|
|
311
|
+
* If set to true, local changes to entities in this collection will be backed up
|
|
312
|
+
* in the browser's local storage. This allows users to recover unsaved changes
|
|
313
|
+
* in case of accidental navigation or browser crashes.
|
|
314
|
+
* Defaults to `true`.
|
|
315
|
+
*/
|
|
316
|
+
enableLocalChangesBackup?: boolean;
|
|
310
317
|
}
|
|
311
318
|
/**
|
|
312
319
|
* Parameter passed to the `Actions` prop in the collection configuration.
|
|
@@ -8,9 +8,10 @@ export declare function saveEntityToCache(path: string, data: object): void;
|
|
|
8
8
|
* Retrieves an entity from the in-memory cache or `localStorage`.
|
|
9
9
|
* If the entity is not in the cache but exists in `localStorage`, it loads it into the cache.
|
|
10
10
|
* @param path - The unique path/key for the entity.
|
|
11
|
+
* @param useLocalStorage
|
|
11
12
|
* @returns The cached entity or `undefined` if not found.
|
|
12
13
|
*/
|
|
13
|
-
export declare function getEntityFromCache(path: string): object | undefined;
|
|
14
|
+
export declare function getEntityFromCache(path: string, useLocalStorage?: boolean): object | undefined;
|
|
14
15
|
export declare function hasEntityInCache(path: string): boolean;
|
|
15
16
|
/**
|
|
16
17
|
* Removes an entity from both the in-memory cache and `localStorage`.
|
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.
|
|
4
|
+
"version": "3.0.0-canary.285",
|
|
5
5
|
"description": "Awesome Firebase/Firestore-based headless open-source CMS",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/firecmsco"
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"@dnd-kit/core": "^6.3.1",
|
|
54
54
|
"@dnd-kit/modifiers": "^9.0.0",
|
|
55
55
|
"@dnd-kit/sortable": "^10.0.0",
|
|
56
|
-
"@firecms/editor": "^3.0.0-canary.
|
|
57
|
-
"@firecms/formex": "^3.0.0-canary.
|
|
58
|
-
"@firecms/ui": "^3.0.0-canary.
|
|
56
|
+
"@firecms/editor": "^3.0.0-canary.285",
|
|
57
|
+
"@firecms/formex": "^3.0.0-canary.285",
|
|
58
|
+
"@firecms/ui": "^3.0.0-canary.285",
|
|
59
59
|
"@radix-ui/react-portal": "^1.1.9",
|
|
60
60
|
"clsx": "^2.1.1",
|
|
61
61
|
"date-fns": "^3.6.0",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"dist",
|
|
109
109
|
"src"
|
|
110
110
|
],
|
|
111
|
-
"gitHead": "
|
|
111
|
+
"gitHead": "a442a00b64764b353c977cc9e758953995bd9513",
|
|
112
112
|
"publishConfig": {
|
|
113
113
|
"access": "public"
|
|
114
114
|
},
|
|
@@ -97,9 +97,11 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
|
|
|
97
97
|
useCache: false
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
const enableLocalChangesBackup = props.collection.enableLocalChangesBackup !== undefined ? props.collection.enableLocalChangesBackup : true;
|
|
101
|
+
|
|
102
|
+
const initialDirtyValues = entityId
|
|
103
|
+
? getEntityFromCache(props.path + "/" + entityId, enableLocalChangesBackup)
|
|
104
|
+
: getEntityFromCache(props.path + "#new", enableLocalChangesBackup);
|
|
103
105
|
|
|
104
106
|
const authController = useAuthController();
|
|
105
107
|
|
|
@@ -114,18 +116,18 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
|
|
|
114
116
|
}
|
|
115
117
|
}, [authController, entity, status]);
|
|
116
118
|
|
|
117
|
-
if ((dataLoading && !
|
|
119
|
+
if ((dataLoading && !initialDirtyValues) || (!entity || canEdit === undefined) && (status === "existing" || status === "copy")) {
|
|
118
120
|
return <CircularProgressCenter/>;
|
|
119
121
|
}
|
|
120
122
|
|
|
121
|
-
if (entityId && !entity && !
|
|
123
|
+
if (entityId && !entity && !initialDirtyValues) {
|
|
122
124
|
console.error(`Entity with id ${entityId} not found in collection ${props.path}`);
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
return <EntityEditViewInner<M> {...props}
|
|
126
128
|
entityId={entityId}
|
|
127
129
|
entity={entity}
|
|
128
|
-
|
|
130
|
+
initialDirtyValues={initialDirtyValues as Partial<M>}
|
|
129
131
|
dataLoading={dataLoading}
|
|
130
132
|
status={status}
|
|
131
133
|
setStatus={setStatus}
|
|
@@ -144,7 +146,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
144
146
|
onSaved,
|
|
145
147
|
onTabChange,
|
|
146
148
|
entity,
|
|
147
|
-
|
|
149
|
+
initialDirtyValues,
|
|
148
150
|
dataLoading,
|
|
149
151
|
layout = "side_panel",
|
|
150
152
|
barActions,
|
|
@@ -154,7 +156,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
154
156
|
canEdit
|
|
155
157
|
}: EntityEditViewProps<M> & {
|
|
156
158
|
entity?: Entity<M>,
|
|
157
|
-
|
|
159
|
+
initialDirtyValues?: Partial<M>, // dirty cached entity in memory
|
|
158
160
|
dataLoading: boolean,
|
|
159
161
|
status: EntityStatus,
|
|
160
162
|
setStatus: (status: EntityStatus) => void,
|
|
@@ -379,7 +381,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
379
381
|
entityId={entityId ?? usedEntity?.id}
|
|
380
382
|
onValuesModified={onValuesModified}
|
|
381
383
|
entity={entity}
|
|
382
|
-
initialDirtyValues={
|
|
384
|
+
initialDirtyValues={initialDirtyValues}
|
|
383
385
|
openEntityMode={layout}
|
|
384
386
|
forceActionsAtTheBottom={actionsAtTheBottom}
|
|
385
387
|
initialStatus={status}
|
|
@@ -523,4 +525,3 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
523
525
|
|
|
524
526
|
return result;
|
|
525
527
|
}
|
|
526
|
-
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
cls,
|
|
18
18
|
defaultBorderMixin,
|
|
19
19
|
DialogActions,
|
|
20
|
+
ErrorIcon,
|
|
20
21
|
IconButton,
|
|
21
22
|
LoadingButton,
|
|
22
23
|
Tooltip,
|
|
@@ -31,6 +32,8 @@ import {
|
|
|
31
32
|
} from "../hooks";
|
|
32
33
|
import { EntityFormActionsProps } from "../form/EntityFormActions";
|
|
33
34
|
import { SideDialogController, useSideDialogContext } from "./SideDialogs";
|
|
35
|
+
import { FormexController } from "@firecms/formex";
|
|
36
|
+
import { ErrorTooltip } from "../components/ErrorTooltip";
|
|
34
37
|
|
|
35
38
|
export function EntityEditViewFormActions({
|
|
36
39
|
collection,
|
|
@@ -80,14 +83,14 @@ export function EntityEditViewFormActions({
|
|
|
80
83
|
collection,
|
|
81
84
|
context,
|
|
82
85
|
sideEntityController,
|
|
83
|
-
isSubmitting: formex.isSubmitting,
|
|
84
86
|
disabled,
|
|
85
87
|
status,
|
|
86
88
|
sideDialogContext,
|
|
87
89
|
pluginActions,
|
|
88
90
|
openEntityMode,
|
|
89
91
|
navigateBack,
|
|
90
|
-
formContext
|
|
92
|
+
formContext,
|
|
93
|
+
formex
|
|
91
94
|
})
|
|
92
95
|
: buildSideActions({
|
|
93
96
|
savingError,
|
|
@@ -96,14 +99,14 @@ export function EntityEditViewFormActions({
|
|
|
96
99
|
collection,
|
|
97
100
|
context,
|
|
98
101
|
sideEntityController,
|
|
99
|
-
isSubmitting: formex.isSubmitting,
|
|
100
102
|
sideDialogContext,
|
|
101
103
|
disabled,
|
|
102
104
|
status,
|
|
103
105
|
pluginActions,
|
|
104
106
|
openEntityMode,
|
|
105
107
|
navigateBack,
|
|
106
|
-
formContext
|
|
108
|
+
formContext,
|
|
109
|
+
formex
|
|
107
110
|
});
|
|
108
111
|
}
|
|
109
112
|
|
|
@@ -114,14 +117,14 @@ type ActionsViewProps<M extends object> = {
|
|
|
114
117
|
collection: ResolvedEntityCollection,
|
|
115
118
|
context: FireCMSContext,
|
|
116
119
|
sideEntityController: SideEntityController,
|
|
117
|
-
isSubmitting: boolean,
|
|
118
120
|
disabled: boolean,
|
|
119
121
|
status: "new" | "existing" | "copy",
|
|
120
122
|
sideDialogContext: SideDialogController,
|
|
121
123
|
pluginActions?: React.ReactNode[],
|
|
122
124
|
openEntityMode: "side_panel" | "full_screen";
|
|
123
125
|
navigateBack: () => void;
|
|
124
|
-
formContext: FormContext
|
|
126
|
+
formContext: FormContext,
|
|
127
|
+
formex: FormexController<any>;
|
|
125
128
|
};
|
|
126
129
|
|
|
127
130
|
function buildBottomActions<M extends object>({
|
|
@@ -131,16 +134,17 @@ function buildBottomActions<M extends object>({
|
|
|
131
134
|
collection,
|
|
132
135
|
context,
|
|
133
136
|
sideEntityController,
|
|
134
|
-
isSubmitting,
|
|
135
137
|
disabled,
|
|
136
138
|
status,
|
|
137
139
|
sideDialogContext,
|
|
138
140
|
pluginActions,
|
|
139
141
|
openEntityMode,
|
|
140
142
|
navigateBack,
|
|
141
|
-
formContext
|
|
143
|
+
formContext,
|
|
144
|
+
formex
|
|
142
145
|
}: ActionsViewProps<M>) {
|
|
143
146
|
|
|
147
|
+
const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
|
|
144
148
|
const canClose = openEntityMode === "side_panel";
|
|
145
149
|
return <DialogActions
|
|
146
150
|
position={"absolute"}>
|
|
@@ -179,16 +183,20 @@ function buildBottomActions<M extends object>({
|
|
|
179
183
|
|
|
180
184
|
{pluginActions}
|
|
181
185
|
|
|
186
|
+
{hasErrors ?
|
|
187
|
+
<ErrorTooltip title={"This form has errors"}>
|
|
188
|
+
<ErrorIcon className="ml-4" color="error" size={"smallest"}/>
|
|
189
|
+
</ErrorTooltip> : null}
|
|
182
190
|
<Button variant="text"
|
|
183
191
|
color="primary"
|
|
184
|
-
disabled={disabled || isSubmitting}
|
|
192
|
+
disabled={disabled || formex.isSubmitting}
|
|
185
193
|
type="reset">
|
|
186
194
|
{status === "existing" ? "Discard" : "Clear"}
|
|
187
195
|
</Button>
|
|
188
196
|
<Button variant={canClose ? "text" : "filled"}
|
|
189
197
|
color="primary"
|
|
190
198
|
type="submit"
|
|
191
|
-
disabled={disabled || isSubmitting}
|
|
199
|
+
disabled={disabled || formex.isSubmitting}
|
|
192
200
|
onClick={() => {
|
|
193
201
|
sideDialogContext.setPendingClose(false);
|
|
194
202
|
}}>
|
|
@@ -199,7 +207,7 @@ function buildBottomActions<M extends object>({
|
|
|
199
207
|
{canClose && <LoadingButton variant="filled"
|
|
200
208
|
color="primary"
|
|
201
209
|
type="submit"
|
|
202
|
-
loading={isSubmitting}
|
|
210
|
+
loading={formex.isSubmitting}
|
|
203
211
|
disabled={disabled}
|
|
204
212
|
onClick={() => {
|
|
205
213
|
sideDialogContext.setPendingClose?.(true);
|
|
@@ -218,28 +226,35 @@ function buildSideActions<M extends object>({
|
|
|
218
226
|
collection,
|
|
219
227
|
context,
|
|
220
228
|
sideEntityController,
|
|
221
|
-
isSubmitting,
|
|
222
229
|
disabled,
|
|
223
230
|
status,
|
|
224
231
|
sideDialogContext,
|
|
225
232
|
pluginActions,
|
|
226
233
|
openEntityMode,
|
|
227
234
|
navigateBack,
|
|
228
|
-
formContext
|
|
235
|
+
formContext,
|
|
236
|
+
formex
|
|
229
237
|
}: ActionsViewProps<M>) {
|
|
230
238
|
|
|
239
|
+
const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
|
|
231
240
|
return <div
|
|
232
241
|
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)}>
|
|
233
|
-
<LoadingButton fullWidth={true}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
242
|
+
<LoadingButton fullWidth={true}
|
|
243
|
+
variant="filled"
|
|
244
|
+
color="primary"
|
|
245
|
+
type="submit"
|
|
246
|
+
size={"large"}
|
|
247
|
+
startIcon={hasErrors ? <ErrorIcon/> : undefined}
|
|
248
|
+
disabled={disabled || formex.isSubmitting}
|
|
249
|
+
onClick={() => {
|
|
250
|
+
sideDialogContext.setPendingClose?.(false);
|
|
251
|
+
}}>
|
|
237
252
|
{status === "existing" && "Save"}
|
|
238
253
|
{status === "copy" && "Create copy"}
|
|
239
254
|
{status === "new" && "Create"}
|
|
240
255
|
</LoadingButton>
|
|
241
256
|
|
|
242
|
-
<Button fullWidth={true} variant="text" disabled={disabled || isSubmitting} type="reset">
|
|
257
|
+
<Button fullWidth={true} variant="text" disabled={disabled || formex.isSubmitting} type="reset">
|
|
243
258
|
{status === "existing" ? "Discard" : "Clear"}
|
|
244
259
|
</Button>
|
|
245
260
|
|
package/src/form/EntityForm.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback,
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
AuthController,
|
|
4
4
|
CMSAnalyticsEvent,
|
|
@@ -32,19 +32,20 @@ import {
|
|
|
32
32
|
useAuthController,
|
|
33
33
|
useCustomizationController,
|
|
34
34
|
useDataSource,
|
|
35
|
-
useFireCMSContext,
|
|
35
|
+
useFireCMSContext,
|
|
36
|
+
useNavigationController,
|
|
36
37
|
useSideEntityController,
|
|
37
38
|
useSnackbarController
|
|
38
39
|
} from "../hooks";
|
|
39
40
|
import { Alert, CheckIcon, Chip, cls, EditIcon, NotesIcon, paperMixin, Tooltip, Typography } from "@firecms/ui";
|
|
40
|
-
import { Formex, FormexController, getIn, setIn, useCreateFormex } from "@firecms/formex";
|
|
41
|
+
import { flattenKeys, Formex, FormexController, getIn, setIn, useCreateFormex } from "@firecms/formex";
|
|
41
42
|
import { useAnalyticsController } from "../hooks/useAnalyticsController";
|
|
42
43
|
import { FormEntry, FormLayout, LabelWithIconAndTooltip, PropertyFieldBinding } from "../form";
|
|
43
44
|
import { ValidationError } from "yup";
|
|
44
45
|
import { removeEntityFromCache, saveEntityToCache } from "../util/entity_cache";
|
|
45
|
-
import { CustomIdField } from "
|
|
46
|
-
import { ErrorFocus } from "
|
|
47
|
-
import { CustomFieldValidator, getYupEntitySchema } from "
|
|
46
|
+
import { CustomIdField } from "./components/CustomIdField";
|
|
47
|
+
import { ErrorFocus } from "./components/ErrorFocus";
|
|
48
|
+
import { CustomFieldValidator, getYupEntitySchema } from "./validation";
|
|
48
49
|
import { EntityFormActions, EntityFormActionsProps } from "./EntityFormActions";
|
|
49
50
|
|
|
50
51
|
export type OnUpdateParams = {
|
|
@@ -96,6 +97,22 @@ export type EntityFormProps<M extends Record<string, any>> = {
|
|
|
96
97
|
children?: React.ReactNode;
|
|
97
98
|
};
|
|
98
99
|
|
|
100
|
+
// extract touched values for nested touched trees and map to current values
|
|
101
|
+
export function extractTouchedValues(values: any, touched: Record<string, boolean>): Record<string, any> {
|
|
102
|
+
let acc: Record<string, any> = {};
|
|
103
|
+
if (!touched || typeof touched !== "object") {
|
|
104
|
+
return acc;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
Object.entries(touched).forEach(([key, value]) => {
|
|
108
|
+
if (value) {
|
|
109
|
+
acc = setIn(acc, key, getIn(values, key));
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
return acc;
|
|
114
|
+
}
|
|
115
|
+
|
|
99
116
|
export function EntityForm<M extends Record<string, any>>({
|
|
100
117
|
path,
|
|
101
118
|
fullIdPath,
|
|
@@ -126,7 +143,6 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
126
143
|
console.warn(`The collection ${collection.path} has customId and formAutoSave enabled. This is not supported and formAutoSave will be ignored`);
|
|
127
144
|
}
|
|
128
145
|
|
|
129
|
-
|
|
130
146
|
const sideEntityController = useSideEntityController();
|
|
131
147
|
const navigationController = useNavigationController();
|
|
132
148
|
|
|
@@ -164,7 +180,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
164
180
|
const context = useFireCMSContext();
|
|
165
181
|
const analyticsController = useAnalyticsController();
|
|
166
182
|
|
|
167
|
-
const [underlyingChanges
|
|
183
|
+
const [underlyingChanges] = useState<Partial<EntityValues<M>>>({});
|
|
168
184
|
|
|
169
185
|
const [customIdLoading, setCustomIdLoading] = useState<boolean>(false);
|
|
170
186
|
|
|
@@ -226,14 +242,31 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
226
242
|
});
|
|
227
243
|
};
|
|
228
244
|
|
|
245
|
+
const baseInitialValues = getInitialEntityValues(authController, collection, path, status, entity, customizationController.propertyConfigs);
|
|
246
|
+
const initialValues = initialDirtyValues ? mergeDeep(baseInitialValues, initialDirtyValues) : baseInitialValues;
|
|
247
|
+
const initialDirty = Boolean(initialDirtyValues) && initialDirtyValues && Object.keys(initialDirtyValues).length > 0;
|
|
229
248
|
const formex: FormexController<M> = formexProp ?? useCreateFormex<M>({
|
|
230
|
-
initialValues:
|
|
231
|
-
initialDirty
|
|
249
|
+
initialValues: initialValues as M,
|
|
250
|
+
initialDirty,
|
|
251
|
+
initialTouched: initialDirtyValues ?
|
|
252
|
+
flattenKeys(initialDirtyValues!)
|
|
253
|
+
.reduce((previousValue, currentValue) => ({
|
|
254
|
+
...previousValue,
|
|
255
|
+
[currentValue]: true
|
|
256
|
+
}), {})
|
|
257
|
+
: {},
|
|
232
258
|
onSubmit,
|
|
233
259
|
onReset: () => {
|
|
234
260
|
clearDirtyCache();
|
|
235
261
|
onValuesModified?.(false);
|
|
236
262
|
},
|
|
263
|
+
onValuesChangeDeferred: (values: M, controller: FormexController<M>) => {
|
|
264
|
+
const key = (status === "new" || status === "copy") ? path + "#new" : path + "/" + entityId;
|
|
265
|
+
if (controller.dirty) {
|
|
266
|
+
const touchedValues = extractTouchedValues(values, controller.touched);
|
|
267
|
+
saveEntityToCache(key, touchedValues);
|
|
268
|
+
}
|
|
269
|
+
},
|
|
237
270
|
validation: (values) => {
|
|
238
271
|
return validationSchema?.validate(values, { abortEarly: false })
|
|
239
272
|
.then(() => {
|
|
@@ -405,7 +438,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
405
438
|
values,
|
|
406
439
|
previousValues: entity?.values,
|
|
407
440
|
autoSave: autoSave ?? false
|
|
408
|
-
}).then((
|
|
441
|
+
}).then(() => {
|
|
409
442
|
const eventName: CMSAnalyticsEvent = status === "new"
|
|
410
443
|
? "new_entity_saved"
|
|
411
444
|
: (status === "copy" ? "entity_copied" : (status === "existing" ? "entity_edited" : "unmapped_event"));
|
|
@@ -443,7 +476,8 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
443
476
|
type: "error",
|
|
444
477
|
message: "Error updating id, check the console"
|
|
445
478
|
});
|
|
446
|
-
|
|
479
|
+
console.error(error);
|
|
480
|
+
}, [snackbarController]);
|
|
447
481
|
|
|
448
482
|
const pluginActions: React.ReactNode[] = [];
|
|
449
483
|
const plugins = customizationController.plugins;
|
|
@@ -507,13 +541,11 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
507
541
|
}
|
|
508
542
|
}, [formex.dirty]);
|
|
509
543
|
|
|
510
|
-
const deferredValues = useDeferredValue(formex.values);
|
|
511
544
|
const modified = formex.dirty;
|
|
512
545
|
|
|
513
546
|
const uniqueFieldValidator: CustomFieldValidator = useCallback(({
|
|
514
547
|
name,
|
|
515
|
-
value
|
|
516
|
-
property
|
|
548
|
+
value
|
|
517
549
|
}) => dataSource.checkUniqueField(path, name, value, entityId, collection),
|
|
518
550
|
[dataSource, path, entityId]);
|
|
519
551
|
|
|
@@ -525,13 +557,6 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
525
557
|
: undefined,
|
|
526
558
|
[entityId, resolvedCollection.properties, uniqueFieldValidator]);
|
|
527
559
|
|
|
528
|
-
useEffect(() => {
|
|
529
|
-
const key = (status === "new" || status === "copy") ? path + "#new" : path + "/" + entityId;
|
|
530
|
-
if (modified) {
|
|
531
|
-
saveEntityToCache(key, deferredValues);
|
|
532
|
-
}
|
|
533
|
-
}, [deferredValues, modified, path, entityId, status]);
|
|
534
|
-
|
|
535
560
|
useOnAutoSave(autoSave, formex, lastSavedValues, save);
|
|
536
561
|
|
|
537
562
|
useEffect(() => {
|
|
@@ -716,7 +741,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
716
741
|
<form
|
|
717
742
|
onSubmit={formex.handleSubmit}
|
|
718
743
|
onReset={() => formex.resetForm({
|
|
719
|
-
values:
|
|
744
|
+
values: baseInitialValues as M
|
|
720
745
|
})}
|
|
721
746
|
noValidate
|
|
722
747
|
className={cls("flex-1 flex flex-row w-full overflow-y-auto justify-center", className)}>
|
|
@@ -752,7 +777,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
752
777
|
);
|
|
753
778
|
}
|
|
754
779
|
|
|
755
|
-
function getInitialEntityValues<M extends object>(
|
|
780
|
+
export function getInitialEntityValues<M extends object>(
|
|
756
781
|
authController: AuthController,
|
|
757
782
|
collection: EntityCollection,
|
|
758
783
|
path: string,
|
|
@@ -811,4 +836,3 @@ function useOnAutoSave(autoSave: undefined | boolean, formex: FormexController<a
|
|
|
811
836
|
}
|
|
812
837
|
}, [formex.values]);
|
|
813
838
|
}
|
|
814
|
-
|
|
@@ -7,7 +7,16 @@ import {
|
|
|
7
7
|
ResolvedEntityCollection,
|
|
8
8
|
SideEntityController
|
|
9
9
|
} from "../types";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
Button,
|
|
12
|
+
cls,
|
|
13
|
+
defaultBorderMixin,
|
|
14
|
+
DialogActions,
|
|
15
|
+
ErrorIcon,
|
|
16
|
+
IconButton,
|
|
17
|
+
LoadingButton,
|
|
18
|
+
Typography
|
|
19
|
+
} from "@firecms/ui";
|
|
11
20
|
import { FormexController } from "@firecms/formex";
|
|
12
21
|
import { useFireCMSContext, useSideEntityController } from "../hooks";
|
|
13
22
|
|
|
@@ -57,13 +66,13 @@ export function EntityFormActions({
|
|
|
57
66
|
collection,
|
|
58
67
|
context,
|
|
59
68
|
sideEntityController,
|
|
60
|
-
isSubmitting: formex.isSubmitting,
|
|
61
69
|
disabled,
|
|
62
70
|
status,
|
|
63
71
|
pluginActions,
|
|
64
72
|
openEntityMode,
|
|
65
73
|
navigateBack,
|
|
66
|
-
formContext
|
|
74
|
+
formContext,
|
|
75
|
+
formex
|
|
67
76
|
})
|
|
68
77
|
: buildSideActions({
|
|
69
78
|
fullPath,
|
|
@@ -73,13 +82,13 @@ export function EntityFormActions({
|
|
|
73
82
|
collection,
|
|
74
83
|
context,
|
|
75
84
|
sideEntityController,
|
|
76
|
-
isSubmitting: formex.isSubmitting,
|
|
77
85
|
disabled,
|
|
78
86
|
status,
|
|
79
87
|
pluginActions,
|
|
80
88
|
openEntityMode,
|
|
81
89
|
navigateBack,
|
|
82
|
-
formContext
|
|
90
|
+
formContext,
|
|
91
|
+
formex
|
|
83
92
|
});
|
|
84
93
|
}
|
|
85
94
|
|
|
@@ -92,13 +101,13 @@ type ActionsViewProps<M extends object> = {
|
|
|
92
101
|
collection: ResolvedEntityCollection,
|
|
93
102
|
context: FireCMSContext,
|
|
94
103
|
sideEntityController: SideEntityController,
|
|
95
|
-
isSubmitting: boolean,
|
|
96
104
|
disabled: boolean,
|
|
97
105
|
status: "new" | "existing" | "copy",
|
|
98
106
|
pluginActions?: React.ReactNode[],
|
|
99
107
|
openEntityMode: "side_panel" | "full_screen";
|
|
100
108
|
navigateBack: () => void;
|
|
101
|
-
formContext: FormContext
|
|
109
|
+
formContext: FormContext,
|
|
110
|
+
formex: FormexController<any>;
|
|
102
111
|
};
|
|
103
112
|
|
|
104
113
|
function buildBottomActions<M extends object>({
|
|
@@ -110,15 +119,17 @@ function buildBottomActions<M extends object>({
|
|
|
110
119
|
collection,
|
|
111
120
|
context,
|
|
112
121
|
sideEntityController,
|
|
113
|
-
isSubmitting,
|
|
114
122
|
disabled,
|
|
115
123
|
status,
|
|
116
124
|
pluginActions,
|
|
117
125
|
openEntityMode,
|
|
118
126
|
navigateBack,
|
|
119
|
-
formContext
|
|
127
|
+
formContext,
|
|
128
|
+
formex
|
|
120
129
|
}: ActionsViewProps<M>) {
|
|
121
130
|
|
|
131
|
+
const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
|
|
132
|
+
|
|
122
133
|
return <DialogActions position={"absolute"}>
|
|
123
134
|
{savingError &&
|
|
124
135
|
<div className="text-right">
|
|
@@ -151,7 +162,7 @@ function buildBottomActions<M extends object>({
|
|
|
151
162
|
))}
|
|
152
163
|
</div>}
|
|
153
164
|
{pluginActions}
|
|
154
|
-
<Button variant="text" disabled={disabled || isSubmitting}
|
|
165
|
+
<Button variant="text" disabled={disabled || formex.isSubmitting}
|
|
155
166
|
color={"primary"}
|
|
156
167
|
type="reset">
|
|
157
168
|
{status === "existing" ? "Discard" : "Clear"}
|
|
@@ -159,7 +170,8 @@ function buildBottomActions<M extends object>({
|
|
|
159
170
|
<Button variant={"filled"}
|
|
160
171
|
color="primary"
|
|
161
172
|
type="submit"
|
|
162
|
-
disabled={disabled || isSubmitting}
|
|
173
|
+
disabled={disabled || formex.isSubmitting}
|
|
174
|
+
startIcon={hasErrors ? <ErrorIcon/> : undefined}>
|
|
163
175
|
{status === "existing" && "Save"}
|
|
164
176
|
{status === "copy" && "Create copy"}
|
|
165
177
|
{status === "new" && "Create"}
|
|
@@ -178,12 +190,14 @@ function buildSideActions<M extends object>({
|
|
|
178
190
|
collection,
|
|
179
191
|
context,
|
|
180
192
|
sideEntityController,
|
|
181
|
-
isSubmitting,
|
|
182
193
|
disabled,
|
|
183
194
|
status,
|
|
184
|
-
pluginActions
|
|
195
|
+
pluginActions,
|
|
196
|
+
formex
|
|
185
197
|
}: ActionsViewProps<M>) {
|
|
186
198
|
|
|
199
|
+
const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
|
|
200
|
+
|
|
187
201
|
return <div
|
|
188
202
|
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)}>
|
|
189
203
|
<LoadingButton fullWidth={true}
|
|
@@ -191,12 +205,13 @@ function buildSideActions<M extends object>({
|
|
|
191
205
|
color="primary"
|
|
192
206
|
type="submit"
|
|
193
207
|
size={"large"}
|
|
194
|
-
|
|
208
|
+
startIcon={hasErrors ? <ErrorIcon/> : undefined}
|
|
209
|
+
disabled={disabled || formex.isSubmitting}>
|
|
195
210
|
{status === "existing" && "Save"}
|
|
196
211
|
{status === "copy" && "Create copy"}
|
|
197
212
|
{status === "new" && "Create"}
|
|
198
213
|
</LoadingButton>
|
|
199
|
-
<Button fullWidth={true} variant="text" disabled={disabled || isSubmitting} type="reset">
|
|
214
|
+
<Button fullWidth={true} variant="text" disabled={disabled || formex.isSubmitting} type="reset">
|
|
200
215
|
{status === "existing" ? "Discard" : "Clear"}
|
|
201
216
|
</Button>
|
|
202
217
|
|
|
@@ -1,51 +1,44 @@
|
|
|
1
|
-
import React, { useEffect } from "react";
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
2
|
import { useFormex } from "@firecms/formex";
|
|
3
3
|
|
|
4
4
|
export const ErrorFocus = ({ containerRef }:
|
|
5
5
|
{
|
|
6
6
|
containerRef?: React.RefObject<HTMLDivElement>
|
|
7
7
|
}) => {
|
|
8
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
isValidating,
|
|
10
|
+
errors,
|
|
11
|
+
version
|
|
12
|
+
} = useFormex();
|
|
13
|
+
|
|
14
|
+
const prevVersion = useRef(version);
|
|
9
15
|
|
|
10
16
|
useEffect(() => {
|
|
17
|
+
|
|
18
|
+
if (version === prevVersion.current) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
const keys = Object.keys(errors);
|
|
12
23
|
|
|
13
|
-
// Whenever there are errors and the form
|
|
14
|
-
if (keys.length > 0
|
|
24
|
+
// Whenever there are errors and the form has been submitted and is not validating
|
|
25
|
+
if (!isValidating && keys.length > 0) {
|
|
15
26
|
const errorElement = containerRef?.current?.querySelector<HTMLDivElement>(
|
|
16
27
|
`#form_field_${keys[0]}`
|
|
17
28
|
);
|
|
18
29
|
|
|
19
|
-
if (errorElement
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
top: scrollableParent.scrollTop + top - 196,
|
|
25
|
-
behavior: "smooth"
|
|
26
|
-
});
|
|
27
|
-
}
|
|
30
|
+
if (errorElement) {
|
|
31
|
+
errorElement.scrollIntoView({
|
|
32
|
+
behavior: "smooth",
|
|
33
|
+
block: "center"
|
|
34
|
+
});
|
|
28
35
|
const input = errorElement.querySelector("input");
|
|
29
36
|
if (input) input.focus();
|
|
30
37
|
}
|
|
38
|
+
prevVersion.current = version;
|
|
31
39
|
}
|
|
32
|
-
}, [
|
|
40
|
+
}, [isValidating, errors, containerRef, version]);
|
|
33
41
|
|
|
34
42
|
// This component does not render anything by itself.
|
|
35
43
|
return null;
|
|
36
44
|
};
|
|
37
|
-
|
|
38
|
-
const isScrollable = (ele: HTMLElement | null) => {
|
|
39
|
-
const hasScrollableContent = ele && ele.scrollHeight > ele.clientHeight;
|
|
40
|
-
|
|
41
|
-
const overflowYStyle = ele ? window.getComputedStyle(ele).overflowY : null;
|
|
42
|
-
const isOverflowHidden = overflowYStyle && overflowYStyle.indexOf("hidden") !== -1;
|
|
43
|
-
|
|
44
|
-
return hasScrollableContent && !isOverflowHidden;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const getScrollableParent = (ele: HTMLElement | null): HTMLElement | null => {
|
|
48
|
-
return (!ele || ele === document.body)
|
|
49
|
-
? document.body
|
|
50
|
-
: (isScrollable(ele) ? ele : getScrollableParent(ele.parentNode as HTMLElement));
|
|
51
|
-
};
|