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:
committed by
Facebook GitHub Bot
parent
3491926d17
commit
b82c41eedd
@@ -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",
|
||||
|
||||
@@ -38,6 +38,7 @@ test('Correct top level API exposed', () => {
|
||||
"DetailSidebar",
|
||||
"Dialog",
|
||||
"ElementsInspector",
|
||||
"FileSelector",
|
||||
"Layout",
|
||||
"MarkerTimeline",
|
||||
"MasterDetail",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
237
desktop/flipper-plugin/src/ui/FileSelector.tsx
Normal file
237
desktop/flipper-plugin/src/ui/FileSelector.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
14
desktop/flipper-plugin/src/utils/assertNever.tsx
Normal file
14
desktop/flipper-plugin/src/utils/assertNever.tsx
Normal 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}`);
|
||||
}
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user