FarmBot/Farmbot-Web-App

View on GitHub
frontend/tools/index.tsx

Summary

Maintainability
D
2 days
Test Coverage
import React from "react";
import { connect } from "react-redux";
import {
  DesignerPanel, DesignerPanelTop, DesignerPanelContent,
} from "../farm_designer/designer_panel";
import { Panel } from "../farm_designer/panel_header";
import {
  EmptyStateWrapper, EmptyStateGraphic,
} from "../ui/empty_state_wrapper";
import { t } from "../i18next_wrapper";
import { Content } from "../constants";
import { useNavigate } from "react-router-dom";
import { Row, Help } from "../ui";
import {
  botPositionLabel,
} from "../farm_designer/map/layers/farmbot/bot_position_label";
import { Link } from "../link";
import { edit, save } from "../api/crud";
import {
  setToolHover, ToolSlotSVG, ToolSVG,
} from "../farm_designer/map/layers/tool_slots/tool_graphics";
import { ToolSelection } from "./tool_slot_edit_components";
import { hasUTM } from "../settings/firmware/firmware_hardware_support";
import {
  ToolsProps, ToolsState, ToolSlotInventoryItemProps, ToolInventoryItemProps,
} from "./interfaces";
import { mapStateToProps } from "./state_to_props";
import { mapPointClickAction, selectPoint } from "../farm_designer/map/actions";
import { getMode } from "../farm_designer/map/util";
import { Mode } from "../farm_designer/map/interfaces";
import { SearchField } from "../ui/search_field";
import { ToolVerification } from "./tool_verification";
import { PanelSection } from "../plants/plant_inventory";
import { createGroup } from "../point_groups/actions";
import { DEFAULT_CRITERIA } from "../point_groups/criteria/interfaces";
import { GroupInventoryItem } from "../point_groups/group_inventory_item";
import { pointGroupSubset } from "../plants/select_plants";
import { Path } from "../internal_urls";
import { UTMProfile } from "../farm_designer/map/profile/tools";
import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources";
import { NavigationContext } from "../routes_helpers";

export class RawTools extends React.Component<ToolsProps, ToolsState> {
  state: ToolsState = { searchTerm: "", groups: false };

  getToolName = (toolId: number | undefined): string | undefined => {
    const foundTool = this.props.tools.filter(tool => tool.body.id === toolId)[0];
    return foundTool ? foundTool.body.name : undefined;
  };

  get mountedToolId() { return this.props.device.body.mounted_tool_id; }

  get mountedTool() { return this.props.findTool(this.mountedToolId || 0); }

  get noUTM() { return !hasUTM(this.props.firmwareHardware); }

  MountedToolInfo = () =>
    <div className="mounted-tool">
      <div className="mounted-tool-header">
        <label>{t("mounted tool")}</label>
        <Help text={Content.MOUNTED_TOOL} />
      </div>
      <ToolSelection
        tools={this.props.tools}
        selectedTool={this.mountedTool}
        onChange={({ tool_id }) => {
          this.props.dispatch(edit(this.props.device,
            { mounted_tool_id: tool_id }));
          this.props.dispatch(save(this.props.device.uuid));
        }}
        noUTM={this.noUTM}
        isActive={this.props.isActive}
        filterSelectedTool={true}
        filterActiveTools={false} />
      <svg className={"utm-and-mounted-tool-graphic"}
        viewBox={"-60 -50 120 140"}>
        <UTMProfile profileAxis={"y"} expanded={true} getX={() => 0}
          position={{ x: 0, y: 0 }} selectionWidth={1}
          mountedToolInfo={{
            name: this.mountedTool?.body.name,
            pulloutDirection: ToolPulloutDirection.POSITIVE_X,
            noUTM: this.noUTM,
            flipped: false,
          }} reversed={false}
          hidePositionIndicator={true}
          gantryHeight={0}
          botPosition={{ x: 0, y: 0, z: 0 }} />
      </svg>
      <ToolVerification sensors={this.props.sensors} bot={this.props.bot} />
    </div>;

  ToolSlots = () =>
    <div className="tool-slots">
      <div className="row grid-exp-1">
        <label>{this.strings.toolSlots}</label>
        <Link to={Path.toolSlots("add")}>
          <div className={"fb-button green"}>
            <i className="fa fa-plus" title={this.strings.addSlot} />
          </div>
        </Link>
      </div>
      {this.props.toolSlots
        .filter(p => (this.getToolName(p.body.tool_id) || "").toLowerCase()
          .includes(this.state.searchTerm.toLowerCase()))
        .map(toolSlot =>
          <ToolSlotInventoryItem key={toolSlot.uuid}
            hovered={toolSlot.uuid === this.props.hoveredToolSlot}
            dispatch={this.props.dispatch}
            toolSlot={toolSlot}
            isActive={this.props.isActive}
            tools={this.props.tools}
            noUTM={this.noUTM}
            toolTransformProps={this.props.toolTransformProps} />)}
    </div>;

  Tools = () =>
    <div className="tools">
      <div className="row grid-exp-1">
        <label>{this.strings.tools}</label>
        <Link to={Path.tools("add")}>
          <div className={"fb-button green add-tool-btn"}>
            <i className="fa fa-plus" title={this.strings.titleText} />
          </div>
        </Link>
      </div>
      {this.props.tools
        .filter(tool => !tool.body.name ||
          tool.body.name && tool.body.name.toLowerCase()
            .includes(this.state.searchTerm.toLowerCase()))
        .filter(tool => tool.body.id)
        .map(tool =>
          <ToolInventoryItem key={tool.uuid}
            toolId={tool.body.id}
            active={this.props.isActive(tool.body.id)}
            mounted={this.mountedTool?.uuid == tool.uuid}
            toolName={tool.body.name || t("Unnamed")} />)}
    </div>;

  get strings() {
    return {
      placeholder: this.noUTM
        ? t("Search your seed containers...")
        : t("Search your tools..."),
      titleText: this.noUTM
        ? t("Add a seed container")
        : t("Add a tool or seed container"),
      emptyStateText: this.noUTM
        ? Content.NO_SEED_CONTAINERS
        : Content.NO_TOOLS,
      tools: this.noUTM
        ? t("seed containers")
        : t("tools and seed containers"),
      toolSlots: t("slots"),
      addSlot: t("Add slot"),
    };
  }

  toggleOpen = (category: keyof ToolsState) => () =>
    this.setState({ ...this.state, [category]: !this.state[category] });

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

  render() {
    const panelName = "tools";
    const hasTools = this.props.tools.length > 0;
    const toolSlotGroups = pointGroupSubset(this.props.groups, "ToolSlot");
    const filteredGroups = toolSlotGroups
      .filter(p => p.body.name.toLowerCase()
        .includes(this.state.searchTerm.toLowerCase()));
    return <DesignerPanel panelName={panelName} panel={Panel.Tools}>
      <DesignerPanelTop
        panel={Panel.Tools}
        linkTo={!hasTools ? Path.tools("add") : undefined}
        title={!hasTools ? this.strings.titleText : undefined}>
        <SearchField nameKey={"tools"}
          searchTerm={this.state.searchTerm}
          placeholder={this.strings.placeholder}
          onChange={searchTerm => this.setState({ searchTerm })} />
      </DesignerPanelTop>
      <DesignerPanelContent panelName={"tools"}>
        <EmptyStateWrapper
          notEmpty={hasTools}
          graphic={EmptyStateGraphic.tools}
          title={this.strings.titleText}
          text={this.strings.emptyStateText}
          colorScheme={"tools"}>
          {!this.noUTM && <this.MountedToolInfo />}
          <this.ToolSlots />
          <this.Tools />
          {toolSlotGroups.length > 0 &&
            <PanelSection isOpen={this.state.groups} panel={Panel.Tools}
              toggleOpen={this.toggleOpen("groups")}
              itemCount={toolSlotGroups.length}
              addNew={() => this.props.dispatch(createGroup({
                criteria: {
                  ...DEFAULT_CRITERIA,
                  string_eq: { pointer_type: ["ToolSlot"] },
                },
              }))}
              addTitle={t("add new group")}
              addClassName={"plus-group"}
              title={t("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>}
        </EmptyStateWrapper>
      </DesignerPanelContent>
    </DesignerPanel>;
  }
}

export const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => {
  const { x, y, z, id, tool_id, gantry_mounted } = props.toolSlot.body;
  const toolName = props.tools
    .filter(tool => tool.body.id == tool_id)[0]?.body.name;
  const navigate = useNavigate();
  return <div
    className={`tool-slot-search-item ${props.hovered ? "hovered" : ""}`}
    onClick={() => {
      if (getMode() == Mode.boxSelect) {
        mapPointClickAction(navigate, props.dispatch, props.toolSlot.uuid)();
        props.dispatch(setToolHover(undefined));
      } else {
        props.dispatch(selectPoint([props.toolSlot.uuid]));
        navigate(Path.toolSlots(id));
      }
    }}
    onMouseEnter={() => props.dispatch(setToolHover(props.toolSlot.uuid))}
    onMouseLeave={() => props.dispatch(setToolHover(undefined))}>
    <Row className="grid-exp-2">
      <div className={"tool-slot-search-item-icon"}>
        <ToolSlotSVG
          toolSlot={props.toolSlot}
          toolName={tool_id ? toolName : "Empty"}
          toolTransformProps={props.toolTransformProps} />
      </div>
      {props.hideDropdown
        ? <span className={"tool-slot-search-item-name"}>
          {toolName || t("Empty")}
        </span>
        : <div className={"tool-selection-wrapper"}
          onClick={e => e.stopPropagation()}>
          <ToolSelection
            tools={props.tools}
            selectedTool={props.tools
              .filter(tool => tool.body.id == tool_id)[0]}
            onChange={update => {
              props.dispatch(edit(props.toolSlot, update));
              props.dispatch(save(props.toolSlot.uuid));
            }}
            noUTM={props.noUTM}
            isActive={props.isActive}
            filterSelectedTool={false}
            filterActiveTools={true} />
        </div>}
      <p className="tool-slot-position">
        <i>{botPositionLabel({ x, y, z }, { gantryMounted: gantry_mounted })}</i>
      </p>
    </Row>
  </div>;
};

const ToolInventoryItem = (props: ToolInventoryItemProps) => {
  const activeText = props.active ? t("in slot") : t("inactive");
  const navigate = useNavigate();
  return <div className={"tool-search-item"}
    onClick={() => navigate(Path.tools(props.toolId))}>
    <Row className="grid-exp-2">
      <ToolSVG toolName={props.toolName} />
      <p className={"tool-search-item-name"}>{t(props.toolName)}</p>
      <p className="tool-status">
        <i>{props.mounted ? t("mounted") : activeText}</i>
      </p>
    </Row>
  </div>;
};

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