Misc bug fixes and refactoring (#627)

* allow external docs links

* match upgrade path test cases

* style updates: prefer return < over (

* refactor util

* import refactor

* fix sequence step scroll

* refactor mocked paths

* remove unnecessary prop

* zoom level checks

* misc fixes

* sync and estop button fixes

* don't sync WebAppConfigs
pull/628/head
Gabriel Burnworth 2018-01-20 06:46:44 -08:00 committed by Rick Carlino
parent 88b6e5625d
commit 29a14e3997
160 changed files with 2261 additions and 2330 deletions

View File

@ -53,7 +53,6 @@ module FarmBot
default_src: %w(https: 'self'),
base_uri: %w('self'),
block_all_mixed_content: false, # :( Some webcam feeds use http://
child_src: %w('self'),
connect_src: ALL_LOCAL_URIS + [ENV["MQTT_HOST"],
"api.github.com",
"raw.githubusercontent.com",
@ -68,7 +67,7 @@ module FarmBot
fonts.gstatic.com
),
form_action: %w('self'),
frame_ancestors: %w(*), # We need "*" to support webcam users.
frame_src: %w(*), # We need "*" to support webcam users.
img_src: %w(* data:), # We need "*" to support webcam users.
manifest_src: %w('self'),
media_src: %w(),
@ -78,6 +77,7 @@ module FarmBot
allow-forms
allow-same-origin
allow-modals
allow-popups
),
plugin_types: %w(),
script_src: %w(

View File

@ -47,9 +47,9 @@ describe SessionToken do
expect(test_case["0.0.0"]).to eq(CalculateUpgrade::OLD_OS_URL)
expect(test_case["5.0.5"]).to eq(CalculateUpgrade::OLD_OS_URL)
expect(test_case["5.0.6"]).to eq(CalculateUpgrade::OLD_OS_URL)
expect(test_case["5.0.7"]).to eq(CalculateUpgrade::MID_OS_URL)
expect(test_case["5.0.8"]).to eq(CalculateUpgrade::MID_OS_URL)
expect(test_case["5.1.0"]).to eq(CalculateUpgrade::OS_RELEASE)
expect(test_case["5.0.9"]).to eq(CalculateUpgrade::MID_OS_URL)
expect(test_case["6.0.1"]).to eq(CalculateUpgrade::OS_RELEASE)
end
it "doesn't honor expired tokens" do

View File

@ -6,6 +6,11 @@ jest.mock("react-redux", () => ({
connect: jest.fn()
}));
let mockPath = "";
jest.mock("../history", () => ({
getPathArray: jest.fn(() => { return mockPath.split("/"); })
}));
import * as React from "react";
import { App, AppProps } from "../app";
import { mount } from "enzyme";
@ -29,9 +34,7 @@ describe("<App />: Controls Pop-Up", () => {
function controlsPopUp(page: string, exists: boolean) {
it(`doesn't render controls pop-up on ${page} page`, () => {
Object.defineProperty(location, "pathname", {
value: "/app/" + page, configurable: true
});
mockPath = "/app/" + page;
const wrapper = mount(<App {...fakeProps() } />);
if (exists) {
expect(wrapper.html()).toContain("controls-popup");

View File

@ -2,13 +2,11 @@ jest.mock("fastclick", () => ({
attach: jest.fn(),
}));
import { auth } from "../__test_support__/fake_state/token";
const mockAuth = auth;
let mockAuth: AuthState | undefined = undefined;
const mockClear = jest.fn();
jest.mock("../session", () => ({
Session: {
fetchStoredToken: jest.fn(() => mockAuth)
.mockImplementationOnce(() => { }),
fetchStoredToken: jest.fn(() => mockAuth),
deprecatedGetNum: () => undefined,
deprecatedGetBool: () => undefined,
getAll: () => undefined,
@ -20,6 +18,8 @@ import * as React from "react";
import { shallow } from "enzyme";
import { RootComponent } from "../routes";
import { store } from "../redux/store";
import { AuthState } from "../auth/interfaces";
import { auth } from "../__test_support__/fake_state/token";
describe("<RootComponent />", () => {
beforeEach(function () {
@ -27,6 +27,7 @@ describe("<RootComponent />", () => {
});
it("clears session when not authorized", () => {
mockAuth = undefined;
Object.defineProperty(location, "pathname", {
value: "/app/account"
});
@ -35,6 +36,7 @@ describe("<RootComponent />", () => {
});
it("authorized", () => {
mockAuth = auth;
Object.defineProperty(location, "pathname", {
value: "/app/account"
});

View File

@ -1,301 +0,0 @@
import {
prettyPrintApiErrors,
defensiveClone,
getParam,
betterCompact,
safeStringFetch,
oneOf,
semverCompare,
SemverResult,
trim,
bitArray,
withTimeout,
minFwVersionCheck,
move,
shortRevision,
clampUnsignedInteger,
isUndefined,
IntegerSize,
Progress,
colors,
randomColor
} from "../util";
import { times } from "lodash";
describe("util", () => {
describe("safeStringFetch", () => {
const data = {
// tslint:disable-next-line:no-null-keyword
"null": null,
"undefined": undefined,
"number": 0,
"string": "hello",
"boolean": false,
"other": () => { "not allowed!"; }
};
it("fetches null", () => {
expect(safeStringFetch(data, "null")).toEqual("");
});
it("fetches undefined", () => {
expect(safeStringFetch(data, "undefined")).toEqual("");
});
it("fetches number", () => {
expect(safeStringFetch(data, "number")).toEqual("0");
});
it("fetches string", () => {
expect(safeStringFetch(data, "string")).toEqual("hello");
});
it("fetches boolean", () => {
expect(safeStringFetch(data, "boolean")).toEqual("false");
});
it("handles others with exception", () => {
expect(() => safeStringFetch(data, "other")).toThrow();
});
});
describe("betterCompact", () => {
it("removes falsy values", () => {
const before = [{}, {}, undefined];
const after = betterCompact(before);
expect(after.length).toBe(2);
expect(after).not.toContain(undefined);
});
});
describe("defensiveClone", () => {
it("deep clones any serializable object", () => {
const origin = { a: "b", c: 2, d: [{ e: { f: "g" } }] };
const child = defensiveClone(origin);
origin.a = "--";
origin.c = 0;
origin.d[0].e.f = "--";
expect(child).not.toBe(origin);
expect(child.a).toEqual("b");
expect(child.c).toEqual(2);
expect(child.d[0].e.f).toEqual("g");
});
});
describe("getParam", () => {
it("gets params", () => {
Object.defineProperty(window.location, "search", {
writable: true,
value: "?foo=bar&baz=wow"
});
expect(getParam("foo")).toEqual("bar");
expect(getParam("baz")).toEqual("wow");
expect(getParam("blah")).toEqual("");
});
});
describe("prettyPrintApiErrors", () => {
it("handles properly formatted API error messages", () => {
const result = prettyPrintApiErrors({
response: {
data: {
email: "can't be blank"
}
}
});
expect(result).toEqual("Email: can't be blank");
});
});
describe("oneOf()", () => {
it("determines matches", () => {
expect(oneOf(["foo"], "foobar")).toBeTruthy();
expect(oneOf(["foo", "baz"], "foo bar baz")).toBeTruthy();
});
it("determines non-matches", () => {
expect(oneOf(["foo"], "QMMADSDASDASD")).toBeFalsy();
expect(oneOf(["foo", "baz"], "nothing to see here.")).toBeFalsy();
});
});
describe("semver compare", () => {
it("knows when RIGHT_IS_GREATER", () => {
expect(semverCompare("3.1.6", "4.0.0"))
.toBe(SemverResult.RIGHT_IS_GREATER);
expect(semverCompare("2.1.6", "4.1.0"))
.toBe(SemverResult.RIGHT_IS_GREATER);
expect(semverCompare("4.1.6", "5.1.9"))
.toBe(SemverResult.RIGHT_IS_GREATER);
expect(semverCompare("1.1.9", "2.0.2"))
.toBe(SemverResult.RIGHT_IS_GREATER);
expect(semverCompare("", "1.0.0"))
.toBe(SemverResult.RIGHT_IS_GREATER);
});
it("knows when LEFT_IS_GREATER", () => {
expect(semverCompare("4.0.0", "3.1.6"))
.toBe(SemverResult.LEFT_IS_GREATER);
expect(semverCompare("4.1.0", "2.1.6"))
.toBe(SemverResult.LEFT_IS_GREATER);
expect(semverCompare("5.1.9", "4.1.6"))
.toBe(SemverResult.LEFT_IS_GREATER);
expect(semverCompare("2.0.2", "1.1.9"))
.toBe(SemverResult.LEFT_IS_GREATER);
expect(semverCompare("1.0.0", ""))
.toBe(SemverResult.LEFT_IS_GREATER);
expect(semverCompare("1.0.0", "x.y.z"))
.toBe(SemverResult.LEFT_IS_GREATER);
expect(semverCompare("x.y.z", "1.0.0"))
.toBe(SemverResult.RIGHT_IS_GREATER);
});
});
});
describe("trim()", () => {
it("formats whitespace", () => {
const string = `foo
bar`;
const formattedString = trim(string);
expect(formattedString).toEqual("foo bar");
});
});
describe("bitArray", () => {
it("converts flags to numbers", () => {
expect(bitArray(true)).toBe(0b1);
expect(bitArray(true, false)).toBe(0b10);
expect(bitArray(false, true)).toBe(0b01);
expect(bitArray(true, true)).toBe(0b11);
});
});
describe("withTimeout()", () => {
it("rejects promises that do not meet a particular deadline", (done) => {
const p = new Promise(res => setTimeout(() => res("Done"), 10));
withTimeout(1, p).then(fail, (y) => {
expect(y).toContain("Timed out");
done();
});
});
it("resolves promises that meet a particular deadline", (done) => {
withTimeout(10, new Promise(res => setTimeout(() => res("Done"), 1)))
.then(y => {
expect(y).toContain("Done");
done();
}, fail);
});
});
describe("minFwVersionCheck()", () => {
it("firmware version meets or exceeds minimum", () => {
expect(minFwVersionCheck("1.0.1R", "1.0.1")).toBeTruthy();
expect(minFwVersionCheck("1.0.2F", "1.0.1")).toBeTruthy();
});
it("firmware version doesn't meet minimum", () => {
expect(minFwVersionCheck("1.0.0R", "1.0.1")).toBeFalsy();
expect(minFwVersionCheck(undefined, "1.0.1")).toBeFalsy();
expect(minFwVersionCheck("1.0.0", "1.0.1")).toBeFalsy();
});
});
describe("move()", () => {
it("shuffles array elems", () => {
const fixture = [0, 1, 2];
const case1 = move(fixture, 0, 2);
expect(case1[0]).toEqual(1);
expect(case1[1]).toEqual(2);
expect(case1[2]).toEqual(0);
const case2 = move(fixture, 1, 0);
expect(case2[0]).toEqual(1);
expect(case2[1]).toEqual(0);
expect(case2[2]).toEqual(2);
const case3 = move(fixture, 0, 0);
expect(case3).toEqual(fixture);
});
});
describe("shortRevision()", () => {
it("none", () => {
globalConfig.SHORT_REVISION = "";
const short = shortRevision();
expect(short).toEqual("NONE");
});
it("slices", () => {
globalConfig.SHORT_REVISION = "0123456789";
const short = shortRevision();
expect(short).toEqual("01234567");
});
});
describe("clampUnsignedInteger()", () => {
function clampTest(
input: string,
output: number | undefined,
message: string,
size: IntegerSize) {
it(`${size}: ${message}`, () => {
const result = clampUnsignedInteger(input, size);
expect(result.outcome).toEqual(message);
expect(result.result).toEqual(output);
});
}
clampTest("nope", undefined, "malformed", "short");
clampTest("100000", 32000, "high", "short");
clampTest("-100000", 0, "low", "short");
clampTest("1000", 1000, "ok", "short");
clampTest("1000000", 1000000, "ok", "long");
clampTest("-1000000", 0, "low", "long");
});
describe("isUndefined()", () => {
it("undefined", () => {
const result = isUndefined(undefined);
expect(result).toBeTruthy();
});
it("defined", () => {
const result = isUndefined({});
expect(result).toBeFalsy();
});
});
describe("Progress", () => {
it("increments", () => {
const cb = jest.fn();
const counter = new Progress(3, cb);
counter.inc();
expect(cb).toHaveBeenCalledWith(counter);
counter.inc();
counter.inc(); // Now we're done.
cb.mockClear();
counter.inc();
expect(cb).not.toHaveBeenCalled();
});
it("force finishes", () => {
const cb = jest.fn();
const counter = new Progress(3, cb);
counter.finish();
expect(cb).toHaveBeenCalled();
expect(counter.isDone).toBeTruthy();
});
});
describe("randomColor()", () => {
it("only picks valid colors", () => {
times(colors.length * 1.5, () => expect(colors).toContain(randomColor()));
});
});

View File

@ -6,7 +6,7 @@ import {
WidgetHeader,
WidgetBody,
SaveBtn
} from "../../ui";
} from "../../ui/index";
import { SpecialStatus } from "../../resources/tagged_resources";
import Axios from "axios";
import { API } from "../../api/index";

View File

@ -7,7 +7,7 @@ import {
WidgetBody,
Col,
Row
} from "../../ui";
} from "../../ui/index";
import { DeleteAccountProps, DeleteAccountState } from "../interfaces";
import { Content } from "../../constants";

View File

@ -1,6 +1,8 @@
import * as React from "react";
import { t } from "i18next";
import { BlurableInput, Widget, WidgetHeader, WidgetBody, SaveBtn } from "../../ui";
import {
BlurableInput, Widget, WidgetHeader, WidgetBody, SaveBtn
} from "../../ui/index";
import { SettingsPropTypes } from "../interfaces";
export class Settings extends React.Component<SettingsPropTypes, {}> {

View File

@ -3,7 +3,7 @@ import { t } from "i18next";
import { connect } from "react-redux";
import { Settings, DeleteAccount, ChangePassword } from "./components";
import { Props } from "./interfaces";
import { Page, Row, Col } from "../ui";
import { Page, Row, Col } from "../ui/index";
import { mapStateToProps } from "./state_to_props";
import { User } from "../auth/interfaces";
import { edit, save } from "../api/crud";

View File

@ -8,7 +8,9 @@ import {
import { GetState, ReduxAction } from "../redux/interfaces";
import { API } from "./index";
import axios from "axios";
import { updateOK, updateNO, destroyOK, destroyNO, GeneralizedError } from "../resources/actions";
import {
updateOK, updateNO, destroyOK, destroyNO, GeneralizedError
} from "../resources/actions";
import { UnsafeError } from "../interfaces";
import { findByUuid } from "../resources/reducer";
import { generateUuid } from "../resources/util";

View File

@ -5,7 +5,8 @@ const BLACKLIST: ResourceName[] = [
"Log",
"Image",
"WebcamFeed",
"User"
"User",
"WebAppConfig",
];
export function maybeStartTracking(uuid: string) {

View File

@ -20,6 +20,7 @@ import { Content } from "./constants";
import { catchErrors } from "./util";
import { Session } from "./session";
import { BooleanSetting } from "./session_keys";
import { getPathArray } from "./history";
/** Remove 300ms delay on touch devices - https://github.com/ftlabs/fastclick */
const fastClick = require("fastclick");
@ -99,7 +100,7 @@ export class App extends React.Component<AppProps, {}> {
render() {
const syncLoaded = this.isLoaded;
const currentPath = window.location.pathname;
const currentPage = getPathArray()[2];
return <div className="app">
<HotKeys dispatch={this.props.dispatch} />
<NavBar
@ -113,9 +114,7 @@ export class App extends React.Component<AppProps, {}> {
/>
{!syncLoaded && <LoadingPlant />}
{syncLoaded && this.props.children}
{!currentPath.startsWith("/app/controls") &&
!currentPath.startsWith("/app/account") &&
!currentPath.startsWith("/app/regimens") &&
{!(["controls", "account", "regimens"].includes(currentPage)) &&
<ControlsPopup
dispatch={this.props.dispatch}
axisInversion={this.props.axisInversion}

View File

@ -19,7 +19,9 @@ jest.mock("../../redux/store", () => {
import { getDevice } from "../../device";
import { store } from "../../redux/store";
import { Actions } from "../../constants";
import { startTracking, outstandingRequests, stopTracking, cleanUUID } from "../data_consistency";
import {
startTracking, outstandingRequests, stopTracking, cleanUUID
} from "../data_consistency";
const unprocessedUuid = "~UU.ID~";
const niceUuid = cleanUUID(unprocessedUuid);

View File

@ -1,6 +1,8 @@
import { GetState } from "../redux/interfaces";
import { maybeDetermineUuid } from "../resources/selectors";
import { ResourceName, TaggedResource, SpecialStatus } from "../resources/tagged_resources";
import {
ResourceName, TaggedResource, SpecialStatus
} from "../resources/tagged_resources";
import { overwrite, init } from "../api/crud";
import { handleInbound } from "./auto_sync_handle_inbound";
import { SyncPayload, MqttDataResult, Reason, UpdateMqttData } from "./interfaces";

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { t } from "i18next";
import { Row, Col } from "../ui";
import { Row, Col } from "../ui/index";
import { AxisDisplayGroupProps } from "./interfaces";
import { isNumber } from "lodash";

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { AxisInputBox } from "./axis_input_box";
import { t } from "i18next";
import { Row, Col } from "../ui";
import { Row, Col } from "../ui/index";
import {
AxisInputBoxGroupProps,
AxisInputBoxGroupState,

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { connect } from "react-redux";
import { Peripherals } from "./peripherals";
import { Row, Page, Col } from "../ui";
import { Row, Page, Col } from "../ui/index";
import { mapStateToProps } from "./state_to_props";
import { WebcamPanel } from "./webcam";
import { Props, MoveProps } from "./interfaces";

View File

@ -4,7 +4,7 @@ import { changeStepSize, moveAbs } from "../devices/actions";
import { EStopButton } from "../devices/components/e_stop_btn";
import { JogButtons } from "./jog_buttons";
import { AxisInputBoxGroup } from "./axis_input_box_group";
import { Row, Col, Widget, WidgetBody, WidgetHeader } from "../ui";
import { Row, Col, Widget, WidgetBody, WidgetHeader } from "../ui/index";
import { StepSizeSelector } from "./step_size_selector";
import { MustBeOnline } from "../devices/must_be_online";
import { ToolTips } from "../constants";
@ -52,113 +52,111 @@ export class Move extends React.Component<MoveProps, {}> {
? "Scaled Encoder (mm)"
: "Scaled Encoder (steps)";
return (
<Widget>
<WidgetHeader
title="Move"
helpText={ToolTips.MOVE}>
<Popover position={Position.BOTTOM_RIGHT}>
<i className="fa fa-gear" />
<div>
<label>
{t("Invert Jog Buttons")}
</label>
<fieldset>
<label>
{t("X Axis")}
</label>
<button
className={"fb-button fb-toggle-button " + xBtnColor}
onClick={this.toggle("x")} />
</fieldset>
<fieldset>
<label>
{t("Y Axis")}
</label>
<button
className={"fb-button fb-toggle-button " + yBtnColor}
onClick={this.toggle("y")} />
</fieldset>
<fieldset>
<label>
{t("Z Axis")}
</label>
<button
className={"fb-button fb-toggle-button " + zBtnColor}
onClick={this.toggle("z")} />
</fieldset>
<label>
{t("Display Encoder Data")}
</label>
<fieldset>
<label>
{t("Scaled encoder position")}
</label>
<button
className={"fb-button fb-toggle-button " + scaledBtnColor}
onClick={this.toggle_encoder_data("scaled_encoders")} />
</fieldset>
<fieldset>
<label>
{t("Raw encoder position")}
</label>
<button
className={"fb-button fb-toggle-button " + rawBtnColor}
onClick={this.toggle_encoder_data("raw_encoders")} />
</fieldset>
</div>
</Popover>
<EStopButton
bot={this.props.bot}
user={this.props.user} />
</WidgetHeader>
<WidgetBody>
<MustBeOnline
lockOpen={process.env.NODE_ENV !== "production"}
networkState={this.props.botToMqttStatus}
syncStatus={this.props.bot.hardware.informational_settings.sync_status}>
<label className="text-center">
{t("MOVE AMOUNT (mm)")}
return <Widget>
<WidgetHeader
title="Move"
helpText={ToolTips.MOVE}>
<Popover position={Position.BOTTOM_RIGHT}>
<i className="fa fa-gear" />
<div>
<label>
{t("Invert Jog Buttons")}
</label>
<StepSizeSelector
choices={[1, 10, 100, 1000, 10000]}
selector={num => this.props.dispatch(changeStepSize(num))}
selected={this.props.bot.stepSize} />
<JogButtons
bot={this.props.bot}
x_axis_inverted={x_axis_inverted}
y_axis_inverted={y_axis_inverted}
z_axis_inverted={z_axis_inverted}
disabled={this.props.disabled} />
<Row>
<Col xs={3}>
<label>{t("X AXIS")}</label>
</Col>
<Col xs={3}>
<label>{t("Y AXIS")}</label>
</Col>
<Col xs={3}>
<label>{t("Z AXIS")}</label>
</Col>
</Row>
<fieldset>
<label>
{t("X Axis")}
</label>
<button
className={"fb-button fb-toggle-button " + xBtnColor}
onClick={this.toggle("x")} />
</fieldset>
<fieldset>
<label>
{t("Y Axis")}
</label>
<button
className={"fb-button fb-toggle-button " + yBtnColor}
onClick={this.toggle("y")} />
</fieldset>
<fieldset>
<label>
{t("Z Axis")}
</label>
<button
className={"fb-button fb-toggle-button " + zBtnColor}
onClick={this.toggle("z")} />
</fieldset>
<label>
{t("Display Encoder Data")}
</label>
<fieldset>
<label>
{t("Scaled encoder position")}
</label>
<button
className={"fb-button fb-toggle-button " + scaledBtnColor}
onClick={this.toggle_encoder_data("scaled_encoders")} />
</fieldset>
<fieldset>
<label>
{t("Raw encoder position")}
</label>
<button
className={"fb-button fb-toggle-button " + rawBtnColor}
onClick={this.toggle_encoder_data("raw_encoders")} />
</fieldset>
</div>
</Popover>
<EStopButton
bot={this.props.bot}
user={this.props.user} />
</WidgetHeader>
<WidgetBody>
<MustBeOnline
lockOpen={process.env.NODE_ENV !== "production"}
networkState={this.props.botToMqttStatus}
syncStatus={this.props.bot.hardware.informational_settings.sync_status}>
<label className="text-center">
{t("MOVE AMOUNT (mm)")}
</label>
<StepSizeSelector
choices={[1, 10, 100, 1000, 10000]}
selector={num => this.props.dispatch(changeStepSize(num))}
selected={this.props.bot.stepSize} />
<JogButtons
bot={this.props.bot}
x_axis_inverted={x_axis_inverted}
y_axis_inverted={y_axis_inverted}
z_axis_inverted={z_axis_inverted}
disabled={this.props.disabled} />
<Row>
<Col xs={3}>
<label>{t("X AXIS")}</label>
</Col>
<Col xs={3}>
<label>{t("Y AXIS")}</label>
</Col>
<Col xs={3}>
<label>{t("Z AXIS")}</label>
</Col>
</Row>
<AxisDisplayGroup
position={motor_coordinates}
label={"Motor Coordinates (mm)"} />
{scaled_encoders &&
<AxisDisplayGroup
position={motor_coordinates}
label={"Motor Coordinates (mm)"} />
{scaled_encoders &&
<AxisDisplayGroup
position={scaled_encoders_data}
label={scaled_encoder_label} />}
{raw_encoders &&
<AxisDisplayGroup
position={raw_encoders_data}
label={"Raw Encoder data"} />}
<AxisInputBoxGroup
position={motor_coordinates}
onCommit={input => moveAbs(input)}
disabled={this.props.disabled} />
</MustBeOnline>
</WidgetBody>
</Widget>
);
position={scaled_encoders_data}
label={scaled_encoder_label} />}
{raw_encoders &&
<AxisDisplayGroup
position={raw_encoders_data}
label={"Raw Encoder data"} />}
<AxisInputBoxGroup
position={motor_coordinates}
onCommit={input => moveAbs(input)}
disabled={this.props.disabled} />
</MustBeOnline>
</WidgetBody>
</Widget>;
}
}

View File

@ -3,10 +3,12 @@ import { t } from "i18next";
import { error } from "farmbot-toastr";
import { PeripheralList } from "./peripheral_list";
import { PeripheralForm } from "./peripheral_form";
import { Widget, WidgetBody, WidgetHeader, SaveBtn } from "../../ui";
import { Widget, WidgetBody, WidgetHeader, SaveBtn } from "../../ui/index";
import { PeripheralsProps } from "../../devices/interfaces";
import { PeripheralState } from "./interfaces";
import { TaggedPeripheral, getArrayStatus, SpecialStatus } from "../../resources/tagged_resources";
import {
TaggedPeripheral, getArrayStatus, SpecialStatus
} from "../../resources/tagged_resources";
import { saveAll, init } from "../../api/crud";
import { ToolTips } from "../../constants";
import * as _ from "lodash";

View File

@ -42,13 +42,11 @@ export class ToggleButton extends React.Component<ToggleButtonProps, {}> {
render() {
const cb = () => !this.props.disabled && this.props.toggleAction();
return (
<button
disabled={!!this.props.disabled}
className={this.css()}
onClick={cb}>
{this.caption()}
</button>
);
return <button
disabled={!!this.props.disabled}
className={this.css()}
onClick={cb}>
{this.caption()}
</button>;
}
}

View File

@ -27,28 +27,26 @@ export function Edit(props: WebcamPanelProps) {
const unsaved = props
.feeds
.filter(x => x.specialStatus === SpecialStatus.DIRTY);
return (
<Widget>
<WidgetHeader title="Edit" helpText={ToolTips.WEBCAM}>
<button
className="fb-button green"
onClick={props.init}>
<i className="fa fa-plus" />
</button>
<button
className="fb-button green"
onClick={() => { unsaved.map(x => props.save(x)); }}>
{t("Save")}{unsaved.length > 0 ? "*" : ""}
</button>
<button
className="fb-button gray"
onClick={props.onToggle}>
{t("View")}
</button>
</WidgetHeader>
<div className="widget-body">
{rows}
</div>
</Widget>
);
return <Widget>
<WidgetHeader title="Edit" helpText={ToolTips.WEBCAM}>
<button
className="fb-button green"
onClick={props.init}>
<i className="fa fa-plus" />
</button>
<button
className="fb-button green"
onClick={() => { unsaved.map(x => props.save(x)); }}>
{t("Save")}{unsaved.length > 0 ? "*" : ""}
</button>
<button
className="fb-button gray"
onClick={props.onToggle}>
{t("View")}
</button>
</WidgetHeader>
<div className="widget-body">
{rows}
</div>
</Widget>;
}

View File

@ -1,11 +1,10 @@
import * as React from "react";
import { Widget, WidgetHeader } from "../../ui/index";
import { Widget, WidgetHeader, FallbackImg } from "../../ui/index";
import { t } from "i18next";
import { ToolTips } from "../../constants";
import { WebcamPanelProps } from "./interfaces";
import { PLACEHOLDER_FARMBOT } from "../../farmware/images/image_flipper";
import { Flipper } from "./flipper";
import { FallbackImg } from "../../ui/fallback_img";
import { sortedFeeds } from "./edit";
type State = {
@ -54,41 +53,39 @@ export class Show extends React.Component<WebcamPanelProps, State> {
const title = flipper.current.name || "Webcam Feeds";
const msg = this.getMessage(flipper.current.url);
const imageClass = msg.length > 0 ? "no-flipper-image-container" : "";
return (
<Widget>
<WidgetHeader title={title} helpText={ToolTips.WEBCAM}>
<button
className="fb-button gray"
onClick={props.onToggle}>
{t("Edit")}
</button>
<IndexIndicator i={this.state.current} total={feeds.length} />
</WidgetHeader>
<div className="widget-body">
<div className="image-flipper">
<div className={imageClass}>
<p>{msg}</p>
<FallbackImg className="image-flipper-image"
src={flipper.current.url}
fallback={PLACEHOLDER_FARMBOT} />
</div>
<button
onClick={() => flipper.down((_, current) => this.setState({ current }))}
hidden={feeds.length < 2}
disabled={false}
className="image-flipper-left fb-button">
{t("Prev")}
</button>
<button
onClick={() => flipper.up((_, current) => this.setState({ current }))}
hidden={feeds.length < 2}
disabled={false}
className="image-flipper-right fb-button">
{t("Next")}
</button>
return <Widget>
<WidgetHeader title={title} helpText={ToolTips.WEBCAM}>
<button
className="fb-button gray"
onClick={props.onToggle}>
{t("Edit")}
</button>
<IndexIndicator i={this.state.current} total={feeds.length} />
</WidgetHeader>
<div className="widget-body">
<div className="image-flipper">
<div className={imageClass}>
<p>{msg}</p>
<FallbackImg className="image-flipper-image"
src={flipper.current.url}
fallback={PLACEHOLDER_FARMBOT} />
</div>
<button
onClick={() => flipper.down((_, current) => this.setState({ current }))}
hidden={feeds.length < 2}
disabled={false}
className="image-flipper-left fb-button">
{t("Prev")}
</button>
<button
onClick={() => flipper.up((_, current) => this.setState({ current }))}
hidden={feeds.length < 2}
disabled={false}
className="image-flipper-right fb-button">
{t("Next")}
</button>
</div>
</Widget>
);
</div>
</Widget>;
}
}

View File

@ -29,16 +29,10 @@ jest.mock("farmbot-toastr", () => ({
error: mockError
}));
let mockGetRelease: Promise<{}> = Promise.resolve({});
jest.mock("axios", () => ({
default: {
get: jest.fn(() => { return Promise.reject("error"); })
.mockImplementationOnce(() => { return Promise.resolve(); })
.mockImplementationOnce(() => {
return Promise.resolve({ data: { tag_name: "v1.0.0" } });
})
.mockImplementationOnce(() => {
return Promise.resolve({ data: { tag_name: "v1.0.0-beta" } });
})
get: jest.fn(() => { return mockGetRelease; })
}
}));
@ -260,6 +254,7 @@ describe("fetchReleases()", () => {
});
it("fetches latest OS release version", async () => {
mockGetRelease = Promise.resolve({ data: { tag_name: "v1.0.0" } });
const dispatch = jest.fn();
await actions.fetchReleases("url")(dispatch, jest.fn());
expect(axios.get).toHaveBeenCalledWith("url");
@ -271,6 +266,7 @@ describe("fetchReleases()", () => {
});
it("fetches latest beta OS release version", async () => {
mockGetRelease = Promise.resolve({ data: { tag_name: "v1.0.0-beta" } });
const dispatch = jest.fn();
await actions.fetchReleases("url", { beta: true })(dispatch, jest.fn());
expect(axios.get).toHaveBeenCalledWith("url");
@ -282,6 +278,7 @@ describe("fetchReleases()", () => {
});
it("fails to fetches latest OS release version", async () => {
mockGetRelease = Promise.reject("error");
const dispatch = jest.fn();
await actions.fetchReleases("url")(dispatch, jest.fn());
await expect(axios.get).toHaveBeenCalledWith("url");
@ -294,6 +291,7 @@ describe("fetchReleases()", () => {
});
it("fails to fetches latest beta OS release version", async () => {
mockGetRelease = Promise.reject("error");
const dispatch = jest.fn();
await actions.fetchReleases("url", { beta: true })(dispatch, jest.fn());
await expect(axios.get).toHaveBeenCalledWith("url");

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { shallow } from "enzyme";
import { MustBeOnline } from "../must_be_online";
import { MustBeOnline, isBotUp } from "../must_be_online";
describe("<MustBeOnline/>", function () {
it("Covers content when status is 'unknown'", function () {
@ -29,3 +29,15 @@ describe("<MustBeOnline/>", function () {
expect(overlay.hasClass("banner")).toBeFalsy();
});
});
describe("isBotUp()", () => {
it("is up", () => {
expect(isBotUp("synced")).toBeTruthy();
});
it("is not up", () => {
expect(isBotUp("unknown")).toBeFalsy();
expect(isBotUp("maintenance")).toBeFalsy();
expect(isBotUp(undefined)).toBeFalsy();
});
});

View File

@ -0,0 +1,29 @@
import * as React from "react";
import { mount } from "enzyme";
import { EStopButton } from "../e_stop_btn";
import { bot } from "../../../__test_support__/fake_state/bot";
import { taggedUser } from "../../../__test_support__/user";
describe("<EStopButton />", () => {
it("renders", () => {
bot.hardware.informational_settings.sync_status = "synced";
const wrapper = mount(<EStopButton bot={bot} user={taggedUser} />);
expect(wrapper.text()).toEqual("E-STOP");
expect(wrapper.find("button").hasClass("red")).toBeTruthy();
});
it("grayed out", () => {
bot.hardware.informational_settings.sync_status = undefined;
const wrapper = mount(<EStopButton bot={bot} user={taggedUser} />);
expect(wrapper.text()).toEqual("E-STOP");
expect(wrapper.find("button").hasClass("gray")).toBeTruthy();
});
it("locked", () => {
bot.hardware.informational_settings.sync_status = "synced";
bot.hardware.informational_settings.locked = true;
const wrapper = mount(<EStopButton bot={bot} user={taggedUser} />);
expect(wrapper.text()).toEqual("UNLOCK");
expect(wrapper.find("button").hasClass("yellow")).toBeTruthy();
});
});

View File

@ -1,10 +1,7 @@
let mockReleaseNoteData = {};
jest.mock("axios", () => ({
default: {
get: jest.fn(() => { return Promise.resolve({ data: "notes" }); })
.mockImplementationOnce(() => { return Promise.resolve(); })
.mockImplementationOnce(() => {
return Promise.resolve({ data: "intro\n\n# v6\n\n* note" });
})
get: jest.fn(() => { return Promise.resolve(mockReleaseNoteData); })
}
}));
@ -12,9 +9,7 @@ import * as React from "react";
import { FarmbotOsSettings } from "../farmbot_os_settings";
import { mount } from "enzyme";
import { bot } from "../../../__test_support__/fake_state/bot";
import { fakeState } from "../../../__test_support__/fake_state";
import { fakeResource } from "../../../__test_support__/fake_resource";
import { AuthState } from "../../../auth/interfaces";
import { FbosDetails } from "../fbos_settings/farmbot_os_row";
import { FarmbotOsProps } from "../../interfaces";
import axios from "axios";
@ -25,7 +20,6 @@ describe("<FarmbotOsSettings/>", () => {
account: fakeResource("Device", { id: 0, name: "", tz_offset_hrs: 0 }),
dispatch: jest.fn(),
bot: bot,
auth: fakeState().auth as AuthState,
botToMqttStatus: "up"
};
}
@ -39,6 +33,7 @@ describe("<FarmbotOsSettings/>", () => {
});
it("fetches OS release notes", async () => {
mockReleaseNoteData = { data: "intro\n\n# v6\n\n* note" };
const osSettings = await mount(<FarmbotOsSettings {...fakeProps() } />);
await expect(axios.get).toHaveBeenCalledWith(
expect.stringContaining("RELEASE_NOTES.md"));
@ -47,6 +42,7 @@ describe("<FarmbotOsSettings/>", () => {
});
it("doesn't fetch OS release notes", async () => {
mockReleaseNoteData = { data: "empty notes" };
const osSettings = await mount(<FarmbotOsSettings {...fakeProps() } />);
await expect(axios.get).toHaveBeenCalledWith(
expect.stringContaining("RELEASE_NOTES.md"));

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { mount } from "enzyme";
import { RpiGpioDiagram, RpiGpioDiagramProps } from "../rpi_gpio_diagram";
import { Color } from "../../../ui/colors";
import { Color } from "../../../ui/index";
describe("<RpiGpioDiagram />", () => {
function fakeProps(): RpiGpioDiagramProps {

View File

@ -23,34 +23,32 @@ export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) {
const { mcu_params } = bot.hardware;
return (
<Row>
<Col xs={6}>
<label>
{name}
{caution &&
<i className="fa fa-exclamation-triangle caution-icon" />}
</label>
<SpacePanelToolTip tooltip={tooltip} />
</Col>
<Col xs={2}>
<ToggleButton
disabled={disableX}
toggleValue={mcu_params[x]}
toggleAction={() => settingToggle(x, bot, displayAlert)} />
</Col>
<Col xs={2}>
<ToggleButton
disabled={disableY}
toggleValue={mcu_params[y]}
toggleAction={() => settingToggle(y, bot, displayAlert)} />
</Col>
<Col xs={2}>
<ToggleButton
disabled={disableZ}
toggleValue={mcu_params[z]}
toggleAction={() => settingToggle(z, bot, displayAlert)} />
</Col>
</Row>
);
return <Row>
<Col xs={6}>
<label>
{name}
{caution &&
<i className="fa fa-exclamation-triangle caution-icon" />}
</label>
<SpacePanelToolTip tooltip={tooltip} />
</Col>
<Col xs={2}>
<ToggleButton
disabled={disableX}
toggleValue={mcu_params[x]}
toggleAction={() => settingToggle(x, bot, displayAlert)} />
</Col>
<Col xs={2}>
<ToggleButton
disabled={disableY}
toggleValue={mcu_params[y]}
toggleAction={() => settingToggle(y, bot, displayAlert)} />
</Col>
<Col xs={2}>
<ToggleButton
disabled={disableZ}
toggleValue={mcu_params[z]}
toggleAction={() => settingToggle(z, bot, displayAlert)} />
</Col>
</Row>;
}

View File

@ -2,13 +2,15 @@ import * as React from "react";
import { t } from "i18next";
import { emergencyLock, emergencyUnlock } from "../actions";
import { EStopButtonProps } from "../interfaces";
import { isBotUp } from "../must_be_online";
export class EStopButton extends React.Component<EStopButtonProps, {}> {
render() {
const i = this.props.bot.hardware.informational_settings;
const isLocked = !!i.locked;
const toggleEmergencyLock = isLocked ? emergencyUnlock : emergencyLock;
const emergencyLockStatusColor = isLocked ? "yellow" : "red";
const color = isLocked ? "yellow" : "red";
const emergencyLockStatusColor = isBotUp(i.sync_status) ? color : "gray";
const emergencyLockStatusText = isLocked ? "UNLOCK" : "E-STOP";
if (this.props.user) {

View File

@ -27,6 +27,15 @@ describe("<OsUpdateButton/>", () => {
const osUpdateButton = buttons.find("button").last();
expect(osUpdateButton.text()).toBe("Can't Connect to release server");
});
it("renders buttons: no beta releases", () => {
bot.hardware.configuration.beta_opt_in = true;
const buttons = mount(<OsUpdateButton bot={bot} />);
expect(buttons.find("button").length).toBe(1);
const autoUpdate = buttons.find("button").first();
expect(autoUpdate.hasClass("yellow")).toBeTruthy();
const osUpdateButton = buttons.find("button").last();
expect(osUpdateButton.text()).toBe("No beta releases available");
});
it("up to date", () => {
bot.hardware.informational_settings.controller_version = "3.1.6";
const buttons = mount(<OsUpdateButton bot={bot} />);

View File

@ -1,7 +1,6 @@
import * as React from "react";
import { Row, Col, DropDownItem } from "../../../ui/index";
import { Row, Col, DropDownItem, FBSelect } from "../../../ui/index";
import { t } from "i18next";
import { FBSelect } from "../../../ui/new_fb_select";
import { getDevice } from "../../../device";
import { info, error } from "farmbot-toastr";
import { FirmwareHardware } from "farmbot";

View File

@ -1,12 +1,11 @@
import * as React from "react";
import { DropDownItem, Row, Col } from "../../../ui/index";
import { DropDownItem, Row, Col, FBSelect } from "../../../ui/index";
import { t } from "i18next";
import {
CameraSelectionProps, CameraSelectionState
} from "../../interfaces";
import { info, success, error } from "farmbot-toastr/dist";
import { getDevice } from "../../../device";
import { FBSelect } from "../../../ui/new_fb_select";
import { ColWidth } from "../farmbot_os_settings";
const CAMERA_CHOICES = [

View File

@ -28,7 +28,9 @@ export let OsUpdateButton = ({ bot }: BotProp) => {
buttonColor = "green";
}
} else {
buttonStr = "Can't Connect to release server";
buttonStr = beta_opt_in
? "No beta releases available"
: "Can't Connect to release server";
}
const osUpdateJob = (bot.hardware.jobs || {})["FBOS_OTA"];

View File

@ -1,9 +1,8 @@
import * as React from "react";
import { MCUFactoryReset, bulkToggleControlPanel } from "../actions";
import { Widget, WidgetHeader, WidgetBody } from "../../ui/index";
import { Widget, WidgetHeader, WidgetBody, SaveBtn } from "../../ui/index";
import { HardwareSettingsProps } from "../interfaces";
import { MustBeOnline } from "../must_be_online";
import { SaveBtn } from "../../ui/save_button";
import { ToolTips } from "../../constants";
import { DangerZone } from "./hardware_settings/danger_zone";
import { PinGuard } from "./hardware_settings/pin_guard";
@ -21,59 +20,57 @@ export class HardwareSettings extends
render() {
const { bot, dispatch } = this.props;
const { sync_status } = this.props.bot.hardware.informational_settings;
return (
<Widget className="hardware-widget">
<WidgetHeader title="Hardware" helpText={ToolTips.HW_SETTINGS}>
<MustBeOnline
hideBanner={true}
syncStatus={sync_status}
networkState={this.props.botToMqttStatus}
lockOpen={process.env.NODE_ENV !== "production"}>
<SaveBtn
status={bot.isUpdating ? SpecialStatus.SAVING : SpecialStatus.SAVED}
dirtyText={" "}
savingText={"Updating..."}
savedText={"saved"}
hidden={false} />
</MustBeOnline>
</WidgetHeader>
<WidgetBody>
<button
className={"fb-button gray no-float"}
onClick={() => dispatch(bulkToggleControlPanel(true))}>
Expand All
return <Widget className="hardware-widget">
<WidgetHeader title="Hardware" helpText={ToolTips.HW_SETTINGS}>
<MustBeOnline
hideBanner={true}
syncStatus={sync_status}
networkState={this.props.botToMqttStatus}
lockOpen={process.env.NODE_ENV !== "production"}>
<SaveBtn
status={bot.isUpdating ? SpecialStatus.SAVING : SpecialStatus.SAVED}
dirtyText={" "}
savingText={"Updating..."}
savedText={"saved"}
hidden={false} />
</MustBeOnline>
</WidgetHeader>
<WidgetBody>
<button
className={"fb-button gray no-float"}
onClick={() => dispatch(bulkToggleControlPanel(true))}>
Expand All
</button>
<button
className={"fb-button gray no-float"}
onClick={() => dispatch(bulkToggleControlPanel(false))}>
Collapse All
<button
className={"fb-button gray no-float"}
onClick={() => dispatch(bulkToggleControlPanel(false))}>
Collapse All
</button>
<MustBeOnline
networkState={this.props.botToMqttStatus}
syncStatus={sync_status}
lockOpen={process.env.NODE_ENV !== "production"}>
<div className="label-headings">
<SpacePanelHeader />
</div>
<HomingAndCalibration
dispatch={dispatch}
bot={bot} />
<Motors
dispatch={dispatch}
bot={bot} />
<EncodersAndEndStops
dispatch={dispatch}
bot={bot} />
<PinGuard
dispatch={dispatch}
bot={bot} />
<DangerZone
dispatch={dispatch}
bot={bot}
onReset={MCUFactoryReset} />
</MustBeOnline>
</WidgetBody>
</Widget>
);
<MustBeOnline
networkState={this.props.botToMqttStatus}
syncStatus={sync_status}
lockOpen={process.env.NODE_ENV !== "production"}>
<div className="label-headings">
<SpacePanelHeader />
</div>
<HomingAndCalibration
dispatch={dispatch}
bot={bot} />
<Motors
dispatch={dispatch}
bot={bot} />
<EncodersAndEndStops
dispatch={dispatch}
bot={bot} />
<PinGuard
dispatch={dispatch}
bot={bot} />
<DangerZone
dispatch={dispatch}
bot={bot}
onReset={MCUFactoryReset} />
</MustBeOnline>
</WidgetBody>
</Widget>;
}
}

View File

@ -1,7 +1,9 @@
import * as React from "react";
import { EncoderType, EncoderTypeProps, LOOKUP, findByType, isEncoderValue } from "../encoder_type";
import {
EncoderType, EncoderTypeProps, LOOKUP, findByType, isEncoderValue
} from "../encoder_type";
import { shallow } from "enzyme";
import { FBSelect } from "../../../../ui/new_fb_select";
import { FBSelect } from "../../../../ui/index";
import { Encoder } from "farmbot";
describe("<EncoderType/>", () => {

View File

@ -1,8 +1,7 @@
import * as React from "react";
import { McuParams, Encoder, McuParamName } from "farmbot/dist";
import { t } from "i18next";
import { FBSelect } from "../../../ui/new_fb_select";
import { DropDownItem } from "../../../ui/fb_select";
import { FBSelect, DropDownItem } from "../../../ui/index";
export interface EncoderTypeProps {
hardware: McuParams;

View File

@ -2,8 +2,7 @@ import * as React from "react";
import { McuInputBox } from "./mcu_input_box";
import { SpacePanelToolTip } from "./space_panel_tool_tip";
import { NumericMCUInputGroupProps } from "./interfaces";
import { Row } from "../../ui/row";
import { Col } from "../../ui/index";
import { Row, Col } from "../../ui/index";
export function NumericMCUInputGroup(props: NumericMCUInputGroupProps) {

View File

@ -2,9 +2,11 @@ import * as React from "react";
import { t } from "i18next";
import * as _ from "lodash";
import {
Widget, WidgetBody, WidgetHeader, Row, Col, BlurableInput, DropDownItem
Widget, WidgetBody, WidgetHeader,
Row, Col,
BlurableInput,
FBSelect, DropDownItem
} from "../../ui/index";
import { FBSelect } from "../../ui/new_fb_select";
import { ToolTips } from "../../constants";
import { BotState } from "../interfaces";
import { registerGpioPin, unregisterGpioPin } from "../actions";

View File

@ -1,8 +1,7 @@
import * as React from "react";
import { McuInputBox } from "./mcu_input_box";
import { PinGuardMCUInputGroupProps } from "./interfaces";
import { Row } from "../../ui/row";
import { Col } from "../../ui/index";
import { Row, Col } from "../../ui/index";
import { settingToggle } from "../actions";
import { ToggleButton } from "../../controls/toggle_button";
import { isUndefined } from "util";

View File

@ -11,7 +11,7 @@ import {
DiagramNodes,
getConnectionColor
} from "../diagram";
import { Color } from "../../../ui/colors";
import { Color } from "../../../ui/index";
describe("<ConnectivityDiagram/>", () => {
function fakeProps(): ConnectivityDiagramProps {

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { StatusRowProps } from "./connectivity_row";
import { CowardlyDictionary } from "../../util";
import { Color } from "../../ui/colors";
import { Color } from "../../ui/index";
export interface ConnectivityDiagramProps {
rowData: StatusRowProps[];

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { connect } from "react-redux";
import { HardwareSettings } from "./components/hardware_settings";
import { FarmbotOsSettings } from "./components/farmbot_os_settings";
import { Page, Col, Row } from "../ui";
import { Page, Col, Row } from "../ui/index";
import { mapStateToProps } from "./state_to_props";
import { Props } from "./interfaces";
import { ConnectivityPanel } from "./connectivity/index";
@ -60,7 +60,6 @@ export class Devices extends React.Component<Props, {}> {
account={this.props.deviceAccount}
dispatch={this.props.dispatch}
bot={this.props.bot}
auth={this.props.auth}
botToMqttStatus={botToMqttStatus} />
<ConnectivityPanel
status={this.props.deviceAccount.specialStatus}

View File

@ -101,7 +101,6 @@ export interface CalibrationButtonProps {
export interface FarmbotOsProps {
bot: BotState;
account: TaggedDevice;
auth: AuthState;
botToMqttStatus: NetworkState;
dispatch: Function;
}

View File

@ -12,10 +12,14 @@ export interface MBOProps {
children?: JSXChildren;
}
export function isBotUp(status: SyncStatus | undefined) {
return status && !(["maintenance", "unknown"].includes(status));
}
export function MustBeOnline(props: MBOProps) {
const { children, hideBanner, lockOpen, networkState, syncStatus } = props;
const banner = hideBanner ? "" : "banner";
const botUp = syncStatus && (syncStatus !== "maintenance");
const botUp = isBotUp(syncStatus);
const netUp = networkState === "up";
if ((botUp && netUp) || lockOpen) {
return <div> {children} </div>;

View File

@ -4,7 +4,9 @@ import { Actions } from "../constants";
import { EncoderDisplay } from "../controls/interfaces";
import { EXPECTED_MAJOR, EXPECTED_MINOR } from "./actions";
import { BooleanSetting } from "../session_keys";
import { maybeNegateStatus, maybeNegateConsistency } from "../connectivity/maybe_negate_status";
import {
maybeNegateStatus, maybeNegateConsistency
} from "../connectivity/maybe_negate_status";
import { EdgeStatus } from "../connectivity/interfaces";
import { ReduxAction } from "../redux/interfaces";
import { connectivityReducer } from "../connectivity/reducer";

View File

@ -1,8 +1,7 @@
import * as React from "react";
import { DropDownItem } from "../../ui/fb_select";
import { FBSelect, DropDownItem } from "../../ui/index";
import { list } from "./tz_list";
import { inferTimezone } from "./guess_timezone";
import { FBSelect } from "../../ui/new_fb_select";
import * as _ from "lodash";
const CHOICES: DropDownItem[] = list.map(x => ({ label: x, value: x }));

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { t } from "i18next";
import { Widget, WidgetHeader } from "../ui";
import { Widget, WidgetHeader } from "../ui/index";
/*
* Widget to display if the desired widget fails to load.
@ -25,15 +25,13 @@ export class FallbackWidget extends
React.Component<FallbackWidgetProps, {}> {
render() {
return (
<Widget>
<WidgetHeader
title={t(this.props.title)}
helpText={this.props.helpText} />
<div className="widget-body">
{t("Widget load failed.")}
</div>
</Widget>
);
return <Widget>
<WidgetHeader
title={t(this.props.title)}
helpText={this.props.helpText} />
<div className="widget-body">
{t("Widget load failed.")}
</div>
</Widget>;
}
}

View File

@ -1,18 +1,10 @@
const mockHistory = jest.fn();
let mockPath = "/app/designer/plants";
jest.mock("../../history", () => ({
history: {
push: mockHistory
},
getPathArray: jest.fn()
.mockImplementationOnce(() => {
return "/app/designer/plants".split("/");
})
.mockImplementationOnce(() => {
return "/app/designer/plants/1/edit".split("/");
})
.mockImplementationOnce(() => {
return "/app/designer/plants/1".split("/");
})
getPathArray: jest.fn(() => { return mockPath.split("/"); })
}));
jest.mock("../../api/crud", () => ({
@ -60,16 +52,27 @@ describe("movePlant", () => {
movePlantTest("too low", { x: -10000, y: -10000 }, { x: 0, y: 0 });
});
describe("close plant", () => {
it("closes plant info", () => {
describe("closePlantInfo()", () => {
it("no plant info open", () => {
mockPath = "/app/designer/plants";
const dispatch = jest.fn();
closePlantInfo(dispatch)(); // no plant info open
closePlantInfo(dispatch)();
expect(mockHistory).not.toHaveBeenCalled();
expect(dispatch).not.toHaveBeenCalled();
closePlantInfo(dispatch)(); // plant edit open
});
it("plant edit open", () => {
mockPath = "/app/designer/plants/1/edit";
const dispatch = jest.fn();
closePlantInfo(dispatch)();
expect(mockHistory).not.toHaveBeenCalled();
expect(dispatch).not.toHaveBeenCalled();
closePlantInfo(dispatch)(); // plant info open
});
it("plant info open", () => {
mockPath = "/app/designer/plants/1";
const dispatch = jest.fn();
closePlantInfo(dispatch)();
expect(mockHistory).toHaveBeenCalledWith("/app/designer/plants");
expect(dispatch).toHaveBeenCalledWith({
payload: undefined, type: "SELECT_PLANT"

View File

@ -87,18 +87,16 @@ export class AddFarmEvent
}
placeholderTemplate(children: JSXChildren) {
return (
<div className="panel-container magenta-panel add-farm-event-panel">
<div className="panel-header magenta-panel">
<p className="panel-title"> <BackArrow /> {t("No Executables")} </p>
</div>
<div className="panel-content">
<label>
{children}
</label>
</div>
return <div className="panel-container magenta-panel add-farm-event-panel">
<div className="panel-header magenta-panel">
<p className="panel-title"> <BackArrow /> {t("No Executables")} </p>
</div>
);
<div className="panel-content">
<label>
{children}
</label>
</div>
</div>;
}
render() {
@ -108,17 +106,15 @@ export class AddFarmEvent
// to mapStateToProps instead of juggling arrays.
const fe = uuid && this.props.farmEvents.filter(x => x.uuid === uuid)[0];
if (fe) {
return (
<EditFEForm
farmEvent={fe}
deviceTimezone={this.props.deviceTimezone}
repeatOptions={this.props.repeatOptions}
executableOptions={this.props.executableOptions}
dispatch={this.props.dispatch}
findExecutable={this.props.findExecutable}
title={t("Add Farm Event")}
timeOffset={this.props.timeOffset} />
);
return <EditFEForm
farmEvent={fe}
deviceTimezone={this.props.deviceTimezone}
repeatOptions={this.props.repeatOptions}
executableOptions={this.props.executableOptions}
dispatch={this.props.dispatch}
findExecutable={this.props.findExecutable}
title={t("Add Farm Event")}
timeOffset={this.props.timeOffset} />;
} else {
return this
.placeholderTemplate(((this.executable) ? this.loading : this.none)());

View File

@ -4,29 +4,17 @@ import * as _ from "lodash";
import { t } from "i18next";
import { success, error } from "farmbot-toastr";
import { TaggedFarmEvent, SpecialStatus } from "../../resources/tagged_resources";
import {
TimeUnit,
ExecutableQuery,
ExecutableType
} from "../interfaces";
import {
formatTime,
formatDate
} from "./map_state_to_props_add_edit";
import { TimeUnit, ExecutableQuery, ExecutableType } from "../interfaces";
import { formatTime, formatDate } from "./map_state_to_props_add_edit";
import {
BackArrow,
BlurableInput,
Col,
Row,
SaveBtn
Col, Row,
SaveBtn,
FBSelect,
DropDownItem
} from "../../ui/index";
import { FBSelect } from "../../ui/new_fb_select";
import {
destroy,
save,
edit
} from "../../api/crud";
import { DropDownItem } from "../../ui/fb_select";
import { destroy, save, edit } from "../../api/crud";
import { history } from "../../history";
// TIL: https://stackoverflow.com/a/24900248/1064917
import { betterMerge } from "../../util";
@ -227,81 +215,79 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
const fe = this.props.farmEvent;
const repeats = this.fieldGet("timeUnit") !== NEVER;
const allowRepeat = (!this.isReg && repeats);
return (
<div className="panel-container magenta-panel add-farm-event-panel">
<div className="panel-header magenta-panel">
<p className="panel-title">
<BackArrow onClick={() => {
if (!this.props.farmEvent.body.id) {
// Throw out unsaved farmevents.
this.props.dispatch(destroyOK(this.props.farmEvent));
return;
}
}} />
{this.props.title}
</p>
</div>
<div className="panel-content">
<label>
{t("Sequence or Regimen")}
</label>
<FBSelect
list={this.props.executableOptions}
onChange={this.executableSet}
selectedItem={this.executableGet()} />
<label>
{t("Starts")}
</label>
<Row>
<Col xs={6}>
<BlurableInput
type="date"
className="add-event-start-date"
name="start_date"
value={this.fieldGet("startDate")}
onCommit={this.fieldSet("startDate")} />
</Col>
<Col xs={6}>
<EventTimePicker
className="add-event-start-time"
name="start_time"
tzOffset={this.props.timeOffset}
value={this.fieldGet("startTime")}
onCommit={this.fieldSet("startTime")} />
</Col>
</Row>
<label>
<input type="checkbox"
onChange={this.toggleRepeat}
disabled={this.isReg}
checked={repeats && !this.isReg} />
&nbsp;{t("Repeats?")}
</label>
<FarmEventRepeatForm
tzOffset={this.props.timeOffset}
disabled={!allowRepeat}
hidden={!allowRepeat}
onChange={this.mergeState}
timeUnit={this.fieldGet("timeUnit") as TimeUnit}
repeat={this.fieldGet("repeat")}
endDate={this.fieldGet("endDate")}
endTime={this.fieldGet("endTime")} />
<SaveBtn
status={fe.specialStatus || this.state.specialStatusLocal}
color="magenta"
onClick={this.commitViewModel} />
<button className="fb-button red" hidden={!this.props.deleteBtn}
onClick={() => {
this.dispatch(destroy(fe.uuid)).then(() => {
history.push("/app/designer/farm_events");
success(t("Deleted farm event."), t("Deleted"));
});
}}>
{t("Delete")}
</button>
<TzWarning deviceTimezone={this.props.deviceTimezone} />
</div>
return <div className="panel-container magenta-panel add-farm-event-panel">
<div className="panel-header magenta-panel">
<p className="panel-title">
<BackArrow onClick={() => {
if (!this.props.farmEvent.body.id) {
// Throw out unsaved farmevents.
this.props.dispatch(destroyOK(this.props.farmEvent));
return;
}
}} />
{this.props.title}
</p>
</div>
);
<div className="panel-content">
<label>
{t("Sequence or Regimen")}
</label>
<FBSelect
list={this.props.executableOptions}
onChange={this.executableSet}
selectedItem={this.executableGet()} />
<label>
{t("Starts")}
</label>
<Row>
<Col xs={6}>
<BlurableInput
type="date"
className="add-event-start-date"
name="start_date"
value={this.fieldGet("startDate")}
onCommit={this.fieldSet("startDate")} />
</Col>
<Col xs={6}>
<EventTimePicker
className="add-event-start-time"
name="start_time"
tzOffset={this.props.timeOffset}
value={this.fieldGet("startTime")}
onCommit={this.fieldSet("startTime")} />
</Col>
</Row>
<label>
<input type="checkbox"
onChange={this.toggleRepeat}
disabled={this.isReg}
checked={repeats && !this.isReg} />
&nbsp;{t("Repeats?")}
</label>
<FarmEventRepeatForm
tzOffset={this.props.timeOffset}
disabled={!allowRepeat}
hidden={!allowRepeat}
onChange={this.mergeState}
timeUnit={this.fieldGet("timeUnit") as TimeUnit}
repeat={this.fieldGet("repeat")}
endDate={this.fieldGet("endDate")}
endTime={this.fieldGet("endTime")} />
<SaveBtn
status={fe.specialStatus || this.state.specialStatusLocal}
color="magenta"
onClick={this.commitViewModel} />
<button className="fb-button red" hidden={!this.props.deleteBtn}
onClick={() => {
this.dispatch(destroy(fe.uuid)).then(() => {
history.push("/app/designer/farm_events");
success(t("Deleted farm event."), t("Deleted"));
});
}}>
{t("Delete")}
</button>
<TzWarning deviceTimezone={this.props.deviceTimezone} />
</div>
</div>;
}
}

View File

@ -1,5 +1,5 @@
import * as React from "react";
import { BlurableInput } from "../../ui/blurable_input";
import { BlurableInput } from "../../ui/index";
import * as moment from "moment";
interface Props {

View File

@ -1,7 +1,8 @@
import * as React from "react";
import { t } from "i18next";
import { Row, Col, BlurableInput, DropDownItem } from "../../ui/index";
import { FBSelect } from "../../ui/new_fb_select";
import {
Row, Col, BlurableInput, FBSelect, DropDownItem
} from "../../ui/index";
import { repeatOptions } from "./map_state_to_props_add_edit";
import { keyBy } from "lodash";
import { TimeUnit } from "../interfaces";

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { Link } from "react-router";
import { connect } from "react-redux";
import { t } from "i18next";
import { Row } from "../../ui";
import { Row } from "../../ui/index";
import { mapStateToProps } from "./map_state_to_props";
import { FarmEventProps, CalendarOccurrence } from "../interfaces";
import * as _ from "lodash";
@ -25,22 +25,20 @@ export class PureFarmEvents extends React.Component<FarmEventProps, {}> {
? <p style={{ color: "gray" }}> {occur.heading} </p>
: <p />;
return (
<div
className="farm-event-data-block"
key={`${occur.sortKey}.${index}`}>
<div className="farm-event-data-time">
{occur.timeStr}
</div>
<div className="farm-event-data-executable">
{heading}
{subHeading}
</div>
<Link to={url}>
<i className="fa fa-pencil-square-o edit-icon" />
</Link>
return <div
className="farm-event-data-block"
key={`${occur.sortKey}.${index}`}>
<div className="farm-event-data-time">
{occur.timeStr}
</div>
);
<div className="farm-event-data-executable">
{heading}
{subHeading}
</div>
<Link to={url}>
<i className="fa fa-pencil-square-o edit-icon" />
</Link>
</div>;
});
}
@ -48,21 +46,19 @@ export class PureFarmEvents extends React.Component<FarmEventProps, {}> {
return this.props.calendarRows.filter((day) => {
return day.year == year;
}).map(item => {
return (
<div className="farm-event" key={item.sortKey}>
<div className="farm-event-date">
<div className="farm-event-date-month">
{item.month}
</div>
<div className="farm-event-date-day">
<b>{item.day}</b>
</div>
return <div className="farm-event" key={item.sortKey}>
<div className="farm-event-date">
<div className="farm-event-date-month">
{item.month}
</div>
<div className="farm-event-data">
{this.innerRows(item.items)}
<div className="farm-event-date-day">
<b>{item.day}</b>
</div>
</div>
);
<div className="farm-event-data">
{this.innerRows(item.items)}
</div>
</div>;
});
}
@ -117,24 +113,22 @@ export class PureFarmEvents extends React.Component<FarmEventProps, {}> {
render() {
return (
<div className="panel-container magenta-panel farm-event-panel">
<div className="panel-header magenta-panel">
<div className="panel-tabs">
<Link to="/app/designer" className="visible-xs">
{t("Designer")}
</Link>
<Link to="/app/designer/plants">
{t("Plants")}
</Link>
<Link to="/app/designer/farm_events" className="active">
{t("Farm Events")}
</Link>
</div>
return <div className="panel-container magenta-panel farm-event-panel">
<div className="panel-header magenta-panel">
<div className="panel-tabs">
<Link to="/app/designer" className="visible-xs">
{t("Designer")}
</Link>
<Link to="/app/designer/plants">
{t("Plants")}
</Link>
<Link to="/app/designer/farm_events" className="active">
{t("Farm Events")}
</Link>
</div>
{this.props.timezoneIsSet ? this.normalContent() : this.tzwarning()}
</div>
);
{this.props.timezoneIsSet ? this.normalContent() : this.tzwarning()}
</div>;
}
}

View File

@ -22,7 +22,7 @@ import {
TaggedSequence,
TaggedRegimen
} from "../../resources/tagged_resources";
import { DropDownItem } from "../../ui/fb_select";
import { DropDownItem } from "../../ui/index";
export let formatTime = (input: string, timeOffset: number) => {
const iso = new Date(input).toISOString();

View File

@ -3,7 +3,7 @@ import { DragHelpers } from "../drag_helpers";
import { shallow } from "enzyme";
import { DragHelpersProps } from "../interfaces";
import { fakePlant } from "../../../__test_support__/fake_state/resources";
import { Color } from "../../../ui/colors";
import { Color } from "../../../ui/index";
describe("<DragHelpers/>", () => {
function fakeProps(): DragHelpersProps {

View File

@ -13,6 +13,11 @@ jest.mock("../../../api/crud", () => ({
save: () => "save resource",
}));
let mockPath = "";
jest.mock("../../../history", () => ({
getPathArray: jest.fn(() => { return mockPath.split("/"); })
}));
import * as React from "react";
import { GardenMap } from "../garden_map";
import { shallow } from "enzyme";
@ -91,10 +96,7 @@ describe("<GardenPlant/>", () => {
const dispatch = jest.fn();
p.dispatch = dispatch;
const wrapper = shallow(<GardenMap {...p} />);
Object.defineProperty(location, "pathname", {
value: "/app/designer/plants/crop_search/strawberry/add",
configurable: true
});
mockPath = "/app/designer/plants/crop_search/strawberry/add";
wrapper.find("#drop-area-svg").simulate("click", {
preventDefault: jest.fn()
});
@ -105,10 +107,7 @@ describe("<GardenPlant/>", () => {
it("doesn't drop plant: error", () => {
const wrapper = shallow(<GardenMap {...fakeProps() } />);
Object.defineProperty(location, "pathname", {
value: "/app/designer/plants/crop_search/aplant/add",
configurable: true
});
mockPath = "/app/designer/plants/crop_search/aplant/add";
Object.defineProperty(document, "querySelector", {
value: () => { }, configurable: true
});
@ -121,10 +120,7 @@ describe("<GardenPlant/>", () => {
it("doesn't drop plant: outside planting area", () => {
const wrapper = shallow(<GardenMap {...fakeProps() } />);
Object.defineProperty(location, "pathname", {
value: "/app/designer/plants/crop_search/aplant/add",
configurable: true
});
mockPath = "/app/designer/plants/crop_search/aplant/add";
wrapper.find("#drop-area-svg").simulate("click", {
preventDefault: jest.fn(), pageX: -100, pageY: -100
});
@ -135,9 +131,7 @@ describe("<GardenPlant/>", () => {
it("starts drag and sets activeSpread", async () => {
const wrapper = shallow(<GardenMap {...fakeProps() } />);
expect(wrapper.state()).toEqual({});
Object.defineProperty(location, "pathname", {
value: "/app/designer/plants/1/edit/"
});
mockPath = "/app/designer/plants/1/edit/";
await wrapper.find("#drop-area-svg").simulate("mouseDown");
expect(wrapper.state()).toEqual({
activeDragSpread: 1000,
@ -165,9 +159,7 @@ describe("<GardenPlant/>", () => {
});
it("drags: editing", () => {
Object.defineProperty(location, "pathname", {
value: "/app/designer/plants/1/edit", configurable: true
});
mockPath = "/app/designer/plants/1/edit";
const p = fakeProps();
const wrapper = shallow(<GardenMap {...p } />);
expect(wrapper.state()).toEqual({});
@ -188,9 +180,7 @@ describe("<GardenPlant/>", () => {
});
it("drags: selecting", () => {
Object.defineProperty(location, "pathname", {
value: "/app/designer/plants/select", configurable: true
});
mockPath = "/app/designer/plants/select";
const p = fakeProps();
const wrapper = shallow(<GardenMap {...p } />);
expect(wrapper.state()).toEqual({});

View File

@ -37,12 +37,24 @@ describe("zoom utilities", () => {
expect(ZoomUtils.atMinZoom()).toBeFalsy();
});
it("beyond max zoom", () => {
mockZoomValue = 999;
const result = ZoomUtils.getZoomLevelIndex();
expect(result).toEqual(ZoomUtils.maxZoomIndex);
});
it("at min zoom", () => {
mockZoomValue = ZoomUtils.minZoomLevel;
expect(ZoomUtils.atMaxZoom()).toBeFalsy();
expect(ZoomUtils.atMinZoom()).toBeTruthy();
});
it("beyond min zoom", () => {
mockZoomValue = -999;
const result = ZoomUtils.getZoomLevelIndex();
expect(result).toEqual(0);
});
it("at unknown zoom", () => {
mockZoomValue = undefined;
const defaultZoom = ZoomUtils.calcZoomLevel(ZoomUtils.getZoomLevelIndex());

View File

@ -3,7 +3,7 @@ import { DragHelpersProps } from "./interfaces";
import { round, getXYFromQuadrant, getMapSize } from "./util";
import { isUndefined } from "util";
import { BotPosition } from "../../devices/interfaces";
import { Color } from "../../ui/colors";
import { Color } from "../../ui/index";
enum Alignment {
NONE = "not aligned",
@ -87,10 +87,9 @@ export function DragHelpers(props: DragHelpersProps) {
</g>
</defs>
{[0, 90, 180, 270].map(rotation => {
return (
<use key={rotation.toString()}
xlinkHref={"#crosshair-segment-" + plant.body.id}
transform={`rotate(${rotation}, ${qx}, ${qy})`} />);
return <use key={rotation.toString()}
xlinkHref={"#crosshair-segment-" + plant.body.id}
transform={`rotate(${rotation}, ${qx}, ${qy})`} />;
})}
</g>}
{!dragging && // Non-active plants
@ -105,10 +104,9 @@ export function DragHelpers(props: DragHelpersProps) {
</g>
</defs>
{rotationArray(getAlignment(activeDragXY, gardenCoord)).map(rotation => {
return (
<use key={rotation.toString()}
xlinkHref={"#alignment-indicator-segment-" + plant.body.id}
transform={`rotate(${rotation}, ${qx}, ${qy})`} />);
return <use key={rotation.toString()}
xlinkHref={"#alignment-indicator-segment-" + plant.body.id}
transform={`rotate(${rotation}, ${qx}, ${qy})`} />;
})}
</g>}
</g>;

View File

@ -18,14 +18,16 @@ import {
import { findBySlug } from "../search_selectors";
import { Grid } from "./grid";
import { MapBackground } from "./map_background";
import { PlantLayer } from "./layers/plant_layer";
import { PointLayer } from "./layers/point_layer";
import { SpreadLayer } from "./layers/spread_layer";
import { ToolSlotLayer } from "./layers/tool_slot_layer";
import { HoveredPlantLayer } from "./layers/hovered_plant_layer";
import { FarmBotLayer } from "./layers/farmbot_layer";
import {
PlantLayer,
SpreadLayer,
PointLayer,
ToolSlotLayer,
FarmBotLayer,
HoveredPlantLayer,
DragHelperLayer
} from "./layers";
import { cachedCrop } from "../../open_farm/icons";
import { DragHelperLayer } from "./layers/drag_helper_layer";
import { AxisNumberProperty } from "./interfaces";
import { SelectionBox, SelectionBoxData } from "./selection_box";
import { Actions } from "../../constants";

View File

@ -23,76 +23,74 @@ export function GardenMapLegend(props: GardenMapLegendProps) {
const minusBtnClass = atMinZoom() ? "disabled" : "";
const menuClass = legendMenuOpen ? "active" : "";
return (
return <div
className={"garden-map-legend " + menuClass}
style={{ zoom: 1 }}>
<div
className={"garden-map-legend " + menuClass}
style={{ zoom: 1 }}>
<div
className={"menu-pullout " + menuClass}
onClick={toggle("legend_menu_open")}>
<span>
{t("Menu")}
</span>
<i className="fa fa-2x fa-arrow-left" />
className={"menu-pullout " + menuClass}
onClick={toggle("legend_menu_open")}>
<span>
{t("Menu")}
</span>
<i className="fa fa-2x fa-arrow-left" />
</div>
<div className="content">
<div className="zoom-buttons">
<button
className={"fb-button gray zoom " + plusBtnClass}
onClick={zoom(1)}>
<i className="fa fa-2x fa-plus" />
</button>
<button
className={"fb-button gray zoom zoom-out " + minusBtnClass}
onClick={zoom(-1)}>
<i className="fa fa-2x fa-minus" />
</button>
</div>
<div className="content">
<div className="zoom-buttons">
<button
className={"fb-button gray zoom " + plusBtnClass}
onClick={zoom(1)}>
<i className="fa fa-2x fa-plus" />
</button>
<button
className={"fb-button gray zoom zoom-out " + minusBtnClass}
onClick={zoom(-1)}>
<i className="fa fa-2x fa-minus" />
</button>
</div>
<div className="toggle-buttons">
<LayerToggle
value={showPlants}
label={t("Plants?")}
onClick={toggle("show_plants")} />
<LayerToggle
value={showPoints}
label={t("Points?")}
onClick={toggle("show_points")} />
<LayerToggle
value={showSpread}
label={t("Spread?")}
onClick={toggle("show_spread")} />
<LayerToggle
value={showFarmbot}
label={t("FarmBot?")}
onClick={toggle("show_farmbot")} />
</div>
<div className="farmbot-origin">
<label>
{t("Origin")}
</label>
<div className="quadrants">
<div
className={"quadrant " + (botOriginQuadrant === 2 && "selected")}
onClick={updateBotOriginQuadrant(2)} />
<div
className={"quadrant " + (botOriginQuadrant === 1 && "selected")}
onClick={updateBotOriginQuadrant(1)} />
<div
className={"quadrant " + (botOriginQuadrant === 3 && "selected")}
onClick={updateBotOriginQuadrant(3)} />
<div
className={"quadrant " + (botOriginQuadrant === 4 && "selected")}
onClick={updateBotOriginQuadrant(4)} />
</div>
</div>
<div className="move-to-mode">
<button
className="fb-button gray"
onClick={() => history.push("/app/designer/plants/move_to")}>
{t("move mode")}
</button>
<div className="toggle-buttons">
<LayerToggle
value={showPlants}
label={t("Plants?")}
onClick={toggle("show_plants")} />
<LayerToggle
value={showPoints}
label={t("Points?")}
onClick={toggle("show_points")} />
<LayerToggle
value={showSpread}
label={t("Spread?")}
onClick={toggle("show_spread")} />
<LayerToggle
value={showFarmbot}
label={t("FarmBot?")}
onClick={toggle("show_farmbot")} />
</div>
<div className="farmbot-origin">
<label>
{t("Origin")}
</label>
<div className="quadrants">
<div
className={"quadrant " + (botOriginQuadrant === 2 && "selected")}
onClick={updateBotOriginQuadrant(2)} />
<div
className={"quadrant " + (botOriginQuadrant === 1 && "selected")}
onClick={updateBotOriginQuadrant(1)} />
<div
className={"quadrant " + (botOriginQuadrant === 3 && "selected")}
onClick={updateBotOriginQuadrant(3)} />
<div
className={"quadrant " + (botOriginQuadrant === 4 && "selected")}
onClick={updateBotOriginQuadrant(4)} />
</div>
</div>
<div className="move-to-mode">
<button
className="fb-button gray"
onClick={() => history.push("/app/designer/plants/move_to")}>
{t("move mode")}
</button>
</div>
</div>
);
</div>;
}

View File

@ -5,7 +5,7 @@ import { round, getXYFromQuadrant } from "./util";
import { DragHelpers } from "./drag_helpers";
import { Session } from "../../session";
import { BooleanSetting } from "../../session_keys";
import { Color } from "../../ui/colors";
import { Color } from "../../ui/index";
export class GardenPlant extends
React.Component<GardenPlantProps, Partial<GardenPlantState>> {

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { GridProps } from "./interfaces";
import { getXYFromQuadrant } from "./util";
import * as _ from "lodash";
import { Color } from "../../ui/colors";
import { Color } from "../../ui/index";
export function Grid(props: GridProps) {
const { quadrant, gridSize } = props.mapTransformProps;

View File

@ -1,7 +1,6 @@
let mockPath = "/app/designer/plants";
jest.mock("../../../../history", () => ({
getPathArray: jest
.fn(() => { return "/app/designer/plants/select".split("/"); })
.mockImplementationOnce(() => { return "/app/designer/plants".split("/"); })
getPathArray: jest.fn(() => { return mockPath.split("/"); })
}));
jest.mock("../../../../session", () => {
@ -59,7 +58,15 @@ describe("<PlantLayer/>", () => {
expect(wrapper.html()).toEqual("<g id=\"plant-layer\"></g>");
});
it("is in clickable mode", () => {
mockPath = "/app/designer/plants";
const p = fakeProps();
const wrapper = shallow(<PlantLayer {...p } />);
expect(wrapper.find("Link").props().style).toEqual({});
});
it("is in non-clickable mode", () => {
mockPath = "/app/designer/plants/select";
const p = fakeProps();
const wrapper = shallow(<PlantLayer {...p } />);
expect(wrapper.find("Link").props().style)

View File

@ -1,11 +1,10 @@
const mockHistory = jest.fn();
let mockPath = "/app/designer/plants";
jest.mock("../../../../history", () => ({
history: {
push: mockHistory
},
getPathArray: jest
.fn(() => { return "/app/designer/plants/select".split("/"); })
.mockImplementationOnce(() => { return "/app/designer/plants".split("/"); })
getPathArray: jest.fn(() => { return mockPath.split("/"); })
}));
import * as React from "react";
@ -53,9 +52,7 @@ describe("<ToolSlotLayer/>", () => {
});
it("navigates to tools page", async () => {
Object.defineProperty(location, "pathname", {
value: "/app/designer/plants", configurable: true
});
mockPath = "/app/designer/plants";
const p = fakeProps();
const wrapper = shallow(<ToolSlotLayer {...p } />);
const tools = wrapper.find("g").first();
@ -64,9 +61,7 @@ describe("<ToolSlotLayer/>", () => {
});
it("doesn't navigate to tools page", async () => {
Object.defineProperty(location, "pathname", {
value: "/app/designer/plants/1", configurable: true
});
mockPath = "/app/designer/plants/1";
const p = fakeProps();
const wrapper = shallow(<ToolSlotLayer {...p } />);
const tools = wrapper.find("g").first();
@ -76,6 +71,7 @@ describe("<ToolSlotLayer/>", () => {
});
it("is in non-clickable mode", () => {
mockPath = "/app/designer/plants/select";
const p = fakeProps();
const wrapper = shallow(<ToolSlotLayer {...p } />);
expect(wrapper.find("g").props().style)

View File

@ -0,0 +1,7 @@
export * from "./drag_helper_layer";
export * from "./farmbot_layer";
export * from "./hovered_plant_layer";
export * from "./plant_layer";
export * from "./point_layer";
export * from "./spread_layer";
export * from "./tool_slot_layer";

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { SlotWithTool } from "../../../resources/interfaces";
import { ToolSlotPoint } from "../tool_slot_point";
import { MapTransformProps } from "../interfaces";
import { history } from "../../../history";
import { history, getPathArray } from "../../../history";
import { getMode, Mode } from "../garden_map";
export interface ToolSlotLayerProps {
@ -13,7 +13,7 @@ export interface ToolSlotLayerProps {
}
export function ToolSlotLayer(props: ToolSlotLayerProps) {
const pathArray = location.pathname.split("/");
const pathArray = getPathArray();
const canClickTool = !(pathArray[3] === "plants" && pathArray.length > 4);
function goToToolsPage() {
if (canClickTool) {

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { MapBackgroundProps } from "./interfaces";
import { Color } from "../../ui/colors";
import { Color } from "../../ui/index";
export function MapBackground(props: MapBackgroundProps) {
const { mapTransformProps, plantAreaOffset } = props;

View File

@ -3,7 +3,7 @@ import { MapTransformProps } from "./interfaces";
import { getXYFromQuadrant, round } from "./util";
import { BotPosition } from "../../devices/interfaces";
import { isNumber } from "lodash";
import { Color } from "../../ui/colors";
import { Color } from "../../ui/index";
export interface TargetCoordinateProps {
chosenLocation: BotPosition;
@ -27,10 +27,9 @@ export function TargetCoordinate(props: TargetCoordinateProps) {
</g>
</defs>
{[45, 135, 225, 315].map(rotation => {
return (
<use key={rotation.toString()}
xlinkHref={"#target-coordinate-crosshair-segment"}
transform={`rotate(${rotation}, ${qx}, ${qy})`} />);
return <use key={rotation.toString()}
xlinkHref={"#target-coordinate-crosshair-segment"}
transform={`rotate(${rotation}, ${qx}, ${qy})`} />;
})}
</g>;
} else {

View File

@ -3,7 +3,7 @@ import { SlotWithTool } from "../../resources/interfaces";
import { getXYFromQuadrant } from "./util";
import { MapTransformProps } from "./interfaces";
import * as _ from "lodash";
import { Color } from "../../ui/colors";
import { Color } from "../../ui/index";
export interface TSPProps {
slot: SlotWithTool;

View File

@ -1,7 +1,4 @@
import {
BotOriginQuadrant,
isBotOriginQuadrant
} from "../interfaces";
import { BotOriginQuadrant, isBotOriginQuadrant } from "../interfaces";
import { McuParams } from "farmbot";
import { StepsPerMmXY } from "../../devices/interfaces";
import { CheckedAxisLength, AxisNumberProperty, BotSize } from "./interfaces";

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { shallow } from "enzyme";
import { BotOriginQuadrant } from "../../../interfaces";
import { BotFigure, BotFigureProps } from "../bot_figure";
import { Color } from "../../../../ui/colors";
import { Color } from "../../../../ui/index";
describe("<BotFigure/>", () => {
function fakeProps(): BotFigureProps {

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { AxisNumberProperty, MapTransformProps } from "../interfaces";
import { getMapSize, getXYFromQuadrant } from "../util";
import { BotPosition } from "../../../devices/interfaces";
import { Color } from "../../../ui/colors";
import { Color } from "../../../ui/index";
export interface BotFigureProps {
name: string;

View File

@ -1,6 +1,6 @@
import { Session } from "../../session";
import { NumericSetting } from "../../session_keys";
import { findIndex, isNumber } from "lodash";
import { findIndex, isNumber, clamp } from "lodash";
/**
* Map Zoom Level utilities
@ -16,11 +16,13 @@ const zoomLevels =
const foundIndex = findIndex(zoomLevels, (x) => x === 1);
const zoomLevel1Index = foundIndex === -1 ? 9 : foundIndex;
const zoomLevelsCount = zoomLevels.length;
export const maxZoomIndex = zoomLevelsCount - 1;
const clampZoom = (index: number): number => clamp(index, 0, maxZoomIndex);
export const maxZoomLevel = zoomLevelsCount - zoomLevel1Index;
export const minZoomLevel = 1 - zoomLevel1Index;
export function atMaxZoom(): boolean {
return getZoomLevelIndex() >= (zoomLevelsCount - 1);
return getZoomLevelIndex() >= maxZoomIndex;
}
export function atMinZoom(): boolean {
@ -30,9 +32,9 @@ export function atMinZoom(): boolean {
/* Load the index of a saved zoom level. */
export function getZoomLevelIndex(): number {
const savedValue = Session.deprecatedGetNum(NumericSetting.zoom_level);
return isNumber(savedValue)
? savedValue + zoomLevel1Index - 1
: zoomLevel1Index;
if (!isNumber(savedValue)) { return zoomLevel1Index; }
const zoomLevelIndex = savedValue + zoomLevel1Index - 1;
return clampZoom(zoomLevelIndex);
}
/* Save a zoom level index. */
@ -43,5 +45,5 @@ export function saveZoomLevelIndex(index: number) {
/* Calculate map zoom level from a zoom level index. */
export function calcZoomLevel(index: number): number {
return zoomLevels[index];
return zoomLevels[clampZoom(index)];
}

View File

@ -2,6 +2,11 @@ jest.mock("react-redux", () => ({
connect: jest.fn()
}));
let mockPath = "";
jest.mock("../../../history", () => ({
getPathArray: jest.fn(() => { return mockPath.split("/"); })
}));
import * as React from "react";
import { mount } from "enzyme";
import { AddPlant, AddPlantProps } from "../add_plant";
@ -24,9 +29,7 @@ describe("<AddPlant />", () => {
}
}]
};
Object.defineProperty(location, "pathname", {
value: "/app/designer/plants/crop_search/mint/add"
});
mockPath = "/app/designer/plants/crop_search/mint/add";
const wrapper = mount(<AddPlant {...props} />);
expect(wrapper.text()).toContain("Mint");
expect(wrapper.text()).toContain("Done");

View File

@ -2,6 +2,11 @@ jest.mock("react-redux", () => ({
connect: jest.fn()
}));
let mockPath = "";
jest.mock("../../../history", () => ({
getPathArray: jest.fn(() => { return mockPath.split("/"); })
}));
jest.mock("../../search_selectors", () => ({
findBySlug: () => {
return {
@ -26,9 +31,7 @@ import { shallow } from "enzyme";
describe("<CropInfo />", () => {
it("renders", () => {
Object.defineProperty(location, "pathname", {
value: "/app/designer/plants/crop_search/mint"
});
mockPath = "/app/designer/plants/crop_search/mint";
const wrapper = shallow(
<CropInfo
OFSearch={jest.fn()}

View File

@ -10,6 +10,11 @@ jest.mock("../../../device", () => ({
getDevice: () => (mockDevice)
}));
let mockPath = "";
jest.mock("../../../history", () => ({
getPathArray: jest.fn(() => { return mockPath.split("/"); })
}));
import * as React from "react";
import { mount } from "enzyme";
import { MoveTo, MoveToProps } from "../move_to";
@ -17,9 +22,7 @@ import { MoveTo, MoveToProps } from "../move_to";
describe("<MoveTo />", () => {
beforeEach(function () {
jest.clearAllMocks();
Object.defineProperty(location, "pathname", {
value: "/app/designer/plants/move_to"
});
mockPath = "/app/designer/plants/move_to";
});
function fakeProps(): MoveToProps {

View File

@ -2,6 +2,11 @@ jest.mock("react-redux", () => ({
connect: jest.fn()
}));
let mockPath = "";
jest.mock("../../../history", () => ({
getPathArray: jest.fn(() => { return mockPath.split("/"); })
}));
import * as React from "react";
import { mount, shallow } from "enzyme";
import { SelectPlants, SelectPlantsProps } from "../select_plants";
@ -11,9 +16,7 @@ import { Actions } from "../../../constants";
describe("<SelectPlants />", () => {
beforeEach(function () {
jest.clearAllMocks();
Object.defineProperty(location, "pathname", {
value: "/app/designer/plants/select"
});
mockPath = "/app/designer/plants/select";
});
function fakeProps(): SelectPlantsProps {

View File

@ -1,5 +1,5 @@
import * as React from "react";
import { BackArrow } from "../../ui";
import { BackArrow } from "../../ui/index";
import { Everything } from "../../interfaces";
import { connect } from "react-redux";
import { t } from "i18next";

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { connect } from "react-redux";
import { t } from "i18next";
import { BackArrow } from "../../ui";
import { BackArrow } from "../../ui/index";
import { TaggedPlantPointer } from "../../resources/tagged_resources";
import { mapStateToProps, formatPlantInfo } from "./map_state_to_props";
import { PlantInfoBase } from "./plant_info_base";

View File

@ -1,9 +1,6 @@
import { CropLiveSearchResult } from "./interfaces";
import { generateReducer } from "../redux/generate_reducer";
import {
DesignerState,
HoveredPlantPayl
} from "./interfaces";
import { DesignerState, HoveredPlantPayl } from "./interfaces";
import { cloneDeep } from "lodash";
import { TaggedResource } from "../resources/tagged_resources";
import { Actions } from "../constants";

View File

@ -16,57 +16,55 @@ export class CameraCalibration extends
React.Component<CameraCalibrationProps, CameraCalibrationState> {
render() {
const classname = "weed-detector-widget";
return (
<Widget className={classname}>
<TitleBar
title={"Camera Calibration"}
help={t(ToolTips.CAMERA_CALIBRATION)}
docs={"farmware#section-weed-detector"}
onCalibrate={this.props.dispatch(calibrate)}
env={this.props.env} />
<WidgetBody>
<Row>
<Col sm={12}>
<MustBeOnline
syncStatus={this.props.syncStatus}
networkState={this.props.botToMqttStatus}
lockOpen={process.env.NODE_ENV !== "production"}>
<ImageWorkspace
onProcessPhoto={(id) => { this.props.dispatch(scanImage(id)); }}
onFlip={(uuid) => this.props.dispatch(selectImage(uuid))}
images={this.props.images}
currentImage={this.props.currentImage}
onChange={(key, value) => {
const MAPPING: Record<typeof key, WDENVKey> = {
"iteration": "CAMERA_CALIBRATION_iteration",
"morph": "CAMERA_CALIBRATION_morph",
"blur": "CAMERA_CALIBRATION_blur",
"H_HI": "CAMERA_CALIBRATION_H_HI",
"H_LO": "CAMERA_CALIBRATION_H_LO",
"S_HI": "CAMERA_CALIBRATION_S_HI",
"S_LO": "CAMERA_CALIBRATION_S_LO",
"V_HI": "CAMERA_CALIBRATION_V_HI",
"V_LO": "CAMERA_CALIBRATION_V_LO"
};
envSave(MAPPING[key], value);
}}
iteration={this.props.iteration}
morph={this.props.morph}
blur={this.props.blur}
H_LO={this.props.H_LO}
S_LO={this.props.S_LO}
V_LO={this.props.V_LO}
H_HI={this.props.H_HI}
S_HI={this.props.S_HI}
V_HI={this.props.V_HI}
invertHue={!!envGet(
"CAMERA_CALIBRATION_invert_hue_selection",
this.props.env)} />
</MustBeOnline>
</Col>
</Row>
</WidgetBody>
</Widget>
);
return <Widget className={classname}>
<TitleBar
title={"Camera Calibration"}
help={t(ToolTips.CAMERA_CALIBRATION)}
docs={"farmware#section-weed-detector"}
onCalibrate={this.props.dispatch(calibrate)}
env={this.props.env} />
<WidgetBody>
<Row>
<Col sm={12}>
<MustBeOnline
syncStatus={this.props.syncStatus}
networkState={this.props.botToMqttStatus}
lockOpen={process.env.NODE_ENV !== "production"}>
<ImageWorkspace
onProcessPhoto={(id) => { this.props.dispatch(scanImage(id)); }}
onFlip={(uuid) => this.props.dispatch(selectImage(uuid))}
images={this.props.images}
currentImage={this.props.currentImage}
onChange={(key, value) => {
const MAPPING: Record<typeof key, WDENVKey> = {
"iteration": "CAMERA_CALIBRATION_iteration",
"morph": "CAMERA_CALIBRATION_morph",
"blur": "CAMERA_CALIBRATION_blur",
"H_HI": "CAMERA_CALIBRATION_H_HI",
"H_LO": "CAMERA_CALIBRATION_H_LO",
"S_HI": "CAMERA_CALIBRATION_S_HI",
"S_LO": "CAMERA_CALIBRATION_S_LO",
"V_HI": "CAMERA_CALIBRATION_V_HI",
"V_LO": "CAMERA_CALIBRATION_V_LO"
};
envSave(MAPPING[key], value);
}}
iteration={this.props.iteration}
morph={this.props.morph}
blur={this.props.blur}
H_LO={this.props.H_LO}
S_LO={this.props.S_LO}
V_LO={this.props.V_LO}
H_HI={this.props.H_HI}
S_HI={this.props.S_HI}
V_HI={this.props.V_HI}
invertHue={!!envGet(
"CAMERA_CALIBRATION_invert_hue_selection",
this.props.env)} />
</MustBeOnline>
</Col>
</Row>
</WidgetBody>
</Widget>;
}
}

View File

@ -8,15 +8,11 @@ import {
import { MustBeOnline } from "../devices/must_be_online";
import { ToolTips, Content } from "../constants";
import {
Widget,
WidgetHeader,
WidgetBody,
Row,
Col,
DropDownItem
} from "../ui";
Widget, WidgetHeader, WidgetBody,
Row, Col,
FBSelect, DropDownItem
} from "../ui/index";
import { betterCompact } from "../util";
import { FBSelect } from "../ui/new_fb_select";
import { Popover, Position } from "@blueprintjs/core";
import { getFirstPartyFarmwareList } from "./actions";
@ -155,88 +151,86 @@ export class FarmwarePanel extends React.Component<FWProps, Partial<FWState>> {
}
render() {
return (
<Widget className="farmware-widget">
<WidgetHeader
title="Farmware"
helpText={ToolTips.FARMWARE}>
<Popover position={Position.BOTTOM_RIGHT}>
<i className="fa fa-gear" />
<FarmwareConfigMenu
show={this.state.showFirstParty}
toggle={this.toggleFirstPartyDisplay}
firstPartyFwsInstalled={
this.firstPartyFarmwaresPresent(this.state.firstPartyList)} />
</Popover>
</WidgetHeader>
<WidgetBody>
<MustBeOnline
syncStatus={this.props.syncStatus}
networkState={this.props.botToMqttStatus}
lockOpen={process.env.NODE_ENV !== "production"}>
<Row>
<fieldset>
<Col xs={12}>
<input type="url"
placeholder={"https://...."}
value={this.state.packageUrl || ""}
onChange={(e) => {
this.setState({ packageUrl: e.currentTarget.value });
}} />
</Col>
<Col xs={12}>
<button
className="fb-button green"
onClick={this.install}>
{t("Install")}
</button>
</Col>
</fieldset>
</Row>
<Row>
<fieldset>
<Col xs={12}>
<FBSelect
key={"farmware_" + this.selectedItem()}
list={this.fwList()}
selectedItem={this.selectedItem()}
onChange={(x) => {
const selectedFarmware = x.value;
if (_.isString(selectedFarmware)) {
this.setState({ selectedFarmware });
} else {
throw new Error(`Bad farmware name: ${x.value}`);
}
}}
placeholder="Installed Farmware Packages" />
</Col>
<Col xs={12}>
<button
className="fb-button red"
onClick={this.remove}>
{t("Remove")}
</button>
<button
className="fb-button yellow"
onClick={this.update}>
{t("Update")}
</button>
<button
className="fb-button green"
onClick={this.run}>
{t("Run")}
</button>
</Col>
</fieldset>
</Row>
<Row>
return <Widget className="farmware-widget">
<WidgetHeader
title="Farmware"
helpText={ToolTips.FARMWARE}>
<Popover position={Position.BOTTOM_RIGHT}>
<i className="fa fa-gear" />
<FarmwareConfigMenu
show={this.state.showFirstParty}
toggle={this.toggleFirstPartyDisplay}
firstPartyFwsInstalled={
this.firstPartyFarmwaresPresent(this.state.firstPartyList)} />
</Popover>
</WidgetHeader>
<WidgetBody>
<MustBeOnline
syncStatus={this.props.syncStatus}
networkState={this.props.botToMqttStatus}
lockOpen={process.env.NODE_ENV !== "production"}>
<Row>
<fieldset>
<Col xs={12}>
{this.fwDescription(this.state.selectedFarmware)}
<input type="url"
placeholder={"https://...."}
value={this.state.packageUrl || ""}
onChange={(e) => {
this.setState({ packageUrl: e.currentTarget.value });
}} />
</Col>
</Row>
</MustBeOnline>
</WidgetBody>
</Widget>
);
<Col xs={12}>
<button
className="fb-button green"
onClick={this.install}>
{t("Install")}
</button>
</Col>
</fieldset>
</Row>
<Row>
<fieldset>
<Col xs={12}>
<FBSelect
key={"farmware_" + this.selectedItem()}
list={this.fwList()}
selectedItem={this.selectedItem()}
onChange={(x) => {
const selectedFarmware = x.value;
if (_.isString(selectedFarmware)) {
this.setState({ selectedFarmware });
} else {
throw new Error(`Bad farmware name: ${x.value}`);
}
}}
placeholder="Installed Farmware Packages" />
</Col>
<Col xs={12}>
<button
className="fb-button red"
onClick={this.remove}>
{t("Remove")}
</button>
<button
className="fb-button yellow"
onClick={this.update}>
{t("Update")}
</button>
<button
className="fb-button green"
onClick={this.run}>
{t("Run")}
</button>
</Col>
</fieldset>
</Row>
<Row>
<Col xs={12}>
{this.fwDescription(this.state.selectedFarmware)}
</Col>
</Row>
</MustBeOnline>
</WidgetBody>
</Widget>;
}
}

View File

@ -63,22 +63,20 @@ export class ImageFlipper extends
render() {
const image = this.imageJSX();
const multipleImages = this.props.images.length > 1;
return (
<div className="image-flipper">
{image}
<button
onClick={this.go(1)}
disabled={!multipleImages || this.state.disablePrev}
className="image-flipper-left fb-button">
{t("Prev")}
</button>
<button
onClick={this.go(-1)}
disabled={!multipleImages || this.state.disableNext}
className="image-flipper-right fb-button">
{t("Next")}
</button>
</div>
);
return <div className="image-flipper">
{image}
<button
onClick={this.go(1)}
disabled={!multipleImages || this.state.disablePrev}
className="image-flipper-left fb-button">
{t("Prev")}
</button>
<button
onClick={this.go(-1)}
disabled={!multipleImages || this.state.disableNext}
className="image-flipper-right fb-button">
{t("Next")}
</button>
</div>;
}
}

View File

@ -3,13 +3,12 @@ import * as _ from "lodash";
import * as moment from "moment";
import { t } from "i18next";
import { success, error } from "farmbot-toastr";
import { Widget, WidgetHeader, WidgetBody } from "../../ui/index";
import { Widget, WidgetHeader, WidgetBody, WidgetFooter } from "../../ui/index";
import { ImageFlipper } from "./image_flipper";
import { PhotosProps } from "./interfaces";
import { getDevice } from "../../device";
import { ToolTips } from "../../constants";
import { selectImage } from "./actions";
import { WidgetFooter } from "../../ui/widget_footer";
import { safeStringFetch } from "../../util";
import { destroy } from "../../api/crud";
@ -26,12 +25,10 @@ interface MetaInfoProps {
function MetaInfo({ obj, attr, label }: MetaInfoProps) {
const top = label || _.startCase(attr.split("_").join());
const bottom = safeStringFetch(obj, attr);
return (
<div>
<label>{top}:</label>
<span>{bottom || "unknown"}</span>
</div>
);
return <div>
<label>{top}:</label>
<span>{bottom || "unknown"}</span>
</div>;
}
export class Photos extends React.Component<PhotosProps, {}> {
@ -66,43 +63,43 @@ export class Photos extends React.Component<PhotosProps, {}> {
render() {
const image = this.props.currentImage;
const created_at = image
? moment(image.body.created_at).utcOffset(this.props.timeOffset).format("MMMM Do, YYYY h:mma")
? moment(image.body.created_at)
.utcOffset(this.props.timeOffset)
.format("MMMM Do, YYYY h:mma")
: "";
return (
<Widget className="photos-widget">
<WidgetHeader helpText={ToolTips.PHOTOS} title={"Photos"}>
<button
className="fb-button gray"
onClick={this.takePhoto}>
{t("Take Photo")}
</button>
<button
className="fb-button red"
onClick={() => this.destroy()}>
{t("Delete Photo")}
</button>
</WidgetHeader>
<WidgetBody>
<ImageFlipper
onFlip={id => { this.props.dispatch(selectImage(id)); }}
currentImage={this.props.currentImage}
images={this.props.images} />
</WidgetBody>
<WidgetFooter>
{/** Separated from <MetaInfo /> for stylistic purposes. */}
{image ?
<div className="image-created-at">
<label>{t("Created At:")}</label>
<span>
{created_at}
</span>
</div>
: ""}
<div className="image-metadatas">
{this.metaDatas()}
return <Widget className="photos-widget">
<WidgetHeader helpText={ToolTips.PHOTOS} title={"Photos"}>
<button
className="fb-button gray"
onClick={this.takePhoto}>
{t("Take Photo")}
</button>
<button
className="fb-button red"
onClick={() => this.destroy()}>
{t("Delete Photo")}
</button>
</WidgetHeader>
<WidgetBody>
<ImageFlipper
onFlip={id => { this.props.dispatch(selectImage(id)); }}
currentImage={this.props.currentImage}
images={this.props.images} />
</WidgetBody>
<WidgetFooter>
{/** Separated from <MetaInfo /> for stylistic purposes. */}
{image ?
<div className="image-created-at">
<label>{t("Created At:")}</label>
<span>
{created_at}
</span>
</div>
</WidgetFooter>
</Widget>
);
: ""}
<div className="image-metadatas">
{this.metaDatas()}
</div>
</WidgetFooter>
</Widget>;
}
}

View File

@ -1,12 +1,15 @@
import * as React from "react";
import { t } from "i18next";
import { DropDownItem } from "../../ui/fb_select";
import { Row, Col, NULL_CHOICE } from "../../ui/index";
import { FBSelect } from "../../ui/new_fb_select";
import {
BlurableInput,
Row, Col,
FBSelect, NULL_CHOICE, DropDownItem
} from "../../ui/index";
import { SettingsMenuProps } from "./interfaces";
import * as _ from "lodash";
import { BlurableInput } from "../../ui/blurable_input";
import { SPECIAL_VALUE_DDI, CALIBRATION_DROPDOWNS, ORIGIN_DROPDOWNS } from "./constants";
import {
SPECIAL_VALUE_DDI, CALIBRATION_DROPDOWNS, ORIGIN_DROPDOWNS
} from "./constants";
import { WD_ENV } from "./remote_env/interfaces";
import { envGet } from "./remote_env/selectors";
import { SPECIAL_VALUES } from "./remote_env/constants";

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { Hue, Saturation } from "react-color/lib/components/common";
import { FarmbotPickerProps } from "./interfaces";
import * as _ from "lodash";
import { Color } from "../../ui/colors";
import { Color } from "../../ui/index";
/** Wrapper class around `react-color`'s `<Saturation />` and `<Hue />`.
* Add an extra white box feature for showing user weed detection settings.

View File

@ -76,92 +76,90 @@ export class ImageWorkspace extends React.Component<Props, {}> {
render() {
const { H_LO, H_HI, S_LO, S_HI, V_LO, V_HI } = this.props;
return (
<div className="widget-content">
<Row>
<Col xs={12} md={6}>
<h4>
<i>{t("Color Range")}</i>
</h4>
<label htmlFor="hue">{t("HUE")}</label>
<WeedDetectorSlider
onRelease={this.onHslChange("H")}
lowest={RANGES.H.LOWEST}
highest={RANGES.H.HIGHEST}
lowValue={Math.min(H_LO, H_HI)}
highValue={Math.max(H_LO, H_HI)} />
<label htmlFor="saturation">{t("SATURATION")}</label>
<WeedDetectorSlider
onRelease={this.onHslChange("S")}
lowest={RANGES.S.LOWEST}
highest={RANGES.S.HIGHEST}
lowValue={S_LO}
highValue={S_HI} />
<label htmlFor="value">{t("VALUE")}</label>
<WeedDetectorSlider
onRelease={this.onHslChange("V")}
lowest={RANGES.V.LOWEST}
highest={RANGES.V.HIGHEST}
lowValue={V_LO}
highValue={V_HI} />
</Col>
<Col xs={12} md={6}>
<FarmbotColorPicker
h={[H_LO, H_HI]}
s={[S_LO, S_HI]}
v={[V_LO, V_HI]}
invertHue={this.props.invertHue} />
</Col>
</Row>
<Row>
<Col xs={12}>
<h4>
<i>{t("Processing Parameters")}</i>
</h4>
</Col>
return <div className="widget-content">
<Row>
<Col xs={12} md={6}>
<h4>
<i>{t("Color Range")}</i>
</h4>
<label htmlFor="hue">{t("HUE")}</label>
<WeedDetectorSlider
onRelease={this.onHslChange("H")}
lowest={RANGES.H.LOWEST}
highest={RANGES.H.HIGHEST}
lowValue={Math.min(H_LO, H_HI)}
highValue={Math.max(H_LO, H_HI)} />
<label htmlFor="saturation">{t("SATURATION")}</label>
<WeedDetectorSlider
onRelease={this.onHslChange("S")}
lowest={RANGES.S.LOWEST}
highest={RANGES.S.HIGHEST}
lowValue={S_LO}
highValue={S_HI} />
<label htmlFor="value">{t("VALUE")}</label>
<WeedDetectorSlider
onRelease={this.onHslChange("V")}
lowest={RANGES.V.LOWEST}
highest={RANGES.V.HIGHEST}
lowValue={V_LO}
highValue={V_HI} />
</Col>
<Col xs={12} md={6}>
<FarmbotColorPicker
h={[H_LO, H_HI]}
s={[S_LO, S_HI]}
v={[V_LO, V_HI]}
invertHue={this.props.invertHue} />
</Col>
</Row>
<Row>
<Col xs={12}>
<h4>
<i>{t("Processing Parameters")}</i>
</h4>
</Col>
<Col xs={4}>
<label>{t("BLUR")}</label>
<BlurableInput type="number"
min={RANGES.BLUR.LOWEST}
max={RANGES.BLUR.HIGHEST}
onCommit={this.numericChange("blur")}
value={"" + this.props.blur} />
</Col>
<Col xs={4}>
<label>{t("BLUR")}</label>
<BlurableInput type="number"
min={RANGES.BLUR.LOWEST}
max={RANGES.BLUR.HIGHEST}
onCommit={this.numericChange("blur")}
value={"" + this.props.blur} />
</Col>
<Col xs={4}>
<label>{t("MORPH")}</label>
<BlurableInput type="number"
min={RANGES.MORPH.LOWEST}
max={RANGES.MORPH.HIGHEST}
onCommit={this.numericChange("morph")}
value={"" + this.props.morph} />
</Col>
<Col xs={4}>
<label>{t("ITERATION")}</label>
<BlurableInput type="number"
min={RANGES.ITERATION.LOWEST}
max={RANGES.ITERATION.HIGHEST}
onCommit={this.numericChange("iteration")}
value={"" + this.props.iteration} />
</Col>
</Row>
<Row>
<Col xs={12}>
<button
className="green fb-button"
title="Scan this image"
onClick={this.maybeProcessPhoto}
hidden={!this.props.images.length} >
{t("Scan image")}
</button>
</Col>
</Row>
<ImageFlipper
onFlip={this.props.onFlip}
images={this.props.images}
currentImage={this.props.currentImage} />
</div>
);
<Col xs={4}>
<label>{t("MORPH")}</label>
<BlurableInput type="number"
min={RANGES.MORPH.LOWEST}
max={RANGES.MORPH.HIGHEST}
onCommit={this.numericChange("morph")}
value={"" + this.props.morph} />
</Col>
<Col xs={4}>
<label>{t("ITERATION")}</label>
<BlurableInput type="number"
min={RANGES.ITERATION.LOWEST}
max={RANGES.ITERATION.HIGHEST}
onCommit={this.numericChange("iteration")}
value={"" + this.props.iteration} />
</Col>
</Row>
<Row>
<Col xs={12}>
<button
className="green fb-button"
title="Scan this image"
onClick={this.maybeProcessPhoto}
hidden={!this.props.images.length} >
{t("Scan image")}
</button>
</Col>
</Row>
<ImageFlipper
onFlip={this.props.onFlip}
images={this.props.images}
currentImage={this.props.currentImage} />
</div>;
}
}

View File

@ -1,4 +1,4 @@
import { DropDownItem, NULL_CHOICE } from "../../ui/fb_select";
import { DropDownItem, NULL_CHOICE } from "../../ui/index";
import { SPECIAL_VALUE_DDI } from "./constants";
import { WD_ENV } from "./remote_env/interfaces";
import { envGet } from "./remote_env/selectors";

View File

@ -5,7 +5,7 @@ import { WidgetHeader } from "../../ui/index";
import { WD_ENV } from "./remote_env/interfaces";
import { envSave } from "./remote_env/actions";
import { Popover, PopoverInteractionKind } from "@blueprintjs/core";
import { DocSlug } from "../../ui/doc_link";
import { DocSlug } from "../../ui/index";
type ClickHandler = React.EventHandler<React.MouseEvent<HTMLButtonElement>>;
@ -32,41 +32,39 @@ export function TitleBar({
help,
docs
}: Props) {
return (
<WidgetHeader helpText={help} title={title} docPage={docs}>
<button
hidden={!onSave}
onClick={onSave}
className="fb-button green" >
{t("SAVE")}
</button>
<button
hidden={!onTest}
onClick={onTest}
className="fb-button yellow" >
{t("TEST")}
</button>
<button
hidden={!onDeletionClick}
onClick={onDeletionClick}
className="fb-button red" >
{deletionProgress || t("CLEAR WEEDS")}
</button>
<button
hidden={!onCalibrate}
onClick={onCalibrate}
className="fb-button green" >
{t("Calibrate")}
</button>
<div hidden={!env}>
<Popover
interactionKind={PopoverInteractionKind.CLICK_TARGET_ONLY}>
<i className="fa fa-cog" />
{(env && <WeedDetectorConfig
values={env}
onChange={envSave} />)}
</Popover>
</div>
</WidgetHeader>
);
return <WidgetHeader helpText={help} title={title} docPage={docs}>
<button
hidden={!onSave}
onClick={onSave}
className="fb-button green" >
{t("SAVE")}
</button>
<button
hidden={!onTest}
onClick={onTest}
className="fb-button yellow" >
{t("TEST")}
</button>
<button
hidden={!onDeletionClick}
onClick={onDeletionClick}
className="fb-button red" >
{deletionProgress || t("CLEAR WEEDS")}
</button>
<button
hidden={!onCalibrate}
onClick={onCalibrate}
className="fb-button green" >
{t("Calibrate")}
</button>
<div hidden={!env}>
<Popover
interactionKind={PopoverInteractionKind.CLICK_TARGET_ONLY}>
<i className="fa fa-cog" />
{(env && <WeedDetectorConfig
values={env}
onChange={envSave} />)}
</Popover>
</div>
</WidgetHeader>;
}

View File

@ -17,7 +17,9 @@ jest.mock("../resend_verification", () => {
});
import * as React from "react";
import { FormField, sendEmail, DidRegister, MustRegister, CreateAccount } from "../create_account";
import {
FormField, sendEmail, DidRegister, MustRegister, CreateAccount
} from "../create_account";
import { shallow } from "enzyme";
import { BlurableInput } from "../../ui/index";
import { success, error } from "farmbot-toastr";

View File

@ -144,32 +144,30 @@ export class FrontPage extends React.Component<{}, Partial<FrontPageState>> {
const TOS_URL = globalConfig.TOS_URL;
if (TOS_URL) {
const PRV_URL = globalConfig.PRIV_URL;
return (
<div>
<div className={"tos"}>
<label>{t("I agree to the terms of use")}</label>
<input type="checkbox"
onChange={this.set("agreeToTerms")}
value={this.state.agreeToTerms ? "false" : "true"} />
</div>
<ul>
<li>
<a
href={PRV_URL}
target="_blank">
{t("Privacy Policy")}
</a>
</li>
<li>
<a
href={TOS_URL}
target="_blank">
{t("Terms of Use")}
</a>
</li>
</ul>
return <div>
<div className={"tos"}>
<label>{t("I agree to the terms of use")}</label>
<input type="checkbox"
onChange={this.set("agreeToTerms")}
value={this.state.agreeToTerms ? "false" : "true"} />
</div>
);
<ul>
<li>
<a
href={PRV_URL}
target="_blank">
{t("Privacy Policy")}
</a>
</li>
<li>
<a
href={TOS_URL}
target="_blank">
{t("Terms of Use")}
</a>
</li>
</ul>
</div>;
}
}

View File

@ -34,35 +34,31 @@ export class HotKeys extends React.Component<Props, Partial<State>> {
state: State = { guideOpen: false };
render() {
return (
<div>
<Overlay
isOpen={this.state.guideOpen}
onClose={this.toggle("guideOpen")}>
<div className={hotkeyGuideClasses}>
<h3>{t("Hotkeys")}</h3>
<i
className="fa fa-times"
onClick={this.toggle("guideOpen")} />
{
this.hotkeys(this.props.dispatch, "")
.map(hotkey => {
return (
<Row key={hotkey.combo}>
<Col xs={5}>
<label>{hotkey.label}</label>
</Col>
<Col xs={7}>
<code>{hotkey.combo}</code>
</Col>
</Row>
);
})
}
</div>
</Overlay>
</div>
);
return <div>
<Overlay
isOpen={this.state.guideOpen}
onClose={this.toggle("guideOpen")}>
<div className={hotkeyGuideClasses}>
<h3>{t("Hotkeys")}</h3>
<i
className="fa fa-times"
onClick={this.toggle("guideOpen")} />
{
this.hotkeys(this.props.dispatch, "")
.map(hotkey => {
return <Row key={hotkey.combo}>
<Col xs={5}>
<label>{hotkey.label}</label>
</Col>
<Col xs={7}>
<code>{hotkey.combo}</code>
</Col>
</Row>;
})
}
</div>
</Overlay>
</div>;
}
toggle = (property: keyof State) => () =>

View File

@ -68,7 +68,7 @@ const filterByVerbosity = (state: LogsState, logs: TaggedLog[]) => {
})
.filter((log: TaggedLog) => {
const type = (log.body.meta || {}).type;
const verbosity = log.body.meta.verbosity;
const { verbosity } = log.body.meta;
const filterLevel = state[type as keyof LogsState];
const displayLog = verbosity
? verbosity <= filterLevel

View File

@ -1,7 +1,7 @@
import * as React from "react";
import * as moment from "moment";
import { connect } from "react-redux";
import { Col, Row, Page, ToolTip } from "../ui";
import { Col, Row, Page, ToolTip } from "../ui/index";
import { mapStateToProps } from "./state_to_props";
import { t } from "i18next";
import { Popover, Position } from "@blueprintjs/core";

View File

@ -3,7 +3,7 @@ import { t } from "i18next";
import { NavBarProps, NavBarState } from "./interfaces";
import { EStopButton } from "../devices/components/e_stop_btn";
import { Session } from "../session";
import { Row, Col } from "../ui";
import { Row, Col } from "../ui/index";
import { getPathArray } from "../history";
import { updatePageInfo } from "../util";
import { SyncButton } from "./sync_button";
@ -61,49 +61,47 @@ export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
const { mobileMenuOpen, tickerListOpen, accountMenuOpen } = this.state;
const { logs, timeOffset } = this.props;
return (
<div className="nav-wrapper">
<nav role="navigation">
<Row>
<Col xs={12}>
<div>
<TickerList { ...{ logs, tickerListOpen, toggle, timeOffset } } />
<div className="nav-group">
<div className="nav-left">
<i
className={menuIconClassNames.join(" ")}
onClick={this.toggle("mobileMenuOpen")} />
<span className="mobile-menu-container">
{MobileMenu({ close, mobileMenuOpen })}
</span>
<span className="top-menu-container">
{NavLinks({ close })}
</span>
</div>
<div className="nav-right">
<Popover
inline
interactionKind={PopoverInteractionKind.HOVER}
target={
<div className="nav-name"
onClick={this.toggle("accountMenuOpen")}>
{firstName}
</div>}
position={Position.BOTTOM_RIGHT}
content={AdditionalMenu({ logout: this.logout, close })}
isOpen={accountMenuOpen}
onClose={this.close("accountMenuOpen")} />
<EStopButton
bot={this.props.bot}
user={this.props.user} />
{this.syncButton()}
</div>
return <div className="nav-wrapper">
<nav role="navigation">
<Row>
<Col xs={12}>
<div>
<TickerList { ...{ logs, tickerListOpen, toggle, timeOffset } } />
<div className="nav-group">
<div className="nav-left">
<i
className={menuIconClassNames.join(" ")}
onClick={this.toggle("mobileMenuOpen")} />
<span className="mobile-menu-container">
{MobileMenu({ close, mobileMenuOpen })}
</span>
<span className="top-menu-container">
{NavLinks({ close })}
</span>
</div>
<div className="nav-right">
<Popover
inline
interactionKind={PopoverInteractionKind.HOVER}
target={
<div className="nav-name"
onClick={this.toggle("accountMenuOpen")}>
{firstName}
</div>}
position={Position.BOTTOM_RIGHT}
content={AdditionalMenu({ logout: this.logout, close })}
isOpen={accountMenuOpen}
onClose={this.close("accountMenuOpen")} />
<EStopButton
bot={this.props.bot}
user={this.props.user} />
{this.syncButton()}
</div>
</div>
</Col>
</Row>
</nav>
</div>
);
</div>
</Col>
</Row>
</nav>
</div>;
}
}

View File

@ -8,15 +8,13 @@ const classes = [Classes.CARD, Classes.ELEVATION_4, "mobile-menu"];
export let MobileMenu = (props: MobileMenuProps) => {
const isActive = props.mobileMenuOpen ? "active" : "inactive";
return (
<div>
<Overlay
isOpen={props.mobileMenuOpen}
onClose={props.close("mobileMenuOpen")}>
<div className={`${classes.join(" ")} ${isActive}`}>
{NavLinks({ close: props.close })}
</div>
</Overlay>
</div>
);
return <div>
<Overlay
isOpen={props.mobileMenuOpen}
onClose={props.close("mobileMenuOpen")}>
<div className={`${classes.join(" ")} ${isActive}`}>
{NavLinks({ close: props.close })}
</div>
</Overlay>
</div>;
};

View File

@ -16,23 +16,19 @@ export const links = [
export const NavLinks = (props: NavLinksProps) => {
const currPageSlug = getPathArray()[2];
return (
<div className="links">
<div className="nav-links">
{links.map(link => {
const isActive = (currPageSlug === link.slug) ? "active" : "";
return (
<Link
to={"/app/" + link.slug}
className={`${isActive}`}
key={link.slug}
onClick={props.close("mobileMenuOpen")}>
<i className={`fa fa-${link.icon}`} />
{link.name}
</Link>
);
})}
</div>
return <div className="links">
<div className="nav-links">
{links.map(link => {
const isActive = (currPageSlug === link.slug) ? "active" : "";
return <Link
to={"/app/" + link.slug}
className={`${isActive}`}
key={link.slug}
onClick={props.close("mobileMenuOpen")}>
<i className={`fa fa-${link.icon}`} />
{link.name}
</Link>;
})}
</div>
);
</div>;
};

View File

@ -31,11 +31,9 @@ export function SyncButton({ user, bot, dispatch, consistent }: NavButtonProps)
sync_status = sync_status || "unknown";
const color = consistent ? (COLOR_MAPPING[sync_status] || "red") : "gray";
const text = TEXT_MAPPING[sync_status] || "DISCONNECTED";
return (
<button
className={`nav-sync ${color} fb-button`}
onClick={() => dispatch(sync())}>
{text}
</button>
);
return <button
className={`nav-sync ${color} fb-button`}
onClick={() => dispatch(sync())}>
{text}
</button>;
}

View File

@ -48,46 +48,42 @@ const getfirstTickerLog = (logs: Log[]): Log => {
const Ticker = (log: Log, index: number, timeOffset: number) => {
const time = formatLogTime(log.created_at, timeOffset);
const type = (log.meta || {}).type;
return (
// TODO: Should utilize log's `uuid` instead of index.
<div key={index} className="status-ticker-wrapper">
<div className={`saucer ${type}`} />
<label className="status-ticker-message">
<Markdown>
{log.message.replace(/\s+/g, " ") || "Loading"}
</Markdown>
</label>
<label className="status-ticker-created-at">
{time}
</label>
</div>
);
// TODO: Should utilize log's `uuid` instead of index.
return <div key={index} className="status-ticker-wrapper">
<div className={`saucer ${type}`} />
<label className="status-ticker-message">
<Markdown>
{log.message.replace(/\s+/g, " ") || "Loading"}
</Markdown>
</label>
<label className="status-ticker-created-at">
{time}
</label>
</div>;
};
export let TickerList = (props: TickerListProps) => {
return (
<div
className="ticker-list"
onClick={props.toggle("tickerListOpen")} >
<div className="first-ticker">
{Ticker(getfirstTickerLog(props.logs), -1, props.timeOffset)}
</div>
<Collapse isOpen={props.tickerListOpen}>
{props
.logs
.filter((log, index) => index !== 0)
.filter((log) => logFilter(log))
.map((log: Log, index: number) => Ticker(log, index, props.timeOffset))}
</Collapse>
<Collapse isOpen={props.tickerListOpen}>
<Link to={"/app/logs"}>
<div className="logs-page-link">
<label>
{t("Filter logs")}
</label>
</div>
</Link>
</Collapse>
return <div
className="ticker-list"
onClick={props.toggle("tickerListOpen")} >
<div className="first-ticker">
{Ticker(getfirstTickerLog(props.logs), -1, props.timeOffset)}
</div>
);
<Collapse isOpen={props.tickerListOpen}>
{props
.logs
.filter((log, index) => index !== 0)
.filter((log) => logFilter(log))
.map((log: Log, index: number) => Ticker(log, index, props.timeOffset))}
</Collapse>
<Collapse isOpen={props.tickerListOpen}>
<Link to={"/app/logs"}>
<div className="logs-page-link">
<label>
{t("Filter logs")}
</label>
</div>
</Link>
</Collapse>
</div>;
};

View File

@ -66,47 +66,45 @@ export class PasswordReset extends React.Component<Props, State> {
borderBottom: "none"
};
return (
<div className="static-page">
<div className="all-content-wrapper">
<h1 className="text-center">
{t("Reset your password")}
</h1>
<br />
<Row>
<Col xs={12} sm={6} className="col-sm-push-3">
<Widget>
<WidgetHeader title={"Reset Password"} />
<WidgetBody>
<form onSubmit={this.submit.bind(this)}>
<label>
{t("New Password")}
</label>
<input
type="password"
onChange={this.set("password").bind(this)} />
<label>
{t("Confirm New Password")}
</label>
<input
type="password"
onChange={this.set("passwordConfirmation").bind(this)} />
<Row>
<Col xs={12}>
<button
className="fb-button green pull-right"
style={buttonStylesUniqueToOnlyThisPage}>
{t("Reset")}
</button>
</Col>
</Row>
</form>
</WidgetBody>
</Widget>
</Col>
</Row>
</div>
return <div className="static-page">
<div className="all-content-wrapper">
<h1 className="text-center">
{t("Reset your password")}
</h1>
<br />
<Row>
<Col xs={12} sm={6} className="col-sm-push-3">
<Widget>
<WidgetHeader title={"Reset Password"} />
<WidgetBody>
<form onSubmit={this.submit.bind(this)}>
<label>
{t("New Password")}
</label>
<input
type="password"
onChange={this.set("password").bind(this)} />
<label>
{t("Confirm New Password")}
</label>
<input
type="password"
onChange={this.set("passwordConfirmation").bind(this)} />
<Row>
<Col xs={12}>
<button
className="fb-button green pull-right"
style={buttonStylesUniqueToOnlyThisPage}>
{t("Reset")}
</button>
</Col>
</Row>
</form>
</WidgetBody>
</Widget>
</Col>
</Row>
</div>
);
</div>;
}
}

Some files were not shown because too many files have changed in this diff Show More