misc fixes and updates

pull/1219/head
gabrielburnworth 2019-06-04 15:07:24 -07:00
parent 6f4e6b7af6
commit feb7b07a6f
19 changed files with 252 additions and 127 deletions

View File

@ -4,7 +4,7 @@ describe("fetchLabFeatures", () => {
Object.defineProperty(window.location, "reload", { value: jest.fn() });
it("basically just initializes stuff", () => {
const val = fetchLabFeatures(jest.fn());
expect(val.length).toBe(10);
expect(val.length).toBe(9);
expect(val[0].value).toBeFalsy();
const { callback } = val[0];
if (callback) {

View File

@ -38,18 +38,6 @@ export const fetchLabFeatures =
storageKey: BooleanSetting.hide_webcam_widget,
value: false
},
{
name: t("Dynamic map size"),
description: t(Content.DYNAMIC_MAP_SIZE),
storageKey: BooleanSetting.dynamic_map,
value: false
},
{
name: t("Double default map dimensions"),
description: t(Content.DOUBLE_MAP_DIMENSIONS),
storageKey: BooleanSetting.map_xl,
value: false
},
{
name: t("Display plant animations"),
description: t(Content.PLANT_ANIMATIONS),
@ -91,6 +79,12 @@ export const fetchLabFeatures =
value: false,
displayInvert: true,
},
{
name: t("Dynamic map size"),
description: t(Content.DYNAMIC_MAP_SIZE),
storageKey: BooleanSetting.dynamic_map,
value: false
},
].map(fetchSettingValue(getConfigValue)));
/** Always allow toggling from true => false (deactivate).

View File

@ -72,7 +72,7 @@ describe("<Move />", () => {
it("changes step size", () => {
const p = fakeProps();
const wrapper = mount(<Move {...p} />);
clickButton(wrapper, 1, "1");
clickButton(wrapper, 0, "1");
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.CHANGE_STEP_SIZE,
payload: 1

View File

@ -1,7 +1,5 @@
import * as React from "react";
import { Widget, WidgetBody, WidgetHeader } from "../../ui";
import { EStopButton } from "../../devices/components/e_stop_btn";
import { MustBeOnline } from "../../devices/must_be_online";
import { validBotLocationData } from "../../util";
import { toggleWebAppBool } from "../../config_storage/actions";
@ -37,10 +35,6 @@ export class Move extends React.Component<MoveProps, {}> {
toggle={this.toggle}
getValue={this.getValue} />
</Popover>
<EStopButton
bot={this.props.bot}
forceUnlock={this.getValue(
BooleanSetting.disable_emergency_unlock_confirmation)} />
</WidgetHeader>
<WidgetBody>
<MustBeOnline

View File

@ -1,9 +1,11 @@
import * as React from "react";
import { mount } from "enzyme";
import { mount, shallow } from "enzyme";
import { PeripheralForm } from "../peripheral_form";
import { TaggedPeripheral, SpecialStatus } from "farmbot";
import { PeripheralFormProps } from "../interfaces";
import { Actions } from "../../../constants";
describe("<PeripheralForm/>", function () {
describe("<PeripheralForm/>", () => {
const dispatch = jest.fn();
const peripherals: TaggedPeripheral[] = [
{
@ -27,22 +29,45 @@ describe("<PeripheralForm/>", function () {
}
},
];
const fakeProps = (): PeripheralFormProps => ({ dispatch, peripherals });
it("renders a list of editable peripherals, in sorted order", function () {
const expectedPayload = (update: Object) =>
expect.objectContaining({
payload: expect.objectContaining({
update
}),
type: Actions.EDIT_RESOURCE
});
it("renders a list of editable peripherals, in sorted order", () => {
const form = mount(<PeripheralForm dispatch={dispatch}
peripherals={peripherals} />);
const inputs = form.find("input");
const buttons = form.find("button");
expect(inputs.at(0).props().value).toEqual("GPIO 2");
inputs.at(0).simulate("change");
expect(inputs.at(1).props().value).toEqual("2");
inputs.at(1).simulate("change");
expect(inputs.at(1).props().value).toEqual("GPIO 13 - LED");
});
it("updates label", () => {
const p = fakeProps();
const form = shallow(<PeripheralForm {...p} />);
const inputs = form.find("input");
inputs.at(0).simulate("change", { currentTarget: { value: "GPIO 3" } });
expect(p.dispatch).toHaveBeenCalledWith(
expectedPayload({ label: "GPIO 3" }));
});
it("updates pin", () => {
const p = fakeProps();
const form = shallow(<PeripheralForm {...p} />);
form.find("FBSelect").at(0).simulate("change", { value: 3 });
expect(p.dispatch).toHaveBeenCalledWith(expectedPayload({ pin: 3 }));
});
it("deletes peripheral", () => {
const p = fakeProps();
const form = shallow(<PeripheralForm {...p} />);
const buttons = form.find("button");
buttons.at(0).simulate("click");
expect(inputs.at(2).props().value).toEqual("GPIO 13 - LED");
inputs.at(2).simulate("change");
expect(inputs.at(3).props().value).toEqual("13");
inputs.at(3).simulate("change");
buttons.at(1).simulate("click");
expect(dispatch).toHaveBeenCalledTimes(6);
expect(p.dispatch).toHaveBeenCalledWith(expect.any(Function));
});
});

View File

@ -1,35 +1,44 @@
import * as React from "react";
import { destroy, edit } from "../../api/crud";
import { PeripheralFormProps } from "./interfaces";
import { sortResourcesById } from "../../util";
import { KeyValEditRow } from "../key_val_edit_row";
import { t } from "../../i18next_wrapper";
import { Row, Col, FBSelect } from "../../ui";
import {
pinDropdowns
} from "../../sequences/step_tiles/pin_and_peripheral_support";
export function PeripheralForm(props: PeripheralFormProps) {
const { dispatch, peripherals } = props;
return <div>
{sortResourcesById(peripherals).map(p => {
return <KeyValEditRow
key={p.uuid}
label={p.body.label}
onLabelChange={(e) => {
const { value } = e.currentTarget;
dispatch(edit(p, { label: value }));
}}
labelPlaceholder="Name"
value={(p.body.pin || "").toString()}
valuePlaceholder={t("Pin #")}
onValueChange={(e) => {
const { value } = e.currentTarget;
const update: Partial<typeof p.body> = { pin: parseInt(value, 10) };
dispatch(edit(p, update));
}}
onClick={() => { dispatch(destroy(p.uuid)); }}
disabled={false}
valueType="number" />;
return <Row key={p.uuid + p.body.id}>
<Col xs={6}>
<input type="text"
placeholder={t("Name")}
value={p.body.label}
onChange={e =>
dispatch(edit(p, { label: e.currentTarget.value }))} />
</Col>
<Col xs={4}>
<FBSelect
selectedItem={{
label: t("Pin ") + `${p.body.pin}`,
value: p.body.pin || ""
}}
onChange={d =>
dispatch(edit(p, { pin: parseInt(d.value.toString(), 10) }))}
list={pinDropdowns(n => n)} />
</Col>
<Col xs={2}>
<button
className="red fb-button"
onClick={() => dispatch(destroy(p.uuid))}>
<i className="fa fa-minus" />
</button>
</Col>
</Row>;
})}
</div>;
}

View File

@ -1,5 +1,4 @@
import * as React from "react";
import { error } from "farmbot-toastr";
import { SensorList } from "./sensor_list";
import { SensorForm } from "./sensor_form";
@ -85,7 +84,7 @@ export class Sensors extends React.Component<SensorsProps, SensorState> {
className="fb-button green"
type="button"
onClick={this.stockSensors}>
<i className="fa fa-plus" />
<i className="fa fa-plus" style={{ marginRight: "0.5rem" }} />
{t("Stock sensors")}
</button>
</WidgetHeader>

View File

@ -286,6 +286,9 @@
.save-btn {
margin: 1rem;
}
.location-form {
width: 100% !important;
}
}
.add-farm-event-panel button.red,

View File

@ -280,7 +280,8 @@ a {
.drag-drop-area {
&.visible {
margin: 0.75rem 0;
margin-right: 15px;
margin-right: 25px;
margin-left: 10px;
border-style: dashed;
border-width: 2px;
border-color: $light_gray;
@ -984,6 +985,10 @@ ul {
}
}
.map-size-inputs {
margin-top: 1rem;
}
.release-notes-button {
font-weight: bold;
cursor: pointer;
@ -1048,9 +1053,6 @@ ul {
p {
margin-top: 0.75rem;
}
.fa-plus {
margin-right: 0.5rem;
}
.sensor-reading-display {
&.moisture-sensor {
background: linear-gradient(to right, rgba($blue, 0) 20%, $blue 80%, rgba($blue, 0) 85%);

View File

@ -70,6 +70,12 @@
}
}
.sequence-editor-content {
hr {
margin-right: 15px;
}
}
.sequence-editor-tools,
.regimen-editor-tools {
margin-right: 15px;
@ -129,7 +135,8 @@
}
.sequence-steps {
margin-right: 15px;
margin-right: 25px;
margin-left: 10px;
}
.step-button-cluster,

View File

@ -6,36 +6,22 @@ describe("getDefaultAxisLength()", () => {
const axes = getDefaultAxisLength(() => false);
expect(axes).toEqual({ x: 2900, y: 1400 });
});
it("returns XL axis lengths", () => {
const axes = getDefaultAxisLength(() => true);
expect(axes).toEqual({ x: 5900, y: 2900 });
});
});
describe("getGridSize()", () => {
it("returns default grid size", () => {
const grid = getGridSize(
k => ({ dynamic_map: false, map_xl: false } as WebAppConfig)[k], {
k => ({ dynamic_map: false } as WebAppConfig)[k], {
x: { value: 100, isDefault: false },
y: { value: 200, isDefault: false }
});
expect(grid).toEqual({ x: 2900, y: 1400 });
});
it("returns XL grid size", () => {
const grid = getGridSize(
k => ({ dynamic_map: false, map_xl: true } as WebAppConfig)[k], {
x: { value: 100, isDefault: false },
y: { value: 200, isDefault: false }
});
expect(grid).toEqual({ x: 5900, y: 2900 });
});
it("returns custom grid size", () => {
const grid = getGridSize(
k => ({
dynamic_map: false, map_xl: true, map_size_x: 300, map_size_y: 400
dynamic_map: false, map_size_x: 300, map_size_y: 400
} as WebAppConfig)[k], {
x: { value: 100, isDefault: false },
y: { value: 200, isDefault: false }
@ -45,7 +31,7 @@ describe("getGridSize()", () => {
it("returns grid size using bot size", () => {
const grid = getGridSize(
k => ({ dynamic_map: true, map_xl: false } as WebAppConfig)[k], {
k => ({ dynamic_map: true } as WebAppConfig)[k], {
x: { value: 100, isDefault: false },
y: { value: 200, isDefault: false }
});

View File

@ -26,11 +26,7 @@ export const getDefaultAxisLength =
if (isFinite(mapSizeX) && isFinite(mapSizeY)) {
return { x: mapSizeX, y: mapSizeY };
}
if (getConfigValue(BooleanSetting.map_xl)) {
return { x: 5900, y: 2900 };
} else {
return { x: 2900, y: 1400 };
}
return { x: 2900, y: 1400 };
};
export const getGridSize =

View File

@ -147,7 +147,7 @@ export function PlantPanel(props: PlantPanelProps) {
const {
info, onDestroy, updatePlant, dispatch, inSavedGarden, timeSettings
} = props;
const { name, slug, plantedAt, daysOld, uuid, plantStatus } = info;
const { slug, plantedAt, daysOld, uuid, plantStatus } = info;
let { x, y } = info;
const isEditing = !!onDestroy;
if (isEditing) { x = round(x); y = round(y); }
@ -157,9 +157,6 @@ export function PlantPanel(props: PlantPanelProps) {
{t("Plant Info")}
</label>
<ul>
<ListItem name={t("Full Name")}>
{startCase(name)}
</ListItem>
<ListItem name={t("Plant Type")}>
<Link
title={t("View crop info")}

View File

@ -1,5 +1,4 @@
import * as React from "react";
import { AccountMenuProps } from "./interfaces";
import { docLink } from "../ui/doc_link";
import { Link } from "../link";
@ -21,11 +20,10 @@ export const AdditionalMenu = (props: AccountMenuProps) => {
{t("Logs")}
</Link>
</div>
{DevSettings.futureFeaturesEnabled() &&
<Link to="/app/help" onClick={props.close("accountMenuOpen")}>
<i className="fa fa-question-circle"></i>
{t("Help")}
</Link>}
<Link to="/app/help" onClick={props.close("accountMenuOpen")}>
<i className="fa fa-question-circle"></i>
{t("Help")}
</Link>
{!DevSettings.futureFeaturesEnabled() &&
<div>
<a href={docLink("the-farmbot-web-app")}

View File

@ -15,6 +15,15 @@ import { VariableDeclaration } from "farmbot";
import { clickButton } from "../../../__test_support__/helpers";
import { Actions } from "../../../constants";
const testVariable: VariableDeclaration = {
kind: "variable_declaration",
args: {
label: "label", data_value: {
kind: "identifier", args: { label: "new_var" }
}
}
};
describe("<ActiveEditor />", () => {
const fakeProps = (): ActiveEditorProps => ({
dispatch: jest.fn(),
@ -64,22 +73,55 @@ describe("<ActiveEditor />", () => {
type: Actions.SET_SCHEDULER_STATE, payload: true
});
});
it("has correct height without variable form", () => {
const p = fakeProps();
p.regimen.body.body = [];
p.shouldDisplay = () => true;
const wrapper = mount(<ActiveEditor {...p} />);
expect(wrapper.find(".regimen").props().style).toEqual({
height: "calc(100vh - 200px)"
});
});
it("has correct height with variable form", () => {
const p = fakeProps();
p.regimen.body.body = [testVariable];
p.shouldDisplay = () => true;
const wrapper = mount(<ActiveEditor {...p} />);
expect(wrapper.find(".regimen").props().style)
.toEqual({ height: "calc(100vh - 500px)" });
});
it("has correct height with variable form collapsed", () => {
const p = fakeProps();
p.regimen.body.body = [testVariable];
p.shouldDisplay = () => true;
const wrapper = mount(<ActiveEditor {...p} />);
wrapper.setState({ variablesCollapsed: true });
expect(wrapper.find(".regimen").props().style)
.toEqual({ height: "calc(100vh - 300px)" });
});
it("automatically calculates height", () => {
document.getElementById = () => ({ offsetHeight: 101 } as HTMLElement);
const wrapper = mount(<ActiveEditor {...fakeProps()} />);
expect(wrapper.find(".regimen").props().style)
.toEqual({ height: "calc(100vh - 301px)" });
});
it("toggles variable form state", () => {
const wrapper = mount<ActiveEditor>(<ActiveEditor {...fakeProps()} />);
wrapper.instance().toggleVarShow();
expect(wrapper.state()).toEqual({ variablesCollapsed: true });
});
});
describe("editRegimenVariables()", () => {
const variables: VariableDeclaration = {
kind: "variable_declaration",
args: {
label: "label", data_value: {
kind: "identifier", args: { label: "new_var" }
}
}
};
it("updates bodyVariables", () => {
const regimen = fakeRegimen();
editRegimenVariables({ dispatch: jest.fn(), regimen })([])(variables);
editRegimenVariables({ dispatch: jest.fn(), regimen })([])(testVariable);
expect(overwrite).toHaveBeenCalledWith(regimen,
expect.objectContaining({ body: [variables] }));
expect.objectContaining({ body: [testVariable] }));
});
});

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { RegimenNameInput } from "./regimen_name_input";
import { ActiveEditorProps } from "./interfaces";
import { ActiveEditorProps, ActiveEditorState } from "./interfaces";
import { push } from "../../history";
import {
RegimenItem, CalendarRow, RegimenItemCalendarRow, RegimenProps
@ -22,26 +22,47 @@ import { Actions } from "../../constants";
* The bottom half of the regimen editor panel (when there's something to
* actually edit).
*/
export function ActiveEditor(props: ActiveEditorProps) {
const regimenProps = { regimen: props.regimen, dispatch: props.dispatch };
return <div className="regimen-editor-content">
<div className="regimen-editor-tools">
<RegimenButtonGroup {...regimenProps} />
<RegimenNameInput {...regimenProps} />
<LocalsList
locationDropdownKey={JSON.stringify(props.regimen)}
bodyVariables={props.regimen.body.body}
variableData={props.variableData}
sequenceUuid={props.regimen.uuid}
resources={props.resources}
onChange={editRegimenVariables(regimenProps)(props.regimen.body.body)}
allowedVariableNodes={AllowedVariableNodes.parameter}
shouldDisplay={props.shouldDisplay} />
<hr />
</div>
<OpenSchedulerButton dispatch={props.dispatch} />
<RegimenRows calendar={props.calendar} dispatch={props.dispatch} />
</div>;
export class ActiveEditor
extends React.Component<ActiveEditorProps, ActiveEditorState> {
state: ActiveEditorState = { variablesCollapsed: false };
get regimenProps() {
return { regimen: this.props.regimen, dispatch: this.props.dispatch };
}
toggleVarShow = () =>
this.setState({ variablesCollapsed: !this.state.variablesCollapsed });
LocalsList = () => {
const { regimen } = this.props;
return <LocalsList
locationDropdownKey={JSON.stringify(regimen)}
bodyVariables={regimen.body.body}
variableData={this.props.variableData}
sequenceUuid={regimen.uuid}
resources={this.props.resources}
onChange={editRegimenVariables(this.regimenProps)(regimen.body.body)}
collapsible={true}
collapsed={this.state.variablesCollapsed}
toggleVarShow={this.toggleVarShow}
allowedVariableNodes={AllowedVariableNodes.parameter}
shouldDisplay={this.props.shouldDisplay} />;
}
render() {
return <div className="regimen-editor-content">
<div className="regimen-editor-tools">
<RegimenButtonGroup {...this.regimenProps} />
<RegimenNameInput {...this.regimenProps} />
<this.LocalsList />
<hr />
</div>
<OpenSchedulerButton dispatch={this.props.dispatch} />
<RegimenRows {...this.regimenProps}
calendar={this.props.calendar}
varsCollapsed={this.state.variablesCollapsed} />
</div>;
}
}
export const OpenSchedulerButton = (props: { dispatch: Function }) =>
@ -73,13 +94,29 @@ const RegimenButtonGroup = (props: RegimenProps) =>
</button>
</div>;
/** Make room for the regimen header variable form when necessary. */
const regimenSectionHeight =
(regimen: TaggedRegimen, varsCollapsed: boolean) => {
let subHeight = 200;
const variables = regimen.body.body.length > 0;
if (variables) { subHeight = 500; }
if (varsCollapsed) { subHeight = 300; }
const variablesDiv = document.getElementById("regimen-editor-tools");
if (variablesDiv) { subHeight = 200 + variablesDiv.offsetHeight; }
return `calc(100vh - ${subHeight}px)`;
};
interface RegimenRowsProps {
regimen: TaggedRegimen;
calendar: CalendarRow[];
dispatch: Function;
varsCollapsed: boolean;
}
const RegimenRows = (props: RegimenRowsProps) =>
<div className="regimen">
<div className="regimen" style={{
height: regimenSectionHeight(props.regimen, props.varsCollapsed)
}}>
{props.calendar.map(regimenDay(props.dispatch))}
</div>;

View File

@ -17,6 +17,10 @@ export interface ActiveEditorProps {
variableData: VariableNameSet;
}
export interface ActiveEditorState {
variablesCollapsed: boolean;
}
export interface RegimenItemListProps {
calendar: RegimenItemCalendarRow[];
dispatch: Function;

View File

@ -0,0 +1,32 @@
import * as React from "react";
import { shallow } from "enzyme";
import { FilterSearch } from "../filter_search";
import { DropDownItem } from "../fb_select";
describe("<FilterSearch />", () => {
const fakeItem = (extra?: Partial<DropDownItem>): DropDownItem =>
Object.assign({ label: "label", value: "value" }, extra);
const fakeProps = () => ({
items: [],
selectedItem: fakeItem(),
onChange: jest.fn(),
nullChoice: fakeItem(),
});
it("selects item", () => {
const p = fakeProps();
const wrapper = shallow(<FilterSearch {...p} />);
const item = fakeItem();
wrapper.simulate("ItemSelect", item);
expect(p.onChange).toHaveBeenCalledWith(item);
});
it("doesn't select header", () => {
const p = fakeProps();
const wrapper = shallow(<FilterSearch {...p} />);
const item = fakeItem({ heading: true });
wrapper.simulate("ItemSelect", item);
expect(p.onChange).not.toHaveBeenCalled();
});
});

View File

@ -75,7 +75,7 @@ export class FilterSearch extends React.Component<Props, Partial<State>> {
}
private handleValueChange = (item: DropDownItem | undefined) => {
if (item) {
if (item && !item.heading) {
this.props.onChange(item);
this.setState({ item });
}