Add FileSelector shared component

Summary: Add shared FileSelector component compatibe with the new FlipperLib API

Reviewed By: mweststrate

Differential Revision: D32667100

fbshipit-source-id: dca1e8b7693d134a99617e916c7cfd30432cef78
This commit is contained in:
Andrey Goncharov
2021-11-26 08:28:50 -08:00
committed by Facebook GitHub Bot
parent 3491926d17
commit b82c41eedd
8 changed files with 272 additions and 10 deletions

View File

@@ -38,6 +38,7 @@ test('Correct top level API exposed', () => {
"DetailSidebar",
"Dialog",
"ElementsInspector",
"FileSelector",
"Layout",
"MarkerTimeline",
"MasterDetail",

View File

@@ -96,6 +96,7 @@ export {Panel} from './ui/Panel';
export {Tabs, Tab} from './ui/Tabs';
export {useLocalStorageState} from './utils/useLocalStorageState';
export {FileSelector} from './ui/FileSelector';
export {HighlightManager} from './ui/Highlight';
export {
DataValueExtractor,

View File

@@ -88,13 +88,6 @@ export interface FlipperLib {
encoding?: FileEncoding;
multi: true;
}): Promise<FileDescriptor[] | undefined>;
importFile(options?: {
defaultPath?: string;
extensions?: string[];
title?: string;
encoding?: FileEncoding;
multi?: boolean;
}): Promise<FileDescriptor[] | FileDescriptor | undefined>;
/**
* @returns

View File

@@ -0,0 +1,237 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import React, {
CSSProperties,
DragEventHandler,
KeyboardEventHandler,
useState,
} from 'react';
import {Button, Input, Row, Col, Tooltip} from 'antd';
import {
CloseOutlined,
ExclamationCircleOutlined,
UploadOutlined,
} from '@ant-design/icons';
import {
FileDescriptor,
FileEncoding,
FlipperLib,
getFlipperLib,
} from '../plugin/FlipperLib';
import {fromUint8Array} from 'js-base64';
import {assertNever} from '../utils/assertNever';
export type FileSelectorProps = {
/**
* Placeholder text displayed in the Input when it is empty
*/
label: string;
/**
* List of allowed file extentions
*/
extensions?: string[];
required?: boolean;
className?: string;
style?: CSSProperties;
/**
* Imported file encoding. Default: UTF-8.
*/
encoding?: FileEncoding;
} & (
| {
multi?: false;
onChange: (newFile?: FileDescriptor) => void;
}
| {
multi: true;
onChange: (newFiles: FileDescriptor[]) => void;
}
);
const formatFileDescriptor = (fileDescriptor?: FileDescriptor) =>
fileDescriptor?.path || fileDescriptor?.name;
export function FileSelector({
onChange,
label,
extensions,
required,
className,
style,
encoding = 'utf-8',
multi,
}: FileSelectorProps) {
const [loading, setLoading] = useState(false);
const [files, setFiles] = useState<FileDescriptor[]>([]);
const onSetFiles = async () => {
setLoading(true);
let defaultPath: string | undefined = files[0]?.path ?? files[0]?.name;
if (multi) {
defaultPath = files[0]?.path;
}
try {
const newFileSelection = await getFlipperLib().importFile?.({
defaultPath,
extensions,
title: label,
encoding,
multi,
} as Parameters<FlipperLib['importFile']>[0]);
if (!newFileSelection) {
return;
}
if (Array.isArray(newFileSelection)) {
if (!newFileSelection.length) {
return;
}
setFiles(newFileSelection);
(onChange as (newFiles: FileDescriptor[]) => void)(newFileSelection);
} else {
setFiles([newFileSelection]);
(onChange as (newFiles?: FileDescriptor) => void)(newFileSelection);
}
} catch (e) {
console.error('FileSelector.onSetFile -> error', label, e);
} finally {
setLoading(false);
}
};
const onFilesDrop: DragEventHandler<HTMLElement> = async (e) => {
setLoading(true);
try {
if (!e.dataTransfer.files.length) {
return;
}
const droppedFiles = multi
? Array.from(e.dataTransfer.files)
: [e.dataTransfer.files[0]];
const droppedFileSelection = await Promise.all(
droppedFiles.map(async (droppedFile) => {
const raw = await droppedFile.arrayBuffer();
let data: string;
switch (encoding) {
case 'utf-8': {
data = new TextDecoder().decode(raw);
break;
}
case 'base64': {
data = fromUint8Array(new Uint8Array(raw));
break;
}
default: {
assertNever(encoding);
}
}
const droppedFileDescriptor: FileDescriptor = {
data: data!,
name: droppedFile.name,
// Electron "File" has "path" attribute
path: (droppedFile as any).path,
};
return droppedFileDescriptor;
}),
);
setFiles(droppedFileSelection);
if (multi) {
(onChange as (newFiles: FileDescriptor[]) => void)(
droppedFileSelection,
);
} else {
(onChange as (newFiles?: FileDescriptor) => void)(
droppedFileSelection[0],
);
}
} catch (e) {
console.error('FileSelector.onFileDrop -> error', label, e);
} finally {
setLoading(false);
}
};
const captureEnterPress: KeyboardEventHandler<HTMLElement> = (e) => {
if (e.key === 'Enter') {
onSetFiles();
}
};
const emptyFileListEventHandlers = !files.length
? {onClick: onSetFiles, onKeyUp: captureEnterPress}
: {};
const inputProps = {
placeholder: label,
disabled: loading,
onDrop: onFilesDrop,
...emptyFileListEventHandlers,
};
return (
<Row
gutter={8}
align="middle"
wrap={false}
className={className}
style={style}>
<Col flex="auto">
{multi ? (
<Input.TextArea
{...inputProps}
value={
loading
? 'Loading...'
: files.map(formatFileDescriptor).join('; ')
}
/>
) : (
<Input
{...inputProps}
value={loading ? 'Loading...' : formatFileDescriptor(files[0])}
/>
)}
</Col>
{required && !files.length ? (
<Tooltip title="Required!">
<Col flex="none">
<ExclamationCircleOutlined />
</Col>
</Tooltip>
) : null}
<Col flex="none">
<Button
icon={<CloseOutlined />}
title="Reset"
disabled={!files.length || loading}
onClick={() => setFiles([])}
/>
</Col>
<Col flex="none">
<Button
icon={<UploadOutlined />}
onClick={onSetFiles}
disabled={loading}
title={label}
/>
</Col>
</Row>
);
}

View File

@@ -0,0 +1,14 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
export const assertNever: (val: never) => asserts val = (val) => {
if (val) {
throw new Error(`Assert never failed. Received ${val}`);
}
};