@firecms/core 3.0.0-rc.2 → 3.0.0-rc.3
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/HomePage/HomePageDnD.d.ts +2 -1
- package/dist/components/PropertyCollectionView.d.ts +23 -0
- package/dist/core/EntityEditView.d.ts +10 -4
- package/dist/form/EntityForm.d.ts +5 -2
- package/dist/form/components/LocalChangesMenu.d.ts +11 -0
- package/dist/form/index.d.ts +2 -1
- package/dist/index.es.js +1288 -364
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1287 -363
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collections.d.ts +11 -0
- package/dist/types/properties.d.ts +32 -6
- package/dist/util/collections.d.ts +1 -0
- package/dist/util/entity_cache.d.ts +6 -1
- package/dist/util/make_properties_editable.d.ts +1 -2
- package/dist/util/objects.d.ts +1 -0
- package/dist/util/useStorageUploadController.d.ts +1 -0
- package/package.json +6 -6
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +47 -47
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +6 -1
- package/src/components/EntityView.tsx +29 -40
- package/src/components/HomePage/DefaultHomePage.tsx +13 -9
- package/src/components/HomePage/HomePageDnD.tsx +140 -38
- package/src/components/PropertyCollectionView.tsx +329 -0
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +2 -1
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +0 -1
- package/src/core/EntityEditView.tsx +27 -14
- package/src/core/EntityEditViewFormActions.tsx +33 -18
- package/src/core/EntitySidePanel.tsx +9 -3
- package/src/form/EntityForm.tsx +173 -42
- package/src/form/EntityFormActions.tsx +30 -15
- package/src/form/components/ErrorFocus.tsx +22 -29
- package/src/form/components/LocalChangesMenu.tsx +144 -0
- package/src/form/index.tsx +5 -1
- package/src/hooks/useBuildNavigationController.tsx +104 -31
- package/src/preview/property_previews/MapPropertyPreview.tsx +2 -2
- package/src/preview/property_previews/NumberPropertyPreview.tsx +2 -2
- package/src/types/collections.ts +12 -0
- package/src/types/properties.ts +35 -6
- package/src/util/collections.ts +8 -0
- package/src/util/createFormexStub.tsx +4 -0
- package/src/util/entity_cache.ts +71 -52
- package/src/util/join_collections.ts +3 -3
- package/src/util/make_properties_editable.ts +0 -22
- package/src/util/objects.ts +40 -2
- package/src/util/useStorageUploadController.tsx +71 -34
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
CancelIcon,
|
|
5
|
+
CheckIcon,
|
|
6
|
+
defaultBorderMixin,
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogActions,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
KeyboardArrowDownIcon,
|
|
12
|
+
Menu,
|
|
13
|
+
MenuItem,
|
|
14
|
+
Typography,
|
|
15
|
+
VisibilityIcon,
|
|
16
|
+
WarningIcon
|
|
17
|
+
} from "@firecms/ui";
|
|
18
|
+
import { FormexController } from "@firecms/formex";
|
|
19
|
+
import { useSnackbarController } from "../../hooks";
|
|
20
|
+
import { mergeDeep } from "../../util";
|
|
21
|
+
import { flattenKeys, removeEntityFromCache } from "../../util/entity_cache";
|
|
22
|
+
import { ResolvedProperties } from "../../types";
|
|
23
|
+
import { PropertyCollectionView } from "../../components/PropertyCollectionView";
|
|
24
|
+
|
|
25
|
+
interface LocalChangesMenuProps<M extends object> {
|
|
26
|
+
cacheKey: string;
|
|
27
|
+
localChangesData: Partial<M>;
|
|
28
|
+
formex: FormexController<M>;
|
|
29
|
+
onClearLocalChanges?: () => void;
|
|
30
|
+
properties: ResolvedProperties<M>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function LocalChangesMenu<M extends object>({
|
|
34
|
+
localChangesData,
|
|
35
|
+
formex,
|
|
36
|
+
onClearLocalChanges,
|
|
37
|
+
cacheKey,
|
|
38
|
+
properties
|
|
39
|
+
}: LocalChangesMenuProps<M>) {
|
|
40
|
+
|
|
41
|
+
const snackbarController = useSnackbarController();
|
|
42
|
+
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
|
|
43
|
+
const [open, setOpen] = useState(false);
|
|
44
|
+
|
|
45
|
+
const handleOpenMenu = () => setOpen(true);
|
|
46
|
+
const handleCloseMenu = () => setOpen(false);
|
|
47
|
+
|
|
48
|
+
const handlePreview = () => {
|
|
49
|
+
setPreviewDialogOpen(true);
|
|
50
|
+
handleCloseMenu();
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleApply = () => {
|
|
54
|
+
const mergedValues = mergeDeep(formex.values, localChangesData);
|
|
55
|
+
const touched = { ...formex.touched };
|
|
56
|
+
const previewKeys = flattenKeys(localChangesData);
|
|
57
|
+
previewKeys.forEach((key) => {
|
|
58
|
+
touched[key] = true;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
formex.setTouched(touched);
|
|
62
|
+
formex.setValues(mergedValues);
|
|
63
|
+
snackbarController.open({
|
|
64
|
+
type: "info",
|
|
65
|
+
message: "Local changes applied to the form"
|
|
66
|
+
});
|
|
67
|
+
handleCloseMenu();
|
|
68
|
+
onClearLocalChanges?.();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handleDiscard = () => {
|
|
72
|
+
removeEntityFromCache(cacheKey);
|
|
73
|
+
snackbarController.open({
|
|
74
|
+
type: "info",
|
|
75
|
+
message: "Local changes discarded"
|
|
76
|
+
});
|
|
77
|
+
handleCloseMenu();
|
|
78
|
+
onClearLocalChanges?.();
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<>
|
|
83
|
+
<Menu
|
|
84
|
+
trigger={
|
|
85
|
+
<Button
|
|
86
|
+
size={"small"}
|
|
87
|
+
className={
|
|
88
|
+
"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"
|
|
89
|
+
}
|
|
90
|
+
onClick={handleOpenMenu}
|
|
91
|
+
>
|
|
92
|
+
<WarningIcon size={"smallest"} className={"mr-1 text-yellow-600 dark:text-yellow-400"}/>
|
|
93
|
+
Unsaved Local changes
|
|
94
|
+
<KeyboardArrowDownIcon size={"smallest"}/>
|
|
95
|
+
</Button>
|
|
96
|
+
}
|
|
97
|
+
open={open}
|
|
98
|
+
onOpenChange={setOpen}
|
|
99
|
+
>
|
|
100
|
+
<div className={"max-w-xs px-4 py-4 text-sm text-gray-700 dark:text-gray-300"}>
|
|
101
|
+
This document was edited locally and has unsaved changes. These local changes will be lost if you
|
|
102
|
+
don't apply them.
|
|
103
|
+
</div>
|
|
104
|
+
<MenuItem dense onClick={handlePreview}><VisibilityIcon size={"small"}/>Preview Changes</MenuItem>
|
|
105
|
+
<MenuItem dense onClick={handleApply}><CheckIcon size={"small"}/>Apply Changes</MenuItem>
|
|
106
|
+
<MenuItem dense onClick={handleDiscard}><CancelIcon size={"small"}/>Discard Local Changes</MenuItem>
|
|
107
|
+
</Menu>
|
|
108
|
+
|
|
109
|
+
<Dialog
|
|
110
|
+
open={previewDialogOpen}
|
|
111
|
+
onOpenChange={setPreviewDialogOpen}
|
|
112
|
+
maxWidth={"4xl"}
|
|
113
|
+
>
|
|
114
|
+
<DialogTitle variant={"h6"}>Preview Local Changes</DialogTitle>
|
|
115
|
+
<DialogContent className={"my-4"}>
|
|
116
|
+
<Typography variant={"body2"} className={"mb-4"}>
|
|
117
|
+
These are the local changes that will be applied to the form.
|
|
118
|
+
</Typography>
|
|
119
|
+
<div className={`border rounded-lg ${defaultBorderMixin}`} style={{
|
|
120
|
+
maxHeight: 520,
|
|
121
|
+
overflow: "auto"
|
|
122
|
+
}}>
|
|
123
|
+
<div className="p-4">
|
|
124
|
+
<PropertyCollectionView data={localChangesData}
|
|
125
|
+
properties={properties as ResolvedProperties}/>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</DialogContent>
|
|
129
|
+
<DialogActions>
|
|
130
|
+
<Button onClick={() => setPreviewDialogOpen(false)}>Close</Button>
|
|
131
|
+
<Button
|
|
132
|
+
variant={"filled"}
|
|
133
|
+
onClick={() => {
|
|
134
|
+
handleApply();
|
|
135
|
+
setPreviewDialogOpen(false);
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
Apply changes
|
|
139
|
+
</Button>
|
|
140
|
+
</DialogActions>
|
|
141
|
+
</Dialog>
|
|
142
|
+
</>
|
|
143
|
+
);
|
|
144
|
+
}
|
package/src/form/index.tsx
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
export
|
|
1
|
+
export {
|
|
2
|
+
EntityForm,
|
|
3
|
+
yupToFormErrors,
|
|
4
|
+
} from "./EntityForm";
|
|
5
|
+
export type { EntityFormProps } from "./EntityForm";
|
|
2
6
|
|
|
3
7
|
export { SelectFieldBinding } from "./field_bindings/SelectFieldBinding";
|
|
4
8
|
export { MultiSelectFieldBinding } from "./field_bindings/MultiSelectFieldBinding";
|
|
@@ -149,26 +149,10 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
149
149
|
const allPluginGroups = plugins?.flatMap(plugin => plugin.homePage?.navigationEntries ? plugin.homePage.navigationEntries.map(e => e.name) : []) ?? [];
|
|
150
150
|
const pluginGroups = [...new Set(allPluginGroups)];
|
|
151
151
|
|
|
152
|
-
const
|
|
153
|
-
if (!plugins) {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
// remove all groups that have no entries
|
|
157
|
-
const filteredEntries = entries.filter(entry => entry.entries.length > 0);
|
|
158
|
-
if (plugins.some(plugin => plugin.homePage?.onNavigationEntriesUpdate)) {
|
|
159
|
-
plugins.forEach(plugin => {
|
|
160
|
-
if (plugin.homePage?.onNavigationEntriesUpdate) {
|
|
161
|
-
plugin.homePage.onNavigationEntriesUpdate(filteredEntries);
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
}, [plugins]);
|
|
167
|
-
|
|
168
|
-
const computeTopNavigation = useCallback((collections: EntityCollection[], views: CMSView[], adminViews: CMSView[], viewsOrder?: string[]): NavigationResult => {
|
|
152
|
+
const computeTopNavigation = useCallback((collections: EntityCollection[], views: CMSView[], adminViews: CMSView[], viewsOrder?: string[], navigationGroupMappingsOverride?: NavigationGroupMapping[], onNavigationEntriesUpdateCallback?: (entries: NavigationGroupMapping[]) => void): NavigationResult => {
|
|
169
153
|
|
|
170
154
|
const finalNavigationGroupMappings: NavigationGroupMapping[] = computeNavigationGroups({
|
|
171
|
-
navigationGroupMappings: navigationGroupMappings,
|
|
155
|
+
navigationGroupMappings: navigationGroupMappingsOverride ?? navigationGroupMappings,
|
|
172
156
|
collections,
|
|
173
157
|
views,
|
|
174
158
|
plugins: plugins
|
|
@@ -209,7 +193,7 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
209
193
|
...(views ?? []).reduce((acc, view) => {
|
|
210
194
|
if (view.hideFromNavigation) return acc;
|
|
211
195
|
|
|
212
|
-
const pathKey =
|
|
196
|
+
const pathKey = view.path;
|
|
213
197
|
let groupName = getGroup(view); // Initial group
|
|
214
198
|
|
|
215
199
|
if (finalNavigationGroupMappings) {
|
|
@@ -237,7 +221,7 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
237
221
|
...(adminViews ?? []).reduce((acc, view) => {
|
|
238
222
|
if (view.hideFromNavigation) return acc;
|
|
239
223
|
|
|
240
|
-
const pathKey =
|
|
224
|
+
const pathKey = view.path;
|
|
241
225
|
const groupName = NAVIGATION_ADMIN_GROUP_NAME;
|
|
242
226
|
|
|
243
227
|
acc.push({
|
|
@@ -280,21 +264,62 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
280
264
|
.map(e => e.group)
|
|
281
265
|
.filter(Boolean) as string[];
|
|
282
266
|
|
|
267
|
+
// Preserve order from finalNavigationGroupMappings (persisted order)
|
|
268
|
+
const groupsFromMappings = finalNavigationGroupMappings.map(g => g.name);
|
|
269
|
+
|
|
270
|
+
// Add any additional groups not in mappings
|
|
271
|
+
const additionalGroups = collectedGroupsFromEntries.filter(g => !groupsFromMappings.includes(g));
|
|
272
|
+
|
|
283
273
|
const allDefinedGroups = [
|
|
284
274
|
...(pluginGroups ?? []),
|
|
285
|
-
...
|
|
275
|
+
...groupsFromMappings,
|
|
276
|
+
...additionalGroups
|
|
286
277
|
];
|
|
287
278
|
|
|
288
|
-
|
|
289
|
-
|
|
279
|
+
// Remove duplicates while preserving order, then separate admin to the end
|
|
280
|
+
const uniqueGroupsArray = [...new Set(allDefinedGroups)];
|
|
281
|
+
const adminGroups = uniqueGroupsArray.filter(g => g === NAVIGATION_ADMIN_GROUP_NAME);
|
|
282
|
+
const nonAdminGroups = uniqueGroupsArray.filter(g => g !== NAVIGATION_ADMIN_GROUP_NAME);
|
|
283
|
+
const uniqueGroups = [...nonAdminGroups, ...adminGroups];
|
|
290
284
|
|
|
291
285
|
return {
|
|
292
286
|
allowDragAndDrop: plugins?.some(plugin => plugin.homePage?.allowDragAndDrop) ?? false,
|
|
293
287
|
navigationEntries,
|
|
294
288
|
groups: uniqueGroups,
|
|
295
|
-
onNavigationEntriesUpdate:
|
|
289
|
+
onNavigationEntriesUpdate: onNavigationEntriesUpdateCallback!,
|
|
296
290
|
};
|
|
297
|
-
}, [navigationGroupMappings, buildCMSUrlPath, buildUrlCollectionPath, pluginGroups
|
|
291
|
+
}, [navigationGroupMappings, buildCMSUrlPath, buildUrlCollectionPath, pluginGroups]);
|
|
292
|
+
|
|
293
|
+
const onNavigationEntriesOrderUpdate = useCallback((entries: NavigationGroupMapping[]) => {
|
|
294
|
+
if (!plugins) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
// remove all groups that have no entries
|
|
298
|
+
const filteredEntries = entries.filter(entry => entry.entries.length > 0);
|
|
299
|
+
|
|
300
|
+
// Immediately update the local topLevelNavigation with new mappings
|
|
301
|
+
if (collectionsRef.current && viewsRef.current) {
|
|
302
|
+
const updatedNav = computeTopNavigation(
|
|
303
|
+
collectionsRef.current,
|
|
304
|
+
viewsRef.current,
|
|
305
|
+
adminViewsRef.current ?? [],
|
|
306
|
+
viewsOrder,
|
|
307
|
+
filteredEntries,
|
|
308
|
+
onNavigationEntriesOrderUpdate
|
|
309
|
+
);
|
|
310
|
+
setTopLevelNavigation(updatedNav);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Then persist to backend
|
|
314
|
+
if (plugins.some(plugin => plugin.homePage?.onNavigationEntriesUpdate)) {
|
|
315
|
+
plugins.forEach(plugin => {
|
|
316
|
+
if (plugin.homePage?.onNavigationEntriesUpdate) {
|
|
317
|
+
plugin.homePage.onNavigationEntriesUpdate(filteredEntries);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
}, [plugins, computeTopNavigation, viewsOrder]);
|
|
298
323
|
|
|
299
324
|
const refreshNavigation = useCallback(async () => {
|
|
300
325
|
|
|
@@ -312,7 +337,7 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
312
337
|
]
|
|
313
338
|
);
|
|
314
339
|
|
|
315
|
-
const computedTopLevelNav = computeTopNavigation(resolvedCollections, resolvedViews, resolvedAdminViews, viewsOrder);
|
|
340
|
+
const computedTopLevelNav = computeTopNavigation(resolvedCollections, resolvedViews, resolvedAdminViews, viewsOrder, undefined, onNavigationEntriesOrderUpdate);
|
|
316
341
|
|
|
317
342
|
let shouldUpdateTopLevelNav = false;
|
|
318
343
|
if (!areCollectionListsEqual(collectionsRef.current ?? [], resolvedCollections)) {
|
|
@@ -457,8 +482,9 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
457
482
|
[fullCollectionPath]);
|
|
458
483
|
|
|
459
484
|
const urlPathToDataPath = useCallback((path: string): string => {
|
|
460
|
-
|
|
461
|
-
|
|
485
|
+
const decodedPath = decodeURIComponent(path);
|
|
486
|
+
if (decodedPath.startsWith(fullCollectionPath))
|
|
487
|
+
return decodedPath.replace(fullCollectionPath, "");
|
|
462
488
|
throw Error("Expected path starting with " + fullCollectionPath);
|
|
463
489
|
}, [fullCollectionPath]);
|
|
464
490
|
|
|
@@ -716,6 +742,7 @@ function computeNavigationGroups({
|
|
|
716
742
|
|
|
717
743
|
let result = navigationGroupMappings;
|
|
718
744
|
|
|
745
|
+
// Merge plugin navigation entries
|
|
719
746
|
result = plugins ? plugins?.reduce((acc, plugin) => {
|
|
720
747
|
if (plugin.homePage?.navigationEntries) {
|
|
721
748
|
plugin.homePage.navigationEntries.forEach((entry) => {
|
|
@@ -738,8 +765,54 @@ function computeNavigationGroups({
|
|
|
738
765
|
return acc;
|
|
739
766
|
}, [...(result ?? [])] as NavigationGroupMapping[]) : result;
|
|
740
767
|
|
|
768
|
+
// Track all entries that are already assigned to groups
|
|
769
|
+
const assignedEntries = new Set<string>();
|
|
770
|
+
if (result) {
|
|
771
|
+
result.forEach(group => {
|
|
772
|
+
group.entries.forEach(entry => assignedEntries.add(entry));
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Find collections and views that are NOT in any persisted group
|
|
777
|
+
const unassignedGroupMap: Record<string, string[]> = {};
|
|
778
|
+
|
|
779
|
+
// Check collections
|
|
780
|
+
(collections ?? []).forEach(collection => {
|
|
781
|
+
const entry = collection.id ?? collection.path;
|
|
782
|
+
if (!assignedEntries.has(entry)) {
|
|
783
|
+
const groupName = getGroup(collection);
|
|
784
|
+
if (!unassignedGroupMap[groupName]) unassignedGroupMap[groupName] = [];
|
|
785
|
+
unassignedGroupMap[groupName].push(entry);
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
// Check views
|
|
790
|
+
(views ?? []).forEach(view => {
|
|
791
|
+
const entry = view.path;
|
|
792
|
+
if (!assignedEntries.has(entry)) {
|
|
793
|
+
const groupName = getGroup(view);
|
|
794
|
+
if (!unassignedGroupMap[groupName]) unassignedGroupMap[groupName] = [];
|
|
795
|
+
unassignedGroupMap[groupName].push(entry);
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// Merge unassigned entries into existing groups or create new groups
|
|
800
|
+
Object.entries(unassignedGroupMap).forEach(([groupName, entries]) => {
|
|
801
|
+
if (result) {
|
|
802
|
+
const existingGroup = result.find(g => g.name === groupName);
|
|
803
|
+
if (existingGroup) {
|
|
804
|
+
existingGroup.entries.push(...entries);
|
|
805
|
+
} else {
|
|
806
|
+
result.push({
|
|
807
|
+
name: groupName,
|
|
808
|
+
entries
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
|
|
741
814
|
if (!result) {
|
|
742
|
-
//
|
|
815
|
+
// No persisted data at all - create from scratch
|
|
743
816
|
result = [];
|
|
744
817
|
const groupMap: Record<string, string[]> = {};
|
|
745
818
|
|
|
@@ -754,12 +827,12 @@ function computeNavigationGroups({
|
|
|
754
827
|
// Add views
|
|
755
828
|
(views ?? []).forEach(view => {
|
|
756
829
|
const name = getGroup(view);
|
|
757
|
-
const entry =
|
|
830
|
+
const entry = view.path;
|
|
758
831
|
if (!groupMap[name]) groupMap[name] = [];
|
|
759
832
|
groupMap[name].push(entry);
|
|
760
833
|
});
|
|
761
834
|
|
|
762
|
-
// Convert groupMap to
|
|
835
|
+
// Convert groupMap to result array
|
|
763
836
|
result = Object.entries(groupMap).map(([name, entries]) => ({
|
|
764
837
|
name,
|
|
765
838
|
entries
|
|
@@ -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/types/collections.ts
CHANGED
|
@@ -352,6 +352,18 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
|
|
|
352
352
|
* This prop has no effect if the history plugin is not enabled
|
|
353
353
|
*/
|
|
354
354
|
history?: boolean;
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Should local changes be backed up in local storage, to prevent data loss on
|
|
358
|
+
* accidental navigations.
|
|
359
|
+
* - `manual_apply`: When the user navigates back to an entity with local changes,
|
|
360
|
+
* they will be prompted to restore the changes.
|
|
361
|
+
* - `auto_apply`: When the user navigates back to an entity with local changes,
|
|
362
|
+
* the changes will be automatically applied.
|
|
363
|
+
* - `false`: Local changes will not be backed up.
|
|
364
|
+
* Defaults to `manual_apply`.
|
|
365
|
+
*/
|
|
366
|
+
localChangesBackup?: "manual_apply" | "auto_apply" | false;
|
|
355
367
|
}
|
|
356
368
|
|
|
357
369
|
/**
|
package/src/types/properties.ts
CHANGED
|
@@ -155,6 +155,10 @@ export interface BaseProperty<T extends CMSType, CustomProps = any> {
|
|
|
155
155
|
/**
|
|
156
156
|
* Should this property be editable. If set to true, the user will be able to modify the property and
|
|
157
157
|
* save the new config. The saved config will then become the source of truth.
|
|
158
|
+
* Defaults to `true.
|
|
159
|
+
* This props is only useful when you are using the collection editor to modify collection
|
|
160
|
+
* configurations from the CMS itself. You can also use the `editable` prop in the
|
|
161
|
+
* `EntityCollection` interface to disable the edition of all properties in a collection.
|
|
158
162
|
*/
|
|
159
163
|
editable?: boolean;
|
|
160
164
|
|
|
@@ -775,8 +779,16 @@ export type StorageConfig = {
|
|
|
775
779
|
/**
|
|
776
780
|
* Use client side image compression and resizing
|
|
777
781
|
* Will only be applied to these MIME types: image/jpeg, image/png and image/webp
|
|
782
|
+
* @deprecated Use `imageResize` instead
|
|
778
783
|
*/
|
|
779
|
-
imageCompression?:
|
|
784
|
+
imageCompression?: ImageResize;
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Advanced image resizing and cropping configuration.
|
|
788
|
+
* Applied before upload to optimize storage and bandwidth.
|
|
789
|
+
* Only applies to image MIME types: image/jpeg, image/png, image/webp
|
|
790
|
+
*/
|
|
791
|
+
imageResize?: ImageResize;
|
|
780
792
|
|
|
781
793
|
/**
|
|
782
794
|
* Specific metadata set in your uploaded file.
|
|
@@ -913,19 +925,36 @@ export type FileType =
|
|
|
913
925
|
| "font/*"
|
|
914
926
|
| string;
|
|
915
927
|
|
|
916
|
-
export interface
|
|
928
|
+
export interface ImageResize {
|
|
917
929
|
/**
|
|
918
|
-
*
|
|
930
|
+
* Maximum width in pixels. Image will be scaled down proportionally if wider.
|
|
931
|
+
*/
|
|
932
|
+
maxWidth?: number;
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Maximum height in pixels. Image will be scaled down proportionally if taller.
|
|
919
936
|
*/
|
|
920
937
|
maxHeight?: number;
|
|
921
938
|
|
|
922
939
|
/**
|
|
923
|
-
*
|
|
940
|
+
* Resize mode determines how the image fits within maxWidth/maxHeight bounds.
|
|
941
|
+
* - `contain`: Scale down to fit within bounds, preserving aspect ratio (default)
|
|
942
|
+
* - `cover`: Scale to fill bounds, preserving aspect ratio (may crop)
|
|
924
943
|
*/
|
|
925
|
-
|
|
944
|
+
mode?: 'contain' | 'cover';
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Output format for the resized image.
|
|
948
|
+
* - `original`: Keep the original format (default)
|
|
949
|
+
* - `jpeg`: Convert to JPEG
|
|
950
|
+
* - `png`: Convert to PNG
|
|
951
|
+
* - `webp`: Convert to WebP
|
|
952
|
+
*/
|
|
953
|
+
format?: 'original' | 'jpeg' | 'png' | 'webp';
|
|
926
954
|
|
|
927
955
|
/**
|
|
928
|
-
*
|
|
956
|
+
* Quality for lossy formats (JPEG, WebP). Number between 0 and 100.
|
|
957
|
+
* Higher is better quality but larger file size. Defaults to 80.
|
|
929
958
|
*/
|
|
930
959
|
quality?: number;
|
|
931
960
|
}
|
package/src/util/collections.ts
CHANGED
|
@@ -70,3 +70,11 @@ export const applyPermissionsFunctionIfEmpty = (collections: EntityCollection[],
|
|
|
70
70
|
});
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
|
+
|
|
74
|
+
export function getLocalChangesBackup(collection: EntityCollection) {
|
|
75
|
+
if (!collection.localChangesBackup) {
|
|
76
|
+
return "manual_apply";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return collection.localChangesBackup;
|
|
80
|
+
}
|
|
@@ -4,6 +4,7 @@ export function createFormexStub<T extends object>(values: T): FormexController<
|
|
|
4
4
|
const errorMessage = "You are in a read-only context. You cannot modify the formex controller.";
|
|
5
5
|
|
|
6
6
|
return {
|
|
7
|
+
debugId: "",
|
|
7
8
|
values,
|
|
8
9
|
initialValues: values,
|
|
9
10
|
touched: {} as Record<string, boolean>,
|
|
@@ -19,6 +20,9 @@ export function createFormexStub<T extends object>(values: T): FormexController<
|
|
|
19
20
|
setValues: () => {
|
|
20
21
|
throw new Error(errorMessage);
|
|
21
22
|
},
|
|
23
|
+
setTouched(touched: Record<string, boolean>): void {
|
|
24
|
+
throw new Error(errorMessage);
|
|
25
|
+
},
|
|
22
26
|
setFieldValue: () => {
|
|
23
27
|
throw new Error(errorMessage);
|
|
24
28
|
},
|