@firecms/core 3.0.0-canary.287 → 3.0.0-canary.288
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 +592 -132
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +599 -139
- 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/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/NumberPropertyPreview.tsx +2 -2
- package/src/util/entity_cache.ts +35 -0
- package/src/util/objects.ts +40 -2
|
@@ -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
|
</>
|
|
@@ -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)) {
|