Searchable

Summary: _typescript_

Reviewed By: passy

Differential Revision: D16807182

fbshipit-source-id: 68bc8365bc5b0d8075d0a93d5963c824c0d66769
This commit is contained in:
Daniel Büchele
2019-08-20 05:40:31 -07:00
committed by Facebook Github Bot
parent c8c90ee413
commit 62a204bdbe
9 changed files with 196 additions and 165 deletions

View File

@@ -51,8 +51,10 @@
},
"devDependencies": {
"@jest-runner/electron": "^2.0.1",
"@types/deep-equal": "^1.0.1",
"@types/invariant": "^2.2.30",
"@types/jest": "^24.0.16",
"@types/lodash.debounce": "^4.0.6",
"@types/react": "^16.8.24",
"@types/react-dom": "^16.8.5",
"@types/react-redux": "^7.1.1",

View File

@@ -157,15 +157,15 @@ export {
SearchBox,
SearchInput,
SearchIcon,
SearchableProps,
default as Searchable,
} from './ui/components/searchable/Searchable.js';
} from './ui/components/searchable/Searchable.tsx';
export {
default as SearchableTable,
} from './ui/components/searchable/SearchableTable.js';
} from './ui/components/searchable/SearchableTable.tsx';
export {
default as SearchableTable_immutable,
} from './ui/components/searchable/SearchableTable_immutable.js';
export {SearchableProps} from './ui/components/searchable/Searchable.js';
} from './ui/components/searchable/SearchableTable_immutable.tsx';
export {
ElementID,
ElementData,

View File

@@ -5,53 +5,59 @@
* @format
*/
import type {Filter} from 'flipper';
import {Filter} from '../filter/types';
import {PureComponent} from 'react';
import Text from '../Text.tsx';
import styled from '../../styled/index.js';
import Text from '../Text';
import styled from 'react-emotion';
import {findDOMNode} from 'react-dom';
import {colors} from '../colors.tsx';
import electron from 'electron';
import {colors} from '../colors';
import electron, {MenuItemConstructorOptions} from 'electron';
import React from 'react';
import {ColorProperty} from 'csstype';
const Token = styled(Text)(props => ({
display: 'inline-flex',
alignItems: 'center',
backgroundColor: props.focused
? colors.macOSHighlightActive
: props.color || colors.macOSHighlight,
borderRadius: 4,
marginRight: 4,
padding: 4,
paddingLeft: 6,
height: 21,
color: props.focused ? 'white' : 'inherit',
'&:active': {
backgroundColor: colors.macOSHighlightActive,
color: colors.white,
},
'&:first-of-type': {
marginLeft: 3,
},
}));
const Token = styled(Text)(
(props: {focused?: boolean; color?: ColorProperty}) => ({
display: 'inline-flex',
alignItems: 'center',
backgroundColor: props.focused
? colors.macOSHighlightActive
: props.color || colors.macOSHighlight,
borderRadius: 4,
marginRight: 4,
padding: 4,
paddingLeft: 6,
height: 21,
color: props.focused ? 'white' : 'inherit',
'&:active': {
backgroundColor: colors.macOSHighlightActive,
color: colors.white,
},
'&:first-of-type': {
marginLeft: 3,
},
}),
);
const Key = styled(Text)(props => ({
position: 'relative',
fontWeight: 500,
paddingRight: 12,
textTransform: 'capitalize',
lineHeight: '21px',
'&:after': {
content: props.type === 'exclude' ? '"≠"' : '"="',
paddingLeft: 5,
position: 'absolute',
top: -1,
right: 0,
fontSize: 14,
},
'&:active:after': {
backgroundColor: colors.macOSHighlightActive,
},
}));
const Key = styled(Text)(
(props: {type: 'exclude' | 'include' | 'enum'; focused?: boolean}) => ({
position: 'relative',
fontWeight: 500,
paddingRight: 12,
textTransform: 'capitalize',
lineHeight: '21px',
'&:after': {
content: props.type === 'exclude' ? '"≠"' : '"="',
paddingLeft: 5,
position: 'absolute',
top: -1,
right: 0,
fontSize: 14,
},
'&:active:after': {
backgroundColor: colors.macOSHighlightActive,
},
}),
);
const Value = styled(Text)({
whiteSpace: 'nowrap',
@@ -62,7 +68,7 @@ const Value = styled(Text)({
paddingLeft: 3,
});
const Chevron = styled('div')(props => ({
const Chevron = styled('div')((props: {focused?: boolean}) => ({
border: 0,
paddingLeft: 3,
paddingRight: 1,
@@ -81,23 +87,24 @@ const Chevron = styled('div')(props => ({
},
}));
type Props = {|
filter: Filter,
focused: boolean,
index: number,
onFocus: (focusedToken: number) => void,
onBlur: () => void,
onDelete: (deletedToken: number) => void,
onReplace: (index: number, filter: Filter) => void,
|};
type Props = {
filter: Filter;
focused: boolean;
index: number;
onFocus: (focusedToken: number) => void;
onBlur: () => void;
onDelete: (deletedToken: number) => void;
onReplace: (index: number, filter: Filter) => void;
};
export default class FilterToken extends PureComponent<Props> {
_ref: ?Element;
_ref: Element | undefined;
onMouseDown = () => {
if (
this.props.filter.persistent == null ||
this.props.filter.persistent === false
this.props.filter.type !== 'enum' ||
(this.props.filter.persistent == null ||
this.props.filter.persistent === false)
) {
this.props.onFocus(this.props.index);
}
@@ -112,7 +119,7 @@ export default class FilterToken extends PureComponent<Props> {
...this.props.filter.enum.map(({value, label}) => ({
label,
click: () => this.changeEnum(value),
type: 'checkbox',
type: 'checkbox' as 'checkbox',
checked: this.props.filter.value.indexOf(value) > -1,
})),
);
@@ -144,12 +151,14 @@ export default class FilterToken extends PureComponent<Props> {
);
}
const menu = electron.remote.Menu.buildFromTemplate(menuTemplate);
const {bottom, left} = this._ref ? this._ref.getBoundingClientRect() : {};
const {bottom, left} = this._ref.getBoundingClientRect();
menu.popup({
window: electron.remote.getCurrentWindow(),
// @ts-ignore: async is private API
async: true,
x: parseInt(left, 10),
y: parseInt(bottom, 10) + 8,
x: left,
y: bottom + 8,
});
};
@@ -185,7 +194,7 @@ export default class FilterToken extends PureComponent<Props> {
}
};
setRef = (ref: React.ElementRef<*>) => {
setRef = (ref: React.ReactInstance) => {
const element = findDOMNode(ref);
if (element instanceof HTMLElement) {
this._ref = element;

View File

@@ -5,20 +5,21 @@
* @format
*/
import type {Filter} from 'flipper';
import type {TableColumns} from '../table/types';
import {Filter} from '../filter/types';
import {TableColumns} from '../table/types';
import {PureComponent} from 'react';
import Toolbar from '../Toolbar.tsx';
import FlexRow from '../FlexRow.tsx';
import Input from '../Input.tsx';
import {colors} from '../colors.tsx';
import Text from '../Text.tsx';
import FlexBox from '../FlexBox.tsx';
import Glyph from '../Glyph.tsx';
import FilterToken from './FilterToken.js';
import styled from '../../styled/index.js';
import Toolbar from '../Toolbar';
import FlexRow from '../FlexRow';
import Input from '../Input';
import {colors} from '../colors';
import Text from '../Text';
import FlexBox from '../FlexBox';
import Glyph from '../Glyph';
import FilterToken from './FilterToken';
import styled from 'react-emotion';
import debounce from 'lodash.debounce';
import ToggleSwitch from '../ToggleSwitch.tsx';
import ToggleSwitch from '../ToggleSwitch';
import React from 'react';
const SearchBar = styled(Toolbar)({
height: 42,
@@ -35,22 +36,24 @@ export const SearchBox = styled(FlexBox)({
paddingLeft: 4,
});
export const SearchInput = styled(Input)(props => ({
border: props.focus ? '1px solid black' : 0,
...(props.regex ? {fontFamily: 'monospace'} : {}),
padding: 0,
fontSize: '1em',
flexGrow: 1,
height: 'auto',
lineHeight: '100%',
marginLeft: 2,
width: '100%',
color: props.regex && !props.isValidInput ? colors.red : colors.black,
'&::-webkit-input-placeholder': {
color: colors.placeholder,
fontWeight: 300,
},
}));
export const SearchInput = styled(Input)(
(props: {focus?: boolean; regex?: boolean; isValidInput?: boolean}) => ({
border: props.focus ? '1px solid black' : 0,
...(props.regex ? {fontFamily: 'monospace'} : {}),
padding: 0,
fontSize: '1em',
flexGrow: 1,
height: 'auto',
lineHeight: '100%',
marginLeft: 2,
width: '100%',
color: props.regex && !props.isValidInput ? colors.red : colors.black,
'&::-webkit-input-placeholder': {
color: colors.placeholder,
fontWeight: 300,
},
}),
);
const Clear = styled(Text)({
position: 'absolute',
@@ -83,34 +86,34 @@ const Actions = styled(FlexRow)({
flexShrink: 0,
});
export type SearchableProps = {|
addFilter: (filter: Filter) => void,
searchTerm: string,
filters: Array<Filter>,
allowRegexSearch?: boolean,
regexEnabled?: boolean,
|};
type Props = {|
placeholder?: string,
actions: React.Node,
tableKey: string,
columns?: TableColumns,
onFilterChange: (filters: Array<Filter>) => void,
defaultFilters: Array<Filter>,
allowRegexSearch: boolean,
|};
type State = {
filters: Array<Filter>,
focusedToken: number,
searchTerm: string,
hasFocus: boolean,
regexEnabled: boolean,
compiledRegex: ?RegExp,
export type SearchableProps = {
addFilter: (filter: Filter) => void;
searchTerm: string;
filters: Array<Filter>;
allowRegexSearch?: boolean;
regexEnabled?: boolean;
};
function compileRegex(s: string): ?RegExp {
type Props = {
placeholder?: string;
actions: React.ReactNode;
tableKey: string;
columns?: TableColumns;
onFilterChange: (filters: Array<Filter>) => void;
defaultFilters: Array<Filter>;
allowRegexSearch: boolean;
};
type State = {
filters: Array<Filter>;
focusedToken: number;
searchTerm: string;
hasFocus: boolean;
regexEnabled: boolean;
compiledRegex: RegExp | null | undefined;
};
function compileRegex(s: string): RegExp | null {
try {
return new RegExp(s);
} catch (e) {
@@ -135,7 +138,7 @@ const Searchable = (
compiledRegex: null,
};
_inputRef: ?HTMLInputElement;
_inputRef: HTMLInputElement | undefined;
componentDidMount() {
window.document.addEventListener('keydown', this.onKeyDown);
@@ -223,7 +226,7 @@ const Searchable = (
window.document.removeEventListener('keydown', this.onKeyDown);
}
getTableKey = (): ?string => {
getTableKey = (): string | null | undefined => {
if (this.props.tableKey) {
return this.props.tableKey;
} else if (this.props.columns) {
@@ -238,7 +241,7 @@ const Searchable = (
}
};
onKeyDown = (e: SyntheticKeyboardEvent<>) => {
onKeyDown = (e: KeyboardEvent) => {
const ctrlOrCmd = e =>
(e.metaKey && process.platform === 'darwin') ||
(e.ctrlKey && process.platform !== 'darwin');
@@ -252,11 +255,12 @@ const Searchable = (
this._inputRef.blur();
this.setState({searchTerm: ''});
} else if (e.key === 'Backspace' && this.hasFocus()) {
const lastFilter = this.state.filters[this.state.filters.length - 1];
if (
this.state.focusedToken === -1 &&
this.state.searchTerm === '' &&
this._inputRef &&
!this.state.filters[this.state.filters.length - 1].persistent
(lastFilter.type !== 'enum' || !lastFilter.persistent)
) {
this._inputRef.blur();
this.setState({focusedToken: this.state.filters.length - 1});
@@ -274,7 +278,7 @@ const Searchable = (
}
};
onChangeSearchTerm = (e: SyntheticInputEvent<HTMLInputElement>) => {
onChangeSearchTerm = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
searchTerm: e.target.value,
compiledRegex: compileRegex(e.target.value),
@@ -291,9 +295,9 @@ const Searchable = (
match.forEach((filter: string) => {
const separator =
filter.indexOf(':') > filter.indexOf('=') ? ':' : '=';
let [key, ...value] = filter.split(separator);
value = value.join(separator).trim();
let type = 'include';
let [key, ...values] = filter.split(separator);
let value = values.join(separator).trim();
let type: 'include' | 'exclude' | 'enum' = 'include';
// if value starts with !, it's an exclude filter
if (value.indexOf('!') === 0) {
type = 'exclude';
@@ -315,7 +319,7 @@ const Searchable = (
}
}, 200);
setInputRef = (ref: ?HTMLInputElement) => {
setInputRef = (ref: HTMLInputElement | undefined) => {
this._inputRef = ref;
};
@@ -326,12 +330,13 @@ const Searchable = (
if (filterIndex > -1) {
const filters = [...this.state.filters];
const defaultFilter: Filter = this.props.defaultFilters[filterIndex];
const filter = filters[filterIndex];
if (
defaultFilter != null &&
defaultFilter.type === 'enum' &&
filters[filterIndex].type === 'enum'
filter.type === 'enum'
) {
filters[filterIndex].enum = defaultFilter.enum;
filter.enum = defaultFilter.enum;
}
this.setState({filters});
// filter for this key already exists
@@ -339,7 +344,7 @@ const Searchable = (
}
// persistent filters are always at the front
const filters =
filter.persistent === true
filter.type === 'enum' && filter.persistent === true
? [filter, ...this.state.filters]
: this.state.filters.concat(filter);
this.setState({
@@ -397,14 +402,14 @@ const Searchable = (
clear = () =>
this.setState({
filters: this.state.filters.filter(
f => f.persistent != null && f.persistent === true,
f => f.type === 'enum' && f.persistent === true,
),
searchTerm: '',
});
getPersistKey = () => `SEARCHABLE_STORAGE_KEY_${this.getTableKey() || ''}`;
render(): React.Node {
render() {
const {placeholder, actions, ...props} = this.props;
return [
<SearchBar position="top" key="searchbar">
@@ -438,7 +443,7 @@ const Searchable = (
? this.state.compiledRegex !== null
: true
}
regex={this.state.regexEnabled && this.state.searchTerm}
regex={Boolean(this.state.regexEnabled && this.state.searchTerm)}
/>
</SearchBox>
{this.props.allowRegexSearch ? (

View File

@@ -5,26 +5,24 @@
* @format
*/
import type {ManagedTableProps, TableBodyRow, Filter} from 'flipper';
import type {SearchableProps} from './Searchable.js';
import {PureComponent} from 'react';
import ManagedTable from '../table/ManagedTable.js';
import textContent from '../../../utils/textContent.tsx';
import Searchable from './Searchable.js';
import {Filter} from '../filter/types';
import ManagedTable, {ManagedTableProps} from '../table/ManagedTable.js';
import {TableBodyRow} from '../table/types.js';
import Searchable, {SearchableProps} from './Searchable';
import React, {PureComponent} from 'react';
import textContent from '../../../utils/textContent';
import deepEqual from 'deep-equal';
type Props = {|
...ManagedTableProps,
...SearchableProps,
type Props = {
/** Reference to the table */
innerRef?: (ref: React.ElementRef<*>) => void,
innerRef?: (ref: React.RefObject<any>) => void;
/** Filters that are added to the filterbar by default */
defaultFilters: Array<Filter>,
|};
defaultFilters: Array<Filter>;
} & ManagedTableProps &
SearchableProps;
type State = {
filterRows: (row: TableBodyRow) => boolean,
filterRows: (row: TableBodyRow) => boolean;
};
const rowMatchesFilters = (filters: Array<Filter>, row: TableBodyRow) =>

View File

@@ -5,26 +5,26 @@
* @format
*/
import type {ManagedTableProps_immutable, TableBodyRow, Filter} from 'flipper';
import type {SearchableProps} from './Searchable.js';
import {Filter} from '../filter/types';
import {ManagedTableProps_immutable} from '../table/ManagedTable_immutable.js';
import {TableBodyRow} from '../table/types.js';
import Searchable, {SearchableProps} from './Searchable';
import {PureComponent} from 'react';
import ManagedTable_immutable from '../table/ManagedTable_immutable.js';
import textContent from '../../../utils/textContent.tsx';
import Searchable from './Searchable.js';
import textContent from '../../../utils/textContent';
import deepEqual from 'deep-equal';
import React from 'react';
type Props = {|
...ManagedTableProps_immutable,
...SearchableProps,
type Props = {
/** Reference to the table */
innerRef?: (ref: React.ElementRef<*>) => void,
innerRef?: (ref: React.RefObject<any>) => void;
/** Filters that are added to the filterbar by default */
defaultFilters: Array<Filter>,
|};
defaultFilters: Array<Filter>;
} & ManagedTableProps_immutable &
SearchableProps;
type State = {
filterRows: (row: TableBodyRow) => boolean,
filterRows: (row: TableBodyRow) => boolean;
};
const rowMatchesFilters = (filters: Array<Filter>, row: TableBodyRow) =>

View File

@@ -14,7 +14,7 @@ import type {
import React from 'react';
import FilterRow from '../filter/FilterRow.tsx';
import styled from '../../styled/index.js';
import styled from 'react-emotion';
import FlexRow from '../FlexRow.tsx';
import {colors} from '../colors.tsx';
import {normaliseColumnWidth} from './utils.js';

View File

@@ -154,14 +154,14 @@ export {
SearchInput,
SearchIcon,
default as Searchable,
} from './components/searchable/Searchable.js';
} from './components/searchable/Searchable.tsx';
export {
default as SearchableTable,
} from './components/searchable/SearchableTable.js';
} from './components/searchable/SearchableTable.tsx';
export {
default as SearchableTable_immutable,
} from './components/searchable/SearchableTable_immutable.js';
export type {SearchableProps} from './components/searchable/Searchable.js';
} from './components/searchable/SearchableTable_immutable.tsx';
export type {SearchableProps} from './components/searchable/Searchable.tsx';
//
export type {

View File

@@ -1062,6 +1062,11 @@
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.4.tgz#56eec47706f0fd0b7c694eae2f3172e6b0b769da"
integrity sha512-D9MyoQFI7iP5VdpEyPZyjjqIJ8Y8EDNQFIFVLOmeg1rI1xiHOChyUPMPRUVfqFCerxfE+yS3vMyj37F6IdtOoQ==
"@types/deep-equal@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-1.0.1.tgz#71cfabb247c22bcc16d536111f50c0ed12476b03"
integrity sha512-mMUu4nWHLBlHtxXY17Fg6+ucS/MnndyOWyOe7MmwkoMYxvfQU2ajtRaEvqSUv+aVkMqH/C0NCI8UoVfRNQ10yg==
"@types/eslint-visitor-keys@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
@@ -1131,6 +1136,18 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==
"@types/lodash.debounce@^4.0.6":
version "4.0.6"
resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.6.tgz#c5a2326cd3efc46566c47e4c0aa248dc0ee57d60"
integrity sha512-4WTmnnhCfDvvuLMaF3KV4Qfki93KebocUF45msxhYyjMttZDQYzHkO639ohhk8+oco2cluAFL3t5+Jn4mleylQ==
dependencies:
"@types/lodash" "*"
"@types/lodash@*":
version "4.14.136"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.136.tgz#413e85089046b865d960c9ff1d400e04c31ab60f"
integrity sha512-0GJhzBdvsW2RUccNHOBkabI8HZVdOXmXbXhuKlDEd5Vv12P7oAVGfomGp3Ne21o5D/qu1WmthlNKFaoZJJeErA==
"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"