@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.
Files changed (96) hide show
  1. package/dist/components/HomePage/HomePageDnD.d.ts +2 -1
  2. package/dist/components/PropertyCollectionView.d.ts +23 -0
  3. package/dist/components/UserDisplay.d.ts +7 -0
  4. package/dist/components/VirtualTable/fields/VirtualTableUserSelect.d.ts +12 -0
  5. package/dist/contexts/InternalUserManagementContext.d.ts +3 -0
  6. package/dist/core/EntityEditView.d.ts +10 -4
  7. package/dist/core/FireCMS.d.ts +0 -1
  8. package/dist/core/field_configs.d.ts +1 -1
  9. package/dist/form/EntityForm.d.ts +5 -2
  10. package/dist/form/components/LocalChangesMenu.d.ts +11 -0
  11. package/dist/form/field_bindings/UserSelectFieldBinding.d.ts +12 -0
  12. package/dist/form/index.d.ts +2 -1
  13. package/dist/hooks/index.d.ts +2 -0
  14. package/dist/hooks/useCollapsedGroups.d.ts +9 -0
  15. package/dist/hooks/useInternalUserManagementController.d.ts +12 -0
  16. package/dist/index.es.js +1983 -650
  17. package/dist/index.es.js.map +1 -1
  18. package/dist/index.umd.js +1981 -648
  19. package/dist/index.umd.js.map +1 -1
  20. package/dist/preview/components/UserPreview.d.ts +8 -0
  21. package/dist/preview/index.d.ts +1 -0
  22. package/dist/types/collections.d.ts +13 -0
  23. package/dist/types/entities.d.ts +5 -1
  24. package/dist/types/firecms.d.ts +15 -0
  25. package/dist/types/firecms_context.d.ts +16 -0
  26. package/dist/types/index.d.ts +1 -0
  27. package/dist/types/internal_user_management.d.ts +20 -0
  28. package/dist/types/plugins.d.ts +2 -0
  29. package/dist/types/properties.d.ts +41 -6
  30. package/dist/types/property_config.d.ts +1 -1
  31. package/dist/types/user.d.ts +1 -1
  32. package/dist/util/collections.d.ts +1 -0
  33. package/dist/util/entity_cache.d.ts +6 -1
  34. package/dist/util/make_properties_editable.d.ts +1 -2
  35. package/dist/util/objects.d.ts +1 -0
  36. package/dist/util/useStorageUploadController.d.ts +1 -0
  37. package/package.json +6 -6
  38. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +47 -47
  39. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +12 -0
  40. package/src/components/EntityCollectionView/EntityCollectionView.tsx +6 -1
  41. package/src/components/EntityView.tsx +29 -40
  42. package/src/components/ErrorView.tsx +1 -1
  43. package/src/components/HomePage/DefaultHomePage.tsx +21 -34
  44. package/src/components/HomePage/HomePageDnD.tsx +143 -83
  45. package/src/components/HomePage/RenameGroupDialog.tsx +9 -3
  46. package/src/components/PropertyCollectionView.tsx +329 -0
  47. package/src/components/PropertyConfigBadge.tsx +2 -2
  48. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +2 -1
  49. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -2
  50. package/src/components/UserDisplay.tsx +55 -0
  51. package/src/components/VirtualTable/fields/VirtualTableUserSelect.tsx +99 -0
  52. package/src/components/common/useColumnsIds.tsx +1 -8
  53. package/src/contexts/InternalUserManagementContext.tsx +4 -0
  54. package/src/core/EntityEditView.tsx +27 -14
  55. package/src/core/EntityEditViewFormActions.tsx +33 -18
  56. package/src/core/EntitySidePanel.tsx +9 -3
  57. package/src/core/FireCMS.tsx +22 -13
  58. package/src/core/field_configs.tsx +15 -1
  59. package/src/form/EntityForm.tsx +173 -42
  60. package/src/form/EntityFormActions.tsx +30 -15
  61. package/src/form/PropertyFieldBinding.tsx +4 -0
  62. package/src/form/components/ErrorFocus.tsx +22 -29
  63. package/src/form/components/LocalChangesMenu.tsx +144 -0
  64. package/src/form/field_bindings/UserSelectFieldBinding.tsx +94 -0
  65. package/src/form/index.tsx +5 -1
  66. package/src/hooks/index.tsx +3 -0
  67. package/src/hooks/useBrowserTitleAndIcon.tsx +1 -1
  68. package/src/hooks/useBuildNavigationController.tsx +104 -31
  69. package/src/hooks/useCollapsedGroups.ts +64 -0
  70. package/src/hooks/useFireCMSContext.tsx +6 -2
  71. package/src/hooks/useInternalUserManagementController.tsx +16 -0
  72. package/src/preview/PropertyPreview.tsx +8 -0
  73. package/src/preview/components/ReferencePreview.tsx +4 -2
  74. package/src/preview/components/UserPreview.tsx +27 -0
  75. package/src/preview/index.ts +1 -0
  76. package/src/preview/property_previews/ArrayPropertyPreview.tsx +1 -1
  77. package/src/preview/property_previews/MapPropertyPreview.tsx +2 -2
  78. package/src/preview/property_previews/NumberPropertyPreview.tsx +2 -2
  79. package/src/types/collections.ts +14 -0
  80. package/src/types/entities.ts +7 -1
  81. package/src/types/firecms.tsx +16 -0
  82. package/src/types/firecms_context.tsx +17 -0
  83. package/src/types/index.ts +1 -0
  84. package/src/types/internal_user_management.ts +24 -0
  85. package/src/types/plugins.tsx +3 -0
  86. package/src/types/properties.ts +45 -6
  87. package/src/types/property_config.tsx +1 -0
  88. package/src/types/user.ts +1 -1
  89. package/src/util/collections.ts +8 -0
  90. package/src/util/createFormexStub.tsx +4 -0
  91. package/src/util/entities.ts +1 -1
  92. package/src/util/entity_cache.ts +72 -53
  93. package/src/util/join_collections.ts +3 -3
  94. package/src/util/make_properties_editable.ts +0 -22
  95. package/src/util/objects.ts +40 -2
  96. package/src/util/useStorageUploadController.tsx +71 -34
@@ -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
- // If source value is an array, create a shallow copy of the array.
51
- (output as any)[key] = [...sourceValue];
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 Resizer from "react-image-file-resizer";
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
- ImageCompression,
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
- const compression: ImageCompression | undefined = storage?.imageCompression;
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 (compression && compressionFormat(file)) {
197
- file = await resizeAndCompressImage(file, compression)
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 (compression && compressionFormat(file)) {
211
- file = await resizeAndCompressImage(file, compression)
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
- const supportedTypes: Record<string, string> = {
280
- "image/jpeg": "JPEG",
281
- "image/png": "PNG",
282
- "image/webp": "WEBP"
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
- const inputQuality = compression.quality === undefined ? defaultQuality : compression.quality;
290
- const quality = inputQuality >= 0 ? inputQuality <= 100 ? inputQuality : 100 : 100;
291
-
292
- const format = compressionFormat(file);
293
- if (!format) {
294
- throw Error("resizeAndCompressImage: Unsupported image format");
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
- Resizer.imageFileResizer(
297
- file,
298
- compression.maxWidth || Number.MAX_VALUE,
299
- compression.maxHeight || Number.MAX_VALUE,
300
- format,
301
- quality,
302
- 0,
303
- (file: string | Blob | File | ProgressEvent<FileReader>) => resolve(file as File),
304
- "file"
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
+ }