@firecms/core 3.0.0-canary.286 → 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/core/EntityEditView.d.ts +8 -2
- package/dist/form/EntityForm.d.ts +2 -1
- package/dist/form/components/LocalChangesMenu.d.ts +11 -0
- package/dist/index.es.js +848 -71
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +847 -70
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collections.d.ts +10 -6
- package/dist/util/collections.d.ts +1 -0
- package/dist/util/entity_cache.d.ts +6 -2
- package/dist/util/objects.d.ts +1 -0
- package/package.json +5 -5
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +4 -3
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +1 -1
- package/src/components/EntityView.tsx +29 -40
- package/src/components/PropertyCollectionView.tsx +329 -0
- package/src/core/EntityEditView.tsx +20 -8
- package/src/core/EntitySidePanel.tsx +9 -3
- package/src/form/EntityForm.tsx +130 -23
- package/src/form/components/LocalChangesMenu.tsx +140 -0
- package/src/preview/property_previews/NumberPropertyPreview.tsx +2 -2
- package/src/types/collections.ts +10 -6
- package/src/util/collections.ts +8 -0
- package/src/util/createFormexStub.tsx +4 -0
- package/src/util/entity_cache.ts +53 -22
- package/src/util/objects.ts +40 -2
|
@@ -308,12 +308,16 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
|
|
|
308
308
|
*/
|
|
309
309
|
history?: boolean;
|
|
310
310
|
/**
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
|
|
316
|
-
|
|
311
|
+
* Should local changes be backed up in local storage, to prevent data loss on
|
|
312
|
+
* accidental navigations.
|
|
313
|
+
* - `manual_apply`: When the user navigates back to an entity with local changes,
|
|
314
|
+
* they will be prompted to restore the changes.
|
|
315
|
+
* - `auto_apply`: When the user navigates back to an entity with local changes,
|
|
316
|
+
* the changes will be automatically applied.
|
|
317
|
+
* - `false`: Local changes will not be backed up.
|
|
318
|
+
* Defaults to `manual_apply`.
|
|
319
|
+
*/
|
|
320
|
+
localChangesBackup?: "manual_apply" | "auto_apply" | false;
|
|
317
321
|
}
|
|
318
322
|
/**
|
|
319
323
|
* Parameter passed to the `Actions` prop in the collection configuration.
|
|
@@ -9,3 +9,4 @@ export declare function resolveDefaultSelectedView(defaultSelectedView: string |
|
|
|
9
9
|
* @param permissionsBuilder
|
|
10
10
|
*/
|
|
11
11
|
export declare const applyPermissionsFunctionIfEmpty: (collections: EntityCollection[], permissionsBuilder?: PermissionsBuilder<any, any>) => EntityCollection[];
|
|
12
|
+
export declare function getLocalChangesBackup(collection: EntityCollection): "manual_apply" | "auto_apply";
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* @param data - The data to cache and persist.
|
|
5
5
|
*/
|
|
6
6
|
export declare function saveEntityToCache(path: string, data: object): void;
|
|
7
|
+
export declare function removeEntityFromMemoryCache(path: string): void;
|
|
8
|
+
export declare function saveEntityToMemoryCache(path: string, data: object): void;
|
|
9
|
+
export declare function getEntityFromMemoryCache(path: string): object | undefined;
|
|
10
|
+
export declare function hasEntityInCache(path: string): boolean;
|
|
7
11
|
/**
|
|
8
12
|
* Retrieves an entity from the in-memory cache or `localStorage`.
|
|
9
13
|
* If the entity is not in the cache but exists in `localStorage`, it loads it into the cache.
|
|
@@ -11,8 +15,7 @@ export declare function saveEntityToCache(path: string, data: object): void;
|
|
|
11
15
|
* @param useLocalStorage
|
|
12
16
|
* @returns The cached entity or `undefined` if not found.
|
|
13
17
|
*/
|
|
14
|
-
export declare function getEntityFromCache(path: string
|
|
15
|
-
export declare function hasEntityInCache(path: string): boolean;
|
|
18
|
+
export declare function getEntityFromCache(path: string): object | undefined;
|
|
16
19
|
/**
|
|
17
20
|
* Removes an entity from both the in-memory cache and `localStorage`.
|
|
18
21
|
* @param path - The unique path/key for the entity to remove.
|
|
@@ -22,3 +25,4 @@ export declare function removeEntityFromCache(path: string): void;
|
|
|
22
25
|
* Clears the entire in-memory cache and removes all related entities from `localStorage`.
|
|
23
26
|
*/
|
|
24
27
|
export declare function clearEntityCache(): void;
|
|
28
|
+
export declare function flattenKeys(obj: any, prefix?: string, result?: string[]): string[];
|
package/dist/util/objects.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare const pick: <T>(obj: T, ...args: any[]) => T;
|
|
2
2
|
export declare function isObject(item: any): any;
|
|
3
|
+
export declare function isPlainObject(obj: any): boolean;
|
|
3
4
|
export declare function mergeDeep<T extends Record<any, any>, U extends Record<any, any>>(target: T, source: U, ignoreUndefined?: boolean): T & U;
|
|
4
5
|
export declare function getValueInPath(o: object | undefined, path: string): any;
|
|
5
6
|
export declare function removeInPath(o: object, path: string): object | undefined;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firecms/core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.0.0-canary.
|
|
4
|
+
"version": "3.0.0-canary.288",
|
|
5
5
|
"description": "Awesome Firebase/Firestore-based headless open-source CMS",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/firecmsco"
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"@dnd-kit/core": "^6.3.1",
|
|
54
54
|
"@dnd-kit/modifiers": "^9.0.0",
|
|
55
55
|
"@dnd-kit/sortable": "^10.0.0",
|
|
56
|
-
"@firecms/editor": "^3.0.0-canary.
|
|
57
|
-
"@firecms/formex": "^3.0.0-canary.
|
|
58
|
-
"@firecms/ui": "^3.0.0-canary.
|
|
56
|
+
"@firecms/editor": "^3.0.0-canary.288",
|
|
57
|
+
"@firecms/formex": "^3.0.0-canary.288",
|
|
58
|
+
"@firecms/ui": "^3.0.0-canary.288",
|
|
59
59
|
"@radix-ui/react-portal": "^1.1.9",
|
|
60
60
|
"clsx": "^2.1.1",
|
|
61
61
|
"date-fns": "^3.6.0",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"dist",
|
|
109
109
|
"src"
|
|
110
110
|
],
|
|
111
|
-
"gitHead": "
|
|
111
|
+
"gitHead": "e609cafa0c505803a4628324a77404ebdfbb62c4",
|
|
112
112
|
"publishConfig": {
|
|
113
113
|
"access": "public"
|
|
114
114
|
},
|
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
Tooltip
|
|
15
15
|
} from "@firecms/ui";
|
|
16
16
|
import { useFireCMSContext, useLargeLayout } from "../../hooks";
|
|
17
|
-
import { hasEntityInCache } from "../../util/entity_cache";
|
|
17
|
+
import { getEntityFromCache, hasEntityInCache } from "../../util/entity_cache";
|
|
18
|
+
import { getLocalChangesBackup } from "../../util";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
*
|
|
@@ -79,8 +80,8 @@ export const EntityCollectionRowActions = function EntityCollectionRowActions({
|
|
|
79
80
|
|
|
80
81
|
const collapsedActions = actions.filter(a => a.collapsed || a.collapsed === undefined);
|
|
81
82
|
const uncollapsedActions = actions.filter(a => a.collapsed === false);
|
|
82
|
-
const enableLocalChangesBackup = collection
|
|
83
|
-
const hasDraft = enableLocalChangesBackup ?
|
|
83
|
+
const enableLocalChangesBackup = collection ? getLocalChangesBackup(collection) : false;
|
|
84
|
+
const hasDraft = enableLocalChangesBackup ? getEntityFromCache(fullPath + "/" + entity.id) : false;
|
|
84
85
|
|
|
85
86
|
return (
|
|
86
87
|
<div
|
|
@@ -694,7 +694,7 @@ export const EntityCollectionView = React.memo(
|
|
|
694
694
|
className="mt-4"
|
|
695
695
|
>
|
|
696
696
|
<AddIcon/>
|
|
697
|
-
Create your first
|
|
697
|
+
Create your first entry
|
|
698
698
|
</Button>
|
|
699
699
|
</div>
|
|
700
700
|
: <Typography variant={"label"}>No results with the applied filter/sort</Typography>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React, { useMemo } from "react";
|
|
2
|
-
import { PropertyPreview } from "../preview";
|
|
3
2
|
import { Entity, EntityCollection, ResolvedEntityCollection, ResolvedProperties } from "../types";
|
|
4
3
|
import { resolveCollection } from "../util";
|
|
5
|
-
import { cls, defaultBorderMixin, IconButton, OpenInNewIcon } from "@firecms/ui";
|
|
4
|
+
import { cls, defaultBorderMixin, IconButton, OpenInNewIcon, Typography } from "@firecms/ui";
|
|
6
5
|
import { CustomizationController } from "../types/customization_controller";
|
|
7
6
|
import { useCustomizationController } from "../hooks/useCustomizationController";
|
|
8
7
|
import { useAuthController } from "../hooks";
|
|
8
|
+
import { PropertyCollectionView } from "./PropertyCollectionView";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* @group Components
|
|
@@ -40,47 +40,36 @@ export function EntityView<M extends Record<string, any>>(
|
|
|
40
40
|
|
|
41
41
|
return (
|
|
42
42
|
<div className={"w-full " + className}>
|
|
43
|
-
<div className={"w-full mb-4"}>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
<div className={"w-full mb-4 p-4"}>
|
|
44
|
+
|
|
45
|
+
<div className={`grid grid-cols-12 gap-x-4 py-4 items-start border-b ${defaultBorderMixin}`}>
|
|
46
|
+
<div className="col-span-4 pr-2">
|
|
47
|
+
<Typography variant="caption"
|
|
48
|
+
color={"secondary"}
|
|
49
|
+
component={"span"}
|
|
50
|
+
className="break-words">
|
|
51
|
+
Id
|
|
52
|
+
</Typography>
|
|
47
53
|
</div>
|
|
48
|
-
<div
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
<div className="col-span-8">
|
|
55
|
+
<div
|
|
56
|
+
className="flex-grow text-surface-900 dark:text-white flex items-center">
|
|
57
|
+
<span className="flex-grow mr-2">{entity.id}</span>
|
|
58
|
+
{customizationController?.entityLinkBuilder &&
|
|
59
|
+
<a href={customizationController.entityLinkBuilder({ entity })}
|
|
60
|
+
rel="noopener noreferrer"
|
|
61
|
+
target="_blank">
|
|
62
|
+
<IconButton>
|
|
63
|
+
<OpenInNewIcon
|
|
64
|
+
size={"small"}/>
|
|
65
|
+
</IconButton>
|
|
66
|
+
</a>}
|
|
67
|
+
</div>
|
|
60
68
|
</div>
|
|
61
69
|
</div>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return (
|
|
66
|
-
<div
|
|
67
|
-
key={`reference_previews_${key}`}
|
|
68
|
-
className={cls(defaultBorderMixin, "flex justify-between py-2 border-b last:border-b-0")}>
|
|
69
|
-
<div className="flex items-center w-1/4">
|
|
70
|
-
<span className="pl-2 text-sm text-surface-600">{property.name}</span>
|
|
71
|
-
</div>
|
|
72
|
-
<div
|
|
73
|
-
className="flex-grow p-2 ml-2 w-3/4 text-surface-900 dark:text-white min-h-[56px] flex items-center">
|
|
74
|
-
<PropertyPreview
|
|
75
|
-
propertyKey={key}
|
|
76
|
-
value={value}
|
|
77
|
-
// entity={entity}
|
|
78
|
-
property={property}
|
|
79
|
-
size={"medium"}/>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
)
|
|
83
|
-
})}
|
|
70
|
+
|
|
71
|
+
<PropertyCollectionView data={entity.values} properties={properties} size={"medium"}/>
|
|
72
|
+
|
|
84
73
|
</div>
|
|
85
74
|
</div>
|
|
86
75
|
);
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { defaultBorderMixin, Typography } from "@firecms/ui";
|
|
3
|
+
import { PreviewSize, PropertyPreview } from "../preview";
|
|
4
|
+
import { ResolvedProperties, ResolvedProperty } from "../types";
|
|
5
|
+
import { getValueInPath } from "../util";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build a readable label for a path and resolve the property
|
|
9
|
+
* Supports map and array (including arrays of maps)
|
|
10
|
+
*/
|
|
11
|
+
export function buildPropertyLabelAndGetProperty(
|
|
12
|
+
properties: ResolvedProperties,
|
|
13
|
+
key: string
|
|
14
|
+
): { label: string; property: ResolvedProperty | undefined } {
|
|
15
|
+
if (!key) return {
|
|
16
|
+
label: "",
|
|
17
|
+
property: undefined
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Parse "a[0].b.c[2]" -> ["a", 0, "b", "c", 2]
|
|
21
|
+
const segments: (string | number)[] = [];
|
|
22
|
+
const re = /([^[.\]]+)|\[(\d+)\]/g;
|
|
23
|
+
let m: RegExpExecArray | null;
|
|
24
|
+
while ((m = re.exec(key)) !== null) {
|
|
25
|
+
if (m[1] !== undefined) segments.push(m[1]);
|
|
26
|
+
else if (m[2] !== undefined) segments.push(Number(m[2]));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let currentProps: ResolvedProperties | undefined = properties;
|
|
30
|
+
let currentProp: ResolvedProperty | undefined;
|
|
31
|
+
let lastLabel = "";
|
|
32
|
+
|
|
33
|
+
const getArrayOfProp = (p?: ResolvedProperty): ResolvedProperty | undefined => {
|
|
34
|
+
if (!p || p.dataType !== "array") return undefined;
|
|
35
|
+
return Array.isArray(p.of) ? (p.of[0] as ResolvedProperty) : (p.of as ResolvedProperty | undefined);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
for (const seg of segments) {
|
|
39
|
+
if (typeof seg === "number") {
|
|
40
|
+
// Last segment label should be the index itself
|
|
41
|
+
lastLabel = `[${seg}]`;
|
|
42
|
+
|
|
43
|
+
// Move schema context into the array element
|
|
44
|
+
if (currentProp?.dataType === "array") {
|
|
45
|
+
currentProp = getArrayOfProp(currentProp);
|
|
46
|
+
if (currentProp?.dataType === "map" && currentProp.properties) {
|
|
47
|
+
currentProps = currentProp.properties as ResolvedProperties;
|
|
48
|
+
} else {
|
|
49
|
+
currentProps = undefined;
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// Index without array schema context
|
|
53
|
+
currentProp = undefined;
|
|
54
|
+
currentProps = undefined;
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// seg is a string key
|
|
60
|
+
if (currentProps && (currentProps as any)[seg]) {
|
|
61
|
+
const nextProp = (currentProps as any)[seg] as ResolvedProperty;
|
|
62
|
+
currentProp = nextProp;
|
|
63
|
+
// Last segment label should be the property name (or the raw key)
|
|
64
|
+
lastLabel = nextProp.name || String(seg);
|
|
65
|
+
|
|
66
|
+
if (nextProp.dataType === "map" && nextProp.properties) {
|
|
67
|
+
currentProps = nextProp.properties as ResolvedProperties;
|
|
68
|
+
} else if (nextProp.dataType === "array") {
|
|
69
|
+
// Keep array prop; the next segment (index) will step into its element schema
|
|
70
|
+
currentProps = undefined;
|
|
71
|
+
} else {
|
|
72
|
+
currentProps = undefined;
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
// Unknown key or no schema context
|
|
76
|
+
currentProp = undefined;
|
|
77
|
+
currentProps = undefined;
|
|
78
|
+
lastLabel = String(seg);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
label: lastLabel,
|
|
84
|
+
property: currentProp
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const pathEndsWithIndex = (p: string) => /\[\d+\]$/.test(p);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Improved simple layout for nested changes:
|
|
92
|
+
* - Map or Array-of-Map -> section header + indented rows
|
|
93
|
+
* - Leaf or Array-of-Primitives -> single row with label and value
|
|
94
|
+
*/
|
|
95
|
+
export const PropertyCollectionView = ({
|
|
96
|
+
data,
|
|
97
|
+
properties,
|
|
98
|
+
baseKey = "",
|
|
99
|
+
suppressHeader = false,
|
|
100
|
+
size = "small"
|
|
101
|
+
}: {
|
|
102
|
+
data: any;
|
|
103
|
+
properties: ResolvedProperties;
|
|
104
|
+
baseKey?: string;
|
|
105
|
+
suppressHeader?: boolean;
|
|
106
|
+
size?: PreviewSize;
|
|
107
|
+
}) => {
|
|
108
|
+
|
|
109
|
+
const isTopLevel = !!baseKey && !baseKey.includes(".") && !baseKey.includes("[");
|
|
110
|
+
|
|
111
|
+
// Arrays
|
|
112
|
+
if (Array.isArray(data)) {
|
|
113
|
+
const {
|
|
114
|
+
label: arrayLabel,
|
|
115
|
+
property
|
|
116
|
+
} = baseKey
|
|
117
|
+
? buildPropertyLabelAndGetProperty(properties, baseKey)
|
|
118
|
+
: {
|
|
119
|
+
label: "",
|
|
120
|
+
property: undefined as ResolvedProperty | undefined
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const ofProp = property?.dataType === "array"
|
|
124
|
+
? (Array.isArray(property.of) ? property.of[0] : property.of) as ResolvedProperty | undefined
|
|
125
|
+
: undefined;
|
|
126
|
+
|
|
127
|
+
const isArrayOfMaps = ofProp?.dataType === "map";
|
|
128
|
+
const isArrayOfPrimitives = property?.dataType === "array" && ofProp && ofProp.dataType !== "map";
|
|
129
|
+
|
|
130
|
+
// Array of primitives -> single row
|
|
131
|
+
if (baseKey && property && isArrayOfPrimitives) {
|
|
132
|
+
return (
|
|
133
|
+
<div
|
|
134
|
+
className={`grid grid-cols-12 gap-x-4 ${isTopLevel ? "py-4" : "py-2"} items-start ${isTopLevel ? `border-b ${defaultBorderMixin}` : ""}`}>
|
|
135
|
+
<div className="col-span-4 pr-2">
|
|
136
|
+
<Typography variant="caption"
|
|
137
|
+
color={"secondary"}
|
|
138
|
+
component={"span"}
|
|
139
|
+
className="break-words">
|
|
140
|
+
{arrayLabel}
|
|
141
|
+
</Typography>
|
|
142
|
+
</div>
|
|
143
|
+
<div className="col-span-8">
|
|
144
|
+
<PropertyPreview propertyKey={baseKey}
|
|
145
|
+
value={data}
|
|
146
|
+
property={property}
|
|
147
|
+
size={size}/>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Array of maps or unknown -> array header + combined item header (MapName [index]) then content
|
|
154
|
+
return (
|
|
155
|
+
<div className={`${isTopLevel ? "py-4" : "py-1"} ${isTopLevel ? `border-b ${defaultBorderMixin}` : ""}`}>
|
|
156
|
+
{baseKey && arrayLabel && !suppressHeader && (
|
|
157
|
+
<Typography variant="caption"
|
|
158
|
+
color={"secondary"}
|
|
159
|
+
component={"span"}>
|
|
160
|
+
{arrayLabel}
|
|
161
|
+
</Typography>
|
|
162
|
+
)}
|
|
163
|
+
<div className={baseKey ? `pl-4 mt-1 border-l ${defaultBorderMixin}` : ""}>
|
|
164
|
+
{data.map((item, index) => {
|
|
165
|
+
if (item === null || item === undefined) return null;
|
|
166
|
+
const currentKey = baseKey ? `${baseKey}[${index}]` : `[${index}]`;
|
|
167
|
+
|
|
168
|
+
// Combined header text
|
|
169
|
+
const itemHeader = isArrayOfMaps && ofProp?.name
|
|
170
|
+
? `${ofProp.name} [${index}]`
|
|
171
|
+
: `[${index}]`;
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<div key={currentKey} className="py-1">
|
|
175
|
+
<Typography variant="caption"
|
|
176
|
+
color={"secondary"}
|
|
177
|
+
component={"span"}>
|
|
178
|
+
{itemHeader}
|
|
179
|
+
</Typography>
|
|
180
|
+
<div className={`pl-4 mt-1 border-l ${defaultBorderMixin}`}>
|
|
181
|
+
<PropertyCollectionView
|
|
182
|
+
data={item}
|
|
183
|
+
properties={properties}
|
|
184
|
+
baseKey={currentKey}
|
|
185
|
+
suppressHeader={true} // don’t repeat the inner map header
|
|
186
|
+
size={size}
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
})}
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Objects (maps or plain objects)
|
|
198
|
+
if (typeof data === "object" && data !== null) {
|
|
199
|
+
const {
|
|
200
|
+
label,
|
|
201
|
+
property
|
|
202
|
+
} = baseKey
|
|
203
|
+
? buildPropertyLabelAndGetProperty(properties, baseKey)
|
|
204
|
+
: {
|
|
205
|
+
label: "",
|
|
206
|
+
property: undefined as ResolvedProperty | undefined
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Non-map leaf-like object -> single row
|
|
210
|
+
if (baseKey && (!property || property.dataType !== "map" || !property.properties)) {
|
|
211
|
+
if (!property) return null;
|
|
212
|
+
return (
|
|
213
|
+
<div
|
|
214
|
+
className={`grid grid-cols-12 gap-x-4 ${isTopLevel ? "py-4" : "py-2"} items-start ${isTopLevel ? `border-b ${defaultBorderMixin}` : ""}`}>
|
|
215
|
+
<div className="col-span-4 pr-2">
|
|
216
|
+
<Typography variant="caption"
|
|
217
|
+
color={"secondary"}
|
|
218
|
+
component={"span"}
|
|
219
|
+
className="break-words">
|
|
220
|
+
{label}
|
|
221
|
+
</Typography>
|
|
222
|
+
</div>
|
|
223
|
+
<div className="col-span-8">
|
|
224
|
+
<PropertyPreview propertyKey={baseKey}
|
|
225
|
+
value={data}
|
|
226
|
+
property={property}
|
|
227
|
+
size={size}/>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Map with defined properties -> show map header only if not suppressed
|
|
234
|
+
const showMapHeader =
|
|
235
|
+
baseKey &&
|
|
236
|
+
!suppressHeader &&
|
|
237
|
+
property?.dataType === "map" &&
|
|
238
|
+
(property.name || !pathEndsWithIndex(baseKey));
|
|
239
|
+
|
|
240
|
+
const headerText = property?.name || label;
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<div className={`${isTopLevel ? "py-4" : "py-1"} ${isTopLevel ? `border-b ${defaultBorderMixin}` : ""}`}>
|
|
244
|
+
{showMapHeader && (
|
|
245
|
+
<Typography variant="caption"
|
|
246
|
+
color={"secondary"}
|
|
247
|
+
component={"span"}
|
|
248
|
+
>
|
|
249
|
+
{headerText}
|
|
250
|
+
</Typography>
|
|
251
|
+
)}
|
|
252
|
+
<div className={baseKey ? `pl-4 mt-1 border-l ${defaultBorderMixin}` : ""}>
|
|
253
|
+
{Object.entries(data).map(([key, value]) => {
|
|
254
|
+
if (value === null || value === undefined) return null;
|
|
255
|
+
const currentKey = baseKey ? `${baseKey}.${key}` : key;
|
|
256
|
+
return (
|
|
257
|
+
<PropertyCollectionView
|
|
258
|
+
key={currentKey}
|
|
259
|
+
data={value}
|
|
260
|
+
properties={properties}
|
|
261
|
+
baseKey={currentKey}
|
|
262
|
+
size={size}
|
|
263
|
+
/>
|
|
264
|
+
);
|
|
265
|
+
})}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Primitives
|
|
272
|
+
if (baseKey) {
|
|
273
|
+
const {
|
|
274
|
+
label,
|
|
275
|
+
property
|
|
276
|
+
} = buildPropertyLabelAndGetProperty(properties, baseKey);
|
|
277
|
+
if (!property) return null;
|
|
278
|
+
return (
|
|
279
|
+
<div
|
|
280
|
+
className={`grid grid-cols-12 gap-x-4 ${isTopLevel ? "py-4" : "py-2"} items-start ${isTopLevel ? `border-b ${defaultBorderMixin}` : ""}`}>
|
|
281
|
+
<div className="col-span-4 pr-2">
|
|
282
|
+
<Typography variant="caption"
|
|
283
|
+
color={"secondary"}
|
|
284
|
+
component={"span"}
|
|
285
|
+
className="break-words">
|
|
286
|
+
{label}
|
|
287
|
+
</Typography>
|
|
288
|
+
</div>
|
|
289
|
+
<div className="col-span-8">
|
|
290
|
+
<PropertyPreview propertyKey={baseKey}
|
|
291
|
+
value={data}
|
|
292
|
+
property={property}
|
|
293
|
+
size={size}/>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return null;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
export function buildDataFromPaths(values: object, paths: string[]): object {
|
|
303
|
+
const result = {};
|
|
304
|
+
paths.forEach(path => {
|
|
305
|
+
const value = getValueInPath(values, path);
|
|
306
|
+
if (value === undefined) return;
|
|
307
|
+
|
|
308
|
+
// lodash.set would be perfect here
|
|
309
|
+
const segments = path.replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
310
|
+
let current: any = result;
|
|
311
|
+
segments.forEach((segment, index) => {
|
|
312
|
+
if (index === segments.length - 1) {
|
|
313
|
+
current[segment] = value;
|
|
314
|
+
} else {
|
|
315
|
+
const nextSegment = segments[index + 1];
|
|
316
|
+
const isNextAnIndex = /^\d+$/.test(nextSegment);
|
|
317
|
+
if (!current[segment]) {
|
|
318
|
+
if (isNextAnIndex) {
|
|
319
|
+
current[segment] = [];
|
|
320
|
+
} else {
|
|
321
|
+
current[segment] = {};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
current = current[segment];
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
Entity,
|
|
4
4
|
EntityCollection,
|
|
5
5
|
EntityStatus,
|
|
6
|
+
EntityValues,
|
|
6
7
|
FireCMSPlugin,
|
|
7
8
|
FormContext,
|
|
8
9
|
PluginFormActionProps,
|
|
@@ -26,7 +27,7 @@ import {
|
|
|
26
27
|
useLargeLayout
|
|
27
28
|
} from "../hooks";
|
|
28
29
|
import { CircularProgress, cls, CodeIcon, defaultBorderMixin, Tab, Tabs, Typography } from "@firecms/ui";
|
|
29
|
-
import {
|
|
30
|
+
import { getEntityFromMemoryCache } from "../util/entity_cache";
|
|
30
31
|
import { EntityForm, EntityFormProps } from "../form";
|
|
31
32
|
import { EntityEditViewFormActions } from "./EntityEditViewFormActions";
|
|
32
33
|
import { EntityJsonPreview } from "../components/EntityJsonPreview";
|
|
@@ -44,6 +45,13 @@ export type OnUpdateParams = {
|
|
|
44
45
|
collection: EntityCollection<any>
|
|
45
46
|
};
|
|
46
47
|
|
|
48
|
+
export type BarActionsParams = {
|
|
49
|
+
values: object,
|
|
50
|
+
status: EntityStatus,
|
|
51
|
+
path: string,
|
|
52
|
+
entityId?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
47
55
|
export type OnTabChangeParams<M extends Record<string, any>> = {
|
|
48
56
|
path: string;
|
|
49
57
|
entityId?: string;
|
|
@@ -67,11 +75,11 @@ export interface EntityEditViewProps<M extends Record<string, any>> {
|
|
|
67
75
|
copy?: boolean;
|
|
68
76
|
selectedTab?: string;
|
|
69
77
|
parentCollectionIds: string[];
|
|
70
|
-
onValuesModified?: (modified: boolean) => void;
|
|
78
|
+
onValuesModified?: (modified: boolean, values:M) => void;
|
|
71
79
|
onSaved?: (params: OnUpdateParams) => void;
|
|
72
80
|
onTabChange?: (props: OnTabChangeParams<M>) => void;
|
|
73
81
|
layout?: "side_panel" | "full_screen";
|
|
74
|
-
barActions?: React.ReactNode;
|
|
82
|
+
barActions?: (params: BarActionsParams) => React.ReactNode;
|
|
75
83
|
formProps?: Partial<EntityFormProps<M>>,
|
|
76
84
|
}
|
|
77
85
|
|
|
@@ -97,11 +105,9 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
|
|
|
97
105
|
useCache: false
|
|
98
106
|
});
|
|
99
107
|
|
|
100
|
-
const enableLocalChangesBackup = props.collection.enableLocalChangesBackup !== undefined ? props.collection.enableLocalChangesBackup : true;
|
|
101
|
-
|
|
102
108
|
const initialDirtyValues = entityId
|
|
103
|
-
?
|
|
104
|
-
:
|
|
109
|
+
? getEntityFromMemoryCache(props.path + "/" + entityId)
|
|
110
|
+
: getEntityFromMemoryCache(props.path + "#new");
|
|
105
111
|
|
|
106
112
|
const authController = useAuthController();
|
|
107
113
|
|
|
@@ -390,6 +396,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
390
396
|
disabled={!canEdit}
|
|
391
397
|
{...formProps}
|
|
392
398
|
onEntityChange={(entity) => {
|
|
399
|
+
console.log("333 EntityEditView onEntityChange:", entity);
|
|
393
400
|
setUsedEntity(entity);
|
|
394
401
|
formProps?.onEntityChange?.(entity);
|
|
395
402
|
}}
|
|
@@ -447,7 +454,12 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
447
454
|
{shouldShowTopBar && <div
|
|
448
455
|
className={cls("h-14 items-center flex overflow-visible overflow-x-scroll w-full no-scrollbar h-14 border-b pl-2 pr-2 pt-1 flex bg-surface-50 dark:bg-surface-900", defaultBorderMixin)}>
|
|
449
456
|
|
|
450
|
-
{barActions
|
|
457
|
+
{barActions?.({
|
|
458
|
+
path: fullIdPath ?? path,
|
|
459
|
+
entityId,
|
|
460
|
+
values: formContext?.values ?? usedEntity?.values ?? {},
|
|
461
|
+
status
|
|
462
|
+
})}
|
|
451
463
|
|
|
452
464
|
<div className={"flex-grow"}/>
|
|
453
465
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useMemo } from "react";
|
|
2
2
|
|
|
3
|
-
import { EntitySidePanelProps } from "../types";
|
|
3
|
+
import { EntityCollection, EntitySidePanelProps } from "../types";
|
|
4
4
|
import { useNavigationController, useSideEntityController } from "../hooks";
|
|
5
5
|
|
|
6
6
|
import { ErrorBoundary } from "../components";
|
|
@@ -8,6 +8,7 @@ import { EntityEditView, OnUpdateParams } from "./EntityEditView";
|
|
|
8
8
|
import { useSideDialogContext } from "./SideDialogs";
|
|
9
9
|
import { CloseIcon, IconButton, OpenInFullIcon } from "@firecms/ui";
|
|
10
10
|
import { useLocation, useNavigate } from "react-router-dom";
|
|
11
|
+
import { saveEntityToMemoryCache } from "../util/entity_cache";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* This is the component in charge of rendering the side dialog used
|
|
@@ -114,11 +115,14 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
114
115
|
{...props}
|
|
115
116
|
fullIdPath={fullIdPath}
|
|
116
117
|
layout={"side_panel"}
|
|
117
|
-
collection={collection}
|
|
118
|
+
collection={collection as EntityCollection}
|
|
118
119
|
parentCollectionIds={parentCollectionIds}
|
|
119
120
|
onValuesModified={onValuesModified}
|
|
120
121
|
onSaved={onUpdate}
|
|
121
|
-
barActions={
|
|
122
|
+
barActions={({
|
|
123
|
+
status,
|
|
124
|
+
values
|
|
125
|
+
}) => <>
|
|
122
126
|
<IconButton
|
|
123
127
|
className="self-center"
|
|
124
128
|
onClick={onClose}>
|
|
@@ -127,6 +131,8 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
127
131
|
{allowFullScreen && <IconButton
|
|
128
132
|
className="self-center"
|
|
129
133
|
onClick={() => {
|
|
134
|
+
const key = (status === "new" || status === "copy") ? path + "#new" : path + "/" + entityId;
|
|
135
|
+
saveEntityToMemoryCache(key, values);
|
|
130
136
|
if (entityId)
|
|
131
137
|
navigate(location.pathname);
|
|
132
138
|
else
|