commit
e490aa83f6
2
Gemfile
2
Gemfile
|
@ -24,7 +24,7 @@ gem "scenic"
|
|||
gem "secure_headers"
|
||||
gem "tzinfo" # For validation of user selected timezone names
|
||||
gem "valid_url"
|
||||
# gem "farady", "~> 1.0.0"
|
||||
gem "kaminari"
|
||||
|
||||
group :development, :test do
|
||||
gem "climate_control"
|
||||
|
|
13
Gemfile.lock
13
Gemfile.lock
|
@ -153,6 +153,18 @@ GEM
|
|||
json (2.3.0)
|
||||
jsonapi-renderer (0.2.2)
|
||||
jwt (2.2.1)
|
||||
kaminari (1.2.0)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.2.0)
|
||||
kaminari-activerecord (= 1.2.0)
|
||||
kaminari-core (= 1.2.0)
|
||||
kaminari-actionview (1.2.0)
|
||||
actionview
|
||||
kaminari-core (= 1.2.0)
|
||||
kaminari-activerecord (1.2.0)
|
||||
activerecord
|
||||
kaminari-core (= 1.2.0)
|
||||
kaminari-core (1.2.0)
|
||||
loofah (2.4.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
|
@ -320,6 +332,7 @@ DEPENDENCIES
|
|||
google-cloud-storage (~> 1.11)
|
||||
hashdiff
|
||||
jwt
|
||||
kaminari
|
||||
mutations
|
||||
passenger
|
||||
pg
|
||||
|
|
|
@ -80,6 +80,16 @@ module Api
|
|||
{ root: false, user: current_user }
|
||||
end
|
||||
|
||||
def maybe_paginate(collection)
|
||||
page = params[:page]
|
||||
per = params[:per]
|
||||
|
||||
if page && per
|
||||
render json: collection.page(page).per(per)
|
||||
else
|
||||
render json: collection
|
||||
end
|
||||
end
|
||||
private
|
||||
|
||||
def clean_expired_farm_events
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module Api
|
||||
class AlertsController < Api::AbstractController
|
||||
def index
|
||||
render json: current_device.alerts
|
||||
maybe_paginate current_device.alerts
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
|
|
@ -3,7 +3,7 @@ module Api
|
|||
before_action :clean_expired_farm_events, only: [:index]
|
||||
|
||||
def index
|
||||
render json: current_device.farm_events
|
||||
maybe_paginate current_device.farm_events
|
||||
end
|
||||
|
||||
def show
|
||||
|
|
|
@ -10,7 +10,7 @@ module Api
|
|||
end
|
||||
|
||||
def index
|
||||
render json: farmware_envs
|
||||
maybe_paginate farmware_envs
|
||||
end
|
||||
|
||||
def show
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module Api
|
||||
class FarmwareInstallationsController < Api::AbstractController
|
||||
def index
|
||||
render json: farmware_installations
|
||||
maybe_paginate farmware_installations
|
||||
end
|
||||
|
||||
def show
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module Api
|
||||
class PeripheralsController < Api::AbstractController
|
||||
def index
|
||||
render json: current_device.peripherals
|
||||
maybe_paginate current_device.peripherals
|
||||
end
|
||||
|
||||
def show
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module Api
|
||||
class PinBindingsController < Api::AbstractController
|
||||
def index
|
||||
render json: pin_bindings
|
||||
maybe_paginate pin_bindings
|
||||
end
|
||||
|
||||
def show
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module Api
|
||||
class PlantTemplatesController < Api::AbstractController
|
||||
def index
|
||||
render json: current_device.plant_templates
|
||||
maybe_paginate current_device.plant_templates
|
||||
end
|
||||
|
||||
def create
|
||||
|
|
|
@ -3,7 +3,7 @@ module Api
|
|||
before_action :clean_expired_farm_events, only: [:destroy]
|
||||
|
||||
def index
|
||||
render json: your_point_groups
|
||||
maybe_paginate your_point_groups
|
||||
end
|
||||
|
||||
def show
|
||||
|
|
|
@ -20,7 +20,7 @@ module Api
|
|||
.where("discarded_at < ?", Time.now - HARD_DELETE_AFTER)
|
||||
.destroy_all
|
||||
|
||||
render json: points(params.fetch(:filter) { "kept" })
|
||||
maybe_paginate points(params.fetch(:filter) { "kept" })
|
||||
end
|
||||
|
||||
def show
|
||||
|
|
|
@ -3,7 +3,7 @@ module Api
|
|||
before_action :clean_expired_farm_events, only: [:destroy]
|
||||
|
||||
def index
|
||||
render json: your_regimens
|
||||
maybe_paginate your_regimens
|
||||
end
|
||||
|
||||
def show
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module Api
|
||||
class SavedGardensController < Api::AbstractController
|
||||
def index
|
||||
render json: current_device.saved_gardens
|
||||
maybe_paginate current_device.saved_gardens
|
||||
end
|
||||
|
||||
def create
|
||||
|
|
|
@ -5,7 +5,7 @@ module Api
|
|||
end
|
||||
|
||||
def index
|
||||
render json: readings
|
||||
maybe_paginate(readings)
|
||||
end
|
||||
|
||||
def show
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module Api
|
||||
class SensorsController < Api::AbstractController
|
||||
def index
|
||||
render json: current_device.sensors
|
||||
maybe_paginate current_device.sensors
|
||||
end
|
||||
|
||||
def show
|
||||
|
|
|
@ -2,7 +2,7 @@ module Api
|
|||
class ToolsController < Api::AbstractController
|
||||
|
||||
def index
|
||||
render json: tools
|
||||
maybe_paginate tools
|
||||
end
|
||||
|
||||
def show
|
||||
|
|
|
@ -7,7 +7,7 @@ module Api
|
|||
end
|
||||
|
||||
def index
|
||||
render json: webcams
|
||||
maybe_paginate webcams
|
||||
end
|
||||
|
||||
def show
|
||||
|
|
|
@ -4,4 +4,8 @@ class PointGroupSerializer < ApplicationSerializer
|
|||
def point_ids
|
||||
object.point_group_items.pluck(:point_id)
|
||||
end
|
||||
|
||||
def criteria
|
||||
object.criteria || PointGroup::DEFAULT_CRITERIA
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
class AddShowZonesToWebAppConfig < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
add_column :web_app_configs,
|
||||
:show_zones,
|
||||
:boolean,
|
||||
default: false
|
||||
end
|
||||
end
|
|
@ -1,3 +1,5 @@
|
|||
import * as React from "react";
|
||||
|
||||
jest.mock("browser-speech", () => ({
|
||||
talk: jest.fn(),
|
||||
}));
|
||||
|
@ -16,3 +18,8 @@ window.location = {
|
|||
pathname: "", href: "", hash: "", search: "",
|
||||
hostname: "", origin: "", port: "", protocol: "", host: "",
|
||||
};
|
||||
|
||||
jest.mock("../error_boundary", () => ({
|
||||
// tslint:disable-next-line:no-any
|
||||
ErrorBoundary: (p: any) => <div>{p.children}</div>,
|
||||
}));
|
|
@ -0,0 +1,36 @@
|
|||
import { DeepPartial } from "redux";
|
||||
|
||||
type DomEvent = React.SyntheticEvent<HTMLInputElement>;
|
||||
export const inputEvent = (value: string, name?: string): DomEvent => {
|
||||
const event: DeepPartial<DomEvent> = { currentTarget: { value, name } };
|
||||
return event as DomEvent;
|
||||
};
|
||||
|
||||
type ChangeEvent = React.ChangeEvent<HTMLInputElement>;
|
||||
export const changeEvent = (value: string): ChangeEvent => {
|
||||
const event: DeepPartial<ChangeEvent> = { currentTarget: { value } };
|
||||
return event as ChangeEvent;
|
||||
};
|
||||
|
||||
type IMGEvent = React.SyntheticEvent<HTMLImageElement, Event>;
|
||||
export const imgEvent = (): IMGEvent => {
|
||||
const event: DeepPartial<IMGEvent> = {
|
||||
currentTarget: {
|
||||
getAttribute: jest.fn(),
|
||||
setAttribute: jest.fn(),
|
||||
}
|
||||
};
|
||||
return event as IMGEvent;
|
||||
};
|
||||
|
||||
type FormEvent = React.FormEvent<HTMLFormElement>;
|
||||
export const formEvent = (): FormEvent => {
|
||||
const event: Partial<FormEvent> = { preventDefault: jest.fn() };
|
||||
return event as FormEvent;
|
||||
};
|
||||
|
||||
type DragEvent = React.DragEvent<HTMLElement>;
|
||||
export const dragEvent = (key: string): DragEvent => {
|
||||
const event: DeepPartial<DragEvent> = { dataTransfer: { getData: () => key } };
|
||||
return event as DragEvent;
|
||||
};
|
|
@ -1,7 +0,0 @@
|
|||
import { DeepPartial } from "redux";
|
||||
|
||||
type DomEvent = React.SyntheticEvent<HTMLInputElement>;
|
||||
export const inputEvent = (value: string): DomEvent => {
|
||||
const event: DeepPartial<DomEvent> = { currentTarget: { value } };
|
||||
return event as DomEvent;
|
||||
};
|
|
@ -316,6 +316,7 @@ export function fakeWebAppConfig(): TaggedWebAppConfig {
|
|||
show_historic_points: false,
|
||||
time_format_24_hour: false,
|
||||
show_pins: false,
|
||||
show_zones: false,
|
||||
disable_emergency_unlock_confirmation: false,
|
||||
map_size_x: 2900,
|
||||
map_size_y: 1400,
|
||||
|
@ -459,7 +460,7 @@ export function fakePointGroup(): TaggedPointGroup {
|
|||
sort_type: "xy_ascending",
|
||||
point_ids: [],
|
||||
criteria: {
|
||||
day: { op: ">", days: 0 },
|
||||
day: { op: "<", days: 0 },
|
||||
number_eq: {},
|
||||
number_gt: {},
|
||||
number_lt: {},
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
jest.unmock("../error_boundary");
|
||||
|
||||
jest.mock("../util/errors.ts", () => ({ catchErrors: jest.fn() }));
|
||||
|
||||
import * as React from "react";
|
||||
|
|
|
@ -4,8 +4,6 @@ jest.mock("../index", () => ({
|
|||
dispatchQosStart: jest.fn(),
|
||||
pingOK: jest.fn()
|
||||
}));
|
||||
const mockTimestamp = 0;
|
||||
jest.mock("../../util", () => ({ timestamp: () => mockTimestamp }));
|
||||
|
||||
import {
|
||||
readPing,
|
||||
|
|
|
@ -572,12 +572,6 @@ export namespace Content {
|
|||
export const CONFIRM_PLANT_DELETION =
|
||||
trim(`Show a confirmation dialog when deleting a plant.`);
|
||||
|
||||
export const SORT_DESCRIPTION =
|
||||
trim(`When executing a sequence over a Group of locations, FarmBot will
|
||||
travel to each group member in the order of the chosen sort method.
|
||||
If the random option is chosen, FarmBot will travel in a random order
|
||||
every time, so the ordering shown below will only be representative.`);
|
||||
|
||||
// Device
|
||||
export const NOT_HTTPS =
|
||||
trim(`WARNING: Sending passwords via HTTP:// is not secure.`);
|
||||
|
@ -824,6 +818,20 @@ export namespace Content {
|
|||
trim(`You haven't made any sequences or regimens yet. To add an event,
|
||||
first create a sequence or regimen.`);
|
||||
|
||||
// Groups
|
||||
export const SORT_DESCRIPTION =
|
||||
trim(`When executing a sequence over a Group of locations, FarmBot will
|
||||
travel to each group member in the order of the chosen sort method.
|
||||
If the random option is chosen, FarmBot will travel in a random order
|
||||
every time, so the ordering shown below will only be representative.`);
|
||||
|
||||
export const CRITERIA_SELECTION_COUNT =
|
||||
trim(`Criteria additions can only be removed by changing criteria.
|
||||
Click and drag in the map to modify zone selection criteria.
|
||||
Criteria will be applied at the time of sequence execution. The final
|
||||
selection at that time may differ from the selection currently
|
||||
displayed.`);
|
||||
|
||||
// Farmware
|
||||
export const NO_IMAGES_YET =
|
||||
trim(`You haven't yet taken any photos with your FarmBot.
|
||||
|
@ -912,12 +920,12 @@ export namespace DiagnosticMessages {
|
|||
network, a firewall may be blocking port 5672. Ensure that the blue LED
|
||||
communications light on the FarmBot electronics box is illuminated.`);
|
||||
|
||||
export const WIFI_OR_CONFIG = trim(`Your browser is connected correctly, but
|
||||
we have no recent record of FarmBot connecting to the internet. This usually
|
||||
happens because of poor WiFi connectivity in the garden, a bad password during
|
||||
configuration, a very long power outage, or blocked ports on FarmBot's local
|
||||
network. Please refer IT staff to
|
||||
https://software.farm.bot/docs/for-it-security-professionals`);
|
||||
export const WIFI_OR_CONFIG = trim(`Your browser is connected correctly,
|
||||
but we have no recent record of FarmBot connecting to the internet.
|
||||
This usually happens because of poor WiFi connectivity in the garden,
|
||||
a bad password during configuration, a very long power outage, or
|
||||
blocked ports on FarmBot's local network. Please refer IT staff to
|
||||
https://software.farm.bot/docs/for-it-security-professionals`);
|
||||
|
||||
export const NO_WS_AVAILABLE = trim(`You are either offline, using a web
|
||||
browser that does not support WebSockets, or are behind a firewall that
|
||||
|
|
|
@ -25,6 +25,7 @@ describe("<Controls />", () => {
|
|||
sensorReadings: [],
|
||||
timeSettings: fakeTimeSettings(),
|
||||
env: {},
|
||||
firmwareHardware: undefined,
|
||||
});
|
||||
|
||||
it("shows webcam widget", () => {
|
||||
|
|
|
@ -34,6 +34,7 @@ export class RawControls extends React.Component<Props, {}> {
|
|||
arduinoBusy={this.arduinoBusy}
|
||||
botToMqttStatus={this.props.botToMqttStatus}
|
||||
firmwareSettings={this.props.firmwareSettings}
|
||||
firmwareHardware={this.props.firmwareHardware}
|
||||
getWebAppConfigVal={this.props.getWebAppConfigVal} />
|
||||
|
||||
peripherals = () => <Peripherals
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
BotState, Xyz, BotPosition, ShouldDisplay, UserEnv
|
||||
} from "../devices/interfaces";
|
||||
import { Vector3, McuParams } from "farmbot/dist";
|
||||
import { Vector3, McuParams, FirmwareHardware } from "farmbot/dist";
|
||||
import {
|
||||
TaggedWebcamFeed,
|
||||
TaggedPeripheral,
|
||||
|
@ -25,6 +25,7 @@ export interface Props {
|
|||
sensorReadings: TaggedSensorReading[];
|
||||
timeSettings: TimeSettings;
|
||||
env: UserEnv;
|
||||
firmwareHardware: FirmwareHardware | undefined;
|
||||
}
|
||||
|
||||
export interface AxisDisplayGroupProps {
|
||||
|
|
|
@ -1,17 +1,12 @@
|
|||
const mockDevice = {
|
||||
moveAbsolute: jest.fn(() => { return Promise.resolve(); }),
|
||||
moveAbsolute: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
jest.mock("../../../device", () => ({ getDevice: () => mockDevice }));
|
||||
|
||||
jest.mock("../../../device", () => ({
|
||||
getDevice: () => (mockDevice)
|
||||
jest.mock("../../../config_storage/actions", () => ({
|
||||
toggleWebAppBool: jest.fn()
|
||||
}));
|
||||
|
||||
jest.mock("../../../config_storage/actions", () => {
|
||||
return {
|
||||
toggleWebAppBool: jest.fn()
|
||||
};
|
||||
});
|
||||
|
||||
import * as React from "react";
|
||||
import { shallow, mount } from "enzyme";
|
||||
import { BotPositionRows, BotPositionRowsProps } from "../bot_position_rows";
|
||||
|
@ -22,14 +17,12 @@ import { BooleanSetting } from "../../../session_keys";
|
|||
describe("<BotPositionRows />", () => {
|
||||
const mockConfig: Dictionary<boolean> = {};
|
||||
|
||||
function fakeProps(): BotPositionRowsProps {
|
||||
return {
|
||||
getValue: jest.fn(key => mockConfig[key]),
|
||||
locationData: bot.hardware.location_data,
|
||||
arduinoBusy: false,
|
||||
firmware_version: undefined,
|
||||
};
|
||||
}
|
||||
const fakeProps = (): BotPositionRowsProps => ({
|
||||
getValue: jest.fn(key => mockConfig[key]),
|
||||
locationData: bot.hardware.location_data,
|
||||
arduinoBusy: false,
|
||||
firmwareHardware: undefined,
|
||||
});
|
||||
|
||||
it("inputs axis destination", () => {
|
||||
const wrapper = shallow(<BotPositionRows {...fakeProps()} />);
|
||||
|
@ -38,19 +31,21 @@ describe("<BotPositionRows />", () => {
|
|||
expect(mockDevice.moveAbsolute).toHaveBeenCalledWith("123");
|
||||
});
|
||||
|
||||
it("shows encoder position in steps", () => {
|
||||
it("shows encoder position", () => {
|
||||
mockConfig[BooleanSetting.scaled_encoders] = true;
|
||||
mockConfig[BooleanSetting.raw_encoders] = true;
|
||||
const p = fakeProps();
|
||||
p.firmware_version = undefined;
|
||||
p.firmwareHardware = undefined;
|
||||
const wrapper = mount(<BotPositionRows {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).toContain("scaled encoder (steps)");
|
||||
expect(wrapper.text().toLowerCase()).toContain("encoder");
|
||||
});
|
||||
|
||||
it("shows encoder position in mm", () => {
|
||||
it("doesn't show encoder position", () => {
|
||||
mockConfig[BooleanSetting.scaled_encoders] = true;
|
||||
mockConfig[BooleanSetting.raw_encoders] = true;
|
||||
const p = fakeProps();
|
||||
p.firmware_version = "6.0.0";
|
||||
p.firmwareHardware = "express_k10";
|
||||
const wrapper = mount(<BotPositionRows {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).toContain("scaled encoder (mm)");
|
||||
expect(wrapper.text().toLowerCase()).not.toContain("encoder");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -33,6 +33,7 @@ describe("<Move />", () => {
|
|||
firmwareSettings: bot.hardware.mcu_params,
|
||||
getWebAppConfigVal: jest.fn((key) => (mockConfig[key])),
|
||||
env: {},
|
||||
firmwareHardware: undefined,
|
||||
});
|
||||
|
||||
it("has default elements", () => {
|
||||
|
|
|
@ -8,7 +8,9 @@ jest.mock("../../../account/dev/dev_support", () => ({
|
|||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { BooleanSetting } from "../../../session_keys";
|
||||
import { moveWidgetSetting, MoveWidgetSettingsMenu } from "../settings_menu";
|
||||
import {
|
||||
moveWidgetSetting, MoveWidgetSettingsMenu, MoveWidgetSettingsMenuProps
|
||||
} from "../settings_menu";
|
||||
|
||||
describe("moveWidgetSetting()", () => {
|
||||
it("renders setting", () => {
|
||||
|
@ -22,9 +24,10 @@ describe("moveWidgetSetting()", () => {
|
|||
});
|
||||
|
||||
describe("<MoveWidgetSettingsMenu />", () => {
|
||||
const fakeProps = () => ({
|
||||
const fakeProps = (): MoveWidgetSettingsMenuProps => ({
|
||||
toggle: jest.fn(),
|
||||
getValue: jest.fn(),
|
||||
firmwareHardware: undefined,
|
||||
});
|
||||
|
||||
it("displays motor plot toggle", () => {
|
||||
|
@ -35,4 +38,16 @@ describe("<MoveWidgetSettingsMenu />", () => {
|
|||
expect(wrapper.text()).toContain("Motor position plot");
|
||||
mockDev = false;
|
||||
});
|
||||
|
||||
it("displays encoder toggles", () => {
|
||||
const wrapper = mount(<MoveWidgetSettingsMenu {...fakeProps()} />);
|
||||
expect(wrapper.text().toLowerCase()).toContain("encoder");
|
||||
});
|
||||
|
||||
it("doesn't display encoder toggles", () => {
|
||||
const p = fakeProps();
|
||||
p.firmwareHardware = "express_k10";
|
||||
const wrapper = mount(<MoveWidgetSettingsMenu {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).not.toContain("encoder");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,26 +2,23 @@ import * as React from "react";
|
|||
import { Row, Col } from "../../ui";
|
||||
import { BotLocationData } from "../../devices/interfaces";
|
||||
import { moveAbs } from "../../devices/actions";
|
||||
import { minFwVersionCheck } from "../../util";
|
||||
import { AxisDisplayGroup } from "../axis_display_group";
|
||||
import { AxisInputBoxGroup } from "../axis_input_box_group";
|
||||
import { GetWebAppBool } from "./interfaces";
|
||||
import { BooleanSetting } from "../../session_keys";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { isExpressBoard } from "../../devices/components/firmware_hardware_support";
|
||||
import { FirmwareHardware } from "farmbot";
|
||||
|
||||
export interface BotPositionRowsProps {
|
||||
locationData: BotLocationData;
|
||||
getValue: GetWebAppBool;
|
||||
arduinoBusy: boolean;
|
||||
firmware_version: string | undefined;
|
||||
firmwareHardware: FirmwareHardware | undefined;
|
||||
}
|
||||
|
||||
export const BotPositionRows = (props: BotPositionRowsProps) => {
|
||||
const { locationData, getValue, arduinoBusy, firmware_version } = props;
|
||||
const scaled_encoder_label =
|
||||
minFwVersionCheck(firmware_version, "5.0.5")
|
||||
? t("Scaled Encoder (mm)")
|
||||
: t("Scaled Encoder (steps)");
|
||||
const { locationData, getValue, arduinoBusy } = props;
|
||||
return <div>
|
||||
<Row>
|
||||
<Col xs={3}>
|
||||
|
@ -37,11 +34,13 @@ export const BotPositionRows = (props: BotPositionRowsProps) => {
|
|||
<AxisDisplayGroup
|
||||
position={locationData.position}
|
||||
label={t("Motor Coordinates (mm)")} />
|
||||
{getValue(BooleanSetting.scaled_encoders) &&
|
||||
{!isExpressBoard(props.firmwareHardware) &&
|
||||
getValue(BooleanSetting.scaled_encoders) &&
|
||||
<AxisDisplayGroup
|
||||
position={locationData.scaled_encoders}
|
||||
label={scaled_encoder_label} />}
|
||||
{getValue(BooleanSetting.raw_encoders) &&
|
||||
label={t("Scaled Encoder (mm)")} />}
|
||||
{!isExpressBoard(props.firmwareHardware) &&
|
||||
getValue(BooleanSetting.raw_encoders) &&
|
||||
<AxisDisplayGroup
|
||||
position={locationData.raw_encoders}
|
||||
label={t("Raw Encoder data")} />}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { BotPosition, BotState, UserEnv } from "../../devices/interfaces";
|
||||
import { McuParams, Xyz } from "farmbot";
|
||||
import { McuParams, Xyz, FirmwareHardware } from "farmbot";
|
||||
import { NetworkState } from "../../connectivity/interfaces";
|
||||
import { GetWebAppConfigValue } from "../../config_storage/actions";
|
||||
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
|
||||
|
@ -15,6 +15,7 @@ export interface MoveProps {
|
|||
firmwareSettings: McuParams;
|
||||
getWebAppConfigVal: GetWebAppConfigValue;
|
||||
env: UserEnv;
|
||||
firmwareHardware: FirmwareHardware | undefined;
|
||||
}
|
||||
|
||||
export interface DirectionButtonProps {
|
||||
|
|
|
@ -33,7 +33,8 @@ export class Move extends React.Component<MoveProps, {}> {
|
|||
<i className="fa fa-gear" />
|
||||
<MoveWidgetSettingsMenu
|
||||
toggle={this.toggle}
|
||||
getValue={this.getValue} />
|
||||
getValue={this.getValue}
|
||||
firmwareHardware={this.props.firmwareHardware} />
|
||||
</Popover>
|
||||
</WidgetHeader>
|
||||
<WidgetBody>
|
||||
|
@ -53,7 +54,7 @@ export class Move extends React.Component<MoveProps, {}> {
|
|||
locationData={locationData}
|
||||
getValue={this.getValue}
|
||||
arduinoBusy={this.props.arduinoBusy}
|
||||
firmware_version={informational_settings.firmware_version} />
|
||||
firmwareHardware={this.props.firmwareHardware} />
|
||||
</MustBeOnline>
|
||||
{this.props.getWebAppConfigVal(BooleanSetting.show_motor_plot) &&
|
||||
<MotorPositionPlot locationData={locationData} />}
|
||||
|
|
|
@ -5,6 +5,8 @@ import { ToggleWebAppBool, GetWebAppBool } from "./interfaces";
|
|||
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
|
||||
import { DevSettings } from "../../account/dev/dev_support";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { FirmwareHardware } from "farmbot";
|
||||
import { isExpressBoard } from "../../devices/components/firmware_hardware_support";
|
||||
|
||||
export const moveWidgetSetting =
|
||||
(toggle: ToggleWebAppBool, getValue: GetWebAppBool) =>
|
||||
|
@ -18,10 +20,15 @@ export const moveWidgetSetting =
|
|||
toggleValue={getValue(setting)} />
|
||||
</fieldset>;
|
||||
|
||||
export const MoveWidgetSettingsMenu = ({ toggle, getValue }: {
|
||||
toggle: ToggleWebAppBool,
|
||||
getValue: GetWebAppBool
|
||||
}) => {
|
||||
export interface MoveWidgetSettingsMenuProps {
|
||||
toggle: ToggleWebAppBool;
|
||||
getValue: GetWebAppBool;
|
||||
firmwareHardware: FirmwareHardware | undefined;
|
||||
}
|
||||
|
||||
export const MoveWidgetSettingsMenu = (
|
||||
{ toggle, getValue, firmwareHardware }: MoveWidgetSettingsMenuProps
|
||||
) => {
|
||||
const Setting = moveWidgetSetting(toggle, getValue);
|
||||
return <div className="move-settings-menu">
|
||||
<p>{t("Invert Jog Buttons")}</p>
|
||||
|
@ -29,13 +36,16 @@ export const MoveWidgetSettingsMenu = ({ toggle, getValue }: {
|
|||
<Setting label={t("Y Axis")} setting={BooleanSetting.y_axis_inverted} />
|
||||
<Setting label={t("Z Axis")} setting={BooleanSetting.z_axis_inverted} />
|
||||
|
||||
<p>{t("Display Encoder Data")}</p>
|
||||
<Setting
|
||||
label={t("Scaled encoder position")}
|
||||
setting={BooleanSetting.scaled_encoders} />
|
||||
<Setting
|
||||
label={t("Raw encoder position")}
|
||||
setting={BooleanSetting.raw_encoders} />
|
||||
{!isExpressBoard(firmwareHardware) &&
|
||||
<div className="display-encoder-data">
|
||||
<p>{t("Display Encoder Data")}</p>
|
||||
<Setting
|
||||
label={t("Scaled encoder position")}
|
||||
setting={BooleanSetting.scaled_encoders} />
|
||||
<Setting
|
||||
label={t("Raw encoder position")}
|
||||
setting={BooleanSetting.raw_encoders} />
|
||||
</div>}
|
||||
|
||||
<p>{t("Swap jog buttons (and rotate map)")}</p>
|
||||
<Setting label={t("x and y axis")} setting={BooleanSetting.xy_swap} />
|
||||
|
|
|
@ -7,12 +7,14 @@ import {
|
|||
maybeGetTimeSettings
|
||||
} from "../resources/selectors";
|
||||
import { Props } from "./interfaces";
|
||||
import { validFwConfig } from "../util";
|
||||
import { validFwConfig, validFbosConfig } from "../util";
|
||||
import { getWebAppConfigValue } from "../config_storage/actions";
|
||||
import { getFirmwareConfig } from "../resources/getters";
|
||||
import { getFirmwareConfig, getFbosConfig } from "../resources/getters";
|
||||
import { uniq } from "lodash";
|
||||
import { getStatus } from "../connectivity/reducer_support";
|
||||
import { getEnv, getShouldDisplayFn } from "../farmware/state_to_props";
|
||||
import { sourceFbosConfigValue } from "../devices/components/source_config_value";
|
||||
import { isFwHardwareValue } from "../devices/components/firmware_hardware_support";
|
||||
|
||||
export function mapStateToProps(props: Everything): Props {
|
||||
const fwConfig = validFwConfig(getFirmwareConfig(props.resources.index));
|
||||
|
@ -21,6 +23,12 @@ export function mapStateToProps(props: Everything): Props {
|
|||
const shouldDisplay = getShouldDisplayFn(props.resources.index, props.bot);
|
||||
const env = getEnv(props.resources.index, shouldDisplay, props.bot);
|
||||
|
||||
const { configuration } = props.bot.hardware;
|
||||
const fbosConfig = validFbosConfig(getFbosConfig(props.resources.index));
|
||||
const sourceFbosConfig = sourceFbosConfigValue(fbosConfig, configuration);
|
||||
const { value } = sourceFbosConfig("firmware_hardware");
|
||||
const firmwareHardware = isFwHardwareValue(value) ? value : undefined;
|
||||
|
||||
return {
|
||||
feeds: selectAllWebcamFeeds(props.resources.index),
|
||||
dispatch: props.dispatch,
|
||||
|
@ -34,5 +42,6 @@ export function mapStateToProps(props: Everything): Props {
|
|||
sensorReadings: selectAllSensorReadings(props.resources.index),
|
||||
timeSettings: maybeGetTimeSettings(props.resources.index),
|
||||
env,
|
||||
firmwareHardware,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -213,7 +213,7 @@
|
|||
.groups-list-wrapper {
|
||||
padding: 0.5em 0em;
|
||||
}
|
||||
.groups-delete-btn {
|
||||
.group-delete-btn {
|
||||
float: left;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
@ -332,6 +332,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
.zones-layer {
|
||||
[id*="zones-1D-"] {
|
||||
stroke: $black;
|
||||
stroke-width: 5;
|
||||
}
|
||||
[id*="zones-"] {
|
||||
opacity: 0.1;
|
||||
&.current {
|
||||
opacity: 0.25;
|
||||
fill: $white;
|
||||
stroke: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.virtual-bot-trail,
|
||||
.virtual-peripherals {
|
||||
pointer-events: none;
|
||||
|
|
|
@ -738,6 +738,114 @@
|
|||
}
|
||||
}
|
||||
|
||||
.group-detail-panel {
|
||||
.panel-content {
|
||||
.group-criteria {
|
||||
margin-top: 1rem;
|
||||
.criteria-heading {
|
||||
margin-top: 0;
|
||||
}
|
||||
.fb-button {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.group-criteria-presets {
|
||||
input[type="radio"] {
|
||||
width: auto;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
p {
|
||||
display: inline;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
.criteria-string,
|
||||
.criteria-pointer-type,
|
||||
.criteria-plant-status,
|
||||
.criteria-slug {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.location-criteria {
|
||||
.row {
|
||||
margin-top: 1rem;
|
||||
p {
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
label {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.day-criteria {
|
||||
p {
|
||||
display: inline;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
.string-eq-criteria {
|
||||
margin-top: 1rem;
|
||||
.row {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
.number-eq-criteria,
|
||||
.number-gt-lt-criteria {
|
||||
margin-top: 1rem;
|
||||
.row {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
p {
|
||||
text-align: center;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
.expandable-header {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
}
|
||||
.criteria-point-count-breakdown {
|
||||
margin-bottom: 1rem;
|
||||
.manual-group-member-count,
|
||||
.criteria-group-member-count {
|
||||
margin-left: 2rem;
|
||||
div {
|
||||
display: inline;
|
||||
padding: 0.25rem;
|
||||
font-size: 1.2rem;
|
||||
border: 1px solid $panel_light_blue;
|
||||
}
|
||||
p {
|
||||
display: inline;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
.criteria-group-member-count {
|
||||
div {
|
||||
border: 1px solid gray;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.zone-info-panel {
|
||||
.panel-content {
|
||||
.location-criteria {
|
||||
.row {
|
||||
margin-top: 1rem;
|
||||
p {
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
label {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.weeds-inventory-panel,
|
||||
.zones-inventory-panel,
|
||||
.groups-panel {
|
||||
|
|
|
@ -1579,7 +1579,7 @@ textarea:focus {
|
|||
}
|
||||
}
|
||||
table {
|
||||
margin: 2rem;
|
||||
margin: auto;
|
||||
margin-top: 3rem;
|
||||
width: 93%;
|
||||
font-size: 1.2rem;
|
||||
|
@ -1596,6 +1596,7 @@ textarea:focus {
|
|||
padding-bottom: 1rem;
|
||||
span {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -95,9 +95,9 @@ select {
|
|||
}
|
||||
}
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
button {
|
||||
background: darken($white, 10%) !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { boardType } from "../firmware_hardware_support";
|
|||
|
||||
describe("boardType()", () => {
|
||||
it("returns Farmduino", () => {
|
||||
expect(boardType("5.0.3.F")).toEqual("farmduino");
|
||||
expect(boardType("5.0.3.F.extra")).toEqual("farmduino");
|
||||
});
|
||||
|
||||
it("returns Farmduino k1.4", () => {
|
||||
|
@ -25,6 +25,7 @@ describe("boardType()", () => {
|
|||
expect(boardType(undefined)).toEqual("unknown");
|
||||
expect(boardType("Arduino Disconnected!")).toEqual("unknown");
|
||||
expect(boardType("STUBFW")).toEqual("unknown");
|
||||
expect(boardType("0.0.0.S.STUB")).toEqual("unknown");
|
||||
});
|
||||
|
||||
it("returns None", () => {
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import { sequence2ddi, mapStateToProps, RawBootSequenceSelector } from "../boot_sequence_selector";
|
||||
import { fakeSequence, fakeFbosConfig } from "../../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
sequence2ddi, mapStateToProps, RawBootSequenceSelector
|
||||
} from "../boot_sequence_selector";
|
||||
import {
|
||||
fakeSequence, fakeFbosConfig
|
||||
} from "../../../../__test_support__/fake_state/resources";
|
||||
import { fakeState } from "../../../../__test_support__/fake_state";
|
||||
import { buildResourceIndex } from "../../../../__test_support__/resource_index_builder";
|
||||
import {
|
||||
buildResourceIndex
|
||||
} from "../../../../__test_support__/resource_index_builder";
|
||||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { FBSelect } from "../../../../ui";
|
||||
|
|
|
@ -37,7 +37,7 @@ describe("<FbosDetails/>", () => {
|
|||
p.botInfoSettings.commit = "fakeCommit";
|
||||
p.botInfoSettings.target = "fakeTarget";
|
||||
p.botInfoSettings.node_name = "fakeName";
|
||||
p.botInfoSettings.firmware_version = "fakeFirmware";
|
||||
p.botInfoSettings.firmware_version = "0.0.0.R.ramps";
|
||||
p.botInfoSettings.firmware_commit = "fakeFwCommit";
|
||||
p.botInfoSettings.soc_temp = 48.3;
|
||||
p.botInfoSettings.wifi_level = -49;
|
||||
|
@ -50,8 +50,9 @@ describe("<FbosDetails/>", () => {
|
|||
"Commit", "fakeComm",
|
||||
"Target", "fakeTarget",
|
||||
"Node name", "fakeName",
|
||||
"Firmware", "fakeFirmware",
|
||||
"Firmware", "0.0.0 Arduino/RAMPS (Genesis v1.2)",
|
||||
"Firmware commit", "fakeFwCo",
|
||||
"Firmware code", "0.0.0.R.ramps",
|
||||
"FAKETARGET CPU temperature", "48.3", "C",
|
||||
"WiFi strength", "-49dBm",
|
||||
"OS release channel",
|
||||
|
@ -70,6 +71,20 @@ describe("<FbosDetails/>", () => {
|
|||
expect(wrapper.text()).not.toContain("name@");
|
||||
});
|
||||
|
||||
it("handles missing firmware version", () => {
|
||||
const p = fakeProps();
|
||||
p.botInfoSettings.firmware_version = undefined;
|
||||
const wrapper = mount(<FbosDetails {...p} />);
|
||||
expect(wrapper.text()).toContain("---");
|
||||
});
|
||||
|
||||
it("handles unknown firmware version", () => {
|
||||
const p = fakeProps();
|
||||
p.botInfoSettings.firmware_version = "0.0.0.S.S";
|
||||
const wrapper = mount(<FbosDetails {...p} />);
|
||||
expect(wrapper.text()).toContain("0.0.0");
|
||||
});
|
||||
|
||||
it("displays commit link", () => {
|
||||
const p = fakeProps();
|
||||
p.botInfoSettings.commit = "abcdefgh";
|
||||
|
|
|
@ -13,6 +13,7 @@ import moment from "moment";
|
|||
import { timeFormatString } from "../../../util";
|
||||
import { TimeSettings } from "../../../interfaces";
|
||||
import { StringConfigKey } from "farmbot/dist/resources/configs/fbos";
|
||||
import { boardType, FIRMWARE_CHOICES_DDI } from "../firmware_hardware_support";
|
||||
|
||||
/** Return an indicator color for the given temperature (C). */
|
||||
export const colorFromTemp = (temp: number | undefined): string => {
|
||||
|
@ -244,6 +245,13 @@ const reformatDatetime = (datetime: string, timeSettings: TimeSettings) =>
|
|||
.utcOffset(timeSettings.utcOffset)
|
||||
.format(`MMMM D, ${timeFormatString(timeSettings)}`);
|
||||
|
||||
const reformatFwVersion = (firmwareVersion: string | undefined): string => {
|
||||
const version = firmwareVersion ?
|
||||
firmwareVersion.split(".").slice(0, 3).join(".") : "none";
|
||||
const board = FIRMWARE_CHOICES_DDI[boardType(firmwareVersion)]?.label || "";
|
||||
return version == "none" ? "---" : `${version} ${board}`;
|
||||
};
|
||||
|
||||
/** Current technical information about FarmBot OS running on the device. */
|
||||
export function FbosDetails(props: FbosDetailsProps) {
|
||||
const {
|
||||
|
@ -265,9 +273,10 @@ export function FbosDetails(props: FbosDetailsProps) {
|
|||
<p><b>{t("Node name")}: </b>{last((node_name || "").split("@"))}</p>
|
||||
<p><b>{t("Device ID")}: </b>{props.deviceAccount.body.id}</p>
|
||||
{isString(private_ip) && <p><b>{t("Local IP address")}: </b>{private_ip}</p>}
|
||||
<p><b>{t("Firmware")}: </b>{firmware_version}</p>
|
||||
<p><b>{t("Firmware")}: </b>{reformatFwVersion(firmware_version)}</p>
|
||||
<CommitDisplay title={t("Firmware commit")}
|
||||
repo={"farmbot-arduino-firmware"} commit={firmware_commit} />
|
||||
<p><b>{t("Firmware code")}: </b>{firmware_version}</p>
|
||||
{isNumber(uptime) && <UptimeDisplay uptime_sec={uptime} />}
|
||||
{isNumber(memory_usage) &&
|
||||
<p><b>{t("Memory usage")}: </b>{memory_usage}MB</p>}
|
||||
|
|
|
@ -11,9 +11,18 @@ export const isFwHardwareValue = (x?: unknown): x is FirmwareHardware => {
|
|||
return !!values.includes(x as FirmwareHardware);
|
||||
};
|
||||
|
||||
const TMC_BOARDS = ["express_k10", "farmduino_k15"];
|
||||
const EXPRESS_BOARDS = ["express_k10"];
|
||||
|
||||
export const isTMCBoard = (firmwareHardware: FirmwareHardware | undefined) =>
|
||||
!!(firmwareHardware && TMC_BOARDS.includes(firmwareHardware));
|
||||
|
||||
export const isExpressBoard = (firmwareHardware: FirmwareHardware | undefined) =>
|
||||
!!(firmwareHardware && EXPRESS_BOARDS.includes(firmwareHardware));
|
||||
|
||||
export const getBoardIdentifier =
|
||||
(firmwareVersion: string | undefined): string =>
|
||||
firmwareVersion ? firmwareVersion.slice(-1) : "undefined";
|
||||
firmwareVersion ? firmwareVersion.split(".")[3] : "undefined";
|
||||
|
||||
export const isKnownBoard = (firmwareVersion: string | undefined): boolean => {
|
||||
const boardIdentifier = getBoardIdentifier(firmwareVersion);
|
||||
|
|
|
@ -25,7 +25,6 @@ export class HardwareSettings extends
|
|||
botToMqttStatus, firmwareHardware, resources
|
||||
} = this.props;
|
||||
const { informational_settings } = this.props.bot.hardware;
|
||||
const firmwareVersion = informational_settings.firmware_version;
|
||||
const { sync_status } = informational_settings;
|
||||
const botDisconnected = !isBotOnline(sync_status, botToMqttStatus);
|
||||
return <Widget className="hardware-widget">
|
||||
|
@ -68,16 +67,15 @@ export class HardwareSettings extends
|
|||
botDisconnected={botDisconnected} />
|
||||
<Motors
|
||||
dispatch={dispatch}
|
||||
firmwareVersion={firmwareVersion}
|
||||
controlPanelState={controlPanelState}
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
isValidFwConfig={!!firmwareConfig}
|
||||
firmwareHardware={firmwareHardware} />
|
||||
<EncodersAndEndStops
|
||||
dispatch={dispatch}
|
||||
shouldDisplay={this.props.shouldDisplay}
|
||||
controlPanelState={controlPanelState}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
firmwareHardware={firmwareHardware} />
|
||||
<PinGuard
|
||||
dispatch={dispatch}
|
||||
resources={resources}
|
||||
|
|
|
@ -14,12 +14,23 @@ describe("<EncodersAndEndStops />", () => {
|
|||
sourceFwConfig: x =>
|
||||
({ value: bot.hardware.mcu_params[x], consistent: true }),
|
||||
shouldDisplay: jest.fn(key => mockFeatures[key]),
|
||||
firmwareHardware: undefined,
|
||||
});
|
||||
|
||||
it("shows new inversion param", () => {
|
||||
mockFeatures.endstop_invert = true;
|
||||
const wrapper = mount(<EncodersAndEndStops {...fakeProps()} />);
|
||||
expect(wrapper.text().toLowerCase()).not.toContain("invert endstops");
|
||||
it("shows encoder labels", () => {
|
||||
const p = fakeProps();
|
||||
p.firmwareHardware = undefined;
|
||||
const wrapper = mount(<EncodersAndEndStops {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).toContain("encoder");
|
||||
expect(wrapper.text().toLowerCase()).not.toContain("stall");
|
||||
});
|
||||
|
||||
it("shows stall labels", () => {
|
||||
const p = fakeProps();
|
||||
p.firmwareHardware = "express_k10";
|
||||
const wrapper = mount(<EncodersAndEndStops {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).not.toContain("encoder");
|
||||
expect(wrapper.text().toLowerCase()).toContain("stall");
|
||||
});
|
||||
|
||||
it.each<["short" | "long"]>([
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
jest.mock("../../../actions", () => ({
|
||||
updateMCU: jest.fn()
|
||||
}));
|
||||
jest.mock("../../../actions", () => ({ updateMCU: jest.fn() }));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
|
@ -11,23 +9,22 @@ import {
|
|||
fakeFirmwareConfig
|
||||
} from "../../../../__test_support__/fake_state/resources";
|
||||
import { error, warning } from "../../../../toast/toast";
|
||||
import { inputEvent } from "../../../../__test_support__/fake_html_events";
|
||||
|
||||
describe("<HomingAndCalibration />", () => {
|
||||
function testAxisLengthInput(
|
||||
fw: string, provided: string, expected: string | undefined) {
|
||||
provided: string, expected: string | undefined) {
|
||||
const dispatch = jest.fn();
|
||||
bot.controlPanelState.homing_and_calibration = true;
|
||||
bot.hardware.informational_settings.firmware_version = fw;
|
||||
const result = mount(<HomingAndCalibration
|
||||
dispatch={dispatch}
|
||||
bot={bot}
|
||||
firmwareConfig={fakeFirmwareConfig().body}
|
||||
sourceFwConfig={(x) => {
|
||||
return { value: bot.hardware.mcu_params[x], consistent: true };
|
||||
}}
|
||||
sourceFwConfig={x => ({
|
||||
value: bot.hardware.mcu_params[x], consistent: true
|
||||
})}
|
||||
botDisconnected={false} />);
|
||||
const e = { currentTarget: { value: provided } } as
|
||||
React.SyntheticEvent<HTMLInputElement>;
|
||||
const e = inputEvent(provided);
|
||||
const input = result.find("input").first().props();
|
||||
input.onChange && input.onChange(e);
|
||||
input.onSubmit && input.onSubmit(e);
|
||||
|
@ -36,20 +33,15 @@ describe("<HomingAndCalibration />", () => {
|
|||
.toHaveBeenCalledWith("movement_axis_nr_steps_x", expected)
|
||||
: expect(updateMCU).not.toHaveBeenCalled();
|
||||
}
|
||||
it("short int", () => {
|
||||
testAxisLengthInput("5.0.0", "100000", undefined);
|
||||
expect(error)
|
||||
.toHaveBeenCalledWith("Value must be less than or equal to 32000.");
|
||||
});
|
||||
|
||||
it("long int: too long", () => {
|
||||
testAxisLengthInput("6.0.0", "10000000000", undefined);
|
||||
testAxisLengthInput("10000000000", undefined);
|
||||
expect(error)
|
||||
.toHaveBeenCalledWith("Value must be less than or equal to 2000000000.");
|
||||
});
|
||||
|
||||
it("long int: ok", () => {
|
||||
testAxisLengthInput("6.0.0", "100000", "100000");
|
||||
testAxisLengthInput("100000", "100000");
|
||||
expect(warning).not.toHaveBeenCalled();
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
@ -28,10 +28,8 @@ describe("<Motors/>", () => {
|
|||
controlPanelState.motors = true;
|
||||
return {
|
||||
dispatch: jest.fn(x => x(jest.fn(), () => state)),
|
||||
firmwareVersion: undefined,
|
||||
controlPanelState,
|
||||
sourceFwConfig: () => ({ value: 0, consistent: true }),
|
||||
isValidFwConfig: true,
|
||||
firmwareHardware: undefined,
|
||||
};
|
||||
};
|
||||
|
@ -46,27 +44,20 @@ describe("<Motors/>", () => {
|
|||
expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase()));
|
||||
});
|
||||
|
||||
it("doesn't render homing speed", () => {
|
||||
it("shows TMC parameters", () => {
|
||||
const p = fakeProps();
|
||||
p.firmwareVersion = "4.0.0R";
|
||||
p.isValidFwConfig = false;
|
||||
p.firmwareHardware = "express_k10";
|
||||
const wrapper = render(<Motors {...p} />);
|
||||
expect(wrapper.text()).not.toContain("Homing Speed");
|
||||
expect(wrapper.text()).toContain("Stall");
|
||||
expect(wrapper.text()).toContain("Current");
|
||||
});
|
||||
|
||||
it("renders homing speed", () => {
|
||||
it("doesn't show TMC parameters", () => {
|
||||
const p = fakeProps();
|
||||
p.firmwareVersion = "5.1.0R";
|
||||
p.firmwareHardware = "farmduino";
|
||||
const wrapper = render(<Motors {...p} />);
|
||||
expect(wrapper.text()).toContain("Homing Speed");
|
||||
});
|
||||
|
||||
it("renders homing speed while disconnected", () => {
|
||||
const p = fakeProps();
|
||||
p.firmwareVersion = undefined;
|
||||
p.isValidFwConfig = true;
|
||||
const wrapper = render(<Motors {...p} />);
|
||||
expect(wrapper.text()).toContain("Homing Speed");
|
||||
expect(wrapper.text()).not.toContain("Stall");
|
||||
expect(wrapper.text()).not.toContain("Current");
|
||||
});
|
||||
|
||||
const testParamToggle = (
|
||||
|
|
|
@ -7,11 +7,12 @@ import { Header } from "./header";
|
|||
import { Collapse } from "@blueprintjs/core";
|
||||
import { Feature } from "../../interfaces";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { isExpressBoard } from "../firmware_hardware_support";
|
||||
|
||||
export function EncodersAndEndStops(props: EncodersProps) {
|
||||
|
||||
const { encoders_and_endstops } = props.controlPanelState;
|
||||
const { dispatch, sourceFwConfig, shouldDisplay } = props;
|
||||
const { dispatch, sourceFwConfig, shouldDisplay, firmwareHardware } = props;
|
||||
|
||||
const encodersDisabled = {
|
||||
x: !sourceFwConfig("encoder_enabled_x").value,
|
||||
|
@ -22,36 +23,42 @@ export function EncodersAndEndStops(props: EncodersProps) {
|
|||
return <section>
|
||||
<Header
|
||||
expanded={encoders_and_endstops}
|
||||
title={t("Encoders and Endstops")}
|
||||
title={isExpressBoard(firmwareHardware)
|
||||
? t("Stall Detection and Endstops")
|
||||
: t("Encoders and Endstops")}
|
||||
name={"encoders_and_endstops"}
|
||||
dispatch={dispatch} />
|
||||
<Collapse isOpen={!!encoders_and_endstops}>
|
||||
<BooleanMCUInputGroup
|
||||
name={t("Enable Encoders")}
|
||||
name={isExpressBoard(firmwareHardware)
|
||||
? t("Enable Stall Detection")
|
||||
: t("Enable Encoders")}
|
||||
tooltip={ToolTips.ENABLE_ENCODERS}
|
||||
x={"encoder_enabled_x"}
|
||||
y={"encoder_enabled_y"}
|
||||
z={"encoder_enabled_z"}
|
||||
dispatch={dispatch}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
<BooleanMCUInputGroup
|
||||
name={t("Use Encoders for Positioning")}
|
||||
tooltip={ToolTips.ENCODER_POSITIONING}
|
||||
x={"encoder_use_for_pos_x"}
|
||||
y={"encoder_use_for_pos_y"}
|
||||
z={"encoder_use_for_pos_z"}
|
||||
grayscale={encodersDisabled}
|
||||
dispatch={dispatch}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
<BooleanMCUInputGroup
|
||||
name={t("Invert Encoders")}
|
||||
tooltip={ToolTips.INVERT_ENCODERS}
|
||||
x={"encoder_invert_x"}
|
||||
y={"encoder_invert_y"}
|
||||
z={"encoder_invert_z"}
|
||||
grayscale={encodersDisabled}
|
||||
dispatch={dispatch}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
{!isExpressBoard(firmwareHardware) &&
|
||||
<BooleanMCUInputGroup
|
||||
name={t("Use Encoders for Positioning")}
|
||||
tooltip={ToolTips.ENCODER_POSITIONING}
|
||||
x={"encoder_use_for_pos_x"}
|
||||
y={"encoder_use_for_pos_y"}
|
||||
z={"encoder_use_for_pos_z"}
|
||||
grayscale={encodersDisabled}
|
||||
dispatch={dispatch}
|
||||
sourceFwConfig={sourceFwConfig} />}
|
||||
{!isExpressBoard(firmwareHardware) &&
|
||||
<BooleanMCUInputGroup
|
||||
name={t("Invert Encoders")}
|
||||
tooltip={ToolTips.INVERT_ENCODERS}
|
||||
x={"encoder_invert_x"}
|
||||
y={"encoder_invert_y"}
|
||||
z={"encoder_invert_z"}
|
||||
grayscale={encodersDisabled}
|
||||
dispatch={dispatch}
|
||||
sourceFwConfig={sourceFwConfig} />}
|
||||
<NumericMCUInputGroup
|
||||
name={t("Max Missed Steps")}
|
||||
tooltip={ToolTips.MAX_MISSED_STEPS}
|
||||
|
@ -62,7 +69,7 @@ export function EncodersAndEndStops(props: EncodersProps) {
|
|||
sourceFwConfig={sourceFwConfig}
|
||||
dispatch={dispatch} />
|
||||
<NumericMCUInputGroup
|
||||
name={t("Encoder Missed Step Decay")}
|
||||
name={t("Missed Step Decay")}
|
||||
tooltip={ToolTips.ENCODER_MISSED_STEP_DECAY}
|
||||
x={"encoder_missed_steps_decay_x"}
|
||||
y={"encoder_missed_steps_decay_y"}
|
||||
|
@ -70,19 +77,20 @@ export function EncodersAndEndStops(props: EncodersProps) {
|
|||
gray={encodersDisabled}
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
dispatch={dispatch} />
|
||||
<NumericMCUInputGroup
|
||||
name={t("Encoder Scaling")}
|
||||
tooltip={ToolTips.ENCODER_SCALING}
|
||||
x={"encoder_scaling_x"}
|
||||
y={"encoder_scaling_y"}
|
||||
z={"encoder_scaling_z"}
|
||||
xScale={sourceFwConfig("movement_microsteps_x").value}
|
||||
yScale={sourceFwConfig("movement_microsteps_y").value}
|
||||
zScale={sourceFwConfig("movement_microsteps_z").value}
|
||||
intSize={shouldDisplay(Feature.long_scaling_factor) ? "long" : "short"}
|
||||
gray={encodersDisabled}
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
dispatch={dispatch} />
|
||||
{!isExpressBoard(firmwareHardware) &&
|
||||
<NumericMCUInputGroup
|
||||
name={t("Encoder Scaling")}
|
||||
tooltip={ToolTips.ENCODER_SCALING}
|
||||
x={"encoder_scaling_x"}
|
||||
y={"encoder_scaling_y"}
|
||||
z={"encoder_scaling_z"}
|
||||
xScale={sourceFwConfig("movement_microsteps_x").value}
|
||||
yScale={sourceFwConfig("movement_microsteps_y").value}
|
||||
zScale={sourceFwConfig("movement_microsteps_z").value}
|
||||
intSize={shouldDisplay(Feature.long_scaling_factor) ? "long" : "short"}
|
||||
gray={encodersDisabled}
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
dispatch={dispatch} />}
|
||||
<BooleanMCUInputGroup
|
||||
name={t("Enable Endstops")}
|
||||
tooltip={ToolTips.ENABLE_ENDSTOPS}
|
||||
|
|
|
@ -9,7 +9,6 @@ import { disabledAxisMap } from "../axis_tracking_status";
|
|||
import { HomingAndCalibrationProps } from "../interfaces";
|
||||
import { Header } from "./header";
|
||||
import { Collapse } from "@blueprintjs/core";
|
||||
import { minFwVersionCheck } from "../../../util";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { calculateScale } from "./motors";
|
||||
|
||||
|
@ -18,14 +17,8 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
|
|||
const { dispatch, bot, sourceFwConfig, firmwareConfig, botDisconnected
|
||||
} = props;
|
||||
const hardware = firmwareConfig ? firmwareConfig : bot.hardware.mcu_params;
|
||||
const { firmware_version } = bot.hardware.informational_settings;
|
||||
const { homing_and_calibration } = props.bot.controlPanelState;
|
||||
|
||||
const axisLengthIntSize =
|
||||
minFwVersionCheck(firmware_version, "6.0.0")
|
||||
? "long"
|
||||
: "short";
|
||||
|
||||
/**
|
||||
* Tells us if X/Y/Z have a means of checking their position.
|
||||
* FARMBOT WILL CRASH INTO WALLS IF THIS IS WRONG! BE CAREFUL.
|
||||
|
@ -94,7 +87,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
|
|||
}}
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
dispatch={dispatch}
|
||||
intSize={axisLengthIntSize} />
|
||||
intSize={"long"} />
|
||||
<NumericMCUInputGroup
|
||||
name={t("Timeout after (seconds)")}
|
||||
tooltip={ToolTips.TIMEOUT_AFTER}
|
||||
|
|
|
@ -9,11 +9,11 @@ import { Row, Col, Help } from "../../../ui/index";
|
|||
import { Header } from "./header";
|
||||
import { Collapse, Position } from "@blueprintjs/core";
|
||||
import { McuInputBox } from "../mcu_input_box";
|
||||
import { minFwVersionCheck } from "../../../util";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { Xyz, McuParamName } from "farmbot";
|
||||
import { SourceFwConfig } from "../../interfaces";
|
||||
import { calcMicrostepsPerMm } from "../../../controls/move/direction_axes_props";
|
||||
import { isTMCBoard, isExpressBoard } from "../firmware_hardware_support";
|
||||
|
||||
const SingleSettingRow =
|
||||
({ label, tooltip, settingType, children }: {
|
||||
|
@ -25,7 +25,7 @@ const SingleSettingRow =
|
|||
<Row>
|
||||
<Col xs={6} className={"widget-body-tooltips"}>
|
||||
<label>{label}</label>
|
||||
<Help text={tooltip} requireClick={true} position={Position.RIGHT}/>
|
||||
<Help text={tooltip} requireClick={true} position={Position.RIGHT} />
|
||||
</Col>
|
||||
{settingType === "button"
|
||||
? <Col xs={2} className={"centered-button-div"}>{children}</Col>
|
||||
|
@ -47,15 +47,17 @@ export const calculateScale =
|
|||
|
||||
export function Motors(props: MotorsProps) {
|
||||
const {
|
||||
dispatch, firmwareVersion, controlPanelState,
|
||||
sourceFwConfig, isValidFwConfig, firmwareHardware
|
||||
dispatch, controlPanelState, sourceFwConfig, firmwareHardware
|
||||
} = props;
|
||||
const enable2ndXMotor = sourceFwConfig("movement_secondary_motor_x");
|
||||
const invert2ndXMotor = sourceFwConfig("movement_secondary_motor_invert_x");
|
||||
const eStopOnMoveError = sourceFwConfig("param_e_stop_on_mov_err");
|
||||
const scale = calculateScale(sourceFwConfig);
|
||||
const isFarmduinoExpress = firmwareHardware &&
|
||||
firmwareHardware.includes("express");
|
||||
const encodersDisabled = {
|
||||
x: !sourceFwConfig("encoder_enabled_x").value,
|
||||
y: !sourceFwConfig("encoder_enabled_y").value,
|
||||
z: !sourceFwConfig("encoder_enabled_z").value,
|
||||
};
|
||||
return <section>
|
||||
<Header
|
||||
expanded={controlPanelState.motors}
|
||||
|
@ -91,18 +93,17 @@ export function Motors(props: MotorsProps) {
|
|||
zScale={scale.z}
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
dispatch={dispatch} />
|
||||
{(minFwVersionCheck(firmwareVersion, "5.0.5") || isValidFwConfig) &&
|
||||
<NumericMCUInputGroup
|
||||
name={t("Homing Speed (mm/s)")}
|
||||
tooltip={ToolTips.HOME_SPEED}
|
||||
x={"movement_home_spd_x"}
|
||||
y={"movement_home_spd_y"}
|
||||
z={"movement_home_spd_z"}
|
||||
xScale={scale.x}
|
||||
yScale={scale.y}
|
||||
zScale={scale.z}
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
dispatch={dispatch} />}
|
||||
<NumericMCUInputGroup
|
||||
name={t("Homing Speed (mm/s)")}
|
||||
tooltip={ToolTips.HOME_SPEED}
|
||||
x={"movement_home_spd_x"}
|
||||
y={"movement_home_spd_y"}
|
||||
z={"movement_home_spd_z"}
|
||||
xScale={scale.x}
|
||||
yScale={scale.y}
|
||||
zScale={scale.z}
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
dispatch={dispatch} />
|
||||
<NumericMCUInputGroup
|
||||
name={t("Minimum Speed (mm/s)")}
|
||||
tooltip={ToolTips.MIN_SPEED}
|
||||
|
@ -161,7 +162,7 @@ export function Motors(props: MotorsProps) {
|
|||
z={"movement_invert_motor_z"}
|
||||
dispatch={dispatch}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
{isFarmduinoExpress &&
|
||||
{isTMCBoard(firmwareHardware) &&
|
||||
<NumericMCUInputGroup
|
||||
name={t("Motor Current")}
|
||||
tooltip={ToolTips.MOTOR_CURRENT}
|
||||
|
@ -170,13 +171,14 @@ export function Motors(props: MotorsProps) {
|
|||
z={"movement_motor_current_z"}
|
||||
dispatch={dispatch}
|
||||
sourceFwConfig={sourceFwConfig} />}
|
||||
{isFarmduinoExpress &&
|
||||
{isExpressBoard(firmwareHardware) &&
|
||||
<NumericMCUInputGroup
|
||||
name={t("Stall Sensitivity")}
|
||||
tooltip={ToolTips.STALL_SENSITIVITY}
|
||||
x={"movement_stall_sensitivity_x"}
|
||||
y={"movement_stall_sensitivity_y"}
|
||||
z={"movement_stall_sensitivity_z"}
|
||||
gray={encodersDisabled}
|
||||
dispatch={dispatch}
|
||||
sourceFwConfig={sourceFwConfig} />}
|
||||
<SingleSettingRow settingType="button"
|
||||
|
|
|
@ -24,7 +24,8 @@ export function PinGuard(props: PinGuardProps) {
|
|||
<label>
|
||||
{t("Pin Number")}
|
||||
</label>
|
||||
<Help text={ToolTips.PIN_GUARD_PIN_NUMBER} requireClick={true} position={Position.RIGHT}/>
|
||||
<Help text={ToolTips.PIN_GUARD_PIN_NUMBER} requireClick={true}
|
||||
position={Position.RIGHT} />
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<label>
|
||||
|
|
|
@ -28,7 +28,8 @@ export function ZeroRow({ botDisconnected }: ZeroRowProps) {
|
|||
<label>
|
||||
{t("SET ZERO POSITION")}
|
||||
</label>
|
||||
<Help text={ToolTips.SET_ZERO_POSITION} requireClick={true} position={Position.RIGHT} />
|
||||
<Help text={ToolTips.SET_ZERO_POSITION} requireClick={true}
|
||||
position={Position.RIGHT} />
|
||||
</Col>
|
||||
{AXES.map((axis) => {
|
||||
return <Col xs={2} key={axis} className={"centered-button-div"}>
|
||||
|
|
|
@ -78,10 +78,8 @@ export interface PinGuardProps {
|
|||
|
||||
export interface MotorsProps {
|
||||
dispatch: Function;
|
||||
firmwareVersion: string | undefined;
|
||||
controlPanelState: ControlPanelState;
|
||||
sourceFwConfig: SourceFwConfig;
|
||||
isValidFwConfig: boolean;
|
||||
firmwareHardware: FirmwareHardware | undefined;
|
||||
}
|
||||
|
||||
|
@ -90,6 +88,7 @@ export interface EncodersProps {
|
|||
shouldDisplay: ShouldDisplay;
|
||||
controlPanelState: ControlPanelState;
|
||||
sourceFwConfig: SourceFwConfig;
|
||||
firmwareHardware: FirmwareHardware | undefined;
|
||||
}
|
||||
|
||||
export interface DangerZoneProps {
|
||||
|
|
|
@ -14,7 +14,7 @@ export function NumericMCUInputGroup(props: NumericMCUInputGroupProps) {
|
|||
<label>
|
||||
{name}
|
||||
</label>
|
||||
<Help text={tooltip} requireClick={true} position={Position.RIGHT}/>
|
||||
<Help text={tooltip} requireClick={true} position={Position.RIGHT} />
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
<McuInputBox
|
||||
|
@ -24,7 +24,7 @@ export function NumericMCUInputGroup(props: NumericMCUInputGroupProps) {
|
|||
intSize={intSize}
|
||||
float={float}
|
||||
scale={props.xScale}
|
||||
gray={gray && gray.x} />
|
||||
gray={gray?.x} />
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
<McuInputBox
|
||||
|
@ -34,7 +34,7 @@ export function NumericMCUInputGroup(props: NumericMCUInputGroupProps) {
|
|||
intSize={intSize}
|
||||
float={float}
|
||||
scale={props.yScale}
|
||||
gray={gray && gray.y} />
|
||||
gray={gray?.y} />
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
<McuInputBox
|
||||
|
@ -44,7 +44,7 @@ export function NumericMCUInputGroup(props: NumericMCUInputGroupProps) {
|
|||
intSize={intSize}
|
||||
float={float}
|
||||
scale={props.zScale}
|
||||
gray={gray && gray.z} />
|
||||
gray={gray?.z} />
|
||||
</Col>
|
||||
</Row>;
|
||||
}
|
||||
|
|
|
@ -73,6 +73,7 @@ export enum Feature {
|
|||
backscheduled_regimens = "backscheduled_regimens",
|
||||
boot_sequence = "boot_sequence",
|
||||
change_ownership = "change_ownership",
|
||||
criteria_groups = "criteria_groups",
|
||||
endstop_invert = "endstop_invert",
|
||||
express_k10 = "express_k10",
|
||||
farmduino_k14 = "farmduino_k14",
|
||||
|
@ -89,7 +90,7 @@ export enum Feature {
|
|||
rpi_led_control = "rpi_led_control",
|
||||
sensors = "sensors",
|
||||
use_update_channel = "use_update_channel",
|
||||
variables = "variables"
|
||||
variables = "variables",
|
||||
}
|
||||
|
||||
/** Object fetched from FEATURE_MIN_VERSIONS_URL. */
|
||||
|
|
|
@ -62,7 +62,8 @@ export const initialState = (): BotState => ({
|
|||
target: "---",
|
||||
env: "---",
|
||||
node_name: "---",
|
||||
firmware_commit: "---"
|
||||
firmware_version: "---",
|
||||
firmware_commit: "---",
|
||||
},
|
||||
user_env: {},
|
||||
process_info: {
|
||||
|
|
|
@ -14,4 +14,11 @@ describe("<DesignerPanelHeader />", () => {
|
|||
const wrapper = mount(<DesignerPanelHeader panelName={"test-panel"} />);
|
||||
expect(wrapper.find("div").first().hasClass("gray-panel")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("goes back", () => {
|
||||
const wrapper = mount(<DesignerPanelHeader panelName={"test-panel"} />);
|
||||
history.back = jest.fn();
|
||||
wrapper.find("i").first().simulate("click");
|
||||
expect(history.back).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -35,7 +35,8 @@ describe("<FarmDesigner/>", () => {
|
|||
selectedPlant: undefined,
|
||||
designer: fakeDesignerState(),
|
||||
hoveredPlant: undefined,
|
||||
points: [],
|
||||
genericPoints: [],
|
||||
allPoints: [],
|
||||
plants: [],
|
||||
toolSlots: [],
|
||||
crops: [],
|
||||
|
@ -59,6 +60,8 @@ describe("<FarmDesigner/>", () => {
|
|||
getConfigValue: jest.fn(),
|
||||
sensorReadings: [],
|
||||
sensors: [],
|
||||
groups: [],
|
||||
shouldDisplay: () => false,
|
||||
});
|
||||
|
||||
it("loads default map settings", () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
let mockPath = "/app/designer/plants";
|
||||
jest.mock("../../history", () => ({
|
||||
getPathArray: jest.fn(() => { return mockPath.split("/"); }),
|
||||
getPathArray: jest.fn(() => mockPath.split("/")),
|
||||
}));
|
||||
|
||||
let mockDev = false;
|
||||
|
|
|
@ -5,7 +5,9 @@ import {
|
|||
HoveredPlantPayl, CurrentPointPayl, CropLiveSearchResult
|
||||
} from "../interfaces";
|
||||
import { BotPosition } from "../../devices/interfaces";
|
||||
import { fakeCropLiveSearchResult } from "../../__test_support__/fake_crop_search_result";
|
||||
import {
|
||||
fakeCropLiveSearchResult
|
||||
} from "../../__test_support__/fake_crop_search_result";
|
||||
import { fakeDesignerState } from "../../__test_support__/fake_designer_state";
|
||||
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ describe("mapStateToProps()", () => {
|
|||
expect.objectContaining({ uuid: plantUuid }));
|
||||
});
|
||||
|
||||
it("returns all points", () => {
|
||||
it("returns all genericPoints", () => {
|
||||
const state = fakeState();
|
||||
const webAppConfig = fakeWebAppConfig();
|
||||
(webAppConfig.body as WebAppConfig).show_historic_points = true;
|
||||
|
@ -67,10 +67,10 @@ describe("mapStateToProps()", () => {
|
|||
const point3 = fakePoint();
|
||||
point3.body.discarded_at = DISCARDED_AT;
|
||||
state.resources = buildResourceIndex([webAppConfig, point1, point2, point3]);
|
||||
expect(mapStateToProps(state).points.length).toEqual(3);
|
||||
expect(mapStateToProps(state).genericPoints.length).toEqual(3);
|
||||
});
|
||||
|
||||
it("returns active points", () => {
|
||||
it("returns active genericPoints", () => {
|
||||
const state = fakeState();
|
||||
const webAppConfig = fakeWebAppConfig();
|
||||
(webAppConfig.body as WebAppConfig).show_historic_points = false;
|
||||
|
@ -81,7 +81,7 @@ describe("mapStateToProps()", () => {
|
|||
const point3 = fakePoint();
|
||||
point3.body.discarded_at = DISCARDED_AT;
|
||||
state.resources = buildResourceIndex([webAppConfig, point1, point2, point3]);
|
||||
expect(mapStateToProps(state).points.length).toEqual(1);
|
||||
expect(mapStateToProps(state).genericPoints.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("returns sensor readings", () => {
|
||||
|
|
|
@ -78,13 +78,15 @@ export const DesignerPanelHeader = (props: DesignerPanelHeaderProps) => {
|
|||
interface DesignerPanelTopProps {
|
||||
panel: Panel;
|
||||
linkTo?: string;
|
||||
onClick?(): void;
|
||||
title?: string;
|
||||
children?: React.ReactNode;
|
||||
noIcon?: boolean;
|
||||
}
|
||||
|
||||
export const DesignerPanelTop = (props: DesignerPanelTopProps) => {
|
||||
return <div className={`panel-top ${props.linkTo ? "with-button" : ""}`}>
|
||||
const withBtn = !!props.linkTo || !!props.onClick;
|
||||
return <div className={`panel-top ${withBtn ? "with-button" : ""}`}>
|
||||
<div className="thin-search-wrapper">
|
||||
<div className="text-input-wrapper">
|
||||
{!props.noIcon &&
|
||||
|
@ -94,6 +96,13 @@ export const DesignerPanelTop = (props: DesignerPanelTopProps) => {
|
|||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
{props.onClick &&
|
||||
<a>
|
||||
<div className={`fb-button panel-${TAB_COLOR[props.panel]}`}
|
||||
onClick={props.onClick}>
|
||||
<i className="fa fa-plus" title={props.title} />
|
||||
</div>
|
||||
</a>}
|
||||
{props.linkTo &&
|
||||
<Link to={props.linkTo}>
|
||||
<div className={`fb-button panel-${TAB_COLOR[props.panel]}`}>
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
let mockPath = "";
|
||||
jest.mock("../../../history", () => ({
|
||||
getPathArray: jest.fn(() => { return mockPath.split("/"); }),
|
||||
history: {
|
||||
push: jest.fn()
|
||||
}
|
||||
getPathArray: jest.fn(() => mockPath.split("/")),
|
||||
history: { push: jest.fn() }
|
||||
}));
|
||||
|
||||
import { mapStateToPropsAddEdit } from "../map_state_to_props_add_edit";
|
||||
|
@ -15,6 +13,7 @@ import {
|
|||
fakeSequence, fakeRegimen, fakeFarmEvent
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { history } from "../../../history";
|
||||
import { inputEvent } from "../../../__test_support__/fake_html_events";
|
||||
|
||||
describe("mapStateToPropsAddEdit()", () => {
|
||||
|
||||
|
@ -22,25 +21,19 @@ describe("mapStateToPropsAddEdit()", () => {
|
|||
const { handleTime } = mapStateToPropsAddEdit(fakeState());
|
||||
|
||||
it("handles an element with name `start_time`", () => {
|
||||
const e = {
|
||||
currentTarget: { value: "10:54", name: "start_time" }
|
||||
} as React.SyntheticEvent<HTMLInputElement>;
|
||||
const e = inputEvent("10:54", "start_time");
|
||||
const result = handleTime(e, "2017-05-21T22:00:00.000");
|
||||
expect(result).toContain("54");
|
||||
});
|
||||
|
||||
it("handles an element with name `end_time`", () => {
|
||||
const e = {
|
||||
currentTarget: { value: "10:53", name: "end_time" }
|
||||
} as React.SyntheticEvent<HTMLInputElement>;
|
||||
const e = inputEvent("10:53", "end_time");
|
||||
const result = handleTime(e, "2017-05-21T22:00:00.000");
|
||||
expect(result).toContain("53");
|
||||
});
|
||||
|
||||
it("crashes on other names", () => {
|
||||
const e = {
|
||||
currentTarget: { value: "10:52", name: "other" }
|
||||
} as React.SyntheticEvent<HTMLInputElement>;
|
||||
const e = inputEvent("10:52", "other");
|
||||
const boom = () => handleTime(e, "2017-05-21T22:00:00.000");
|
||||
expect(boom).toThrowError("Expected a name attribute from time field.");
|
||||
});
|
||||
|
|
|
@ -73,6 +73,7 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
|||
show_spread: init(BooleanSetting.show_spread, false),
|
||||
show_farmbot: init(BooleanSetting.show_farmbot, true),
|
||||
show_images: init(BooleanSetting.show_images, false),
|
||||
show_zones: init(BooleanSetting.show_zones, false),
|
||||
show_sensor_readings: init(BooleanSetting.show_sensor_readings, false),
|
||||
bot_origin_quadrant: this.getBotOriginQuadrant(),
|
||||
zoom_level: calcZoomLevel(getZoomLevelIndex(this.props.getConfigValue)),
|
||||
|
@ -118,6 +119,7 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
|||
show_spread,
|
||||
show_farmbot,
|
||||
show_images,
|
||||
show_zones,
|
||||
show_sensor_readings,
|
||||
zoom_level
|
||||
} = this.state;
|
||||
|
@ -156,6 +158,7 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
|||
showSpread={show_spread}
|
||||
showFarmbot={show_farmbot}
|
||||
showImages={show_images}
|
||||
showZones={show_zones}
|
||||
showSensorReadings={show_sensor_readings}
|
||||
hasSensorReadings={this.props.sensorReadings.length > 0}
|
||||
dispatch={this.props.dispatch}
|
||||
|
@ -181,13 +184,15 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
|||
showSpread={show_spread}
|
||||
showFarmbot={show_farmbot}
|
||||
showImages={show_images}
|
||||
showZones={show_zones}
|
||||
showSensorReadings={show_sensor_readings}
|
||||
selectedPlant={this.props.selectedPlant}
|
||||
crops={this.props.crops}
|
||||
dispatch={this.props.dispatch}
|
||||
designer={this.props.designer}
|
||||
plants={this.props.plants}
|
||||
points={this.props.points}
|
||||
genericPoints={this.props.genericPoints}
|
||||
allPoints={this.props.allPoints}
|
||||
toolSlots={this.props.toolSlots}
|
||||
botLocationData={this.props.botLocationData}
|
||||
botSize={botSize}
|
||||
|
@ -204,7 +209,9 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
|||
getConfigValue={this.props.getConfigValue}
|
||||
sensorReadings={this.props.sensorReadings}
|
||||
timeSettings={this.props.timeSettings}
|
||||
sensors={this.props.sensors} />
|
||||
sensors={this.props.sensors}
|
||||
groups={this.props.groups}
|
||||
shouldDisplay={this.props.shouldDisplay} />
|
||||
</div>
|
||||
|
||||
{this.props.designer.openedSavedGarden &&
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
TaggedImage,
|
||||
TaggedSensorReading,
|
||||
TaggedSensor,
|
||||
TaggedPoint,
|
||||
TaggedPointGroup,
|
||||
} from "farmbot";
|
||||
import { SlotWithTool, ResourceIndex } from "../resources/interfaces";
|
||||
import {
|
||||
|
@ -49,6 +51,7 @@ export interface State extends TypeCheckerHint {
|
|||
show_spread: boolean;
|
||||
show_farmbot: boolean;
|
||||
show_images: boolean;
|
||||
show_zones: boolean;
|
||||
show_sensor_readings: boolean;
|
||||
bot_origin_quadrant: BotOriginQuadrant;
|
||||
zoom_level: number;
|
||||
|
@ -59,7 +62,8 @@ export interface Props {
|
|||
selectedPlant: TaggedPlant | undefined;
|
||||
designer: DesignerState;
|
||||
hoveredPlant: TaggedPlant | undefined;
|
||||
points: TaggedGenericPointer[];
|
||||
genericPoints: TaggedGenericPointer[];
|
||||
allPoints: TaggedPoint[];
|
||||
plants: TaggedPlant[];
|
||||
toolSlots: SlotWithTool[];
|
||||
crops: TaggedCrop[];
|
||||
|
@ -74,6 +78,8 @@ export interface Props {
|
|||
getConfigValue: GetWebAppConfigValue;
|
||||
sensorReadings: TaggedSensorReading[];
|
||||
sensors: TaggedSensor[];
|
||||
groups: TaggedPointGroup[];
|
||||
shouldDisplay: ShouldDisplay;
|
||||
}
|
||||
|
||||
export interface MovePlantProps {
|
||||
|
@ -176,10 +182,12 @@ export interface GardenMapProps {
|
|||
showSpread: boolean | undefined;
|
||||
showFarmbot: boolean | undefined;
|
||||
showImages: boolean | undefined;
|
||||
showZones: boolean | undefined;
|
||||
showSensorReadings: boolean | undefined;
|
||||
dispatch: Function;
|
||||
designer: DesignerState;
|
||||
points: TaggedGenericPointer[];
|
||||
genericPoints: TaggedGenericPointer[];
|
||||
allPoints: TaggedPoint[];
|
||||
plants: TaggedPlant[];
|
||||
toolSlots: SlotWithTool[];
|
||||
selectedPlant: TaggedPlant | undefined;
|
||||
|
@ -200,6 +208,8 @@ export interface GardenMapProps {
|
|||
sensorReadings: TaggedSensorReading[];
|
||||
sensors: TaggedSensor[];
|
||||
timeSettings: TimeSettings;
|
||||
groups: TaggedPointGroup[];
|
||||
shouldDisplay: ShouldDisplay;
|
||||
}
|
||||
|
||||
export interface GardenMapState {
|
||||
|
|
|
@ -12,7 +12,7 @@ jest.mock("../../../api/crud", () => ({
|
|||
import { fakePointGroup } from "../../../__test_support__/fake_state/resources";
|
||||
const mockGroup = fakePointGroup();
|
||||
jest.mock("../../point_groups/group_detail", () => ({
|
||||
fetchGroupFromUrl: jest.fn(() => mockGroup)
|
||||
findGroupFromUrl: jest.fn(() => mockGroup)
|
||||
}));
|
||||
|
||||
import {
|
||||
|
|
|
@ -32,6 +32,7 @@ jest.mock("../drawn_point/drawn_point_actions", () => ({
|
|||
jest.mock("../background/selection_box_actions", () => ({
|
||||
startNewSelectionBox: jest.fn(),
|
||||
resizeBox: jest.fn(),
|
||||
maybeUpdateGroupCriteria: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../move_to", () => ({ chooseLocation: jest.fn() }));
|
||||
|
@ -45,6 +46,11 @@ jest.mock("../../../history", () => ({
|
|||
getPathArray: () => [],
|
||||
}));
|
||||
|
||||
let mockGroup: TaggedPointGroup | undefined = undefined;
|
||||
jest.mock("../../point_groups/group_detail", () => ({
|
||||
findGroupFromUrl: () => mockGroup,
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { GardenMap } from "../garden_map";
|
||||
import { shallow, mount } from "enzyme";
|
||||
|
@ -55,7 +61,7 @@ import {
|
|||
dropPlant, beginPlantDrag, maybeSavePlantLocation, dragPlant
|
||||
} from "../layers/plants/plant_actions";
|
||||
import {
|
||||
startNewSelectionBox, resizeBox
|
||||
startNewSelectionBox, resizeBox, maybeUpdateGroupCriteria
|
||||
} from "../background/selection_box_actions";
|
||||
import { getGardenCoordinates } from "../util";
|
||||
import { chooseLocation } from "../../move_to";
|
||||
|
@ -63,9 +69,12 @@ import { startNewPoint, resizePoint } from "../drawn_point/drawn_point_actions";
|
|||
import {
|
||||
fakeDesignerState
|
||||
} from "../../../__test_support__/fake_designer_state";
|
||||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
fakePlant, fakePointGroup, fakePoint
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
|
||||
import { history } from "../../../history";
|
||||
import { TaggedPointGroup } from "farmbot";
|
||||
|
||||
const DEFAULT_EVENT = { preventDefault: jest.fn(), pageX: NaN, pageY: NaN };
|
||||
|
||||
|
@ -75,13 +84,15 @@ const fakeProps = (): GardenMapProps => ({
|
|||
showSpread: false,
|
||||
showFarmbot: false,
|
||||
showImages: false,
|
||||
showZones: false,
|
||||
showSensorReadings: false,
|
||||
selectedPlant: undefined,
|
||||
crops: [],
|
||||
dispatch: jest.fn(),
|
||||
designer: fakeDesignerState(),
|
||||
plants: [],
|
||||
points: [],
|
||||
genericPoints: [],
|
||||
allPoints: [],
|
||||
toolSlots: [],
|
||||
botLocationData: {
|
||||
position: { x: 0, y: 0, z: 0 },
|
||||
|
@ -111,6 +122,8 @@ const fakeProps = (): GardenMapProps => ({
|
|||
sensorReadings: [],
|
||||
sensors: [],
|
||||
timeSettings: fakeTimeSettings(),
|
||||
groups: [],
|
||||
shouldDisplay: () => false,
|
||||
});
|
||||
|
||||
describe("<GardenMap/>", () => {
|
||||
|
@ -144,6 +157,7 @@ describe("<GardenMap/>", () => {
|
|||
wrapper.setState({ isDragging: true });
|
||||
wrapper.find(".drop-area-svg").simulate("mouseUp", DEFAULT_EVENT);
|
||||
expect(maybeSavePlantLocation).toHaveBeenCalled();
|
||||
expect(maybeUpdateGroupCriteria).toHaveBeenCalled();
|
||||
expect(wrapper.instance().state.isDragging).toBeFalsy();
|
||||
});
|
||||
|
||||
|
@ -198,6 +212,18 @@ describe("<GardenMap/>", () => {
|
|||
expect.objectContaining(e));
|
||||
});
|
||||
|
||||
it("starts drag on background: selecting zone", () => {
|
||||
const wrapper = mount(<GardenMap {...fakeProps()} />);
|
||||
mockMode = Mode.editGroup;
|
||||
const e = { pageX: 1000, pageY: 2000 };
|
||||
wrapper.find(".drop-area-background").simulate("mouseDown", e);
|
||||
expect(startNewSelectionBox).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ plantActions: false }));
|
||||
expect(history.push).not.toHaveBeenCalled();
|
||||
expect(getGardenCoordinates).toHaveBeenCalledWith(
|
||||
expect.objectContaining(e));
|
||||
});
|
||||
|
||||
it("starts drag: click-to-add mode", () => {
|
||||
const wrapper = shallow(<GardenMap {...fakeProps()} />);
|
||||
mockMode = Mode.clickToAdd;
|
||||
|
@ -217,6 +243,17 @@ describe("<GardenMap/>", () => {
|
|||
expect.objectContaining(e));
|
||||
});
|
||||
|
||||
it("drags: selecting zone", () => {
|
||||
const wrapper = shallow(<GardenMap {...fakeProps()} />);
|
||||
mockMode = Mode.editGroup;
|
||||
const e = { pageX: 2000, pageY: 2000 };
|
||||
wrapper.find(".drop-area-svg").simulate("mouseMove", e);
|
||||
expect(resizeBox).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ plantActions: false }));
|
||||
expect(getGardenCoordinates).toHaveBeenCalledWith(
|
||||
expect.objectContaining(e));
|
||||
});
|
||||
|
||||
it("selects location", () => {
|
||||
const wrapper = shallow(<GardenMap {...fakeProps()} />);
|
||||
mockMode = Mode.moveTo;
|
||||
|
@ -361,4 +398,14 @@ describe("<GardenMap/>", () => {
|
|||
expect(svg.props().height).toEqual(1000);
|
||||
});
|
||||
|
||||
it("gets group points", () => {
|
||||
mockGroup = fakePointGroup();
|
||||
mockGroup.body.point_ids = [1];
|
||||
const p = fakeProps();
|
||||
const point = fakePoint();
|
||||
point.body.id = 1;
|
||||
p.allPoints = [point];
|
||||
const wrapper = shallow<GardenMap>(<GardenMap {...p} />);
|
||||
expect(wrapper.instance().pointsSelectedByGroup).toEqual([point]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,10 +7,11 @@ import { svgToUrl, DEFAULT_ICON } from "../../open_farm/icons";
|
|||
import { Mode } from "../map/interfaces";
|
||||
import { clamp, uniq } from "lodash";
|
||||
import { GetState } from "../../redux/interfaces";
|
||||
import { fetchGroupFromUrl } from "../point_groups/group_detail";
|
||||
import { findGroupFromUrl } from "../point_groups/group_detail";
|
||||
import { TaggedPoint } from "farmbot";
|
||||
import { getMode } from "../map/util";
|
||||
import { ResourceIndex, UUID } from "../../resources/interfaces";
|
||||
import { selectAllPointGroups } from "../../resources/selectors";
|
||||
|
||||
export function movePlant(payload: MovePlantProps) {
|
||||
const tr = payload.plant;
|
||||
|
@ -33,7 +34,7 @@ export const setHoveredPlant = (plantUUID: string | undefined, icon = "") => ({
|
|||
|
||||
const addOrRemoveFromGroup =
|
||||
(clickedPlantUuid: UUID, resources: ResourceIndex) => {
|
||||
const group = fetchGroupFromUrl(resources);
|
||||
const group = findGroupFromUrl(selectAllPointGroups(resources));
|
||||
const point =
|
||||
resources.references[clickedPlantUuid] as TaggedPoint | undefined;
|
||||
if (group && point?.body.id) {
|
||||
|
|
|
@ -46,9 +46,9 @@ describe("<Grid/>", () => {
|
|||
const minorGrid = wrapper.find("#minor_grid>path");
|
||||
const majorGrid = wrapper.find("#major_grid>path");
|
||||
const superiorGrid = wrapper.find("#superior_grid>path");
|
||||
expect(minorGrid.props()).toHaveProperty("stroke", "rgba(0, 0, 0, 0.15)");
|
||||
expect(majorGrid.props()).toHaveProperty("stroke", "rgba(0, 0, 0, 0.3)");
|
||||
expect(superiorGrid.props()).toHaveProperty("stroke", "rgba(0, 0, 0, 0.4)");
|
||||
expect(minorGrid.props()).toHaveProperty("strokeWidth", "1");
|
||||
expect(majorGrid.props()).toHaveProperty("strokeWidth", "2");
|
||||
expect(superiorGrid.props()).toHaveProperty("strokeWidth", "4");
|
||||
});
|
||||
|
||||
it("change patterns strokes on 0.5 zoom and below", () => {
|
||||
|
@ -58,8 +58,48 @@ describe("<Grid/>", () => {
|
|||
const minorGrid = wrapper.find("#minor_grid>path");
|
||||
const majorGrid = wrapper.find("#major_grid>path");
|
||||
const superiorGrid = wrapper.find("#superior_grid>path");
|
||||
expect(minorGrid.props()).toHaveProperty("stroke", "rgba(0, 0, 0, 0)");
|
||||
expect(majorGrid.props()).toHaveProperty("stroke", "rgba(0, 0, 0, 0.6)");
|
||||
expect(superiorGrid.props()).toHaveProperty("stroke", "rgba(0, 0, 0, 0.8)");
|
||||
expect(minorGrid.props()).toHaveProperty("strokeWidth", "0");
|
||||
expect(majorGrid.props()).toHaveProperty("strokeWidth", "3");
|
||||
expect(superiorGrid.props()).toHaveProperty("strokeWidth", "6");
|
||||
});
|
||||
|
||||
it("visualizes axis values every 100mm above 0.5 zoom", () => {
|
||||
const p = fakeProps();
|
||||
p.zoomLvl = 0.6;
|
||||
const wrapper = shallow(<Grid {...p} />);
|
||||
const axisValues = wrapper.find(".x-label").children();
|
||||
expect(axisValues).toHaveLength(29);
|
||||
});
|
||||
|
||||
it("visualizes axis values every 200mm between 0.5 and 0.2 excluded zoom", () => {
|
||||
const p = fakeProps();
|
||||
p.zoomLvl = 0.5;
|
||||
const wrapper = shallow(<Grid {...p} />);
|
||||
const axisValues = wrapper.find(".x-label").children();
|
||||
expect(axisValues).toHaveLength(14);
|
||||
});
|
||||
|
||||
it("visualizes axis values every 500mm on 0.2 zoom and below", () => {
|
||||
const p = fakeProps();
|
||||
p.zoomLvl = 0.2;
|
||||
const wrapper = shallow(<Grid {...p} />);
|
||||
const axisValues = wrapper.find(".x-label").children();
|
||||
expect(axisValues).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("use transform scale 1 for zoom above 1", () => {
|
||||
const p = fakeProps();
|
||||
p.zoomLvl = 1.1;
|
||||
const wrapper = shallow(<Grid {...p} />);
|
||||
const textNode = wrapper.find(".x-label").first();
|
||||
expect(textNode.prop("style")).toHaveProperty("transform", "scale(1)");
|
||||
});
|
||||
|
||||
it("use transform scale 1.5 for zoom on 0.5", () => {
|
||||
const p = fakeProps();
|
||||
p.zoomLvl = 0.5;
|
||||
const wrapper = shallow(<Grid {...p} />);
|
||||
const textNode = wrapper.find(".x-label").first();
|
||||
expect(textNode.prop("style")).toHaveProperty("transform", "scale(1.5)");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,12 +4,22 @@ jest.mock("../../util", () => ({ getMode: () => mockMode }));
|
|||
|
||||
jest.mock("../../../../history", () => ({ history: { push: jest.fn() } }));
|
||||
|
||||
import { fakePlant } from "../../../../__test_support__/fake_state/resources";
|
||||
jest.mock("../../../point_groups/criteria", () => ({
|
||||
editGtLtCriteria: jest.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
getSelected, resizeBox, startNewSelectionBox, ResizeSelectionBoxProps
|
||||
fakePlant, fakePointGroup
|
||||
} from "../../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
getSelected, resizeBox, startNewSelectionBox, ResizeSelectionBoxProps,
|
||||
StartNewSelectionBoxProps,
|
||||
maybeUpdateGroupCriteria,
|
||||
MaybeUpdateGroupCriteriaProps,
|
||||
} from "../selection_box_actions";
|
||||
import { Actions } from "../../../../constants";
|
||||
import { history } from "../../../../history";
|
||||
import { editGtLtCriteria } from "../../../point_groups/criteria";
|
||||
|
||||
describe("getSelected", () => {
|
||||
it("returns some", () => {
|
||||
|
@ -41,6 +51,7 @@ describe("resizeBox", () => {
|
|||
gardenCoords: { x: 100, y: 200 },
|
||||
setMapState: jest.fn(),
|
||||
dispatch: jest.fn(),
|
||||
plantActions: true,
|
||||
});
|
||||
|
||||
it("resizes selection box", () => {
|
||||
|
@ -55,6 +66,16 @@ describe("resizeBox", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("resizes selection box without plant actions", () => {
|
||||
const p = fakeProps();
|
||||
p.plantActions = false;
|
||||
resizeBox(p);
|
||||
expect(p.setMapState).toHaveBeenCalledWith({
|
||||
selectionBox: { x0: 0, y0: 0, x1: 100, y1: 200 }
|
||||
});
|
||||
expect(p.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("doesn't resize box: no location", () => {
|
||||
const p = fakeProps();
|
||||
// tslint:disable-next-line:no-any
|
||||
|
@ -93,10 +114,11 @@ describe("resizeBox", () => {
|
|||
});
|
||||
|
||||
describe("startNewSelectionBox", () => {
|
||||
const fakeProps = () => ({
|
||||
const fakeProps = (): StartNewSelectionBoxProps => ({
|
||||
gardenCoords: { x: 100, y: 200 },
|
||||
setMapState: jest.fn(),
|
||||
dispatch: jest.fn(),
|
||||
plantActions: true,
|
||||
});
|
||||
|
||||
it("starts selection box", () => {
|
||||
|
@ -111,6 +133,16 @@ describe("startNewSelectionBox", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("starts selection box without plant actions", () => {
|
||||
const p = fakeProps();
|
||||
p.plantActions = false;
|
||||
startNewSelectionBox(p);
|
||||
expect(p.setMapState).toHaveBeenCalledWith({
|
||||
selectionBox: { x0: 100, y0: 200, x1: undefined, y1: undefined }
|
||||
});
|
||||
expect(p.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("doesn't start box", () => {
|
||||
const p = fakeProps();
|
||||
// tslint:disable-next-line:no-any
|
||||
|
@ -123,3 +155,25 @@ describe("startNewSelectionBox", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("maybeUpdateGroupCriteria()", () => {
|
||||
const fakeProps = (): MaybeUpdateGroupCriteriaProps => ({
|
||||
selectionBox: { x0: 0, y0: 0, x1: undefined, y1: undefined },
|
||||
dispatch: jest.fn(),
|
||||
group: fakePointGroup(),
|
||||
shouldDisplay: () => true,
|
||||
});
|
||||
|
||||
it("updates criteria", () => {
|
||||
const p = fakeProps();
|
||||
maybeUpdateGroupCriteria(p);
|
||||
expect(editGtLtCriteria).toHaveBeenCalledWith(p.group, p.selectionBox);
|
||||
});
|
||||
|
||||
it("doesn't update criteria", () => {
|
||||
const p = fakeProps();
|
||||
p.shouldDisplay = () => false;
|
||||
maybeUpdateGroupCriteria(p);
|
||||
expect(editGtLtCriteria).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,27 +13,43 @@ export function Grid(props: GridProps) {
|
|||
const arrowEnd = transformXY(25, 25, mapTransformProps);
|
||||
const xLabel = transformXY(15, -10, mapTransformProps);
|
||||
const yLabel = transformXY(-11, 18, mapTransformProps);
|
||||
const minorGridStroke = zoomLvl <= 0.5 ? "rgba(0, 0, 0, 0)" : "rgba(0, 0, 0, 0.15)";
|
||||
const majorGridStroke = zoomLvl <= 0.5 ? "rgba(0, 0, 0, 0.6)" : "rgba(0, 0, 0, 0.3)";
|
||||
const superiorGridStroke = zoomLvl <= 0.5 ? "rgba(0, 0, 0, 0.8)" : "rgba(0, 0, 0, 0.4)";
|
||||
const minorStrokeWidth = zoomLvl <= 0.5 ? "0" : "1";
|
||||
const majorStrokeWidth = zoomLvl <= 0.5 ? "3" : "2";
|
||||
const superiorStrokeWidth = zoomLvl <= 0.5 ? "6" : "4";
|
||||
|
||||
// Start axis-values controls
|
||||
// TODO: Create helper to regroup code and clean grid.tsx
|
||||
// Text transform:scale value
|
||||
const axisTransformValue = zoomLvl <= 1 ? 2 - zoomLvl : 1;
|
||||
// Start and increment steps to visualize in grid
|
||||
let axisStep;
|
||||
if (zoomLvl <= 0.2) {
|
||||
axisStep = 500;
|
||||
} else if (zoomLvl <= 0.5) {
|
||||
axisStep = 200;
|
||||
} else {
|
||||
axisStep = 100;
|
||||
}
|
||||
// End axis-values controls
|
||||
|
||||
return <g className="drop-area-background" onClick={props.onClick}
|
||||
onMouseDown={props.onMouseDown}>
|
||||
<defs>
|
||||
<pattern id="minor_grid"
|
||||
width={10} height={10} patternUnits="userSpaceOnUse">
|
||||
<path d="M10,0 L0,0 L0,10" strokeWidth={1}
|
||||
fill="none" stroke={minorGridStroke} />
|
||||
<path d="M10,0 L0,0 L0,10" strokeWidth={minorStrokeWidth}
|
||||
fill="none" stroke="rgba(0, 0, 0, 0.15)" />
|
||||
</pattern>
|
||||
|
||||
<pattern id={"major_grid"}
|
||||
width={100} height={100} patternUnits="userSpaceOnUse">
|
||||
<path d="M100,0 L0,0 0,100" strokeWidth={2}
|
||||
fill="none" stroke={majorGridStroke} />
|
||||
<path d="M100,0 L0,0 0,100" strokeWidth={majorStrokeWidth}
|
||||
fill="none" stroke="rgba(0, 0, 0, 0.3)" />
|
||||
</pattern>
|
||||
|
||||
<pattern id="superior_grid" width={1000} height={1000} patternUnits="userSpaceOnUse">
|
||||
<path d="M1000,0 L0,0 0,1000" strokeWidth={2}
|
||||
fill="none" stroke={superiorGridStroke} />
|
||||
<path d="M1000,0 L0,0 0,1000" strokeWidth={superiorStrokeWidth}
|
||||
fill="none" stroke="rgba(0, 0, 0, 0.4)" />
|
||||
</pattern>
|
||||
|
||||
<marker id="arrow"
|
||||
|
@ -70,10 +86,21 @@ export function Grid(props: GridProps) {
|
|||
|
||||
<g id="axis-values" fontFamily="Arial" fontSize="10"
|
||||
textAnchor="middle" dominantBaseline="central" fill="rgba(0, 0, 0, 0.3)">
|
||||
{range(100, gridSize.x, 100).map((i) => {
|
||||
{range(axisStep, gridSize.x, axisStep).map((i) => {
|
||||
const location = transformXY(i, -10, mapTransformProps);
|
||||
return <text key={"x-label-" + i}
|
||||
x={location.qx} y={location.qy}>{i}</text>;
|
||||
return (
|
||||
<text key={"x-label-" + i}
|
||||
fontSize="16"
|
||||
fontWeight="bold"
|
||||
className="x-label"
|
||||
color="rgba(0, 0, 0, 0.4)"
|
||||
x={location.qx}
|
||||
y={location.qy}
|
||||
style={{transformOrigin: "center", transformBox: "fill-box", transform: `scale(${axisTransformValue})`}}>
|
||||
{i}
|
||||
</text>
|
||||
);
|
||||
|
||||
})}
|
||||
{range(100, gridSize.y, 100).map((i) => {
|
||||
const location = transformXY(-15, i, mapTransformProps);
|
||||
|
|
|
@ -5,6 +5,9 @@ import { GardenMapState } from "../../interfaces";
|
|||
import { history } from "../../../history";
|
||||
import { selectPlant } from "../actions";
|
||||
import { getMode } from "../util";
|
||||
import { editGtLtCriteria } from "../../point_groups/criteria";
|
||||
import { TaggedPointGroup } from "farmbot";
|
||||
import { ShouldDisplay, Feature } from "../../../devices/interfaces";
|
||||
|
||||
/** Return all plants within the selection box. */
|
||||
export const getSelected = (
|
||||
|
@ -32,6 +35,7 @@ export interface ResizeSelectionBoxProps {
|
|||
gardenCoords: AxisNumberProperty | undefined;
|
||||
setMapState: (x: Partial<GardenMapState>) => void;
|
||||
dispatch: Function;
|
||||
plantActions: boolean;
|
||||
}
|
||||
|
||||
/** Resize a selection box. */
|
||||
|
@ -45,24 +49,29 @@ export const resizeBox = (props: ResizeSelectionBoxProps) => {
|
|||
x1: current.x, y1: current.y // Update box active corner
|
||||
};
|
||||
props.setMapState({ selectionBox: newSelectionBox });
|
||||
// Select all plants within the updated selection box
|
||||
const payload = getSelected(props.plants, newSelectionBox);
|
||||
if (payload && getMode() === Mode.none) {
|
||||
history.push("/app/designer/plants/select");
|
||||
if (props.plantActions) {
|
||||
// Select all plants within the updated selection box
|
||||
const payload = getSelected(props.plants, newSelectionBox);
|
||||
if (payload && getMode() === Mode.none) {
|
||||
history.push("/app/designer/plants/select");
|
||||
}
|
||||
props.dispatch(selectPlant(payload));
|
||||
}
|
||||
props.dispatch(selectPlant(payload));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export interface StartNewSelectionBoxProps {
|
||||
gardenCoords: AxisNumberProperty | undefined;
|
||||
setMapState: (x: Partial<GardenMapState>) => void;
|
||||
dispatch: Function;
|
||||
plantActions: boolean;
|
||||
}
|
||||
|
||||
/** Create a new selection box. */
|
||||
export const startNewSelectionBox = (props: {
|
||||
gardenCoords: AxisNumberProperty | undefined,
|
||||
setMapState: (x: Partial<GardenMapState>) => void,
|
||||
dispatch: Function,
|
||||
}) => {
|
||||
export const startNewSelectionBox = (props: StartNewSelectionBoxProps) => {
|
||||
if (props.gardenCoords) {
|
||||
// Set the starting point (initial corner) of a selection box
|
||||
// Set the starting point (initial corner) of a selection box
|
||||
props.setMapState({
|
||||
selectionBox: {
|
||||
x0: props.gardenCoords.x, y0: props.gardenCoords.y,
|
||||
|
@ -70,6 +79,23 @@ export const startNewSelectionBox = (props: {
|
|||
}
|
||||
});
|
||||
}
|
||||
// Clear the previous plant selection when starting a new selection box
|
||||
props.dispatch(selectPlant(undefined));
|
||||
if (props.plantActions) {
|
||||
// Clear the previous plant selection when starting a new selection box
|
||||
props.dispatch(selectPlant(undefined));
|
||||
}
|
||||
};
|
||||
|
||||
export interface MaybeUpdateGroupCriteriaProps {
|
||||
selectionBox: SelectionBoxData | undefined;
|
||||
dispatch: Function;
|
||||
group: TaggedPointGroup | undefined;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
}
|
||||
|
||||
export const maybeUpdateGroupCriteria =
|
||||
(props: MaybeUpdateGroupCriteriaProps) => {
|
||||
if (props.selectionBox && props.group &&
|
||||
props.shouldDisplay(Feature.criteria_groups)) {
|
||||
props.dispatch(editGtLtCriteria(props.group, props.selectionBox));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
import {
|
||||
Grid, MapBackground,
|
||||
TargetCoordinate,
|
||||
SelectionBox, resizeBox, startNewSelectionBox
|
||||
SelectionBox, resizeBox, startNewSelectionBox, maybeUpdateGroupCriteria,
|
||||
} from "./background";
|
||||
import {
|
||||
PlantLayer,
|
||||
|
@ -32,6 +32,11 @@ import { chooseLocation } from "../move_to";
|
|||
import { GroupOrder } from "../point_groups/group_order_visual";
|
||||
import { NNPath } from "../point_groups/paths";
|
||||
import { history } from "../../history";
|
||||
import { ZonesLayer } from "./layers/zones/zones_layer";
|
||||
import { ErrorBoundary } from "../../error_boundary";
|
||||
import { TaggedPoint, TaggedPointGroup } from "farmbot";
|
||||
import { findGroupFromUrl } from "../point_groups/group_detail";
|
||||
import { pointsSelectedByGroup } from "../point_groups/criteria";
|
||||
|
||||
export class GardenMap extends
|
||||
React.Component<GardenMapProps, Partial<GardenMapState>> {
|
||||
|
@ -67,6 +72,14 @@ export class GardenMap extends
|
|||
get animate(): boolean {
|
||||
return !this.props.getConfigValue(BooleanSetting.disable_animations);
|
||||
}
|
||||
get group(): TaggedPointGroup | undefined {
|
||||
return findGroupFromUrl(this.props.groups);
|
||||
}
|
||||
|
||||
get pointsSelectedByGroup(): TaggedPoint[] {
|
||||
return this.group ?
|
||||
pointsSelectedByGroup(this.group, this.props.allPoints) : [];
|
||||
}
|
||||
|
||||
/** Save the current plant (if needed) and reset drag state. */
|
||||
endDrag = () => {
|
||||
|
@ -75,6 +88,12 @@ export class GardenMap extends
|
|||
isDragging: this.state.isDragging,
|
||||
dispatch: this.props.dispatch,
|
||||
});
|
||||
maybeUpdateGroupCriteria({
|
||||
selectionBox: this.state.selectionBox,
|
||||
group: this.group,
|
||||
dispatch: this.props.dispatch,
|
||||
shouldDisplay: this.props.shouldDisplay,
|
||||
});
|
||||
this.setState({
|
||||
isDragging: false, qPageX: 0, qPageY: 0,
|
||||
activeDragXY: { x: undefined, y: undefined, z: undefined },
|
||||
|
@ -114,9 +133,18 @@ export class GardenMap extends
|
|||
gardenCoords,
|
||||
setMapState: this.setMapState,
|
||||
dispatch: this.props.dispatch,
|
||||
plantActions: true,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case Mode.editGroup:
|
||||
startNewSelectionBox({
|
||||
gardenCoords: this.getGardenCoordinates(e),
|
||||
setMapState: this.setMapState,
|
||||
dispatch: this.props.dispatch,
|
||||
plantActions: false,
|
||||
});
|
||||
break;
|
||||
case Mode.createPoint:
|
||||
startNewPoint({
|
||||
gardenCoords: this.getGardenCoordinates(e),
|
||||
|
@ -141,6 +169,15 @@ export class GardenMap extends
|
|||
gardenCoords: this.getGardenCoordinates(e),
|
||||
setMapState: this.setMapState,
|
||||
dispatch: this.props.dispatch,
|
||||
plantActions: true,
|
||||
});
|
||||
break;
|
||||
case Mode.editGroup:
|
||||
startNewSelectionBox({
|
||||
gardenCoords: this.getGardenCoordinates(e),
|
||||
setMapState: this.setMapState,
|
||||
dispatch: this.props.dispatch,
|
||||
plantActions: false,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
|
@ -149,6 +186,7 @@ export class GardenMap extends
|
|||
gardenCoords: this.getGardenCoordinates(e),
|
||||
setMapState: this.setMapState,
|
||||
dispatch: this.props.dispatch,
|
||||
plantActions: true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
@ -236,6 +274,16 @@ export class GardenMap extends
|
|||
isDragging: this.state.isDragging,
|
||||
});
|
||||
break;
|
||||
case Mode.editGroup:
|
||||
resizeBox({
|
||||
selectionBox: this.state.selectionBox,
|
||||
plants: this.props.plants,
|
||||
gardenCoords: this.getGardenCoordinates(e),
|
||||
setMapState: this.setMapState,
|
||||
dispatch: this.props.dispatch,
|
||||
plantActions: false,
|
||||
});
|
||||
break;
|
||||
case Mode.boxSelect:
|
||||
default:
|
||||
resizeBox({
|
||||
|
@ -244,6 +292,7 @@ export class GardenMap extends
|
|||
gardenCoords: this.getGardenCoordinates(e),
|
||||
setMapState: this.setMapState,
|
||||
dispatch: this.props.dispatch,
|
||||
plantActions: true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
@ -302,6 +351,12 @@ export class GardenMap extends
|
|||
onMouseDown={this.startDragOnBackground}
|
||||
mapTransformProps={this.mapTransformProps}
|
||||
zoomLvl={this.props.zoomLvl} />
|
||||
ZonesLayer = () => <ZonesLayer
|
||||
visible={!!this.props.showZones}
|
||||
botSize={this.props.botSize}
|
||||
mapTransformProps={this.mapTransformProps}
|
||||
groups={this.props.groups}
|
||||
currentGroup={this.group?.uuid} />
|
||||
SensorReadingsLayer = () => <SensorReadingsLayer
|
||||
visible={!!this.props.showSensorReadings}
|
||||
sensorReadings={this.props.sensorReadings}
|
||||
|
@ -324,7 +379,7 @@ export class GardenMap extends
|
|||
dispatch={this.props.dispatch}
|
||||
hoveredPoint={this.props.designer.hoveredPoint}
|
||||
visible={!!this.props.showPoints}
|
||||
points={this.props.points} />
|
||||
genericPoints={this.props.genericPoints} />
|
||||
PlantLayer = () => <PlantLayer
|
||||
mapTransformProps={this.mapTransformProps}
|
||||
dispatch={this.props.dispatch}
|
||||
|
@ -333,7 +388,8 @@ export class GardenMap extends
|
|||
currentPlant={this.getPlant()}
|
||||
dragging={!!this.state.isDragging}
|
||||
editing={this.isEditing}
|
||||
selectedForDel={this.props.designer.selectedPlants}
|
||||
boxSelected={this.props.designer.selectedPlants}
|
||||
groupSelected={this.pointsSelectedByGroup.map(point => point.uuid)}
|
||||
zoomLvl={this.props.zoomLvl}
|
||||
activeDragXY={this.state.activeDragXY}
|
||||
animate={this.animate} />
|
||||
|
@ -383,9 +439,10 @@ export class GardenMap extends
|
|||
key={"currentPoint"}
|
||||
mapTransformProps={this.mapTransformProps} />
|
||||
GroupOrder = () => <GroupOrder
|
||||
plants={this.props.plants}
|
||||
group={this.group}
|
||||
groupPoints={this.pointsSelectedByGroup}
|
||||
mapTransformProps={this.mapTransformProps} />
|
||||
NNPath = () => <NNPath plants={this.props.plants}
|
||||
NNPath = () => <NNPath pathPoints={this.props.allPoints}
|
||||
mapTransformProps={this.mapTransformProps} />
|
||||
Bugs = () => showBugs() ? <Bugs mapTransformProps={this.mapTransformProps}
|
||||
botSize={this.props.botSize} /> : <g />
|
||||
|
@ -393,27 +450,30 @@ export class GardenMap extends
|
|||
/** Render layers in order from back to front. */
|
||||
render() {
|
||||
return <div className={"drop-area"} {...this.mapDropAreaProps()}>
|
||||
<svg id={"map-background-svg"}>
|
||||
<this.MapBackground />
|
||||
<svg className={"drop-area-svg"} {...this.svgDropAreaProps()}>
|
||||
<this.ImageLayer />
|
||||
<this.Grid />
|
||||
<this.SensorReadingsLayer />
|
||||
<this.SpreadLayer />
|
||||
<this.PointLayer />
|
||||
<this.PlantLayer />
|
||||
<this.ToolSlotLayer />
|
||||
<this.FarmBotLayer />
|
||||
<this.HoveredPlant />
|
||||
<this.DragHelper />
|
||||
<this.SelectionBox />
|
||||
<this.TargetCoordinate />
|
||||
<this.DrawnPoint />
|
||||
<this.GroupOrder />
|
||||
<this.NNPath />
|
||||
<this.Bugs />
|
||||
<ErrorBoundary>
|
||||
<svg id={"map-background-svg"}>
|
||||
<this.MapBackground />
|
||||
<svg className={"drop-area-svg"} {...this.svgDropAreaProps()}>
|
||||
<this.ImageLayer />
|
||||
<this.Grid />
|
||||
<this.ZonesLayer />
|
||||
<this.SensorReadingsLayer />
|
||||
<this.SpreadLayer />
|
||||
<this.PointLayer />
|
||||
<this.PlantLayer />
|
||||
<this.ToolSlotLayer />
|
||||
<this.FarmBotLayer />
|
||||
<this.HoveredPlant />
|
||||
<this.DragHelper />
|
||||
<this.SelectionBox />
|
||||
<this.TargetCoordinate />
|
||||
<this.DrawnPoint />
|
||||
<this.GroupOrder />
|
||||
<this.NNPath />
|
||||
<this.Bugs />
|
||||
</svg>
|
||||
</svg>
|
||||
</svg>
|
||||
</ErrorBoundary>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import {
|
||||
TaggedPlantPointer,
|
||||
TaggedGenericPointer,
|
||||
TaggedPlantTemplate
|
||||
TaggedPlantTemplate,
|
||||
} from "farmbot";
|
||||
import { State, BotOriginQuadrant } from "../interfaces";
|
||||
import { BotPosition, BotLocationData } from "../../devices/interfaces";
|
||||
import { GetWebAppConfigValue } from "../../config_storage/actions";
|
||||
import { TimeSettings } from "../../interfaces";
|
||||
import { UUID } from "../../resources/interfaces";
|
||||
|
||||
export type TaggedPlant = TaggedPlantPointer | TaggedPlantTemplate;
|
||||
|
||||
|
@ -20,7 +21,8 @@ export interface PlantLayerProps {
|
|||
mapTransformProps: MapTransformProps;
|
||||
zoomLvl: number;
|
||||
activeDragXY: BotPosition | undefined;
|
||||
selectedForDel: string[] | undefined;
|
||||
boxSelected: string[] | undefined;
|
||||
groupSelected: UUID[];
|
||||
animate: boolean;
|
||||
}
|
||||
|
||||
|
@ -33,6 +35,7 @@ export interface GardenMapLegendProps {
|
|||
showSpread: boolean;
|
||||
showFarmbot: boolean;
|
||||
showImages: boolean;
|
||||
showZones: boolean;
|
||||
showSensorReadings: boolean;
|
||||
hasSensorReadings: boolean;
|
||||
dispatch: Function;
|
||||
|
@ -59,7 +62,6 @@ export interface GardenPlantProps {
|
|||
zoomLvl: number;
|
||||
activeDragXY: BotPosition | undefined;
|
||||
uuid: string;
|
||||
multiselected: boolean;
|
||||
animate: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ describe("<GardenPlant/>", () => {
|
|||
plant: fakePlant(),
|
||||
selected: false,
|
||||
editing: false,
|
||||
multiselected: false,
|
||||
dragging: false,
|
||||
dispatch: jest.fn(),
|
||||
zoomLvl: 1.8,
|
||||
|
@ -27,7 +26,7 @@ describe("<GardenPlant/>", () => {
|
|||
|
||||
it("renders plant", () => {
|
||||
const p = fakeProps();
|
||||
p.multiselected = true;
|
||||
p.selected = true;
|
||||
p.animate = false;
|
||||
const wrapper = shallow(<GardenPlant {...p} />);
|
||||
expect(wrapper.find("image").length).toEqual(1);
|
||||
|
@ -42,7 +41,7 @@ describe("<GardenPlant/>", () => {
|
|||
it("renders plant animations", () => {
|
||||
const p = fakeProps();
|
||||
p.animate = true;
|
||||
p.multiselected = true;
|
||||
p.selected = true;
|
||||
const wrapper = shallow(<GardenPlant {...p} />);
|
||||
expect(wrapper.find(".soil-cloud").length).toEqual(1);
|
||||
expect(wrapper.find(".animate").length).toEqual(2);
|
||||
|
@ -82,9 +81,9 @@ describe("<GardenPlant/>", () => {
|
|||
expect(wrapper.find(".plant-indicator").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("indicator cirlce is there", () => {
|
||||
it("indicator circle is rendered", () => {
|
||||
const p = fakeProps();
|
||||
p.multiselected = true;
|
||||
p.selected = true;
|
||||
const wrapper = shallow(<GardenPlant {...p} />);
|
||||
expect(wrapper.find(".plant-indicator").length).toEqual(1);
|
||||
expect(wrapper.find("Circle").length).toEqual(1);
|
||||
|
|
|
@ -22,7 +22,8 @@ describe("<PlantLayer/>", () => {
|
|||
currentPlant: undefined,
|
||||
dragging: false,
|
||||
editing: false,
|
||||
selectedForDel: undefined,
|
||||
boxSelected: undefined,
|
||||
groupSelected: [],
|
||||
dispatch: jest.fn(),
|
||||
zoomLvl: 1,
|
||||
activeDragXY: { x: undefined, y: undefined, z: undefined },
|
||||
|
@ -97,14 +98,14 @@ describe("<PlantLayer/>", () => {
|
|||
expect(wrapper.find("GardenPlant").props().selected).toEqual(true);
|
||||
});
|
||||
|
||||
it("has plant selected for deletion", () => {
|
||||
it("has plant selected by selection box", () => {
|
||||
mockPath = "/app/designer/plants";
|
||||
const p = fakeProps();
|
||||
const plant = fakePlant();
|
||||
p.plants = [plant];
|
||||
p.selectedForDel = [plant.uuid];
|
||||
p.boxSelected = [plant.uuid];
|
||||
const wrapper = svgMount(<PlantLayer {...p} />);
|
||||
expect((wrapper.find("GardenPlant").props() as GardenPlantProps).multiselected)
|
||||
expect((wrapper.find("GardenPlant").props() as GardenPlantProps).selected)
|
||||
.toEqual(true);
|
||||
});
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ export class GardenPlant extends
|
|||
}
|
||||
|
||||
render() {
|
||||
const { selected, dragging, plant, multiselected, mapTransformProps,
|
||||
const { selected, dragging, plant, mapTransformProps,
|
||||
activeDragXY, zoomLvl, animate, editing } = this.props;
|
||||
const { id, radius, x, y } = plant.body;
|
||||
const { icon } = this.state;
|
||||
|
@ -65,7 +65,7 @@ export class GardenPlant extends
|
|||
fill={Color.soilCloud}
|
||||
fillOpacity={0} />}
|
||||
|
||||
{multiselected && !editing &&
|
||||
{selected && !editing &&
|
||||
<g id="selected-plant-indicator">
|
||||
<Circle
|
||||
className={`plant-indicator ${animate ? "animate" : ""}`}
|
||||
|
@ -73,8 +73,7 @@ export class GardenPlant extends
|
|||
y={qy}
|
||||
r={radius}
|
||||
selected={true} />
|
||||
</g>
|
||||
}
|
||||
</g>}
|
||||
|
||||
<g id="plant-icon">
|
||||
<image
|
||||
|
|
|
@ -16,14 +16,16 @@ export function PlantLayer(props: PlantLayerProps) {
|
|||
mapTransformProps,
|
||||
zoomLvl,
|
||||
activeDragXY,
|
||||
selectedForDel,
|
||||
boxSelected,
|
||||
groupSelected,
|
||||
animate,
|
||||
} = props;
|
||||
|
||||
return <g id="plant-layer">
|
||||
{visible && plants.map(p => {
|
||||
const selected = !!(currentPlant && (p.uuid === currentPlant.uuid));
|
||||
const multiselected = !!(selectedForDel && (selectedForDel.includes(p.uuid)));
|
||||
const selected = !!(p.uuid === currentPlant?.uuid);
|
||||
const selectedByBox = !!boxSelected?.includes(p.uuid);
|
||||
const selectedByGroup = groupSelected.includes(p.uuid);
|
||||
const plantCategory = unpackUUID(p.uuid).kind === "PlantTemplate"
|
||||
? "gardens/templates"
|
||||
: "plants";
|
||||
|
@ -31,9 +33,8 @@ export function PlantLayer(props: PlantLayerProps) {
|
|||
uuid={p.uuid}
|
||||
mapTransformProps={mapTransformProps}
|
||||
plant={p}
|
||||
selected={selected}
|
||||
selected={selected || selectedByBox || selectedByGroup}
|
||||
editing={editing}
|
||||
multiselected={multiselected}
|
||||
dragging={selected && dragging && editing}
|
||||
dispatch={dispatch}
|
||||
zoomLvl={zoomLvl}
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
let mockPath = "/app/designer/plants";
|
||||
jest.mock("../../../../../history", () => ({
|
||||
getPathArray: jest.fn(() => mockPath.split("/")),
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { PointLayer, PointLayerProps } from "../point_layer";
|
||||
import { fakePoint } from "../../../../../__test_support__/fake_state/resources";
|
||||
|
@ -8,21 +13,20 @@ import { GardenPoint } from "../garden_point";
|
|||
import { svgMount } from "../../../../../__test_support__/svg_mount";
|
||||
|
||||
describe("<PointLayer/>", () => {
|
||||
function fakeProps(): PointLayerProps {
|
||||
return {
|
||||
visible: true,
|
||||
points: [fakePoint()],
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
hoveredPoint: undefined,
|
||||
dispatch: jest.fn(),
|
||||
};
|
||||
}
|
||||
const fakeProps = (): PointLayerProps => ({
|
||||
visible: true,
|
||||
genericPoints: [fakePoint()],
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
hoveredPoint: undefined,
|
||||
dispatch: jest.fn(),
|
||||
});
|
||||
|
||||
it("shows points", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = svgMount(<PointLayer {...p} />);
|
||||
const layer = wrapper.find("#point-layer");
|
||||
expect(layer.find(GardenPoint).html()).toContain("r=\"100\"");
|
||||
expect(layer.props().style).toEqual({ pointerEvents: "none" });
|
||||
});
|
||||
|
||||
it("toggles visibility off", () => {
|
||||
|
@ -32,4 +36,12 @@ describe("<PointLayer/>", () => {
|
|||
const layer = wrapper.find("#point-layer");
|
||||
expect(layer.find(GardenPoint).length).toEqual(0);
|
||||
});
|
||||
|
||||
it("allows point mode interaction", () => {
|
||||
mockPath = "/app/designer/points";
|
||||
const p = fakeProps();
|
||||
const wrapper = svgMount(<PointLayer {...p} />);
|
||||
const layer = wrapper.find("#point-layer");
|
||||
expect(layer.props().style).toEqual({});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,19 +6,19 @@ import { getMode } from "../../util";
|
|||
|
||||
export interface PointLayerProps {
|
||||
visible: boolean;
|
||||
points: TaggedGenericPointer[];
|
||||
genericPoints: TaggedGenericPointer[];
|
||||
mapTransformProps: MapTransformProps;
|
||||
hoveredPoint: string | undefined;
|
||||
dispatch: Function;
|
||||
}
|
||||
|
||||
export function PointLayer(props: PointLayerProps) {
|
||||
const { visible, points, mapTransformProps, hoveredPoint } = props;
|
||||
const { visible, genericPoints, mapTransformProps, hoveredPoint } = props;
|
||||
const style: React.CSSProperties =
|
||||
getMode() === Mode.points ? {} : { pointerEvents: "none" };
|
||||
return <g id="point-layer" style={style}>
|
||||
{visible &&
|
||||
points.map(p =>
|
||||
genericPoints.map(p =>
|
||||
<GardenPoint
|
||||
point={p}
|
||||
key={p.uuid}
|
||||
|
|
|
@ -1,26 +1,27 @@
|
|||
import * as React from "react";
|
||||
import { SpreadLayer, SpreadLayerProps } from "../spread_layer";
|
||||
import {
|
||||
SpreadLayer, SpreadLayerProps, SpreadCircle, SpreadCircleProps
|
||||
} from "../spread_layer";
|
||||
import { shallow } from "enzyme";
|
||||
import { fakePlant } from "../../../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
fakeMapTransformProps
|
||||
} from "../../../../../__test_support__/map_transform_props";
|
||||
import { SpreadOverlapHelper } from "../spread_overlap_helper";
|
||||
|
||||
describe("<SpreadLayer/>", () => {
|
||||
function fakeProps(): SpreadLayerProps {
|
||||
return {
|
||||
visible: true,
|
||||
plants: [fakePlant()],
|
||||
currentPlant: undefined,
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
dragging: false,
|
||||
zoomLvl: 1.8,
|
||||
activeDragXY: { x: undefined, y: undefined, z: undefined },
|
||||
activeDragSpread: undefined,
|
||||
editing: false,
|
||||
animate: false,
|
||||
};
|
||||
}
|
||||
const fakeProps = (): SpreadLayerProps => ({
|
||||
visible: true,
|
||||
plants: [fakePlant()],
|
||||
currentPlant: undefined,
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
dragging: false,
|
||||
zoomLvl: 1.8,
|
||||
activeDragXY: { x: undefined, y: undefined, z: undefined },
|
||||
activeDragSpread: undefined,
|
||||
editing: false,
|
||||
animate: false,
|
||||
});
|
||||
|
||||
it("shows spread", () => {
|
||||
const p = fakeProps();
|
||||
|
@ -36,4 +37,30 @@ describe("<SpreadLayer/>", () => {
|
|||
const layer = wrapper.find("#spread-layer");
|
||||
expect(layer.find("SpreadCircle").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("is dragging", () => {
|
||||
const p = fakeProps();
|
||||
p.dragging = true;
|
||||
p.editing = true;
|
||||
p.currentPlant = p.plants[0];
|
||||
const wrapper = shallow(<SpreadLayer {...p} />);
|
||||
expect(wrapper.find(SpreadOverlapHelper).props().dragging).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<SpreadCircle />", () => {
|
||||
const fakeProps = (): SpreadCircleProps => ({
|
||||
plant: fakePlant(),
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
visible: true,
|
||||
animate: true,
|
||||
});
|
||||
|
||||
it("uses spread value", () => {
|
||||
const wrapper = shallow(<SpreadCircle {...fakeProps()} />);
|
||||
wrapper.setState({ spread: 20 });
|
||||
expect(wrapper.find("circle").props().r).toEqual(100);
|
||||
expect(wrapper.find("circle").hasClass("animate")).toBeTruthy();
|
||||
expect(wrapper.find("circle").props().fill).toEqual("url(#SpreadGradient)");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -36,7 +36,7 @@ export function SpreadLayer(props: SpreadLayerProps) {
|
|||
</defs>
|
||||
|
||||
{plants.map(p => {
|
||||
const selected = !!(currentPlant && (p.uuid === currentPlant.uuid));
|
||||
const selected = p.uuid === currentPlant?.uuid;
|
||||
return <g id={"spread-components-" + p.body.id} key={p.uuid}>
|
||||
{visible &&
|
||||
<SpreadCircle
|
||||
|
@ -58,7 +58,7 @@ export function SpreadLayer(props: SpreadLayerProps) {
|
|||
</g>;
|
||||
}
|
||||
|
||||
interface SpreadCircleProps {
|
||||
export interface SpreadCircleProps {
|
||||
plant: TaggedPlant;
|
||||
mapTransformProps: MapTransformProps;
|
||||
visible: boolean;
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
import * as React from "react";
|
||||
import { svgMount } from "../../../../../__test_support__/svg_mount";
|
||||
import { ZonesLayer, ZonesLayerProps } from "../zones_layer";
|
||||
import {
|
||||
fakePointGroup
|
||||
} from "../../../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
fakeMapTransformProps
|
||||
} from "../../../../../__test_support__/map_transform_props";
|
||||
import { PointGroup } from "farmbot/dist/resources/api_resources";
|
||||
|
||||
describe("<ZonesLayer />", () => {
|
||||
const fakeProps = (): ZonesLayerProps => ({
|
||||
visible: true,
|
||||
groups: [fakePointGroup(), fakePointGroup()],
|
||||
currentGroup: undefined,
|
||||
botSize: {
|
||||
x: { value: 3000, isDefault: true },
|
||||
y: { value: 1500, isDefault: true }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = svgMount(<ZonesLayer {...fakeProps()} />);
|
||||
expect(wrapper.find(".zones-layer").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("renders current group's zones: 2D", () => {
|
||||
const p = fakeProps();
|
||||
p.visible = false;
|
||||
p.groups[0].body.id = 1;
|
||||
p.groups[0].body.criteria.number_gt = { x: 100 };
|
||||
p.currentGroup = p.groups[0].uuid;
|
||||
p.groups[1].body.id = 2;
|
||||
p.groups[1].body.criteria.number_gt = { x: 200 };
|
||||
const wrapper = svgMount(<ZonesLayer {...p} />);
|
||||
expect(wrapper.find("#zones-0D-1").length).toEqual(0);
|
||||
expect(wrapper.find("#zones-1D-1").length).toEqual(0);
|
||||
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
|
||||
expect(wrapper.find("#zones-2D-2").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("renders current group's zones: 1D", () => {
|
||||
const p = fakeProps();
|
||||
p.visible = false;
|
||||
p.groups[0].body.id = 1;
|
||||
p.groups[0].body.criteria.number_eq = { x: [100] };
|
||||
p.currentGroup = p.groups[0].uuid;
|
||||
const wrapper = svgMount(<ZonesLayer {...p} />);
|
||||
expect(wrapper.find("#zones-0D-1").length).toEqual(0);
|
||||
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
|
||||
expect(wrapper.find("#zones-2D-1").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("renders current group's zones: 0D", () => {
|
||||
const p = fakeProps();
|
||||
p.visible = false;
|
||||
p.groups[0].body.id = 1;
|
||||
p.groups[0].body.criteria.number_eq = { x: [100], y: [100] };
|
||||
p.currentGroup = p.groups[0].uuid;
|
||||
const wrapper = svgMount(<ZonesLayer {...p} />);
|
||||
expect(wrapper.find("#zones-0D-1").length).toEqual(1);
|
||||
expect(wrapper.find("#zones-1D-1").length).toEqual(0);
|
||||
expect(wrapper.find("#zones-2D-1").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("renders current group's zones: none", () => {
|
||||
const p = fakeProps();
|
||||
p.visible = false;
|
||||
p.groups[0].body.id = 1;
|
||||
p.groups[0].body.criteria = undefined as unknown as PointGroup["criteria"];
|
||||
p.currentGroup = p.groups[0].uuid;
|
||||
const wrapper = svgMount(<ZonesLayer {...p} />);
|
||||
expect(wrapper.html())
|
||||
.toEqual("<svg><g class=\"zones-layer\"></g></svg>");
|
||||
});
|
||||
|
||||
it("doesn't render current group's zones", () => {
|
||||
const p = fakeProps();
|
||||
p.visible = false;
|
||||
const wrapper = svgMount(<ZonesLayer {...p} />);
|
||||
expect(wrapper.html())
|
||||
.toEqual("<svg><g class=\"zones-layer\"></g></svg>");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,165 @@
|
|||
import * as React from "react";
|
||||
import { svgMount } from "../../../../../__test_support__/svg_mount";
|
||||
import {
|
||||
Zones0D, ZonesProps, Zones1D, Zones2D, getZoneType, ZoneType
|
||||
} from "../zones";
|
||||
import {
|
||||
fakePointGroup
|
||||
} from "../../../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
fakeMapTransformProps
|
||||
} from "../../../../../__test_support__/map_transform_props";
|
||||
import { PointGroup } from "farmbot/dist/resources/api_resources";
|
||||
|
||||
const fakeProps = (): ZonesProps => ({
|
||||
group: fakePointGroup(),
|
||||
botSize: {
|
||||
x: { value: 3000, isDefault: true },
|
||||
y: { value: 1500, isDefault: true }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
currentGroup: undefined,
|
||||
});
|
||||
|
||||
describe("<Zones0D />", () => {
|
||||
it("renders none: no data", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria = undefined as unknown as PointGroup["criteria"];
|
||||
const wrapper = svgMount(<Zones0D {...p} />);
|
||||
expect(wrapper.find("#zones-0D-1").length).toEqual(1);
|
||||
expect(wrapper.find("circle").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("renders none: some data", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria.number_eq = { x: [100] };
|
||||
const wrapper = svgMount(<Zones0D {...p} />);
|
||||
expect(wrapper.find("#zones-0D-1").length).toEqual(1);
|
||||
expect(wrapper.find("circle").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("renders one", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria.number_eq = { x: [100], y: [200] };
|
||||
const wrapper = svgMount(<Zones0D {...p} />);
|
||||
expect(wrapper.find("#zones-0D-1").length).toEqual(1);
|
||||
expect(wrapper.find("circle").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("renders some", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria.number_eq = { x: [100], y: [200, 300] };
|
||||
const wrapper = svgMount(<Zones0D {...p} />);
|
||||
expect(wrapper.find("#zones-0D-1").length).toEqual(1);
|
||||
expect(wrapper.find("circle").length).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<Zones1D />", () => {
|
||||
it("renders none: no data", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria = undefined as unknown as PointGroup["criteria"];
|
||||
const wrapper = svgMount(<Zones1D {...p} />);
|
||||
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
|
||||
expect(wrapper.find("line").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("renders none: too constrained", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria.number_eq = { x: [100], y: [100] };
|
||||
const wrapper = svgMount(<Zones1D {...p} />);
|
||||
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
|
||||
expect(wrapper.find("line").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("renders one: x", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria.number_eq = { x: [100] };
|
||||
const wrapper = svgMount(<Zones1D {...p} />);
|
||||
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
|
||||
expect(wrapper.find("line").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("renders one: y", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria.number_eq = { y: [100] };
|
||||
const wrapper = svgMount(<Zones1D {...p} />);
|
||||
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
|
||||
expect(wrapper.find("line").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("renders some", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria.number_eq = { x: [], y: [200, 300] };
|
||||
const wrapper = svgMount(<Zones1D {...p} />);
|
||||
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
|
||||
expect(wrapper.find("line").length).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<Zones2D />", () => {
|
||||
it("renders none", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria = undefined as unknown as PointGroup["criteria"];
|
||||
const wrapper = svgMount(<Zones2D {...p} />);
|
||||
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
|
||||
expect(wrapper.find("rect").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("renders one", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria.number_gt = { x: 100, y: 200 };
|
||||
p.group.body.criteria.number_lt = { x: 300, y: 400 };
|
||||
const wrapper = svgMount(<Zones2D {...p} />);
|
||||
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
|
||||
expect(wrapper.find("rect").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("renders one: rotated", () => {
|
||||
const p = fakeProps();
|
||||
p.mapTransformProps.quadrant = 4;
|
||||
p.mapTransformProps.xySwap = true;
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria.number_gt = { x: 100, y: 200 };
|
||||
p.group.body.criteria.number_lt = { x: 300, y: 400 };
|
||||
const wrapper = svgMount(<Zones2D {...p} />);
|
||||
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
|
||||
expect(wrapper.find("rect").length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getZoneType()", () => {
|
||||
it("returns none", () => {
|
||||
const group = fakePointGroup();
|
||||
expect(getZoneType(group)).toEqual(ZoneType.none);
|
||||
});
|
||||
|
||||
it("returns area", () => {
|
||||
const group = fakePointGroup();
|
||||
group.body.criteria.number_gt = { x: 100 };
|
||||
expect(getZoneType(group)).toEqual(ZoneType.area);
|
||||
});
|
||||
|
||||
it("returns lines", () => {
|
||||
const group = fakePointGroup();
|
||||
group.body.criteria.number_eq = { x: [100] };
|
||||
expect(getZoneType(group)).toEqual(ZoneType.lines);
|
||||
});
|
||||
|
||||
it("returns points", () => {
|
||||
const group = fakePointGroup();
|
||||
group.body.criteria.number_eq = { x: [100], y: [100] };
|
||||
expect(getZoneType(group)).toEqual(ZoneType.points);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,161 @@
|
|||
import * as React from "react";
|
||||
import { TaggedPointGroup } from "farmbot";
|
||||
import { MapTransformProps, BotSize } from "../../interfaces";
|
||||
import { transformXY } from "../../util";
|
||||
import { isUndefined } from "lodash";
|
||||
import { UUID } from "../../../../resources/interfaces";
|
||||
|
||||
export interface ZonesProps {
|
||||
currentGroup: UUID | undefined;
|
||||
group: TaggedPointGroup;
|
||||
mapTransformProps: MapTransformProps;
|
||||
botSize: BotSize;
|
||||
}
|
||||
|
||||
interface GetBoundaryProps {
|
||||
group: TaggedPointGroup;
|
||||
botSize: BotSize;
|
||||
}
|
||||
|
||||
type Boundary = {
|
||||
x1: number, y1: number, x2: number, y2: number,
|
||||
selectsAll: boolean
|
||||
};
|
||||
type Line = { x?: number, y?: number };
|
||||
type Point = { x: number, y: number };
|
||||
export enum ZoneType { points, lines, area, none }
|
||||
|
||||
export const getZoneType = (group: TaggedPointGroup): ZoneType => {
|
||||
const numEq = group.body.criteria?.number_eq || {};
|
||||
const numGt = group.body.criteria?.number_gt || {};
|
||||
const numLt = group.body.criteria?.number_lt || {};
|
||||
const hasXEq = !!numEq.x?.length;
|
||||
const hasYEq = !!numEq.y?.length;
|
||||
if (hasXEq && hasYEq) {
|
||||
return ZoneType.points;
|
||||
}
|
||||
if ((hasXEq && !hasYEq) || (!hasXEq && hasYEq)) {
|
||||
return ZoneType.lines;
|
||||
}
|
||||
if (numGt.x || numGt.y || numLt.x || numLt.y) {
|
||||
return ZoneType.area;
|
||||
}
|
||||
return ZoneType.none;
|
||||
};
|
||||
|
||||
/** Bounds for area selected by criteria or bot extents. */
|
||||
const getBoundary = (props: GetBoundaryProps): Boundary => {
|
||||
const { criteria } = props.group.body;
|
||||
const gt = criteria?.number_gt || {};
|
||||
const lt = criteria?.number_lt || {};
|
||||
const x1 = gt.x || 0;
|
||||
const x2 = lt.x || props.botSize.x.value;
|
||||
const y1 = gt.y || 0;
|
||||
const y2 = lt.y || props.botSize.y.value;
|
||||
const selectsAll = !(gt.x || lt.x || gt.y || lt.y);
|
||||
return { x1, x2, y1, y2, selectsAll };
|
||||
};
|
||||
|
||||
/** Apply bounds to zone data. */
|
||||
const filter: <T extends Point | Line>(
|
||||
boundary: Boundary, data: T[] | undefined) => T[] =
|
||||
(boundary, data) =>
|
||||
data?.filter(({ x, y }) =>
|
||||
(isUndefined(x) || (x > boundary.x1 && x < boundary.x2)) &&
|
||||
(isUndefined(y) || (y > boundary.y1 && y < boundary.y2))) || [];
|
||||
|
||||
/** Coordinates selected by both x and y number equal values. */
|
||||
const getPoints =
|
||||
(boundary: Boundary, group: TaggedPointGroup): Point[] => {
|
||||
const xs = group.body.criteria?.number_eq.x;
|
||||
const ys = group.body.criteria?.number_eq.y;
|
||||
const points: Point[] = [];
|
||||
xs?.map(x => ys?.map(y => points.push({ x, y })));
|
||||
return filter<Point>(boundary, points);
|
||||
};
|
||||
|
||||
/** Coordinates selected by both x and y number equal values. */
|
||||
const zone0D = (props: ZonesProps) =>
|
||||
getPoints(getBoundary(props), props.group)
|
||||
.map(point => {
|
||||
const { qx, qy } = transformXY(point.x, point.y, props.mapTransformProps);
|
||||
return { x: qx, y: qy };
|
||||
});
|
||||
|
||||
/** Coordinates selected by both x and y number equal values. */
|
||||
export const Zones0D = (props: ZonesProps) => {
|
||||
const current = props.group.uuid == props.currentGroup;
|
||||
return <g id={`zones-0D-${props.group.body.id}`}
|
||||
className={current ? "current" : ""}>
|
||||
{zone0D(props).map((point, i) =>
|
||||
<circle key={i} cx={point.x} cy={point.y} r={5} />)}
|
||||
</g>;
|
||||
};
|
||||
|
||||
/** Lines selected by an x or y number equal value. */
|
||||
const getLines =
|
||||
(boundary: Boundary, group: TaggedPointGroup): Line[] => {
|
||||
const xs = group.body.criteria?.number_eq.x;
|
||||
const ys = group.body.criteria?.number_eq.y;
|
||||
const onlyXs = !!xs?.length && !ys?.length;
|
||||
const onlyYs = !!ys?.length && !xs?.length;
|
||||
const xLineData = onlyXs ? xs?.map(x => ({ x })) : undefined;
|
||||
const yLineData = onlyYs ? ys?.map(y => ({ y })) : undefined;
|
||||
return filter<Line>(boundary, xLineData || yLineData);
|
||||
};
|
||||
|
||||
/** Lines selected by an x or y number equal value. */
|
||||
const zone1D = (props: ZonesProps) => {
|
||||
const boundary = getBoundary(props);
|
||||
return getLines(boundary, props.group).map(line => {
|
||||
const min = transformXY(
|
||||
line.x || boundary.x1,
|
||||
line.y || boundary.y1, props.mapTransformProps);
|
||||
const max = transformXY(
|
||||
line.x || boundary.x2,
|
||||
line.y || boundary.y2, props.mapTransformProps);
|
||||
return {
|
||||
x1: min.qx,
|
||||
y1: min.qy,
|
||||
x2: max.qx,
|
||||
y2: max.qy,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/** Lines selected by an x or y number equal value. */
|
||||
export const Zones1D = (props: ZonesProps) => {
|
||||
const current = props.group.uuid == props.currentGroup;
|
||||
return <g id={`zones-1D-${props.group.body.id}`}
|
||||
className={current ? "current" : ""}>
|
||||
{zone1D(props).map((line, i) =>
|
||||
<line key={i} x1={line.x1} y1={line.y1}
|
||||
x2={line.x2} y2={line.y2} />)}
|
||||
</g>;
|
||||
};
|
||||
|
||||
/** Area selected by x and y number gt/lt values. */
|
||||
const zone2D = (boundary: Boundary, mapTransformProps: MapTransformProps) => {
|
||||
const position = transformXY(boundary.x1, boundary.y1, mapTransformProps);
|
||||
const { xySwap, quadrant } = mapTransformProps;
|
||||
const xLength = boundary.x2 - boundary.x1;
|
||||
const yLength = boundary.y2 - boundary.y1;
|
||||
return {
|
||||
x: [1, 4].includes(quadrant) ? position.qx - xLength : position.qx,
|
||||
y: [3, 4].includes(quadrant) ? position.qy - yLength : position.qy,
|
||||
width: xySwap ? yLength : xLength,
|
||||
height: xySwap ? xLength : yLength,
|
||||
selectsAll: boundary.selectsAll,
|
||||
};
|
||||
};
|
||||
|
||||
/** Area selected by x and y number gt/lt values. */
|
||||
export const Zones2D = (props: ZonesProps) => {
|
||||
const zone = zone2D(getBoundary(props), props.mapTransformProps);
|
||||
const current = props.group.uuid == props.currentGroup;
|
||||
return <g id={`zones-2D-${props.group.body.id}`}
|
||||
className={current ? "current" : ""}>
|
||||
{!zone.selectsAll &&
|
||||
<rect x={zone.x} y={zone.y} width={zone.width} height={zone.height} />}
|
||||
</g>;
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
import * as React from "react";
|
||||
import { TaggedPointGroup } from "farmbot";
|
||||
import { MapTransformProps, BotSize } from "../../interfaces";
|
||||
import { Zones0D, Zones1D, Zones2D, getZoneType, ZoneType } from "./zones";
|
||||
import { UUID } from "../../../../resources/interfaces";
|
||||
|
||||
export interface ZonesLayerProps {
|
||||
visible: boolean;
|
||||
currentGroup: UUID | undefined;
|
||||
groups: TaggedPointGroup[];
|
||||
botSize: BotSize;
|
||||
mapTransformProps: MapTransformProps;
|
||||
}
|
||||
|
||||
export function ZonesLayer(props: ZonesLayerProps) {
|
||||
const { groups, botSize, mapTransformProps, currentGroup } = props;
|
||||
const commonProps = { botSize, mapTransformProps, currentGroup };
|
||||
const visible = (group: TaggedPointGroup) =>
|
||||
props.visible || (group.uuid == currentGroup);
|
||||
return <g className="zones-layer">
|
||||
{groups.map(group => visible(group) &&
|
||||
getZoneType(group) === ZoneType.area &&
|
||||
<Zones2D {...commonProps} key={group.uuid} group={group} />)}
|
||||
{groups.map(group => visible(group) &&
|
||||
getZoneType(group) === ZoneType.lines &&
|
||||
<Zones1D {...commonProps} key={group.uuid} group={group} />)}
|
||||
{groups.map(group => visible(group) &&
|
||||
getZoneType(group) === ZoneType.points &&
|
||||
<Zones0D {...commonProps} key={group.uuid} group={group} />)}
|
||||
</g>;
|
||||
}
|
|
@ -40,6 +40,7 @@ describe("<GardenMapLegend />", () => {
|
|||
showSpread: false,
|
||||
showFarmbot: false,
|
||||
showImages: false,
|
||||
showZones: false,
|
||||
showSensorReadings: false,
|
||||
hasSensorReadings: false,
|
||||
dispatch: jest.fn(),
|
||||
|
|
|
@ -84,6 +84,11 @@ const LayerToggles = (props: GardenMapLegendProps) => {
|
|||
dispatch={props.dispatch}
|
||||
getConfigValue={getConfigValue}
|
||||
imageAgeInfo={props.imageAgeInfo} />} />
|
||||
{DevSettings.futureFeaturesEnabled() &&
|
||||
<LayerToggle
|
||||
value={props.showZones}
|
||||
label={t("Zones?")}
|
||||
onClick={toggle(BooleanSetting.show_zones)} />}
|
||||
{DevSettings.futureFeaturesEnabled() && props.hasSensorReadings &&
|
||||
<LayerToggle
|
||||
value={props.showSensorReadings}
|
||||
|
|
|
@ -291,7 +291,8 @@ export const transformForQuadrant =
|
|||
export const getMode = (): Mode => {
|
||||
const pathArray = getPathArray();
|
||||
if (pathArray) {
|
||||
if ((pathArray[3] === "groups") && pathArray[4]) { return Mode.editGroup; }
|
||||
if ((pathArray[3] === "groups" || pathArray[3] === "zones")
|
||||
&& pathArray[4]) { return Mode.editGroup; }
|
||||
if (pathArray[6] === "add") { return Mode.clickToAdd; }
|
||||
if (!isNaN(parseInt(pathArray.slice(-1)[0]))) { return Mode.editPlant; }
|
||||
if (pathArray[5] === "edit") { return Mode.editPlant; }
|
||||
|
|
|
@ -5,7 +5,7 @@ jest.mock("../../../history", () => ({
|
|||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { render } from "enzyme";
|
||||
import { mount } from "enzyme";
|
||||
import {
|
||||
RawAddPlant as AddPlant, AddPlantProps, mapStateToProps
|
||||
} from "../add_plant";
|
||||
|
@ -14,7 +14,8 @@ import {
|
|||
} from "../../../__test_support__/fake_crop_search_result";
|
||||
import { svgToUrl } from "../../../open_farm/icons";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import { CropLiveSearchResult } from "../../interfaces";
|
||||
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
|
||||
import { fakeWebAppConfig } from "../../../__test_support__/fake_state/resources";
|
||||
|
||||
describe("<AddPlant />", () => {
|
||||
const fakeProps = (): AddPlantProps => {
|
||||
|
@ -24,40 +25,40 @@ describe("<AddPlant />", () => {
|
|||
cropSearchResults: [cropSearchResult],
|
||||
dispatch: jest.fn(),
|
||||
xy_swap: false,
|
||||
openfarmSearch: jest.fn(),
|
||||
openfarmSearch: jest.fn(() => jest.fn()),
|
||||
};
|
||||
};
|
||||
|
||||
it("renders", () => {
|
||||
mockPath = "/app/designer/plants/crop_search/mint/add";
|
||||
const wrapper = render(<AddPlant {...fakeProps()} />);
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn(x => x(jest.fn()));
|
||||
const wrapper = mount(<AddPlant {...p} />);
|
||||
expect(wrapper.text()).toContain("Mint");
|
||||
expect(wrapper.text()).toContain("Preview");
|
||||
const img = wrapper.find("img");
|
||||
expect(img).toBeDefined();
|
||||
expect(img.attr("src")).toEqual(svgToUrl("fake_mint_svg"));
|
||||
expect(img.props().src).toEqual(svgToUrl("fake_mint_svg"));
|
||||
expect(p.openfarmSearch).toHaveBeenCalledWith("mint");
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapStateToProps", () => {
|
||||
it("maps state to props", () => {
|
||||
const state = fakeState();
|
||||
const crop: CropLiveSearchResult = {
|
||||
crop: {
|
||||
name: "fake",
|
||||
slug: "fake",
|
||||
binomial_name: "fake",
|
||||
common_names: ["fake"],
|
||||
description: "",
|
||||
sun_requirements: "",
|
||||
sowing_method: "",
|
||||
processing_pictures: 0
|
||||
},
|
||||
image: "X"
|
||||
};
|
||||
const crop = fakeCropLiveSearchResult();
|
||||
state.resources.consumers.farm_designer.cropSearchResults = [crop];
|
||||
const results = mapStateToProps(state);
|
||||
expect(results.cropSearchResults).toEqual([crop]);
|
||||
expect(results.xy_swap).toEqual(false);
|
||||
});
|
||||
|
||||
it("returns xy_swap equals true", () => {
|
||||
const state = fakeState();
|
||||
const webAppConfig = fakeWebAppConfig();
|
||||
webAppConfig.body.xy_swap = true;
|
||||
state.resources = buildResourceIndex([webAppConfig]);
|
||||
const results = mapStateToProps(state);
|
||||
expect(results.xy_swap).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { mapStateToProps } from "../map_state_to_props";
|
||||
import { mapStateToProps, plantAge } from "../map_state_to_props";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import {
|
||||
buildResourceIndex
|
||||
|
@ -32,3 +32,19 @@ describe("mapStateToProps()", () => {
|
|||
expect.objectContaining({ uuid }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("plantAge()", () => {
|
||||
it("returns planted at date", () => {
|
||||
const plant = fakePlant();
|
||||
plant.body.planted_at = "2018-01-11T20:20:38.362Z";
|
||||
plant.body.created_at = undefined;
|
||||
expect(plantAge(plant)).toBeGreaterThan(100);
|
||||
});
|
||||
|
||||
it("returns created at date", () => {
|
||||
const plant = fakePlant();
|
||||
plant.body.planted_at = undefined;
|
||||
plant.body.created_at = "2018-01-11T20:20:38.362Z";
|
||||
expect(plantAge(plant)).toBeGreaterThan(100);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
jest.mock("../../../open_farm/cached_crop", () => ({
|
||||
cachedCrop: jest.fn(() => Promise.resolve({ svg_icon: "icon" })),
|
||||
maybeGetCachedPlantIcon: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../history", () => ({ push: jest.fn() }));
|
||||
|
@ -8,22 +8,20 @@ import * as React from "react";
|
|||
import {
|
||||
PlantInventoryItem, PlantInventoryItemProps
|
||||
} from "../plant_inventory_item";
|
||||
import { shallow } from "enzyme";
|
||||
import { shallow, mount } from "enzyme";
|
||||
import {
|
||||
fakePlant, fakePlantTemplate
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { Actions } from "../../../constants";
|
||||
import { push } from "../../../history";
|
||||
import { svgToUrl } from "../../../open_farm/icons";
|
||||
import { maybeGetCachedPlantIcon } from "../../../open_farm/cached_crop";
|
||||
|
||||
describe("<PlantInventoryItem />", () => {
|
||||
const fakeProps = (): PlantInventoryItemProps => {
|
||||
return {
|
||||
tpp: fakePlant(),
|
||||
dispatch: jest.fn(),
|
||||
hovered: false,
|
||||
};
|
||||
};
|
||||
const fakeProps = (): PlantInventoryItemProps => ({
|
||||
plant: fakePlant(),
|
||||
dispatch: jest.fn(),
|
||||
hovered: false,
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = shallow(<PlantInventoryItem {...fakeProps()} />);
|
||||
|
@ -45,7 +43,7 @@ describe("<PlantInventoryItem />", () => {
|
|||
expect(p.dispatch).toBeCalledWith({
|
||||
payload: {
|
||||
icon: "",
|
||||
plantUUID: p.tpp.uuid
|
||||
plantUUID: p.plant.uuid
|
||||
},
|
||||
type: Actions.TOGGLE_HOVERED_PLANT
|
||||
});
|
||||
|
@ -69,30 +67,31 @@ describe("<PlantInventoryItem />", () => {
|
|||
const wrapper = shallow(<PlantInventoryItem {...p} />);
|
||||
wrapper.simulate("click");
|
||||
expect(p.dispatch).toBeCalledWith({
|
||||
payload: [p.tpp.uuid],
|
||||
payload: [p.plant.uuid],
|
||||
type: Actions.SELECT_PLANT
|
||||
});
|
||||
expect(push).toHaveBeenCalledWith("/app/designer/plants/" + p.tpp.body.id);
|
||||
expect(push).toHaveBeenCalledWith("/app/designer/plants/" + p.plant.body.id);
|
||||
});
|
||||
|
||||
it("selects plant template", () => {
|
||||
const p = fakeProps();
|
||||
p.tpp = fakePlantTemplate();
|
||||
p.plant = fakePlantTemplate();
|
||||
const wrapper = shallow(<PlantInventoryItem {...p} />);
|
||||
wrapper.simulate("click");
|
||||
expect(p.dispatch).toBeCalledWith({
|
||||
payload: [p.tpp.uuid],
|
||||
payload: [p.plant.uuid],
|
||||
type: Actions.SELECT_PLANT
|
||||
});
|
||||
expect(push).toHaveBeenCalledWith(
|
||||
"/app/designer/gardens/templates/" + p.tpp.body.id);
|
||||
"/app/designer/gardens/templates/" + p.plant.body.id);
|
||||
});
|
||||
|
||||
it("gets cached icon", async () => {
|
||||
it("gets cached icon", () => {
|
||||
const wrapper =
|
||||
shallow<PlantInventoryItem>(<PlantInventoryItem {...fakeProps()} />);
|
||||
const img = new Image;
|
||||
await wrapper.find("img").simulate("load", { currentTarget: img });
|
||||
expect(wrapper.state().icon).toEqual(svgToUrl("icon"));
|
||||
mount<PlantInventoryItem>(<PlantInventoryItem {...fakeProps()} />);
|
||||
const img = wrapper.find("img");
|
||||
img.simulate("load");
|
||||
expect(maybeGetCachedPlantIcon).toHaveBeenCalledWith("strawberry",
|
||||
img.instance(), expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
jest.mock("../../../open_farm/cached_crop", () => ({
|
||||
maybeGetCachedPlantIcon: jest.fn(),
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { RawPlants as Plants, PlantInventoryProps } from "../plant_inventory";
|
||||
import {
|
||||
RawPlants as Plants, PlantInventoryProps, mapStateToProps
|
||||
} from "../plant_inventory";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
|
||||
describe("<PlantInventory />", () => {
|
||||
const fakeProps = (): PlantInventoryProps => ({
|
||||
|
@ -32,3 +39,12 @@ describe("<PlantInventory />", () => {
|
|||
expect(wrapper.state().searchTerm).toEqual("mint");
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
it("returns props", () => {
|
||||
const state = fakeState();
|
||||
state.resources.consumers.farm_designer.hoveredPlantListItem = "uuid";
|
||||
const result = mapStateToProps(state);
|
||||
expect(result.hoveredPlantListItem).toEqual("uuid");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@ import { testGrid } from "./generate_grid_test";
|
|||
import { GridInput, InputCell, InputCellProps, createCB } from "../grid_input";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { BlurableInput } from "../../../../ui/blurable_input";
|
||||
import { DeepPartial } from "redux";
|
||||
import { changeEvent } from "../../../../__test_support__/fake_html_events";
|
||||
|
||||
describe("<GridInput/>", () => {
|
||||
it("renders", () => {
|
||||
|
@ -37,15 +37,8 @@ describe("<InputCell/>", () => {
|
|||
|
||||
describe("createCB", () => {
|
||||
it("creates a callback", () => {
|
||||
type E = React.ChangeEvent<HTMLInputElement>;
|
||||
const e: DeepPartial<E> = {
|
||||
currentTarget: {
|
||||
value: "7"
|
||||
}
|
||||
};
|
||||
const dispatch = jest.fn();
|
||||
const cb = createCB("numPlantsH", dispatch);
|
||||
cb(e as E);
|
||||
createCB("numPlantsH", dispatch)(changeEvent("7"));
|
||||
expect(dispatch).toHaveBeenCalledWith("numPlantsH", 7);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -49,22 +49,30 @@ export interface FormattedPlantInfo {
|
|||
plantStatus: PlantStage;
|
||||
}
|
||||
|
||||
export function formatPlantInfo(rsrc: TaggedPlant): FormattedPlantInfo {
|
||||
const p = rsrc.body;
|
||||
const plantedAt = get(p, "planted_at", moment())
|
||||
? moment(get(p, "planted_at", moment()))
|
||||
: moment(get(p, "created_at", moment()));
|
||||
const currentDay = moment();
|
||||
const daysOld = currentDay.diff(plantedAt, "days") + 1;
|
||||
/** Get date planted or fallback to creation date. */
|
||||
const plantDate = (plant: TaggedPlant): moment.Moment => {
|
||||
const plantedAt = get(plant, "body.planted_at");
|
||||
const createdAt = get(plant, "body.created_at", moment());
|
||||
return plantedAt ? moment(plantedAt) : moment(createdAt);
|
||||
};
|
||||
|
||||
/** Compare planted or created date vs time now to determine age. */
|
||||
export const plantAge = (plant: TaggedPlant): number => {
|
||||
const currentDate = moment();
|
||||
const daysOld = currentDate.diff(plantDate(plant), "days") + 1;
|
||||
return daysOld;
|
||||
};
|
||||
|
||||
export function formatPlantInfo(plant: TaggedPlant): FormattedPlantInfo {
|
||||
return {
|
||||
slug: p.openfarm_slug,
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
daysOld,
|
||||
x: p.x,
|
||||
y: p.y,
|
||||
uuid: rsrc.uuid,
|
||||
plantedAt,
|
||||
plantStatus: get(p, "plant_stage", "planned"),
|
||||
slug: plant.body.openfarm_slug,
|
||||
id: plant.body.id,
|
||||
name: plant.body.name,
|
||||
daysOld: plantAge(plant),
|
||||
x: plant.body.x,
|
||||
y: plant.body.y,
|
||||
uuid: plant.uuid,
|
||||
plantedAt: plantDate(plant),
|
||||
plantStatus: get(plant, "plant_stage", "planned"),
|
||||
};
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue