@firecms/core 3.0.0-beta.7 → 3.0.0-beta.8

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