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

@@ -16,6 +16,7 @@
"@types/uuid": "^8.3.1",
"flipper-common": "0.0.0",
"immer": "^9.0.6",
"js-base64": "^3.7.2",
"lodash": "^4.17.21",
"react-color": "^2.19.3",
"react-element-to-jsx-string": "^14.3.4",

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}`);
}
};

View File

@@ -13533,9 +13533,9 @@ ws@1.1.5, ws@^1.1.5:
ultron "1.0.x"
ws@^7.4.5, ws@^7.4.6, ws@^8.2.3, ws@~8.2.3:
version "7.5.5"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881"
integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==
version "7.5.6"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b"
integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==
xdg-basedir@^4.0.0:
version "4.0.0"

View File

@@ -935,6 +935,21 @@ Shows a loading spinner. Accept an optional `size` to make the spinner larger /
An element that can be used to provide a New User eXperience: Hints that give a one time introduction to new features to the current user.
See `View > Flipper Style Guide` inside the Flipper application for more details.
### FileSelector
Enables file uploading. Shows an input with an upload button. User can select and upload files by clicking on the button, on the input, by pressing enter when the input is focued, and by dropping a file on the input. The input's value is a path to a file or its name if path is not available (in browsers).
Exports `FileSelector` components with the following props:
1. `label` - placeholder text displayed in the input when it is empty
1. `onChange` - callback called when new files are selected or when the exisitng selection is reset
1. `multi` - *optional* allows selecting multiple files at once
1. `extensions` - *optional* list of allowed file extentions
1. `required` - *optional* boolean to mark the file selection input as required
1. `encoding` - *optional* imported file encoding. Default: UTF-8.
1. `className` - *optional* class name string
1. `style` - *optional* CSS styles object
### DetailSidebar
An element that can be passed children which will be shown in the right sidebar of Flipper.