diff --git a/desktop/app/src/electron/initializeElectron.tsx b/desktop/app/src/electron/initializeElectron.tsx index 3da31dfcd..dc3e3d899 100644 --- a/desktop/app/src/electron/initializeElectron.tsx +++ b/desktop/app/src/electron/initializeElectron.tsx @@ -111,27 +111,46 @@ export function initializeElectron() { return undefined; }); }, - async importFile({defaultPath, extensions} = {}) { - const {filePaths} = await remote.dialog.showOpenDialog({ + importFile: (async ({ + defaultPath, + extensions, + title, + encoding = 'utf-8', + multi, + } = {}) => { + let {filePaths} = await remote.dialog.showOpenDialog({ defaultPath, - properties: ['openFile'], + properties: [ + 'openFile', + ...(multi ? (['multiSelections'] as const) : []), + ], filters: extensions ? [{extensions, name: ''}] : undefined, + title, }); if (!filePaths.length) { return; } - const filePath = filePaths[0]; - const fileName = path.basename(filePath); + if (!multi) { + filePaths = [filePaths[0]]; + } - const data = await fs.promises.readFile(filePath, {encoding: 'utf-8'}); - return { - data, - name: fileName, - }; - }, - async exportFile(data, {defaultPath} = {}) { + const descriptors = await Promise.all( + filePaths.map(async (filePath) => { + const fileName = path.basename(filePath); + + const data = await fs.promises.readFile(filePath, {encoding}); + return { + data, + name: fileName, + }; + }), + ); + + return multi ? descriptors : descriptors[0]; + }) as RenderHost['importFile'], + async exportFile(data, {defaultPath, encoding = 'utf-8'} = {}) { const {filePath} = await remote.dialog.showSaveDialog({ defaultPath, }); @@ -140,7 +159,7 @@ export function initializeElectron() { return; } - await fs.promises.writeFile(filePath, data); + await fs.promises.writeFile(filePath, data, {encoding}); return filePath; }, openLink(url: string) { diff --git a/desktop/flipper-plugin/src/__tests__/api.node.tsx b/desktop/flipper-plugin/src/__tests__/api.node.tsx index 3ac5fb4cc..30f61eb57 100644 --- a/desktop/flipper-plugin/src/__tests__/api.node.tsx +++ b/desktop/flipper-plugin/src/__tests__/api.node.tsx @@ -98,6 +98,7 @@ test('Correct top level API exposed', () => { "ElementsInspectorElement", "ElementsInspectorProps", "FileDescriptor", + "FileEncoding", "FlipperLib", "HighlightManager", "Idler", diff --git a/desktop/flipper-plugin/src/index.ts b/desktop/flipper-plugin/src/index.ts index 9550a6a5d..583e3d170 100644 --- a/desktop/flipper-plugin/src/index.ts +++ b/desktop/flipper-plugin/src/index.ts @@ -38,6 +38,7 @@ export { getFlipperLib, setFlipperLibImplementation as _setFlipperLibImplementation, FileDescriptor, + FileEncoding, } from './plugin/FlipperLib'; export { MenuEntry, diff --git a/desktop/flipper-plugin/src/plugin/FlipperLib.tsx b/desktop/flipper-plugin/src/plugin/FlipperLib.tsx index f174ec85b..01686583c 100644 --- a/desktop/flipper-plugin/src/plugin/FlipperLib.tsx +++ b/desktop/flipper-plugin/src/plugin/FlipperLib.tsx @@ -14,9 +14,12 @@ import {RealFlipperClient} from './Plugin'; import {Notification} from './Notification'; import {DetailSidebarProps} from '../ui/DetailSidebar'; +export type FileEncoding = 'utf-8' | 'base64'; + export interface FileDescriptor { data: string; name: string; + path?: string; } /** @@ -40,6 +43,10 @@ export interface FlipperLib { DetailsSidebarImplementation?( props: DetailSidebarProps, ): React.ReactElement | null; + /** + * @deprecated + * Will be removed in subsequent commits + */ showOpenDialog?(options: { defaultPath?: string; filter?: { @@ -52,19 +59,64 @@ export interface FlipperLib { * Imported file data. * If user cancelled a file selection - undefined. */ - importFile(options: { + importFile(options?: { + /** + * Default directory to start the file selection from + */ + defaultPath?: string; + /** + * List of allowed file extensions + */ + extensions?: string[]; + /** + * Open file dialog title + */ + title?: string; + /** + * File encoding + */ + encoding?: FileEncoding; + /** + * Allow selection of multiple files + */ + multi?: false; + }): Promise; + importFile(options?: { defaultPath?: string; extensions?: string[]; - }): Promise; + title?: string; + encoding?: FileEncoding; + multi: true; + }): Promise; + importFile(options?: { + defaultPath?: string; + extensions?: string[]; + title?: string; + encoding?: FileEncoding; + multi?: boolean; + }): Promise; + /** * @returns * An exported file path (if available) or a file name. * If user cancelled a file selection - undefined. */ exportFile( + /** + * New file data + */ data: string, options?: { + /** + * A file path suggestion for a new file. + * A dialog to save file will use it as a starting point. + * Either a complete path to the newly created file, a path to a directory containing the file, or the file name. + */ defaultPath?: string; + /** + * File encoding + */ + encoding?: FileEncoding; }, ): Promise; paths: { diff --git a/desktop/flipper-ui-core/src/RenderHost.tsx b/desktop/flipper-ui-core/src/RenderHost.tsx index 92af9366b..391d50ce6 100644 --- a/desktop/flipper-ui-core/src/RenderHost.tsx +++ b/desktop/flipper-ui-core/src/RenderHost.tsx @@ -59,6 +59,9 @@ export interface RenderHost { writeTextToClipboard(text: string): void; /** * @deprecated + * WARNING! + * It is a low-level API call that might be removed in the future. + * It is not really deprecated yet, but we'll try to make it so. * TODO: Remove in favor of "exportFile" */ showSaveDialog?(options: { @@ -68,9 +71,18 @@ export interface RenderHost { }): Promise; /** * @deprecated + * WARNING! + * It is a low-level API call that might be removed in the future. + * It is not really deprecated yet, but we'll try to make it so. * TODO: Remove in favor of "importFile" */ - showOpenDialog?: FlipperLib['showOpenDialog']; + showOpenDialog?(options: { + defaultPath?: string; + filter?: { + extensions: string[]; + name: string; + }; + }): Promise; showSelectDirectoryDialog?(defaultPath?: string): Promise; importFile: FlipperLib['importFile']; exportFile: FlipperLib['exportFile'];