Merge branch 'staging' of github.com:FarmBot/Farmbot-Web-App into new_steps
commit
f2ca8fc6fa
|
@ -0,0 +1,35 @@
|
|||
module Api
|
||||
class FoldersController < Api::AbstractController
|
||||
def create
|
||||
mutate Folders::Create.run(raw_json, device: current_device)
|
||||
end
|
||||
|
||||
def index
|
||||
render json: folders
|
||||
end
|
||||
|
||||
def show
|
||||
render json: folder
|
||||
end
|
||||
|
||||
def update
|
||||
mutate Folders::Update.run(raw_json,
|
||||
folder: folder,
|
||||
device: current_device)
|
||||
end
|
||||
|
||||
def destroy
|
||||
mutate Folders::Destroy.run(folder: folder, device: current_device)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def folder
|
||||
folders.find(params[:id])
|
||||
end
|
||||
|
||||
def folders
|
||||
current_device.folders
|
||||
end
|
||||
end
|
||||
end
|
|
@ -90,6 +90,7 @@ module CeleryScript
|
|||
id: sequence.id,
|
||||
created_at: sequence.created_at,
|
||||
updated_at: sequence.updated_at,
|
||||
folder_id: sequence.folder_id,
|
||||
args: Sequence::DEFAULT_ARGS,
|
||||
color: sequence.color,
|
||||
name: sequence.name,
|
||||
|
|
|
@ -34,6 +34,7 @@ class Device < ApplicationRecord
|
|||
has_many :in_use_tools
|
||||
has_many :in_use_points
|
||||
has_many :users
|
||||
has_many :folders
|
||||
|
||||
validates_presence_of :name
|
||||
validates :timezone, inclusion: {
|
||||
|
|
|
@ -12,6 +12,7 @@ class FbosConfig < ApplicationRecord
|
|||
ARDUINO = "arduino",
|
||||
FARMDUINO = "farmduino",
|
||||
FARMDUINO_K14 = "farmduino_k14",
|
||||
FARMDUINO_K15 = "farmduino_k15",
|
||||
EXPRESS_K10 = "express_k10",
|
||||
]
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
class Folder < ApplicationRecord
|
||||
belongs_to :device
|
||||
|
||||
has_many :sub_folders, class_name: "Folder",
|
||||
foreign_key: "parent_id"
|
||||
|
||||
belongs_to :parent, class_name: "Folder", optional: true
|
||||
end
|
|
@ -16,6 +16,7 @@ class Sequence < ApplicationRecord
|
|||
include CeleryScriptSettingsBag
|
||||
|
||||
belongs_to :device
|
||||
belongs_to :folder
|
||||
belongs_to :fbos_config, foreign_key: :boot_sequence_id
|
||||
has_one :sequence_usage_report
|
||||
has_many :farm_events, as: :executable
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
module Folders
|
||||
class Create < Mutations::Command
|
||||
required do
|
||||
model :device
|
||||
string :color
|
||||
string :name
|
||||
end
|
||||
|
||||
optional do
|
||||
integer :parent_id
|
||||
end
|
||||
|
||||
def execute
|
||||
Folder.create!(update_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_params
|
||||
inputs.except(:parent_id).merge({ parent: parent })
|
||||
end
|
||||
|
||||
def parent
|
||||
@parent ||= device.folders.find_by(id: parent_id)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,44 @@
|
|||
module Folders
|
||||
class Destroy < Mutations::Command
|
||||
IN_USE = "This folder still contains %s %s(s). " \
|
||||
"They must be removed prior to deletion"
|
||||
|
||||
required do
|
||||
model :device
|
||||
model :folder
|
||||
end
|
||||
|
||||
def validate
|
||||
check_subfolders
|
||||
check_sequences
|
||||
end
|
||||
|
||||
def execute
|
||||
folder.destroy! && ""
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sequences
|
||||
@sequences ||= Sequence.where(folder: folder)
|
||||
end
|
||||
|
||||
def subfolders
|
||||
@subfolders ||= Folder.where(parent: folder)
|
||||
end
|
||||
|
||||
def check_sequences
|
||||
count = sequences.count
|
||||
if count > 0
|
||||
add_error :in_use, :in_use, IN_USE % [count, "sequence"]
|
||||
end
|
||||
end
|
||||
|
||||
def check_subfolders
|
||||
count = subfolders.count
|
||||
if count > 0
|
||||
add_error :in_use, :in_use, IN_USE % [count, "subfolder"]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
module Folders
|
||||
class Update < Mutations::Command
|
||||
required do
|
||||
model :device
|
||||
model :folder
|
||||
integer :parent_id, nils: true, empty_is_nil: true
|
||||
end
|
||||
|
||||
optional do
|
||||
string :name
|
||||
string :color
|
||||
end
|
||||
|
||||
def execute
|
||||
folder.update!(update_params) && folder
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_params
|
||||
inputs.except(:device, :folder).merge({ parent: parent })
|
||||
end
|
||||
|
||||
def parent
|
||||
@parent ||= parent_id && device.folders.find_by(id: parent_id)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -12,6 +12,7 @@ module Sequences
|
|||
optional do
|
||||
color
|
||||
args
|
||||
integer :folder_id
|
||||
end
|
||||
|
||||
def validate
|
||||
|
@ -25,6 +26,7 @@ module Sequences
|
|||
p = inputs
|
||||
.merge(migrated_nodes: true)
|
||||
.without(:body, :args, "body", "args")
|
||||
.merge(folder: device.folders.find_by(id: folder_id))
|
||||
seq = Sequence.create!(p)
|
||||
x = CeleryScript::FirstPass.run!(sequence: seq,
|
||||
args: args || {},
|
||||
|
|
|
@ -2,7 +2,12 @@ module Sequences
|
|||
class Update < Mutations::Command
|
||||
include CeleryScriptValidators
|
||||
using CanonicalCeleryHelpers
|
||||
BLACKLIST = [:sequence, :device, :args, :body]
|
||||
BLACKLIST = [:sequence, :device, :args, :body, :folder_id]
|
||||
BASE = "Can't add 'parent' to sequence because "
|
||||
EXPL = {
|
||||
FarmEvent => BASE + "it is in use by FarmEvents on these dates: %{items}",
|
||||
Regimen => BASE + "the following Regimen(s) are using it: %{items}",
|
||||
}
|
||||
|
||||
required do
|
||||
model :device, class: Device
|
||||
|
@ -27,6 +32,7 @@ module Sequences
|
|||
|
||||
optional do
|
||||
color
|
||||
integer :folder_id
|
||||
end
|
||||
|
||||
def validate
|
||||
|
@ -40,7 +46,7 @@ module Sequences
|
|||
Sequence.auto_sync_debounce do
|
||||
ActiveRecord::Base.transaction do
|
||||
sequence.migrated_nodes = true
|
||||
sequence.update!(inputs.except(*BLACKLIST))
|
||||
sequence.update!(inputs.except(*BLACKLIST).merge(folder_stuff))
|
||||
CeleryScript::StoreCelery.run!(sequence: sequence,
|
||||
args: args,
|
||||
body: body)
|
||||
|
@ -49,11 +55,12 @@ module Sequences
|
|||
end
|
||||
CeleryScript::FetchCelery.run!(sequence: sequence, args: args, body: body)
|
||||
end
|
||||
|
||||
BASE = "Can't add 'parent' to sequence because "
|
||||
EXPL = {
|
||||
FarmEvent => BASE + "it is in use by FarmEvents on these dates: %{items}",
|
||||
Regimen => BASE + "the following Regimen(s) are using it: %{items}",
|
||||
}
|
||||
def folder_stuff
|
||||
if folder_id
|
||||
return { folder: device.folders.find_by(id: folder_id) }
|
||||
else
|
||||
return {}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
class FolderSerializer < ApplicationSerializer
|
||||
attributes :id, :parent_id, :color, :name
|
||||
end
|
|
@ -25,6 +25,7 @@ FarmBot::Application.routes.draw do
|
|||
sequences: [:create, :destroy, :index, :show, :update],
|
||||
tools: [:create, :destroy, :index, :show, :update],
|
||||
webcam_feeds: [:create, :destroy, :index, :show, :update],
|
||||
folders: [:create, :destroy, :index, :show, :update],
|
||||
}.to_a.map { |(name, only)| resources name, only: only }
|
||||
|
||||
# Singular API Resources:
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
class AddFolderColumns < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
create_table :folders do |t|
|
||||
t.references :device, null: false
|
||||
t.timestamps
|
||||
# https://twitter.com/wesbos/status/719678818831757313?lang=en
|
||||
t.string :color, limit: 20, null: false
|
||||
t.string :name, limit: 40, null: false
|
||||
end
|
||||
add_column :folders,
|
||||
:parent_id,
|
||||
:integer,
|
||||
null: true,
|
||||
index: true
|
||||
add_foreign_key :folders,
|
||||
:folders,
|
||||
column: :parent_id
|
||||
|
||||
add_reference :sequences, :folder, index: true
|
||||
end
|
||||
end
|
|
@ -1406,7 +1406,7 @@ CREATE VIEW public.resource_update_steps AS
|
|||
edge_nodes.kind,
|
||||
edge_nodes.value
|
||||
FROM public.edge_nodes
|
||||
WHERE (((edge_nodes.kind)::text = 'resource_type'::text) AND ((edge_nodes.value)::text = ANY (ARRAY[('"GenericPointer"'::character varying)::text, ('"ToolSlot"'::character varying)::text, ('"Plant"'::character varying)::text])))
|
||||
WHERE (((edge_nodes.kind)::text = 'resource_type'::text) AND ((edge_nodes.value)::text = ANY ((ARRAY['"GenericPointer"'::character varying, '"ToolSlot"'::character varying, '"Plant"'::character varying])::text[])))
|
||||
), resource_id AS (
|
||||
SELECT edge_nodes.primary_node_id,
|
||||
edge_nodes.kind,
|
||||
|
@ -1686,8 +1686,7 @@ CREATE TABLE public.users (
|
|||
agreed_to_terms_at timestamp without time zone,
|
||||
confirmation_sent_at timestamp without time zone,
|
||||
unconfirmed_email character varying,
|
||||
inactivity_warning_sent_at timestamp without time zone,
|
||||
inactivity_warning_count integer
|
||||
inactivity_warning_sent_at timestamp without time zone
|
||||
);
|
||||
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ export const fakeDesignerState = (): DesignerState => ({
|
|||
},
|
||||
hoveredPoint: undefined,
|
||||
hoveredPlantListItem: undefined,
|
||||
hoveredToolSlot: undefined,
|
||||
cropSearchQuery: "",
|
||||
cropSearchResults: [],
|
||||
cropSearchInProgress: false,
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { HardwareFlags, FarmwareData } from "../sequences/interfaces";
|
||||
|
||||
export const fakeHardwareFlags = (): HardwareFlags => ({
|
||||
findHomeEnabled: { x: false, y: false, z: false },
|
||||
stopAtHome: { x: false, y: false, z: false },
|
||||
stopAtMax: { x: false, y: false, z: false },
|
||||
negativeOnly: { x: false, y: false, z: false },
|
||||
axisLength: { x: 0, y: 0, z: 0 },
|
||||
});
|
||||
|
||||
export const fakeFarmwareData = (): FarmwareData => ({
|
||||
farmwareNames: [],
|
||||
firstPartyFarmwareNames: [],
|
||||
showFirstPartyFarmware: false,
|
||||
farmwareConfigs: {},
|
||||
cameraDisabled: false,
|
||||
});
|
|
@ -1,6 +1,5 @@
|
|||
import { noop } from "lodash";
|
||||
import { Everything } from "../interfaces";
|
||||
import { peripherals as Peripheral } from "./fake_state/peripherals";
|
||||
import { auth } from "./fake_state/token";
|
||||
import { bot } from "./fake_state/bot";
|
||||
import { config } from "./fake_state/config";
|
||||
|
@ -11,7 +10,6 @@ import { resources } from "./fake_state/resources";
|
|||
export function fakeState(_: Function = noop): Everything {
|
||||
return {
|
||||
dispatch: jest.fn(),
|
||||
Peripheral,
|
||||
auth,
|
||||
bot,
|
||||
config,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Everything } from "../../interfaces";
|
||||
|
||||
export let bot: Everything["bot"] = {
|
||||
export const bot: Everything["bot"] = {
|
||||
"consistent": true,
|
||||
"stepSize": 100,
|
||||
"controlPanelState": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Everything } from "../../interfaces";
|
||||
|
||||
export let config: Everything["config"] = {
|
||||
export const config: Everything["config"] = {
|
||||
"host": "localhost",
|
||||
"port": "3000"
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Everything } from "../../interfaces";
|
||||
|
||||
export let draggable: Everything["draggable"] = {
|
||||
export const draggable: Everything["draggable"] = {
|
||||
"dataTransfer": {}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { TaggedImage, SpecialStatus } from "farmbot";
|
||||
|
||||
export let fakeImages: TaggedImage[] = [
|
||||
export const fakeImages: TaggedImage[] = [
|
||||
{
|
||||
"kind": "Image",
|
||||
"specialStatus": SpecialStatus.SAVED,
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import { Everything } from "../../interfaces";
|
||||
|
||||
export let peripherals: Everything["Peripheral"] = {
|
||||
"isEditing": true
|
||||
};
|
|
@ -26,29 +26,42 @@ import {
|
|||
TaggedFarmwareInstallation,
|
||||
TaggedAlert,
|
||||
TaggedPointGroup,
|
||||
TaggedFolder,
|
||||
} from "farmbot";
|
||||
import { fakeResource } from "../fake_resource";
|
||||
import {
|
||||
ExecutableType, PinBindingType
|
||||
ExecutableType, PinBindingType, Folder
|
||||
} from "farmbot/dist/resources/api_resources";
|
||||
import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
|
||||
import { MessageType } from "../../sequences/interfaces";
|
||||
|
||||
export let resources: Everything["resources"] = buildResourceIndex();
|
||||
export const resources: Everything["resources"] = buildResourceIndex();
|
||||
let idCounter = 1;
|
||||
|
||||
export function fakeSequence(): TaggedSequence {
|
||||
return fakeResource("Sequence", {
|
||||
args: {
|
||||
version: 4,
|
||||
locals: { kind: "scope_declaration", args: {} },
|
||||
},
|
||||
export const fakeSequence =
|
||||
(body: Partial<TaggedSequence["body"]> = {}): TaggedSequence => {
|
||||
return fakeResource("Sequence", {
|
||||
args: {
|
||||
version: 4,
|
||||
locals: { kind: "scope_declaration", args: {} },
|
||||
},
|
||||
id: idCounter++,
|
||||
color: "red",
|
||||
folder_id: undefined,
|
||||
name: "fake",
|
||||
kind: "sequence",
|
||||
body: [],
|
||||
...body
|
||||
});
|
||||
};
|
||||
|
||||
export function fakeFolder(input: Partial<Folder> = {}): TaggedFolder {
|
||||
return fakeResource("Folder", {
|
||||
id: idCounter++,
|
||||
color: "red",
|
||||
parent_id: undefined,
|
||||
name: "fake",
|
||||
kind: "sequence",
|
||||
folder_id: undefined,
|
||||
body: []
|
||||
...input
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { AuthState } from "../../auth/interfaces";
|
||||
|
||||
export let auth: AuthState = {
|
||||
export const auth: AuthState = {
|
||||
"token": {
|
||||
"unencoded": {
|
||||
"jti": "xyz",
|
||||
|
|
|
@ -12,7 +12,7 @@ export const TIME = {
|
|||
SATURDAY: moment("2017-06-24T06:30:00.000-05:00")
|
||||
};
|
||||
|
||||
export let fakeFarmEventWithExecutable = (): FarmEventWithExecutable => {
|
||||
export const fakeFarmEventWithExecutable = (): FarmEventWithExecutable => {
|
||||
return {
|
||||
id: 1,
|
||||
start_time: "---",
|
||||
|
@ -29,7 +29,7 @@ export let fakeFarmEventWithExecutable = (): FarmEventWithExecutable => {
|
|||
};
|
||||
};
|
||||
|
||||
export let calendarRows = [
|
||||
export const calendarRows = [
|
||||
{
|
||||
"sortKey": 1500922800,
|
||||
"year": 17,
|
||||
|
|
|
@ -328,7 +328,7 @@ const log: TaggedLog = {
|
|||
uuid: "Log.1091396.70"
|
||||
};
|
||||
|
||||
export let FAKE_RESOURCES: TaggedResource[] = [
|
||||
export const FAKE_RESOURCES: TaggedResource[] = [
|
||||
tr1,
|
||||
fakeDevice(),
|
||||
tr2,
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import { HardwareFlags } from "../sequences/interfaces";
|
||||
|
||||
export const fakeHardwareFlags = (): HardwareFlags => {
|
||||
return {
|
||||
findHomeEnabled: { x: false, y: false, z: false },
|
||||
stopAtHome: { x: false, y: false, z: false },
|
||||
stopAtMax: { x: false, y: false, z: false },
|
||||
negativeOnly: { x: false, y: false, z: false },
|
||||
axisLength: { x: 0, y: 0, z: 0 },
|
||||
};
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
import { User } from "farmbot/dist/resources/api_resources";
|
||||
import { TaggedUser, SpecialStatus } from "farmbot";
|
||||
|
||||
export let user: User = {
|
||||
export const user: User = {
|
||||
created_at: "2016-10-05T03:02:58.000Z",
|
||||
email: "farmbot1@farmbot.io",
|
||||
id: 2,
|
||||
|
@ -9,7 +9,7 @@ export let user: User = {
|
|||
updated_at: "2017-08-04T19:53:29.724Z"
|
||||
};
|
||||
|
||||
export let taggedUser: TaggedUser = {
|
||||
export const taggedUser: TaggedUser = {
|
||||
kind: "User",
|
||||
uuid: "1234-5678",
|
||||
specialStatus: SpecialStatus.SAVED,
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
jest.mock("../session", () => ({ Session: { clear: jest.fn() } }));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { Apology } from "../apology";
|
||||
import { Session } from "../session";
|
||||
|
||||
describe("<Apology />", () => {
|
||||
it("clears session", () => {
|
||||
const wrapper = mount(<Apology />);
|
||||
wrapper.find("a").first().simulate("click");
|
||||
expect(Session.clear).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -9,7 +9,7 @@ import { RawApp as App, AppProps, mapStateToProps } from "../app";
|
|||
import { mount } from "enzyme";
|
||||
import { bot } from "../__test_support__/fake_state/bot";
|
||||
import {
|
||||
fakeUser, fakeWebAppConfig
|
||||
fakeUser, fakeWebAppConfig, fakeFbosConfig, fakeFarmwareEnv
|
||||
} from "../__test_support__/fake_state/resources";
|
||||
import { fakeState } from "../__test_support__/fake_state";
|
||||
import {
|
||||
|
@ -40,7 +40,8 @@ const fakeProps = (): AppProps => ({
|
|||
resources: buildResourceIndex().index,
|
||||
autoSync: false,
|
||||
alertCount: 0,
|
||||
pings: fakePings()
|
||||
pings: fakePings(),
|
||||
env: {},
|
||||
});
|
||||
|
||||
describe("<App />: Controls Pop-Up", () => {
|
||||
|
@ -145,7 +146,24 @@ describe("mapStateToProps()", () => {
|
|||
const config = fakeWebAppConfig();
|
||||
config.body.x_axis_inverted = true;
|
||||
state.resources = buildResourceIndex([config]);
|
||||
state.bot.hardware.user_env = { fake: "value" };
|
||||
const result = mapStateToProps(state);
|
||||
expect(result.axisInversion.x).toEqual(true);
|
||||
expect(result.autoSync).toEqual(false);
|
||||
expect(result.env).toEqual({ fake: "value" });
|
||||
});
|
||||
|
||||
it("returns api props", () => {
|
||||
const state = fakeState();
|
||||
const config = fakeFbosConfig();
|
||||
config.body.auto_sync = true;
|
||||
config.body.api_migrated = true;
|
||||
const fakeEnv = fakeFarmwareEnv();
|
||||
state.resources = buildResourceIndex([config, fakeEnv]);
|
||||
state.bot.minOsFeatureData = { api_farmware_env: "8.0.0" };
|
||||
state.bot.hardware.informational_settings.controller_version = "8.0.0";
|
||||
const result = mapStateToProps(state);
|
||||
expect(result.autoSync).toEqual(true);
|
||||
expect(result.env).toEqual({ [fakeEnv.body.key]: fakeEnv.body.value });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,38 +9,37 @@ import { ControlsPopup } from "../controls_popup";
|
|||
import { mount } from "enzyme";
|
||||
import { bot } from "../__test_support__/fake_state/bot";
|
||||
import { ControlsPopupProps } from "../controls/move/interfaces";
|
||||
import { error } from "../toast/toast";
|
||||
import { Content, ToolTips } from "../constants";
|
||||
|
||||
describe("<ControlsPopup />", () => {
|
||||
const fakeProps = (): ControlsPopupProps => {
|
||||
return {
|
||||
dispatch: jest.fn(),
|
||||
axisInversion: { x: false, y: false, z: false },
|
||||
botPosition: { x: undefined, y: undefined, z: undefined },
|
||||
firmwareSettings: bot.hardware.mcu_params,
|
||||
xySwap: false,
|
||||
arduinoBusy: false,
|
||||
stepSize: 100,
|
||||
botOnline: true,
|
||||
};
|
||||
};
|
||||
|
||||
const p = fakeProps();
|
||||
p.axisInversion.x = true;
|
||||
const wrapper = mount(<ControlsPopup {...p} />);
|
||||
|
||||
afterAll(wrapper.unmount);
|
||||
const fakeProps = (): ControlsPopupProps => ({
|
||||
dispatch: jest.fn(),
|
||||
axisInversion: { x: true, y: false, z: false },
|
||||
botPosition: { x: undefined, y: undefined, z: undefined },
|
||||
firmwareSettings: bot.hardware.mcu_params,
|
||||
xySwap: false,
|
||||
arduinoBusy: false,
|
||||
stepSize: 100,
|
||||
botOnline: true,
|
||||
env: {},
|
||||
});
|
||||
|
||||
it("Has a false initial state", () => {
|
||||
const wrapper = mount(<ControlsPopup {...fakeProps()} />);
|
||||
expect(wrapper.state("isOpen")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("Toggles state", () => {
|
||||
const wrapper = mount(<ControlsPopup {...fakeProps()} />);
|
||||
const parent = wrapper.find("i").first();
|
||||
parent.simulate("click");
|
||||
expect(wrapper.state("isOpen")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("x axis is inverted", () => {
|
||||
const wrapper = mount(<ControlsPopup {...fakeProps()} />);
|
||||
wrapper.setState({ isOpen: true });
|
||||
const button = wrapper.find("button").at(3);
|
||||
expect(button.props().title).toBe("move x axis (100)");
|
||||
button.simulate("click");
|
||||
|
@ -49,6 +48,8 @@ describe("<ControlsPopup />", () => {
|
|||
});
|
||||
|
||||
it("y axis is not inverted", () => {
|
||||
const wrapper = mount(<ControlsPopup {...fakeProps()} />);
|
||||
wrapper.setState({ isOpen: true });
|
||||
const button = wrapper.find("button").at(1);
|
||||
expect(button.props().title).toBe("move y axis (100)");
|
||||
button.simulate("click");
|
||||
|
@ -57,6 +58,7 @@ describe("<ControlsPopup />", () => {
|
|||
});
|
||||
|
||||
it("disabled when closed", () => {
|
||||
const wrapper = mount(<ControlsPopup {...fakeProps()} />);
|
||||
wrapper.setState({ isOpen: false });
|
||||
[0, 1, 2, 3].map((i) => wrapper.find("button").at(i).simulate("click"));
|
||||
expect(mockDevice.moveRelative).not.toHaveBeenCalled();
|
||||
|
@ -65,6 +67,7 @@ describe("<ControlsPopup />", () => {
|
|||
it("swaps axes", () => {
|
||||
const swappedProps = fakeProps();
|
||||
swappedProps.xySwap = true;
|
||||
swappedProps.axisInversion.x = false;
|
||||
const swapped = mount(<ControlsPopup {...swappedProps} />);
|
||||
swapped.setState({ isOpen: true });
|
||||
expect(swapped.state("isOpen")).toBeTruthy();
|
||||
|
@ -76,7 +79,25 @@ describe("<ControlsPopup />", () => {
|
|||
});
|
||||
|
||||
it("takes photo", () => {
|
||||
wrapper.find("button").at(4).simulate("click");
|
||||
const wrapper = mount(<ControlsPopup {...fakeProps()} />);
|
||||
wrapper.setState({ isOpen: true });
|
||||
const btn = wrapper.find("button").at(4);
|
||||
expect(btn.props().title).not.toEqual(Content.NO_CAMERA_SELECTED);
|
||||
btn.simulate("click");
|
||||
expect(mockDevice.takePhoto).toHaveBeenCalled();
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows camera as disabled", () => {
|
||||
const p = fakeProps();
|
||||
p.env = { camera: "NONE" };
|
||||
const wrapper = mount(<ControlsPopup {...p} />);
|
||||
wrapper.setState({ isOpen: true });
|
||||
const btn = wrapper.find("button").at(4);
|
||||
expect(btn.props().title).toEqual(Content.NO_CAMERA_SELECTED);
|
||||
btn.simulate("click");
|
||||
expect(error).toHaveBeenCalledWith(
|
||||
ToolTips.SELECT_A_CAMERA, Content.NO_CAMERA_SELECTED);
|
||||
expect(mockDevice.takePhoto).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -164,5 +164,7 @@ export class API {
|
|||
get alertPath() { return `${this.baseUrl}/api/alerts/`; }
|
||||
/** /api/global_bulletins/:id */
|
||||
get globalBulletinPath() { return `${this.baseUrl}/api/global_bulletins/`; }
|
||||
/** /api/folders */
|
||||
get foldersPath() { return `${this.baseUrl}/api/folders/`; }
|
||||
// get syncPath() { return `${this.baseUrl}/api/device/sync/`; }
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import {
|
||||
TaggedResource, SpecialStatus, ResourceName, TaggedSequence
|
||||
TaggedResource,
|
||||
SpecialStatus,
|
||||
ResourceName,
|
||||
TaggedSequence,
|
||||
} from "farmbot";
|
||||
import {
|
||||
isTaggedResource,
|
||||
|
@ -8,7 +11,11 @@ import { GetState, ReduxAction } from "../redux/interfaces";
|
|||
import { API } from "./index";
|
||||
import axios from "axios";
|
||||
import {
|
||||
updateNO, destroyOK, destroyNO, GeneralizedError, saveOK
|
||||
updateNO,
|
||||
destroyOK,
|
||||
destroyNO,
|
||||
GeneralizedError,
|
||||
saveOK,
|
||||
} from "../resources/actions";
|
||||
import { UnsafeError } from "../interfaces";
|
||||
import { defensiveClone, unpackUUID } from "../util";
|
||||
|
@ -274,6 +281,7 @@ export function urlFor(tag: ResourceName) {
|
|||
User: API.current.usersPath,
|
||||
WebAppConfig: API.current.webAppConfigPath,
|
||||
WebcamFeed: API.current.webcamFeedPath,
|
||||
Folder: API.current.foldersPath,
|
||||
};
|
||||
const url = OPTIONS[tag];
|
||||
if (url) {
|
||||
|
|
|
@ -15,6 +15,7 @@ const BLACKLIST: ResourceName[] = [
|
|||
"WebAppConfig",
|
||||
"WebcamFeed",
|
||||
"Alert",
|
||||
"Folder",
|
||||
];
|
||||
|
||||
export function maybeStartTracking(uuid: string) {
|
||||
|
|
|
@ -1,20 +1,29 @@
|
|||
import * as React from "react";
|
||||
import { Session } from "./session";
|
||||
|
||||
const STYLE: React.CSSProperties = {
|
||||
border: "2px solid #434343",
|
||||
background: "#a4c2f4",
|
||||
fontSize: "18px",
|
||||
const OUTER_STYLE: React.CSSProperties = {
|
||||
borderRadius: "10px",
|
||||
background: "repeating-linear-gradient(-45deg," +
|
||||
"#ffff55, #ffff55 20px," +
|
||||
"#ff5555 20px, #ff5555 40px)",
|
||||
fontSize: "100%",
|
||||
color: "black",
|
||||
display: "block",
|
||||
overflow: "auto",
|
||||
padding: "1rem",
|
||||
margin: "1rem",
|
||||
};
|
||||
|
||||
const INNER_STYLE: React.CSSProperties = {
|
||||
borderRadius: "10px",
|
||||
background: "#ffffffdd",
|
||||
padding: "1rem",
|
||||
};
|
||||
|
||||
export function Apology(_: {}) {
|
||||
return <div style={STYLE}>
|
||||
<div>
|
||||
<h1>Page Error</h1>
|
||||
return <div style={OUTER_STYLE}>
|
||||
<div style={INNER_STYLE}>
|
||||
<h1 style={{ fontSize: "175%" }}>Page Error</h1>
|
||||
<span>
|
||||
We can't render this part of the page due to an unrecoverable error.
|
||||
Here are some things you can try:
|
||||
|
|
|
@ -4,7 +4,7 @@ import { init, error } from "./toast/toast";
|
|||
import { NavBar } from "./nav";
|
||||
import { Everything, TimeSettings } from "./interfaces";
|
||||
import { LoadingPlant } from "./loading_plant";
|
||||
import { BotState, Xyz } from "./devices/interfaces";
|
||||
import { BotState, Xyz, UserEnv } from "./devices/interfaces";
|
||||
import { ResourceName, TaggedUser, TaggedLog } from "farmbot";
|
||||
import {
|
||||
maybeFetchUser,
|
||||
|
@ -30,6 +30,7 @@ import { isBotOnline } from "./devices/must_be_online";
|
|||
import { getStatus } from "./connectivity/reducer_support";
|
||||
import { getAllAlerts } from "./messages/state_to_props";
|
||||
import { PingDictionary } from "./devices/connectivity/qos";
|
||||
import { getEnv, getShouldDisplayFn } from "./farmware/state_to_props";
|
||||
|
||||
/** For the logger module */
|
||||
init();
|
||||
|
@ -52,11 +53,14 @@ export interface AppProps {
|
|||
autoSync: boolean;
|
||||
alertCount: number;
|
||||
pings: PingDictionary;
|
||||
env: UserEnv;
|
||||
}
|
||||
|
||||
export function mapStateToProps(props: Everything): AppProps {
|
||||
const webAppConfigValue = getWebAppConfigValue(() => props);
|
||||
const fbosConfig = validFbosConfig(getFbosConfig(props.resources.index));
|
||||
const shouldDisplay = getShouldDisplayFn(props.resources.index, props.bot);
|
||||
const env = getEnv(props.resources.index, shouldDisplay, props.bot);
|
||||
return {
|
||||
timeSettings: maybeGetTimeSettings(props.resources.index),
|
||||
dispatch: props.dispatch,
|
||||
|
@ -78,7 +82,8 @@ export function mapStateToProps(props: Everything): AppProps {
|
|||
resources: props.resources.index,
|
||||
autoSync: !!(fbosConfig && fbosConfig.auto_sync),
|
||||
alertCount: getAllAlerts(props.resources).length,
|
||||
pings: props.bot.connectivity.pings
|
||||
pings: props.bot.connectivity.pings,
|
||||
env,
|
||||
};
|
||||
}
|
||||
/** Time at which the app gives up and asks the user to refresh */
|
||||
|
@ -147,6 +152,7 @@ export class RawApp extends React.Component<AppProps, {}> {
|
|||
xySwap={this.props.xySwap}
|
||||
arduinoBusy={!!this.props.bot.hardware.informational_settings.busy}
|
||||
botOnline={isBotOnline(sync_status, getStatus(bot2mqtt))}
|
||||
env={this.props.env}
|
||||
stepSize={this.props.bot.stepSize} />}
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { AuthState } from "./interfaces";
|
|||
import { generateReducer } from "../redux/generate_reducer";
|
||||
import { Actions } from "../constants";
|
||||
|
||||
export let authReducer = generateReducer<AuthState | undefined>(undefined)
|
||||
export const authReducer = generateReducer<AuthState | undefined>(undefined)
|
||||
.add<AuthState>(Actions.REPLACE_TOKEN, (_, { payload }) => {
|
||||
return payload;
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ const initialState: ConfigState = {
|
|||
port: API.inferPort()
|
||||
};
|
||||
|
||||
export let configReducer = generateReducer<ConfigState>(initialState)
|
||||
export const configReducer = generateReducer<ConfigState>(initialState)
|
||||
.add<ChangeApiPort>(Actions.CHANGE_API_PORT, (s, { payload }) => {
|
||||
s.port = payload.port.replace(/\D/g, "");
|
||||
return s;
|
||||
|
|
|
@ -14,4 +14,4 @@ const change = (state: "up" | "down") =>
|
|||
|
||||
export const networkUp = change("up");
|
||||
|
||||
export let networkDown = change("down");
|
||||
export const networkDown = change("down");
|
||||
|
|
|
@ -202,6 +202,6 @@ export const attachEventListeners =
|
|||
};
|
||||
|
||||
/** Connect to MQTT and attach all relevant event handlers. */
|
||||
export let connectDevice = (token: AuthState) =>
|
||||
export const connectDevice = (token: AuthState) =>
|
||||
(dispatch: Function, getState: GetState) => fetchNewDevice(token)
|
||||
.then(bot => attachEventListeners(bot, dispatch, getState), onOffline);
|
||||
|
|
|
@ -37,13 +37,13 @@ export const dispatchQosStart = (id: string) => {
|
|||
});
|
||||
};
|
||||
|
||||
export let dispatchNetworkUp = (edge: Edge, at: number) => {
|
||||
export const dispatchNetworkUp = (edge: Edge, at: number) => {
|
||||
if (shouldThrottle(edge, at)) { return; }
|
||||
store.dispatch(networkUp(edge, at));
|
||||
bumpThrottle(edge, at);
|
||||
};
|
||||
|
||||
export let dispatchNetworkDown = (edge: Edge, at: number) => {
|
||||
export const dispatchNetworkDown = (edge: Edge, at: number) => {
|
||||
if (shouldThrottle(edge, at)) { return; }
|
||||
store.dispatch(networkDown(edge, at));
|
||||
bumpThrottle(edge, at);
|
||||
|
|
|
@ -17,7 +17,7 @@ export const DEFAULT_STATE: ConnectionState = {
|
|||
};
|
||||
export type PingResultPayload = { id: string, at: number };
|
||||
|
||||
export let connectivityReducer =
|
||||
export const connectivityReducer =
|
||||
generateReducer<ConnectionState>(DEFAULT_STATE)
|
||||
.add<{ id: string }>(Actions.PING_START, (s, { payload }) => {
|
||||
return { ...s, pings: startPing(s.pings, payload.id) };
|
||||
|
|
|
@ -2,5 +2,5 @@ import { throttle } from "lodash";
|
|||
|
||||
/** Too many status updates === too many screen redraws. */
|
||||
export const slowDown =
|
||||
(fn: (...args: unknown[]) => unknown) =>
|
||||
<Returns, Args, Fn extends (u: Args) => Returns>(fn: Fn) =>
|
||||
throttle(fn, 600, { leading: false, trailing: true });
|
||||
|
|
|
@ -287,6 +287,9 @@ export namespace ToolTips {
|
|||
export const EMERGENCY_LOCK =
|
||||
trim(`Stops a device from moving until it is unlocked by a user.`);
|
||||
|
||||
export const SELECT_A_CAMERA =
|
||||
trim(`Select a camera on the Device page to take photos.`);
|
||||
|
||||
export const MARK_AS =
|
||||
trim(`The Mark As step allows FarmBot to programmatically edit the
|
||||
properties of the UTM, plants, and weeds from within a sequence.
|
||||
|
@ -787,6 +790,10 @@ export namespace Content {
|
|||
export const NO_TOOLS =
|
||||
trim(`Press "+" to add a new tool.`);
|
||||
|
||||
export const MOUNTED_TOOL =
|
||||
trim(`The tool currently mounted to the UTM can be set here or by using
|
||||
a MARK AS step in a sequence.`);
|
||||
|
||||
// Farm Events
|
||||
export const NOTHING_SCHEDULED =
|
||||
trim(`Press "+" to schedule an event.`);
|
||||
|
@ -827,6 +834,9 @@ export namespace Content {
|
|||
|
||||
export const NOT_AVAILABLE_WHEN_OFFLINE =
|
||||
trim(`Not available when device is offline.`);
|
||||
|
||||
export const NO_CAMERA_SELECTED =
|
||||
trim(`No camera selected`);
|
||||
}
|
||||
|
||||
export namespace TourContent {
|
||||
|
@ -978,6 +988,7 @@ export enum Actions {
|
|||
TOGGLE_HOVERED_PLANT = "TOGGLE_HOVERED_PLANT",
|
||||
TOGGLE_HOVERED_POINT = "TOGGLE_HOVERED_POINT",
|
||||
HOVER_PLANT_LIST_ITEM = "HOVER_PLANT_LIST_ITEM",
|
||||
HOVER_TOOL_SLOT = "HOVER_TOOL_SLOT",
|
||||
OF_SEARCH_RESULTS_START = "OF_SEARCH_RESULTS_START",
|
||||
OF_SEARCH_RESULTS_OK = "OF_SEARCH_RESULTS_OK",
|
||||
OF_SEARCH_RESULTS_NO = "OF_SEARCH_RESULTS_NO",
|
||||
|
@ -1016,5 +1027,11 @@ export enum Actions {
|
|||
SET_CONSISTENCY = "SET_CONSISTENCY",
|
||||
PING_START = "PING_START",
|
||||
PING_OK = "PING_OK",
|
||||
PING_NO = "PING_NO"
|
||||
PING_NO = "PING_NO",
|
||||
|
||||
// Sequence Folders
|
||||
FOLDER_TOGGLE = "FOLDER_TOGGLE",
|
||||
FOLDER_TOGGLE_ALL = "FOLDER_TOGGLE_ALL",
|
||||
FOLDER_TOGGLE_EDIT = "FOLDER_TOGGLE_EDIT",
|
||||
FOLDER_SEARCH = "FOLDER_SEARCH"
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ describe("<Controls />", () => {
|
|||
getWebAppConfigVal: jest.fn((key) => (mockConfig[key])),
|
||||
sensorReadings: [],
|
||||
timeSettings: fakeTimeSettings(),
|
||||
env: {},
|
||||
});
|
||||
|
||||
it("shows webcam widget", () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { mapStateToProps } from "../state_to_props";
|
||||
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
|
||||
import { fakeUser } from "../../__test_support__/fake_state/resources";
|
||||
import { fakeUser, fakeFarmwareEnv } from "../../__test_support__/fake_state/resources";
|
||||
import { fakeState } from "../../__test_support__/fake_state";
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
|
@ -10,4 +10,14 @@ describe("mapStateToProps()", () => {
|
|||
const result = mapStateToProps(state);
|
||||
expect(result.timeSettings).toEqual({ utcOffset: 0, hour24: false });
|
||||
});
|
||||
|
||||
it("returns api props", () => {
|
||||
const state = fakeState();
|
||||
const fakeEnv = fakeFarmwareEnv();
|
||||
state.resources = buildResourceIndex([fakeEnv]);
|
||||
state.bot.minOsFeatureData = { api_farmware_env: "8.0.0" };
|
||||
state.bot.hardware.informational_settings.controller_version = "8.0.0";
|
||||
const result = mapStateToProps(state);
|
||||
expect(result.env).toEqual({ [fakeEnv.body.key]: fakeEnv.body.value });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@ const Axis = ({ val }: { val: number | undefined }) => <Col xs={3}>
|
|||
<input disabled value={isNumber(val) ? val : "---"} />
|
||||
</Col>;
|
||||
|
||||
export let AxisDisplayGroup = ({ position, label }: AxisDisplayGroupProps) => {
|
||||
export const AxisDisplayGroup = ({ position, label }: AxisDisplayGroupProps) => {
|
||||
const { x, y, z } = position;
|
||||
return <Row>
|
||||
<Axis val={x} />
|
||||
|
|
|
@ -3,7 +3,7 @@ import { AxisInputBoxProps } from "./interfaces";
|
|||
import { Col, BlurableInput } from "../ui/index";
|
||||
import { isUndefined } from "lodash";
|
||||
|
||||
export let AxisInputBox = ({ onChange, value, axis }: AxisInputBoxProps) => {
|
||||
export const AxisInputBox = ({ onChange, value, axis }: AxisInputBoxProps) => {
|
||||
return <Col xs={3}>
|
||||
<BlurableInput
|
||||
value={(isUndefined(value) ? "" : value)}
|
||||
|
|
|
@ -29,6 +29,7 @@ export class RawControls extends React.Component<Props, {}> {
|
|||
|
||||
move = () => <Move
|
||||
bot={this.props.bot}
|
||||
env={this.props.env}
|
||||
dispatch={this.props.dispatch}
|
||||
arduinoBusy={this.arduinoBusy}
|
||||
botToMqttStatus={this.props.botToMqttStatus}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { BotState, Xyz, BotPosition, ShouldDisplay } from "../devices/interfaces";
|
||||
import {
|
||||
BotState, Xyz, BotPosition, ShouldDisplay, UserEnv
|
||||
} from "../devices/interfaces";
|
||||
import { Vector3, McuParams } from "farmbot/dist";
|
||||
import {
|
||||
TaggedWebcamFeed,
|
||||
|
@ -22,6 +24,7 @@ export interface Props {
|
|||
getWebAppConfigVal: GetWebAppConfigValue;
|
||||
sensorReadings: TaggedSensorReading[];
|
||||
timeSettings: TimeSettings;
|
||||
env: UserEnv;
|
||||
}
|
||||
|
||||
export interface AxisDisplayGroupProps {
|
||||
|
|
|
@ -15,6 +15,8 @@ import { mount } from "enzyme";
|
|||
import { JogButtons } from "../jog_buttons";
|
||||
import { JogMovementControlsProps } from "../interfaces";
|
||||
import { bot } from "../../../__test_support__/fake_state/bot";
|
||||
import { error } from "../../../toast/toast";
|
||||
import { Content, ToolTips } from "../../../constants";
|
||||
|
||||
describe("<JogButtons/>", function () {
|
||||
const jogButtonProps = (): JogMovementControlsProps => {
|
||||
|
@ -26,6 +28,7 @@ describe("<JogButtons/>", function () {
|
|||
firmwareSettings: bot.hardware.mcu_params,
|
||||
xySwap: false,
|
||||
doFindHome: false,
|
||||
env: {},
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -60,8 +63,12 @@ describe("<JogButtons/>", function () {
|
|||
|
||||
it("takes photo", () => {
|
||||
const jogButtons = mount(<JogButtons {...jogButtonProps()} />);
|
||||
jogButtons.find("button").at(0).simulate("click");
|
||||
const cameraBtn = jogButtons.find("button").at(0);
|
||||
expect(cameraBtn.props().title).not.toEqual(Content.NO_CAMERA_SELECTED);
|
||||
|
||||
cameraBtn.simulate("click");
|
||||
expect(mockDevice.takePhoto).toHaveBeenCalled();
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("error taking photo", () => {
|
||||
|
@ -71,6 +78,18 @@ describe("<JogButtons/>", function () {
|
|||
expect(mockDevice.takePhoto).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows camera as disabled", () => {
|
||||
const p = jogButtonProps();
|
||||
p.env = { camera: "NONE" };
|
||||
const jogButtons = mount(<JogButtons {...p} />);
|
||||
const cameraBtn = jogButtons.find("button").at(0);
|
||||
expect(cameraBtn.props().title).toEqual(Content.NO_CAMERA_SELECTED);
|
||||
cameraBtn.simulate("click");
|
||||
expect(error).toHaveBeenCalledWith(
|
||||
ToolTips.SELECT_A_CAMERA, Content.NO_CAMERA_SELECTED);
|
||||
expect(mockDevice.takePhoto).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("has unswapped xy jog buttons", () => {
|
||||
const jogButtons = mount(<JogButtons {...jogButtonProps()} />);
|
||||
const button = jogButtons.find("button").at(6);
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
const mockDevice = {
|
||||
moveAbsolute: jest.fn(() => { return Promise.resolve(); }),
|
||||
};
|
||||
const mockDevice = { 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()
|
||||
};
|
||||
});
|
||||
jest.mock("../../../account/dev/dev_support", () => ({
|
||||
DevSettings: {
|
||||
futureFeaturesEnabled: () => false,
|
||||
}
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
|
@ -26,16 +25,15 @@ import { clickButton } from "../../../__test_support__/helpers";
|
|||
describe("<Move />", () => {
|
||||
const mockConfig: Dictionary<boolean> = {};
|
||||
|
||||
function fakeProps(): MoveProps {
|
||||
return {
|
||||
dispatch: jest.fn(),
|
||||
bot: bot,
|
||||
arduinoBusy: false,
|
||||
botToMqttStatus: "up",
|
||||
firmwareSettings: bot.hardware.mcu_params,
|
||||
getWebAppConfigVal: jest.fn((key) => (mockConfig[key])),
|
||||
};
|
||||
}
|
||||
const fakeProps = (): MoveProps => ({
|
||||
dispatch: jest.fn(),
|
||||
bot: bot,
|
||||
arduinoBusy: false,
|
||||
botToMqttStatus: "up",
|
||||
firmwareSettings: bot.hardware.mcu_params,
|
||||
getWebAppConfigVal: jest.fn((key) => (mockConfig[key])),
|
||||
env: {},
|
||||
});
|
||||
|
||||
it("has default elements", () => {
|
||||
const wrapper = mount(<Move {...fakeProps()} />);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { BotPosition, BotState } from "../../devices/interfaces";
|
||||
import { BotPosition, BotState, UserEnv } from "../../devices/interfaces";
|
||||
import { McuParams, Xyz } from "farmbot";
|
||||
import { NetworkState } from "../../connectivity/interfaces";
|
||||
import { GetWebAppConfigValue } from "../../config_storage/actions";
|
||||
|
@ -14,6 +14,7 @@ export interface MoveProps {
|
|||
botToMqttStatus: NetworkState;
|
||||
firmwareSettings: McuParams;
|
||||
getWebAppConfigVal: GetWebAppConfigValue;
|
||||
env: UserEnv;
|
||||
}
|
||||
|
||||
export interface DirectionButtonProps {
|
||||
|
@ -47,6 +48,7 @@ interface JogMovementControlsBaseProps extends DirectionAxesProps {
|
|||
stepSize: number;
|
||||
arduinoBusy: boolean;
|
||||
xySwap: boolean;
|
||||
env: UserEnv;
|
||||
}
|
||||
|
||||
export interface JogMovementControlsProps extends JogMovementControlsBaseProps {
|
||||
|
|
|
@ -5,6 +5,9 @@ import { JogMovementControlsProps } from "./interfaces";
|
|||
import { getDevice } from "../../device";
|
||||
import { buildDirectionProps } from "./direction_axes_props";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import {
|
||||
cameraBtnProps
|
||||
} from "../../devices/components/fbos_settings/camera_selection";
|
||||
const DEFAULT_STEP_SIZE = 100;
|
||||
|
||||
/*
|
||||
|
@ -20,35 +23,37 @@ export function JogButtons(props: JogMovementControlsProps) {
|
|||
const directionAxesProps = buildDirectionProps(props);
|
||||
const rightLeft = xySwap ? "y" : "x";
|
||||
const upDown = xySwap ? "x" : "y";
|
||||
|
||||
const commonProps = {
|
||||
steps: stepSize || DEFAULT_STEP_SIZE,
|
||||
disabled: arduinoBusy
|
||||
};
|
||||
const camDisabled = cameraBtnProps(props.env);
|
||||
return <table className="jog-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<button
|
||||
className="i fa fa-camera arrow-button fb-button"
|
||||
title={t("Take a photo")}
|
||||
onClick={() => getDevice().takePhoto().catch(() => { })} />
|
||||
className={`fa fa-camera arrow-button fb-button ${
|
||||
camDisabled.class}`}
|
||||
title={camDisabled.title || t("Take a photo")}
|
||||
onClick={camDisabled.click ||
|
||||
(() => getDevice().takePhoto().catch(() => { }))} />
|
||||
</td>
|
||||
<td />
|
||||
<td />
|
||||
<td>
|
||||
<DirectionButton
|
||||
<DirectionButton {...commonProps}
|
||||
axis={upDown}
|
||||
direction="up"
|
||||
directionAxisProps={directionAxesProps[upDown]}
|
||||
steps={stepSize || DEFAULT_STEP_SIZE}
|
||||
disabled={arduinoBusy} />
|
||||
directionAxisProps={directionAxesProps[upDown]} />
|
||||
</td>
|
||||
<td />
|
||||
<td />
|
||||
<td>
|
||||
<DirectionButton
|
||||
<DirectionButton {...commonProps}
|
||||
axis="z"
|
||||
direction="up"
|
||||
directionAxisProps={directionAxesProps.z}
|
||||
steps={stepSize || DEFAULT_STEP_SIZE}
|
||||
disabled={arduinoBusy} />
|
||||
directionAxisProps={directionAxesProps.z} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -61,37 +66,29 @@ export function JogButtons(props: JogMovementControlsProps) {
|
|||
</td>
|
||||
<td />
|
||||
<td>
|
||||
<DirectionButton
|
||||
<DirectionButton {...commonProps}
|
||||
axis={rightLeft}
|
||||
direction="left"
|
||||
directionAxisProps={directionAxesProps[rightLeft]}
|
||||
steps={stepSize || DEFAULT_STEP_SIZE}
|
||||
disabled={arduinoBusy} />
|
||||
directionAxisProps={directionAxesProps[rightLeft]} />
|
||||
</td>
|
||||
<td>
|
||||
<DirectionButton
|
||||
<DirectionButton {...commonProps}
|
||||
axis={upDown}
|
||||
direction="down"
|
||||
directionAxisProps={directionAxesProps[upDown]}
|
||||
steps={stepSize || DEFAULT_STEP_SIZE}
|
||||
disabled={arduinoBusy} />
|
||||
directionAxisProps={directionAxesProps[upDown]} />
|
||||
</td>
|
||||
<td>
|
||||
<DirectionButton
|
||||
<DirectionButton {...commonProps}
|
||||
axis={rightLeft}
|
||||
direction="right"
|
||||
directionAxisProps={directionAxesProps[rightLeft]}
|
||||
steps={stepSize || DEFAULT_STEP_SIZE}
|
||||
disabled={arduinoBusy} />
|
||||
directionAxisProps={directionAxesProps[rightLeft]} />
|
||||
</td>
|
||||
<td />
|
||||
<td>
|
||||
<DirectionButton
|
||||
<DirectionButton {...commonProps}
|
||||
axis="z"
|
||||
direction="down"
|
||||
directionAxisProps={directionAxesProps.z}
|
||||
steps={stepSize || DEFAULT_STEP_SIZE}
|
||||
disabled={arduinoBusy} />
|
||||
directionAxisProps={directionAxesProps.z} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react";
|
||||
import { McuParams } from "farmbot";
|
||||
import { BotPosition } from "../../devices/interfaces";
|
||||
import { BotPosition, UserEnv } from "../../devices/interfaces";
|
||||
import { changeStepSize } from "../../devices/actions";
|
||||
import { StepSizeSelector } from "./step_size_selector";
|
||||
import { GetWebAppBool } from "./interfaces";
|
||||
|
@ -15,6 +15,7 @@ interface JogControlsGroupProps {
|
|||
getValue: GetWebAppBool;
|
||||
arduinoBusy: boolean;
|
||||
firmwareSettings: McuParams;
|
||||
env: UserEnv;
|
||||
}
|
||||
|
||||
export const JogControlsGroup = (props: JogControlsGroupProps) => {
|
||||
|
@ -38,6 +39,7 @@ export const JogControlsGroup = (props: JogControlsGroupProps) => {
|
|||
z: getValue(BooleanSetting.z_axis_inverted)
|
||||
}}
|
||||
arduinoBusy={arduinoBusy}
|
||||
env={props.env}
|
||||
firmwareSettings={firmwareSettings}
|
||||
xySwap={getValue(BooleanSetting.xy_swap)}
|
||||
doFindHome={getValue(BooleanSetting.home_button_homing)} />
|
||||
|
|
|
@ -47,6 +47,7 @@ export class Move extends React.Component<MoveProps, {}> {
|
|||
botPosition={locationData.position}
|
||||
getValue={this.getValue}
|
||||
arduinoBusy={this.props.arduinoBusy}
|
||||
env={this.props.env}
|
||||
firmwareSettings={this.props.firmwareSettings} />
|
||||
<BotPositionRows
|
||||
locationData={locationData}
|
||||
|
|
|
@ -3,31 +3,23 @@ import {
|
|||
selectAllPeripherals,
|
||||
selectAllWebcamFeeds,
|
||||
selectAllSensors,
|
||||
maybeGetDevice,
|
||||
selectAllSensorReadings,
|
||||
maybeGetTimeSettings
|
||||
} from "../resources/selectors";
|
||||
import { Props } from "./interfaces";
|
||||
import {
|
||||
validFwConfig,
|
||||
createShouldDisplayFn as shouldDisplayFunc,
|
||||
determineInstalledOsVersion
|
||||
} from "../util";
|
||||
import { validFwConfig } from "../util";
|
||||
import { getWebAppConfigValue } from "../config_storage/actions";
|
||||
import { getFirmwareConfig } from "../resources/getters";
|
||||
import { uniq } from "lodash";
|
||||
import { getStatus } from "../connectivity/reducer_support";
|
||||
import { DevSettings } from "../account/dev/dev_support";
|
||||
import { getEnv, getShouldDisplayFn } from "../farmware/state_to_props";
|
||||
|
||||
export function mapStateToProps(props: Everything): Props {
|
||||
const fwConfig = validFwConfig(getFirmwareConfig(props.resources.index));
|
||||
const { mcu_params } = props.bot.hardware;
|
||||
|
||||
const device = maybeGetDevice(props.resources.index);
|
||||
const installedOsVersion = determineInstalledOsVersion(props.bot, device);
|
||||
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
|
||||
const shouldDisplay = shouldDisplayFunc(
|
||||
installedOsVersion, props.bot.minOsFeatureData, fbosVersionOverride);
|
||||
const shouldDisplay = getShouldDisplayFn(props.resources.index, props.bot);
|
||||
const env = getEnv(props.resources.index, shouldDisplay, props.bot);
|
||||
|
||||
return {
|
||||
feeds: selectAllWebcamFeeds(props.resources.index),
|
||||
|
@ -41,5 +33,6 @@ export function mapStateToProps(props: Everything): Props {
|
|||
shouldDisplay,
|
||||
sensorReadings: selectAllSensorReadings(props.resources.index),
|
||||
timeSettings: maybeGetTimeSettings(props.resources.index),
|
||||
env,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,6 +5,10 @@ import { buildDirectionProps } from "./controls/move/direction_axes_props";
|
|||
import { ControlsPopupProps } from "./controls/move/interfaces";
|
||||
import { commandErr } from "./devices/actions";
|
||||
import { mapPanelClassName } from "./farm_designer/map/util";
|
||||
import {
|
||||
cameraBtnProps
|
||||
} from "./devices/components/fbos_settings/camera_selection";
|
||||
import { t } from "./i18next_wrapper";
|
||||
|
||||
interface State {
|
||||
isOpen: boolean;
|
||||
|
@ -23,40 +27,38 @@ export class ControlsPopup
|
|||
const directionAxesProps = buildDirectionProps(this.props);
|
||||
const rightLeft = xySwap ? "y" : "x";
|
||||
const upDown = xySwap ? "x" : "y";
|
||||
const movementDisabled = !isOpen || arduinoBusy || !botOnline;
|
||||
const commonProps = { steps: stepSize, disabled: movementDisabled };
|
||||
const camDisabled = cameraBtnProps(this.props.env);
|
||||
return <div
|
||||
className={`controls-popup ${isOpen} ${mapPanelClassName()}`}>
|
||||
<i className="fa fa-crosshairs"
|
||||
onClick={this.toggle("isOpen")} />
|
||||
<div className="controls-popup-menu-outer">
|
||||
<div className="controls-popup-menu-inner">
|
||||
<DirectionButton
|
||||
<DirectionButton {...commonProps}
|
||||
axis={rightLeft}
|
||||
direction="right"
|
||||
directionAxisProps={directionAxesProps[rightLeft]}
|
||||
steps={stepSize}
|
||||
disabled={!isOpen || arduinoBusy || !botOnline} />
|
||||
<DirectionButton
|
||||
directionAxisProps={directionAxesProps[rightLeft]} />
|
||||
<DirectionButton {...commonProps}
|
||||
axis={upDown}
|
||||
direction="up"
|
||||
directionAxisProps={directionAxesProps[upDown]}
|
||||
steps={stepSize}
|
||||
disabled={!isOpen || arduinoBusy || !botOnline} />
|
||||
<DirectionButton
|
||||
directionAxisProps={directionAxesProps[upDown]} />
|
||||
<DirectionButton {...commonProps}
|
||||
axis={upDown}
|
||||
direction="down"
|
||||
directionAxisProps={directionAxesProps[upDown]}
|
||||
steps={stepSize}
|
||||
disabled={!isOpen || arduinoBusy || !botOnline} />
|
||||
<DirectionButton
|
||||
directionAxisProps={directionAxesProps[upDown]} />
|
||||
<DirectionButton {...commonProps}
|
||||
axis={rightLeft}
|
||||
direction="left"
|
||||
directionAxisProps={directionAxesProps[rightLeft]}
|
||||
steps={stepSize}
|
||||
disabled={!isOpen || arduinoBusy || !botOnline} />
|
||||
directionAxisProps={directionAxesProps[rightLeft]} />
|
||||
<button
|
||||
className="i fa fa-camera arrow-button fb-button brown"
|
||||
disabled={!botOnline}
|
||||
onClick={() => getDevice().takePhoto().catch(commandErr("Photo"))} />
|
||||
className={`fa fa-camera arrow-button fb-button brown ${
|
||||
camDisabled.class}`}
|
||||
disabled={!isOpen || !botOnline}
|
||||
title={camDisabled.title || t("Take a photo")}
|
||||
onClick={camDisabled.click ||
|
||||
(() => getDevice().takePhoto().catch(commandErr("Photo")))} />
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
|
|
|
@ -279,6 +279,7 @@
|
|||
&.pseudo-disabled {
|
||||
background: $medium_light_gray !important;
|
||||
box-shadow: 0 2px 0px 0px lighten($medium_light_gray, 5%) !important;
|
||||
border-bottom: none !important;
|
||||
&:focus,
|
||||
&:hover,
|
||||
&.active {
|
||||
|
|
|
@ -3,6 +3,7 @@ $translucent: rgba(0, 0, 0, 0.2);
|
|||
$translucent2: rgba(0, 0, 0, 0.6);
|
||||
$white: #fff;
|
||||
$off_white: #f4f4f4;
|
||||
$lighter_gray: #eee;
|
||||
$light_gray: #ddd;
|
||||
$gray: #ccc;
|
||||
$medium_light_gray: #bcbcbc;
|
||||
|
@ -129,9 +130,36 @@ $panel_light_red: #fff7f6;
|
|||
background: $blue !important;
|
||||
}
|
||||
|
||||
|
||||
.dark-blue,
|
||||
.fun,
|
||||
.saucer-fun {
|
||||
background: $dark_blue !important;
|
||||
}
|
||||
|
||||
.icon-saucer {
|
||||
background: none !important;
|
||||
&.blue {
|
||||
color: $blue;
|
||||
}
|
||||
&.green {
|
||||
color: $green;
|
||||
}
|
||||
&.yellow {
|
||||
color: $yellow;
|
||||
}
|
||||
&.orange {
|
||||
color: $orange;
|
||||
}
|
||||
&.purple {
|
||||
color: $purple;
|
||||
}
|
||||
&.pink {
|
||||
color: $pink;
|
||||
}
|
||||
&.gray {
|
||||
color: $gray;
|
||||
}
|
||||
&.red {
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -394,7 +394,7 @@
|
|||
a {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
i {
|
||||
i:not(.fa-stack-2x) {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
@ -509,9 +509,125 @@
|
|||
}
|
||||
}
|
||||
|
||||
.tools-panel-content {
|
||||
.tool-slots-panel,
|
||||
.tools-panel {
|
||||
.panel-top {
|
||||
display: flex;
|
||||
margin-top: 5rem;
|
||||
}
|
||||
.tool-slots-panel-content,
|
||||
.tools-panel-content {
|
||||
.tool-search-item,
|
||||
.tool-slot-search-item {
|
||||
cursor: pointer;
|
||||
margin-left: -15px;
|
||||
margin-right: -15px;
|
||||
.row {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
p {
|
||||
line-height: 3rem;
|
||||
}
|
||||
}
|
||||
.mounted-tool-header {
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
label {
|
||||
margin: 0;
|
||||
}
|
||||
.help-icon {
|
||||
margin-left: 1rem;
|
||||
vertical-align: top;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
.tool-slots-header {
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
label {
|
||||
margin: 0;
|
||||
line-height: 2.1rem;
|
||||
}
|
||||
a {
|
||||
margin-left: auto;
|
||||
}
|
||||
.fa-plus {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
button:not(.bp3-button) {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
float: none;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.tool-verification-status {
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
button {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-tool-panel-content,
|
||||
.edit-tool-panel-content {
|
||||
button {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
float: none;
|
||||
margin-top: 1rem;
|
||||
&.red {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
.add-stock-tools {
|
||||
ul {
|
||||
font-size: 1.2rem;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
button {
|
||||
.fa-plus {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-tool-slot-panel-content,
|
||||
.edit-tool-slot-panel-content {
|
||||
label {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
.row, fieldset {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
fieldset button {
|
||||
margin: 0;
|
||||
}
|
||||
.direction-icon {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.use-current-location-input {
|
||||
button {
|
||||
margin: 0;
|
||||
float: none;
|
||||
margin-left: 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
.gantry-mounted-input {
|
||||
label {
|
||||
margin-top: 0;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
float: left;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,10 @@ body {
|
|||
width: 13rem;
|
||||
background: $dark_gray;
|
||||
}
|
||||
div {
|
||||
.bp3-popover-content,
|
||||
.color-picker-cluster,
|
||||
.color-picker-item-wrapper,
|
||||
.saucer {
|
||||
display: inline-block;
|
||||
padding: 0.4rem;
|
||||
}
|
||||
|
@ -113,6 +116,21 @@ fieldset {
|
|||
}
|
||||
}
|
||||
|
||||
.icon-saucer {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
color: $dark_gray;
|
||||
cursor: pointer;
|
||||
&.active {
|
||||
border: 2px solid white;
|
||||
}
|
||||
&.hover {
|
||||
border: 2px solid $dark_gray;
|
||||
}
|
||||
}
|
||||
|
||||
.saucer-connector {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
|
|
@ -160,17 +160,17 @@
|
|||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.step-button-cluster,
|
||||
.sequence-list,
|
||||
.regimen-list {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: calc(100vh - 21rem);
|
||||
max-height: calc(100vh - 19rem);
|
||||
}
|
||||
|
||||
.step-button-cluster,
|
||||
.sequence-list {
|
||||
.step-button-cluster {
|
||||
margin-right: -15px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: calc(100vh - 16rem);
|
||||
}
|
||||
|
||||
.farmware-info-panel,
|
||||
|
@ -229,15 +229,230 @@
|
|||
}
|
||||
}
|
||||
|
||||
.sequence-list-item {
|
||||
margin-right: 15px;
|
||||
.folders-panel {
|
||||
margin-left: -30px;
|
||||
margin-right: -20px;
|
||||
@media screen and (max-width: 767px) {
|
||||
margin-left: -15px;
|
||||
}
|
||||
.non-empty-state {
|
||||
height: calc(100vh - 19rem);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.panel-top {
|
||||
margin-left: 1rem !important;
|
||||
button {
|
||||
margin-top: 0.7rem !important;
|
||||
}
|
||||
}
|
||||
.panel-top,
|
||||
.folder-button-cluster {
|
||||
i {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.fa-stack {
|
||||
font-size: 1rem;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
.fa-stack-2x {
|
||||
font-size: 2rem;
|
||||
}
|
||||
.fa-stack-1x {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.5rem;
|
||||
margin-left: 0.75rem;
|
||||
filter: drop-shadow(0 0 0.2rem $dark_green);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.folder-button-cluster {
|
||||
display: flex;
|
||||
i {
|
||||
width: 1.5rem !important;
|
||||
line-height: 2rem !important;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.folder-drop-area {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
transition: height 0.5s ease-out,
|
||||
padding-top 0.5s ease-out,
|
||||
padding-bottom 0.5s ease-out;
|
||||
transition-delay: 0.4s;
|
||||
color: $gray;
|
||||
font-weight: bold;
|
||||
background: $white;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
&.visible {
|
||||
transition: height 0.3s ease-in,
|
||||
padding-top 0.3s ease-in,
|
||||
padding-bottom 0.3s ease-in;
|
||||
transition-delay: 0.2s;
|
||||
height: 3rem;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
&:hover, &.hovered {
|
||||
color: $medium_gray;
|
||||
}
|
||||
}
|
||||
.folders {
|
||||
.folder > div:not(:first-child), ul {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
.folder-list-item,
|
||||
.sequence-list-item {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 3.5rem;
|
||||
border-bottom: 1px solid $light_gray;
|
||||
cursor: pointer;
|
||||
background: $lighter_gray;
|
||||
border-left: 4px solid transparent;
|
||||
&.active {
|
||||
border-left: 4px solid $dark_gray;
|
||||
}
|
||||
.fa-chevron-down, .fa-chevron-right {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
width: 3rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.folder-settings-icon,
|
||||
.fa-bars {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
.fa-bars, .fa-ellipsis-v {
|
||||
display: none;
|
||||
}
|
||||
.fa-ellipsis-v {
|
||||
&.open {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.fa-bars, .fa-ellipsis-v {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
i {
|
||||
margin: 0;
|
||||
line-height: 3.5rem;
|
||||
width: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
.saucer, .icon-saucer {
|
||||
position: absolute;
|
||||
margin: 0.5rem;
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
}
|
||||
a {
|
||||
width: 100%;
|
||||
}
|
||||
p {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
width: 75%;
|
||||
padding: 0.5rem;
|
||||
padding-left: 0;
|
||||
margin-left: 3rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
.folder-name {
|
||||
width: 100%;
|
||||
margin-right: 3rem;
|
||||
margin-left: 6rem;
|
||||
p {
|
||||
margin-left: 0;
|
||||
}
|
||||
.folder-name-input {
|
||||
display: flex;
|
||||
button {
|
||||
top: 0.5rem;
|
||||
width: auto;
|
||||
height: 2rem;
|
||||
i {
|
||||
line-height: 0;
|
||||
width: 1rem;
|
||||
}
|
||||
}
|
||||
.input {
|
||||
margin: 0.3rem;
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.sequence-list-item-icons {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
.fa-hdd-o {
|
||||
margin-right: 3rem;
|
||||
}
|
||||
}
|
||||
button {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
&.move-source {
|
||||
border: 1px solid $dark_gray;
|
||||
}
|
||||
&.move-target {
|
||||
background: $white;
|
||||
}
|
||||
}
|
||||
.folder-list-item,
|
||||
.sequence-list-item {
|
||||
.bp3-popover-wrapper.color-picker {
|
||||
position: absolute;
|
||||
line-height: 0;
|
||||
.bp3-popover-target {
|
||||
width: 2rem;
|
||||
height: 3.5rem;
|
||||
}
|
||||
}
|
||||
padding-left: 3rem;
|
||||
.saucer, .icon-saucer {
|
||||
position: relative;
|
||||
top: 0.55rem;
|
||||
margin: auto;
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
}
|
||||
.folder-list-item {
|
||||
padding-left: 0;
|
||||
.bp3-popover-wrapper.color-picker {
|
||||
margin-left: 3rem;
|
||||
}
|
||||
.color-picker {
|
||||
.icon-saucer {
|
||||
top: 0;
|
||||
margin-top: 0;
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.farmware-list-panel,
|
||||
.sequence-list-panel,
|
||||
.regimen-list-panel {
|
||||
padding-top: 0.4rem;
|
||||
margin-bottom: 3rem;
|
||||
margin-right: 5px;
|
||||
@media screen and (max-width: 1075px) {
|
||||
margin-left: 15px;
|
||||
|
@ -268,7 +483,15 @@
|
|||
}
|
||||
|
||||
.farmware-list-panel {
|
||||
margin-bottom: 5rem;
|
||||
.farmware-list-panel-contents {
|
||||
height: calc(100vh - 15rem);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin-right: -20px;
|
||||
margin-left: -15px;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
label {
|
||||
font-weight: bold;
|
||||
font-size: 1.4rem;
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import {
|
||||
fakeFbosConfig, fakeImage, fakeFarmwareEnv, fakeWebAppConfig
|
||||
fakeFbosConfig,
|
||||
fakeImage,
|
||||
fakeFarmwareEnv,
|
||||
fakeWebAppConfig
|
||||
} from "../../__test_support__/fake_state/resources";
|
||||
|
||||
let mockFbosConfig: TaggedFbosConfig | undefined = fakeFbosConfig();
|
||||
|
@ -12,6 +15,8 @@ jest.mock("../../resources/selectors_by_kind", () => ({
|
|||
selectAllRegimens: () => [],
|
||||
selectAllLogs: () => [],
|
||||
selectAllImages: () => [mockImages],
|
||||
selectAllFolders: () => [],
|
||||
selectAllSequences: () => [],
|
||||
selectAllFarmwareEnvs: () => [fakeFarmwareEnv()]
|
||||
}));
|
||||
|
||||
|
|
|
@ -245,7 +245,7 @@ function validMinOsFeatureLookup(x: MinOsFeatureLookup): boolean {
|
|||
* Fetch and save minimum FBOS version data for UI feature display.
|
||||
* @param url location of data
|
||||
*/
|
||||
export let fetchMinOsFeatureData = (url: string) =>
|
||||
export const fetchMinOsFeatureData = (url: string) =>
|
||||
(dispatch: Function) => {
|
||||
axios
|
||||
.get<MinOsFeatureLookup>(url)
|
||||
|
|
|
@ -9,6 +9,10 @@ describe("boardType()", () => {
|
|||
expect(boardType("5.0.3.G")).toEqual("farmduino_k14");
|
||||
});
|
||||
|
||||
it("returns Farmduino k1.5", () => {
|
||||
expect(boardType("5.0.3.H")).toEqual("farmduino_k15");
|
||||
});
|
||||
|
||||
it("returns Farmduino Express k1.0", () => {
|
||||
expect(boardType("5.0.3.E")).toEqual("express_k10");
|
||||
});
|
||||
|
|
|
@ -81,6 +81,7 @@ describe("<BoardType/>", () => {
|
|||
{ label: "Arduino/RAMPS (Genesis v1.2)", value: "arduino" },
|
||||
{ label: "Farmduino (Genesis v1.3)", value: "farmduino" },
|
||||
{ label: "Farmduino (Genesis v1.4)", value: "farmduino_k14" },
|
||||
{ label: "Farmduino (Genesis v1.5)", value: "farmduino_k15" },
|
||||
{ label: "Farmduino (Express v1.0)", value: "express_k10" },
|
||||
{ label: "None", value: "none" },
|
||||
]);
|
||||
|
|
|
@ -1,26 +1,20 @@
|
|||
const mockDevice = {
|
||||
setUserEnv: jest.fn(() => { return Promise.resolve(); }),
|
||||
};
|
||||
jest.mock("../../../../device", () => ({
|
||||
getDevice: () => mockDevice
|
||||
}));
|
||||
const mockDevice = { setUserEnv: jest.fn(() => Promise.resolve()) };
|
||||
jest.mock("../../../../device", () => ({ getDevice: () => mockDevice }));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { CameraSelection } from "../camera_selection";
|
||||
import { CameraSelection, cameraDisabled } from "../camera_selection";
|
||||
import { CameraSelectionProps } from "../interfaces";
|
||||
import { info, error } from "../../../../toast/toast";
|
||||
|
||||
describe("<CameraSelection/>", () => {
|
||||
const fakeProps = (): CameraSelectionProps => {
|
||||
return {
|
||||
env: {},
|
||||
botOnline: true,
|
||||
shouldDisplay: () => false,
|
||||
saveFarmwareEnv: jest.fn(),
|
||||
dispatch: jest.fn(),
|
||||
};
|
||||
};
|
||||
const fakeProps = (): CameraSelectionProps => ({
|
||||
env: {},
|
||||
botOnline: true,
|
||||
shouldDisplay: () => false,
|
||||
saveFarmwareEnv: jest.fn(),
|
||||
dispatch: jest.fn(),
|
||||
});
|
||||
|
||||
it("doesn't render camera", () => {
|
||||
const cameraSelection = mount(<CameraSelection {...fakeProps()} />);
|
||||
|
@ -66,3 +60,15 @@ describe("<CameraSelection/>", () => {
|
|||
expect(p.saveFarmwareEnv).toHaveBeenCalledWith("camera", "\"mycamera\"");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cameraDisabled()", () => {
|
||||
it("returns enabled", () => {
|
||||
expect(cameraDisabled({ camera: "USB" })).toEqual(false);
|
||||
expect(cameraDisabled({ camera: "" })).toEqual(false);
|
||||
});
|
||||
|
||||
it("returns disabled", () => {
|
||||
expect(cameraDisabled({ camera: "none" })).toEqual(true);
|
||||
expect(cameraDisabled({ camera: "\"NONE\"" })).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,25 +6,56 @@ import {
|
|||
import { info, success, error } from "../../../toast/toast";
|
||||
import { getDevice } from "../../../device";
|
||||
import { ColWidth } from "../farmbot_os_settings";
|
||||
import { Feature } from "../../interfaces";
|
||||
import { Feature, UserEnv } from "../../interfaces";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { Content, ToolTips } from "../../../constants";
|
||||
|
||||
/** Check if the camera has been disabled. */
|
||||
export const cameraDisabled = (env: UserEnv): boolean =>
|
||||
parseCameraSelection(env) === Camera.NONE;
|
||||
|
||||
/** `disabled` and `title` props for buttons with actions that use the camera. */
|
||||
export const cameraBtnProps = (env: UserEnv) => {
|
||||
const disabled = cameraDisabled(env);
|
||||
return disabled
|
||||
? {
|
||||
class: "pseudo-disabled",
|
||||
click: () =>
|
||||
error(t(ToolTips.SELECT_A_CAMERA), t(Content.NO_CAMERA_SELECTED)),
|
||||
title: t(Content.NO_CAMERA_SELECTED)
|
||||
}
|
||||
: { class: "", click: undefined, title: "" };
|
||||
};
|
||||
|
||||
enum Camera {
|
||||
USB = "USB",
|
||||
RPI = "RPI",
|
||||
NONE = "NONE",
|
||||
}
|
||||
|
||||
const parseCameraSelection = (env: UserEnv): Camera => {
|
||||
const camera = env["camera"]?.toUpperCase();
|
||||
if (camera?.includes(Camera.NONE)) {
|
||||
return Camera.NONE;
|
||||
} else if (camera?.includes(Camera.RPI)) {
|
||||
return Camera.RPI;
|
||||
} else {
|
||||
return Camera.USB;
|
||||
}
|
||||
};
|
||||
|
||||
const CAMERA_CHOICES = () => ([
|
||||
{ label: t("USB Camera"), value: "USB" },
|
||||
{ label: t("Raspberry Pi Camera"), value: "RPI" }
|
||||
{ label: t("USB Camera"), value: Camera.USB },
|
||||
{ label: t("Raspberry Pi Camera"), value: Camera.RPI },
|
||||
{ label: t("None"), value: Camera.NONE },
|
||||
]);
|
||||
|
||||
const CAMERA_CHOICES_DDI = () => {
|
||||
const CHOICES = CAMERA_CHOICES();
|
||||
return {
|
||||
[CHOICES[0].value]: {
|
||||
label: CHOICES[0].label,
|
||||
value: CHOICES[0].value
|
||||
},
|
||||
[CHOICES[1].value]: {
|
||||
label: CHOICES[1].label,
|
||||
value: CHOICES[1].value
|
||||
}
|
||||
[CHOICES[0].value]: { label: CHOICES[0].label, value: CHOICES[0].value },
|
||||
[CHOICES[1].value]: { label: CHOICES[1].label, value: CHOICES[1].value },
|
||||
[CHOICES[2].value]: { label: CHOICES[2].label, value: CHOICES[2].value },
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -35,12 +66,8 @@ export class CameraSelection
|
|||
cameraStatus: ""
|
||||
};
|
||||
|
||||
selectedCamera(): DropDownItem {
|
||||
const camera = this.props.env["camera"];
|
||||
return camera
|
||||
? CAMERA_CHOICES_DDI()[JSON.parse(camera)]
|
||||
: CAMERA_CHOICES_DDI()["USB"];
|
||||
}
|
||||
selectedCamera = (): DropDownItem =>
|
||||
CAMERA_CHOICES_DDI()[parseCameraSelection(this.props.env)]
|
||||
|
||||
sendOffConfig = (selectedCamera: DropDownItem) => {
|
||||
const { props } = this;
|
||||
|
|
|
@ -6,6 +6,7 @@ import { ColWidth } from "../farmbot_os_settings";
|
|||
import { FarmbotOsRowProps } from "./interfaces";
|
||||
import { FbosDetails } from "./fbos_details";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { ErrorBoundary } from "../../../error_boundary";
|
||||
|
||||
const getVersionString =
|
||||
(fbosVersion: string | undefined, onBeta: boolean | undefined): string => {
|
||||
|
@ -30,14 +31,16 @@ export function FarmbotOsRow(props: FarmbotOsRowProps) {
|
|||
<p>
|
||||
{t("Version {{ version }}", { version })}
|
||||
</p>
|
||||
<FbosDetails
|
||||
botInfoSettings={bot.hardware.informational_settings}
|
||||
dispatch={dispatch}
|
||||
shouldDisplay={props.shouldDisplay}
|
||||
sourceFbosConfig={sourceFbosConfig}
|
||||
botToMqttLastSeen={props.botToMqttLastSeen}
|
||||
timeSettings={props.timeSettings}
|
||||
deviceAccount={props.deviceAccount} />
|
||||
<ErrorBoundary>
|
||||
<FbosDetails
|
||||
botInfoSettings={bot.hardware.informational_settings}
|
||||
dispatch={dispatch}
|
||||
shouldDisplay={props.shouldDisplay}
|
||||
sourceFbosConfig={sourceFbosConfig}
|
||||
botToMqttLastSeen={props.botToMqttLastSeen}
|
||||
timeSettings={props.timeSettings}
|
||||
deviceAccount={props.deviceAccount} />
|
||||
</ErrorBoundary>
|
||||
</Popover>
|
||||
</Col>
|
||||
<Col xs={3}>
|
||||
|
|
|
@ -2,8 +2,12 @@ import { FirmwareHardware } from "farmbot";
|
|||
import { ShouldDisplay, Feature } from "../interfaces";
|
||||
|
||||
export const isFwHardwareValue = (x?: unknown): x is FirmwareHardware => {
|
||||
const values: FirmwareHardware[] =
|
||||
["arduino", "farmduino", "farmduino_k14", "express_k10", "none"];
|
||||
const values: FirmwareHardware[] = [
|
||||
"arduino",
|
||||
"farmduino", "farmduino_k14", "farmduino_k15",
|
||||
"express_k10",
|
||||
"none"
|
||||
];
|
||||
return !!values.includes(x as FirmwareHardware);
|
||||
};
|
||||
|
||||
|
@ -13,7 +17,7 @@ export const getBoardIdentifier =
|
|||
|
||||
export const isKnownBoard = (firmwareVersion: string | undefined): boolean => {
|
||||
const boardIdentifier = getBoardIdentifier(firmwareVersion);
|
||||
return ["R", "F", "G", "E"].includes(boardIdentifier);
|
||||
return ["R", "F", "G", "H", "E"].includes(boardIdentifier);
|
||||
};
|
||||
|
||||
export const getBoardCategory =
|
||||
|
@ -33,6 +37,8 @@ export const boardType =
|
|||
return "farmduino";
|
||||
case "G":
|
||||
return "farmduino_k14";
|
||||
case "H":
|
||||
return "farmduino_k15";
|
||||
case "E":
|
||||
return "express_k10";
|
||||
default:
|
||||
|
@ -45,6 +51,9 @@ const FARMDUINO = { label: "Farmduino (Genesis v1.3)", value: "farmduino" };
|
|||
const FARMDUINO_K14 = {
|
||||
label: "Farmduino (Genesis v1.4)", value: "farmduino_k14"
|
||||
};
|
||||
const FARMDUINO_K15 = {
|
||||
label: "Farmduino (Genesis v1.5)", value: "farmduino_k15"
|
||||
};
|
||||
const EXPRESS_K10 = {
|
||||
label: "Farmduino (Express v1.0)", value: "express_k10"
|
||||
};
|
||||
|
@ -54,6 +63,7 @@ export const FIRMWARE_CHOICES_DDI = {
|
|||
[ARDUINO.value]: ARDUINO,
|
||||
[FARMDUINO.value]: FARMDUINO,
|
||||
[FARMDUINO_K14.value]: FARMDUINO_K14,
|
||||
[FARMDUINO_K15.value]: FARMDUINO_K15,
|
||||
[EXPRESS_K10.value]: EXPRESS_K10,
|
||||
[NONE.value]: NONE
|
||||
};
|
||||
|
@ -63,6 +73,7 @@ export const getFirmwareChoices =
|
|||
ARDUINO,
|
||||
FARMDUINO,
|
||||
FARMDUINO_K14,
|
||||
...(shouldDisplay(Feature.farmduino_k15) ? [FARMDUINO_K15] : []),
|
||||
...(shouldDisplay(Feature.express_k10) ? [EXPRESS_K10] : []),
|
||||
...(shouldDisplay(Feature.none_firmware) ? [NONE] : []),
|
||||
]);
|
||||
|
|
|
@ -10,7 +10,7 @@ interface Props {
|
|||
expanded: boolean;
|
||||
}
|
||||
|
||||
export let Header = (props: Props) => {
|
||||
export const Header = (props: Props) => {
|
||||
const { dispatch, name, title, expanded } = props;
|
||||
return <ExpandableHeader
|
||||
expanded={expanded}
|
||||
|
|
|
@ -78,6 +78,7 @@ export enum Feature {
|
|||
endstop_invert = "endstop_invert",
|
||||
express_k10 = "express_k10",
|
||||
farmduino_k14 = "farmduino_k14",
|
||||
farmduino_k15 = "farmduino_k15",
|
||||
firmware_restart = "firmware_restart",
|
||||
flash_firmware = "flash_firmware",
|
||||
groups = "groups",
|
||||
|
@ -209,8 +210,8 @@ export interface SensorsProps {
|
|||
|
||||
export interface FarmwareProps {
|
||||
dispatch: Function;
|
||||
env: Partial<WD_ENV>;
|
||||
user_env: UserEnv;
|
||||
wDEnv: Partial<WD_ENV>;
|
||||
env: UserEnv;
|
||||
images: TaggedImage[];
|
||||
currentImage: TaggedImage | undefined;
|
||||
botToMqttStatus: NetworkState;
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import {
|
||||
BotState, HardwareState, ControlPanelState, OsUpdateInfo,
|
||||
MinOsFeatureLookup
|
||||
BotState,
|
||||
ControlPanelState,
|
||||
HardwareState,
|
||||
MinOsFeatureLookup,
|
||||
OsUpdateInfo
|
||||
} from "./interfaces";
|
||||
import { generateReducer } from "../redux/generate_reducer";
|
||||
import { Actions } from "../constants";
|
||||
|
@ -18,7 +21,7 @@ const afterEach = (state: BotState, a: ReduxAction<{}>) => {
|
|||
return state;
|
||||
};
|
||||
|
||||
export let initialState = (): BotState => ({
|
||||
export const initialState = (): BotState => ({
|
||||
consistent: true,
|
||||
stepSize: 100,
|
||||
controlPanelState: {
|
||||
|
@ -81,7 +84,7 @@ export let initialState = (): BotState => ({
|
|||
}
|
||||
});
|
||||
|
||||
export let botReducer = generateReducer<BotState>(initialState())
|
||||
export const botReducer = generateReducer<BotState>(initialState())
|
||||
.afterEach(afterEach)
|
||||
.add<boolean>(Actions.SET_CONSISTENCY, (s, a) => {
|
||||
s.consistent = a.payload;
|
||||
|
|
|
@ -1,38 +1,28 @@
|
|||
import { Everything } from "../interfaces";
|
||||
import { Props, Feature } from "./interfaces";
|
||||
import { Props } from "./interfaces";
|
||||
import {
|
||||
selectAllImages,
|
||||
getDeviceAccountSettings,
|
||||
maybeGetDevice,
|
||||
maybeGetTimeSettings,
|
||||
} from "../resources/selectors";
|
||||
import {
|
||||
sourceFbosConfigValue, sourceFwConfigValue
|
||||
} from "./components/source_config_value";
|
||||
import { validFwConfig, validFbosConfig } from "../util";
|
||||
import {
|
||||
determineInstalledOsVersion, validFwConfig, validFbosConfig,
|
||||
createShouldDisplayFn as shouldDisplayFunc
|
||||
} from "../util";
|
||||
import {
|
||||
saveOrEditFarmwareEnv, reduceFarmwareEnv
|
||||
saveOrEditFarmwareEnv, getEnv, getShouldDisplayFn
|
||||
} from "../farmware/state_to_props";
|
||||
import { getFbosConfig, getFirmwareConfig, getWebAppConfig } from "../resources/getters";
|
||||
import { DevSettings } from "../account/dev/dev_support";
|
||||
import {
|
||||
getFbosConfig, getFirmwareConfig, getWebAppConfig
|
||||
} from "../resources/getters";
|
||||
import { getAllAlerts } from "../messages/state_to_props";
|
||||
|
||||
export function mapStateToProps(props: Everything): Props {
|
||||
const { hardware } = props.bot;
|
||||
const fbosConfig = validFbosConfig(getFbosConfig(props.resources.index));
|
||||
const firmwareConfig = validFwConfig(getFirmwareConfig(props.resources.index));
|
||||
const installedOsVersion = determineInstalledOsVersion(
|
||||
props.bot, maybeGetDevice(props.resources.index));
|
||||
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
|
||||
const shouldDisplay = shouldDisplayFunc(installedOsVersion,
|
||||
props.bot.minOsFeatureData,
|
||||
fbosVersionOverride);
|
||||
const env = shouldDisplay(Feature.api_farmware_env)
|
||||
? reduceFarmwareEnv(props.resources.index)
|
||||
: props.bot.hardware.user_env;
|
||||
const shouldDisplay = getShouldDisplayFn(props.resources.index, props.bot);
|
||||
const env = getEnv(props.resources.index, shouldDisplay, props.bot);
|
||||
const webAppConfig = getWebAppConfig(props.resources.index);
|
||||
if (!webAppConfig) {
|
||||
throw new Error("Missing web app config");
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export let list = ["Africa/Abidjan",
|
||||
export const list = ["Africa/Abidjan",
|
||||
"Africa/Accra",
|
||||
"Africa/Addis_Ababa",
|
||||
"Africa/Algiers",
|
||||
|
|
|
@ -4,6 +4,7 @@ import { Everything } from "../interfaces";
|
|||
import { ReduxAction } from "../redux/interfaces";
|
||||
import * as React from "react";
|
||||
import { Actions } from "../constants";
|
||||
import { UUID } from "../resources/interfaces";
|
||||
export const STEP_DATATRANSFER_IDENTIFER = "farmbot/sequence-step";
|
||||
|
||||
/** SIDE EFFECT-Y!! Stores a step into store.draggable.dataTransfer and
|
||||
|
@ -12,7 +13,8 @@ export const STEP_DATATRANSFER_IDENTIFER = "farmbot/sequence-step";
|
|||
export function stepPut(value: Step,
|
||||
ev: React.DragEvent<HTMLElement>,
|
||||
intent: DataXferIntent,
|
||||
draggerId: number):
|
||||
draggerId: number,
|
||||
resourceUuid?: UUID):
|
||||
ReduxAction<DataXferBase> {
|
||||
const uuid = id();
|
||||
ev.dataTransfer.setData(STEP_DATATRANSFER_IDENTIFER, uuid);
|
||||
|
@ -22,7 +24,8 @@ export function stepPut(value: Step,
|
|||
intent,
|
||||
uuid,
|
||||
value,
|
||||
draggerId
|
||||
draggerId,
|
||||
resourceUuid,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { SequenceBodyItem as Step } from "farmbot";
|
||||
import { UUID } from "../resources/interfaces";
|
||||
|
||||
/** An entry in the data transfer table. Used to transfer data from a "draggable"
|
||||
* to a "dropable". For type safety, this is a "tagged union". See Typescript
|
||||
|
@ -17,6 +18,8 @@ export interface DataXferBase {
|
|||
/** "why" the drag/drop event took place (tagged union- See Typescript
|
||||
* documentation for more information). */
|
||||
intent: DataXferIntent;
|
||||
/** Optional resource UUID. */
|
||||
resourceUuid?: UUID;
|
||||
}
|
||||
|
||||
/** Data transfer payload used when moving a *new* step into an existing step */
|
||||
|
@ -51,4 +54,5 @@ export interface StepDraggerProps {
|
|||
intent: DataXferIntent;
|
||||
children?: React.ReactNode;
|
||||
draggerId: number;
|
||||
resourceUuid?: UUID;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ const INITIAL_STATE: DraggableState = {
|
|||
dataTransfer: {}
|
||||
};
|
||||
|
||||
export let draggableReducer = generateReducer<DraggableState>(INITIAL_STATE)
|
||||
export const draggableReducer = generateReducer<DraggableState>(INITIAL_STATE)
|
||||
.add<DataXfer>(Actions.PUT_DATA_XFER, (s, { payload }) => {
|
||||
s.dataTransfer[payload.uuid] = payload;
|
||||
return s;
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as React from "react";
|
|||
import { stepPut } from "./actions";
|
||||
import { SequenceBodyItem as Step } from "farmbot";
|
||||
import { DataXferIntent, StepDraggerProps } from "./interfaces";
|
||||
import { UUID } from "../resources/interfaces";
|
||||
|
||||
/** Magic number to indicate that the draggerId was not provided or can't be
|
||||
* known. */
|
||||
|
@ -18,25 +19,24 @@ export const NULL_DRAGGER_ID = 0xCAFEF00D;
|
|||
* Drag this!
|
||||
* </button>
|
||||
* */
|
||||
export let stepDragEventHandler = (dispatch: Function,
|
||||
export const stepDragEventHandler = (dispatch: Function,
|
||||
step: Step,
|
||||
intent: DataXferIntent,
|
||||
draggerId: number) => {
|
||||
draggerId: number,
|
||||
resourceUuid?: UUID) => {
|
||||
return (ev: React.DragEvent<HTMLElement>) => {
|
||||
dispatch(stepPut(step, ev, intent, draggerId));
|
||||
dispatch(stepPut(step, ev, intent, draggerId, resourceUuid));
|
||||
};
|
||||
};
|
||||
|
||||
export function StepDragger({ dispatch,
|
||||
step,
|
||||
children,
|
||||
intent,
|
||||
draggerId }: StepDraggerProps) {
|
||||
return <div
|
||||
export function StepDragger(props: StepDraggerProps) {
|
||||
const { dispatch, step, children, intent, draggerId, resourceUuid } = props;
|
||||
return <div className="step-dragger"
|
||||
onDragStart={stepDragEventHandler(dispatch,
|
||||
step,
|
||||
intent,
|
||||
draggerId)}>
|
||||
draggerId,
|
||||
resourceUuid)}>
|
||||
{children}
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/// <reference path="./hacks.d.ts" />
|
||||
/**
|
||||
* THIS IS THE ENTRY POINT FOR THE MAIN PORTION OF THE WEB APP.
|
||||
*
|
||||
* Try to keep this file light. */
|
||||
import { detectLanguage } from "./i18n";
|
||||
import { stopIE } from "./util/stop_ie";
|
||||
|
|
|
@ -3,7 +3,7 @@ import { catchErrors } from "./util";
|
|||
import { Apology } from "./apology";
|
||||
|
||||
interface State { hasError?: boolean; }
|
||||
interface Props { }
|
||||
interface Props { fallback?: React.ReactElement }
|
||||
|
||||
export class ErrorBoundary extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
|
@ -16,7 +16,7 @@ export class ErrorBoundary extends React.Component<Props, State> {
|
|||
this.setState({ hasError: true });
|
||||
}
|
||||
|
||||
no = () => <Apology />;
|
||||
no = () => this.props.fallback || <Apology />;
|
||||
|
||||
ok = () => this.props.children || <div />;
|
||||
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { DesignerPanel, DesignerPanelHeader } from "../../designer_panel";
|
||||
import { DesignerPanel, DesignerPanelHeader } from "../designer_panel";
|
||||
|
||||
describe("<DesignerPanel />", () => {
|
||||
it("renders default panel", () => {
|
||||
const wrapper = mount(<DesignerPanel panelName={"test-panel"} />);
|
||||
expect(wrapper.find("div").hasClass("gray-panel")).toBeTruthy();
|
||||
expect(wrapper.find("div").first().hasClass("gray-panel")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("<DesignerPanelHeader />", () => {
|
||||
it("renders default panel header", () => {
|
||||
const wrapper = mount(<DesignerPanelHeader panelName={"test-panel"} />);
|
||||
expect(wrapper.find("div").hasClass("gray-panel")).toBeTruthy();
|
||||
expect(wrapper.find("div").first().hasClass("gray-panel")).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -63,6 +63,15 @@ describe("designer reducer", () => {
|
|||
expect(newState.hoveredPoint).toEqual("uuid");
|
||||
});
|
||||
|
||||
it("sets hovered tool slot", () => {
|
||||
const action: ReduxAction<string> = {
|
||||
type: Actions.HOVER_TOOL_SLOT,
|
||||
payload: "toolSlotUuid"
|
||||
};
|
||||
const newState = designer(oldState(), action);
|
||||
expect(newState.hoveredToolSlot).toEqual("toolSlotUuid");
|
||||
});
|
||||
|
||||
it("sets chosen location", () => {
|
||||
const action: ReduxAction<BotPosition> = {
|
||||
type: Actions.CHOOSE_LOCATION,
|
||||
|
|
|
@ -4,6 +4,7 @@ import { last, trim } from "lodash";
|
|||
import { Link } from "../link";
|
||||
import { Panel, TAB_COLOR, PanelColor } from "./panel_header";
|
||||
import { t } from "../i18next_wrapper";
|
||||
import { ErrorBoundary } from "../error_boundary";
|
||||
|
||||
interface DesignerPanelProps {
|
||||
panelName: string;
|
||||
|
@ -19,7 +20,9 @@ export const DesignerPanel = (props: DesignerPanelProps) => {
|
|||
"panel-container",
|
||||
`${color || PanelColor.gray}-panel`,
|
||||
`${props.panelName}-panel`].join(" ")}>
|
||||
{props.children}
|
||||
<ErrorBoundary>
|
||||
{props.children}
|
||||
</ErrorBoundary>
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
@ -86,7 +89,9 @@ export const DesignerPanelTop = (props: DesignerPanelTopProps) => {
|
|||
<div className="text-input-wrapper">
|
||||
{!props.noIcon &&
|
||||
<i className="fa fa-search"></i>}
|
||||
{props.children}
|
||||
<ErrorBoundary>
|
||||
{props.children}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
{props.linkTo &&
|
||||
|
@ -105,8 +110,11 @@ interface DesignerPanelContentProps {
|
|||
}
|
||||
|
||||
export const DesignerPanelContent = (props: DesignerPanelContentProps) =>
|
||||
<div className={
|
||||
`panel-content ${props.panelName}-panel-content ${props.className || ""}`
|
||||
}>
|
||||
{props.children}
|
||||
<div className={[
|
||||
"panel-content",
|
||||
`${props.panelName}-panel-content`,
|
||||
props.className || ""].join(" ")}>
|
||||
<ErrorBoundary>
|
||||
{props.children}
|
||||
</ErrorBoundary>
|
||||
</div>;
|
||||
|
|
|
@ -161,4 +161,4 @@ export class PureFarmEvents
|
|||
* It avoids mocking `connect` in unit tests.
|
||||
* See testing pattern noted here: https://github.com/airbnb/enzyme/issues/98
|
||||
*/
|
||||
export let FarmEvents = connect(mapStateToProps)(PureFarmEvents);
|
||||
export const FarmEvents = connect(mapStateToProps)(PureFarmEvents);
|
||||
|
|
|
@ -65,7 +65,7 @@ export const nextRegItemTimes =
|
|||
&& time.isSameOrAfter(moment(startTime)));
|
||||
};
|
||||
|
||||
export let regimenCalendarAdder = (
|
||||
export const regimenCalendarAdder = (
|
||||
index: ResourceIndex, timeSettings: TimeSettings) =>
|
||||
(f: FarmEventWithRegimen, c: Calendar, now = moment()) => {
|
||||
const { regimen_items } = f.executable;
|
||||
|
@ -96,7 +96,7 @@ export let regimenCalendarAdder = (
|
|||
}
|
||||
};
|
||||
|
||||
export let addSequenceToCalendar =
|
||||
export const addSequenceToCalendar =
|
||||
(f: FarmEventWithSequence, c: Calendar, timeSettings: TimeSettings,
|
||||
now = moment()) => {
|
||||
const schedule = scheduleForFarmEvent(f, now);
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
findSequenceById,
|
||||
findRegimenById,
|
||||
getDeviceAccountSettings,
|
||||
maybeGetDevice,
|
||||
maybeGetTimeSettings
|
||||
} from "../../resources/selectors";
|
||||
import {
|
||||
|
@ -22,11 +21,7 @@ import {
|
|||
TaggedRegimen
|
||||
} from "farmbot";
|
||||
import { DropDownItem } from "../../ui/index";
|
||||
import {
|
||||
validFbosConfig,
|
||||
createShouldDisplayFn as shouldDisplayFunc,
|
||||
determineInstalledOsVersion
|
||||
} from "../../util";
|
||||
import { validFbosConfig } from "../../util";
|
||||
import {
|
||||
sourceFbosConfigValue
|
||||
} from "../../devices/components/source_config_value";
|
||||
|
@ -34,19 +29,19 @@ import { hasId } from "../../resources/util";
|
|||
import { ExecutableType } from "farmbot/dist/resources/api_resources";
|
||||
import { getFbosConfig } from "../../resources/getters";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { DevSettings } from "../../account/dev/dev_support";
|
||||
import { getShouldDisplayFn } from "../../farmware/state_to_props";
|
||||
|
||||
export let formatTime = (input: string, timeSettings: TimeSettings) => {
|
||||
export const formatTime = (input: string, timeSettings: TimeSettings) => {
|
||||
const iso = new Date(input).toISOString();
|
||||
return moment(iso).utcOffset(timeSettings.utcOffset).format("HH:mm");
|
||||
};
|
||||
|
||||
export let formatDate = (input: string, timeSettings: TimeSettings) => {
|
||||
export const formatDate = (input: string, timeSettings: TimeSettings) => {
|
||||
const iso = new Date(input).toISOString();
|
||||
return moment(iso).utcOffset(timeSettings.utcOffset).format("YYYY-MM-DD");
|
||||
};
|
||||
|
||||
export let repeatOptions = [
|
||||
export const repeatOptions = [
|
||||
{ label: t("Minutes"), value: "minutely", name: "time_unit" },
|
||||
{ label: t("Hours"), value: "hourly", name: "time_unit" },
|
||||
{ label: t("Days"), value: "daily", name: "time_unit" },
|
||||
|
@ -143,12 +138,6 @@ export function mapStateToPropsAddEdit(props: Everything): AddEditFarmEventProps
|
|||
const autoSyncEnabled =
|
||||
!!sourceFbosConfigValue(fbosConfig, configuration)("auto_sync").value;
|
||||
|
||||
const installedOsVersion = determineInstalledOsVersion(
|
||||
props.bot, maybeGetDevice(props.resources.index));
|
||||
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
|
||||
const shouldDisplay = shouldDisplayFunc(
|
||||
installedOsVersion, props.bot.minOsFeatureData, fbosVersionOverride);
|
||||
|
||||
return {
|
||||
deviceTimezone: dev
|
||||
.body
|
||||
|
@ -167,6 +156,6 @@ export function mapStateToPropsAddEdit(props: Everything): AddEditFarmEventProps
|
|||
timeSettings: maybeGetTimeSettings(props.resources.index),
|
||||
autoSyncEnabled,
|
||||
resources: props.resources.index,
|
||||
shouldDisplay,
|
||||
shouldDisplay: getShouldDisplayFn(props.resources.index, props.bot),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -103,6 +103,7 @@ export interface DesignerState {
|
|||
hoveredPlant: HoveredPlantPayl;
|
||||
hoveredPoint: string | undefined;
|
||||
hoveredPlantListItem: string | undefined;
|
||||
hoveredToolSlot: string | undefined;
|
||||
cropSearchQuery: string;
|
||||
cropSearchResults: CropLiveSearchResult[];
|
||||
cropSearchInProgress: boolean;
|
||||
|
|
|
@ -340,6 +340,8 @@ export class GardenMap extends
|
|||
ToolSlotLayer = () => <ToolSlotLayer
|
||||
mapTransformProps={this.mapTransformProps}
|
||||
visible={!!this.props.showFarmbot}
|
||||
dispatch={this.props.dispatch}
|
||||
hoveredToolSlot={this.props.designer.hoveredToolSlot}
|
||||
botPositionX={this.props.botLocationData.position.x}
|
||||
slots={this.props.toolSlots} />
|
||||
FarmBotLayer = () => <FarmBotLayer
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
import { BotOriginQuadrant } from "../../../../interfaces";
|
||||
import { Color } from "../../../../../ui";
|
||||
import { svgMount } from "../../../../../__test_support__/svg_mount";
|
||||
import { Actions } from "../../../../../constants";
|
||||
|
||||
describe("<ToolbaySlot />", () => {
|
||||
const fakeProps = (): ToolSlotGraphicProps => ({
|
||||
|
@ -61,7 +62,8 @@ describe("<Tool/>", () => {
|
|||
x: 10,
|
||||
y: 20,
|
||||
hovered: false,
|
||||
setHoverState: jest.fn(),
|
||||
dispatch: jest.fn(),
|
||||
uuid: "fakeUuid",
|
||||
xySwap: false,
|
||||
});
|
||||
|
||||
|
@ -75,9 +77,13 @@ describe("<Tool/>", () => {
|
|||
p.tool = toolName;
|
||||
const wrapper = svgMount(<Tool {...p} />);
|
||||
wrapper.find("g").simulate("mouseOver");
|
||||
expect(p.toolProps.setHoverState).toHaveBeenCalledWith(true);
|
||||
expect(p.toolProps.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.HOVER_TOOL_SLOT, payload: "fakeUuid"
|
||||
});
|
||||
wrapper.find("g").simulate("mouseLeave");
|
||||
expect(p.toolProps.setHoverState).toHaveBeenCalledWith(false);
|
||||
expect(p.toolProps.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.HOVER_TOOL_SLOT, payload: undefined
|
||||
});
|
||||
};
|
||||
|
||||
it("renders standard tool styling", () => {
|
||||
|
|
|
@ -14,6 +14,7 @@ import { shallow } from "enzyme";
|
|||
import { history } from "../../../../../history";
|
||||
import { ToolSlotPointer } from "farmbot/dist/resources/api_resources";
|
||||
import { TaggedToolSlotPointer } from "farmbot";
|
||||
import { ToolSlotPoint } from "../tool_slot_point";
|
||||
|
||||
describe("<ToolSlotLayer/>", () => {
|
||||
function fakeProps(): ToolSlotLayerProps {
|
||||
|
@ -35,18 +36,20 @@ describe("<ToolSlotLayer/>", () => {
|
|||
slots: [{ toolSlot, tool: undefined }],
|
||||
botPositionX: undefined,
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
dispatch: jest.fn(),
|
||||
hoveredToolSlot: undefined,
|
||||
};
|
||||
}
|
||||
it("toggles visibility off", () => {
|
||||
const result = shallow(<ToolSlotLayer {...fakeProps()} />);
|
||||
expect(result.find("ToolSlotPoint").length).toEqual(0);
|
||||
expect(result.find(ToolSlotPoint).length).toEqual(0);
|
||||
});
|
||||
|
||||
it("toggles visibility on", () => {
|
||||
const p = fakeProps();
|
||||
p.visible = true;
|
||||
const result = shallow(<ToolSlotLayer {...p} />);
|
||||
expect(result.find("ToolSlotPoint").length).toEqual(1);
|
||||
expect(result.find(ToolSlotPoint).length).toEqual(1);
|
||||
});
|
||||
|
||||
it("navigates to tools page", async () => {
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
let mockDev = false;
|
||||
jest.mock("../../../../../account/dev/dev_support", () => ({
|
||||
DevSettings: { futureFeaturesEnabled: () => mockDev, }
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../history", () => ({ history: { push: jest.fn() } }));
|
||||
|
||||
import * as React from "react";
|
||||
import { ToolSlotPoint, TSPProps } from "../tool_slot_point";
|
||||
import {
|
||||
|
@ -7,12 +14,19 @@ import {
|
|||
fakeMapTransformProps
|
||||
} from "../../../../../__test_support__/map_transform_props";
|
||||
import { svgMount } from "../../../../../__test_support__/svg_mount";
|
||||
import { history } from "../../../../../history";
|
||||
|
||||
describe("<ToolSlotPoint/>", () => {
|
||||
beforeEach(() => {
|
||||
mockDev = false;
|
||||
});
|
||||
|
||||
const fakeProps = (): TSPProps => ({
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
botPositionX: undefined,
|
||||
slot: { toolSlot: fakeToolSlot(), tool: fakeTool() }
|
||||
slot: { toolSlot: fakeToolSlot(), tool: fakeTool() },
|
||||
dispatch: jest.fn(),
|
||||
hoveredToolSlot: undefined,
|
||||
});
|
||||
|
||||
const testToolSlotGraphics = (tool: 0 | 1, slot: 0 | 1) => {
|
||||
|
@ -31,11 +45,23 @@ describe("<ToolSlotPoint/>", () => {
|
|||
testToolSlotGraphics(1, 0);
|
||||
testToolSlotGraphics(1, 1);
|
||||
|
||||
it("opens tool info", () => {
|
||||
const p = fakeProps();
|
||||
p.slot.toolSlot.body.id = 1;
|
||||
const wrapper = svgMount(<ToolSlotPoint {...p} />);
|
||||
mockDev = false;
|
||||
wrapper.find("g").first().simulate("click");
|
||||
expect(history.push).not.toHaveBeenCalled();
|
||||
mockDev = true;
|
||||
wrapper.find("g").first().simulate("click");
|
||||
expect(history.push).toHaveBeenCalledWith("/app/designer/tool-slots/1");
|
||||
});
|
||||
|
||||
it("displays tool name", () => {
|
||||
const p = fakeProps();
|
||||
p.slot.toolSlot.body.pullout_direction = 2;
|
||||
p.hoveredToolSlot = p.slot.toolSlot.uuid;
|
||||
const wrapper = svgMount(<ToolSlotPoint {...p} />);
|
||||
wrapper.find(ToolSlotPoint).setState({ hovered: true });
|
||||
expect(wrapper.find("text").props().visibility).toEqual("visible");
|
||||
expect(wrapper.find("text").text()).toEqual("Foo");
|
||||
expect(wrapper.find("text").props().dx).toEqual(-40);
|
||||
|
@ -44,8 +70,8 @@ describe("<ToolSlotPoint/>", () => {
|
|||
it("displays 'no tool'", () => {
|
||||
const p = fakeProps();
|
||||
p.slot.tool = undefined;
|
||||
p.hoveredToolSlot = p.slot.toolSlot.uuid;
|
||||
const wrapper = svgMount(<ToolSlotPoint {...p} />);
|
||||
wrapper.find(ToolSlotPoint).setState({ hovered: true });
|
||||
expect(wrapper.find("text").text()).toEqual("no tool");
|
||||
expect(wrapper.find("text").props().dx).toEqual(40);
|
||||
});
|
||||
|
@ -74,13 +100,21 @@ describe("<ToolSlotPoint/>", () => {
|
|||
p.slot.toolSlot.body.gantry_mounted = true;
|
||||
if (p.slot.tool) { p.slot.tool.body.name = "seed trough"; }
|
||||
const wrapper = svgMount(<ToolSlotPoint {...p} />);
|
||||
expect(wrapper.find("#seed-trough").length).toEqual(1);
|
||||
expect(wrapper.find("#seed-trough").find("rect").props().width)
|
||||
.toEqual(45);
|
||||
expect(wrapper.find("#gantry-toolbay-slot").find("rect").props().width)
|
||||
.toEqual(49);
|
||||
});
|
||||
|
||||
it("sets hover", () => {
|
||||
const wrapper = svgMount(<ToolSlotPoint {...fakeProps()} />);
|
||||
expect(wrapper.find(ToolSlotPoint).state().hovered).toBeFalsy();
|
||||
(wrapper.find(ToolSlotPoint).instance() as ToolSlotPoint).setHover(true);
|
||||
expect(wrapper.find(ToolSlotPoint).state().hovered).toBeTruthy();
|
||||
it("renders rotated trough", () => {
|
||||
const p = fakeProps();
|
||||
p.mapTransformProps.xySwap = true;
|
||||
p.slot.toolSlot.body.gantry_mounted = true;
|
||||
if (p.slot.tool) { p.slot.tool.body.name = "seed trough"; }
|
||||
const wrapper = svgMount(<ToolSlotPoint {...p} />);
|
||||
expect(wrapper.find("#seed-trough").find("rect").props().width)
|
||||
.toEqual(20);
|
||||
expect(wrapper.find("#gantry-toolbay-slot").find("rect").props().width)
|
||||
.toEqual(24);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,13 +3,16 @@ import { Color } from "../../../../ui/index";
|
|||
import { trim } from "../../../../util";
|
||||
import { BotOriginQuadrant } from "../../../interfaces";
|
||||
import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources";
|
||||
import { Actions } from "../../../../constants";
|
||||
import { UUID } from "../../../../resources/interfaces";
|
||||
|
||||
export interface ToolGraphicProps {
|
||||
x: number;
|
||||
y: number;
|
||||
hovered: boolean;
|
||||
setHoverState(hoverState: boolean): void;
|
||||
dispatch: Function;
|
||||
xySwap: boolean;
|
||||
uuid: UUID | undefined;
|
||||
}
|
||||
|
||||
export interface ToolProps {
|
||||
|
@ -95,11 +98,14 @@ export const Tool = (props: ToolProps) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const setToolHover = (payload: string | undefined) =>
|
||||
({ type: Actions.HOVER_TOOL_SLOT, payload });
|
||||
|
||||
const StandardTool = (props: ToolGraphicProps) => {
|
||||
const { x, y, hovered, setHoverState } = props;
|
||||
const { x, y, hovered, dispatch, uuid } = props;
|
||||
return <g id={"tool"}
|
||||
onMouseOver={() => setHoverState(true)}
|
||||
onMouseLeave={() => setHoverState(false)}>
|
||||
onMouseOver={() => dispatch(setToolHover(uuid))}
|
||||
onMouseLeave={() => dispatch(setToolHover(undefined))}>
|
||||
<circle
|
||||
cx={x}
|
||||
cy={y}
|
||||
|
@ -116,10 +122,10 @@ const seedBinGradient =
|
|||
</radialGradient>;
|
||||
|
||||
const SeedBin = (props: ToolGraphicProps) => {
|
||||
const { x, y, hovered, setHoverState } = props;
|
||||
const { x, y, hovered, dispatch, uuid } = props;
|
||||
return <g id={"seed-bin"}
|
||||
onMouseOver={() => setHoverState(true)}
|
||||
onMouseLeave={() => setHoverState(false)}>
|
||||
onMouseOver={() => dispatch(setToolHover(uuid))}
|
||||
onMouseLeave={() => dispatch(setToolHover(undefined))}>
|
||||
|
||||
<defs>
|
||||
{seedBinGradient}
|
||||
|
@ -140,10 +146,10 @@ const SeedBin = (props: ToolGraphicProps) => {
|
|||
};
|
||||
|
||||
const SeedTray = (props: ToolGraphicProps) => {
|
||||
const { x, y, hovered, setHoverState } = props;
|
||||
const { x, y, hovered, dispatch, uuid } = props;
|
||||
return <g id={"seed-tray"}
|
||||
onMouseOver={() => setHoverState(true)}
|
||||
onMouseLeave={() => setHoverState(false)}>
|
||||
onMouseOver={() => dispatch(setToolHover(uuid))}
|
||||
onMouseLeave={() => dispatch(setToolHover(undefined))}>
|
||||
|
||||
<defs>
|
||||
{seedBinGradient}
|
||||
|
@ -188,12 +194,12 @@ export const GantryToolSlot = (props: GantryToolSlotGraphicProps) => {
|
|||
};
|
||||
|
||||
const SeedTrough = (props: ToolGraphicProps) => {
|
||||
const { x, y, hovered, setHoverState, xySwap } = props;
|
||||
const { x, y, hovered, dispatch, uuid, xySwap } = props;
|
||||
const slotLengthX = xySwap ? 20 : 45;
|
||||
const slotLengthY = xySwap ? 45 : 20;
|
||||
return <g id={"seed-trough"}
|
||||
onMouseOver={() => setHoverState(true)}
|
||||
onMouseLeave={() => setHoverState(false)}>
|
||||
onMouseOver={() => dispatch(setToolHover(uuid))}
|
||||
onMouseLeave={() => dispatch(setToolHover(undefined))}>
|
||||
<rect
|
||||
x={x - slotLengthX / 2} y={y - slotLengthY / 2}
|
||||
width={slotLengthX} height={slotLengthY}
|
||||
|
|
|
@ -1,21 +1,25 @@
|
|||
import * as React from "react";
|
||||
import { SlotWithTool } from "../../../../resources/interfaces";
|
||||
import { SlotWithTool, UUID } from "../../../../resources/interfaces";
|
||||
import { ToolSlotPoint } from "./tool_slot_point";
|
||||
import { MapTransformProps } from "../../interfaces";
|
||||
import { history, getPathArray } from "../../../../history";
|
||||
import { maybeNoPointer } from "../../util";
|
||||
import { DevSettings } from "../../../../account/dev/dev_support";
|
||||
|
||||
export interface ToolSlotLayerProps {
|
||||
visible: boolean;
|
||||
slots: SlotWithTool[];
|
||||
botPositionX: number | undefined;
|
||||
mapTransformProps: MapTransformProps;
|
||||
dispatch: Function;
|
||||
hoveredToolSlot: UUID | undefined;
|
||||
}
|
||||
|
||||
export function ToolSlotLayer(props: ToolSlotLayerProps) {
|
||||
const pathArray = getPathArray();
|
||||
const canClickTool = !(pathArray[3] === "plants" && pathArray.length > 4);
|
||||
const goToToolsPage = () => canClickTool && history.push("/app/tools");
|
||||
const goToToolsPage = () => canClickTool &&
|
||||
!DevSettings.futureFeaturesEnabled() && history.push("/app/tools");
|
||||
const { slots, visible, mapTransformProps } = props;
|
||||
const cursor = canClickTool ? "pointer" : "default";
|
||||
|
||||
|
@ -28,6 +32,8 @@ export function ToolSlotLayer(props: ToolSlotLayerProps) {
|
|||
<ToolSlotPoint
|
||||
key={slot.toolSlot.uuid}
|
||||
slot={slot}
|
||||
hoveredToolSlot={props.hoveredToolSlot}
|
||||
dispatch={props.dispatch}
|
||||
botPositionX={props.botPositionX}
|
||||
mapTransformProps={mapTransformProps} />)}
|
||||
</g>;
|
||||
|
|
|
@ -1,78 +1,73 @@
|
|||
import * as React from "react";
|
||||
import { SlotWithTool } from "../../../../resources/interfaces";
|
||||
import { SlotWithTool, UUID } from "../../../../resources/interfaces";
|
||||
import { transformXY } from "../../util";
|
||||
import { MapTransformProps } from "../../interfaces";
|
||||
import { ToolbaySlot, ToolNames, Tool, GantryToolSlot } from "./tool_graphics";
|
||||
import { ToolLabel } from "./tool_label";
|
||||
import { includes } from "lodash";
|
||||
import { DevSettings } from "../../../../account/dev/dev_support";
|
||||
import { history } from "../../../../history";
|
||||
|
||||
export interface TSPProps {
|
||||
slot: SlotWithTool;
|
||||
botPositionX: number | undefined;
|
||||
mapTransformProps: MapTransformProps;
|
||||
dispatch: Function;
|
||||
hoveredToolSlot: UUID | undefined;
|
||||
}
|
||||
|
||||
interface TSPState {
|
||||
hovered: boolean;
|
||||
}
|
||||
const reduceToolName = (raw: string | undefined) => {
|
||||
const lower = (raw || "").toLowerCase();
|
||||
if (includes(lower, "seed bin")) { return ToolNames.seedBin; }
|
||||
if (includes(lower, "seed tray")) { return ToolNames.seedTray; }
|
||||
if (includes(lower, "seed trough")) { return ToolNames.seedTrough; }
|
||||
return ToolNames.tool;
|
||||
};
|
||||
|
||||
export class ToolSlotPoint extends
|
||||
React.Component<TSPProps, Partial<TSPState>> {
|
||||
state: TSPState = { hovered: false };
|
||||
|
||||
setHover = (state: boolean) => this.setState({ hovered: state });
|
||||
|
||||
get slot() { return this.props.slot; }
|
||||
|
||||
reduceToolName = (raw: string | undefined) => {
|
||||
const lower = (raw || "").toLowerCase();
|
||||
if (includes(lower, "seed bin")) { return ToolNames.seedBin; }
|
||||
if (includes(lower, "seed tray")) { return ToolNames.seedTray; }
|
||||
if (includes(lower, "seed trough")) { return ToolNames.seedTrough; }
|
||||
return ToolNames.tool;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
id, x, y, pullout_direction, gantry_mounted
|
||||
} = this.slot.toolSlot.body;
|
||||
const { mapTransformProps, botPositionX } = this.props;
|
||||
const { quadrant, xySwap } = mapTransformProps;
|
||||
const xPosition = gantry_mounted ? (botPositionX || 0) : x;
|
||||
const { qx, qy } = transformXY(xPosition, y, this.props.mapTransformProps);
|
||||
const toolName = this.slot.tool ? this.slot.tool.body.name : "no tool";
|
||||
const toolProps = {
|
||||
x: qx,
|
||||
y: qy,
|
||||
hovered: this.state.hovered,
|
||||
setHoverState: this.setHover,
|
||||
xySwap,
|
||||
};
|
||||
return <g id={"toolslot-" + id}>
|
||||
{pullout_direction &&
|
||||
<ToolbaySlot
|
||||
id={id}
|
||||
x={qx}
|
||||
y={qy}
|
||||
pulloutDirection={pullout_direction}
|
||||
quadrant={quadrant}
|
||||
xySwap={xySwap} />}
|
||||
|
||||
{gantry_mounted && <GantryToolSlot x={qx} y={qy} xySwap={xySwap} />}
|
||||
|
||||
{(this.slot.tool || (!pullout_direction && !gantry_mounted)) &&
|
||||
<Tool
|
||||
tool={this.reduceToolName(toolName)}
|
||||
toolProps={toolProps} />}
|
||||
|
||||
<ToolLabel
|
||||
toolName={toolName}
|
||||
hovered={this.state.hovered}
|
||||
export const ToolSlotPoint = (props: TSPProps) => {
|
||||
const {
|
||||
id, x, y, pullout_direction, gantry_mounted
|
||||
} = props.slot.toolSlot.body;
|
||||
const { mapTransformProps, botPositionX } = props;
|
||||
const { quadrant, xySwap } = mapTransformProps;
|
||||
const xPosition = gantry_mounted ? (botPositionX || 0) : x;
|
||||
const { qx, qy } = transformXY(xPosition, y, props.mapTransformProps);
|
||||
const toolName = props.slot.tool ? props.slot.tool.body.name : "no tool";
|
||||
const hovered = props.slot.toolSlot.uuid === props.hoveredToolSlot;
|
||||
const toolProps = {
|
||||
x: qx,
|
||||
y: qy,
|
||||
hovered,
|
||||
dispatch: props.dispatch,
|
||||
uuid: props.slot.toolSlot.uuid,
|
||||
xySwap,
|
||||
};
|
||||
return <g id={"toolslot-" + id}
|
||||
onClick={() => DevSettings.futureFeaturesEnabled() &&
|
||||
history.push(`/app/designer/tool-slots/${id}`)}>
|
||||
{pullout_direction &&
|
||||
<ToolbaySlot
|
||||
id={id}
|
||||
x={qx}
|
||||
y={qy}
|
||||
pulloutDirection={pullout_direction}
|
||||
quadrant={quadrant}
|
||||
xySwap={xySwap} />
|
||||
</g>;
|
||||
}
|
||||
}
|
||||
xySwap={xySwap} />}
|
||||
|
||||
{gantry_mounted && <GantryToolSlot x={qx} y={qy} xySwap={xySwap} />}
|
||||
|
||||
{(props.slot.tool || (!pullout_direction && !gantry_mounted)) &&
|
||||
<Tool
|
||||
tool={reduceToolName(toolName)}
|
||||
toolProps={toolProps} />}
|
||||
|
||||
<ToolLabel
|
||||
toolName={toolName}
|
||||
hovered={hovered}
|
||||
x={qx}
|
||||
y={qy}
|
||||
pulloutDirection={pullout_direction}
|
||||
quadrant={quadrant}
|
||||
xySwap={xySwap} />
|
||||
</g>;
|
||||
};
|
||||
|
|
|
@ -68,8 +68,8 @@ export namespace OpenFarm {
|
|||
attributes: ImageAttrs;
|
||||
}
|
||||
|
||||
export let cropUrl = "https://openfarm.cc/api/v1/crops";
|
||||
export let browsingCropUrl = "https://openfarm.cc/crops/";
|
||||
export const cropUrl = "https://openfarm.cc/api/v1/crops";
|
||||
export const browsingCropUrl = "https://openfarm.cc/crops/";
|
||||
}
|
||||
/** Returned by https://openfarm.cc/api/v1/crops?filter=q */
|
||||
export interface CropSearchResult {
|
||||
|
|
|
@ -47,8 +47,7 @@ export class RawPlantInfo extends React.Component<EditPlantInfoProps, {}> {
|
|||
panel={Panel.Plants}
|
||||
title={`${t("Edit")} ${info.name}`}
|
||||
backTo={"/app/designer/plants"}
|
||||
onBack={unselectPlant(this.props.dispatch)}>
|
||||
</DesignerPanelHeader>
|
||||
onBack={unselectPlant(this.props.dispatch)} />
|
||||
<PlantPanel
|
||||
info={info}
|
||||
onDestroy={this.destroy}
|
||||
|
|
|
@ -76,8 +76,7 @@ export class GroupDetailActive
|
|||
panelName={Panel.Groups}
|
||||
panel={Panel.Groups}
|
||||
title={t("Edit Group")}
|
||||
backTo={"/app/designer/groups"}>
|
||||
</DesignerPanelHeader>
|
||||
backTo={"/app/designer/groups"} />
|
||||
<DesignerPanelContent
|
||||
panelName={"groups"}>
|
||||
<label>{t("GROUP NAME")}{this.saved ? "" : "*"}</label>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue