diff --git a/Gemfile.lock b/Gemfile.lock
index 9c3325c3e8..5a7745dc89 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -67,7 +67,7 @@ GEM
zeitwerk (~> 2.3)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
- amq-protocol (2.3.2)
+ amq-protocol (2.3.3)
ast (2.4.2)
base64 (0.2.0)
bcrypt (3.1.20)
@@ -104,7 +104,7 @@ GEM
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
- diff-lcs (1.5.1)
+ diff-lcs (1.6.0)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
discard (1.4.0)
@@ -112,8 +112,8 @@ GEM
docile (1.4.1)
e2mmap (0.1.0)
erubi (1.13.1)
- factory_bot (6.5.0)
- activesupport (>= 5.0.0)
+ factory_bot (6.5.1)
+ activesupport (>= 6.1.0)
factory_bot_rails (6.4.4)
factory_bot (~> 6.5)
railties (>= 5.0.0)
@@ -139,7 +139,7 @@ GEM
retriable (>= 2.0, < 4.a)
google-apis-iamcredentials_v1 (0.22.0)
google-apis-core (>= 0.15.0, < 2.a)
- google-apis-storage_v1 (0.49.0)
+ google-apis-storage_v1 (0.50.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-core (1.7.1)
google-cloud-env (>= 1.0, < 3.a)
@@ -147,12 +147,12 @@ GEM
google-cloud-env (2.2.1)
faraday (>= 1.0, < 3.a)
google-cloud-errors (1.4.0)
- google-cloud-storage (1.54.0)
+ google-cloud-storage (1.55.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-core (~> 0.13)
google-apis-iamcredentials_v1 (~> 0.18)
- google-apis-storage_v1 (~> 0.38)
+ google-apis-storage_v1 (>= 0.42)
google-cloud-core (~> 1.6)
googleauth (~> 1.9)
mini_mime (~> 1.0)
@@ -167,10 +167,11 @@ GEM
signet (>= 0.16, < 2.a)
hashdiff (1.1.2)
hashie (4.1.0)
- httpclient (2.8.3)
+ httpclient (2.9.0)
+ mutex_m
i18n (1.14.7)
concurrent-ruby (~> 1.0)
- json (2.9.1)
+ json (2.10.1)
jsonapi-renderer (0.2.2)
jwt (2.10.1)
base64
@@ -186,7 +187,7 @@ GEM
activerecord
kaminari-core (= 1.2.2)
kaminari-core (1.2.2)
- logger (1.6.5)
+ logger (1.6.6)
lograge (0.14.0)
actionpack (>= 4)
activesupport (>= 4)
@@ -210,23 +211,23 @@ GEM
mutex_m (0.3.0)
net-http (0.6.0)
uri
- net-imap (0.5.5)
+ net-imap (0.5.6)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
- net-smtp (0.5.0)
+ net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
- nokogiri (1.18.2-aarch64-linux-gnu)
+ nokogiri (1.18.3-aarch64-linux-gnu)
racc (~> 1.4)
- nokogiri (1.18.2-x86_64-linux-gnu)
+ nokogiri (1.18.3-x86_64-linux-gnu)
racc (~> 1.4)
orm_adapter (0.5.0)
os (1.1.4)
- parser (3.3.7.0)
+ parser (3.3.7.1)
ast (~> 2.4.1)
racc
passenger (6.0.23)
@@ -247,7 +248,7 @@ GEM
hashie (~> 4.1)
multi_json (~> 1.15)
racc (1.8.1)
- rack (2.2.10)
+ rack (2.2.11)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-cors (2.0.2)
@@ -303,13 +304,13 @@ GEM
actionpack (>= 5.2)
railties (>= 5.2)
retriable (3.1.2)
- rexml (3.4.0)
- rollbar (3.6.0)
+ rexml (3.4.1)
+ rollbar (3.6.1)
rspec (3.13.0)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
- rspec-core (3.13.2)
+ rspec-core (3.13.3)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.3)
diff-lcs (>= 1.2.0, < 2.0)
@@ -331,7 +332,7 @@ GEM
scenic (1.8.0)
activerecord (>= 4.0.0)
railties (>= 4.0.0)
- scout_apm (5.6.0)
+ scout_apm (5.6.1)
parser
secure_headers (7.1.0)
set (1.1.1)
@@ -369,13 +370,13 @@ GEM
tzinfo-data (1.2025.1)
tzinfo (>= 1.0.0)
uber (0.1.0)
- uri (1.0.2)
+ uri (1.0.3)
valid_url (0.0.4)
addressable
rails
warden (1.2.9)
rack (>= 2.0.9)
- webmock (3.24.0)
+ webmock (3.25.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@@ -384,7 +385,7 @@ GEM
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
- zeitwerk (2.7.1)
+ zeitwerk (2.7.2)
PLATFORMS
aarch64-linux
diff --git a/frontend/__test_support__/additional_mocks.tsx b/frontend/__test_support__/additional_mocks.tsx
index 7ecb5b793f..43577a2752 100644
--- a/frontend/__test_support__/additional_mocks.tsx
+++ b/frontend/__test_support__/additional_mocks.tsx
@@ -13,7 +13,7 @@ window.location = {
ancestorOrigins,
pathname: "", href: "", hash: "", search: "",
hostname: "", origin: "", port: "", protocol: "", host: "",
-};
+} as unknown as Location & string;
console.error = jest.fn(); // enzyme
@@ -43,6 +43,7 @@ global.mockNavigate = jest.fn(() => jest.fn());
jest.mock("react-router", () => ({
BrowserRouter: jest.fn(({ children }) =>
{children}
),
+ MemoryRouter: jest.fn(({ children }) => {children}
),
Route: jest.fn(({ children }) => {children}
),
Routes: jest.fn(({ children }) => {children}
),
useNavigate: () => mockNavigate,
diff --git a/frontend/__test_support__/three_d_mocks.tsx b/frontend/__test_support__/three_d_mocks.tsx
index a4a59c1ef3..664ce68788 100644
--- a/frontend/__test_support__/three_d_mocks.tsx
+++ b/frontend/__test_support__/three_d_mocks.tsx
@@ -10,6 +10,7 @@ import * as THREE from "three";
import React, { ReactNode } from "react";
import { TransitionFn, UseSpringProps } from "@react-spring/three";
import { ThreeElements } from "@react-three/fiber";
+import { Cloud, Clouds, Image, Tube } from "@react-three/drei";
const GroupForTests = (props: ThreeElements["group"]) =>
// @ts-expect-error Property does not exist on type JSX.IntrinsicElements
@@ -612,8 +613,9 @@ jest.mock("@react-three/drei", () => {
{name}
,
Trail: ({ name }: { name: string }) =>
{name}
,
- Tube: ({ name, children }: { name: string, children: ReactNode }) =>
- {children}
,
+ Tube: (props: React.ComponentProps) =>
+ // @ts-expect-error geometry props not assignable to div
+ {props.children}
,
Center: ({ children }: { children: ReactNode }) =>
{children}
,
Text3D: ({ children }: { children: ReactNode }) =>
@@ -642,12 +644,15 @@ jest.mock("@react-three/drei", () => {
{name}
,
Billboard: ({ name, children }: { name: string, children: ReactNode }) =>
{children}
,
- Image: ({ name, url }: { name: string, url: string }) =>
- {name} {url}
,
- Clouds: ({ name }: { name: string }) =>
- {name}
,
- Cloud: ({ name }: { name: string }) =>
- {name}
,
+ Image: (props: React.ComponentProps) =>
+ // @ts-expect-error geometry props not assignable to div
+ {props.name} {props.url}
,
+ Clouds: (props: React.ComponentProps) =>
+ // @ts-expect-error geometry props not assignable to div
+ {props.children}
,
+ Cloud: (props: React.ComponentProps) =>
+ // @ts-expect-error geometry props not assignable to div
+ ,
OrthographicCamera: ({ name }: { name: string }) =>
{name}
,
};
diff --git a/frontend/__tests__/hotkeys_test.tsx b/frontend/__tests__/hotkeys_test.tsx
index 2d38712ebc..18b81c9ab3 100644
--- a/frontend/__tests__/hotkeys_test.tsx
+++ b/frontend/__tests__/hotkeys_test.tsx
@@ -1,6 +1,5 @@
const mockSyncThunk = jest.fn();
jest.mock("../devices/actions", () => ({ sync: () => mockSyncThunk }));
-jest.mock("../farm_designer/map/actions", () => ({ unselectPlant: jest.fn() }));
import { fakeState } from "../__test_support__/fake_state";
const mockState = fakeState();
@@ -16,10 +15,10 @@ import {
HotKey, HotKeys, HotKeysProps, hotkeysWithActions, toggleHotkeyHelpOverlay,
} from "../hotkeys";
import { sync } from "../devices/actions";
-import { unselectPlant } from "../farm_designer/map/actions";
import { save } from "../api/crud";
import { Actions } from "../constants";
import { Path } from "../internal_urls";
+import { mockDispatch } from "../__test_support__/fake_dispatch";
describe("hotkeysWithActions()", () => {
beforeEach(() => {
@@ -59,14 +58,12 @@ describe("hotkeysWithActions()", () => {
hotkeys[HotKey.addEvent].onKeyDown?.(e);
expect(navigate).toHaveBeenCalledWith(Path.farmEvents("add"));
- hotkeysSettingsPath[HotKey.backToPlantOverview].onKeyDown?.(e);
- expect(navigate).toHaveBeenCalledWith(Path.plants());
- expect(unselectPlant).toHaveBeenCalled();
- jest.clearAllMocks();
- const hotkeysPhotosPath = hotkeysWithActions(navigate, dispatch, "photos");
- hotkeysPhotosPath[HotKey.backToPlantOverview].onKeyDown?.(e);
- expect(navigate).not.toHaveBeenCalled();
- expect(unselectPlant).not.toHaveBeenCalled();
+ const hotkeysWithDispatch =
+ hotkeysWithActions(navigate, mockDispatch(dispatch), "");
+ hotkeysWithDispatch[HotKey.closePanel].onKeyDown?.(e);
+ expect(dispatch).toHaveBeenCalledWith({
+ type: Actions.SET_PANEL_OPEN, payload: false,
+ });
});
});
diff --git a/frontend/css/farm_designer/farm_designer_panels.scss b/frontend/css/farm_designer/farm_designer_panels.scss
index 05b07cb3dc..ac2839a854 100644
--- a/frontend/css/farm_designer/farm_designer_panels.scss
+++ b/frontend/css/farm_designer/farm_designer_panels.scss
@@ -362,16 +362,6 @@
}
}
-.additional-weed-properties {
- li {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 1rem;
- align-items: center;
- margin-top: 1rem;
- }
-}
-
.panel-section {
.delete {
height: 2rem;
diff --git a/frontend/farm_designer/__tests__/map_size_setting_test.tsx b/frontend/farm_designer/__tests__/map_size_setting_test.tsx
index afc3a1f8ba..8f8d9f74e2 100644
--- a/frontend/farm_designer/__tests__/map_size_setting_test.tsx
+++ b/frontend/farm_designer/__tests__/map_size_setting_test.tsx
@@ -5,21 +5,27 @@ jest.mock("../../config_storage/actions", () => ({
import React from "react";
import { MapSizeInputs, MapSizeInputsProps } from "../map_size_setting";
-import { mount } from "enzyme";
+import { render, screen } from "@testing-library/react";
import { setWebAppConfigValue } from "../../config_storage/actions";
import { NumericSetting } from "../../session_keys";
+import { fakeWebAppConfig } from "../../__test_support__/fake_state/resources";
+import { WebAppConfig } from "farmbot/dist/resources/configs/web_app";
+import { changeBlurableInputRTL } from "../../__test_support__/helpers";
describe("", () => {
- const fakeProps = (): MapSizeInputsProps => ({
- getConfigValue: () => 100,
- dispatch: jest.fn(),
- });
+ const fakeProps = (config: WebAppConfig): MapSizeInputsProps => {
+ return {
+ getConfigValue: key => config[key],
+ dispatch: jest.fn(),
+ };
+ };
it("changes value", () => {
- const wrapper = mount();
- wrapper.find("input").last().simulate("change"), {
- currentTarget: { value: 100 }
- };
+ const config = fakeWebAppConfig();
+ const p = fakeProps(config.body);
+ render();
+ const input = screen.getByDisplayValue("" + config.body.map_size_y);
+ changeBlurableInputRTL(input, "100");
expect(setWebAppConfigValue).toHaveBeenCalledWith(
NumericSetting.map_size_y, "100");
});
diff --git a/frontend/farm_designer/map_size_setting.tsx b/frontend/farm_designer/map_size_setting.tsx
index 3b107f52d9..8623dbe7d5 100644
--- a/frontend/farm_designer/map_size_setting.tsx
+++ b/frontend/farm_designer/map_size_setting.tsx
@@ -3,7 +3,7 @@ import {
GetWebAppConfigValue, setWebAppConfigValue,
} from "../config_storage/actions";
import { t } from "../i18next_wrapper";
-import { Row } from "../ui";
+import { BlurableInput, Row } from "../ui";
import { NumericSetting } from "../session_keys";
import {
NumberConfigKey as WebAppNumberConfigKey,
@@ -20,12 +20,12 @@ interface LengthInputProps {
const LengthInput = (props: LengthInputProps) =>
- props.dispatch(setWebAppConfigValue(
+ onCommit={e => props.dispatch(setWebAppConfigValue(
props.setting, e.currentTarget.value))} />
;
diff --git a/frontend/hotkeys.tsx b/frontend/hotkeys.tsx
index 25b4abdff9..55382f130f 100644
--- a/frontend/hotkeys.tsx
+++ b/frontend/hotkeys.tsx
@@ -2,8 +2,9 @@ import React from "react";
import { getLinks } from "./nav/nav_links";
import { sync } from "./devices/actions";
import { HotkeyConfig, useHotkeys, HotkeysDialog2 } from "@blueprintjs/core";
-import { unselectPlant } from "./farm_designer/map/actions";
-import { getPanelPath, PANEL_BY_SLUG } from "./farm_designer/panel_header";
+import {
+ getPanelPath, PANEL_BY_SLUG, setPanelOpen,
+} from "./farm_designer/panel_header";
import { t } from "./i18next_wrapper";
import { store } from "./redux/store";
import { save } from "./api/crud";
@@ -25,7 +26,7 @@ export enum HotKey {
navigateLeft = "navigateLeft",
addPlant = "addPlant",
addEvent = "addEvent",
- backToPlantOverview = "backToPlantOverview",
+ closePanel = "closePanel",
openGuide = "openGuide",
}
@@ -54,9 +55,9 @@ const HOTKEY_BASE_MAP = (): HotkeyConfigs => ({
combo: "ctrl + shift + e",
label: t("Add Event"),
},
- [HotKey.backToPlantOverview]: {
+ [HotKey.closePanel]: {
combo: "escape",
- label: t("Back to plant overview"),
+ label: t("Close panel"),
},
[HotKey.openGuide]: {
combo: "shift + ?",
@@ -106,14 +107,9 @@ export const hotkeysWithActions = (
...hotkeysBase[HotKey.addEvent],
onKeyDown: () => { navigate(Path.farmEvents("add")); },
},
- [HotKey.backToPlantOverview]: {
- ...hotkeysBase[HotKey.backToPlantOverview],
- onKeyDown: () => {
- if (slug != "photos") {
- navigate(Path.plants());
- dispatch(unselectPlant(dispatch));
- }
- },
+ [HotKey.closePanel]: {
+ ...hotkeysBase[HotKey.closePanel],
+ onKeyDown: () => { dispatch(setPanelOpen(false)); },
},
[HotKey.openGuide]: hotkeysBase[HotKey.openGuide],
};
diff --git a/frontend/nav/__tests__/index_test.tsx b/frontend/nav/__tests__/index_test.tsx
index 3d4c6eadd0..90e158bca8 100644
--- a/frontend/nav/__tests__/index_test.tsx
+++ b/frontend/nav/__tests__/index_test.tsx
@@ -39,6 +39,7 @@ import {
import { app } from "../../__test_support__/fake_state/app";
import { Actions } from "../../constants";
import { cloneDeep } from "lodash";
+import { mountWithContext } from "../../__test_support__/mount_with_context";
describe("", () => {
const fakeProps = (): NavBarProps => ({
@@ -166,7 +167,7 @@ describe("", () => {
});
it("displays setup button", () => {
- const wrapper = mount();
+ const wrapper = mountWithContext();
wrapper.find(".setup-button").simulate("click");
expect(mockNavigate).toHaveBeenCalledWith(Path.setup());
expect(wrapper.text().toLowerCase()).toContain("complete");
diff --git a/frontend/nav/index.tsx b/frontend/nav/index.tsx
index 0363d454cc..9fb413dd9e 100644
--- a/frontend/nav/index.tsx
+++ b/frontend/nav/index.tsx
@@ -2,7 +2,6 @@ import React from "react";
import { NavBarProps, NavBarState } from "./interfaces";
import { EStopButton } from "./e_stop_btn";
import { Popover } from "../ui";
-import { useNavigate } from "react-router";
import { updatePageInfo } from "../util";
import { validBotLocationData } from "../util/location";
import { NavLinks } from "./nav_links";
@@ -34,6 +33,8 @@ import { PopupsState } from "../interfaces";
import { Panel, TAB_ICON } from "../farm_designer/panel_header";
import { movementPercentRemaining } from "../farm_designer/move_to";
import { isMobile } from "../screen_size";
+import { NavigationContext } from "../routes_helpers";
+import { NavigateFunction } from "react-router";
export class NavBar extends React.Component> {
state: NavBarState = {
@@ -54,6 +55,10 @@ export class NavBar extends React.Component> {
}
};
+ static contextType = NavigationContext;
+ context!: React.ContextType;
+ navigate: NavigateFunction = url => { this.context(url as string); };
+
get isStaff() { return this.props.authAud == "staff"; }
toggle = (key: keyof NavBarState) => () =>
@@ -191,10 +196,9 @@ export class NavBar extends React.Component> {
SetupButton = () => {
const firmwareHardware = this.props.apiFirmwareValue;
const { wizardStepResults, device } = this.props;
- const navigate = useNavigate();
return !device.body.setup_completed_at
? { navigate(Path.setup()); }}>
+ onClick={() => { this.navigate(Path.setup()); }}>
{t("Setup")}
{!isMobile() &&
`: ${setupProgressString(wizardStepResults, { firmwareHardware })}`}
diff --git a/frontend/points/__tests__/point_edit_actions_test.tsx b/frontend/points/__tests__/point_edit_actions_test.tsx
index 43bf9b4aeb..d629709661 100644
--- a/frontend/points/__tests__/point_edit_actions_test.tsx
+++ b/frontend/points/__tests__/point_edit_actions_test.tsx
@@ -15,10 +15,10 @@ import {
EditPointRadius, EditPointRadiusProps,
EditPointColor, EditPointColorProps, updatePoint, EditPointName,
EditPointNameProps,
- AdditionalWeedProperties,
- AdditionalWeedPropertiesProps,
EditPointSoilHeightTag,
EditPointSoilHeightTagProps,
+ EditWeedProperties,
+ EditWeedPropertiesProps,
} from "../point_edit_actions";
import {
fakePoint, fakeWeed,
@@ -140,26 +140,32 @@ describe("", () => {
});
describe("", () => {
- const fakeProps = (): AdditionalWeedPropertiesProps => ({
- point: fakeWeed(),
+ const fakeProps = (): EditWeedPropertiesProps => ({
+ weed: fakeWeed(),
updatePoint: jest.fn(),
+ botOnline: true,
+ dispatch: jest.fn(),
+ defaultAxes: "XY",
+ arduinoBusy: false,
+ currentBotLocation: { x: 10, y: 20, z: 30 },
+ movementState: fakeMovementState(),
});
it("renders unknown source", () => {
const p = fakeProps();
- p.point.body.meta = {
+ p.weed.body.meta = {
meta_key: "meta value", created_by: undefined, key: undefined,
color: "red", type: "weed",
};
- const wrapper = mount();
+ const wrapper = mount();
expect(wrapper.text()).toContain("unknown");
expect(wrapper.text()).toContain("meta value");
});
it("changes method", () => {
const p = fakeProps();
- p.point.body.meta = { removal_method: "automatic" };
- const wrapper = shallow();
+ p.weed.body.meta = { removal_method: "automatic" };
+ const wrapper = shallow();
wrapper.find("input").last().simulate("change");
expect(p.updatePoint).toHaveBeenCalledWith({
meta: { removal_method: "manual" }
diff --git a/frontend/points/point_edit_actions.tsx b/frontend/points/point_edit_actions.tsx
index 87c8b9f3d6..f247faf516 100644
--- a/frontend/points/point_edit_actions.tsx
+++ b/frontend/points/point_edit_actions.tsx
@@ -31,20 +31,12 @@ export const updatePoint =
}
};
-interface EditPointPropertiesProps {
- point: TaggedGenericPointer | TaggedWeedPointer;
- updatePoint(update: PointUpdate): void;
- botOnline: boolean;
- defaultAxes: string;
- arduinoBusy: boolean;
- dispatch: Function;
- currentBotLocation: BotPosition;
- movementState: MovementState;
+interface EditPointPropertiesProps extends EditPointLocationBaseProps {
+ point: TaggedGenericPointer;
}
-export interface AdditionalWeedPropertiesProps {
- point: TaggedWeedPointer;
- updatePoint(update: PointUpdate): void;
+export interface EditWeedPropertiesProps extends EditPointLocationBaseProps {
+ weed: TaggedWeedPointer;
}
export const EditPointProperties = (props: EditPointPropertiesProps) =>
@@ -64,35 +56,72 @@ export const EditPointProperties = (props: EditPointPropertiesProps) =>
defaultAxes={props.defaultAxes}
updatePoint={props.updatePoint} />
- {props.point.body.pointer_type == "GenericPointer" &&
-
-
- }
+
+
+
+
+
+
+ {Object.entries(props.point.body.meta).map(([key, value]) => {
+ switch (key) {
+ case "color":
+ case "at_soil_level":
+ case "removal_method":
+ case "type":
+ case "gridId":
+ return ;
+ case "created_by":
+ return
+ {lookupPointSource(value)}
+ ;
+ default:
+ return
+ {value || ""}
+ ;
+ }
+ })}
;
-export const AdditionalWeedProperties = (props: AdditionalWeedPropertiesProps) =>
-
+export const EditWeedProperties = (props: EditWeedPropertiesProps) =>
+