From 62a204bdbed280a0193196fcbda5172ddef5417f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20B=C3=BCchele?= Date: Tue, 20 Aug 2019 05:40:31 -0700 Subject: [PATCH] Searchable Summary: _typescript_ Reviewed By: passy Differential Revision: D16807182 fbshipit-source-id: 68bc8365bc5b0d8075d0a93d5963c824c0d66769 --- package.json | 2 + src/index.js | 8 +- .../{FilterToken.js => FilterToken.tsx} | 131 ++++++++-------- .../{Searchable.js => Searchable.tsx} | 143 +++++++++--------- ...SearchableTable.js => SearchableTable.tsx} | 26 ++-- ...table.js => SearchableTable_immutable.tsx} | 24 +-- src/ui/components/table/TableRow.js | 2 +- src/ui/index.js | 8 +- yarn.lock | 17 +++ 9 files changed, 196 insertions(+), 165 deletions(-) rename src/ui/components/searchable/{FilterToken.js => FilterToken.tsx} (68%) rename src/ui/components/searchable/{Searchable.js => Searchable.tsx} (82%) rename src/ui/components/searchable/{SearchableTable.js => SearchableTable.tsx} (86%) rename src/ui/components/searchable/{SearchableTable_immutable.js => SearchableTable_immutable.tsx} (87%) diff --git a/package.json b/package.json index 7993daffd..04bec6217 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/index.js b/src/index.js index 8e5c056a4..7db5955d4 100644 --- a/src/index.js +++ b/src/index.js @@ -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, diff --git a/src/ui/components/searchable/FilterToken.js b/src/ui/components/searchable/FilterToken.tsx similarity index 68% rename from src/ui/components/searchable/FilterToken.js rename to src/ui/components/searchable/FilterToken.tsx index e35713e8d..641e97a8f 100644 --- a/src/ui/components/searchable/FilterToken.js +++ b/src/ui/components/searchable/FilterToken.tsx @@ -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 { - _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 { ...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 { ); } 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 { } }; - setRef = (ref: React.ElementRef<*>) => { + setRef = (ref: React.ReactInstance) => { const element = findDOMNode(ref); if (element instanceof HTMLElement) { this._ref = element; diff --git a/src/ui/components/searchable/Searchable.js b/src/ui/components/searchable/Searchable.tsx similarity index 82% rename from src/ui/components/searchable/Searchable.js rename to src/ui/components/searchable/Searchable.tsx index d0858830a..22d15c4f2 100644 --- a/src/ui/components/searchable/Searchable.js +++ b/src/ui/components/searchable/Searchable.tsx @@ -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, - allowRegexSearch?: boolean, - regexEnabled?: boolean, -|}; - -type Props = {| - placeholder?: string, - actions: React.Node, - tableKey: string, - columns?: TableColumns, - onFilterChange: (filters: Array) => void, - defaultFilters: Array, - allowRegexSearch: boolean, -|}; - -type State = { - filters: Array, - focusedToken: number, - searchTerm: string, - hasFocus: boolean, - regexEnabled: boolean, - compiledRegex: ?RegExp, +export type SearchableProps = { + addFilter: (filter: Filter) => void; + searchTerm: string; + filters: Array; + allowRegexSearch?: boolean; + regexEnabled?: boolean; }; -function compileRegex(s: string): ?RegExp { +type Props = { + placeholder?: string; + actions: React.ReactNode; + tableKey: string; + columns?: TableColumns; + onFilterChange: (filters: Array) => void; + defaultFilters: Array; + allowRegexSearch: boolean; +}; + +type State = { + filters: Array; + 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) => { + onChangeSearchTerm = (e: React.ChangeEvent) => { 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 [ @@ -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)} /> {this.props.allowRegexSearch ? ( diff --git a/src/ui/components/searchable/SearchableTable.js b/src/ui/components/searchable/SearchableTable.tsx similarity index 86% rename from src/ui/components/searchable/SearchableTable.js rename to src/ui/components/searchable/SearchableTable.tsx index e91a15829..ab1b1b1e0 100644 --- a/src/ui/components/searchable/SearchableTable.js +++ b/src/ui/components/searchable/SearchableTable.tsx @@ -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) => void; /** Filters that are added to the filterbar by default */ - defaultFilters: Array, -|}; + defaultFilters: Array; +} & ManagedTableProps & + SearchableProps; type State = { - filterRows: (row: TableBodyRow) => boolean, + filterRows: (row: TableBodyRow) => boolean; }; const rowMatchesFilters = (filters: Array, row: TableBodyRow) => diff --git a/src/ui/components/searchable/SearchableTable_immutable.js b/src/ui/components/searchable/SearchableTable_immutable.tsx similarity index 87% rename from src/ui/components/searchable/SearchableTable_immutable.js rename to src/ui/components/searchable/SearchableTable_immutable.tsx index 340fc7de3..a6f0c3150 100644 --- a/src/ui/components/searchable/SearchableTable_immutable.js +++ b/src/ui/components/searchable/SearchableTable_immutable.tsx @@ -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) => void; /** Filters that are added to the filterbar by default */ - defaultFilters: Array, -|}; + defaultFilters: Array; +} & ManagedTableProps_immutable & + SearchableProps; type State = { - filterRows: (row: TableBodyRow) => boolean, + filterRows: (row: TableBodyRow) => boolean; }; const rowMatchesFilters = (filters: Array, row: TableBodyRow) => diff --git a/src/ui/components/table/TableRow.js b/src/ui/components/table/TableRow.js index 52b376d0e..3471307f7 100644 --- a/src/ui/components/table/TableRow.js +++ b/src/ui/components/table/TableRow.js @@ -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'; diff --git a/src/ui/index.js b/src/ui/index.js index 94a395617..ec2e3c2a6 100644 --- a/src/ui/index.js +++ b/src/ui/index.js @@ -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 { diff --git a/yarn.lock b/yarn.lock index 6cf342b76..2d03aa820 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"