improve element annotation

pull/1724/head
gabrielburnworth 2020-02-28 08:34:28 -08:00
parent cccecf58f6
commit 800625e8a1
175 changed files with 741 additions and 556 deletions

View File

@ -20,7 +20,7 @@ export class DangerousDeleteWidget extends
return <Widget>
<WidgetHeader title={this.props.title} />
<WidgetBody>
<div>
<div className={"dangerous-delete-warning-messages"}>
{t(this.props.warning)}
<br /><br />
{t(this.props.confirmation)}
@ -42,6 +42,7 @@ export class DangerousDeleteWidget extends
<button
onClick={this.onClick}
className="red fb-button"
title={t(this.props.title)}
type="button">
{t(this.props.title)}
</button>

View File

@ -7,7 +7,7 @@ export function ExportAccountPanel(props: { onClick: () => void }) {
return <Widget>
<WidgetHeader title={t("Export Account Data")} />
<WidgetBody>
<div>
<div className={"export-account-data-description"}>
{t(Content.EXPORT_DATA_DESC)}
</div>
<form>
@ -19,6 +19,7 @@ export function ExportAccountPanel(props: { onClick: () => void }) {
</Col>
<Col xs={4}>
<button className="green fb-button" type="button"
title={t("Export")}
onClick={props.onClick}>
{t("Export")}
</button>

View File

@ -47,12 +47,13 @@ export class RawAccount extends React.Component<Props, State> {
(key: keyof User) => (key === "email") && this.setState({ warnThem: true });
onChange = (e: React.FormEvent<HTMLInputElement>) => {
const { name, value } = e.currentTarget;
if (isKey(name)) {
this.tempHack(name);
this.props.dispatch(edit(this.props.user, { [name]: value }));
const { value } = e.currentTarget;
const field = e.currentTarget.name;
if (isKey(field)) {
this.tempHack(field);
this.props.dispatch(edit(this.props.user, { [field]: value }));
} else {
throw new Error("Bad key: " + name);
throw new Error("Bad key: " + field);
}
};

View File

@ -11,7 +11,7 @@ interface LabsFeaturesListProps {
}
export function LabsFeaturesList(props: LabsFeaturesListProps) {
return <div>
return <div className="labs-features-list">
{fetchLabFeatures(props.getConfigValue).map((feature, i) => {
const displayValue = feature.displayInvert ? !feature.value : feature.value;
return <Row key={i}>
@ -23,6 +23,7 @@ export function LabsFeaturesList(props: LabsFeaturesListProps) {
</Col>
<Col xs={2}>
<ToggleButton
title={t("toggle feature")}
toggleValue={displayValue ? 1 : 0}
toggleAction={() => props.onToggle(feature)
.then(() => feature.callback && feature.callback())}

View File

@ -9,9 +9,8 @@ interface DataDumpExport { device?: DeviceAccountSettings; }
type Response = AxiosResponse<DataDumpExport | undefined>;
export function generateFilename({ device }: DataDumpExport): string {
let name: string;
name = device ? (device.name + "_" + device.id) : "farmbot";
return `export_${name}.json`.toLowerCase();
const nameAndId = device ? (device.name + "_" + device.id) : "farmbot";
return `export_${nameAndId}.json`.toLowerCase();
}
// Thanks, @KOL - https://stackoverflow.com/a/19328891/1064917

View File

@ -3,17 +3,19 @@ import { Row, Col } from "../ui/index";
import { AxisDisplayGroupProps } from "./interfaces";
import { isNumber } from "lodash";
import { t } from "../i18next_wrapper";
import { Xyz } from "farmbot";
const Axis = ({ val }: { val: number | undefined }) => <Col xs={3}>
<input disabled value={isNumber(val) ? val : "---"} />
</Col>;
const Axis = ({ axis, val }: { val: number | undefined, axis: Xyz }) =>
<Col xs={3}>
<input disabled name={axis} value={isNumber(val) ? val : "---"} />
</Col>;
export const AxisDisplayGroup = ({ position, label }: AxisDisplayGroupProps) => {
const { x, y, z } = position;
return <Row>
<Axis val={x} />
<Axis val={y} />
<Axis val={z} />
<Axis axis={"x"} val={x} />
<Axis axis={"y"} val={y} />
<Axis axis={"z"} val={z} />
<Col xs={3}>
<label>
{t(label)}

View File

@ -15,12 +15,14 @@ export function KeyValEditRow(p: Props) {
return <Row>
<Col xs={6}>
<input type="text"
name="label"
placeholder={p.labelPlaceholder}
value={p.label}
onChange={p.onLabelChange} />
</Col>
<Col xs={4}>
<input type={p.valueType}
name="value"
value={p.value}
placeholder={p.valuePlaceholder}
onChange={p.onValueChange} />

View File

@ -19,7 +19,7 @@ export interface BotPositionRowsProps {
export const BotPositionRows = (props: BotPositionRowsProps) => {
const { locationData, getValue, arduinoBusy } = props;
return <div>
return <div className={"bot-position-rows"}>
<Row>
<Col xs={3}>
<label>{t("X AXIS")}</label>

View File

@ -22,7 +22,7 @@ export const JogControlsGroup = (props: JogControlsGroupProps) => {
const {
dispatch, stepSize, botPosition, getValue, arduinoBusy, firmwareSettings
} = props;
return <div>
return <div className={"jog-controls-group"}>
<label className="text-center">
{t("MOVE AMOUNT (mm)")}
</label>

View File

@ -46,9 +46,9 @@ const getLastEntry = (): Entry | undefined => {
const findYLimit = (): number => {
const array = getArray();
const arrayAbsMax = max(array.map(entry =>
max(["position", "scaled_encoders"].map((name: LocationName) =>
max(["position", "scaled_encoders"].map((key: LocationName) =>
max(["x", "y", "z"].map((axis: Xyz) =>
Math.abs(entry.locationData[name][axis] || 0) + 1))))));
Math.abs(entry.locationData[key][axis] || 0) + 1))))));
return Math.max(ceil(arrayAbsMax || 0, -2), DEFAULT_Y_MAX);
};
@ -80,19 +80,19 @@ const getPaths = (): Paths => {
const paths = newPaths();
if (last) {
getReversedArray().map(entry => {
["position", "scaled_encoders"].map((name: LocationName) => {
["position", "scaled_encoders"].map((key: LocationName) => {
["x", "y", "z"].map((axis: Xyz) => {
const lastPos = last.locationData[name][axis];
const pos = entry.locationData[name][axis];
const lastPos = last.locationData[key][axis];
const pos = entry.locationData[key][axis];
if (isNumber(lastPos) && isFinite(lastPos)
&& isNumber(maxY) && isNumber(pos)) {
if (!paths[name][axis].startsWith("M")) {
if (!paths[key][axis].startsWith("M")) {
const yStart = -lastPos / maxY * HEIGHT / 2;
paths[name][axis] = `M ${MAX_X},${yStart} `;
paths[key][axis] = `M ${MAX_X},${yStart} `;
}
const x = MAX_X - (last.timestamp - entry.timestamp);
const y = -pos / maxY * HEIGHT / 2;
paths[name][axis] += `L ${x},${y} `;
paths[key][axis] += `L ${x},${y} `;
}
});
});
@ -154,12 +154,12 @@ const PlotLines = ({ locationData }: { locationData: BotLocationData }) => {
updateArray({ timestamp: moment().unix(), locationData });
const paths = getPaths();
return <g id="plot_lines">
{["position", "scaled_encoders"].map((name: LocationName) =>
{["position", "scaled_encoders"].map((key: LocationName) =>
["x", "y", "z"].map((axis: Xyz) =>
<path key={name + axis} fill={"none"}
stroke={COLOR_LOOKUP[axis]} strokeWidth={LINEWIDTH_LOOKUP[name]}
<path key={key + axis} fill={"none"}
stroke={COLOR_LOOKUP[axis]} strokeWidth={LINEWIDTH_LOOKUP[key]}
strokeLinecap={"round"} strokeLinejoin={"round"}
d={paths[name][axis]} />))}
d={paths[key][axis]} />))}
</g>;
};

View File

@ -56,7 +56,7 @@ export const MoveWidgetSettingsMenu = (
setting={BooleanSetting.home_button_homing} />
{DevSettings.futureFeaturesEnabled() &&
<div>
<div className={"motor-position-plot-setting-row"}>
<p>{t("Motor position plot")}</p>
<Setting
label={t("show")}

View File

@ -1,6 +1,7 @@
import * as React from "react";
import { StepSizeSelectorProps } from "./interfaces";
import { first, last } from "lodash";
import { t } from "../../i18next_wrapper";
export class StepSizeSelector extends React.Component<StepSizeSelectorProps, {}> {
cssForIndex(num: number) {
@ -22,10 +23,10 @@ export class StepSizeSelector extends React.Component<StepSizeSelectorProps, {}>
return <div className="move-amount-wrapper">
{
this.props.choices.map(
(item: number, inx: number) => <button
(item: number, inx: number) => <button key={inx}
title={t("{{ amount }}mm", { amount: item })}
className={this.cssForIndex(item)}
onClick={() => this.props.selector(item)}
key={inx}>
onClick={() => this.props.selector(item)}>
{item}
</button>
)

View File

@ -86,15 +86,17 @@ export class Peripherals
render() {
const { isEditing } = this.state;
const status = getArrayStatus(this.props.peripherals);
const editButtonText = isEditing
? t("Back")
: t("Edit");
return <Widget className="peripherals-widget">
<WidgetHeader title={t("Peripherals")} helpText={ToolTips.PERIPHERALS}>
<button
className="fb-button gray"
onClick={this.toggle}
title={editButtonText}
disabled={!!status && isEditing}>
{!isEditing && t("Edit")}
{isEditing && t("Back")}
{editButtonText}
</button>
<SaveBtn
hidden={!isEditing}
@ -104,6 +106,7 @@ export class Peripherals
hidden={!isEditing}
className="fb-button green"
type="button"
title={t("add peripheral")}
onClick={() => this.newPeripheral()}>
<i className="fa fa-plus" />
</button>
@ -111,6 +114,7 @@ export class Peripherals
hidden={!isEditing || this.props.firmwareHardware == "none"}
className="fb-button green"
type="button"
title={t("add stock peripherals")}
onClick={() => this.stockPeripherals.map(p =>
this.newPeripheral(p.pin, p.label))}>
<i className="fa fa-plus" style={{ marginRight: "0.5rem" }} />

View File

@ -22,6 +22,7 @@ interface NameInputBoxProps {
export const NameInputBox = (props: NameInputBoxProps) =>
<input type="text"
name="name"
placeholder={t("Name")}
value={props.value}
onChange={e => props.dispatch(edit(props.resource, {

View File

@ -25,11 +25,11 @@ describe("filterSensorReadings()", () => {
});
const createLocatedReading = (locations: Record<Xyz, number | undefined>[]) =>
locations.map(location => {
locations.map(xyzLocation => {
const sr = fakeSensorReading();
sr.body.x = location.x;
sr.body.y = location.y;
sr.body.z = location.z;
sr.body.x = xyzLocation.x;
sr.body.y = xyzLocation.y;
sr.body.z = xyzLocation.z;
sr.body.created_at = defaultCreatedAt;
return sr;
});
@ -38,7 +38,7 @@ describe("filterSensorReadings()", () => {
sensor: undefined,
timePeriod: 3600 * 24 * 7,
endDate: 1515715140,
location: undefined,
xyzLocation: undefined,
showPreviousPeriod: false,
deviation: 0,
hovered: undefined,
@ -87,7 +87,7 @@ describe("filterSensorReadings()", () => {
const locations = [expected, { x: 0, y: 0, z: 0 }];
const filters = sensorReadingsState();
filters.endDate = moment(defaultCreatedAt).unix() + 1;
filters.location = expected;
filters.xyzLocation = expected;
const result = filterSensorReadings(
createLocatedReading(locations), filters)("current");
expect(result.length).toEqual(1);
@ -101,7 +101,7 @@ describe("filterSensorReadings()", () => {
const locations = [{ x: 1, y: 2, z: 3 }, { x: 0, y: 0, z: 0 }];
const filters = sensorReadingsState();
filters.endDate = moment(defaultCreatedAt).unix() + 1;
filters.location = expected;
filters.xyzLocation = expected;
filters.deviation = 100;
const result = filterSensorReadings(
createLocatedReading(locations), filters)("current");

View File

@ -6,7 +6,7 @@ import { LocationSelectionProps } from "../interfaces";
describe("<LocationSelection />", () => {
function fakeProps(): LocationSelectionProps {
return {
location: undefined,
xyzLocation: undefined,
deviation: 0,
setLocation: jest.fn(),
setDeviation: jest.fn(),
@ -22,7 +22,7 @@ describe("<LocationSelection />", () => {
it("changes location", () => {
const p = fakeProps();
p.location = { x: 10, y: 20, z: 30 };
p.xyzLocation = { x: 10, y: 20, z: 30 };
const wrapper = mount(<LocationSelection {...p} />);
wrapper.find("input").first().simulate("submit");
expect(p.setLocation).toHaveBeenCalledWith({ x: 10, y: 20, z: 30 });
@ -30,7 +30,7 @@ describe("<LocationSelection />", () => {
it("changes location: undefined", () => {
const p = fakeProps();
p.location = undefined;
p.xyzLocation = undefined;
const wrapper = mount(<LocationSelection {...p} />);
wrapper.find("input").first().simulate("submit");
expect(p.setLocation).toHaveBeenCalledWith({ x: undefined });
@ -47,7 +47,7 @@ describe("<LocationSelection />", () => {
describe("<LocationDisplay />", () => {
it("renders location ranges", () => {
const p = { location: { x: 10, y: 20, z: 30 }, deviation: 2 };
const p = { xyzLocation: { x: 10, y: 20, z: 30 }, deviation: 2 };
const wrapper = mount(<LocationDisplay {...p} />);
const txt = wrapper.text().toLowerCase();
["x", "y", "z", "812", "1822", "2832"]

View File

@ -43,9 +43,9 @@ describe("<SensorReadings />", () => {
it("sets location", () => {
const expectedLocation = { x: 1, y: 2, z: undefined };
const wrapper = mount<SensorReadings>(<SensorReadings {...fakeProps()} />);
expect(wrapper.instance().state.location).toEqual(undefined);
expect(wrapper.instance().state.xyzLocation).toEqual(undefined);
wrapper.instance().setLocation(expectedLocation);
expect(wrapper.instance().state.location).toEqual(expectedLocation);
expect(wrapper.instance().state.xyzLocation).toEqual(expectedLocation);
});
it("sets end date", () => {
@ -87,9 +87,9 @@ describe("<SensorReadings />", () => {
const p = fakeProps();
p.sensors = [s];
const wrapper = mount<SensorReadings>(<SensorReadings {...p} />);
wrapper.setState({ location: { x: 1, y: 2, z: 3 }, sensor: s });
wrapper.setState({ xyzLocation: { x: 1, y: 2, z: 3 }, sensor: s });
wrapper.instance().clearFilters();
expect(wrapper.instance().state.location).toEqual(undefined);
expect(wrapper.instance().state.xyzLocation).toEqual(undefined);
expect(wrapper.instance().state.sensor).toEqual(undefined);
});
});

View File

@ -20,7 +20,7 @@ export const filterSensorReadings =
sensorReadingsState: SensorReadingsState) =>
(period: "current" | "previous"): TaggedSensorReading[] => {
const {
sensor, endDate, timePeriod, showPreviousPeriod, location, deviation
sensor, endDate, timePeriod, showPreviousPeriod, xyzLocation, deviation
} = sensorReadingsState;
// Don't return sensor readings from the previous period if not desired.
@ -41,11 +41,11 @@ export const filterSensorReadings =
.filter(x => sensor ? x.body.pin === sensor.body.pin : true)
// Filter by location
.filter(sensorReading => {
if (location) {
if (xyzLocation) {
const { body } = sensorReading;
return every(["x", "y", "z"].map((axis: Xyz) => {
const a = body[axis];
const input = location[axis];
const input = xyzLocation[axis];
return isNumber(a) && isNumber(input)
? (a <= input + deviation) && (a >= input - deviation)
: true;

View File

@ -15,7 +15,7 @@ export interface SensorReadingsState {
/** seconds */
endDate: number;
/** location filter setting */
location: AxisInputBoxGroupState | undefined;
xyzLocation: AxisInputBoxGroupState | undefined;
/** Show the previous time period in addition to the current time period. */
showPreviousPeriod: boolean;
/** mm */
@ -50,11 +50,11 @@ export interface SensorSelectionProps {
}
export interface LocationSelectionProps {
location: AxisInputBoxGroupState | undefined;
xyzLocation: AxisInputBoxGroupState | undefined;
/** mm */
deviation: number;
setDeviation: (deviation: number) => void;
setLocation: (location: AxisInputBoxGroupState | undefined) => void;
setLocation: (xyzLocation: AxisInputBoxGroupState | undefined) => void;
}
export interface TimePeriodSelectionProps {

View File

@ -10,7 +10,7 @@ import { t } from "../../i18next_wrapper";
/** Select a location filter for sensor readings. */
export const LocationSelection =
({ location, deviation, setDeviation, setLocation }: LocationSelectionProps) =>
({ xyzLocation, deviation, setDeviation, setLocation }: LocationSelectionProps) =>
<div className="sensor-history-location-selection">
<Row>
{["x", "y", "z"].map(axis =>
@ -26,9 +26,9 @@ export const LocationSelection =
<AxisInputBox
key={axis}
axis={axis}
value={location ? location[axis] : undefined}
value={xyzLocation ? xyzLocation[axis] : undefined}
onChange={(a: Xyz, v) => {
const newLocation = (location || {});
const newLocation = (xyzLocation || {});
newLocation[a] = v;
setLocation(newLocation);
}} />)}
@ -42,15 +42,15 @@ export const LocationSelection =
</div>;
/** Display sensor reading location filter settings. */
export const LocationDisplay = ({ location, deviation }: {
location: AxisInputBoxGroupState | undefined,
export const LocationDisplay = ({ xyzLocation, deviation }: {
xyzLocation: AxisInputBoxGroupState | undefined,
deviation: number
}) => {
return <div className="location">
{["x", "y", "z"].map((axis: Xyz) => {
const axisString = () => {
if (location) {
const axisValue = location[axis];
if (xyzLocation) {
const axisValue = xyzLocation[axis];
if (isNumber(axisValue)) {
return deviation
? `${axisValue - deviation}${axisValue + deviation}`

View File

@ -20,7 +20,7 @@ export class SensorReadings
sensor: undefined,
timePeriod: 3600 * 24,
endDate: getEndDate(this.props.sensorReadings),
location: undefined,
xyzLocation: undefined,
showPreviousPeriod: false,
deviation: 0,
hovered: undefined,
@ -32,15 +32,15 @@ export class SensorReadings
setSensor = (sensor: TaggedSensor | undefined) => this.setState({ sensor });
setEndDate = (endDate: number) => this.setState({ endDate });
setTimePeriod = (timePeriod: number) => this.setState({ timePeriod });
setLocation = (location: AxisInputBoxGroupState | undefined) =>
this.setState({ location });
setLocation = (xyzLocation: AxisInputBoxGroupState | undefined) =>
this.setState({ xyzLocation });
setDeviation = (deviation: number) => this.setState({ deviation });
hover = (hovered: string | undefined) => this.setState({ hovered });
clearFilters = () => this.setState({
sensor: undefined,
timePeriod: 3600 * 24,
endDate: getEndDate(this.props.sensorReadings),
location: undefined,
xyzLocation: undefined,
showPreviousPeriod: false,
deviation: 0,
});
@ -55,7 +55,9 @@ export class SensorReadings
<WidgetHeader
title={t("Sensor History")}
helpText={ToolTips.SENSOR_HISTORY}>
<button className="fb-button gray" onClick={this.clearFilters}>
<button className="fb-button gray"
title={t("clear filters")}
onClick={this.clearFilters}>
{t("clear filters")}
</button>
</WidgetHeader>
@ -72,7 +74,7 @@ export class SensorReadings
setPeriod={this.setTimePeriod}
togglePrevious={this.togglePrevious} />
<LocationSelection
location={this.state.location}
xyzLocation={this.state.xyzLocation}
deviation={this.state.deviation}
setLocation={this.setLocation}
setDeviation={this.setDeviation} />
@ -100,7 +102,7 @@ export class SensorReadings
timePeriod={this.state.timePeriod}
timeSettings={this.props.timeSettings} />
<LocationDisplay
location={this.state.location}
xyzLocation={this.state.xyzLocation}
deviation={this.state.deviation} />
</div>
</WidgetFooter>

View File

@ -74,6 +74,7 @@ export const TimePeriodSelection = (props: TimePeriodSelectionProps) => {
<Col xs={ColWidth.showPrevious}>
<div className="fb-checkbox large">
<input type="checkbox"
name="previous"
checked={showPreviousPeriod}
onChange={togglePrevious} />
</div>

View File

@ -58,15 +58,17 @@ export class Sensors extends React.Component<SensorsProps, SensorState> {
render() {
const { isEditing } = this.state;
const status = getArrayStatus(this.props.sensors);
const editButtonText = isEditing
? t("Back")
: t("Edit");
return <Widget className="sensors-widget">
<WidgetHeader title={t("Sensors")} helpText={ToolTips.SENSORS}>
<button
className="fb-button gray"
onClick={this.toggle}
title={editButtonText}
disabled={!!status && isEditing}>
{!isEditing && t("Edit")}
{isEditing && t("Back")}
{editButtonText}
</button>
<SaveBtn
hidden={!isEditing}
@ -76,6 +78,7 @@ export class Sensors extends React.Component<SensorsProps, SensorState> {
hidden={!isEditing}
className="fb-button green"
type="button"
title={t("add sensors")}
onClick={() => this.newSensor()}>
<i className="fa fa-plus" />
</button>
@ -83,6 +86,7 @@ export class Sensors extends React.Component<SensorsProps, SensorState> {
hidden={!isEditing || this.props.firmwareHardware == "none"}
className="fb-button green"
type="button"
title={t("add stock sensors")}
onClick={this.stockSensors}>
<i className="fa fa-plus" style={{ marginRight: "0.5rem" }} />
{t("Stock sensors")}

View File

@ -40,6 +40,6 @@ describe("<IndexIndicator/>", () => {
it("doesn't render index indicator", () => {
const wrapper = mount(<IndexIndicator i={0} total={1} />);
expect(wrapper.html()).toEqual("<div></div>");
expect(wrapper.html()).toEqual("<div class=\"no-index-indicator\"></div>");
});
});

View File

@ -32,16 +32,19 @@ export function Edit(props: WebcamPanelProps) {
<button
className="fb-button gray"
disabled={unsaved.length > 0}
title={t("Back")}
onClick={props.onToggle}>
{t("Back")}
</button>
<button
className="fb-button green"
title={t("Save")}
onClick={() => { unsaved.map(x => props.save(x)); }}>
{t("Save")}{unsaved.length > 0 ? "*" : ""}
</button>
<button
className="fb-button green"
title={t("Add webcam")}
onClick={props.init}>
<i className="fa fa-plus" />
</button>

View File

@ -25,7 +25,7 @@ export function IndexIndicator(props: { i: number, total: number }): JSX.Element
style={{
width: `${percentWidth}%`,
left: `calc(-10px + ${props.i} * ${percentWidth}%)`
}} /> : <div />;
}} /> : <div className={"no-index-indicator"} />;
}
export class Show extends React.Component<WebcamPanelProps, State> {
@ -57,6 +57,7 @@ export class Show extends React.Component<WebcamPanelProps, State> {
<WidgetHeader title={title} helpText={ToolTips.WEBCAM}>
<button
className="fb-button gray"
title={t("Edit")}
onClick={props.onToggle}>
{t("Edit")}
</button>
@ -74,6 +75,7 @@ export class Show extends React.Component<WebcamPanelProps, State> {
onClick={() => flipper.down((_, current) => this.setState({ current }))}
hidden={feeds.length < 2}
disabled={false}
title={t("Previous image")}
className="image-flipper-left fb-button">
{t("Prev")}
</button>
@ -81,6 +83,7 @@ export class Show extends React.Component<WebcamPanelProps, State> {
onClick={() => flipper.up((_, current) => this.setState({ current }))}
hidden={feeds.length < 2}
disabled={false}
title={t("Next image")}
className="image-flipper-right fb-button">
{t("Next")}
</button>

View File

@ -945,7 +945,9 @@
margin: 0;
}
}
.clear-button {
.preview-button,
.cancel-button,
.save-button {
text-transform: uppercase;
font-size: 1rem;
border: 1px solid;

View File

@ -3,6 +3,7 @@ import React from "react";
import { uuid } from "farmbot";
import axios from "axios";
import { ExternalUrl } from "../external_urls";
import { t } from "../i18next_wrapper";
interface State {
error: Error | undefined;
@ -23,7 +24,7 @@ export const WAITING_ON_API = "Planting your demo garden...";
// APPLICATION CODE ==============================
export class DemoIframe extends React.Component<{}, State> {
state: State =
{ error: undefined, stage: "DEMO THE APP" };
{ error: undefined, stage: t("DEMO THE APP") };
setError = (error?: Error) => this.setState({ error });
@ -63,7 +64,9 @@ export class DemoIframe extends React.Component<{}, State> {
<source src={ExternalUrl.Video.desktop} type="video/mp4" />
</video>
<img className="demo-phone" src={ExternalUrl.Video.mobile} />
<button className="demo-button" onClick={this.requestAccount}>
<button className="demo-button"
title={t("demo the app")}
onClick={this.requestAccount}>
{this.state.stage}
</button>
</div>;

View File

@ -285,13 +285,13 @@ export function MCUFactoryReset() {
/** Toggle a firmware setting. */
export function settingToggle(
name: ConfigKey,
key: ConfigKey,
sourceFwConfig: SourceFwConfig,
displayAlert?: string | undefined
) {
return function (dispatch: Function, getState: () => Everything) {
if (displayAlert) { alert(trim(displayAlert)); }
const update = { [name]: (sourceFwConfig(name).value === 0) ? ON : OFF };
const update = { [key]: (sourceFwConfig(key).value === 0) ? ON : OFF };
const firmwareConfig = getFirmwareConfig(getState().resources.index);
const toggleFirmwareConfig = (fwConfig: TaggedFirmwareConfig) => {
dispatch(edit(fwConfig, update));

View File

@ -13,7 +13,7 @@ import {
describe("<PinGuardMCUInputGroup/>", () => {
const fakeProps = (): PinGuardMCUInputGroupProps => {
return {
name: "Pin Guard 1",
label: "Pin Guard 1",
pinNumKey: "pin_guard_1_pin_nr",
timeoutKey: "pin_guard_1_time_out",
activeStateKey: "pin_guard_1_active_state",

View File

@ -17,7 +17,7 @@ import { updateMCU } from "../../actions";
describe("<PinNumberDropdown />", () => {
const fakeProps =
(firmwareConfig?: TaggedFirmwareConfig): PinGuardMCUInputGroupProps => ({
name: "Pin Guard 1",
label: "Pin Guard 1",
pinNumKey: "pin_guard_1_pin_nr",
timeoutKey: "pin_guard_1_time_out",
activeStateKey: "pin_guard_1_active_state",

View File

@ -110,11 +110,9 @@ export class FarmbotOsSettings
<div className="note">
{this.maybeWarnTz()}
</div>
<div>
<TimezoneSelector
currentTimezone={this.props.deviceAccount.body.timezone}
onUpdate={this.handleTimezone} />
</div>
<TimezoneSelector
currentTimezone={this.props.deviceAccount.body.timezone}
onUpdate={this.handleTimezone} />
</Col>
</Highlight>
</Row>

View File

@ -56,14 +56,12 @@ export class BoardType extends React.Component<BoardTypeProps, BoardTypeState> {
</label>
</Col>
<Col xs={ColWidth.description}>
<div>
<FBSelect
key={this.apiValue}
extraClass={this.state.sending ? "dim" : ""}
list={getFirmwareChoices()}
selectedItem={this.selectedBoard}
onChange={this.sendOffConfig} />
</div>
<FBSelect
key={this.apiValue}
extraClass={this.state.sending ? "dim" : ""}
list={getFirmwareChoices()}
selectedItem={this.selectedBoard}
onChange={this.sendOffConfig} />
</Col>
<Col xs={ColWidth.button}>
<FirmwareHardwareStatus

View File

@ -92,14 +92,12 @@ export class CameraSelection
</label>
</Col>
<Col xs={ColWidth.description}>
<div>
<FBSelect
allowEmpty={false}
list={CAMERA_CHOICES()}
selectedItem={this.selectedCamera()}
onChange={this.sendOffConfig}
extraClass={this.props.botOnline ? "" : "disabled"} />
</div>
<FBSelect
allowEmpty={false}
list={CAMERA_CHOICES()}
selectedItem={this.selectedCamera()}
onChange={this.sendOffConfig}
extraClass={this.props.botOnline ? "" : "disabled"} />
</Col>
</Highlight>
</Row>;

View File

@ -90,6 +90,7 @@ export class ChangeOwnershipForm
<Row>
<button
className={"fb-button gray"}
title={t("submit")}
onClick={() => submitOwnershipChange(this.state)}>
{t("submit")}
</button>

View File

@ -4,16 +4,16 @@ import { Content, DeviceSetting } from "../../../constants";
import { factoryReset, updateConfig } from "../../actions";
import { ToggleButton } from "../../../controls/toggle_button";
import { BotConfigInputBox } from "../bot_config_input_box";
import { FactoryResetRowProps } from "./interfaces";
import { FactoryResetRowsProps } from "./interfaces";
import { ColWidth } from "../farmbot_os_settings";
import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
export function FactoryResetRow(props: FactoryResetRowProps) {
export function FactoryResetRows(props: FactoryResetRowsProps) {
const { dispatch, sourceFbosConfig, botOnline } = props;
const disableFactoryReset = sourceFbosConfig("disable_factory_reset");
const maybeDisableTimer = disableFactoryReset.value ? { color: "grey" } : {};
return <div>
return <div className={"factory-reset-options"}>
<Row>
<Highlight settingName={DeviceSetting.factoryReset}>
<Col xs={ColWidth.label}>
@ -31,6 +31,7 @@ export function FactoryResetRow(props: FactoryResetRowProps) {
className="fb-button red"
type="button"
onClick={factoryReset}
title={t("FACTORY RESET")}
disabled={!botOnline}>
{t("FACTORY RESET")}
</button>

View File

@ -32,6 +32,7 @@ export const FbosButtonRow = (props: FbosButtonRowProps) => {
className={`fb-button ${props.color}`}
type="button"
onClick={props.action}
title={t(props.buttonText)}
disabled={!props.botOnline}>
{t(props.buttonText)}
</button>

View File

@ -267,7 +267,7 @@ export function FbosDetails(props: FbosDetailsProps) {
const infoFwCommit = firmware_version?.includes(".") ? firmware_commit : "---";
const firmwareCommit = firmware_version?.split("-")[1] || infoFwCommit;
return <div>
return <div className={"farmbot-os-details"}>
<LastSeen
dispatch={props.dispatch}
botToMqttLastSeen={props.botToMqttLastSeen}

View File

@ -49,6 +49,7 @@ export const FlashFirmwareBtn = (props: FlashFirmwareBtnProps) => {
const { apiFirmwareValue } = props;
return <button className="fb-button yellow"
disabled={!apiFirmwareValue || !props.botOnline}
title={t("flash firmware")}
onClick={() => isFwHardwareValue(apiFirmwareValue) &&
flashFirmware(apiFirmwareValue)}>
{t("flash firmware")}

View File

@ -59,7 +59,7 @@ export interface PowerAndResetProps {
botOnline: boolean;
}
export interface FactoryResetRowProps {
export interface FactoryResetRowsProps {
dispatch: Function;
sourceFbosConfig: SourceFbosConfig;
botOnline: boolean;

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { Header } from "../hardware_settings/header";
import { Collapse, Popover, Position } from "@blueprintjs/core";
import { FactoryResetRow } from "./factory_reset_row";
import { FactoryResetRows } from "./factory_reset_row";
import { PowerAndResetProps } from "./interfaces";
import { ChangeOwnershipForm } from "./change_ownership_form";
import { FbosButtonRow } from "./fbos_button_row";
@ -42,7 +42,7 @@ export function PowerAndReset(props: PowerAndResetProps) {
buttonText={t("RESTART")}
color={"yellow"}
action={restartFirmware} />
<FactoryResetRow
<FactoryResetRows
dispatch={dispatch}
sourceFbosConfig={sourceFbosConfig}
botOnline={botOnline} />

View File

@ -45,11 +45,13 @@ export class HardwareSettings extends
<WidgetBody>
<button
className={"fb-button gray no-float"}
title={t("Expand All")}
onClick={() => dispatch(bulkToggleControlPanel(true))}>
{t("Expand All")}
</button>
<button
className={"fb-button gray no-float"}
title={t("Collapse All")}
onClick={() => dispatch(bulkToggleControlPanel(false))}>
{t("Collapse All")}
</button>

View File

@ -27,6 +27,7 @@ export function CalibrationRow(props: CalibrationRowProps) {
return <Col xs={2} key={axis} className={"centered-button-div"}>
<LockableButton
disabled={hardwareDisabled || botDisconnected}
title={t(props.axisTitle)}
onClick={() => props.action(axis)}>
{`${t(props.axisTitle)} ${axis}`}
</LockableButton>

View File

@ -36,6 +36,7 @@ export function DangerZone(props: DangerZoneProps) {
<button
className="fb-button red"
disabled={botDisconnected}
title={t("RESET")}
onClick={onReset}>
{t("RESET")}
</button>

View File

@ -16,7 +16,7 @@ import { Highlight } from "../maybe_highlight";
export const calculateScale =
(sourceFwConfig: SourceFwConfig): Record<Xyz, number | undefined> => {
const getV = (name: McuParamName) => sourceFwConfig(name).value;
const getV = (key: McuParamName) => sourceFwConfig(key).value;
return {
x: calcMicrostepsPerMm(getV("movement_step_per_mm_x"),
getV("movement_microsteps_x")),

View File

@ -41,7 +41,7 @@ export function PinGuard(props: PinGuardProps) {
</Col>
</Row>
<PinGuardMCUInputGroup
name={t("Pin Guard {{ num }}", { num: 1 })}
label={t("Pin Guard {{ num }}", { num: 1 })}
pinNumKey={"pin_guard_1_pin_nr"}
timeoutKey={"pin_guard_1_time_out"}
activeStateKey={"pin_guard_1_active_state"}
@ -49,7 +49,7 @@ export function PinGuard(props: PinGuardProps) {
resources={resources}
sourceFwConfig={sourceFwConfig} />
<PinGuardMCUInputGroup
name={t("Pin Guard {{ num }}", { num: 2 })}
label={t("Pin Guard {{ num }}", { num: 2 })}
pinNumKey={"pin_guard_2_pin_nr"}
timeoutKey={"pin_guard_2_time_out"}
activeStateKey={"pin_guard_2_active_state"}
@ -57,7 +57,7 @@ export function PinGuard(props: PinGuardProps) {
resources={resources}
sourceFwConfig={sourceFwConfig} />
<PinGuardMCUInputGroup
name={t("Pin Guard {{ num }}", { num: 3 })}
label={t("Pin Guard {{ num }}", { num: 3 })}
pinNumKey={"pin_guard_3_pin_nr"}
timeoutKey={"pin_guard_3_time_out"}
activeStateKey={"pin_guard_3_active_state"}
@ -65,7 +65,7 @@ export function PinGuard(props: PinGuardProps) {
resources={resources}
sourceFwConfig={sourceFwConfig} />
<PinGuardMCUInputGroup
name={t("Pin Guard {{ num }}", { num: 4 })}
label={t("Pin Guard {{ num }}", { num: 4 })}
pinNumKey={"pin_guard_4_pin_nr"}
timeoutKey={"pin_guard_4_time_out"}
activeStateKey={"pin_guard_4_active_state"}
@ -73,7 +73,7 @@ export function PinGuard(props: PinGuardProps) {
resources={resources}
sourceFwConfig={sourceFwConfig} />
<PinGuardMCUInputGroup
name={t("Pin Guard {{ num }}", { num: 5 })}
label={t("Pin Guard {{ num }}", { num: 5 })}
pinNumKey={"pin_guard_5_pin_nr"}
timeoutKey={"pin_guard_5_time_out"}
activeStateKey={"pin_guard_5_active_state"}

View File

@ -65,7 +65,7 @@ export interface NumericMCUInputGroupProps {
export interface PinGuardMCUInputGroupProps {
sourceFwConfig: SourceFwConfig;
dispatch: Function;
name: string;
label: string;
pinNumKey: McuParamName;
timeoutKey: McuParamName;
activeStateKey: McuParamName;

View File

@ -4,13 +4,15 @@ interface Props {
onClick: Function;
disabled: boolean;
children?: React.ReactNode;
title?: string;
}
export function LockableButton({ onClick, disabled, children }: Props) {
export function LockableButton({ onClick, disabled, children, title }: Props) {
const className = disabled ? "gray" : "yellow";
return <button
className={"fb-button " + className}
disabled={disabled}
title={title}
onClick={() => disabled ? "" : onClick()}>
{children}
</button>;

View File

@ -10,7 +10,7 @@ import { PinNumberDropdown } from "./pin_number_dropdown";
export function PinGuardMCUInputGroup(props: PinGuardMCUInputGroupProps) {
const { sourceFwConfig, dispatch, name, pinNumKey, timeoutKey, activeStateKey
const { sourceFwConfig, dispatch, label, pinNumKey, timeoutKey, activeStateKey
} = props;
const activeStateValue = sourceFwConfig(activeStateKey).value;
const inactiveState = isUndefined(activeStateValue)
@ -19,7 +19,7 @@ export function PinGuardMCUInputGroup(props: PinGuardMCUInputGroupProps) {
return <Row>
<Col xs={3}>
<label>
{name}
{label}
</label>
</Col>
<Col xs={3}>

View File

@ -26,8 +26,8 @@ export class Connectivity
extends React.Component<ConnectivityProps, ConnectivityState> {
state: ConnectivityState = { hoveredConnection: undefined };
hover = (name: string) =>
() => this.setState({ hoveredConnection: name });
hover = (connectionName: string) =>
() => this.setState({ hoveredConnection: connectionName });
render() {
const { informational_settings } = this.props.bot.hardware;

View File

@ -28,7 +28,7 @@ export function Diagnosis(props: DiagnosisProps) {
const diagnosisBoolean = diagnosisStatus(props);
const diagnosisColor = diagnosisBoolean ? "green" : "red";
const title = diagnosisBoolean ? t("Ok") : t("Error");
return <div>
return <div className={"diagnosis-section"}>
<div className={"connectivity-diagnosis"}>
<h4>{t("Diagnosis")}</h4>
</div>

View File

@ -49,8 +49,9 @@ const diagramPositions: CowardlyDictionary<Record<"x" | "y", number>> = {
subRight: { x: 40, y: 110 }
};
export function getTextPosition(name: DiagramNodes): Record<"x" | "y", number> {
const position = diagramPositions[name];
export function getTextPosition(
positionKey: DiagramNodes): Record<"x" | "y", number> {
const position = diagramPositions[positionKey];
if (position) {
return {
x: position.x,

View File

@ -27,7 +27,7 @@ export function MustBeOnline(props: MBOProps) {
const { children, hideBanner, lockOpen, networkState, syncStatus } = props;
const banner = hideBanner ? "" : "banner";
if (isBotOnline(syncStatus, networkState) || lockOpen) {
return <div> {children} </div>;
return <div className={"bot-is-online-wrapper"}>{children}</div>;
} else {
return <div
className={`unavailable ${banner}`}

View File

@ -58,7 +58,7 @@ describe("<PinBindingInputGroup/>", () => {
it("no pin selected", () => {
const wrapper = mount(<PinBindingInputGroup {...fakeProps()} />);
const buttons = wrapper.find("button");
expect(buttons.last().text()).toEqual("BIND");
expect(buttons.last().props().title).toEqual("BIND");
buttons.last().simulate("click");
expect(error).toHaveBeenCalledWith("Pin number cannot be blank.");
});
@ -66,7 +66,7 @@ describe("<PinBindingInputGroup/>", () => {
it("no target selected", () => {
const wrapper = mount(<PinBindingInputGroup {...fakeProps()} />);
const buttons = wrapper.find("button");
expect(buttons.last().text()).toEqual("BIND");
expect(buttons.last().props().title).toEqual("BIND");
wrapper.setState({ pinNumberInput: AVAILABLE_PIN });
buttons.last().simulate("click");
expect(error).toHaveBeenCalledWith("Please select a sequence or action.");
@ -77,7 +77,7 @@ describe("<PinBindingInputGroup/>", () => {
p.dispatch = jest.fn();
const wrapper = mount(<PinBindingInputGroup {...p} />);
const buttons = wrapper.find("button");
expect(buttons.last().text()).toEqual("BIND");
expect(buttons.last().props().title).toEqual("BIND");
wrapper.setState({ pinNumberInput: 1, sequenceIdInput: 2 });
buttons.last().simulate("click");
expect(mockDevice.registerGpio).not.toHaveBeenCalled();
@ -94,7 +94,7 @@ describe("<PinBindingInputGroup/>", () => {
p.dispatch = jest.fn();
const wrapper = mount(<PinBindingInputGroup {...p} />);
const buttons = wrapper.find("button");
expect(buttons.last().text()).toEqual("BIND");
expect(buttons.last().props().title).toEqual("BIND");
wrapper.setState({
pinNumberInput: 0,
bindingType: PinBindingType.special,

View File

@ -133,8 +133,9 @@ export class PinBindingInputGroup
<button
className="fb-button green"
type="button"
title={t("BIND")}
onClick={this.bindPin}>
{t("BIND")}
<i className={"fa fa-plus"} />
</button>
</Col>
</Row>;

View File

@ -81,12 +81,12 @@ export const PinBindingsContent = (props: PinBindingsContentProps) => {
portalClassName={"bindings-warning-icon"}
popoverClassName={"help"}>
<i className="fa fa-exclamation-triangle" />
<div>
<div className={"pin-binding-warning"}>
{t(ToolTips.PIN_BINDING_WARNING)}
</div>
</Popover>
</Row>
<div>
<div className={"pin-bindings-list-and-input"}>
<PinBindingsListHeader />
<PinBindingsList
pinBindings={pinBindings}

View File

@ -47,6 +47,7 @@ export const StockPinBindingsButton = (props: StockPinBindingsButtonProps) =>
<button
className="fb-button green"
hidden={!hasButtons(props.firmwareHardware)}
title={t("add stock pin bindings")}
onClick={() => stockPinBindings.map(binding =>
props.dispatch(initSave("PinBinding", pinBindingBody(binding))))}>
<i className="fa fa-plus" />

View File

@ -18,7 +18,7 @@ export class ErrorBoundary extends React.Component<Props, State> {
no = () => this.props.fallback || <Apology />;
ok = () => this.props.children || <div />;
ok = () => this.props.children || <div className={"no-children"} />;
render() { return (this.state.hasError ? this.no : this.ok)(); }
}

View File

@ -256,15 +256,15 @@ export class EditFEForm extends React.Component<EditFEProps, EditFEFormState> {
};
}
fieldSet = (name: FarmEventViewModelKey, value: string) =>
fieldSet = (key: FarmEventViewModelKey, value: string) =>
// A merge is required to not overwrite `fe`.
this.setState(betterMerge(this.state, {
fe: { [name]: value },
fe: { [key]: value },
specialStatusLocal: SpecialStatus.DIRTY
}))
fieldGet = (name: FarmEventViewModelKey): string =>
(this.state.fe[name] || this.viewModel[name] || "").toString()
fieldGet = (key: FarmEventViewModelKey): string =>
(this.state.fe[key] || this.viewModel[key] || "").toString()
nextItemTime = (fe: FarmEvent, now: moment.Moment
): moment.Moment | undefined => {
@ -390,8 +390,8 @@ export class EditFEForm extends React.Component<EditFEProps, EditFEFormState> {
export interface StartTimeFormProps {
isRegimen: boolean;
fieldGet(name: FarmEventViewModelKey): string;
fieldSet(name: FarmEventViewModelKey, value: string): void;
fieldGet(key: FarmEventViewModelKey): string;
fieldSet(key: FarmEventViewModelKey, value: string): void;
timeSettings: TimeSettings;
}
@ -426,8 +426,8 @@ export const StartTimeForm = (props: StartTimeFormProps) => {
export interface RepeatFormProps {
isRegimen: boolean;
fieldGet(name: FarmEventViewModelKey): string;
fieldSet(name: FarmEventViewModelKey, value: string): void;
fieldGet(key: FarmEventViewModelKey): string;
fieldSet(key: FarmEventViewModelKey, value: string): void;
timeSettings: TimeSettings;
}
@ -437,13 +437,14 @@ export const RepeatForm = (props: RepeatFormProps) => {
{!props.isRegimen
? <label>
<input type="checkbox"
name="timeUnit"
onChange={e => props.fieldSet("timeUnit",
(!e.currentTarget.checked || props.isRegimen) ? "never" : "daily")}
disabled={props.isRegimen}
checked={allowRepeat} />
{t("Repeats?")}
</label>
: <div />}
: <div className={"no-repeat"} />}
<FarmEventRepeatForm
timeSettings={props.timeSettings}
disabled={!allowRepeat}
@ -459,7 +460,7 @@ export const RepeatForm = (props: RepeatFormProps) => {
};
export const dateCheck = (
fieldGet: (name: FarmEventViewModelKey) => string
fieldGet: (key: FarmEventViewModelKey) => string
): string | undefined => {
const startDate = fieldGet("startDate");
const endDate = fieldGet("endDate");
@ -469,7 +470,7 @@ export const dateCheck = (
};
export const timeCheck = (
fieldGet: (name: FarmEventViewModelKey) => string,
fieldGet: (key: FarmEventViewModelKey) => string,
timeSettings: TimeSettings
): string | undefined => {
const startDate = fieldGet("startDate");
@ -491,6 +492,7 @@ export interface FarmEventDeleteButtonProps {
export const FarmEventDeleteButton = (props: FarmEventDeleteButtonProps) =>
<button className="fb-button red" hidden={props.hidden}
title={t("Delete")}
onClick={() =>
props.dispatch(destroy(props.farmEvent.uuid))
.then(() => {
@ -502,8 +504,8 @@ export const FarmEventDeleteButton = (props: FarmEventDeleteButtonProps) =>
export interface FarmEventFormProps {
isRegimen: boolean;
fieldGet(name: FarmEventViewModelKey): string;
fieldSet(name: FarmEventViewModelKey, value: string): void;
fieldGet(key: FarmEventViewModelKey): string;
fieldSet(key: FarmEventViewModelKey, value: string): void;
timeSettings: TimeSettings;
executableOptions: DropDownItem[];
executableSet(ddi: DropDownItem): void;

View File

@ -15,7 +15,7 @@ export interface FarmEventRepeatFormProps {
disabled: boolean;
/** Should the form be shown _at all_? */
hidden: boolean;
fieldSet(name: keyof FarmEventViewModel, value: string): void;
fieldSet(key: keyof FarmEventViewModel, value: string): void;
timeUnit: TimeUnit;
repeat: string;
endDate: string;
@ -30,53 +30,55 @@ const OPTN_LOOKUP = keyBy(repeatOptions, indexKey);
export function FarmEventRepeatForm(props: FarmEventRepeatFormProps) {
const { disabled, fieldSet, repeat, endDate, endTime, timeUnit } = props;
return props.hidden ? <div /> : <div className="farm-event-repeat-form">
<label>
{t("Every")}
</label>
<Row>
<Col xs={4}>
<BlurableInput
disabled={disabled}
placeholder="(Number)"
type="number"
className="add-event-repeat-frequency"
name="repeat"
value={repeat}
onCommit={e => fieldSet("repeat", e.currentTarget.value)}
min={1} />
</Col>
<Col xs={8}>
<FBSelect
list={repeatOptions}
onChange={ddi => fieldSet("timeUnit", "" + ddi.value)}
selectedItem={OPTN_LOOKUP[timeUnit] || OPTN_LOOKUP["daily"]} />
</Col>
</Row>
<label>
{t("Until")}
</label>
<Row>
<Col xs={6}>
<BlurableInput
disabled={disabled}
type="date"
className="add-event-end-date"
name="endDate"
value={endDate}
onCommit={e => fieldSet("endDate", e.currentTarget.value)}
error={props.dateError} />
</Col>
<Col xs={6}>
<EventTimePicker
disabled={disabled}
className="add-event-end-time"
name="endTime"
timeSettings={props.timeSettings}
value={endTime}
onCommit={e => fieldSet("endTime", e.currentTarget.value)}
error={props.timeError} />
</Col>
</Row>
</div>;
return props.hidden
? <div className={"no-repeat-form"} />
: <div className="farm-event-repeat-form">
<label>
{t("Every")}
</label>
<Row>
<Col xs={4}>
<BlurableInput
disabled={disabled}
placeholder="(Number)"
type="number"
className="add-event-repeat-frequency"
name="repeat"
value={repeat}
onCommit={e => fieldSet("repeat", e.currentTarget.value)}
min={1} />
</Col>
<Col xs={8}>
<FBSelect
list={repeatOptions}
onChange={ddi => fieldSet("timeUnit", "" + ddi.value)}
selectedItem={OPTN_LOOKUP[timeUnit] || OPTN_LOOKUP["daily"]} />
</Col>
</Row>
<label>
{t("Until")}
</label>
<Row>
<Col xs={6}>
<BlurableInput
disabled={disabled}
type="date"
className="add-event-end-date"
name="endDate"
value={endDate}
onCommit={e => fieldSet("endDate", e.currentTarget.value)}
error={props.dateError} />
</Col>
<Col xs={6}>
<EventTimePicker
disabled={disabled}
className="add-event-end-time"
name="endTime"
timeSettings={props.timeSettings}
value={endTime}
onCommit={e => fieldSet("endTime", e.currentTarget.value)}
error={props.timeError} />
</Col>
</Row>
</div>;
}

View File

@ -109,6 +109,7 @@ export class PureFarmEvents
noIcon={true}>
<i className="fa fa-calendar" onClick={this.resetCalendar} />
<input
name="searchTerm"
value={this.state.searchTerm}
onChange={e => this.setState({ searchTerm: e.currentTarget.value })}
placeholder={t("Search your events...")} />

View File

@ -49,10 +49,10 @@ export const gridOffset: AxisNumberProperty = { x: 50, y: 50 };
export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
initializeSetting =
(name: keyof State, defaultValue: boolean): boolean => {
const currentValue = this.props.getConfigValue(name);
(key: keyof State, defaultValue: boolean): boolean => {
const currentValue = this.props.getConfigValue(key);
if (isUndefined(currentValue)) {
this.props.dispatch(setWebAppConfigValue(name, defaultValue));
this.props.dispatch(setWebAppConfigValue(key, defaultValue));
return defaultValue;
} else {
return !!currentValue;
@ -87,10 +87,10 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
this.updateZoomLevel(0)();
}
toggle = (name: keyof State) => () => {
const newValue = !this.state[name];
this.props.dispatch(setWebAppConfigValue(name, newValue));
this.setState({ [name]: newValue });
toggle = (key: keyof State) => () => {
const newValue = !this.state[key];
this.props.dispatch(setWebAppConfigValue(key, newValue));
this.setState({ [key]: newValue });
}
updateBotOriginQuadrant = (payload: BotOriginQuadrant) => () => {

View File

@ -125,6 +125,7 @@ export const BugsControls = () =>
? <div className="more-bugs">
<button
className="fb-button green"
title={t("more bugs!")}
onClick={resetBugs}>
{t("more bugs!")}
</button>
@ -133,4 +134,4 @@ export const BugsControls = () =>
{t("{{seconds}} seconds!", { seconds: getBugTime() })}
</p>}
</div>
: <div />;
: <div className={"no-bugs"} />;

View File

@ -9,7 +9,7 @@ import {
describe("<BotFigure/>", () => {
const fakeProps = (): BotFigureProps => ({
name: "",
figureName: "",
position: { x: 0, y: 0, z: 0 },
mapTransformProps: fakeMapTransformProps(),
plantAreaOffset: { x: 100, y: 100 },
@ -30,11 +30,11 @@ describe("<BotFigure/>", () => {
["motors", 4, { x: 3000, y: 1500 }, true, EXPECTED_MOTORS_OPACITY],
["encoders", 2, { x: 0, y: 0 }, false, 0.25],
])("shows %s in correct location for quadrant %i",
(name, quadrant, expected, xySwap, opacity) => {
(figureName, quadrant, expected, xySwap, opacity) => {
const p = fakeProps();
p.mapTransformProps.quadrant = quadrant;
p.mapTransformProps.xySwap = xySwap;
p.name = name;
p.figureName = figureName;
const result = shallow<BotFigure>(<BotFigure {...p} />);
const expectedGantryProps = expect.objectContaining({

View File

@ -5,6 +5,7 @@ import { VirtualFarmBotProps } from "../../../interfaces";
import {
fakeMapTransformProps
} from "../../../../../__test_support__/map_transform_props";
import { BotFigure } from "../bot_figure";
describe("<VirtualFarmBot/>", () => {
function fakeProps(): VirtualFarmBotProps {
@ -27,9 +28,9 @@ describe("<VirtualFarmBot/>", () => {
const p = fakeProps();
p.getConfigValue = () => false;
const wrapper = shallow(<VirtualFarmBot {...p} />);
const figures = wrapper.find("BotFigure");
const figures = wrapper.find(BotFigure);
expect(figures.length).toEqual(1);
expect(figures.last().props().name).toEqual("motor-position");
expect(figures.last().props().figureName).toEqual("motor-position");
});
it("shows trail", () => {
@ -39,8 +40,8 @@ describe("<VirtualFarmBot/>", () => {
it("shows encoder position", () => {
const wrapper = shallow(<VirtualFarmBot {...fakeProps()} />);
const figures = wrapper.find("BotFigure");
const figures = wrapper.find(BotFigure);
expect(figures.length).toEqual(2);
expect(figures.last().props().name).toEqual("encoder-position");
expect(figures.last().props().figureName).toEqual("encoder-position");
});
});

View File

@ -9,7 +9,7 @@ import { reduceToolName } from "../tool_slots/tool_slot_point";
import { noop } from "lodash";
export interface BotFigureProps {
name: string;
figureName: string;
position: BotPosition;
mapTransformProps: MapTransformProps;
plantAreaOffset: AxisNumberProperty;
@ -29,14 +29,14 @@ export class BotFigure extends
render() {
const {
name, position, plantAreaOffset, eStopStatus, mapTransformProps,
figureName, position, plantAreaOffset, eStopStatus, mapTransformProps,
} = this.props;
const { xySwap } = mapTransformProps;
const mapSize = getMapSize(mapTransformProps, plantAreaOffset);
const positionQ = transformXY(
(position.x || 0), (position.y || 0), mapTransformProps);
const color = eStopStatus ? Color.virtualRed : Color.darkGray;
const opacity = name.includes("encoder") ? 0.25 : 0.5;
const opacity = figureName.includes("encoder") ? 0.25 : 0.5;
const toolProps = {
x: positionQ.qx,
y: positionQ.qy,
@ -45,7 +45,7 @@ export class BotFigure extends
uuid: "utm",
xySwap,
};
return <g id={name}>
return <g id={figureName}>
<rect id="gantry"
x={xySwap ? -plantAreaOffset.x : positionQ.qx - 10}
y={xySwap ? positionQ.qy - 10 : -plantAreaOffset.y}

View File

@ -24,14 +24,14 @@ export function VirtualFarmBot(props: VirtualFarmBotProps) {
plantAreaOffset={plantAreaOffset}
peripherals={peripherals}
getConfigValue={getConfigValue} />
<BotFigure name={"motor-position"}
<BotFigure figureName={"motor-position"}
position={props.botLocationData.position}
mapTransformProps={mapTransformProps}
plantAreaOffset={plantAreaOffset}
mountedToolName={props.mountedToolName}
eStopStatus={eStopStatus} />
{encoderFigure &&
<BotFigure name={"encoder-position"}
<BotFigure figureName={"encoder-position"}
position={props.botLocationData.scaled_encoders}
mapTransformProps={mapTransformProps}
plantAreaOffset={plantAreaOffset} />}

View File

@ -17,12 +17,12 @@ const parse = (str: string | undefined) => {
};
/* Check if the image has been rotated according to the calibration value. */
const isRotated = (name: string | undefined, noCalib: boolean) => {
const isRotated = (annotation: string | undefined, noCalib: boolean) => {
if (PRE_CALIBRATION_PREVIEW && noCalib) { return true; }
return name &&
(name.includes("rotated")
|| name.includes("marked")
|| name.includes("calibration_result"));
return annotation &&
(annotation.includes("rotated")
|| annotation.includes("marked")
|| annotation.includes("calibration_result"));
};
/* Check if the calibration data is valid for the image provided using z. */

View File

@ -24,6 +24,7 @@ const LengthInput = (props: LengthInputProps) =>
<Col xs={5}>
<input
type="number"
name={props.setting}
value={"" + props.value}
onChange={e => props.dispatch(setWebAppConfigValue(
props.setting, e.currentTarget.value))} />

View File

@ -64,7 +64,7 @@ export class MoveToForm extends React.Component<MoveToFormProps, MoveToFormState
render() {
const { x, y } = this.props.chosenLocation;
const { botOnline } = this.props;
return <div>
return <div className={"move-to-form"}>
<Row>
<Col xs={4}>
<label>{t("X AXIS")}</label>
@ -78,10 +78,10 @@ export class MoveToForm extends React.Component<MoveToFormProps, MoveToFormState
</Row>
<Row>
<Col xs={4}>
<input disabled value={isNumber(x) ? x : "---"} />
<input disabled name="x" value={isNumber(x) ? x : "---"} />
</Col>
<Col xs={4}>
<input disabled value={isNumber(y) ? y : "---"} />
<input disabled name="y" value={isNumber(y) ? y : "---"} />
</Col>
<AxisInputBox
onChange={(_, val: number) => this.setState({ z: val })}
@ -91,7 +91,9 @@ export class MoveToForm extends React.Component<MoveToFormProps, MoveToFormState
<button
onClick={() => moveAbs(this.vector)}
className={`fb-button gray ${botOnline ? "" : "pseudo-disabled"}`}
title={botOnline ? "" : t(Content.NOT_AVAILABLE_WHEN_OFFLINE)}>
title={botOnline
? t("Move to this coordinate")
: t(Content.NOT_AVAILABLE_WHEN_OFFLINE)}>
{t("Move to this coordinate")}
</button>
</Row>

View File

@ -189,7 +189,7 @@ describe("<EditDatePlanted />", () => {
describe("<EditPlantLocation />", () => {
const fakeProps = (): EditPlantLocationProps => ({
uuid: "Plant.0.0",
location: { x: 1, y: 2 },
xyLocation: { x: 1, y: 2 },
updatePlant: jest.fn(),
});

View File

@ -30,7 +30,7 @@ interface APDProps {
}
const AddPlantDescription = ({ svgIcon, children }: APDProps) =>
<div>
<div className={"add-plant-description"}>
<img className="crop-drag-info-image"
src={svgToUrl(svgIcon)}
alt={t("plant icon")}

View File

@ -74,6 +74,7 @@ export class RawCropCatalog extends React.Component<CropCatalogProps, {}> {
onChange={this.handleChange}
onKeyPress={this.handleChange}
className="search"
name="searchTerm"
placeholder={t("Search OpenFarm...")} />
{this.showResultChangeSpinner &&
<Spinner radius={10} strokeWidth={3} />}

View File

@ -44,7 +44,7 @@ const InfoField = (props: InfoFieldProps) =>
<p>
{t(startCase(props.title))}
</p>
<div>
<div className={"crop-info-field-data"}>
{props.children}
</div>
</li>;
@ -67,11 +67,13 @@ const NO_VALUE = t("Not Set");
const SvgIcon = ({ i, field, value }: SummaryItemProps) =>
<InfoField key={i} title={field}>
{value
? <div><img
src={svgToUrl(value)}
width={100}
height={100}
onDragStart={setDragIcon(value)} /></div>
? <div className={"svg-img"}>
<img
src={svgToUrl(value)}
width={100}
height={100}
onDragStart={setDragIcon(value)} />
</div>
: <span>{NO_VALUE}</span>}
</InfoField>;
@ -149,6 +151,7 @@ const AddPlantHereButton = (props: {
dispatch, openedSavedGarden
}) : () => { };
return <button className="fb-button gray no-float"
title={t("Add plant at current location")}
disabled={!botXY} onClick={click}>
{t("Add plant at current FarmBot location {{coordinate}}",
{ coordinate: botXYLabel })}

View File

@ -23,14 +23,13 @@ describe("PlantGrid", () => {
const p = fakeProps();
const el = mount<PlantGrid>(<PlantGrid {...p} />);
// Upon load, there should be one button.
const previewButton = el.find("a.clear-button");
const previewButton = el.find("a.preview-button");
expect(previewButton.text()).toContain("Preview");
previewButton.simulate("click");
// After clicking PREVIEW, there should be two buttons.
const saveAndCancelBtns = el.find("a.clear-button");
const cancel = saveAndCancelBtns.at(0);
const save = saveAndCancelBtns.at(1);
const cancel = el.find("a.cancel-button");
const save = el.find("a.save-button");
expect(cancel.text()).toContain("Cancel");
expect(save.text()).toContain("Save");
expect(el.state().status).toEqual("dirty");

View File

@ -77,17 +77,23 @@ export class PlantGrid extends React.Component<PlantGridProps, PlantGridState> {
buttons = () => {
switch (this.state.status) {
case "clean":
return <div>
<a className={"clear-button"} onClick={this.performPreview}>
return <div className={"preview-grid-button"}>
<a className={"preview-button"}
title={t("Preview")}
onClick={this.performPreview}>
{t("Preview")}
</a>
</div>;
case "dirty":
return <div>
<a className={"clear-button"} onClick={this.revertPreview}>
return <div className={"save-or-cancel-grid-button"}>
<a className={"cancel-button"}
title={t("Cancel")}
onClick={this.revertPreview}>
{t("Cancel")}
</a>
<a className={"clear-button"} onClick={this.saveGrid}>
<a className={"save-button"}
title={t("Save")}
onClick={this.saveGrid}>
{t("Save")}
</a>
</div>;
@ -95,7 +101,7 @@ export class PlantGrid extends React.Component<PlantGridProps, PlantGridState> {
}
render() {
return <div>
return <div className={"grid-and-row-planting"}>
<hr style={{ borderTop: "1.5px solid rgba(255, 255, 255 ,0.7)" }} />
<h3>
{t("Grid and Row Planting")}

View File

@ -32,33 +32,31 @@ export class OpenFarmResults extends React.Component<SearchResultProps, {}> {
}
render() {
return <div>
<EmptyStateWrapper
notEmpty={this.props.cropSearchResults.length > 0}
graphic={EmptyStateGraphic.no_crop_results}
title={this.props.cropSearchInProgress
? t("Searching...")
: t("No search results")}
textElement={this.props.cropSearchInProgress ? <div /> : this.text}
colorScheme={"plants"}>
{this.props.cropSearchResults.map(resp => {
const { crop, image } = resp;
return <Link
key={resp.crop.slug}
draggable={false}
to={`/app/designer/plants/crop_search/` + crop.slug.toString()}>
<div className="plant-catalog-tile col-xs-6">
<label>
{crop.name}
</label>
<div
className="plant-catalog-image"
style={{ background: `url(${image}) top center no-repeat` }}
draggable={false} />
</div>
</Link>;
})}
</EmptyStateWrapper>
</div>;
return <EmptyStateWrapper
notEmpty={this.props.cropSearchResults.length > 0}
graphic={EmptyStateGraphic.no_crop_results}
title={this.props.cropSearchInProgress
? t("Searching...")
: t("No search results")}
textElement={this.props.cropSearchInProgress ? undefined : this.text}
colorScheme={"plants"}>
{this.props.cropSearchResults.map(resp => {
const { crop, image } = resp;
return <Link
key={resp.crop.slug}
draggable={false}
to={`/app/designer/plants/crop_search/` + crop.slug.toString()}>
<div className="plant-catalog-tile col-xs-6">
<label>
{crop.name}
</label>
<div
className="plant-catalog-image"
style={{ background: `url(${image}) top center no-repeat` }}
draggable={false} />
</div>
</Link>;
})}
</EmptyStateWrapper>;
}
}

View File

@ -47,7 +47,7 @@ export class RawPlants extends React.Component<PlantInventoryProps, State> {
panel={Panel.Plants}
linkTo={"/app/designer/plants/crop_search"}
title={t("Add plant")}>
<input type="text" onChange={this.update}
<input type="text" onChange={this.update} name="searchTerm"
placeholder={t("Search your plants...")} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"plant"}>

View File

@ -51,18 +51,18 @@ export const EditDatePlanted = (props: EditDatePlantedProps) => {
};
export interface EditPlantLocationProps extends EditPlantProperty {
location: Record<"x" | "y", number>;
xyLocation: Record<"x" | "y", number>;
}
export const EditPlantLocation = (props: EditPlantLocationProps) => {
const { location, updatePlant, uuid } = props;
const { xyLocation, updatePlant, uuid } = props;
return <Row>
{["x", "y"].map((axis: "x" | "y") =>
<Col xs={6} key={axis}>
<label style={{ marginTop: 0 }}>{t("{{axis}} (mm)", { axis })}</label>
<BlurableInput
type="number"
value={location[axis]}
value={xyLocation[axis]}
min={0}
onCommit={e => updatePlant(uuid, {
[axis]: round(parseIntInput(e.currentTarget.value))
@ -89,6 +89,7 @@ interface MoveToPlantProps {
const MoveToPlant = (props: MoveToPlantProps) =>
<button className="fb-button gray no-float"
style={{ marginTop: "1rem" }}
title={t("Move to this plant")}
onClick={() => props.dispatch(chooseLocation({ x: props.x, y: props.y }))
.then(() => history.push("/app/designer/move_to"))}>
{t("Move FarmBot to this plant")}
@ -99,20 +100,22 @@ interface DeleteButtonsProps {
}
const DeleteButtons = (props: DeleteButtonsProps) =>
<div>
<div>
<div className={"plant-delete-buttons"}>
<div className={"plant-delete-button-label"}>
<label>
{t("Delete this plant")}
</label>
</div>
<button
className="fb-button red no-float"
title={t("Delete")}
onClick={props.destroy}>
{t("Delete")}
</button>
<button
className="fb-button gray no-float"
style={{ marginRight: "10px" }}
title={t("Delete multiple")}
onClick={() => history.push("/app/designer/plants/select")}>
{t("Delete multiple")}
</button>
@ -128,7 +131,7 @@ export const ListItem = (props: ListItemProps) =>
<p>
{props.name}
</p>
<div>
<div className={"plant-info-field-data"}>
{props.children}
</div>
</li>;
@ -171,7 +174,7 @@ export function PlantPanel(props: PlantPanelProps) {
</Row>}
<ListItem name={t("Location")}>
<EditPlantLocation uuid={uuid}
location={{ x, y }}
xyLocation={{ x, y }}
updatePlant={updatePlant} />
</ListItem>
<MoveToPlant x={x} y={y} dispatch={dispatch} />

View File

@ -60,10 +60,12 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
<div className="panel-action-buttons">
<div className="button-row">
<button className="fb-button gray"
title={t("Select none")}
onClick={() => this.props.dispatch(selectPlant(undefined))}>
{t("Select none")}
</button>
<button className="fb-button gray"
title={t("Select all")}
onClick={() => this.props
.dispatch(selectPlant(this.props.plants.map(p => p.uuid)))}>
{t("Select all")}
@ -72,10 +74,12 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
<label>{t("SELECTION ACTIONS")}</label>
<div className="button-row">
<button className="fb-button red"
title={t("Delete")}
onClick={() => this.destroySelected(this.props.selected)}>
{t("Delete")}
</button>
<button className="fb-button dark-blue"
title={t("Create group")}
onClick={() => !this.props.gardenOpen
? this.props.dispatch(createGroup({ pointUuids: this.selected }))
: error(t(Content.ERROR_PLANT_TEMPLATE_GROUP))}>

View File

@ -4,9 +4,9 @@ import { TaggedPoint } from "farmbot";
import { fakePlant } from "../../../__test_support__/fake_state/resources";
describe("sort()", () => {
const phony = (name: string, x: number, y: number): TaggedPoint => {
const phony = (plantName: string, x: number, y: number): TaggedPoint => {
const plant = fakePlant();
plant.body.name = name;
plant.body.name = plantName;
plant.body.x = x;
plant.body.y = y;
return plant;

View File

@ -45,12 +45,15 @@ export class AddEqCriteria<T extends string | number>
</Col>
<Col xs={4}>
<input type={this.props.type}
name="value"
placeholder={t("value")}
value={this.state.value}
onChange={e => this.setState({ value: e.currentTarget.value })} />
</Col>
<Col xs={2}>
<button className="fb-button green" onClick={this.commit}>
<button className="fb-button green"
title={t("add criteria")}
onClick={this.commit}>
<i className="fa fa-plus" />
</button>
</Col>
@ -149,7 +152,9 @@ export class AddStringCriteria
onChange={this.change} />
</Col>
<Col xs={2}>
<button className="fb-button green" onClick={this.commit}>
<button className="fb-button green"
title={t("add string criteria")}
onClick={this.commit}>
<i className="fa fa-plus" />
</button>
</Col>
@ -182,6 +187,7 @@ export class AddNumberCriteria
<Row>
<Col xs={4}>
<input type="string"
name="key"
placeholder={t("field")}
value={this.state.key}
onChange={this.changeKey} />
@ -191,11 +197,14 @@ export class AddNumberCriteria
</Col>
<Col xs={4}>
<input type="number"
name="value"
value={this.state.value}
onChange={this.changeValue} />
</Col>
<Col xs={2}>
<button className="fb-button green" onClick={this.commit}>
<button className="fb-button green"
title={t("add number criteria")}
onClick={this.commit}>
<i className="fa fa-plus" />
</button>
</Col>

View File

@ -21,12 +21,14 @@ export class GroupCriteria extends
const commonProps = { group, criteria, dispatch };
return <div className="group-criteria">
<label className="criteria-heading">{t("criteria")}</label>
<button className="fb-button red" onClick={() => {
dispatch(overwrite(group, {
...group.body, criteria: DEFAULT_CRITERIA
}));
dispatch(save(group.uuid));
}}>
<button className="fb-button red"
title={t("clear all criteria")}
onClick={() => {
dispatch(overwrite(group, {
...group.body, criteria: DEFAULT_CRITERIA
}));
dispatch(save(group.uuid));
}}>
{t("clear all criteria")}
</button>
<div className="group-criteria-presets">
@ -60,13 +62,13 @@ export class GroupCriteria extends
export const GroupPointCountBreakdown = (props: GroupPointCountBreakdownProps) =>
<div className={"criteria-point-count-breakdown"}>
<div className={"manual-group-member-count"}>
<div>
<div className={"manual-selection-count"}>
{props.manualCount}
</div>
<p>{t("manually selected")}</p>
</div>
<div className={"criteria-group-member-count"}>
<div>
<div className={"criteria-selection-count"}>
{props.totalCount - props.manualCount}
</div>
<p>{t("selected by criteria")}</p>

View File

@ -32,18 +32,20 @@ export class EqCriteriaSelection<T extends string | number>
{values.map((value, valueIndex) =>
<Row key={"" + keyIndex + valueIndex}>
<Col xs={9}>
<input
<input name="value"
disabled={true}
value={value} />
</Col>
<Col xs={2}>
<button className="fb-button red" onClick={() => {
const tempCriteriaField = cloneDeep(criteriaField);
toggleEqCriteria<T>(tempCriteriaField)(key, value);
dispatch(editCriteria(group, {
[criteriaKey]: tempCriteriaField
}));
}}>
<button className="fb-button red"
title={t("remove criteria")}
onClick={() => {
const tempCriteriaField = cloneDeep(criteriaField);
toggleEqCriteria<T>(tempCriteriaField)(key, value);
dispatch(editCriteria(group, {
[criteriaKey]: tempCriteriaField
}));
}}>
<i className="fa fa-minus" />
</button>
</Col>
@ -69,17 +71,20 @@ export const NumberCriteriaSelection = (props: NumberCriteriaProps) => {
</Col>
<Col xs={4}>
<input key={"" + keyIndex}
name="value"
disabled={true}
value={value} />
</Col>
<Col xs={2}>
<button className="fb-button red" onClick={() => {
const tempNumberCriteria = cloneDeep(criteriaField);
delete tempNumberCriteria[key];
props.dispatch(editCriteria(props.group, {
[props.criteriaKey]: tempNumberCriteria
}));
}}>
<button className="fb-button red"
title={t("remove number criteria")}
onClick={() => {
const tempNumberCriteria = cloneDeep(criteriaField);
delete tempNumberCriteria[key];
props.dispatch(editCriteria(props.group, {
[props.criteriaKey]: tempNumberCriteria
}));
}}>
<i className="fa fa-minus" />
</button>
</Col>
@ -112,11 +117,12 @@ export const DaySelection = (props: CriteriaSelectionProps) => {
}))} />
</Col>
<Col xs={3}>
<input type="number" value={dayCriteria.days_ago} onChange={e => {
const { op } = dayCriteria;
const days_ago = parseInt(e.currentTarget.value);
dispatch(editCriteria(group, { day: { days_ago, op } }));
}} />
<input type="number" value={dayCriteria.days_ago} name="days_ago"
onChange={e => {
const { op } = dayCriteria;
const days_ago = parseInt(e.currentTarget.value);
dispatch(editCriteria(group, { day: { days_ago, op } }));
}} />
</Col>
<Col xs={4}>
<p>{t("days old")}</p>
@ -136,6 +142,7 @@ export const LocationSelection = (props: LocationSelectionProps) => {
<Col xs={4}>
<input key={JSON.stringify(gtCriteria)}
type="number"
name={`${axis}-number-gt`}
defaultValue={gtCriteria[axis]}
onBlur={e => {
const tempGtCriteria = cloneDeep(gtCriteria);
@ -155,6 +162,7 @@ export const LocationSelection = (props: LocationSelectionProps) => {
<Col xs={4}>
<input key={JSON.stringify(ltCriteria)}
type="number"
name={`${axis}-number-lt`}
defaultValue={ltCriteria[axis]}
onBlur={e => {
const tempLtCriteria = cloneDeep(ltCriteria);
@ -195,14 +203,19 @@ export class AddCriteria
<Row>
<Col xs={5}>
<input value={CRITERIA_TYPE_DDI_LOOKUP()[key].label}
name="key"
disabled={true} />
</Col>
<Col xs={5}>
<input value={this.labelLookup(key, value)} disabled={true} />
<input value={this.labelLookup(key, value)}
name="value"
disabled={true} />
</Col>
<Col xs={2}>
<button className="fb-button red" onClick={() => props.dispatch(
toggleStringCriteria(props.group, key, value))}>
<button className="fb-button red"
title={t("remove criteria")}
onClick={() => props.dispatch(
toggleStringCriteria(props.group, key, value))}>
<i className="fa fa-minus" />
</button>
</Col>

View File

@ -50,6 +50,7 @@ export class RawGroupListPanel extends React.Component<GroupListPanelProps, Stat
linkTo={"/app/designer/plants/select"}
title={t("Add group")}>
<input type="text"
name="searchTerm"
onChange={this.update}
placeholder={t("Search your groups...")} />
</DesignerPanelTop>

View File

@ -47,7 +47,7 @@ describe("<EditPointName />", () => {
describe("<EditPointLocation />", () => {
const fakeProps = (): EditPointLocationProps => ({
updatePoint: jest.fn(),
location: { x: 1, y: 2 },
xyLocation: { x: 1, y: 2 },
});
it("edits location", () => {

View File

@ -187,7 +187,7 @@ export class RawCreatePoints
PointProperties = () =>
<ul>
<li>
<div>
<div className={"point-name-input"}>
<label>{t("Name")}</label>
<BlurableInput
name="name"
@ -241,6 +241,7 @@ export class RawCreatePoints
PointActions = () =>
<Row>
<button className="fb-button green"
title={t("save")}
onClick={this.createPoint}>
{t("Save")}
</button>
@ -254,6 +255,7 @@ export class RawCreatePoints
? t("Delete all of the weeds created through this panel.")
: t("Delete all of the points created through this panel.")}</p>
<button className="fb-button red delete"
title={t("delete all")}
onClick={() => {
if (confirm(type === "weed"
? t("Delete all the weeds you have created?")

View File

@ -27,7 +27,7 @@ export interface EditPointPropertiesProps {
export const EditPointProperties = (props: EditPointPropertiesProps) =>
<ul>
<li>
<div>
<div className={"point-name-input"}>
<EditPointName
name={props.point.body.name}
updatePoint={props.updatePoint} />
@ -35,7 +35,7 @@ export const EditPointProperties = (props: EditPointPropertiesProps) =>
</li>
<ListItem name={t("Location")}>
<EditPointLocation
location={{ x: props.point.body.x, y: props.point.body.y }}
xyLocation={{ x: props.point.body.x, y: props.point.body.y }}
updatePoint={props.updatePoint} />
</ListItem>
<ListItem name={t("Size")}>
@ -59,15 +59,17 @@ export interface PointActionsProps {
}
export const PointActions = ({ x, y, z, uuid, dispatch }: PointActionsProps) =>
<div>
<div className={"point-actions"}>
<button
className="fb-button gray no-float"
type="button"
title={t("move to location")}
onClick={() => getDevice().moveAbsolute({ x, y, z })}>
{t("Move Device to location")}
</button>
<button
className="fb-button red no-float"
title={t("delete")}
onClick={() => dispatch(destroy(uuid))}>
{t("Delete")}
</button>
@ -84,6 +86,7 @@ export const EditPointName = (props: EditPointNameProps) =>
<label>{t("Name")}</label>
<BlurableInput
type="text"
name="name"
value={props.name}
onCommit={e => props.updatePoint({ name: e.currentTarget.value })} />
</Col>
@ -91,7 +94,7 @@ export const EditPointName = (props: EditPointNameProps) =>
export interface EditPointLocationProps {
updatePoint(update: Partial<TaggedGenericPointer["body"]>): void;
location: Record<"x" | "y", number>;
xyLocation: Record<"x" | "y", number>;
}
export const EditPointLocation = (props: EditPointLocationProps) =>
@ -101,7 +104,8 @@ export const EditPointLocation = (props: EditPointLocationProps) =>
<label style={{ marginTop: 0 }}>{t("{{axis}} (mm)", { axis })}</label>
<BlurableInput
type="number"
value={props.location[axis]}
name={axis}
value={props.xyLocation[axis]}
min={0}
onCommit={e => props.updatePoint({
[axis]: round(parseIntInput(e.currentTarget.value))
@ -120,6 +124,7 @@ export const EditPointRadius = (props: EditPointRadiusProps) =>
<label style={{ marginTop: 0 }}>{t("radius (mm)")}</label>
<BlurableInput
type="number"
name="radius"
value={props.radius}
min={0}
onCommit={e => props.updatePoint({

View File

@ -50,7 +50,7 @@ export class RawPoints extends React.Component<PointsProps, PointsState> {
panel={Panel.Points}
linkTo={"/app/designer/points/add"}
title={t("Add point")}>
<input type="text" onChange={this.update}
<input type="text" onChange={this.update} name="searchTerm"
placeholder={t("Search your points...")} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"points"}>

View File

@ -49,7 +49,7 @@ export class RawWeeds extends React.Component<WeedsProps, WeedsState> {
panel={Panel.Weeds}
linkTo={"/app/designer/weeds/add"}
title={t("Add weed")}>
<input type="text" onChange={this.update}
<input type="text" onChange={this.update} name="searchTerm"
placeholder={t("Search your weeds...")} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"weeds-inventory"}>

View File

@ -12,6 +12,7 @@ jest.mock("../../../api/crud", () => ({
let mockPath = "";
jest.mock("../../../history", () => ({
history: { push: jest.fn() },
getPathArray: jest.fn(() => mockPath.split("/")),
}));

View File

@ -46,14 +46,14 @@ describe("<GardenSnapshot />", () => {
wrapper.find("input").first().simulate("change", {
currentTarget: { value: "new name" }
});
expect(wrapper.instance().state.name).toEqual("new name");
expect(wrapper.instance().state.gardenName).toEqual("new name");
});
it("creates new garden", () => {
const wrapper = shallow<GardenSnapshot>(<GardenSnapshot {...fakeProps()} />);
wrapper.setState({ name: "new saved garden" });
wrapper.setState({ gardenName: "new saved garden" });
wrapper.find("button").last().simulate("click");
expect(newSavedGarden).toHaveBeenCalledWith("new saved garden");
expect(wrapper.instance().state.name).toEqual("");
expect(wrapper.instance().state.gardenName).toEqual("");
});
});

View File

@ -11,8 +11,9 @@ import { t } from "../../i18next_wrapper";
import { stopTracking } from "../../connectivity/data_consistency";
/** Save all Plant to PlantTemplates in a new SavedGarden. */
export const snapshotGarden = (name?: string | undefined) =>
axios.post<void>(API.current.snapshotPath, name ? { name } : {})
export const snapshotGarden = (gardenName?: string | undefined) =>
axios.post<void>(API.current.snapshotPath, gardenName
? { name: gardenName } : {})
.then(() => {
success(t("Garden Saved."));
history.push("/app/designer/gardens");
@ -63,9 +64,9 @@ export const openOrCloseGarden = (props: {
: props.dispatch(closeSavedGarden());
/** Create a new SavedGarden with the chosen name. */
export const newSavedGarden = (name: string) =>
export const newSavedGarden = (gardenName: string) =>
(dispatch: Function) => {
dispatch(initSave("SavedGarden", { name: name || "Untitled Garden" }))
dispatch(initSave("SavedGarden", { name: gardenName || "Untitled Garden" }))
.then(() => {
success(t("Garden Saved."));
history.push("/app/designer/gardens");
@ -93,8 +94,8 @@ export const copySavedGarden = ({ newSGName, savedGarden, plantTemplates }: {
}) =>
(dispatch: Function) => {
const sourceSavedGardenId = savedGarden.body.id;
const name = newSGName || `${savedGarden.body.name} (${t("copy")})`;
dispatch(initSaveGetId(savedGarden.kind, { name }))
const gardenName = newSGName || `${savedGarden.body.name} (${t("copy")})`;
dispatch(initSaveGetId(savedGarden.kind, { name: gardenName }))
.then((newSGId: number) => {
plantTemplates
.filter(x => x.body.saved_garden_id === sourceSavedGardenId)

View File

@ -13,7 +13,7 @@ import { Everything } from "../../interfaces";
import {
DesignerPanel, DesignerPanelHeader, DesignerPanelContent
} from "../designer_panel";
import { getPathArray } from "../../history";
import { history, getPathArray } from "../../history";
import { isNumber } from "lodash";
import { ResourceIndex } from "../../resources/interfaces";
import { t } from "../../i18next_wrapper";
@ -28,6 +28,7 @@ const GardenViewButton = (props: GardenViewButtonProps) => {
: t("view");
return <button
className={`fb-button ${gardenIsOpen ? "gray" : "yellow"}`}
title={btnText}
onClick={onClick}>
{btnText}
</button>;
@ -38,6 +39,7 @@ const ApplyGardenButton =
(props: { plantPointerCount: number, gardenId: number, dispatch: Function }) =>
<button
className="fb-button green"
title={t("apply garden")}
onClick={() => props.plantPointerCount > 0
? error(trim(`${t("Please clear current garden first.")}
(${props.plantPointerCount} ${t("plants")})`))
@ -49,6 +51,7 @@ const DestroyGardenButton =
(props: { dispatch: Function, gardenUuid: string }) =>
<button
className="fb-button red"
title={t("delete garden")}
onClick={() => props.dispatch(destroySavedGarden(props.gardenUuid))}>
{t("delete")}
</button>;
@ -75,6 +78,7 @@ export const mapStateToProps = (props: Everything): EditGardenProps => {
export class RawEditGarden extends React.Component<EditGardenProps, {}> {
render() {
const { savedGarden } = this.props;
!savedGarden && history.push("/app/designer/gardens");
return <DesignerPanel panelName={"saved-garden-edit"}
panel={Panel.SavedGardens}>
<DesignerPanelHeader
@ -84,7 +88,7 @@ export class RawEditGarden extends React.Component<EditGardenProps, {}> {
backTo={"/app/designer/gardens"} />
<DesignerPanelContent panelName={"saved-garden-edit"}>
{savedGarden
? <div>
? <div className={"saved-garden-content"}>
<Row>
<label>{t("name")}</label>
<BlurableInput

View File

@ -10,44 +10,46 @@ export interface GardenSnapshotProps {
}
interface GardenSnapshotState {
name: string;
gardenName: string;
}
/** New SavedGarden name input and snapshot/create buttons. */
export class GardenSnapshot
extends React.Component<GardenSnapshotProps, GardenSnapshotState> {
state = { name: "" };
state = { gardenName: "" };
snapshot = () => {
const { currentSavedGarden, plantTemplates } = this.props;
!currentSavedGarden
? snapshotGarden(this.state.name)
? snapshotGarden(this.state.gardenName)
: this.props.dispatch(copySavedGarden({
newSGName: this.state.name,
newSGName: this.state.gardenName,
savedGarden: currentSavedGarden,
plantTemplates
}));
this.setState({ name: "" });
this.setState({ gardenName: "" });
}
new = () => {
this.props.dispatch(newSavedGarden(this.state.name));
this.setState({ name: "" });
this.props.dispatch(newSavedGarden(this.state.gardenName));
this.setState({ gardenName: "" });
};
render() {
return <div className="garden-snapshot">
<label>{t("name")}</label>
<input
onChange={e => this.setState({ name: e.currentTarget.value })}
value={this.state.name} />
<input name="name"
onChange={e => this.setState({ gardenName: e.currentTarget.value })}
value={this.state.gardenName} />
<button
className={"fb-button gray wide"}
title={t("Snapshot current garden")}
onClick={this.snapshot}>
{t("Snapshot current garden")}
</button>
<button
className="fb-button green wide"
title={t("Add new garden")}
onClick={this.new}>
{t("Add new garden")}
</button>

View File

@ -46,7 +46,7 @@ export class RawSavedGardens
panel={Panel.SavedGardens}
linkTo={"/app/designer/gardens/add"}
title={t("Add garden")}>
<input type="text" onChange={this.onChange}
<input type="text" onChange={this.onChange} name="searchTerm"
placeholder={t("Search your gardens...")} />
</DesignerPanelTop>
<EmptyStateWrapper
@ -66,6 +66,7 @@ export class RawSavedGardens
export const SavedGardensLink = () =>
<button className="fb-button green"
hidden={true}
title={t("open saved gardens panel")}
onClick={() => history.push("/app/designer/gardens")}>
{t("Saved Gardens")}
</button>;
@ -80,10 +81,12 @@ export const SavedGardenHUD = (props: { dispatch: Function }) =>
<div className="saved-garden-indicator">
<label>{t("Viewing saved garden")}</label>
<button className="fb-button gray"
title={t("open saved gardens panel")}
onClick={() => history.push("/app/designer/gardens")}>
{t("Menu")}
</button>
<button className="fb-button green"
title={t("open plants panel")}
onClick={() => {
history.push("/app/designer/plants");
unselectPlant(props.dispatch)();
@ -91,6 +94,7 @@ export const SavedGardenHUD = (props: { dispatch: Function }) =>
{t("Edit")}
</button>
<button className="fb-button red"
title={t("close saved garden")}
onClick={() => props.dispatch(closeSavedGarden())}>
{t("Exit")}
</button>

View File

@ -99,6 +99,7 @@ export class RawAddTool extends React.Component<AddToolProps, AddToolState> {
return <div className={`fb-checkbox ${alreadyAdded ? "disabled" : ""}`}>
<input type="checkbox" key={JSON.stringify(this.state.toAdd)}
title={alreadyAdded ? t("Already added.") : ""}
name="toolName"
checked={checked}
onChange={() => checked
? this.remove(toolName)
@ -118,6 +119,7 @@ export class RawAddTool extends React.Component<AddToolProps, AddToolState> {
</ul>
<button
className="fb-button green"
title={t("add selected stock names")}
onClick={() => {
this.state.toAdd.filter(this.filterExisting)
.map(n => this.newTool(n));
@ -141,6 +143,7 @@ export class RawAddTool extends React.Component<AddToolProps, AddToolState> {
<ToolSVG toolName={this.state.toolName} />
<label>{t("Name")}</label>
<input defaultValue={this.state.toolName}
name="name"
onChange={e =>
this.setState({ toolName: e.currentTarget.value })} />
<SaveBtn onClick={this.save} status={SpecialStatus.DIRTY} />

View File

@ -226,7 +226,7 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
panel={Panel.Tools}
linkTo={!hasTools ? "/app/designer/tools/add" : undefined}
title={!hasTools ? this.strings.titleText : undefined}>
<input type="text" onChange={this.update}
<input type="text" onChange={this.update} name="searchTerm"
placeholder={this.strings.placeholder} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"tools"}>

View File

@ -19,7 +19,7 @@ export interface GantryMountedInputProps {
export const GantryMountedInput = (props: GantryMountedInputProps) =>
<fieldset className="gantry-mounted-input">
<label>{t("Gantry-mounted")}</label>
<input type="checkbox"
<input type="checkbox" name="gantry_mounted"
onChange={() => props.onChange({ gantry_mounted: !props.gantryMounted })}
checked={props.gantryMounted} />
</fieldset>;
@ -120,7 +120,7 @@ export const SlotLocationInputRow = (props: SlotLocationInputRowProps) =>
<Col xs={4} key={axis}>
<label>{t("{{axis}} (mm)", { axis })}</label>
{axis == "x" && props.gantryMounted
? <input disabled value={t("Gantry")} />
? <input disabled value={t("Gantry")} name={axis} />
: <BlurableInput
type="number"
value={props.slotLocation[axis]}

View File

@ -53,7 +53,7 @@ export class RawZones extends React.Component<ZonesProps, ZonesState> {
}))
.then((id: number) => this.navigate(id)).catch(() => { })}
title={t("Add zone")}>
<input type="text" onChange={this.update}
<input type="text" onChange={this.update} name="searchTerm"
placeholder={t("Search your zones...")} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"zones-inventory"}>

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