misc fixes and cleanup
parent
68c3ac9308
commit
76153f28e3
|
@ -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"));
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() } }));
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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", () => {
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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" });
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
|
|
@ -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 />;
|
||||
};
|
|
@ -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", () => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"]>) => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue