refactor search fields

pull/1754/head
gabrielburnworth 2020-04-13 18:15:11 -07:00
parent 73422eb8ea
commit 3c3b120b9b
34 changed files with 244 additions and 222 deletions

View File

@ -120,46 +120,57 @@
.thin-search-wrapper {
width: 100%;
.text-input-wrapper {
position: relative;
margin: 1rem;
border-bottom: 1px solid $dark_gray;
&:before,
&:after {
content: "";
.thin-search {
.spinner-container {
position: absolute;
bottom: 0;
background: $dark_gray;
width: 1px;
height: 3px;
}
&:before {
left: 0;
}
&:after {
top: 0;
right: 0;
width: 2rem;
height: 2rem;
padding: 0;
margin-right: 1rem;
}
i {
font-size: 1.5rem;
.text-input-wrapper {
position: relative;
margin: 1rem;
border-bottom: 1px solid $dark_gray;
&:before,
&:after {
content: "";
position: absolute;
bottom: 0;
background: $dark_gray;
width: 1px;
height: 3px;
}
&:before {
left: 0;
}
&:after {
right: 0;
}
i {
font-size: 1.5rem;
}
.fa-search {
position: absolute;
top: 0.8rem;
left: 1rem;
cursor: default !important;
}
}
.fa-search {
position: absolute;
top: 0.8rem;
left: 1rem;
cursor: default !important;
}
}
input {
background: transparent;
box-shadow: none !important;
padding-left: 3rem !important;
font-size: 1.4rem !important;
&:active,
&:focus {
background: transparent !important;
}
&::-webkit-input-placeholder {
color: $placeholder_gray;
input {
background: transparent;
box-shadow: none !important;
padding-left: 3rem !important;
font-size: 1.4rem !important;
&:active,
&:focus {
background: transparent !important;
}
&::-webkit-input-placeholder {
color: $placeholder_gray;
}
}
}
}
@ -289,18 +300,6 @@
}
}
.thin-search {
.spinner-container {
position: absolute;
top: 0;
right: 0;
width: 2rem;
height: 2rem;
padding: 0;
margin-right: 1rem;
}
}
.hovered-plant-copy {
cursor: pointer;
transform-origin: center;

View File

@ -925,6 +925,10 @@
display: inline;
text-transform: uppercase;
}
input[type="text"] {
width: 50%;
height: 2rem;
}
}
.point-type-checkboxes {
.point-type-section {
@ -1043,6 +1047,11 @@
margin-left: 1rem;
font-size: 1.4rem;
}
.filter-search {
.bp3-popover-wrapper {
margin-left: 0;
}
}
.row {
margin-left: 0;
}
@ -1074,7 +1083,7 @@
}
p {
display: inline;
margin-left: 1rem;
margin-left: 0.5rem;
}
}
}

View File

@ -81,21 +81,12 @@ interface DesignerPanelTopProps {
onClick?(): void;
title?: string;
children?: React.ReactNode;
noIcon?: boolean;
}
export const DesignerPanelTop = (props: DesignerPanelTopProps) => {
const withBtn = !!props.linkTo || !!props.onClick;
return <div className={`panel-top ${withBtn ? "with-button" : ""}`}>
<div className="thin-search-wrapper">
<div className="text-input-wrapper">
{!props.noIcon &&
<i className="fa fa-search" />}
<ErrorBoundary>
{props.children}
</ErrorBoundary>
</div>
</div>
{props.children}
{props.onClick &&
<a>
<div className={`fb-button panel-${TAB_COLOR[props.panel]}`}

View File

@ -16,6 +16,7 @@ import {
} from "../../ui/empty_state_wrapper";
import { some, uniq, map, sortBy } from "lodash";
import { t } from "../../i18next_wrapper";
import { SearchField } from "../../ui/search_field";
const filterSearch = (term: string) => (item: CalendarOccurrence) =>
item.heading.toLowerCase().includes(term)
@ -105,14 +106,12 @@ export class PureFarmEvents
<DesignerPanelTop
panel={Panel.FarmEvents}
linkTo={"/app/designer/events/add"}
title={t("Add event")}
noIcon={true}>
<i className="fa fa-calendar" onClick={this.resetCalendar} />
<input
name="searchTerm"
value={this.state.searchTerm}
onChange={e => this.setState({ searchTerm: e.currentTarget.value })}
placeholder={t("Search your events...")} />
title={t("Add event")}>
<SearchField searchTerm={this.state.searchTerm}
customLeftIcon={
<i className="fa fa-calendar" onClick={this.resetCalendar} />}
placeholder={t("Search your events...")}
onChange={searchTerm => this.setState({ searchTerm })} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"farm-event"}>
<div className="farm-events">

View File

@ -4,11 +4,6 @@ jest.mock("../../../history", () => ({
history: { getCurrentLocation: () => ({ pathname: mockPath }) }
}));
let mockGardenOpen = true;
jest.mock("../../saved_gardens/saved_gardens", () => ({
savedGardenOpen: () => mockGardenOpen,
}));
import {
round,
translateScreenToGarden,
@ -23,6 +18,7 @@ import {
cursorAtPlant,
allowInteraction,
allowGroupAreaInteraction,
savedGardenOpen,
} from "../util";
import { McuParams } from "farmbot";
import {
@ -374,17 +370,22 @@ describe("getMode()", () => {
expect(getMode()).toEqual(Mode.weeds);
mockPath = "/app/designer/weeds/add";
expect(getMode()).toEqual(Mode.createWeed);
mockPath = "/app/designer/gardens";
mockGardenOpen = true;
mockPath = "/app/designer/gardens/1";
expect(getMode()).toEqual(Mode.templateView);
mockPath = "/app/designer/groups/1";
expect(getMode()).toEqual(Mode.editGroup);
mockPath = "";
mockGardenOpen = false;
expect(getMode()).toEqual(Mode.none);
});
});
describe("savedGardenOpen", () => {
it("is open", () => {
const result = savedGardenOpen(["", "", "", "gardens", "4", ""]);
expect(result).toEqual(4);
});
});
describe("getGardenCoordinates()", () => {
beforeEach(() => {
Object.defineProperty(document, "querySelector", {

View File

@ -7,7 +7,6 @@ import {
} from "./interfaces";
import { trim } from "../../util";
import { history, getPathArray } from "../../history";
import { savedGardenOpen } from "../saved_gardens/saved_gardens";
/*
* Farm Designer Map Utilities
@ -293,6 +292,7 @@ export const getMode = (): Mode => {
if ((pathArray[3] === "groups" || pathArray[3] === "zones")
&& pathArray[4]) { return Mode.editGroup; }
if (pathArray[6] === "add") { return Mode.clickToAdd; }
if (savedGardenOpen(pathArray)) { return Mode.templateView; }
if (!isNaN(parseInt(pathArray.slice(-1)[0]))) { return Mode.editPlant; }
if (pathArray[5] === "edit") { return Mode.editPlant; }
if (pathArray[6] === "edit") { return Mode.editPlant; }
@ -307,11 +307,15 @@ export const getMode = (): Mode => {
if (pathArray[4] === "add") { return Mode.createWeed; }
return Mode.weeds;
}
if (savedGardenOpen(pathArray)) { return Mode.templateView; }
}
return Mode.none;
};
/** Check if a SavedGarden is currently open (URL approach). */
export const savedGardenOpen = (pathArray: string[]) =>
pathArray[3] === "gardens" && parseInt(pathArray[4]) > 0
? parseInt(pathArray[4]) : false;
export const getZoomLevelFromMap = (map: Element) =>
parseFloat((window.getComputedStyle(map).transform || "(1").split("(")[1]);

View File

@ -18,6 +18,7 @@ import { history } from "../../../history";
import {
fakeCropLiveSearchResult,
} from "../../../__test_support__/fake_crop_search_result";
import { SearchField } from "../../../ui/search_field";
describe("<CropCatalog />", () => {
const fakeProps = (): CropCatalogProps => {
@ -40,9 +41,7 @@ describe("<CropCatalog />", () => {
it("handles search term change", () => {
const p = fakeProps();
const wrapper = shallow(<CropCatalog {...p} />);
wrapper.find("input").first().simulate("change", {
currentTarget: { value: "apple" }
});
wrapper.find(SearchField).simulate("change", "apple");
expect(p.dispatch).toHaveBeenCalledWith({
payload: "apple",
type: Actions.SEARCH_QUERY_CHANGE
@ -69,7 +68,7 @@ describe("<CropCatalog />", () => {
p.cropSearchQuery = "abc";
p.cropSearchInProgress = true;
p.cropSearchResults = [fakeCropLiveSearchResult()];
const wrapper = shallow(<CropCatalog {...p} />);
expect(wrapper.find("Spinner").length).toEqual(1);
const wrapper = mount(<CropCatalog {...p} />);
expect(wrapper.find(".spinner").length).toEqual(1);
});
});

View File

@ -9,6 +9,7 @@ import {
import { mount, shallow } from "enzyme";
import { fakePlant } from "../../../__test_support__/fake_state/resources";
import { fakeState } from "../../../__test_support__/fake_state";
import { SearchField } from "../../../ui/search_field";
describe("<PlantInventory />", () => {
const fakeProps = (): PlantInventoryProps => ({
@ -31,11 +32,10 @@ describe("<PlantInventory />", () => {
expect(wrapper.html()).toContain("/app/designer/plants/crop_search");
});
it("updates search term", () => {
it("changes search term", () => {
const wrapper = shallow<Plants>(<Plants {...fakeProps()} />);
expect(wrapper.state().searchTerm).toEqual("");
wrapper.find("input").first().simulate("change",
{ currentTarget: { value: "mint" } });
wrapper.find(SearchField).simulate("change", "mint");
expect(wrapper.state().searchTerm).toEqual("mint");
});
});

View File

@ -15,6 +15,7 @@ import {
} from "../designer_panel";
import { t } from "../../i18next_wrapper";
import { Panel } from "../panel_header";
import { SearchField } from "../../ui/search_field";
export function mapStateToProps(props: Everything): CropCatalogProps {
const { cropSearchQuery, cropSearchInProgress, cropSearchResults
@ -34,8 +35,7 @@ export class RawCropCatalog extends React.Component<CropCatalogProps, {}> {
this.props.openfarmSearch(searchTerm)(this.props.dispatch);
}, 500);
handleChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
const { value } = e.currentTarget;
handleChange = (value: string) => {
this.props.dispatch({ type: Actions.SEARCH_QUERY_CHANGE, payload: value });
this.debouncedOFSearch(value);
}
@ -67,18 +67,14 @@ export class RawCropCatalog extends React.Component<CropCatalogProps, {}> {
title={t("Choose a crop")}
backTo={"/app/designer/plants"} />
<DesignerPanelTop panel={Panel.Plants}>
<div className="thin-search">
<input
autoFocus={true}
value={this.props.cropSearchQuery}
onChange={this.handleChange}
onKeyPress={this.handleChange}
className="search"
name="searchTerm"
placeholder={t("Search OpenFarm...")} />
{this.showResultChangeSpinner &&
<Spinner radius={10} strokeWidth={3} />}
</div>
<SearchField
searchTerm={this.props.cropSearchQuery}
placeholder={t("Search OpenFarm...")}
onChange={this.handleChange}
onKeyPress={this.handleChange}
autoFocus={true}
customRightIcon={this.showResultChangeSpinner ?
<Spinner radius={10} strokeWidth={3} /> : undefined} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"crop-catalog"}>
<div className="crop-search-result-wrapper row">

View File

@ -13,6 +13,7 @@ import {
DesignerPanel, DesignerPanelContent, DesignerPanelTop,
} from "../designer_panel";
import { t } from "../../i18next_wrapper";
import { SearchField } from "../../ui/search_field";
export interface PlantInventoryProps {
plants: TaggedPlant[];
@ -34,12 +35,8 @@ export function mapStateToProps(props: Everything): PlantInventoryProps {
}
export class RawPlants extends React.Component<PlantInventoryProps, State> {
state: State = { searchTerm: "" };
update = ({ currentTarget }: React.SyntheticEvent<HTMLInputElement>) =>
this.setState({ searchTerm: currentTarget.value })
render() {
return <DesignerPanel panelName={"plant-inventory"} panel={Panel.Plants}>
<DesignerNavTabs />
@ -47,8 +44,9 @@ export class RawPlants extends React.Component<PlantInventoryProps, State> {
panel={Panel.Plants}
linkTo={"/app/designer/plants/crop_search"}
title={t("Add plant")}>
<input type="text" onChange={this.update} name="searchTerm"
placeholder={t("Search your plants...")} />
<SearchField searchTerm={this.state.searchTerm}
placeholder={t("Search your plants...")}
onChange={searchTerm => this.setState({ searchTerm })} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"plant"}>
<EmptyStateWrapper

View File

@ -22,6 +22,7 @@ import {
} from "../../../__test_support__/resource_index_builder";
import { createGroup } from "../actions";
import { DesignerPanelTop } from "../../designer_panel";
import { SearchField } from "../../../ui/search_field";
describe("<GroupListPanel />", () => {
const fakeProps = (): GroupListPanelProps => {
@ -55,8 +56,7 @@ describe("<GroupListPanel />", () => {
it("changes search term", () => {
const p = fakeProps();
const wrapper = shallow<GroupListPanel>(<GroupListPanel {...p} />);
wrapper.find("input").first().simulate("change",
{ currentTarget: { value: "one" } });
wrapper.find(SearchField).simulate("change", "one");
expect(wrapper.state().searchTerm).toEqual("one");
});

View File

@ -95,7 +95,7 @@ describe("<GroupPointCountBreakdown />", () => {
p.pointsSelectedByGroup = [point1, point2, point3];
p.group.body.point_ids = [1];
const wrapper = mount(<GroupPointCountBreakdown {...p} />);
["1manually selected", "2selected by filters"].map(string =>
["1 manually selected", "2 selected by filters"].map(string =>
expect(wrapper.text()).toContain(string));
});
@ -108,7 +108,7 @@ describe("<GroupPointCountBreakdown />", () => {
p.pointsSelectedByGroup = [point1, point2];
p.group.body.point_ids = [];
const wrapper = mount(<GroupPointCountBreakdown {...p} />);
["0manually selected", "2selected by filters"].map(string =>
["0 manually selected", "2 selected by filters"].map(string =>
expect(wrapper.text()).toContain(string));
});

View File

@ -153,10 +153,7 @@ export const GroupPointCountBreakdown =
dispatch={props.dispatch} />;
return <div className={"group-member-count-breakdown"}>
<div className={"manual-group-member-count"}>
<div className={"manual-selection-count"}>
{manualPoints.length}
</div>
<p>{t("manually selected")}</p>
<p>{`${manualPoints.length} ${t("manually selected")}`}</p>
<ClearPointIds dispatch={props.dispatch} group={props.group} />
</div>
{props.iconDisplay && manualPoints.length > 0 &&
@ -166,10 +163,7 @@ export const GroupPointCountBreakdown =
{props.shouldDisplay(Feature.criteria_groups) &&
<div className={"group-member-section"}>
<div className={"criteria-group-member-count"}>
<div className={"criteria-selection-count"}>
{criteriaPoints.length}
</div>
<p>{t("selected by filters")}</p>
<p>{`${criteriaPoints.length} ${t("selected by filters")}`}</p>
<ClearCriteria dispatch={props.dispatch} group={props.group} />
</div>
{props.iconDisplay && criteriaPoints.length > 0 &&
@ -186,7 +180,7 @@ export const PointTypeSelection = (props: PointTypeSelectionProps) =>
<div className={"point-type-selection"}>
<p className={"category"}>{t("Select all")}</p>
<FBSelect
key={JSON.stringify(props.group.body.criteria)}
key={JSON.stringify(props.group.body)}
list={POINTER_TYPE_LIST().slice(0, -1)}
customNullLabel={t("Select one")}
selectedItem={props.pointTypes[0]

View File

@ -117,7 +117,7 @@ export const DaySelection = (props: DaySelectionProps) => {
</div>}
<Row>
<Col xs={5}>
<FBSelect key={JSON.stringify(criteria)}
<FBSelect key={JSON.stringify(group.body)}
list={[DAY_OPERATOR_DDI_LOOKUP()["<"],
DAY_OPERATOR_DDI_LOOKUP()[">"]]}
selectedItem={noDayCriteria

View File

@ -16,6 +16,7 @@ import {
import { Content } from "../../constants";
import { selectAllActivePoints } from "../../resources/selectors";
import { createGroup } from "./actions";
import { SearchField } from "../../ui/search_field";
export interface GroupListPanelProps {
dispatch: Function;
@ -39,10 +40,6 @@ export class RawGroupListPanel
extends React.Component<GroupListPanelProps, State> {
state: State = { searchTerm: "" };
update = ({ currentTarget }: React.SyntheticEvent<HTMLInputElement>) => {
this.setState({ searchTerm: currentTarget.value });
}
navigate = (id: number) => history.push(`/app/designer/groups/${id}`);
render() {
@ -52,10 +49,9 @@ export class RawGroupListPanel
panel={Panel.Groups}
onClick={() => this.props.dispatch(createGroup({ pointUuids: [] }))}
title={t("Add group")}>
<input type="text"
name="searchTerm"
onChange={this.update}
placeholder={t("Search your groups...")} />
<SearchField searchTerm={this.state.searchTerm}
placeholder={t("Search your groups...")}
onChange={searchTerm => this.setState({ searchTerm })} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"groups"}>
<EmptyStateWrapper

View File

@ -13,6 +13,7 @@ import {
buildResourceIndex,
} from "../../../__test_support__/resource_index_builder";
import { mapStateToProps } from "../point_inventory";
import { SearchField } from "../../../ui/search_field";
describe("<Points> />", () => {
const fakeProps = (): PointsProps => ({
@ -48,8 +49,7 @@ describe("<Points> />", () => {
p.genericPoints[0].body.name = "point 0";
p.genericPoints[1].body.name = "point 1";
const wrapper = shallow<Points>(<Points {...p} />);
wrapper.find("input").first().simulate("change",
{ currentTarget: { value: "0" } });
wrapper.find(SearchField).simulate("change", "0");
expect(wrapper.state().searchTerm).toEqual("0");
});

View File

@ -56,6 +56,7 @@ export class RawEditPoint extends React.Component<EditPointProps, {}> {
switch (key) {
case "color":
case "created_by":
case "removal_method":
case "type":
return <div key={key}
className={`meta-${key}-not-displayed`} />;

View File

@ -13,6 +13,7 @@ import {
import { selectAllGenericPointers } from "../../resources/selectors";
import { TaggedGenericPointer } from "farmbot";
import { t } from "../../i18next_wrapper";
import { SearchField } from "../../ui/search_field";
export interface PointsProps {
genericPoints: TaggedGenericPointer[];
@ -37,10 +38,6 @@ export function mapStateToProps(props: Everything): PointsProps {
export class RawPoints extends React.Component<PointsProps, PointsState> {
state: PointsState = { searchTerm: "" };
update = ({ currentTarget }: React.SyntheticEvent<HTMLInputElement>) => {
this.setState({ searchTerm: currentTarget.value });
}
render() {
return <DesignerPanel panelName={"point-inventory"} panel={Panel.Points}>
<DesignerNavTabs />
@ -48,8 +45,9 @@ export class RawPoints extends React.Component<PointsProps, PointsState> {
panel={Panel.Points}
linkTo={"/app/designer/points/add"}
title={t("Add point")}>
<input type="text" onChange={this.update} name="searchTerm"
placeholder={t("Search your points...")} />
<SearchField searchTerm={this.state.searchTerm}
placeholder={t("Search your points...")}
onChange={searchTerm => this.setState({ searchTerm })} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"points"}>
<EmptyStateWrapper

View File

@ -16,8 +16,7 @@ jest.mock("../../../api/crud", () => ({ edit: jest.fn() }));
import * as React from "react";
import { mount, shallow } from "enzyme";
import {
RawSavedGardens as SavedGardens, mapStateToProps,
SavedGardenHUD, savedGardenOpen,
RawSavedGardens as SavedGardens, mapStateToProps, SavedGardenHUD,
} from "../saved_gardens";
import { clickButton } from "../../../__test_support__/helpers";
import {
@ -31,6 +30,7 @@ import {
import { SavedGardensProps } from "../interfaces";
import { closeSavedGarden } from "../actions";
import { Actions } from "../../../constants";
import { SearchField } from "../../../ui/search_field";
describe("<SavedGardens />", () => {
const fakeProps = (): SavedGardensProps => ({
@ -60,8 +60,7 @@ describe("<SavedGardens />", () => {
it("changes search term", () => {
const wrapper = shallow<SavedGardens>(<SavedGardens {...fakeProps()} />);
expect(wrapper.state().searchTerm).toEqual("");
wrapper.find("input").first().simulate("change",
{ currentTarget: { value: "spring" } });
wrapper.find(SearchField).simulate("change", "spring");
expect(wrapper.state().searchTerm).toEqual("spring");
});
@ -99,13 +98,6 @@ describe("mapStateToProps()", () => {
});
});
describe("savedGardenOpen", () => {
it("is open", () => {
const result = savedGardenOpen(["", "", "", "gardens", "4", ""]);
expect(result).toEqual(4);
});
});
describe("<SavedGardenHUD />", () => {
it("renders", () => {
const wrapper = mount(<SavedGardenHUD dispatch={jest.fn()} />);

View File

@ -18,6 +18,7 @@ import {
EmptyStateWrapper, EmptyStateGraphic,
} from "../../ui/empty_state_wrapper";
import { Content } from "../../constants";
import { SearchField } from "../../ui/search_field";
export const mapStateToProps = (props: Everything): SavedGardensProps => ({
savedGardens: selectAllSavedGardens(props.resources.index),
@ -35,9 +36,6 @@ export class RawSavedGardens
unselectPlant(this.props.dispatch)();
}
onChange = (e: React.SyntheticEvent<HTMLInputElement>) =>
this.setState({ searchTerm: e.currentTarget.value });
render() {
return <DesignerPanel panelName={"saved-garden"} panel={Panel.SavedGardens}>
<DesignerNavTabs />
@ -46,8 +44,9 @@ export class RawSavedGardens
panel={Panel.SavedGardens}
linkTo={"/app/designer/gardens/add"}
title={t("Add garden")}>
<input type="text" onChange={this.onChange} name="searchTerm"
placeholder={t("Search your gardens...")} />
<SearchField searchTerm={this.state.searchTerm}
placeholder={t("Search your gardens...")}
onChange={searchTerm => this.setState({ searchTerm })} />
</DesignerPanelTop>
<EmptyStateWrapper
notEmpty={this.props.savedGardens.length > 0}
@ -62,11 +61,6 @@ export class RawSavedGardens
}
}
/** Check if a SavedGarden is currently open (URL approach). */
export const savedGardenOpen = (pathArray: string[]) =>
pathArray[3] === "gardens" && parseInt(pathArray[4]) > 0
? parseInt(pathArray[4]) : false;
/** Sticky an indicator and actions menu when a SavedGarden is open. */
export const SavedGardenHUD = (props: { dispatch: Function }) =>
<div className="saved-garden-indicator">

View File

@ -34,6 +34,7 @@ import { edit, save } from "../../../api/crud";
import { ToolSelection } from "../tool_slot_edit_components";
import { ToolsProps } from "../interfaces";
import { mapPointClickAction } from "../../map/actions";
import { SearchField } from "../../../ui/search_field";
describe("<Tools />", () => {
const fakeProps = (): ToolsProps => ({
@ -117,8 +118,7 @@ describe("<Tools />", () => {
p.tools[0].body.name = "tool 0";
p.tools[1].body.name = "tool 1";
const wrapper = shallow<Tools>(<Tools {...p} />);
wrapper.find("input").first().simulate("change",
{ currentTarget: { value: "0" } });
wrapper.find(SearchField).simulate("change", "0");
expect(wrapper.state().searchTerm).toEqual("0");
});

View File

@ -29,6 +29,7 @@ import { BotOriginQuadrant } from "../interfaces";
import { mapPointClickAction } from "../map/actions";
import { getMode } from "../map/util";
import { Mode } from "../map/interfaces";
import { SearchField } from "../../ui/search_field";
const toolStatus = (value: number | undefined): string => {
switch (value) {
@ -41,10 +42,6 @@ const toolStatus = (value: number | undefined): string => {
export class RawTools extends React.Component<ToolsProps, ToolsState> {
state: ToolsState = { searchTerm: "" };
update = ({ currentTarget }: React.SyntheticEvent<HTMLInputElement>) => {
this.setState({ searchTerm: currentTarget.value });
}
getToolName = (toolId: number | undefined): string | undefined => {
const foundTool = this.props.tools.filter(tool => tool.body.id === toolId)[0];
return foundTool ? foundTool.body.name : undefined;
@ -183,8 +180,9 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
panel={Panel.Tools}
linkTo={!hasTools ? "/app/designer/tools/add" : undefined}
title={!hasTools ? this.strings.titleText : undefined}>
<input type="text" onChange={this.update} name="searchTerm"
placeholder={this.strings.placeholder} />
<SearchField searchTerm={this.state.searchTerm}
placeholder={this.strings.placeholder}
onChange={searchTerm => this.setState({ searchTerm })} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"tools"}>
<EmptyStateWrapper

View File

@ -5,6 +5,7 @@ import {
} from "../weeds_inventory";
import { fakeState } from "../../../__test_support__/fake_state";
import { fakeWeed } from "../../../__test_support__/fake_state/resources";
import { SearchField } from "../../../ui/search_field";
describe("<Weeds> />", () => {
const fakeProps = (): WeedsProps => ({
@ -20,8 +21,7 @@ describe("<Weeds> />", () => {
it("changes search term", () => {
const wrapper = shallow<Weeds>(<Weeds {...fakeProps()} />);
wrapper.find("input").first().simulate("change",
{ currentTarget: { value: "0" } });
wrapper.find(SearchField).simulate("change", "0");
expect(wrapper.state().searchTerm).toEqual("0");
});

View File

@ -13,6 +13,7 @@ import { t } from "../../i18next_wrapper";
import { TaggedWeedPointer } from "farmbot";
import { selectAllWeedPointers } from "../../resources/selectors";
import { WeedInventoryItem } from "./weed_inventory_item";
import { SearchField } from "../../ui/search_field";
export interface WeedsProps {
weeds: TaggedWeedPointer[];
@ -33,10 +34,6 @@ export const mapStateToProps = (props: Everything): WeedsProps => ({
export class RawWeeds extends React.Component<WeedsProps, WeedsState> {
state: WeedsState = { searchTerm: "" };
update = ({ currentTarget }: React.SyntheticEvent<HTMLInputElement>) => {
this.setState({ searchTerm: currentTarget.value });
}
render() {
return <DesignerPanel panelName={"weeds-inventory"} panel={Panel.Weeds}>
<DesignerNavTabs />
@ -44,8 +41,9 @@ export class RawWeeds extends React.Component<WeedsProps, WeedsState> {
panel={Panel.Weeds}
linkTo={"/app/designer/weeds/add"}
title={t("Add weed")}>
<input type="text" onChange={this.update} name="searchTerm"
placeholder={t("Search your weeds...")} />
<SearchField searchTerm={this.state.searchTerm}
placeholder={t("Search your weeds...")}
onChange={searchTerm => this.setState({ searchTerm })} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"weeds-inventory"}>
<EmptyStateWrapper

View File

@ -15,6 +15,7 @@ import { fakePointGroup } from "../../../__test_support__/fake_state/resources";
import { history } from "../../../history";
import { initSaveGetId } from "../../../api/crud";
import { DesignerPanelTop } from "../../designer_panel";
import { SearchField } from "../../../ui/search_field";
describe("<Zones> />", () => {
const fakeProps = (): ZonesProps => ({
@ -30,8 +31,7 @@ describe("<Zones> />", () => {
it("changes search term", () => {
const wrapper = shallow<Zones>(<Zones {...fakeProps()} />);
wrapper.find("input").first().simulate("change",
{ currentTarget: { value: "0" } });
wrapper.find(SearchField).simulate("change", "0");
expect(wrapper.state().searchTerm).toEqual("0");
});

View File

@ -17,6 +17,7 @@ import {
import { GroupInventoryItem } from "../point_groups/group_inventory_item";
import { history } from "../../history";
import { initSaveGetId } from "../../api/crud";
import { SearchField } from "../../ui/search_field";
export interface ZonesProps {
dispatch: Function;
@ -37,10 +38,6 @@ export const mapStateToProps = (props: Everything): ZonesProps => ({
export class RawZones extends React.Component<ZonesProps, ZonesState> {
state: ZonesState = { searchTerm: "" };
update = ({ currentTarget }: React.SyntheticEvent<HTMLInputElement>) => {
this.setState({ searchTerm: currentTarget.value });
}
navigate = (id: number) => history.push(`/app/designer/zones/${id}`);
render() {
@ -53,8 +50,9 @@ export class RawZones extends React.Component<ZonesProps, ZonesState> {
}))
.then((id: number) => this.navigate(id)).catch(() => { })}
title={t("Add zone")}>
<input type="text" onChange={this.update} name="searchTerm"
placeholder={t("Search your zones...")} />
<SearchField searchTerm={this.state.searchTerm}
placeholder={t("Search your zones...")}
onChange={searchTerm => this.setState({ searchTerm })} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"zones-inventory"}>
<EmptyStateWrapper

View File

@ -59,6 +59,7 @@ import {
} from "../actions";
import { fakeSequence } from "../../__test_support__/fake_state/resources";
import { SpecialStatus, Color } from "farmbot";
import { SearchField } from "../../ui/search_field";
const fakeRootFolder = (): FolderNodeInitial => ({
kind: "initial",
@ -540,9 +541,7 @@ describe("<FolderPanelTop />", () => {
it("changes search term", () => {
const p = fakeProps();
const wrapper = shallow(<FolderPanelTop {...p} />);
wrapper.find("input").simulate("change", {
currentTarget: { value: "new" }
});
wrapper.find(SearchField).simulate("change", "new");
expect(updateSearchTerm).toHaveBeenCalledWith("new");
});

View File

@ -46,6 +46,7 @@ import { Content } from "../constants";
import { StepDragger, NULL_DRAGGER_ID } from "../draggable/step_dragger";
import { variableList } from "../sequences/locals_list/variable_support";
import { UUID } from "../resources/interfaces";
import { SearchField } from "../ui/search_field";
export const FolderListItem = (props: FolderItemProps) => {
const { sequence, movedSequenceUuid } = props;
@ -328,16 +329,10 @@ export class Folders extends React.Component<FolderProps, FolderState> {
export const FolderPanelTop = (props: FolderPanelTopProps) =>
<div className="panel-top with-button">
<div className="thin-search-wrapper">
<div className="text-input-wrapper">
<i className="fa fa-search" />
<input
value={props.searchTerm || ""}
onChange={e => updateSearchTerm(e.currentTarget.value)}
type="text" name="searchTerm"
placeholder={t("Search sequences...")} />
</div>
</div>
<SearchField
placeholder={t("Search sequences...")}
searchTerm={props.searchTerm || ""}
onChange={updateSearchTerm} />
<ToggleFolderBtn
expanded={props.toggleDirection}
onClick={props.toggleAll} />

View File

@ -10,6 +10,7 @@ import { fakeLog } from "../../__test_support__/fake_state/resources";
import { LogsProps } from "../interfaces";
import { MessageType } from "../../sequences/interfaces";
import { fakeTimeSettings } from "../../__test_support__/fake_time_settings";
import { SearchField } from "../../ui/search_field";
describe("<Logs />", () => {
function fakeLogs(): TaggedLog[] {
@ -176,8 +177,7 @@ describe("<Logs />", () => {
it("changes search term", () => {
const p = fakeProps();
const wrapper = shallow<Logs>(<Logs {...p} />);
wrapper.find("input").first().simulate("change",
{ currentTarget: { value: "one" } });
wrapper.find(SearchField).first().simulate("change", "one");
expect(wrapper.state().searchTerm).toEqual("one");
});
});

View File

@ -17,6 +17,7 @@ import { NumberConfigKey } from "farmbot/dist/resources/configs/web_app";
import { t } from "../i18next_wrapper";
import { TimeSettings } from "../interfaces";
import { timeFormatString } from "../util";
import { SearchField } from "../ui/search_field";
/** Format log date and time for display in the app. */
export const formatLogTime =
@ -125,15 +126,10 @@ export class RawLogs extends React.Component<LogsProps, Partial<LogsState>> {
</Row>
<Row>
<Col xs={12} md={5} lg={4}>
<div className="thin-search-wrapper">
<div className="text-input-wrapper">
<i className="fa fa-search" />
<input name="searchTerm"
onChange={e =>
this.setState({ searchTerm: e.currentTarget.value })}
placeholder={t("Search logs...")} />
</div>
</div>
<SearchField
placeholder={t("Search logs...")}
searchTerm={this.state.searchTerm}
onChange={searchTerm => this.setState({ searchTerm })} />
</Col>
</Row>
<Row>

View File

@ -1,9 +1,8 @@
import * as React from "react";
import { mount } from "enzyme";
import { mount, shallow } from "enzyme";
import { RegimensList } from "../index";
import { RegimensListProps } from "../../interfaces";
import { fakeRegimen } from "../../../__test_support__/fake_state/resources";
import { inputEvent } from "../../../__test_support__/fake_html_events";
describe("<RegimensList />", () => {
function fakeProps(): RegimensListProps {
@ -25,8 +24,8 @@ describe("<RegimensList />", () => {
});
it("sets search term", () => {
const wrapper = mount<RegimensList>(<RegimensList {...fakeProps()} />);
wrapper.instance().onChange(inputEvent("term"));
const wrapper = shallow<RegimensList>(<RegimensList {...fakeProps()} />);
wrapper.find("RegimenListHeader").simulate("change", "term");
expect(wrapper.state().searchTerm).toEqual("term");
});
});

View File

@ -7,23 +7,21 @@ import { sortResourcesById } from "../../util";
import { t } from "../../i18next_wrapper";
import { EmptyStateWrapper, EmptyStateGraphic } from "../../ui/empty_state_wrapper";
import { Content } from "../../constants";
import { SearchField } from "../../ui/search_field";
interface RegimenListHeaderProps {
onChange(e: React.SyntheticEvent<HTMLInputElement>): void;
searchTerm: string;
onChange(searchTerm: string): void;
regimenCount: number;
dispatch: Function;
}
const RegimenListHeader = (props: RegimenListHeaderProps) =>
<div className={"panel-top with-button"}>
<div className="thin-search-wrapper">
<div className="text-input-wrapper">
<i className="fa fa-search" />
<input name="searchTerm"
onChange={props.onChange}
placeholder={t("Search regimens...")} />
</div>
</div>
<SearchField
placeholder={t("Search regimens...")}
searchTerm={props.searchTerm}
onChange={props.onChange} />
<AddRegimen dispatch={props.dispatch} length={props.regimenCount} />
</div>;
@ -55,16 +53,13 @@ export class RegimensList extends
</Col>;
}
onChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
this.setState({ searchTerm: e.currentTarget.value });
}
render() {
return <div className={"regimens-list-wrapper"}>
<RegimenListHeader
dispatch={this.props.dispatch}
regimenCount={this.props.regimens.length}
onChange={this.onChange} />
searchTerm={this.state.searchTerm}
onChange={searchTerm => this.setState({ searchTerm })} />
<Row>
<EmptyStateWrapper
notEmpty={this.props.regimens.length > 0}

View File

@ -0,0 +1,43 @@
import React from "react";
import { mount, shallow } from "enzyme";
import { SearchField, SearchFieldProps } from "../search_field";
import { changeEvent } from "../../__test_support__/fake_html_events";
describe("<SearchField />", () => {
const fakeProps = (): SearchFieldProps => ({
onChange: jest.fn(),
searchTerm: "",
placeholder: "search...",
});
it("renders", () => {
const wrapper = mount(<SearchField {...fakeProps()} />);
expect(wrapper.find("input").props().placeholder).toEqual("search...");
});
it("changes search term", () => {
const p = fakeProps();
const wrapper = shallow(<SearchField {...p} />);
const e = changeEvent("new");
wrapper.find("input").simulate("change", e);
expect(p.onChange).toHaveBeenCalledWith("new");
});
it("changes search term on key press", () => {
const p = fakeProps();
p.onKeyPress = jest.fn();
const wrapper = shallow(<SearchField {...p} />);
const e = changeEvent("new");
wrapper.find("input").simulate("KeyPress", e);
expect(p.onKeyPress).toHaveBeenCalledWith("new");
});
it("doesn't change search term on key press", () => {
const p = fakeProps();
p.onKeyPress = undefined;
const wrapper = shallow(<SearchField {...p} />);
const e = changeEvent("new");
wrapper.find("input").simulate("KeyPress", e);
expect(p.onChange).not.toHaveBeenCalled();
});
});

View File

@ -0,0 +1,30 @@
import * as React from "react";
import { ErrorBoundary } from "../error_boundary";
export interface SearchFieldProps {
onChange(searchTerm: string): void;
onKeyPress?: (searchTerm: string) => void;
searchTerm: string;
placeholder: string;
customLeftIcon?: React.ReactElement;
customRightIcon?: React.ReactElement;
autoFocus?: boolean;
}
export const SearchField = (props: SearchFieldProps) =>
<div className="thin-search-wrapper">
<div className="thin-search">
<div className="text-input-wrapper">
<ErrorBoundary>
{props.customLeftIcon || <i className="fa fa-search" />}
<input name="searchTerm"
value={props.searchTerm}
autoFocus={props.autoFocus}
onChange={e => props.onChange(e.currentTarget.value)}
onKeyPress={e => props.onKeyPress?.(e.currentTarget.value)}
placeholder={props.placeholder} />
{props.searchTerm && props.customRightIcon}
</ErrorBoundary>
</div>
</div>
</div>;