FarmBot/Farmbot-Web-App

View on GitHub
frontend/weeds/weeds_inventory.tsx

Summary

Maintainability
D
2 days
Test Coverage
import React from "react";
import { connect } from "react-redux";
import { Everything, WeedsPanelState } from "../interfaces";
import { Panel } from "../farm_designer/panel_header";
import {
  EmptyStateWrapper, EmptyStateGraphic,
} from "../ui/empty_state_wrapper";
import { Actions, Content } from "../constants";
import {
  DesignerPanel, DesignerPanelContent, DesignerPanelTop,
} from "../farm_designer/designer_panel";
import { t } from "../i18next_wrapper";
import { TaggedPoint, TaggedPointGroup, TaggedWeedPointer } from "farmbot";
import {
  selectAllActivePoints, selectAllPointGroups, selectAllWeedPointers,
} from "../resources/selectors";
import { WeedInventoryItem } from "./weed_inventory_item";
import { SearchField } from "../ui/search_field";
import {
  SortOptions, PointSortMenu, orderedPoints,
} from "../farm_designer/sort_options";
import { ToggleButton } from "../ui";
import {
  setWebAppConfigValue, GetWebAppConfigValue, getWebAppConfigValue,
} from "../config_storage/actions";
import { BooleanSetting } from "../session_keys";
import { UUID } from "../resources/interfaces";
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
import { edit, save } from "../api/crud";
import { Collapse } from "@blueprintjs/core";
import { PanelSection } from "../plants/plant_inventory";
import { pointGroupSubset } from "../plants/select_plants";
import { DEFAULT_CRITERIA } from "../point_groups/criteria/interfaces";
import { createGroup } from "../point_groups/actions";
import { GroupInventoryItem } from "../point_groups/group_inventory_item";
import { Path } from "../internal_urls";
import { deleteAllIds } from "../api/delete_points_handler";
import { NavigationContext } from "../routes_helpers";

export interface WeedsProps {
  weeds: TaggedWeedPointer[];
  dispatch: Function;
  hoveredPoint: string | undefined;
  getConfigValue: GetWebAppConfigValue;
  groups: TaggedPointGroup[];
  allPoints: TaggedPoint[];
  weedsPanelState: WeedsPanelState;
}

interface WeedsState extends SortOptions {
  searchTerm: string;
}

export const mapStateToProps = (props: Everything): WeedsProps => ({
  weeds: selectAllWeedPointers(props.resources.index),
  dispatch: props.dispatch,
  hoveredPoint: props.resources.consumers.farm_designer.hoveredPoint,
  getConfigValue: getWebAppConfigValue(() => props),
  groups: selectAllPointGroups(props.resources.index),
  allPoints: selectAllActivePoints(props.resources.index),
  weedsPanelState: props.app.weedsPanelState,
});

export interface WeedsSectionProps {
  category: "pending" | "active" | "removed";
  sectionTitle: string;
  emptyStateText: string;
  open: boolean;
  hoveredPoint: UUID | undefined;
  clickOpen(): void;
  items: TaggedWeedPointer[];
  dispatch: Function;
  layerValue?: boolean;
  layerSetting?: BooleanConfigKey;
  layerDisabled?: boolean;
  children?: JSX.Element;
  allWeeds?: TaggedWeedPointer[];
}

export const WeedsSection = (props: WeedsSectionProps) => {
  const { layerSetting } = props;
  const rawMaxSize = Math.max(...props.items.map(item => item.body.radius));
  const maxSize = isFinite(rawMaxSize) ? rawMaxSize : 0;
  const noWeeds = props.allWeeds?.length == 0;
  return <div className={`${props.category}-weeds`}>
    <div className={`${props.category}-weeds-header section-header`}
      onClick={props.clickOpen}>
      <label>{`${t(props.sectionTitle)} (${props.items.length})`}</label>
      <div className="row">
        {props.category == "pending" && props.items.length > 0 && props.open &&
          <div className={"approval-buttons row"}>
            <button className={"fb-button green"} onClick={e => {
              e.stopPropagation();
              props.items.map(weed => {
                props.dispatch(edit(weed, { plant_stage: "active" }));
                props.dispatch(save(weed.uuid));
              });
            }}>
              <i className={"fa fa-check"} />{t("all")}
            </button>
            <button className={"fb-button red"}
              onClick={deleteAllIds("weeds", props.items)}>
              <i className={"fa fa-times"} />{t("all")}
            </button>
          </div>}
        {layerSetting && props.open &&
          <ToggleButton disabled={props.layerDisabled}
            toggleValue={props.layerValue}
            customText={{ textFalse: t("off"), textTrue: t("on") }}
            toggleAction={e => {
              e.stopPropagation();
              props.dispatch(setWebAppConfigValue(
                layerSetting, !props.layerValue));
            }} />}
        {props.open && props.children}
        <i className={`fa fa-caret-${props.open ? "up" : "down"}`} />
      </div>
    </div>
    <Collapse isOpen={props.open}>
      {noWeeds && <EmptyStateWrapper
        notEmpty={false}
        graphic={EmptyStateGraphic.weeds}
        title={t("No weeds yet.")}
        text={Content.NO_WEEDS}
        colorScheme={"weeds"}>
      </EmptyStateWrapper>}
      {props.items.length == 0 && !noWeeds &&
        <p className={"no-weeds"}>{t(props.emptyStateText)}</p>}
      {props.items.map(p => <WeedInventoryItem
        key={p.uuid}
        tpp={p}
        maxSize={maxSize}
        pending={props.category == "pending"}
        hovered={props.hoveredPoint === p.uuid}
        dispatch={props.dispatch} />)}
    </Collapse>
  </div>;
};

export class RawWeeds extends React.Component<WeedsProps, WeedsState> {
  state: WeedsState = {
    searchTerm: "", sortBy: "radius", reverse: true,
  };

  get weeds() {
    return orderedPoints(this.props.weeds, this.state).filter(p =>
      p.body.name.toLowerCase().includes(this.state.searchTerm.toLowerCase()));
  }

  toggleOpen = (section: keyof WeedsPanelState) => () =>
    this.props.dispatch({
      type: Actions.TOGGLE_WEEDS_PANEL_OPTION, payload: section,
    });

  PendingWeeds = () => <WeedsSection
    category={"pending"}
    sectionTitle={t("Pending")}
    emptyStateText={t("No pending weeds.")}
    items={this.weeds.filter(p => p.body.plant_stage === "pending")}
    open={this.props.weedsPanelState.pending}
    hoveredPoint={this.props.hoveredPoint}
    clickOpen={this.toggleOpen("pending")}
    dispatch={this.props.dispatch} />;

  ActiveWeeds = () => {
    const items = this.weeds.filter(p =>
      !["removed", "pending"].includes(p.body.plant_stage));
    return <WeedsSection
      category={"active"}
      sectionTitle={t("Active")}
      emptyStateText={t("No active weeds.")}
      items={items}
      open={this.props.weedsPanelState.active}
      hoveredPoint={this.props.hoveredPoint}
      clickOpen={this.toggleOpen("active")}
      layerSetting={BooleanSetting.show_weeds}
      layerValue={!!this.props.getConfigValue(BooleanSetting.show_weeds)}
      allWeeds={this.props.weeds}
      dispatch={this.props.dispatch}>
      <div className={"section-action-btn-group row"}>
        {items.length > 0 &&
          <button className={"fb-button red delete"}
            title={t("delete all")}
            onClick={deleteAllIds("weeds", items)}>
            {t("delete all")}
          </button>}
        <div
          className={"fb-button green plus-weed"}
          onClick={e => {
            e.stopPropagation();
            this.navigate(Path.weeds("add"));
          }}>
          <i className={"fa fa-plus"} title={t("add weed")} />
        </div>
      </div>
    </WeedsSection>;
  };

  RemovedWeeds = () => {
    const items = this.weeds.filter(p => p.body.plant_stage === "removed");
    return <WeedsSection
      category={"removed"}
      sectionTitle={t("Removed")}
      emptyStateText={t("No removed weeds.")}
      items={items}
      open={this.props.weedsPanelState.removed}
      hoveredPoint={this.props.hoveredPoint}
      clickOpen={this.toggleOpen("removed")}
      layerSetting={BooleanSetting.show_historic_points}
      layerValue={!!this.props.getConfigValue(BooleanSetting.show_historic_points)}
      layerDisabled={!this.props.getConfigValue(BooleanSetting.show_weeds)}
      dispatch={this.props.dispatch}>
      <div className={"section-action-btn-group row"}>
        {items.length > 0 &&
          <button className={"fb-button red delete"}
            title={t("delete all")}
            onClick={deleteAllIds("weeds", items)}>
            {t("delete all")}
          </button>}
      </div>
    </WeedsSection>;
  };

  static contextType = NavigationContext;
  context!: React.ContextType<typeof NavigationContext>;
  navigate = this.context;
  navigateById = (id: number | undefined) => () => {
    this.navigate(Path.groups(id));
  };

  render() {
    const weedGroups = pointGroupSubset(this.props.groups, "Weed");
    const filteredGroups = weedGroups
      .filter(p => p.body.name.toLowerCase()
        .includes(this.state.searchTerm.toLowerCase()));
    return <DesignerPanel panelName={"weeds-inventory"} panel={Panel.Weeds}>
      <DesignerPanelTop panel={Panel.Weeds}>
        <SearchField nameKey={"weeds"}
          searchTerm={this.state.searchTerm}
          placeholder={t("Search your weeds...")}
          customLeftIcon={<PointSortMenu
            sortOptions={this.state} onChange={u => this.setState(u)} />}
          onChange={searchTerm => this.setState({ searchTerm })} />
      </DesignerPanelTop>
      <DesignerPanelContent panelName={"weeds-inventory"}>
        <PanelSection isOpen={this.props.weedsPanelState.groups}
          panel={Panel.Weeds}
          toggleOpen={this.toggleOpen("groups")}
          itemCount={weedGroups.length}
          addNew={() => this.props.dispatch(createGroup({
            criteria: {
              ...DEFAULT_CRITERIA,
              string_eq: { pointer_type: ["Weed"] },
            },
          }))}
          addTitle={t("add new group")}
          addClassName={"plus-group"}
          title={t("Weed Groups")}>
          {filteredGroups
            .map(group => <GroupInventoryItem
              key={group.uuid}
              group={group}
              allPoints={this.props.allPoints}
              hovered={false}
              dispatch={this.props.dispatch}
              onClick={this.navigateById(group.body.id)}
            />)}
        </PanelSection>
        <this.PendingWeeds />
        <this.ActiveWeeds />
        <this.RemovedWeeds />
      </DesignerPanelContent>
    </DesignerPanel>;
  }
}

export const Weeds = connect(mapStateToProps)(RawWeeds);
// eslint-disable-next-line import/no-default-export
export default Weeds;