@firecms/core 3.0.0-canary.66 → 3.0.0-canary.67
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 +17 -3
- package/dist/form/PropertiesForm.d.ts +8 -0
- package/dist/form/components/FieldHelperText.d.ts +3 -3
- package/dist/form/components/StorageItemPreview.d.ts +2 -4
- package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +2 -4
- package/dist/form/index.d.ts +0 -2
- package/dist/index.es.js +4269 -4322
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +5 -5
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collections.d.ts +14 -0
- package/dist/types/fields.d.ts +31 -30
- package/dist/types/plugins.d.ts +2 -2
- package/dist/types/properties.d.ts +1 -1
- package/dist/util/storage.d.ts +23 -2
- package/dist/util/useStorageUploadController.d.ts +1 -1
- package/package.json +4 -4
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +2 -1
- package/src/core/EntityEditView.tsx +662 -120
- package/src/core/EntitySidePanel.tsx +0 -1
- package/src/form/PropertiesForm.tsx +81 -0
- package/src/form/PropertyFieldBinding.tsx +28 -5
- package/src/form/components/FieldHelperText.tsx +3 -3
- package/src/form/components/StorageItemPreview.tsx +0 -4
- package/src/form/field_bindings/MapFieldBinding.tsx +10 -3
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +0 -7
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +3 -26
- package/src/form/index.tsx +4 -4
- package/src/form/validation.ts +1 -17
- package/src/types/collections.ts +14 -0
- package/src/types/customization_controller.tsx +0 -1
- package/src/types/fields.tsx +33 -33
- package/src/types/plugins.tsx +2 -2
- package/src/types/properties.ts +1 -1
- package/src/util/permissions.ts +1 -0
- package/src/util/storage.ts +75 -21
- package/src/util/useStorageUploadController.tsx +21 -3
- package/dist/form/EntityForm.d.ts +0 -77
- package/src/form/EntityForm.tsx +0 -735
package/src/form/EntityForm.tsx
DELETED
|
@@ -1,735 +0,0 @@
|
|
|
1
|
-
import React, { MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
CMSAnalyticsEvent,
|
|
5
|
-
Entity,
|
|
6
|
-
EntityAction,
|
|
7
|
-
EntityCollection,
|
|
8
|
-
EntityStatus,
|
|
9
|
-
EntityValues,
|
|
10
|
-
FormContext,
|
|
11
|
-
PluginFormActionProps,
|
|
12
|
-
PropertyFieldBindingProps,
|
|
13
|
-
ResolvedEntityCollection
|
|
14
|
-
} from "../types";
|
|
15
|
-
import { Formex, FormexController, getIn, setIn, useCreateFormex } from "@firecms/formex";
|
|
16
|
-
import { PropertyFieldBinding } from "./PropertyFieldBinding";
|
|
17
|
-
import { CustomFieldValidator, getYupEntitySchema } from "./validation";
|
|
18
|
-
import equal from "react-fast-compare"
|
|
19
|
-
import {
|
|
20
|
-
canCreateEntity,
|
|
21
|
-
canDeleteEntity,
|
|
22
|
-
getDefaultValuesFor,
|
|
23
|
-
getEntityTitlePropertyKey,
|
|
24
|
-
getValueInPath,
|
|
25
|
-
isHidden,
|
|
26
|
-
isReadOnly,
|
|
27
|
-
resolveCollection
|
|
28
|
-
} from "../util";
|
|
29
|
-
import {
|
|
30
|
-
useAuthController,
|
|
31
|
-
useCustomizationController,
|
|
32
|
-
useDataSource,
|
|
33
|
-
useFireCMSContext,
|
|
34
|
-
useSideEntityController
|
|
35
|
-
} from "../hooks";
|
|
36
|
-
import { ErrorFocus } from "./components/ErrorFocus";
|
|
37
|
-
import { CustomIdField } from "./components/CustomIdField";
|
|
38
|
-
import { Alert, Button, CircularProgress, cls, DialogActions, IconButton, Tooltip, Typography } from "@firecms/ui";
|
|
39
|
-
import { CircularProgressCenter, ErrorBoundary } from "../components";
|
|
40
|
-
import { copyEntityAction, deleteEntityAction } from "../components/common/default_entity_actions";
|
|
41
|
-
import { useAnalyticsController } from "../hooks/useAnalyticsController";
|
|
42
|
-
import { ValidationError } from "yup";
|
|
43
|
-
import { PropertyIdCopyTooltipContent } from "../components/PropertyIdCopyTooltipContent";
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* @group Components
|
|
47
|
-
*/
|
|
48
|
-
export interface EntityFormProps<M extends Record<string, any>> {
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* New or existing status
|
|
52
|
-
*/
|
|
53
|
-
status: EntityStatus;
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Path of the collection this entity is located
|
|
57
|
-
*/
|
|
58
|
-
path: string;
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* The collection is used to build the fields of the form
|
|
62
|
-
*/
|
|
63
|
-
collection: EntityCollection<M>
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* The updated entity is passed from the parent component when the underlying data
|
|
67
|
-
* has changed in the datasource
|
|
68
|
-
*/
|
|
69
|
-
entity?: Entity<M>;
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* The callback function called when Save is clicked and validation is correct
|
|
73
|
-
*/
|
|
74
|
-
onEntitySaveRequested: (
|
|
75
|
-
props: EntityFormSaveParams<M>
|
|
76
|
-
) => Promise<void>;
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* The callback function called when discard is clicked
|
|
80
|
-
*/
|
|
81
|
-
onDiscard?: () => void;
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* The callback function when the form is dirty, so the values are different
|
|
85
|
-
* from the original ones
|
|
86
|
-
*/
|
|
87
|
-
onModified?: (dirty: boolean) => void;
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* The callback function when the form original values have been modified
|
|
91
|
-
*/
|
|
92
|
-
onValuesChanged?: (values?: EntityValues<M>) => void;
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
*
|
|
96
|
-
* @param id
|
|
97
|
-
*/
|
|
98
|
-
onIdChange?: (id: string) => void;
|
|
99
|
-
|
|
100
|
-
currentEntityId?: string;
|
|
101
|
-
|
|
102
|
-
onFormContextChange?: (formContext: FormContext<M>) => void;
|
|
103
|
-
|
|
104
|
-
hideId?: boolean;
|
|
105
|
-
|
|
106
|
-
autoSave?: boolean;
|
|
107
|
-
|
|
108
|
-
onIdUpdateError?: (error: any) => void;
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export type EntityFormSaveParams<M extends Record<string, any>> = {
|
|
113
|
-
collection: ResolvedEntityCollection<M>,
|
|
114
|
-
path: string,
|
|
115
|
-
entityId: string | undefined,
|
|
116
|
-
values: EntityValues<M>,
|
|
117
|
-
previousValues?: EntityValues<M>,
|
|
118
|
-
closeAfterSave: boolean,
|
|
119
|
-
autoSave: boolean
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* This is the form used internally by the CMS
|
|
124
|
-
* @param status
|
|
125
|
-
* @param path
|
|
126
|
-
* @param collection
|
|
127
|
-
* @param entity
|
|
128
|
-
* @param onEntitySave
|
|
129
|
-
* @param onDiscard
|
|
130
|
-
* @param onModified
|
|
131
|
-
* @param onValuesChanged
|
|
132
|
-
* @constructor
|
|
133
|
-
* @group Components
|
|
134
|
-
*/
|
|
135
|
-
export const EntityForm = React.memo<EntityFormProps<any>>(EntityFormInternal,
|
|
136
|
-
(a: EntityFormProps<any>, b: EntityFormProps<any>) => {
|
|
137
|
-
return a.status === b.status &&
|
|
138
|
-
a.path === b.path &&
|
|
139
|
-
equal(a.entity?.values, b.entity?.values);
|
|
140
|
-
}) as typeof EntityFormInternal;
|
|
141
|
-
|
|
142
|
-
function getDataSourceEntityValues<M extends object>(initialResolvedCollection: ResolvedEntityCollection,
|
|
143
|
-
status: "new" | "existing" | "copy",
|
|
144
|
-
entity: Entity<M> | undefined): Partial<EntityValues<M>> {
|
|
145
|
-
const properties = initialResolvedCollection.properties;
|
|
146
|
-
if ((status === "existing" || status === "copy") && entity) {
|
|
147
|
-
return entity.values ?? getDefaultValuesFor(properties);
|
|
148
|
-
} else if (status === "new") {
|
|
149
|
-
return getDefaultValuesFor(properties);
|
|
150
|
-
} else {
|
|
151
|
-
console.error({
|
|
152
|
-
status,
|
|
153
|
-
entity
|
|
154
|
-
});
|
|
155
|
-
throw new Error("Form has not been initialised with the correct parameters");
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function EntityFormInternal<M extends Record<string, any>>({
|
|
160
|
-
status,
|
|
161
|
-
path,
|
|
162
|
-
collection: inputCollection,
|
|
163
|
-
entity,
|
|
164
|
-
onEntitySaveRequested,
|
|
165
|
-
onDiscard,
|
|
166
|
-
onModified,
|
|
167
|
-
onValuesChanged,
|
|
168
|
-
onIdChange,
|
|
169
|
-
onFormContextChange,
|
|
170
|
-
hideId,
|
|
171
|
-
autoSave,
|
|
172
|
-
onIdUpdateError,
|
|
173
|
-
}: EntityFormProps<M>) {
|
|
174
|
-
|
|
175
|
-
const analyticsController = useAnalyticsController();
|
|
176
|
-
|
|
177
|
-
const customizationController = useCustomizationController();
|
|
178
|
-
|
|
179
|
-
const context = useFireCMSContext();
|
|
180
|
-
const dataSource = useDataSource(inputCollection);
|
|
181
|
-
const plugins = customizationController.plugins;
|
|
182
|
-
|
|
183
|
-
const initialResolvedCollection = useMemo(() => resolveCollection({
|
|
184
|
-
collection: inputCollection,
|
|
185
|
-
path,
|
|
186
|
-
values: entity?.values,
|
|
187
|
-
fields: customizationController.propertyConfigs
|
|
188
|
-
}), [entity?.values, path, customizationController.propertyConfigs]);
|
|
189
|
-
|
|
190
|
-
const mustSetCustomId: boolean = (status === "new" || status === "copy") &&
|
|
191
|
-
(Boolean(initialResolvedCollection.customId) && initialResolvedCollection.customId !== "optional");
|
|
192
|
-
|
|
193
|
-
const initialEntityId = useMemo(() => {
|
|
194
|
-
if (status === "new" || status === "copy") {
|
|
195
|
-
if (mustSetCustomId) {
|
|
196
|
-
return undefined;
|
|
197
|
-
} else {
|
|
198
|
-
return dataSource.generateEntityId(path);
|
|
199
|
-
}
|
|
200
|
-
} else {
|
|
201
|
-
return entity?.id;
|
|
202
|
-
}
|
|
203
|
-
}, []);
|
|
204
|
-
|
|
205
|
-
const closeAfterSaveRef = useRef(false);
|
|
206
|
-
|
|
207
|
-
const baseDataSourceValuesRef = useRef<Partial<EntityValues<M>>>(getDataSourceEntityValues(initialResolvedCollection, status, entity));
|
|
208
|
-
|
|
209
|
-
const [entityId, setEntityId] = React.useState<string | undefined>(initialEntityId);
|
|
210
|
-
const [entityIdError, setEntityIdError] = React.useState<boolean>(false);
|
|
211
|
-
const [savingError, setSavingError] = React.useState<Error | undefined>();
|
|
212
|
-
|
|
213
|
-
const [customIdLoading, setCustomIdLoading] = React.useState<boolean>(false);
|
|
214
|
-
|
|
215
|
-
// const initialValuesRef = useRef<EntityValues<M>>(entity?.values ?? baseDataSourceValues as EntityValues<M>);
|
|
216
|
-
const [internalValues, setInternalValues] = useState<EntityValues<M> | undefined>(entity?.values ?? baseDataSourceValuesRef.current as EntityValues<M>);
|
|
217
|
-
|
|
218
|
-
const save = (values: EntityValues<M>): Promise<void> => {
|
|
219
|
-
return onEntitySaveRequested({
|
|
220
|
-
collection: resolvedCollection,
|
|
221
|
-
path,
|
|
222
|
-
entityId,
|
|
223
|
-
values,
|
|
224
|
-
previousValues: entity?.values,
|
|
225
|
-
closeAfterSave: closeAfterSaveRef.current,
|
|
226
|
-
autoSave: autoSave ?? false
|
|
227
|
-
}).then(_ => {
|
|
228
|
-
const eventName: CMSAnalyticsEvent = status === "new"
|
|
229
|
-
? "new_entity_saved"
|
|
230
|
-
: (status === "copy" ? "entity_copied" : (status === "existing" ? "entity_edited" : "unmapped_event"));
|
|
231
|
-
analyticsController.onAnalyticsEvent?.(eventName, { path });
|
|
232
|
-
}).catch(e => {
|
|
233
|
-
console.error(e);
|
|
234
|
-
setSavingError(e);
|
|
235
|
-
}).finally(() => {
|
|
236
|
-
closeAfterSaveRef.current = false;
|
|
237
|
-
});
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
const onSubmit = (values: EntityValues<M>, formexController: FormexController<EntityValues<M>>) => {
|
|
241
|
-
|
|
242
|
-
if (mustSetCustomId && !entityId) {
|
|
243
|
-
console.error("Missing custom Id");
|
|
244
|
-
setEntityIdError(true);
|
|
245
|
-
formexController.setSubmitting(false);
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
setSavingError(undefined);
|
|
250
|
-
setEntityIdError(false);
|
|
251
|
-
|
|
252
|
-
if (status === "existing") {
|
|
253
|
-
if (!entity?.id) throw Error("Form misconfiguration when saving, no id for existing entity");
|
|
254
|
-
} else if (status === "new" || status === "copy") {
|
|
255
|
-
if (inputCollection.customId) {
|
|
256
|
-
if (inputCollection.customId !== "optional" && !entityId) {
|
|
257
|
-
throw Error("Form misconfiguration when saving, entityId should be set");
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
} else {
|
|
261
|
-
throw Error("New FormType added, check EntityForm");
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return save(values)
|
|
265
|
-
?.then(_ => {
|
|
266
|
-
formexController.resetForm({
|
|
267
|
-
values,
|
|
268
|
-
submitCount: 0,
|
|
269
|
-
touched: {}
|
|
270
|
-
});
|
|
271
|
-
})
|
|
272
|
-
.finally(() => {
|
|
273
|
-
formexController.setSubmitting(false);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
const formex: FormexController<M> = useCreateFormex<M>({
|
|
279
|
-
initialValues: baseDataSourceValuesRef.current as M,
|
|
280
|
-
onSubmit,
|
|
281
|
-
validation: (values) => {
|
|
282
|
-
return validationSchema?.validate(values, { abortEarly: false })
|
|
283
|
-
.then(() => {
|
|
284
|
-
return {};
|
|
285
|
-
})
|
|
286
|
-
.catch((e) => {
|
|
287
|
-
|
|
288
|
-
const errors: Record<string, string> = {};
|
|
289
|
-
e.inner.forEach((error: any) => {
|
|
290
|
-
errors[error.path] = error.message;
|
|
291
|
-
});
|
|
292
|
-
return yupToFormErrors(e);
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
useEffect(() => {
|
|
298
|
-
baseDataSourceValuesRef.current = getDataSourceEntityValues(initialResolvedCollection, status, entity);
|
|
299
|
-
const initialValues = formex.initialValues;
|
|
300
|
-
if (!formex.isSubmitting && initialValues && status === "existing") {
|
|
301
|
-
setUnderlyingChanges(
|
|
302
|
-
Object.entries(resolvedCollection.properties)
|
|
303
|
-
.map(([key, property]) => {
|
|
304
|
-
if (isHidden(property)) {
|
|
305
|
-
return {};
|
|
306
|
-
}
|
|
307
|
-
const initialValue = initialValues[key];
|
|
308
|
-
const latestValue = baseDataSourceValuesRef.current[key];
|
|
309
|
-
if (!equal(initialValue, latestValue)) {
|
|
310
|
-
return { [key]: latestValue };
|
|
311
|
-
}
|
|
312
|
-
return {};
|
|
313
|
-
})
|
|
314
|
-
.reduce((a, b) => ({ ...a, ...b }), {}) as Partial<EntityValues<M>>
|
|
315
|
-
);
|
|
316
|
-
} else {
|
|
317
|
-
setUnderlyingChanges({});
|
|
318
|
-
}
|
|
319
|
-
}, [entity, initialResolvedCollection, status]);
|
|
320
|
-
|
|
321
|
-
const doOnValuesChanges = (values?: EntityValues<M>) => {
|
|
322
|
-
const initialValues = formex.initialValues;
|
|
323
|
-
setInternalValues(values);
|
|
324
|
-
if (onValuesChanged)
|
|
325
|
-
onValuesChanged(values);
|
|
326
|
-
if (autoSave && values && !equal(values, initialValues)) {
|
|
327
|
-
save(values);
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
useEffect(() => {
|
|
332
|
-
if (entityId && onIdChange)
|
|
333
|
-
onIdChange(entityId);
|
|
334
|
-
}, [entityId, onIdChange]);
|
|
335
|
-
|
|
336
|
-
const resolvedCollection = resolveCollection<M>({
|
|
337
|
-
collection: inputCollection,
|
|
338
|
-
path,
|
|
339
|
-
entityId,
|
|
340
|
-
values: internalValues,
|
|
341
|
-
previousValues: formex.initialValues,
|
|
342
|
-
fields: customizationController.propertyConfigs
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
const titlePropertyKey = getEntityTitlePropertyKey(resolvedCollection, customizationController.propertyConfigs);
|
|
346
|
-
const title = internalValues && titlePropertyKey ? getValueInPath(internalValues, titlePropertyKey) : undefined;
|
|
347
|
-
|
|
348
|
-
const onIdUpdate = inputCollection.callbacks?.onIdUpdate;
|
|
349
|
-
|
|
350
|
-
const doOnIdUpdate = useCallback(async () => {
|
|
351
|
-
if (onIdUpdate && internalValues && (status === "new" || status === "copy")) {
|
|
352
|
-
setCustomIdLoading(true);
|
|
353
|
-
try {
|
|
354
|
-
const updatedId = await onIdUpdate({
|
|
355
|
-
collection: resolvedCollection,
|
|
356
|
-
path,
|
|
357
|
-
entityId,
|
|
358
|
-
values: internalValues,
|
|
359
|
-
context
|
|
360
|
-
});
|
|
361
|
-
setEntityId(updatedId);
|
|
362
|
-
} catch (e) {
|
|
363
|
-
onIdUpdateError && onIdUpdateError(e);
|
|
364
|
-
console.error(e);
|
|
365
|
-
}
|
|
366
|
-
setCustomIdLoading(false);
|
|
367
|
-
}
|
|
368
|
-
}, [entityId, internalValues, status]);
|
|
369
|
-
|
|
370
|
-
useEffect(() => {
|
|
371
|
-
doOnIdUpdate();
|
|
372
|
-
}, [doOnIdUpdate]);
|
|
373
|
-
|
|
374
|
-
const [underlyingChanges, setUnderlyingChanges] = useState<Partial<EntityValues<M>>>({});
|
|
375
|
-
|
|
376
|
-
const uniqueFieldValidator: CustomFieldValidator = useCallback(({
|
|
377
|
-
name,
|
|
378
|
-
value,
|
|
379
|
-
property
|
|
380
|
-
}) => dataSource.checkUniqueField(path, name, value, entityId),
|
|
381
|
-
[dataSource, path, entityId]);
|
|
382
|
-
|
|
383
|
-
const validationSchema = useMemo(() => entityId
|
|
384
|
-
? getYupEntitySchema(
|
|
385
|
-
entityId,
|
|
386
|
-
resolvedCollection.properties,
|
|
387
|
-
uniqueFieldValidator)
|
|
388
|
-
: undefined,
|
|
389
|
-
[entityId, resolvedCollection.properties, uniqueFieldValidator]);
|
|
390
|
-
|
|
391
|
-
const authController = useAuthController();
|
|
392
|
-
|
|
393
|
-
const getActionsForEntity = useCallback(({
|
|
394
|
-
entity,
|
|
395
|
-
customEntityActions
|
|
396
|
-
}: {
|
|
397
|
-
entity?: Entity<M>,
|
|
398
|
-
customEntityActions?: EntityAction[]
|
|
399
|
-
}): EntityAction[] => {
|
|
400
|
-
const createEnabled = canCreateEntity(inputCollection, authController, path, null);
|
|
401
|
-
const deleteEnabled = entity ? canDeleteEntity(inputCollection, authController, path, entity) : true;
|
|
402
|
-
const actions: EntityAction[] = [];
|
|
403
|
-
if (createEnabled)
|
|
404
|
-
actions.push(copyEntityAction);
|
|
405
|
-
if (deleteEnabled)
|
|
406
|
-
actions.push(deleteEntityAction);
|
|
407
|
-
if (customEntityActions)
|
|
408
|
-
actions.push(...customEntityActions);
|
|
409
|
-
return actions;
|
|
410
|
-
}, [authController, inputCollection, path]);
|
|
411
|
-
|
|
412
|
-
const pluginActions: React.ReactNode[] = [];
|
|
413
|
-
|
|
414
|
-
const formContext: FormContext<M> = {
|
|
415
|
-
// @ts-ignore
|
|
416
|
-
setFieldValue: useCallback(formex.setFieldValue, []),
|
|
417
|
-
values: formex.values,
|
|
418
|
-
collection: resolvedCollection,
|
|
419
|
-
entityId,
|
|
420
|
-
path,
|
|
421
|
-
save
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
const submittedFormContext = useRef<FormContext<M> | null>(null);
|
|
425
|
-
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
426
|
-
useEffect(() => {
|
|
427
|
-
if (onFormContextChange && !formContextsEqual(submittedFormContext.current ?? undefined, formContext)) {
|
|
428
|
-
onFormContextChange(formContext);
|
|
429
|
-
submittedFormContext.current = formContext;
|
|
430
|
-
}
|
|
431
|
-
}, [formContext, onFormContextChange]);
|
|
432
|
-
|
|
433
|
-
if (plugins && inputCollection) {
|
|
434
|
-
const actionProps: PluginFormActionProps = {
|
|
435
|
-
entityId,
|
|
436
|
-
path,
|
|
437
|
-
status,
|
|
438
|
-
collection: inputCollection,
|
|
439
|
-
context,
|
|
440
|
-
currentEntityId: entityId,
|
|
441
|
-
formContext
|
|
442
|
-
};
|
|
443
|
-
pluginActions.push(...plugins.map((plugin, i) => (
|
|
444
|
-
plugin.form?.Actions
|
|
445
|
-
? <plugin.form.Actions
|
|
446
|
-
key={`actions_${plugin.key}`} {...actionProps}/>
|
|
447
|
-
: null
|
|
448
|
-
)).filter(Boolean));
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
return <Formex value={formex}>
|
|
452
|
-
<div className="h-full overflow-auto">
|
|
453
|
-
|
|
454
|
-
{pluginActions.length > 0 && <div
|
|
455
|
-
className={cls("w-full flex justify-end items-center sticky top-0 right-0 left-0 z-10 bg-opacity-60 bg-slate-200 dark:bg-opacity-60 dark:bg-slate-800 backdrop-blur-md")}>
|
|
456
|
-
{pluginActions}
|
|
457
|
-
</div>}
|
|
458
|
-
|
|
459
|
-
<div className="pt-12 pb-16 pl-8 pr-8 md:pl-10 md:pr-10">
|
|
460
|
-
<div
|
|
461
|
-
className={`w-full py-2 flex flex-col items-start mt-${4 + (pluginActions ? 8 : 0)} lg:mt-${8 + (pluginActions ? 8 : 0)} mb-8`}>
|
|
462
|
-
|
|
463
|
-
<Typography
|
|
464
|
-
className={"mt-4 flex-grow line-clamp-1 " + inputCollection.hideIdFromForm ? "mb-2" : "mb-0"}
|
|
465
|
-
variant={"h4"}>{title ?? inputCollection.singularName ?? inputCollection.name}
|
|
466
|
-
</Typography>
|
|
467
|
-
<Alert color={"base"} className={"w-full"} size={"small"}>
|
|
468
|
-
<code className={"text-xs select-all"}>{path}/{entityId}</code>
|
|
469
|
-
</Alert>
|
|
470
|
-
</div>
|
|
471
|
-
|
|
472
|
-
{!hideId &&
|
|
473
|
-
<CustomIdField customId={inputCollection.customId}
|
|
474
|
-
entityId={entityId}
|
|
475
|
-
status={status}
|
|
476
|
-
onChange={setEntityId}
|
|
477
|
-
error={entityIdError}
|
|
478
|
-
loading={customIdLoading}
|
|
479
|
-
entity={entity}/>}
|
|
480
|
-
|
|
481
|
-
{entityId && <InnerForm
|
|
482
|
-
{...formex}
|
|
483
|
-
initialValues={formex.initialValues}
|
|
484
|
-
onModified={onModified}
|
|
485
|
-
onDiscard={onDiscard}
|
|
486
|
-
onValuesChanged={doOnValuesChanges}
|
|
487
|
-
underlyingChanges={underlyingChanges}
|
|
488
|
-
entity={entity}
|
|
489
|
-
resolvedCollection={resolvedCollection}
|
|
490
|
-
formContext={formContext}
|
|
491
|
-
status={status}
|
|
492
|
-
savingError={savingError}
|
|
493
|
-
closeAfterSaveRef={closeAfterSaveRef}
|
|
494
|
-
autoSave={autoSave}
|
|
495
|
-
entityActions={getActionsForEntity({
|
|
496
|
-
entity,
|
|
497
|
-
customEntityActions: inputCollection.entityActions
|
|
498
|
-
})}/>}
|
|
499
|
-
|
|
500
|
-
</div>
|
|
501
|
-
</div>
|
|
502
|
-
</Formex>
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function InnerForm<M extends Record<string, any>>(props: FormexController<M> & {
|
|
506
|
-
initialValues: EntityValues<M>,
|
|
507
|
-
onModified: ((modified: boolean) => void) | undefined,
|
|
508
|
-
onValuesChanged?: (changedValues?: EntityValues<M>) => void,
|
|
509
|
-
underlyingChanges: Partial<M>,
|
|
510
|
-
entity: Entity<M> | undefined,
|
|
511
|
-
resolvedCollection: ResolvedEntityCollection<M>,
|
|
512
|
-
formContext: FormContext<M>,
|
|
513
|
-
onDiscard?: () => void,
|
|
514
|
-
status: "new" | "existing" | "copy",
|
|
515
|
-
savingError?: Error,
|
|
516
|
-
closeAfterSaveRef: MutableRefObject<boolean>,
|
|
517
|
-
autoSave?: boolean,
|
|
518
|
-
entityActions: EntityAction[],
|
|
519
|
-
}) {
|
|
520
|
-
|
|
521
|
-
const {
|
|
522
|
-
values,
|
|
523
|
-
onDiscard,
|
|
524
|
-
onModified,
|
|
525
|
-
onValuesChanged,
|
|
526
|
-
underlyingChanges,
|
|
527
|
-
formContext,
|
|
528
|
-
entity,
|
|
529
|
-
touched,
|
|
530
|
-
setFieldValue,
|
|
531
|
-
resolvedCollection,
|
|
532
|
-
isSubmitting,
|
|
533
|
-
status,
|
|
534
|
-
handleSubmit,
|
|
535
|
-
resetForm,
|
|
536
|
-
savingError,
|
|
537
|
-
dirty,
|
|
538
|
-
closeAfterSaveRef,
|
|
539
|
-
autoSave,
|
|
540
|
-
entityActions,
|
|
541
|
-
} = props;
|
|
542
|
-
|
|
543
|
-
const context = useFireCMSContext();
|
|
544
|
-
const formActions = entityActions.filter(a => a.includeInForm === undefined || a.includeInForm);
|
|
545
|
-
const sideEntityController = useSideEntityController();
|
|
546
|
-
|
|
547
|
-
const modified = dirty;
|
|
548
|
-
useEffect(() => {
|
|
549
|
-
if (onModified)
|
|
550
|
-
onModified(modified);
|
|
551
|
-
if (onValuesChanged)
|
|
552
|
-
onValuesChanged(values);
|
|
553
|
-
}, [modified, values]);
|
|
554
|
-
|
|
555
|
-
useEffect(() => {
|
|
556
|
-
if (!autoSave && !isSubmitting && underlyingChanges && entity) {
|
|
557
|
-
// we update the form fields from the Firestore data
|
|
558
|
-
// if they were not touched
|
|
559
|
-
Object.entries(underlyingChanges).forEach(([key, value]) => {
|
|
560
|
-
const formValue = values[key];
|
|
561
|
-
if (!equal(value, formValue) && !touched[key]) {
|
|
562
|
-
console.debug("Updated value from the datasource:", key, value);
|
|
563
|
-
setFieldValue(key, value !== undefined ? value : null);
|
|
564
|
-
}
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
}, [isSubmitting, autoSave, underlyingChanges, entity, values, touched, setFieldValue]);
|
|
568
|
-
|
|
569
|
-
const formFields = (
|
|
570
|
-
<div className={"flex flex-col gap-8"}>
|
|
571
|
-
{(resolvedCollection.propertiesOrder ?? Object.keys(resolvedCollection.properties))
|
|
572
|
-
.map((key) => {
|
|
573
|
-
|
|
574
|
-
const property = resolvedCollection.properties[key];
|
|
575
|
-
if (!property) {
|
|
576
|
-
console.warn(`Property ${key} not found in collection ${resolvedCollection.name}`);
|
|
577
|
-
return null;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
const underlyingValueHasChanged: boolean =
|
|
581
|
-
!!underlyingChanges &&
|
|
582
|
-
Object.keys(underlyingChanges).includes(key) &&
|
|
583
|
-
!!touched[key];
|
|
584
|
-
|
|
585
|
-
const disabled = (!autoSave && isSubmitting) || isReadOnly(property) || Boolean(property.disabled);
|
|
586
|
-
const hidden = isHidden(property);
|
|
587
|
-
if (hidden) return null;
|
|
588
|
-
const cmsFormFieldProps: PropertyFieldBindingProps<any, M> = {
|
|
589
|
-
propertyKey: key,
|
|
590
|
-
disabled,
|
|
591
|
-
property,
|
|
592
|
-
includeDescription: property.description || property.longDescription,
|
|
593
|
-
underlyingValueHasChanged: underlyingValueHasChanged && !autoSave,
|
|
594
|
-
context: formContext,
|
|
595
|
-
tableMode: false,
|
|
596
|
-
partOfArray: false,
|
|
597
|
-
partOfBlock: false,
|
|
598
|
-
autoFocus: false
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
return (
|
|
602
|
-
<div id={`form_field_${key}`}
|
|
603
|
-
key={`field_${resolvedCollection.name}_${key}`}>
|
|
604
|
-
<ErrorBoundary>
|
|
605
|
-
<Tooltip title={<PropertyIdCopyTooltipContent propertyId={key}/>}
|
|
606
|
-
delayDuration={800}
|
|
607
|
-
side={"left"}
|
|
608
|
-
align={"start"}
|
|
609
|
-
sideOffset={16}>
|
|
610
|
-
<PropertyFieldBinding {...cmsFormFieldProps}/>
|
|
611
|
-
</Tooltip>
|
|
612
|
-
</ErrorBoundary>
|
|
613
|
-
</div>
|
|
614
|
-
);
|
|
615
|
-
})
|
|
616
|
-
.filter(Boolean)}
|
|
617
|
-
|
|
618
|
-
</div>
|
|
619
|
-
);
|
|
620
|
-
|
|
621
|
-
const disabled = isSubmitting || (!modified && status === "existing");
|
|
622
|
-
const formRef = React.useRef<HTMLDivElement>(null);
|
|
623
|
-
|
|
624
|
-
return (
|
|
625
|
-
|
|
626
|
-
<form onSubmit={handleSubmit}
|
|
627
|
-
onReset={() => {
|
|
628
|
-
console.debug("Resetting form")
|
|
629
|
-
resetForm();
|
|
630
|
-
return onDiscard && onDiscard();
|
|
631
|
-
}}
|
|
632
|
-
noValidate>
|
|
633
|
-
<div className="mt-12"
|
|
634
|
-
ref={formRef}>
|
|
635
|
-
|
|
636
|
-
{formFields}
|
|
637
|
-
|
|
638
|
-
<ErrorFocus containerRef={formRef}/>
|
|
639
|
-
|
|
640
|
-
</div>
|
|
641
|
-
|
|
642
|
-
<div className="h-14"/>
|
|
643
|
-
|
|
644
|
-
{!autoSave && <DialogActions position={"absolute"}>
|
|
645
|
-
|
|
646
|
-
{savingError &&
|
|
647
|
-
<div className="text-right">
|
|
648
|
-
<Typography color={"error"}>
|
|
649
|
-
{savingError.message}
|
|
650
|
-
</Typography>
|
|
651
|
-
</div>}
|
|
652
|
-
|
|
653
|
-
{entity && formActions.length > 0 && <div className="flex-grow flex overflow-auto no-scrollbar">
|
|
654
|
-
{formActions.map(action => (
|
|
655
|
-
<IconButton
|
|
656
|
-
key={action.name}
|
|
657
|
-
color="primary"
|
|
658
|
-
onClick={(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
|
659
|
-
event.stopPropagation();
|
|
660
|
-
if (entity)
|
|
661
|
-
action.onClick({
|
|
662
|
-
entity,
|
|
663
|
-
fullPath: resolvedCollection.path,
|
|
664
|
-
collection: resolvedCollection,
|
|
665
|
-
context,
|
|
666
|
-
sideEntityController,
|
|
667
|
-
});
|
|
668
|
-
}}>
|
|
669
|
-
{action.icon}
|
|
670
|
-
</IconButton>
|
|
671
|
-
))}
|
|
672
|
-
</div>}
|
|
673
|
-
{isSubmitting && <CircularProgress size={"small"}/>}
|
|
674
|
-
<Button
|
|
675
|
-
variant="text"
|
|
676
|
-
disabled={disabled || isSubmitting}
|
|
677
|
-
type="reset"
|
|
678
|
-
>
|
|
679
|
-
{status === "existing" ? "Discard" : "Clear"}
|
|
680
|
-
</Button>
|
|
681
|
-
|
|
682
|
-
<Button
|
|
683
|
-
variant="text"
|
|
684
|
-
color="primary"
|
|
685
|
-
type="submit"
|
|
686
|
-
disabled={disabled || isSubmitting}
|
|
687
|
-
onClick={() => {
|
|
688
|
-
closeAfterSaveRef.current = false;
|
|
689
|
-
}}
|
|
690
|
-
>
|
|
691
|
-
{status === "existing" && "Save"}
|
|
692
|
-
{status === "copy" && "Create copy"}
|
|
693
|
-
{status === "new" && "Create"}
|
|
694
|
-
</Button>
|
|
695
|
-
|
|
696
|
-
<Button
|
|
697
|
-
variant="filled"
|
|
698
|
-
color="primary"
|
|
699
|
-
type="submit"
|
|
700
|
-
disabled={disabled || isSubmitting}
|
|
701
|
-
onClick={() => {
|
|
702
|
-
closeAfterSaveRef.current = true;
|
|
703
|
-
}}
|
|
704
|
-
>
|
|
705
|
-
{status === "existing" && "Save and close"}
|
|
706
|
-
{status === "copy" && "Create copy and close"}
|
|
707
|
-
{status === "new" && "Create and close"}
|
|
708
|
-
</Button>
|
|
709
|
-
|
|
710
|
-
</DialogActions>}
|
|
711
|
-
</form>
|
|
712
|
-
);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
export function yupToFormErrors(yupError: ValidationError): Record<string, any> {
|
|
716
|
-
let errors: Record<string, any> = {};
|
|
717
|
-
if (yupError.inner) {
|
|
718
|
-
if (yupError.inner.length === 0) {
|
|
719
|
-
return setIn(errors, yupError.path!, yupError.message);
|
|
720
|
-
}
|
|
721
|
-
for (const err of yupError.inner) {
|
|
722
|
-
if (!getIn(errors, err.path!)) {
|
|
723
|
-
errors = setIn(errors, err.path!, err.message);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
return errors;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
function formContextsEqual(a: FormContext<any> | undefined, b: FormContext<any> | undefined): boolean {
|
|
731
|
-
return a?.path === b?.path &&
|
|
732
|
-
a?.entityId === b?.entityId &&
|
|
733
|
-
equal(a?.values, b?.values) &&
|
|
734
|
-
equal(a?.collection, b?.collection);
|
|
735
|
-
}
|