Skip to content

Commit 42bb703

Browse files
authored
feat: drag and drop for folders and file pickers (#101)
1 parent 682703c commit 42bb703

File tree

7 files changed

+121
-32
lines changed

7 files changed

+121
-32
lines changed

example/public/sdk-code.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ function selectItemsAspera(selectFolders) {
5151
* They both return the same response type. So swapping
5252
* out the function is safe.
5353
*/
54-
(selectFolders ? asperaSdk.showSelectFolderDialog() : asperaSdk.showSelectFileDialog()).then(response => {
54+
(selectFolders ? asperaSdk.showSelectFolderDialog({multiple: true}) : asperaSdk.showSelectFileDialog({multiple: true})).then(response => {
5555
/**
5656
* File list for transferSpec is returned in `response.dataTransfer.files` array
5757
* where name is the path to the selected item.

example/src/Views/SelectItems.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export default function SelectItems() {
3535
<div className="ending-content">
3636
{window.selectedFiles.length ? (
3737
<UnorderedList>
38-
{window.selectedFiles.map((file: {name: string; size: number; type: string}) => <ListItem>{file.name} ({file.size} - {file.type})</ListItem>)}
38+
{window.selectedFiles.map((file: {name: string; size: number; type: string}, index: number) => <ListItem key={index}>{file.name} ({file.size} - {file.type})</ListItem>)}
3939
</UnorderedList> ) : (
4040
<h4>No files selected</h4>
4141
)

src/app/core.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {messages} from '../constants/messages';
22
import {client} from '../helpers/client/client';
33
import {errorLog, generateErrorBody, generatePromiseObjects, isValidTransferSpec, randomUUID, throwError} from '../helpers/helpers';
4-
import {getApiCall, handleHttpGatewayDrop, httpGatewaySelectFileDialog, httpGatewaySelectFolderDialog} from '../http-gateway/core';
4+
import {getApiCall, handleHttpGatewayDrop, httpGatewaySelectFileFolderDialog} from '../http-gateway/core';
55
import {HttpGatewayInfo} from '../http-gateway/models';
66
import {asperaSdk} from '../index';
77
import {AsperaSdkInfo, AsperaSdkClientInfo, TransferResponse} from '../models/aspera-sdk.model';
@@ -348,7 +348,7 @@ export const resumeTransfer = (id: string, options?: ResumeTransferOptions): Pro
348348
*/
349349
export const showSelectFileDialog = (options?: FileDialogOptions): Promise<DataTransferResponse> => {
350350
if (asperaSdk.useHttpGateway) {
351-
return httpGatewaySelectFileDialog(options);
351+
return httpGatewaySelectFileFolderDialog(options, false);
352352
} else if (!asperaSdk.isReady) {
353353
return throwError(messages.serverNotVerified);
354354
}
@@ -379,7 +379,7 @@ export const showSelectFileDialog = (options?: FileDialogOptions): Promise<DataT
379379
*/
380380
export const showSelectFolderDialog = (options?: FolderDialogOptions): Promise<DataTransferResponse> => {
381381
if (asperaSdk.useHttpGateway) {
382-
return httpGatewaySelectFolderDialog(options);
382+
return httpGatewaySelectFileFolderDialog(options, true);
383383
} else if (!asperaSdk.isReady) {
384384
return throwError(messages.serverNotVerified);
385385
}
@@ -597,11 +597,9 @@ export const createDropzone = (
597597
event.preventDefault();
598598
if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length && event.dataTransfer.files[0]) {
599599
const files: BrowserStyleFile[] = [];
600-
const rawFiles: File[] = [];
601600

602601
for (let i = 0; i < event.dataTransfer.files.length; i++) {
603602
const file = event.dataTransfer.files[i];
604-
rawFiles.push(file);
605603
files.push({
606604
lastModified: file.lastModified,
607605
name: file.name,
@@ -615,16 +613,14 @@ export const createDropzone = (
615613
app_id: asperaSdk.globals.appId,
616614
};
617615

618-
handleHttpGatewayDrop(rawFiles);
619-
620616
if (asperaSdk.isReady) {
621617
client.request('dropped_files', payload)
622618
.then((data: any) => callback({event, files: data}))
623619
.catch(error => {
624620
errorLog(messages.unableToReadDropped, error);
625621
});
626-
} else {
627-
callback({event, files: {dataTransfer: {files}}});
622+
} else if (asperaSdk.httpGatewayIsReady) {
623+
handleHttpGatewayDrop(event.dataTransfer.items, callback, event);
628624
}
629625
}
630626
};

src/constants/messages.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ export const messages = {
2929
websocketNotReady: 'The websocket is not ready. Run init first',
3030
httpNotAvailable: 'IBM Aspera HTTP Gateway is not available',
3131
httpInitFail: 'IBM Aspera HTTP Gateway could not be started',
32+
filePickerCancel: 'User canceled the select file or folder dialog.',
3233
};

src/http-gateway/core.ts

Lines changed: 109 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {messages} from '../constants/messages';
2+
import {generatePromiseObjects} from '../helpers/helpers';
13
import {asperaSdk} from '../index';
24
import {FileDialogOptions, DataTransferResponse} from '../models/models';
35
import {HttpGatewayDownload, HttpGatewayDownloadLegacy, HttpGatewayInfo, HttpGatewayPresign, HttpGatewayUpload} from './models';
@@ -48,33 +50,123 @@ export const getApiCall = (type: 'INFO'|'DOWNLOAD'|'UPLOAD'|'PRESIGN', body?: Bo
4850
};
4951

5052
/**
51-
* Handle drop events and store files for HTTP Gateway
52-
* This works on top of desktop.
53+
* Create HTML input element for file picking
5354
*/
54-
export const handleHttpGatewayDrop = (files: File[]): void => {
55-
files.forEach(file => {
56-
asperaSdk.httpGatewaySelectedFiles.set(file.name, file);
57-
});
55+
export const createHtmlInputElement = (): HTMLInputElement => {
56+
const element = window.document.createElement('input');
57+
element.type = 'file';
58+
element.style = 'display: none;';
59+
window.document.body.appendChild(element);
60+
return element;
5861
};
5962

6063
/**
61-
* Open native browser file picker for files
62-
*
63-
* @param options - File picker options
64-
*
65-
* @returns Promise that resolves with info about the files picked
64+
* Handle drop events and store files for HTTP Gateway
65+
* This works on top of desktop.
6666
*/
67-
export const httpGatewaySelectFileDialog = (options?: FileDialogOptions): Promise<DataTransferResponse> => {
68-
return Promise.reject('TODO: Select file. Need to create form since showOpenFilePicker has limited browser support');
67+
export const handleHttpGatewayDrop = (items: DataTransferItemList, callback: (data: {event: DragEvent; files: DataTransferResponse}) => void, event: DragEvent): void => {
68+
const files: File[] = [];
69+
let callbackCount = 0;
70+
let callbackFinishCount = 0;
71+
72+
const finalCallback = (): void => {
73+
if (callbackFinishCount >= callbackCount) {
74+
const finalFiles = files.map(file => {
75+
asperaSdk.httpGatewaySelectedFiles.set(file.name, file);
76+
77+
return {
78+
lastModified: file.lastModified,
79+
name: file.name,
80+
size: file.size,
81+
type: file.type
82+
};
83+
});
84+
callback({event, files: {dataTransfer: {files: finalFiles}}});
85+
}
86+
};
87+
88+
const traverse = (item: FileSystemEntry) => {
89+
if (item.isFile) {
90+
(item as FileSystemFileEntry).file(file => {
91+
files.push(file);
92+
callbackFinishCount++;
93+
finalCallback();
94+
});
95+
96+
} else if (item.isDirectory) {
97+
const dirReader = (item as FileSystemDirectoryEntry).createReader();
98+
99+
dirReader.readEntries(entries => {
100+
for (let k = 0; k < entries.length; k++) {
101+
callbackCount++;
102+
traverse(entries[k]);
103+
}
104+
});
105+
106+
callbackFinishCount++;
107+
} else {
108+
callbackFinishCount++;
109+
finalCallback();
110+
}
111+
112+
};
113+
114+
for (let i = 0; i < items.length; i++) {
115+
const item = items[i].webkitGetAsEntry();
116+
117+
if (item) {
118+
callbackCount++;
119+
traverse(item);
120+
}
121+
}
69122
};
70123

71124
/**
72-
* Open native browser file picker for folders
125+
* Open native browser file or folder picker for files
73126
*
74127
* @param options - File picker options
128+
* @param folder - Indicate if choosing folders
75129
*
76-
* @returns Promise that resolves with info about the folders picked
130+
* @returns Promise that resolves with info about the files picked
77131
*/
78-
export const httpGatewaySelectFolderDialog = (options?: FileDialogOptions): Promise<DataTransferResponse> => {
79-
return Promise.reject('TODO: Select folder. Need to create form since showOpenFilePicker has limited browser support');
132+
export const httpGatewaySelectFileFolderDialog = (options?: FileDialogOptions, folder?: boolean): Promise<DataTransferResponse> => {
133+
const {promise, rejecter, resolver} = generatePromiseObjects();
134+
const element = createHtmlInputElement();
135+
136+
element.multiple = !!options?.multiple;
137+
138+
if (folder) {
139+
element.webkitdirectory = true;
140+
}
141+
142+
element.oncancel = () => {
143+
rejecter({debugData: {code: -32002, message: messages.filePickerCancel}});
144+
};
145+
146+
element.onchange = () => {
147+
const returnFiles: File[] = [];
148+
149+
for (let i = 0; i < element.files.length; i++) {
150+
const file = element.files[i];
151+
returnFiles.push(file);
152+
asperaSdk.httpGatewaySelectedFiles.set(file.name, file);
153+
}
154+
155+
resolver({
156+
dataTransfer: {
157+
files: returnFiles.map(file => {
158+
return {
159+
size: file.size,
160+
lastModified: file.lastModified,
161+
name: file.webkitRelativePath || file.name,
162+
type: file.type,
163+
};
164+
})
165+
}
166+
});
167+
};
168+
169+
element.click();
170+
171+
return promise;
80172
};

src/http-gateway/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {httpDownload} from './download';
22
import {httpUpload} from './upload';
3-
import {getApiCall, handleHttpGatewayDrop, httpGatewaySelectFileDialog, httpGatewaySelectFolderDialog} from './core';
3+
import {getApiCall, handleHttpGatewayDrop, httpGatewaySelectFileFolderDialog, createHtmlInputElement} from './core';
44

55
/**
66
* HTTP Gateway Exports
@@ -15,6 +15,6 @@ export {
1515
httpDownload,
1616
getApiCall,
1717
handleHttpGatewayDrop,
18-
httpGatewaySelectFileDialog,
19-
httpGatewaySelectFolderDialog
18+
httpGatewaySelectFileFolderDialog,
19+
createHtmlInputElement,
2020
};

src/models/aspera-sdk.model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class AsperaSdkGlobals {
3030
httpGatewayUrl?: string;
3131
/** Indicate that the HTTP Gateway has been verified as working */
3232
httpGatewayVerified = false;
33-
/** Aspera HTTP Gateway info */
33+
/** HTTP Gateway info */
3434
httpGatewayInfo?: HttpGatewayInfo;
3535

3636
backupLaunchMethod(url: string): void {

0 commit comments

Comments
 (0)