From cd3035a6927d025b67d7ff3be0de3bd8e978a6a9 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Thu, 9 Jan 2020 19:14:34 -0800 Subject: [PATCH] feat: Add search input to collection and home (#1149) * feat: Add search input to collection and home * Tweak spacing * Add input to drafts/starred too --- app/components/Input.js | 45 +++++++++++-- app/components/InputSearch.js | 70 +++++++++++++++++++++ app/scenes/Collection.js | 10 ++- app/scenes/Dashboard.js | 4 ++ app/scenes/Drafts.js | 4 ++ app/scenes/Search/Search.js | 3 +- app/scenes/Search/components/SearchField.js | 11 +++- app/scenes/Starred.js | 4 ++ app/utils/routeHelpers.js | 11 +++- 9 files changed, 150 insertions(+), 12 deletions(-) create mode 100644 app/components/InputSearch.js diff --git a/app/components/Input.js b/app/components/Input.js index 7b5fe266..3fc6e582 100644 --- a/app/components/Input.js +++ b/app/components/Input.js @@ -9,7 +9,7 @@ import Flex from 'shared/components/Flex'; const RealTextarea = styled.textarea` border: 0; flex: 1; - padding: 8px 12px; + padding: 8px 12px 8px ${props => (props.hasIcon ? '8px' : '12px')}; outline: none; background: none; color: ${props => props.theme.text}; @@ -23,10 +23,11 @@ const RealTextarea = styled.textarea` const RealInput = styled.input` border: 0; flex: 1; - padding: 8px 12px; + padding: 8px 12px 8px ${props => (props.hasIcon ? '8px' : '12px')}; outline: none; background: none; color: ${props => props.theme.text}; + height: 30px; &:disabled, &::placeholder { @@ -45,10 +46,17 @@ const Wrapper = styled.div` max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : 'initial')}; `; +const IconWrapper = styled.span` + position: relative; + left: 4px; + width: 24px; + height: 24px; +`; + export const Outline = styled(Flex)` display: flex; flex: 1; - margin: 0 0 16px; + margin: ${props => (props.margin !== undefined ? props.margin : '0 0 16px')}; color: inherit; border-width: 1px; border-style: solid; @@ -60,6 +68,7 @@ export const Outline = styled(Flex)` : props.theme.inputBorder}; border-radius: 4px; font-weight: normal; + align-items: center; `; export const LabelText = styled.div` @@ -75,28 +84,49 @@ export type Props = { labelHidden?: boolean, flex?: boolean, short?: boolean, + margin?: string | number, + icon?: React.Node, + onFocus?: (ev: SyntheticEvent<>) => void, + onBlur?: (ev: SyntheticEvent<>) => void, }; @observer class Input extends React.Component { + input: ?HTMLInputElement; @observable focused: boolean = false; - handleBlur = () => { + handleBlur = (ev: SyntheticEvent<>) => { this.focused = false; + if (this.props.onBlur) { + this.props.onBlur(ev); + } }; - handleFocus = () => { + handleFocus = (ev: SyntheticEvent<>) => { this.focused = true; + if (this.props.onFocus) { + this.props.onFocus(ev); + } }; + focus() { + if (this.input) { + this.input.focus(); + } + } + render() { const { type = 'text', + icon, label, + margin, className, short, flex, labelHidden, + onFocus, + onBlur, ...rest } = this.props; @@ -112,11 +142,14 @@ class Input extends React.Component { ) : ( wrappedLabel ))} - + + {icon && {icon}} (this.input = ref)} onBlur={this.handleBlur} onFocus={this.handleFocus} type={type === 'textarea' ? undefined : type} + hasIcon={!!icon} {...rest} /> diff --git a/app/components/InputSearch.js b/app/components/InputSearch.js new file mode 100644 index 00000000..5406ac6b --- /dev/null +++ b/app/components/InputSearch.js @@ -0,0 +1,70 @@ +// @flow +import * as React from 'react'; +import keydown from 'react-keydown'; +import { observer } from 'mobx-react'; +import { observable } from 'mobx'; +import { withRouter, type RouterHistory } from 'react-router-dom'; +import { withTheme } from 'styled-components'; +import { SearchIcon } from 'outline-icons'; +import { searchUrl } from 'utils/routeHelpers'; +import Input from './Input'; + +type Props = { + history: RouterHistory, + theme: Object, + placeholder?: string, + collectionId?: string, +}; + +@observer +class InputSearch extends React.Component { + input: ?Input; + @observable focused: boolean = false; + + @keydown('meta+f') + focus(ev) { + ev.preventDefault(); + + if (this.input) { + this.input.focus(); + } + } + + handleSearchInput = ev => { + ev.preventDefault(); + this.props.history.push( + searchUrl(ev.target.value, this.props.collectionId) + ); + }; + + handleFocus = () => { + this.focused = true; + }; + + handleBlur = () => { + this.focused = false; + }; + + render() { + const { theme, placeholder = 'Search…' } = this.props; + + return ( + (this.input = ref)} + type="search" + placeholder={placeholder} + onInput={this.handleSearchInput} + icon={ + + } + onFocus={this.handleFocus} + onBlur={this.handleBlur} + margin={0} + /> + ); + } +} + +export default withTheme(withRouter(InputSearch)); diff --git a/app/scenes/Collection.js b/app/scenes/Collection.js index b215c241..60de729f 100644 --- a/app/scenes/Collection.js +++ b/app/scenes/Collection.js @@ -29,6 +29,7 @@ import Heading from 'components/Heading'; import Tooltip from 'components/Tooltip'; import CenteredContent from 'components/CenteredContent'; import { ListPlaceholder } from 'components/LoadingPlaceholder'; +import InputSearch from 'components/InputSearch'; import Mask from 'components/Mask'; import Button from 'components/Button'; import HelpText from 'components/HelpText'; @@ -114,12 +115,19 @@ class CollectionScene extends React.Component { }; renderActions() { - const can = this.props.policies.abilities(this.props.match.params.id); + const { match, policies } = this.props; + const can = policies.abilities(match.params.id); return ( {can.update && ( + + + { + + + diff --git a/app/scenes/Drafts.js b/app/scenes/Drafts.js index 5e7fe0a0..b4aa1394 100644 --- a/app/scenes/Drafts.js +++ b/app/scenes/Drafts.js @@ -9,6 +9,7 @@ import Empty from 'components/Empty'; import PageTitle from 'components/PageTitle'; import DocumentList from 'components/DocumentList'; import Subheading from 'components/Subheading'; +import InputSearch from 'components/InputSearch'; import NewDocumentMenu from 'menus/NewDocumentMenu'; import Actions, { Action } from 'components/Actions'; import DocumentsStore from 'stores/DocumentsStore'; @@ -42,6 +43,9 @@ class Drafts extends React.Component { )} + + + diff --git a/app/scenes/Search/Search.js b/app/scenes/Search/Search.js index cc9cbb38..3bf3e234 100644 --- a/app/scenes/Search/Search.js +++ b/app/scenes/Search/Search.js @@ -48,7 +48,8 @@ type Props = { class Search extends React.Component { firstDocument: ?DocumentPreview; - @observable query: string = ''; + @observable + query: string = decodeURIComponent(this.props.match.params.term || ''); @observable params: URLSearchParams = new URLSearchParams(); @observable offset: number = 0; @observable allowLoadMore: boolean = true; diff --git a/app/scenes/Search/components/SearchField.js b/app/scenes/Search/components/SearchField.js index cd36e6fa..5816dcce 100644 --- a/app/scenes/Search/components/SearchField.js +++ b/app/scenes/Search/components/SearchField.js @@ -6,12 +6,21 @@ import Flex from 'shared/components/Flex'; type Props = { onChange: string => void, + defaultValue?: string, theme: Object, }; class SearchField extends React.Component { input: ?HTMLInputElement; + componentDidMount() { + if (this.props && this.input) { + // ensure that focus is placed at end of input + const len = (this.props.defaultValue || '').length; + this.input.setSelectionRange(len, len); + } + } + handleChange = (ev: SyntheticEvent) => { this.props.onChange(ev.currentTarget.value ? ev.currentTarget.value : ''); }; @@ -34,7 +43,7 @@ class SearchField extends React.Component { ref={ref => (this.input = ref)} onChange={this.handleChange} spellCheck="false" - placeholder="search…" + placeholder="Search…" type="search" autoFocus /> diff --git a/app/scenes/Starred.js b/app/scenes/Starred.js index 7f02e430..566a47c8 100644 --- a/app/scenes/Starred.js +++ b/app/scenes/Starred.js @@ -8,6 +8,7 @@ import Empty from 'components/Empty'; import PageTitle from 'components/PageTitle'; import Heading from 'components/Heading'; import DocumentList from 'components/DocumentList'; +import InputSearch from 'components/InputSearch'; import Tabs from 'components/Tabs'; import Tab from 'components/Tab'; import NewDocumentMenu from 'menus/NewDocumentMenu'; @@ -62,6 +63,9 @@ class Starred extends React.Component { )} {showLoading && } + + + diff --git a/app/utils/routeHelpers.js b/app/utils/routeHelpers.js index e376709c..b557ba97 100644 --- a/app/utils/routeHelpers.js +++ b/app/utils/routeHelpers.js @@ -64,9 +64,14 @@ export function newDocumentUrl( return route; } -export function searchUrl(query?: string): string { - if (query) return `/search/${encodeURIComponent(query)}`; - return `/search`; +export function searchUrl(query?: string, collectionId?: string): string { + let route = '/search'; + if (query) route += `/${encodeURIComponent(query)}`; + + if (collectionId) { + route += `?collectionId=${collectionId}`; + } + return route; } export function notFoundUrl(): string {