Merge branch 'staging' of github.com:FarmBot/Farmbot-Web-App into guest_accounts
commit
b9607c09c8
|
@ -3,17 +3,18 @@
|
|||
# this part last
|
||||
module CeleryScript
|
||||
UNBOUND_VAR = "Unbound variable: %s"
|
||||
|
||||
class TypeCheckError < StandardError; end
|
||||
|
||||
class Checker
|
||||
MISSING_ARG = "Expected node '%s' to have a '%s', but got: %s."
|
||||
EXTRA_ARGS = "'%s' has unexpected arguments: %s. Allowed arguments: %s"
|
||||
BAD_LEAF = "Expected leaf '%{kind}' within '%{parent_kind}'"\
|
||||
" to be one of: %{allowed} but got %{actual}"
|
||||
MALFORMED = "Expected '%s' to be a node or leaf, but it was neither"
|
||||
BAD_BODY = "Body of '%s' node contains '%s' node. "\
|
||||
"Expected one of: %s"
|
||||
T_MISMATCH = "Type mismatch. %s must be one of: %s. Got: %s"
|
||||
EXTRA_ARGS = "'%s' has unexpected arguments: %s. Allowed arguments: %s"
|
||||
BAD_LEAF = "Expected leaf '%{kind}' within '%{parent_kind}'" \
|
||||
" to be one of: %{allowed} but got %{actual}"
|
||||
MALFORMED = "Expected '%s' to be a node or leaf, but it was neither"
|
||||
BAD_BODY = "Body of '%s' node contains '%s' node. " \
|
||||
"Expected one of: %s"
|
||||
T_MISMATCH = "Type mismatch. %s must be one of: %s. Got: %s"
|
||||
|
||||
# Certain CeleryScript pairing errors are more than just a syntax error.
|
||||
# For instance, A `nothing` node in a `parameter_declaration` is often an
|
||||
|
@ -22,6 +23,7 @@ module CeleryScript
|
|||
# BAD_LEAF template.
|
||||
FRIENDLY_ERRORS = {
|
||||
nothing: {
|
||||
write_pin: "You must select a Peripheral in the Control Peripheral step.",
|
||||
variable_declaration: "You must provide a value for all parameters",
|
||||
parameter_declaration: "You must provide a value for all parameters",
|
||||
},
|
||||
|
@ -89,7 +91,7 @@ module CeleryScript
|
|||
unless has_key
|
||||
msgs = node.args.keys.join(", ")
|
||||
msgs = "nothing" if msgs.length < 1
|
||||
msg = MISSING_ARG % [node.kind, arg, msgs]
|
||||
msg = MISSING_ARG % [node.kind, arg, msgs]
|
||||
raise TypeCheckError, msg
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
# Support class for Fragment. Please see fragment.rb for documentation.
|
||||
class ArgName < ApplicationRecord
|
||||
EXPIRY = Rails.env.test? ? 1.second : 2.hours
|
||||
KEY = "arg_names/%s"
|
||||
KEY = "arg_names:%s"
|
||||
|
||||
validates_uniqueness_of :value
|
||||
|
||||
has_many :primitive_pairs, autosave: true
|
||||
has_many :standard_pairs, autosave: true
|
||||
has_many :standard_pairs, autosave: true
|
||||
|
||||
def self.cached_by_value(v)
|
||||
key = KEY % v
|
||||
|
|
|
@ -10,7 +10,7 @@ class Device < ApplicationRecord
|
|||
"Suspending log storage and display until %s."
|
||||
THROTTLE_OFF = "Cooldown period has ended. " \
|
||||
"Resuming log storage."
|
||||
CACHE_KEY = "devices.%s"
|
||||
CACHE_KEY = "devices:%s"
|
||||
|
||||
PLURAL_RESOURCES = %i(alerts farmware_envs farm_events farmware_installations
|
||||
images logs peripherals pin_bindings plant_templates
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Support class for Fragment. Please see fragment.rb for documentation.
|
||||
class Kind < ApplicationRecord
|
||||
EXPIRY = Rails.env.test? ? 1.second : 2.hours
|
||||
KEY = "kinds/%s"
|
||||
KEY = "kinds:%s"
|
||||
has_many :nodes
|
||||
|
||||
def self.cached_by_value(v)
|
||||
|
|
|
@ -30,7 +30,6 @@ module Devices
|
|||
:settings_device_name,
|
||||
:settings_enable_encoders,
|
||||
:settings_firmware,
|
||||
:settings_map_xl,
|
||||
:settings_hide_sensors,
|
||||
|
||||
# TOOLS ==================================
|
||||
|
@ -78,10 +77,6 @@ module Devices
|
|||
@device = device
|
||||
end
|
||||
|
||||
def settings_map_xl
|
||||
device.web_app_config.update_attributes!(map_xl: false)
|
||||
end
|
||||
|
||||
def settings_hide_sensors
|
||||
device.web_app_config.update_attributes!(hide_sensors: false)
|
||||
end
|
||||
|
|
|
@ -18,10 +18,6 @@ module Devices
|
|||
def settings_default_map_size_y
|
||||
device.web_app_config.update_attributes!(map_size_y: 2_900)
|
||||
end
|
||||
|
||||
def settings_map_xl
|
||||
device.web_app_config.update_attributes!(map_xl: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,7 +21,6 @@ module Devices
|
|||
def settings_default_map_size_x; end
|
||||
def settings_default_map_size_y; end
|
||||
def settings_firmware; end
|
||||
def settings_map_xl; end
|
||||
def settings_hide_sensors; end
|
||||
def tool_slots_slot_1; end
|
||||
def tool_slots_slot_2; end
|
||||
|
|
|
@ -132,7 +132,6 @@
|
|||
encoder_figure: false,
|
||||
hide_webcam_widget: false,
|
||||
legend_menu_open: true,
|
||||
map_xl: false,
|
||||
raw_encoders: false,
|
||||
scaled_encoders: false,
|
||||
show_spread: false,
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
class RemoveMapXlFromWebAppConfigs < ActiveRecord::Migration[5.2]
|
||||
safety_assured
|
||||
def change
|
||||
remove_column :web_app_configs, :map_xl, :boolean, default: false
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
class AddConfirmPlantDeletionToWebAppConfigs < ActiveRecord::Migration[5.2]
|
||||
safety_assured
|
||||
def change
|
||||
add_column :web_app_configs,
|
||||
:confirm_plant_deletion,
|
||||
:boolean,
|
||||
default: true
|
||||
end
|
||||
end
|
|
@ -1521,7 +1521,6 @@ CREATE TABLE public.web_app_configs (
|
|||
encoder_figure boolean DEFAULT false,
|
||||
hide_webcam_widget boolean DEFAULT false,
|
||||
legend_menu_open boolean DEFAULT false,
|
||||
map_xl boolean DEFAULT false,
|
||||
raw_encoders boolean DEFAULT false,
|
||||
scaled_encoders boolean DEFAULT false,
|
||||
show_spread boolean DEFAULT true,
|
||||
|
@ -1560,7 +1559,8 @@ CREATE TABLE public.web_app_configs (
|
|||
map_size_x integer DEFAULT 2900,
|
||||
map_size_y integer DEFAULT 1400,
|
||||
expand_step_options boolean DEFAULT false,
|
||||
hide_sensors boolean DEFAULT false
|
||||
hide_sensors boolean DEFAULT false,
|
||||
confirm_plant_deletion boolean DEFAULT true
|
||||
);
|
||||
|
||||
|
||||
|
@ -2759,14 +2759,6 @@ ALTER TABLE ONLY public.points
|
|||
ADD CONSTRAINT fk_rails_a62cbb8aca FOREIGN KEY (tool_id) REFERENCES public.tools(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: farmware_envs fk_rails_ab55c3a1d1; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.farmware_envs
|
||||
ADD CONSTRAINT fk_rails_ab55c3a1d1 FOREIGN KEY (device_id) REFERENCES public.devices(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: primary_nodes fk_rails_bca7fee3b9; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -2775,6 +2767,14 @@ ALTER TABLE ONLY public.primary_nodes
|
|||
ADD CONSTRAINT fk_rails_bca7fee3b9 FOREIGN KEY (sequence_id) REFERENCES public.sequences(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: farmware_envs fk_rails_bdadc396eb; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.farmware_envs
|
||||
ADD CONSTRAINT fk_rails_bdadc396eb FOREIGN KEY (device_id) REFERENCES public.devices(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: alerts fk_rails_c0132c78be; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -2979,6 +2979,8 @@ INSERT INTO "schema_migrations" (version) VALUES
|
|||
('20190515205442'),
|
||||
('20190603233157'),
|
||||
('20190605185311'),
|
||||
('20190607192429');
|
||||
('20190607192429'),
|
||||
('20190613190531'),
|
||||
('20190613215319');
|
||||
|
||||
|
||||
|
|
|
@ -270,6 +270,7 @@ export function fakeWebAppConfig(): TaggedWebAppConfig {
|
|||
device_id: idCounter++,
|
||||
created_at: "2018-01-11T20:20:38.362Z",
|
||||
updated_at: "2018-01-22T15:32:41.970Z",
|
||||
confirm_plant_deletion: true,
|
||||
confirm_step_deletion: false,
|
||||
disable_animations: false,
|
||||
disable_i18n: false,
|
||||
|
@ -278,7 +279,6 @@ export function fakeWebAppConfig(): TaggedWebAppConfig {
|
|||
encoder_figure: false,
|
||||
hide_webcam_widget: false,
|
||||
legend_menu_open: false,
|
||||
map_xl: false,
|
||||
raw_encoders: true,
|
||||
scaled_encoders: true,
|
||||
show_spread: false,
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
jest.mock("../../../config_storage/actions", () => ({
|
||||
setWebAppConfigValue: jest.fn()
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { MapSizeSetting, MapSizeSettingProps } from "../map_size_setting";
|
||||
import { mount } from "enzyme";
|
||||
import { setWebAppConfigValue } from "../../../config_storage/actions";
|
||||
import { NumericSetting } from "../../../session_keys";
|
||||
|
||||
describe("<MapSizeSetting />", () => {
|
||||
const fakeProps = (): MapSizeSettingProps => ({
|
||||
getConfigValue: () => 100,
|
||||
dispatch: jest.fn(),
|
||||
});
|
||||
|
||||
it("changes value", () => {
|
||||
const wrapper = mount(<MapSizeSetting {...fakeProps()} />);
|
||||
wrapper.find("input").last().simulate("change"), {
|
||||
currentTarget: { value: 100 }
|
||||
};
|
||||
expect(setWebAppConfigValue).toHaveBeenCalledWith(
|
||||
NumericSetting.map_size_y, "100");
|
||||
});
|
||||
});
|
|
@ -474,16 +474,12 @@ export namespace Content {
|
|||
widget from the Controls page.`);
|
||||
|
||||
export const DYNAMIC_MAP_SIZE =
|
||||
trim(`Change the Farm Designer map size based on axis length.
|
||||
trim(`Change the garden map size based on axis length.
|
||||
A value must be input in AXIS LENGTH and STOP AT MAX must be enabled in
|
||||
the HARDWARE widget. Overrides MAP SIZE values.`);
|
||||
|
||||
export const DOUBLE_MAP_DIMENSIONS =
|
||||
trim(`Double the default dimensions of the Farm Designer map
|
||||
for a map with four times the area. Overriden by MAP SIZE values.`);
|
||||
|
||||
export const PLANT_ANIMATIONS =
|
||||
trim(`Enable plant animations in the Farm Designer.`);
|
||||
trim(`Enable plant animations in the garden map.`);
|
||||
|
||||
export const BROWSER_SPEAK_LOGS =
|
||||
trim(`Have the browser also read aloud log messages on the
|
||||
|
@ -498,7 +494,7 @@ export namespace Content {
|
|||
will be discarded when refreshing or closing the page. Are you sure?`);
|
||||
|
||||
export const VIRTUAL_TRAIL =
|
||||
trim(`Display a virtual trail for FarmBot in the Farm Designer map to show
|
||||
trim(`Display a virtual trail for FarmBot in the garden map to show
|
||||
movement and watering history while the map is open. Toggling this setting
|
||||
will clear data for the current trail.`);
|
||||
|
||||
|
@ -523,9 +519,21 @@ export namespace Content {
|
|||
Are you sure you want to disable this feature?`);
|
||||
|
||||
export const MAP_SIZE =
|
||||
trim(`Specify custom Farm Designer Garden Map dimensions (in millimeters).
|
||||
These values set the size of the garden map displayed in the designer
|
||||
unless DYNAMIC MAP SIZE is enabled.`);
|
||||
trim(`Specify custom map dimensions (in millimeters).
|
||||
These values set the size of the garden map unless
|
||||
DYNAMIC MAP SIZE is enabled.`);
|
||||
|
||||
export const MAP_SWAP_XY =
|
||||
trim(`Swap map X and Y axes, making the Y axis horizontal and X axis
|
||||
vertical. This setting will also swap the X and Y jog control buttons
|
||||
in the Move widget.`);
|
||||
|
||||
export const MAP_ORIGIN =
|
||||
trim(`Select a map origin by clicking on one of the four quadrants to
|
||||
adjust the garden map to your viewing angle.`);
|
||||
|
||||
export const CONFIRM_PLANT_DELETION =
|
||||
trim(`Show a confirmation dialog when deleting a plant.`);
|
||||
|
||||
// Device
|
||||
export const NOT_HTTPS =
|
||||
|
|
|
@ -5,7 +5,7 @@ import { mount } from "enzyme";
|
|||
import { Controls } from "../controls";
|
||||
import { bot } from "../../__test_support__/fake_state/bot";
|
||||
import {
|
||||
fakePeripheral, fakeWebcamFeed, fakeSensor, fakeSensorReading
|
||||
fakePeripheral, fakeWebcamFeed, fakeSensor
|
||||
} from "../../__test_support__/fake_state/resources";
|
||||
import { Dictionary } from "farmbot";
|
||||
import { Props } from "../interfaces";
|
||||
|
@ -67,7 +67,7 @@ describe("<Controls />", () => {
|
|||
|
||||
it("doesn't show sensor readings widget", () => {
|
||||
const p = fakeProps();
|
||||
p.sensorReadings = [];
|
||||
mockConfig.hide_sensors = true;
|
||||
const wrapper = mount(<Controls {...p} />);
|
||||
const txt = wrapper.text().toLowerCase();
|
||||
expect(txt).not.toContain("sensor history");
|
||||
|
@ -75,7 +75,7 @@ describe("<Controls />", () => {
|
|||
|
||||
it("shows sensor readings widget", () => {
|
||||
const p = fakeProps();
|
||||
p.sensorReadings = [fakeSensorReading()];
|
||||
mockConfig.hide_sensors = false;
|
||||
const wrapper = mount(<Controls {...p} />);
|
||||
const txt = wrapper.text().toLowerCase();
|
||||
expect(txt).toContain("sensor history");
|
||||
|
|
|
@ -25,7 +25,7 @@ export class Controls extends React.Component<Props, {}> {
|
|||
}
|
||||
|
||||
get hideSensors() {
|
||||
return this.props.getWebAppConfigVal("hide_sensors");
|
||||
return this.props.getWebAppConfigVal(BooleanSetting.hide_sensors);
|
||||
}
|
||||
|
||||
move = () => <Move
|
||||
|
@ -47,19 +47,19 @@ export class Controls extends React.Component<Props, {}> {
|
|||
dispatch={this.props.dispatch} />
|
||||
|
||||
sensors = () => this.hideSensors
|
||||
? <div />
|
||||
? <div id="hidden-sensors-widget" />
|
||||
: <Sensors
|
||||
bot={this.props.bot}
|
||||
sensors={this.props.sensors}
|
||||
dispatch={this.props.dispatch}
|
||||
disabled={this.arduinoBusy || !this.botOnline} />
|
||||
|
||||
sensorReadings = () => this.props.sensorReadings.length > 0
|
||||
? <SensorReadings
|
||||
sensorReadings = () => this.hideSensors
|
||||
? <div id="hidden-sensor-history-widget" />
|
||||
: <SensorReadings
|
||||
sensorReadings={this.props.sensorReadings}
|
||||
sensors={this.props.sensors}
|
||||
timeSettings={this.props.timeSettings} />
|
||||
: <div id="hidden-sensor-history-widget" />
|
||||
|
||||
render() {
|
||||
const showWebcamWidget =
|
||||
|
|
|
@ -73,6 +73,7 @@
|
|||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin-right: -10px;
|
||||
padding-bottom: 5rem;
|
||||
.plant-catalog-tile {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
@ -449,86 +450,6 @@
|
|||
font-size: medium;
|
||||
cursor: pointer;
|
||||
}
|
||||
.farmbot-origin {
|
||||
.quadrants {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
border: 1px solid $dark_gray;
|
||||
}
|
||||
.quadrant {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 0, 0, 0.05) 1px, transparent 1px), linear-gradient(rgba(0, 0, 0, 0.05) 2px, transparent 2px), linear-gradient(90deg, rgba(0, 0, 0, 0.05) 2px, transparent 2px);
|
||||
background-size: 4px 4px, 4px 4px, 100px 100px, 100px 100px;
|
||||
cursor: pointer;
|
||||
border: 1px solid $dark_gray;
|
||||
width: 50%;
|
||||
height: 24px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
&.selected {
|
||||
box-shadow: inset 0 0 8px $dark_gray;
|
||||
} // Quadrant 1
|
||||
&:nth-child(2) {
|
||||
&:before {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
&:after {
|
||||
top: 8px;
|
||||
right: 16px;
|
||||
}
|
||||
} // Quadrant 2
|
||||
&:nth-child(1) {
|
||||
&:before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
&:after {
|
||||
top: 8px;
|
||||
left: 16px;
|
||||
}
|
||||
} // Quadrant 3
|
||||
&:nth-child(3) {
|
||||
&:before {
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
&:after {
|
||||
left: 16px;
|
||||
bottom: 8px;
|
||||
}
|
||||
} // Quadrant 4
|
||||
&:nth-child(4) {
|
||||
&:before {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
&:after {
|
||||
bottom: 8px;
|
||||
right: 16px;
|
||||
}
|
||||
}
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: $black;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: $green;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.more-bugs,
|
||||
.move-to-mode {
|
||||
button {
|
||||
|
@ -580,6 +501,89 @@
|
|||
}
|
||||
}
|
||||
|
||||
.farmbot-origin {
|
||||
margin: auto;
|
||||
width: 120px;
|
||||
.quadrants {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
border: 1px solid $dark_gray;
|
||||
}
|
||||
.quadrant {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 0, 0, 0.05) 1px, transparent 1px), linear-gradient(rgba(0, 0, 0, 0.05) 2px, transparent 2px), linear-gradient(90deg, rgba(0, 0, 0, 0.05) 2px, transparent 2px);
|
||||
background-size: 4px 4px, 4px 4px, 100px 100px, 100px 100px;
|
||||
cursor: pointer;
|
||||
border: 1px solid $dark_gray;
|
||||
width: 50%;
|
||||
height: 24px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
&.selected {
|
||||
box-shadow: inset 0 0 8px $dark_gray;
|
||||
} // Quadrant 1
|
||||
&:nth-child(2) {
|
||||
&:before {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
&:after {
|
||||
top: 8px;
|
||||
right: 16px;
|
||||
}
|
||||
} // Quadrant 2
|
||||
&:nth-child(1) {
|
||||
&:before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
&:after {
|
||||
top: 8px;
|
||||
left: 16px;
|
||||
}
|
||||
} // Quadrant 3
|
||||
&:nth-child(3) {
|
||||
&:before {
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
&:after {
|
||||
left: 16px;
|
||||
bottom: 8px;
|
||||
}
|
||||
} // Quadrant 4
|
||||
&:nth-child(4) {
|
||||
&:before {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
&:after {
|
||||
bottom: 8px;
|
||||
right: 16px;
|
||||
}
|
||||
}
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: $black;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: $green;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.map-points-submenu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -193,6 +193,7 @@
|
|||
max-height: calc(100vh - 19rem);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding-bottom: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -211,6 +212,7 @@
|
|||
padding-top: 5rem;
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
padding-bottom: 5rem;
|
||||
max-height: calc(100vh - 10rem);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
@ -329,6 +331,7 @@
|
|||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 2rem 1rem 6rem;
|
||||
padding-bottom: 10rem;
|
||||
li {
|
||||
margin-bottom: 1rem;
|
||||
p {
|
||||
|
@ -382,13 +385,18 @@
|
|||
}
|
||||
|
||||
.settings-panel-content {
|
||||
max-height: calc(100vh - 15rem);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin-top: 5rem;
|
||||
padding-bottom: 5rem;
|
||||
button {
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
p {
|
||||
padding: 1rem;
|
||||
padding: 0.5rem;
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
.map-size-inputs {
|
||||
.row {
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
overflow-x: hidden;
|
||||
margin-top: 1.8rem;
|
||||
margin-right: -10px;
|
||||
padding-bottom: 5rem;
|
||||
}
|
||||
|
||||
.farm-event {
|
||||
|
|
|
@ -872,11 +872,15 @@ ul {
|
|||
.farmware-button,
|
||||
.farmware-settings-menu {
|
||||
position: absolute !important;
|
||||
top: 1rem;
|
||||
top: 3rem;
|
||||
right: 3rem;
|
||||
float: right !important;
|
||||
}
|
||||
|
||||
.farmware-settings-menu {
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
.farmware-settings-menu-contents {
|
||||
label {
|
||||
margin-top: 0.5rem;
|
||||
|
|
|
@ -29,7 +29,11 @@
|
|||
margin-right: 15px;
|
||||
}
|
||||
h3 {
|
||||
max-width: 60%;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 2.5rem;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
.button-group {
|
||||
margin-right: 15px;
|
||||
|
@ -326,7 +330,7 @@
|
|||
&.open {
|
||||
display: block;
|
||||
margin: 4rem;
|
||||
margin-top: 0;
|
||||
margin-top: 1rem;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
&.farmware-info-open {
|
||||
|
|
|
@ -25,6 +25,7 @@ import { fakeTimeSettings } from "../../__test_support__/fake_time_settings";
|
|||
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
|
||||
import { fakeState } from "../../__test_support__/fake_state";
|
||||
import { edit } from "../../api/crud";
|
||||
import { BooleanSetting } from "../../session_keys";
|
||||
|
||||
describe("<FarmDesigner/>", () => {
|
||||
function fakeProps(): Props {
|
||||
|
@ -71,7 +72,6 @@ describe("<FarmDesigner/>", () => {
|
|||
expect(legendProps.showSpread).toBeFalsy();
|
||||
expect(legendProps.showFarmbot).toBeTruthy();
|
||||
expect(legendProps.showImages).toBeFalsy();
|
||||
expect(legendProps.botOriginQuadrant).toEqual(2);
|
||||
expect(legendProps.imageAgeInfo).toEqual({ newestDate: "", toOldest: 1 });
|
||||
// tslint:disable-next-line:no-any
|
||||
const gardenMapProps = wrapper.find("GardenMap").props() as any;
|
||||
|
@ -127,7 +127,7 @@ describe("<FarmDesigner/>", () => {
|
|||
state.resources = buildResourceIndex([fakeWebAppConfig()]);
|
||||
p.dispatch = jest.fn(x => x(dispatch, () => state));
|
||||
const wrapper = mount<FarmDesigner>(<FarmDesigner {...p} />);
|
||||
wrapper.instance().toggle("show_plants")();
|
||||
wrapper.instance().toggle(BooleanSetting.show_plants)();
|
||||
expect(edit).toHaveBeenCalledWith(expect.any(Object), { bot_origin_quadrant: 2 });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
jest.mock("../../config_storage/actions", () => ({
|
||||
setWebAppConfigValue: jest.fn()
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { MapSizeInputs, MapSizeInputsProps } from "../map_size_setting";
|
||||
import { mount } from "enzyme";
|
||||
import { setWebAppConfigValue } from "../../config_storage/actions";
|
||||
import { NumericSetting } from "../../session_keys";
|
||||
|
||||
describe("<MapSizeInputs />", () => {
|
||||
const fakeProps = (): MapSizeInputsProps => ({
|
||||
getConfigValue: () => 100,
|
||||
dispatch: jest.fn(),
|
||||
});
|
||||
|
||||
it("changes value", () => {
|
||||
const wrapper = mount(<MapSizeInputs {...fakeProps()} />);
|
||||
wrapper.find("input").last().simulate("change"), {
|
||||
currentTarget: { value: 100 }
|
||||
};
|
||||
expect(setWebAppConfigValue).toHaveBeenCalledWith(
|
||||
NumericSetting.map_size_y, "100");
|
||||
});
|
||||
});
|
|
@ -1,19 +1,27 @@
|
|||
jest.mock("react-redux", () => ({ connect: jest.fn() }));
|
||||
|
||||
jest.mock("../../config_storage/actions", () => ({
|
||||
getWebAppConfigValue: jest.fn(() => jest.fn(() => true)),
|
||||
getWebAppConfigValue: jest.fn(x => { x(); return jest.fn(() => true); }),
|
||||
setWebAppConfigValue: jest.fn(),
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { mount, ReactWrapper } from "enzyme";
|
||||
import {
|
||||
DesignerSettings, DesignerSettingsProps, mapStateToProps
|
||||
} from "../settings";
|
||||
import { fakeState } from "../../__test_support__/fake_state";
|
||||
import { BooleanSetting } from "../../session_keys";
|
||||
import { BooleanSetting, NumericSetting } from "../../session_keys";
|
||||
import { setWebAppConfigValue } from "../../config_storage/actions";
|
||||
|
||||
const getSetting =
|
||||
(wrapper: ReactWrapper, position: number, containsString: string) => {
|
||||
const setting = wrapper.find(".designer-setting").at(position);
|
||||
expect(setting.text().toLowerCase())
|
||||
.toContain(containsString.toLowerCase());
|
||||
return setting;
|
||||
};
|
||||
|
||||
describe("<DesignerSettings />", () => {
|
||||
const fakeProps = (): DesignerSettingsProps => ({
|
||||
dispatch: jest.fn(),
|
||||
|
@ -23,14 +31,35 @@ describe("<DesignerSettings />", () => {
|
|||
it("renders settings", () => {
|
||||
const wrapper = mount(<DesignerSettings {...fakeProps()} />);
|
||||
expect(wrapper.text()).toContain("size");
|
||||
const settings = wrapper.find(".designer-setting");
|
||||
expect(settings.length).toEqual(7);
|
||||
});
|
||||
|
||||
it("renders defaultOn setting", () => {
|
||||
const p = fakeProps();
|
||||
p.getConfigValue = () => undefined;
|
||||
const wrapper = mount(<DesignerSettings {...p} />);
|
||||
const confirmDeletion = getSetting(wrapper, 6, "confirm plant");
|
||||
expect(confirmDeletion.find("button").text()).toEqual("on");
|
||||
});
|
||||
|
||||
it("toggles setting", () => {
|
||||
const wrapper = mount(<DesignerSettings {...fakeProps()} />);
|
||||
wrapper.find("button").at(1).simulate("click");
|
||||
const trailSetting = getSetting(wrapper, 1, "trail");
|
||||
trailSetting.find("button").simulate("click");
|
||||
expect(setWebAppConfigValue)
|
||||
.toHaveBeenCalledWith(BooleanSetting.display_trail, true);
|
||||
});
|
||||
|
||||
it("changes origin", () => {
|
||||
const p = fakeProps();
|
||||
p.getConfigValue = () => 2;
|
||||
const wrapper = mount(<DesignerSettings {...p} />);
|
||||
const originSetting = getSetting(wrapper, 5, "origin");
|
||||
originSetting.find("div").last().simulate("click");
|
||||
expect(setWebAppConfigValue).toHaveBeenCalledWith(
|
||||
NumericSetting.bot_origin_quadrant, 4);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { GardenMap } from "./map/garden_map";
|
||||
import { Props, State, BotOriginQuadrant, isBotOriginQuadrant } from "./interfaces";
|
||||
import {
|
||||
Props, State, BotOriginQuadrant, isBotOriginQuadrant
|
||||
} from "./interfaces";
|
||||
import { mapStateToProps } from "./state_to_props";
|
||||
import { Plants } from "./plants/plant_inventory";
|
||||
import { GardenMapLegend } from "./map/legend/garden_map_legend";
|
||||
|
@ -11,7 +13,9 @@ import { AxisNumberProperty, BotSize } from "./map/interfaces";
|
|||
import {
|
||||
getBotSize, round, getPanelStatus, MapPanelStatus, mapPanelClassName
|
||||
} from "./map/util";
|
||||
import { calcZoomLevel, getZoomLevelIndex, saveZoomLevelIndex } from "./map/zoom";
|
||||
import {
|
||||
calcZoomLevel, getZoomLevelIndex, saveZoomLevelIndex
|
||||
} from "./map/zoom";
|
||||
import moment from "moment";
|
||||
import { DesignerNavTabs } from "./panel_header";
|
||||
import {
|
||||
|
@ -111,7 +115,6 @@ export class FarmDesigner extends React.Component<Props, Partial<State>> {
|
|||
show_farmbot,
|
||||
show_images,
|
||||
show_sensor_readings,
|
||||
bot_origin_quadrant,
|
||||
zoom_level
|
||||
} = this.state;
|
||||
|
||||
|
@ -140,8 +143,6 @@ export class FarmDesigner extends React.Component<Props, Partial<State>> {
|
|||
className={this.mapPanelClassName}
|
||||
zoom={this.updateZoomLevel}
|
||||
toggle={this.toggle}
|
||||
updateBotOriginQuadrant={this.updateBotOriginQuadrant}
|
||||
botOriginQuadrant={bot_origin_quadrant}
|
||||
legendMenuOpen={legend_menu_open}
|
||||
showPlants={show_plants}
|
||||
showPoints={show_points}
|
||||
|
@ -182,7 +183,7 @@ export class FarmDesigner extends React.Component<Props, Partial<State>> {
|
|||
stopAtHome={stopAtHome}
|
||||
hoveredPlant={this.props.hoveredPlant}
|
||||
zoomLvl={zoom_level}
|
||||
botOriginQuadrant={bot_origin_quadrant}
|
||||
botOriginQuadrant={this.getBotOriginQuadrant()}
|
||||
gridSize={getGridSize(this.props.getConfigValue, botSize)}
|
||||
gridOffset={gridOffset}
|
||||
peripherals={this.props.peripherals}
|
||||
|
|
|
@ -217,6 +217,7 @@ export interface EditPlantInfoProps {
|
|||
findPlant(stringyID: string | undefined): TaggedPlant | undefined;
|
||||
openedSavedGarden: string | undefined;
|
||||
timeSettings: TimeSettings;
|
||||
getConfigValue: GetWebAppConfigValue;
|
||||
}
|
||||
|
||||
export interface DraggableEvent {
|
||||
|
|
|
@ -4,6 +4,7 @@ jest.mock("../../../config_storage/actions", () => ({
|
|||
|
||||
import * as ZoomUtils from "../zoom";
|
||||
import { setWebAppConfigValue } from "../../../config_storage/actions";
|
||||
import { NumericSetting } from "../../../session_keys";
|
||||
|
||||
describe("zoom utilities", () => {
|
||||
it("getZoomLevelIndex()", () => {
|
||||
|
@ -12,7 +13,8 @@ describe("zoom utilities", () => {
|
|||
|
||||
it("saveZoomLevelIndex()", () => {
|
||||
ZoomUtils.saveZoomLevelIndex(jest.fn(), 9);
|
||||
expect(setWebAppConfigValue).toHaveBeenCalledWith("zoom_level", 1);
|
||||
expect(setWebAppConfigValue)
|
||||
.toHaveBeenCalledWith(NumericSetting.zoom_level, 1);
|
||||
});
|
||||
|
||||
it("calcZoomLevel()", () => {
|
||||
|
|
|
@ -31,8 +31,6 @@ export interface CropSpreadDict {
|
|||
export interface GardenMapLegendProps {
|
||||
zoom: (value: number) => () => void;
|
||||
toggle: (property: keyof State) => () => void;
|
||||
updateBotOriginQuadrant: (quadrant: number) => () => void;
|
||||
botOriginQuadrant: number;
|
||||
legendMenuOpen: boolean;
|
||||
showPlants: boolean;
|
||||
showPoints: boolean;
|
||||
|
|
|
@ -5,12 +5,10 @@ jest.mock("../../../../history", () => ({
|
|||
|
||||
let mockAtMax = false;
|
||||
let mockAtMin = false;
|
||||
jest.mock("../../zoom", () => {
|
||||
return {
|
||||
atMaxZoom: () => mockAtMax,
|
||||
atMinZoom: () => mockAtMin,
|
||||
};
|
||||
});
|
||||
jest.mock("../../zoom", () => ({
|
||||
atMaxZoom: () => mockAtMax,
|
||||
atMinZoom: () => mockAtMin,
|
||||
}));
|
||||
|
||||
let mockDev = false;
|
||||
jest.mock("../../../../account/dev/dev_support", () => ({
|
||||
|
@ -22,20 +20,20 @@ jest.mock("../../../../account/dev/dev_support", () => ({
|
|||
import * as React from "react";
|
||||
import { shallow, mount } from "enzyme";
|
||||
import {
|
||||
GardenMapLegend, ZoomControls, PointsSubMenu, RotationSelector
|
||||
GardenMapLegend, ZoomControls, PointsSubMenu
|
||||
} from "../garden_map_legend";
|
||||
import { GardenMapLegendProps } from "../../interfaces";
|
||||
import { clickButton } from "../../../../__test_support__/helpers";
|
||||
import { history } from "../../../../history";
|
||||
import { BooleanSetting } from "../../../../session_keys";
|
||||
import { fakeTimeSettings } from "../../../../__test_support__/fake_time_settings";
|
||||
import {
|
||||
fakeTimeSettings
|
||||
} from "../../../../__test_support__/fake_time_settings";
|
||||
|
||||
describe("<GardenMapLegend />", () => {
|
||||
const fakeProps = (): GardenMapLegendProps => ({
|
||||
zoom: () => () => undefined,
|
||||
toggle: () => () => undefined,
|
||||
updateBotOriginQuadrant: () => () => undefined,
|
||||
botOriginQuadrant: 2,
|
||||
legendMenuOpen: true,
|
||||
showPlants: false,
|
||||
showPoints: false,
|
||||
|
@ -52,7 +50,7 @@ describe("<GardenMapLegend />", () => {
|
|||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<GardenMapLegend {...fakeProps()} />);
|
||||
["plants", "origin", "move"].map(string =>
|
||||
["plants", "move"].map(string =>
|
||||
expect(wrapper.text().toLowerCase()).toContain(string));
|
||||
expect(wrapper.html()).toContain("filter");
|
||||
expect(wrapper.html()).not.toContain("extras");
|
||||
|
@ -117,19 +115,3 @@ describe("<PointsSubMenu />", () => {
|
|||
expect(toggle).toHaveBeenCalledWith(BooleanSetting.show_historic_points);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<RotationSelector />", () => {
|
||||
it("swaps map x&y", () => {
|
||||
const dispatch = jest.fn();
|
||||
const wrapper = mount(<RotationSelector
|
||||
dispatch={dispatch} value={false} />);
|
||||
wrapper.find("button").simulate("click");
|
||||
expect(dispatch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows correct status", () => {
|
||||
const wrapper = mount(<RotationSelector
|
||||
dispatch={jest.fn()} value={true} />);
|
||||
expect(wrapper.find("button").hasClass("green")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,43 +5,14 @@ import { history } from "../../../history";
|
|||
import { atMaxZoom, atMinZoom } from "../zoom";
|
||||
import { ImageFilterMenu } from "../layers/images/image_filter_menu";
|
||||
import { BugsControls } from "../easter_eggs/bugs";
|
||||
import { BotOriginQuadrant, State } from "../../interfaces";
|
||||
import { State } from "../../interfaces";
|
||||
import { MoveModeLink } from "../../move_to";
|
||||
import { SavedGardensLink } from "../../saved_gardens/saved_gardens";
|
||||
import {
|
||||
GetWebAppConfigValue, setWebAppConfigValue
|
||||
} from "../../../config_storage/actions";
|
||||
import { GetWebAppConfigValue } from "../../../config_storage/actions";
|
||||
import { BooleanSetting } from "../../../session_keys";
|
||||
import { DevSettings } from "../../../account/dev/dev_support";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
|
||||
const OriginSelector = ({ quadrant, update }: {
|
||||
quadrant: BotOriginQuadrant,
|
||||
update: (quadrant: number) => () => void
|
||||
}) =>
|
||||
<div className="farmbot-origin">
|
||||
<label>
|
||||
{t("Origin")}
|
||||
</label>
|
||||
<div className="quadrants">
|
||||
{[2, 1, 3, 4].map(q =>
|
||||
<div key={"quadrant_" + q}
|
||||
className={"quadrant " + (quadrant === q && "selected")}
|
||||
onClick={update(q)} />
|
||||
)}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
export const RotationSelector = ({ dispatch, value }:
|
||||
{ dispatch: Function, value: boolean }) => {
|
||||
const classNames = `fb-button fb-toggle-button ${value ? "green" : "red"}`;
|
||||
return <div className={"map-rotate-button"}>
|
||||
<label>{t("rotate")}</label>
|
||||
<button className={classNames} onClick={() =>
|
||||
dispatch(setWebAppConfigValue(BooleanSetting.xy_swap, !value))} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
export const ZoomControls = ({ zoom, getConfigValue }: {
|
||||
zoom: (value: number) => () => void,
|
||||
getConfigValue: GetWebAppConfigValue
|
||||
|
@ -85,11 +56,11 @@ const LayerToggles = (props: GardenMapLegendProps) => {
|
|||
<LayerToggle
|
||||
value={props.showPlants}
|
||||
label={t("Plants?")}
|
||||
onClick={toggle("show_plants")} />
|
||||
onClick={toggle(BooleanSetting.show_plants)} />
|
||||
<LayerToggle
|
||||
value={props.showPoints}
|
||||
label={t("Points?")}
|
||||
onClick={toggle("show_points")}
|
||||
onClick={toggle(BooleanSetting.show_points)}
|
||||
submenuTitle={t("extras")}
|
||||
popover={DevSettings.futureFeaturesEnabled()
|
||||
? <PointsSubMenu toggle={toggle} getConfigValue={getConfigValue} />
|
||||
|
@ -97,15 +68,15 @@ const LayerToggles = (props: GardenMapLegendProps) => {
|
|||
<LayerToggle
|
||||
value={props.showSpread}
|
||||
label={t("Spread?")}
|
||||
onClick={toggle("show_spread")} />
|
||||
onClick={toggle(BooleanSetting.show_spread)} />
|
||||
<LayerToggle
|
||||
value={props.showFarmbot}
|
||||
label={t("FarmBot?")}
|
||||
onClick={toggle("show_farmbot")} />
|
||||
onClick={toggle(BooleanSetting.show_farmbot)} />
|
||||
<LayerToggle
|
||||
value={props.showImages}
|
||||
label={t("Photos?")}
|
||||
onClick={toggle("show_images")}
|
||||
onClick={toggle(BooleanSetting.show_images)}
|
||||
submenuTitle={t("filter")}
|
||||
popover={<ImageFilterMenu
|
||||
timeSettings={props.timeSettings}
|
||||
|
@ -116,7 +87,7 @@ const LayerToggles = (props: GardenMapLegendProps) => {
|
|||
<LayerToggle
|
||||
value={props.showSensorReadings}
|
||||
label={t("Readings?")}
|
||||
onClick={toggle("show_sensor_readings")} />}
|
||||
onClick={toggle(BooleanSetting.show_sensor_readings)} />}
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
@ -127,7 +98,7 @@ export function GardenMapLegend(props: GardenMapLegendProps) {
|
|||
style={{ zoom: 1 }}>
|
||||
<div
|
||||
className={"menu-pullout " + menuClass}
|
||||
onClick={props.toggle("legend_menu_open")}>
|
||||
onClick={props.toggle(BooleanSetting.legend_menu_open)}>
|
||||
<span>
|
||||
{t("Menu")}
|
||||
</span>
|
||||
|
@ -136,11 +107,6 @@ export function GardenMapLegend(props: GardenMapLegendProps) {
|
|||
<div className="content">
|
||||
<ZoomControls zoom={props.zoom} getConfigValue={props.getConfigValue} />
|
||||
<LayerToggles {...props} />
|
||||
<OriginSelector
|
||||
quadrant={props.botOriginQuadrant}
|
||||
update={props.updateBotOriginQuadrant} />
|
||||
<RotationSelector dispatch={props.dispatch}
|
||||
value={!!props.getConfigValue(BooleanSetting.xy_swap)} />
|
||||
<MoveModeLink />
|
||||
<SavedGardensLink />
|
||||
<BugsControls />
|
||||
|
|
|
@ -1,20 +1,14 @@
|
|||
import * as React from "react";
|
||||
import {
|
||||
GetWebAppConfigValue, setWebAppConfigValue
|
||||
} from "../../config_storage/actions";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { Row, Col } from "../../ui";
|
||||
import { NumericSetting } from "../../session_keys";
|
||||
import { Content } from "../../constants";
|
||||
} from "../config_storage/actions";
|
||||
import { t } from "../i18next_wrapper";
|
||||
import { Row, Col } from "../ui";
|
||||
import { NumericSetting } from "../session_keys";
|
||||
import {
|
||||
NumberConfigKey as WebAppNumberConfigKey
|
||||
} from "farmbot/dist/resources/configs/web_app";
|
||||
|
||||
export interface MapSizeSettingProps {
|
||||
getConfigValue: GetWebAppConfigValue;
|
||||
dispatch: Function;
|
||||
}
|
||||
|
||||
interface LengthInputProps {
|
||||
value: number;
|
||||
label: string;
|
||||
|
@ -24,10 +18,10 @@ interface LengthInputProps {
|
|||
|
||||
const LengthInput = (props: LengthInputProps) =>
|
||||
<Row>
|
||||
<Col xs={5}>
|
||||
<Col xs={4}>
|
||||
<label style={{ float: "right" }}>{t(props.label)}</label>
|
||||
</Col>
|
||||
<Col xs={7}>
|
||||
<Col xs={5}>
|
||||
<input
|
||||
type="number"
|
||||
value={"" + props.value}
|
||||
|
@ -36,25 +30,7 @@ const LengthInput = (props: LengthInputProps) =>
|
|||
</Col>
|
||||
</Row>;
|
||||
|
||||
export const MapSizeSetting =
|
||||
({ dispatch, getConfigValue }: MapSizeSettingProps) =>
|
||||
<div className={"map-size-setting"}>
|
||||
<Row>
|
||||
<Col xs={4}>
|
||||
<label>{t("garden map size")}</label>
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<p>{t(Content.MAP_SIZE)}</p>
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<MapSizeInputs
|
||||
getConfigValue={getConfigValue}
|
||||
dispatch={dispatch} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>;
|
||||
|
||||
interface MapSizeInputsProps {
|
||||
export interface MapSizeInputsProps {
|
||||
dispatch: Function;
|
||||
getConfigValue: GetWebAppConfigValue;
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
jest.mock("react-redux", () => ({ connect: jest.fn() }));
|
||||
|
||||
jest.mock("../../../history", () => ({
|
||||
history: { push: jest.fn() },
|
||||
getPathArray: () => []
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { EditPlantInfo } from "../edit_plant_info";
|
||||
import { mount } from "enzyme";
|
||||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||
import { EditPlantInfoProps } from "../../interfaces";
|
||||
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
|
||||
|
||||
describe("<EditPlantInfo />", () => {
|
||||
const fakeProps = (): EditPlantInfoProps => ({
|
||||
push: jest.fn(),
|
||||
dispatch: jest.fn(),
|
||||
findPlant: fakePlant,
|
||||
openedSavedGarden: undefined,
|
||||
timeSettings: fakeTimeSettings(),
|
||||
});
|
||||
|
||||
it("renders", async () => {
|
||||
const wrapper = mount(<EditPlantInfo {...fakeProps()} />);
|
||||
["Strawberry Plant 1", "Plant Type", "Strawberry"].map(string =>
|
||||
expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase()));
|
||||
const buttons = wrapper.find("button");
|
||||
expect(buttons.at(1).text()).toEqual("Move FarmBot to this plant");
|
||||
expect(buttons.at(1).props().hidden).toBeFalsy();
|
||||
});
|
||||
|
||||
it("deletes plant", async () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn(() => { return Promise.resolve(); });
|
||||
const wrapper = mount(<EditPlantInfo {...p} />);
|
||||
const deleteButton = wrapper.find("button").at(2);
|
||||
expect(deleteButton.text()).toEqual("Delete");
|
||||
expect(deleteButton.props().hidden).toBeFalsy();
|
||||
deleteButton.simulate("click");
|
||||
expect(p.dispatch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -1,10 +1,17 @@
|
|||
jest.mock("react-redux", () => ({ connect: jest.fn() }));
|
||||
|
||||
let mockPath = "/app/designer/plants/1";
|
||||
jest.mock("../../../history", () => ({
|
||||
getPathArray: jest.fn(() => { return []; }),
|
||||
getPathArray: jest.fn(() => mockPath.split("/")),
|
||||
history: { push: jest.fn() }
|
||||
}));
|
||||
|
||||
jest.mock("../../../api/crud", () => ({
|
||||
destroy: jest.fn(),
|
||||
save: jest.fn(),
|
||||
edit: jest.fn(),
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { PlantInfo } from "../plant_info";
|
||||
import { mount } from "enzyme";
|
||||
|
@ -12,6 +19,7 @@ import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
|||
import { EditPlantInfoProps } from "../../interfaces";
|
||||
import { history } from "../../../history";
|
||||
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
|
||||
import { edit, save, destroy } from "../../../api/crud";
|
||||
|
||||
describe("<PlantInfo />", () => {
|
||||
const fakeProps = (): EditPlantInfoProps => ({
|
||||
|
@ -20,6 +28,7 @@ describe("<PlantInfo />", () => {
|
|||
dispatch: jest.fn(),
|
||||
openedSavedGarden: undefined,
|
||||
timeSettings: fakeTimeSettings(),
|
||||
getConfigValue: jest.fn(),
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
|
@ -27,8 +36,8 @@ describe("<PlantInfo />", () => {
|
|||
["Strawberry Plant 1", "Plant Type", "Strawberry"].map(string =>
|
||||
expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase()));
|
||||
const buttons = wrapper.find("button");
|
||||
expect(buttons.at(1).text()).toEqual("Move FarmBot to this plant");
|
||||
expect(buttons.at(1).props().hidden).toBeFalsy();
|
||||
expect(buttons.at(0).text()).toEqual("Move FarmBot to this plant");
|
||||
expect(buttons.at(1).text()).toEqual("Planned");
|
||||
});
|
||||
|
||||
it("renders: no plant", () => {
|
||||
|
@ -46,4 +55,57 @@ describe("<PlantInfo />", () => {
|
|||
expect(wrapper.find("Link").first().props().to)
|
||||
.toContain("/app/designer/plants");
|
||||
});
|
||||
|
||||
it("gets plant id", () => {
|
||||
mockPath = "/app/designer/plants/1";
|
||||
const p = fakeProps();
|
||||
p.openedSavedGarden = undefined;
|
||||
const wrapper = mount<PlantInfo>(<PlantInfo {...p} />);
|
||||
expect(wrapper.instance().stringyID).toEqual("1");
|
||||
});
|
||||
|
||||
it("gets template id", () => {
|
||||
mockPath = "/app/designer/saved_gardens/templates/2";
|
||||
const p = fakeProps();
|
||||
p.openedSavedGarden = "uuid";
|
||||
const wrapper = mount<PlantInfo>(<PlantInfo {...p} />);
|
||||
expect(wrapper.instance().stringyID).toEqual("2");
|
||||
});
|
||||
|
||||
it("handles missing plant id", () => {
|
||||
mockPath = "/app/designer/plants";
|
||||
const p = fakeProps();
|
||||
p.openedSavedGarden = undefined;
|
||||
const wrapper = mount<PlantInfo>(<PlantInfo {...p} />);
|
||||
expect(wrapper.instance().stringyID).toEqual("");
|
||||
});
|
||||
|
||||
it("updates plant", () => {
|
||||
const wrapper = mount<PlantInfo>(<PlantInfo {...fakeProps()} />);
|
||||
wrapper.instance().updatePlant("uuid", {});
|
||||
expect(edit).toHaveBeenCalled();
|
||||
expect(save).toHaveBeenCalledWith("uuid");
|
||||
});
|
||||
|
||||
it("handles missing plant", () => {
|
||||
const p = fakeProps();
|
||||
p.findPlant = jest.fn();
|
||||
const wrapper = mount<PlantInfo>(<PlantInfo {...p} />);
|
||||
wrapper.instance().updatePlant("uuid", {});
|
||||
expect(edit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("destroys plant", () => {
|
||||
const wrapper = mount<PlantInfo>(<PlantInfo {...fakeProps()} />);
|
||||
wrapper.instance().destroy("uuid");
|
||||
expect(destroy).toHaveBeenCalledWith("uuid", false);
|
||||
});
|
||||
|
||||
it("force destroys plant", () => {
|
||||
const p = fakeProps();
|
||||
p.getConfigValue = jest.fn(() => false);
|
||||
const wrapper = mount<PlantInfo>(<PlantInfo {...p} />);
|
||||
wrapper.instance().destroy("uuid");
|
||||
expect(destroy).toHaveBeenCalledWith("uuid", true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -43,8 +43,8 @@ describe("<PlantPanel/>", () => {
|
|||
expect(txt).toContain("1 days old");
|
||||
const x = wrapper.find("input").at(1).props().value;
|
||||
const y = wrapper.find("input").at(2).props().value;
|
||||
expect(x).toEqual(10);
|
||||
expect(y).toEqual(30);
|
||||
expect(x).toEqual(12);
|
||||
expect(y).toEqual(34);
|
||||
});
|
||||
|
||||
it("calls destroy", () => {
|
||||
|
@ -56,20 +56,26 @@ describe("<PlantPanel/>", () => {
|
|||
|
||||
it("renders", () => {
|
||||
const p = fakeProps();
|
||||
p.onDestroy = undefined;
|
||||
p.updatePlant = undefined;
|
||||
const wrapper = mount(<PlantPanel {...p} />);
|
||||
const txt = wrapper.text().toLowerCase();
|
||||
expect(txt).toContain("1 days old");
|
||||
expect(txt).toContain("(12, 34)");
|
||||
expect(txt).not.toContain("june");
|
||||
expect(wrapper.find("button").length).toEqual(4);
|
||||
});
|
||||
|
||||
it("renders in saved garden", () => {
|
||||
const p = fakeProps();
|
||||
p.inSavedGarden = true;
|
||||
const wrapper = mount(<PlantPanel {...p} />);
|
||||
const txt = wrapper.text().toLowerCase();
|
||||
expect(txt).toContain("june");
|
||||
expect(wrapper.find("button").length).toEqual(3);
|
||||
});
|
||||
|
||||
it("enters select mode", () => {
|
||||
const p = fakeProps();
|
||||
p.onDestroy = undefined;
|
||||
p.updatePlant = undefined;
|
||||
const wrapper = mount(<PlantPanel {...p} />);
|
||||
clickButton(wrapper, 2, "Delete multiple");
|
||||
clickButton(wrapper, 3, "Delete multiple");
|
||||
expect(history.push).toHaveBeenCalledWith("/app/designer/plants/select");
|
||||
});
|
||||
|
||||
|
@ -77,8 +83,6 @@ describe("<PlantPanel/>", () => {
|
|||
const p = fakeProps();
|
||||
const innerDispatch = jest.fn();
|
||||
p.dispatch = jest.fn(x => x(innerDispatch));
|
||||
p.onDestroy = undefined;
|
||||
p.updatePlant = undefined;
|
||||
const wrapper = mount(<PlantPanel {...p} />);
|
||||
await clickButton(wrapper, 0, "Move FarmBot to this plant");
|
||||
expect(history.push).toHaveBeenCalledWith("/app/designer/move_to");
|
||||
|
@ -90,13 +94,11 @@ describe("<PlantPanel/>", () => {
|
|||
});
|
||||
|
||||
describe("<EditPlantStatus />", () => {
|
||||
const fakeProps = (): EditPlantStatusProps => {
|
||||
return {
|
||||
uuid: "Plant.0.0",
|
||||
plantStatus: "planned",
|
||||
updatePlant: jest.fn(),
|
||||
};
|
||||
};
|
||||
const fakeProps = (): EditPlantStatusProps => ({
|
||||
uuid: "Plant.0.0",
|
||||
plantStatus: "planned",
|
||||
updatePlant: jest.fn(),
|
||||
});
|
||||
|
||||
it("changes stage to planted", () => {
|
||||
const p = fakeProps();
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { mapStateToProps, formatPlantInfo } from "./map_state_to_props";
|
||||
import { PlantInfoBase } from "./plant_info_base";
|
||||
import { PlantPanel } from "./plant_panel";
|
||||
import { TaggedPlant } from "../map/interfaces";
|
||||
import { DesignerPanel, DesignerPanelHeader } from "./designer_panel";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
|
||||
@connect(mapStateToProps)
|
||||
export class EditPlantInfo extends PlantInfoBase {
|
||||
|
||||
default = (plant_info: TaggedPlant) => {
|
||||
const info = formatPlantInfo(plant_info);
|
||||
return <DesignerPanel panelName={"plant"} panelColor={"green"}>
|
||||
<DesignerPanelHeader
|
||||
panelName={"plant"}
|
||||
title={`${t("Edit")} ${info.name}`}
|
||||
panelColor={"green"} />
|
||||
<PlantPanel
|
||||
info={info}
|
||||
onDestroy={this.destroy}
|
||||
updatePlant={this.updatePlant}
|
||||
dispatch={this.props.dispatch}
|
||||
timeSettings={this.props.timeSettings}
|
||||
inSavedGarden={!!this.props.openedSavedGarden} />
|
||||
</DesignerPanel>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const plant_info = this.plant;
|
||||
return plant_info ? this.default(plant_info) : this.fallback();
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import { history } from "../../history";
|
|||
import { PlantStage } from "farmbot";
|
||||
import { TaggedPlant } from "../map/interfaces";
|
||||
import { isNumber, get } from "lodash";
|
||||
import { getWebAppConfigValue } from "../../config_storage/actions";
|
||||
|
||||
export function mapStateToProps(props: Everything): EditPlantInfoProps {
|
||||
const openedSavedGarden =
|
||||
|
@ -28,6 +29,7 @@ export function mapStateToProps(props: Everything): EditPlantInfoProps {
|
|||
push: history.push,
|
||||
dispatch: props.dispatch,
|
||||
timeSettings: maybeGetTimeSettings(props.resources.index),
|
||||
getConfigValue: getWebAppConfigValue(() => props),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,43 @@
|
|||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { mapStateToProps, formatPlantInfo } from "./map_state_to_props";
|
||||
import { PlantInfoBase } from "./plant_info_base";
|
||||
import { PlantPanel } from "./plant_panel";
|
||||
import { unselectPlant } from "../actions";
|
||||
import { TaggedPlant } from "../map/interfaces";
|
||||
import { DesignerPanel, DesignerPanelHeader } from "./designer_panel";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { EditPlantInfoProps, PlantOptions } from "../interfaces";
|
||||
import { isString, isUndefined } from "lodash";
|
||||
import { history, getPathArray } from "../../history";
|
||||
import { destroy, edit, save } from "../../api/crud";
|
||||
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
|
||||
|
||||
@connect(mapStateToProps)
|
||||
export class PlantInfo extends PlantInfoBase {
|
||||
export class PlantInfo extends React.Component<EditPlantInfoProps, {}> {
|
||||
get templates() { return isString(this.props.openedSavedGarden); }
|
||||
get stringyID() { return getPathArray()[this.templates ? 5 : 4] || ""; }
|
||||
get plant() { return this.props.findPlant(this.stringyID); }
|
||||
get confirmDelete() {
|
||||
const confirmSetting = this.props.getConfigValue(
|
||||
"confirm_plant_deletion" as BooleanConfigKey);
|
||||
return isUndefined(confirmSetting) ? true : confirmSetting;
|
||||
}
|
||||
|
||||
destroy = (plantUUID: string) => {
|
||||
this.props.dispatch(destroy(plantUUID, !this.confirmDelete));
|
||||
}
|
||||
|
||||
updatePlant = (plantUUID: string, update: PlantOptions) => {
|
||||
if (this.plant) {
|
||||
this.props.dispatch(edit(this.plant, update));
|
||||
this.props.dispatch(save(plantUUID));
|
||||
}
|
||||
}
|
||||
|
||||
fallback = () => {
|
||||
history.push("/app/designer/plants");
|
||||
return <span>{t("Redirecting...")}</span>;
|
||||
}
|
||||
|
||||
default = (plant_info: TaggedPlant) => {
|
||||
const info = formatPlantInfo(plant_info);
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { EditPlantInfoProps, PlantOptions } from "../interfaces";
|
||||
import { history, getPathArray } from "../../history";
|
||||
import { destroy, edit, save } from "../../api/crud";
|
||||
import { isString } from "lodash";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
|
||||
export abstract class PlantInfoBase extends
|
||||
React.Component<EditPlantInfoProps, {}> {
|
||||
|
||||
get templates() { return isString(this.props.openedSavedGarden); }
|
||||
|
||||
get plantCategory() {
|
||||
return this.templates ? "saved_gardens/templates" : "plants";
|
||||
}
|
||||
|
||||
get stringyID() { return getPathArray()[this.templates ? 5 : 4] || ""; }
|
||||
get plant() { return this.props.findPlant(this.stringyID); }
|
||||
destroy = (plantUUID: string) => {
|
||||
this.props.dispatch(destroy(plantUUID));
|
||||
}
|
||||
|
||||
updatePlant = (plantUUID: string, update: PlantOptions) => {
|
||||
if (this.plant) {
|
||||
this.props.dispatch(edit(this.plant, update));
|
||||
this.props.dispatch(save(plantUUID));
|
||||
}
|
||||
}
|
||||
|
||||
fallback = () => {
|
||||
history.push("/app/designer/plants");
|
||||
return <span>{t("Redirecting...")}</span>;
|
||||
}
|
||||
|
||||
}
|
|
@ -17,8 +17,8 @@ import { TimeSettings } from "../../interfaces";
|
|||
|
||||
export interface PlantPanelProps {
|
||||
info: FormattedPlantInfo;
|
||||
onDestroy?(uuid: string): void;
|
||||
updatePlant?(uuid: string, update: PlantOptions): void;
|
||||
onDestroy(uuid: string): void;
|
||||
updatePlant(uuid: string, update: PlantOptions): void;
|
||||
inSavedGarden: boolean;
|
||||
dispatch: Function;
|
||||
timeSettings?: TimeSettings;
|
||||
|
@ -128,39 +128,35 @@ interface MoveToPlantProps {
|
|||
x: number;
|
||||
y: number;
|
||||
dispatch: Function;
|
||||
hidden: boolean;
|
||||
}
|
||||
|
||||
const MoveToPlant = (props: MoveToPlantProps) =>
|
||||
<button className="fb-button gray"
|
||||
hidden={props.hidden}
|
||||
<button className="fb-button gray no-float"
|
||||
style={{ marginTop: "1rem" }}
|
||||
onClick={() => props.dispatch(chooseLocation({ x: props.x, y: props.y }))
|
||||
.then(() => history.push("/app/designer/move_to"))}>
|
||||
{t("Move FarmBot to this plant")}
|
||||
</button>;
|
||||
|
||||
interface DeleteButtonsProps {
|
||||
hidden: boolean;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
const DeleteButtons = (props: DeleteButtonsProps) =>
|
||||
<div>
|
||||
<div>
|
||||
<label hidden={props.hidden}>
|
||||
<label>
|
||||
{t("Delete this plant")}
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
className="fb-button red"
|
||||
hidden={props.hidden}
|
||||
className="fb-button red no-float"
|
||||
onClick={props.destroy}>
|
||||
{t("Delete")}
|
||||
</button>
|
||||
<button
|
||||
className="fb-button gray"
|
||||
className="fb-button gray no-float"
|
||||
style={{ marginRight: "10px" }}
|
||||
hidden={props.hidden}
|
||||
onClick={() => history.push("/app/designer/plants/select")} >
|
||||
{t("Delete multiple")}
|
||||
</button>
|
||||
|
@ -186,10 +182,8 @@ export function PlantPanel(props: PlantPanelProps) {
|
|||
info, onDestroy, updatePlant, dispatch, inSavedGarden, timeSettings
|
||||
} = props;
|
||||
const { slug, plantedAt, daysOld, uuid, plantStatus } = info;
|
||||
let { x, y } = info;
|
||||
const isEditing = !!onDestroy;
|
||||
if (isEditing) { x = round(x); y = round(y); }
|
||||
const destroy = () => onDestroy && onDestroy(uuid);
|
||||
const { x, y } = info;
|
||||
const destroy = () => onDestroy(uuid);
|
||||
return <DesignerPanelContent panelName={"plants"}>
|
||||
<label>
|
||||
{t("Plant Info")}
|
||||
|
@ -203,7 +197,7 @@ export function PlantPanel(props: PlantPanelProps) {
|
|||
</Link>
|
||||
</ListItem>
|
||||
<ListItem name={t("Started")}>
|
||||
{(updatePlant && timeSettings && !inSavedGarden)
|
||||
{(timeSettings && !inSavedGarden)
|
||||
? <EditDatePlanted
|
||||
uuid={uuid}
|
||||
datePlanted={plantedAt}
|
||||
|
@ -215,14 +209,13 @@ export function PlantPanel(props: PlantPanelProps) {
|
|||
{`${daysOld} ${t("days old")}`}
|
||||
</ListItem>
|
||||
<ListItem name={t("Location")}>
|
||||
{updatePlant
|
||||
? <EditPlantLocation uuid={uuid}
|
||||
location={{ x, y }}
|
||||
updatePlant={updatePlant} />
|
||||
: `(${x}, ${y})`}
|
||||
<EditPlantLocation uuid={uuid}
|
||||
location={{ x, y }}
|
||||
updatePlant={updatePlant} />
|
||||
</ListItem>
|
||||
<MoveToPlant x={x} y={y} dispatch={dispatch} />
|
||||
<ListItem name={t("Status")}>
|
||||
{(updatePlant && !inSavedGarden)
|
||||
{(!inSavedGarden)
|
||||
? <EditPlantStatus
|
||||
uuid={uuid}
|
||||
plantStatus={plantStatus}
|
||||
|
@ -230,7 +223,6 @@ export function PlantPanel(props: PlantPanelProps) {
|
|||
: t(startCase(plantStatus))}
|
||||
</ListItem>
|
||||
</ul>
|
||||
<MoveToPlant x={x} y={y} dispatch={dispatch} hidden={false} />
|
||||
<DeleteButtons destroy={destroy} hidden={!isEditing} />
|
||||
<DeleteButtons destroy={destroy} />
|
||||
</DesignerPanelContent>;
|
||||
}
|
||||
|
|
|
@ -10,10 +10,11 @@ import {
|
|||
import { Row, Col } from "../ui";
|
||||
import { ToggleButton } from "../controls/toggle_button";
|
||||
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
|
||||
import { BooleanSetting } from "../session_keys";
|
||||
import { BooleanSetting, NumericSetting } from "../session_keys";
|
||||
import { resetVirtualTrail } from "./map/layers/farmbot/bot_trail";
|
||||
import { MapSizeInputs } from "../account/components/map_size_setting";
|
||||
import { MapSizeInputs } from "./map_size_setting";
|
||||
import { DesignerNavTabs } from "./panel_header";
|
||||
import { isUndefined } from "lodash";
|
||||
|
||||
export const mapStateToProps = (props: Everything): DesignerSettingsProps => ({
|
||||
dispatch: props.dispatch,
|
||||
|
@ -30,21 +31,13 @@ export class DesignerSettings
|
|||
extends React.Component<DesignerSettingsProps, {}> {
|
||||
|
||||
render() {
|
||||
const { getConfigValue, dispatch } = this.props;
|
||||
const settingsProps = { getConfigValue, dispatch };
|
||||
return <DesignerPanel panelName={"settings"} panelColor={"gray"}>
|
||||
<DesignerNavTabs />
|
||||
<DesignerPanelContent panelName={"settings"}>
|
||||
{DESIGNER_SETTINGS.map(setting =>
|
||||
<Setting key={setting.title}
|
||||
dispatch={this.props.dispatch}
|
||||
getConfigValue={this.props.getConfigValue}
|
||||
setting={setting.setting}
|
||||
title={setting.title}
|
||||
description={setting.description}
|
||||
invert={setting.invert}
|
||||
callback={setting.callback} />)}
|
||||
<MapSizeInputs
|
||||
getConfigValue={this.props.getConfigValue}
|
||||
dispatch={this.props.dispatch} />
|
||||
{DESIGNER_SETTINGS(settingsProps).map(setting =>
|
||||
<Setting key={setting.title} {...setting} {...settingsProps} />)}
|
||||
</DesignerPanelContent>
|
||||
</DesignerPanel>;
|
||||
}
|
||||
|
@ -56,14 +49,17 @@ interface SettingDescriptionProps {
|
|||
description: string;
|
||||
invert?: boolean;
|
||||
callback?: () => void;
|
||||
children?: React.ReactChild;
|
||||
defaultOn?: boolean;
|
||||
}
|
||||
|
||||
interface SettingProps
|
||||
extends DesignerSettingsProps, SettingDescriptionProps { }
|
||||
|
||||
const Setting = (props: SettingProps) => {
|
||||
const { title, setting, callback } = props;
|
||||
const value = setting ? !!props.getConfigValue(setting) : undefined;
|
||||
const { title, setting, callback, defaultOn } = props;
|
||||
const raw_value = setting ? props.getConfigValue(setting) : undefined;
|
||||
const value = (defaultOn && isUndefined(raw_value)) ? true : !!raw_value;
|
||||
return <div className="designer-setting">
|
||||
<Row>
|
||||
<Col xs={9}>
|
||||
|
@ -83,29 +79,63 @@ const Setting = (props: SettingProps) => {
|
|||
<Row>
|
||||
<p>{t(props.description)}</p>
|
||||
</Row>
|
||||
{props.children}
|
||||
</div>;
|
||||
};
|
||||
|
||||
const DESIGNER_SETTINGS: SettingDescriptionProps[] = [
|
||||
{
|
||||
title: t("Display plant animations"),
|
||||
description: t(Content.PLANT_ANIMATIONS),
|
||||
setting: BooleanSetting.disable_animations,
|
||||
invert: true
|
||||
},
|
||||
{
|
||||
title: t("Display virtual FarmBot trail"),
|
||||
description: t(Content.VIRTUAL_TRAIL),
|
||||
setting: BooleanSetting.display_trail,
|
||||
callback: resetVirtualTrail,
|
||||
},
|
||||
{
|
||||
title: t("Dynamic map size"),
|
||||
description: t(Content.DYNAMIC_MAP_SIZE),
|
||||
setting: BooleanSetting.dynamic_map,
|
||||
},
|
||||
{
|
||||
title: t("Map size"),
|
||||
description: t(Content.MAP_SIZE),
|
||||
},
|
||||
];
|
||||
const DESIGNER_SETTINGS =
|
||||
(settingsProps: DesignerSettingsProps): SettingDescriptionProps[] => ([
|
||||
{
|
||||
title: t("Display plant animations"),
|
||||
description: t(Content.PLANT_ANIMATIONS),
|
||||
setting: BooleanSetting.disable_animations,
|
||||
invert: true
|
||||
},
|
||||
{
|
||||
title: t("Display virtual FarmBot trail"),
|
||||
description: t(Content.VIRTUAL_TRAIL),
|
||||
setting: BooleanSetting.display_trail,
|
||||
callback: resetVirtualTrail,
|
||||
},
|
||||
{
|
||||
title: t("Dynamic map size"),
|
||||
description: t(Content.DYNAMIC_MAP_SIZE),
|
||||
setting: BooleanSetting.dynamic_map,
|
||||
},
|
||||
{
|
||||
title: t("Map size"),
|
||||
description: t(Content.MAP_SIZE),
|
||||
children: <MapSizeInputs {...settingsProps} />
|
||||
},
|
||||
{
|
||||
title: t("Rotate map"),
|
||||
description: t(Content.MAP_SWAP_XY),
|
||||
setting: BooleanSetting.xy_swap,
|
||||
},
|
||||
{
|
||||
title: t("Map origin"),
|
||||
description: t(Content.MAP_ORIGIN),
|
||||
children: <OriginSelector {...settingsProps} />
|
||||
},
|
||||
{
|
||||
title: t("Confirm plant deletion"),
|
||||
description: t(Content.CONFIRM_PLANT_DELETION),
|
||||
setting: "confirm_plant_deletion" as BooleanConfigKey,
|
||||
defaultOn: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const OriginSelector = (props: DesignerSettingsProps) => {
|
||||
const quadrant = props.getConfigValue(NumericSetting.bot_origin_quadrant);
|
||||
const update = (value: number) => () => props.dispatch(setWebAppConfigValue(
|
||||
NumericSetting.bot_origin_quadrant, value));
|
||||
return <div className="farmbot-origin">
|
||||
<div className="quadrants">
|
||||
{[2, 1, 3, 4].map(q =>
|
||||
<div key={"quadrant_" + q}
|
||||
className={`quadrant ${quadrant === q ? "selected" : ""}`}
|
||||
onClick={update(q)} />
|
||||
)}
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -25,6 +25,7 @@ import { getDevice } from "../../device";
|
|||
import { toggleWebAppBool } from "../../config_storage/actions";
|
||||
import { destroyAll } from "../../api/crud";
|
||||
import { success, error } from "farmbot-toastr";
|
||||
import { BooleanSetting } from "../../session_keys";
|
||||
|
||||
describe("<FarmwareConfigMenu />", () => {
|
||||
const fakeProps = (): FarmwareConfigMenuProps => ({
|
||||
|
@ -57,7 +58,8 @@ describe("<FarmwareConfigMenu />", () => {
|
|||
expect(button.hasClass("green")).toBeTruthy();
|
||||
expect(button.hasClass("fb-toggle-button")).toBeTruthy();
|
||||
button.simulate("click");
|
||||
expect(toggleWebAppBool).toHaveBeenCalledWith("show_first_party_farmware");
|
||||
expect(toggleWebAppBool).toHaveBeenCalledWith(
|
||||
BooleanSetting.show_first_party_farmware);
|
||||
});
|
||||
|
||||
it("1st party farmware display is disabled", () => {
|
||||
|
|
|
@ -7,33 +7,33 @@ import * as React from "react";
|
|||
import { mount } from "enzyme";
|
||||
import { FarmwarePage, BasicFarmwarePage } from "../index";
|
||||
import { FarmwareProps } from "../../devices/interfaces";
|
||||
import { fakeFarmware, fakeFarmwares } from "../../__test_support__/fake_farmwares";
|
||||
import {
|
||||
fakeFarmware, fakeFarmwares
|
||||
} from "../../__test_support__/fake_farmwares";
|
||||
import { clickButton } from "../../__test_support__/helpers";
|
||||
import { Actions } from "../../constants";
|
||||
import { fakeTimeSettings } from "../../__test_support__/fake_time_settings";
|
||||
|
||||
describe("<FarmwarePage />", () => {
|
||||
const fakeProps = (): FarmwareProps => {
|
||||
return {
|
||||
farmwares: fakeFarmwares(),
|
||||
botToMqttStatus: "up",
|
||||
env: {},
|
||||
user_env: {},
|
||||
dispatch: jest.fn(),
|
||||
currentImage: undefined,
|
||||
images: [],
|
||||
timeSettings: fakeTimeSettings(),
|
||||
syncStatus: "synced",
|
||||
getConfigValue: jest.fn(),
|
||||
firstPartyFarmwareNames: [],
|
||||
currentFarmware: undefined,
|
||||
shouldDisplay: () => false,
|
||||
saveFarmwareEnv: jest.fn(),
|
||||
taggedFarmwareInstallations: [],
|
||||
imageJobs: [],
|
||||
infoOpen: false,
|
||||
};
|
||||
};
|
||||
const fakeProps = (): FarmwareProps => ({
|
||||
farmwares: fakeFarmwares(),
|
||||
botToMqttStatus: "up",
|
||||
env: {},
|
||||
user_env: {},
|
||||
dispatch: jest.fn(),
|
||||
currentImage: undefined,
|
||||
images: [],
|
||||
timeSettings: fakeTimeSettings(),
|
||||
syncStatus: "synced",
|
||||
getConfigValue: jest.fn(),
|
||||
firstPartyFarmwareNames: [],
|
||||
currentFarmware: undefined,
|
||||
shouldDisplay: () => false,
|
||||
saveFarmwareEnv: jest.fn(),
|
||||
taggedFarmwareInstallations: [],
|
||||
imageJobs: [],
|
||||
infoOpen: false,
|
||||
});
|
||||
|
||||
it("renders panels", () => {
|
||||
const wrapper = mount(<FarmwarePage {...fakeProps()} />);
|
||||
|
@ -42,8 +42,24 @@ describe("<FarmwarePage />", () => {
|
|||
});
|
||||
|
||||
it("renders photos page by default", () => {
|
||||
const wrapper = mount(<FarmwarePage {...fakeProps()} />);
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<FarmwarePage {...p} />);
|
||||
expect(wrapper.text()).toContain("Take Photo");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SELECT_FARMWARE,
|
||||
payload: "Photos"
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't render photos page by default", () => {
|
||||
Object.defineProperty(window, "innerWidth", {
|
||||
value: 400,
|
||||
configurable: true
|
||||
});
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<FarmwarePage {...p} />);
|
||||
wrapper.mount();
|
||||
expect(p.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders photos page by default without farmware data", () => {
|
||||
|
|
|
@ -36,7 +36,7 @@ export class CameraCalibration extends
|
|||
lockOpen={process.env.NODE_ENV !== "production"}>
|
||||
<button
|
||||
onClick={this.props.dispatch(calibrate)}
|
||||
className="fb-button green farmware-button" >
|
||||
className="fb-button green" >
|
||||
{t("Calibrate")}
|
||||
</button>
|
||||
</MustBeOnline>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { getDevice } from "../device";
|
||||
import { FarmwareConfigMenuProps } from "./interfaces";
|
||||
import { commandErr } from "../devices/actions";
|
||||
|
@ -8,6 +7,7 @@ import { destroyAll } from "../api/crud";
|
|||
import { success, error } from "farmbot-toastr";
|
||||
import { Feature } from "../devices/interfaces";
|
||||
import { t } from "../i18next_wrapper";
|
||||
import { BooleanSetting } from "../session_keys";
|
||||
|
||||
/** First-party Farmware settings. */
|
||||
export function FarmwareConfigMenu(props: FarmwareConfigMenuProps) {
|
||||
|
@ -34,8 +34,8 @@ export function FarmwareConfigMenu(props: FarmwareConfigMenuProps) {
|
|||
</label>
|
||||
<button
|
||||
className={"fb-button fb-toggle-button " + listBtnColor}
|
||||
onClick={() =>
|
||||
props.dispatch(toggleWebAppBool("show_first_party_farmware"))} />
|
||||
onClick={() => props.dispatch(
|
||||
toggleWebAppBool(BooleanSetting.show_first_party_farmware))} />
|
||||
</fieldset>
|
||||
{props.shouldDisplay(Feature.api_farmware_env) &&
|
||||
<fieldset>
|
||||
|
|
|
@ -52,10 +52,10 @@ const getDocLinkByFarmware =
|
|||
if (farmwareName) {
|
||||
switch (urlFriendly(farmwareName).replace("-", "_")) {
|
||||
case "camera_calibration":
|
||||
return "farmware#section-camera-calibration";
|
||||
return "camera-calibration";
|
||||
case "plant_detection":
|
||||
case "weed_detector":
|
||||
return "farmware#section-weed-detector";
|
||||
return "weed-detection";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -124,10 +124,12 @@ export class FarmwarePage extends React.Component<FarmwareProps, {}> {
|
|||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.props.dispatch({
|
||||
type: Actions.SELECT_FARMWARE,
|
||||
payload: "Photos"
|
||||
});
|
||||
if (window.innerWidth > 450) {
|
||||
this.props.dispatch({
|
||||
type: Actions.SELECT_FARMWARE,
|
||||
payload: "Photos"
|
||||
});
|
||||
}
|
||||
const farmwareNames = Object.values(this.props.farmwares).map(x => x.name)
|
||||
.concat(Object.keys(FARMWARE_NAMES_1ST_PARTY));
|
||||
setActiveFarmwareByName(farmwareNames);
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
jest.mock("../../history", () => ({ history: { push: jest.fn() } }));
|
||||
jest.mock("../../history", () => ({
|
||||
history: {
|
||||
push: jest.fn(),
|
||||
getCurrentLocation: () => ({ pathname: "/app/messages" }),
|
||||
}
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { tourNames, TOUR_STEPS } from "../tours";
|
||||
|
@ -37,7 +42,9 @@ describe("<Tour />", () => {
|
|||
const steps = [TOUR_STEPS()[tourNames()[0].name][0]];
|
||||
const wrapper = shallow<Tour>(<Tour steps={steps} />);
|
||||
wrapper.instance().callback(fakeCallbackData({ type: "tour:end" }));
|
||||
expect(wrapper.state()).toEqual({ run: false, index: 0 });
|
||||
expect(wrapper.state()).toEqual({
|
||||
run: false, index: 0, returnPath: "/app/messages"
|
||||
});
|
||||
expect(history.push).toHaveBeenCalledWith("/app/messages");
|
||||
});
|
||||
|
||||
|
@ -46,7 +53,9 @@ describe("<Tour />", () => {
|
|||
const wrapper = shallow<Tour>(<Tour steps={steps} />);
|
||||
wrapper.instance().callback(
|
||||
fakeCallbackData({ action: "next", type: "step:after" }));
|
||||
expect(wrapper.state()).toEqual({ run: true, index: 1 });
|
||||
expect(wrapper.state()).toEqual({
|
||||
run: true, index: 1, returnPath: "/app/messages"
|
||||
});
|
||||
expect(history.push).toHaveBeenCalledWith("/app/tools");
|
||||
});
|
||||
|
||||
|
@ -55,7 +64,9 @@ describe("<Tour />", () => {
|
|||
const wrapper = shallow<Tour>(<Tour steps={steps} />);
|
||||
wrapper.instance().callback(
|
||||
fakeCallbackData({ action: "prev", index: 9, type: "step:after" }));
|
||||
expect(wrapper.state()).toEqual({ run: true, index: 8 });
|
||||
expect(wrapper.state()).toEqual({
|
||||
run: true, index: 8, returnPath: "/app/messages"
|
||||
});
|
||||
expect(history.push).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,10 +27,11 @@ interface TourProps {
|
|||
interface TourState {
|
||||
run: boolean;
|
||||
index: number;
|
||||
returnPath: string;
|
||||
}
|
||||
|
||||
export class Tour extends React.Component<TourProps, TourState> {
|
||||
state: TourState = { run: false, index: 0, };
|
||||
state: TourState = { run: false, index: 0, returnPath: "", };
|
||||
|
||||
callback = ({ action, index, step, type }: CallBackProps) => {
|
||||
console.log("Tour debug:", step.target, type, action);
|
||||
|
@ -45,13 +46,17 @@ export class Tour extends React.Component<TourProps, TourState> {
|
|||
}
|
||||
if (type === "tour:end") {
|
||||
this.setState({ run: false });
|
||||
history.push("/app/messages");
|
||||
history.push(this.state.returnPath);
|
||||
store.dispatch({ type: Actions.START_TOUR, payload: undefined });
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({ run: true, index: 0 });
|
||||
this.setState({
|
||||
run: true,
|
||||
index: 0,
|
||||
returnPath: history.getCurrentLocation().pathname,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -45,7 +45,6 @@ export class ActiveEditor
|
|||
collapsible={true}
|
||||
collapsed={this.state.variablesCollapsed}
|
||||
toggleVarShow={this.toggleVarShow}
|
||||
listVarLabel={t("Defined outside of regimen")}
|
||||
allowedVariableNodes={AllowedVariableNodes.parameter}
|
||||
shouldDisplay={this.props.shouldDisplay} />;
|
||||
}
|
||||
|
|
|
@ -32,8 +32,8 @@ describe("determineDropdown", () => {
|
|||
}
|
||||
}
|
||||
}, buildResourceIndex([]).index);
|
||||
expect(r.label).toBe("Defined outside of sequence");
|
||||
expect(r.value).toBe("parameter_declaration");
|
||||
expect(r.label).toBe("Externally defined");
|
||||
expect(r.value).toBe("?");
|
||||
});
|
||||
|
||||
it("Returns a label for `coordinate`", () => {
|
||||
|
@ -209,7 +209,9 @@ describe("createSequenceMeta", () => {
|
|||
describe("determineVarDDILabel()", () => {
|
||||
it("returns 'add new' variable label", () => {
|
||||
const ri = buildResourceIndex().index;
|
||||
const label = determineVarDDILabel("variable", ri, undefined);
|
||||
const label = determineVarDDILabel({
|
||||
label: "variable", resources: ri, uuid: undefined
|
||||
});
|
||||
expect(label).toEqual("Location Variable - Add new");
|
||||
});
|
||||
|
||||
|
@ -219,7 +221,9 @@ describe("determineVarDDILabel()", () => {
|
|||
data && (data.celeryNode = NOTHING_SELECTED);
|
||||
const ri = buildResourceIndex().index;
|
||||
ri.sequenceMetas = { "sequence uuid": varData };
|
||||
const label = determineVarDDILabel("variable", ri, "sequence uuid");
|
||||
const label = determineVarDDILabel({
|
||||
label: "variable", resources: ri, uuid: "sequence uuid"
|
||||
});
|
||||
expect(label).toEqual("Location Variable - Select a location");
|
||||
});
|
||||
|
||||
|
@ -236,7 +240,9 @@ describe("determineVarDDILabel()", () => {
|
|||
});
|
||||
const ri = buildResourceIndex().index;
|
||||
ri.sequenceMetas = { "sequence uuid": varData };
|
||||
const label = determineVarDDILabel("variable", ri, "sequence uuid");
|
||||
const label = determineVarDDILabel({
|
||||
label: "variable", resources: ri, uuid: "sequence uuid"
|
||||
});
|
||||
expect(label).toEqual("Location Variable - Externally defined");
|
||||
});
|
||||
|
||||
|
@ -246,7 +252,9 @@ describe("determineVarDDILabel()", () => {
|
|||
data && (data.celeryNode.kind = "variable_declaration");
|
||||
const ri = buildResourceIndex().index;
|
||||
ri.sequenceMetas = { "sequence uuid": varData };
|
||||
const label = determineVarDDILabel("variable", ri, "sequence uuid");
|
||||
const label = determineVarDDILabel({
|
||||
label: "variable", resources: ri, uuid: "sequence uuid"
|
||||
});
|
||||
expect(label).toEqual("Location Variable - variable");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -30,6 +30,7 @@ import { joinKindAndId } from "./reducer_support";
|
|||
import { chain } from "lodash";
|
||||
import { getWebAppConfig } from "./getters";
|
||||
import { TimeSettings } from "../interfaces";
|
||||
import { BooleanSetting } from "../session_keys";
|
||||
|
||||
export * from "./selectors_by_id";
|
||||
export * from "./selectors_by_kind";
|
||||
|
@ -195,7 +196,7 @@ export function maybeGetTimeOffset(index: ResourceIndex): number {
|
|||
/** Return 12/24hr time format preference if possible. If not, use 12hr. */
|
||||
export function maybeGet24HourTimeSetting(index: ResourceIndex): boolean {
|
||||
const conf = getWebAppConfig(index);
|
||||
return conf ? conf.body["time_format_24_hour"] : false;
|
||||
return conf ? conf.body[BooleanSetting.time_format_24_hour] : false;
|
||||
}
|
||||
|
||||
export function maybeGetTimeSettings(index: ResourceIndex): TimeSettings {
|
||||
|
|
|
@ -57,8 +57,17 @@ const maybeFindVariable = (
|
|||
|
||||
const withPrefix = (label: string) => `${t("Location Variable")} - ${label}`;
|
||||
|
||||
interface DetermineVarDDILabelProps {
|
||||
label: string;
|
||||
resources: ResourceIndex;
|
||||
uuid?: UUID;
|
||||
forceExternal?: boolean;
|
||||
}
|
||||
|
||||
export const determineVarDDILabel =
|
||||
(label: string, resources: ResourceIndex, uuid?: UUID): string => {
|
||||
({ label, resources, uuid, forceExternal }: DetermineVarDDILabelProps):
|
||||
string => {
|
||||
if (forceExternal) { return t("Externally defined"); }
|
||||
const variable = maybeFindVariable(label, resources, uuid);
|
||||
if (variable) {
|
||||
if (variable.celeryNode.kind === "parameter_declaration") {
|
||||
|
@ -78,8 +87,8 @@ export const determineDropdown =
|
|||
(node: VariableNode, resources: ResourceIndex, uuid?: UUID): DropDownItem => {
|
||||
if (node.kind === "parameter_declaration") {
|
||||
return {
|
||||
label: t("Defined outside of sequence"),
|
||||
value: "parameter_declaration"
|
||||
label: t("Externally defined"),
|
||||
value: "?"
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -90,7 +99,7 @@ export const determineDropdown =
|
|||
return { label: `Coordinate (${x}, ${y}, ${z})`, value: "?" };
|
||||
case "identifier":
|
||||
const { label } = data_value.args;
|
||||
const varName = determineVarDDILabel(label, resources, uuid);
|
||||
const varName = determineVarDDILabel({ label, resources, uuid });
|
||||
return { label: varName, value: "?" };
|
||||
// tslint:disable-next-line:no-any
|
||||
case "every_point" as any:
|
||||
|
|
|
@ -248,14 +248,6 @@ export const UNBOUND_ROUTES = [
|
|||
getChild: () => import("./farm_designer/plants/crop_info"),
|
||||
childKey: "CropInfo"
|
||||
}),
|
||||
route({
|
||||
children: true,
|
||||
$: "/designer/plants/:plant_id/edit",
|
||||
getModule,
|
||||
key,
|
||||
getChild: () => import("./farm_designer/plants/edit_plant_info"),
|
||||
childKey: "EditPlantInfo"
|
||||
}),
|
||||
route({
|
||||
children: true,
|
||||
$: "/designer/plants/:plant_id",
|
||||
|
@ -280,14 +272,6 @@ export const UNBOUND_ROUTES = [
|
|||
getChild: () => import("./farm_designer/plants/plant_inventory"),
|
||||
childKey: "Plants"
|
||||
}),
|
||||
route({
|
||||
children: true,
|
||||
$: "/designer/saved_gardens/templates/:plant_template_id/edit",
|
||||
getModule,
|
||||
key,
|
||||
getChild: () => import("./farm_designer/plants/edit_plant_info"),
|
||||
childKey: "EditPlantInfo"
|
||||
}),
|
||||
route({
|
||||
children: true,
|
||||
$: "/designer/saved_gardens/templates/:plant_template_id",
|
||||
|
|
|
@ -76,6 +76,7 @@ describe("<LocationForm/>", () => {
|
|||
|
||||
it("shows parent in dropdown", () => {
|
||||
const p = fakeProps();
|
||||
p.allowedVariableNodes = AllowedVariableNodes.identifier;
|
||||
p.shouldDisplay = () => true;
|
||||
const wrapper = shallow(<LocationForm {...p} />);
|
||||
expect(wrapper.find(FBSelect).first().props().list)
|
||||
|
@ -92,19 +93,18 @@ describe("<LocationForm/>", () => {
|
|||
it("shows correct variable label", () => {
|
||||
const p = fakeProps();
|
||||
p.shouldDisplay = () => true;
|
||||
p.variable.dropdown.label = "not shown";
|
||||
p.variable.dropdown.value = "parameter_declaration";
|
||||
p.listVarLabel = "Variable Label";
|
||||
p.variable.dropdown.label = "Externally defined";
|
||||
const wrapper = shallow(<LocationForm {...p} />);
|
||||
expect(wrapper.find(FBSelect).props().selectedItem).toEqual({
|
||||
label: "Variable Label", value: "parameter_declaration"
|
||||
label: "Externally defined", value: 0
|
||||
});
|
||||
expect(wrapper.find(FBSelect).first().props().list)
|
||||
.toEqual(expect.arrayContaining([PARENT(p.listVarLabel)]));
|
||||
.toEqual(expect.arrayContaining([PARENT("Externally defined")]));
|
||||
});
|
||||
|
||||
it("shows add new variable option", () => {
|
||||
const p = fakeProps();
|
||||
p.allowedVariableNodes = AllowedVariableNodes.identifier;
|
||||
p.shouldDisplay = () => true;
|
||||
p.variable.dropdown.isNull = true;
|
||||
const wrapper = shallow(<LocationForm {...p} />);
|
||||
|
|
|
@ -113,8 +113,8 @@ describe("getRegimenVariableData()", () => {
|
|||
parent1: {
|
||||
celeryNode: paramDeclaration,
|
||||
dropdown: {
|
||||
label: "Defined outside of sequence",
|
||||
value: "parameter_declaration"
|
||||
label: "Externally defined",
|
||||
value: "?"
|
||||
},
|
||||
vector: undefined
|
||||
},
|
||||
|
|
|
@ -60,7 +60,6 @@ export const LocalsList = (props: LocalsListProps) => {
|
|||
collapsible={props.collapsible}
|
||||
collapsed={props.collapsed}
|
||||
toggleVarShow={props.toggleVarShow}
|
||||
listVarLabel={props.listVarLabel}
|
||||
onChange={props.onChange} />)}
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -55,8 +55,6 @@ interface CommonProps {
|
|||
collapsible?: boolean;
|
||||
collapsed?: boolean;
|
||||
toggleVarShow?: () => void;
|
||||
/** Label to display for variable option in dropdown. */
|
||||
listVarLabel?: string;
|
||||
}
|
||||
|
||||
export interface LocalsListProps extends CommonProps {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from "react";
|
||||
import { Row, Col, FBSelect, DropDownItem } from "../../ui";
|
||||
import { Row, Col, FBSelect } from "../../ui";
|
||||
import { locationFormList, NO_VALUE_SELECTED_DDI } from "./location_form_list";
|
||||
import { convertDDItoVariable } from "../locals_list/handle_select";
|
||||
import {
|
||||
|
@ -38,14 +38,6 @@ const maybeUseStepData = ({ resources, bodyVariables, variable, uuid }: {
|
|||
return variable;
|
||||
};
|
||||
|
||||
/** Determine DropdownItem for the current LocationForm selection. */
|
||||
const selectedLabelDDI = (ddi: DropDownItem, override?: string) => {
|
||||
const newDDI = Object.assign({}, ddi);
|
||||
newDDI.label = (ddi.value === "parameter_declaration" && override)
|
||||
? override : newDDI.label;
|
||||
return newDDI;
|
||||
};
|
||||
|
||||
/**
|
||||
* Form with an "import from" dropdown and coordinate input boxes.
|
||||
* Can be used to set a specific value, import a value, or declare a variable.
|
||||
|
@ -59,8 +51,10 @@ export const LocationForm =
|
|||
});
|
||||
const displayVariables = props.shouldDisplay(Feature.variables) &&
|
||||
allowedVariableNodes !== AllowedVariableNodes.variable;
|
||||
const variableListItems = displayVariables ? [PARENT(props.listVarLabel ||
|
||||
determineVarDDILabel("parent", resources, sequenceUuid))] : [];
|
||||
const headerForm = allowedVariableNodes === AllowedVariableNodes.parameter;
|
||||
const variableListItems = displayVariables ? [PARENT(determineVarDDILabel({
|
||||
label: "parent", resources, uuid: sequenceUuid, forceExternal: headerForm
|
||||
}))] : [];
|
||||
const displayGroups = props.shouldDisplay(Feature.loops) && !disallowGroups;
|
||||
const list = locationFormList(resources, variableListItems, displayGroups);
|
||||
/** Variable name. */
|
||||
|
@ -82,7 +76,7 @@ export const LocationForm =
|
|||
<FBSelect
|
||||
key={props.locationDropdownKey}
|
||||
list={list}
|
||||
selectedItem={selectedLabelDDI(dropdown, props.listVarLabel)}
|
||||
selectedItem={dropdown}
|
||||
customNullLabel={NO_VALUE_SELECTED_DDI().label}
|
||||
onChange={ddi => props.onChange(convertDDItoVariable({
|
||||
label, allowedVariableNodes
|
||||
|
|
|
@ -90,7 +90,7 @@ export const SequenceSettingsMenu =
|
|||
label={t("Show pins")}
|
||||
description={Content.SHOW_PINS} />
|
||||
<Setting {...commonProps}
|
||||
setting={"expand_step_options"}
|
||||
setting={BooleanSetting.expand_step_options}
|
||||
label={t("Open options by default")}
|
||||
description={Content.EXPAND_STEP_OPTIONS} />
|
||||
</div>;
|
||||
|
@ -184,7 +184,6 @@ const SequenceHeader = (props: SequenceHeaderProps) => {
|
|||
collapsible={true}
|
||||
collapsed={props.variablesCollapsed}
|
||||
toggleVarShow={props.toggleVarShow}
|
||||
listVarLabel={t("Defined outside of sequence")}
|
||||
shouldDisplay={props.shouldDisplay} />
|
||||
</div>;
|
||||
};
|
||||
|
@ -222,7 +221,7 @@ export class SequenceEditorMiddleActive extends
|
|||
shouldDisplay: this.props.shouldDisplay,
|
||||
confirmStepDeletion: !!getConfig(BooleanSetting.confirm_step_deletion),
|
||||
showPins: !!getConfig(BooleanSetting.show_pins),
|
||||
expandStepOptions: !!getConfig("expand_step_options"),
|
||||
expandStepOptions: !!getConfig(BooleanSetting.expand_step_options),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -4,57 +4,69 @@ import {
|
|||
} from "farmbot/dist/resources/configs/web_app";
|
||||
|
||||
export const BooleanSetting: Record<BooleanConfigKey, BooleanConfigKey> = {
|
||||
/** Move settings */
|
||||
x_axis_inverted: "x_axis_inverted",
|
||||
y_axis_inverted: "y_axis_inverted",
|
||||
z_axis_inverted: "z_axis_inverted",
|
||||
raw_encoders: "raw_encoders",
|
||||
scaled_encoders: "scaled_encoders",
|
||||
raw_encoders: "raw_encoders",
|
||||
home_button_homing: "home_button_homing",
|
||||
show_motor_plot: "show_motor_plot",
|
||||
|
||||
/** Designer settings */
|
||||
legend_menu_open: "legend_menu_open",
|
||||
show_plants: "show_plants",
|
||||
show_points: "show_points",
|
||||
show_historic_points: "show_historic_points",
|
||||
show_spread: "show_spread",
|
||||
show_farmbot: "show_farmbot",
|
||||
show_images: "show_images",
|
||||
show_sensor_readings: "show_sensor_readings",
|
||||
xy_swap: "xy_swap",
|
||||
home_button_homing: "home_button_homing",
|
||||
show_motor_plot: "show_motor_plot",
|
||||
show_historic_points: "show_historic_points",
|
||||
time_format_24_hour: "time_format_24_hour",
|
||||
show_pins: "show_pins",
|
||||
disable_emergency_unlock_confirmation: "disable_emergency_unlock_confirmation",
|
||||
expand_step_options: "expand_step_options",
|
||||
|
||||
/** "Labs" feature names. (App preferences) */
|
||||
stub_config: "stub_config",
|
||||
disable_i18n: "disable_i18n",
|
||||
confirm_step_deletion: "confirm_step_deletion",
|
||||
hide_webcam_widget: "hide_webcam_widget",
|
||||
hide_sensors: "hide_sensors",
|
||||
dynamic_map: "dynamic_map",
|
||||
map_xl: "map_xl",
|
||||
disable_animations: "disable_animations",
|
||||
display_trail: "display_trail",
|
||||
encoder_figure: "encoder_figure",
|
||||
dynamic_map: "dynamic_map",
|
||||
xy_swap: "xy_swap",
|
||||
confirm_plant_deletion: "confirm_plant_deletion",
|
||||
|
||||
/** Sequence settings */
|
||||
confirm_step_deletion: "confirm_step_deletion",
|
||||
show_pins: "show_pins",
|
||||
expand_step_options: "expand_step_options",
|
||||
|
||||
/** App settings */
|
||||
disable_i18n: "disable_i18n",
|
||||
hide_webcam_widget: "hide_webcam_widget",
|
||||
hide_sensors: "hide_sensors",
|
||||
enable_browser_speak: "enable_browser_speak",
|
||||
discard_unsaved: "discard_unsaved",
|
||||
time_format_24_hour: "time_format_24_hour",
|
||||
disable_emergency_unlock_confirmation: "disable_emergency_unlock_confirmation",
|
||||
|
||||
/** Farmware Settings Panel */
|
||||
/** Farmware settings */
|
||||
show_first_party_farmware: "show_first_party_farmware",
|
||||
|
||||
/** Other */
|
||||
stub_config: "stub_config",
|
||||
};
|
||||
|
||||
export const NumericSetting: Record<NumberConfigKey, NumberConfigKey> = {
|
||||
bot_origin_quadrant: "bot_origin_quadrant",
|
||||
busy_log: "busy_log",
|
||||
debug_log: "debug_log",
|
||||
device_id: "device_id",
|
||||
error_log: "error_log",
|
||||
fun_log: "fun_log",
|
||||
id: "id",
|
||||
info_log: "info_log",
|
||||
/** Logs settings */
|
||||
success_log: "success_log",
|
||||
busy_log: "busy_log",
|
||||
warn_log: "warn_log",
|
||||
error_log: "error_log",
|
||||
info_log: "info_log",
|
||||
fun_log: "fun_log",
|
||||
debug_log: "debug_log",
|
||||
|
||||
/** Designer settings */
|
||||
zoom_level: "zoom_level",
|
||||
map_size_x: "map_size_x",
|
||||
map_size_y: "map_size_y",
|
||||
bot_origin_quadrant: "bot_origin_quadrant",
|
||||
|
||||
/** Other */
|
||||
id: "id",
|
||||
device_id: "device_id",
|
||||
};
|
||||
|
|
|
@ -3,8 +3,8 @@ export const BASE_URL = "https://software.farm.bot/docs/";
|
|||
/** A centralized list of all documentation slugs in the app makes it easier to
|
||||
* rename / move links in the future. */
|
||||
export const DOC_SLUGS = {
|
||||
"farmware#section-weed-detector": "Weed Detector",
|
||||
"farmware#section-camera-calibration": "Camera Calibration",
|
||||
"weed-detection": "Weed Detector",
|
||||
"camera-calibration": "Camera Calibration",
|
||||
"the-farmbot-web-app": "Web App",
|
||||
"farmware": "Farmware",
|
||||
};
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
"coveralls": "3.0.4",
|
||||
"enzyme": "3.10.0",
|
||||
"enzyme-adapter-react-16": "1.14.0",
|
||||
"farmbot": "8.0.1-rc5",
|
||||
"farmbot": "8.0.1-rc7",
|
||||
"farmbot-toastr": "1.0.3",
|
||||
"i18next": "17.0.3",
|
||||
"jest": "24.8.0",
|
||||
|
|
|
@ -21,7 +21,6 @@ describe Api::WebAppConfigsController do
|
|||
encoder_figure: false,
|
||||
hide_webcam_widget: false,
|
||||
legend_menu_open: false,
|
||||
map_xl: false,
|
||||
raw_encoders: false,
|
||||
scaled_encoders: false,
|
||||
show_spread: true,
|
||||
|
|
|
@ -61,10 +61,6 @@ describe Api::DevicesController do
|
|||
device.name
|
||||
end
|
||||
|
||||
def settings_map_xl?(device)
|
||||
device.web_app_config.map_xl
|
||||
end
|
||||
|
||||
def settings_hide_sensors?(device)
|
||||
device.web_app_config.hide_sensors
|
||||
end
|
||||
|
@ -227,7 +223,6 @@ describe Api::DevicesController do
|
|||
expect(settings_device_name?(device)).to eq("FarmBot Genesis")
|
||||
expect(settings_enable_encoders?(device)).to be(true)
|
||||
expect(settings_firmware?(device)).to eq("arduino")
|
||||
expect(settings_map_xl?(device)).to be(false)
|
||||
expect(settings_hide_sensors?(device)).to be(false)
|
||||
expect(tool_slots_slot_1?(device).name).to eq("Seeder")
|
||||
expect(tool_slots_slot_2?(device).name).to eq("Seed Bin")
|
||||
|
@ -278,7 +273,6 @@ describe Api::DevicesController do
|
|||
expect(settings_device_name?(device)).to eq("FarmBot Genesis")
|
||||
expect(settings_enable_encoders?(device)).to be(true)
|
||||
expect(settings_firmware?(device)).to eq("farmduino")
|
||||
expect(settings_map_xl?(device)).to be(false)
|
||||
expect(settings_hide_sensors?(device)).to be(false)
|
||||
expect(tool_slots_slot_1?(device).name).to eq("Seeder")
|
||||
expect(tool_slots_slot_2?(device).name).to eq("Seed Bin")
|
||||
|
@ -330,7 +324,6 @@ describe Api::DevicesController do
|
|||
expect(settings_device_name?(device)).to eq("FarmBot Genesis")
|
||||
expect(settings_enable_encoders?(device)).to be(true)
|
||||
expect(settings_firmware?(device)).to eq("farmduino_k14")
|
||||
expect(settings_map_xl?(device)).to be(false)
|
||||
expect(settings_hide_sensors?(device)).to be(false)
|
||||
expect(tool_slots_slot_1?(device).name).to eq("Seeder")
|
||||
expect(tool_slots_slot_2?(device).name).to eq("Seed Bin")
|
||||
|
@ -375,7 +368,6 @@ describe Api::DevicesController do
|
|||
expect(settings_device_name?(device)).to eq("FarmBot Genesis XL")
|
||||
expect(settings_enable_encoders?(device)).to be(true)
|
||||
expect(settings_firmware?(device)).to eq("farmduino_k14")
|
||||
expect(settings_map_xl?(device)).to be(true)
|
||||
expect(settings_hide_sensors?(device)).to be(false)
|
||||
expect(tool_slots_slot_1?(device).name).to eq("Seeder")
|
||||
expect(tool_slots_slot_2?(device).name).to eq("Seed Bin")
|
||||
|
@ -428,7 +420,6 @@ describe Api::DevicesController do
|
|||
expect(settings_device_name?(device)).to eq("FarmBot Express")
|
||||
expect(settings_enable_encoders?(device)).to be(false)
|
||||
expect(settings_firmware?(device)).to eq("express_k10")
|
||||
expect(settings_map_xl?(device)).to be(false)
|
||||
expect(settings_hide_sensors?(device)).to be(true)
|
||||
expect(tool_slots_slot_1?(device).name).to eq("Seed Trough 1")
|
||||
expect(tool_slots_slot_2?(device).name).to eq("Seed Trough 2")
|
||||
|
@ -476,7 +467,6 @@ describe Api::DevicesController do
|
|||
expect(settings_device_name?(device)).to eq("FarmBot Express XL")
|
||||
expect(settings_enable_encoders?(device)).to be(false)
|
||||
expect(settings_firmware?(device)).to eq("express_k10")
|
||||
expect(settings_map_xl?(device)).to be(false)
|
||||
expect(settings_hide_sensors?(device)).to be(true)
|
||||
expect(tool_slots_slot_1?(device).name).to eq("Seed Trough 1")
|
||||
expect(tool_slots_slot_2?(device).name).to eq("Seed Trough 2")
|
||||
|
|
|
@ -1,51 +1,72 @@
|
|||
require 'spec_helper'
|
||||
require "spec_helper"
|
||||
HAS_POINTS = JSON.parse(File.read("spec/lib/celery_script/ast_has_points.json"))
|
||||
|
||||
describe Api::SequencesController do
|
||||
before :each do
|
||||
request.headers["accept"] = 'application/json'
|
||||
request.headers["accept"] = "application/json"
|
||||
end
|
||||
|
||||
include Devise::Test::ControllerHelpers
|
||||
|
||||
describe '#create' do
|
||||
describe "#create" do
|
||||
let(:user) { FactoryBot.create(:user) }
|
||||
let(:nodes) { sequence_body_for(user) }
|
||||
|
||||
it 'handles a well formed AST in the body attribute' do
|
||||
it "provides human readable errors for empty write_pin nodes" do
|
||||
sign_in user
|
||||
body = [
|
||||
{
|
||||
kind: "write_pin",
|
||||
args: {
|
||||
pin_number: { kind: "nothing", args: {} },
|
||||
pin_value: 0,
|
||||
pin_mode: 0,
|
||||
},
|
||||
},
|
||||
]
|
||||
input = { name: "Scare Birds", body: body }
|
||||
sequence_body_for(user)
|
||||
post :create, body: input.to_json, params: { format: :json }
|
||||
expect(response.status).to eq(422)
|
||||
err = json.fetch(:body)
|
||||
expected = "You must select a Peripheral in the Control Peripheral step."
|
||||
expect(err).to eq(expected)
|
||||
end
|
||||
|
||||
it "handles a well formed AST in the body attribute" do
|
||||
sign_in user
|
||||
input = { name: "Scare Birds",
|
||||
body: nodes }
|
||||
sequence_body_for(user)
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
post :create, body: input.to_json, params: { format: :json }
|
||||
expect(response.status).to eq(200)
|
||||
expect(json[:args]).to be_kind_of(Hash)
|
||||
expect(json[:body]).to be_kind_of(Array)
|
||||
expect(json[:body].length).to eq(nodes.length)
|
||||
end
|
||||
|
||||
it 'disregards extra attrs (like `uuid`) on sequence body nodes' do
|
||||
it "disregards extra attrs (like `uuid`) on sequence body nodes" do
|
||||
sign_in user
|
||||
input = { name: "Scare Birds",
|
||||
body: nodes }
|
||||
input[:body].first[:uuid] = SecureRandom.uuid
|
||||
input[:body].first["uuid"] = SecureRandom.uuid
|
||||
sequence_body_for(user)
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
post :create, body: input.to_json, params: { format: :json }
|
||||
expect(response.status).to eq(200)
|
||||
expect(json[:args]).to be_kind_of(Hash)
|
||||
expect(json[:body]).to be_kind_of(Array)
|
||||
expect(json[:body].length).to eq(nodes.length)
|
||||
end
|
||||
|
||||
it 'creates a new sequences for a user' do
|
||||
it "creates a new sequences for a user" do
|
||||
sign_in user
|
||||
input = { name: "Scare Birds", body: [] }
|
||||
post :create, body: input.to_json, format: :json
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
it 'handles invalid params' do
|
||||
it "handles invalid params" do
|
||||
# Needed to test the `else` branch of mutate() somewhere
|
||||
sign_in user
|
||||
input = {}
|
||||
|
@ -55,32 +76,31 @@ describe Api::SequencesController do
|
|||
expect(json[:name]).to eq("Name is required")
|
||||
end
|
||||
|
||||
it 'doesnt allow nonsense in `sequence.args.locals`' do
|
||||
it "doesnt allow nonsense in `sequence.args.locals`" do
|
||||
PinBinding.destroy_all
|
||||
Sequence.destroy_all
|
||||
input = { name: "Scare Birds",
|
||||
body: [],
|
||||
# Intentional nonsense to check validation logic.
|
||||
args: { locals: { kind: "wait", args: { milliseconds: 5000 } } }
|
||||
}
|
||||
args: { locals: { kind: "wait", args: { milliseconds: 5000 } } } }
|
||||
|
||||
sign_in user
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
post :create, body: input.to_json, params: { format: :json }
|
||||
expect(response.status).to eq(422)
|
||||
expect(Sequence.last).to_not be
|
||||
xpectd = "Expected leaf 'wait' within 'sequence' to be one of: "\
|
||||
xpectd = "Expected leaf 'wait' within 'sequence' to be one of: " \
|
||||
"[\"scope_declaration\"] but got wait"
|
||||
expect(json.fetch(:body)).to eq(xpectd)
|
||||
end
|
||||
|
||||
it 'strips excess `args`' do
|
||||
it "strips excess `args`" do
|
||||
input = { name: "Scare Birds",
|
||||
body: [],
|
||||
# Intentional nonsense to check validation logic.
|
||||
args: { foo: "BAR" } }
|
||||
|
||||
sign_in user
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
post :create, body: input.to_json, params: { format: :json }
|
||||
expect(response.status).to eq(200)
|
||||
expect(json[:args][:foo]).to eq(nil)
|
||||
generated_result = CeleryScript::FetchCelery
|
||||
|
@ -89,7 +109,7 @@ describe Api::SequencesController do
|
|||
expect(generated_result.dig(:args, :foo)).to eq(nil)
|
||||
end
|
||||
|
||||
it 'disallows bad default_values' do
|
||||
it "disallows bad default_values" do
|
||||
input = {
|
||||
name: "Scare Birds",
|
||||
body: [],
|
||||
|
@ -105,24 +125,24 @@ describe Api::SequencesController do
|
|||
label: "parent",
|
||||
default_value: {
|
||||
kind: "wait",
|
||||
args: { milliseconds: 12 }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
args: { milliseconds: 12 },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
sign_in user
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
post :create, body: input.to_json, params: { format: :json }
|
||||
expect(response.status).to eq(422)
|
||||
expect(json.fetch(:body)).to include('"tool"')
|
||||
expect(json[:body]).to include("Expected leaf 'wait' within "\
|
||||
expect(json[:body]).to include("Expected leaf 'wait' within " \
|
||||
"'parameter_declaration' to be one of: [")
|
||||
end
|
||||
|
||||
it 'disallows erroneous `locals` declaration' do
|
||||
it "disallows erroneous `locals` declaration" do
|
||||
input = {
|
||||
name: "Scare Birds",
|
||||
body: [],
|
||||
|
@ -132,21 +152,21 @@ describe Api::SequencesController do
|
|||
kind: "scope_declaration",
|
||||
args: {},
|
||||
body: [
|
||||
{ kind: "wait", args: { milliseconds: 5000 } }
|
||||
]
|
||||
}
|
||||
}
|
||||
{ kind: "wait", args: { milliseconds: 5000 } },
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
sign_in user
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
post :create, body: input.to_json, params: { format: :json }
|
||||
expect(response.status).to eq(422)
|
||||
expctd =
|
||||
"Expected one of: [:variable_declaration, :parameter_declaration]"
|
||||
expect(json[:body]).to include(expctd)
|
||||
end
|
||||
|
||||
it 'allows declaration of a variable named `parent`' do
|
||||
it "allows declaration of a variable named `parent`" do
|
||||
input = {
|
||||
name: "Scare Birds",
|
||||
args: {
|
||||
|
@ -163,10 +183,10 @@ describe Api::SequencesController do
|
|||
args: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
z: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "variable_declaration",
|
||||
|
@ -174,12 +194,12 @@ describe Api::SequencesController do
|
|||
label: "parent2",
|
||||
data_value: {
|
||||
kind: "coordinate",
|
||||
args: { x: 9, y: 9, z: 9, }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
args: { x: 9, y: 9, z: 9 },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
body: [
|
||||
{
|
||||
|
@ -187,30 +207,34 @@ describe Api::SequencesController do
|
|||
args: {
|
||||
location: {
|
||||
kind: "identifier",
|
||||
args: { label: "parent" } },
|
||||
offset: {
|
||||
args: { label: "parent" },
|
||||
},
|
||||
offset: {
|
||||
kind: "coordinate",
|
||||
args: { x: 0, y: 0, z: 0 } },
|
||||
speed: 100,
|
||||
}
|
||||
args: { x: 0, y: 0, z: 0 },
|
||||
},
|
||||
speed: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "move_absolute",
|
||||
args: {
|
||||
location: {
|
||||
kind: "identifier",
|
||||
args: { label: "parent2" } },
|
||||
offset: {
|
||||
args: { label: "parent2" },
|
||||
},
|
||||
offset: {
|
||||
kind: "coordinate",
|
||||
args: { x: 0, y: 0, z: 0 } },
|
||||
speed: 100,
|
||||
}
|
||||
}
|
||||
]
|
||||
args: { x: 0, y: 0, z: 0 },
|
||||
},
|
||||
speed: 100,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
sign_in user
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
post :create, body: input.to_json, params: { format: :json }
|
||||
expect(response.status).to eq(200)
|
||||
dig_path = [:args, :locals, :body, 0, :args, :label]
|
||||
generated_result = CeleryScript::FetchCelery
|
||||
|
@ -220,7 +244,7 @@ describe Api::SequencesController do
|
|||
expect(json.dig(*dig_path)).to eq("parent")
|
||||
end
|
||||
|
||||
it 'tracks Points' do
|
||||
it "tracks Points" do
|
||||
point = FactoryBot.create(:generic_pointer, device: user.device)
|
||||
PinBinding.destroy_all
|
||||
Sequence.destroy_all
|
||||
|
@ -231,14 +255,14 @@ describe Api::SequencesController do
|
|||
sign_in user
|
||||
input = { name: "Scare Birds", body: HAS_POINTS["body"] }
|
||||
sequence_body_for(user)
|
||||
before = EdgeNode.where(kind: "pointer_id").count
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
before = EdgeNode.where(kind: "pointer_id").count
|
||||
post :create, body: input.to_json, params: { format: :json }
|
||||
expect(response.status).to eq(200)
|
||||
now = EdgeNode.where(kind: "pointer_id").count
|
||||
expect(now).to be > before
|
||||
end
|
||||
|
||||
it 'prevents unbound variables' do
|
||||
it "prevents unbound variables" do
|
||||
sign_in user
|
||||
input = {
|
||||
name: "Unbound Variable Exception",
|
||||
|
@ -249,23 +273,23 @@ describe Api::SequencesController do
|
|||
args: {
|
||||
location: {
|
||||
kind: "identifier",
|
||||
args: { label: "parent" }
|
||||
args: { label: "parent" },
|
||||
},
|
||||
offset: {
|
||||
offset: {
|
||||
kind: "coordinate",
|
||||
args: { x: 0, y: 0, z: 0 }
|
||||
args: { x: 0, y: 0, z: 0 },
|
||||
},
|
||||
speed: 100,
|
||||
}
|
||||
}
|
||||
]
|
||||
speed: 100,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
post :create, body: input.to_json, params: { format: :json }
|
||||
expect(response.status).to eq(422)
|
||||
expect(json[:body]).to eq("Unbound variable: parent")
|
||||
end
|
||||
|
||||
it 'does not let you use other peoples point resources' do
|
||||
it "does not let you use other peoples point resources" do
|
||||
sign_in user
|
||||
not_yours = FactoryBot.create(:plant)
|
||||
expect(not_yours.device_id).to_not eq(user.device_id)
|
||||
|
@ -277,52 +301,50 @@ describe Api::SequencesController do
|
|||
kind: "move_absolute",
|
||||
args: {
|
||||
location: {
|
||||
kind: "point",
|
||||
args: { pointer_type: "Plant", pointer_id: not_yours.id }
|
||||
},
|
||||
speed: 100,
|
||||
offset: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } }
|
||||
}
|
||||
}
|
||||
kind: "point",
|
||||
args: { pointer_type: "Plant", pointer_id: not_yours.id },
|
||||
},
|
||||
speed: 100,
|
||||
offset: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } },
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
post :create, body: input.to_json, params: { format: :json }
|
||||
expect(response.status).to eq(422)
|
||||
expect(json[:body]).to include("Bad point ID")
|
||||
end
|
||||
|
||||
it 'prevents type errors from bad identifier / binding combos' do
|
||||
it "prevents type errors from bad identifier / binding combos" do
|
||||
sign_in user
|
||||
input = { name: "type mismatch",
|
||||
args: {
|
||||
locals: {
|
||||
kind: "scope_declaration",
|
||||
args: {},
|
||||
body: [
|
||||
{
|
||||
kind: "parameter_declaration",
|
||||
args: {
|
||||
label: "parent",
|
||||
default_value: { kind: "sync", args: {} }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
body: [
|
||||
{ kind: "move_absolute",
|
||||
args: {
|
||||
location: { kind: "identifier", args: { label: "parent" } },
|
||||
offset: {
|
||||
kind: "coordinate",
|
||||
args: { x: 0, y: 0, z: 0 }
|
||||
},
|
||||
speed: 100,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
args: {
|
||||
locals: {
|
||||
kind: "scope_declaration",
|
||||
args: {},
|
||||
body: [
|
||||
{
|
||||
kind: "parameter_declaration",
|
||||
args: {
|
||||
label: "parent",
|
||||
default_value: { kind: "sync", args: {} },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
body: [
|
||||
{ kind: "move_absolute",
|
||||
args: {
|
||||
location: { kind: "identifier", args: { label: "parent" } },
|
||||
offset: {
|
||||
kind: "coordinate",
|
||||
args: { x: 0, y: 0, z: 0 },
|
||||
},
|
||||
speed: 100,
|
||||
} },
|
||||
] }
|
||||
post :create, body: input.to_json, params: { format: :json }
|
||||
expect(response.status).to eq(422)
|
||||
expect(json.fetch(:body)).to include('"point"')
|
||||
expect(json[:body]).to include("but got sync")
|
||||
|
@ -331,27 +353,26 @@ describe Api::SequencesController do
|
|||
it 'provides human readable errors for "nothing" mismatches' do
|
||||
sign_in user
|
||||
input = { name: "type mismatch",
|
||||
args: {
|
||||
locals: {
|
||||
kind: "scope_declaration",
|
||||
args: { },
|
||||
body: [
|
||||
{
|
||||
kind: "parameter_declaration",
|
||||
args: {
|
||||
label: "x",
|
||||
default_value: {
|
||||
kind: "nothing",
|
||||
args: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
args: {
|
||||
locals: {
|
||||
kind: "scope_declaration",
|
||||
args: {},
|
||||
body: [
|
||||
{
|
||||
kind: "parameter_declaration",
|
||||
args: {
|
||||
label: "x",
|
||||
default_value: {
|
||||
kind: "nothing",
|
||||
args: {},
|
||||
},
|
||||
body: [ ]
|
||||
}
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
body: [] }
|
||||
post :create, body: input.to_json, params: { format: :json }
|
||||
expect(response.status).to eq(422)
|
||||
expect(json[:body]).to include("must provide a value for all parameters")
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue