FarmBot/Farmbot-Web-App

View on GitHub
frontend/plants/plant_panel.tsx

Summary

Maintainability
D
2 days
Test Coverage
import React from "react";
import { FormattedPlantInfo } from "./map_state_to_props";
import { useNavigate } from "react-router-dom";
import { BlurableInput, Row, Help } from "../ui";
import {
  PlantStage, TaggedCurve, TaggedFarmwareEnv, TaggedGenericPointer,
  TaggedPlantPointer, Xyz,
} from "farmbot";
import moment, { Moment } from "moment";
import { Link } from "../link";
import { DesignerPanelContent } from "../farm_designer/designer_panel";
import { parseIntInput } from "../util";
import { isUndefined, startCase } from "lodash";
import { t } from "../i18next_wrapper";
import { MovementState, TimeSettings } from "../interfaces";
import { EditPlantStatus } from "./edit_plant_status";
import {
  getZAtLocation,
} from "../farm_designer/map/layers/points/interpolation_map";
import { Path } from "../internal_urls";
import { Actions } from "../constants";
import { daysOldText } from "./plant_inventory_item";
import { GoToThisLocationButton } from "../farm_designer/move_to";
import { BotPosition, SourceFbosConfig } from "../devices/interfaces";
import { AllCurveInfo, CURVE_KEY_LOOKUP } from "./curve_info";
import { BotSize } from "../farm_designer/map/interfaces";
import { UpdatePlant } from "./plant_info";

export interface PlantPanelProps {
  info: FormattedPlantInfo;
  updatePlant: UpdatePlant;
  inSavedGarden: boolean;
  dispatch: Function;
  timeSettings?: TimeSettings;
  soilHeightPoints: TaggedGenericPointer[];
  farmwareEnvs: TaggedFarmwareEnv[];
  botOnline: boolean;
  defaultAxes: string;
  arduinoBusy: boolean;
  currentBotLocation: BotPosition;
  movementState: MovementState;
  sourceFbosConfig: SourceFbosConfig;
  botSize: BotSize;
  curves: TaggedCurve[];
  plants: TaggedPlantPointer[];
}

interface EditPlantProperty {
  uuid: string;
  updatePlant: UpdatePlant;
}

export interface EditPlantStatusProps extends EditPlantProperty {
  plantStatus: PlantStage;
}

export interface EditDatePlantedProps extends EditPlantProperty {
  datePlanted: Moment | undefined;
  timeSettings: TimeSettings;
}

export const EditDatePlanted = (props: EditDatePlantedProps) => {
  const { datePlanted, updatePlant, uuid, timeSettings } = props;
  return <BlurableInput
    type="date"
    value={datePlanted?.utcOffset(timeSettings.utcOffset)
      .format("YYYY-MM-DD") || ""}
    onCommit={e => updatePlant(uuid, {
      planted_at: "" + moment(e.currentTarget.value)
        .utcOffset(timeSettings.utcOffset).toISOString()
    })} />;
};

export interface EditPlantLocationProps extends EditPlantProperty {
  plantLocation: Record<Xyz, number>;
  soilHeightPoints: TaggedGenericPointer[];
  farmwareEnvs: TaggedFarmwareEnv[];
}

export const EditPlantLocation = (props: EditPlantLocationProps) => {
  const { plantLocation, updatePlant, uuid } = props;
  const soilZ = getZAtLocation({
    x: plantLocation.x,
    y: plantLocation.y,
    points: props.soilHeightPoints,
    farmwareEnvs: props.farmwareEnvs,
  });
  return <Row>
    {["x", "y", "z"].map((axis: Xyz) =>
      <div key={axis}>
        <div className="row grid-exp-2 half-gap">
          <label style={{ marginTop: 0 }}>{t("{{axis}} (mm)", { axis })}</label>
          {axis == "z" && !isUndefined(soilZ) &&
            <Help text={`${t("soil height at plant location")}: ${soilZ}mm`} />}
        </div>
        <BlurableInput
          type="number"
          value={plantLocation[axis]}
          min={axis == "z" ? undefined : 0}
          onCommit={e => updatePlant(uuid, {
            [axis]: parseIntInput(e.currentTarget.value)
          })} />
      </div>)}
  </Row>;
};

export interface EditPlantRadiusProps extends EditPlantProperty {
  radius: number;
}

export const EditPlantRadius = (props: EditPlantRadiusProps) =>
  <Row className="grid-2-col">
    <label style={{ marginTop: 0 }}>{t("radius (mm)")}</label>
    <BlurableInput
      type="number"
      name="radius"
      value={props.radius}
      min={0}
      onCommit={e => props.updatePlant(props.uuid, {
        radius: parseIntInput(e.currentTarget.value)
      })} />
  </Row>;

export interface EditPlantDepthProps extends EditPlantProperty {
  depth: number;
}

export const EditPlantDepth = (props: EditPlantDepthProps) =>
  <Row className="grid-2-col">
    <label style={{ marginTop: 0 }}>{t("depth (mm)")}</label>
    <BlurableInput
      type="number"
      name="depth"
      value={props.depth}
      min={0}
      onCommit={e => props.updatePlant(props.uuid, {
        depth: parseIntInput(e.currentTarget.value)
      })} />
  </Row>;

interface ListItemProps {
  name?: string;
  children: React.ReactChild | React.ReactChild[];
}

export const ListItem = (props: ListItemProps) =>
  <li>
    {props.name &&
      <label>
        {props.name}
      </label>}
    <div className={"plant-info-field-data"}>
      {props.children}
    </div>
  </li>;

export function PlantPanel(props: PlantPanelProps) {
  const {
    info, updatePlant, dispatch, inSavedGarden, timeSettings
  } = props;
  const { slug, plantedAt, daysOld, uuid, plantStatus } = info;
  const { x, y, z } = info;
  const commonProps = { uuid, updatePlant };
  const navigate = useNavigate();
  return <DesignerPanelContent panelName={"plants"}>
    <ul className="grid">
      <ListItem name={t("Plant Type")}>
        <Link
          title={t("View crop info")}
          to={Path.cropSearch(slug)}>
          {startCase(slug)}
        </Link>
        <i className={"fa fa-pencil fb-icon-button"}
          onClick={() => {
            dispatch({ type: Actions.SET_PLANT_TYPE_CHANGE_ID, payload: info.id });
            dispatch({ type: Actions.SET_SLUG_BULK, payload: undefined });
            navigate(Path.cropSearch());
          }} />
      </ListItem>
      {(timeSettings && !inSavedGarden) &&
        <Row>
          <div>
            <ListItem name={t("Started")}>
              <EditDatePlanted {...commonProps}
                datePlanted={plantedAt}
                timeSettings={timeSettings} />
            </ListItem>
          </div>
          <div>
            <ListItem name={t("Age")}>
              {daysOldText({ age: daysOld, stage: plantStatus })}
            </ListItem>
          </div>
        </Row>}
      <ListItem>
        <EditPlantLocation {...commonProps}
          plantLocation={{ x, y, z }}
          soilHeightPoints={props.soilHeightPoints}
          farmwareEnvs={props.farmwareEnvs} />
      </ListItem>
      <GoToThisLocationButton
        dispatch={props.dispatch}
        locationCoordinate={{ x, y, z }}
        botOnline={props.botOnline}
        arduinoBusy={props.arduinoBusy}
        currentBotLocation={props.currentBotLocation}
        movementState={props.movementState}
        defaultAxes={props.defaultAxes} />
      <ListItem>
        <EditPlantRadius {...commonProps} radius={info.radius} />
      </ListItem>
      {!isUndefined(info.depth) && <ListItem>
        <EditPlantDepth {...commonProps} depth={info.depth} />
      </ListItem>}
      <ListItem>
        {(!inSavedGarden)
          ? <EditPlantStatus {...commonProps} plantStatus={plantStatus} />
          : t(startCase(plantStatus))}
      </ListItem>
      {info.uuid.startsWith("Point") &&
        <AllCurveInfo
          dispatch={props.dispatch}
          sourceFbosConfig={props.sourceFbosConfig}
          botSize={props.botSize}
          farmwareEnvs={props.farmwareEnvs}
          soilHeightPoints={props.soilHeightPoints}
          curves={props.curves}
          plants={props.plants}
          plant={info}
          findCurve={curveType => props.curves.filter(curve => curve.body.id &&
            curve.body.id == info[CURVE_KEY_LOOKUP[curveType
            ] as keyof FormattedPlantInfo])[0]}
          onChange={(id, curveType) =>
            updatePlant(info.uuid, { [CURVE_KEY_LOOKUP[curveType]]: id })} />}
      {Object.entries(info.meta || {}).map(([key, value]) => {
        switch (key) {
          case "gridId":
            return <div key={key} className={`meta-${key}-not-displayed`} />;
          default:
            return <ListItem key={key} name={key}>{value || ""}</ListItem>;
        }
      })}
    </ul>
  </DesignerPanelContent>;
}