Repository: FarmBot/Farmbot-Web-App (ts)

Issue:
Crop search cosmetic improvements
* `text-align: center` the crop label
* Change crop label background to `linear-gradient(transparent, rgba(0,0,0,.5))`
* Add `border-radius: 5px` to the crop tiles
* Remove double-column gutter so vertical and horizontal padding between all crop tiles and the panel edges is consistent
* Add `box-shadow: 0px 4px 7px 2px rgba(0,0,0,0.2)` to crop tiles
* On crop tile hover, change `box-shadow` to `0px 5px 8px 2px rgba(0,0,0,0.25)` and `transform: translateY(-1px)` with `animation: 0.2s ease`

### Before
![Screen Shot 2019-12-19 at 12 30 51 PM](https://user-images.githubusercontent.com/5524043/71207313-7182fe80-225b-11ea-930a-4b789524b45f.png)

### After
![Screen Shot 2019-12-19 at 12 30 32 PM](https://user-images.githubusercontent.com/5524043/71207320-78aa0c80-225b-11ea-97c6-6d31afe08797.png)


Pull request description:
Crop search CSS improvements
fixes #1632

Patch:
diff --git a/db/migrate/20220819170955_add_uncropped_camera_view_to_web_app_config.rb b/db/migrate/20220819170955_add_uncropped_camera_view_to_web_app_config.rb
new file mode 100644
index 000000000..465ee8732
--- /dev/null
+++ b/db/migrate/20220819170955_add_uncropped_camera_view_to_web_app_config.rb
@@ -0,0 +1,9 @@
+class AddUncroppedCameraViewToWebAppConfig < ActiveRecord::Migration[6.1]
+  def up
+    add_column :web_app_configs, :show_uncropped_camera_view_area, :boolean, default: false
+  end
+
+  def down
+    remove_column :web_app_configs, :show_uncropped_camera_view_area
+  end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 583fd4999..fc7c5914d 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -1947,7 +1947,8 @@ CREATE TABLE public.web_app_configs (
     clip_image_layer boolean DEFAULT true,
     beep_verbosity integer DEFAULT 0,
     landing_page character varying(100) DEFAULT 'controls'::character varying,
-    go_button_axes character varying(3) DEFAULT 'XY'::character varying NOT NULL
+    go_button_axes character varying(3) DEFAULT 'XY'::character varying NOT NULL,
+    show_uncropped_camera_view_area boolean DEFAULT false
 );
 
 
@@ -3829,6 +3830,7 @@ INSERT INTO "schema_migrations" (version) VALUES
 ('20220413194334'),
 ('20220415191331'),
 ('20220620225957'),
-('20220810212545');
+('20220810212545'),
+('20220819170955');
 
 
diff --git a/frontend/constants.ts b/frontend/constants.ts
index 984167704..ce039bfea 100644
--- a/frontend/constants.ts
+++ b/frontend/constants.ts
@@ -986,6 +986,10 @@ export namespace Content {
   export const SHOW_CAMERA_VIEW_AREA =
     trim(`Show the camera's field of view in the garden map.`);
 
+  export const SHOW_UNCROPPED_CAMERA_VIEW_AREA =
+    trim(`Show the camera's uncropped and unrotated field of view in the
+    garden map when CROP MAP IMAGES is enabled.`);
+
   export const CONFIRM_PLANT_DELETION =
     trim(`Show a confirmation dialog when deleting a plant.`);
 
@@ -1985,6 +1989,7 @@ export enum DeviceSetting {
   cropMapImages = `Crop map images`,
   clipPhotosOutOfBounds = `Clip photos out of bounds`,
   cameraView = `Camera view`,
+  uncroppedCameraView = `Uncropped Camera view`,
   confirmPlantDeletion = `Confirm plant deletion`,
 
   // Account
diff --git a/frontend/css/colors.scss b/frontend/css/colors.scss
index 4770a8627..e195910c3 100644
--- a/frontend/css/colors.scss
+++ b/frontend/css/colors.scss
@@ -3,6 +3,7 @@ $transparent: #00000000;
 $translucent: rgba(0, 0, 0, 0.2);
 $translucent3: rgba(0, 0, 0, 0.3);
 $translucent4: rgba(0, 0, 0, 0.4);
+$translucent5: rgba(0, 0, 0, 0.5);
 $white: #fff;
 $off_white: #f4f4f4;
 $lighter_gray: #eee;
diff --git a/frontend/css/farm_designer/farm_designer.scss b/frontend/css/farm_designer/farm_designer.scss
index d30edc9aa..fba20ca42 100644
--- a/frontend/css/farm_designer/farm_designer.scss
+++ b/frontend/css/farm_designer/farm_designer.scss
@@ -69,6 +69,7 @@
   background-size: 158% !important;
   background-color: $translucent;
   height: 12rem;
+  border-radius: 5px;
 }
 
 .crop-search-result-wrapper {
@@ -77,25 +78,35 @@
   overflow-x: hidden;
   margin-right: -10px;
   padding-bottom: inherit;
+  .openfarm-search-results-wrapper {
+    display: grid;
+    gap: 2rem;
+    grid-template-columns: repeat(2, 1fr);
+    margin-left: 2.5rem;
+    margin-right: 2rem;
+  }
   .plant-catalog-tile {
     position: relative;
     cursor: pointer;
-    &:nth-child(4n),
-    &:nth-child(4n-1) {
-      margin-bottom: 2rem;
-    }
-    &:nth-child(4n-2),
-    &:nth-child(4n-3) {
-      margin-bottom: 2rem;
+    text-align: center;
+    border-radius: 5px;
+    box-shadow: 0px 4px 7px 2px $translucent;
+    transition: transform 0.2s ease;
+    padding: 0;
+    &:hover {
+      box-shadow: 0px 5px 8px 2px rgba(0, 0, 0, 0.25);
+      transform: translateY(-1px);
     }
     label {
       position: absolute;
-      left: 15px;
+      left: 0;
       bottom: -5px;
-      right: 15px;
+      right: 0;
       margin-top: 0 !important;
       padding: 0.4rem 0.6rem 0.2rem;
-      background: rgba(0, 0, 0, 0.5);
+      background: linear-gradient(transparent, $translucent5);
+      border-bottom-left-radius: 5px;
+      border-bottom-right-radius: 5px;
       color: $white;
       font-size: 1.2rem !important;
     }
@@ -386,6 +397,14 @@
   transform-box: fill-box;
 }
 
+.tool-slot-indicator {
+  &.animate {
+    animation: pulse-and-rotate 4s 0.2s infinite linear;
+  }
+  transform-origin: center;
+  transform-box: fill-box;
+}
+
 .map-point {
   cursor: pointer !important;
   stroke-width: 2;
diff --git a/frontend/farm_designer/map/garden_map.tsx b/frontend/farm_designer/map/garden_map.tsx
index 53ed12e1b..a5e5c9d06 100644
--- a/frontend/farm_designer/map/garden_map.tsx
+++ b/frontend/farm_designer/map/garden_map.tsx
@@ -568,6 +568,8 @@ export class GardenMap extends
       !!this.props.getConfigValue(BooleanSetting.show_soil_interpolation_map)}
     cameraCalibrationData={this.props.cameraCalibrationData}
     cropPhotos={!!this.props.getConfigValue(BooleanSetting.crop_images)}
+    showUncroppedArea={!!this.props.getConfigValue(
+      BooleanSetting.show_uncropped_camera_view_area)}
     interactions={this.interactions("GenericPointer")}
     farmwareEnvs={this.props.farmwareEnvs}
     animate={this.animate}
@@ -604,8 +606,10 @@ export class GardenMap extends
     visible={!!this.props.showFarmbot}
     dispatch={this.props.dispatch}
     hoveredToolSlot={this.props.designer.hoveredToolSlot}
+    currentPoint={this.currentPoint}
     botPositionX={this.props.botLocationData.position.x}
     interactions={this.interactions("ToolSlot")}
+    animate={this.animate}
     slots={this.props.toolSlots} />;
   FarmBotLayer = () => <FarmBotLayer
     mapTransformProps={this.mapTransformProps}
diff --git a/frontend/farm_designer/map/interfaces.ts b/frontend/farm_designer/map/interfaces.ts
index 8615ba477..30435c33e 100644
--- a/frontend/farm_designer/map/interfaces.ts
+++ b/frontend/farm_designer/map/interfaces.ts
@@ -96,6 +96,7 @@ export interface GardenPointProps {
   cameraViewGridId: string | undefined;
   cameraCalibrationData: CameraCalibrationData;
   cropPhotos: boolean;
+  showUncroppedArea: boolean;
   current: boolean;
   hovered: boolean;
   dispatch: Function;
diff --git a/frontend/farm_designer/map/layers/farmbot/bot_figure.tsx b/frontend/farm_designer/map/layers/farmbot/bot_figure.tsx
index 14e72594f..d425c2caa 100644
--- a/frontend/farm_designer/map/layers/farmbot/bot_figure.tsx
+++ b/frontend/farm_designer/map/layers/farmbot/bot_figure.tsx
@@ -26,6 +26,7 @@ export interface BotFigureProps {
   cameraCalibrationData?: CameraCalibrationData;
   cameraViewArea?: boolean;
   cropPhotos?: boolean;
+  showUncroppedArea?: boolean;
   color?: Color;
 }
 
@@ -127,6 +128,7 @@ export class BotFigure extends
         <CameraViewArea
           position={this.props.position}
           cropPhotos={this.props.cropPhotos}
+          showUncroppedArea={this.props.showUncroppedArea}
           cameraCalibrationData={this.props.cameraCalibrationData}
           mapTransformProps={mapTransformProps} />}
       <text
@@ -152,6 +154,7 @@ interface CameraViewAreaProps {
   cameraCalibrationData: CameraCalibrationData;
   mapTransformProps: MapTransformProps;
   cropPhotos: boolean | undefined;
+  showUncroppedArea: boolean | undefined;
   logVisual?: boolean;
 }
 
@@ -200,6 +203,7 @@ export const CameraViewArea = (props: CameraViewAreaProps) => {
     noRotation: xySwap && cameraRotated,
   });
   const croppedLogVisual = props.logVisual && cropPhotos;
+  const showCroppedArea = !cropPhotos || props.showUncroppedArea;
   return <g id="camera-view-area-wrapper" fill={"none"}
     stroke={Color.darkGray} strokeWidth={2} strokeOpacity={0.75}>
     <clipPath id={`snapped-camera-view-area-clip-path-${x}-${y}`}>
@@ -207,17 +211,19 @@ export const CameraViewArea = (props: CameraViewAreaProps) => {
     </clipPath>
     {angledView && <ViewCircle id={"camera-photo-center"}
       center={scaledCenter} radius={5} position={angledView} />}
-    {props.logVisual
-      ? <ImageLogVisuals position={cropPhotos ? croppedView : angledView} />
-      : <ViewRectangle id={"snapped-camera-view-area"}
+    {props.logVisual &&
+      <ImageLogVisuals position={cropPhotos ? croppedView : angledView} />}
+    {!props.logVisual && showCroppedArea &&
+      <ViewRectangle id={"snapped-camera-view-area"}
         dashed={cropPhotos} position={snappedView} />}
-    {!croppedLogVisual && <g id={"angled-camera-view-area-wrapper"}
-      clipPath={props.logVisual
-        ? undefined
-        : `url(#snapped-camera-view-area-clip-path-${x}-${y})`}>
-      <ViewRectangle id={"angled-camera-view-area"}
-        dashed={cropPhotos} position={angledView} />
-    </g>}
+    {!croppedLogVisual && showCroppedArea &&
+      <g id={"angled-camera-view-area-wrapper"}
+        clipPath={props.logVisual
+          ? undefined
+          : `url(#snapped-camera-view-area-clip-path-${x}-${y})`}>
+        <ViewRectangle id={"angled-camera-view-area"}
+          dashed={cropPhotos} position={angledView} />
+      </g>}
     {cropPhotos && croppedView && (largeCrop(rotationAngle) && angledView
       ? <ViewCircle id={"cropped-camera-view-area"}
         center={scaledCenter}
diff --git a/frontend/farm_designer/map/layers/farmbot/index.tsx b/frontend/farm_designer/map/layers/farmbot/index.tsx
index a5dcc6b46..385dc36aa 100644
--- a/frontend/farm_designer/map/layers/farmbot/index.tsx
+++ b/frontend/farm_designer/map/layers/farmbot/index.tsx
@@ -1,4 +1,4 @@
-import * as React from "react";
+import React from "react";
 import { VirtualFarmBotProps } from "../../interfaces";
 import { BooleanSetting } from "../../../../session_keys";
 import { BotFigure } from "./bot_figure";
@@ -15,6 +15,8 @@ export function VirtualFarmBot(props: VirtualFarmBotProps) {
     !!getConfigValue(BooleanSetting.display_map_missed_steps);
   const encoderFigure = !!getConfigValue(BooleanSetting.encoder_figure);
   const cameraViewArea = !!getConfigValue(BooleanSetting.show_camera_view_area);
+  const showUncroppedArea = !!getConfigValue(
+    BooleanSetting.show_uncropped_camera_view_area);
   const cropPhotos = !!getConfigValue(BooleanSetting.crop_images);
 
   return <g id="virtual-farmbot">
@@ -36,6 +38,7 @@ export function VirtualFarmBot(props: VirtualFarmBotProps) {
       cameraCalibrationData={props.cameraCalibrationData}
       cameraViewArea={cameraViewArea}
       cropPhotos={cropPhotos}
+      showUncroppedArea={showUncroppedArea}
       eStopStatus={eStopStatus} />
     {encoderFigure &&
       <BotFigure figureName={"encoder-position"}
diff --git a/frontend/farm_designer/map/layers/logs/interfaces.ts b/frontend/farm_designer/map/layers/logs/interfaces.ts
index d61326d59..98c5a7d34 100644
--- a/frontend/farm_designer/map/layers/logs/interfaces.ts
+++ b/frontend/farm_designer/map/layers/logs/interfaces.ts
@@ -41,6 +41,7 @@ export interface LogVisualProps {
   visual: RenderedLog;
   cameraCalibrationData: CameraCalibrationData;
   cropImage: boolean;
+  showUncroppedArea: boolean;
   animate: boolean;
   mapTransformProps: MapTransformProps;
   deviceTarget: string;
diff --git a/frontend/farm_designer/map/layers/logs/logs_layer.tsx b/frontend/farm_designer/map/layers/logs/logs_layer.tsx
index 7fd138e50..067f6f36a 100644
--- a/frontend/farm_designer/map/layers/logs/logs_layer.tsx
+++ b/frontend/farm_designer/map/layers/logs/logs_layer.tsx
@@ -73,6 +73,8 @@ export const LogsLayer = (props: LogsLayerProps) =>
             log={log}
             visual={LOG_VISUAL_LOOKUP[log.body.message]}
             cropImage={!!props.getConfigValue(BooleanSetting.crop_images)}
+            showUncroppedArea={!!props.getConfigValue(
+              BooleanSetting.show_uncropped_camera_view_area)}
             animate={!props.getConfigValue(BooleanSetting.disable_animations)}
             cameraCalibrationData={props.cameraCalibrationData}
             deviceTarget={props.deviceTarget}
@@ -118,6 +120,7 @@ const ImageVisual = (props: LogVisualProps) => {
     {display && <CameraViewArea logVisual={true}
       position={{ x, y, z }}
       cropPhotos={props.cropImage}
+      showUncroppedArea={props.showUncroppedArea}
       cameraCalibrationData={props.cameraCalibrationData}
       mapTransformProps={props.mapTransformProps} />}
   </g>;
diff --git a/frontend/farm_designer/map/layers/points/garden_point.tsx b/frontend/farm_designer/map/layers/points/garden_point.tsx
index 7950a200c..ae02f8059 100644
--- a/frontend/farm_designer/map/layers/points/garden_point.tsx
+++ b/frontend/farm_designer/map/layers/points/garden_point.tsx
@@ -55,6 +55,7 @@ export const GardenPoint = (props: GardenPointProps) => {
       <CameraViewArea
         position={{ x, y, z: 0 }}
         cropPhotos={props.cropPhotos}
+        showUncroppedArea={props.showUncroppedArea}
         cameraCalibrationData={props.cameraCalibrationData}
         mapTransformProps={mapTransformProps} />}
   </g>;
diff --git a/frontend/farm_designer/map/layers/points/point_layer.tsx b/frontend/farm_designer/map/layers/points/point_layer.tsx
index fd43f978c..ce6cbffb3 100644
--- a/frontend/farm_designer/map/layers/points/point_layer.tsx
+++ b/frontend/farm_designer/map/layers/points/point_layer.tsx
@@ -21,6 +21,7 @@ export interface PointLayerProps {
   interactions: boolean;
   cameraCalibrationData: CameraCalibrationData;
   cropPhotos: boolean;
+  showUncroppedArea: boolean;
   designer: DesignerState;
   farmwareEnvs: TaggedFarmwareEnv[];
   animate: boolean;
@@ -58,6 +59,7 @@ export function PointLayer(props: PointLayerProps) {
             cameraViewGridId={cameraViewGridId}
             cameraCalibrationData={props.cameraCalibrationData}
             cropPhotos={props.cropPhotos}
+            showUncroppedArea={props.showUncroppedArea}
             dispatch={props.dispatch}
             soilHeightLabels={soilHeightLabels}
             getSoilHeightColor={getColor}
diff --git a/frontend/farm_designer/map/layers/tool_slots/tool_slot_layer.tsx b/frontend/farm_designer/map/layers/tool_slots/tool_slot_layer.tsx
index c32549f8a..9ff6ad0a4 100644
--- a/frontend/farm_designer/map/layers/tool_slots/tool_slot_layer.tsx
+++ b/frontend/farm_designer/map/layers/tool_slots/tool_slot_layer.tsx
@@ -1,4 +1,4 @@
-import * as React from "react";
+import React from "react";
 import { SlotWithTool, UUID } from "../../../../resources/interfaces";
 import { ToolSlotPoint } from "./tool_slot_point";
 import { MapTransformProps } from "../../interfaces";
@@ -11,6 +11,8 @@ export interface ToolSlotLayerProps {
   dispatch: Function;
   hoveredToolSlot: UUID | undefined;
   interactions: boolean;
+  currentPoint: UUID | undefined;
+  animate: boolean;
 }
 
 export function ToolSlotLayer(props: ToolSlotLayerProps) {
@@ -27,6 +29,8 @@ export function ToolSlotLayer(props: ToolSlotLayerProps) {
           key={slot.toolSlot.uuid}
           slot={slot}
           hoveredToolSlot={props.hoveredToolSlot}
+          current={slot.toolSlot.uuid === props.currentPoint}
+          animate={props.animate}
           dispatch={props.dispatch}
           botPositionX={props.botPositionX}
           mapTransformProps={mapTransformProps} />)}
diff --git a/frontend/farm_designer/map/layers/tool_slots/tool_slot_point.tsx b/frontend/farm_designer/map/layers/tool_slots/tool_slot_point.tsx
index b4aa066fa..89390c3ef 100644
--- a/frontend/farm_designer/map/layers/tool_slots/tool_slot_point.tsx
+++ b/frontend/farm_designer/map/layers/tool_slots/tool_slot_point.tsx
@@ -5,12 +5,14 @@ import { MapTransformProps } from "../../interfaces";
 import { RotatedTool } from "./tool_graphics";
 import { ToolLabel } from "./tool_label";
 import { t } from "../../../../i18next_wrapper";
-import { mapPointClickAction, setHoveredPlant } from "../../actions";
+import { mapPointClickAction, selectPoint, setHoveredPlant } from "../../actions";
 import { isToolFlipped } from "../../../../tools/tool_slot_edit_components";
 import { ToolbaySlot } from "../../tool_graphics/slot";
 import { GantryToolSlot } from "../../tool_graphics/seed_trough";
 import { reduceToolName } from "../../tool_graphics/all_tools";
 import { Path } from "../../../../internal_urls";
+import { Actions } from "../../../../constants";
+import { Circle } from "../plants/circle";
 
 export interface TSPProps {
   slot: SlotWithTool;
@@ -18,6 +20,8 @@ export interface TSPProps {
   mapTransformProps: MapTransformProps;
   dispatch: Function;
   hoveredToolSlot: UUID | undefined;
+  current: boolean;
+  animate: boolean;
 }
 
 export const ToolSlotPoint = (props: TSPProps) => {
@@ -25,7 +29,7 @@ export const ToolSlotPoint = (props: TSPProps) => {
   const {
     id, x, y, pullout_direction, gantry_mounted
   } = toolSlot.body;
-  const { mapTransformProps, botPositionX } = props;
+  const { mapTransformProps, botPositionX, current, animate } = props;
   const { quadrant, xySwap } = mapTransformProps;
   const xPosition = gantry_mounted ? (botPositionX || 0) : x;
   const { qx, qy } = transformXY(xPosition, y, props.mapTransformProps);
@@ -42,8 +46,19 @@ export const ToolSlotPoint = (props: TSPProps) => {
     flipped: isToolFlipped(toolSlot.body.meta),
     toolTransformProps: { quadrant, xySwap },
   };
+  const selected = current || hovered;
+  const iconHover = (action: "start" | "end") => () => {
+    const hover = action === "start";
+    props.dispatch({
+      type: Actions.TOGGLE_HOVERED_POINT,
+      payload: hover ? toolSlot.uuid : undefined
+    });
+  };
   return <g id={"toolslot-" + id}
+    onMouseEnter={iconHover("start")}
+    onMouseLeave={iconHover("end")}
     onClick={() => {
+      props.dispatch(selectPoint([toolSlot.uuid]));
       mapPointClickAction(props.dispatch, toolSlot.uuid,
         Path.toolSlots(id))();
       props.dispatch(setHoveredPlant(undefined));
@@ -60,6 +75,16 @@ export const ToolSlotPoint = (props: TSPProps) => {
 
     {gantry_mounted && <GantryToolSlot x={qx} y={qy} xySwap={xySwap} />}
 
+    {selected &&
+      <g id="selected-tool-slot-indicator">
+        <Circle
+          className={`tool-slot-indicator ${animate ? "animate" : ""}`}
+          x={qx}
+          y={qy}
+          r={40}
+          selected={true} />
+      </g>}
+
     {(props.slot.tool || (!pullout_direction && !gantry_mounted)) &&
       <RotatedTool
         tool={reduceToolName(toolName)}
diff --git a/frontend/farm_designer/map/legend/garden_map_legend.tsx b/frontend/farm_designer/map/legend/garden_map_legend.tsx
index b641efcfd..9f459f95e 100644
--- a/frontend/farm_designer/map/legend/garden_map_legend.tsx
+++ b/frontend/farm_designer/map/legend/garden_map_legend.tsx
@@ -151,6 +151,10 @@ const LayerToggles = (props: GardenMapLegendProps) => {
           setting={BooleanSetting.show_camera_view_area}
           label={DeviceSetting.cameraView}
           helpText={Content.SHOW_CAMERA_VIEW_AREA} />
+        <NonLayerToggle {...subMenuProps}
+          setting={BooleanSetting.show_uncropped_camera_view_area}
+          label={DeviceSetting.uncroppedCameraView}
+          helpText={Content.SHOW_UNCROPPED_CAMERA_VIEW_AREA} />
       </div>} />
     <LayerToggle
       settingName={BooleanSetting.show_zones}
diff --git a/frontend/plants/crop_catalog.tsx b/frontend/plants/crop_catalog.tsx
index 3db143d36..e24b72de9 100644
--- a/frontend/plants/crop_catalog.tsx
+++ b/frontend/plants/crop_catalog.tsx
@@ -18,6 +18,7 @@ import { Panel } from "../farm_designer/panel_header";
 import { SearchField } from "../ui/search_field";
 import { Path } from "../internal_urls";
 import { maybeFindPlantById } from "../resources/selectors_by_id";
+import { Row } from "../ui";
 
 export function mapStateToProps(props: Everything): CropCatalogProps {
   const {
@@ -95,7 +96,7 @@ export class RawCropCatalog extends React.Component<CropCatalogProps, {}> {
             : undefined} />
       </DesignerPanelTop>
       <DesignerPanelContent panelName={"crop-catalog"}>
-        <div className="crop-search-result-wrapper row">
+        <Row className={"crop-search-result-wrapper"}>
           <EmptyStateWrapper
             notEmpty={this.validSearchTerm}
             graphic={EmptyStateGraphic.crops}
@@ -112,7 +113,7 @@ export class RawCropCatalog extends React.Component<CropCatalogProps, {}> {
               bulkPlantSlug={this.props.bulkPlantSlug}
               dispatch={this.props.dispatch} />
           </EmptyStateWrapper>
-        </div>
+        </Row>
       </DesignerPanelContent>
     </DesignerPanel>;
   }
diff --git a/frontend/plants/openfarm_search_results.tsx b/frontend/plants/openfarm_search_results.tsx
index 92ea5692e..fe9cc1e1b 100644
--- a/frontend/plants/openfarm_search_results.tsx
+++ b/frontend/plants/openfarm_search_results.tsx
@@ -85,24 +85,26 @@ export class OpenFarmResults extends React.Component<SearchResultProps, {}> {
         : t("No search results")}
       textElement={cropSearchInProgress ? undefined : this.text}
       colorScheme={"plants"}>
-      {cropSearchResults.map(resp => {
-        const { crop, image } = resp;
-        return <Link
-          key={crop.slug}
-          draggable={false}
-          onClick={click(crop)}
-          to={to(crop.slug)}>
-          <div className={"plant-catalog-tile col-xs-6"}>
-            <label>
-              {crop.name}
-            </label>
-            <div
-              className={"plant-catalog-image"}
-              style={{ background: `url(${image}) top center no-repeat` }}
-              draggable={false} />
-          </div>
-        </Link>;
-      })}
+      <div className={"openfarm-search-results-wrapper"}>
+        {cropSearchResults.map(resp => {
+          const { crop, image } = resp;
+          return <Link
+            key={crop.slug}
+            draggable={false}
+            onClick={click(crop)}
+            to={to(crop.slug)}>
+            <div className={"plant-catalog-tile"}>
+              <label>
+                {crop.name}
+              </label>
+              <div
+                className={"plant-catalog-image"}
+                style={{ background: `url(${image}) top center no-repeat` }}
+                draggable={false} />
+            </div>
+          </Link>;
+        })}
+      </div>
     </EmptyStateWrapper>;
   }
 }
diff --git a/frontend/session_keys.ts b/frontend/session_keys.ts
index b46b1e6a1..c0bee1eb1 100644
--- a/frontend/session_keys.ts
+++ b/frontend/session_keys.ts
@@ -4,7 +4,8 @@ import {
   StringConfigKey as WebAppStringConfigKey,
 } from "farmbot/dist/resources/configs/web_app";
 
-type WebAppBooleanConfigKeyAll = WebAppBooleanConfigKey;
+type WebAppBooleanConfigKeyAll = WebAppBooleanConfigKey
+  | "show_uncropped_camera_view_area";
 type WebAppNumberConfigKeyAll = WebAppNumberConfigKey;
 type WebAppStringConfigKeyAll = WebAppStringConfigKey;
 
@@ -37,6 +38,8 @@ export const BooleanSetting: BooleanSettings = {
   show_sensor_readings: "show_sensor_readings",
   show_moisture_interpolation_map: "show_moisture_interpolation_map",
   show_camera_view_area: "show_camera_view_area",
+  show_uncropped_camera_view_area: "show_uncropped_camera_view_area" as
+    WebAppBooleanConfigKey,
   disable_animations: "disable_animations",
   display_map_missed_steps: "display_map_missed_steps",
   display_trail: "display_trail",
diff --git a/frontend/settings/default_values.ts b/frontend/settings/default_values.ts
index 029838ee5..9899a65cd 100644
--- a/frontend/settings/default_values.ts
+++ b/frontend/settings/default_values.ts
@@ -2,6 +2,7 @@ import {
   BooleanConfigKey as BooleanWebAppConfigKey,
   StringConfigKey as StringWebAppConfigKey,
   NumberConfigKey as NumberWebAppConfigKey,
+  WebAppConfig,
 } from "farmbot/dist/resources/configs/web_app";
 import { getWebAppConfigValue } from "../config_storage/actions";
 import { store } from "../redux/store";
@@ -76,6 +77,7 @@ const DEFAULT_WEB_APP_CONFIG_VALUES: Record<Key, Value> = {
   crop_images: true,
   clip_image_layer: true,
   show_camera_view_area: true,
+  ["show_uncropped_camera_view_area" as keyof WebAppConfig]: false,
   view_celery_script: false,
   highlight_modified_settings: true,
   show_advanced_settings: false,
diff --git a/frontend/settings/farm_designer_settings.tsx b/frontend/settings/farm_designer_settings.tsx
index e1b8d7ee0..236929187 100644
--- a/frontend/settings/farm_designer_settings.tsx
+++ b/frontend/settings/farm_designer_settings.tsx
@@ -128,6 +128,11 @@ const DESIGNER_SETTINGS =
       description: Content.SHOW_CAMERA_VIEW_AREA,
       setting: BooleanSetting.show_camera_view_area,
     },
+    {
+      title: DeviceSetting.uncroppedCameraView,
+      description: Content.SHOW_UNCROPPED_CAMERA_VIEW_AREA,
+      setting: BooleanSetting.show_uncropped_camera_view_area,
+    },
     {
       title: DeviceSetting.confirmPlantDeletion,
       description: Content.CONFIRM_PLANT_DELETION,
diff --git a/frontend/tools/index.tsx b/frontend/tools/index.tsx
index 46d6e729b..8a8b2fa67 100644
--- a/frontend/tools/index.tsx
+++ b/frontend/tools/index.tsx
@@ -27,7 +27,7 @@ import {
   ToolsProps, ToolsState, ToolSlotInventoryItemProps, ToolInventoryItemProps,
 } from "./interfaces";
 import { mapStateToProps } from "./state_to_props";
-import { mapPointClickAction } from "../farm_designer/map/actions";
+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";
@@ -210,6 +210,7 @@ export const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => {
         mapPointClickAction(props.dispatch, props.toolSlot.uuid)();
         props.dispatch(setToolHover(undefined));
       } else {
+        props.dispatch(selectPoint([props.toolSlot.uuid]));
         push(Path.toolSlots(id));
       }
     }}


Test patch:
diff --git a/frontend/__test_support__/fake_state/resources.ts b/frontend/__test_support__/fake_state/resources.ts
index 000794835..edb033c90 100644
--- a/frontend/__test_support__/fake_state/resources.ts
+++ b/frontend/__test_support__/fake_state/resources.ts
@@ -35,6 +35,7 @@ import {
 } from "farmbot/dist/resources/api_resources";
 import { MessageType } from "../../sequences/interfaces";
 import { TaggedPointGroup } from "../../resources/interfaces";
+import { WebAppConfig } from "farmbot/dist/resources/configs/web_app";
 
 export const resources: Everything["resources"] = buildResourceIndex();
 let idCounter = 1;
@@ -376,6 +377,7 @@ export function fakeWebAppConfig(): TaggedWebAppConfig {
     show_pins: false,
     show_zones: false,
     show_camera_view_area: false,
+    ["show_uncropped_camera_view_area" as keyof WebAppConfig]: false,
     disable_emergency_unlock_confirmation: false,
     map_size_x: 2900,
     map_size_y: 1400,
diff --git a/frontend/farm_designer/map/layers/farmbot/__tests__/bot_figure_test.tsx b/frontend/farm_designer/map/layers/farmbot/__tests__/bot_figure_test.tsx
index df3c97b4b..e141bd65d 100644
--- a/frontend/farm_designer/map/layers/farmbot/__tests__/bot_figure_test.tsx
+++ b/frontend/farm_designer/map/layers/farmbot/__tests__/bot_figure_test.tsx
@@ -219,6 +219,7 @@ describe("<BotFigure/>", () => {
     p.cameraCalibrationData = fakeCameraCalibrationDataFull();
     p.cameraCalibrationData.rotation = "10";
     p.cameraViewArea = true;
+    p.showUncroppedArea = true;
     p.cropPhotos = true;
     const wrapper = svgMount(<BotFigure {...p} />);
     const view = wrapper.find("#camera-view-area-wrapper");
@@ -228,11 +229,27 @@ describe("<BotFigure/>", () => {
       .toBeGreaterThanOrEqual(1);
   });
 
+  it("doesn't show uncropped camera view area", () => {
+    const p = fakeProps();
+    p.cameraCalibrationData = fakeCameraCalibrationDataFull();
+    p.cameraCalibrationData.rotation = "10";
+    p.cameraViewArea = true;
+    p.showUncroppedArea = false;
+    p.cropPhotos = true;
+    const wrapper = svgMount(<BotFigure {...p} />);
+    const view = wrapper.find("#camera-view-area-wrapper");
+    expect(view.find("#angled-camera-view-area").length).toEqual(0);
+    expect(view.find("#snapped-camera-view-area").length).toEqual(0);
+    expect(view.find("#cropped-camera-view-area").length)
+      .toBeGreaterThanOrEqual(1);
+  });
+
   it("shows large cropped camera view area", () => {
     const p = fakeProps();
     p.cameraCalibrationData = fakeCameraCalibrationDataFull();
     p.cameraCalibrationData.rotation = "47";
     p.cameraViewArea = true;
+    p.showUncroppedArea = true;
     p.cropPhotos = true;
     const wrapper = svgMount(<BotFigure {...p} />);
     const view = wrapper.find("#camera-view-area-wrapper");
@@ -249,6 +266,7 @@ describe("<BotFigure/>", () => {
     p.cameraCalibrationData.center = { x: undefined, y: undefined };
     p.cameraCalibrationData.rotation = "47";
     p.cameraViewArea = true;
+    p.showUncroppedArea = true;
     p.cropPhotos = true;
     const wrapper = svgMount(<BotFigure {...p} />);
     const view = wrapper.find("#camera-view-area-wrapper");
diff --git a/frontend/farm_designer/map/layers/points/__tests__/garden_point_test.tsx b/frontend/farm_designer/map/layers/points/__tests__/garden_point_test.tsx
index 76f51e41b..2b77cdbf0 100644
--- a/frontend/farm_designer/map/layers/points/__tests__/garden_point_test.tsx
+++ b/frontend/farm_designer/map/layers/points/__tests__/garden_point_test.tsx
@@ -33,6 +33,7 @@ describe("<GardenPoint/>", () => {
     cameraViewGridId: undefined,
     cameraCalibrationData: fakeCameraCalibrationData(),
     cropPhotos: false,
+    showUncroppedArea: false,
     soilHeightLabels: false,
     getSoilHeightColor: () => "rgb(128, 128, 128)",
     current: false,
diff --git a/frontend/farm_designer/map/layers/points/__tests__/point_layer_test.tsx b/frontend/farm_designer/map/layers/points/__tests__/point_layer_test.tsx
index 5b48588c7..37dacfcf0 100644
--- a/frontend/farm_designer/map/layers/points/__tests__/point_layer_test.tsx
+++ b/frontend/farm_designer/map/layers/points/__tests__/point_layer_test.tsx
@@ -31,6 +31,7 @@ describe("<PointLayer/>", () => {
     interactions: true,
     cameraCalibrationData: fakeCameraCalibrationData(),
     cropPhotos: false,
+    showUncroppedArea: false,
     farmwareEnvs: [],
     currentPoint: undefined,
     animate: false,
diff --git a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_layer_test.tsx b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_layer_test.tsx
index 9d4254e52..f023bbcaa 100644
--- a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_layer_test.tsx
+++ b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_layer_test.tsx
@@ -39,6 +39,8 @@ describe("<ToolSlotLayer/>", () => {
       dispatch: jest.fn(),
       hoveredToolSlot: undefined,
       interactions: true,
+      currentPoint: undefined,
+      animate: false,
     };
   }
   it("toggles visibility off", () => {
diff --git a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx
index a1fa59a5d..487e62b7d 100644
--- a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx
+++ b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx
@@ -15,6 +15,8 @@ import {
 } from "../../../../../__test_support__/map_transform_props";
 import { svgMount } from "../../../../../__test_support__/svg_mount";
 import { push } from "../../../../../history";
+import { shallow } from "enzyme";
+import { Actions } from "../../../../../constants";
 
 describe("<ToolSlotPoint/>", () => {
   const fakeProps = (): TSPProps => ({
@@ -23,6 +25,8 @@ describe("<ToolSlotPoint/>", () => {
     slot: { toolSlot: fakeToolSlot(), tool: fakeTool() },
     dispatch: jest.fn(),
     hoveredToolSlot: undefined,
+    current: false,
+    animate: false,
   });
 
   it.each<[0 | 1, 0 | 1]>([
@@ -142,4 +146,42 @@ describe("<ToolSlotPoint/>", () => {
     expect(wrapper.find("#gantry-toolbay-slot").find("rect").props().width)
       .toEqual(24);
   });
+
+  it("animates tool", () => {
+    const p = fakeProps();
+    p.animate = true;
+    p.current = true;
+    const wrapper = svgMount(<ToolSlotPoint {...p} />);
+    expect(wrapper.find(".tool-slot-indicator").first().hasClass("animate"))
+      .toBeTruthy();
+  });
+
+  it("doesn't animate tool", () => {
+    const p = fakeProps();
+    p.animate = false;
+    p.current = true;
+    const wrapper = svgMount(<ToolSlotPoint {...p} />);
+    expect(wrapper.find(".tool-slot-indicator").first().hasClass("animate"))
+      .toBeFalsy();
+  });
+
+  it("begins hover", () => {
+    const p = fakeProps();
+    const wrapper = shallow(<ToolSlotPoint {...p} />);
+    wrapper.find("g").simulate("mouseEnter");
+    expect(p.dispatch).toHaveBeenCalledWith({
+      type: Actions.TOGGLE_HOVERED_POINT,
+      payload: p.slot.toolSlot.uuid
+    });
+  });
+
+  it("ends hover", () => {
+    const p = fakeProps();
+    const wrapper = shallow(<ToolSlotPoint {...p} />);
+    wrapper.find("g").simulate("mouseLeave");
+    expect(p.dispatch).toHaveBeenCalledWith({
+      type: Actions.TOGGLE_HOVERED_POINT,
+      payload: undefined
+    });
+  });
 });
diff --git a/frontend/settings/__tests__/index_test.tsx b/frontend/settings/__tests__/index_test.tsx
index 98e0a7107..f016d0f23 100644
--- a/frontend/settings/__tests__/index_test.tsx
+++ b/frontend/settings/__tests__/index_test.tsx
@@ -164,7 +164,7 @@ describe("<DesignerSettings />", () => {
     p.settingsPanelState.farm_designer = true;
     p.getConfigValue = () => undefined;
     const wrapper = mount(<DesignerSettings {...p} />);
-    const confirmDeletion = getSetting(wrapper, 10, "confirm plant");
+    const confirmDeletion = getSetting(wrapper, 11, "confirm plant");
     expect(confirmDeletion.find("button").text()).toEqual("on");
   });
 
diff --git a/frontend/tools/__tests__/index_test.tsx b/frontend/tools/__tests__/index_test.tsx
index 39a6b270f..373e85f40 100644
--- a/frontend/tools/__tests__/index_test.tsx
+++ b/frontend/tools/__tests__/index_test.tsx
@@ -12,6 +12,7 @@ jest.mock("../../api/crud", () => ({
 
 jest.mock("../../farm_designer/map/actions", () => ({
   mapPointClickAction: jest.fn(() => jest.fn()),
+  selectPoint: jest.fn(),
 }));
 
 jest.mock("../../point_groups/actions", () => ({
@@ -39,7 +40,7 @@ import { edit, save } from "../../api/crud";
 import { ToolSelection } from "../tool_slot_edit_components";
 import { fakeToolTransformProps } from "../../__test_support__/fake_tool_info";
 import { ToolsProps, ToolSlotInventoryItemProps } from "../interfaces";
-import { mapPointClickAction } from "../../farm_designer/map/actions";
+import { mapPointClickAction, selectPoint } from "../../farm_designer/map/actions";
 import { SearchField } from "../../ui/search_field";
 import { PanelSection } from "../../plants/plant_inventory";
 import { createGroup } from "../../point_groups/actions";
@@ -316,7 +317,10 @@ describe("<ToolSlotInventoryItem />", () => {
     wrapper.find("div").first().simulate("click");
     expect(mapPointClickAction).not.toHaveBeenCalled();
     expect(push).toHaveBeenCalledWith(Path.toolSlots(1));
-    expect(p.dispatch).not.toHaveBeenCalled();
+    expect(selectPoint).toHaveBeenCalled();
+    expect(p.dispatch).not.toHaveBeenCalledWith({
+      type: Actions.HOVER_TOOL_SLOT, payload: undefined,
+    });
   });
 
   it("removes item in box select mode", () => {