misc fixes and cleanup

pull/1224/head
gabrielburnworth 2019-06-06 16:54:48 -07:00
parent 68c3ac9308
commit 76153f28e3
25 changed files with 342 additions and 221 deletions

View File

@ -159,7 +159,10 @@ export function onMalformed() {
}
export const onOnline =
() => dispatchNetworkUp("user.mqtt", undefined, "MQTT.js is online");
() => {
success(t("Reconnected to the message broker."), t("Online"));
dispatchNetworkUp("user.mqtt", undefined, "MQTT.js is online");
};
export const onReconnect =
() => warning(t("Attempting to reconnect to the message broker"), t("Offline"));

View File

@ -214,6 +214,11 @@ export namespace ToolTips {
export const SEQUENCE_LIST =
trim(`Here is the list of all of your sequences. Click one to edit.`);
export const DEFAULT_VALUE =
trim(`Select a location to be used as the default value for this variable.
If the sequence is ever run without the variable explicitly set to
another value, the default value will be used.`);
export const MOVE_ABSOLUTE =
trim(`The Move To step instructs FarmBot to move to the specified
coordinate regardless of the current position. For example, if FarmBot is

View File

@ -846,6 +846,10 @@ ul {
}
}
.farmware-info {
margin-top: 1rem;
}
.farmware-list-items {
margin-left: -30px;
margin-right: -20px;
@ -1147,6 +1151,14 @@ ul {
}
}
.default-value-form {
position: relative;
.default-value-tooltip {
position: absolute;
right: 0;
}
}
.documentation-widget {
.fa-external-link {
margin-left: 1rem;

View File

@ -70,6 +70,14 @@
}
}
.farmware-input-panel-contents {
.farmware-info-button {
position: absolute;
top: 7rem;
left: 35%;
}
}
.sequence-editor-content {
hr {
margin-right: 15px;
@ -171,8 +179,13 @@
}
}
.farmware-info-panel button {
margin-bottom: 3rem;
.farmware-info-panel {
.back-to-farmware {
text-align: left !important;
}
button {
margin-bottom: 3rem;
}
}
.step-button-cluster {
@ -263,6 +276,7 @@
}
}
.back-to-farmware,
.back-to-regimens,
.back-to-sequences {
display: none;
@ -277,6 +291,7 @@
text-align: center;
line-height: 6rem;
margin-left: 15px;
&.farmware-info-open,
&.inserting-step,
&.inserting-item {
margin-left: 0;
@ -285,22 +300,6 @@
}
}
.back-to-farmware {
display: none;
&.open {
@media screen and (max-width: 767px) {
display: block;
margin: 4rem;
margin-top: 0;
margin-left: 2rem;
float: left !important;
i {
margin-right: 1rem;
}
}
}
}
.drag-drop-area {
@media screen and (max-width: 767px) {
display: none;

View File

@ -1,6 +1,8 @@
jest.mock("axios", () => ({
post: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve({
headers: { "x-farmbot-rpc-id": "123" }
})),
}));
jest.mock("../../../history", () => ({ history: { push: jest.fn() } }));

View File

@ -1,6 +1,5 @@
import axios from "axios";
import { API } from "../../api";
import { success, info } from "farmbot-toastr";
import { history } from "../../history";
import { Actions } from "../../constants";
@ -9,6 +8,7 @@ import { unpackUUID } from "../../util";
import { isString } from "lodash";
import { TaggedSavedGarden, TaggedPlantTemplate } from "farmbot";
import { t } from "../../i18next_wrapper";
import { stopTracking } from "../../connectivity/data_consistency";
/** Save all Plant to PlantTemplates in a new SavedGarden. */
export const snapshotGarden = (name?: string | undefined) =>
@ -23,7 +23,8 @@ export const unselectSavedGarden = {
/** Save a SavedGarden's PlantTemplates as Plants. */
export const applyGarden = (gardenId: number) => (dispatch: Function) => axios
.patch<void>(API.current.applyGardenPath(gardenId))
.then(() => {
.then(data => {
stopTracking(data.headers["x-farmbot-rpc-id"]);
history.push("/app/designer/plants");
dispatch(unselectSavedGarden);
const busyToastTitle = t("Please wait");

View File

@ -105,7 +105,7 @@ describe("<FarmwarePage />", () => {
p.farmwares["My Fake Test Farmware"] = farmware;
p.currentFarmware = "My Fake Test Farmware";
const wrapper = mount(<FarmwarePage {...p} />);
clickButton(wrapper, 3, "Run");
clickButton(wrapper, 2, "Run");
expect(mockDevice.execScript).toHaveBeenCalledWith("My Fake Test Farmware");
});
@ -122,7 +122,9 @@ describe("<FarmwarePage />", () => {
p.botToMqttStatus = "up";
p.infoOpen = false;
const wrapper = mount(<FarmwarePage {...p} />);
clickButton(wrapper, 0, "farmware list");
const back = wrapper.find(".fa-arrow-left").first();
expect(back.props().title).toEqual("back to farmware list");
back.simulate("click");
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.SELECT_FARMWARE, payload: undefined
});
@ -133,7 +135,9 @@ describe("<FarmwarePage />", () => {
p.botToMqttStatus = "up";
p.infoOpen = true;
const wrapper = mount(<FarmwarePage {...p} />);
clickButton(wrapper, 0, "back");
const back = wrapper.find(".fa-arrow-left").first();
expect(back.props().title).toEqual("back to farmware");
back.simulate("click");
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.SET_FARMWARE_INFO_STATE, payload: false
});

View File

@ -1,5 +1,4 @@
import * as React from "react";
import { TaggedFarmwareInstallation } from "farmbot";
import { getDevice } from "../device";
import { commandErr } from "../devices/actions";
@ -138,7 +137,7 @@ const uninstallFarmware = (props: RemoveFarmwareProps) =>
export function FarmwareInfo(props: FarmwareInfoProps) {
const { farmware } = props;
return farmware ? <div>
return farmware ? <div className="farmware-info">
<label>{t("Description")}</label>
<p>{farmware.meta.description}</p>
<label>{t("Version")}</label>

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { connect } from "react-redux";
import {
Page, Row, LeftPanel, CenterPanel, RightPanel, DocSlug, Col
Page, Row, LeftPanel, CenterPanel, RightPanel, DocSlug
} from "../ui/index";
import { mapStateToProps, isPendingInstallation } from "./state_to_props";
import { Photos } from "./images/photos";
@ -187,32 +187,27 @@ export class FarmwarePage extends React.Component<FarmwareProps, {}> {
FarmwareBackButton = (props: { className: string }) => {
const infoOpen = props.className.includes("farmware-info-open");
return <Row>
<button
className={`back-to-farmware fb-button gray ${props.className}`}
onClick={() => infoOpen
? this.props.dispatch({
type: Actions.SET_FARMWARE_INFO_STATE, payload: false
})
: this.props.dispatch({
type: Actions.SELECT_FARMWARE, payload: undefined
})}>
{infoOpen ? t("back") : t("farmware list")}
</button>
</Row>;
return <i
className={`back-to-farmware fa fa-arrow-left ${props.className}`}
onClick={() => infoOpen
? this.props.dispatch({
type: Actions.SET_FARMWARE_INFO_STATE, payload: false
})
: this.props.dispatch({
type: Actions.SELECT_FARMWARE, payload: undefined
})}
title={infoOpen ? t("back to farmware") : t("back to farmware list")} />;
};
FarmwareInfoButton = (props: { className: string, online: boolean }) =>
<Row>
<button
className={`farmware-info-button fb-button gray ${props.className}`}
disabled={!props.online}
onClick={() => this.props.dispatch({
type: Actions.SET_FARMWARE_INFO_STATE, payload: true
})}>
{t("farmware info")}
</button>
</Row>;
<button
className={`farmware-info-button fb-button gray ${props.className}`}
disabled={!props.online}
onClick={() => this.props.dispatch({
type: Actions.SET_FARMWARE_INFO_STATE, payload: true
})}>
{t("farmware info")}
</button>
render() {
const farmware = getFarmwareByName(
@ -224,14 +219,6 @@ export class FarmwarePage extends React.Component<FarmwareProps, {}> {
const showFirstParty =
!!this.props.getConfigValue(BooleanSetting.show_first_party_farmware);
return <Page className="farmware-page">
<Row>
<Col xs={6}>
<this.FarmwareBackButton className={activeClasses} />
</Col>
<Col xs={6}>
<this.FarmwareInfoButton className={activeClasses} online={online} />
</Col>
</Row>
<Row>
<LeftPanel
className={`farmware-list-panel ${activeClasses}`}
@ -248,16 +235,19 @@ export class FarmwarePage extends React.Component<FarmwareProps, {}> {
</LeftPanel>
<CenterPanel
className={`farmware-input-panel ${activeClasses}`}
backButton={<this.FarmwareBackButton className={activeClasses} />}
title={getFormattedFarmwareName(this.current || "Photos")}
helpText={getToolTipByFarmware(this.props.farmwares, this.current)
|| ToolTips.PHOTOS}
docPage={getDocLinkByFarmware(this.current)}>
{<div className="farmware-input-panel-contents">
<this.FarmwareInfoButton className={activeClasses} online={online} />
{this.getPanelByFarmware(this.current ? this.current : "photos")}
</div>}
</CenterPanel>
<RightPanel
className={`farmware-info-panel ${activeClasses}`}
backButton={<this.FarmwareBackButton className={activeClasses} />}
title={t("Information")}
helpText={ToolTips.FARMWARE_INFO}
show={!!farmware}>

View File

@ -1,11 +1,16 @@
jest.mock("i18next", () => ({ t: (i: string) => i }));
jest.mock("../../../api/crud", () => ({ overwrite: jest.fn() }));
import { commitBulkEditor, setTimeOffset, toggleDay, setSequence } from "../actions";
import {
commitBulkEditor, setTimeOffset, toggleDay, setSequence
} from "../actions";
import { fakeState } from "../../../__test_support__/fake_state";
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
import { TaggedResource, TaggedSequence, TaggedRegimen, Coordinate } from "farmbot";
import {
buildResourceIndex
} from "../../../__test_support__/resource_index_builder";
import {
TaggedResource, TaggedSequence, TaggedRegimen, Coordinate
} from "farmbot";
import { Actions } from "../../../constants";
import { Everything } from "../../../interfaces";
import { ToggleDayParams } from "../interfaces";
@ -21,22 +26,22 @@ describe("commitBulkEditor()", () => {
function newFakeState() {
const state = fakeState();
const regBody: TaggedRegimen["body"] = {
"id": regimen_id,
"name": "Test Regimen",
"color": "gray",
"regimen_items": [
id: regimen_id,
name: "Test Regimen",
color: "gray",
regimen_items: [
{ regimen_id, sequence_id, time_offset: 1000 }
],
body: [],
};
const reg = newTaggedResource("Regimen", regBody)[0];
const seqBody: TaggedSequence["body"] = {
"id": sequence_id,
"name": "Test Sequence",
"color": "gray",
"body": [{ kind: "wait", args: { milliseconds: 100 } }],
"args": { "locals": { kind: "scope_declaration", args: {} }, "version": 4 },
"kind": "sequence"
id: sequence_id,
name: "Test Sequence",
color: "gray",
body: [{ kind: "wait", args: { milliseconds: 100 } }],
args: { "locals": { kind: "scope_declaration", args: {} }, "version": 4 },
kind: "sequence"
};
const seq = arrayUnwrap(newTaggedResource("Sequence", seqBody));
const regimenUuid = reg.uuid;
@ -101,6 +106,7 @@ describe("commitBulkEditor()", () => {
});
it("adds items", () => {
console.log = jest.fn();
const state = newFakeState();
const getState = () => state;
const dispatch = jest.fn();
@ -117,6 +123,7 @@ describe("commitBulkEditor()", () => {
});
it("merges variables", () => {
console.log = jest.fn();
const state = newFakeState();
const seqUUID = state.resources.consumers.regimens.selectedSequenceUUID;
const label = "variable_label";

View File

@ -2,7 +2,8 @@ import {
createSequenceMeta,
determineDropdown,
findVariableByName,
determineVector
determineVector,
determineVarDDILabel
} from "../sequence_meta";
import {
fakeSequence,
@ -19,6 +20,7 @@ import {
} from "../../sequences/locals_list/location_form_list";
import { Point, Tool } from "farmbot";
import { fakeVariableNameSet } from "../../__test_support__/fake_variables";
import { NOTHING_SELECTED } from "../../sequences/locals_list/handle_select";
describe("determineDropdown", () => {
it("Returns a label for `parameter_declarations`", () => {
@ -57,7 +59,7 @@ describe("determineDropdown", () => {
data_value: { kind: "identifier", args: { label: "variable" } }
}
}, ri, "sequence uuid");
expect(r.label).toBe("Location Variable - variable");
expect(r.label).toBe("Location Variable - Select a location");
expect(r.value).toBe("?");
});
@ -203,3 +205,48 @@ describe("createSequenceMeta", () => {
}
});
});
describe("determineVarDDILabel()", () => {
it("returns 'add new' variable label", () => {
const ri = buildResourceIndex().index;
const label = determineVarDDILabel("variable", ri, undefined);
expect(label).toEqual("Location Variable - Add new");
});
it("returns 'select location' variable label", () => {
const varData = fakeVariableNameSet("variable");
const data = Object.values(varData)[0];
data && (data.celeryNode = NOTHING_SELECTED);
const ri = buildResourceIndex().index;
ri.sequenceMetas = { "sequence uuid": varData };
const label = determineVarDDILabel("variable", ri, "sequence uuid");
expect(label).toEqual("Location Variable - Select a location");
});
it("returns 'externally defined' variable label", () => {
const varData = fakeVariableNameSet("variable");
const data = Object.values(varData)[0];
data && (data.celeryNode = {
kind: "parameter_declaration",
args: {
label: "variable", default_value: {
kind: "coordinate", args: { x: 0, y: 0, z: 0 }
}
}
});
const ri = buildResourceIndex().index;
ri.sequenceMetas = { "sequence uuid": varData };
const label = determineVarDDILabel("variable", ri, "sequence uuid");
expect(label).toEqual("Location Variable - Externally defined");
});
it("returns variable label", () => {
const varData = fakeVariableNameSet("variable");
const data = Object.values(varData)[0];
data && (data.celeryNode.kind = "variable_declaration");
const ri = buildResourceIndex().index;
ri.sequenceMetas = { "sequence uuid": varData };
const label = determineVarDDILabel("variable", ri, "sequence uuid");
expect(label).toEqual("Location Variable - variable");
});
});

View File

@ -50,13 +50,26 @@ export const determineVector =
};
/** Try to find a vector in scope declarations for the variable. */
const maybeFindVariable =
(label: string, resources: ResourceIndex, uuid?: UUID):
SequenceMeta | undefined => {
if (uuid) {
return findVariableByName(resources, uuid, label);
const maybeFindVariable = (
label: string, resources: ResourceIndex, uuid?: UUID
): SequenceMeta | undefined =>
uuid ? findVariableByName(resources, uuid, label) : undefined;
const withPrefix = (label: string) => `${t("Location Variable")} - ${label}`;
export const determineVarDDILabel =
(label: string, resources: ResourceIndex, uuid?: UUID): string => {
const variable = maybeFindVariable(label, resources, uuid);
if (variable) {
if (variable.celeryNode.kind === "parameter_declaration") {
return withPrefix(t("Externally defined"));
}
if (variable.celeryNode.kind !== "variable_declaration") {
return withPrefix(t("Select a location"));
}
return withPrefix(variable.dropdown.label);
}
return undefined;
return withPrefix(t("Add new"));
};
/** Given a CeleryScript parameter application and a resource index
@ -77,9 +90,7 @@ export const determineDropdown =
return { label: `Coordinate (${x}, ${y}, ${z})`, value: "?" };
case "identifier":
const { label } = data_value.args;
const variable = maybeFindVariable(label, resources, uuid);
const varName = `${t("Location Variable")} - ${variable
? variable.dropdown.label : t("Select a location")}`;
const varName = determineVarDDILabel(label, resources, uuid);
return { label: varName, value: "?" };
// tslint:disable-next-line:no-any
case "every_point" as any:

View File

@ -1,9 +1,6 @@
import { localListCallback } from "../locals_list";
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { inputEvent } from "../../../__test_support__/fake_input_event";
import { ParameterApplication, ParameterDeclaration, VariableDeclaration } from "farmbot";
import { AxisEditProps, manuallyEditAxis } from "../location_form";
import { VariableNode } from "../locals_list_support";
import { ParameterDeclaration, VariableDeclaration } from "farmbot";
describe("localListCallback", () => {
it("handles a new local declaration", () => {
@ -41,43 +38,3 @@ describe("localListCallback", () => {
}));
});
});
const isParameterApplication =
(x: VariableNode): x is ParameterApplication =>
x.kind === "parameter_application";
describe("manuallyEditAxis()", () => {
const fakeProps = (): AxisEditProps => ({
axis: "x",
onChange: jest.fn(),
editableVariable: {
kind: "parameter_application",
args: {
label: "parent",
data_value: { kind: "coordinate", args: { x: 10, y: 20, z: 30 } }
}
},
});
it("edits an axis", () => {
const expected = fakeProps();
if (isParameterApplication(expected.editableVariable) &&
expected.editableVariable.args.data_value.kind === "coordinate") {
expected.editableVariable.args.data_value.args.x = 1.23;
}
const p = fakeProps();
manuallyEditAxis(p)(inputEvent("1.23"));
expect(p.onChange).toHaveBeenCalledWith(expected.editableVariable);
});
it("can't edit when not a coordinate (inputs also disabled)", () => {
const p = fakeProps();
p.editableVariable.args = {
label: "", data_value: {
kind: "identifier", args: { label: "" }
}
};
manuallyEditAxis(p)(inputEvent("1.23"));
expect(p.onChange).not.toHaveBeenCalled();
});
});

View File

@ -0,0 +1,46 @@
import { inputEvent } from "../../../__test_support__/fake_input_event";
import { ParameterApplication } from "farmbot";
import {
AxisEditProps, manuallyEditAxis
} from "../location_form_coordinate_input_boxes";
import { VariableNode } from "../locals_list_support";
const isParameterApplication =
(x: VariableNode): x is ParameterApplication =>
x.kind === "parameter_application";
describe("manuallyEditAxis()", () => {
const fakeProps = (): AxisEditProps => ({
axis: "x",
onChange: jest.fn(),
editableVariable: {
kind: "parameter_application",
args: {
label: "parent",
data_value: { kind: "coordinate", args: { x: 10, y: 20, z: 30 } }
}
},
});
it("edits an axis", () => {
const expected = fakeProps();
if (isParameterApplication(expected.editableVariable) &&
expected.editableVariable.args.data_value.kind === "coordinate") {
expected.editableVariable.args.data_value.args.x = 1.23;
}
const p = fakeProps();
manuallyEditAxis(p)(inputEvent("1.23"));
expect(p.onChange).toHaveBeenCalledWith(expected.editableVariable);
});
it("can't edit when not a coordinate (inputs also disabled)", () => {
const p = fakeProps();
p.editableVariable.args = {
label: "", data_value: {
kind: "identifier", args: { label: "" }
}
};
manuallyEditAxis(p)(inputEvent("1.23"));
expect(p.onChange).not.toHaveBeenCalled();
});
});

View File

@ -44,7 +44,7 @@ describe("<LocationForm/>", () => {
expect(selects.length).toBe(1);
const select = selects.first().props();
const choices = locationFormList(p.resources, [PARENT()], true);
const choices = locationFormList(p.resources, [PARENT("")], true);
const actualLabels = select.list.map(x => x.label).sort();
const expectedLabels = choices.map(x => x.label).sort();
const diff = difference(actualLabels, expectedLabels);
@ -56,7 +56,7 @@ describe("<LocationForm/>", () => {
label: "label",
allowedVariableNodes: p.allowedVariableNodes
})(choice));
expect(inputs.length).toBe(3);
expect(inputs.length).toBe(0);
});
it("uses body variable data", () => {
@ -71,7 +71,7 @@ describe("<LocationForm/>", () => {
}];
const wrapper = mount(<LocationForm {...p} />);
expect(wrapper.text().toLowerCase())
.toContain("location variable - select a location");
.toContain("location variable - add new");
});
it("shows parent in dropdown", () => {
@ -79,7 +79,7 @@ describe("<LocationForm/>", () => {
p.shouldDisplay = () => true;
const wrapper = shallow(<LocationForm {...p} />);
expect(wrapper.find(FBSelect).first().props().list)
.toEqual(expect.arrayContaining([PARENT("label")]));
.toEqual(expect.arrayContaining([PARENT("Location Variable - Add new")]));
});
it("doesn't show parent in dropdown", () => {

View File

@ -6,6 +6,8 @@ import { LocationForm } from "./location_form";
import {
SequenceMeta, determineVector, determineDropdown
} from "../../resources/sequence_meta";
import { Help } from "../../ui";
import { ToolTips } from "../../constants";
export interface DefaultValueFormProps {
variableNode: VariableNode;
@ -15,16 +17,21 @@ export interface DefaultValueFormProps {
export const DefaultValueForm = (props: DefaultValueFormProps) =>
props.variableNode.kind === "parameter_declaration"
? <LocationForm
key={props.variableNode.args.label + "default_value"}
locationDropdownKey={JSON.stringify(props.variableNode) + "default_value"}
variable={defaultValueVariableData(props.resources, props.variableNode)}
sequenceUuid={""}
resources={props.resources}
shouldDisplay={() => true}
allowedVariableNodes={AllowedVariableNodes.variable}
hideTypeLabel={true}
onChange={change(props.onChange, props.variableNode)} />
? <div className="default-value-form">
<div className="default-value-tooltip">
<Help text={ToolTips.DEFAULT_VALUE} />
</div>
<LocationForm
key={props.variableNode.args.label + "default_value"}
locationDropdownKey={JSON.stringify(props.variableNode) + "default_value"}
variable={defaultValueVariableData(props.resources, props.variableNode)}
sequenceUuid={""}
resources={props.resources}
shouldDisplay={() => true}
allowedVariableNodes={AllowedVariableNodes.variable}
hideTypeLabel={true}
onChange={change(props.onChange, props.variableNode)} />
</div>
: <div />;
const change =

View File

@ -8,7 +8,6 @@ import {
} from "../../resources/interfaces";
import { SequenceMeta } from "../../resources/sequence_meta";
import { ShouldDisplay } from "../../devices/interfaces";
import { t } from "../../i18next_wrapper";
export type VariableNode =
ParameterDeclaration | VariableDeclaration | ParameterApplication;
@ -77,8 +76,5 @@ export interface LocationFormProps extends CommonProps {
hideTypeLabel?: boolean;
}
export const PARENT = (label?: string) => ({
value: "parent",
label: label ? label : t("Location Variable - Add new"),
headingId: "parameter"
});
export const PARENT = (label: string) =>
({ value: "parent", label, headingId: "parameter" });

View File

@ -1,38 +1,18 @@
import * as React from "react";
import { Row, Col, FBSelect, BlurableInput, DropDownItem } from "../../ui";
import { Row, Col, FBSelect, DropDownItem } from "../../ui";
import { locationFormList, NO_VALUE_SELECTED_DDI } from "./location_form_list";
import { convertDDItoVariable } from "../locals_list/handle_select";
import {
LocationFormProps, PARENT, AllowedVariableNodes, VariableNode,
} from "../locals_list/locals_list_support";
import { defensiveClone } from "../../util/util";
import { Xyz } from "farmbot";
import {
determineVector, determineDropdown, determineEditable, SequenceMeta
determineVector, determineDropdown, SequenceMeta, determineVarDDILabel,
} from "../../resources/sequence_meta";
import { ResourceIndex, UUID } from "../../resources/interfaces";
import { Feature } from "../../devices/interfaces";
import { DefaultValueForm } from "./default_value_form";
import { t } from "../../i18next_wrapper";
/** For LocationForm coordinate input boxes. */
export interface AxisEditProps {
axis: Xyz;
onChange: (sd: VariableNode) => void;
editableVariable: VariableNode;
}
/** Update a ParameterApplication coordinate. */
export const manuallyEditAxis = (props: AxisEditProps) =>
(e: React.SyntheticEvent<HTMLInputElement>) => {
const { axis, onChange, editableVariable } = props;
const num = parseFloat(e.currentTarget.value);
if (editableVariable.kind !== "parameter_declaration" &&
editableVariable.args.data_value.kind === "coordinate") {
editableVariable.args.data_value.args[axis] = num;
!isNaN(num) && onChange(editableVariable);
}
};
import { CoordinateInputBoxes } from "./location_form_coordinate_input_boxes";
/**
* If a variable with a matching label exists in local parameter applications
@ -58,12 +38,7 @@ const maybeUseStepData = ({ resources, bodyVariables, variable, uuid }: {
return variable;
};
const listLabelDDI = (ddi: DropDownItem) => {
const newDDI = Object.assign({}, ddi);
newDDI.label = ddi.isNull ? t("Location Variable - Add new") : newDDI.label;
return newDDI;
};
/** 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)
@ -72,31 +47,24 @@ const selectedLabelDDI = (ddi: DropDownItem, override?: string) => {
};
/**
* Form with an "import from" dropdown and coordinate display/input boxes.
* 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.
*/
export const LocationForm =
(props: LocationFormProps) => {
const {
sequenceUuid, resources, onChange, bodyVariables, variable,
locationDropdownKey, allowedVariableNodes, disallowGroups, listVarLabel
} = props;
const { celeryNode, dropdown, vector } =
maybeUseStepData({
resources, bodyVariables, variable, uuid: sequenceUuid
});
/** For disabling coordinate input boxes when using external data. */
const isDisabled = !determineEditable(celeryNode);
const variableListItems = (props.shouldDisplay(Feature.variables) &&
allowedVariableNodes !== AllowedVariableNodes.variable)
? [PARENT(listVarLabel || listLabelDDI(dropdown).label)]
: [];
const { sequenceUuid, resources, bodyVariables, variable,
allowedVariableNodes, disallowGroups } = props;
const { celeryNode, dropdown, vector } = maybeUseStepData({
resources, bodyVariables, variable, uuid: sequenceUuid
});
const displayVariables = props.shouldDisplay(Feature.variables) &&
allowedVariableNodes !== AllowedVariableNodes.variable;
const variableListItems = displayVariables ? [PARENT(props.listVarLabel ||
determineVarDDILabel("parent", resources, sequenceUuid))] : [];
const displayGroups = props.shouldDisplay(Feature.loops) && !disallowGroups;
const list = locationFormList(resources, variableListItems, displayGroups);
/** Variable name. */
const { label } = celeryNode.args;
const editableVariable = defensiveClone(celeryNode);
const axisPartialProps = { onChange, editableVariable };
const formTitleWithType =
props.hideVariableLabel ? t("Location") : `${label} (${t("Location")})`;
const formTitle = props.hideTypeLabel ? label : formTitleWithType;
@ -112,33 +80,24 @@ export const LocationForm =
<Row>
<Col xs={12}>
<FBSelect
key={locationDropdownKey}
key={props.locationDropdownKey}
list={list}
selectedItem={selectedLabelDDI(dropdown, listVarLabel)}
selectedItem={selectedLabelDDI(dropdown, props.listVarLabel)}
customNullLabel={NO_VALUE_SELECTED_DDI().label}
onChange={ddi => onChange(convertDDItoVariable({
onChange={ddi => props.onChange(convertDDItoVariable({
label, allowedVariableNodes
})(ddi))} />
</Col>
</Row>
{vector &&
<Row>
{["x", "y", "z"].map((axis: Xyz) =>
<Col xs={props.width || 4} key={axis}>
<label>
{t("{{axis}} (mm)", { axis })}
</label>
<BlurableInput type="number"
disabled={isDisabled}
onCommit={manuallyEditAxis({ ...axisPartialProps, axis })}
name={`location-${axis}`}
value={"" + vector[axis]} />
</Col>)}
</Row>}
<CoordinateInputBoxes
variableNode={celeryNode}
vector={vector}
width={props.width}
onChange={props.onChange} />
<DefaultValueForm
variableNode={celeryNode}
resources={resources}
onChange={onChange} />
onChange={props.onChange} />
</div>}
</div>;
};

View File

@ -0,0 +1,55 @@
import * as React from "react";
import { Row, Col, BlurableInput } from "../../ui";
import { VariableNode } from "../locals_list/locals_list_support";
import { defensiveClone } from "../../util/util";
import { Xyz, Vector3 } from "farmbot";
import { determineEditable } from "../../resources/sequence_meta";
import { t } from "../../i18next_wrapper";
export interface AxisEditProps {
axis: Xyz;
onChange: (sd: VariableNode) => void;
editableVariable: VariableNode;
}
/** Update a variable coordinate. */
export const manuallyEditAxis = (props: AxisEditProps) =>
(e: React.SyntheticEvent<HTMLInputElement>) => {
const { axis, onChange, editableVariable } = props;
const num = parseFloat(e.currentTarget.value);
if (editableVariable.kind !== "parameter_declaration" &&
editableVariable.args.data_value.kind === "coordinate") {
editableVariable.args.data_value.args[axis] = num;
!isNaN(num) && onChange(editableVariable);
}
};
/** For LocationForm coordinate input boxes. */
interface CoordinateInputBoxesProps {
vector: Vector3 | undefined;
variableNode: VariableNode;
width: number | undefined;
onChange: (sd: VariableNode | undefined) => void;
}
/** LocationForm coordinate input boxes. */
export const CoordinateInputBoxes = (props: CoordinateInputBoxesProps) => {
const { variableNode, vector, onChange } = props;
/** Show coordinate input boxes if editable (not using external data). */
const visible = determineEditable(variableNode);
const editableVariable = defensiveClone(variableNode);
return (vector && visible)
? <Row>
{["x", "y", "z"].map((axis: Xyz) =>
<Col xs={props.width || 4} key={axis}>
<label>
{t("{{axis}} (mm)", { axis })}
</label>
<BlurableInput type="number"
onCommit={manuallyEditAxis({ axis, onChange, editableVariable })}
name={`location-${axis}`}
value={"" + vector[axis]} />
</Col>)}
</Row>
: <div />;
};

View File

@ -226,10 +226,7 @@ describe("Pin and Peripheral support files", () => {
const ri = buildResourceIndex([]).index;
const n: Nothing = { kind: "nothing", args: {} };
const result = namedPin2DropDown(ri, n);
const expected: DropDownItem = {
label: "Select a pin", value: "", isNull: true
};
expect(result).toEqual(expected);
expect(result).toEqual(undefined);
});
it("Rejects typos", () => {

View File

@ -81,12 +81,30 @@ describe("<TileMoveAbsolute/>", () => {
});
it("expands form", () => {
const wrapper = mount<TileMoveAbsolute>(<TileMoveAbsolute {...fakeProps()} />);
const p = fakeProps();
p.expandStepOptions = false;
(p.currentStep as MoveAbsolute).args.offset.args = { x: 0, y: 0, z: 0 };
const wrapper = mount<TileMoveAbsolute>(<TileMoveAbsolute {...p} />);
expect(wrapper.state().more).toEqual(false);
wrapper.find("h4").simulate("click");
expect(wrapper.state().more).toEqual(true);
});
it("expands form by default", () => {
const p = fakeProps();
p.expandStepOptions = true;
const wrapper = mount<TileMoveAbsolute>(<TileMoveAbsolute {...p} />);
expect(wrapper.state().more).toEqual(true);
});
it("expands form when offset is present", () => {
const p = fakeProps();
p.expandStepOptions = false;
(p.currentStep as MoveAbsolute).args.offset.args.z = 100;
const wrapper = mount<TileMoveAbsolute>(<TileMoveAbsolute {...p} />);
expect(wrapper.state().more).toEqual(true);
});
describe("updateArgs", () => {
it("calls OVERWRITE_RESOURCE for the correct resource", () => {
const tma = ordinaryMoveAbs();

View File

@ -141,7 +141,7 @@ export const findByPinNumber =
};
export function namedPin2DropDown(ri: ResourceIndex, input: NamedPin | Nothing):
DropDownItem {
DropDownItem | undefined {
if (input.kind === "named_pin") {
const { pin_type } = input.args;
if (isPinType(pin_type)) {
@ -164,7 +164,6 @@ export function namedPin2DropDown(ri: ResourceIndex, input: NamedPin | Nothing):
bail("Bad pin_type: " + JSON.stringify(pin_type));
}
}
return { label: t("Select a pin"), value: "", isNull: true };
}
export const dropDown2CeleryArg =
@ -211,7 +210,8 @@ export const setArgsDotPinNumber =
type PinNumber = ReadPin["args"]["pin_number"];
export function celery2DropDown(input: PinNumber, ri: ResourceIndex): DropDownItem {
export function celery2DropDown(input: PinNumber, ri: ResourceIndex):
DropDownItem | undefined {
return isNumber(input)
? pinNumber2DropDown(n => n)(input)
: namedPin2DropDown(ri, input);

View File

@ -24,9 +24,13 @@ import { Collapse } from "@blueprintjs/core";
import { ExpandableHeader } from "../../ui/expandable_header";
export class TileMoveAbsolute extends React.Component<StepParams, MoveAbsState> {
state: MoveAbsState = { more: !!this.props.expandStepOptions };
state: MoveAbsState = {
more: !!this.props.expandStepOptions || this.hasOffset };
get step() { return this.props.currentStep as MoveAbsolute; }
get args() { return this.step.args; }
get hasOffset(): boolean {
const {x, y, z} = this.args.offset.args;
return !!(x || y || z); }
/** Merge step args update into step args. */
updateArgs = (update: Partial<MoveAbsolute["args"]>) => {

View File

@ -45,6 +45,7 @@ export function TileReadPin(props: StepParams) {
<FBSelect
key={JSON.stringify(props.currentSequence)}
selectedItem={celery2DropDown(pin_number, props.resources)}
customNullLabel={t("Select a sensor")}
onChange={setArgsDotPinNumber(props)}
list={pinsAsDropDownsReadPin(props.resources, !!props.showPins)} />
</Col>

View File

@ -54,6 +54,7 @@ export function TileWritePin(props: StepParams) {
<FBSelect
key={JSON.stringify(props.currentSequence)}
selectedItem={celery2DropDown(pin_number, props.resources)}
customNullLabel={t("Select a peripheral")}
onChange={setArgsDotPinNumber(props)}
list={pinsAsDropDownsWritePin(props.resources, !!props.showPins)} />
</Col>