@firecms/core 3.0.0-beta.12 → 3.0.0-beta.14

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 (120) hide show
  1. package/dist/app/Scaffold.d.ts +4 -0
  2. package/dist/components/ArrayContainer.d.ts +13 -11
  3. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +4 -0
  4. package/dist/components/EntityJsonPreview.d.ts +3 -0
  5. package/dist/components/EntityPreview.d.ts +4 -2
  6. package/dist/components/SelectableTable/SelectableTable.d.ts +1 -1
  7. package/dist/components/VirtualTable/VirtualTableProps.d.ts +0 -4
  8. package/dist/components/index.d.ts +1 -0
  9. package/dist/core/DefaultAppBar.d.ts +8 -2
  10. package/dist/core/EntityEditView.d.ts +8 -1
  11. package/dist/core/FireCMS.d.ts +2 -2
  12. package/dist/form/EntityForm.d.ts +5 -1
  13. package/dist/form/PropertyFieldBinding.d.ts +1 -1
  14. package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
  15. package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +3 -9
  16. package/dist/hooks/data/save.d.ts +1 -1
  17. package/dist/hooks/useBuildNavigationController.d.ts +2 -9
  18. package/dist/index.es.js +9251 -8721
  19. package/dist/index.es.js.map +1 -1
  20. package/dist/index.umd.js +16686 -16160
  21. package/dist/index.umd.js.map +1 -1
  22. package/dist/internal/useBuildDataSource.d.ts +3 -2
  23. package/dist/internal/useBuildSideEntityController.d.ts +3 -3
  24. package/dist/types/collections.d.ts +26 -2
  25. package/dist/types/fields.d.ts +4 -1
  26. package/dist/types/firecms.d.ts +3 -2
  27. package/dist/types/navigation.d.ts +9 -0
  28. package/dist/types/plugins.d.ts +13 -0
  29. package/dist/types/properties.d.ts +22 -1
  30. package/dist/types/side_entity_controller.d.ts +4 -0
  31. package/dist/util/callbacks.d.ts +2 -0
  32. package/dist/util/createFormexStub.d.ts +2 -0
  33. package/dist/util/index.d.ts +1 -0
  34. package/dist/util/navigation_utils.d.ts +2 -1
  35. package/dist/util/objects.d.ts +1 -1
  36. package/dist/util/property_utils.d.ts +2 -2
  37. package/dist/util/references.d.ts +2 -2
  38. package/dist/util/resolutions.d.ts +11 -6
  39. package/package.json +9 -6
  40. package/src/app/Scaffold.tsx +13 -3
  41. package/src/components/ArrayContainer.tsx +414 -282
  42. package/src/components/ClearFilterSortButton.tsx +1 -1
  43. package/src/components/ConfirmationDialog.tsx +9 -9
  44. package/src/components/DeleteEntityDialog.tsx +4 -2
  45. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +8 -6
  46. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +1 -1
  47. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +1 -1
  48. package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +13 -2
  49. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +58 -29
  50. package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +9 -9
  51. package/src/components/EntityCollectionView/EntityCollectionView.tsx +26 -6
  52. package/src/components/EntityJsonPreview.tsx +66 -0
  53. package/src/components/EntityPreview.tsx +27 -16
  54. package/src/components/EntityView.tsx +4 -1
  55. package/src/components/ErrorView.tsx +1 -1
  56. package/src/components/HomePage/DefaultHomePage.tsx +2 -1
  57. package/src/components/HomePage/NavigationCardBinding.tsx +3 -1
  58. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +3 -1
  59. package/src/components/SelectableTable/SelectableTable.tsx +140 -143
  60. package/src/components/VirtualTable/VirtualTable.tsx +37 -31
  61. package/src/components/VirtualTable/VirtualTableProps.tsx +0 -5
  62. package/src/components/VirtualTable/fields/VirtualTableInput.tsx +0 -1
  63. package/src/components/common/default_entity_actions.tsx +15 -4
  64. package/src/components/index.tsx +2 -0
  65. package/src/core/DefaultAppBar.tsx +17 -5
  66. package/src/core/EntityEditView.tsx +135 -47
  67. package/src/core/EntitySidePanel.tsx +15 -20
  68. package/src/core/FireCMS.tsx +19 -12
  69. package/src/form/EntityForm.tsx +32 -24
  70. package/src/form/PropertyFieldBinding.tsx +8 -6
  71. package/src/form/components/CustomIdField.tsx +3 -1
  72. package/src/form/components/LabelWithIcon.tsx +1 -1
  73. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +4 -1
  74. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +4 -2
  75. package/src/form/field_bindings/BlockFieldBinding.tsx +1 -1
  76. package/src/form/field_bindings/KeyValueFieldBinding.tsx +1 -1
  77. package/src/form/field_bindings/MapFieldBinding.tsx +5 -4
  78. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +6 -2
  79. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +3 -3
  80. package/src/form/field_bindings/RepeatFieldBinding.tsx +9 -2
  81. package/src/form/field_bindings/SelectFieldBinding.tsx +3 -2
  82. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +227 -156
  83. package/src/hooks/data/save.ts +24 -35
  84. package/src/hooks/useBuildNavigationController.tsx +42 -17
  85. package/src/hooks/useFireCMSContext.tsx +0 -30
  86. package/src/internal/useBuildDataSource.ts +9 -5
  87. package/src/internal/useBuildSideEntityController.tsx +26 -20
  88. package/src/preview/PropertyPreview.tsx +4 -2
  89. package/src/preview/components/ImagePreview.tsx +2 -2
  90. package/src/preview/components/ReferencePreview.tsx +1 -1
  91. package/src/preview/property_previews/ArrayOfMapsPreview.tsx +5 -4
  92. package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +4 -3
  93. package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +4 -2
  94. package/src/preview/property_previews/ArrayOfStringsPreview.tsx +4 -3
  95. package/src/preview/property_previews/ArrayOneOfPreview.tsx +4 -2
  96. package/src/preview/property_previews/ArrayPropertyPreview.tsx +4 -2
  97. package/src/preview/property_previews/MapPropertyPreview.tsx +1 -1
  98. package/src/preview/property_previews/SkeletonPropertyComponent.tsx +1 -1
  99. package/src/preview/property_previews/StringPropertyPreview.tsx +1 -1
  100. package/src/routes/FireCMSRoute.tsx +15 -4
  101. package/src/types/collections.ts +31 -2
  102. package/src/types/fields.tsx +5 -1
  103. package/src/types/firecms.tsx +3 -3
  104. package/src/types/navigation.ts +11 -0
  105. package/src/types/plugins.tsx +17 -0
  106. package/src/types/properties.ts +27 -1
  107. package/src/types/side_entity_controller.tsx +5 -0
  108. package/src/util/callbacks.ts +119 -0
  109. package/src/util/createFormexStub.tsx +62 -0
  110. package/src/util/index.ts +1 -0
  111. package/src/util/join_collections.ts +3 -1
  112. package/src/util/navigation_from_path.ts +5 -1
  113. package/src/util/navigation_utils.ts +84 -20
  114. package/src/util/objects.ts +54 -17
  115. package/src/util/property_utils.tsx +7 -3
  116. package/src/util/references.ts +8 -6
  117. package/src/util/resolutions.ts +17 -9
  118. package/src/util/useStorageUploadController.tsx +21 -2
  119. package/dist/components/EntityCollectionTable/internal/popup_field/ElementResizeListener.d.ts +0 -5
  120. package/src/components/EntityCollectionTable/internal/popup_field/ElementResizeListener.tsx +0 -59
@@ -1,7 +1,6 @@
1
- import React, { useCallback } from "react";
1
+ import React, { useCallback, useState } from "react";
2
2
 
3
3
  import {
4
- ArrayProperty,
5
4
  FieldProps,
6
5
  PropertyOrBuilder,
7
6
  ResolvedArrayProperty,
@@ -13,8 +12,24 @@ import { PreviewSize } from "../../preview";
13
12
  import { FieldHelperText, LabelWithIconAndTooltip } from "../components";
14
13
 
15
14
  import { getIconForProperty, isReadOnly, resolveProperty } from "../../util";
16
- import { useSnackbarController, useStorageSource } from "../../hooks";
17
- import { DragDropContext, Draggable, Droppable } from "@hello-pangea/dnd";
15
+ import { useAuthController, useSnackbarController, useStorageSource } from "../../hooks";
16
+ import {
17
+ closestCenter,
18
+ DndContext,
19
+ DragEndEvent,
20
+ DragStartEvent,
21
+ KeyboardSensor,
22
+ PointerSensor,
23
+ useSensor,
24
+ useSensors
25
+ } from "@dnd-kit/core";
26
+ import {
27
+ horizontalListSortingStrategy,
28
+ SortableContext,
29
+ sortableKeyboardCoordinates,
30
+ useSortable
31
+ } from "@dnd-kit/sortable";
32
+ import { CSS } from "@dnd-kit/utilities";
18
33
  import { StorageFieldItem, useStorageUploadController } from "../../util/useStorageUploadController";
19
34
  import { StorageUploadProgress } from "../components/StorageUploadProgress";
20
35
  import { StorageItemPreview } from "../components/StorageItemPreview";
@@ -36,13 +51,6 @@ const rejectDropClasses = "transition-colors duration-200 ease-[cubic-bezier(0,0
36
51
 
37
52
  type StorageUploadFieldProps = FieldProps<string | string[]>;
38
53
 
39
- /**
40
- * Field that allows to upload files to Google Cloud Storage.
41
- *
42
- * This is one of the internal components that get mapped natively inside forms
43
- * and tables to the specified properties.
44
- * @group Form fields
45
- */
46
54
  export function StorageUploadFieldBinding({
47
55
  propertyKey,
48
56
  value,
@@ -57,11 +65,13 @@ export function StorageUploadFieldBinding({
57
65
  isSubmitting,
58
66
  }: StorageUploadFieldProps) {
59
67
 
68
+ const authController = useAuthController();
69
+
60
70
  if (!context.entityId)
61
71
  throw new Error("StorageUploadFieldBinding: Entity id is null");
62
72
 
63
73
  const storageSource = useStorageSource(context.collection);
64
- const disabled = isReadOnly(property) || !!property.disabled || isSubmitting;
74
+ const disabled = isReadOnly(property) || !!property.disabled || isSubmitting || context.disabled;
65
75
 
66
76
  const {
67
77
  internalValue,
@@ -91,6 +101,7 @@ export function StorageUploadFieldBinding({
91
101
 
92
102
  const resolvedProperty = resolveProperty({
93
103
  propertyOrBuilder: property as PropertyOrBuilder,
104
+ authController
94
105
  }) as ResolvedStringProperty | ResolvedArrayProperty<string[]>;
95
106
 
96
107
  return (
@@ -129,13 +140,104 @@ export function StorageUploadFieldBinding({
129
140
  );
130
141
  }
131
142
 
143
+ interface SortableStorageItemProps {
144
+ id: number;
145
+ entry: StorageFieldItem;
146
+ property: ResolvedStringProperty;
147
+ name: string;
148
+ metadata?: Record<string, unknown>;
149
+ storagePathBuilder: (file: File) => string;
150
+ onFileUploadComplete: (uploadedPath: string, entry: StorageFieldItem, fileMetadata?: any) => Promise<void>;
151
+ onClear: (clearedStoragePathOrDownloadUrl: string) => void;
152
+ disabled: boolean;
153
+ isSortable: boolean; // Kept for consistency, though dnd-kit handles sortability via context
154
+ }
155
+
156
+ function SortableStorageItem({
157
+ id,
158
+ entry,
159
+ property,
160
+ name,
161
+ metadata,
162
+ storagePathBuilder,
163
+ onFileUploadComplete,
164
+ onClear,
165
+ disabled,
166
+ isSortable // This prop might be redundant if SortableContext is always used for multiple items
167
+ }: SortableStorageItemProps) {
168
+
169
+ const {
170
+ attributes,
171
+ listeners,
172
+ setNodeRef,
173
+ transform,
174
+ transition,
175
+ isDragging
176
+ } = useSortable({ id });
177
+
178
+ const style: React.CSSProperties = {
179
+ transform: CSS.Transform.toString(transform),
180
+ transition,
181
+ zIndex: isDragging ? 100 : undefined, // Higher z-index when dragging
182
+ opacity: isDragging ? 0.8 : 1 // Slight opacity for dragged item
183
+ };
184
+
185
+ const getImageSizeNumber = (previewSize: PreviewSize): number => {
186
+ switch (previewSize) {
187
+ case "small":
188
+ return 40;
189
+ case "medium":
190
+ return 118; // As per original logic for multiple items
191
+ case "large":
192
+ return 220; // As per original logic for single item
193
+ default:
194
+ return 118;
195
+ }
196
+ };
197
+
198
+ let child: React.ReactNode;
199
+ if (entry.storagePathOrDownloadUrl) {
200
+ child = (
201
+ <StorageItemPreview
202
+ name={`storage_preview_${entry.storagePathOrDownloadUrl}`}
203
+ property={property}
204
+ disabled={disabled}
205
+ value={entry.storagePathOrDownloadUrl}
206
+ onRemove={() => onClear(entry.storagePathOrDownloadUrl!)}
207
+ size={entry.size}/>
208
+ );
209
+ } else if (entry.file) {
210
+ child = (
211
+ <StorageUploadProgress
212
+ entry={entry}
213
+ metadata={metadata}
214
+ storagePath={storagePathBuilder(entry.file)}
215
+ onFileUploadComplete={onFileUploadComplete}
216
+ imageSize={getImageSizeNumber(entry.size)}
217
+ simple={false}
218
+ />
219
+ );
220
+ }
221
+
222
+ return (
223
+ <div
224
+ ref={setNodeRef}
225
+ style={style}
226
+ {...attributes}
227
+ {...listeners}
228
+ className={cls("rounded-md m-1")} // Added margin for spacing between items
229
+ tabIndex={-1}
230
+ >
231
+ {child}
232
+ </div>
233
+ );
234
+ }
235
+
132
236
  function FileDropComponent({
133
237
  storage,
134
238
  disabled,
135
- isDraggingOver,
136
239
  onFilesAdded,
137
240
  multipleFilesSupported,
138
- droppableProvided,
139
241
  autoFocus,
140
242
  internalValue,
141
243
  property,
@@ -143,26 +245,24 @@ function FileDropComponent({
143
245
  metadata,
144
246
  storagePathBuilder,
145
247
  onFileUploadComplete,
146
- size,
147
248
  name,
148
- helpText
249
+ helpText,
250
+ isDndItemDragging // New prop to disable dropzone when internal D&D is active
149
251
  }: {
150
252
  storage: StorageConfig,
151
253
  disabled: boolean,
152
- isDraggingOver: boolean,
153
- droppableProvided: any,
154
- onFilesAdded: (acceptedFiles: File[]) => void,
254
+ onFilesAdded: (acceptedFiles: File[]) => Promise<void>, // useStorageUploadController returns Promise<void>
155
255
  multipleFilesSupported: boolean,
156
256
  autoFocus: boolean,
157
257
  internalValue: StorageFieldItem[],
158
258
  property: ResolvedStringProperty,
159
259
  onClear: (clearedStoragePathOrDownloadUrl: string) => void,
160
- metadata: any,
260
+ metadata?: any,
161
261
  storagePathBuilder: (file: File) => string,
162
262
  onFileUploadComplete: (uploadedPath: string, entry: StorageFieldItem, fileMetadata?: any) => Promise<void>,
163
- size: PreviewSize,
164
263
  name: string,
165
- helpText: string
264
+ helpText: string,
265
+ isDndItemDragging?: boolean
166
266
  }) {
167
267
 
168
268
  const snackbarContext = useSnackbarController();
@@ -170,16 +270,19 @@ function FileDropComponent({
170
270
  const {
171
271
  getRootProps,
172
272
  getInputProps,
173
- isDragActive,
273
+ isDragActive, // This is for files dragged from OS
174
274
  isDragAccept,
175
275
  isDragReject
176
276
  } = useDropzone({
177
- accept: storage.acceptedFiles ? storage.acceptedFiles.map(e => ({ [e]: [] })).reduce((a, b) => ({ ...a, ...b }), {}) : undefined,
178
- disabled: disabled || isDraggingOver,
277
+ accept: storage.acceptedFiles ? storage.acceptedFiles.reduce((acc, ext) => ({
278
+ ...acc,
279
+ [ext]: []
280
+ }), {}) : undefined,
281
+ disabled: disabled || isDndItemDragging, // Disable if form field is disabled OR an internal item is being dragged
179
282
  noDragEventsBubbling: true,
180
283
  maxSize: storage.maxSize,
181
284
  onDrop: onFilesAdded,
182
- onDropRejected: (fileRejections, event) => {
285
+ onDropRejected: (fileRejections) => {
183
286
  for (const fileRejection of fileRejections) {
184
287
  for (const error of fileRejection.errors) {
185
288
  console.error("Error uploading file: ", error);
@@ -208,79 +311,44 @@ function FileDropComponent({
208
311
  disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin,
209
312
  disabled ? "text-surface-accent-600 dark:text-surface-accent-500" : "",
210
313
  dropZoneClasses,
211
- multipleFilesSupported && internalValue.length ? "" : "flex",
314
+ multipleFilesSupported && internalValue.length === 0 && "flex", // Keep flex for empty state centering
212
315
  {
213
316
  [nonActiveDropClasses]: !isDragActive,
214
- [activeDropClasses]: isDragActive,
215
- [rejectDropClasses]: isDragReject,
216
- [acceptDropClasses]: isDragAccept,
217
- [disabledClasses]: disabled
317
+ [activeDropClasses]: isDragActive, // OS file drag active
318
+ [rejectDropClasses]: isDragReject, // OS file drag reject
319
+ [acceptDropClasses]: isDragAccept, // OS file drag accept
320
+ [disabledClasses]: disabled || isDndItemDragging // Visually disable if internal drag
218
321
  })}
219
322
  >
220
323
  <div
221
- {...droppableProvided.droppableProps}
222
- ref={droppableProvided.innerRef}
223
324
  className={cls("flex items-center p-1 no-scrollbar",
224
- multipleFilesSupported && internalValue.length ? "overflow-auto" : "",
225
- multipleFilesSupported && internalValue.length ? "min-h-[180px]" : "min-h-[250px]"
325
+ multipleFilesSupported && internalValue.length ? "flex-row overflow-x-auto" : "flex-col", // flex-col for single or empty
326
+ internalValue.length === 0 && "min-h-[250px] justify-center", // Centering for empty dropzone
327
+ multipleFilesSupported && internalValue.length > 0 && "min-h-[180px]", // Min height for multiple items
328
+ !multipleFilesSupported && internalValue.length > 0 && "min-h-[250px]" // Min height for single item
226
329
  )}
227
330
  >
228
-
229
331
  <input
230
332
  autoFocus={autoFocus}
231
333
  {...getInputProps()} />
232
334
 
233
- {internalValue.map((entry, index) => {
234
- let child: any;
235
- if (entry.storagePathOrDownloadUrl) {
236
- child = (
237
- <StorageItemPreview
238
- name={`storage_preview_${entry.storagePathOrDownloadUrl}`}
239
- property={property}
240
- disabled={disabled}
241
- value={entry.storagePathOrDownloadUrl}
242
- onRemove={onClear}
243
- size={entry.size}/>
244
- );
245
- } else if (entry.file) {
246
- child = (
247
- <StorageUploadProgress
248
- entry={entry}
249
- metadata={metadata}
250
- storagePath={storagePathBuilder(entry.file)}
251
- onFileUploadComplete={onFileUploadComplete}
252
- imageSize={size === "large" ? 220 : 118}
253
- simple={false}
254
- />
255
- );
256
- }
257
-
258
- return (
259
- <Draggable
260
- key={`array_field_${name}_${entry.id}`}
261
- draggableId={`array_field_${name}_${entry.id}`}
262
- index={index}>
263
- {(provided, snapshot) => (
264
- <div
265
- tabIndex={-1}
266
- ref={provided.innerRef}
267
- {...provided.draggableProps}
268
- {...provided.dragHandleProps}
269
- className={cls("rounded-md")}
270
- style={{
271
- ...provided.draggableProps.style
272
- }}
273
- >
274
- {child}
275
- </div>
276
- )}
277
- </Draggable>
278
- );
279
- })
280
- }
281
-
282
- {droppableProvided.placeholder}
283
-
335
+ {internalValue.map((entry) => (
336
+ <SortableStorageItem
337
+ key={entry.id}
338
+ id={entry.id}
339
+ entry={entry}
340
+ property={property}
341
+ name={name}
342
+ metadata={metadata}
343
+ storagePathBuilder={storagePathBuilder}
344
+ onFileUploadComplete={onFileUploadComplete}
345
+ onClear={onClear}
346
+ disabled={disabled}
347
+ isSortable={multipleFilesSupported}
348
+ />
349
+ ))}
350
+
351
+ {/* Placeholder for empty dropzone text is handled by the outer Typography */}
284
352
  </div>
285
353
 
286
354
  <div
@@ -291,7 +359,6 @@ function FileDropComponent({
291
359
  {helpText}
292
360
  </Typography>
293
361
  </div>
294
-
295
362
  </div>
296
363
  );
297
364
  }
@@ -306,7 +373,7 @@ export interface StorageUploadProps {
306
373
  autoFocus: boolean;
307
374
  disabled: boolean;
308
375
  storage: StorageConfig;
309
- onFilesAdded: (acceptedFiles: File[]) => void;
376
+ onFilesAdded: (acceptedFiles: File[]) => Promise<void>; // Updated from useStorageUploadController
310
377
  storagePathBuilder: (file: File) => string;
311
378
  onFileUploadComplete: (uploadedPath: string, entry: StorageFieldItem, fileMetadata?: any) => Promise<void>;
312
379
  }
@@ -314,7 +381,7 @@ export interface StorageUploadProps {
314
381
  export function StorageUpload({
315
382
  property,
316
383
  name,
317
- value,
384
+ value, // This is internalValue from useStorageUploadController
318
385
  setInternalValue,
319
386
  onChange,
320
387
  multipleFilesSupported,
@@ -341,10 +408,10 @@ export function StorageUpload({
341
408
  }
342
409
 
343
410
  const metadata: Record<string, unknown> | undefined = storage?.metadata;
344
- const size = multipleFilesSupported ? "medium" : "large";
411
+ const [isDndItemDragging, setIsDndItemDragging] = useState(false);
345
412
 
346
413
  const moveItem = useCallback((fromIndex: number, toIndex: number) => {
347
- if (!multipleFilesSupported) return;
414
+ if (!multipleFilesSupported || fromIndex === toIndex) return;
348
415
  const newValue = [...value];
349
416
  const item = newValue[fromIndex];
350
417
  newValue.splice(fromIndex, 1);
@@ -356,84 +423,88 @@ export function StorageUpload({
356
423
  onChange(fieldValue);
357
424
  }, [multipleFilesSupported, onChange, setInternalValue, value]);
358
425
 
359
- const onDragEnd = useCallback((result: any) => {
360
- // dropped outside the list
361
- if (!result.destination) {
362
- return;
363
- }
364
-
365
- moveItem(result.source.index, result.destination.index);
426
+ const sensors = useSensors(
427
+ useSensor(PointerSensor, {
428
+ activationConstraint: {
429
+ distance: 5, // Start dragging after 5px movement
430
+ },
431
+ }),
432
+ useSensor(KeyboardSensor, {
433
+ coordinateGetter: sortableKeyboardCoordinates,
434
+ })
435
+ );
366
436
 
367
- }, [moveItem])
437
+ const handleDragStart = useCallback((event: DragStartEvent) => {
438
+ setIsDndItemDragging(true);
439
+ }, []);
440
+
441
+ const handleDragEnd = useCallback((event: DragEndEvent) => {
442
+ setIsDndItemDragging(false);
443
+ const {
444
+ active,
445
+ over
446
+ } = event;
447
+ if (over && active.id !== over.id) {
448
+ const oldIndex = value.findIndex(item => item.id === active.id);
449
+ const newIndex = value.findIndex(item => item.id === over.id);
450
+ if (oldIndex !== -1 && newIndex !== -1) {
451
+ moveItem(oldIndex, newIndex);
452
+ }
453
+ }
454
+ }, [value, moveItem]);
368
455
 
369
456
  const onClear = useCallback((clearedStoragePathOrDownloadUrl: string) => {
457
+ let newValue: StorageFieldItem[];
370
458
  if (multipleFilesSupported) {
371
- const newValue: StorageFieldItem[] = value.filter(v => v.storagePathOrDownloadUrl !== clearedStoragePathOrDownloadUrl);
459
+ newValue = value.filter(v => v.storagePathOrDownloadUrl !== clearedStoragePathOrDownloadUrl);
372
460
  onChange(newValue.filter(v => !!v.storagePathOrDownloadUrl).map(v => v.storagePathOrDownloadUrl as string));
373
- setInternalValue(newValue);
374
461
  } else {
462
+ newValue = [];
375
463
  onChange(null);
376
- setInternalValue([]);
377
464
  }
378
- }, [value, multipleFilesSupported, onChange]);
465
+ setInternalValue(newValue);
466
+ }, [value, multipleFilesSupported, onChange, setInternalValue]);
379
467
 
380
468
  const helpText = multipleFilesSupported
381
- ? "Drag 'n' drop some files here, or click to select files"
469
+ ? "Drag 'n' drop some files here, or click to select files. Drag to reorder."
382
470
  : "Drag 'n' drop a file here, or click to select one";
383
471
 
384
472
  const renderProperty: ResolvedStringProperty = multipleFilesSupported
385
- ? (property as ArrayProperty<string[]>).of as ResolvedStringProperty
473
+ ? (property as ResolvedArrayProperty<string[]>).of as ResolvedStringProperty
386
474
  : property as ResolvedStringProperty;
387
475
 
388
- return (
389
- <DragDropContext onDragEnd={onDragEnd}>
390
- <Droppable
391
- droppableId={`droppable_${name}`}
392
- direction="horizontal"
393
- renderClone={(provided, snapshot, rubric) => {
394
- const entry = value[rubric.source.index];
395
- return (
396
- <div
397
- ref={provided.innerRef}
398
- {...provided.draggableProps}
399
- {...provided.dragHandleProps}
400
- style={
401
- provided.draggableProps.style
402
- }
403
- className="rounded"
404
- >
405
- <StorageItemPreview
406
- name={`storage_preview_${entry.storagePathOrDownloadUrl}`}
407
- placeholder={true}
408
- property={renderProperty}
409
- disabled={true}
410
- value={entry.storagePathOrDownloadUrl as string}
411
- onRemove={onClear}
412
- size={entry.size}/>
413
- </div>
414
- );
415
- }}
416
- >
417
- {(provided, snapshot) => {
418
- return <FileDropComponent storage={storage}
419
- disabled={disabled}
420
- isDraggingOver={snapshot.isDraggingOver}
421
- droppableProvided={provided}
422
- onFilesAdded={onFilesAdded}
423
- multipleFilesSupported={multipleFilesSupported}
424
- autoFocus={autoFocus}
425
- internalValue={value}
426
- property={renderProperty}
427
- onClear={onClear}
428
- metadata={metadata}
429
- storagePathBuilder={storagePathBuilder}
430
- onFileUploadComplete={onFileUploadComplete}
431
- size={size}
432
- name={name}
433
- helpText={helpText}/>
434
- }}
435
- </Droppable>
436
- </DragDropContext>
437
- );
476
+ const fileDropProps = {
477
+ storage,
478
+ disabled,
479
+ onFilesAdded,
480
+ multipleFilesSupported,
481
+ autoFocus,
482
+ internalValue: value, // Pass current internalValue
483
+ property: renderProperty,
484
+ onClear,
485
+ metadata,
486
+ storagePathBuilder,
487
+ onFileUploadComplete,
488
+ name,
489
+ helpText,
490
+ isDndItemDragging // Pass this down
491
+ };
438
492
 
493
+ if (multipleFilesSupported) {
494
+ return (
495
+ <DndContext
496
+ sensors={sensors}
497
+ collisionDetection={closestCenter}
498
+ onDragStart={handleDragStart}
499
+ onDragEnd={handleDragEnd}
500
+ >
501
+ <SortableContext items={value.map(v => v.id)} strategy={horizontalListSortingStrategy}>
502
+ <FileDropComponent {...fileDropProps} />
503
+ </SortableContext>
504
+ </DndContext>
505
+ );
506
+ } else {
507
+ // For single file, no D&D context is needed
508
+ return <FileDropComponent {...fileDropProps} isDndItemDragging={false}/>;
509
+ }
439
510
  }
@@ -1,12 +1,4 @@
1
- import {
2
- DataSource,
3
- Entity,
4
- EntityCollection,
5
- EntityValues,
6
- FireCMSContext,
7
- SaveEntityProps,
8
- User
9
- } from "../../types";
1
+ import { DataSource, Entity, EntityCollection, EntityValues, FireCMSContext, SaveEntityProps, User } from "../../types";
10
2
  import { useDataSource } from "./useDataSource";
11
3
  import { resolveCollection } from "../../util";
12
4
 
@@ -49,23 +41,23 @@ export type SaveEntityWithCallbacksProps<M extends Record<string, any>> =
49
41
  * @group Hooks and utilities
50
42
  */
51
43
  export async function saveEntityWithCallbacks<M extends Record<string, any>, USER extends User>({
52
- collection,
53
- path,
54
- entityId,
55
- values,
56
- previousValues,
57
- status,
58
- dataSource,
59
- context,
60
- onSaveSuccess,
61
- onSaveFailure,
62
- onPreSaveHookError,
63
- onSaveSuccessHookError
64
- }: SaveEntityWithCallbacksProps<M> & {
65
- collection: EntityCollection<M, USER>,
66
- dataSource: DataSource,
67
- context: FireCMSContext<USER>,
68
- }
44
+ collection,
45
+ path,
46
+ entityId,
47
+ values,
48
+ previousValues,
49
+ status,
50
+ dataSource,
51
+ context,
52
+ onSaveSuccess,
53
+ onSaveFailure,
54
+ onPreSaveHookError,
55
+ onSaveSuccessHookError
56
+ }: SaveEntityWithCallbacksProps<M> & {
57
+ collection: EntityCollection<M, USER>,
58
+ dataSource: DataSource,
59
+ context: FireCMSContext,
60
+ }
69
61
  ): Promise<void> {
70
62
 
71
63
  if (status !== "new" && !entityId) {
@@ -85,7 +77,8 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
85
77
  path,
86
78
  values: previousValues as EntityValues<M>,
87
79
  entityId,
88
- propertyConfigs: customizationController.propertyConfigs
80
+ propertyConfigs: customizationController.propertyConfigs,
81
+ authController: context.authController
89
82
  });
90
83
  updatedValues = await callbacks.onPreSave({
91
84
  collection: resolvedCollection,
@@ -107,11 +100,6 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
107
100
  updatedValues = values;
108
101
  }
109
102
 
110
- console.debug("Saving entity", {
111
- entityId,
112
- updatedValues,
113
- collection
114
- });
115
103
  return dataSource.saveEntity({
116
104
  collection,
117
105
  path: resolvedPath,
@@ -127,7 +115,8 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
127
115
  path,
128
116
  values: updatedValues as EntityValues<M>,
129
117
  entityId,
130
- propertyConfigs: customizationController.propertyConfigs
118
+ propertyConfigs: customizationController.propertyConfigs,
119
+ authController: context.authController
131
120
  });
132
121
  callbacks.onSaveSuccess({
133
122
  collection: resolvedCollection,
@@ -148,7 +137,6 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
148
137
  onSaveSuccess(entity);
149
138
  })
150
139
  .catch((e) => {
151
- console.error("!!!", e);
152
140
  if (callbacks?.onSaveFailure) {
153
141
 
154
142
  const resolvedCollection = resolveCollection<M>({
@@ -156,7 +144,8 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
156
144
  path,
157
145
  values: updatedValues as EntityValues<M>,
158
146
  entityId,
159
- propertyConfigs: customizationController.propertyConfigs
147
+ propertyConfigs: customizationController.propertyConfigs,
148
+ authController: context.authController
160
149
  });
161
150
  callbacks.onSaveFailure({
162
151
  collection: resolvedCollection,