@firecms/core 3.0.0-beta.13 → 3.0.0-beta.15

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