@firecms/core 3.0.0-canary.242 → 3.0.0-canary.244

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.
@@ -114,6 +114,7 @@ export function DefaultHomePage({
114
114
  );
115
115
  })}
116
116
  </>;
117
+
117
118
  additionalPluginChildrenStart = <div className={"flex flex-col gap-2"}>
118
119
  {customizationController.plugins.filter(plugin => plugin.homePage?.additionalChildrenStart)
119
120
  .map((plugin, i) => {
@@ -115,7 +115,6 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
115
115
  endAdornment,
116
116
  AddColumnComponent,
117
117
  initialScroll = 0,
118
- debug
119
118
  }: VirtualTableProps<T>) {
120
119
 
121
120
  const sortByProperty: string | undefined = sortBy ? sortBy[0] : undefined;
@@ -171,7 +170,6 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
171
170
  });
172
171
 
173
172
  const onColumnResizeInternal = useCallback((params: OnVirtualTableColumnResizeParams) => {
174
- if (debug) console.log("onColumnResizeInternal", params);
175
173
  setColumns(prevColumns =>
176
174
  prevColumns.map((column) =>
177
175
  column.key === params.column.key ? params.column : column
@@ -180,8 +178,6 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
180
178
  }, []);
181
179
 
182
180
  const onColumnResizeEndInternal = useCallback((params: OnVirtualTableColumnResizeParams) => {
183
- if (debug)
184
- console.log("onColumnResizeEndInternal", params);
185
181
  setColumns(columns.map((column) => column.key === params.column.key ? params.column : column));
186
182
  if (onColumnResize) {
187
183
  onColumnResize(params);
@@ -192,14 +188,10 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
192
188
  const filterRef = useRef<VirtualTableFilterValues<any> | undefined>();
193
189
 
194
190
  useEffect(() => {
195
- if (debug)
196
- console.log("Filter updated", filterInput);
197
191
  filterRef.current = filterInput;
198
192
  }, [filterInput]);
199
193
 
200
194
  const scrollToTop = useCallback(() => {
201
- if (debug)
202
- console.log("scrollToTop");
203
195
  endReachCallbackThreshold.current = 0;
204
196
  if (tableRef.current) {
205
197
  tableRef.current.scrollTo(tableRef.current?.scrollLeft, 0);
@@ -208,9 +200,6 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
208
200
 
209
201
  const onColumnSort = useCallback((key: string) => {
210
202
 
211
- if (debug)
212
- console.log("onColumnSort", key);
213
-
214
203
  const isDesc = sortByProperty === key && currentSort === "desc";
215
204
  const isAsc = sortByProperty === key && currentSort === "asc";
216
205
  const newSort = isAsc ? "desc" : (isDesc ? undefined : "asc");
@@ -238,12 +227,8 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
238
227
  }, [checkFilterCombination, currentSort, onFilterUpdate, onResetPagination, onSortByUpdate, scrollToTop, sortByProperty]);
239
228
 
240
229
  const maxScroll = Math.max((data?.length ?? 0) * rowHeight - bounds.height, 0);
241
- if (debug)
242
- console.log("maxScroll", maxScroll);
243
230
 
244
231
  const onEndReachedInternal = useCallback((scrollOffset: number) => {
245
- if (debug)
246
- console.log("onEndReachedInternal", scrollOffset, endReachCallbackThreshold.current + endOffset);
247
232
  if (onEndReached && (data?.length ?? 0) > 0 && scrollOffset > endReachCallbackThreshold.current + endOffset) {
248
233
  endReachCallbackThreshold.current = scrollOffset;
249
234
  onEndReached();
@@ -259,12 +244,6 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
259
244
  scrollOffset: number,
260
245
  scrollUpdateWasRequested: boolean;
261
246
  }) => {
262
- if (debug)
263
- console.log("onScroll", {
264
- scrollDirection,
265
- scrollOffset,
266
- scrollUpdateWasRequested
267
- });
268
247
  if (onScrollProp) {
269
248
  debouncedScroll({
270
249
  scrollDirection,
@@ -277,8 +256,6 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
277
256
  }, [maxScroll, onEndReachedInternal]);
278
257
 
279
258
  const onFilterUpdateInternal = useCallback((column: VirtualTableColumn, filterForProperty?: [VirtualTableWhereFilterOp, any]) => {
280
- if (debug)
281
- console.log("onFilterUpdateInternal", column, filterForProperty);
282
259
 
283
260
  endReachCallbackThreshold.current = 0;
284
261
  const filter = filterRef.current;
@@ -341,9 +318,6 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
341
318
  AddColumnComponent
342
319
  };
343
320
 
344
- if (debug)
345
- console.log("VirtualTable render", virtualListController);
346
-
347
321
  return (
348
322
  <div
349
323
  ref={measureRef}
@@ -162,11 +162,6 @@ export interface VirtualTableProps<T extends Record<string, any>> {
162
162
  */
163
163
  initialScroll?: number;
164
164
 
165
- /**
166
- * Debug mode
167
- */
168
- debug?: boolean;
169
-
170
165
  }
171
166
 
172
167
  export type CellRendererParams<T = any> = {
@@ -255,6 +255,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
255
255
  <ErrorBoundary>
256
256
  {usedFormContext && <Builder
257
257
  collection={collection}
258
+ parentCollectionIds={parentCollectionIds}
258
259
  entity={usedEntity}
259
260
  modifiedValues={usedFormContext?.formex?.values ?? usedEntity?.values}
260
261
  formContext={usedFormContext}
@@ -74,19 +74,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
74
74
  return navigationController.getParentCollectionIds(path);
75
75
  }, [navigationController, path]);
76
76
 
77
- const collection = useMemo(() => {
78
- if (props.collection) {
79
- return props.collection;
80
- }
81
-
82
- const registryCollection = navigationController.getCollection(path);
83
- if (registryCollection) {
84
- return registryCollection;
85
- }
86
-
87
- console.error("ERROR: No collection found in path `", path, "`. Entity id: ", entityId);
88
- throw Error("ERROR: No collection found in path `" + path + "`. Make sure you have defined a collection for this path in the root navigation.");
89
- }, [navigationController, props.collection]);
77
+ const collection = props.collection ?? navigationController.getCollection(path);
90
78
 
91
79
  useEffect(() => {
92
80
  function beforeunload(e: any) {
@@ -103,7 +103,9 @@ export function CustomIdField<M extends Record<string, any>>({
103
103
  size={"large"}
104
104
  error={error}
105
105
  fullWidth={true}
106
- onValueChange={(v) => onChange(v as string)}
106
+ onValueChange={(v) => {
107
+ onChange(v as string);
108
+ }}
107
109
  {...fieldProps}
108
110
  renderValue={(option) => {
109
111
  const enumConfig = enumValues.find(e => e.id === option);
@@ -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,
@@ -132,13 +140,104 @@ 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
+ 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
+
135
236
  function FileDropComponent({
136
237
  storage,
137
238
  disabled,
138
- isDraggingOver,
139
239
  onFilesAdded,
140
240
  multipleFilesSupported,
141
- droppableProvided,
142
241
  autoFocus,
143
242
  internalValue,
144
243
  property,
@@ -146,26 +245,24 @@ function FileDropComponent({
146
245
  metadata,
147
246
  storagePathBuilder,
148
247
  onFileUploadComplete,
149
- size,
150
248
  name,
151
- helpText
249
+ helpText,
250
+ isDndItemDragging // New prop to disable dropzone when internal D&D is active
152
251
  }: {
153
252
  storage: StorageConfig,
154
253
  disabled: boolean,
155
- isDraggingOver: boolean,
156
- droppableProvided: any,
157
- onFilesAdded: (acceptedFiles: File[]) => void,
254
+ onFilesAdded: (acceptedFiles: File[]) => Promise<void>, // useStorageUploadController returns Promise<void>
158
255
  multipleFilesSupported: boolean,
159
256
  autoFocus: boolean,
160
257
  internalValue: StorageFieldItem[],
161
258
  property: ResolvedStringProperty,
162
259
  onClear: (clearedStoragePathOrDownloadUrl: string) => void,
163
- metadata: any,
260
+ metadata?: any,
164
261
  storagePathBuilder: (file: File) => string,
165
262
  onFileUploadComplete: (uploadedPath: string, entry: StorageFieldItem, fileMetadata?: any) => Promise<void>,
166
- size: PreviewSize,
167
263
  name: string,
168
- helpText: string
264
+ helpText: string,
265
+ isDndItemDragging?: boolean
169
266
  }) {
170
267
 
171
268
  const snackbarContext = useSnackbarController();
@@ -173,16 +270,19 @@ function FileDropComponent({
173
270
  const {
174
271
  getRootProps,
175
272
  getInputProps,
176
- isDragActive,
273
+ isDragActive, // This is for files dragged from OS
177
274
  isDragAccept,
178
275
  isDragReject
179
276
  } = useDropzone({
180
- accept: storage.acceptedFiles ? storage.acceptedFiles.map(e => ({ [e]: [] })).reduce((a, b) => ({ ...a, ...b }), {}) : undefined,
181
- 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
182
282
  noDragEventsBubbling: true,
183
283
  maxSize: storage.maxSize,
184
284
  onDrop: onFilesAdded,
185
- onDropRejected: (fileRejections, event) => {
285
+ onDropRejected: (fileRejections) => {
186
286
  for (const fileRejection of fileRejections) {
187
287
  for (const error of fileRejection.errors) {
188
288
  console.error("Error uploading file: ", error);
@@ -211,79 +311,44 @@ function FileDropComponent({
211
311
  disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin,
212
312
  disabled ? "text-surface-accent-600 dark:text-surface-accent-500" : "",
213
313
  dropZoneClasses,
214
- multipleFilesSupported && internalValue.length ? "" : "flex",
314
+ multipleFilesSupported && internalValue.length === 0 && "flex", // Keep flex for empty state centering
215
315
  {
216
316
  [nonActiveDropClasses]: !isDragActive,
217
- [activeDropClasses]: isDragActive,
218
- [rejectDropClasses]: isDragReject,
219
- [acceptDropClasses]: isDragAccept,
220
- [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
221
321
  })}
222
322
  >
223
323
  <div
224
- {...droppableProvided.droppableProps}
225
- ref={droppableProvided.innerRef}
226
324
  className={cls("flex items-center p-1 no-scrollbar",
227
- multipleFilesSupported && internalValue.length ? "overflow-auto" : "",
228
- 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
229
329
  )}
230
330
  >
231
-
232
331
  <input
233
332
  autoFocus={autoFocus}
234
333
  {...getInputProps()} />
235
334
 
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
-
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 */}
287
352
  </div>
288
353
 
289
354
  <div
@@ -294,7 +359,6 @@ function FileDropComponent({
294
359
  {helpText}
295
360
  </Typography>
296
361
  </div>
297
-
298
362
  </div>
299
363
  );
300
364
  }
@@ -309,7 +373,7 @@ export interface StorageUploadProps {
309
373
  autoFocus: boolean;
310
374
  disabled: boolean;
311
375
  storage: StorageConfig;
312
- onFilesAdded: (acceptedFiles: File[]) => void;
376
+ onFilesAdded: (acceptedFiles: File[]) => Promise<void>; // Updated from useStorageUploadController
313
377
  storagePathBuilder: (file: File) => string;
314
378
  onFileUploadComplete: (uploadedPath: string, entry: StorageFieldItem, fileMetadata?: any) => Promise<void>;
315
379
  }
@@ -317,7 +381,7 @@ export interface StorageUploadProps {
317
381
  export function StorageUpload({
318
382
  property,
319
383
  name,
320
- value,
384
+ value, // This is internalValue from useStorageUploadController
321
385
  setInternalValue,
322
386
  onChange,
323
387
  multipleFilesSupported,
@@ -344,10 +408,10 @@ export function StorageUpload({
344
408
  }
345
409
 
346
410
  const metadata: Record<string, unknown> | undefined = storage?.metadata;
347
- const size = multipleFilesSupported ? "medium" : "large";
411
+ const [isDndItemDragging, setIsDndItemDragging] = useState(false);
348
412
 
349
413
  const moveItem = useCallback((fromIndex: number, toIndex: number) => {
350
- if (!multipleFilesSupported) return;
414
+ if (!multipleFilesSupported || fromIndex === toIndex) return;
351
415
  const newValue = [...value];
352
416
  const item = newValue[fromIndex];
353
417
  newValue.splice(fromIndex, 1);
@@ -359,84 +423,88 @@ export function StorageUpload({
359
423
  onChange(fieldValue);
360
424
  }, [multipleFilesSupported, onChange, setInternalValue, value]);
361
425
 
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);
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
+ );
369
436
 
370
- }, [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]);
371
455
 
372
456
  const onClear = useCallback((clearedStoragePathOrDownloadUrl: string) => {
457
+ let newValue: StorageFieldItem[];
373
458
  if (multipleFilesSupported) {
374
- const newValue: StorageFieldItem[] = value.filter(v => v.storagePathOrDownloadUrl !== clearedStoragePathOrDownloadUrl);
459
+ newValue = value.filter(v => v.storagePathOrDownloadUrl !== clearedStoragePathOrDownloadUrl);
375
460
  onChange(newValue.filter(v => !!v.storagePathOrDownloadUrl).map(v => v.storagePathOrDownloadUrl as string));
376
- setInternalValue(newValue);
377
461
  } else {
462
+ newValue = [];
378
463
  onChange(null);
379
- setInternalValue([]);
380
464
  }
381
- }, [value, multipleFilesSupported, onChange]);
465
+ setInternalValue(newValue);
466
+ }, [value, multipleFilesSupported, onChange, setInternalValue]);
382
467
 
383
468
  const helpText = multipleFilesSupported
384
- ? "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."
385
470
  : "Drag 'n' drop a file here, or click to select one";
386
471
 
387
472
  const renderProperty: ResolvedStringProperty = multipleFilesSupported
388
- ? (property as ArrayProperty<string[]>).of as ResolvedStringProperty
473
+ ? (property as ResolvedArrayProperty<string[]>).of as ResolvedStringProperty
389
474
  : property as ResolvedStringProperty;
390
475
 
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
- );
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
+ };
441
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
+ }
442
510
  }