Merge pull request #1644 from gabrielburnworth/staging

Tools panel updates
pull/1645/head
Rick Carlino 2019-12-30 11:52:43 -06:00 committed by GitHub
commit 3677fb8a37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1488 additions and 186 deletions

View File

@ -8,6 +8,7 @@ export const fakeDesignerState = (): DesignerState => ({
},
hoveredPoint: undefined,
hoveredPlantListItem: undefined,
hoveredToolSlot: undefined,
cropSearchQuery: "",
cropSearchResults: [],
cropSearchInProgress: false,

View File

@ -785,6 +785,10 @@ export namespace Content {
export const NO_TOOLS =
trim(`Press "+" to add a new tool.`);
export const MOUNTED_TOOL =
trim(`The tool currently mounted to the UTM can be set here or by using
a MARK AS step in a sequence.`);
// Farm Events
export const NOTHING_SCHEDULED =
trim(`Press "+" to schedule an event.`);
@ -979,6 +983,7 @@ export enum Actions {
TOGGLE_HOVERED_PLANT = "TOGGLE_HOVERED_PLANT",
TOGGLE_HOVERED_POINT = "TOGGLE_HOVERED_POINT",
HOVER_PLANT_LIST_ITEM = "HOVER_PLANT_LIST_ITEM",
HOVER_TOOL_SLOT = "HOVER_TOOL_SLOT",
OF_SEARCH_RESULTS_START = "OF_SEARCH_RESULTS_START",
OF_SEARCH_RESULTS_OK = "OF_SEARCH_RESULTS_OK",
OF_SEARCH_RESULTS_NO = "OF_SEARCH_RESULTS_NO",

View File

@ -509,9 +509,125 @@
}
}
.tools-panel-content {
.tool-slots-panel,
.tools-panel {
.panel-top {
display: flex;
margin-top: 5rem;
}
.tool-slots-panel-content,
.tools-panel-content {
.tool-search-item,
.tool-slot-search-item {
cursor: pointer;
margin-left: -15px;
margin-right: -15px;
.row {
margin-left: 0;
margin-right: 0;
}
p {
line-height: 3rem;
}
}
.mounted-tool-header {
display: flex;
margin-top: 1rem;
label {
margin: 0;
}
.help-icon {
margin-left: 1rem;
vertical-align: top;
font-size: 1.4rem;
}
}
.tool-slots-header {
display: flex;
margin-top: 1rem;
margin-bottom: 1rem;
label {
margin: 0;
line-height: 2.1rem;
}
a {
margin-left: auto;
}
.fa-plus {
font-size: 1.5rem;
}
}
button:not(.bp3-button) {
display: block;
margin-left: auto;
float: none;
margin-top: 1rem;
}
.tool-verification-status {
display: flex;
margin-top: 1rem;
margin-bottom: 2rem;
button {
margin-top: 0;
}
}
}
}
.add-tool-panel-content,
.edit-tool-panel-content {
button {
display: block;
margin-left: auto;
float: none;
margin-top: 1rem;
&.red {
float: left;
}
}
.add-stock-tools {
ul {
font-size: 1.2rem;
padding-left: 1rem;
}
button {
.fa-plus {
margin-right: 0.5rem;
}
}
}
}
.add-tool-slot-panel-content,
.edit-tool-slot-panel-content {
label {
margin-top: 0 !important;
}
.row, fieldset {
margin-top: 2rem;
}
fieldset button {
margin: 0;
}
.direction-icon {
margin-left: 1rem;
}
.use-current-location-input {
button {
margin: 0;
float: none;
margin-left: 1rem;
vertical-align: middle;
}
}
.gantry-mounted-input {
label {
margin-top: 0;
}
input[type="checkbox"] {
float: left;
margin-right: 1rem;
}
}
}

View File

@ -160,17 +160,17 @@
margin-left: 10px;
}
.step-button-cluster,
.sequence-list,
.regimen-list {
overflow-y: auto;
overflow-x: hidden;
max-height: calc(100vh - 21rem);
max-height: calc(100vh - 19rem);
}
.step-button-cluster,
.sequence-list {
.step-button-cluster {
margin-right: -15px;
overflow-y: auto;
overflow-x: hidden;
max-height: calc(100vh - 16rem);
}
.farmware-info-panel,
@ -236,7 +236,7 @@
margin-left: -15px;
}
.non-empty-state {
height: calc(100vh - 15rem);
height: calc(100vh - 19rem);
overflow-y: auto;
overflow-x: hidden;
}
@ -453,7 +453,6 @@
.sequence-list-panel,
.regimen-list-panel {
padding-top: 0.4rem;
margin-bottom: 3rem;
margin-right: 5px;
@media screen and (max-width: 1075px) {
margin-left: 15px;
@ -484,7 +483,15 @@
}
.farmware-list-panel {
margin-bottom: 5rem;
.farmware-list-panel-contents {
height: calc(100vh - 15rem);
overflow-y: auto;
overflow-x: hidden;
margin-right: -20px;
margin-left: -15px;
padding-left: 1rem;
padding-right: 1rem;
}
label {
font-weight: bold;
font-size: 1.4rem;

View File

@ -63,6 +63,15 @@ describe("designer reducer", () => {
expect(newState.hoveredPoint).toEqual("uuid");
});
it("sets hovered tool slot", () => {
const action: ReduxAction<string> = {
type: Actions.HOVER_TOOL_SLOT,
payload: "toolSlotUuid"
};
const newState = designer(oldState(), action);
expect(newState.hoveredToolSlot).toEqual("toolSlotUuid");
});
it("sets chosen location", () => {
const action: ReduxAction<BotPosition> = {
type: Actions.CHOOSE_LOCATION,

View File

@ -103,6 +103,7 @@ export interface DesignerState {
hoveredPlant: HoveredPlantPayl;
hoveredPoint: string | undefined;
hoveredPlantListItem: string | undefined;
hoveredToolSlot: string | undefined;
cropSearchQuery: string;
cropSearchResults: CropLiveSearchResult[];
cropSearchInProgress: boolean;

View File

@ -340,6 +340,8 @@ export class GardenMap extends
ToolSlotLayer = () => <ToolSlotLayer
mapTransformProps={this.mapTransformProps}
visible={!!this.props.showFarmbot}
dispatch={this.props.dispatch}
hoveredToolSlot={this.props.designer.hoveredToolSlot}
botPositionX={this.props.botLocationData.position.x}
slots={this.props.toolSlots} />
FarmBotLayer = () => <FarmBotLayer

View File

@ -5,6 +5,7 @@ import {
import { BotOriginQuadrant } from "../../../../interfaces";
import { Color } from "../../../../../ui";
import { svgMount } from "../../../../../__test_support__/svg_mount";
import { Actions } from "../../../../../constants";
describe("<ToolbaySlot />", () => {
const fakeProps = (): ToolSlotGraphicProps => ({
@ -61,7 +62,8 @@ describe("<Tool/>", () => {
x: 10,
y: 20,
hovered: false,
setHoverState: jest.fn(),
dispatch: jest.fn(),
uuid: "fakeUuid",
xySwap: false,
});
@ -75,9 +77,13 @@ describe("<Tool/>", () => {
p.tool = toolName;
const wrapper = svgMount(<Tool {...p} />);
wrapper.find("g").simulate("mouseOver");
expect(p.toolProps.setHoverState).toHaveBeenCalledWith(true);
expect(p.toolProps.dispatch).toHaveBeenCalledWith({
type: Actions.HOVER_TOOL_SLOT, payload: "fakeUuid"
});
wrapper.find("g").simulate("mouseLeave");
expect(p.toolProps.setHoverState).toHaveBeenCalledWith(false);
expect(p.toolProps.dispatch).toHaveBeenCalledWith({
type: Actions.HOVER_TOOL_SLOT, payload: undefined
});
};
it("renders standard tool styling", () => {

View File

@ -14,6 +14,7 @@ import { shallow } from "enzyme";
import { history } from "../../../../../history";
import { ToolSlotPointer } from "farmbot/dist/resources/api_resources";
import { TaggedToolSlotPointer } from "farmbot";
import { ToolSlotPoint } from "../tool_slot_point";
describe("<ToolSlotLayer/>", () => {
function fakeProps(): ToolSlotLayerProps {
@ -35,18 +36,20 @@ describe("<ToolSlotLayer/>", () => {
slots: [{ toolSlot, tool: undefined }],
botPositionX: undefined,
mapTransformProps: fakeMapTransformProps(),
dispatch: jest.fn(),
hoveredToolSlot: undefined,
};
}
it("toggles visibility off", () => {
const result = shallow(<ToolSlotLayer {...fakeProps()} />);
expect(result.find("ToolSlotPoint").length).toEqual(0);
expect(result.find(ToolSlotPoint).length).toEqual(0);
});
it("toggles visibility on", () => {
const p = fakeProps();
p.visible = true;
const result = shallow(<ToolSlotLayer {...p} />);
expect(result.find("ToolSlotPoint").length).toEqual(1);
expect(result.find(ToolSlotPoint).length).toEqual(1);
});
it("navigates to tools page", async () => {

View File

@ -1,3 +1,10 @@
let mockDev = false;
jest.mock("../../../../../account/dev/dev_support", () => ({
DevSettings: { futureFeaturesEnabled: () => mockDev, }
}));
jest.mock("../../../../../history", () => ({ history: { push: jest.fn() } }));
import * as React from "react";
import { ToolSlotPoint, TSPProps } from "../tool_slot_point";
import {
@ -7,12 +14,19 @@ import {
fakeMapTransformProps
} from "../../../../../__test_support__/map_transform_props";
import { svgMount } from "../../../../../__test_support__/svg_mount";
import { history } from "../../../../../history";
describe("<ToolSlotPoint/>", () => {
beforeEach(() => {
mockDev = false;
});
const fakeProps = (): TSPProps => ({
mapTransformProps: fakeMapTransformProps(),
botPositionX: undefined,
slot: { toolSlot: fakeToolSlot(), tool: fakeTool() }
slot: { toolSlot: fakeToolSlot(), tool: fakeTool() },
dispatch: jest.fn(),
hoveredToolSlot: undefined,
});
const testToolSlotGraphics = (tool: 0 | 1, slot: 0 | 1) => {
@ -31,11 +45,23 @@ describe("<ToolSlotPoint/>", () => {
testToolSlotGraphics(1, 0);
testToolSlotGraphics(1, 1);
it("opens tool info", () => {
const p = fakeProps();
p.slot.toolSlot.body.id = 1;
const wrapper = svgMount(<ToolSlotPoint {...p} />);
mockDev = false;
wrapper.find("g").first().simulate("click");
expect(history.push).not.toHaveBeenCalled();
mockDev = true;
wrapper.find("g").first().simulate("click");
expect(history.push).toHaveBeenCalledWith("/app/designer/tool-slots/1");
});
it("displays tool name", () => {
const p = fakeProps();
p.slot.toolSlot.body.pullout_direction = 2;
p.hoveredToolSlot = p.slot.toolSlot.uuid;
const wrapper = svgMount(<ToolSlotPoint {...p} />);
wrapper.find(ToolSlotPoint).setState({ hovered: true });
expect(wrapper.find("text").props().visibility).toEqual("visible");
expect(wrapper.find("text").text()).toEqual("Foo");
expect(wrapper.find("text").props().dx).toEqual(-40);
@ -44,8 +70,8 @@ describe("<ToolSlotPoint/>", () => {
it("displays 'no tool'", () => {
const p = fakeProps();
p.slot.tool = undefined;
p.hoveredToolSlot = p.slot.toolSlot.uuid;
const wrapper = svgMount(<ToolSlotPoint {...p} />);
wrapper.find(ToolSlotPoint).setState({ hovered: true });
expect(wrapper.find("text").text()).toEqual("no tool");
expect(wrapper.find("text").props().dx).toEqual(40);
});
@ -74,13 +100,21 @@ describe("<ToolSlotPoint/>", () => {
p.slot.toolSlot.body.gantry_mounted = true;
if (p.slot.tool) { p.slot.tool.body.name = "seed trough"; }
const wrapper = svgMount(<ToolSlotPoint {...p} />);
expect(wrapper.find("#seed-trough").length).toEqual(1);
expect(wrapper.find("#seed-trough").find("rect").props().width)
.toEqual(45);
expect(wrapper.find("#gantry-toolbay-slot").find("rect").props().width)
.toEqual(49);
});
it("sets hover", () => {
const wrapper = svgMount(<ToolSlotPoint {...fakeProps()} />);
expect(wrapper.find(ToolSlotPoint).state().hovered).toBeFalsy();
(wrapper.find(ToolSlotPoint).instance() as ToolSlotPoint).setHover(true);
expect(wrapper.find(ToolSlotPoint).state().hovered).toBeTruthy();
it("renders rotated trough", () => {
const p = fakeProps();
p.mapTransformProps.xySwap = true;
p.slot.toolSlot.body.gantry_mounted = true;
if (p.slot.tool) { p.slot.tool.body.name = "seed trough"; }
const wrapper = svgMount(<ToolSlotPoint {...p} />);
expect(wrapper.find("#seed-trough").find("rect").props().width)
.toEqual(20);
expect(wrapper.find("#gantry-toolbay-slot").find("rect").props().width)
.toEqual(24);
});
});

View File

@ -3,13 +3,16 @@ import { Color } from "../../../../ui/index";
import { trim } from "../../../../util";
import { BotOriginQuadrant } from "../../../interfaces";
import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources";
import { Actions } from "../../../../constants";
import { UUID } from "../../../../resources/interfaces";
export interface ToolGraphicProps {
x: number;
y: number;
hovered: boolean;
setHoverState(hoverState: boolean): void;
dispatch: Function;
xySwap: boolean;
uuid: UUID | undefined;
}
export interface ToolProps {
@ -95,11 +98,14 @@ export const Tool = (props: ToolProps) => {
}
};
export const setToolHover = (payload: string | undefined) =>
({ type: Actions.HOVER_TOOL_SLOT, payload });
const StandardTool = (props: ToolGraphicProps) => {
const { x, y, hovered, setHoverState } = props;
const { x, y, hovered, dispatch, uuid } = props;
return <g id={"tool"}
onMouseOver={() => setHoverState(true)}
onMouseLeave={() => setHoverState(false)}>
onMouseOver={() => dispatch(setToolHover(uuid))}
onMouseLeave={() => dispatch(setToolHover(undefined))}>
<circle
cx={x}
cy={y}
@ -116,10 +122,10 @@ const seedBinGradient =
</radialGradient>;
const SeedBin = (props: ToolGraphicProps) => {
const { x, y, hovered, setHoverState } = props;
const { x, y, hovered, dispatch, uuid } = props;
return <g id={"seed-bin"}
onMouseOver={() => setHoverState(true)}
onMouseLeave={() => setHoverState(false)}>
onMouseOver={() => dispatch(setToolHover(uuid))}
onMouseLeave={() => dispatch(setToolHover(undefined))}>
<defs>
{seedBinGradient}
@ -140,10 +146,10 @@ const SeedBin = (props: ToolGraphicProps) => {
};
const SeedTray = (props: ToolGraphicProps) => {
const { x, y, hovered, setHoverState } = props;
const { x, y, hovered, dispatch, uuid } = props;
return <g id={"seed-tray"}
onMouseOver={() => setHoverState(true)}
onMouseLeave={() => setHoverState(false)}>
onMouseOver={() => dispatch(setToolHover(uuid))}
onMouseLeave={() => dispatch(setToolHover(undefined))}>
<defs>
{seedBinGradient}
@ -188,12 +194,12 @@ export const GantryToolSlot = (props: GantryToolSlotGraphicProps) => {
};
const SeedTrough = (props: ToolGraphicProps) => {
const { x, y, hovered, setHoverState, xySwap } = props;
const { x, y, hovered, dispatch, uuid, xySwap } = props;
const slotLengthX = xySwap ? 20 : 45;
const slotLengthY = xySwap ? 45 : 20;
return <g id={"seed-trough"}
onMouseOver={() => setHoverState(true)}
onMouseLeave={() => setHoverState(false)}>
onMouseOver={() => dispatch(setToolHover(uuid))}
onMouseLeave={() => dispatch(setToolHover(undefined))}>
<rect
x={x - slotLengthX / 2} y={y - slotLengthY / 2}
width={slotLengthX} height={slotLengthY}

View File

@ -1,21 +1,25 @@
import * as React from "react";
import { SlotWithTool } from "../../../../resources/interfaces";
import { SlotWithTool, UUID } from "../../../../resources/interfaces";
import { ToolSlotPoint } from "./tool_slot_point";
import { MapTransformProps } from "../../interfaces";
import { history, getPathArray } from "../../../../history";
import { maybeNoPointer } from "../../util";
import { DevSettings } from "../../../../account/dev/dev_support";
export interface ToolSlotLayerProps {
visible: boolean;
slots: SlotWithTool[];
botPositionX: number | undefined;
mapTransformProps: MapTransformProps;
dispatch: Function;
hoveredToolSlot: UUID | undefined;
}
export function ToolSlotLayer(props: ToolSlotLayerProps) {
const pathArray = getPathArray();
const canClickTool = !(pathArray[3] === "plants" && pathArray.length > 4);
const goToToolsPage = () => canClickTool && history.push("/app/tools");
const goToToolsPage = () => canClickTool &&
!DevSettings.futureFeaturesEnabled() && history.push("/app/tools");
const { slots, visible, mapTransformProps } = props;
const cursor = canClickTool ? "pointer" : "default";
@ -28,6 +32,8 @@ export function ToolSlotLayer(props: ToolSlotLayerProps) {
<ToolSlotPoint
key={slot.toolSlot.uuid}
slot={slot}
hoveredToolSlot={props.hoveredToolSlot}
dispatch={props.dispatch}
botPositionX={props.botPositionX}
mapTransformProps={mapTransformProps} />)}
</g>;

View File

@ -1,78 +1,73 @@
import * as React from "react";
import { SlotWithTool } from "../../../../resources/interfaces";
import { SlotWithTool, UUID } from "../../../../resources/interfaces";
import { transformXY } from "../../util";
import { MapTransformProps } from "../../interfaces";
import { ToolbaySlot, ToolNames, Tool, GantryToolSlot } from "./tool_graphics";
import { ToolLabel } from "./tool_label";
import { includes } from "lodash";
import { DevSettings } from "../../../../account/dev/dev_support";
import { history } from "../../../../history";
export interface TSPProps {
slot: SlotWithTool;
botPositionX: number | undefined;
mapTransformProps: MapTransformProps;
dispatch: Function;
hoveredToolSlot: UUID | undefined;
}
interface TSPState {
hovered: boolean;
}
const reduceToolName = (raw: string | undefined) => {
const lower = (raw || "").toLowerCase();
if (includes(lower, "seed bin")) { return ToolNames.seedBin; }
if (includes(lower, "seed tray")) { return ToolNames.seedTray; }
if (includes(lower, "seed trough")) { return ToolNames.seedTrough; }
return ToolNames.tool;
};
export class ToolSlotPoint extends
React.Component<TSPProps, Partial<TSPState>> {
state: TSPState = { hovered: false };
setHover = (state: boolean) => this.setState({ hovered: state });
get slot() { return this.props.slot; }
reduceToolName = (raw: string | undefined) => {
const lower = (raw || "").toLowerCase();
if (includes(lower, "seed bin")) { return ToolNames.seedBin; }
if (includes(lower, "seed tray")) { return ToolNames.seedTray; }
if (includes(lower, "seed trough")) { return ToolNames.seedTrough; }
return ToolNames.tool;
}
render() {
const {
id, x, y, pullout_direction, gantry_mounted
} = this.slot.toolSlot.body;
const { mapTransformProps, botPositionX } = this.props;
const { quadrant, xySwap } = mapTransformProps;
const xPosition = gantry_mounted ? (botPositionX || 0) : x;
const { qx, qy } = transformXY(xPosition, y, this.props.mapTransformProps);
const toolName = this.slot.tool ? this.slot.tool.body.name : "no tool";
const toolProps = {
x: qx,
y: qy,
hovered: this.state.hovered,
setHoverState: this.setHover,
xySwap,
};
return <g id={"toolslot-" + id}>
{pullout_direction &&
<ToolbaySlot
id={id}
x={qx}
y={qy}
pulloutDirection={pullout_direction}
quadrant={quadrant}
xySwap={xySwap} />}
{gantry_mounted && <GantryToolSlot x={qx} y={qy} xySwap={xySwap} />}
{(this.slot.tool || (!pullout_direction && !gantry_mounted)) &&
<Tool
tool={this.reduceToolName(toolName)}
toolProps={toolProps} />}
<ToolLabel
toolName={toolName}
hovered={this.state.hovered}
export const ToolSlotPoint = (props: TSPProps) => {
const {
id, x, y, pullout_direction, gantry_mounted
} = props.slot.toolSlot.body;
const { mapTransformProps, botPositionX } = props;
const { quadrant, xySwap } = mapTransformProps;
const xPosition = gantry_mounted ? (botPositionX || 0) : x;
const { qx, qy } = transformXY(xPosition, y, props.mapTransformProps);
const toolName = props.slot.tool ? props.slot.tool.body.name : "no tool";
const hovered = props.slot.toolSlot.uuid === props.hoveredToolSlot;
const toolProps = {
x: qx,
y: qy,
hovered,
dispatch: props.dispatch,
uuid: props.slot.toolSlot.uuid,
xySwap,
};
return <g id={"toolslot-" + id}
onClick={() => DevSettings.futureFeaturesEnabled() &&
history.push(`/app/designer/tool-slots/${id}`)}>
{pullout_direction &&
<ToolbaySlot
id={id}
x={qx}
y={qy}
pulloutDirection={pullout_direction}
quadrant={quadrant}
xySwap={xySwap} />
</g>;
}
}
xySwap={xySwap} />}
{gantry_mounted && <GantryToolSlot x={qx} y={qy} xySwap={xySwap} />}
{(props.slot.tool || (!pullout_direction && !gantry_mounted)) &&
<Tool
tool={reduceToolName(toolName)}
toolProps={toolProps} />}
<ToolLabel
toolName={toolName}
hovered={hovered}
x={qx}
y={qy}
pulloutDirection={pullout_direction}
quadrant={quadrant}
xySwap={xySwap} />
</g>;
};

View File

@ -15,6 +15,7 @@ export const initialState: DesignerState = {
},
hoveredPoint: undefined,
hoveredPlantListItem: undefined,
hoveredToolSlot: undefined,
cropSearchQuery: "",
cropSearchResults: [],
cropSearchInProgress: false,
@ -55,6 +56,10 @@ export const designer = generateReducer<DesignerState>(initialState)
s.hoveredPoint = payload;
return s;
})
.add<string | undefined>(Actions.HOVER_TOOL_SLOT, (s, { payload }) => {
s.hoveredToolSlot = payload;
return s;
})
.add<CurrentPointPayl | undefined>(Actions.SET_CURRENT_POINT_DATA, (s, { payload }) => {
const { color } =
(!payload || !payload.color) ? (s.currentPoint || { color: "green" }) : payload;

View File

@ -0,0 +1,118 @@
jest.mock("../../../api/crud", () => ({
init: jest.fn(() => ({ type: "", payload: { uuid: "fakeUuid" } })),
save: jest.fn(),
edit: jest.fn(),
destroy: jest.fn(),
}));
jest.mock("../../../history", () => ({ history: { push: jest.fn() } }));
import * as React from "react";
import { mount, shallow } from "enzyme";
import {
RawAddToolSlot as AddToolSlot, AddToolSlotProps, mapStateToProps
} from "../add_tool_slot";
import { fakeState } from "../../../__test_support__/fake_state";
import {
fakeTool, fakeToolSlot
} from "../../../__test_support__/fake_state/resources";
import {
buildResourceIndex
} from "../../../__test_support__/resource_index_builder";
import { init, save, edit, destroy } from "../../../api/crud";
import { history } from "../../../history";
import { SpecialStatus } from "farmbot";
describe("<AddToolSlot />", () => {
const fakeProps = (): AddToolSlotProps => ({
tools: [],
findTool: jest.fn(),
botPosition: { x: undefined, y: undefined, z: undefined },
dispatch: jest.fn(),
findToolSlot: fakeToolSlot,
});
it("renders", () => {
const wrapper = mount(<AddToolSlot {...fakeProps()} />);
["add new tool slot", "x (mm)", "y (mm)", "z (mm)", "toolnone",
"change slot direction", "use current location", "gantry-mounted"
].map(string => expect(wrapper.text().toLowerCase()).toContain(string));
expect(init).toHaveBeenCalled();
});
it("renders while loading", () => {
const p = fakeProps();
p.findToolSlot = () => undefined;
const wrapper = mount(<AddToolSlot {...p} />);
expect(wrapper.text()).toContain("initializing");
});
it("updates tool slot", () => {
const toolSlot = fakeToolSlot();
const p = fakeProps();
const wrapper = mount<AddToolSlot>(<AddToolSlot {...p} />);
wrapper.instance().updateSlot(toolSlot)({ x: 123 });
expect(edit).toHaveBeenCalledWith(toolSlot, { x: 123 });
});
it("saves tool slot", () => {
const wrapper = shallow<AddToolSlot>(<AddToolSlot {...fakeProps()} />);
wrapper.find("SaveBtn").simulate("click");
expect(save).toHaveBeenCalled();
expect(history.push).toHaveBeenCalledWith("/app/designer/tools");
});
it("saves on unmount", () => {
const toolSlot = fakeToolSlot();
toolSlot.specialStatus = SpecialStatus.DIRTY;
const p = fakeProps();
p.findToolSlot = () => toolSlot;
const wrapper = mount<AddToolSlot>(<AddToolSlot {...p} />);
window.confirm = () => true;
wrapper.unmount();
expect(save).toHaveBeenCalledWith("fakeUuid");
});
it("destroys on unmount", () => {
const toolSlot = fakeToolSlot();
toolSlot.specialStatus = SpecialStatus.DIRTY;
const p = fakeProps();
p.findToolSlot = () => toolSlot;
const wrapper = mount<AddToolSlot>(<AddToolSlot {...p} />);
window.confirm = () => false;
wrapper.unmount();
expect(destroy).toHaveBeenCalledWith("fakeUuid", true);
});
it("doesn't confirm save", () => {
const toolSlot = fakeToolSlot();
toolSlot.specialStatus = SpecialStatus.SAVED;
const p = fakeProps();
p.findToolSlot = () => toolSlot;
const wrapper = mount<AddToolSlot>(<AddToolSlot {...p} />);
window.confirm = jest.fn();
wrapper.unmount();
expect(destroy).not.toHaveBeenCalled();
expect(save).not.toHaveBeenCalled();
});
it("can't find tool without tool slot", () => {
const p = fakeProps();
p.findToolSlot = () => undefined;
const wrapper = mount<AddToolSlot>(<AddToolSlot {...p} />);
expect(wrapper.instance().tool).toEqual(undefined);
});
});
describe("mapStateToProps()", () => {
it("returns props", () => {
const tool = fakeTool();
tool.body.id = 1;
const toolSlot = fakeToolSlot();
const state = fakeState();
state.resources = buildResourceIndex([tool, toolSlot]);
const props = mapStateToProps(state);
expect(props.findTool(1)).toEqual(tool);
expect(props.findToolSlot(toolSlot.uuid)).toEqual(toolSlot);
});
});

View File

@ -1,5 +1,7 @@
jest.mock("../../../api/crud", () => ({ initSave: jest.fn() }));
jest.mock("../../../history", () => ({ history: { push: jest.fn() } }));
import * as React from "react";
import { mount, shallow } from "enzyme";
import {
@ -8,6 +10,7 @@ import {
import { fakeState } from "../../../__test_support__/fake_state";
import { SaveBtn } from "../../../ui";
import { initSave } from "../../../api/crud";
import { history } from "../../../history";
describe("<AddTool />", () => {
const fakeProps = (): AddToolProps => ({
@ -33,6 +36,13 @@ describe("<AddTool />", () => {
wrapper.find(SaveBtn).simulate("click");
expect(initSave).toHaveBeenCalledWith("Tool", { name: "Foo" });
});
it("adds stock tools", () => {
const wrapper = mount(<AddTool {...fakeProps()} />);
wrapper.find("button").last().simulate("click");
expect(initSave).toHaveBeenCalledTimes(6);
expect(history.push).toHaveBeenCalledWith("/app/designer/tools");
});
});
describe("mapStateToProps()", () => {

View File

@ -0,0 +1,96 @@
jest.mock("../../../api/crud", () => ({
edit: jest.fn(),
save: jest.fn(),
destroy: jest.fn(),
}));
const mockDevice = { moveAbsolute: jest.fn(() => Promise.resolve()) };
jest.mock("../../../device", () => ({ getDevice: () => mockDevice }));
import * as React from "react";
import { mount, shallow } from "enzyme";
import {
RawEditToolSlot as EditToolSlot, EditToolSlotProps, mapStateToProps
} from "../edit_tool_slot";
import { fakeState } from "../../../__test_support__/fake_state";
import {
fakeToolSlot, fakeTool
} from "../../../__test_support__/fake_state/resources";
import {
buildResourceIndex
} from "../../../__test_support__/resource_index_builder";
import { destroy, edit, save } from "../../../api/crud";
describe("<EditToolSlot />", () => {
const fakeProps = (): EditToolSlotProps => ({
findToolSlot: jest.fn(),
tools: [],
findTool: jest.fn(),
botPosition: { x: undefined, y: undefined, z: undefined },
dispatch: jest.fn(),
});
it("redirects", () => {
const wrapper = mount(<EditToolSlot {...fakeProps()} />);
expect(wrapper.text().toLowerCase()).toContain("redirecting");
});
it("renders", () => {
const p = fakeProps();
p.findToolSlot = () => fakeToolSlot();
const wrapper = mount(<EditToolSlot {...p} />);
["edit tool slot", "x (mm)", "y (mm)", "z (mm)", "toolnone",
"change slot direction", "use current location", "gantry-mounted"
].map(string => expect(wrapper.text().toLowerCase()).toContain(string));
});
it("updates tool slot", () => {
const slot = fakeToolSlot();
const wrapper = mount<EditToolSlot>(<EditToolSlot {...fakeProps()} />);
wrapper.instance().updateSlot(slot)({ x: 123 });
expect(edit).toHaveBeenCalledWith(slot, { x: 123 });
expect(save).toHaveBeenCalledWith(slot.uuid);
});
it("moves to tool slot", () => {
const p = fakeProps();
const toolSlot = fakeToolSlot();
toolSlot.body.x = 1;
toolSlot.body.y = 2;
toolSlot.body.z = 3;
p.findToolSlot = () => toolSlot;
const wrapper = shallow(<EditToolSlot {...p} />);
wrapper.find(".gray").last().simulate("click");
expect(mockDevice.moveAbsolute).toHaveBeenCalledWith({ x: 1, y: 2, z: 3 });
});
it("removes tool slot", () => {
const p = fakeProps();
const toolSlot = fakeToolSlot();
p.findToolSlot = () => toolSlot;
const wrapper = shallow(<EditToolSlot {...p} />);
wrapper.find("button").last().simulate("click");
expect(destroy).toHaveBeenCalledWith(toolSlot.uuid);
});
});
describe("mapStateToProps()", () => {
it("returns props", () => {
const tool = fakeTool();
tool.body.id = 1;
const toolSlot = fakeToolSlot();
toolSlot.body.id = 1;
const state = fakeState();
state.resources = buildResourceIndex([tool, toolSlot]);
const props = mapStateToProps(state);
expect(props.findTool(1)).toEqual(tool);
expect(props.findToolSlot("1")).toEqual(toolSlot);
});
it("doesn't find tool slot", () => {
const state = fakeState();
state.resources = buildResourceIndex([]);
const props = mapStateToProps(state);
expect(props.findToolSlot("1")).toEqual(undefined);
});
});

View File

@ -1,8 +1,13 @@
jest.mock("../../../api/crud", () => ({ edit: jest.fn() }));
jest.mock("../../../api/crud", () => ({
edit: jest.fn(),
save: jest.fn(),
destroy: jest.fn(),
}));
let mockPath = "/app/designer/tools/1";
jest.mock("../../../history", () => ({
history: { push: jest.fn() },
getPathArray: () => "/app/designer/tools/1".split("/"),
getPathArray: () => mockPath.split("/"),
}));
import * as React from "react";
@ -17,9 +22,13 @@ import {
} from "../../../__test_support__/resource_index_builder";
import { SaveBtn } from "../../../ui";
import { history } from "../../../history";
import { edit } from "../../../api/crud";
import { edit, destroy } from "../../../api/crud";
describe("<EditTool />", () => {
beforeEach(() => {
mockPath = "/app/designer/tools/1";
});
const fakeProps = (): EditToolProps => ({
findTool: jest.fn(() => fakeTool()),
dispatch: jest.fn(),
@ -27,14 +36,26 @@ describe("<EditTool />", () => {
it("renders", () => {
const wrapper = mount(<EditTool {...fakeProps()} />);
expect(wrapper.text()).toContain("Edit Foo");
expect(wrapper.text()).toContain("Edit tool");
});
it("handles missing tool name", () => {
const p = fakeProps();
const tool = fakeTool();
tool.body.name = undefined;
p.findTool = () => tool;
const wrapper = mount<EditTool>(<EditTool {...p} />);
expect(wrapper.state().toolName).toEqual("");
});
it("redirects", () => {
mockPath = "/app/designer/tools/";
const p = fakeProps();
p.findTool = jest.fn(() => undefined);
const wrapper = mount(<EditTool {...p} />);
const wrapper = mount<EditTool>(<EditTool {...p} />);
expect(wrapper.instance().stringyID).toEqual("");
expect(wrapper.text()).toContain("Redirecting...");
expect(history.push).toHaveBeenCalledWith("/app/designer/tools");
});
it("edits tool name", () => {
@ -50,6 +71,15 @@ describe("<EditTool />", () => {
expect(edit).toHaveBeenCalledWith(expect.any(Object), { name: "Foo" });
expect(history.push).toHaveBeenCalledWith("/app/designer/tools");
});
it("removes tool", () => {
const p = fakeProps();
const tool = fakeTool();
p.findTool = () => tool;
const wrapper = shallow(<EditTool {...p} />);
wrapper.find("button").last().simulate("click");
expect(destroy).toHaveBeenCalledWith(tool.uuid);
});
});
describe("mapStateToProps()", () => {

View File

@ -3,23 +3,42 @@ jest.mock("../../../history", () => ({
getPathArray: () => "/app/designer/tools".split("/"),
}));
jest.mock("../../../api/crud", () => ({
edit: jest.fn(),
save: jest.fn(),
}));
const mockDevice = { readPin: jest.fn(() => Promise.resolve()) };
jest.mock("../../../device", () => ({ getDevice: () => mockDevice }));
import * as React from "react";
import { mount, shallow } from "enzyme";
import { RawTools as Tools, ToolsProps, mapStateToProps } from "../index";
import {
fakeTool, fakeToolSlot
fakeTool, fakeToolSlot, fakeSensor
} from "../../../__test_support__/fake_state/resources";
import { history } from "../../../history";
import { fakeState } from "../../../__test_support__/fake_state";
import {
buildResourceIndex
buildResourceIndex, fakeDevice
} from "../../../__test_support__/resource_index_builder";
import { bot } from "../../../__test_support__/fake_state/bot";
import { error } from "../../../toast/toast";
import { Content, Actions } from "../../../constants";
import { edit, save } from "../../../api/crud";
import { ToolSelection } from "../tool_slot_edit_components";
describe("<Tools />", () => {
const fakeProps = (): ToolsProps => ({
tools: [],
toolSlots: [],
dispatch: jest.fn(),
findTool: () => fakeTool(),
device: fakeDevice(),
sensors: [fakeSensor()],
bot,
botToMqttStatus: "down",
hoveredToolSlot: undefined,
});
it("renders with no tools", () => {
@ -29,14 +48,18 @@ describe("<Tools />", () => {
it("renders with tools", () => {
const p = fakeProps();
p.tools = [fakeTool()];
p.tools = [fakeTool(), fakeTool()];
p.tools[0].body.id = 1;
p.tools[0].body.status = "inactive";
p.tools[0].body.name = undefined;
p.tools[1].body.id = 2;
p.tools[1].body.name = "my tool";
p.toolSlots = [fakeToolSlot()];
p.toolSlots[0].body.tool_id = 2;
p.toolSlots[0].body.x = 1;
const wrapper = mount(<Tools {...p} />);
expect(wrapper.text()).toContain("Foo");
expect(wrapper.text()).toContain("(1, 0, 0)");
["foo", "my tool", "unnamed tool", "(1, 0, 0)", "unknown"].map(string =>
expect(wrapper.text().toLowerCase()).toContain(string));
});
it("navigates to tool", () => {
@ -45,14 +68,33 @@ describe("<Tools />", () => {
p.tools[0].body.id = 1;
p.tools[0].body.status = "inactive";
p.toolSlots = [fakeToolSlot()];
p.toolSlots[0].body.tool_id = 2;
p.toolSlots[0].body.id = 2;
p.toolSlots[0].body.tool_id = 3;
const wrapper = mount(<Tools {...p} />);
wrapper.find("p").first().simulate("click");
expect(history.push).toHaveBeenCalledWith("/app/designer/tools/2");
wrapper.find("p").last().simulate("click");
wrapper.find(".tool-slot-search-item").first().simulate("click");
expect(history.push).toHaveBeenCalledWith("/app/designer/tool-slots/2");
wrapper.find(".tool-search-item").first().simulate("click");
expect(history.push).toHaveBeenCalledWith("/app/designer/tools/1");
});
it("hovers tool", () => {
const p = fakeProps();
p.tools = [fakeTool()];
p.tools[0].body.id = 1;
p.toolSlots = [fakeToolSlot()];
p.toolSlots[0].body.id = 1;
p.hoveredToolSlot = p.toolSlots[0].uuid;
const wrapper = mount(<Tools {...p} />);
wrapper.find(".tool-slot-search-item").simulate("mouseEnter");
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.HOVER_TOOL_SLOT, payload: p.toolSlots[0].uuid
});
wrapper.find(".tool-slot-search-item").simulate("mouseLeave");
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.HOVER_TOOL_SLOT, payload: undefined
});
});
it("changes search term", () => {
const p = fakeProps();
p.tools = [fakeTool(), fakeTool()];
@ -73,14 +115,67 @@ describe("<Tools />", () => {
wrapper.setState({ searchTerm: "0" });
expect(wrapper.text()).not.toContain("tool 1");
});
it("changes mounted tool", () => {
const p = fakeProps();
p.tools = [fakeTool()];
const wrapper = mount<Tools>(<Tools {...p} />);
shallow(wrapper.instance().MountedToolInfo()).find(ToolSelection)
.simulate("change", { tool_id: 123 });
expect(edit).toHaveBeenCalledWith(p.device, { mounted_tool_id: 123 });
expect(save).toHaveBeenCalledWith(p.device.uuid);
});
it("displays tool verification result: disconnected", () => {
const p = fakeProps();
p.tools = [fakeTool()];
p.sensors[0].body.label = "tool verification";
p.sensors[0].body.pin = undefined;
p.bot.hardware.pins = { "63": { value: 1, mode: 0 } };
const wrapper = mount(<Tools {...p} />);
expect(wrapper.text()).toContain("disconnected");
});
it("displays tool verification result: connected", () => {
const p = fakeProps();
p.tools = [fakeTool()];
p.sensors[0].body.label = "tool verification";
p.sensors[0].body.pin = 64;
p.bot.hardware.pins = { "64": { value: 0, mode: 0 } };
const wrapper = mount(<Tools {...p} />);
expect(wrapper.text()).toContain("connected");
});
it("verifies tool attachment", () => {
const p = fakeProps();
p.tools = [fakeTool()];
p.bot.hardware.informational_settings.sync_status = "synced";
p.botToMqttStatus = "up";
const wrapper = mount(<Tools {...p} />);
wrapper.find(".yellow").first().simulate("click");
expect(mockDevice.readPin).toHaveBeenCalledWith({
label: "pin63", pin_mode: 0, pin_number: 63
});
});
it("can't verify tool attachment when offline", () => {
const p = fakeProps();
p.tools = [fakeTool()];
p.botToMqttStatus = "down";
const wrapper = mount(<Tools {...p} />);
wrapper.find(".yellow").first().simulate("click");
expect(mockDevice.readPin).not.toHaveBeenCalled();
expect(error).toHaveBeenCalledWith(Content.NOT_AVAILABLE_WHEN_OFFLINE);
});
});
describe("mapStateToProps()", () => {
it("returns props", () => {
const state = fakeState();
const tool = fakeTool();
state.resources = buildResourceIndex([tool]);
tool.body.id = 1;
state.resources = buildResourceIndex([tool, fakeDevice()]);
const props = mapStateToProps(state);
expect(props.tools).toEqual([tool]);
expect(props.findTool(tool.body.id)).toEqual(tool);
});
});

View File

@ -0,0 +1,190 @@
import * as React from "react";
import { shallow, mount } from "enzyme";
import {
GantryMountedInput, GantryMountedInputProps,
UseCurrentLocationInputRow, UseCurrentLocationInputRowProps,
SlotDirectionInputRow, SlotDirectionInputRowProps,
ToolInputRow, ToolInputRowProps,
SlotLocationInputRow, SlotLocationInputRowProps,
ToolSelection, ToolSelectionProps,
} from "../tool_slot_edit_components";
import { fakeTool } from "../../../__test_support__/fake_state/resources";
import { FBSelect } from "../../../ui";
describe("<GantryMountedInput />", () => {
const fakeProps = (): GantryMountedInputProps => ({
gantryMounted: false,
onChange: jest.fn(),
});
it("renders", () => {
const wrapper = shallow(<GantryMountedInput {...fakeProps()} />);
expect(wrapper.text().toLowerCase()).toContain("gantry-mounted");
});
it("changes value", () => {
const p = fakeProps();
const wrapper = shallow(<GantryMountedInput {...p} />);
wrapper.find("input").simulate("change");
expect(p.onChange).toHaveBeenCalledWith({ gantry_mounted: true });
});
});
describe("<UseCurrentLocationInputRow />", () => {
const fakeProps = (): UseCurrentLocationInputRowProps => ({
botPosition: { x: undefined, y: undefined, z: undefined },
onChange: jest.fn(),
});
it("renders", () => {
const wrapper = mount(<UseCurrentLocationInputRow {...fakeProps()} />);
expect(wrapper.text().toLowerCase()).toContain("use current location");
});
it("doesn't change value", () => {
const p = fakeProps();
const wrapper = shallow(<UseCurrentLocationInputRow {...p} />);
wrapper.find("button").simulate("click");
expect(p.onChange).not.toHaveBeenCalled();
});
it("changes value", () => {
const p = fakeProps();
p.botPosition = { x: 0, y: 1, z: 2 };
const wrapper = shallow(<UseCurrentLocationInputRow {...p} />);
wrapper.find("button").simulate("click");
expect(p.onChange).toHaveBeenCalledWith(p.botPosition);
});
});
describe("<SlotDirectionInputRow />", () => {
const fakeProps = (): SlotDirectionInputRowProps => ({
toolPulloutDirection: 0,
onChange: jest.fn(),
});
it("renders", () => {
const wrapper = mount(<SlotDirectionInputRow {...fakeProps()} />);
expect(wrapper.text().toLowerCase()).toContain("change slot direction");
});
it("changes value by click", () => {
const p = fakeProps();
const wrapper = shallow(<SlotDirectionInputRow {...p} />);
wrapper.find("i").first().simulate("click");
expect(p.onChange).toHaveBeenCalledWith({ pullout_direction: 1 });
});
it("changes value by selection", () => {
const p = fakeProps();
const wrapper = shallow(<SlotDirectionInputRow {...p} />);
wrapper.find("FBSelect").simulate("change", { label: "", value: 1 });
expect(p.onChange).toHaveBeenCalledWith({ pullout_direction: 1 });
});
});
describe("<ToolSelection />", () => {
const fakeProps = (): ToolSelectionProps => ({
tools: [],
selectedTool: undefined,
onChange: jest.fn(),
filterSelectedTool: false,
});
it("renders", () => {
const wrapper = mount(<ToolSelection {...fakeProps()} />);
expect(wrapper.text().toLowerCase()).toContain("none");
});
it("handles missing tool data", () => {
const p = fakeProps();
const tool = fakeTool();
tool.body.name = undefined;
tool.body.id = undefined;
p.tools = [tool];
const wrapper = shallow(<ToolSelection {...p} />);
expect(wrapper.find("FBSelect").props().list).toEqual([]);
});
it("handles missing selected tool data", () => {
const p = fakeProps();
const tool = fakeTool();
tool.body.name = undefined;
p.selectedTool = tool;
const wrapper = shallow(<ToolSelection {...p} />);
expect(wrapper.find(FBSelect).props().selectedItem)
.toEqual(expect.objectContaining({ label: "untitled" }));
});
it("shows selected tool", () => {
const p = fakeProps();
p.selectedTool = fakeTool();
const wrapper = mount(<ToolSelection {...p} />);
expect(wrapper.text().toLowerCase()).toContain("foo");
});
it("changes value", () => {
const p = fakeProps();
const wrapper = shallow(<ToolSelection {...p} />);
wrapper.find("FBSelect").simulate("change", { label: "", value: 1 });
expect(p.onChange).toHaveBeenCalledWith({ tool_id: 1 });
});
});
describe("<ToolInputRow />", () => {
const fakeProps = (): ToolInputRowProps => ({
tools: [],
selectedTool: undefined,
onChange: jest.fn(),
});
it("renders", () => {
const wrapper = mount(<ToolInputRow {...fakeProps()} />);
expect(wrapper.text().toLowerCase()).toContain("tool");
});
it("shows selected tool", () => {
const p = fakeProps();
p.selectedTool = fakeTool();
const wrapper = mount(<ToolInputRow {...p} />);
expect(wrapper.text().toLowerCase()).toContain("foo");
});
});
describe("<SlotLocationInputRow />", () => {
const fakeProps = (): SlotLocationInputRowProps => ({
slotLocation: { x: 0, y: 0, z: 0 },
gantryMounted: false,
onChange: jest.fn(),
});
it("renders", () => {
const wrapper = mount(<SlotLocationInputRow {...fakeProps()} />);
expect(wrapper.text().toLowerCase()).toContain("x (mm)y (mm)z (mm)");
expect(wrapper.find("input").first().props().value).toEqual(0);
});
it("renders gantry-mounted slot", () => {
const p = fakeProps();
p.gantryMounted = true;
const wrapper = mount(<SlotLocationInputRow {...p} />);
expect(wrapper.find("input").first().props().value).toEqual("Gantry");
});
it("changes value", () => {
const p = fakeProps();
const wrapper = shallow(<SlotLocationInputRow {...p} />);
wrapper.find("BlurableInput").at(0).simulate("commit", {
currentTarget: { value: 1 }
});
wrapper.find("BlurableInput").at(1).simulate("commit", {
currentTarget: { value: 2 }
});
wrapper.find("BlurableInput").at(2).simulate("commit", {
currentTarget: { value: 3 }
});
expect(p.onChange).toHaveBeenCalledWith({ x: 1 });
expect(p.onChange).toHaveBeenCalledWith({ y: 2 });
expect(p.onChange).toHaveBeenCalledWith({ z: 3 });
});
});

View File

@ -9,6 +9,7 @@ import { SaveBtn } from "../../ui";
import { SpecialStatus } from "farmbot";
import { initSave } from "../../api/crud";
import { Panel } from "../panel_header";
import { history } from "../../history";
export interface AddToolProps {
dispatch: Function;
@ -24,21 +25,60 @@ export const mapStateToProps = (props: Everything): AddToolProps => ({
export class RawAddTool extends React.Component<AddToolProps, AddToolState> {
state: AddToolState = { toolName: "" };
newTool = (name: string) => {
this.props.dispatch(initSave("Tool", { name }));
};
save = () => {
this.newTool(this.state.toolName);
history.push("/app/designer/tools");
}
get stockToolNames() {
return [
t("Seeder"),
t("Watering Nozzle"),
t("Weeder"),
t("Soil Sensor"),
t("Seed Bin"),
t("Seed Tray"),
];
}
AddStockTools = () =>
<div className="add-stock-tools">
<label>{t("Add stock tools")}</label>
<ul>
{this.stockToolNames.map(n => <li key={n}>{n}</li>)}
</ul>
<button
className="fb-button green"
onClick={() => {
this.stockToolNames.map(n => this.newTool(n));
history.push("/app/designer/tools");
}}>
<i className="fa fa-plus" />
{t("Stock Tools")}
</button>
</div>
render() {
return <DesignerPanel panelName={"tool"} panel={Panel.Tools}>
const panelName = "add-tool";
return <DesignerPanel panelName={panelName} panel={Panel.Tools}>
<DesignerPanelHeader
panelName={"tool"}
panelName={panelName}
title={t("Add new tool")}
backTo={"/app/designer/tools"}
panel={Panel.Tools} />
<DesignerPanelContent panelName={"tools"}>
<label>{t("Tool Name")}</label>
<input
onChange={e => this.setState({ toolName: e.currentTarget.value })} />
<SaveBtn
onClick={() =>
this.props.dispatch(initSave("Tool", { name: this.state.toolName }))}
status={SpecialStatus.DIRTY} />
<DesignerPanelContent panelName={panelName}>
<div className="add-new-tool">
<label>{t("Tool Name")}</label>
<input onChange={e =>
this.setState({ toolName: e.currentTarget.value })} />
<SaveBtn onClick={this.save} status={SpecialStatus.DIRTY} />
</div>
<this.AddStockTools />
</DesignerPanelContent>
</DesignerPanel>;
}

View File

@ -0,0 +1,107 @@
import React from "react";
import { connect } from "react-redux";
import {
DesignerPanel, DesignerPanelContent, DesignerPanelHeader
} from "../designer_panel";
import { Everything } from "../../interfaces";
import { t } from "../../i18next_wrapper";
import { SaveBtn } from "../../ui";
import { SpecialStatus, TaggedTool, TaggedToolSlotPointer } from "farmbot";
import { init, save, edit, destroy } from "../../api/crud";
import { Panel } from "../panel_header";
import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources";
import {
selectAllTools, maybeFindToolById, maybeGetToolSlot
} from "../../resources/selectors";
import { BotPosition } from "../../devices/interfaces";
import { validBotLocationData } from "../../util";
import { history } from "../../history";
import { SlotEditRows } from "./tool_slot_edit_components";
import { UUID } from "../../resources/interfaces";
export interface AddToolSlotProps {
tools: TaggedTool[];
dispatch: Function;
botPosition: BotPosition;
findTool(id: number): TaggedTool | undefined;
findToolSlot(uuid: UUID | undefined): TaggedToolSlotPointer | undefined;
}
export interface AddToolSlotState {
uuid: UUID | undefined;
}
export const mapStateToProps = (props: Everything): AddToolSlotProps => ({
tools: selectAllTools(props.resources.index),
dispatch: props.dispatch,
botPosition: validBotLocationData(props.bot.hardware.location_data).position,
findTool: (id: number) => maybeFindToolById(props.resources.index, id),
findToolSlot: (uuid: UUID | undefined) =>
maybeGetToolSlot(props.resources.index, uuid),
});
export class RawAddToolSlot
extends React.Component<AddToolSlotProps, AddToolSlotState> {
state: AddToolSlotState = { uuid: undefined };
componentDidMount() {
const action = init("Point", {
pointer_type: "ToolSlot", name: "Tool Slot", radius: 0, meta: {},
x: 0, y: 0, z: 0, tool_id: undefined,
pullout_direction: ToolPulloutDirection.NONE, gantry_mounted: false
});
this.setState({ uuid: action.payload.uuid });
this.props.dispatch(action);
}
componentWillUnmount() {
if (this.state.uuid && this.toolSlot
&& this.toolSlot.specialStatus == SpecialStatus.DIRTY) {
confirm(t("Save new tool?"))
? this.props.dispatch(save(this.state.uuid))
: this.props.dispatch(destroy(this.state.uuid, true));
}
}
get toolSlot() {
return this.props.findToolSlot(this.state.uuid);
}
get tool() {
return this.toolSlot ?
this.props.findTool(this.toolSlot.body.tool_id || 0) : undefined;
}
updateSlot = (toolSlot: TaggedToolSlotPointer) =>
(update: Partial<TaggedToolSlotPointer["body"]>) =>
this.props.dispatch(edit(toolSlot, update));
save = () => {
this.state.uuid && this.props.dispatch(save(this.state.uuid));
history.push("/app/designer/tools");
}
render() {
const panelName = "add-tool-slot";
return <DesignerPanel panelName={panelName} panel={Panel.Tools}>
<DesignerPanelHeader
panelName={panelName}
title={t("Add new tool slot")}
backTo={"/app/designer/tools"}
panel={Panel.Tools} />
<DesignerPanelContent panelName={panelName}>
{this.toolSlot
? <SlotEditRows
toolSlot={this.toolSlot}
tools={this.props.tools}
tool={this.tool}
botPosition={this.props.botPosition}
updateToolSlot={this.updateSlot(this.toolSlot)} />
: "initializing"}
<SaveBtn onClick={this.save} status={SpecialStatus.DIRTY} />
</DesignerPanelContent>
</DesignerPanel >;
}
}
export const AddToolSlot = connect(mapStateToProps)(RawAddToolSlot);

View File

@ -9,7 +9,7 @@ import { getPathArray } from "../../history";
import { TaggedTool, SpecialStatus } from "farmbot";
import { maybeFindToolById } from "../../resources/selectors";
import { SaveBtn } from "../../ui";
import { edit } from "../../api/crud";
import { edit, destroy } from "../../api/crud";
import { history } from "../../history";
import { Panel } from "../panel_header";
@ -40,26 +40,35 @@ export class RawEditTool extends React.Component<EditToolProps, EditToolState> {
return <span>{t("Redirecting...")}</span>;
}
default = (tool: TaggedTool) =>
<DesignerPanel panelName={"tool"} panel={Panel.Tools}>
default = (tool: TaggedTool) => {
const { dispatch } = this.props;
const { toolName } = this.state;
const panelName = "edit-tool";
return <DesignerPanel panelName={panelName} panel={Panel.Tools}>
<DesignerPanelHeader
panelName={"tool"}
title={`${t("Edit")} ${tool.body.name}`}
panelName={panelName}
title={t("Edit tool")}
backTo={"/app/designer/tools"}
panel={Panel.Tools} />
<DesignerPanelContent panelName={"tools"}>
<DesignerPanelContent panelName={panelName}>
<label>{t("Tool Name")}</label>
<input
value={this.state.toolName}
value={toolName}
onChange={e => this.setState({ toolName: e.currentTarget.value })} />
<SaveBtn
onClick={() => {
this.props.dispatch(edit(tool, { name: this.state.toolName }));
dispatch(edit(tool, { name: toolName }));
history.push("/app/designer/tools");
}}
status={SpecialStatus.DIRTY} />
<button
className="fb-button red no-float"
onClick={() => dispatch(destroy(tool.uuid))}>
{t("Delete")}
</button>
</DesignerPanelContent>
</DesignerPanel>;
}
render() {
return this.tool ? this.default(this.tool) : this.fallback();

View File

@ -0,0 +1,94 @@
import React from "react";
import { connect } from "react-redux";
import {
DesignerPanel, DesignerPanelContent, DesignerPanelHeader
} from "../designer_panel";
import { Everything } from "../../interfaces";
import { t } from "../../i18next_wrapper";
import { getPathArray } from "../../history";
import { TaggedToolSlotPointer, TaggedTool } from "farmbot";
import { edit, save, destroy } from "../../api/crud";
import { history } from "../../history";
import { Panel } from "../panel_header";
import {
maybeFindToolSlotById, selectAllTools, maybeFindToolById
} from "../../resources/selectors";
import { BotPosition } from "../../devices/interfaces";
import { validBotLocationData } from "../../util";
import { SlotEditRows } from "./tool_slot_edit_components";
import { moveAbs } from "../../devices/actions";
export interface EditToolSlotProps {
findToolSlot(id: string): TaggedToolSlotPointer | undefined;
tools: TaggedTool[];
findTool(id: number): TaggedTool | undefined;
dispatch: Function;
botPosition: BotPosition;
}
export const mapStateToProps = (props: Everything): EditToolSlotProps => ({
findToolSlot: (id: string) =>
maybeFindToolSlotById(props.resources.index, parseInt(id)),
tools: selectAllTools(props.resources.index),
findTool: (id: number) => maybeFindToolById(props.resources.index, id),
dispatch: props.dispatch,
botPosition: validBotLocationData(props.bot.hardware.location_data).position,
});
export class RawEditToolSlot extends React.Component<EditToolSlotProps> {
get stringyID() { return getPathArray()[4] || ""; }
get toolSlot() { return this.props.findToolSlot(this.stringyID); }
get tool() {
return this.toolSlot && this.props.findTool(this.toolSlot.body.tool_id || 0);
}
fallback = () => {
history.push("/app/designer/tools");
return <span>{t("Redirecting...")}</span>;
}
updateSlot = (toolSlot: TaggedToolSlotPointer) =>
(update: Partial<TaggedToolSlotPointer["body"]>) => {
this.props.dispatch(edit(toolSlot, update));
this.props.dispatch(save(toolSlot.uuid));
}
default = (toolSlot: TaggedToolSlotPointer) => {
const panelName = "edit-tool-slot";
return <DesignerPanel panelName={panelName} panel={Panel.Tools}>
<DesignerPanelHeader
panelName={panelName}
title={t("Edit tool slot")}
backTo={"/app/designer/tools"}
panel={Panel.Tools} />
<DesignerPanelContent panelName={panelName}>
<SlotEditRows
toolSlot={toolSlot}
tools={this.props.tools}
tool={this.tool}
botPosition={this.props.botPosition}
updateToolSlot={this.updateSlot(toolSlot)} />
<button
className="fb-button gray no-float"
onClick={() => {
const { x, y, z } = toolSlot.body;
moveAbs({ x, y, z });
}}>
{t("Move FarmBot to tool slot location")}
</button>
<button
className="fb-button red no-float"
onClick={() => this.props.dispatch(destroy(toolSlot.uuid))}>
{t("Delete")}
</button>
</DesignerPanelContent>
</DesignerPanel>;
}
render() {
return this.toolSlot ? this.default(this.toolSlot) : this.fallback();
}
}
export const EditToolSlot = connect(mapStateToProps)(RawEditToolSlot);

View File

@ -4,24 +4,44 @@ import {
DesignerPanel, DesignerPanelTop, DesignerPanelContent
} from "../designer_panel";
import { Everything } from "../../interfaces";
import { DesignerNavTabs, Panel } from "../panel_header";
import { DesignerNavTabs, Panel, TAB_COLOR } from "../panel_header";
import {
EmptyStateWrapper, EmptyStateGraphic
} from "../../ui/empty_state_wrapper";
import { t } from "../../i18next_wrapper";
import { TaggedTool, TaggedToolSlotPointer } from "farmbot";
import {
selectAllTools, selectAllToolSlotPointers
TaggedTool, TaggedToolSlotPointer, TaggedDevice, TaggedSensor,
} from "farmbot";
import {
selectAllTools, selectAllToolSlotPointers, getDeviceAccountSettings,
maybeFindToolById,
selectAllSensors
} from "../../resources/selectors";
import { Content } from "../../constants";
import { history } from "../../history";
import { Row, Col } from "../../ui";
import { Row, Col, Help } from "../../ui";
import { botPositionLabel } from "../map/layers/farmbot/bot_position_label";
import { Link } from "../../link";
import { edit, save } from "../../api/crud";
import { readPin } from "../../devices/actions";
import { isBotOnline } from "../../devices/must_be_online";
import { BotState } from "../../devices/interfaces";
import { NetworkState } from "../../connectivity/interfaces";
import { getStatus } from "../../connectivity/reducer_support";
import { setToolHover } from "../map/layers/tool_slots/tool_graphics";
import { ToolSelection } from "./tool_slot_edit_components";
import { error } from "../../toast/toast";
export interface ToolsProps {
tools: TaggedTool[];
toolSlots: TaggedToolSlotPointer[];
dispatch: Function;
findTool(id: number): TaggedTool | undefined;
device: TaggedDevice;
sensors: TaggedSensor[];
bot: BotState;
botToMqttStatus: NetworkState;
hoveredToolSlot: string | undefined;
}
export interface ToolsState {
@ -32,8 +52,22 @@ export const mapStateToProps = (props: Everything): ToolsProps => ({
tools: selectAllTools(props.resources.index),
toolSlots: selectAllToolSlotPointers(props.resources.index),
dispatch: props.dispatch,
findTool: (id: number) => maybeFindToolById(props.resources.index, id),
device: getDeviceAccountSettings(props.resources.index),
sensors: selectAllSensors(props.resources.index),
bot: props.bot,
botToMqttStatus: getStatus(props.bot.connectivity.uptime["bot.mqtt"]),
hoveredToolSlot: props.resources.consumers.farm_designer.hoveredToolSlot,
});
const toolStatus = (value: number | undefined): string => {
switch (value) {
case 1: return t("disconnected");
case 0: return t("connected");
default: return t("unknown");
}
};
export class RawTools extends React.Component<ToolsProps, ToolsState> {
state: ToolsState = { searchTerm: "" };
@ -46,6 +80,99 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
return foundTool ? foundTool.body.name : undefined;
};
get mountedToolId() { return this.props.device.body.mounted_tool_id; }
get mountedTool() { return this.props.findTool(this.mountedToolId || 0); }
get toolVerificationPin() {
const toolVerificationSensor =
this.props.sensors.filter(sensor => sensor.body.label.toLowerCase()
.includes("tool verification"))[0] as TaggedSensor | undefined;
return toolVerificationSensor ? toolVerificationSensor.body.pin || 63 : 63;
}
get pins() { return this.props.bot.hardware.pins; }
get toolVerificationValue() {
const pinData = this.pins[this.toolVerificationPin];
return pinData ? pinData.value : undefined;
}
get arduinoBusy() {
return !!this.props.bot.hardware.informational_settings.busy;
}
get botOnline() {
return isBotOnline(
this.props.bot.hardware.informational_settings.sync_status,
this.props.botToMqttStatus);
}
MountedToolInfo = () =>
<div className="mounted-tool">
<div className="mounted-tool-header">
<label>{t("mounted tool")}</label>
<Help text={Content.MOUNTED_TOOL} />
</div>
<ToolSelection
tools={this.props.tools}
selectedTool={this.mountedTool}
onChange={({ tool_id }) => {
this.props.dispatch(edit(this.props.device,
{ mounted_tool_id: tool_id }));
this.props.dispatch(save(this.props.device.uuid));
}}
filterSelectedTool={true} />
<div className="tool-verification-status">
<p>{t("status")}: {toolStatus(this.toolVerificationValue)}</p>
<button
className={`fb-button yellow ${this.botOnline ? "" : "pseudo-disabled"}`}
disabled={this.arduinoBusy}
title={this.botOnline ? "" : t(Content.NOT_AVAILABLE_WHEN_OFFLINE)}
onClick={() => this.botOnline
? readPin(this.toolVerificationPin,
`pin${this.toolVerificationPin}`, 0)
: error(t(Content.NOT_AVAILABLE_WHEN_OFFLINE))}>
{t("verify")}
</button>
</div>
</div>
ToolSlots = () =>
<div className="tool-slots">
<div className="tool-slots-header">
<label>{t("tool slots")}</label>
<Link to={"/app/designer/tool-slots/add"}>
<div className={`fb-button panel-${TAB_COLOR[Panel.Tools]}`}>
<i className="fa fa-plus" title={t("Add tool slot")} />
</div>
</Link>
</div>
{this.props.toolSlots
.filter(p => (this.getToolName(p.body.tool_id) || "").toLowerCase()
.includes(this.state.searchTerm.toLowerCase()))
.map(toolSlot =>
<ToolSlotInventoryItem key={toolSlot.uuid}
hovered={toolSlot.uuid === this.props.hoveredToolSlot}
dispatch={this.props.dispatch}
toolSlot={toolSlot}
getToolName={this.getToolName} />)}
</div>
InactiveTools = () =>
<div className="inactive-tools">
<label>{t("inactive tools")}</label>
{this.props.tools
.filter(tool => !tool.body.name ||
tool.body.name && tool.body.name.toLowerCase()
.includes(this.state.searchTerm.toLowerCase()))
.filter(tool => tool.body.status === "inactive")
.map(tool =>
<ToolInventoryItem key={tool.uuid}
toolId={tool.body.id}
toolName={tool.body.name || t("Unnamed tool")} />)}
</div>
render() {
const panelName = "tools";
return <DesignerPanel panelName={panelName} panel={Panel.Tools}>
@ -64,26 +191,9 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
title={t("Add a tool")}
text={Content.NO_TOOLS}
colorScheme={"tools"}>
<div>
<label>{t("tool slots")}</label>
{this.props.toolSlots
.filter(p => (this.getToolName(p.body.tool_id) || "").toLowerCase()
.includes(this.state.searchTerm.toLowerCase()))
.map(toolSlot =>
<ToolSlotInventoryItem key={toolSlot.uuid}
toolSlot={toolSlot}
getToolName={this.getToolName} />)}
<br />
<label>{t("inactive tools")}</label>
{this.props.tools
.filter(tool => tool.body.name && tool.body.name.toLowerCase()
.includes(this.state.searchTerm.toLowerCase()))
.filter(tool => tool.body.status === "inactive")
.map(tool =>
<ToolInventoryItem key={tool.uuid}
toolId={tool.body.id}
toolName={tool.body.name || t("Unnammed tool")} />)}
</div>
<this.MountedToolInfo />
<this.ToolSlots />
<this.InactiveTools />
</EmptyStateWrapper>
</DesignerPanelContent>
</DesignerPanel>;
@ -93,20 +203,26 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
interface ToolSlotInventoryItemProps {
toolSlot: TaggedToolSlotPointer;
getToolName(toolId: number | undefined): string | undefined;
hovered: boolean;
dispatch: Function;
}
const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => {
const { x, y, z, tool_id } = props.toolSlot.body;
return <Row>
<Col xs={7}>
<p onClick={() => history.push(`/app/designer/tools/${tool_id}`)}>
{props.getToolName(tool_id) || t("No tool")}
</p>
</Col>
<Col xs={5}>
<p style={{ float: "right" }}>{botPositionLabel({ x, y, z })}</p>
</Col>
</Row>;
const { x, y, z, id, tool_id } = props.toolSlot.body;
return <div
className={`tool-slot-search-item ${props.hovered ? "hovered" : ""}`}
onClick={() => history.push(`/app/designer/tool-slots/${id}`)}
onMouseEnter={() => props.dispatch(setToolHover(props.toolSlot.uuid))}
onMouseLeave={() => props.dispatch(setToolHover(undefined))}>
<Row>
<Col xs={7}>
<p>{props.getToolName(tool_id) || t("No tool")}</p>
</Col>
<Col xs={5}>
<p style={{ float: "right" }}>{botPositionLabel({ x, y, z })}</p>
</Col>
</Row>
</div>;
};
interface ToolInventoryItemProps {
@ -115,12 +231,13 @@ interface ToolInventoryItemProps {
}
const ToolInventoryItem = (props: ToolInventoryItemProps) =>
<Row>
<Col xs={12}>
<p onClick={() => history.push(`/app/designer/tools/${props.toolId}`)}>
{t(props.toolName)}
</p>
</Col>
</Row>;
<div className={"tool-search-item"}
onClick={() => history.push(`/app/designer/tools/${props.toolId}`)}>
<Row>
<Col xs={12}>
<p>{t(props.toolName)}</p>
</Col>
</Row>
</div>;
export const Tools = connect(mapStateToProps)(RawTools);

View File

@ -0,0 +1,168 @@
import React from "react";
import { t } from "../../i18next_wrapper";
import { Xyz, TaggedTool, TaggedToolSlotPointer } from "farmbot";
import { Row, Col, BlurableInput, FBSelect, NULL_CHOICE } from "../../ui";
import {
directionIconClass, positionButtonTitle, newSlotDirection, positionIsDefined
} from "../../tools/components/toolbay_slot_menu";
import {
DIRECTION_CHOICES, DIRECTION_CHOICES_DDI
} from "../../tools/components/toolbay_slot_direction_selection";
import { BotPosition } from "../../devices/interfaces";
import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources";
export interface GantryMountedInputProps {
gantryMounted: boolean;
onChange(update: { gantry_mounted: boolean }): void;
}
export const GantryMountedInput = (props: GantryMountedInputProps) =>
<fieldset className="gantry-mounted-input">
<label>{t("Gantry-mounted")}</label>
<input type="checkbox"
onChange={() => props.onChange({ gantry_mounted: !props.gantryMounted })}
checked={props.gantryMounted} />
</fieldset>;
export interface UseCurrentLocationInputRowProps {
botPosition: BotPosition;
onChange(botPosition: BotPosition): void;
}
export const UseCurrentLocationInputRow =
(props: UseCurrentLocationInputRowProps) =>
<fieldset className="use-current-location-input">
<label>{t("Use current location")}</label>
<button
className="blue fb-button"
title={positionButtonTitle(props.botPosition)}
onClick={() => positionIsDefined(props.botPosition) &&
props.onChange(props.botPosition)}>
<i className="fa fa-crosshairs" />
</button>
<p>{positionButtonTitle(props.botPosition)}</p>
</fieldset>;
export interface SlotDirectionInputRowProps {
toolPulloutDirection: ToolPulloutDirection;
onChange(update: { pullout_direction: ToolPulloutDirection }): void;
}
export const SlotDirectionInputRow = (props: SlotDirectionInputRowProps) =>
<fieldset className="tool-slot-direction-input">
<label>
{t("Change slot direction")}
</label>
<i className={"direction-icon "
+ directionIconClass(props.toolPulloutDirection)}
onClick={() => props.onChange({
pullout_direction: newSlotDirection(props.toolPulloutDirection)
})} />
<FBSelect
key={props.toolPulloutDirection}
list={DIRECTION_CHOICES}
selectedItem={DIRECTION_CHOICES_DDI[props.toolPulloutDirection]}
onChange={ddi => props.onChange({
pullout_direction: parseInt("" + ddi.value)
})} />
</fieldset>;
export interface ToolSelectionProps {
tools: TaggedTool[];
selectedTool: TaggedTool | undefined;
onChange(update: { tool_id: number }): void;
filterSelectedTool: boolean;
}
export const ToolSelection = (props: ToolSelectionProps) =>
<FBSelect
list={props.tools
.filter(tool => (!props.filterSelectedTool || !props.selectedTool)
|| tool.body.id != props.selectedTool.body.id)
.map(tool => ({
label: tool.body.name || "untitled",
value: tool.body.id || 0,
}))
.filter(ddi => ddi.value > 0)}
selectedItem={props.selectedTool
? {
label: props.selectedTool.body.name || "untitled",
value: "" + props.selectedTool.body.id
} : NULL_CHOICE}
allowEmpty={true}
onChange={ddi =>
props.onChange({ tool_id: parseInt("" + ddi.value) })} />;
export interface ToolInputRowProps {
tools: TaggedTool[];
selectedTool: TaggedTool | undefined;
onChange(update: { tool_id: number }): void;
}
export const ToolInputRow = (props: ToolInputRowProps) =>
<div className="tool-slot-tool-input">
<Row>
<Col xs={12}>
<label>{t("Tool")}</label>
<ToolSelection
tools={props.tools}
selectedTool={props.selectedTool}
onChange={props.onChange}
filterSelectedTool={false} />
</Col>
</Row>
</div>;
export interface SlotLocationInputRowProps {
slotLocation: Record<Xyz, number>;
gantryMounted: boolean;
onChange(update: Partial<Record<Xyz, number>>): void;
}
export const SlotLocationInputRow = (props: SlotLocationInputRowProps) =>
<div className="tool-slot-location-input">
<Row>
{["x", "y", "z"].map((axis: Xyz) =>
<Col xs={4} key={axis}>
<label>{t("{{axis}} (mm)", { axis })}</label>
{axis == "x" && props.gantryMounted
? <input disabled value={t("Gantry")} />
: <BlurableInput
type="number"
value={props.slotLocation[axis]}
min={axis == "z" ? undefined : 0}
onCommit={e => props.onChange({
[axis]: parseFloat(e.currentTarget.value)
})} />}
</Col>)}
</Row>
</div>;
export interface SlotEditRowsProps {
toolSlot: TaggedToolSlotPointer;
tools: TaggedTool[];
tool: TaggedTool | undefined;
botPosition: BotPosition;
updateToolSlot(update: Partial<TaggedToolSlotPointer["body"]>): void;
}
export const SlotEditRows = (props: SlotEditRowsProps) =>
<div className="tool-slot-edit-rows">
<SlotLocationInputRow
slotLocation={props.toolSlot.body}
gantryMounted={props.toolSlot.body.gantry_mounted}
onChange={props.updateToolSlot} />
<ToolInputRow
tools={props.tools}
selectedTool={props.tool}
onChange={props.updateToolSlot} />
<SlotDirectionInputRow
toolPulloutDirection={props.toolSlot.body.pullout_direction}
onChange={props.updateToolSlot} />
<UseCurrentLocationInputRow
botPosition={props.botPosition}
onChange={props.updateToolSlot} />
<GantryMountedInput
gantryMounted={props.toolSlot.body.gantry_mounted}
onChange={props.updateToolSlot} />
</div>;

View File

@ -94,7 +94,7 @@ export class FarmwareList
.filter(x => showFirstParty || !firstPartyFarmwareNames.includes(x))
.filter(x => !listed1stPartyNames.includes(x));
return <div>
return <div className="farmware-list-panel-contents">
<div className="farmware-settings-menu">
<Popover position={Position.BOTTOM_RIGHT}>
<i className="fa fa-gear dark" />

View File

@ -178,6 +178,11 @@ export function maybeGetRegimen(index: ResourceIndex,
if (tr && isTaggedRegimen(tr)) { return tr; }
}
export function maybeGetToolSlot(index: ResourceIndex,
uuid: string | undefined): TaggedToolSlotPointer | undefined {
return selectAllToolSlotPointers(index).filter(x => x.uuid === uuid)[0];
}
/** Return the UTC offset of current bot if possible. If not, use UTC (0). */
export function maybeGetTimeOffset(index: ResourceIndex): number {
const dev = maybeGetDevice(index);

View File

@ -50,6 +50,16 @@ export const findFarmEventById = (ri: ResourceIndex, fe_id: number) => {
}
};
export const maybeFindToolSlotById = (ri: ResourceIndex, tool_slot_id?: number):
TaggedToolSlotPointer | undefined => {
const toolSlot = tool_slot_id && byId("Point")(ri, tool_slot_id);
if (toolSlot && isTaggedToolSlotPointer(toolSlot) && sanityCheck(toolSlot)) {
return toolSlot;
} else {
return undefined;
}
};
export const maybeFindToolById = (ri: ResourceIndex, tool_id?: number):
TaggedTool | undefined => {
const tool = tool_id && byId("Tool")(ri, tool_id);

View File

@ -345,6 +345,22 @@ export const UNBOUND_ROUTES = [
getChild: () => import("./farm_designer/tools/edit_tool"),
childKey: "EditTool"
}),
route({
children: true,
$: "/designer/tool-slots/add",
getModule,
key,
getChild: () => import("./farm_designer/tools/add_tool_slot"),
childKey: "AddToolSlot"
}),
route({
children: true,
$: "/designer/tool-slots/:tool_id",
getModule,
key,
getChild: () => import("./farm_designer/tools/edit_tool_slot"),
childKey: "EditToolSlot"
}),
route({
children: true,
$: "/designer/groups",

View File

@ -6,7 +6,7 @@ import { isNumber } from "lodash";
import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources";
import { t } from "../../i18next_wrapper";
const DIRECTION_CHOICES_DDI: { [index: number]: DropDownItem } = {
export const DIRECTION_CHOICES_DDI: { [index: number]: DropDownItem } = {
[ToolPulloutDirection.NONE]:
{ label: t("None"), value: ToolPulloutDirection.NONE },
[ToolPulloutDirection.POSITIVE_X]:
@ -19,7 +19,7 @@ const DIRECTION_CHOICES_DDI: { [index: number]: DropDownItem } = {
{ label: t("Negative Y"), value: ToolPulloutDirection.NEGATIVE_Y },
};
const DIRECTION_CHOICES: DropDownItem[] = [
export const DIRECTION_CHOICES: DropDownItem[] = [
DIRECTION_CHOICES_DDI[ToolPulloutDirection.NONE],
DIRECTION_CHOICES_DDI[ToolPulloutDirection.POSITIVE_X],
DIRECTION_CHOICES_DDI[ToolPulloutDirection.NEGATIVE_X],

View File

@ -7,31 +7,32 @@ import { SlotDirectionSelect } from "./toolbay_slot_direction_selection";
import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources";
import { t } from "../../i18next_wrapper";
const positionIsDefined = (position: BotPosition): boolean =>
export const positionIsDefined = (position: BotPosition): boolean =>
isNumber(position.x) && isNumber(position.y) && isNumber(position.z);
const useCurrentPosition = (
export const useCurrentPosition = (
dispatch: Function, slot: TaggedToolSlotPointer, position: BotPosition) => {
if (positionIsDefined(position)) {
dispatch(edit(slot, { x: position.x, y: position.y, z: position.z }));
}
};
const positionButtonTitle = (position: BotPosition): string =>
export const positionButtonTitle = (position: BotPosition): string =>
positionIsDefined(position)
? `(${position.x}, ${position.y}, ${position.z})`
: t("(unknown)");
const changePulloutDirection =
export const newSlotDirection =
(old: ToolPulloutDirection | undefined): ToolPulloutDirection =>
isNumber(old) && old < 4 ? old + 1 : ToolPulloutDirection.NONE;
export const changePulloutDirection =
(dispatch: Function, slot: TaggedToolSlotPointer) => () => {
const newDirection =
(old: ToolPulloutDirection | undefined): ToolPulloutDirection =>
isNumber(old) && old < 4 ? old + 1 : ToolPulloutDirection.NONE;
dispatch(edit(slot,
{ pullout_direction: newDirection(slot.body.pullout_direction) }));
{ pullout_direction: newSlotDirection(slot.body.pullout_direction) }));
};
const directionIconClass = (slotDirection: ToolPulloutDirection) => {
export const directionIconClass = (slotDirection: ToolPulloutDirection) => {
switch (slotDirection) {
case ToolPulloutDirection.POSITIVE_X: return "fa fa-arrow-circle-right";
case ToolPulloutDirection.NEGATIVE_X: return "fa fa-arrow-circle-left";