diff --git a/src/chrome/plugin-manager/PluginInstaller.tsx b/src/chrome/plugin-manager/PluginInstaller.tsx index cb78fbdef..755f8cd64 100644 --- a/src/chrome/plugin-manager/PluginInstaller.tsx +++ b/src/chrome/plugin-manager/PluginInstaller.tsx @@ -25,6 +25,7 @@ import { LoadingIndicator, Tooltip, } from 'flipper'; +import {default as FileSelector} from '../../ui/components/FileSelector'; import React, {useCallback, useState, useMemo, useEffect} from 'react'; import {List} from 'immutable'; import {SearchIndex} from 'algoliasearch'; @@ -144,6 +145,8 @@ export function annotatePluginsWithUpdates( const PluginInstaller = function props(props: Props) { const [restartRequired, setRestartRequired] = useState(false); const [query, setQuery] = useState(''); + const [_localPackagePath, setLocalPackagePath] = useState(''); + const [isLocalPackagePathValid, setIsLocalPackagePathValud] = useState(false); const rows = useNPMSearch( setRestartRequired, query, @@ -158,34 +161,56 @@ const PluginInstaller = function props(props: Props) { }, []); return ( - - {restartRequired && ( - - To activate this plugin, Flipper needs to restart. Click here to - restart! - - )} + <> + + {restartRequired && ( + + To activate this plugin, Flipper needs to restart. Click here to + restart! + + )} + + + setQuery(e.target.value)} + value={query} + placeholder="Search Flipper plugins..." + /> + + + + - - setQuery(e.target.value)} - value={query} - placeholder="Search Flipper plugins..." - /> - + { + setLocalPackagePath(e.path); + setIsLocalPackagePathValud(e.isValid); + }} + /> + - - + ); }; PluginInstaller.defaultProps = defaultProps; diff --git a/src/ui/components/FileSelector.tsx b/src/ui/components/FileSelector.tsx new file mode 100644 index 000000000..1e6298fb2 --- /dev/null +++ b/src/ui/components/FileSelector.tsx @@ -0,0 +1,127 @@ +/** + * 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, {useState} from 'react'; +import FlexRow from './FlexRow'; +import FlexColumn from './FlexColumn'; +import Glyph from './Glyph'; +import Input from './Input'; +import electron from 'electron'; +import styled from '@emotion/styled'; +import {colors} from './colors'; +import Electron from 'electron'; +import fs from 'fs'; + +const CenteredGlyph = styled(Glyph)({ + flexGrow: 0, + margin: 'auto', + marginLeft: 10, +}); + +const Container = styled(FlexRow)({ + width: '100%', +}); + +const FileInputBox = styled(Input)<{isValid: boolean}>(({isValid}) => ({ + flexGrow: 1, + color: isValid ? undefined : colors.red, + '&::-webkit-input-placeholder': { + color: colors.placeholder, + fontWeight: 300, + }, +})); + +function strToArr(item: T): T[] { + return [item]; +} + +export interface Props { + onPathChanged: (evtArgs: {path: string; isValid: boolean}) => void; + placeholderText: string; + defaultPath: string; + showHiddenFiles: boolean; +} + +const defaultProps: Props = { + onPathChanged: () => {}, + placeholderText: '', + defaultPath: '/', + showHiddenFiles: false, +}; + +export default function FileSelector({ + onPathChanged, + placeholderText, + + defaultPath, + showHiddenFiles, +}: Props) { + const [value, setValue] = useState(''); + const [isValid, setIsValid] = useState(false); + const options: Electron.OpenDialogOptions = { + properties: [ + 'openFile', + ...(showHiddenFiles ? strToArr('showHiddenFiles') : []), + ], + defaultPath, + }; + const onChange = (path: string) => { + setValue(path); + let isNewPathValid = false; + try { + isNewPathValid = fs.statSync(path).isFile(); + } catch { + isNewPathValid = false; + } + setIsValid(isNewPathValid); + onPathChanged({path, isValid: isNewPathValid}); + }; + return ( + + { + if (e.dataTransfer.files.length) { + onChange(e.dataTransfer.files[0].path); + } + }} + onChange={e => { + onChange(e.target.value); + }} + /> + + electron.remote.dialog + .showOpenDialog(options) + .then((result: electron.OpenDialogReturnValue) => { + if (result && !result.canceled && result.filePaths.length) { + onChange(result.filePaths[0]); + } + }) + }> + + + {isValid ? null : ( + + )} + + ); +} + +FileSelector.defaultProps = defaultProps; diff --git a/src/ui/components/Glyph.tsx b/src/ui/components/Glyph.tsx index 47146dbfb..90f710233 100644 --- a/src/ui/components/Glyph.tsx +++ b/src/ui/components/Glyph.tsx @@ -48,12 +48,20 @@ function ColoredIcon( className?: string; color?: string; style?: React.CSSProperties; + title?: string; }, context: { glyphColor?: string; }, ) { - const {color = context.glyphColor, name, size = 16, src, style} = props; + const { + color = context.glyphColor, + name, + size = 16, + src, + style, + title, + } = props; const isBlack = color == null || @@ -69,6 +77,7 @@ function ColoredIcon( size={size} className={props.className} style={style} + title={title} /> ); } else { @@ -79,6 +88,7 @@ function ColoredIcon( src={src} className={props.className} style={style} + title={title} /> ); } @@ -98,7 +108,15 @@ export default class Glyph extends React.PureComponent<{ title?: string; }> { render() { - const {name, size = 16, variant, color, className, style} = this.props; + const { + name, + size = 16, + variant, + color, + className, + style, + title, + } = this.props; return (