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
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
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",
"weinre": "^2.0.0-pre-I0Z7U9OV",
"which": "^1.3.0",
"yarn": "^1.2.1"
"yarn": "^1.5.1"
},
"devDependencies": {
"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 Axios from "axios";
import { API } from "../../api/index";
import { prettyPrintApiErrors } from "../../util";
import { prettyPrintApiErrors, equals } from "../../util";
import { success, error } from "farmbot-toastr/dist";
interface PasswordForm {
@ -19,26 +19,18 @@ interface PasswordForm {
password: string;
}
interface ChangePWState {
status: SpecialStatus;
form: PasswordForm
}
const EMPTY_FORM = {
new_password: "",
new_password_confirmation: "",
password: ""
};
interface ChangePWState { status: SpecialStatus; form: PasswordForm }
const EMPTY_FORM =
({ new_password: "", new_password_confirmation: "", password: "" });
export class ChangePassword extends React.Component<{}, ChangePWState> {
state: ChangePWState = {
status: SpecialStatus.SAVED,
form: EMPTY_FORM
};
state: ChangePWState = { status: SpecialStatus.SAVED, form: EMPTY_FORM };
/** Set the `status` flag to `undefined`, but only if the form is empty.
* Useful when the user manually clears the form. */
maybeClearForm = () => wowFixMe(EMPTY_FORM, this.state.form) ?
this.clearForm() : false;
maybeClearForm =
() => equals(EMPTY_FORM, this.state.form) ? this.clearForm() : false;
clearForm = () => this.setState({ status: SpecialStatus.SAVED, form: EMPTY_FORM });
@ -103,9 +95,3 @@ export class ChangePassword extends React.Component<{}, ChangePWState> {
</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, {}> {
componentDidCatch(x: Error, y: React.ErrorInfo) { catchErrors(x, y); }
get isLoaded() {
private get isLoaded() {
return (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 { JogMovementControlsProps } from "./interfaces";
import { getDevice } from "../device";
import { validBotLocationData } from "../util";
import { buildDirectionProps } from "./direction_axes_props";
export class JogButtons extends React.Component<JogMovementControlsProps, {}> {
render() {
const { location_data, mcu_params } = this.props.bot.hardware;
const botLocationData = validBotLocationData(location_data);
const directionAxesProps = {
x: {
isInverted: this.props.x_axis_inverted,
stopAtHome: !!mcu_params.movement_stop_at_home_x,
stopAtMax: !!mcu_params.movement_stop_at_max_x,
axisLength: (mcu_params.movement_axis_nr_steps_x || 0)
/ (mcu_params.movement_step_per_mm_x || 1),
negativeOnly: !!mcu_params.movement_home_up_x,
position: botLocationData.position.x
},
y: {
isInverted: this.props.y_axis_inverted,
stopAtHome: !!mcu_params.movement_stop_at_home_y,
stopAtMax: !!mcu_params.movement_stop_at_max_y,
axisLength: (mcu_params.movement_axis_nr_steps_y || 0)
/ (mcu_params.movement_step_per_mm_y || 1),
negativeOnly: !!mcu_params.movement_home_up_y,
position: botLocationData.position.y
},
z: {
isInverted: this.props.z_axis_inverted,
stopAtHome: !!mcu_params.movement_stop_at_home_z,
stopAtMax: !!mcu_params.movement_stop_at_max_z,
axisLength: (mcu_params.movement_axis_nr_steps_z || 0)
/ (mcu_params.movement_step_per_mm_z || 1),
negativeOnly: !!mcu_params.movement_home_up_z,
position: botLocationData.position.z
},
};
return <table className="jog-table" style={{ border: 0 }}>
<tbody>
<tr>
<td>
<button
className="i fa fa-camera arrow-button fb-button"
onClick={() => getDevice().takePhoto()} />
</td>
<td />
<td />
<td>
<DirectionButton
axis="y"
direction="up"
directionAxisProps={directionAxesProps.y}
steps={this.props.bot.stepSize || 1000}
disabled={this.props.disabled} />
</td>
<td />
<td />
<td>
<DirectionButton
axis="z"
direction="up"
directionAxisProps={directionAxesProps.z}
steps={this.props.bot.stepSize || 1000}
disabled={this.props.disabled} />
</td>
</tr>
<tr>
<td>
<button
className="i fa fa-home arrow-button fb-button"
onClick={() => homeAll(100)}
disabled={this.props.disabled || false} />
</td>
<td />
<td>
<DirectionButton
axis="x"
direction="left"
directionAxisProps={directionAxesProps.x}
steps={this.props.bot.stepSize || 1000}
disabled={this.props.disabled} />
</td>
<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>;
}
export function JogButtons(props: JogMovementControlsProps) {
const directionAxesProps = buildDirectionProps(props);
return <table className="jog-table" style={{ border: 0 }}>
<tbody>
<tr>
<td>
<button
className="i fa fa-camera arrow-button fb-button"
onClick={() => getDevice().takePhoto()} />
</td>
<td />
<td />
<td>
<DirectionButton
axis="y"
direction="up"
directionAxisProps={directionAxesProps.y}
steps={props.bot.stepSize || 1000}
disabled={props.disabled} />
</td>
<td />
<td />
<td>
<DirectionButton
axis="z"
direction="up"
directionAxisProps={directionAxesProps.z}
steps={props.bot.stepSize || 1000}
disabled={props.disabled} />
</td>
</tr>
<tr>
<td>
<button
className="i fa fa-home arrow-button fb-button"
onClick={() => homeAll(100)}
disabled={props.disabled || false} />
</td>
<td />
<td>
<DirectionButton
axis="x"
direction="left"
directionAxisProps={directionAxesProps.x}
steps={props.bot.stepSize || 1000}
disabled={props.disabled} />
</td>
<td>
<DirectionButton
axis="y"
direction="down"
directionAxisProps={directionAxesProps.y}
steps={props.bot.stepSize || 1000}
disabled={props.disabled} />
</td>
<td>
<DirectionButton
axis="x"
direction="right"
directionAxisProps={directionAxesProps.x}
steps={props.bot.stepSize || 1000}
disabled={props.disabled} />
</td>
<td />
<td>
<DirectionButton
axis="z"
direction="down"
directionAxisProps={directionAxesProps.z}
steps={props.bot.stepSize || 1000}
disabled={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}
className={this.css() + addCss}
onClick={cb}>
{this.caption()}
{t(this.caption())}
</button>;
}
}

View File

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

View File

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

View File

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

View File

@ -8,9 +8,11 @@ jest.mock("../../../../device", () => ({
import * as React from "react";
import { MotorsProps } from "../../interfaces";
import { bot } from "../../../../__test_support__/fake_state/bot";
import { Motors, StepsPerMmSettings } from "../motors";
import { Motors } from "../motors";
import { render, shallow, mount } from "enzyme";
import { McuParamName } from "farmbot";
import { StepsPerMmSettings } from "../steps_per_mm_settings";
import { NumericMCUInputGroup } from "../../numeric_mcu_input_group";
describe("<Motors/>", () => {
beforeEach(function () {
@ -28,7 +30,7 @@ describe("<Motors/>", () => {
};
it("renders the base case", () => {
const el = render(<Motors {...fakeProps() } />);
const el = render(<Motors {...fakeProps()} />);
const txt = el.text();
[ // Not a whole lot to test here....
"Enable 2nd X Motor",
@ -88,12 +90,11 @@ describe("<StepsPerMmSettings/>", () => {
expect(firstInputProps.setting).toBe("steps_per_mm_x");
});
it("renders mcu settings", () => {
fit("renders mcu settings", () => {
const p = fakeProps();
p.bot.hardware.informational_settings.firmware_version = "5.0.5R";
const wrapper = shallow(<StepsPerMmSettings {...p} />);
const firstInputProps = wrapper.find("NumericMCUInputGroup")
.first().props();
const firstInputProps = wrapper.find(NumericMCUInputGroup).first().props();
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 { settingToggle } from "../../actions";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { BotConfigInputBox } from "../bot_config_input_box";
import { MotorsProps } from "../interfaces";
import { Row, Col } from "../../../ui/index";
import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { McuInputBox } from "../mcu_input_box";
import { minFwVersionCheck } from "../../../util";
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>;
}
}
import { StepsPerMmSettings } from "./steps_per_mm_settings";
export function Motors(props: MotorsProps) {
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 =
props.userMQTT && props.botAPI && props.botMQTT && props.botFirmware;
const diagnosisColor = diagnosisStatus ? "green" : "red";
const title = diagnosisStatus ? "Ok" : "Error";
const title = diagnosisStatus ? t("Ok") : t("Error");
return <div>
<div className={"connectivity-diagnosis"}>
<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 calendar = mapResourcesToCalendar(
fakeRegFEResources().index, testTime);
const calendar =
mapResourcesToCalendar(fakeRegFEResources().index, testTime);
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 calendar = mapResourcesToCalendar(
fakeRegFEResources().index, testTime);
const calendar =
mapResourcesToCalendar(fakeRegFEResources().index, testTime);
expect(calendar.getAll()).toEqual([]);
});
});

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import axios from "axios";
import { InitOptions } from "i18next";
/** @public */
export function generateUrl(langCode: string) {
const lang = langCode.slice(0, 2);
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 RESOURCES: ResourceName[] = [
"Point",
"Regimen",
"Peripheral",
"Log",
"Sequence",
"FarmEvent",
"Point",
"Device"
];
/** Temporary stub until auto_sync rollout. TODO: Remove */
export const RESOURNCE_NAME_IN_URL = [
const RESOURNCE_NAME_IN_URL = [
"device",
"farm_events",
"logs",

View File

@ -26,6 +26,7 @@ export function responseFulfilled(input: AxiosResponse): AxiosResponse {
return input;
}
let ONLY_ONCE = true;
export function responseRejected(x: SafeError | undefined) {
if (x && isSafeError(x)) {
dispatchNetworkUp("user.api");
@ -50,7 +51,8 @@ export function responseRejected(x: SafeError | undefined) {
break;
case 451:
// 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";
break;
}

View File

@ -10,17 +10,6 @@ import { RestResources } from "./resources/interfaces";
in the UI. Only certain colors are valid. */
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 {
id?: number;
sequence_id: number;
@ -144,9 +133,3 @@ export type Point =
| PlantPointer;
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 { t } from "i18next";
import { Session } from "./session";
import { BooleanSetting } from "./session_keys";
@ -73,7 +74,7 @@ export function LoadingPlant() {
fontSize={35}
textAnchor="middle"
fill="#434343">
Loading...
{t("Loading...")}
</text>
</svg>
</div>;

View File

@ -30,7 +30,7 @@ export const LogsFilterMenu = (props: LogsFilterMenuProps) => {
return <fieldset key={logType}>
<label>
<div className={`saucer ${logType}`} />
{_.startCase(logType)}
{t(_.startCase(logType))}
</label>
<button
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,
TaggedFbosConfig,
TaggedWebAppConfig,
SpecialStatus
SpecialStatus,
TaggedPoint
} from "./tagged_resources";
import { CowardlyDictionary, betterCompact, sortResourcesById, bail } from "../util";
import { isNumber } from "util";
import { buildIndexer } from "./selector_support";
type StringMap = CowardlyDictionary<string>;
/** 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[];
}
export function indexSequenceById(index: ResourceIndex) {
const output: CowardlyDictionary<TaggedSequence> = {};
const uuids = index.byKind.Sequence;
uuids.map(uuid => {
assertUuid("Sequence", uuid);
const sequence = index.references[uuid];
if (sequence && isTaggedSequence(sequence) && sequence.body.id) {
output[sequence.body.id] = sequence;
}
});
return output;
}
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;
}
const mapper = (i: TaggedPoint): TaggedToolSlotPointer | undefined => {
if (i.kind == "Point" && (i.body.pointer_type === "ToolSlot")) {
return i as TaggedToolSlotPointer;
}
return undefined;
};
export const indexBySlotId = buildIndexer<TaggedToolSlotPointer>("Point", mapper);
export const indexSequenceById = buildIndexer<TaggedSequence>("Sequence");
export const indexRegimenById = buildIndexer<TaggedRegimen>("Regimen");
export const indexFarmEventById = buildIndexer<TaggedFarmEvent>("FarmEvent");
export const indexByToolId = buildIndexer<TaggedTool>("Tool");
export function assertUuid(expected: ResourceName, actual: string | undefined) {
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 * as React from "react";
import { Provider } from "react-redux";
import { Router, RedirectFunction, RouterState } from "react-router";
import { App } from "./app";
import { Router } from "react-router";
import { store as _store } from "./redux/store";
import { history } from "./history";
import { Store } from "./redux/interfaces";
@ -10,27 +9,9 @@ import { ready } from "./config/actions";
import { Session } from "./session";
import { attachToRoot } from "./util";
import { Callback } from "i18next";
import { crashPage } from "./crash_page";
import { topLevelRoutes } from "./route_config";
const key = "Jan 20 23:52";
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)));
}
};
interface RootComponentProps { store: Store; }
export const attachAppToDom: Callback = (err, t) => {
attachToRoot(RootComponent, { store: _store });
@ -38,238 +19,6 @@ export const attachAppToDom: Callback = (err, t) => {
};
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() {
// ==== TEMPORARY HACK. TODO: Add a before hook, if such a thing exists in
// React Router. Or switch routing libs.
@ -281,7 +30,7 @@ export class RootComponent extends React.Component<RootComponentProps, {}> {
// ==== END HACK ====
return <Provider store={_store}>
<Router history={history}>
{this.routes}
{topLevelRoutes}
</Router>
</Provider>;
}

View File

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

View File

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

View File

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