fix(files): route drops through the selected target

This commit is contained in:
2026-07-20 16:57:34 +02:00
parent f3210234d3
commit 8c5f149f07
3 changed files with 101 additions and 16 deletions

View File

@@ -439,6 +439,16 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
return activeDialogTarget;
}
function uploadRejectedReason(target: FileActionTarget | null): string {
if (busy || uploadActive) return "Another file operation is already running. Wait until it finishes before dropping files.";
if (!canUpload) return "You do not have permission to upload files here.";
if (!target) return "Choose a destination folder before dropping files.";
const targetSpace = findSpace(target.spaceId);
if (!targetSpace) return "The selected upload destination is no longer available.";
if (isConnectorSpace(targetSpace)) return "Files cannot be uploaded into connector spaces from this view.";
return "The dropped item cannot be uploaded here.";
}
function openDialog(kind: DialogKind, target: FileActionTarget | null = null) {
if ((kind === "upload" || kind === "connector-sync") && !canUpload) return;
if (kind === "connector-space" && !canOrganize) return;
@@ -615,10 +625,12 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
async function handleFilesUpload(fileList: FileList | File[], options: {conflictStrategy?: ConflictStrategy;conflictResolutions?: ConflictResolution[];bypassConflictDialog?: boolean;target?: FileActionTarget;} = {}) {
if (!canUpload) return;
const target = options.target ?? currentActionTarget();
const targetSpace = target ? findSpace(target.spaceId) : null;
if (!target || !targetSpace || isConnectorSpace(targetSpace)) return;
if (busy || uploadActive || !canUpload || !target || !targetSpace || isConnectorSpace(targetSpace)) {
setError(uploadRejectedReason(target));
return;
}
const selected = Array.from(fileList);
if (selected.length === 0) return;
if (!options.bypassConflictDialog && !unpackZip) {
@@ -678,13 +690,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
}
async function uploadExternalFilesToTarget(fileList: FileList | File[], target: FileActionTarget) {
const previousTarget = dialogTarget;
setDialogTarget(target);
try {
await handleFilesUpload(fileList);
} finally {
setDialogTarget(previousTarget);
}
await handleFilesUpload(fileList, { target });
}
async function openConnectorSyncDialog(target: FileActionTarget | null = toolbarTarget()) {
@@ -1339,8 +1345,20 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
setDropTargetKey(reason || noop ? "" : dropTargetId(target));
return;
}
event.dataTransfer.dropEffect = canUpload ? "copy" : "none";
setDropTargetKey(canUpload ? dropTargetId(target) : "");
event.dataTransfer.dropEffect = canUpload && !busy && !uploadActive ? "copy" : "none";
setDropTargetKey(canUpload && !busy && !uploadActive ? dropTargetId(target) : "");
}
function handleCurrentFolderDragOver(event: ReactDragEvent<HTMLElement>) {
const target = toolbarTarget();
if (!target) {
event.preventDefault();
event.stopPropagation();
event.dataTransfer.dropEffect = "none";
setDropTargetKey("");
return;
}
handleDropTargetDragOver(event, target);
}
async function handleDropOnTarget(event: ReactDragEvent<HTMLElement>, target: FileActionTarget) {
@@ -1348,7 +1366,11 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
event.stopPropagation();
setDragActive(false);
setDropTargetKey("");
if (isConnectorSpace(findSpace(target.spaceId))) return;
const hasDroppedFiles = event.dataTransfer.files.length > 0;
if (isConnectorSpace(findSpace(target.spaceId))) {
if (hasDroppedFiles) setError(uploadRejectedReason(target));
return;
}
if (isInternalDrag(event)) {
if (!canOrganize) return;
const state = internalDrag ?? parseDragState(event.dataTransfer.getData(INTERNAL_DRAG_TYPE));
@@ -1361,9 +1383,27 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
await runTransfer(event.ctrlKey ? "copy" : "move", state.sourceSpaceId, { fileIds: new Set(state.fileIds), folderPaths: new Set(state.folderPaths) }, target);
return;
}
if (canUpload && event.dataTransfer.files.length > 0) {
if (hasDroppedFiles) {
if (!canUpload || busy || uploadActive) {
setError(uploadRejectedReason(target));
return;
}
await uploadExternalFilesToTarget(event.dataTransfer.files, target);
return;
}
setError("Drop one or more files from your computer, or drag files and folders from this file space.");
}
async function handleDropOnCurrentFolder(event: ReactDragEvent<HTMLElement>) {
const target = toolbarTarget();
if (!target) {
event.preventDefault();
event.stopPropagation();
setDropTargetKey("");
setError(uploadRejectedReason(null));
return;
}
await handleDropOnTarget(event, target);
}
function clearDropState() {
@@ -1940,6 +1980,9 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
}
const currentFolderDropTarget = toolbarTarget();
const currentFolderDropActive = currentFolderDropTarget ? dropTargetKey === dropTargetId(currentFolderDropTarget) : false;
return (
<div className="workspace-data-page module-entry-page file-manager-page file-manager-fullscreen">
{error &&
@@ -2051,9 +2094,12 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
renderConnectorSpaceContent() :
<div
className="file-list-drop-target"
className={`file-list-drop-target ${currentFolderDropActive ? "is-drop-target" : ""}`}
ref={fileListViewportRef}
onScroll={(event) => setFileListScrollTop(event.currentTarget.scrollTop)}
onDragOver={handleCurrentFolderDragOver}
onDragLeave={clearDropState}
onDrop={(event) => void handleDropOnCurrentFolder(event)}
onContextMenu={(event) => openContextMenu(event, "empty")}
onClick={(event) => {
if (event.target === event.currentTarget) applySelectionKeys(new Set<EntrySelectionKey>());
@@ -2179,13 +2225,14 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
{dialog === "upload" &&
<FileDialog title={i18nMessage("i18n:govoplan-files.upload_to_value_value.b83a34b4", { value0: activeDialogSpace?.label || "i18n:govoplan-files.files.6ce6c512", value1: activeDialogTarget?.folderPath || "i18n:govoplan-files.root.e96857c5" })} onClose={closeDialog}>
<FileDropZone
disabled={busy || !activeDialogSpace || !canUpload}
disabled={busy || !activeDialogTarget || !activeDialogSpace || isConnectorSpace(activeDialogSpace) || !canUpload}
busy={uploadActive}
progress={visibleUploadProgress}
busyLabel={uploadBusyLabel}
progressLabel={uploadProgressLabel}
note={`Files are uploaded into ${activeDialogTarget?.folderPath || "i18n:govoplan-files.root.e96857c5"}.`}
onFiles={(files) => handleFilesUpload(files)} />
onRejectedDrop={(reason) => setError(reason === "disabled" ? uploadRejectedReason(activeDialogTarget) : "The browser did not provide readable file data for this drop. Use the file picker, or drag local files from a file manager that exposes file contents to the browser.")}
onFiles={(files) => handleFilesUpload(files, { target: activeDialogTarget || undefined })} />
<ToggleSwitch label="i18n:govoplan-files.unpack_zip_uploads.256fead4" checked={unpackZip} onChange={setUnpackZip} disabled={busy} />
<div className="field-block">