@firecms/core 3.0.0-rc.2 → 3.0.0-rc.4
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/PropertyFieldBinding.d.ts +1 -1
- package/dist/form/components/LocalChangesMenu.d.ts +11 -0
- package/dist/form/index.d.ts +2 -1
- package/dist/index.es.js +1307 -384
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1306 -383
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collections.d.ts +11 -0
- package/dist/types/fields.d.ts +8 -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/SelectableTable.tsx +0 -12
- 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/PropertyFieldBinding.tsx +4 -4
- package/src/form/components/ErrorFocus.tsx +22 -29
- package/src/form/components/LocalChangesMenu.tsx +144 -0
- package/src/form/field_bindings/BlockFieldBinding.tsx +1 -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/fields.tsx +10 -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
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::";
|
|
@@ -18,25 +19,40 @@ function customReplacer(key: string): any {
|
|
|
18
19
|
// Handle Date objects
|
|
19
20
|
// @ts-ignore
|
|
20
21
|
if (value instanceof Date) {
|
|
21
|
-
return {
|
|
22
|
+
return {
|
|
23
|
+
__type: "Date",
|
|
24
|
+
value: value.toISOString()
|
|
25
|
+
};
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
// Handle EntityReference
|
|
25
29
|
// @ts-ignore
|
|
26
30
|
if (value instanceof EntityReference) {
|
|
27
|
-
return {
|
|
31
|
+
return {
|
|
32
|
+
__type: "EntityReference",
|
|
33
|
+
id: value.id,
|
|
34
|
+
path: value.path,
|
|
35
|
+
databaseId: value.databaseId
|
|
36
|
+
};
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
// Handle GeoPoint
|
|
31
40
|
// @ts-ignore
|
|
32
41
|
if (value instanceof GeoPoint) {
|
|
33
|
-
return {
|
|
42
|
+
return {
|
|
43
|
+
__type: "GeoPoint",
|
|
44
|
+
latitude: value.latitude,
|
|
45
|
+
longitude: value.longitude
|
|
46
|
+
};
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
// Handle Vector
|
|
37
50
|
// @ts-ignore
|
|
38
51
|
if (value instanceof Vector) {
|
|
39
|
-
return {
|
|
52
|
+
return {
|
|
53
|
+
__type: "Vector",
|
|
54
|
+
value: value.value
|
|
55
|
+
};
|
|
40
56
|
}
|
|
41
57
|
|
|
42
58
|
return value;
|
|
@@ -61,47 +77,21 @@ function customReviver(key: string, value: any): any {
|
|
|
61
77
|
return value;
|
|
62
78
|
}
|
|
63
79
|
|
|
64
|
-
// Initialize the in-memory cache by loading entities from `localStorage`
|
|
65
|
-
if (isLocalStorageAvailable) {
|
|
66
|
-
try {
|
|
67
|
-
// Iterate over all keys in localStorage to find those with the specified prefix
|
|
68
|
-
for (let i = 0; i < localStorage.length; i++) {
|
|
69
|
-
const fullKey = localStorage.key(i);
|
|
70
|
-
if (fullKey && fullKey.startsWith(LOCAL_STORAGE_PREFIX)) {
|
|
71
|
-
const path = fullKey.substring(LOCAL_STORAGE_PREFIX.length);
|
|
72
|
-
const entityString = localStorage.getItem(fullKey);
|
|
73
|
-
if (entityString) {
|
|
74
|
-
try {
|
|
75
|
-
const entity: object = JSON.parse(entityString, customReviver);
|
|
76
|
-
entityCache.set(path, entity);
|
|
77
|
-
} catch (parseError) {
|
|
78
|
-
console.error(
|
|
79
|
-
`Failed to parse entity for path "${path}" from localStorage:`,
|
|
80
|
-
parseError
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
} catch (error) {
|
|
87
|
-
console.error("Error accessing localStorage during initialization:", error);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
80
|
/**
|
|
92
81
|
* Saves data to the in-memory cache and persists it individually in `localStorage`.
|
|
93
82
|
* @param path - The unique path/key for the data.
|
|
94
83
|
* @param data - The data to cache and persist.
|
|
95
84
|
*/
|
|
96
85
|
export function saveEntityToCache(path: string, data: object): void {
|
|
97
|
-
// Update the in-memory cache
|
|
98
|
-
entityCache.set(path, data);
|
|
99
|
-
|
|
100
86
|
// Persist the data individually in localStorage
|
|
101
87
|
if (isLocalStorageAvailable) {
|
|
102
88
|
try {
|
|
103
89
|
const key = LOCAL_STORAGE_PREFIX + path;
|
|
104
90
|
const entityString = JSON.stringify(data, customReplacer);
|
|
91
|
+
console.log("Saving entity to localStorage:", {
|
|
92
|
+
key,
|
|
93
|
+
entityString
|
|
94
|
+
});
|
|
105
95
|
localStorage.setItem(key, entityString);
|
|
106
96
|
} catch (error) {
|
|
107
97
|
console.error(
|
|
@@ -112,19 +102,31 @@ export function saveEntityToCache(path: string, data: object): void {
|
|
|
112
102
|
}
|
|
113
103
|
}
|
|
114
104
|
|
|
105
|
+
export function removeEntityFromMemoryCache(path: string): void {
|
|
106
|
+
entityCache.delete(path);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function saveEntityToMemoryCache(path: string, data: object): void {
|
|
110
|
+
entityCache.set(path, data);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function getEntityFromMemoryCache(path: string): object | undefined {
|
|
114
|
+
return entityCache.get(path);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function hasEntityInCache(path: string): boolean {
|
|
118
|
+
return entityCache.has(path);
|
|
119
|
+
}
|
|
120
|
+
|
|
115
121
|
/**
|
|
116
122
|
* Retrieves an entity from the in-memory cache or `localStorage`.
|
|
117
123
|
* If the entity is not in the cache but exists in `localStorage`, it loads it into the cache.
|
|
118
124
|
* @param path - The unique path/key for the entity.
|
|
125
|
+
* @param useLocalStorage
|
|
119
126
|
* @returns The cached entity or `undefined` if not found.
|
|
120
127
|
*/
|
|
121
128
|
export function getEntityFromCache(path: string): object | undefined {
|
|
122
129
|
|
|
123
|
-
// Attempt to retrieve the entity from the in-memory cache
|
|
124
|
-
if (entityCache.has(path)) {
|
|
125
|
-
return entityCache.get(path);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
130
|
// If not in the cache, attempt to load it from localStorage
|
|
129
131
|
if (isLocalStorageAvailable) {
|
|
130
132
|
try {
|
|
@@ -132,7 +134,10 @@ export function getEntityFromCache(path: string): object | undefined {
|
|
|
132
134
|
const entityString = localStorage.getItem(key);
|
|
133
135
|
if (entityString) {
|
|
134
136
|
const entity: object = JSON.parse(entityString, customReviver);
|
|
135
|
-
|
|
137
|
+
console.log("Loaded entity from localStorage:", {
|
|
138
|
+
key,
|
|
139
|
+
entity
|
|
140
|
+
});
|
|
136
141
|
return entity;
|
|
137
142
|
}
|
|
138
143
|
} catch (error) {
|
|
@@ -147,23 +152,11 @@ export function getEntityFromCache(path: string): object | undefined {
|
|
|
147
152
|
return undefined;
|
|
148
153
|
}
|
|
149
154
|
|
|
150
|
-
export function hasEntityInCache(path: string): boolean {
|
|
151
|
-
return entityCache.has(path);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
155
|
/**
|
|
155
156
|
* Removes an entity from both the in-memory cache and `localStorage`.
|
|
156
157
|
* @param path - The unique path/key for the entity to remove.
|
|
157
158
|
*/
|
|
158
159
|
export function removeEntityFromCache(path: string): void {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
console.debug("Removing entity from cache", path);
|
|
162
|
-
|
|
163
|
-
// Remove from the in-memory cache
|
|
164
|
-
entityCache.delete(path);
|
|
165
|
-
|
|
166
|
-
// Remove from localStorage
|
|
167
160
|
if (isLocalStorageAvailable) {
|
|
168
161
|
try {
|
|
169
162
|
const key = LOCAL_STORAGE_PREFIX + path;
|
|
@@ -202,3 +195,29 @@ export function clearEntityCache(): void {
|
|
|
202
195
|
}
|
|
203
196
|
}
|
|
204
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
|
+
}
|
|
@@ -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
|
-
}
|
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)) {
|
|
@@ -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
|
+
}
|