@firecms/core 3.0.0-canary.287 → 3.0.0-canary.289
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/PropertyCollectionView.d.ts +23 -0
- package/dist/form/EntityForm.d.ts +1 -0
- package/dist/index.es.js +624 -158
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +630 -164
- package/dist/index.umd.js.map +1 -1
- package/dist/util/entity_cache.d.ts +1 -0
- package/dist/util/objects.d.ts +1 -0
- package/package.json +5 -5
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +44 -47
- package/src/components/EntityView.tsx +29 -40
- package/src/components/PropertyCollectionView.tsx +329 -0
- package/src/form/EntityForm.tsx +63 -11
- package/src/form/components/LocalChangesMenu.tsx +38 -55
- package/src/preview/property_previews/MapPropertyPreview.tsx +2 -2
- package/src/preview/property_previews/NumberPropertyPreview.tsx +2 -2
- package/src/util/entity_cache.ts +35 -0
- package/src/util/objects.ts +40 -2
package/src/form/EntityForm.tsx
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
getLocalChangesBackup,
|
|
23
23
|
getValueInPath,
|
|
24
24
|
isHidden,
|
|
25
|
+
isObject,
|
|
25
26
|
isReadOnly,
|
|
26
27
|
mergeDeep,
|
|
27
28
|
resolveCollection,
|
|
@@ -39,11 +40,12 @@ import {
|
|
|
39
40
|
useSnackbarController
|
|
40
41
|
} from "../hooks";
|
|
41
42
|
import { Alert, CheckIcon, Chip, cls, EditIcon, NotesIcon, paperMixin, Tooltip, Typography } from "@firecms/ui";
|
|
42
|
-
import {
|
|
43
|
+
import { Formex, FormexController, getIn, setIn, useCreateFormex } from "@firecms/formex";
|
|
43
44
|
import { useAnalyticsController } from "../hooks/useAnalyticsController";
|
|
44
45
|
import { FormEntry, FormLayout, LabelWithIconAndTooltip, PropertyFieldBinding } from "../form";
|
|
45
46
|
import { ValidationError } from "yup";
|
|
46
47
|
import {
|
|
48
|
+
flattenKeys,
|
|
47
49
|
getEntityFromCache,
|
|
48
50
|
removeEntityFromCache,
|
|
49
51
|
removeEntityFromMemoryCache,
|
|
@@ -120,6 +122,64 @@ export function extractTouchedValues(values: any, touched: Record<string, boolea
|
|
|
120
122
|
return acc;
|
|
121
123
|
}
|
|
122
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
|
+
|
|
123
183
|
export function EntityForm<M extends Record<string, any>>({
|
|
124
184
|
path,
|
|
125
185
|
fullIdPath,
|
|
@@ -272,16 +332,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
272
332
|
if (!localChangesDataRaw) {
|
|
273
333
|
return undefined;
|
|
274
334
|
}
|
|
275
|
-
|
|
276
|
-
const flattenedKeys = flattenKeys(localChangesDataRaw);
|
|
277
|
-
flattenedKeys.forEach(key => {
|
|
278
|
-
const localValue = getIn(localChangesDataRaw, key);
|
|
279
|
-
const initialValue = getIn(initialValues, key);
|
|
280
|
-
if (!equal(localValue, initialValue)) {
|
|
281
|
-
filteredChanges = setIn(filteredChanges, key, localValue);
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
return filteredChanges;
|
|
335
|
+
return getChanges(localChangesDataRaw, initialValues);
|
|
285
336
|
}, [localChangesDataRaw, initialValues]);
|
|
286
337
|
|
|
287
338
|
const hasLocalChanges = !localChangesCleared && localChangesData && Object.keys(localChangesData).length > 0;
|
|
@@ -891,3 +942,4 @@ function useOnAutoSave(autoSave: undefined | boolean, formex: FormexController<a
|
|
|
891
942
|
}
|
|
892
943
|
}, [formex.values]);
|
|
893
944
|
}
|
|
945
|
+
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
2
|
import {
|
|
3
|
-
Button,
|
|
3
|
+
Button,
|
|
4
|
+
CancelIcon,
|
|
5
|
+
CheckIcon,
|
|
4
6
|
defaultBorderMixin,
|
|
5
7
|
Dialog,
|
|
6
8
|
DialogActions,
|
|
7
9
|
DialogContent,
|
|
8
10
|
KeyboardArrowDownIcon,
|
|
9
11
|
Menu,
|
|
10
|
-
MenuItem,
|
|
11
|
-
Typography, VisibilityIcon,
|
|
12
|
+
MenuItem, VisibilityIcon,
|
|
12
13
|
WarningIcon
|
|
13
14
|
} from "@firecms/ui";
|
|
14
|
-
import {
|
|
15
|
+
import { FormexController } from "@firecms/formex";
|
|
15
16
|
import { useSnackbarController } from "../../hooks";
|
|
16
17
|
import { mergeDeep } from "../../util";
|
|
17
|
-
import { removeEntityFromCache } from "../../util/entity_cache";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { ResolvedProperties, ResolvedProperty } from "../../types";
|
|
18
|
+
import { flattenKeys, removeEntityFromCache } from "../../util/entity_cache";
|
|
19
|
+
import { ResolvedProperties } from "../../types";
|
|
20
|
+
import { PropertyCollectionView } from "../../components/PropertyCollectionView";
|
|
21
21
|
|
|
22
22
|
interface LocalChangesMenuProps<M extends object> {
|
|
23
23
|
cacheKey: string;
|
|
@@ -38,13 +38,9 @@ export function LocalChangesMenu<M extends object>({
|
|
|
38
38
|
const snackbarController = useSnackbarController();
|
|
39
39
|
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
|
|
40
40
|
const [open, setOpen] = useState(false);
|
|
41
|
-
const handleOpenMenu = () => {
|
|
42
|
-
setOpen(true)
|
|
43
|
-
};
|
|
44
41
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
};
|
|
42
|
+
const handleOpenMenu = () => setOpen(true);
|
|
43
|
+
const handleCloseMenu = () => setOpen(false);
|
|
48
44
|
|
|
49
45
|
const handlePreview = () => {
|
|
50
46
|
setPreviewDialogOpen(true);
|
|
@@ -54,8 +50,8 @@ export function LocalChangesMenu<M extends object>({
|
|
|
54
50
|
const handleApply = () => {
|
|
55
51
|
const mergedValues = mergeDeep(formex.values, localChangesData);
|
|
56
52
|
const touched = { ...formex.touched };
|
|
57
|
-
const
|
|
58
|
-
|
|
53
|
+
const previewKeys = flattenKeys(localChangesData);
|
|
54
|
+
previewKeys.forEach((key) => {
|
|
59
55
|
touched[key] = true;
|
|
60
56
|
});
|
|
61
57
|
|
|
@@ -81,23 +77,26 @@ export function LocalChangesMenu<M extends object>({
|
|
|
81
77
|
|
|
82
78
|
return (
|
|
83
79
|
<>
|
|
84
|
-
|
|
85
80
|
<Menu
|
|
86
|
-
trigger={
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
81
|
+
trigger={
|
|
82
|
+
<Button
|
|
83
|
+
size={"small"}
|
|
84
|
+
className={
|
|
85
|
+
"font-semibold text-xs rounded-full px-4 py-1 bg-yellow-200 dark:bg-yellow-900 hover:bg-yellow-300 dark:hover:bg-yellow-800 text-yellow-800 dark:text-yellow-200"
|
|
86
|
+
}
|
|
87
|
+
onClick={handleOpenMenu}
|
|
88
|
+
>
|
|
89
|
+
<WarningIcon size={"smallest"} className={"mr-1 text-yellow-600 dark:text-yellow-400"}/>
|
|
90
|
+
Unsaved Local changes
|
|
91
|
+
<KeyboardArrowDownIcon size={"smallest"}/>
|
|
92
|
+
</Button>
|
|
93
|
+
}
|
|
96
94
|
open={open}
|
|
97
95
|
onOpenChange={setOpen}
|
|
98
96
|
>
|
|
99
97
|
<div className={"max-w-xs px-4 py-4 text-sm text-gray-700 dark:text-gray-300"}>
|
|
100
|
-
This document was edited locally and has unsaved changes.
|
|
98
|
+
This document was edited locally and has unsaved changes. These local changes will be lost if you
|
|
99
|
+
don't apply them.
|
|
101
100
|
</div>
|
|
102
101
|
<MenuItem dense onClick={handlePreview}><VisibilityIcon size={"small"}/>Preview Changes</MenuItem>
|
|
103
102
|
<MenuItem dense onClick={handleApply}><CheckIcon size={"small"}/>Apply Changes</MenuItem>
|
|
@@ -114,32 +113,13 @@ export function LocalChangesMenu<M extends object>({
|
|
|
114
113
|
<p className={"mb-4"}>
|
|
115
114
|
These are the local changes that will be applied to the form.
|
|
116
115
|
</p>
|
|
117
|
-
<div
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
return (
|
|
126
|
-
<div key={key}
|
|
127
|
-
className="grid grid-cols-12 gap-x-4 px-4 py-3 items-center">
|
|
128
|
-
<div
|
|
129
|
-
className="col-span-3 text-right">
|
|
130
|
-
<Typography variant="caption"
|
|
131
|
-
className="text-gray-500 dark:text-gray-400 break-words">{property.name || key}</Typography>
|
|
132
|
-
</div>
|
|
133
|
-
<div className="col-span-9">
|
|
134
|
-
<PropertyPreview
|
|
135
|
-
propertyKey={key}
|
|
136
|
-
value={value}
|
|
137
|
-
property={property}
|
|
138
|
-
size={"small"}/>
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
141
|
-
);
|
|
142
|
-
})}
|
|
116
|
+
<div className={`border rounded-lg ${defaultBorderMixin}`} style={{
|
|
117
|
+
maxHeight: 520,
|
|
118
|
+
overflow: "auto"
|
|
119
|
+
}}>
|
|
120
|
+
<div className="p-4">
|
|
121
|
+
<PropertyCollectionView data={localChangesData} properties={properties as ResolvedProperties}/>
|
|
122
|
+
</div>
|
|
143
123
|
</div>
|
|
144
124
|
</DialogContent>
|
|
145
125
|
<DialogActions>
|
|
@@ -149,7 +129,10 @@ export function LocalChangesMenu<M extends object>({
|
|
|
149
129
|
onClick={() => {
|
|
150
130
|
handleApply();
|
|
151
131
|
setPreviewDialogOpen(false);
|
|
152
|
-
}}
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
Apply changes
|
|
135
|
+
</Button>
|
|
153
136
|
</DialogActions>
|
|
154
137
|
</Dialog>
|
|
155
138
|
</>
|
|
@@ -68,7 +68,7 @@ export function MapPropertyPreview<T extends Record<string, any> = Record<string
|
|
|
68
68
|
<div
|
|
69
69
|
className="min-w-[140px] w-[25%] py-1">
|
|
70
70
|
<Typography variant={"caption"}
|
|
71
|
-
className={"
|
|
71
|
+
className={"break-words font-semibold"}
|
|
72
72
|
color={"secondary"}>
|
|
73
73
|
{childProperty.name}
|
|
74
74
|
</Typography>
|
|
@@ -121,7 +121,7 @@ export function KeyValuePreview({ value }: { value: any }) {
|
|
|
121
121
|
key={`table-cell-title-${key}-${key}`}
|
|
122
122
|
className="min-w-[140px] w-[25%] py-1">
|
|
123
123
|
<Typography variant={"caption"}
|
|
124
|
-
className={"font-
|
|
124
|
+
className={"font-semibold break-words"}
|
|
125
125
|
color={"secondary"}>
|
|
126
126
|
{key}
|
|
127
127
|
</Typography>
|
|
@@ -16,12 +16,12 @@ export function NumberPropertyPreview({
|
|
|
16
16
|
const enumKey = value;
|
|
17
17
|
const enumValues = enumToObjectEntries(property.enumValues);
|
|
18
18
|
if (!enumValues)
|
|
19
|
-
return
|
|
19
|
+
return <span className={size === "small" ? "text-sm" : ""}>{value}</span>;
|
|
20
20
|
return <EnumValuesChip
|
|
21
21
|
enumKey={enumKey}
|
|
22
22
|
enumValues={enumValues}
|
|
23
23
|
size={size !== "medium" ? "small" : "medium"}/>;
|
|
24
24
|
} else {
|
|
25
|
-
return
|
|
25
|
+
return <span className={size === "small" ? "text-sm" : ""}>{value}</span>;
|
|
26
26
|
}
|
|
27
27
|
}
|
package/src/util/entity_cache.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EntityReference, GeoPoint, Vector } from "../types";
|
|
2
|
+
import { isObject, isPlainObject } from "./objects";
|
|
2
3
|
|
|
3
4
|
// Define a unique prefix for entity keys in localStorage to avoid key collisions
|
|
4
5
|
const LOCAL_STORAGE_PREFIX = "entity_cache::";
|
|
@@ -87,6 +88,10 @@ export function saveEntityToCache(path: string, data: object): void {
|
|
|
87
88
|
try {
|
|
88
89
|
const key = LOCAL_STORAGE_PREFIX + path;
|
|
89
90
|
const entityString = JSON.stringify(data, customReplacer);
|
|
91
|
+
console.log("Saving entity to localStorage:", {
|
|
92
|
+
key,
|
|
93
|
+
entityString
|
|
94
|
+
});
|
|
90
95
|
localStorage.setItem(key, entityString);
|
|
91
96
|
} catch (error) {
|
|
92
97
|
console.error(
|
|
@@ -129,6 +134,10 @@ export function getEntityFromCache(path: string): object | undefined {
|
|
|
129
134
|
const entityString = localStorage.getItem(key);
|
|
130
135
|
if (entityString) {
|
|
131
136
|
const entity: object = JSON.parse(entityString, customReviver);
|
|
137
|
+
console.log("Loaded entity from localStorage:", {
|
|
138
|
+
key,
|
|
139
|
+
entity
|
|
140
|
+
});
|
|
132
141
|
return entity;
|
|
133
142
|
}
|
|
134
143
|
} catch (error) {
|
|
@@ -186,3 +195,29 @@ export function clearEntityCache(): void {
|
|
|
186
195
|
}
|
|
187
196
|
}
|
|
188
197
|
}
|
|
198
|
+
|
|
199
|
+
export function flattenKeys(obj: any, prefix = "", result: string[] = []): string[] {
|
|
200
|
+
|
|
201
|
+
if (isObject(obj) || Array.isArray(obj)) {
|
|
202
|
+
const plainObject = isPlainObject(obj);
|
|
203
|
+
if (!plainObject && prefix) {
|
|
204
|
+
result.push(prefix);
|
|
205
|
+
} else {
|
|
206
|
+
for (const key in obj) {
|
|
207
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
208
|
+
const newKey = prefix
|
|
209
|
+
? Array.isArray(obj)
|
|
210
|
+
? `${prefix}[${key}]`
|
|
211
|
+
: `${prefix}.${key}`
|
|
212
|
+
: key;
|
|
213
|
+
if (isObject(obj[key]) || Array.isArray(obj[key])) {
|
|
214
|
+
flattenKeys(obj[key], newKey, result);
|
|
215
|
+
} else {
|
|
216
|
+
result.push(newKey);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
package/src/util/objects.ts
CHANGED
|
@@ -12,6 +12,21 @@ export function isObject(item: any) {
|
|
|
12
12
|
return item && typeof item === "object" && !Array.isArray(item);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
|
|
16
|
+
export function isPlainObject(obj:any) {
|
|
17
|
+
// 1. Rule out non-objects, null, and arrays
|
|
18
|
+
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 2. Get the object's direct prototype
|
|
23
|
+
const proto = Object.getPrototypeOf(obj);
|
|
24
|
+
|
|
25
|
+
// 3. A plain object's direct prototype is Object.prototype
|
|
26
|
+
return proto === Object.prototype;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
15
30
|
export function mergeDeep<T extends Record<any, any>, U extends Record<any, any>>(
|
|
16
31
|
target: T,
|
|
17
32
|
source: U,
|
|
@@ -47,8 +62,31 @@ export function mergeDeep<T extends Record<any, any>, U extends Record<any, any>
|
|
|
47
62
|
// If source value is a Date, create a new Date instance.
|
|
48
63
|
(output as any)[key] = new Date(sourceValue.getTime());
|
|
49
64
|
} else if (Array.isArray(sourceValue)) {
|
|
50
|
-
|
|
51
|
-
|
|
65
|
+
if (Array.isArray(outputValue)) {
|
|
66
|
+
const newArray = [];
|
|
67
|
+
const maxLength = Math.max(outputValue.length, sourceValue.length);
|
|
68
|
+
for (let i = 0; i < maxLength; i++) {
|
|
69
|
+
const sourceItem = sourceValue[i];
|
|
70
|
+
const targetItem = outputValue[i];
|
|
71
|
+
|
|
72
|
+
if (i >= sourceValue.length) { // source is shorter
|
|
73
|
+
newArray[i] = targetItem;
|
|
74
|
+
} else if (i >= outputValue.length) { // target is shorter
|
|
75
|
+
newArray[i] = sourceItem;
|
|
76
|
+
} else if (sourceItem === null) {
|
|
77
|
+
newArray[i] = targetItem;
|
|
78
|
+
} else if (isObject(sourceItem) && isObject(targetItem)) {
|
|
79
|
+
newArray[i] = mergeDeep(targetItem, sourceItem, ignoreUndefined);
|
|
80
|
+
} else {
|
|
81
|
+
newArray[i] = sourceItem;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
(output as any)[key] = newArray;
|
|
85
|
+
} else {
|
|
86
|
+
// If output's value (from target) is not an array,
|
|
87
|
+
// overwrite with a shallow copy of the source array.
|
|
88
|
+
(output as any)[key] = [...sourceValue];
|
|
89
|
+
}
|
|
52
90
|
} else if (isObject(sourceValue)) {
|
|
53
91
|
// If source value is an object:
|
|
54
92
|
if (isObject(outputValue)) {
|