@firecms/core 3.0.0-canary.289 → 3.0.0-canary.290
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/form/PropertyFieldBinding.d.ts +1 -1
- package/dist/index.es.js +328 -193
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +329 -194
- package/dist/index.umd.js.map +1 -1
- package/dist/types/fields.d.ts +8 -0
- package/dist/types/properties.d.ts +32 -6
- package/dist/util/make_properties_editable.d.ts +1 -2
- package/dist/util/useStorageUploadController.d.ts +1 -0
- package/package.json +15 -15
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +5 -0
- package/src/components/HomePage/DefaultHomePage.tsx +13 -9
- package/src/components/HomePage/HomePageDnD.tsx +140 -38
- package/src/components/PropertyCollectionView.tsx +5 -5
- package/src/components/SelectableTable/SelectableTable.tsx +0 -12
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +2 -1
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +0 -1
- package/src/form/EntityForm.tsx +3 -3
- package/src/form/PropertyFieldBinding.tsx +4 -4
- package/src/form/components/LocalChangesMenu.tsx +10 -6
- package/src/form/field_bindings/BlockFieldBinding.tsx +1 -0
- package/src/hooks/useBuildNavigationController.tsx +101 -29
- package/src/types/fields.tsx +10 -0
- package/src/types/properties.ts +35 -6
- package/src/util/join_collections.ts +3 -3
- package/src/util/make_properties_editable.ts +0 -22
- package/src/util/useStorageUploadController.tsx +71 -34
|
@@ -83,6 +83,7 @@ function PropertyFieldBindingInternal<T extends CMSType = CMSType, M extends Rec
|
|
|
83
83
|
underlyingValueHasChanged,
|
|
84
84
|
disabled: disabledProp,
|
|
85
85
|
partOfArray,
|
|
86
|
+
partOfBlock,
|
|
86
87
|
minimalistView,
|
|
87
88
|
autoFocus,
|
|
88
89
|
index,
|
|
@@ -93,10 +94,6 @@ function PropertyFieldBindingInternal<T extends CMSType = CMSType, M extends Rec
|
|
|
93
94
|
const authController = useAuthController();
|
|
94
95
|
const customizationController = useCustomizationController();
|
|
95
96
|
|
|
96
|
-
if(propertyKey === "created_by"){
|
|
97
|
-
console.log("Rendering field for created_by", {propertyKey, property, context});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
97
|
return (
|
|
101
98
|
<Field
|
|
102
99
|
key={propertyKey}
|
|
@@ -168,6 +165,7 @@ function PropertyFieldBindingInternal<T extends CMSType = CMSType, M extends Rec
|
|
|
168
165
|
context,
|
|
169
166
|
disabled,
|
|
170
167
|
partOfArray,
|
|
168
|
+
partOfBlock,
|
|
171
169
|
minimalistView,
|
|
172
170
|
autoFocus,
|
|
173
171
|
size,
|
|
@@ -199,6 +197,7 @@ function FieldInternal<T extends CMSType, CustomProps, M extends Record<string,
|
|
|
199
197
|
includeDescription,
|
|
200
198
|
underlyingValueHasChanged,
|
|
201
199
|
partOfArray,
|
|
200
|
+
partOfBlock,
|
|
202
201
|
minimalistView,
|
|
203
202
|
autoFocus,
|
|
204
203
|
context,
|
|
@@ -261,6 +260,7 @@ function FieldInternal<T extends CMSType, CustomProps, M extends Record<string,
|
|
|
261
260
|
disabled: disabled ?? false,
|
|
262
261
|
underlyingValueHasChanged: underlyingValueHasChanged ?? false,
|
|
263
262
|
partOfArray: partOfArray ?? false,
|
|
263
|
+
partOfBlock: partOfBlock ?? false,
|
|
264
264
|
minimalistView: minimalistView ?? false,
|
|
265
265
|
autoFocus: autoFocus ?? false,
|
|
266
266
|
customProps: customFieldProps,
|
|
@@ -7,9 +7,12 @@ import {
|
|
|
7
7
|
Dialog,
|
|
8
8
|
DialogActions,
|
|
9
9
|
DialogContent,
|
|
10
|
+
DialogTitle,
|
|
10
11
|
KeyboardArrowDownIcon,
|
|
11
12
|
Menu,
|
|
12
|
-
MenuItem,
|
|
13
|
+
MenuItem,
|
|
14
|
+
Typography,
|
|
15
|
+
VisibilityIcon,
|
|
13
16
|
WarningIcon
|
|
14
17
|
} from "@firecms/ui";
|
|
15
18
|
import { FormexController } from "@firecms/formex";
|
|
@@ -108,17 +111,18 @@ export function LocalChangesMenu<M extends object>({
|
|
|
108
111
|
onOpenChange={setPreviewDialogOpen}
|
|
109
112
|
maxWidth={"4xl"}
|
|
110
113
|
>
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
<
|
|
114
|
+
<DialogTitle variant={"h6"}>Preview Local Changes</DialogTitle>
|
|
115
|
+
<DialogContent className={"my-4"}>
|
|
116
|
+
<Typography variant={"body2"} className={"mb-4"}>
|
|
114
117
|
These are the local changes that will be applied to the form.
|
|
115
|
-
</
|
|
118
|
+
</Typography>
|
|
116
119
|
<div className={`border rounded-lg ${defaultBorderMixin}`} style={{
|
|
117
120
|
maxHeight: 520,
|
|
118
121
|
overflow: "auto"
|
|
119
122
|
}}>
|
|
120
123
|
<div className="p-4">
|
|
121
|
-
<PropertyCollectionView data={localChangesData}
|
|
124
|
+
<PropertyCollectionView data={localChangesData}
|
|
125
|
+
properties={properties as ResolvedProperties}/>
|
|
122
126
|
</div>
|
|
123
127
|
</div>
|
|
124
128
|
</DialogContent>
|
|
@@ -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)) {
|
|
@@ -717,6 +742,7 @@ function computeNavigationGroups({
|
|
|
717
742
|
|
|
718
743
|
let result = navigationGroupMappings;
|
|
719
744
|
|
|
745
|
+
// Merge plugin navigation entries
|
|
720
746
|
result = plugins ? plugins?.reduce((acc, plugin) => {
|
|
721
747
|
if (plugin.homePage?.navigationEntries) {
|
|
722
748
|
plugin.homePage.navigationEntries.forEach((entry) => {
|
|
@@ -739,8 +765,54 @@ function computeNavigationGroups({
|
|
|
739
765
|
return acc;
|
|
740
766
|
}, [...(result ?? [])] as NavigationGroupMapping[]) : result;
|
|
741
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
|
+
|
|
742
814
|
if (!result) {
|
|
743
|
-
//
|
|
815
|
+
// No persisted data at all - create from scratch
|
|
744
816
|
result = [];
|
|
745
817
|
const groupMap: Record<string, string[]> = {};
|
|
746
818
|
|
|
@@ -755,12 +827,12 @@ function computeNavigationGroups({
|
|
|
755
827
|
// Add views
|
|
756
828
|
(views ?? []).forEach(view => {
|
|
757
829
|
const name = getGroup(view);
|
|
758
|
-
const entry =
|
|
830
|
+
const entry = view.path;
|
|
759
831
|
if (!groupMap[name]) groupMap[name] = [];
|
|
760
832
|
groupMap[name].push(entry);
|
|
761
833
|
});
|
|
762
834
|
|
|
763
|
-
// Convert groupMap to
|
|
835
|
+
// Convert groupMap to result array
|
|
764
836
|
result = Object.entries(groupMap).map(([name, entries]) => ({
|
|
765
837
|
name,
|
|
766
838
|
entries
|
package/src/types/fields.tsx
CHANGED
|
@@ -81,6 +81,11 @@ export interface FieldProps<T extends CMSType = any, CustomProps = any, M extend
|
|
|
81
81
|
*/
|
|
82
82
|
partOfArray?: boolean;
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Is this field part of a block
|
|
86
|
+
*/
|
|
87
|
+
partOfBlock?: boolean;
|
|
88
|
+
|
|
84
89
|
/**
|
|
85
90
|
* Display the child properties directly, without being wrapped in an
|
|
86
91
|
* extendable panel. Note that this will also hide the title of this property.
|
|
@@ -220,6 +225,11 @@ export interface PropertyFieldBindingProps<T extends CMSType, M extends Record<s
|
|
|
220
225
|
*/
|
|
221
226
|
partOfArray?: boolean;
|
|
222
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Is this field part of a block
|
|
230
|
+
*/
|
|
231
|
+
partOfBlock?: boolean;
|
|
232
|
+
|
|
223
233
|
/**
|
|
224
234
|
* Display the child properties directly, without being wrapped in an
|
|
225
235
|
* extendable panel. Note that this will also hide the title of this property.
|
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
|
}
|
|
@@ -78,7 +78,6 @@ export function mergeCollection(target: EntityCollection,
|
|
|
78
78
|
modifyCollection?: (props: ModifyCollectionProps) => EntityCollection | void
|
|
79
79
|
): EntityCollection {
|
|
80
80
|
|
|
81
|
-
|
|
82
81
|
const subcollectionsMerged = joinCollectionLists(
|
|
83
82
|
target?.subcollections ?? [],
|
|
84
83
|
source?.subcollections ?? [],
|
|
@@ -132,8 +131,9 @@ function mergePropertyOrBuilder(target: PropertyOrBuilder, source: PropertyOrBui
|
|
|
132
131
|
return target;
|
|
133
132
|
} else {
|
|
134
133
|
const mergedProperty = mergeDeep(target, source);
|
|
135
|
-
const targetEditable = Boolean(target.editable);
|
|
136
|
-
const sourceEditable = Boolean(source.editable);
|
|
134
|
+
const targetEditable = target.editable === undefined ? true : Boolean(target.editable);
|
|
135
|
+
const sourceEditable = source.editable === undefined ? true : Boolean(source.editable);
|
|
136
|
+
|
|
137
137
|
if (source.dataType === "map" && source.properties) {
|
|
138
138
|
const targetProperties = ("properties" in target ? target.properties : {}) as PropertiesOrBuilders;
|
|
139
139
|
const sourceProperties = ("properties" in source ? source.properties : {}) as PropertiesOrBuilders;
|
|
@@ -13,25 +13,3 @@ export function makePropertiesEditable(properties: Properties) {
|
|
|
13
13
|
});
|
|
14
14
|
return properties;
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
export function makePropertiesNonEditable(properties: PropertiesOrBuilders): PropertiesOrBuilders {
|
|
18
|
-
return Object.entries(properties).reduce((acc, [key, property]) => {
|
|
19
|
-
if (!isPropertyBuilder(property) && property.dataType === "map" && property.properties) {
|
|
20
|
-
const updated = {
|
|
21
|
-
...property,
|
|
22
|
-
properties: makePropertiesNonEditable(property.properties as PropertiesOrBuilders)
|
|
23
|
-
};
|
|
24
|
-
acc[key] = updated;
|
|
25
|
-
}
|
|
26
|
-
if (isPropertyBuilder(property)) {
|
|
27
|
-
acc[key] = property;
|
|
28
|
-
} else {
|
|
29
|
-
acc[key] = {
|
|
30
|
-
...property,
|
|
31
|
-
editable: false
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
return acc;
|
|
35
|
-
}, {} as PropertiesOrBuilders);
|
|
36
|
-
|
|
37
|
-
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Compressor from "compressorjs";
|
|
2
2
|
import equal from "react-fast-compare";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
ArrayProperty,
|
|
6
6
|
EntityValues,
|
|
7
|
-
|
|
7
|
+
ImageResize,
|
|
8
8
|
Property,
|
|
9
9
|
PropertyOrBuilder,
|
|
10
10
|
ResolvedArrayProperty,
|
|
@@ -76,7 +76,9 @@ export function useStorageUploadController<M extends object>({
|
|
|
76
76
|
const metadata: Record<string, any> | undefined = storage?.metadata;
|
|
77
77
|
const size = multipleFilesSupported ? "medium" : "large";
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
// Support both new imageResize and deprecated imageCompression
|
|
80
|
+
const imageResize = storage?.imageResize;
|
|
81
|
+
const legacyCompression = storage?.imageCompression;
|
|
80
82
|
|
|
81
83
|
const internalInitialValue: StorageFieldItem[] =
|
|
82
84
|
getInternalInitialValue(multipleFilesSupported, value, metadata, size);
|
|
@@ -169,6 +171,14 @@ export function useStorageUploadController<M extends object>({
|
|
|
169
171
|
}
|
|
170
172
|
}, [internalValue, multipleFilesSupported, onChange, storage, storageSource]);
|
|
171
173
|
|
|
174
|
+
const onFileUploadError = useCallback((entry: StorageFieldItem) => {
|
|
175
|
+
console.debug("onFileUploadError", entry);
|
|
176
|
+
|
|
177
|
+
// Remove the failed entry from internalValue
|
|
178
|
+
const newValue = internalValue.filter(item => item.id !== entry.id);
|
|
179
|
+
setInternalValue(newValue);
|
|
180
|
+
}, [internalValue]);
|
|
181
|
+
|
|
172
182
|
const onFilesAdded = useCallback(async (acceptedFiles: File[]) => {
|
|
173
183
|
|
|
174
184
|
if (!acceptedFiles.length || disabled)
|
|
@@ -193,8 +203,8 @@ export function useStorageUploadController<M extends object>({
|
|
|
193
203
|
if (multipleFilesSupported) {
|
|
194
204
|
newInternalValue = [...internalValue,
|
|
195
205
|
...(await Promise.all(acceptedFiles.map(async file => {
|
|
196
|
-
if (
|
|
197
|
-
file = await
|
|
206
|
+
if ((imageResize || legacyCompression) && isImageFile(file)) {
|
|
207
|
+
file = await resizeImage(file, imageResize, legacyCompression);
|
|
198
208
|
}
|
|
199
209
|
|
|
200
210
|
return {
|
|
@@ -206,9 +216,9 @@ export function useStorageUploadController<M extends object>({
|
|
|
206
216
|
} as StorageFieldItem;
|
|
207
217
|
})))];
|
|
208
218
|
} else {
|
|
209
|
-
let file = acceptedFiles[0]
|
|
210
|
-
if (
|
|
211
|
-
file = await
|
|
219
|
+
let file = acceptedFiles[0];
|
|
220
|
+
if ((imageResize || legacyCompression) && isImageFile(file)) {
|
|
221
|
+
file = await resizeImage(file, imageResize, legacyCompression);
|
|
212
222
|
}
|
|
213
223
|
|
|
214
224
|
newInternalValue = [{
|
|
@@ -223,7 +233,7 @@ export function useStorageUploadController<M extends object>({
|
|
|
223
233
|
// Remove either storage path or file duplicates
|
|
224
234
|
newInternalValue = removeDuplicates(newInternalValue);
|
|
225
235
|
setInternalValue(newInternalValue);
|
|
226
|
-
}, [disabled, fileNameBuilder, internalValue, metadata, multipleFilesSupported, size]);
|
|
236
|
+
}, [disabled, fileNameBuilder, internalValue, metadata, multipleFilesSupported, size, imageResize, legacyCompression]);
|
|
227
237
|
|
|
228
238
|
return {
|
|
229
239
|
internalValue,
|
|
@@ -232,6 +242,7 @@ export function useStorageUploadController<M extends object>({
|
|
|
232
242
|
fileNameBuilder,
|
|
233
243
|
storagePathBuilder,
|
|
234
244
|
onFileUploadComplete,
|
|
245
|
+
onFileUploadError,
|
|
235
246
|
onFilesAdded,
|
|
236
247
|
multipleFilesSupported
|
|
237
248
|
}
|
|
@@ -276,31 +287,57 @@ function getRandomId() {
|
|
|
276
287
|
return Math.floor(Math.random() * Math.floor(Number.MAX_SAFE_INTEGER));
|
|
277
288
|
}
|
|
278
289
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
290
|
+
/**
|
|
291
|
+
* Check if a file is an image type supported for resizing
|
|
292
|
+
*/
|
|
293
|
+
function isImageFile(file: File): boolean {
|
|
294
|
+
return file.type === "image/jpeg" ||
|
|
295
|
+
file.type === "image/png" ||
|
|
296
|
+
file.type === "image/webp";
|
|
283
297
|
}
|
|
284
|
-
const compressionFormat = (file: File) => supportedTypes[file.type] ? supportedTypes[file.type] : null;
|
|
285
|
-
|
|
286
|
-
const defaultQuality = 100;
|
|
287
|
-
const resizeAndCompressImage = (file: File, compression: ImageCompression) => new Promise<File>((resolve) => {
|
|
288
298
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
299
|
+
/**
|
|
300
|
+
* Resize and compress an image using compressorjs.
|
|
301
|
+
* Supports both the new imageResize API and legacy imageCompression for backward compatibility.
|
|
302
|
+
*/
|
|
303
|
+
async function resizeImage(
|
|
304
|
+
file: File,
|
|
305
|
+
imageResize?: StorageConfig["imageResize"],
|
|
306
|
+
legacyCompression?: ImageResize
|
|
307
|
+
): Promise<File> {
|
|
308
|
+
// Determine configuration (new API takes precedence)
|
|
309
|
+
const maxWidth = imageResize?.maxWidth ?? legacyCompression?.maxWidth;
|
|
310
|
+
const maxHeight = imageResize?.maxHeight ?? legacyCompression?.maxHeight;
|
|
311
|
+
const quality = (imageResize?.quality ?? legacyCompression?.quality ?? 80) / 100;
|
|
312
|
+
const mode = imageResize?.mode ?? "contain";
|
|
313
|
+
|
|
314
|
+
// Determine output format
|
|
315
|
+
let mimeType = file.type;
|
|
316
|
+
if (imageResize?.format && imageResize.format !== "original") {
|
|
317
|
+
mimeType = `image/${imageResize.format}`;
|
|
295
318
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
319
|
+
|
|
320
|
+
return new Promise<File>((resolve, reject) => {
|
|
321
|
+
new Compressor(file, {
|
|
322
|
+
quality,
|
|
323
|
+
maxWidth,
|
|
324
|
+
maxHeight,
|
|
325
|
+
mimeType,
|
|
326
|
+
// Use cover mode if specified (crops to fit)
|
|
327
|
+
// Otherwise use contain mode (scales to fit)
|
|
328
|
+
...(mode === "cover" || mode === undefined ? {
|
|
329
|
+
width: maxWidth,
|
|
330
|
+
height: maxHeight,
|
|
331
|
+
resize: "cover" as const
|
|
332
|
+
} : {}),
|
|
333
|
+
success: (result) => {
|
|
334
|
+
const compressedFile = new File([result], file.name, {
|
|
335
|
+
type: result.type,
|
|
336
|
+
lastModified: Date.now(),
|
|
337
|
+
});
|
|
338
|
+
resolve(compressedFile);
|
|
339
|
+
},
|
|
340
|
+
error: reject,
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
}
|