fix(uui-file-dropzone): fix multi-folder drop losing all but first folder#1339
Conversation
…fore first await DataTransferItem.webkitGetAsEntry() returns null after the first await because the browser expires the drag data store. By collecting all FileSystemEntry references in a synchronous pass first, all dropped folders are processed — not just the first one. Also removes _processFileEntry which used DataTransferItem.getAsFile() (same staleness issue), replaced by FileSystemFileEntry.file().
|
Azure Static Web Apps: Your stage site is ready! Visit it here: https://delightful-beach-055ecb503-1339.westeurope.azurestaticapps.net |
There was a problem hiding this comment.
Pull request overview
This PR fixes a browser-level data staleness bug in uui-file-dropzone where only the first dropped folder was processed when multiple folders were dragged and dropped simultaneously. The root cause is that DataTransfer.items and DataTransferItem.webkitGetAsEntry() become invalid after the first await in an async event handler.
Changes:
_getAllEntriesis rewritten to extract allFileSystemEntryrefs synchronously (before anyawait) in a Phase 1 pass, then delegates to a new_processRootEntriesfor async processing._processRootEntriesreplaces the removed_processFileEntry, usingFileSystemFileEntry.file()instead of the staleDataTransferItem.getAsFile()._getEntryreturn type broadened fromFileSystemDirectoryEntry | null→FileSystemEntry | null.- 4 new unit tests for
_processRootEntriesadded, exercising the new method directly with mockFileSystemEntryobjects.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
packages/uui-file-dropzone/lib/uui-file-dropzone.element.ts |
Core fix: refactors _getAllEntries/removes _processFileEntry/adds _processRootEntries; updates _getEntry return type |
packages/uui-file-dropzone/lib/uui-file-dropzone.test.ts |
Adds 4 unit tests for the new _processRootEntries method |
Two issues were identified:
-
Stale internal type in
_getEntry(moderate): The internaldirvariable is still typed asFileSystemDirectoryEntry | nulland the value fromwebkitGetAsEntry()is still cast asFileSystemDirectoryEntryon line 160, even though the method now correctly returnsFileSystemEntry | null. SincewebkitGetAsEntry()can return aFileSystemFileEntryfor root-level files, the cast is incorrect and should be changed toFileSystemEntry. -
Outdated JSDoc on
_getEntry(nit): The description still says "Get the directory entry" but the method now returns any filesystem entry (file or directory).
Comments suppressed due to low confidence (1)
packages/uui-file-dropzone/lib/uui-file-dropzone.element.ts:160
- The internal variable
dirin_getEntryis still typed asFileSystemDirectoryEntry | null, even though the method's return type was broadened toFileSystemEntry | null. A file entry dropped at the root level is aFileSystemFileEntry, not aFileSystemDirectoryEntry. The forced cast on line 160 (as FileSystemDirectoryEntry) is now incorrect — it should be cast toFileSystemEntryto match the updated return type. While this doesn't cause a runtime error (because the value is treated asFileSystemEntryin callers), the internal typing is misleading and the wrong cast (as FileSystemDirectoryEntry) remains on the assignment.
let dir: FileSystemDirectoryEntry | null = null;
if ('webkitGetAsEntry' in entry) {
dir = entry.webkitGetAsEntry() as FileSystemDirectoryEntry;
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
|
Azure Static Web Apps: Your stage site is ready! Visit it here: https://delightful-beach-055ecb503-1339.westeurope.azurestaticapps.net |
Includes the fix for multi-folder drop DataTransfer staleness (umbraco/Umbraco.UI#1339).
…r drag-and-drop failing (closes #21837) (#21886) * fix(media): ensure sequential creation in media drag-and-drop When multiple folders are dragged into the Media section, the creation handlers (#handleFile/#handleFolder) were not awaited in the batch loop. This caused child items to attempt server operations before their parent folders were fully created, resulting in 404 errors for subsequent items. Adding await ensures each item is fully created before the next is processed, which is required because child items in the flat list reference parent folder IDs that must exist on the server. Closes #21837 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Task: Bump @umbraco-ui/uui to 1.17.2 Includes the fix for multi-folder drop DataTransfer staleness (umbraco/Umbraco.UI#1339). * qa(dropzone): add unit tests for UmbDropzoneManager folder flattening order Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>



Summary
Fixes a browser-level data staleness bug in
uui-file-dropzonethat caused only the first dropped folder to be processed when multiple folders were dragged and dropped simultaneously (closes umbraco/Umbraco-CMS#21837).Root cause
The
DataTransferobject (and itsitemslist) is only valid for the duration of a synchronous event handler. Once anasyncdrop handler hits its firstawait, the browser marks the drag data store as expired — all subsequent calls toDataTransferItem.webkitGetAsEntry()returnnull.The old
_getAllEntriesloop calledawait this._mkdir(firstFolder)before reading the second item, so only folder #1 was ever retrieved:sequenceDiagram participant B as Browser participant H as _getAllEntries (OLD) participant DT as DataTransfer B->>H: drop event (DataTransfer valid) H->>DT: getEntry(item[0]) → folderA ✓ H->>H: await _mkdir(folderA) Note over DT: ⚠️ Browser expires drag data store H->>DT: getEntry(item[1]) → null ✗ H->>DT: getEntry(item[2]) → null ✗ H-->>B: { folders: [folderA] } ← only first folder!Fix
Split
_getAllEntriesinto two phases:FileSystemEntryrefs in one pass, before anyawait.FileSystemEntryobjects remain valid indefinitely once obtained._processRootEntries): Iterate the stable refs and recursively read each folder/file.sequenceDiagram participant B as Browser participant H as _getAllEntries (NEW) participant DT as DataTransfer B->>H: drop event (DataTransfer valid) H->>DT: getEntry(item[0]) → folderA ✓ H->>DT: getEntry(item[1]) → folderB ✓ H->>DT: getEntry(item[2]) → fileC ✓ Note over H: Phase 1 complete — DataTransfer no longer needed H->>H: await _processRootEntries([folderA, folderB, fileC]) H-->>B: { folders: [folderA, folderB], files: [fileC] } ✓_processRootEntriesalso replaces the removed_processFileEntrymethod, which had the same staleness issue (DataTransferItem.getAsFile()→ now usesFileSystemFileEntry.file()instead).Changes
_getEntryreturn type:FileSystemDirectoryEntry | null→FileSystemEntry | null(files at the root level have aFileSystemFileEntry, not aFileSystemDirectoryEntry)_getAllEntries: rewritten to extract all refs synchronously first_processRootEntries: new private method for phase 2 — directly testable without mockingDataTransfer_processFileEntry: removed (dead code — replaced by_processRootEntries)Test plan
Automated tests
4 new unit tests for
_processRootEntriesusing mockFileSystemEntryobjects (noDataTransfermocking needed):multiple=truemultiple=falseManual — Storybook
The
uui-file-dropzoneStorybook story (packages/uui-file-dropzone) can be used to verify the fix:npm run storybookand open theuui-file-dropzonestorychangeevent fires with all folders inevent.detail.foldersevent.detail.foldersThe story's event log panel shows the fired events and their payloads, making it easy to inspect the result without needing a full CMS.