[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