Merge branch 'staging' of https://github.com/FarmBot/Farmbot-Web-App into plant_stages

pull/697/head
Rick Carlino 2018-03-05 11:16:04 -06:00
commit b259970e98
33 changed files with 1048 additions and 1095 deletions

View File

@ -63,7 +63,7 @@ FarmBot::Application.routes.draw do
get "/tos_update" => "dashboard#tos_update", as: :tos_update get "/tos_update" => "dashboard#tos_update", as: :tos_update
post "/csp_reports" => "dashboard#csp_reports", as: :csp_report post "/csp_reports" => "dashboard#csp_reports", as: :csp_report
get "/password_reset/:token" => "dashboard#password_reset", as: :password_reset get "/password_reset/*token" => "dashboard#password_reset", as: :password_reset
get "/verify/:token" => "dashboard#verify", as: :verify_user get "/verify/:token" => "dashboard#verify", as: :verify_user
match "/app/*path", to: "dashboard#main_app", via: :all, constraints: { format: "html" } match "/app/*path", to: "dashboard#main_app", via: :all, constraints: { format: "html" }

View File

@ -93,7 +93,7 @@
"webpack-uglify-js-plugin": "^1.1.9", "webpack-uglify-js-plugin": "^1.1.9",
"weinre": "^2.0.0-pre-I0Z7U9OV", "weinre": "^2.0.0-pre-I0Z7U9OV",
"which": "^1.3.0", "which": "^1.3.0",
"yarn": "^1.2.1" "yarn": "^1.5.1"
}, },
"devDependencies": { "devDependencies": {
"jscpd": "^0.6.15", "jscpd": "^0.6.15",

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,44 @@
jest.mock("fastclick", () => ({ attach: jest.fn() }));
import {
topLevelRoutes,
designerRoutes,
maybeReplaceDesignerModules
} from "../route_config";
import { RouterState, RedirectFunction } from "react-router";
async function makeSureTheyAreRoutes(input: typeof topLevelRoutes.childRoutes) {
const cb = jest.fn();
await Promise.all(input.map(route => route.getComponent(undefined, cb)));
expect(cb).toHaveBeenCalled();
expect(cb).toHaveBeenCalledTimes(input.length);
cb.mock.calls.map(x => expect(!!x[1]).toBeTruthy());
}
describe("top level routes", () => {
it("generates all of them",
() => makeSureTheyAreRoutes(topLevelRoutes.childRoutes));
});
describe("designer routes", () => {
it("generates all of them",
() => makeSureTheyAreRoutes(designerRoutes.childRoutes));
});
describe("maybeReplaceDesignerModules", () => {
it("does replace the route", () => {
const pathname = "/app/designer";
const next = { location: { pathname } } as RouterState;
const replace = jest.fn() as RedirectFunction;
maybeReplaceDesignerModules(next, replace);
expect(replace).toHaveBeenCalledWith(`${pathname}/plants`);
});
it("does not replace the route", () => {
const pathname = "/app/nope";
const next = { location: { pathname } } as RouterState;
const replace = jest.fn() as RedirectFunction;
maybeReplaceDesignerModules(next, replace);
expect(replace).not.toHaveBeenCalled();
});
});

View File

@ -10,7 +10,7 @@ import {
import { SpecialStatus } from "../../resources/tagged_resources"; import { SpecialStatus } from "../../resources/tagged_resources";
import Axios from "axios"; import Axios from "axios";
import { API } from "../../api/index"; import { API } from "../../api/index";
import { prettyPrintApiErrors } from "../../util"; import { prettyPrintApiErrors, equals } from "../../util";
import { success, error } from "farmbot-toastr/dist"; import { success, error } from "farmbot-toastr/dist";
interface PasswordForm { interface PasswordForm {
@ -19,26 +19,18 @@ interface PasswordForm {
password: string; password: string;
} }
interface ChangePWState { interface ChangePWState { status: SpecialStatus; form: PasswordForm }
status: SpecialStatus;
form: PasswordForm const EMPTY_FORM =
} ({ new_password: "", new_password_confirmation: "", password: "" });
const EMPTY_FORM = {
new_password: "",
new_password_confirmation: "",
password: ""
};
export class ChangePassword extends React.Component<{}, ChangePWState> { export class ChangePassword extends React.Component<{}, ChangePWState> {
state: ChangePWState = { state: ChangePWState = { status: SpecialStatus.SAVED, form: EMPTY_FORM };
status: SpecialStatus.SAVED,
form: EMPTY_FORM
};
/** Set the `status` flag to `undefined`, but only if the form is empty. /** Set the `status` flag to `undefined`, but only if the form is empty.
* Useful when the user manually clears the form. */ * Useful when the user manually clears the form. */
maybeClearForm = () => wowFixMe(EMPTY_FORM, this.state.form) ? maybeClearForm =
this.clearForm() : false; () => equals(EMPTY_FORM, this.state.form) ? this.clearForm() : false;
clearForm = () => this.setState({ status: SpecialStatus.SAVED, form: EMPTY_FORM }); clearForm = () => this.setState({ status: SpecialStatus.SAVED, form: EMPTY_FORM });
@ -103,9 +95,3 @@ export class ChangePassword extends React.Component<{}, ChangePWState> {
</Widget>; </Widget>;
} }
} }
// TODO: Why does Object.is() not work when comparing EMPTY_FORM to
// this.state.form? Using this in the meantime. PRs and feedback welcome.
function wowFixMe<T>(l: T, r: T): boolean {
return (JSON.stringify(l) === JSON.stringify(r));
}

View File

@ -81,7 +81,7 @@ const MUST_LOAD: ResourceName[] = [
export class App extends React.Component<AppProps, {}> { export class App extends React.Component<AppProps, {}> {
componentDidCatch(x: Error, y: React.ErrorInfo) { catchErrors(x, y); } componentDidCatch(x: Error, y: React.ErrorInfo) { catchErrors(x, y); }
get isLoaded() { private get isLoaded() {
return (MUST_LOAD.length === return (MUST_LOAD.length ===
_.intersection(this.props.loaded, MUST_LOAD).length); _.intersection(this.props.loaded, MUST_LOAD).length);
} }

View File

@ -0,0 +1,48 @@
import { validBotLocationData } from "../util";
import { JogMovementControlsProps } from "./interfaces";
const _ = (nr_steps: number | undefined, steps_mm: number | undefined) => {
return (nr_steps || 0) / (steps_mm || 1);
};
function calculateAxialLengths(props: JogMovementControlsProps) {
const mp = props.bot.hardware.mcu_params;
return {
x: _(mp.movement_axis_nr_steps_x, mp.movement_step_per_mm_x),
y: _(mp.movement_axis_nr_steps_y, mp.movement_step_per_mm_y),
z: _(mp.movement_axis_nr_steps_z, mp.movement_step_per_mm_z),
};
}
export function buildDirectionProps(props: JogMovementControlsProps) {
const { location_data, mcu_params } = props.bot.hardware;
const botLocationData = validBotLocationData(location_data);
const lengths = calculateAxialLengths(props);
return {
x: {
isInverted: props.x_axis_inverted,
stopAtHome: !!mcu_params.movement_stop_at_home_x,
stopAtMax: !!mcu_params.movement_stop_at_max_x,
axisLength: lengths.x,
negativeOnly: !!mcu_params.movement_home_up_x,
position: botLocationData.position.x
},
y: {
isInverted: props.y_axis_inverted,
stopAtHome: !!mcu_params.movement_stop_at_home_y,
stopAtMax: !!mcu_params.movement_stop_at_max_y,
axisLength: lengths.y,
negativeOnly: !!mcu_params.movement_home_up_y,
position: botLocationData.position.y
},
z: {
isInverted: props.z_axis_inverted,
stopAtHome: !!mcu_params.movement_stop_at_home_z,
stopAtMax: !!mcu_params.movement_stop_at_max_z,
axisLength: lengths.z,
negativeOnly: !!mcu_params.movement_home_up_z,
position: botLocationData.position.z
},
};
}

View File

@ -3,116 +3,84 @@ import { DirectionButton } from "./direction_button";
import { homeAll } from "../devices/actions"; import { homeAll } from "../devices/actions";
import { JogMovementControlsProps } from "./interfaces"; import { JogMovementControlsProps } from "./interfaces";
import { getDevice } from "../device"; import { getDevice } from "../device";
import { validBotLocationData } from "../util"; import { buildDirectionProps } from "./direction_axes_props";
export class JogButtons extends React.Component<JogMovementControlsProps, {}> { export function JogButtons(props: JogMovementControlsProps) {
render() { const directionAxesProps = buildDirectionProps(props);
const { location_data, mcu_params } = this.props.bot.hardware; return <table className="jog-table" style={{ border: 0 }}>
const botLocationData = validBotLocationData(location_data); <tbody>
const directionAxesProps = { <tr>
x: { <td>
isInverted: this.props.x_axis_inverted, <button
stopAtHome: !!mcu_params.movement_stop_at_home_x, className="i fa fa-camera arrow-button fb-button"
stopAtMax: !!mcu_params.movement_stop_at_max_x, onClick={() => getDevice().takePhoto()} />
axisLength: (mcu_params.movement_axis_nr_steps_x || 0) </td>
/ (mcu_params.movement_step_per_mm_x || 1), <td />
negativeOnly: !!mcu_params.movement_home_up_x, <td />
position: botLocationData.position.x <td>
}, <DirectionButton
y: { axis="y"
isInverted: this.props.y_axis_inverted, direction="up"
stopAtHome: !!mcu_params.movement_stop_at_home_y, directionAxisProps={directionAxesProps.y}
stopAtMax: !!mcu_params.movement_stop_at_max_y, steps={props.bot.stepSize || 1000}
axisLength: (mcu_params.movement_axis_nr_steps_y || 0) disabled={props.disabled} />
/ (mcu_params.movement_step_per_mm_y || 1), </td>
negativeOnly: !!mcu_params.movement_home_up_y, <td />
position: botLocationData.position.y <td />
}, <td>
z: { <DirectionButton
isInverted: this.props.z_axis_inverted, axis="z"
stopAtHome: !!mcu_params.movement_stop_at_home_z, direction="up"
stopAtMax: !!mcu_params.movement_stop_at_max_z, directionAxisProps={directionAxesProps.z}
axisLength: (mcu_params.movement_axis_nr_steps_z || 0) steps={props.bot.stepSize || 1000}
/ (mcu_params.movement_step_per_mm_z || 1), disabled={props.disabled} />
negativeOnly: !!mcu_params.movement_home_up_z, </td>
position: botLocationData.position.z </tr>
}, <tr>
}; <td>
return <table className="jog-table" style={{ border: 0 }}> <button
<tbody> className="i fa fa-home arrow-button fb-button"
<tr> onClick={() => homeAll(100)}
<td> disabled={props.disabled || false} />
<button </td>
className="i fa fa-camera arrow-button fb-button" <td />
onClick={() => getDevice().takePhoto()} /> <td>
</td> <DirectionButton
<td /> axis="x"
<td /> direction="left"
<td> directionAxisProps={directionAxesProps.x}
<DirectionButton steps={props.bot.stepSize || 1000}
axis="y" disabled={props.disabled} />
direction="up" </td>
directionAxisProps={directionAxesProps.y} <td>
steps={this.props.bot.stepSize || 1000} <DirectionButton
disabled={this.props.disabled} /> axis="y"
</td> direction="down"
<td /> directionAxisProps={directionAxesProps.y}
<td /> steps={props.bot.stepSize || 1000}
<td> disabled={props.disabled} />
<DirectionButton </td>
axis="z" <td>
direction="up" <DirectionButton
directionAxisProps={directionAxesProps.z} axis="x"
steps={this.props.bot.stepSize || 1000} direction="right"
disabled={this.props.disabled} /> directionAxisProps={directionAxesProps.x}
</td> steps={props.bot.stepSize || 1000}
</tr> disabled={props.disabled} />
<tr> </td>
<td> <td />
<button <td>
className="i fa fa-home arrow-button fb-button" <DirectionButton
onClick={() => homeAll(100)} axis="z"
disabled={this.props.disabled || false} /> direction="down"
</td> directionAxisProps={directionAxesProps.z}
<td /> steps={props.bot.stepSize || 1000}
<td> disabled={props.disabled} />
<DirectionButton </td>
axis="x" </tr>
direction="left" <tr>
directionAxisProps={directionAxesProps.x} <td />
steps={this.props.bot.stepSize || 1000} </tr>
disabled={this.props.disabled} /> </tbody>
</td> </table>;
<td>
<DirectionButton
axis="y"
direction="down"
directionAxisProps={directionAxesProps.y}
steps={this.props.bot.stepSize || 1000}
disabled={this.props.disabled} />
</td>
<td>
<DirectionButton
axis="x"
direction="right"
directionAxisProps={directionAxesProps.x}
steps={this.props.bot.stepSize || 1000}
disabled={this.props.disabled} />
</td>
<td />
<td>
<DirectionButton
axis="z"
direction="down"
directionAxisProps={directionAxesProps.z}
steps={this.props.bot.stepSize || 1000}
disabled={this.props.disabled} />
</td>
</tr>
<tr>
<td />
</tr>
</tbody>
</table>;
}
} }

View File

@ -48,7 +48,7 @@ export class ToggleButton extends React.Component<ToggleButtonProps, {}> {
disabled={!!this.props.disabled} disabled={!!this.props.disabled}
className={this.css() + addCss} className={this.css() + addCss}
onClick={cb}> onClick={cb}>
{this.caption()} {t(this.caption())}
</button>; </button>;
} }
} }

View File

@ -5,12 +5,12 @@ import { Xyz, BotPosition } from "./devices/interfaces";
import { McuParams } from "farmbot"; import { McuParams } from "farmbot";
import { getDevice } from "./device"; import { getDevice } from "./device";
export interface State { interface State {
isOpen: boolean; isOpen: boolean;
stepSize: number; stepSize: number;
} }
export interface Props { interface Props {
dispatch: Function; dispatch: Function;
axisInversion: Record<Xyz, boolean>; axisInversion: Record<Xyz, boolean>;
botPosition: BotPosition; botPosition: BotPosition;
@ -24,7 +24,7 @@ export class ControlsPopup extends React.Component<Props, Partial<State>> {
stepSize: 100 stepSize: 100
}; };
toggle = (property: keyof State) => () => private toggle = (property: keyof State) => () =>
this.setState({ [property]: !this.state[property] }); this.setState({ [property]: !this.state[property] });
public render() { public render() {

View File

@ -46,8 +46,7 @@ export class LastSeen extends React.Component<LastSeenProps, {}> {
}; };
return t(text, data); return t(text, data);
} else { } else {
return t(" The device has never been seen. Most likely, " + return t("The device has never been seen. Most likely, there is a network connectivity issue on the device's end.");
"there is a network connectivity issue on the device's end.");
} }
} }

View File

@ -31,8 +31,8 @@ export class HardwareSettings extends
<SaveBtn <SaveBtn
status={bot.isUpdating ? SpecialStatus.SAVING : SpecialStatus.SAVED} status={bot.isUpdating ? SpecialStatus.SAVING : SpecialStatus.SAVED}
dirtyText={" "} dirtyText={" "}
savingText={"Updating..."} savingText={t("Updating...")}
savedText={"saved"} savedText={t("saved")}
hidden={false} /> hidden={false} />
</MustBeOnline> </MustBeOnline>
</WidgetHeader> </WidgetHeader>

View File

@ -8,9 +8,11 @@ jest.mock("../../../../device", () => ({
import * as React from "react"; import * as React from "react";
import { MotorsProps } from "../../interfaces"; import { MotorsProps } from "../../interfaces";
import { bot } from "../../../../__test_support__/fake_state/bot"; import { bot } from "../../../../__test_support__/fake_state/bot";
import { Motors, StepsPerMmSettings } from "../motors"; import { Motors } from "../motors";
import { render, shallow, mount } from "enzyme"; import { render, shallow, mount } from "enzyme";
import { McuParamName } from "farmbot"; import { McuParamName } from "farmbot";
import { StepsPerMmSettings } from "../steps_per_mm_settings";
import { NumericMCUInputGroup } from "../../numeric_mcu_input_group";
describe("<Motors/>", () => { describe("<Motors/>", () => {
beforeEach(function () { beforeEach(function () {
@ -28,7 +30,7 @@ describe("<Motors/>", () => {
}; };
it("renders the base case", () => { it("renders the base case", () => {
const el = render(<Motors {...fakeProps() } />); const el = render(<Motors {...fakeProps()} />);
const txt = el.text(); const txt = el.text();
[ // Not a whole lot to test here.... [ // Not a whole lot to test here....
"Enable 2nd X Motor", "Enable 2nd X Motor",
@ -88,12 +90,11 @@ describe("<StepsPerMmSettings/>", () => {
expect(firstInputProps.setting).toBe("steps_per_mm_x"); expect(firstInputProps.setting).toBe("steps_per_mm_x");
}); });
it("renders mcu settings", () => { fit("renders mcu settings", () => {
const p = fakeProps(); const p = fakeProps();
p.bot.hardware.informational_settings.firmware_version = "5.0.5R"; p.bot.hardware.informational_settings.firmware_version = "5.0.5R";
const wrapper = shallow(<StepsPerMmSettings {...p} />); const wrapper = shallow(<StepsPerMmSettings {...p} />);
const firstInputProps = wrapper.find("NumericMCUInputGroup") const firstInputProps = wrapper.find(NumericMCUInputGroup).first().props();
.first().props();
expect(firstInputProps.x).toBe("movement_step_per_mm_x"); expect(firstInputProps.x).toBe("movement_step_per_mm_x");
}); });
}); });

View File

@ -6,55 +6,13 @@ import { SpacePanelToolTip } from "../space_panel_tool_tip";
import { ToggleButton } from "../../../controls/toggle_button"; import { ToggleButton } from "../../../controls/toggle_button";
import { settingToggle } from "../../actions"; import { settingToggle } from "../../actions";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { BotConfigInputBox } from "../bot_config_input_box";
import { MotorsProps } from "../interfaces"; import { MotorsProps } from "../interfaces";
import { Row, Col } from "../../../ui/index"; import { Row, Col } from "../../../ui/index";
import { Header } from "./header"; import { Header } from "./header";
import { Collapse } from "@blueprintjs/core"; import { Collapse } from "@blueprintjs/core";
import { McuInputBox } from "../mcu_input_box"; import { McuInputBox } from "../mcu_input_box";
import { minFwVersionCheck } from "../../../util"; import { minFwVersionCheck } from "../../../util";
import { StepsPerMmSettings } from "./steps_per_mm_settings";
export function StepsPerMmSettings(props: MotorsProps) {
const { dispatch, bot, sourceFbosConfig } = props;
const { firmware_version } = bot.hardware.informational_settings;
if (minFwVersionCheck(firmware_version, "5.0.5")) {
return <NumericMCUInputGroup
name={t("Steps per MM")}
tooltip={ToolTips.STEPS_PER_MM}
x={"movement_step_per_mm_x"}
y={"movement_step_per_mm_y"}
z={"movement_step_per_mm_z"}
bot={bot}
dispatch={dispatch} />;
} else {
return <Row>
<Col xs={6}>
<label>
{t("Steps per MM")}
</label>
<SpacePanelToolTip tooltip={ToolTips.STEPS_PER_MM} />
</Col>
<Col xs={2}>
<BotConfigInputBox
setting="steps_per_mm_x"
sourceFbosConfig={sourceFbosConfig}
dispatch={dispatch} />
</Col>
<Col xs={2}>
<BotConfigInputBox
setting="steps_per_mm_y"
sourceFbosConfig={sourceFbosConfig}
dispatch={dispatch} />
</Col>
<Col xs={2}>
<BotConfigInputBox
setting="steps_per_mm_z"
sourceFbosConfig={sourceFbosConfig}
dispatch={dispatch} />
</Col>
</Row>;
}
}
export function Motors(props: MotorsProps) { export function Motors(props: MotorsProps) {
const { dispatch, bot, sourceFbosConfig } = props; const { dispatch, bot, sourceFbosConfig } = props;

View File

@ -0,0 +1,57 @@
import * as React from "react";
import { BotConfigInputBox } from "../bot_config_input_box";
import { MotorsProps } from "../interfaces";
import { minFwVersionCheck } from "../../../util";
import { ToolTips } from "../../../constants";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { Row, Col } from "../../../ui";
import { SpacePanelToolTip } from "../space_panel_tool_tip";
import { t } from "i18next";
function LegacyStepsPerMm(props: MotorsProps) {
const { dispatch, sourceFbosConfig } = props;
return <Row>
<Col xs={6}>
<label>
{t("Steps per MM")}
</label>
<SpacePanelToolTip tooltip={ToolTips.STEPS_PER_MM} />
</Col>
<Col xs={2}>
<BotConfigInputBox
setting="steps_per_mm_x"
sourceFbosConfig={sourceFbosConfig}
dispatch={dispatch} />
</Col>
<Col xs={2}>
<BotConfigInputBox
setting="steps_per_mm_y"
sourceFbosConfig={sourceFbosConfig}
dispatch={dispatch} />
</Col>
<Col xs={2}>
<BotConfigInputBox
setting="steps_per_mm_z"
sourceFbosConfig={sourceFbosConfig}
dispatch={dispatch} />
</Col>
</Row>;
}
export function StepsPerMmSettings(props: MotorsProps) {
const { dispatch, bot } = props;
const { firmware_version } = bot.hardware.informational_settings;
if (minFwVersionCheck(firmware_version, "5.0.5")) {
return <NumericMCUInputGroup
name={t("Steps per MM")}
tooltip={ToolTips.STEPS_PER_MM}
x={"movement_step_per_mm_x"}
y={"movement_step_per_mm_y"}
z={"movement_step_per_mm_z"}
bot={bot}
dispatch={dispatch} />;
} else {
return <LegacyStepsPerMm {...props} />;
}
}

View File

@ -18,7 +18,7 @@ export function Diagnosis(props: DiagnosisProps) {
const diagnosisStatus = const diagnosisStatus =
props.userMQTT && props.botAPI && props.botMQTT && props.botFirmware; props.userMQTT && props.botAPI && props.botMQTT && props.botFirmware;
const diagnosisColor = diagnosisStatus ? "green" : "red"; const diagnosisColor = diagnosisStatus ? "green" : "red";
const title = diagnosisStatus ? "Ok" : "Error"; const title = diagnosisStatus ? t("Ok") : t("Error");
return <div> return <div>
<div className={"connectivity-diagnosis"}> <div className={"connectivity-diagnosis"}>
<h4>{t("Diagnosis")}</h4> <h4>{t("Diagnosis")}</h4>

View File

@ -247,17 +247,17 @@ describe("mapResourcesToCalendar(): regimen farm events", () => {
} }
]; ];
it("returns calendar rows", () => { fit("returns calendar rows", () => {
const testTime = moment("2017-12-15T01:00:00.000Z"); const testTime = moment("2017-12-15T01:00:00.000Z");
const calendar = mapResourcesToCalendar( const calendar =
fakeRegFEResources().index, testTime); mapResourcesToCalendar(fakeRegFEResources().index, testTime);
expect(calendar.getAll()).toEqual(fakeRegimenFE); expect(calendar.getAll()).toEqual(fakeRegimenFE);
}); });
it("doesn't return calendar row after event is over", () => { fit("doesn't return calendar row after event is over", () => {
const testTime = moment("2017-12-27T01:00:00.000Z"); const testTime = moment("2017-12-27T01:00:00.000Z");
const calendar = mapResourcesToCalendar( const calendar =
fakeRegFEResources().index, testTime); mapResourcesToCalendar(fakeRegFEResources().index, testTime);
expect(calendar.getAll()).toEqual([]); expect(calendar.getAll()).toEqual([]);
}); });
}); });

View File

@ -128,7 +128,6 @@ export function mapStateToPropsAddEdit(props: Everything): AddEditFarmEventProps
const regimensById = indexRegimenById(props.resources.index); const regimensById = indexRegimenById(props.resources.index);
const sequencesById = indexSequenceById(props.resources.index); const sequencesById = indexSequenceById(props.resources.index);
const farmEventsById = indexFarmEventById(props.resources.index); const farmEventsById = indexFarmEventById(props.resources.index);
const farmEvents = selectAllFarmEvents(props.resources.index); const farmEvents = selectAllFarmEvents(props.resources.index);
const getFarmEvent = (): TaggedFarmEvent | undefined => { const getFarmEvent = (): TaggedFarmEvent | undefined => {

View File

@ -1,7 +1,7 @@
import { browserHistory } from "react-router"; import { browserHistory } from "react-router";
export let history = browserHistory; export let history = browserHistory;
export let push = (url: string) => history.push(url); export let push = (url: string) => history.push(url);
export let pathname = history.getCurrentLocation().pathname;
export function getPathArray() { export function getPathArray() {
return history.getCurrentLocation().pathname.split("/"); return history.getCurrentLocation().pathname.split("/");
} }

View File

@ -61,10 +61,10 @@ export class HotKeys extends React.Component<Props, Partial<State>> {
</div>; </div>;
} }
toggle = (property: keyof State) => () => private toggle = (property: keyof State) => () =>
this.setState({ [property]: !this.state[property] }); this.setState({ [property]: !this.state[property] });
hotkeys(dispatch: Function, slug: string) { private hotkeys(dispatch: Function, slug: string) {
const idx = _.findIndex(links, { slug }); const idx = _.findIndex(links, { slug });
const right = "/app/" + (links[idx + 1] || links[0]).slug; const right = "/app/" + (links[idx + 1] || links[0]).slug;
const left = "/app/" + (links[idx - 1] || links[links.length - 1]).slug; const left = "/app/" + (links[idx - 1] || links[links.length - 1]).slug;
@ -103,7 +103,7 @@ export class HotKeys extends React.Component<Props, Partial<State>> {
return hotkeyMap; return hotkeyMap;
} }
renderHotkeys() { public renderHotkeys() {
const slug = getPathArray()[2]; const slug = getPathArray()[2];
return <Hotkeys> return <Hotkeys>
{ {

View File

@ -1,6 +1,6 @@
import axios from "axios"; import axios from "axios";
import { InitOptions } from "i18next"; import { InitOptions } from "i18next";
/** @public */
export function generateUrl(langCode: string) { export function generateUrl(langCode: string) {
const lang = langCode.slice(0, 2); const lang = langCode.slice(0, 2);
const url = "//" + location.host.split(":") const url = "//" + location.host.split(":")

View File

@ -12,19 +12,8 @@ export let METHOD_MAP: Dictionary<DataChangeType> = {
}; };
export let METHODS = ["post", "put", "patch", "delete"]; export let METHODS = ["post", "put", "patch", "delete"];
export let RESOURCES: ResourceName[] = [
"Point",
"Regimen",
"Peripheral",
"Log",
"Sequence",
"FarmEvent",
"Point",
"Device"
];
/** Temporary stub until auto_sync rollout. TODO: Remove */ /** Temporary stub until auto_sync rollout. TODO: Remove */
export const RESOURNCE_NAME_IN_URL = [ const RESOURNCE_NAME_IN_URL = [
"device", "device",
"farm_events", "farm_events",
"logs", "logs",

View File

@ -26,6 +26,7 @@ export function responseFulfilled(input: AxiosResponse): AxiosResponse {
return input; return input;
} }
let ONLY_ONCE = true;
export function responseRejected(x: SafeError | undefined) { export function responseRejected(x: SafeError | undefined) {
if (x && isSafeError(x)) { if (x && isSafeError(x)) {
dispatchNetworkUp("user.api"); dispatchNetworkUp("user.api");
@ -50,7 +51,8 @@ export function responseRejected(x: SafeError | undefined) {
break; break;
case 451: case 451:
// DONT REFACTOR: I want to use alert() because it's blocking. // DONT REFACTOR: I want to use alert() because it's blocking.
alert(t(Content.TOS_UPDATE)); ONLY_ONCE && alert(t(Content.TOS_UPDATE));
ONLY_ONCE = false;
window.location.href = "/tos_update"; window.location.href = "/tos_update";
break; break;
} }

View File

@ -10,17 +10,6 @@ import { RestResources } from "./resources/interfaces";
in the UI. Only certain colors are valid. */ in the UI. Only certain colors are valid. */
export type Color = FarmBotJsColor; export type Color = FarmBotJsColor;
export interface SelectOptionsParams {
label: string;
value: string | number | undefined;
disabled?: boolean;
field?: string;
type?: string;
x?: number;
y?: number;
z?: number;
}
export interface PinBinding { export interface PinBinding {
id?: number; id?: number;
sequence_id: number; sequence_id: number;
@ -144,9 +133,3 @@ export type Point =
| PlantPointer; | PlantPointer;
export type PointerTypeName = Point["pointer_type"]; export type PointerTypeName = Point["pointer_type"];
export const POINTER_NAMES: Readonly<PointerTypeName>[] = [
"Plant",
"GenericPointer",
"ToolSlot"
];

View File

@ -1,4 +1,5 @@
import * as React from "react"; import * as React from "react";
import { t } from "i18next";
import { Session } from "./session"; import { Session } from "./session";
import { BooleanSetting } from "./session_keys"; import { BooleanSetting } from "./session_keys";
@ -73,7 +74,7 @@ export function LoadingPlant() {
fontSize={35} fontSize={35}
textAnchor="middle" textAnchor="middle"
fill="#434343"> fill="#434343">
Loading... {t("Loading...")}
</text> </text>
</svg> </svg>
</div>; </div>;

View File

@ -30,7 +30,7 @@ export const LogsFilterMenu = (props: LogsFilterMenuProps) => {
return <fieldset key={logType}> return <fieldset key={logType}>
<label> <label>
<div className={`saucer ${logType}`} /> <div className={`saucer ${logType}`} />
{_.startCase(logType)} {t(_.startCase(logType))}
</label> </label>
<button <button
className={"fb-button fb-toggle-button " + btnColor(logType)} className={"fb-button fb-toggle-button " + btnColor(logType)}

View File

@ -0,0 +1,42 @@
import { TaggedResource } from "./tagged_resources";
import { CowardlyDictionary } from "../util";
import { ResourceIndex } from "./interfaces";
import { assertUuid } from "./selectors";
interface IndexLookupDictionary<T extends TaggedResource>
extends CowardlyDictionary<T> { }
interface Indexer<T extends TaggedResource> {
(index: ResourceIndex): IndexLookupDictionary<T>;
}
interface MapperFn<T extends TaggedResource> {
(item: T): T | undefined;
}
/** Build a function,
* that returns a function,
* that returns a dictionary,
* that contains TaggedResource of kind T
* that uses the resource's id as the dictionary key.
* */
export const buildIndexer =
<T extends TaggedResource>(kind: T["kind"], mapper?: MapperFn<T>): Indexer<T> => {
return function (index: ResourceIndex, ) {
const noop: MapperFn<T> = (i) => i;
const output: CowardlyDictionary<T> = {};
const uuids = index.byKind[kind];
const m = mapper || noop;
uuids.map(uuid => {
assertUuid(kind, uuid);
const resource = index.references[uuid];
if (resource
&& (resource.kind === kind)
&& resource.body.id
&& m(resource as T)) {
output[resource.body.id] = resource as T;
}
});
return output;
};
};

View File

@ -29,10 +29,12 @@ import {
TaggedDevice, TaggedDevice,
TaggedFbosConfig, TaggedFbosConfig,
TaggedWebAppConfig, TaggedWebAppConfig,
SpecialStatus SpecialStatus,
TaggedPoint
} from "./tagged_resources"; } from "./tagged_resources";
import { CowardlyDictionary, betterCompact, sortResourcesById, bail } from "../util"; import { CowardlyDictionary, betterCompact, sortResourcesById, bail } from "../util";
import { isNumber } from "util"; import { isNumber } from "util";
import { buildIndexer } from "./selector_support";
type StringMap = CowardlyDictionary<string>; type StringMap = CowardlyDictionary<string>;
/** Similar to findId(), but does not throw exceptions. Do NOT use this method /** Similar to findId(), but does not throw exceptions. Do NOT use this method
@ -219,74 +221,17 @@ export function selectAllSequences(index: ResourceIndex) {
return findAll(index, "Sequence") as TaggedSequence[]; return findAll(index, "Sequence") as TaggedSequence[];
} }
export function indexSequenceById(index: ResourceIndex) { const mapper = (i: TaggedPoint): TaggedToolSlotPointer | undefined => {
const output: CowardlyDictionary<TaggedSequence> = {}; if (i.kind == "Point" && (i.body.pointer_type === "ToolSlot")) {
const uuids = index.byKind.Sequence; return i as TaggedToolSlotPointer;
uuids.map(uuid => { }
assertUuid("Sequence", uuid); return undefined;
const sequence = index.references[uuid]; };
if (sequence && isTaggedSequence(sequence) && sequence.body.id) { export const indexBySlotId = buildIndexer<TaggedToolSlotPointer>("Point", mapper);
output[sequence.body.id] = sequence; export const indexSequenceById = buildIndexer<TaggedSequence>("Sequence");
} export const indexRegimenById = buildIndexer<TaggedRegimen>("Regimen");
}); export const indexFarmEventById = buildIndexer<TaggedFarmEvent>("FarmEvent");
return output; export const indexByToolId = buildIndexer<TaggedTool>("Tool");
}
export function indexRegimenById(index: ResourceIndex) {
const output: CowardlyDictionary<TaggedRegimen> = {};
const uuids = index.byKind.Regimen;
uuids.map(uuid => {
assertUuid("Regimen", uuid);
const regimen = index.references[uuid];
if (regimen && isTaggedRegimen(regimen) && regimen.body.id) {
output[regimen.body.id] = regimen;
}
});
return output;
}
export function indexFarmEventById(index: ResourceIndex) {
const output: CowardlyDictionary<TaggedFarmEvent> = {};
const uuids = index.byKind.FarmEvent;
uuids.map(uuid => {
assertUuid("FarmEvent", uuid);
const farmEvent = index.references[uuid];
if (farmEvent && isTaggedFarmEvent(farmEvent) && farmEvent.body.id) {
output[farmEvent.body.id] = farmEvent;
}
});
return output;
}
export function indexByToolId(index: ResourceIndex) {
const output: CowardlyDictionary<TaggedTool> = {};
const uuids = index.byKind.Tool;
uuids.map(uuid => {
assertUuid("Tool", uuid);
const Tool = index.references[uuid];
if (Tool && isTaggedTool(Tool) && Tool.body.id) {
output[Tool.body.id] = Tool;
}
});
return output;
}
export function indexBySlotId(index: ResourceIndex) {
const output: CowardlyDictionary<TaggedToolSlotPointer> = {};
const uuids = index.byKind.Point;
uuids.map(uuid => {
assertUuid("Point", uuid);
const tool_slot = index.references[uuid];
if (tool_slot && isTaggedToolSlotPointer(tool_slot) && tool_slot.body.id) {
output[tool_slot.body.id] = tool_slot;
}
});
return output;
}
export function assertUuid(expected: ResourceName, actual: string | undefined) { export function assertUuid(expected: ResourceName, actual: string | undefined) {
if (actual && !actual.startsWith(expected)) { if (actual && !actual.startsWith(expected)) {

View File

@ -0,0 +1,80 @@
import { App } from "./app";
import { crashPage } from "./crash_page";
import { RouterState, RedirectFunction } from "react-router";
/** These methods are a way to determine how to load certain modules
* based on the device (mobile or desktop) for optimization/css purposes.
*/
export function maybeReplaceDesignerModules(next: RouterState,
replace: RedirectFunction) {
if (next.location.pathname === "/app/designer") {
replace(`${next.location.pathname}/plants`);
}
}
function page(path: string, getter: () => Promise<React.ReactType>) {
return {
path,
getComponent(_: void, cb: Function) {
const ok = (component: React.ReactType) => cb(undefined, component);
const no = (e: object) => cb(undefined, crashPage(e));
return getter().then(ok, no);
}
};
}
const controlsRoute =
page("app/controls", async () => (await import("./controls/controls")).Controls);
export const designerRoutes = {
path: "app/designer",
onEnter: maybeReplaceDesignerModules,
getComponent(_discard: void, cb: Function) {
import("./farm_designer/index")
.then(module => cb(undefined, module.FarmDesigner))
.catch((e: object) => cb(undefined, crashPage(e)));
},
childRoutes: [
page("plants",
async () => (await import("./farm_designer/plants/plant_inventory")).Plants),
page("plants/crop_search",
async () => (await import("./farm_designer/plants/crop_catalog")).CropCatalog),
page("plants/crop_search/:crop",
async () => (await import("./farm_designer/plants/crop_info")).CropInfo),
page("plants/crop_search/:crop/add",
async () => (await import("./farm_designer/plants/add_plant")).AddPlant),
page("plants/select",
async () => (await import("./farm_designer/plants/select_plants")).SelectPlants),
page("plants/move_to", async () => (await import("./farm_designer/plants/move_to")).MoveTo),
page("plants/create_point",
async () => (await import("./farm_designer/plants/create_points")).CreatePoints),
page("plants/:plant_id",
async () => (await import("./farm_designer/plants/plant_info")).PlantInfo),
page("plants/:plant_id/edit",
async () => (await import("./farm_designer/plants/edit_plant_info")).EditPlantInfo),
page("farm_events",
async () => (await import("./farm_designer/farm_events/farm_events")).FarmEvents),
page("farm_events/add",
async () => (await import("./farm_designer/farm_events/add_farm_event")).AddFarmEvent),
page("farm_events/:farm_event_id",
async () => (await import("./farm_designer/farm_events/edit_farm_event")).EditFarmEvent),
]
};
export const topLevelRoutes = {
component: App,
indexRoute: controlsRoute,
childRoutes: [
page("app/account", async () => (await import("./account/index")).Account),
controlsRoute,
page("app/device", async () => (await import("./devices/devices")).Devices),
page("app/farmware", async () => (await import("./farmware/index")).FarmwarePage),
page("app/regimens", async () => (await import("./regimens/index")).Regimens),
page("app/regimens/:regimen", async () => (await import("./regimens/index")).Regimens),
page("app/sequences", async () => (await import("./sequences/sequences")).Sequences),
page("app/sequences/:sequence", async () => (await import("./sequences/sequences")).Sequences),
page("app/tools", async () => (await import("./tools/index")).Tools),
page("app/logs", async () => (await import("./logs/index")).Logs),
page("*", async () => (await import("./404")).FourOhFour),
designerRoutes,
]
};

View File

@ -1,8 +1,7 @@
import "./css/_index.scss"; import "./css/_index.scss";
import * as React from "react"; import * as React from "react";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { Router, RedirectFunction, RouterState } from "react-router"; import { Router } from "react-router";
import { App } from "./app";
import { store as _store } from "./redux/store"; import { store as _store } from "./redux/store";
import { history } from "./history"; import { history } from "./history";
import { Store } from "./redux/interfaces"; import { Store } from "./redux/interfaces";
@ -10,27 +9,9 @@ import { ready } from "./config/actions";
import { Session } from "./session"; import { Session } from "./session";
import { attachToRoot } from "./util"; import { attachToRoot } from "./util";
import { Callback } from "i18next"; import { Callback } from "i18next";
import { crashPage } from "./crash_page"; import { topLevelRoutes } from "./route_config";
const key = "Jan 20 23:52"; interface RootComponentProps { store: Store; }
if (!localStorage[key]) {
localStorage[key] = JSON.stringify("X");
location.reload(true);
}
interface RootComponentProps {
store: Store;
}
const controlsRoute = {
path: "app/controls",
getComponent(_discard: void, cb: Function) {
import("./controls/controls")
.then((module) => cb(undefined, module.Controls))
.catch((e: object) => cb(undefined, crashPage(e)));
}
};
export const attachAppToDom: Callback = (err, t) => { export const attachAppToDom: Callback = (err, t) => {
attachToRoot(RootComponent, { store: _store }); attachToRoot(RootComponent, { store: _store });
@ -38,238 +19,6 @@ export const attachAppToDom: Callback = (err, t) => {
}; };
export class RootComponent extends React.Component<RootComponentProps, {}> { export class RootComponent extends React.Component<RootComponentProps, {}> {
requireAuth(_discard: RouterState, replace: RedirectFunction) {
const { store } = this.props;
if (Session.fetchStoredToken()) { // has a previous session in cache
if (store.getState().auth) { // Has session, logged in.
return;
} else { // Has session but not logged in (returning visitor).
store.dispatch(ready());
}
} else { // Not logged in yet.
Session.clear();
}
}
/** These methods are a way to determine how to load certain modules
* based on the device (mobile or desktop) for optimization/css purposes.
* Open to revision.
*/
maybeReplaceDesignerModules(next: RouterState, replace: RedirectFunction) {
if (next.location.pathname === "/app/designer") {
replace(`${next.location.pathname}/plants`);
}
}
/*
/app => App
/app/account => Account
/app/controls => Controls
/app/device => Devices
/app/designer?p1&p2 => FarmDesigner
/app/regimens => Regimens
/app/sequences => Sequences
/app/tools => Tools
/app/404 => 404
*/
routes = {
component: App,
indexRoute: controlsRoute,
childRoutes: [
{
path: "app/account",
getComponent(_discard: void, cb: Function) {
import("./account/index")
.then(module => cb(undefined, module.Account))
.catch((e: object) => cb(undefined, crashPage(e)));
}
},
controlsRoute,
{
path: "app/device",
getComponent(_discard: void, cb: Function) {
import("./devices/devices")
.then(module => cb(undefined, module.Devices))
.catch((e: object) => cb(undefined, crashPage(e)));
}
},
{
path: "app/farmware",
getComponent(_discard: void, cb: Function) {
import("./farmware/index")
.then(module => cb(undefined, module.FarmwarePage))
.catch((e: object) => cb(undefined, crashPage(e)));
}
},
{
path: "app/designer",
onEnter: this.maybeReplaceDesignerModules.bind(this),
getComponent(_discard: void, cb: Function) {
import("./farm_designer/index")
.then(module => cb(undefined, module.FarmDesigner))
.catch((e: object) => cb(undefined, crashPage(e)));
},
childRoutes: [
{
path: "plants",
getComponent(_discard: void, cb: Function) {
import("./farm_designer/plants/plant_inventory")
.then(module => cb(undefined, module.Plants))
.catch((e: object) => cb(undefined, crashPage(e)));
},
},
{
path: "plants/crop_search",
getComponent(_discard: void, cb: Function) {
import("./farm_designer/plants/crop_catalog")
.then(module => cb(undefined, module.CropCatalog))
.catch((e: object) => cb(undefined, crashPage(e)));
},
},
{
path: "plants/crop_search/:crop",
getComponent(_discard: void, cb: Function) {
import("./farm_designer/plants/crop_info")
.then(module => cb(undefined, module.CropInfo))
.catch((e: object) => cb(undefined, crashPage(e)));
},
},
{
path: "plants/crop_search/:crop/add",
getComponent(_discard: void, cb: Function) {
import("./farm_designer/plants/add_plant")
.then(module => cb(undefined, module.AddPlant))
.catch((e: object) => cb(undefined, crashPage(e)));
},
},
{
path: "plants/select",
getComponent(_discard: void, cb: Function) {
import("./farm_designer/plants/select_plants")
.then(module => cb(undefined, module.SelectPlants))
.catch((e: object) => cb(undefined, crashPage(e)));
},
},
{
path: "plants/move_to",
getComponent(_discard: void, cb: Function) {
import("./farm_designer/plants/move_to")
.then(module => cb(undefined, module.MoveTo))
.catch((e: object) => cb(undefined, crashPage(e)));
},
},
{
path: "plants/create_point",
getComponent(_discard: void, cb: Function) {
import("./farm_designer/plants/create_points")
.then(module => cb(undefined, module.CreatePoints))
.catch((e: object) => cb(undefined, crashPage(e)));
},
},
{
path: "plants/:plant_id",
getComponent(_discard: void, cb: Function) {
import("./farm_designer/plants/plant_info")
.then(module => cb(undefined, module.PlantInfo))
.catch((e: object) => cb(undefined, crashPage(e)));
},
},
{
path: "plants/:plant_id/edit",
getComponent(_discard: void, cb: Function) {
import("./farm_designer/plants/edit_plant_info")
.then(module => cb(undefined, module.EditPlantInfo))
.catch((e: object) => cb(undefined, crashPage(e)));
},
},
{
path: "farm_events",
getComponent(_discard: void, cb: Function) {
import("./farm_designer/farm_events/farm_events")
.then(module => cb(undefined, module.FarmEvents))
.catch((e: object) => cb(undefined, crashPage(e)));
}
},
{
path: "farm_events/add",
getComponent(_discard: void, cb: Function) {
import("./farm_designer/farm_events/add_farm_event")
.then(module => cb(undefined, module.AddFarmEvent))
.catch((e: object) => cb(undefined, crashPage(e)));
}
},
{
path: "farm_events/:farm_event_id",
getComponent(_discard: void, cb: Function) {
import("./farm_designer/farm_events/edit_farm_event")
.then(module => cb(undefined, module.EditFarmEvent))
.catch((e: object) => cb(undefined, crashPage(e)));
}
}
]
},
{
path: "app/regimens",
getComponent(_discard: void, cb: Function) {
import("./regimens/index")
.then(module => cb(undefined, module.Regimens))
.catch((e: object) => cb(undefined, crashPage(e)));
},
},
{
path: "app/regimens/:regimen",
getComponent(_discard: void, cb: Function) {
import("./regimens/index")
.then(module => cb(undefined, module.Regimens))
.catch((e: object) => cb(undefined, crashPage(e)));
}
},
{
path: "app/sequences",
getComponent(_discard: void, cb: Function) {
import("./sequences/sequences")
.then(module => {
cb(undefined, module.Sequences);
})
.catch((e: object) => cb(undefined, crashPage(e)));
},
},
{
path: "app/sequences/:sequence",
getComponent(_discard: void, cb: Function) {
import("./sequences/sequences")
.then(module => cb(undefined, module.Sequences))
.catch((e: object) => cb(undefined, crashPage(e)));
},
},
{
path: "app/tools",
getComponent(_discard: void, cb: Function) {
import("./tools/index")
.then(module => cb(undefined, module.Tools))
.catch((e: object) => cb(undefined, crashPage(e)));
}
},
{
path: "app/logs",
getComponent(_discard: void, cb: Function) {
import("./logs/index")
.then(module => cb(undefined, module.Logs))
.catch((e: object) => cb(undefined, crashPage(e)));
}
},
{
path: "*",
getComponent(_discard: void, cb: Function) {
import("./404")
.then(module => cb(undefined, module.FourOhFour))
.catch((e: object) => cb(undefined, crashPage(e)));
}
}
]
};
render() { render() {
// ==== TEMPORARY HACK. TODO: Add a before hook, if such a thing exists in // ==== TEMPORARY HACK. TODO: Add a before hook, if such a thing exists in
// React Router. Or switch routing libs. // React Router. Or switch routing libs.
@ -281,7 +30,7 @@ export class RootComponent extends React.Component<RootComponentProps, {}> {
// ==== END HACK ==== // ==== END HACK ====
return <Provider store={_store}> return <Provider store={_store}>
<Router history={history}> <Router history={history}>
{this.routes} {topLevelRoutes}
</Router> </Router>
</Provider>; </Provider>;
} }

View File

@ -32,7 +32,7 @@ export class Sequences extends React.Component<Props, {}> {
<h3> <h3>
<i>{t("Sequence Editor")}</i> <i>{t("Sequence Editor")}</i>
</h3> </h3>
<ToolTip helpText={ToolTips.SEQUENCE_EDITOR} /> <ToolTip helpText={t(ToolTips.SEQUENCE_EDITOR)} />
<SequenceEditorMiddle <SequenceEditorMiddle
syncStatus={this.props.syncStatus} syncStatus={this.props.syncStatus}
dispatch={this.props.dispatch} dispatch={this.props.dispatch}

View File

@ -39,7 +39,7 @@ export function stopIE() {
/** Dynamically change the meta title of the page. */ /** Dynamically change the meta title of the page. */
export function updatePageInfo(pageName: string) { export function updatePageInfo(pageName: string) {
if (pageName === "designer") { pageName = "Farm Designer"; } if (pageName === "designer") { pageName = "Farm Designer"; }
document.title = capitalize(pageName); document.title = t(capitalize(pageName));
// Possibly add meta "content" here dynamically as well // Possibly add meta "content" here dynamically as well
} }

View File

@ -7466,6 +7466,6 @@ yargs@~3.10.0:
decamelize "^1.0.0" decamelize "^1.0.0"
window-size "0.1.0" window-size "0.1.0"
yarn@^1.2.1: yarn@^1.5.1:
version "1.3.2" version "1.5.1"
resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.3.2.tgz#5939762581b5b4ddcd3418c0f6be42df3aee195f" resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.5.1.tgz#e8680360e832ac89521eb80dad3a7bc27a40bab4"