@firecms/core 3.0.0-rc.1 → 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/components/UserDisplay.d.ts +7 -0
- package/dist/components/VirtualTable/fields/VirtualTableUserSelect.d.ts +12 -0
- package/dist/contexts/InternalUserManagementContext.d.ts +3 -0
- package/dist/core/EntityEditView.d.ts +10 -4
- package/dist/core/FireCMS.d.ts +0 -1
- package/dist/core/field_configs.d.ts +1 -1
- package/dist/form/EntityForm.d.ts +5 -2
- package/dist/form/components/LocalChangesMenu.d.ts +11 -0
- package/dist/form/field_bindings/UserSelectFieldBinding.d.ts +12 -0
- package/dist/form/index.d.ts +2 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/useCollapsedGroups.d.ts +9 -0
- package/dist/hooks/useInternalUserManagementController.d.ts +12 -0
- package/dist/index.es.js +1983 -650
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1981 -648
- package/dist/index.umd.js.map +1 -1
- package/dist/preview/components/UserPreview.d.ts +8 -0
- package/dist/preview/index.d.ts +1 -0
- package/dist/types/collections.d.ts +13 -0
- package/dist/types/entities.d.ts +5 -1
- package/dist/types/firecms.d.ts +15 -0
- package/dist/types/firecms_context.d.ts +16 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/internal_user_management.d.ts +20 -0
- package/dist/types/plugins.d.ts +2 -0
- package/dist/types/properties.d.ts +41 -6
- package/dist/types/property_config.d.ts +1 -1
- package/dist/types/user.d.ts +1 -1
- 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/EntityCollectionTable/PropertyTableCell.tsx +12 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +6 -1
- package/src/components/EntityView.tsx +29 -40
- package/src/components/ErrorView.tsx +1 -1
- package/src/components/HomePage/DefaultHomePage.tsx +21 -34
- package/src/components/HomePage/HomePageDnD.tsx +143 -83
- package/src/components/HomePage/RenameGroupDialog.tsx +9 -3
- package/src/components/PropertyCollectionView.tsx +329 -0
- package/src/components/PropertyConfigBadge.tsx +2 -2
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +2 -1
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -2
- package/src/components/UserDisplay.tsx +55 -0
- package/src/components/VirtualTable/fields/VirtualTableUserSelect.tsx +99 -0
- package/src/components/common/useColumnsIds.tsx +1 -8
- package/src/contexts/InternalUserManagementContext.tsx +4 -0
- package/src/core/EntityEditView.tsx +27 -14
- package/src/core/EntityEditViewFormActions.tsx +33 -18
- package/src/core/EntitySidePanel.tsx +9 -3
- package/src/core/FireCMS.tsx +22 -13
- package/src/core/field_configs.tsx +15 -1
- package/src/form/EntityForm.tsx +173 -42
- package/src/form/EntityFormActions.tsx +30 -15
- package/src/form/PropertyFieldBinding.tsx +4 -0
- package/src/form/components/ErrorFocus.tsx +22 -29
- package/src/form/components/LocalChangesMenu.tsx +144 -0
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +94 -0
- package/src/form/index.tsx +5 -1
- package/src/hooks/index.tsx +3 -0
- package/src/hooks/useBrowserTitleAndIcon.tsx +1 -1
- package/src/hooks/useBuildNavigationController.tsx +104 -31
- package/src/hooks/useCollapsedGroups.ts +64 -0
- package/src/hooks/useFireCMSContext.tsx +6 -2
- package/src/hooks/useInternalUserManagementController.tsx +16 -0
- package/src/preview/PropertyPreview.tsx +8 -0
- package/src/preview/components/ReferencePreview.tsx +4 -2
- package/src/preview/components/UserPreview.tsx +27 -0
- package/src/preview/index.ts +1 -0
- package/src/preview/property_previews/ArrayPropertyPreview.tsx +1 -1
- package/src/preview/property_previews/MapPropertyPreview.tsx +2 -2
- package/src/preview/property_previews/NumberPropertyPreview.tsx +2 -2
- package/src/types/collections.ts +14 -0
- package/src/types/entities.ts +7 -1
- package/src/types/firecms.tsx +16 -0
- package/src/types/firecms_context.tsx +17 -0
- package/src/types/index.ts +1 -0
- package/src/types/internal_user_management.ts +24 -0
- package/src/types/plugins.tsx +3 -0
- package/src/types/properties.ts +45 -6
- package/src/types/property_config.tsx +1 -0
- package/src/types/user.ts +1 -1
- package/src/util/collections.ts +8 -0
- package/src/util/createFormexStub.tsx +4 -0
- package/src/util/entities.ts +1 -1
- package/src/util/entity_cache.ts +72 -53
- 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/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
|
+
}
|