@firecms/core 3.2.0-canary.9c3d298 → 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.
- package/dist/components/VirtualTable/VirtualTableHeader.d.ts +1 -0
- package/dist/components/VirtualTable/VirtualTableHeaderRow.d.ts +1 -1
- package/dist/components/VirtualTable/VirtualTableProps.d.ts +6 -1
- package/dist/components/VirtualTable/types.d.ts +1 -0
- package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +20186 -19539
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +24292 -23645
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collections.d.ts +38 -0
- package/dist/types/properties.d.ts +9 -8
- package/dist/types/translations.d.ts +23 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/lazy_eager.d.ts +7 -0
- package/dist/util/objects.d.ts +1 -0
- package/package.json +4 -4
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +9 -3
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +3 -5
- package/src/components/EntityJsonPreview.tsx +2 -1
- package/src/components/ErrorBoundary.tsx +3 -3
- package/src/components/VirtualTable/VirtualTable.tsx +5 -3
- package/src/components/VirtualTable/VirtualTableHeader.tsx +9 -8
- package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +8 -3
- package/src/components/VirtualTable/VirtualTableProps.tsx +7 -1
- package/src/components/VirtualTable/types.tsx +1 -0
- package/src/core/DrawerNavigationGroup.tsx +1 -1
- package/src/core/EntityEditView.tsx +50 -5
- package/src/core/EntitySidePanel.tsx +2 -1
- package/src/core/field_configs.tsx +4 -2
- package/src/form/EntityForm.tsx +64 -4
- package/src/form/PropertyFieldBinding.tsx +4 -3
- package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +18 -5
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +18 -5
- package/src/form/field_bindings/BlockFieldBinding.tsx +21 -7
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +1 -1
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +23 -6
- package/src/form/field_bindings/MapFieldBinding.tsx +23 -8
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +43 -20
- package/src/form/field_bindings/MultiSelectFieldBinding.tsx +15 -1
- package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +25 -11
- package/src/form/field_bindings/ReferenceFieldBinding.tsx +25 -11
- package/src/form/field_bindings/RepeatFieldBinding.tsx +18 -5
- package/src/form/field_bindings/SelectFieldBinding.tsx +7 -5
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +24 -7
- package/src/form/field_bindings/SwitchFieldBinding.tsx +31 -14
- package/src/form/field_bindings/TextFieldBinding.tsx +10 -7
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +7 -5
- package/src/index.ts +1 -0
- package/src/locales/de.ts +28 -1
- package/src/locales/en.ts +27 -0
- package/src/locales/es.ts +28 -1
- package/src/locales/fr.ts +28 -1
- package/src/locales/hi.ts +28 -1
- package/src/locales/it.ts +28 -1
- package/src/locales/pt.ts +28 -1
- package/src/preview/PropertyPreview.tsx +3 -2
- package/src/preview/components/ReferencePreview.tsx +2 -1
- package/src/preview/property_previews/MapPropertyPreview.tsx +49 -27
- package/src/routes/FireCMSRoute.tsx +63 -54
- package/src/types/collections.ts +40 -0
- package/src/types/properties.ts +11 -10
- package/src/types/translations.ts +27 -0
- package/src/util/index.ts +1 -0
- package/src/util/lazy_eager.tsx +33 -0
- package/src/util/objects.ts +15 -0
package/src/form/EntityForm.tsx
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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={"
|
|
60
|
-
{Array.isArray(value) && <
|
|
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={"
|
|
110
|
-
{Array.isArray(value) && <
|
|
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
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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 =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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={<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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 {
|
|
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 =
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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 && <
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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 && <
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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={"
|
|
111
|
-
{Array.isArray(value) && <
|
|
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 &&
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|