cleanup and refactoring

pull/1652/head
gabrielburnworth 2020-01-03 12:13:49 -08:00
parent 9d7833b71c
commit 781ac33b10
31 changed files with 211 additions and 323 deletions

View File

@ -26,6 +26,7 @@ module CeleryScript
FRIENDLY_ERRORS = { FRIENDLY_ERRORS = {
nothing: { nothing: {
write_pin: "You must select a Peripheral in the Control Peripheral step.", write_pin: "You must select a Peripheral in the Control Peripheral step.",
toggle_pin: "You must select a Peripheral in the Toggle Peripheral step.",
variable_declaration: MISSING_VAR, variable_declaration: MISSING_VAR,
parameter_declaration: MISSING_PARAM, parameter_declaration: MISSING_PARAM,
read_pin: "You must select a Sensor in the Read Sensor step.", read_pin: "You must select a Sensor in the Read Sensor step.",

View File

@ -300,7 +300,9 @@ export namespace ToolTips {
trim(`Power cycle FarmBot's onboard computer or microcontroller.`); trim(`Power cycle FarmBot's onboard computer or microcontroller.`);
export const SET_SERVO_ANGLE = export const SET_SERVO_ANGLE =
trim(`Move a servo to the provided angle.`); trim(`Move a servo to the provided angle. An angle of 90 degrees
corresponds to the servo midpoint (or, for a continuous rotation
servo, no movement).`);
export const TOGGLE_PIN = export const TOGGLE_PIN =
trim(`Toggle a digital pin on or off.`); trim(`Toggle a digital pin on or off.`);

View File

@ -23,6 +23,12 @@
margin-left: 1rem; margin-left: 1rem;
box-shadow: none; box-shadow: none;
} }
input[type="radio"] {
margin-right: 0.25rem;
margin-bottom: 0.25rem;
margin-top: 0;
vertical-align: middle;
}
} }
} }
} }
@ -107,7 +113,7 @@
background: $blue; background: $blue;
} }
&.toggle-pin-step { &.toggle-pin-step {
background: $yellow; background: $orange;
} }
&.set-zero-step { &.set-zero-step {
background: $blue; background: $blue;
@ -124,6 +130,12 @@
&.system-action-step { &.system-action-step {
background: $brown; background: $brown;
} }
&.emergency-stop-step {
background: $brown;
}
&.reboot-step {
background: $brown;
}
&.unknown-step { &.unknown-step {
background: $gray; background: $gray;
} }
@ -221,7 +233,7 @@
background: $light_blue; background: $light_blue;
} }
&.toggle-pin-step { &.toggle-pin-step {
background: $light_yellow; background: $light_orange;
} }
&.set-zero-step { &.set-zero-step {
background: $light_blue; background: $light_blue;
@ -238,6 +250,12 @@
&.system-action-step { &.system-action-step {
background: $light_brown; background: $light_brown;
} }
&.emergency-stop-step {
background: $light_brown;
}
&.reboot-step {
background: $light_brown;
}
&.unknown-step { &.unknown-step {
background: $light_gray; background: $light_gray;
} }

View File

@ -6,7 +6,7 @@ jest.mock("../../draggable/actions", () => ({
stepGet: jest.fn(() => () => mockStepGetResult), stepGet: jest.fn(() => () => mockStepGetResult),
})); }));
import { FolderNode } from "../constants"; import { FolderNode } from "../interfaces";
import { ingest } from "../data_transfer"; import { ingest } from "../data_transfer";
import { import {
collapseAll, collapseAll,
@ -240,11 +240,11 @@ describe("deleteFolder", () => {
describe("updateSearchTerm", () => { describe("updateSearchTerm", () => {
it("updates a search term", () => { it("updates a search term", () => {
const argss = const args =
(payload: string | undefined) => ({ type: "FOLDER_SEARCH", payload }); (payload: string | undefined) => ({ type: "FOLDER_SEARCH", payload });
[undefined, "foo"].map(term => { [undefined, "foo"].map(term => {
updateSearchTerm(term); updateSearchTerm(term);
expect(store.dispatch).toHaveBeenCalledWith(argss(term)); expect(store.dispatch).toHaveBeenCalledWith(args(term));
}); });
}); });
}); });

View File

@ -35,7 +35,7 @@ import {
FolderNameInputProps, FolderNameInputProps,
FolderNodeMedial, FolderNodeMedial,
FolderNodeTerminal, FolderNodeTerminal,
} from "../constants"; } from "../interfaces";
import { import {
updateSearchTerm, toggleAll, moveSequence, dropSequence, updateSearchTerm, toggleAll, moveSequence, dropSequence,
sequenceEditMaybeSave, sequenceEditMaybeSave,

View File

@ -1,4 +1,4 @@
import { FolderNode } from "../constants"; import { FolderNode } from "../interfaces";
import { ingest } from "../data_transfer"; import { ingest } from "../data_transfer";
import { climb } from "../climb"; import { climb } from "../climb";

View File

@ -1,7 +1,7 @@
import { TEST_GRAPH } from "./actions_test"; import { TEST_GRAPH } from "./actions_test";
import { searchFolderTree, FolderSearchProps } from "../search_folder_tree"; import { searchFolderTree, FolderSearchProps } from "../search_folder_tree";
import { TaggedResource } from "farmbot"; import { TaggedResource } from "farmbot";
import { FolderUnion } from "../constants"; import { FolderUnion } from "../interfaces";
describe("searchFolderTree", () => { describe("searchFolderTree", () => {
const searchFor = (input: string) => searchFolderTree({ const searchFor = (input: string) => searchFolderTree({
@ -19,7 +19,7 @@ describe("searchFolderTree", () => {
expect(before).toEqual(after); // Prevent mutation of original data. expect(before).toEqual(after); // Prevent mutation of original data.
}); });
it("finds an `inital` folder", () => { it("finds an `initial` folder", () => {
const results = searchFor("one").map(x => x.name); const results = searchFor("one").map(x => x.name);
expect(results.length).toEqual(1); expect(results.length).toEqual(1);
expect(results).toContain("One"); expect(results).toContain("One");

View File

@ -1,4 +1,4 @@
import { RootFolderNode as Tree } from "./constants"; import { RootFolderNode as Tree } from "./interfaces";
import { cloneAndClimb } from "./climb"; import { cloneAndClimb } from "./climb";
import { Color, SpecialStatus, TaggedSequence } from "farmbot"; import { Color, SpecialStatus, TaggedSequence } from "farmbot";
import { store } from "../redux/store"; import { store } from "../redux/store";

View File

@ -1,4 +1,6 @@
import { RootFolderNode, FolderUnion, FolderNodeMedial, FolderNodeInitial } from "./constants"; import {
RootFolderNode, FolderUnion, FolderNodeMedial, FolderNodeInitial
} from "./interfaces";
import { defensiveClone } from "../util"; import { defensiveClone } from "../util";
interface TreeClimberState { interface TreeClimberState {

View File

@ -20,7 +20,7 @@ import {
FolderButtonClusterProps, FolderButtonClusterProps,
FolderNameInputProps, FolderNameInputProps,
SequenceDropAreaState, SequenceDropAreaState,
} from "./constants"; } from "./interfaces";
import { import {
createFolder, createFolder,
deleteFolder, deleteFolder,

View File

@ -4,7 +4,7 @@ import {
FolderNodeTerminal, FolderNodeTerminal,
RootFolderNode, RootFolderNode,
FolderMeta, FolderMeta,
} from "./constants"; } from "./interfaces";
import { sortBy } from "lodash"; import { sortBy } from "lodash";
type FoldersIndexedByParentId = Record<number, FolderNode[]>; type FoldersIndexedByParentId = Record<number, FolderNode[]>;

View File

@ -1,4 +1,4 @@
import { FolderProps } from "./constants"; import { FolderProps } from "./interfaces";
import { selectAllSequences } from "../resources/selectors"; import { selectAllSequences } from "../resources/selectors";
import { TaggedSequence } from "farmbot"; import { TaggedSequence } from "farmbot";
import { resourceUsageList } from "../resources/in_use"; import { resourceUsageList } from "../resources/in_use";

View File

@ -1,5 +1,5 @@
import { TaggedResource, TaggedSequence } from "farmbot"; import { TaggedResource, TaggedSequence } from "farmbot";
import { RootFolderNode, FolderUnion } from "./constants"; import { RootFolderNode, FolderUnion } from "./interfaces";
export interface FolderSearchProps { export interface FolderSearchProps {
references: Record<string, TaggedResource | undefined>; references: Record<string, TaggedResource | undefined>;

View File

@ -14,7 +14,7 @@ import { HelpState } from "../help/reducer";
import { UsageIndex } from "./in_use"; import { UsageIndex } from "./in_use";
import { SequenceMeta } from "./sequence_meta"; import { SequenceMeta } from "./sequence_meta";
import { AlertReducerState } from "../messages/interfaces"; import { AlertReducerState } from "../messages/interfaces";
import { RootFolderNode, FolderMeta } from "../folders/constants"; import { RootFolderNode, FolderMeta } from "../folders/interfaces";
export type UUID = string; export type UUID = string;
export type VariableNameSet = Record<string, SequenceMeta | undefined>; export type VariableNameSet = Record<string, SequenceMeta | undefined>;

View File

@ -33,7 +33,9 @@ import { get } from "lodash";
import { Actions } from "../constants"; import { Actions } from "../constants";
import { getFbosConfig } from "./getters"; import { getFbosConfig } from "./getters";
import { ingest, PARENTLESS as NO_PARENT } from "../folders/data_transfer"; import { ingest, PARENTLESS as NO_PARENT } from "../folders/data_transfer";
import { FolderNode, FolderMeta, FolderNodeTerminal, FolderNodeMedial } from "../folders/constants"; import {
FolderNode, FolderMeta, FolderNodeTerminal, FolderNodeMedial
} from "../folders/interfaces";
import { climb } from "../folders/climb"; import { climb } from "../folders/climb";
export function findByUuid(index: ResourceIndex, uuid: string): TaggedResource { export function findByUuid(index: ResourceIndex, uuid: string): TaggedResource {

View File

@ -52,7 +52,7 @@ export function StepButtonCluster(props: StepButtonProps) {
step={{ step={{
kind: "toggle_pin", kind: "toggle_pin",
args: { args: {
pin_number: 4 pin_number: NOTHING_SELECTED
} }
}} }}
color="orange"> color="orange">

View File

@ -97,61 +97,6 @@ describe("renderCeleryNode()", () => {
} }
const TEST_DATA: TestData[] = [ const TEST_DATA: TestData[] = [
{
expected: "MarkPlantasx = 300",
node: {
kind: "resource_update",
args: {
resource_id: 23,
resource_type: "Plant",
label: "x",
value: 300
}
}
},
{
expected: "System",
node: {
kind: "check_updates",
args: {
package: "farmbot_os"
}
}
},
{
expected: "System",
node: {
kind: "factory_reset",
args: {
package: "farmbot_os"
}
}
},
{
expected: "Unlocking a device requires user intervention",
node: {
kind: "emergency_lock",
args: {}
}
},
{
expected: "Unable to properly display this step",
node: {
kind: "power_off",
args: {}
}
},
{
expected: "Unable to properly display this step",
node: { kind: "read_status", args: {} }
},
{
expected: "",
node: {
kind: "install_first_party_farmware",
args: {}
}
},
{ {
node: { node: {
kind: "_if", kind: "_if",
@ -166,31 +111,13 @@ describe("renderCeleryNode()", () => {
expected: "Then Execute" expected: "Then Execute"
}, },
{ {
node: { node: { kind: "execute_script", args: { label: "farmware-to-execute" } },
kind: "execute_script",
args: { label: "farmware-to-execute" }
},
expected: "Manual Input" expected: "Manual Input"
}, },
{ {
node: { node: { kind: "execute", args: { sequence_id: 0 } },
kind: "execute",
args: {
sequence_id: 0
}
},
expected: "Select a sequence" expected: "Select a sequence"
}, },
{
node: {
kind: "find_home",
args: {
speed: 100,
axis: "all"
}
},
expected: "Find x"
},
{ {
node: { node: {
kind: "move_absolute", kind: "move_absolute",
@ -213,40 +140,37 @@ describe("renderCeleryNode()", () => {
expected: "Message" expected: "Message"
}, },
{ {
node: { node: { kind: "take_photo", args: {} },
kind: "take_photo",
args: {}
},
expected: "Photo" expected: "Photo"
}, },
{ {
node: { node: { kind: "wait", args: { milliseconds: 100 } },
kind: "wait",
args: {
milliseconds: 100
}
},
expected: "milliseconds" expected: "milliseconds"
}, },
{ {
node: { node: {
kind: "set_servo_angle", kind: "resource_update",
args: { args: {
pin_number: 4, resource_id: 23,
pin_value: 90, resource_type: "Plant",
label: "x",
value: 300
} }
}, },
expected: "MarkPlantasx = 300"
},
{
node: { kind: "set_servo_angle", args: { pin_number: 4, pin_value: 90, } },
expected: "Servo" expected: "Servo"
}, },
{ {
node: { node: { kind: "toggle_pin", args: { pin_number: 13 } },
kind: "toggle_pin",
args: {
pin_number: 13
}
},
expected: "Pin" expected: "Pin"
}, },
{
node: { kind: "find_home", args: { speed: 100, axis: "all" } },
expected: "Find x"
},
{ {
node: { kind: "zero", args: { axis: "all" } }, node: { kind: "zero", args: { axis: "all" } },
expected: "Zero x" expected: "Zero x"
@ -259,52 +183,36 @@ describe("renderCeleryNode()", () => {
node: { kind: "home", args: { axis: "all", speed: 100, } }, node: { kind: "home", args: { axis: "all", speed: 100, } },
expected: "Home x" expected: "Home x"
}, },
{
node: { kind: "reboot", args: { package: "farmbot_os" } },
expected: "System"
},
{
node: {
kind: "check_updates", args: { package: "farmbot_os" }
},
expected: "System"
},
{
node: {
kind: "factory_reset", args: { package: "farmbot_os" }
},
expected: "System"
},
{
node: {
kind: "sync",
args: {}
},
expected: ""
},
{
node: {
kind: "dump_info",
args: {}
},
expected: ""
},
{
node: {
kind: "power_off",
args: {}
},
expected: ""
},
{
node: {
kind: "read_status",
args: {}
},
expected: ""
},
{ {
node: { kind: "emergency_lock", args: {} }, node: { kind: "emergency_lock", args: {} },
expected: "Unlocking a device requires user intervention"
},
{
node: { kind: "reboot", args: { package: "farmbot_os" } },
expected: "entire system"
},
{
node: { kind: "check_updates", args: { package: "farmbot_os" } },
expected: "System"
},
{
node: { kind: "factory_reset", args: { package: "farmbot_os" } },
expected: "System"
},
{
node: { kind: "sync", args: {} },
expected: ""
},
{
node: { kind: "dump_info", args: {} },
expected: ""
},
{
node: { kind: "power_off", args: {} },
expected: ""
},
{
node: { kind: "read_status", args: {} },
expected: "" expected: ""
}, },
{ {
@ -312,19 +220,12 @@ describe("renderCeleryNode()", () => {
expected: "" expected: ""
}, },
{ {
node: { node: { kind: "install_first_party_farmware", args: {} },
kind: "install_first_party_farmware", args: {}
},
expected: "" expected: ""
}, },
{ {
node: { // tslint:disable-next-line: no-any
kind: "unknown", node: { kind: "unknown", args: { unknown: 0 } } as any,
args: {
unknown: 0
}
// tslint:disable-next-line:no-any
} as any,
expected: "unknown" expected: "unknown"
}, },
]; ];

View File

@ -1,13 +1,12 @@
jest.mock("../../api/crud", () => { jest.mock("../../../api/crud", () => ({ editStep: jest.fn() }));
return { editStep: jest.fn() };
}); import { TileReboot, editTheRebootStep, rebootExecutor } from "../tile_reboot";
import { TileReboot, editTheRebootStep, rebootExecutor, MultiChoiceRadio } from "../step_tiles/tile_reboot"; import { render } from "enzyme";
import { render, mount } from "enzyme";
import React from "react"; import React from "react";
import { StepParams } from "../interfaces"; import { StepParams } from "../../interfaces";
import { fakeSequence } from "../../__test_support__/fake_state/resources"; import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { buildResourceIndex } from "../../__test_support__/resource_index_builder"; import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
import { editStep } from "../../api/crud"; import { editStep } from "../../../api/crud";
import { Reboot } from "farmbot"; import { Reboot } from "farmbot";
const fakeProps = (): StepParams => { const fakeProps = (): StepParams => {
@ -66,21 +65,3 @@ describe("<TileReboot/>", () => {
expect(step.args.package).toBe("arduino_firmware"); expect(step.args.package).toBe("arduino_firmware");
}); });
}); });
describe("MultiChoiceRadio", () => {
it("triggers callbacks", () => {
const PACKAGE_CHOICES = {
"a": "1",
"b": "2"
};
const onChange = jest.fn();
const el = mount(<MultiChoiceRadio
uuid={"WHATEVER"}
choices={PACKAGE_CHOICES}
currentChoice={"a"}
onChange={onChange} />);
const radio = el.find("input[type='radio']").first();
radio.simulate("change", "a");
expect(onChange).toHaveBeenCalledWith("a");
});
});

View File

@ -1,7 +1,9 @@
jest.mock("../../../api/crud", () => ({ editStep: jest.fn() })); jest.mock("../../../api/crud", () => ({ editStep: jest.fn() }));
import * as React from "react"; import * as React from "react";
import { TileSetServoAngle, pinNumberChanger, createServoEditFn, ServoPinSelection } from "../tile_set_servo_angle"; import {
TileSetServoAngle, pinNumberChanger, createServoEditFn, ServoPinSelection
} from "../tile_set_servo_angle";
import { mount } from "enzyme"; import { mount } from "enzyme";
import { fakeSequence } from "../../../__test_support__/fake_state/resources"; import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { SetServoAngle } from "farmbot"; import { SetServoAngle } from "farmbot";
@ -33,8 +35,8 @@ describe("<TileSetServoAngle/>", () => {
const inputs = block.find("input"); const inputs = block.find("input");
const labels = block.find("label"); const labels = block.find("label");
const stepArgs = props.currentStep.args as SetServoAngle["args"]; const stepArgs = props.currentStep.args as SetServoAngle["args"];
expect(inputs.length).toEqual(4); expect(inputs.length).toEqual(6);
expect(labels.length).toEqual(4); expect(labels.length).toEqual(6);
expect(inputs.first().props().placeholder).toEqual("Control Servo"); expect(inputs.first().props().placeholder).toEqual("Control Servo");
expect(labels.at(0).text()).toContain("Servo angle (0-180)"); expect(labels.at(0).text()).toContain("Servo angle (0-180)");
expect(inputs.at(1).props().value).toEqual(stepArgs.pin_value); expect(inputs.at(1).props().value).toEqual(stepArgs.pin_value);
@ -53,7 +55,7 @@ describe("<TileSetServoAngle/>", () => {
}); });
}); });
it("dissallows named_pins", () => { it("disallows named_pins", () => {
const p = fakeProps(); const p = fakeProps();
const step = p.currentStep; const step = p.currentStep;
if (step.kind === "set_servo_angle") { if (step.kind === "set_servo_angle") {

View File

@ -157,10 +157,11 @@ export function renderCeleryNode(props: StepParams) {
case "home": return <TileMoveHome {...props} />; case "home": return <TileMoveHome {...props} />;
case "reboot": return <TileReboot {...props} />; case "reboot": return <TileReboot {...props} />;
case "emergency_lock": return <TileEmergencyStop {...props} />; case "emergency_lock": return <TileEmergencyStop {...props} />;
case "install_first_party_farmware": return <TileSystemAction {...props} />;
case "assertion": return <TileAssertion {...props} />; case "assertion": return <TileAssertion {...props} />;
case "check_updates": case "sync": case "dump_info": case "power_off": case "read_status":
case "factory_reset": case "emergency_unlock": case "install_first_party_farmware":
return <TileSystemAction {...props} />;
case "check_updates": case "factory_reset":
return <TileFirmwareAction {...props} />; return <TileFirmwareAction {...props} />;
default: default:
return <TileUnknown {...props} />; return <TileUnknown {...props} />;

View File

@ -4,7 +4,7 @@ import { ToolTips, Content } from "../../constants";
import { import {
StepWrapper, StepHeader, StepContent, conflictsString, StepWarning StepWrapper, StepHeader, StepContent, conflictsString, StepWarning
} from "../step_ui/index"; } from "../step_ui/index";
import { StepRadio } from "../step_ui/step_radio"; import { AxisStepRadio } from "../step_ui/step_radio";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { Xyz, Calibrate, TaggedSequence } from "farmbot"; import { Xyz, Calibrate, TaggedSequence } from "farmbot";
import { some } from "lodash"; import { some } from "lodash";
@ -81,7 +81,7 @@ class InnerTileCalibrate extends React.Component<CalibrateParams, {}> {
conflicts={this.settingConflicts} />} conflicts={this.settingConflicts} />}
</StepHeader> </StepHeader>
<StepContent className={className}> <StepContent className={className}>
<StepRadio <AxisStepRadio
currentSequence={currentSequence} currentSequence={currentSequence}
currentStep={currentStep} currentStep={currentStep}
dispatch={dispatch} dispatch={dispatch}

View File

@ -7,7 +7,7 @@ import { t } from "../../i18next_wrapper";
export function TileEmergencyStop(props: StepParams) { export function TileEmergencyStop(props: StepParams) {
const { dispatch, currentStep, index, currentSequence } = props; const { dispatch, currentStep, index, currentSequence } = props;
const className = "take-photo-step"; const className = "emergency-stop-step";
return <StepWrapper> return <StepWrapper>
<StepHeader <StepHeader
className={className} className={className}

View File

@ -7,7 +7,7 @@ import {
StepWrapper, StepHeader, StepContent, StepWarning, conflictsString StepWrapper, StepHeader, StepContent, StepWarning, conflictsString
} from "../step_ui/index"; } from "../step_ui/index";
import { some } from "lodash"; import { some } from "lodash";
import { StepRadio } from "../step_ui/step_radio"; import { AxisStepRadio } from "../step_ui/step_radio";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
export function TileFindHome(props: StepParams) { export function TileFindHome(props: StepParams) {
@ -80,7 +80,7 @@ class InnerFindHome extends React.Component<FindHomeParams, {}> {
conflicts={this.settingConflicts} />} conflicts={this.settingConflicts} />}
</StepHeader> </StepHeader>
<StepContent className={className}> <StepContent className={className}>
<StepRadio <AxisStepRadio
currentSequence={currentSequence} currentSequence={currentSequence}
currentStep={currentStep} currentStep={currentStep}
dispatch={dispatch} dispatch={dispatch}

View File

@ -2,10 +2,11 @@ import * as React from "react";
import { StepParams } from "../interfaces"; import { StepParams } from "../interfaces";
import { ToolTips } from "../../constants"; import { ToolTips } from "../../constants";
import { StepWrapper, StepHeader, StepContent } from "../step_ui/index"; import { StepWrapper, StepHeader, StepContent } from "../step_ui/index";
import { StepRadio } from "../step_ui/step_radio"; import { AxisStepRadio } from "../step_ui/step_radio";
import { StepInputBox } from "../inputs/step_input_box"; import { StepInputBox } from "../inputs/step_input_box";
import { Row, Col } from "../../ui"; import { Row, Col } from "../../ui";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { Home } from "farmbot";
export function TileMoveHome(props: StepParams) { export function TileMoveHome(props: StepParams) {
const { dispatch, currentStep, index, currentSequence } = props; const { dispatch, currentStep, index, currentSequence } = props;
@ -20,9 +21,9 @@ export function TileMoveHome(props: StepParams) {
index={index} index={index}
confirmStepDeletion={props.confirmStepDeletion} /> confirmStepDeletion={props.confirmStepDeletion} />
<StepContent className={className}> <StepContent className={className}>
<StepRadio <AxisStepRadio
currentSequence={currentSequence} currentSequence={currentSequence}
currentStep={currentStep} currentStep={currentStep as Home}
dispatch={dispatch} dispatch={dispatch}
index={index} index={index}
label={t("Home")} /> label={t("Home")} />

View File

@ -3,52 +3,18 @@ import { StepParams } from "../interfaces";
import { ToolTips } from "../../constants"; import { ToolTips } from "../../constants";
import { StepWrapper, StepHeader, StepContent } from "../step_ui/index"; import { StepWrapper, StepHeader, StepContent } from "../step_ui/index";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { Row, Col } from "../../ui";
import { ALLOWED_PACKAGES, SequenceBodyItem, Reboot } from "farmbot"; import { ALLOWED_PACKAGES, SequenceBodyItem, Reboot } from "farmbot";
import { editStep } from "../../api/crud"; import { editStep } from "../../api/crud";
import { StepRadio } from "../step_ui/step_radio";
type StringMap = Record<string, string>; const PACKAGE_CHOICES = (): Record<ALLOWED_PACKAGES, string> => ({
"arduino_firmware": t("Just the Arduino"),
interface MultiChoiceRadioProps<T extends StringMap> { "farmbot_os": t("Entire system")
uuid: string; });
choices: T;
currentChoice: keyof T;
onChange(key: keyof T): void;
}
export const MultiChoiceRadio =
<T extends StringMap>(props: MultiChoiceRadioProps<T>) => {
const choices = Object.keys(props.choices);
return <Row>
<Col xs={12}>
<div className="bottom-content">
<div className="channel-fields">
<form>
{choices.map((choice, i) =>
<div key={`${props.uuid} ${i}`} style={{ display: "inline" }}>
<label>
<input type="radio"
value={choice}
onChange={() => props.onChange(choice)}
checked={props.currentChoice === choice} />
{t(props.choices[choice])}
</label>
</div>)}
</form>
</div>
</div>
</Col>
</Row>;
};
const PACKAGE_CHOICES: Record<ALLOWED_PACKAGES, string> = {
"arduino_firmware": "Just the Arduino",
"farmbot_os": "Entire system"
};
function assertReboot(x: SequenceBodyItem): asserts x is Reboot { function assertReboot(x: SequenceBodyItem): asserts x is Reboot {
if (x.kind !== "reboot") { if (x.kind !== "reboot") {
throw new Error("Impossible"); throw new Error(`${x.kind} is not "reboot"`);
} }
} }
@ -74,7 +40,7 @@ export const editTheRebootStep =
export function TileReboot(props: StepParams) { export function TileReboot(props: StepParams) {
const { dispatch, currentStep, index, currentSequence } = props; const { dispatch, currentStep, index, currentSequence } = props;
const className = "take-photo-step"; const className = "reboot-step";
assertReboot(currentStep); assertReboot(currentStep);
return <StepWrapper> return <StepWrapper>
<StepHeader <StepHeader
@ -86,9 +52,9 @@ export function TileReboot(props: StepParams) {
index={index} index={index}
confirmStepDeletion={props.confirmStepDeletion} /> confirmStepDeletion={props.confirmStepDeletion} />
<StepContent className={className}> <StepContent className={className}>
<MultiChoiceRadio <StepRadio
uuid={currentSequence.uuid + index} choices={Object.keys(PACKAGE_CHOICES())}
choices={PACKAGE_CHOICES} choiceLabelLookup={PACKAGE_CHOICES()}
currentChoice={currentStep.args.package as ALLOWED_PACKAGES} currentChoice={currentStep.args.package as ALLOWED_PACKAGES}
onChange={editTheRebootStep(props)} /> onChange={editTheRebootStep(props)} />
</StepContent> </StepContent>

View File

@ -2,17 +2,18 @@ import * as React from "react";
import { StepInputBox } from "../inputs/step_input_box"; import { StepInputBox } from "../inputs/step_input_box";
import { StepParams } from "../interfaces"; import { StepParams } from "../interfaces";
import { ToolTips } from "../../constants"; import { ToolTips } from "../../constants";
import { StepWrapper, StepHeader, StepContent } from "../step_ui/index"; import { StepWrapper, StepHeader, StepContent } from "../step_ui";
import { Row, Col } from "../../ui/index"; import { Row, Col } from "../../ui/index";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { MultiChoiceRadio } from "./tile_reboot";
import { SetServoAngle } from "farmbot"; import { SetServoAngle } from "farmbot";
import { editStep } from "../../api/crud"; import { editStep } from "../../api/crud";
import { StepRadio } from "../step_ui/step_radio";
const PACKAGE_CHOICES: Record<string, string> = { const PIN_CHOICES = ["4", "5", "6", "11"];
"4": "Pin 4", const CHOICE_LABELS = () => PIN_CHOICES.reduce((acc, pinNumber) => {
"5": "Pin 5", acc[pinNumber] = `${t("Pin")} ${pinNumber}`;
}; return acc;
}, {} as Record<string, string>);
type Keys = type Keys =
| "dispatch" | "dispatch"
@ -35,14 +36,14 @@ export const pinNumberChanger = (props: Props) => (y: string) => {
}; };
export function ServoPinSelection(props: Props) { export function ServoPinSelection(props: Props) {
const { currentSequence, index, currentStep } = props; const { currentStep } = props;
const num = (currentStep as SetServoAngle).args.pin_number; const num = (currentStep as SetServoAngle).args.pin_number;
if (typeof num !== "number") { throw new Error("NO!"); } if (typeof num !== "number") { throw new Error("NO!"); }
const onChange = pinNumberChanger(props); const onChange = pinNumberChanger(props);
return <MultiChoiceRadio return <StepRadio
uuid={currentSequence.uuid + index} choices={PIN_CHOICES}
choices={PACKAGE_CHOICES} choiceLabelLookup={CHOICE_LABELS()}
currentChoice={"" + num} currentChoice={"" + num}
onChange={onChange} />; onChange={onChange} />;
} }
@ -79,5 +80,4 @@ export function TileSetServoAngle(props: StepParams) {
</Row> </Row>
</StepContent> </StepContent>
</StepWrapper>; </StepWrapper>;
} }

View File

@ -2,8 +2,9 @@ import * as React from "react";
import { StepParams } from "../interfaces"; import { StepParams } from "../interfaces";
import { ToolTips } from "../../constants"; import { ToolTips } from "../../constants";
import { StepWrapper, StepHeader, StepContent } from "../step_ui/index"; import { StepWrapper, StepHeader, StepContent } from "../step_ui/index";
import { StepRadio } from "../step_ui/step_radio";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { AxisStepRadio } from "../step_ui/step_radio";
import { Zero } from "farmbot";
export function TileSetZero(props: StepParams) { export function TileSetZero(props: StepParams) {
const { dispatch, currentStep, index, currentSequence } = props; const { dispatch, currentStep, index, currentSequence } = props;
@ -18,9 +19,9 @@ export function TileSetZero(props: StepParams) {
index={index} index={index}
confirmStepDeletion={props.confirmStepDeletion} /> confirmStepDeletion={props.confirmStepDeletion} />
<StepContent className={className}> <StepContent className={className}>
<StepRadio <AxisStepRadio
currentSequence={currentSequence} currentSequence={currentSequence}
currentStep={currentStep} currentStep={currentStep as Zero}
dispatch={dispatch} dispatch={dispatch}
index={index} index={index}
label={t("Zero")} /> label={t("Zero")} />

View File

@ -1,14 +1,16 @@
jest.mock("../../../api/crud", () => ({ overwrite: jest.fn() })); let mockStep = {};
jest.mock("../../../api/crud", () => ({
editStep: jest.fn(x => x.executor(mockStep)),
}));
import * as React from "react"; import * as React from "react";
import { mount } from "enzyme"; import { mount } from "enzyme";
import { StepRadio, StepRadioProps } from "../step_radio"; import { AxisStepRadio, AxisStepRadioProps } from "../step_radio";
import { fakeSequence } from "../../../__test_support__/fake_state/resources"; import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { FindHome, Calibrate, Zero } from "farmbot"; import { FindHome, Calibrate, Zero } from "farmbot";
import { overwrite } from "../../../api/crud";
describe("<StepRadio />", () => { describe("<StepRadio />", () => {
const currentStep: FindHome = { const findHomeStep: FindHome = {
kind: "find_home", kind: "find_home",
args: { args: {
speed: 100, speed: 100,
@ -16,55 +18,55 @@ describe("<StepRadio />", () => {
} }
}; };
const fakeProps = (): StepRadioProps => ({ const fakeProps = (): AxisStepRadioProps => ({
currentSequence: fakeSequence(), currentSequence: fakeSequence(),
currentStep, currentStep: findHomeStep,
dispatch: jest.fn(), dispatch: jest.fn(),
index: 0, index: 0,
label: "Find", label: "Find",
}); });
it("renders", () => { it("renders", () => {
const wrapper = mount(<StepRadio {...fakeProps()} />); const wrapper = mount(<AxisStepRadio {...fakeProps()} />);
expect(wrapper.find("input").length).toEqual(4); expect(wrapper.find("input").length).toEqual(4);
expect(wrapper.text()).toContain("Find"); expect(wrapper.text()).toContain("Find");
}); });
it("handles update for find_home", () => { it("handles update for find_home", () => {
const p = fakeProps(); const p = fakeProps();
const wrapper = mount(<StepRadio {...p} />); mockStep = p.currentStep;
const wrapper = mount(<AxisStepRadio {...p} />);
wrapper.find("input").last().simulate("change"); wrapper.find("input").last().simulate("change");
const expectedStep: FindHome = { const expectedStep: FindHome = {
kind: "find_home", kind: "find_home",
args: { speed: 100, axis: "all" } args: { speed: 100, axis: "all" }
}; };
expect(overwrite).toHaveBeenCalledWith(p.currentSequence, expect(mockStep).toEqual(expectedStep);
expect.objectContaining({ body: [expectedStep] }));
}); });
it("handles update for calibrate", () => { it("handles update for calibrate", () => {
const p = fakeProps(); const p = fakeProps();
p.currentStep = { kind: "calibrate", args: { axis: "x" } }; p.currentStep = { kind: "calibrate", args: { axis: "x" } };
const wrapper = mount(<StepRadio {...p} />); mockStep = p.currentStep;
const wrapper = mount(<AxisStepRadio {...p} />);
wrapper.find("input").last().simulate("change"); wrapper.find("input").last().simulate("change");
const expectedStep: Calibrate = { const expectedStep: Calibrate = {
kind: "calibrate", kind: "calibrate",
args: { axis: "all" } args: { axis: "all" }
}; };
expect(overwrite).toHaveBeenCalledWith(p.currentSequence, expect(mockStep).toEqual(expectedStep);
expect.objectContaining({ body: [expectedStep] }));
}); });
it("handles update for zero", () => { it("handles update for zero", () => {
const p = fakeProps(); const p = fakeProps();
p.currentStep = { kind: "zero", args: { axis: "x" } }; p.currentStep = { kind: "zero", args: { axis: "x" } };
const wrapper = mount(<StepRadio {...p} />); mockStep = p.currentStep;
const wrapper = mount(<AxisStepRadio {...p} />);
wrapper.find("input").last().simulate("change"); wrapper.find("input").last().simulate("change");
const expectedStep: Zero = { const expectedStep: Zero = {
kind: "zero", kind: "zero",
args: { axis: "all" } args: { axis: "all" }
}; };
expect(overwrite).toHaveBeenCalledWith(p.currentSequence, expect(mockStep).toEqual(expectedStep);
expect.objectContaining({ body: [expectedStep] }));
}); });
}); });

View File

@ -1,57 +1,32 @@
import * as React from "react"; import * as React from "react";
import { Row, Col } from "../../ui/index"; import { Row, Col } from "../../ui/index";
import {
TaggedSequence, SequenceBodyItem, ALLOWED_AXIS
} from "farmbot";
import { overwrite } from "../../api/crud";
import { defensiveClone } from "../../util";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import {
TaggedSequence, ALLOWED_AXIS, FindHome, Home, Calibrate, Zero
} from "farmbot";
import { editStep } from "../../api/crud";
export interface StepRadioProps { export interface StepRadioProps<T extends string> {
currentSequence: TaggedSequence; choices: T[];
currentStep: SequenceBodyItem; choiceLabelLookup: Record<T, string>;
dispatch: Function; currentChoice: T;
index: number; onChange(key: T): void;
label: string;
} }
const AXIS_CHOICES: ALLOWED_AXIS[] = ["x", "y", "z", "all"]; export const StepRadio = <T extends string>(props: StepRadioProps<T>) =>
<Row>
export function StepRadio(props: StepRadioProps) {
const isSelected = (choice: ALLOWED_AXIS) => {
if (props.currentStep.kind === "find_home"
|| props.currentStep.kind === "calibrate"
|| props.currentStep.kind === "zero") {
return props.currentStep.args.axis === choice;
}
};
const handleUpdate = (choice: ALLOWED_AXIS) => {
const update = defensiveClone(props.currentStep);
if (update.kind === "find_home"
|| update.kind === "calibrate"
|| update.kind === "zero") {
const nextSequence = defensiveClone(props.currentSequence).body;
update.args.axis = choice;
(nextSequence.body || [])[props.index] = update;
props.dispatch(overwrite(props.currentSequence, nextSequence));
}
};
return <Row>
<Col xs={12}> <Col xs={12}>
<div className="bottom-content"> <div className="bottom-content">
<div className="channel-fields"> <div className="channel-fields">
<form> <form>
{AXIS_CHOICES.map((choice, i) => {props.choices.map((choice, i) =>
<div key={i} style={{ display: "inline" }}> <div key={i} style={{ display: "inline" }}>
<label> <label>
<input type="radio" <input type="radio"
value={choice} value={choice}
onChange={e => onChange={() => props.onChange(choice)}
handleUpdate(e.currentTarget.value as typeof choice)} checked={props.currentChoice === choice} />
checked={isSelected(choice)} /> {t(props.choiceLabelLookup[choice])}
{` ${t(props.label)} ${choice}`}
</label> </label>
</div>)} </div>)}
</form> </form>
@ -59,4 +34,37 @@ export function StepRadio(props: StepRadioProps) {
</div> </div>
</Col> </Col>
</Row>; </Row>;
type AxisStep = FindHome | Home | Calibrate | Zero;
export interface AxisStepRadioProps {
currentSequence: TaggedSequence;
currentStep: AxisStep;
dispatch: Function;
index: number;
label: string;
} }
export const AxisStepRadio = (props: AxisStepRadioProps) => {
const AXIS_CHOICES: ALLOWED_AXIS[] = ["x", "y", "z", "all"];
const CHOICE_LABELS = AXIS_CHOICES.reduce((acc, axis) => {
acc[axis] = `${t(props.label)} ${axis}`;
return acc;
}, {} as Record<ALLOWED_AXIS, string>);
const handleUpdate = (axis: ALLOWED_AXIS) => {
const { currentStep, index, currentSequence } = props;
props.dispatch(editStep({
step: currentStep,
index,
sequence: currentSequence,
executor: (step: AxisStep) => step.args.axis = axis,
}));
};
return <StepRadio
choices={AXIS_CHOICES}
choiceLabelLookup={CHOICE_LABELS}
currentChoice={props.currentStep.args.axis}
onChange={handleUpdate} />;
};