[ARVADOS-WORKBENCH2] updated: 1.1.4-126-gac57fa7

Git user git at public.curoverse.com
Thu Jun 21 10:06:47 EDT 2018


Summary of changes:
 src/components/data-explorer/data-explorer.tsx     | 29 ++++++++++---
 .../search-input.test.tsx}                         | 50 +++++++++++-----------
 .../search-input.tsx}                              | 50 ++++++++++++----------
 .../project-explorer/project-explorer.tsx          |  8 ++++
 4 files changed, 84 insertions(+), 53 deletions(-)
 copy src/components/{search-bar/search-bar.test.tsx => search-input/search-input.test.tsx} (52%)
 copy src/components/{search-bar/search-bar.tsx => search-input/search-input.tsx} (63%)

       via  ac57fa77b04d290285388036f7929631e05965e4 (commit)
      from  f0c4e703bd7bdcf90265c96d6f714bf831f6947a (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit ac57fa77b04d290285388036f7929631e05965e4
Author: Michal Klobukowski <michal.klobukowski at contractors.roche.com>
Date:   Thu Jun 21 16:06:20 2018 +0200

    Add search input component
    
    Feature #13633
    
    Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski at contractors.roche.com>

diff --git a/src/components/data-explorer/data-explorer.tsx b/src/components/data-explorer/data-explorer.tsx
index 4486880..cf9886c 100644
--- a/src/components/data-explorer/data-explorer.tsx
+++ b/src/components/data-explorer/data-explorer.tsx
@@ -3,18 +3,21 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { Grid, Paper, Toolbar } from '@material-ui/core';
+import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, Theme, WithStyles } from '@material-ui/core';
 import ContextMenu, { ContextMenuActionGroup, ContextMenuAction } from "../../components/context-menu/context-menu";
 import ColumnSelector from "../../components/column-selector/column-selector";
 import DataTable from "../../components/data-table/data-table";
 import { mockAnchorFromMouseEvent } from "../../components/popover/helpers";
 import { DataColumn, toggleSortDirection } from "../../components/data-table/data-column";
 import { DataTableFilterItem } from '../../components/data-table-filters/data-table-filters';
+import SearchInput from '../search-input/search-input';
 
 interface DataExplorerProps<T> {
     items: T[];
     columns: Array<DataColumn<T>>;
     contextActions: ContextMenuActionGroup[];
+    searchValue: string;
+    onSearch: (value: string) => void;
     onRowClick: (item: T) => void;
     onColumnToggle: (column: DataColumn<T>) => void;
     onContextAction: (action: ContextMenuAction, item: T) => void;
@@ -29,7 +32,7 @@ interface DataExplorerState<T> {
     };
 }
 
-class DataExplorer<T> extends React.Component<DataExplorerProps<T>, DataExplorerState<T>> {
+class DataExplorer<T> extends React.Component<DataExplorerProps<T> & WithStyles<CssRules>, DataExplorerState<T>> {
     state: DataExplorerState<T> = {
         contextMenu: {}
     };
@@ -41,8 +44,13 @@ class DataExplorer<T> extends React.Component<DataExplorerProps<T>, DataExplorer
                 actions={this.props.contextActions}
                 onActionClick={this.callAction}
                 onClose={this.closeContextMenu} />
-            <Toolbar>
-                <Grid container justify="flex-end">
+            <Toolbar className={this.props.classes.toolbar}>
+                <Grid container justify="space-between" wrap="nowrap" alignItems="center">
+                    <div className={this.props.classes.searchBox}>
+                        <SearchInput
+                            value={this.props.searchValue}
+                            onSearch={this.props.onSearch} />
+                    </div>
                     <ColumnSelector
                         columns={this.props.columns}
                         onColumnToggle={this.props.onColumnToggle} />
@@ -83,4 +91,15 @@ class DataExplorer<T> extends React.Component<DataExplorerProps<T>, DataExplorer
 
 }
 
-export default DataExplorer;
+type CssRules = "searchBox" | "toolbar";
+
+const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
+    searchBox: {
+        paddingBottom: theme.spacing.unit * 2
+    },
+    toolbar: {
+        paddingTop: theme.spacing.unit * 2
+    }
+});
+
+export default withStyles(styles)(DataExplorer);
diff --git a/src/components/search-input/search-input.test.tsx b/src/components/search-input/search-input.test.tsx
new file mode 100644
index 0000000..b07445a
--- /dev/null
+++ b/src/components/search-input/search-input.test.tsx
@@ -0,0 +1,99 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { mount, configure } from "enzyme";
+import SearchInput, { DEFAULT_SEARCH_DEBOUNCE } from "./search-input";
+
+import * as Adapter from 'enzyme-adapter-react-16';
+
+configure({ adapter: new Adapter() });
+
+describe("<SearchInput />", () => {
+
+    jest.useFakeTimers();
+
+    let onSearch: () => void;
+
+    beforeEach(() => {
+        onSearch = jest.fn();
+    });
+
+    describe("on submit", () => {
+        it("calls onSearch with initial value passed via props", () => {
+            const searchInput = mount(<SearchInput value="initial value" onSearch={onSearch} />);
+            searchInput.find("form").simulate("submit");
+            expect(onSearch).toBeCalledWith("initial value");
+        });
+
+        it("calls onSearch with current value", () => {
+            const searchInput = mount(<SearchInput value="" onSearch={onSearch} />);
+            searchInput.find("input").simulate("change", { target: { value: "current value" } });
+            searchInput.find("form").simulate("submit");
+            expect(onSearch).toBeCalledWith("current value");
+        });
+
+        it("calls onSearch with new value passed via props", () => {
+            const searchInput = mount(<SearchInput value="" onSearch={onSearch} />);
+            searchInput.find("input").simulate("change", { target: { value: "current value" } });
+            searchInput.setProps({value: "new value"});
+            searchInput.find("form").simulate("submit");
+            expect(onSearch).toBeCalledWith("new value");
+        });
+
+        it("cancels timeout set on input value change", () => {
+            const searchInput = mount(<SearchInput value="" onSearch={onSearch} debounce={1000} />);
+            searchInput.find("input").simulate("change", { target: { value: "current value" } });
+            searchInput.find("form").simulate("submit");
+            jest.advanceTimersByTime(1000);
+            expect(onSearch).toHaveBeenCalledTimes(1);
+            expect(onSearch).toBeCalledWith("current value");
+        });
+
+    });
+
+    describe("on input value change", () => {
+        it("calls onSearch after default timeout", () => {
+            const searchInput = mount(<SearchInput value="" onSearch={onSearch} />);
+            searchInput.find("input").simulate("change", { target: { value: "current value" } });
+            expect(onSearch).not.toBeCalled();
+            jest.advanceTimersByTime(DEFAULT_SEARCH_DEBOUNCE);
+            expect(onSearch).toBeCalledWith("current value");
+        });
+        
+        it("calls onSearch after the time specified in props has passed", () => {
+            const searchInput = mount(<SearchInput value="" onSearch={onSearch} debounce={2000}/>);
+            searchInput.find("input").simulate("change", { target: { value: "current value" } });
+            jest.advanceTimersByTime(1000);
+            expect(onSearch).not.toBeCalled();
+            jest.advanceTimersByTime(1000);
+            expect(onSearch).toBeCalledWith("current value");
+        });
+        
+        it("calls onSearch only once after no change happened during the specified time", () => {
+            const searchInput = mount(<SearchInput value="" onSearch={onSearch} debounce={1000}/>);
+            searchInput.find("input").simulate("change", { target: { value: "current value" } });
+            jest.advanceTimersByTime(500);
+            searchInput.find("input").simulate("change", { target: { value: "changed value" } });
+            jest.advanceTimersByTime(1000);
+            expect(onSearch).toHaveBeenCalledTimes(1);
+        });
+        
+        it("calls onSearch again after the specified time has passed since previous call", () => {
+            const searchInput = mount(<SearchInput value="" onSearch={onSearch} debounce={1000}/>);
+            searchInput.find("input").simulate("change", { target: { value: "current value" } });
+            jest.advanceTimersByTime(500);
+            searchInput.find("input").simulate("change", { target: { value: "intermediate value" } });
+            jest.advanceTimersByTime(1000);
+            expect(onSearch).toBeCalledWith("intermediate value");
+            searchInput.find("input").simulate("change", { target: { value: "latest value" } });
+            jest.advanceTimersByTime(1000);
+            expect(onSearch).toBeCalledWith("latest value");
+            expect(onSearch).toHaveBeenCalledTimes(2);
+            
+        });
+
+    });
+
+});
\ No newline at end of file
diff --git a/src/components/search-input/search-input.tsx b/src/components/search-input/search-input.tsx
new file mode 100644
index 0000000..edc82d5
--- /dev/null
+++ b/src/components/search-input/search-input.tsx
@@ -0,0 +1,113 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { IconButton, Paper, StyleRulesCallback, withStyles, WithStyles, FormControl, InputLabel, Input, InputAdornment, FormHelperText } from '@material-ui/core';
+import SearchIcon from '@material-ui/icons/Search';
+
+interface SearchInputDataProps {
+    value: string;
+}
+
+interface SearchInputActionProps {
+    onSearch: (value: string) => any;
+    debounce?: number;
+}
+
+type SearchInputProps = SearchInputDataProps & SearchInputActionProps & WithStyles<CssRules>;
+
+interface SearchInputState {
+    value: string;
+}
+
+export const DEFAULT_SEARCH_DEBOUNCE = 1000;
+
+class SearchInput extends React.Component<SearchInputProps> {
+
+    state: SearchInputState = {
+        value: ""
+    };
+
+    timeout: number;
+
+    render() {
+        const { classes } = this.props;
+        return <form onSubmit={this.handleSubmit}>
+            <FormControl>
+                <InputLabel>Search</InputLabel>
+                <Input
+                    type="text"
+                    value={this.state.value}
+                    onChange={this.handleChange}
+                    endAdornment={
+                        <InputAdornment position="end">
+                            <IconButton
+                                onClick={this.handleSubmit}>
+                                <SearchIcon />
+                            </IconButton>
+                        </InputAdornment>
+                    } />
+            </FormControl>
+        </form>;
+    }
+
+    componentDidMount() {
+        this.setState({ value: this.props.value });
+    }
+
+    componentWillReceiveProps(nextProps: SearchInputProps) {
+        if (nextProps.value !== this.props.value) {
+            this.setState({ value: nextProps.value });
+        }
+    }
+
+    componentWillUnmount() {
+        clearTimeout(this.timeout);
+    }
+
+    handleSubmit = (event: React.FormEvent<HTMLElement>) => {
+        event.preventDefault();
+        clearTimeout(this.timeout);
+        this.props.onSearch(this.state.value);
+    }
+
+    handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+        clearTimeout(this.timeout);
+        this.setState({ value: event.target.value });
+        this.timeout = window.setTimeout(
+            () => this.props.onSearch(this.state.value),
+            this.props.debounce || DEFAULT_SEARCH_DEBOUNCE
+        );
+
+    }
+
+}
+
+type CssRules = 'container' | 'input' | 'button';
+
+const styles: StyleRulesCallback<CssRules> = theme => {
+    return {
+        container: {
+            position: 'relative',
+            width: '100%'
+        },
+        input: {
+            border: 'none',
+            borderRadius: theme.spacing.unit / 4,
+            boxSizing: 'border-box',
+            padding: theme.spacing.unit,
+            paddingRight: theme.spacing.unit * 4,
+            width: '100%',
+        },
+        button: {
+            position: 'absolute',
+            top: theme.spacing.unit / 2,
+            right: theme.spacing.unit / 2,
+            width: theme.spacing.unit * 3,
+            height: theme.spacing.unit * 3
+        }
+    };
+};
+
+export default withStyles(styles)(SearchInput);
\ No newline at end of file
diff --git a/src/views-components/project-explorer/project-explorer.tsx b/src/views-components/project-explorer/project-explorer.tsx
index 3fac6df..4757440 100644
--- a/src/views-components/project-explorer/project-explorer.tsx
+++ b/src/views-components/project-explorer/project-explorer.tsx
@@ -27,10 +27,12 @@ interface ProjectExplorerProps {
 
 interface ProjectExplorerState {
     columns: Array<DataColumn<ProjectExplorerItem>>;
+    searchValue: string;
 }
 
 class ProjectExplorer extends React.Component<ProjectExplorerProps, ProjectExplorerState> {
     state: ProjectExplorerState = {
+        searchValue: "",
         columns: [{
             name: "Name",
             selected: true,
@@ -103,10 +105,12 @@ class ProjectExplorer extends React.Component<ProjectExplorerProps, ProjectExplo
             items={this.props.items}
             columns={this.state.columns}
             contextActions={this.contextMenuActions}
+            searchValue={this.state.searchValue}
             onColumnToggle={this.toggleColumn}
             onFiltersChange={this.changeFilters}
             onRowClick={console.log}
             onSortToggle={this.toggleSort}
+            onSearch={this.search}
             onContextAction={this.executeAction} />;
     }
 
@@ -143,6 +147,10 @@ class ProjectExplorer extends React.Component<ProjectExplorerProps, ProjectExplo
     executeAction = (action: ContextMenuAction, item: ProjectExplorerItem) => {
         alert(`Executing ${action.name} on ${item.name}`);
     }
+
+    search = (searchValue: string) => {
+        this.setState({ searchValue });
+    }
 }
 
 const renderName = (item: ProjectExplorerItem) =>

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list