Skip to content

Commit 3265dbf

Browse files
authored
feat: showSaveFileDialog() (#228)
1 parent 060ff01 commit 3265dbf

File tree

12 files changed

+140
-8
lines changed

12 files changed

+140
-8
lines changed

examples/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// Import all example functions
88
import { initializeAspera } from './initialize';
99
import { testAspera } from './test-connection';
10-
import { selectItemsAspera } from './select-items';
10+
import { selectItemsAspera, showSaveFileDialogAspera } from './select-items';
1111
import { selectAndPreviewImageAspera } from './image-preview';
1212
import { selectAndCalculateChecksumAspera } from './file-checksum';
1313
import { startTransferAspera, authenticateAspera, testSshPortsAspera } from './start-transfer';
@@ -34,6 +34,7 @@ declare global {
3434
initializeAspera: typeof initializeAspera;
3535
testAspera: typeof testAspera;
3636
selectItemsAspera: typeof selectItemsAspera;
37+
showSaveFileDialogAspera: typeof showSaveFileDialogAspera;
3738
selectAndPreviewImageAspera: typeof selectAndPreviewImageAspera;
3839
selectAndCalculateChecksumAspera: typeof selectAndCalculateChecksumAspera;
3940
startTransferAspera: typeof startTransferAspera;
@@ -62,6 +63,7 @@ declare global {
6263
window.initializeAspera = initializeAspera;
6364
window.testAspera = testAspera;
6465
window.selectItemsAspera = selectItemsAspera;
66+
window.showSaveFileDialogAspera = showSaveFileDialogAspera;
6567
window.selectAndPreviewImageAspera = selectAndPreviewImageAspera;
6668
window.selectAndCalculateChecksumAspera = selectAndCalculateChecksumAspera;
6769
window.startTransferAspera = startTransferAspera;
@@ -89,6 +91,7 @@ export {
8991
initializeAspera,
9092
testAspera,
9193
selectItemsAspera,
94+
showSaveFileDialogAspera,
9295
selectAndPreviewImageAspera,
9396
selectAndCalculateChecksumAspera,
9497
startTransferAspera,

examples/select-items.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* This example demonstrates how to let users select items from their local filesystem.
44
*/
55

6-
import { showSelectFileDialog, showSelectFolderDialog } from '@ibm-aspera/sdk';
6+
import { showSelectFileDialog, showSelectFolderDialog, showSaveFileDialog } from '@ibm-aspera/sdk';
77

88
// Global state for selected files (used by the example app)
99
declare global {
@@ -38,3 +38,18 @@ export function selectItemsAspera(selectFolders: boolean) {
3838
}
3939
});
4040
}
41+
42+
export function showSaveFileDialogAspera() {
43+
/** Open a save file dialog for the user to choose a save location. */
44+
showSaveFileDialog({title: 'Save file'}).then(response => {
45+
console.info('Save file dialog response', response);
46+
alert(`Save file dialog response:\n\n${JSON.stringify(response, undefined, 2)}`);
47+
}).catch(error => {
48+
if (error.debugData?.code === -32002) {
49+
alert('User canceled save file dialog');
50+
} else {
51+
console.error('Save file dialog failed', error);
52+
alert(`Save file dialog failed\n\n${JSON.stringify(error, undefined, 2)}`);
53+
}
54+
});
55+
}

examples/src/Views/SelectItems.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export default function SelectItems() {
8888
window.readDirectoryAspera(options);
8989
};
9090

91-
const codeSnippet = [window.selectItemsAspera.toString(), window.readDirectoryAspera.toString()].join('\n\n');
91+
const codeSnippet = [window.selectItemsAspera.toString(), window.showSaveFileDialogAspera.toString(), window.readDirectoryAspera.toString()].join('\n\n');
9292

9393
const visibleEntries = directoryData?.entries.slice(0, MAX_VISIBLE_ENTRIES) ?? [];
9494
const totalEntries = directoryData?.totalCount ?? 0;
@@ -101,6 +101,7 @@ export default function SelectItems() {
101101
<h2>Try it out</h2>
102102
<Button onClick={() => window.selectItemsAspera(false)}>Select files</Button>
103103
<Button onClick={() => window.selectItemsAspera(true)}>Select folder</Button>
104+
<Button onClick={() => window.showSaveFileDialogAspera()}>Save file dialog</Button>
104105
<div className="ending-content">
105106
{window.selectedFiles.length ? (
106107
<UnorderedList>

src/app/core.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {httpDownload, httpUpload, initHttpGateway} from '../http-gateway';
55
import {handleHttpGatewayDrop, httpGatewayReadAsArrayBuffer, httpGatewayReadChunkAsArrayBuffer, httpGatewaySelectFileFolderDialog, httpGetAllTransfers, httpGetTransfer, httpRemoveTransfer, httpStopTransfer, sendTransferUpdate} from '../http-gateway/core';
66
import {asperaSdk} from '../index';
77
import {AsperaSdkInfo, AsperaSdkClientInfo, TransferResponse} from '../models/aspera-sdk.model';
8-
import {CustomBrandingOptions, DataTransferResponse, DropzoneEventData, DropzoneEventType, DropzoneOptions, AsperaSdkSpec, BrowserStyleFile, AsperaSdkTransfer, FileDialogOptions, FolderDialogOptions, InitOptions, ModifyTransferOptions, Pagination, PaginatedFilesResponse, ResumeTransferOptions, TransferSpec, WebsocketEvent, ReadChunkAsArrayBufferResponse, ReadAsArrayBufferResponse, OpenRpcSpec, SdkCapabilities, GetChecksumOptions, ChecksumFileResponse, ReadDirectoryOptions, ReadDirectoryResponse, ShowPreferencesPageOptions, PreferencesPage, TestSshPortsOptions} from '../models/models';
8+
import {CustomBrandingOptions, DataTransferResponse, DropzoneEventData, DropzoneEventType, DropzoneOptions, AsperaSdkSpec, BrowserStyleFile, AsperaSdkTransfer, FileDialogOptions, FolderDialogOptions, SaveFileDialogOptions, InitOptions, ModifyTransferOptions, Pagination, PaginatedFilesResponse, ResumeTransferOptions, TransferSpec, WebsocketEvent, ReadChunkAsArrayBufferResponse, ReadAsArrayBufferResponse, OpenRpcSpec, SdkCapabilities, GetChecksumOptions, ChecksumFileResponse, ReadDirectoryOptions, ReadDirectoryResponse, ShowPreferencesPageOptions, PreferencesPage, TestSshPortsOptions} from '../models/models';
99
import {Connect, ConnectInstaller} from '@ibm-aspera/connect-sdk-js';
1010
import {initConnect} from '../connect/core';
1111
import * as ConnectTypes from '@ibm-aspera/connect-sdk-js/dist/esm/core/types';
@@ -544,6 +544,50 @@ export const showSelectFolderDialog = (options?: FolderDialogOptions): Promise<D
544544
return promiseInfo.promise;
545545
};
546546

547+
/**
548+
* Displays a save file dialog for the user to choose a save location and filename.
549+
*
550+
* Supported for Connect and IBM Aspera for desktop. Not supported for HTTP Gateway.
551+
*
552+
* @param options save file dialog options
553+
*
554+
* @returns a promise that resolves with the selected save path and rejects if user cancels dialog
555+
*/
556+
export const showSaveFileDialog = (options?: SaveFileDialogOptions): Promise<DataTransferResponse> => {
557+
if (asperaSdk.useHttpGateway) {
558+
return throwError(messages.showSaveFileDialogNotSupported);
559+
}
560+
561+
if (asperaSdk.useConnect) {
562+
const connectPromiseInfo = generatePromiseObjects();
563+
asperaSdk.globals.connect.showSaveFileDialog({
564+
success: (data: any) => connectPromiseInfo.resolver(data as unknown as DataTransferResponse),
565+
error: (error: any) => connectPromiseInfo.rejecter(error),
566+
}, options);
567+
return connectPromiseInfo.promise;
568+
}
569+
570+
if (!asperaSdk.isReady) {
571+
return throwError(messages.serverNotVerified);
572+
}
573+
574+
const promiseInfo = generatePromiseObjects();
575+
576+
const payload = {
577+
options: options || {},
578+
app_id: asperaSdk.globals.appId,
579+
};
580+
581+
client.request('show_save_file_dialog', payload)
582+
.then((data: any) => promiseInfo.resolver(data))
583+
.catch(error => {
584+
errorLog(messages.showSaveFileDialogFailed, error);
585+
promiseInfo.rejecter(generateErrorBody(messages.showSaveFileDialogFailed, error));
586+
});
587+
588+
return promiseInfo.promise;
589+
};
590+
547591
/**
548592
* Shows the about page of the transfer client.
549593
*
@@ -1295,7 +1339,7 @@ const supportsMethod = (method: string): boolean => {
12951339
// We currently do not support calculating file checksums when using HTTP Gateway. In theory it should be possible
12961340
// to calculate this directly in the browser similar to how `readAsArrayBuffer()` is implemented.
12971341
// HTTP Gateway also does not support showing native transfer client UI (about, preferences, etc.).
1298-
if (asperaSdk.useHttpGateway && (method === 'get_checksum' || method === 'show_about' || method === 'open_preferences' || method === 'show_transfer_manager' || method === 'show_transfer_monitor' || method === 'authenticate' || method === 'test_ssh_ports' || method === 'read_directory')) {
1342+
if (asperaSdk.useHttpGateway && (method === 'get_checksum' || method === 'show_about' || method === 'open_preferences' || method === 'show_transfer_manager' || method === 'show_transfer_monitor' || method === 'authenticate' || method === 'test_ssh_ports' || method === 'show_save_file_dialog' || method === 'read_directory')) {
12991343
return false;
13001344
}
13011345

@@ -1342,6 +1386,7 @@ export const getCapabilities = (): SdkCapabilities => {
13421386
showTransferMonitor: supportsMethod('show_transfer_monitor'),
13431387
authenticate: supportsMethod('authenticate'),
13441388
testSshPorts: supportsMethod('test_ssh_ports'),
1389+
showSaveFileDialog: supportsMethod('show_save_file_dialog'),
13451390
readDirectory: supportsMethod('read_directory'),
13461391
};
13471392
};

src/constants/messages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,6 @@ export const messages = {
5353
authenticateNotSupported: 'Authenticate is not supported for current transfer client',
5454
testSshPortsFailed: 'Unable to test SSH ports',
5555
testSshPortsNotSupported: 'Test SSH ports is not supported for current transfer client',
56+
showSaveFileDialogFailed: 'Unable to show save file dialog',
57+
showSaveFileDialogNotSupported: 'Show save file dialog is not supported for current transfer client',
5658
};

src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {AsperaSdk} from './models/aspera-sdk.model';
2-
import {authenticate, createDropzone, deregisterActivityCallback, deregisterStatusCallback, getAllTransfers, getCapabilities, getChecksum, getFilesList, getInfo, getTransfer, hasCapability, init, initDragDrop, modifyTransfer, showPreferencesPage, readAsArrayBuffer, readChunkAsArrayBuffer, readDirectory, registerActivityCallback, registerStatusCallback, removeDropzone, removeTransfer, resumeTransfer, setBranding, showAbout, showDirectory, showPreferences, showSelectFileDialog, showSelectFolderDialog, showTransferManager, showTransferMonitor, startTransfer, stopTransfer, testConnection, testSshPorts,} from './app/core';
2+
import {authenticate, createDropzone, deregisterActivityCallback, deregisterStatusCallback, getAllTransfers, getCapabilities, getChecksum, getFilesList, getInfo, getTransfer, hasCapability, init, initDragDrop, modifyTransfer, showPreferencesPage, readAsArrayBuffer, readChunkAsArrayBuffer, readDirectory, registerActivityCallback, registerStatusCallback, removeDropzone, removeTransfer, resumeTransfer, setBranding, showAbout, showDirectory, showPreferences, showSaveFileDialog, showSelectFileDialog, showSelectFolderDialog, showTransferManager, showTransferMonitor, startTransfer, stopTransfer, testConnection, testSshPorts,} from './app/core';
33
import {getInstallerInfo} from './app/installer';
44
import {getInstallerUrls, isSafari} from './helpers/helpers';
55
import * as httpGatewayCalls from './http-gateway';
@@ -23,6 +23,7 @@ asperaSdk.getTransfer = getTransfer;
2323
asperaSdk.getFilesList = getFilesList;
2424
asperaSdk.showSelectFileDialog = showSelectFileDialog;
2525
asperaSdk.showSelectFolderDialog = showSelectFolderDialog;
26+
asperaSdk.showSaveFileDialog = showSaveFileDialog;
2627
asperaSdk.showPreferences = showPreferences;
2728
asperaSdk.showTransferManager = showTransferManager;
2829
asperaSdk.showTransferMonitor = showTransferMonitor;
@@ -73,6 +74,7 @@ export {
7374
getFilesList,
7475
showSelectFileDialog,
7576
showSelectFolderDialog,
77+
showSaveFileDialog,
7678
showPreferences,
7779
showTransferManager,
7880
showTransferMonitor,
@@ -116,6 +118,7 @@ export type {
116118
FileStat,
117119
FileStatus,
118120
FolderDialogOptions,
121+
SaveFileDialogOptions,
119122
GetChecksumOptions,
120123
InitOptions,
121124
InstallerInfo,

src/models/aspera-sdk.model.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {CustomBrandingOptions, DataTransferResponse, DropzoneEventData, DropzoneEventType, DropzoneOptions, AsperaSdkSpec, AsperaSdkTransfer, FileDialogOptions, FolderDialogOptions, InitOptions, InstallerInfoResponse, InstallerOptions, ModifyTransferOptions, Pagination, PaginatedFilesResponse, ResumeTransferOptions, SafariExtensionEvent, TransferSpec, WebsocketEvent, InstallerUrlInfo, RpcMethod, SdkCapabilities, GetChecksumOptions, ChecksumFileResponse, ReadDirectoryOptions, ReadDirectoryResponse, ShowPreferencesPageOptions, TestSshPortsOptions} from './models';
1+
import {CustomBrandingOptions, DataTransferResponse, DropzoneEventData, DropzoneEventType, DropzoneOptions, AsperaSdkSpec, AsperaSdkTransfer, FileDialogOptions, FolderDialogOptions, SaveFileDialogOptions, InitOptions, InstallerInfoResponse, InstallerOptions, ModifyTransferOptions, Pagination, PaginatedFilesResponse, ResumeTransferOptions, SafariExtensionEvent, TransferSpec, WebsocketEvent, InstallerUrlInfo, RpcMethod, SdkCapabilities, GetChecksumOptions, ChecksumFileResponse, ReadDirectoryOptions, ReadDirectoryResponse, ShowPreferencesPageOptions, TestSshPortsOptions} from './models';
22
import {hiddenStyleList, installerUrl, protocol} from '../constants/constants';
33
import {messages} from '../constants/messages';
44
import {safariClient} from '../helpers/client/safari-client';
@@ -391,6 +391,8 @@ export class AsperaSdk {
391391
showSelectFileDialog: (options?: FileDialogOptions) => Promise<DataTransferResponse>;
392392
/** Function to display a folder dialog for the user to select folders. */
393393
showSelectFolderDialog: (options?: FolderDialogOptions) => Promise<DataTransferResponse>;
394+
/** Function to display a save file dialog for the user to choose a save location */
395+
showSaveFileDialog: (options?: SaveFileDialogOptions) => Promise<DataTransferResponse>;
394396
/** Function to show the about page of the transfer client */
395397
showAbout: () => Promise<any>;
396398
/** Function to display the IBM Aspera preferences page */

src/models/models.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ export interface FolderDialogOptions {
3333
multiple?: boolean;
3434
}
3535

36+
export interface SaveFileDialogOptions {
37+
/** Filter the files displayed by file extension. */
38+
allowedFileTypes?: any;
39+
/** The filename to pre-fill the dialog with. */
40+
suggestedName?: string;
41+
/** The name of the dialog window. */
42+
title?: string;
43+
}
44+
3645
/**
3746
* Options related to fetching the latest Aspera installer information.
3847
*
@@ -1029,6 +1038,13 @@ export interface SdkCapabilities {
10291038
* but not HTTP Gateway.
10301039
*/
10311040
testSshPorts: boolean,
1041+
/**
1042+
* Whether the transfer client supports showing the save file dialog.
1043+
*
1044+
* This is supported when using Connect or IBM Aspera for desktop with the required RPC methods,
1045+
* but not HTTP Gateway.
1046+
*/
1047+
showSaveFileDialog: boolean,
10321048
/**
10331049
* Whether the SDK can read directory contents.
10341050
*

tests/integration/connect.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
resumeTransfer,
66
showSelectFileDialog,
77
showSelectFolderDialog,
8+
showSaveFileDialog,
89
showAbout,
910
showPreferences,
1011
getAllTransfers,
@@ -102,6 +103,18 @@ describe('Connect SDK', () => {
102103
});
103104
});
104105

106+
describe('showSaveFileDialog', () => {
107+
it('should call showSaveFileDialog on Connect SDK with callbacks and options', async () => {
108+
await showSaveFileDialog({title: 'Save file', suggestedName: 'report.pdf'});
109+
110+
const mock = getConnectMock();
111+
expect(mock.showSaveFileDialog).toHaveBeenCalledWith(
112+
expect.objectContaining({success: expect.any(Function), error: expect.any(Function)}),
113+
{title: 'Save file', suggestedName: 'report.pdf'},
114+
);
115+
});
116+
});
117+
105118
describe('showAbout', () => {
106119
it('should call showAbout on Connect SDK', async () => {
107120
await showAbout();
@@ -313,6 +326,7 @@ describe('Connect SDK', () => {
313326
expect(hasCapability('showTransferMonitor')).toBe(true);
314327
expect(hasCapability('authenticate')).toBe(true);
315328
expect(hasCapability('testSshPorts')).toBe(true);
329+
expect(hasCapability('showSaveFileDialog')).toBe(true);
316330
expect(hasCapability('imagePreview')).toBe(true);
317331
expect(hasCapability('fileChecksum')).toBe(true);
318332
});

tests/integration/desktop-app.spec.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
resumeTransfer,
66
showSelectFileDialog,
77
showSelectFolderDialog,
8+
showSaveFileDialog,
89
showAbout,
910
showPreferences,
1011
getAllTransfers,
@@ -118,6 +119,19 @@ describe('Desktop App', () => {
118119
});
119120
});
120121

122+
describe('showSaveFileDialog', () => {
123+
it('should call show_save_file_dialog RPC', async () => {
124+
await showSaveFileDialog({title: 'Save file', suggestedName: 'report.pdf'});
125+
126+
const call = lastFetchCall();
127+
expect(call.body.method).toBe('show_save_file_dialog');
128+
expect(call.body.params).toEqual({
129+
options: {title: 'Save file', suggestedName: 'report.pdf'},
130+
app_id: APP_ID,
131+
});
132+
});
133+
});
134+
121135
describe('showAbout', () => {
122136
it('should call show_about RPC', async () => {
123137
await showAbout();
@@ -386,14 +400,15 @@ describe('Desktop App', () => {
386400

387401
describe('hasCapability', () => {
388402
it('should return true for capabilities whose RPC methods are discovered', () => {
389-
asperaSdk.globals.rpcMethods = ['show_about', 'open_preferences', 'show_transfer_manager', 'show_transfer_monitor', 'authenticate', 'test_ssh_ports', 'read_as_array_buffer', 'read_chunk_as_array_buffer', 'get_checksum', 'read_directory'];
403+
asperaSdk.globals.rpcMethods = ['show_about', 'open_preferences', 'show_transfer_manager', 'show_transfer_monitor', 'authenticate', 'test_ssh_ports', 'show_save_file_dialog', 'read_as_array_buffer', 'read_chunk_as_array_buffer', 'get_checksum', 'read_directory'];
390404

391405
expect(hasCapability('showAbout')).toBe(true);
392406
expect(hasCapability('showPreferences')).toBe(true);
393407
expect(hasCapability('showTransferManager')).toBe(true);
394408
expect(hasCapability('showTransferMonitor')).toBe(true);
395409
expect(hasCapability('authenticate')).toBe(true);
396410
expect(hasCapability('testSshPorts')).toBe(true);
411+
expect(hasCapability('showSaveFileDialog')).toBe(true);
397412
expect(hasCapability('imagePreview')).toBe(true);
398413
expect(hasCapability('fileChecksum')).toBe(true);
399414
expect(hasCapability('readDirectory')).toBe(true);
@@ -408,6 +423,7 @@ describe('Desktop App', () => {
408423
expect(hasCapability('showTransferMonitor')).toBe(false);
409424
expect(hasCapability('authenticate')).toBe(false);
410425
expect(hasCapability('testSshPorts')).toBe(false);
426+
expect(hasCapability('showSaveFileDialog')).toBe(false);
411427
expect(hasCapability('imagePreview')).toBe(false);
412428
expect(hasCapability('fileChecksum')).toBe(false);
413429
expect(hasCapability('readDirectory')).toBe(false);

0 commit comments

Comments
 (0)