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 (