commit
4931d96d74
|
@ -27,8 +27,9 @@ if Rails.env == "development"
|
|||
SavedGarden,
|
||||
SensorReading,
|
||||
FarmwareInstallation,
|
||||
Device,
|
||||
PointGroup,
|
||||
Tool,
|
||||
Device,
|
||||
Delayed::Job,
|
||||
Delayed::Backend::ActiveRecord::Job,
|
||||
].map(&:delete_all)
|
||||
|
|
|
@ -9,7 +9,8 @@ import {
|
|||
} from "../resources/selectors";
|
||||
import { Props } from "./interfaces";
|
||||
import {
|
||||
validFwConfig, shouldDisplay as shouldDisplayFunc,
|
||||
validFwConfig,
|
||||
createShouldDisplayFn as shouldDisplayFunc,
|
||||
determineInstalledOsVersion
|
||||
} from "../util";
|
||||
import { getWebAppConfigValue } from "../config_storage/actions";
|
||||
|
|
|
@ -181,7 +181,7 @@
|
|||
transition: all 0.2s ease;
|
||||
}
|
||||
}
|
||||
.plant-search-item {
|
||||
%search-item {
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 1rem;
|
||||
&:hover,
|
||||
|
@ -195,6 +195,19 @@
|
|||
width: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.plant-search-item {
|
||||
@extend %search-item;
|
||||
}
|
||||
|
||||
.group-search-item {
|
||||
@extend %search-item;
|
||||
&:hover,
|
||||
&.hovered {
|
||||
background: #d7eaea;
|
||||
}
|
||||
}
|
||||
|
||||
%panel-item-base {
|
||||
text-align: right;
|
||||
font-size: 1rem;
|
||||
|
|
|
@ -229,9 +229,8 @@
|
|||
max-height: calc(100vh - 10rem);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
.plant-search-item {
|
||||
pointer-events: none;
|
||||
}
|
||||
.plant-search-item,
|
||||
.group-search-item { pointer-events: none; }
|
||||
img {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import axios from "axios";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { FarmbotOsProps, FarmbotOsState } from "../interfaces";
|
||||
import { FarmbotOsProps, FarmbotOsState, Feature } from "../interfaces";
|
||||
import { Widget, WidgetHeader, WidgetBody, Row, Col } from "../../ui";
|
||||
import { save, edit } from "../../api/crud";
|
||||
import { MustBeOnline, isBotOnline } from "../must_be_online";
|
||||
|
@ -157,7 +157,7 @@ export class FarmbotOsSettings
|
|||
</label>
|
||||
</Col>
|
||||
<Col xs={7}>
|
||||
<BootSequenceSelector />
|
||||
{this.props.shouldDisplay(Feature.boot_sequence) && <BootSequenceSelector />}
|
||||
</Col>
|
||||
</Row>
|
||||
<PowerAndReset
|
||||
|
|
|
@ -65,29 +65,31 @@ export type SourceFwConfig = (config: McuParamName) =>
|
|||
export type ShouldDisplay = (x: Feature) => boolean;
|
||||
/** Names of features that use minimum FBOS version checking. */
|
||||
export enum Feature {
|
||||
assertion_block = "assertion_block",
|
||||
named_pins = "named_pins",
|
||||
sensors = "sensors",
|
||||
change_ownership = "change_ownership",
|
||||
variables = "variables",
|
||||
loops = "loops",
|
||||
api_pin_bindings = "api_pin_bindings",
|
||||
farmduino_k14 = "farmduino_k14",
|
||||
jest_feature = "jest_feature", // for tests
|
||||
backscheduled_regimens = "backscheduled_regimens",
|
||||
endstop_invert = "endstop_invert",
|
||||
diagnostic_dumps = "diagnostic_dumps",
|
||||
rpi_led_control = "rpi_led_control",
|
||||
mark_as_step = "mark_as_step",
|
||||
firmware_restart = "firmware_restart",
|
||||
api_farmware_installations = "api_farmware_installations",
|
||||
api_farmware_env = "api_farmware_env",
|
||||
use_update_channel = "use_update_channel",
|
||||
long_scaling_factor = "long_scaling_factor",
|
||||
flash_firmware = "flash_firmware",
|
||||
api_farmware_installations = "api_farmware_installations",
|
||||
api_pin_bindings = "api_pin_bindings",
|
||||
assertion_block = "assertion_block",
|
||||
backscheduled_regimens = "backscheduled_regimens",
|
||||
boot_sequence = "boot_sequence",
|
||||
change_ownership = "change_ownership",
|
||||
diagnostic_dumps = "diagnostic_dumps",
|
||||
endstop_invert = "endstop_invert",
|
||||
express_k10 = "express_k10",
|
||||
farmduino_k14 = "farmduino_k14",
|
||||
firmware_restart = "firmware_restart",
|
||||
flash_firmware = "flash_firmware",
|
||||
groups = "groups",
|
||||
jest_feature = "jest_feature",
|
||||
long_scaling_factor = "long_scaling_factor",
|
||||
mark_as_step = "mark_as_step",
|
||||
named_pins = "named_pins",
|
||||
none_firmware = "none_firmware",
|
||||
rpi_led_control = "rpi_led_control",
|
||||
sensors = "sensors",
|
||||
use_update_channel = "use_update_channel",
|
||||
variables = "variables"
|
||||
}
|
||||
|
||||
/** Object fetched from FEATURE_MIN_VERSIONS_URL. */
|
||||
export type MinOsFeatureLookup = Partial<Record<Feature, string>>;
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from "./components/source_config_value";
|
||||
import {
|
||||
determineInstalledOsVersion, validFwConfig, validFbosConfig,
|
||||
shouldDisplay as shouldDisplayFunc
|
||||
createShouldDisplayFn as shouldDisplayFunc
|
||||
} from "../util";
|
||||
import {
|
||||
saveOrEditFarmwareEnv, reduceFarmwareEnv
|
||||
|
@ -27,8 +27,9 @@ export function mapStateToProps(props: Everything): Props {
|
|||
const installedOsVersion = determineInstalledOsVersion(
|
||||
props.bot, maybeGetDevice(props.resources.index));
|
||||
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
|
||||
const shouldDisplay = shouldDisplayFunc(
|
||||
installedOsVersion, props.bot.minOsFeatureData, fbosVersionOverride);
|
||||
const shouldDisplay = shouldDisplayFunc(installedOsVersion,
|
||||
props.bot.minOsFeatureData,
|
||||
fbosVersionOverride);
|
||||
const env = shouldDisplay(Feature.api_farmware_env)
|
||||
? reduceFarmwareEnv(props.resources.index)
|
||||
: props.bot.hardware.user_env;
|
||||
|
|
|
@ -21,7 +21,7 @@ describe("mapStateToPropsAddEdit()", () => {
|
|||
describe("handleTime()", () => {
|
||||
const { handleTime } = mapStateToPropsAddEdit(fakeState());
|
||||
|
||||
it("start_time", () => {
|
||||
it("handles an element with name `start_time`", () => {
|
||||
const e = {
|
||||
currentTarget: { value: "10:54", name: "start_time" }
|
||||
} as React.SyntheticEvent<HTMLInputElement>;
|
||||
|
@ -29,13 +29,21 @@ describe("mapStateToPropsAddEdit()", () => {
|
|||
expect(result).toContain("54");
|
||||
});
|
||||
|
||||
it("end_time", () => {
|
||||
it("handles an element with name `end_time`", () => {
|
||||
const e = {
|
||||
currentTarget: { value: "10:53", name: "end_time" }
|
||||
} as React.SyntheticEvent<HTMLInputElement>;
|
||||
const result = handleTime(e, "2017-05-21T22:00:00.000");
|
||||
expect(result).toContain("53");
|
||||
});
|
||||
|
||||
it("crashes on other names", () => {
|
||||
const e = {
|
||||
currentTarget: { value: "10:52", name: "other" }
|
||||
} as React.SyntheticEvent<HTMLInputElement>;
|
||||
const boom = () => handleTime(e, "2017-05-21T22:00:00.000");
|
||||
expect(boom).toThrowError("Expected a name attribute from time field.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("executableOptions()", () => {
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
import { DropDownItem } from "../../ui/index";
|
||||
import {
|
||||
validFbosConfig,
|
||||
shouldDisplay as shouldDisplayFunc,
|
||||
createShouldDisplayFn as shouldDisplayFunc,
|
||||
determineInstalledOsVersion
|
||||
} from "../../util";
|
||||
import {
|
||||
|
@ -134,7 +134,6 @@ export function mapStateToPropsAddEdit(props: Everything): AddEditFarmEventProps
|
|||
switch (kind) {
|
||||
case "Sequence": return findSequenceById(props.resources.index, id);
|
||||
case "Regimen": return findRegimenById(props.resources.index, id);
|
||||
default: throw new Error("GOT A BAD `KIND` STRING");
|
||||
}
|
||||
};
|
||||
const dev = getDeviceAccountSettings(props.resources.index);
|
||||
|
|
|
@ -9,14 +9,12 @@ jest.mock("../../actions", () => ({
|
|||
}));
|
||||
|
||||
import React from "react";
|
||||
import { GroupDetailActive, LittleIcon } from "../group_detail_active";
|
||||
import { GroupDetailActive } from "../group_detail_active";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import {
|
||||
fakePointGroup, fakePlant
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { save, overwrite, edit } from "../../../api/crud";
|
||||
import { toggleHoveredPlant } from "../../actions";
|
||||
import { DEFAULT_ICON } from "../../../open_farm/icons";
|
||||
import { save, edit } from "../../../api/crud";
|
||||
import { SpecialStatus } from "farmbot";
|
||||
|
||||
describe("<GroupDetailActive/>", () => {
|
||||
|
@ -30,47 +28,6 @@ describe("<GroupDetailActive/>", () => {
|
|||
group.body.point_ids = [plant.body.id];
|
||||
return { dispatch: jest.fn(), group, plants };
|
||||
}
|
||||
const icon = "doge.jpg";
|
||||
|
||||
it("removes points onClick", () => {
|
||||
const { plants, dispatch, group } = fakeProps();
|
||||
const el = shallow(<LittleIcon
|
||||
plant={plants[0]}
|
||||
group={group}
|
||||
dispatch={dispatch}
|
||||
icon="doge.jpg" />);
|
||||
el.simulate("click");
|
||||
const emptyGroup = expect.objectContaining({
|
||||
name: "XYZ",
|
||||
point_ids: []
|
||||
});
|
||||
expect(overwrite).toHaveBeenCalledWith(group, emptyGroup);
|
||||
expect(dispatch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("toggles onMouseEnter", () => {
|
||||
const { plants, dispatch, group } = fakeProps();
|
||||
const plant = plants[0];
|
||||
const el = shallow(<LittleIcon
|
||||
plant={plant}
|
||||
group={group}
|
||||
dispatch={dispatch}
|
||||
icon={icon} />);
|
||||
el.simulate("mouseEnter");
|
||||
expect(toggleHoveredPlant).toHaveBeenCalledWith(plant.uuid, icon);
|
||||
});
|
||||
|
||||
it("toggled onMouseLeave", () => {
|
||||
const { plants, dispatch, group } = fakeProps();
|
||||
const plant = plants[0];
|
||||
const el = shallow(<LittleIcon
|
||||
plant={plant}
|
||||
group={group}
|
||||
dispatch={dispatch}
|
||||
icon={icon} />);
|
||||
el.simulate("mouseLeave");
|
||||
expect(toggleHoveredPlant).toHaveBeenCalledWith(undefined, icon);
|
||||
});
|
||||
|
||||
it("saves", () => {
|
||||
const p = fakeProps();
|
||||
|
@ -87,19 +44,6 @@ describe("<GroupDetailActive/>", () => {
|
|||
expect(el.find("input").prop("defaultValue")).toContain("XYZ");
|
||||
});
|
||||
|
||||
it("provides the DEFAULT_ICON when OF has no icon to provide", () => {
|
||||
const plant = fakePlant();
|
||||
const comp = new GroupDetailActive(fakeProps());
|
||||
comp.state = {
|
||||
[plant.uuid]: {
|
||||
slug: plant.uuid,
|
||||
svg_icon: undefined
|
||||
}
|
||||
};
|
||||
const result = comp.findIcon(plant);
|
||||
expect(result).toEqual(DEFAULT_ICON);
|
||||
});
|
||||
|
||||
it("changes group name", () => {
|
||||
const NEW_NAME = "new group name";
|
||||
const wrapper = shallow(<GroupDetailActive {...fakeProps()} />);
|
||||
|
@ -123,7 +67,7 @@ describe("<GroupDetailActive/>", () => {
|
|||
},
|
||||
kind: "PointGroup",
|
||||
specialStatus: "DIRTY",
|
||||
uuid: "PointGroup.0.16",
|
||||
uuid: p.group.uuid,
|
||||
},
|
||||
{ sort_type: "random" });
|
||||
});
|
||||
|
|
|
@ -39,7 +39,7 @@ describe("<GroupListPanel />", () => {
|
|||
it("renders relevant group data as a list", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<GroupListPanel {...p} />);
|
||||
wrapper.find(".plant-search-item").first().simulate("click");
|
||||
wrapper.find(".group-search-item").first().simulate("click");
|
||||
expect(history.push).toHaveBeenCalledWith("/app/designer/groups/9");
|
||||
|
||||
["3 items",
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
jest.mock("../../actions", () => ({ toggleHoveredPlant: jest.fn() }));
|
||||
jest.mock("../../../api/crud", () => ({ overwrite: jest.fn() }));
|
||||
|
||||
import React from "react";
|
||||
import { PointGroupItem } from "../point_group_item";
|
||||
import { shallow } from "enzyme";
|
||||
import { fakePlant, fakePointGroup } from "../../../__test_support__/fake_state/resources";
|
||||
import { DeepPartial } from "redux";
|
||||
import { cachedCrop } from "../../../open_farm/cached_crop";
|
||||
import { toggleHoveredPlant } from "../../actions";
|
||||
import { overwrite } from "../../../api/crud";
|
||||
|
||||
describe("<PointGroupItem/>", () => {
|
||||
const newProps = (): PointGroupItem["props"] => ({
|
||||
dispatch: jest.fn(),
|
||||
plant: fakePlant(),
|
||||
group: fakePointGroup(),
|
||||
hovered: true
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const props = newProps();
|
||||
const el = shallow<HTMLSpanElement>(<PointGroupItem {...props} />);
|
||||
const i = el.instance() as PointGroupItem;
|
||||
expect(el.first().prop("onMouseEnter")).toEqual(i.enter);
|
||||
expect(el.first().prop("onMouseLeave")).toEqual(i.leave);
|
||||
expect(el.first().prop("onClick")).toEqual(i.click);
|
||||
});
|
||||
|
||||
it("handles hovering", async () => {
|
||||
const i = new PointGroupItem(newProps());
|
||||
i.setState = jest.fn();
|
||||
type E = React.SyntheticEvent<HTMLImageElement, Event>;
|
||||
const partialE: DeepPartial<E> = {
|
||||
currentTarget: {
|
||||
getAttribute: jest.fn(),
|
||||
setAttribute: jest.fn(),
|
||||
}
|
||||
};
|
||||
const e = partialE as E;
|
||||
await i.maybeGetCachedIcon(e as E);
|
||||
const slug = i.props.plant.body.openfarm_slug;
|
||||
expect(cachedCrop).toHaveBeenCalledWith(slug);
|
||||
const icon = "data:image/svg+xml;utf8,icon";
|
||||
expect(i.setState).toHaveBeenCalledWith({ icon });
|
||||
expect(e.currentTarget.setAttribute).toHaveBeenCalledWith("src", icon);
|
||||
});
|
||||
|
||||
it("handles mouse enter", () => {
|
||||
const i = new PointGroupItem(newProps());
|
||||
i.state.icon = "X";
|
||||
i.enter();
|
||||
expect(i.props.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(toggleHoveredPlant)
|
||||
.toHaveBeenCalledWith(i.props.plant.uuid, "X");
|
||||
});
|
||||
|
||||
it("handles mouse exit", () => {
|
||||
const i = new PointGroupItem(newProps());
|
||||
i.state.icon = "X";
|
||||
i.leave();
|
||||
expect(i.props.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(toggleHoveredPlant).toHaveBeenCalledWith(undefined, "");
|
||||
});
|
||||
|
||||
it("handles clicks", () => {
|
||||
const i = new PointGroupItem(newProps());
|
||||
i.click();
|
||||
expect(i.props.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(overwrite).toHaveBeenCalledWith({
|
||||
body: { name: "Fake", point_ids: [], sort_type: "xy_ascending" },
|
||||
kind: "PointGroup",
|
||||
specialStatus: "",
|
||||
uuid: expect.any(String),
|
||||
}, {
|
||||
name: "Fake",
|
||||
point_ids: [],
|
||||
sort_type: "xy_ascending",
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,14 +8,13 @@ import {
|
|||
} from "../plants/designer_panel";
|
||||
import { TaggedPointGroup } from "farmbot";
|
||||
import { DeleteButton } from "../../controls/pin_form_fields";
|
||||
import { svgToUrl, DEFAULT_ICON } from "../../open_farm/icons";
|
||||
import { overwrite, save, edit } from "../../api/crud";
|
||||
import { save, edit } from "../../api/crud";
|
||||
import { Dictionary } from "lodash";
|
||||
import { cachedCrop, OFIcon } from "../../open_farm/cached_crop";
|
||||
import { toggleHoveredPlant } from "../actions";
|
||||
import { OFIcon } from "../../open_farm/cached_crop";
|
||||
import { TaggedPlant } from "../map/interfaces";
|
||||
import { PointGroupSortSelector, sortGroupBy } from "./point_group_sort_selector";
|
||||
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
||||
import { PointGroupItem } from "./point_group_item";
|
||||
|
||||
interface GroupDetailActiveProps {
|
||||
dispatch: Function;
|
||||
|
@ -24,34 +23,6 @@ interface GroupDetailActiveProps {
|
|||
}
|
||||
|
||||
type State = Dictionary<OFIcon | undefined>;
|
||||
const removePoint = (group: TaggedPointGroup, pointId: number) => {
|
||||
type Body = (typeof group)["body"];
|
||||
const nextGroup: Body = { ...group.body };
|
||||
nextGroup.point_ids = nextGroup.point_ids.filter(x => x !== pointId);
|
||||
return overwrite(group, nextGroup);
|
||||
};
|
||||
|
||||
interface LittleIconProps {
|
||||
/** URL (or even a data-url) to the icon image. */
|
||||
icon: string;
|
||||
group: TaggedPointGroup;
|
||||
plant: TaggedPlant;
|
||||
dispatch: Function;
|
||||
}
|
||||
|
||||
export const LittleIcon =
|
||||
({ group, plant: point, icon, dispatch }: LittleIconProps) => {
|
||||
const { body } = point;
|
||||
const p = point;
|
||||
const plantUUID = point.uuid;
|
||||
return <span
|
||||
key={plantUUID}
|
||||
onMouseEnter={() => dispatch(toggleHoveredPlant(plantUUID, icon))}
|
||||
onMouseLeave={() => dispatch(toggleHoveredPlant(undefined, icon))}
|
||||
onClick={() => dispatch(removePoint(group, body.id || 0))}>
|
||||
<img src={icon} alt={p.body.name} width={32} height={32} />
|
||||
</span>;
|
||||
};
|
||||
|
||||
export class GroupDetailActive
|
||||
extends React.Component<GroupDetailActiveProps, State> {
|
||||
|
@ -61,41 +32,14 @@ export class GroupDetailActive
|
|||
this.props.dispatch(edit(this.props.group, { name: currentTarget.value }));
|
||||
};
|
||||
|
||||
handleIcon =
|
||||
(uuid: string) =>
|
||||
(icon: Readonly<OFIcon>) =>
|
||||
this.setState({ [uuid]: icon });
|
||||
|
||||
performLookup = (plant: TaggedPlant) => {
|
||||
cachedCrop(plant.body.openfarm_slug).then(this.handleIcon(plant.uuid));
|
||||
return DEFAULT_ICON;
|
||||
}
|
||||
|
||||
findIcon = (plant: TaggedPlant) => {
|
||||
const svg = this.state[plant.uuid];
|
||||
if (svg) {
|
||||
if (svg.svg_icon) {
|
||||
return svgToUrl(svg.svg_icon);
|
||||
}
|
||||
return DEFAULT_ICON;
|
||||
}
|
||||
return this.performLookup(plant);
|
||||
|
||||
}
|
||||
|
||||
get name() {
|
||||
const { group } = this.props;
|
||||
return group ? group.body.name : "Group Not found";
|
||||
}
|
||||
|
||||
get icons() {
|
||||
const plants = sortGroupBy(this.props.group.body.sort_type,
|
||||
this.props.plants);
|
||||
|
||||
return plants.map(point => {
|
||||
return <LittleIcon
|
||||
return <PointGroupItem
|
||||
key={point.uuid}
|
||||
icon={this.findIcon(point)}
|
||||
hovered={false}
|
||||
group={this.props.group}
|
||||
plant={point}
|
||||
dispatch={this.props.dispatch} />;
|
||||
|
@ -130,7 +74,7 @@ export class GroupDetailActive
|
|||
panelName={"groups"}>
|
||||
<label>{t("GROUP NAME")}{this.saved ? "" : "*"}</label>
|
||||
<input
|
||||
defaultValue={this.name}
|
||||
defaultValue={this.props.group.body.name}
|
||||
onChange={this.update}
|
||||
onBlur={this.saveGroup} />
|
||||
<PointGroupSortSelector
|
||||
|
|
|
@ -12,8 +12,8 @@ interface GroupInventoryItemProps {
|
|||
export function GroupInventoryItem(props: GroupInventoryItemProps) {
|
||||
return <div
|
||||
onClick={props.onClick}
|
||||
className={`plant-search-item ${props.hovered ? "hovered" : ""}`}>
|
||||
<span className="plant-search-item-name">
|
||||
className={`group-search-item ${props.hovered ? "hovered" : ""}`}>
|
||||
<span className="group-search-item-name">
|
||||
{props.group.body.name}
|
||||
</span>
|
||||
<i className="group-item-count">
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import * as React from "react";
|
||||
import { DEFAULT_ICON, svgToUrl } from "../../open_farm/icons";
|
||||
import { TaggedPlant } from "../map/interfaces";
|
||||
import { cachedCrop } from "../../open_farm/cached_crop";
|
||||
import { toggleHoveredPlant } from "../actions";
|
||||
import { TaggedPointGroup, uuid } from "farmbot";
|
||||
import { overwrite } from "../../api/crud";
|
||||
|
||||
type IMGEvent = React.SyntheticEvent<HTMLImageElement>;
|
||||
|
||||
export interface PointGroupItemProps {
|
||||
plant: TaggedPlant;
|
||||
group: TaggedPointGroup;
|
||||
dispatch: Function;
|
||||
hovered: boolean;
|
||||
}
|
||||
|
||||
interface PointGroupItemState { icon: string; }
|
||||
|
||||
const removePoint = (group: TaggedPointGroup, pointId: number) => {
|
||||
type Body = (typeof group)["body"];
|
||||
const nextGroup: Body = { ...group.body };
|
||||
nextGroup.point_ids = nextGroup.point_ids.filter(x => x !== pointId);
|
||||
return overwrite(group, nextGroup);
|
||||
};
|
||||
|
||||
// The individual plants in the point group detail page.
|
||||
export class PointGroupItem extends React.Component<PointGroupItemProps, PointGroupItemState> {
|
||||
|
||||
state: PointGroupItemState = { icon: "" };
|
||||
|
||||
key = uuid();
|
||||
|
||||
enter = () => this
|
||||
.props
|
||||
.dispatch(toggleHoveredPlant(this.props.plant.uuid, this.state.icon));
|
||||
|
||||
leave = () => this
|
||||
.props
|
||||
.dispatch(toggleHoveredPlant(undefined, ""));
|
||||
|
||||
click = () => this
|
||||
.props
|
||||
.dispatch(removePoint(this.props.group, this.props.plant.body.id || 0));
|
||||
|
||||
maybeGetCachedIcon = ({ currentTarget }: IMGEvent) => {
|
||||
return cachedCrop(this.props.plant.body.openfarm_slug).then((crop) => {
|
||||
const i = svgToUrl(crop.svg_icon);
|
||||
if (i !== currentTarget.getAttribute("src")) {
|
||||
currentTarget.setAttribute("src", i);
|
||||
}
|
||||
this.setState({ icon: i });
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return <span
|
||||
key={this.key}
|
||||
onMouseEnter={this.enter}
|
||||
onMouseLeave={this.leave}
|
||||
onClick={this.click}>
|
||||
<img
|
||||
src={DEFAULT_ICON}
|
||||
onLoad={this.maybeGetCachedIcon}
|
||||
width={32}
|
||||
height={32} />
|
||||
</span>;
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from "../resources/selectors";
|
||||
import {
|
||||
validBotLocationData, validFwConfig, unpackUUID,
|
||||
shouldDisplay as shouldDisplayFunc,
|
||||
createShouldDisplayFn as shouldDisplayFunc,
|
||||
determineInstalledOsVersion
|
||||
} from "../util";
|
||||
import { getWebAppConfigValue } from "../config_storage/actions";
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from "../resources/selectors_by_kind";
|
||||
import {
|
||||
determineInstalledOsVersion,
|
||||
shouldDisplay as shouldDisplayFunc,
|
||||
createShouldDisplayFn as shouldDisplayFunc,
|
||||
betterCompact
|
||||
} from "../util";
|
||||
import { ResourceIndex } from "../resources/interfaces";
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from "../devices/components/source_config_value";
|
||||
import {
|
||||
validFbosConfig, determineInstalledOsVersion,
|
||||
shouldDisplay as shouldDisplayFunc
|
||||
createShouldDisplayFn as shouldDisplayFunc
|
||||
} from "../util";
|
||||
import { ResourceIndex } from "../resources/interfaces";
|
||||
import { TaggedLog } from "farmbot";
|
||||
|
|
|
@ -4,6 +4,8 @@ import { isObject } from "lodash";
|
|||
import { OFCropAttrs, OFCropResponse, OpenFarmAPI } from "./icons";
|
||||
|
||||
export type OFIcon = Readonly<OFCropAttrs>;
|
||||
type IconDictionary = Dictionary<OFIcon | undefined>;
|
||||
|
||||
const STORAGE_KEY = "openfarm_icons_with_spread";
|
||||
|
||||
function initLocalStorage() {
|
||||
|
@ -11,8 +13,6 @@ function initLocalStorage() {
|
|||
return {};
|
||||
}
|
||||
|
||||
type IconDictionary = Dictionary<OFIcon | undefined>;
|
||||
|
||||
function getAllIconsFromCache(): IconDictionary {
|
||||
try {
|
||||
const dictionary = JSON.parse(localStorage.getItem(STORAGE_KEY) || "");
|
||||
|
@ -40,21 +40,6 @@ function localStorageIconSet(icon: OFIcon): void {
|
|||
* efficient */
|
||||
const promiseCache: Dictionary<Promise<Readonly<OFCropAttrs>>> = {};
|
||||
|
||||
function HTTPIconFetch(slug: string) {
|
||||
const url = OpenFarmAPI.OFBaseURL + slug;
|
||||
promiseCache[url] = axios
|
||||
.get<OFCropResponse>(url)
|
||||
.then(cacheTheIcon(slug), cacheTheIcon(slug));
|
||||
return promiseCache[url];
|
||||
}
|
||||
|
||||
/** PROBLEM: You have 100 lettuce plants. You don't want to download an SVG icon
|
||||
* 100 times.
|
||||
* SOLUTION: Cache stuff. */
|
||||
export function cachedCrop(slug: string): Promise<OFIcon> {
|
||||
return localStorageIconFetch(slug) || HTTPIconFetch(slug);
|
||||
}
|
||||
|
||||
const cacheTheIcon = (slug: string) =>
|
||||
(resp: AxiosResponse<OFCropResponse>): OFIcon => {
|
||||
if (resp
|
||||
|
@ -72,3 +57,18 @@ const cacheTheIcon = (slug: string) =>
|
|||
return { slug, spread: undefined, svg_icon: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
function HTTPIconFetch(slug: string) {
|
||||
const url = OpenFarmAPI.OFBaseURL + slug;
|
||||
promiseCache[url] = axios
|
||||
.get<OFCropResponse>(url)
|
||||
.then(cacheTheIcon(slug), cacheTheIcon(slug));
|
||||
return promiseCache[url];
|
||||
}
|
||||
|
||||
/** PROBLEM: You have 100 lettuce plants. You don't want to download an SVG icon
|
||||
* 100 times.
|
||||
* SOLUTION: Cache stuff. */
|
||||
export function cachedCrop(slug: string): Promise<OFIcon> {
|
||||
return localStorageIconFetch(slug) || HTTPIconFetch(slug);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ import moment from "moment";
|
|||
import { ResourceIndex, UUID, VariableNameSet } from "../resources/interfaces";
|
||||
import {
|
||||
randomColor, determineInstalledOsVersion,
|
||||
shouldDisplay as shouldDisplayFunc,
|
||||
createShouldDisplayFn as shouldDisplayFunc,
|
||||
timeFormatString
|
||||
} from "../util";
|
||||
import { resourceUsageList } from "../resources/in_use";
|
||||
|
|
|
@ -55,7 +55,7 @@ export const LocationForm =
|
|||
const variableListItems = displayVariables ? [PARENT(determineVarDDILabel({
|
||||
label: "parent", resources, uuid: sequenceUuid, forceExternal: headerForm
|
||||
}))] : [];
|
||||
const displayGroups = props.shouldDisplay(Feature.loops) && !disallowGroups;
|
||||
const displayGroups = props.shouldDisplay(Feature.groups) && !disallowGroups;
|
||||
const list = locationFormList(resources, variableListItems, displayGroups);
|
||||
/** Variable name. */
|
||||
const { label } = celeryNode.args;
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
import { getStepTag } from "../resources/sequence_tagging";
|
||||
import { enabledAxisMap } from "../devices/components/axis_tracking_status";
|
||||
import {
|
||||
shouldDisplay as shouldDisplayFunc,
|
||||
createShouldDisplayFn as shouldDisplayFunc,
|
||||
determineInstalledOsVersion, validFwConfig
|
||||
} from "../util";
|
||||
import { BooleanSetting } from "../session_keys";
|
||||
|
|
|
@ -2,7 +2,7 @@ import {
|
|||
semverCompare,
|
||||
SemverResult,
|
||||
minFwVersionCheck,
|
||||
shouldDisplay,
|
||||
createShouldDisplayFn,
|
||||
determineInstalledOsVersion,
|
||||
versionOK,
|
||||
} from "../version";
|
||||
|
@ -121,34 +121,34 @@ describe("shouldDisplay()", () => {
|
|||
const fakeMinOsData = { jest_feature: "1.0.0" };
|
||||
|
||||
it("should display", () => {
|
||||
expect(shouldDisplay("1.0.0", fakeMinOsData, undefined)(
|
||||
expect(createShouldDisplayFn("1.0.0", fakeMinOsData, undefined)(
|
||||
Feature.jest_feature)).toBeTruthy();
|
||||
expect(shouldDisplay("10.0.0", fakeMinOsData, undefined)(
|
||||
expect(createShouldDisplayFn("10.0.0", fakeMinOsData, undefined)(
|
||||
Feature.jest_feature)).toBeTruthy();
|
||||
expect(shouldDisplay("10.0.0",
|
||||
expect(createShouldDisplayFn("10.0.0",
|
||||
{ jest_feature: "1.0.0" }, undefined)(
|
||||
Feature.jest_feature)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shouldn't display", () => {
|
||||
expect(shouldDisplay("0.9.0", fakeMinOsData, undefined)(
|
||||
expect(createShouldDisplayFn("0.9.0", fakeMinOsData, undefined)(
|
||||
Feature.jest_feature)).toBeFalsy();
|
||||
expect(shouldDisplay(undefined, fakeMinOsData, undefined)(
|
||||
expect(createShouldDisplayFn(undefined, fakeMinOsData, undefined)(
|
||||
Feature.jest_feature)).toBeFalsy();
|
||||
// tslint:disable-next-line:no-any
|
||||
const unknown_feature = "unknown_feature" as any;
|
||||
expect(shouldDisplay("1.0.0", fakeMinOsData, undefined)(
|
||||
expect(createShouldDisplayFn("1.0.0", fakeMinOsData, undefined)(
|
||||
unknown_feature)).toBeFalsy();
|
||||
expect(shouldDisplay("1.0.0", undefined, undefined)(
|
||||
expect(createShouldDisplayFn("1.0.0", undefined, undefined)(
|
||||
unknown_feature)).toBeFalsy();
|
||||
// tslint:disable-next-line:no-any
|
||||
expect(shouldDisplay("1.0.0", "" as any, undefined)(
|
||||
expect(createShouldDisplayFn("1.0.0", "" as any, undefined)(
|
||||
unknown_feature)).toBeFalsy();
|
||||
// tslint:disable-next-line:no-any
|
||||
expect(shouldDisplay("1.0.0", "{}" as any, undefined)(
|
||||
expect(createShouldDisplayFn("1.0.0", "{}" as any, undefined)(
|
||||
unknown_feature)).toBeFalsy();
|
||||
// tslint:disable-next-line:no-any
|
||||
expect(shouldDisplay("1.0.0", "bad" as any, undefined)(
|
||||
expect(createShouldDisplayFn("1.0.0", "bad" as any, undefined)(
|
||||
unknown_feature)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -109,14 +109,15 @@ export enum MinVersionOverride {
|
|||
* @param current installed OS version string to compare against data ("0.0.0")
|
||||
* @param lookupData min req versions data, for example {"feature": "1.0.0"}
|
||||
*/
|
||||
export function shouldDisplay(
|
||||
export function createShouldDisplayFn(
|
||||
current: string | undefined,
|
||||
lookupData: MinOsFeatureLookup | undefined,
|
||||
override: string | undefined) {
|
||||
return function (feature: Feature): boolean {
|
||||
const target = override || current;
|
||||
if (isString(target)) {
|
||||
const min = (lookupData || {})[feature] || MinVersionOverride.NEVER;
|
||||
const table = lookupData || {};
|
||||
const min = table[feature] || MinVersionOverride.NEVER;
|
||||
switch (semverCompare(target, min)) {
|
||||
case SemverResult.LEFT_IS_GREATER:
|
||||
case SemverResult.EQUAL:
|
||||
|
@ -143,8 +144,6 @@ export function determineInstalledOsVersion(
|
|||
return fromBotState === "" ? undefined : fromBotState;
|
||||
case SemverResult.RIGHT_IS_GREATER:
|
||||
return fromAPI === "" ? undefined : fromAPI;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue