@firecms/core 3.0.0-rc.2 → 3.0.0-rc.4

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 (52) hide show
  1. package/dist/components/HomePage/HomePageDnD.d.ts +2 -1
  2. package/dist/components/PropertyCollectionView.d.ts +23 -0
  3. package/dist/core/EntityEditView.d.ts +10 -4
  4. package/dist/form/EntityForm.d.ts +5 -2
  5. package/dist/form/PropertyFieldBinding.d.ts +1 -1
  6. package/dist/form/components/LocalChangesMenu.d.ts +11 -0
  7. package/dist/form/index.d.ts +2 -1
  8. package/dist/index.es.js +1307 -384
  9. package/dist/index.es.js.map +1 -1
  10. package/dist/index.umd.js +1306 -383
  11. package/dist/index.umd.js.map +1 -1
  12. package/dist/types/collections.d.ts +11 -0
  13. package/dist/types/fields.d.ts +8 -0
  14. package/dist/types/properties.d.ts +32 -6
  15. package/dist/util/collections.d.ts +1 -0
  16. package/dist/util/entity_cache.d.ts +6 -1
  17. package/dist/util/make_properties_editable.d.ts +1 -2
  18. package/dist/util/objects.d.ts +1 -0
  19. package/dist/util/useStorageUploadController.d.ts +1 -0
  20. package/package.json +6 -6
  21. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +47 -47
  22. package/src/components/EntityCollectionView/EntityCollectionView.tsx +6 -1
  23. package/src/components/EntityView.tsx +29 -40
  24. package/src/components/HomePage/DefaultHomePage.tsx +13 -9
  25. package/src/components/HomePage/HomePageDnD.tsx +140 -38
  26. package/src/components/PropertyCollectionView.tsx +329 -0
  27. package/src/components/SelectableTable/SelectableTable.tsx +0 -12
  28. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +2 -1
  29. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +0 -1
  30. package/src/core/EntityEditView.tsx +27 -14
  31. package/src/core/EntityEditViewFormActions.tsx +33 -18
  32. package/src/core/EntitySidePanel.tsx +9 -3
  33. package/src/form/EntityForm.tsx +173 -42
  34. package/src/form/EntityFormActions.tsx +30 -15
  35. package/src/form/PropertyFieldBinding.tsx +4 -4
  36. package/src/form/components/ErrorFocus.tsx +22 -29
  37. package/src/form/components/LocalChangesMenu.tsx +144 -0
  38. package/src/form/field_bindings/BlockFieldBinding.tsx +1 -0
  39. package/src/form/index.tsx +5 -1
  40. package/src/hooks/useBuildNavigationController.tsx +104 -31
  41. package/src/preview/property_previews/MapPropertyPreview.tsx +2 -2
  42. package/src/preview/property_previews/NumberPropertyPreview.tsx +2 -2
  43. package/src/types/collections.ts +12 -0
  44. package/src/types/fields.tsx +10 -0
  45. package/src/types/properties.ts +35 -6
  46. package/src/util/collections.ts +8 -0
  47. package/src/util/createFormexStub.tsx +4 -0
  48. package/src/util/entity_cache.ts +71 -52
  49. package/src/util/join_collections.ts +3 -3
  50. package/src/util/make_properties_editable.ts +0 -22
  51. package/src/util/objects.ts +40 -2
  52. package/src/util/useStorageUploadController.tsx +71 -34
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import {
3
3
  AuthController,
4
4
  CMSAnalyticsEvent,
@@ -19,8 +19,10 @@ import { ErrorBoundary, getFormFieldKeys } from "../components";
19
19
  import {
20
20
  getDefaultValuesFor,
21
21
  getEntityTitlePropertyKey,
22
+ getLocalChangesBackup,
22
23
  getValueInPath,
23
24
  isHidden,
25
+ isObject,
24
26
  isReadOnly,
25
27
  mergeDeep,
26
28
  resolveCollection,
@@ -32,7 +34,8 @@ import {
32
34
  useAuthController,
33
35
  useCustomizationController,
34
36
  useDataSource,
35
- useFireCMSContext, useNavigationController,
37
+ useFireCMSContext,
38
+ useNavigationController,
36
39
  useSideEntityController,
37
40
  useSnackbarController
38
41
  } from "../hooks";
@@ -41,11 +44,18 @@ import { Formex, FormexController, getIn, setIn, useCreateFormex } from "@firecm
41
44
  import { useAnalyticsController } from "../hooks/useAnalyticsController";
42
45
  import { FormEntry, FormLayout, LabelWithIconAndTooltip, PropertyFieldBinding } from "../form";
43
46
  import { ValidationError } from "yup";
44
- import { removeEntityFromCache, saveEntityToCache } from "../util/entity_cache";
45
- import { CustomIdField } from "../form/components/CustomIdField";
46
- import { ErrorFocus } from "../form/components/ErrorFocus";
47
- import { CustomFieldValidator, getYupEntitySchema } from "../form/validation";
47
+ import {
48
+ flattenKeys,
49
+ getEntityFromCache,
50
+ removeEntityFromCache,
51
+ removeEntityFromMemoryCache,
52
+ saveEntityToCache
53
+ } from "../util/entity_cache";
54
+ import { CustomIdField } from "./components/CustomIdField";
55
+ import { ErrorFocus } from "./components/ErrorFocus";
56
+ import { CustomFieldValidator, getYupEntitySchema } from "./validation";
48
57
  import { EntityFormActions, EntityFormActionsProps } from "./EntityFormActions";
58
+ import { LocalChangesMenu } from "./components/LocalChangesMenu";
49
59
 
50
60
  export type OnUpdateParams = {
51
61
  entity: Entity<any>,
@@ -64,7 +74,7 @@ export type EntityFormProps<M extends Record<string, any>> = {
64
74
  entity?: Entity<M>;
65
75
  databaseId?: string;
66
76
  onIdChange?: (id: string) => void;
67
- onValuesModified?: (modified: boolean) => void;
77
+ onValuesModified?: (modified: boolean, values: M) => void;
68
78
  onSaved?: (params: OnUpdateParams) => void;
69
79
  initialDirtyValues?: Partial<M>; // dirty cached entity in memory
70
80
  onFormContextReady?: (formContext: FormContext) => void;
@@ -96,6 +106,80 @@ export type EntityFormProps<M extends Record<string, any>> = {
96
106
  children?: React.ReactNode;
97
107
  };
98
108
 
109
+ // extract touched values for nested touched trees and map to current values
110
+ export function extractTouchedValues(values: any, touched: Record<string, boolean>): Record<string, any> {
111
+ let acc: Record<string, any> = {};
112
+ if (!touched || typeof touched !== "object") {
113
+ return acc;
114
+ }
115
+
116
+ Object.entries(touched).forEach(([key, value]) => {
117
+ if (value) {
118
+ acc = setIn(acc, key, getIn(values, key));
119
+ }
120
+ })
121
+
122
+ return acc;
123
+ }
124
+
125
+ export function getChanges<T extends object>(source: Partial<T>, comparison: Partial<T>): Partial<T> {
126
+ const changes: Partial<T> = {};
127
+
128
+ if (!source) {
129
+ return {};
130
+ }
131
+ if (!comparison) {
132
+ return source;
133
+ }
134
+
135
+ const allKeys = Array.from(new Set([...Object.keys(source), ...Object.keys(comparison)]));
136
+
137
+ for (const key of allKeys) {
138
+ const sourceValue = (source as any)[key];
139
+ const comparisonValue = (comparison as any)[key];
140
+
141
+ if (equal(sourceValue, comparisonValue)) {
142
+ continue;
143
+ }
144
+
145
+ const sourceHasKey = source && typeof source === "object" && Object.prototype.hasOwnProperty.call(source, key);
146
+ const comparisonHasKey = comparison && typeof comparison === "object" && Object.prototype.hasOwnProperty.call(comparison, key);
147
+
148
+ if (comparisonHasKey && !sourceHasKey) {
149
+ (changes as any)[key] = undefined;
150
+ } else if (Array.isArray(sourceValue)) {
151
+ const comparisonArray = Array.isArray(comparisonValue) ? comparisonValue : [];
152
+ if (sourceValue.length < comparisonArray.length) {
153
+ (changes as any)[key] = sourceValue;
154
+ continue;
155
+ }
156
+ const changedArray = sourceValue.map((item, index) => {
157
+ const comparisonItem = comparisonArray[index];
158
+ if (equal(item, comparisonItem)) {
159
+ return null;
160
+ }
161
+ if (isObject(item) && item && isObject(comparisonItem) && comparisonItem) {
162
+ const nestedChanges = getChanges(item, comparisonItem);
163
+ return Object.keys(nestedChanges).length > 0 ? nestedChanges : item;
164
+ }
165
+ return item;
166
+ });
167
+ if (changedArray.some(item => item !== null) || sourceValue.length > comparisonArray.length) {
168
+ (changes as any)[key] = changedArray;
169
+ }
170
+ } else if (isObject(sourceValue) && sourceValue && isObject(comparisonValue) && comparisonValue) {
171
+ const nestedChanges = getChanges(sourceValue, comparisonValue);
172
+ if (Object.keys(nestedChanges).length > 0) {
173
+ (changes as any)[key] = nestedChanges;
174
+ }
175
+ } else {
176
+ (changes as any)[key] = sourceValue;
177
+ }
178
+ }
179
+
180
+ return changes;
181
+ }
182
+
99
183
  export function EntityForm<M extends Record<string, any>>({
100
184
  path,
101
185
  fullIdPath,
@@ -126,7 +210,6 @@ export function EntityForm<M extends Record<string, any>>({
126
210
  console.warn(`The collection ${collection.path} has customId and formAutoSave enabled. This is not supported and formAutoSave will be ignored`);
127
211
  }
128
212
 
129
-
130
213
  const sideEntityController = useSideEntityController();
131
214
  const navigationController = useNavigationController();
132
215
 
@@ -164,7 +247,7 @@ export function EntityForm<M extends Record<string, any>>({
164
247
  const context = useFireCMSContext();
165
248
  const analyticsController = useAnalyticsController();
166
249
 
167
- const [underlyingChanges, setUnderlyingChanges] = useState<Partial<EntityValues<M>>>({});
250
+ const [underlyingChanges] = useState<Partial<EntityValues<M>>>({});
168
251
 
169
252
  const [customIdLoading, setCustomIdLoading] = useState<boolean>(false);
170
253
 
@@ -189,6 +272,18 @@ export function EntityForm<M extends Record<string, any>>({
189
272
 
190
273
  const autoSave = collection.formAutoSave && !collection.customId;
191
274
 
275
+ const baseInitialValues = useMemo(() => getInitialEntityValues(authController, collection, path, status, entity, customizationController.propertyConfigs), [authController, collection, path, status, entity, customizationController.propertyConfigs]);
276
+
277
+ const localChangesDataRaw = useMemo(() => entityId
278
+ ? getEntityFromCache(path + "/" + entityId)
279
+ : getEntityFromCache(path + "#new"), [entityId, path]);
280
+
281
+ const [localChangesCleared, setLocalChangesCleared] = useState<boolean>(false);
282
+
283
+ const localChangesBackup = getLocalChangesBackup(collection);
284
+ const autoApplyLocalChanges = localChangesBackup === "auto_apply";
285
+ const manualApplyLocalChanges = localChangesBackup === "manual_apply";
286
+
192
287
  const onSubmit = (values: EntityValues<M>, formexController: FormexController<EntityValues<M>>) => {
193
288
 
194
289
  if (mustSetCustomId && !entityId) {
@@ -226,13 +321,43 @@ export function EntityForm<M extends Record<string, any>>({
226
321
  });
227
322
  };
228
323
 
324
+ const [initialValues, initialDirty] = useMemo(() => {
325
+ const initialValuesWithLocalChanges: Partial<M> = autoApplyLocalChanges && localChangesDataRaw ? mergeDeep(baseInitialValues, localChangesDataRaw as Partial<M>) : baseInitialValues;
326
+ const initialValues = initialDirtyValues ? mergeDeep(initialValuesWithLocalChanges, initialDirtyValues) : initialValuesWithLocalChanges;
327
+ const initialDirty = Boolean(initialDirtyValues) && initialDirtyValues && Object.keys(initialDirtyValues).length > 0;
328
+ return [initialValues, initialDirty];
329
+ }, [autoApplyLocalChanges, localChangesDataRaw, baseInitialValues, initialDirtyValues]);
330
+
331
+ const localChangesData = useMemo(() => {
332
+ if (!localChangesDataRaw) {
333
+ return undefined;
334
+ }
335
+ return getChanges(localChangesDataRaw, initialValues);
336
+ }, [localChangesDataRaw, initialValues]);
337
+
338
+ const hasLocalChanges = !localChangesCleared && localChangesData && Object.keys(localChangesData).length > 0;
339
+
229
340
  const formex: FormexController<M> = formexProp ?? useCreateFormex<M>({
230
- initialValues: (initialDirtyValues ?? getInitialEntityValues(authController, collection, path, status, entity, customizationController.propertyConfigs)) as M,
231
- initialDirty: Boolean(initialDirtyValues),
341
+ initialValues: initialValues as M,
342
+ initialDirty,
343
+ initialTouched: initialDirtyValues ?
344
+ flattenKeys(initialDirtyValues!)
345
+ .reduce((previousValue, currentValue) => ({
346
+ ...previousValue,
347
+ [currentValue]: true
348
+ }), {})
349
+ : {},
232
350
  onSubmit,
233
351
  onReset: () => {
234
352
  clearDirtyCache();
235
- onValuesModified?.(false);
353
+ onValuesModified?.(false, initialValues as M);
354
+ },
355
+ onValuesChangeDeferred: (values: M, controller: FormexController<M>) => {
356
+ const key = (status === "new" || status === "copy") ? path + "#new" : path + "/" + entityId;
357
+ if (controller.dirty) {
358
+ const touchedValues = extractTouchedValues(values, controller.touched);
359
+ saveEntityToCache(key, touchedValues);
360
+ }
236
361
  },
237
362
  validation: (values) => {
238
363
  return validationSchema?.validate(values, { abortEarly: false })
@@ -295,8 +420,10 @@ export function EntityForm<M extends Record<string, any>>({
295
420
 
296
421
  function clearDirtyCache() {
297
422
  if (status === "new" || status === "copy") {
423
+ removeEntityFromMemoryCache(path + "#new");
298
424
  removeEntityFromCache(path + "#new");
299
425
  } else {
426
+ removeEntityFromMemoryCache(path + "/" + entityId);
300
427
  removeEntityFromCache(path + "/" + entityId);
301
428
  }
302
429
  }
@@ -304,7 +431,7 @@ export function EntityForm<M extends Record<string, any>>({
304
431
  const onSaveSuccess = (updatedEntity: Entity<M>) => {
305
432
 
306
433
  clearDirtyCache();
307
- onValuesModified?.(false);
434
+ onValuesModified?.(false, updatedEntity.values);
308
435
  if (!autoSave)
309
436
  snackbarController.open({
310
437
  type: "success",
@@ -405,7 +532,7 @@ export function EntityForm<M extends Record<string, any>>({
405
532
  values,
406
533
  previousValues: entity?.values,
407
534
  autoSave: autoSave ?? false
408
- }).then((res) => {
535
+ }).then(() => {
409
536
  const eventName: CMSAnalyticsEvent = status === "new"
410
537
  ? "new_entity_saved"
411
538
  : (status === "copy" ? "entity_copied" : (status === "existing" ? "entity_edited" : "unmapped_event"));
@@ -443,7 +570,8 @@ export function EntityForm<M extends Record<string, any>>({
443
570
  type: "error",
444
571
  message: "Error updating id, check the console"
445
572
  });
446
- }, []);
573
+ console.error(error);
574
+ }, [snackbarController]);
447
575
 
448
576
  const pluginActions: React.ReactNode[] = [];
449
577
  const plugins = customizationController.plugins;
@@ -503,17 +631,15 @@ export function EntityForm<M extends Record<string, any>>({
503
631
 
504
632
  useEffect(() => {
505
633
  if (!autoSave) {
506
- onValuesModified?.(modified);
634
+ onValuesModified?.(modified, formex.values);
507
635
  }
508
636
  }, [formex.dirty]);
509
637
 
510
- const deferredValues = useDeferredValue(formex.values);
511
638
  const modified = formex.dirty;
512
639
 
513
640
  const uniqueFieldValidator: CustomFieldValidator = useCallback(({
514
641
  name,
515
- value,
516
- property
642
+ value
517
643
  }) => dataSource.checkUniqueField(path, name, value, entityId, collection),
518
644
  [dataSource, path, entityId]);
519
645
 
@@ -525,13 +651,6 @@ export function EntityForm<M extends Record<string, any>>({
525
651
  : undefined,
526
652
  [entityId, resolvedCollection.properties, uniqueFieldValidator]);
527
653
 
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
654
  useOnAutoSave(autoSave, formex, lastSavedValues, save);
536
655
 
537
656
  useEffect(() => {
@@ -716,7 +835,7 @@ export function EntityForm<M extends Record<string, any>>({
716
835
  <form
717
836
  onSubmit={formex.handleSubmit}
718
837
  onReset={() => formex.resetForm({
719
- values: getInitialEntityValues(authController, collection, path, status, entity, customizationController.propertyConfigs) as M
838
+ values: baseInitialValues as M
720
839
  })}
721
840
  noValidate
722
841
  className={cls("flex-1 flex flex-row w-full overflow-y-auto justify-center", className)}>
@@ -725,34 +844,46 @@ export function EntityForm<M extends Record<string, any>>({
725
844
  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")}>
726
845
 
727
846
  <div className={cls("flex flex-col w-full pt-12 pb-16 px-4 sm:px-8 md:px-10")}>
728
-
729
- {formex.dirty
730
- ? <Tooltip title={"Local unsaved changes"}
731
- className={"self-end sticky top-4 z-10"}>
732
- <Chip size={"small"} colorScheme={"orangeDarker"}>
733
- <EditIcon size={"smallest"}/>
734
- </Chip>
735
- </Tooltip>
736
- : <Tooltip title={"In sync with the database"}
737
- className={"self-end sticky top-4 z-10"}>
738
- <Chip size={"small"}>
739
- <CheckIcon size={"smallest"}/>
740
- </Chip>
741
- </Tooltip>}
847
+ <div
848
+ className={"flex flex-row gap-4 self-end sticky top-4 z-10"}>
849
+
850
+ {manualApplyLocalChanges && hasLocalChanges &&
851
+ <LocalChangesMenu
852
+ cacheKey={status === "new" || status === "copy" ? path + "#new" : path + "/" + entityId}
853
+ properties={resolvedCollection.properties}
854
+ localChangesData={localChangesData as Partial<M>}
855
+ formex={formex}
856
+ onClearLocalChanges={() => setLocalChangesCleared(true)}
857
+ />}
858
+
859
+ {formex.dirty
860
+ ? <Tooltip title={"This form has been modified"}>
861
+ <Chip size={"small"} className={"py-1"} colorScheme={"orangeDarker"}>
862
+ <EditIcon size={"smallest"}/>
863
+ </Chip>
864
+ </Tooltip>
865
+ : <Tooltip title={"The current form is in sync with the database"}>
866
+ <Chip size={"small"} className={"py-1"} >
867
+ <CheckIcon size={"smallest"}/>
868
+ </Chip>
869
+ </Tooltip>}
870
+ </div>
742
871
 
743
872
  {formView}
744
873
 
745
874
  </div>
746
875
 
747
876
  </div>
877
+
748
878
  {dialogActions}
879
+
749
880
  </form>
750
881
 
751
882
  </Formex>
752
883
  );
753
884
  }
754
885
 
755
- function getInitialEntityValues<M extends object>(
886
+ export function getInitialEntityValues<M extends object>(
756
887
  authController: AuthController,
757
888
  collection: EntityCollection,
758
889
  path: string,
@@ -7,7 +7,16 @@ import {
7
7
  ResolvedEntityCollection,
8
8
  SideEntityController
9
9
  } from "../types";
10
- import { Button, cls, defaultBorderMixin, DialogActions, IconButton, LoadingButton, Typography } from "@firecms/ui";
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
- disabled={disabled || isSubmitting}>
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
 
@@ -83,6 +83,7 @@ function PropertyFieldBindingInternal<T extends CMSType = CMSType, M extends Rec
83
83
  underlyingValueHasChanged,
84
84
  disabled: disabledProp,
85
85
  partOfArray,
86
+ partOfBlock,
86
87
  minimalistView,
87
88
  autoFocus,
88
89
  index,
@@ -93,10 +94,6 @@ function PropertyFieldBindingInternal<T extends CMSType = CMSType, M extends Rec
93
94
  const authController = useAuthController();
94
95
  const customizationController = useCustomizationController();
95
96
 
96
- if(propertyKey === "created_by"){
97
- console.log("Rendering field for created_by", {propertyKey, property, context});
98
- }
99
-
100
97
  return (
101
98
  <Field
102
99
  key={propertyKey}
@@ -168,6 +165,7 @@ function PropertyFieldBindingInternal<T extends CMSType = CMSType, M extends Rec
168
165
  context,
169
166
  disabled,
170
167
  partOfArray,
168
+ partOfBlock,
171
169
  minimalistView,
172
170
  autoFocus,
173
171
  size,
@@ -199,6 +197,7 @@ function FieldInternal<T extends CMSType, CustomProps, M extends Record<string,
199
197
  includeDescription,
200
198
  underlyingValueHasChanged,
201
199
  partOfArray,
200
+ partOfBlock,
202
201
  minimalistView,
203
202
  autoFocus,
204
203
  context,
@@ -261,6 +260,7 @@ function FieldInternal<T extends CMSType, CustomProps, M extends Record<string,
261
260
  disabled: disabled ?? false,
262
261
  underlyingValueHasChanged: underlyingValueHasChanged ?? false,
263
262
  partOfArray: partOfArray ?? false,
263
+ partOfBlock: partOfBlock ?? false,
264
264
  minimalistView: minimalistView ?? false,
265
265
  autoFocus: autoFocus ?? false,
266
266
  customProps: customFieldProps,
@@ -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 { isSubmitting, isValidating, errors } = useFormex();
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 is submitting but finished validating.
14
- if (keys.length > 0 && isSubmitting && !isValidating) {
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 && containerRef?.current) {
20
- const scrollableParent = getScrollableParent(containerRef.current);
21
- if (scrollableParent) {
22
- const top = errorElement.getBoundingClientRect().top;
23
- scrollableParent.scrollTo({
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
- }, [isSubmitting, isValidating, errors, containerRef]);
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
- };