FarmBot/Farmbot-Web-App

View on GitHub
frontend/saved_gardens/garden_edit.tsx

Summary

Maintainability
B
4 hrs
Test Coverage
import React from "react";
import { GardenViewButtonProps, EditGardenProps } from "./interfaces";
import { openOrCloseGarden, applyGarden, destroySavedGarden } from "./actions";
import { error } from "../toast/toast";
import { trim } from "../util";
import { BlurableInput } from "../ui";
import { edit, save } from "../api/crud";
import { connect } from "react-redux";
import {
  selectAllPlantPointers, maybeFindSavedGardenById, selectAllPlantTemplates,
} from "../resources/selectors";
import { Everything } from "../interfaces";
import {
  DesignerPanel, DesignerPanelHeader, DesignerPanelContent,
} from "../farm_designer/designer_panel";
import { isNumber, take } from "lodash";
import { ResourceIndex } from "../resources/interfaces";
import { t } from "../i18next_wrapper";
import { Panel } from "../farm_designer/panel_header";
import { Path } from "../internal_urls";
import { PointGroupItem } from "../point_groups/point_group_item";
import {
  calcMaxCount, MoreIndicatorIcon,
} from "../point_groups/criteria/component";
import { Navigate, useNavigate } from "react-router-dom";

/** Open or close a SavedGarden. */
const GardenViewButton = (props: GardenViewButtonProps) => {
  const { navigate, dispatch, savedGardenId, gardenIsOpen } = props;
  const onClick = openOrCloseGarden({
    navigate,
    savedGardenId,
    gardenIsOpen,
    dispatch,
  });
  const btnText = gardenIsOpen
    ? t("exit")
    : t("view");
  return <button
    className={`fb-button ${gardenIsOpen ? "gray" : "yellow"}`}
    title={btnText}
    onClick={onClick}>
    {btnText}
  </button>;
};

/** Apply a SavedGarden after checking that the current garden is empty. */
const ApplyGardenButton =
  (props: {
    plantPointerCount: number,
    gardenId: number,
    dispatch: Function,
    navigate: (path: string) => void,
  }) =>
    <button
      className="fb-button green"
      title={t("apply garden")}
      onClick={() => props.plantPointerCount > 0
        ? error(trim(`${t("Please clear current garden first.")}
        (${props.plantPointerCount} ${t("plants")})`))
        : props.dispatch(applyGarden(props.navigate, props.gardenId))}>
      {t("apply")}
    </button>;

const DestroyGardenButton =
  (props: {
    navigate: (path: string) => void,
    dispatch: Function,
    gardenUuid: string,
  }) =>
    <i className={"fa fa-trash fb-icon-button"}
      title={t("delete garden")}
      onClick={() => props.dispatch(destroySavedGarden(
        props.navigate, props.gardenUuid))} />;

const findSavedGardenByUrl = (ri: ResourceIndex) => {
  const id = Path.getSlug(Path.savedGardens());
  const num = parseInt(id, 10);
  if (isNumber(num) && !isNaN(num)) {
    return maybeFindSavedGardenById(ri, num);
  }
};

export const mapStateToProps = (props: Everything): EditGardenProps => {
  const { openedSavedGarden } = props.resources.consumers.farm_designer;
  const savedGarden = findSavedGardenByUrl(props.resources.index);
  return {
    savedGarden,
    gardenIsOpen: !!(savedGarden?.body.id === openedSavedGarden),
    dispatch: props.dispatch,
    plantPointerCount: selectAllPlantPointers(props.resources.index).length,
    gardenPlants: selectAllPlantTemplates(props.resources.index)
      .filter(p => p.body.saved_garden_id == savedGarden?.body.id),
  };
};

export const RawEditGarden = (props: EditGardenProps) => {
  const navigate = useNavigate();
  const [notes, setNotes] = React.useState(props.savedGarden?.body.notes || "");
  const [expand, setExpand] = React.useState(false);

  const toggleExpand = () => setExpand(!expand);

  const { savedGarden } = props;
  const gardensPath = Path.savedGardens();
  const plantsPath = Path.plants();
  const maxCount = expand ? 1000 : calcMaxCount(3);
  return <DesignerPanel panelName={"saved-garden-edit"}
    panel={Panel.SavedGardens}>
    {!savedGarden && Path.startsWith(gardensPath) && <Navigate to={plantsPath} />}
    <DesignerPanelHeader
      panelName={"saved-garden"}
      panel={Panel.SavedGardens}
      title={t("Edit garden")}
      backTo={plantsPath}>
      {savedGarden &&
        <div className={"buttons"}>
          <ApplyGardenButton
            navigate={navigate}
            dispatch={props.dispatch}
            plantPointerCount={props.plantPointerCount}
            gardenId={savedGarden.body.id || -1} />
          <DestroyGardenButton
            navigate={navigate}
            dispatch={props.dispatch}
            gardenUuid={savedGarden.uuid} />
          <GardenViewButton
            navigate={navigate}
            dispatch={props.dispatch}
            savedGardenId={savedGarden.body.id}
            gardenIsOpen={props.gardenIsOpen} />
        </div>}
    </DesignerPanelHeader>
    <DesignerPanelContent panelName={"saved-garden-edit"}>
      {savedGarden
        ? <div className={"grid saved-garden-grid"}>
          <label>{t("name")}</label>
          <BlurableInput
            value={savedGarden.body.name || ""}
            onCommit={e => {
              props.dispatch(edit(savedGarden, {
                name: e.currentTarget.value
              }));
              props.dispatch(save(savedGarden.uuid));
            }} />
          <label>{t("notes")}</label>
          <textarea
            value={notes}
            onChange={e => setNotes(e.currentTarget.value)}
            onBlur={() => {
              props.dispatch(edit(savedGarden, {
                notes: notes
              }));
              props.dispatch(save(savedGarden.uuid));
            }} />
          <label>{t("plants")}</label>
          <div className={"point-list-wrapper"}>
            {take(props.gardenPlants, maxCount).map(point =>
              <PointGroupItem key={point.uuid} point={point} />)}
            <MoreIndicatorIcon count={props.gardenPlants.length}
              maxCount={maxCount} onClick={toggleExpand} />
          </div>
        </div>
        : <p>{t("Garden not found.")}</p>}
    </DesignerPanelContent>
  </DesignerPanel>;
};

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