@firecms/core 3.2.0 → 3.3.0-canary.451aa49

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 (67) hide show
  1. package/dist/components/VirtualTable/VirtualTableHeader.d.ts +1 -0
  2. package/dist/components/VirtualTable/VirtualTableHeaderRow.d.ts +1 -1
  3. package/dist/components/VirtualTable/VirtualTableProps.d.ts +6 -1
  4. package/dist/components/VirtualTable/types.d.ts +1 -0
  5. package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
  6. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.es.js +20186 -19539
  9. package/dist/index.es.js.map +1 -1
  10. package/dist/index.umd.js +24292 -23645
  11. package/dist/index.umd.js.map +1 -1
  12. package/dist/types/collections.d.ts +38 -0
  13. package/dist/types/properties.d.ts +9 -8
  14. package/dist/types/translations.d.ts +23 -0
  15. package/dist/util/index.d.ts +1 -0
  16. package/dist/util/lazy_eager.d.ts +7 -0
  17. package/dist/util/objects.d.ts +1 -0
  18. package/package.json +4 -4
  19. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +9 -3
  20. package/src/components/EntityCollectionView/EntityCollectionView.tsx +3 -5
  21. package/src/components/EntityJsonPreview.tsx +2 -1
  22. package/src/components/ErrorBoundary.tsx +3 -3
  23. package/src/components/VirtualTable/VirtualTable.tsx +5 -3
  24. package/src/components/VirtualTable/VirtualTableHeader.tsx +9 -8
  25. package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +8 -3
  26. package/src/components/VirtualTable/VirtualTableProps.tsx +7 -1
  27. package/src/components/VirtualTable/types.tsx +1 -0
  28. package/src/core/DrawerNavigationGroup.tsx +1 -1
  29. package/src/core/EntityEditView.tsx +50 -5
  30. package/src/core/EntitySidePanel.tsx +2 -1
  31. package/src/core/field_configs.tsx +4 -2
  32. package/src/form/EntityForm.tsx +64 -4
  33. package/src/form/PropertyFieldBinding.tsx +4 -3
  34. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +18 -5
  35. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +18 -5
  36. package/src/form/field_bindings/BlockFieldBinding.tsx +21 -7
  37. package/src/form/field_bindings/DateTimeFieldBinding.tsx +1 -1
  38. package/src/form/field_bindings/KeyValueFieldBinding.tsx +23 -6
  39. package/src/form/field_bindings/MapFieldBinding.tsx +23 -8
  40. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +43 -20
  41. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +15 -1
  42. package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +25 -11
  43. package/src/form/field_bindings/ReferenceFieldBinding.tsx +25 -11
  44. package/src/form/field_bindings/RepeatFieldBinding.tsx +18 -5
  45. package/src/form/field_bindings/SelectFieldBinding.tsx +7 -5
  46. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +24 -7
  47. package/src/form/field_bindings/SwitchFieldBinding.tsx +31 -14
  48. package/src/form/field_bindings/TextFieldBinding.tsx +10 -7
  49. package/src/form/field_bindings/UserSelectFieldBinding.tsx +7 -5
  50. package/src/index.ts +1 -0
  51. package/src/locales/de.ts +28 -1
  52. package/src/locales/en.ts +27 -0
  53. package/src/locales/es.ts +28 -1
  54. package/src/locales/fr.ts +28 -1
  55. package/src/locales/hi.ts +28 -1
  56. package/src/locales/it.ts +28 -1
  57. package/src/locales/pt.ts +28 -1
  58. package/src/preview/PropertyPreview.tsx +3 -2
  59. package/src/preview/components/ReferencePreview.tsx +2 -1
  60. package/src/preview/property_previews/MapPropertyPreview.tsx +49 -27
  61. package/src/routes/FireCMSRoute.tsx +63 -54
  62. package/src/types/collections.ts +40 -0
  63. package/src/types/properties.ts +11 -10
  64. package/src/types/translations.ts +27 -0
  65. package/src/util/index.ts +1 -0
  66. package/src/util/lazy_eager.tsx +33 -0
  67. package/src/util/objects.ts +15 -0
@@ -125,6 +125,35 @@ export function extractTouchedValues(values: any, touched: Record<string, boolea
125
125
  return acc;
126
126
  }
127
127
 
128
+ /**
129
+ * Recursively removes empty plain objects `{}` and empty arrays `[]` from a value tree.
130
+ * This prevents ghost containers created by `setIn` intermediate path construction
131
+ * (e.g. `{ address: {} }` when only `address.city` was touched but value is undefined)
132
+ * from falsely triggering the unsaved local changes indicator.
133
+ */
134
+ function removeEmptyContainers(obj: any): any {
135
+ if (Array.isArray(obj)) {
136
+ const cleaned = obj.map(removeEmptyContainers);
137
+ // Keep arrays even if they contain only nulls/undefined — that's intentional data
138
+ return cleaned;
139
+ }
140
+ if (obj && typeof obj === "object" && Object.getPrototypeOf(obj) === Object.prototype) {
141
+ const result: Record<string, any> = {};
142
+ for (const key of Object.keys(obj)) {
143
+ const cleaned = removeEmptyContainers(obj[key]);
144
+ // Skip empty plain objects
145
+ if (cleaned && typeof cleaned === "object" && !Array.isArray(cleaned)
146
+ && Object.getPrototypeOf(cleaned) === Object.prototype
147
+ && Object.keys(cleaned).length === 0) {
148
+ continue;
149
+ }
150
+ result[key] = cleaned;
151
+ }
152
+ return result;
153
+ }
154
+ return obj;
155
+ }
156
+
128
157
  export function getChanges<T extends object>(source: Partial<T>, comparison: Partial<T>): Partial<T> {
129
158
  const changes: Partial<T> = {};
130
159
 
@@ -251,7 +280,7 @@ export function EntityForm<M extends Record<string, any>>({
251
280
  const context = useFireCMSContext();
252
281
  const analyticsController = useAnalyticsController();
253
282
 
254
- const [underlyingChanges] = useState<Partial<EntityValues<M>>>({});
283
+ const [underlyingChanges, setUnderlyingChanges] = useState<Partial<EntityValues<M>>>({});
255
284
 
256
285
  const [customIdLoading, setCustomIdLoading] = useState<boolean>(false);
257
286
 
@@ -332,7 +361,15 @@ export function EntityForm<M extends Record<string, any>>({
332
361
  return [initialValues, initialDirty];
333
362
  }, [autoApplyLocalChanges, localChangesDataRaw, baseInitialValues, initialDirtyValues]);
334
363
 
335
- const hasLocalChanges = !localChangesCleared && localChangesDataRaw && Object.keys(localChangesDataRaw).length > 0;
364
+ const hasLocalChanges = useMemo(() => {
365
+ if (localChangesCleared || !localChangesDataRaw || Object.keys(localChangesDataRaw).length === 0) {
366
+ return false;
367
+ }
368
+ // Compare cached values against entity values to check for real differences
369
+ const entityValues = entity?.values ?? {};
370
+ const realChanges = getChanges(localChangesDataRaw as Partial<M>, entityValues as Partial<M>);
371
+ return Object.keys(realChanges).length > 0;
372
+ }, [localChangesCleared, localChangesDataRaw, entity?.values]);
336
373
 
337
374
  const formex: FormexController<M> = formexProp ?? useCreateFormex<M>({
338
375
  initialValues: initialValues as M,
@@ -352,8 +389,10 @@ export function EntityForm<M extends Record<string, any>>({
352
389
  onValuesChangeDeferred: (values: M, controller: FormexController<M>) => {
353
390
  const key = (status === "new" || status === "copy") ? path + "#new" : path + "/" + entityId;
354
391
  if (controller.dirty) {
355
- const touchedValues = extractTouchedValues(values, controller.touched);
356
- saveEntityToCache(key, touchedValues);
392
+ const touchedValues = removeEmptyContainers(extractTouchedValues(values, controller.touched));
393
+ if (touchedValues && Object.keys(touchedValues).length > 0) {
394
+ saveEntityToCache(key, touchedValues);
395
+ }
357
396
  }
358
397
  },
359
398
  validation: (values) => {
@@ -666,6 +705,27 @@ export function EntityForm<M extends Record<string, any>>({
666
705
 
667
706
  useOnAutoSave(autoSave, formex, lastSavedValues, save);
668
707
 
708
+ // Detect external changes to the entity (e.g. from onSnapshot after Admin SDK writes)
709
+ const prevEntityValuesRef = useRef<EntityValues<M> | undefined>(entity?.values);
710
+ useEffect(() => {
711
+ if (!entity?.values || status !== "existing") return;
712
+ const prev = prevEntityValuesRef.current;
713
+ prevEntityValuesRef.current = entity.values;
714
+ if (prev && !equal(prev, entity.values)) {
715
+ // Compute the diff between the old and new entity values
716
+ const changes: Partial<EntityValues<M>> = {};
717
+ const allKeys = new Set([...Object.keys(prev), ...Object.keys(entity.values)]);
718
+ for (const key of allKeys) {
719
+ if (!equal((prev as any)[key], (entity.values as any)[key])) {
720
+ (changes as any)[key] = (entity.values as any)[key];
721
+ }
722
+ }
723
+ if (Object.keys(changes).length > 0) {
724
+ setUnderlyingChanges(changes);
725
+ }
726
+ }
727
+ }, [entity?.values, status]);
728
+
669
729
  useEffect(() => {
670
730
  if (!autoSave && !formex.isSubmitting && underlyingChanges && entity) {
671
731
  // we update the form fields from the Firestore data
@@ -20,7 +20,7 @@ import { isHidden, isPropertyBuilder, isReadOnly, resolveProperty } from "../uti
20
20
  import { useAuthController, useCustomizationController, useTranslation } from "../hooks";
21
21
  import { Typography } from "@firecms/ui";
22
22
  import { getFieldConfig, getFieldId } from "../core";
23
- import { ErrorBoundary } from "../components";
23
+ import { ErrorBoundary, CircularProgressCenter } from "../components";
24
24
 
25
25
  /**
26
26
  * This component renders a form field creating the corresponding configuration
@@ -287,8 +287,9 @@ function FieldInternal<T extends CMSType, CustomProps, M extends Record<string,
287
287
 
288
288
  return (
289
289
  <ErrorBoundary>
290
-
291
- <UsedComponent {...cmsFieldProps} />
290
+ <React.Suspense fallback={<CircularProgressCenter />}>
291
+ <UsedComponent {...cmsFieldProps} />
292
+ </React.Suspense>
292
293
 
293
294
  {underlyingValueHasChanged && !isSubmitting &&
294
295
  <Typography variant={"caption"} className={"ml-3.5"}>
@@ -2,7 +2,7 @@ import React from "react";
2
2
  import { FieldProps } from "../../types";
3
3
  import { FieldHelperText, LabelWithIconAndTooltip } from "../components";
4
4
  import { PropertyFieldBinding } from "../PropertyFieldBinding";
5
- import { ExpandablePanel, Typography } from "@firecms/ui";
5
+ import { ExpandablePanel, IconButton, CloseIcon } from "@firecms/ui";
6
6
  import { getArrayResolvedProperties, getIconForProperty, isReadOnly } from "../../util";
7
7
  import { useClearRestoreValue } from "../useClearRestoreValue";
8
8
  import { useAuthController } from "../../hooks";
@@ -50,15 +50,28 @@ export function ArrayCustomShapedFieldBinding<T extends Array<any>>({
50
50
  setValue
51
51
  });
52
52
 
53
- const title = (<>
53
+ const title = (<div className="flex items-center w-full">
54
54
  <LabelWithIconAndTooltip
55
55
  propertyKey={propertyKey}
56
56
  icon={getIconForProperty(property, "small")}
57
57
  required={property.validation?.required}
58
58
  title={property.name}
59
- className={"h-8 flex-grow text-text-secondary dark:text-text-secondary-dark"}/>
60
- {Array.isArray(value) && <Typography variant={"caption"} className={"px-4"}>({value.length})</Typography>}
61
- </>);
59
+ className={"text-text-secondary dark:text-text-secondary-dark"}/>
60
+ {Array.isArray(value) && <span className={"text-sm text-text-secondary dark:text-text-secondary-dark ml-1"}>({value.length})</span>}
61
+ <div className="flex-grow"/>
62
+ {(property.nullable || property.clearable) && !disabled && (
63
+ <IconButton
64
+ size="small"
65
+ onClick={(e) => {
66
+ e.stopPropagation();
67
+ e.preventDefault();
68
+ setValue(null);
69
+ }}
70
+ >
71
+ <CloseIcon size={"small"}/>
72
+ </IconButton>
73
+ )}
74
+ </div>);
62
75
 
63
76
  const body = resolvedProperties.map((childProperty, index) => {
64
77
  const thisDisabled = isReadOnly(childProperty) || Boolean(childProperty.disabled);
@@ -5,7 +5,7 @@ import { FieldHelperText, LabelWithIconAndTooltip } from "../components";
5
5
  import { ArrayContainer, ArrayEntryParams, ErrorView } from "../../components";
6
6
  import { getIconForProperty, getReferenceFrom } from "../../util";
7
7
  import { useNavigationController, useReferenceDialog, useTranslation } from "../../hooks";
8
- import { Button, cls, EditIcon, ExpandablePanel, fieldBackgroundMixin, Typography } from "@firecms/ui";
8
+ import { Button, cls, EditIcon, ExpandablePanel, fieldBackgroundMixin, Typography, IconButton, CloseIcon } from "@firecms/ui";
9
9
  import { useClearRestoreValue } from "../useClearRestoreValue";
10
10
 
11
11
  type ArrayOfReferencesFieldProps = FieldProps<EntityReference[]>;
@@ -100,15 +100,28 @@ export function ArrayOfReferencesFieldBinding({
100
100
  );
101
101
  }, [ofProperty.path, ofProperty.previewProperties, value]);
102
102
 
103
- const title = (<>
103
+ const title = (<div className="flex items-center w-full">
104
104
  <LabelWithIconAndTooltip
105
105
  propertyKey={propertyKey}
106
106
  icon={getIconForProperty(property, "small")}
107
107
  required={property.validation?.required}
108
108
  title={property.name}
109
- className={"h-8 flex flex-grow text-text-secondary dark:text-text-secondary-dark"}/>
110
- {Array.isArray(value) && <Typography variant={"caption"} className={"px-4"}>({value.length})</Typography>}
111
- </>);
109
+ className={"text-text-secondary dark:text-text-secondary-dark"}/>
110
+ {Array.isArray(value) && <span className={"text-sm text-text-secondary dark:text-text-secondary-dark ml-1"}>({value.length})</span>}
111
+ <div className="flex-grow"/>
112
+ {(property.nullable || property.clearable) && !disabled && (
113
+ <IconButton
114
+ size="small"
115
+ onClick={(e) => {
116
+ e.stopPropagation();
117
+ e.preventDefault();
118
+ setValue(null);
119
+ }}
120
+ >
121
+ <CloseIcon size={"small"}/>
122
+ </IconButton>
123
+ )}
124
+ </div>);
112
125
 
113
126
  const body = <>
114
127
  {!collection && <ErrorView
@@ -8,7 +8,7 @@ import { EnumValuesChip } from "../../preview";
8
8
  import { FieldProps, FormContext, PropertyFieldBindingProps, PropertyOrBuilder } from "../../types";
9
9
  import { getDefaultValueFor, getIconForProperty, mergeDeep, } from "../../util";
10
10
  import { DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE } from "../../util/common";
11
- import { cls, ExpandablePanel, paperMixin, Select, SelectItem, Typography } from "@firecms/ui";
11
+ import { cls, ExpandablePanel, paperMixin, Select, SelectItem, Typography, IconButton, CloseIcon } from "@firecms/ui";
12
12
  import { useClearRestoreValue } from "../useClearRestoreValue";
13
13
  import { ArrayContainer, ArrayEntryParams } from "../../components";
14
14
  import { useTranslation } from "../../hooks/useTranslation";
@@ -74,12 +74,26 @@ export function BlockFieldBinding<T extends Array<any>>({
74
74
  };
75
75
 
76
76
  const title = (
77
- <LabelWithIconAndTooltip
78
- propertyKey={propertyKey}
79
- icon={getIconForProperty(property, "small")}
80
- required={property.validation?.required}
81
- title={property.name}
82
- className={"text-text-secondary dark:text-text-secondary-dark"}/>
77
+ <div className="flex items-center w-full">
78
+ <LabelWithIconAndTooltip
79
+ propertyKey={propertyKey}
80
+ icon={getIconForProperty(property, "small")}
81
+ required={property.validation?.required}
82
+ title={property.name}
83
+ className={"text-text-secondary dark:text-text-secondary-dark flex-grow"}/>
84
+ {(property.nullable || property.clearable) && !disabled && (
85
+ <IconButton
86
+ size="small"
87
+ onClick={(e) => {
88
+ e.stopPropagation();
89
+ e.preventDefault();
90
+ setValue(null);
91
+ }}
92
+ >
93
+ <CloseIcon size={"small"}/>
94
+ </IconButton>
95
+ )}
96
+ </div>
83
97
  );
84
98
 
85
99
  const firstOneOfKey = Object.keys(property.oneOf.properties)[0];
@@ -48,7 +48,7 @@ export function DateTimeFieldBinding({
48
48
  onChange={(dateValue) => setValue(dateValue)}
49
49
  size={"large"}
50
50
  mode={property.mode}
51
- clearable={property.clearable}
51
+ clearable={property.nullable || property.clearable}
52
52
  locale={locale}
53
53
  timezone={property.timezone}
54
54
  error={showError}
@@ -13,6 +13,7 @@ import {
13
13
  defaultBorderMixin,
14
14
  ExpandablePanel,
15
15
  IconButton,
16
+ CloseIcon,
16
17
  Menu,
17
18
  MenuItem,
18
19
  RemoveIcon,
@@ -64,12 +65,28 @@ export function KeyValueFieldBinding({
64
65
  initialValue={initialValues}
65
66
  fieldName={property.name ?? propertyKey}/>;
66
67
 
67
- const title = <LabelWithIconAndTooltip
68
- propertyKey={propertyKey}
69
- icon={getIconForProperty(property, "small")}
70
- required={property.validation?.required}
71
- title={property.name}
72
- className={"text-text-secondary dark:text-text-secondary-dark"}/>;
68
+ const title = (
69
+ <div className="flex items-center w-full">
70
+ <LabelWithIconAndTooltip
71
+ propertyKey={propertyKey}
72
+ icon={getIconForProperty(property, "small")}
73
+ required={property.validation?.required}
74
+ title={property.name}
75
+ className={"text-text-secondary dark:text-text-secondary-dark flex-grow"}/>
76
+ {(property.nullable || property.clearable) && !disabled && (
77
+ <IconButton
78
+ size="small"
79
+ onClick={(e) => {
80
+ e.stopPropagation();
81
+ e.preventDefault();
82
+ setValue(null);
83
+ }}
84
+ >
85
+ <CloseIcon size={"small"}/>
86
+ </IconButton>
87
+ )}
88
+ </div>
89
+ );
73
90
 
74
91
  return (
75
92
  <>
@@ -6,7 +6,7 @@ import { getIconForProperty, isHidden, isReadOnly, pick } from "../../util";
6
6
  import { FieldHelperText, LabelWithIconAndTooltip } from "../components";
7
7
  import { FormEntry } from "../components/FormEntry";
8
8
  import { PropertyFieldBinding } from "../PropertyFieldBinding";
9
- import { cls, ExpandablePanel, InputLabel, Select, SelectItem } from "@firecms/ui";
9
+ import { cls, ExpandablePanel, InputLabel, Select, SelectItem, IconButton, CloseIcon } from "@firecms/ui";
10
10
  import { useTranslation } from "../../hooks";
11
11
 
12
12
  /**
@@ -28,7 +28,8 @@ export function MapFieldBinding({
28
28
  includeDescription,
29
29
  autoFocus,
30
30
  context,
31
- onPropertyChange
31
+ onPropertyChange,
32
+ setValue
32
33
  }: FieldProps<Record<string, any>>) {
33
34
 
34
35
  const pickOnlySomeKeys = property.pickOnlySomeKeys || false;
@@ -108,12 +109,26 @@ export function MapFieldBinding({
108
109
  }}
109
110
  className={property.widthPercentage !== undefined ? "mt-8" : undefined}
110
111
  innerClassName={"px-2 md:px-4 pb-2 md:pb-4 pt-1 md:pt-2 bg-white dark:bg-surface-900"}
111
- title={<LabelWithIconAndTooltip
112
- propertyKey={propertyKey}
113
- icon={getIconForProperty(property, "small")}
114
- required={property.validation?.required}
115
- title={property.name}
116
- className={"text-text-secondary dark:text-text-secondary-dark"} />}>
112
+ title={<div className="flex items-center w-full">
113
+ <LabelWithIconAndTooltip
114
+ propertyKey={propertyKey}
115
+ icon={getIconForProperty(property, "small")}
116
+ required={property.validation?.required}
117
+ title={property.name}
118
+ className={"text-text-secondary dark:text-text-secondary-dark flex-grow"} />
119
+ {(property.nullable || property.clearable) && !disabled && (
120
+ <IconButton
121
+ size="small"
122
+ onClick={(e) => {
123
+ e.stopPropagation();
124
+ e.preventDefault();
125
+ setValue(null);
126
+ }}
127
+ >
128
+ <CloseIcon size={"small"}/>
129
+ </IconButton>
130
+ )}
131
+ </div>}>
117
132
  {mapFormView}
118
133
  </ExpandablePanel>}
119
134
 
@@ -11,10 +11,14 @@ import {
11
11
  useAuthController,
12
12
  useStorageSource
13
13
  } from "../../index";
14
- import { cls, fieldBackgroundDisabledMixin, fieldBackgroundHoverMixin, fieldBackgroundMixin } from "@firecms/ui";
15
- import { FireCMSEditor, FireCMSEditorProps } from "../../editor";
14
+ import { cls, fieldBackgroundDisabledMixin, fieldBackgroundHoverMixin, fieldBackgroundMixin, IconButton, CloseIcon } from "@firecms/ui";
15
+ import type { FireCMSEditorProps } from "../../editor";
16
16
  import { resolveProperty, resolveStorageFilenameString, resolveStoragePathString } from "../../util";
17
17
  import { isImageFile, resizeImage } from "../../util/useStorageUploadController";
18
+ import { lazyEager } from "../../util/lazy_eager";
19
+ import { CircularProgressCenter } from "../../components/CircularProgressCenter";
20
+
21
+ const FireCMSEditor = lazyEager<typeof import("../../editor/editor")["FireCMSEditor"]>(() => import("../../editor/editor"), "FireCMSEditor");
18
22
 
19
23
  interface MarkdownEditorFieldProps {
20
24
  highlight?: { from: number, to: number };
@@ -51,7 +55,7 @@ export function MarkdownEditorFieldBinding({
51
55
  const internalValue = useRef<string | null>(value);
52
56
 
53
57
  const onContentChange = useCallback((content: string) => {
54
- if (content === value || (value === null && content === "")) {
58
+ if (content === value || ((value === null || value === undefined) && content === "")) {
55
59
  return;
56
60
  }
57
61
  internalValue.current = content;
@@ -135,17 +139,21 @@ export function MarkdownEditorFieldBinding({
135
139
  return url;
136
140
  };
137
141
 
138
- const editor = <FireCMSEditor
139
- key={context.formex.version + fieldVersion}
140
- content={value}
141
- onMarkdownContentChange={onContentChange}
142
- version={context.formex.version + fieldVersion}
143
- highlight={highlight}
144
- disabled={disabled}
145
- markdownConfig={markdownConfig}
146
- handleImageUpload={handleImageUpload}
147
- {...editorProps}
148
- />;
142
+ const editor = (
143
+ <React.Suspense fallback={<CircularProgressCenter />}>
144
+ <FireCMSEditor
145
+ key={context.formex.version + fieldVersion}
146
+ content={value}
147
+ onMarkdownContentChange={onContentChange}
148
+ version={context.formex.version + fieldVersion}
149
+ highlight={highlight}
150
+ disabled={disabled}
151
+ markdownConfig={markdownConfig}
152
+ handleImageUpload={handleImageUpload}
153
+ {...editorProps}
154
+ />
155
+ </React.Suspense>
156
+ );
149
157
 
150
158
  if (minimalistView)
151
159
  return (
@@ -156,12 +164,27 @@ export function MarkdownEditorFieldBinding({
156
164
 
157
165
  return (
158
166
  <>
159
- <LabelWithIconAndTooltip
160
- propertyKey={propertyKey}
161
- icon={getIconForProperty(property, "small")}
162
- required={property.validation?.required}
163
- title={property.name}
164
- className={"h-8 text-text-secondary dark:text-text-secondary-dark ml-3.5"} />
167
+ <div className="flex items-center w-full">
168
+ <LabelWithIconAndTooltip
169
+ propertyKey={propertyKey}
170
+ icon={getIconForProperty(property, "small")}
171
+ required={property.validation?.required}
172
+ title={property.name}
173
+ className={"h-8 text-text-secondary dark:text-text-secondary-dark ml-3.5"} />
174
+ <div className="flex-grow"/>
175
+ {(property.nullable || property.clearable) && !disabled && (
176
+ <IconButton
177
+ size="small"
178
+ onClick={(e) => {
179
+ e.stopPropagation();
180
+ e.preventDefault();
181
+ setValue(null);
182
+ }}
183
+ >
184
+ <CloseIcon size={"small"}/>
185
+ </IconButton>
186
+ )}
187
+ </div>
165
188
  <div
166
189
  className={cls("rounded-md", fieldBackgroundMixin, disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin)}>
167
190
  {editor}
@@ -4,7 +4,7 @@ import { EnumType, FieldProps, ResolvedProperty } from "../../types";
4
4
  import { FieldHelperText, LabelWithIconAndTooltip } from "../components";
5
5
  import { EnumValuesChip } from "../../preview";
6
6
  import { enumToObjectEntries, getIconForProperty, getLabelOrConfigFrom } from "../../util";
7
- import { CloseIcon, MultiSelect, MultiSelectItem } from "@firecms/ui";
7
+ import { CloseIcon, MultiSelect, MultiSelectItem, IconButton } from "@firecms/ui";
8
8
  import { useClearRestoreValue } from "../useClearRestoreValue";
9
9
 
10
10
  /**
@@ -93,6 +93,20 @@ export function MultiSelectFieldBinding({
93
93
  required={property.validation?.required}
94
94
  title={property.name}
95
95
  className={"h-8 text-text-secondary dark:text-text-secondary-dark ml-3.5"}/>}
96
+ endAdornment={
97
+ (property.nullable || property.clearable) && !disabled && value !== null && value !== undefined ? (
98
+ <IconButton
99
+ size="small"
100
+ onClick={(e) => {
101
+ e.stopPropagation();
102
+ e.preventDefault();
103
+ setValue(null);
104
+ }}
105
+ >
106
+ <CloseIcon size={"small"}/>
107
+ </IconButton>
108
+ ) : undefined
109
+ }
96
110
  onValueChange={(updatedValue: string[]) => {
97
111
  let newValue: EnumType[] | null;
98
112
  if (of && (of as ResolvedProperty)?.dataType === "number") {
@@ -8,7 +8,7 @@ import { ReferencePreview } from "../../preview";
8
8
  import { getIconForProperty, IconForView } from "../../util";
9
9
  import { useClearRestoreValue } from "../useClearRestoreValue";
10
10
  import { EntityPreviewContainer } from "../../components/EntityPreview";
11
- import { cls } from "@firecms/ui";
11
+ import { cls, IconButton, CloseIcon } from "@firecms/ui";
12
12
 
13
13
  /**
14
14
  * Field that opens a reference selection dialog and stores the entity ID as a string.
@@ -98,16 +98,30 @@ function ReferenceAsStringFieldBindingInternal({
98
98
 
99
99
  {collection && <>
100
100
 
101
- {referenceValue && <ReferencePreview
102
- disabled={!path}
103
- previewProperties={property.reference?.previewProperties}
104
- hover={!disabled}
105
- size={size}
106
- onClick={disabled || isSubmitting ? undefined : onEntryClick}
107
- reference={referenceValue}
108
- includeEntityLink={property.reference?.includeEntityLink}
109
- includeId={property.reference?.includeId}
110
- />}
101
+ {referenceValue && <div className="flex items-center gap-2">
102
+ <ReferencePreview
103
+ disabled={!path}
104
+ previewProperties={property.reference?.previewProperties}
105
+ hover={!disabled}
106
+ size={size}
107
+ onClick={disabled || isSubmitting ? undefined : onEntryClick}
108
+ reference={referenceValue}
109
+ includeEntityLink={property.reference?.includeEntityLink}
110
+ includeId={property.reference?.includeId}
111
+ />
112
+ {(property.nullable || property.clearable) && !disabled && (
113
+ <IconButton
114
+ size="small"
115
+ onClick={(e) => {
116
+ e.stopPropagation();
117
+ e.preventDefault();
118
+ setValue(null);
119
+ }}
120
+ >
121
+ <CloseIcon size={"small"}/>
122
+ </IconButton>
123
+ )}
124
+ </div>}
111
125
 
112
126
  {!value && <div className="justify-center text-left">
113
127
  <EntityPreviewContainer
@@ -9,7 +9,7 @@ import { EmptyValue, ReferencePreview } from "../../preview";
9
9
  import { getIconForProperty, getReferenceFrom, IconForView } from "../../util";
10
10
  import { useClearRestoreValue } from "../useClearRestoreValue";
11
11
  import { EntityPreviewContainer } from "../../components/EntityPreview";
12
- import { cls } from "@firecms/ui";
12
+ import { cls, IconButton, CloseIcon } from "@firecms/ui";
13
13
 
14
14
  /**
15
15
  * Field that opens a reference selection dialog.
@@ -97,16 +97,30 @@ function ReferenceFieldBindingInternal({
97
97
 
98
98
  {collection && <>
99
99
 
100
- {value && <ReferencePreview
101
- disabled={!property.path}
102
- previewProperties={property.previewProperties}
103
- hover={!disabled}
104
- size={size}
105
- onClick={disabled || isSubmitting ? undefined : onEntryClick}
106
- reference={value}
107
- includeEntityLink={property.includeEntityLink}
108
- includeId={property.includeId}
109
- />}
100
+ {value && <div className="flex items-center gap-2">
101
+ <ReferencePreview
102
+ disabled={!property.path}
103
+ previewProperties={property.previewProperties}
104
+ hover={!disabled}
105
+ size={size}
106
+ onClick={disabled || isSubmitting ? undefined : onEntryClick}
107
+ reference={value}
108
+ includeEntityLink={property.includeEntityLink}
109
+ includeId={property.includeId}
110
+ />
111
+ {(property.nullable || property.clearable) && !disabled && (
112
+ <IconButton
113
+ size="small"
114
+ onClick={(e) => {
115
+ e.stopPropagation();
116
+ e.preventDefault();
117
+ setValue(null);
118
+ }}
119
+ >
120
+ <CloseIcon size={"small"}/>
121
+ </IconButton>
122
+ )}
123
+ </div>}
110
124
 
111
125
  {!value && <div className="justify-center text-left">
112
126
  <EntityPreviewContainer className={cls("px-6 h-16 text-sm font-medium flex items-center gap-6",
@@ -4,7 +4,7 @@ import { FieldHelperText, LabelWithIconAndTooltip } from "../components";
4
4
  import { ArrayContainer, ArrayEntryParams, ErrorBoundary } from "../../components";
5
5
  import { getArrayResolvedProperties, getDefaultValueFor, getIconForProperty, mergeDeep } from "../../util";
6
6
  import { PropertyFieldBinding } from "../PropertyFieldBinding";
7
- import { ExpandablePanel, Typography } from "@firecms/ui";
7
+ import { ExpandablePanel, Typography, IconButton, CloseIcon } from "@firecms/ui";
8
8
  import { useClearRestoreValue } from "../useClearRestoreValue";
9
9
  import { useAuthController } from "../../hooks";
10
10
  import { useTranslation } from "../../hooks/useTranslation";
@@ -101,15 +101,28 @@ export function RepeatFieldBinding<T extends Array<any>>({
101
101
  className={property.widthPercentage !== undefined ? "mt-8" : undefined}
102
102
  />;
103
103
 
104
- const title = (<>
104
+ const title = (<div className="flex items-center w-full">
105
105
  <LabelWithIconAndTooltip
106
106
  propertyKey={propertyKey}
107
107
  icon={getIconForProperty(property, "small")}
108
108
  required={property.validation?.required}
109
109
  title={property.name}
110
- className={"h-8 flex flex-grow text-text-secondary dark:text-text-secondary-dark"}/>
111
- {Array.isArray(value) && <Typography variant={"caption"} className={"px-4"}>({value.length})</Typography>}
112
- </>);
110
+ className={"text-text-secondary dark:text-text-secondary-dark"}/>
111
+ {Array.isArray(value) && <span className={"text-sm text-text-secondary dark:text-text-secondary-dark ml-1"}>({value.length})</span>}
112
+ <div className="flex-grow"/>
113
+ {(property.nullable || property.clearable) && !disabled && (
114
+ <IconButton
115
+ size="small"
116
+ onClick={(e) => {
117
+ e.stopPropagation();
118
+ e.preventDefault();
119
+ setValue(null);
120
+ }}
121
+ >
122
+ <CloseIcon size={"small"}/>
123
+ </IconButton>
124
+ )}
125
+ </div>);
113
126
 
114
127
  return (
115
128
 
@@ -66,11 +66,13 @@ export function SelectFieldBinding<T extends EnumType>({
66
66
  />
67
67
  </PropertyIdCopyTooltip>}
68
68
  endAdornment={
69
- property.clearable && !disabled && <IconButton
70
- size="small"
71
- onClick={handleClearClick}>
72
- <CloseIcon size={"small"}/>
73
- </IconButton>
69
+ (property.nullable || property.clearable) && !disabled && value !== null && value !== undefined ? (
70
+ <IconButton
71
+ size="small"
72
+ onClick={handleClearClick}>
73
+ <CloseIcon size={"small"}/>
74
+ </IconButton>
75
+ ) : undefined
74
76
  }
75
77
  onValueChange={(updatedValue: string) => {
76
78
  const newValue = updatedValue