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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,6 @@ import {
} from "./interfaces"; } from "./interfaces";
import { trim } from "../../util"; import { trim } from "../../util";
import { history, getPathArray } from "../../history"; import { history, getPathArray } from "../../history";
import { savedGardenOpen } from "../saved_gardens/saved_gardens";
/* /*
* Farm Designer Map Utilities * Farm Designer Map Utilities
@ -293,6 +292,7 @@ export const getMode = (): Mode => {
if ((pathArray[3] === "groups" || pathArray[3] === "zones") if ((pathArray[3] === "groups" || pathArray[3] === "zones")
&& pathArray[4]) { return Mode.editGroup; } && pathArray[4]) { return Mode.editGroup; }
if (pathArray[6] === "add") { return Mode.clickToAdd; } if (pathArray[6] === "add") { return Mode.clickToAdd; }
if (savedGardenOpen(pathArray)) { return Mode.templateView; }
if (!isNaN(parseInt(pathArray.slice(-1)[0]))) { return Mode.editPlant; } if (!isNaN(parseInt(pathArray.slice(-1)[0]))) { return Mode.editPlant; }
if (pathArray[5] === "edit") { return Mode.editPlant; } if (pathArray[5] === "edit") { return Mode.editPlant; }
if (pathArray[6] === "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; } if (pathArray[4] === "add") { return Mode.createWeed; }
return Mode.weeds; return Mode.weeds;
} }
if (savedGardenOpen(pathArray)) { return Mode.templateView; }
} }
return Mode.none; 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) => export const getZoomLevelFromMap = (map: Element) =>
parseFloat((window.getComputedStyle(map).transform || "(1").split("(")[1]); parseFloat((window.getComputedStyle(map).transform || "(1").split("(")[1]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -95,7 +95,7 @@ describe("<GroupPointCountBreakdown />", () => {
p.pointsSelectedByGroup = [point1, point2, point3]; p.pointsSelectedByGroup = [point1, point2, point3];
p.group.body.point_ids = [1]; p.group.body.point_ids = [1];
const wrapper = mount(<GroupPointCountBreakdown {...p} />); 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)); expect(wrapper.text()).toContain(string));
}); });
@ -108,7 +108,7 @@ describe("<GroupPointCountBreakdown />", () => {
p.pointsSelectedByGroup = [point1, point2]; p.pointsSelectedByGroup = [point1, point2];
p.group.body.point_ids = []; p.group.body.point_ids = [];
const wrapper = mount(<GroupPointCountBreakdown {...p} />); 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)); expect(wrapper.text()).toContain(string));
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@ import {
EmptyStateWrapper, EmptyStateGraphic, EmptyStateWrapper, EmptyStateGraphic,
} from "../../ui/empty_state_wrapper"; } from "../../ui/empty_state_wrapper";
import { Content } from "../../constants"; import { Content } from "../../constants";
import { SearchField } from "../../ui/search_field";
export const mapStateToProps = (props: Everything): SavedGardensProps => ({ export const mapStateToProps = (props: Everything): SavedGardensProps => ({
savedGardens: selectAllSavedGardens(props.resources.index), savedGardens: selectAllSavedGardens(props.resources.index),
@ -35,9 +36,6 @@ export class RawSavedGardens
unselectPlant(this.props.dispatch)(); unselectPlant(this.props.dispatch)();
} }
onChange = (e: React.SyntheticEvent<HTMLInputElement>) =>
this.setState({ searchTerm: e.currentTarget.value });
render() { render() {
return <DesignerPanel panelName={"saved-garden"} panel={Panel.SavedGardens}> return <DesignerPanel panelName={"saved-garden"} panel={Panel.SavedGardens}>
<DesignerNavTabs /> <DesignerNavTabs />
@ -46,8 +44,9 @@ export class RawSavedGardens
panel={Panel.SavedGardens} panel={Panel.SavedGardens}
linkTo={"/app/designer/gardens/add"} linkTo={"/app/designer/gardens/add"}
title={t("Add garden")}> title={t("Add garden")}>
<input type="text" onChange={this.onChange} name="searchTerm" <SearchField searchTerm={this.state.searchTerm}
placeholder={t("Search your gardens...")} /> placeholder={t("Search your gardens...")}
onChange={searchTerm => this.setState({ searchTerm })} />
</DesignerPanelTop> </DesignerPanelTop>
<EmptyStateWrapper <EmptyStateWrapper
notEmpty={this.props.savedGardens.length > 0} 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. */ /** Sticky an indicator and actions menu when a SavedGarden is open. */
export const SavedGardenHUD = (props: { dispatch: Function }) => export const SavedGardenHUD = (props: { dispatch: Function }) =>
<div className="saved-garden-indicator"> <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 { ToolSelection } from "../tool_slot_edit_components";
import { ToolsProps } from "../interfaces"; import { ToolsProps } from "../interfaces";
import { mapPointClickAction } from "../../map/actions"; import { mapPointClickAction } from "../../map/actions";
import { SearchField } from "../../../ui/search_field";
describe("<Tools />", () => { describe("<Tools />", () => {
const fakeProps = (): ToolsProps => ({ const fakeProps = (): ToolsProps => ({
@ -117,8 +118,7 @@ describe("<Tools />", () => {
p.tools[0].body.name = "tool 0"; p.tools[0].body.name = "tool 0";
p.tools[1].body.name = "tool 1"; p.tools[1].body.name = "tool 1";
const wrapper = shallow<Tools>(<Tools {...p} />); const wrapper = shallow<Tools>(<Tools {...p} />);
wrapper.find("input").first().simulate("change", wrapper.find(SearchField).simulate("change", "0");
{ currentTarget: { value: "0" } });
expect(wrapper.state().searchTerm).toEqual("0"); expect(wrapper.state().searchTerm).toEqual("0");
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -59,6 +59,7 @@ import {
} from "../actions"; } from "../actions";
import { fakeSequence } from "../../__test_support__/fake_state/resources"; import { fakeSequence } from "../../__test_support__/fake_state/resources";
import { SpecialStatus, Color } from "farmbot"; import { SpecialStatus, Color } from "farmbot";
import { SearchField } from "../../ui/search_field";
const fakeRootFolder = (): FolderNodeInitial => ({ const fakeRootFolder = (): FolderNodeInitial => ({
kind: "initial", kind: "initial",
@ -540,9 +541,7 @@ describe("<FolderPanelTop />", () => {
it("changes search term", () => { it("changes search term", () => {
const p = fakeProps(); const p = fakeProps();
const wrapper = shallow(<FolderPanelTop {...p} />); const wrapper = shallow(<FolderPanelTop {...p} />);
wrapper.find("input").simulate("change", { wrapper.find(SearchField).simulate("change", "new");
currentTarget: { value: "new" }
});
expect(updateSearchTerm).toHaveBeenCalledWith("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 { StepDragger, NULL_DRAGGER_ID } from "../draggable/step_dragger";
import { variableList } from "../sequences/locals_list/variable_support"; import { variableList } from "../sequences/locals_list/variable_support";
import { UUID } from "../resources/interfaces"; import { UUID } from "../resources/interfaces";
import { SearchField } from "../ui/search_field";
export const FolderListItem = (props: FolderItemProps) => { export const FolderListItem = (props: FolderItemProps) => {
const { sequence, movedSequenceUuid } = props; const { sequence, movedSequenceUuid } = props;
@ -328,16 +329,10 @@ export class Folders extends React.Component<FolderProps, FolderState> {
export const FolderPanelTop = (props: FolderPanelTopProps) => export const FolderPanelTop = (props: FolderPanelTopProps) =>
<div className="panel-top with-button"> <div className="panel-top with-button">
<div className="thin-search-wrapper"> <SearchField
<div className="text-input-wrapper"> placeholder={t("Search sequences...")}
<i className="fa fa-search" /> searchTerm={props.searchTerm || ""}
<input onChange={updateSearchTerm} />
value={props.searchTerm || ""}
onChange={e => updateSearchTerm(e.currentTarget.value)}
type="text" name="searchTerm"
placeholder={t("Search sequences...")} />
</div>
</div>
<ToggleFolderBtn <ToggleFolderBtn
expanded={props.toggleDirection} expanded={props.toggleDirection}
onClick={props.toggleAll} /> onClick={props.toggleAll} />

View File

@ -10,6 +10,7 @@ import { fakeLog } from "../../__test_support__/fake_state/resources";
import { LogsProps } from "../interfaces"; import { LogsProps } from "../interfaces";
import { MessageType } from "../../sequences/interfaces"; import { MessageType } from "../../sequences/interfaces";
import { fakeTimeSettings } from "../../__test_support__/fake_time_settings"; import { fakeTimeSettings } from "../../__test_support__/fake_time_settings";
import { SearchField } from "../../ui/search_field";
describe("<Logs />", () => { describe("<Logs />", () => {
function fakeLogs(): TaggedLog[] { function fakeLogs(): TaggedLog[] {
@ -176,8 +177,7 @@ describe("<Logs />", () => {
it("changes search term", () => { it("changes search term", () => {
const p = fakeProps(); const p = fakeProps();
const wrapper = shallow<Logs>(<Logs {...p} />); const wrapper = shallow<Logs>(<Logs {...p} />);
wrapper.find("input").first().simulate("change", wrapper.find(SearchField).first().simulate("change", "one");
{ currentTarget: { value: "one" } });
expect(wrapper.state().searchTerm).toEqual("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 { t } from "../i18next_wrapper";
import { TimeSettings } from "../interfaces"; import { TimeSettings } from "../interfaces";
import { timeFormatString } from "../util"; import { timeFormatString } from "../util";
import { SearchField } from "../ui/search_field";
/** Format log date and time for display in the app. */ /** Format log date and time for display in the app. */
export const formatLogTime = export const formatLogTime =
@ -125,15 +126,10 @@ export class RawLogs extends React.Component<LogsProps, Partial<LogsState>> {
</Row> </Row>
<Row> <Row>
<Col xs={12} md={5} lg={4}> <Col xs={12} md={5} lg={4}>
<div className="thin-search-wrapper"> <SearchField
<div className="text-input-wrapper"> placeholder={t("Search logs...")}
<i className="fa fa-search" /> searchTerm={this.state.searchTerm}
<input name="searchTerm" onChange={searchTerm => this.setState({ searchTerm })} />
onChange={e =>
this.setState({ searchTerm: e.currentTarget.value })}
placeholder={t("Search logs...")} />
</div>
</div>
</Col> </Col>
</Row> </Row>
<Row> <Row>

View File

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

View File

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