input box updates
parent
5e7c9cdd1c
commit
ed56ef7fb5
|
@ -9,6 +9,7 @@ import {
|
|||
} from "./interfaces";
|
||||
import * as _ from "lodash";
|
||||
|
||||
/** Coordinate input and GO button for Move widget. */
|
||||
export class AxisInputBoxGroup extends
|
||||
React.Component<AxisInputBoxGroupProps, Partial<AxisInputBoxGroupState>> {
|
||||
constructor(props: AxisInputBoxGroupProps) {
|
||||
|
|
|
@ -6,6 +6,7 @@ import { Xyz } from "../../devices/interfaces";
|
|||
import { AxisInputBox } from "../axis_input_box";
|
||||
import { isNumber } from "lodash";
|
||||
import { LocationSelectionProps } from "./interfaces";
|
||||
import { parseIntInput } from "../../util";
|
||||
|
||||
/** Select a location filter for sensor readings. */
|
||||
export const LocationSelection =
|
||||
|
@ -33,8 +34,9 @@ export const LocationSelection =
|
|||
}} />)}
|
||||
<Col xs={3}>
|
||||
<BlurableInput
|
||||
type="number"
|
||||
value={deviation}
|
||||
onCommit={e => setDeviation(parseInt(e.currentTarget.value))} />
|
||||
onCommit={e => setDeviation(parseIntInput(e.currentTarget.value))} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>;
|
||||
|
|
|
@ -31,6 +31,27 @@ input:not([role="combobox"]) {
|
|||
&.dim {
|
||||
background: darken($white, 2%) !important;
|
||||
}
|
||||
&.error {
|
||||
border: 2px solid $red;
|
||||
background-color: $white !important;
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
|
||||
.input-error-wrapper {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.input-error {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
color: $white;
|
||||
background: $red;
|
||||
padding: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.day-selector-wrapper {
|
||||
|
|
|
@ -19,7 +19,7 @@ describe("McuInputBox", () => {
|
|||
};
|
||||
};
|
||||
|
||||
it("clamps numbers", () => {
|
||||
it("clamps negative numbers", () => {
|
||||
const mib = new McuInputBox(fakeProps());
|
||||
const result = mib.clampInputAndWarn("-1", "short");
|
||||
expect(result).toEqual(0);
|
||||
|
@ -27,7 +27,15 @@ describe("McuInputBox", () => {
|
|||
.toHaveBeenCalledWith("Must be a positive number. Rounding up to 0.");
|
||||
});
|
||||
|
||||
it("clamps numbers", () => {
|
||||
it("clamps large numbers", () => {
|
||||
const mib = new McuInputBox(fakeProps());
|
||||
const result = mib.clampInputAndWarn("100000", "short");
|
||||
expect(result).toEqual(32000);
|
||||
expect(warning)
|
||||
.toHaveBeenCalledWith("Maximum input is 32,000. Rounding down.");
|
||||
});
|
||||
|
||||
it("handles bad input", () => {
|
||||
const mib = new McuInputBox(fakeProps());
|
||||
expect(() => mib.clampInputAndWarn("QQQ", "short"))
|
||||
.toThrowError("Bad input in mcu_input_box. Impossible?");
|
||||
|
|
|
@ -4,6 +4,7 @@ import { BlurableInput } from "../../ui/index";
|
|||
import { SourceFbosConfig } from "../interfaces";
|
||||
import { ConfigurationName } from "farmbot/dist";
|
||||
import { updateConfig } from "../actions";
|
||||
import { parseIntInput } from "../../util";
|
||||
|
||||
export interface BotConfigInputBoxProps {
|
||||
setting: ConfigurationName;
|
||||
|
@ -12,6 +13,9 @@ export interface BotConfigInputBoxProps {
|
|||
sourceFbosConfig: SourceFbosConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently only used for `network_not_found_timer` and `steps_per_mm_?`.
|
||||
*/
|
||||
export class BotConfigInputBox
|
||||
extends React.Component<BotConfigInputBoxProps, {}> {
|
||||
|
||||
|
@ -21,7 +25,7 @@ export class BotConfigInputBox
|
|||
|
||||
change = (key: ConfigurationName, dispatch: Function) => {
|
||||
return (event: React.FormEvent<HTMLInputElement>) => {
|
||||
const next = parseInt(event.currentTarget.value, 10);
|
||||
const next = parseIntInput(event.currentTarget.value);
|
||||
const current = this.config.value;
|
||||
if (!_.isNaN(next) && (next !== current)) {
|
||||
dispatch(updateConfig({ [key]: next }));
|
||||
|
|
|
@ -10,11 +10,11 @@ import { updateMCU } from "../../../actions";
|
|||
import {
|
||||
fakeFirmwareConfig
|
||||
} from "../../../../__test_support__/fake_state/resources";
|
||||
import { warning } from "farmbot-toastr";
|
||||
import { warning, error } from "farmbot-toastr";
|
||||
|
||||
describe("<HomingAndCalibration />", () => {
|
||||
function testAxisLengthInput(
|
||||
fw: string, provided: string, expected: string) {
|
||||
fw: string, provided: string, expected: string | undefined) {
|
||||
const dispatch = jest.fn();
|
||||
bot.controlPanelState.homing_and_calibration = true;
|
||||
bot.hardware.informational_settings.firmware_version = fw;
|
||||
|
@ -31,23 +31,26 @@ describe("<HomingAndCalibration />", () => {
|
|||
const input = result.find("input").first().props();
|
||||
input.onChange && input.onChange(e);
|
||||
input.onSubmit && input.onSubmit(e);
|
||||
expect(updateMCU)
|
||||
.toHaveBeenCalledWith("movement_axis_nr_steps_x", expected);
|
||||
expected
|
||||
? expect(updateMCU)
|
||||
.toHaveBeenCalledWith("movement_axis_nr_steps_x", expected)
|
||||
: expect(updateMCU).not.toHaveBeenCalled();
|
||||
}
|
||||
it("short int", () => {
|
||||
testAxisLengthInput("5.0.0", "100000", "32000");
|
||||
expect(warning)
|
||||
.toHaveBeenCalledWith("Maximum input is 32,000. Rounding down.");
|
||||
testAxisLengthInput("5.0.0", "100000", undefined);
|
||||
expect(error)
|
||||
.toHaveBeenCalledWith("Value must be less than or equal to 32000.");
|
||||
});
|
||||
|
||||
it("long int: too long", () => {
|
||||
testAxisLengthInput("6.0.0", "10000000000", "2000000000");
|
||||
expect(warning)
|
||||
.toHaveBeenCalledWith("Maximum input is 2,000,000,000. Rounding down.");
|
||||
testAxisLengthInput("6.0.0", "10000000000", undefined);
|
||||
expect(error)
|
||||
.toHaveBeenCalledWith("Value must be less than or equal to 2000000000.");
|
||||
});
|
||||
|
||||
it("long int: ok", () => {
|
||||
testAxisLengthInput("6.0.0", "100000", "100000");
|
||||
expect(warning).not.toHaveBeenCalled();
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@ import { warning } from "farmbot-toastr";
|
|||
import { McuInputBoxProps } from "../interfaces";
|
||||
import { updateMCU } from "../actions";
|
||||
import { BlurableInput } from "../../ui/index";
|
||||
import { clampUnsignedInteger, IntegerSize } from "../../util";
|
||||
import { clampUnsignedInteger, IntegerSize, getMaxInputFromIntSize } from "../../util";
|
||||
import { t } from "i18next";
|
||||
|
||||
export class McuInputBox extends React.Component<McuInputBoxProps, {}> {
|
||||
|
@ -63,6 +63,8 @@ export class McuInputBox extends React.Component<McuInputBoxProps, {}> {
|
|||
type="number"
|
||||
className={this.className}
|
||||
value={this.value}
|
||||
onCommit={this.commit} />;
|
||||
onCommit={this.commit}
|
||||
min={0}
|
||||
max={getMaxInputFromIntSize(this.props.intSize)} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
jest.mock("../../../history", () => ({
|
||||
history: {
|
||||
push: jest.fn()
|
||||
}
|
||||
jest.mock("../../../history", () => ({ history: { push: jest.fn() } }));
|
||||
|
||||
jest.mock("../../../api/crud", () => ({
|
||||
destroy: jest.fn(),
|
||||
overwrite: jest.fn(),
|
||||
save: jest.fn(),
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import {
|
||||
fakeFarmEvent, fakeSequence, fakeRegimen
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { mount } from "enzyme";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import {
|
||||
EditFEForm,
|
||||
EditFEProps,
|
||||
|
@ -17,7 +19,7 @@ import {
|
|||
destructureFarmEvent,
|
||||
offsetTime
|
||||
} from "../edit_fe_form";
|
||||
import { isString } from "lodash";
|
||||
import { isString, isFunction } from "lodash";
|
||||
import { repeatOptions } from "../map_state_to_props_add_edit";
|
||||
import { SpecialStatus, VariableDeclaration } from "farmbot";
|
||||
import { success, error } from "farmbot-toastr";
|
||||
|
@ -28,6 +30,8 @@ import {
|
|||
buildResourceIndex
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
import { fakeVariableNameSet } from "../../../__test_support__/fake_variables";
|
||||
import { clickButton } from "../../../__test_support__/helpers";
|
||||
import { destroy } from "../../../api/crud";
|
||||
|
||||
const mockSequence = fakeSequence();
|
||||
|
||||
|
@ -261,11 +265,7 @@ describe("<FarmEventForm/>", () => {
|
|||
const p = props();
|
||||
const state = fakeState();
|
||||
state.resources.index.references = { [p.farmEvent.uuid]: p.farmEvent };
|
||||
p.dispatch = jest.fn((x) => {
|
||||
x(() => { }, () => state);
|
||||
return Promise.resolve();
|
||||
})
|
||||
.mockImplementationOnce(() => Promise.resolve());
|
||||
p.dispatch = jest.fn(x => { isFunction(x) && x(); return Promise.resolve(); });
|
||||
p.farmEvent.body.executable_type = "Regimen";
|
||||
p.farmEvent.body.start_time = "2017-05-22T05:00:00.000Z";
|
||||
p.farmEvent.body.end_time = "2017-05-22T06:00:00.000Z";
|
||||
|
@ -478,6 +478,46 @@ describe("<FarmEventForm/>", () => {
|
|||
const inst = instance(p);
|
||||
expect(inst.updatedFarmEvent.body).toEqual([oldDeclaration]);
|
||||
});
|
||||
|
||||
it("deletes a farmEvent", async () => {
|
||||
const p = props();
|
||||
p.dispatch = jest.fn(() => Promise.resolve());
|
||||
const inst = instance(p);
|
||||
const wrapper = shallow(<inst.FarmEventDeleteButton />);
|
||||
clickButton(wrapper, 0, "delete");
|
||||
await expect(destroy).toHaveBeenCalledWith(p.farmEvent.uuid);
|
||||
expect(history.push).toHaveBeenCalledWith("/app/designer/farm_events");
|
||||
expect(success).toHaveBeenCalledWith("Deleted farm event.", "Deleted");
|
||||
});
|
||||
|
||||
it("sets repeat", () => {
|
||||
const p = props();
|
||||
p.dispatch = jest.fn(() => Promise.resolve());
|
||||
const e = {
|
||||
currentTarget: { checked: true }
|
||||
} as React.ChangeEvent<HTMLInputElement>;
|
||||
const inst = instance(p);
|
||||
inst.toggleRepeat(e);
|
||||
expect(inst.state).toEqual({
|
||||
fe: { timeUnit: "daily" },
|
||||
specialStatusLocal: SpecialStatus.DIRTY
|
||||
});
|
||||
});
|
||||
|
||||
it("sets repeat: regimen", () => {
|
||||
const p = props();
|
||||
p.farmEvent.body.executable_type = "Regimen";
|
||||
p.dispatch = jest.fn(() => Promise.resolve());
|
||||
const e = {
|
||||
currentTarget: { checked: true }
|
||||
} as React.ChangeEvent<HTMLInputElement>;
|
||||
const inst = instance(p);
|
||||
inst.toggleRepeat(e);
|
||||
expect(inst.state).toEqual({
|
||||
fe: { timeUnit: "never" },
|
||||
specialStatusLocal: SpecialStatus.DIRTY
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("destructureFarmEvent", () => {
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
import { destroy, save, overwrite } from "../../api/crud";
|
||||
import { history } from "../../history";
|
||||
// TIL: https://stackoverflow.com/a/24900248/1064917
|
||||
import { betterMerge } from "../../util";
|
||||
import { betterMerge, parseIntInput } from "../../util";
|
||||
import { maybeWarnAboutMissedTasks } from "./util";
|
||||
import { FarmEventRepeatForm } from "./farm_event_repeat_form";
|
||||
import { scheduleForFarmEvent } from "./calendar/scheduler";
|
||||
|
@ -109,7 +109,7 @@ export function recombine(vm: FarmEventViewModel,
|
|||
end_time: offsetTime(vm.endDate, vm.endTime, vm.timeOffset),
|
||||
repeat: parseInt(vm.repeat, 10) || 1,
|
||||
time_unit: (isReg ? "never" : vm.timeUnit) as TimeUnit,
|
||||
executable_id: parseInt(vm.executable_id, 10),
|
||||
executable_id: parseIntInput(vm.executable_id),
|
||||
executable_type: vm.executable_type as ("Sequence" | "Regimen"),
|
||||
body: vm.body,
|
||||
};
|
||||
|
@ -418,6 +418,26 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
|
|||
{t("Repeats?")}
|
||||
</label> : <div />
|
||||
|
||||
dateCheck = (): string | undefined => {
|
||||
const startDate = this.fieldGet("startDate");
|
||||
const endDate = this.fieldGet("endDate");
|
||||
if (!moment(endDate).isSameOrAfter(moment(startDate))) {
|
||||
return t("End date must not be before start date.");
|
||||
}
|
||||
}
|
||||
|
||||
timeCheck = (): string | undefined => {
|
||||
const startDate = this.fieldGet("startDate");
|
||||
const startTime = this.fieldGet("startTime");
|
||||
const endDate = this.fieldGet("endDate");
|
||||
const endTime = this.fieldGet("endTime");
|
||||
const start = offsetTime(startDate, startTime, this.props.timeOffset);
|
||||
const end = offsetTime(endDate, endTime, this.props.timeOffset);
|
||||
if (moment(start).isSameOrAfter(moment(end))) {
|
||||
return t("End time must be after start time.");
|
||||
}
|
||||
}
|
||||
|
||||
RepeatForm = () => {
|
||||
const allowRepeat = !this.isReg && this.repeats;
|
||||
return <div>
|
||||
|
@ -430,7 +450,9 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
|
|||
timeUnit={this.fieldGet("timeUnit") as TimeUnit}
|
||||
repeat={this.fieldGet("repeat")}
|
||||
endDate={this.fieldGet("endDate")}
|
||||
endTime={this.fieldGet("endTime")} />
|
||||
endTime={this.fieldGet("endTime")}
|
||||
dateError={this.dateCheck()}
|
||||
timeError={this.timeCheck()} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ interface Props {
|
|||
hidden?: boolean;
|
||||
name: string;
|
||||
className: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function EventTimePicker(props: Props) {
|
||||
|
@ -22,5 +23,6 @@ export function EventTimePicker(props: Props) {
|
|||
type="time"
|
||||
className="add-event-start-time"
|
||||
value={value}
|
||||
onCommit={onCommit} />;
|
||||
onCommit={onCommit}
|
||||
error={props.error} />;
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ export interface RepeatFormProps {
|
|||
endDate: string;
|
||||
endTime: string;
|
||||
tzOffset: number;
|
||||
dateError?: string;
|
||||
timeError?: string;
|
||||
}
|
||||
|
||||
const indexKey: keyof DropDownItem = "value";
|
||||
|
@ -45,7 +47,8 @@ export function FarmEventRepeatForm(props: RepeatFormProps) {
|
|||
className="add-event-repeat-frequency"
|
||||
name="repeat"
|
||||
value={repeat}
|
||||
onCommit={changeHandler("repeat")} />
|
||||
onCommit={changeHandler("repeat")}
|
||||
min={1} />
|
||||
</Col>
|
||||
<Col xs={8}>
|
||||
<FBSelect
|
||||
|
@ -65,7 +68,8 @@ export function FarmEventRepeatForm(props: RepeatFormProps) {
|
|||
className="add-event-end-date"
|
||||
name="endDate"
|
||||
value={endDate}
|
||||
onCommit={changeHandler("endDate")} />
|
||||
onCommit={changeHandler("endDate")}
|
||||
error={props.dateError} />
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<EventTimePicker
|
||||
|
@ -74,7 +78,8 @@ export function FarmEventRepeatForm(props: RepeatFormProps) {
|
|||
name="endTime"
|
||||
tzOffset={props.tzOffset}
|
||||
value={endTime}
|
||||
onCommit={changeHandler("endTime")} />
|
||||
onCommit={changeHandler("endTime")}
|
||||
error={props.timeError} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>;
|
||||
|
|
|
@ -14,6 +14,7 @@ import { GenericPointer } from "farmbot/dist/resources/api_resources";
|
|||
import {
|
||||
DesignerPanel, DesignerPanelHeader, DesignerPanelContent
|
||||
} from "./designer_panel";
|
||||
import { parseIntInput } from "../../util";
|
||||
|
||||
export function mapStateToProps(props: Everything) {
|
||||
return {
|
||||
|
@ -77,7 +78,7 @@ export class CreatePoints
|
|||
|
||||
update = (key: keyof CreatePointsState) => {
|
||||
return (e: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(e.currentTarget.value);
|
||||
const value = parseIntInput(e.currentTarget.value);
|
||||
this.setState({ [key]: value });
|
||||
if (this.props.currentPoint) {
|
||||
const point = clone(this.props.currentPoint);
|
||||
|
@ -142,7 +143,8 @@ export class CreatePoints
|
|||
name="r"
|
||||
type="number"
|
||||
onCommit={this.update("r")}
|
||||
value={r || 0} />
|
||||
value={r || 0}
|
||||
min={0} />
|
||||
</Col>
|
||||
<Col xs={3}>
|
||||
<label>{t("color")}</label>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { WeedDetectorSlider } from "./slider";
|
|||
import { TaggedImage } from "farmbot";
|
||||
import { t } from "i18next";
|
||||
import { PhotoFooter } from "../images/photos";
|
||||
import { parseIntInput } from "../../util";
|
||||
|
||||
const RANGES = {
|
||||
H: { LOWEST: 0, HIGHEST: 179 },
|
||||
|
@ -53,7 +54,7 @@ export class ImageWorkspace extends React.Component<ImageWorkspaceProps, {}> {
|
|||
/** Generates a function to handle changes to blur/morph/iteration. */
|
||||
numericChange = (key: NumericKeyName) =>
|
||||
(e: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
this.props.onChange(key, parseInt(e.currentTarget.value, 10) || 0);
|
||||
this.props.onChange(key, parseIntInput(e.currentTarget.value) || 0);
|
||||
};
|
||||
|
||||
maybeProcessPhoto = () => {
|
||||
|
|
|
@ -8,13 +8,13 @@ import { shallow, mount } from "enzyme";
|
|||
import {
|
||||
buildResourceIndex
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
import { FBSelect } from "../../../ui/index";
|
||||
import { FBSelect, BlurableInput } from "../../../ui/index";
|
||||
import {
|
||||
LocationFormProps, LocalsListProps, PARENT, AllowedDeclaration
|
||||
} from "../locals_list_support";
|
||||
import { difference } from "lodash";
|
||||
import { VariableNameSet } from "../../../resources/interfaces";
|
||||
import { InputBox, generateList } from "../../step_tiles/tile_move_absolute/index";
|
||||
import { generateList } from "../../step_tiles/tile_move_absolute/generate_list";
|
||||
import { convertDDItoDeclaration } from "../handle_select";
|
||||
|
||||
describe("<LocationForm/>", () => {
|
||||
|
@ -38,7 +38,7 @@ describe("<LocationForm/>", () => {
|
|||
const p = fakeProps();
|
||||
const el = shallow(<LocationForm {...p} />);
|
||||
const selects = el.find(FBSelect);
|
||||
const inputs = el.find(InputBox);
|
||||
const inputs = el.find(BlurableInput);
|
||||
|
||||
expect(selects.length).toBe(1);
|
||||
const select = selects.first().props();
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { Row, Col, FBSelect } from "../../ui";
|
||||
import { Row, Col, FBSelect, BlurableInput } from "../../ui";
|
||||
import { t } from "i18next";
|
||||
import {
|
||||
generateList, InputBox
|
||||
} from "../step_tiles/tile_move_absolute/index";
|
||||
import { generateList } from "../step_tiles/tile_move_absolute/generate_list";
|
||||
import {
|
||||
convertDDItoDeclaration, addOrEditDeclaration
|
||||
} from "../locals_list/handle_select";
|
||||
|
@ -112,13 +110,14 @@ export const LocationForm =
|
|||
<Row>
|
||||
{["x", "y", "z"].map((axis: Xyz) =>
|
||||
<Col xs={props.width || 4} key={axis}>
|
||||
<InputBox
|
||||
onCommit={manuallyEditAxis({ ...axisPartialProps, axis })}
|
||||
disabled={isDisabled}
|
||||
name={`location-${axis}`}
|
||||
value={"" + vector[axis]}>
|
||||
<label>
|
||||
{t("{{axis}} (mm)", { axis })}
|
||||
</InputBox>
|
||||
</label>
|
||||
<BlurableInput type="number"
|
||||
disabled={isDisabled}
|
||||
onCommit={manuallyEditAxis({ ...axisPartialProps, axis })}
|
||||
name={`location-${axis}`}
|
||||
value={"" + vector[axis]} />
|
||||
</Col>)}
|
||||
</Row>}
|
||||
</div>;
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
ScopeDeclarationBodyItem,
|
||||
VariableDeclaration
|
||||
} from "farmbot";
|
||||
import { Row, Col } from "../../ui/index";
|
||||
import { Row, Col, BlurableInput } from "../../ui/index";
|
||||
import {
|
||||
isTaggedSequence,
|
||||
} from "../../resources/tagged_resources";
|
||||
|
@ -27,7 +27,6 @@ import {
|
|||
import { defensiveClone, betterMerge } from "../../util";
|
||||
import { overwrite } from "../../api/crud";
|
||||
import { Xyz } from "../../devices/interfaces";
|
||||
import { InputBox } from "./tile_move_absolute/index";
|
||||
import { ToolTips } from "../../constants";
|
||||
import {
|
||||
StepWrapper,
|
||||
|
@ -201,12 +200,13 @@ export class TileMoveAbsolute extends Component<StepParams, MoveAbsState> {
|
|||
<Row>
|
||||
{["x", "y", "z"].map((axis: Xyz) =>
|
||||
<Col xs={3} key={axis}>
|
||||
<InputBox
|
||||
<label>
|
||||
{t("{{axis}}-Offset", { axis })}
|
||||
</label>
|
||||
<BlurableInput type="number"
|
||||
onCommit={this.updateInputValue(axis, "offset")}
|
||||
name={`offset-${axis}`}
|
||||
value={this.getOffsetValue(axis)}>
|
||||
{t("{{axis}}-Offset", { axis })}
|
||||
</InputBox>
|
||||
value={this.getOffsetValue(axis)} />
|
||||
</Col>)}
|
||||
<this.SpeedForm />
|
||||
</Row>
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
export * from "./generate_list";
|
||||
export * from "./interfaces";
|
||||
export * from "./variables_support";
|
||||
export * from "./input_box";
|
|
@ -1,17 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { BlurableInput } from "../../../ui/index";
|
||||
import { InputBoxProps } from "./interfaces";
|
||||
|
||||
export function InputBox(p: InputBoxProps) {
|
||||
return <div>
|
||||
<label>
|
||||
{p.children}
|
||||
</label>
|
||||
<BlurableInput
|
||||
disabled={!!p.disabled}
|
||||
onCommit={p.onCommit}
|
||||
type="number"
|
||||
name={p.name}
|
||||
value={p.value} />
|
||||
</div>;
|
||||
}
|
|
@ -45,4 +45,39 @@ describe("<BlurableInput />", () => {
|
|||
expect(error).toHaveBeenCalledWith(
|
||||
"Value must be less than or equal to 100.");
|
||||
});
|
||||
|
||||
it("checks for non-number input", () => {
|
||||
const p = fakeProps();
|
||||
p.type = "number";
|
||||
const wrapper = shallow<BlurableInput>(<BlurableInput {...p} />);
|
||||
wrapper.find("input").simulate("change", { currentTarget: { value: "" } });
|
||||
expect(wrapper.instance().state.buffer).toEqual("");
|
||||
expect(wrapper.instance().state.error).toEqual("Please enter a number.");
|
||||
wrapper.find("input").simulate("submit");
|
||||
expect(p.onCommit).not.toHaveBeenCalled();
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("checks for non-number input", () => {
|
||||
const p = fakeProps();
|
||||
p.type = "number";
|
||||
p.allowEmpty = true;
|
||||
const wrapper = shallow<BlurableInput>(<BlurableInput {...p} />);
|
||||
wrapper.find("input").simulate("change", { currentTarget: { value: "" } });
|
||||
expect(wrapper.instance().state.buffer).toEqual("");
|
||||
expect(wrapper.instance().state.error).toEqual(undefined);
|
||||
wrapper.find("input").simulate("submit");
|
||||
expect(p.onCommit).toHaveBeenCalled();
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("parses number", () => {
|
||||
const p = fakeProps();
|
||||
p.type = "number";
|
||||
const wrapper = shallow<BlurableInput>(<BlurableInput {...p} />);
|
||||
const e = { currentTarget: { value: "-1.1e+2" } };
|
||||
wrapper.setState({ buffer: e.currentTarget.value });
|
||||
wrapper.find("input").simulate("change", e);
|
||||
expect(wrapper.instance().state.buffer).toEqual(e.currentTarget.value);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import * as React from "react";
|
||||
import { equals } from "../util";
|
||||
import { equals, parseIntInput } from "../util";
|
||||
import { isNumber } from "lodash";
|
||||
import { error } from "farmbot-toastr";
|
||||
import { t } from "i18next";
|
||||
import { InputError } from "./input_error";
|
||||
|
||||
export interface BIProps {
|
||||
value: string | number;
|
||||
|
@ -25,49 +26,69 @@ export interface BIProps {
|
|||
className?: string;
|
||||
placeholder?: string;
|
||||
hidden?: boolean;
|
||||
error?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
interface BIState {
|
||||
buffer: string;
|
||||
isEditing: boolean;
|
||||
error: string | undefined;
|
||||
}
|
||||
|
||||
export class BlurableInput extends React.Component<BIProps, Partial<BIState>> {
|
||||
|
||||
state: BIState = { buffer: "", isEditing: false };
|
||||
state: BIState = { buffer: "", isEditing: false, error: undefined };
|
||||
|
||||
get error() { return this.props.error || this.state.error; }
|
||||
|
||||
withinLimits = (options?: { toasts?: boolean }): boolean => {
|
||||
const onError = (msg: string) => {
|
||||
this.setState({ error: msg });
|
||||
options && options.toasts && error(msg);
|
||||
};
|
||||
|
||||
withinLimits = (): boolean => {
|
||||
if (this.props.type === "number") {
|
||||
const value = parseInt(this.state.buffer);
|
||||
const value = parseIntInput(this.state.buffer);
|
||||
if (isNumber(this.props.min) && value < this.props.min) {
|
||||
error(t("Value must be greater than or equal to {{min}}.",
|
||||
onError(t("Value must be greater than or equal to {{min}}.",
|
||||
{ min: this.props.min }));
|
||||
return false;
|
||||
}
|
||||
if (isNumber(this.props.max) && value > this.props.max) {
|
||||
error(t("Value must be less than or equal to {{max}}.",
|
||||
onError(t("Value must be less than or equal to {{max}}.",
|
||||
{ max: this.props.max }));
|
||||
return false;
|
||||
}
|
||||
/** `e.currentTarget.value` is "" for any invalid number input. */
|
||||
if ((this.state.buffer === "") && !this.props.allowEmpty) {
|
||||
onError(t("Please enter a number."));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this.setState({ error: undefined });
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Called on blur. */
|
||||
maybeCommit = (e: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
const bufferOk = this.state.buffer || this.props.allowEmpty;
|
||||
const shouldPassToParent = bufferOk && this.withinLimits();
|
||||
const shouldPassToParent = bufferOk && this.withinLimits({ toasts: true });
|
||||
shouldPassToParent && this.props.onCommit(e);
|
||||
this.setState({ isEditing: false, buffer: "" });
|
||||
this.setState({ isEditing: false, buffer: "", error: undefined });
|
||||
}
|
||||
|
||||
focus = () => {
|
||||
const { value } = this.props;
|
||||
this.setState({ isEditing: true, buffer: "" + (value || "") });
|
||||
this.setState({
|
||||
isEditing: true,
|
||||
buffer: "" + (value || ""),
|
||||
error: undefined
|
||||
});
|
||||
}
|
||||
|
||||
updateBuffer = (e: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
this.setState({ buffer: e.currentTarget.value });
|
||||
this.setState({ buffer: e.currentTarget.value }, this.withinLimits);
|
||||
}
|
||||
|
||||
usualProps = () => {
|
||||
|
@ -86,7 +107,8 @@ export class BlurableInput extends React.Component<BIProps, Partial<BIState>> {
|
|||
max: this.props.max,
|
||||
type: this.props.type || "text",
|
||||
disabled: this.props.disabled,
|
||||
className: this.props.className,
|
||||
className: (this.props.className || "") + (this.error ? " error" : ""),
|
||||
title: this.props.title || "",
|
||||
placeholder: this.props.placeholder,
|
||||
};
|
||||
}
|
||||
|
@ -96,6 +118,9 @@ export class BlurableInput extends React.Component<BIProps, Partial<BIState>> {
|
|||
}
|
||||
|
||||
render() {
|
||||
return <input {...this.usualProps()} />;
|
||||
return <div>
|
||||
<InputError error={this.error} />
|
||||
<input {...this.usualProps()} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import * as React from "react";
|
||||
import { Popover, PopoverInteractionKind, Position } from "@blueprintjs/core";
|
||||
|
||||
interface InputErrorProps {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const InputError = (props: InputErrorProps) =>
|
||||
props.error
|
||||
? <Popover
|
||||
minimal={true}
|
||||
usePortal={false}
|
||||
position={Position.TOP_LEFT}
|
||||
modifiers={{ offset: { offset: "0, 20" } }}
|
||||
interactionKind={PopoverInteractionKind.HOVER}
|
||||
className="input-error-wrapper">
|
||||
<i className="fa fa-exclamation-circle input-error" />
|
||||
<p>{props.error}</p>
|
||||
</Popover>
|
||||
: <div />;
|
|
@ -1,7 +1,7 @@
|
|||
import * as Util from "../util";
|
||||
import { times } from "lodash";
|
||||
import { validBotLocationData } from "../index";
|
||||
import { parseClassNames } from "../../ui/util";
|
||||
|
||||
describe("util", () => {
|
||||
describe("safeStringFetch", () => {
|
||||
const data = {
|
||||
|
@ -127,7 +127,7 @@ describe("util", () => {
|
|||
|
||||
describe("validBotLocationData()", () => {
|
||||
it("returns valid location_data object", () => {
|
||||
const result = validBotLocationData(undefined);
|
||||
const result = Util.validBotLocationData(undefined);
|
||||
expect(result).toEqual({
|
||||
position: { x: undefined, y: undefined, z: undefined },
|
||||
scaled_encoders: { x: undefined, y: undefined, z: undefined },
|
||||
|
@ -176,3 +176,16 @@ describe("parseClassNames", () => {
|
|||
].map(string => expect(results).toContain(string));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("parseIntInput()", () => {
|
||||
it("parses int from number input", () => {
|
||||
expect(Util.parseIntInput("-1.1e+2")).toEqual(-110);
|
||||
expect(Util.parseIntInput("-1.1e-1")).toEqual(0);
|
||||
expect(Util.parseIntInput("1.1E1")).toEqual(11);
|
||||
expect(Util.parseIntInput("+123")).toEqual(123);
|
||||
expect(Util.parseIntInput("1.5")).toEqual(1);
|
||||
expect(Util.parseIntInput("e")).toEqual(NaN);
|
||||
expect(Util.parseIntInput("")).toEqual(NaN);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import * as _ from "lodash";
|
||||
import { parseIntInput } from "./util";
|
||||
|
||||
/** The firmware will have an integer overflow if you don't check this one. */
|
||||
const MAX_SHORT_INPUT = 32000;
|
||||
|
@ -13,24 +14,26 @@ type ClampResult = High | Low | Malformed | Ok;
|
|||
|
||||
export type IntegerSize = "short" | "long" | undefined;
|
||||
|
||||
export const getMaxInputFromIntSize = (size: IntegerSize) => {
|
||||
switch (size) {
|
||||
case "long":
|
||||
return MAX_LONG_INPUT;
|
||||
case "short":
|
||||
default:
|
||||
return MAX_SHORT_INPUT;
|
||||
}
|
||||
};
|
||||
|
||||
/** Handle all the possible ways a user could give us bad data or cause an
|
||||
* integer overflow in the firmware. */
|
||||
export function clampUnsignedInteger(
|
||||
input: string, size: IntegerSize): ClampResult {
|
||||
const result = Math.round(parseInt(input, 10));
|
||||
const maxInput = () => {
|
||||
switch (size) {
|
||||
case "long":
|
||||
return MAX_LONG_INPUT;
|
||||
case "short":
|
||||
default:
|
||||
return MAX_SHORT_INPUT;
|
||||
}
|
||||
};
|
||||
const result = parseIntInput(input);
|
||||
|
||||
// Clamp to prevent overflow.
|
||||
if (_.isNaN(result)) { return { outcome: "malformed", result: undefined }; }
|
||||
if (result > maxInput()) { return { outcome: "high", result: maxInput() }; }
|
||||
const max = getMaxInputFromIntSize(size);
|
||||
if (result > max) { return { outcome: "high", result: max }; }
|
||||
if (result < MIN_INPUT) { return { outcome: "low", result: MIN_INPUT }; }
|
||||
|
||||
return { outcome: "ok", result };
|
||||
|
|
|
@ -217,3 +217,12 @@ export function unpackUUID(uuid: string): BetterUUID {
|
|||
remoteId: id > 0 ? id : undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Integer parsed from float
|
||||
* since number type inputs allow floating point notation.
|
||||
*/
|
||||
export const parseIntInput = (input: string): number => {
|
||||
const int = parseInt("" + parseFloat(input).toFixed(1), 10);
|
||||
return int === 0 ? 0 : int;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue