Merge branch 'staging' into point_meta_updates

point_meta_updates
Rick Carlino 2020-05-06 15:10:17 -05:00 committed by GitHub
commit 9e14c2125d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 1655 additions and 1121 deletions

View File

@ -1,6 +1,24 @@
class BasePointSerializer < ApplicationSerializer
attributes :device_id, :name, :pointer_type, :meta, :x, :y, :z
# PROBLEM:
# * Users need a mutable way to mark a plant's creation time => `planted_at`
# * DB Admin needs to know the _real_ created_at time.
# * We can't change field names (or destroy data) that is in use by legacy devices
#
# SOLUTION:
# * Don't allow users to modify `created_at`
# * Provide `planted_at` if possible.
# * Always provide `planted_at` if it is available
# * Provide a read-only view of `created_at` if `planted_at` is `nil`
def planted_at
object.planted_at || object.created_at
end
def created_at
planted_at
end
def meta
object.meta || {}
end

View File

@ -348,9 +348,6 @@ export namespace ToolTips {
For example, you can mark a plant as "planted" during a seeding
sequence or mark a weed as "removed" after removing it.`);
export const REBOOT =
trim(`Power cycle FarmBot's onboard computer.`);
export const SET_SERVO_ANGLE =
trim(`Move a servo to the provided angle. An angle of 90 degrees
corresponds to the servo midpoint (or, for a continuous rotation
@ -362,6 +359,9 @@ export namespace ToolTips {
export const MOVE_TO_HOME =
trim(`Move FarmBot to home for the provided axis.`);
export const ASSERTION =
trim(`Evaluate Lua commands. For power users and software developers.`);
export const FIRMWARE_ACTION =
trim(`FarmBot OS or micro-controller firmware action.`);
@ -740,6 +740,15 @@ export namespace Content {
encoders, stall detection, or endstops enabled for the chosen axis.
Enable endstops, encoders, or stall detection from the Device page for: `);
export const REBOOT_STEP =
trim(`Power cycle FarmBot's onboard computer.`);
export const SHUTDOWN_STEP =
trim(`Power down FarmBot's onboard computer.`);
export const ESTOP_STEP =
trim(`Unlocking a device requires user intervention.`);
export const IN_USE =
trim(`Used in another resource. Protected from deletion.`);

View File

@ -1314,6 +1314,24 @@ ul {
}
}
.update-resource-step {
.custom-meta-field {
position: relative;
.fa-undo {
position: absolute;
top: 0.65rem;
right: 0.5rem;
color: $medium_light_gray;
&:hover {
color: $dark_gray;
}
}
}
.update-resource-pair {
margin-top: 1rem;
}
}
.farmware-name-manual-input {
margin-top: 1rem;
}

View File

@ -136,6 +136,9 @@
&.reboot-step {
background: $brown;
}
&.shutdown-step {
background: $brown;
}
&.unknown-step {
background: $gray;
}
@ -253,6 +256,9 @@
&.emergency-stop-step {
background: $light_red;
}
&.shutdown-step {
background: $light_brown;
}
&.reboot-step {
background: $light_brown;
}

View File

@ -11,12 +11,15 @@ import { OsUpdateButton } from "../os_update_button";
import { OsUpdateButtonProps } from "../interfaces";
import { ShouldDisplay } from "../../../interfaces";
import { Content } from "../../../../constants";
import { ConfigurationName } from "farmbot";
const UPDATE_CHANNEL = "update_channel" as ConfigurationName;
describe("<OsUpdateButton/>", () => {
beforeEach(() => {
bot.currentOSVersion = "6.1.6";
bot.hardware.informational_settings.controller_version = "6.1.6";
bot.hardware.configuration.beta_opt_in = false;
(bot.hardware.configuration[UPDATE_CHANNEL] as string) = "stable";
});
const fakeProps = (): OsUpdateButtonProps => ({
@ -33,7 +36,6 @@ describe("<OsUpdateButton/>", () => {
availableVersion: string | undefined;
availableBetaVersion: string | undefined;
availableBetaCommit: string | undefined;
betaOptIn: boolean | undefined;
onBeta: boolean | undefined;
update_available?: boolean | undefined;
shouldDisplay: ShouldDisplay;
@ -46,7 +48,6 @@ describe("<OsUpdateButton/>", () => {
availableVersion: "6.1.6",
availableBetaVersion: undefined,
availableBetaCommit: undefined,
betaOptIn: false,
onBeta: false,
shouldDisplay: () => false,
update_channel: "stable",
@ -104,7 +105,7 @@ describe("<OsUpdateButton/>", () => {
expected: Results) => {
const {
installedVersion, installedCommit, onBeta, update_available,
availableVersion, availableBetaVersion, availableBetaCommit, betaOptIn,
availableVersion, availableBetaVersion, availableBetaCommit,
shouldDisplay, update_channel,
} = testProps;
bot.hardware.informational_settings.controller_version = installedVersion;
@ -115,9 +116,7 @@ describe("<OsUpdateButton/>", () => {
bot.currentOSVersion = availableVersion;
bot.currentBetaOSVersion = availableBetaVersion;
bot.currentBetaOSCommit = availableBetaCommit;
bot.hardware.configuration.beta_opt_in = betaOptIn;
// tslint:disable-next-line:no-any
(bot.hardware.configuration as any).update_channel = update_channel;
(bot.hardware.configuration[UPDATE_CHANNEL] as string) = update_channel;
const p = fakeProps();
p.shouldDisplay = shouldDisplay;
@ -156,7 +155,7 @@ describe("<OsUpdateButton/>", () => {
const testProps = defaultTestProps();
testProps.installedVersion = "6.1.6";
testProps.availableVersion = undefined;
testProps.betaOptIn = true;
testProps.update_channel = "beta";
const expectedResults = cantConnect("release server");
testButtonState(testProps, expectedResults);
});
@ -166,7 +165,7 @@ describe("<OsUpdateButton/>", () => {
testProps.installedVersion = "6.1.6";
testProps.availableVersion = undefined;
testProps.availableBetaVersion = "6.1.7-beta";
testProps.betaOptIn = true;
testProps.update_channel = "beta";
const expectedResults = updateNeeded("6.1.7-beta");
testButtonState(testProps, expectedResults);
});
@ -175,7 +174,7 @@ describe("<OsUpdateButton/>", () => {
const testProps = defaultTestProps();
testProps.installedVersion = "6.1.6";
testProps.availableBetaVersion = undefined;
testProps.betaOptIn = true;
testProps.update_channel = "beta";
const expectedResults = upToDate("6.1.6");
testButtonState(testProps, expectedResults);
});
@ -205,7 +204,7 @@ describe("<OsUpdateButton/>", () => {
const testProps = defaultTestProps();
testProps.installedVersion = "6.1.5";
testProps.availableBetaVersion = "7.0.0-beta";
testProps.betaOptIn = true;
testProps.update_channel = "beta";
const expectedResults = updateNeeded("7.0.0-beta");
testButtonState(testProps, expectedResults);
});
@ -214,7 +213,7 @@ describe("<OsUpdateButton/>", () => {
const testProps = defaultTestProps();
testProps.installedVersion = "6.1.6";
testProps.availableBetaVersion = "6.1.6-beta";
testProps.betaOptIn = true;
testProps.update_channel = "beta";
const expectedResults = upToDate("6.1.6");
testButtonState(testProps, expectedResults);
});
@ -223,7 +222,7 @@ describe("<OsUpdateButton/>", () => {
const testProps = defaultTestProps();
testProps.installedVersion = "6.1.6";
testProps.availableBetaVersion = "6.1.6-beta";
testProps.betaOptIn = true;
testProps.update_channel = "beta";
testProps.onBeta = true;
const expectedResults = updateNeeded("6.1.6");
testButtonState(testProps, expectedResults);
@ -233,7 +232,7 @@ describe("<OsUpdateButton/>", () => {
const testProps = defaultTestProps();
testProps.installedVersion = "6.1.6";
testProps.availableBetaVersion = "6.1.6-beta";
testProps.betaOptIn = false;
testProps.update_channel = "stable";
testProps.onBeta = true;
const expectedResults = updateNeeded("6.1.6");
testButtonState(testProps, expectedResults);
@ -243,7 +242,7 @@ describe("<OsUpdateButton/>", () => {
const testProps = defaultTestProps();
testProps.installedVersion = "6.1.7";
testProps.availableBetaVersion = "6.1.7-beta";
testProps.betaOptIn = true;
testProps.update_channel = "beta";
testProps.onBeta = true;
const expectedResults = upToDate("6.1.7-beta");
testButtonState(testProps, expectedResults);
@ -253,7 +252,7 @@ describe("<OsUpdateButton/>", () => {
const testProps = defaultTestProps();
testProps.installedVersion = "6.1.7-beta";
testProps.availableBetaVersion = "6.1.7-beta";
testProps.betaOptIn = true;
testProps.update_channel = "beta";
const expectedResults = upToDate("6.1.7-beta");
testButtonState(testProps, expectedResults);
});
@ -264,7 +263,7 @@ describe("<OsUpdateButton/>", () => {
testProps.installedCommit = "old commit";
testProps.availableBetaVersion = "7.0.0-beta";
testProps.availableBetaCommit = "new commit";
testProps.betaOptIn = true;
testProps.update_channel = "beta";
testProps.onBeta = true;
const expectedResults = updateNeeded("7.0.0-beta");
testButtonState(testProps, expectedResults);
@ -273,7 +272,7 @@ describe("<OsUpdateButton/>", () => {
it("handles installed version newer than available (beta enabled)", () => {
const testProps = defaultTestProps();
testProps.installedVersion = "6.1.7";
testProps.betaOptIn = true;
testProps.update_channel = "beta";
testProps.onBeta = false;
testProps.availableBetaVersion = "6.1.7-beta";
const expectedResults = upToDate("6.1.7-beta");
@ -308,16 +307,6 @@ describe("<OsUpdateButton/>", () => {
testButtonState(testProps, expectedResults);
});
it("doesn't use update_channel value", () => {
const testProps = defaultTestProps();
testProps.installedVersion = "6.1.6";
testProps.shouldDisplay = () => false;
testProps.update_channel = "beta";
testProps.availableBetaVersion = "6.1.7-beta";
const expectedResults = upToDate("6.1.6");
testButtonState(testProps, expectedResults);
});
it("compares release candidates: newer", () => {
const testProps = defaultTestProps();
testProps.availableVersion = "6.1.5";

View File

@ -263,7 +263,7 @@ export function FbosDetails(props: FbosDetailsProps) {
soc_temp, wifi_level, uptime, memory_usage, disk_usage, throttled,
wifi_level_percent, cpu_usage, private_ip,
} = props.botInfoSettings;
const { last_ota, last_ota_checkup } = props.deviceAccount.body;
const { last_ota, last_ota_checkup, fbos_version } = props.deviceAccount.body;
const infoFwCommit = firmware_version?.includes(".") ? firmware_commit : "---";
const firmwareCommit = firmware_version?.split("-")[1] || infoFwCommit;
@ -273,6 +273,7 @@ export function FbosDetails(props: FbosDetailsProps) {
botToMqttLastSeen={props.botToMqttLastSeen}
timeSettings={props.timeSettings}
device={props.deviceAccount} />
<p><b>{t("Version last seen")}: </b>{fbos_version}</p>
<p><b>{t("Environment")}: </b>{env}</p>
<CommitDisplay title={t("Commit")}
repo={FarmBotRepo.FarmBotOS} commit={commit} />

View File

@ -4,7 +4,7 @@ import { SemverResult, semverCompare } from "../../../util";
import { OsUpdateButtonProps } from "./interfaces";
import { checkControllerUpdates } from "../../actions";
import { isString } from "lodash";
import { BotState, Feature } from "../../interfaces";
import { BotState } from "../../interfaces";
import { Content } from "../../../constants";
import { t } from "../../../i18next_wrapper";
@ -154,9 +154,8 @@ export const OsUpdateButton = (props: OsUpdateButtonProps) => {
const { controller_version } = bot.hardware.informational_settings;
/** FBOS beta release opt-in setting. */
const betaOptIn = props.shouldDisplay(Feature.use_update_channel)
? sourceFbosConfig("update_channel" as ConfigurationName).value !== "stable"
: !!sourceFbosConfig("beta_opt_in").value;
const betaOptIn =
sourceFbosConfig("update_channel" as ConfigurationName).value !== "stable";
/** FBOS update availability. */
const buttonStatusProps = buttonVersionStatus({ bot, betaOptIn });

View File

@ -117,7 +117,6 @@ describe("<Logs />", () => {
it("shows filtered overall filter status", () => {
const p = fakeProps();
p.shouldDisplay = () => true;
const wrapper = mount(<Logs {...p} />);
const state = fakeLogsState();
state.assertion = 2;
@ -129,10 +128,9 @@ describe("<Logs />", () => {
it("shows unfiltered overall filter status", () => {
const p = fakeProps();
p.shouldDisplay = () => false;
const wrapper = mount(<Logs {...p} />);
const state = fakeLogsState();
state.assertion = 2;
state.assertion = 3;
wrapper.setState(state);
const filterBtn = wrapper.find("button").first();
expect(filterBtn.text().toLowerCase()).toEqual("filter");

View File

@ -5,7 +5,6 @@ import { Filters } from "../interfaces";
import { startCase } from "lodash";
import { MESSAGE_TYPES, MessageType } from "../../sequences/interfaces";
import { t } from "../../i18next_wrapper";
import { Feature, ShouldDisplay } from "../../devices/interfaces";
const MENU_ORDER: string[] = [
MessageType.success,
@ -26,11 +25,9 @@ const menuSort = (a: string, b: string) =>
/** Get log filter keys from LogsState. */
export const filterStateKeys =
(state: LogsState, shouldDisplay: ShouldDisplay) =>
(state: LogsState) =>
Object.keys(state)
.filter(key => !["autoscroll", "markdown", "searchTerm"].includes(key))
.filter(key => shouldDisplay(Feature.assertion_block)
|| key !== "assertion");
.filter(key => !["autoscroll", "markdown", "searchTerm"].includes(key));
export const LogsFilterMenu = (props: LogsFilterMenuProps) => {
/** Filter level 0: logs hidden. */
@ -56,7 +53,7 @@ export const LogsFilterMenu = (props: LogsFilterMenuProps) => {
{t("normal")}
</button>
</fieldset>
{filterStateKeys(props.state, props.shouldDisplay).sort(menuSort)
{filterStateKeys(props.state).sort(menuSort)
.map((logType: keyof Filters) =>
<fieldset key={logType}>
<label>

View File

@ -76,7 +76,7 @@ export class RawLogs extends React.Component<LogsProps, Partial<LogsState>> {
/** Determine if log type filters are active. */
get filterActive() {
const filterKeys = filterStateKeys(this.state, this.props.shouldDisplay);
const filterKeys = filterStateKeys(this.state);
const filterValues = filterKeys
.map((key: keyof Filters) => this.state[key]);
// Filters active if every log type level is not equal to 3 (max verbosity)

View File

@ -1,25 +1,22 @@
import { regimensReducer, RegimenState } from "../reducer";
import { regimensReducer, RegimenState, newWeek } from "../reducer";
import { Actions } from "../../constants";
import { popWeek, pushWeek, selectDays, deselectDays } from "../bulk_scheduler/actions";
import {
popWeek, pushWeek, selectDays, deselectDays,
} from "../bulk_scheduler/actions";
import { defensiveClone } from "../../util";
import { Week } from "../bulk_scheduler/interfaces";
const week = newWeek();
Object.entries(week.days).map(([day, _]: [keyof Week["days"], boolean]) => {
week.days[day] = true;
});
week.days.day7 = false;
const STATE: RegimenState = {
dailyOffsetMs: 300000,
selectedSequenceUUID: "Sequence.71.167",
currentRegimen: "Regimen.4.56",
weeks: [
{
"days": {
"day1": true,
"day2": true,
"day3": true,
"day4": true,
"day5": true,
"day6": true,
"day7": false
}
},
],
weeks: [week],
schedulerOpen: false,
};

View File

@ -1,39 +0,0 @@
import * as React from "react";
import { render } from "enzyme";
import { WeekRow } from "../bulk_scheduler/week_row";
import { WeekRowProps } from "../bulk_scheduler/interfaces";
import { betterMerge } from "../../util";
function weekProps(p?: Partial<WeekRowProps>): WeekRowProps {
return betterMerge({
dispatch: jest.fn(),
index: 0,
week: {
"days": {
"day1": false,
"day2": false,
"day3": false,
"day4": false,
"day5": false,
"day6": false,
"day7": false
}
}
}, p || {});
}
describe("<WeekRow/>", () => {
it("renders week 1 day numbers", () => {
const wrapper = render(<WeekRow {...weekProps() } />);
const txt = wrapper.text();
expect(txt).toEqual("Week 11234567");
});
});
describe("<WeekRow/>", () => {
it("renders week 2 day numbers", () => {
const wrapper = render(<WeekRow {...weekProps({ index: 1 }) } />);
const txt = wrapper.text();
expect(txt).toEqual("Week 2891011121314");
});
});

View File

@ -19,6 +19,7 @@ import { arrayUnwrap } from "../../../resources/util";
import { overwrite } from "../../../api/crud";
import { fakeVariableNameSet } from "../../../__test_support__/fake_variables";
import { error, warning } from "../../../toast/toast";
import { newWeek } from "../../reducer";
const sequence_id = 23;
const regimen_id = 32;
@ -53,18 +54,9 @@ describe("commitBulkEditor()", () => {
state.resources.consumers.regimens.currentRegimen = regimenUuid;
state.resources.consumers.regimens.selectedSequenceUUID = sequenceUuid;
state.resources.consumers.regimens.dailyOffsetMs = 2000;
state.resources.consumers.regimens.weeks = [{
days:
{
day1: true,
day2: false,
day3: false,
day4: false,
day5: false,
day6: false,
day7: false
}
}];
const week = newWeek();
week.days.day1 = true;
state.resources.consumers.regimens.weeks = [week];
return state;
}

View File

@ -0,0 +1,36 @@
import { groupRegimenItemsByWeek } from "../group_regimen_items_by_week";
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { newWeek } from "../../reducer";
describe("groupRegimenItemsByWeek()", () => {
it("groups regimen items by week", () => {
const sequence = fakeSequence();
sequence.body.id = 1;
const week1 = newWeek();
week1.days.day1 = true;
const week2 = newWeek();
const week3 = newWeek();
week3.days.day2 = true;
week3.days.day4 = true;
const { day1, day2, day3, day4, day5, day6, day7 } = week3.days;
week3.days = { day1, day4, day3, day2, day5, day6, day7 };
const weeks = [week1, week2, week3];
const result = groupRegimenItemsByWeek(weeks, 100, sequence.body);
expect(result).toEqual([
{ time_offset: 100, sequence_id: 1 },
{ time_offset: 1296000100, sequence_id: 1 },
{ time_offset: 1468800100, sequence_id: 1 },
]);
});
it("handles missing sequence id", () => {
const sequence = fakeSequence();
sequence.body.id = undefined;
const week = newWeek();
week.days.day1 = true;
const result = groupRegimenItemsByWeek([week], 0, sequence.body);
expect(result).toEqual([
{ time_offset: 0, sequence_id: -1 },
]);
});
});

View File

@ -8,20 +8,12 @@ import {
import { Actions } from "../../../constants";
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { AddButton } from "../add_button";
import { newWeek } from "../../reducer";
describe("<BulkScheduler />", () => {
const weeks = [{
days:
{
day1: true,
day2: false,
day3: false,
day4: false,
day5: false,
day6: false,
day7: false
}
}];
const week = newWeek();
week.days.day1 = true;
const weeks = [week];
function fakeProps(): BulkEditorProps {
const sequence = fakeSequence();

View File

@ -3,20 +3,12 @@ import { mount } from "enzyme";
import { WeekGrid } from "../week_grid";
import { WeekGridProps } from "../interfaces";
import { Actions } from "../../../constants";
import { newWeek } from "../../reducer";
describe("<WeekGrid />", () => {
const weeks = [{
days:
{
day1: true,
day2: false,
day3: false,
day4: false,
day5: false,
day6: false,
day7: false
}
}];
const week = newWeek();
week.days.day1 = true;
const weeks = [week];
it("renders", () => {
const props: WeekGridProps = { weeks, dispatch: jest.fn() };

View File

@ -0,0 +1,36 @@
import * as React from "react";
import { render, mount } from "enzyme";
import { WeekRow } from "../week_row";
import { WeekRowProps } from "../interfaces";
import { betterMerge } from "../../../util";
import { newWeek } from "../../reducer";
import { Actions } from "../../../constants";
describe("<WeekRow/>", () => {
const fakeProps = (p?: Partial<WeekRowProps>): WeekRowProps =>
betterMerge({
dispatch: jest.fn(),
index: 0,
week: newWeek()
}, p || {});
it("renders week 1 day numbers", () => {
const wrapper = render(<WeekRow {...fakeProps()} />);
expect(wrapper.text()).toEqual("Week 11234567");
});
it("renders week 2 day numbers", () => {
const wrapper = render(<WeekRow {...fakeProps({ index: 1 })} />);
expect(wrapper.text()).toEqual("Week 2891011121314");
});
it("selects day", () => {
const p = fakeProps();
const wrapper = mount(<WeekRow {...p} />);
wrapper.find("input").first().simulate("click");
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.TOGGLE_DAY,
payload: { week: 0, day: 1 },
});
});
});

View File

@ -6,17 +6,15 @@ import { t } from "../../i18next_wrapper";
export function WeekRow({ index, dispatch, week }: WeekRowProps) {
return <div className="week-row">
<label className="week-label">{t("Week")} {index + 1}</label>
{
DAYS.map(function (day, i) {
const id = `${index}-${day}`;
return <Day day={i + 1}
week={index}
dispatch={dispatch}
id={id}
key={id}
active={week.days[day]} />;
})
}
{DAYS.map(function (day, i) {
const id = `${index}-${day}`;
return <Day day={i + 1}
week={index}
dispatch={dispatch}
id={id}
key={id}
active={week.days[day]} />;
})}
</div>;
}

View File

@ -12,7 +12,7 @@ export interface RegimenState {
schedulerOpen: boolean;
}
function newWeek() {
export function newWeek(): Week {
return {
days: {
day1: false,

View File

@ -60,7 +60,7 @@ export const determineVector =
};
/** Try to find a vector in scope declarations for the variable. */
const maybeFindVariable = (
export const maybeFindVariable = (
label: string, resources: ResourceIndex, uuid?: UUID,
): SequenceMeta | undefined =>
uuid ? findVariableByName(resources, uuid, label) : undefined;

View File

@ -33,7 +33,8 @@ describe("<StepButtonCluster />", () => {
it("has correct drag data", () => {
const p = fakeProps();
const wrapper = mount(<StepButtonCluster {...p} />);
const stepButton = wrapper.find("div").last();
const steps = wrapper.find(".step-dragger");
const stepButton = steps.at(steps.length - 2);
expect(stepButton.text().toLowerCase()).toEqual("take photo");
stepButton.simulate("dragStart", { dataTransfer: { setData: jest.fn() } });
expect(p.dispatch).toHaveBeenCalledWith(expect.objectContaining({

View File

@ -9,7 +9,6 @@ import {
determineVector, determineDropdown, SequenceMeta, determineVarDDILabel,
} from "../../resources/sequence_meta";
import { ResourceIndex, UUID } from "../../resources/interfaces";
import { Feature } from "../../devices/interfaces";
import { DefaultValueForm } from "./default_value_form";
import { t } from "../../i18next_wrapper";
import { CoordinateInputBoxes } from "./location_form_coordinate_input_boxes";
@ -49,13 +48,12 @@ export const LocationForm =
const { celeryNode, dropdown, vector } = maybeUseStepData({
resources, bodyVariables, variable, uuid: sequenceUuid
});
const displayVariables = props.shouldDisplay(Feature.variables) &&
allowedVariableNodes !== AllowedVariableNodes.variable;
const displayVariables = allowedVariableNodes !== AllowedVariableNodes.variable;
const headerForm = allowedVariableNodes === AllowedVariableNodes.parameter;
const variableListItems = displayVariables ? [PARENT(determineVarDDILabel({
label: "parent", resources, uuid: sequenceUuid, forceExternal: headerForm
}))] : [];
const displayGroups = props.shouldDisplay(Feature.groups) && !hideGroups;
const displayGroups = !hideGroups;
const unfiltered = locationFormList(resources, variableListItems, displayGroups);
const list = props.customFilterRule ?
unfiltered.filter(props.customFilterRule) : unfiltered;

View File

@ -95,6 +95,11 @@ export function StepButtonCluster(props: StepButtonProps) {
color="brown">
{t("REBOOT")}
</StepButton>,
<StepButton {...commonStepProps}
step={{ kind: "power_off", args: {} }}
color="brown">
{t("SHUTDOWN")}
</StepButton>,
<StepButton {...commonStepProps}
step={{ kind: "emergency_lock", args: {} }}
color="red">
@ -163,39 +168,32 @@ export function StepButtonCluster(props: StepButtonProps) {
step={{ kind: "take_photo", args: {} }}>
{t("TAKE PHOTO")}
</StepButton>,
<StepButton
{...commonStepProps}
step={{
kind: "assertion",
args: {
lua: "return 2 + 2 == 4",
_then: { kind: "nothing", args: {} },
assertion_type: "abort_recover",
}
}}
color="purple">
{t("ASSERTION")}
</StepButton>,
];
shouldDisplay(Feature.assertion_block) && ALL_THE_BUTTONS.push(<StepButton
{...commonStepProps}
step={{
kind: "assertion",
args: {
lua: "return 2 + 2 == 4",
_then: { kind: "nothing", args: {} },
assertion_type: "abort_recover",
}
}}
color="purple">
{t("ASSERTION")}
</StepButton>);
shouldDisplay(Feature.update_resource) && ALL_THE_BUTTONS.push(<StepButton
{...commonStepProps}
step={{
kind: "update_resource",
args: {
resource: {
kind: "resource",
args: { resource_id: 0, resource_type: "Device" }
}
},
body: [
{ kind: "pair", args: { label: "mounted_tool_id", value: 0 } },
],
args: { resource: NOTHING_SELECTED },
body: [],
}}
color="brown">
{t("Mark As...")}
</StepButton>);
return <Row>
<div className="step-button-cluster">
{ALL_THE_BUTTONS.map((stepButton, inx) =>

View File

@ -3,12 +3,16 @@ jest.mock("../../../api/crud", () => ({
}));
import { remove, move, splice, renderCeleryNode } from "../index";
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import {
fakeSequence, fakePlant,
} from "../../../__test_support__/fake_state/resources";
import { overwrite } from "../../../api/crud";
import { SequenceBodyItem, Wait } from "farmbot";
import { mount } from "enzyme";
import { StepParams, MessageType } from "../../interfaces";
import { emptyState } from "../../../resources/reducer";
import {
buildResourceIndex,
} from "../../../__test_support__/resource_index_builder";
describe("remove()", () => {
const fakeProps = () => ({
@ -82,12 +86,15 @@ describe("splice()", () => {
describe("renderCeleryNode()", () => {
const currentStep: Wait = { kind: "wait", args: { milliseconds: 100 } };
const plant = fakePlant();
plant.body.id = 23;
const fakeProps = (): StepParams => ({
currentSequence: fakeSequence(),
currentStep: currentStep,
dispatch: jest.fn(),
index: 0,
resources: emptyState().index,
resources: buildResourceIndex([plant]).index,
confirmStepDeletion: false,
});
@ -154,13 +161,13 @@ describe("renderCeleryNode()", () => {
resource: {
kind: "resource",
args: { resource_id: 23, resource_type: "Plant" }
},
body: [
{ kind: "pair", args: { label: "plant_stage", value: "planted" } },
]
}
}
},
body: [
{ kind: "pair", args: { label: "plant_stage", value: "planted" } },
]
},
expected: "markplant 23 as"
expected: "markstrawberry plant 1 (100, 200, 0)fieldplant stageasplanted"
},
{
node: {

View File

@ -2,11 +2,12 @@ import * as React from "react";
import { TileIf } from "../tile_if";
import { mount } from "enzyme";
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { If } from "farmbot/dist";
import { If, Wait } from "farmbot/dist";
import { emptyState } from "../../../resources/reducer";
import { StepParams } from "../../interfaces";
describe("<TileIf/>", () => {
function bootstrapTest() {
describe("<TileIf />", () => {
const fakeProps = (): StepParams => {
const currentStep: If = {
kind: "_if",
args: {
@ -18,35 +19,27 @@ describe("<TileIf/>", () => {
}
};
return {
component: mount(<TileIf
currentSequence={fakeSequence()}
currentStep={currentStep}
dispatch={jest.fn()}
index={0}
resources={emptyState().index}
confirmStepDeletion={false}
showPins={true} />)
currentSequence: fakeSequence(),
currentStep: currentStep,
dispatch: jest.fn(),
index: 0,
resources: emptyState().index,
confirmStepDeletion: false,
showPins: true,
};
}
};
it("renders inputs", () => {
const block = bootstrapTest().component;
const inputs = block.find("input");
const labels = block.find("label");
const buttons = block.find("button");
expect(inputs.length).toEqual(2);
expect(labels.length).toEqual(5);
expect(buttons.length).toEqual(4);
expect(inputs.first().props().placeholder).toEqual("If ...");
expect(labels.at(0).text()).toEqual("Variable");
expect(buttons.at(0).text()).toEqual("Pin 0");
expect(labels.at(1).text()).toEqual("Operator");
expect(buttons.at(1).text()).toEqual("is");
expect(labels.at(2).text()).toEqual("Value");
expect(inputs.at(1).props().value).toEqual(0);
expect(labels.at(3).text()).toEqual("Then Execute");
expect(buttons.at(2).text()).toEqual("None");
expect(labels.at(4).text()).toEqual("Else Execute");
expect(buttons.at(3).text()).toEqual("None");
it("renders if step", () => {
const wrapper = mount(<TileIf {...fakeProps()} />);
["Variable", "Operator", "Value", "Then Execute", "Else Execute"]
.map(string => expect(wrapper.text()).toContain(string));
});
it("doesn't render if step", () => {
const p = fakeProps();
const waitStep: Wait = { kind: "wait", args: { milliseconds: 0 } };
p.currentStep = waitStep;
const wrapper = mount(<TileIf {...p} />);
expect(wrapper.text()).toEqual("Expected `_if` node");
});
});

View File

@ -0,0 +1,52 @@
import * as React from "react";
import { TileMarkAs } from "../tile_mark_as";
import { mount } from "enzyme";
import {
fakeSequence, fakePlant,
} from "../../../__test_support__/fake_state/resources";
import { UpdateResource, Wait } from "farmbot/dist";
import { StepParams } from "../../interfaces";
import {
buildResourceIndex,
} from "../../../__test_support__/resource_index_builder";
describe("<TileMarkAs />", () => {
const fakeProps = (): StepParams => {
const currentStep: UpdateResource = {
kind: "update_resource",
args: {
resource: {
kind: "resource",
args: { resource_type: "Plant", resource_id: 1 }
},
},
body: [
{ kind: "pair", args: { label: "some_attr", value: "some_value" } },
],
};
const plant = fakePlant();
plant.body.id = 1;
return {
currentSequence: fakeSequence(),
currentStep: currentStep,
dispatch: jest.fn(),
index: 0,
resources: buildResourceIndex([plant]).index,
confirmStepDeletion: false,
};
};
it("renders if step", () => {
const wrapper = mount(<TileMarkAs {...fakeProps()} />);
["Mark", "Strawberry plant 1 (100, 200, 0)", "field", "as"]
.map(string => expect(wrapper.text()).toContain(string));
});
it("doesn't render update_resource step", () => {
const p = fakeProps();
const waitStep: Wait = { kind: "wait", args: { milliseconds: 0 } };
p.currentStep = waitStep;
const wrapper = mount(<TileMarkAs {...p} />);
expect(wrapper.text()).toEqual("Expected `update_resource` node");
});
});

View File

@ -5,59 +5,56 @@ import { render } from "enzyme";
import React from "react";
import { StepParams } from "../../interfaces";
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
import {
buildResourceIndex,
} from "../../../__test_support__/resource_index_builder";
import { editStep } from "../../../api/crud";
import { Reboot } from "farmbot";
import { Content } from "../../../constants";
const fakeProps = (): StepParams => {
const currentSequence = fakeSequence();
const resources = buildResourceIndex().index;
return {
currentSequence,
currentStep: {
kind: "reboot",
args: {
package: "farmbot_os"
}
},
dispatch: jest.fn(),
index: 1,
resources,
confirmStepDeletion: false,
};
};
const fakeProps = (): StepParams => ({
currentSequence: fakeSequence(),
currentStep: {
kind: "reboot",
args: {
package: "farmbot_os"
}
},
dispatch: jest.fn(),
index: 1,
resources: buildResourceIndex().index,
confirmStepDeletion: false,
});
describe("<TileReboot/>", () => {
it("renders", () => {
const el = render(<TileReboot {...fakeProps()} />);
const verbiage = el.text();
expect(verbiage).toContain("Power cycle FarmBot's onboard computer.");
const block = render(<TileReboot {...fakeProps()} />);
expect(block.text()).toContain(Content.REBOOT_STEP);
});
it("crashes if the step is of the wrong `kind`", () => {
const props = fakeProps();
props.currentStep = { kind: "sync", args: {} };
const boom = () => TileReboot(props);
const p = fakeProps();
p.currentStep = { kind: "sync", args: {} };
const boom = () => TileReboot(p);
expect(boom).toThrowError();
});
it("edits the reboot step", () => {
const props = fakeProps();
const editFn = editTheRebootStep(props);
const p = fakeProps();
const editFn = editTheRebootStep(p);
editFn("arduino_firmware");
expect(props.dispatch).toHaveBeenCalled();
expect(p.dispatch).toHaveBeenCalled();
expect(editStep).toHaveBeenCalledWith({
step: props.currentStep,
index: props.index,
sequence: props.currentSequence,
step: p.currentStep,
index: p.index,
sequence: p.currentSequence,
executor: expect.any(Function),
});
});
it("executes the executor", () => {
const props = fakeProps();
const step = props.currentStep as Reboot;
const p = fakeProps();
const step = p.currentStep as Reboot;
step.args.package = "X";
const fn = rebootExecutor("arduino_firmware");
fn(step);

View File

@ -0,0 +1,29 @@
import * as React from "react";
import { mount } from "enzyme";
import { TileShutdown } from "../tile_shutdown";
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { PowerOff } from "farmbot";
import { emptyState } from "../../../resources/reducer";
import { StepParams } from "../../interfaces";
import { Content } from "../../../constants";
describe("<TileShutdown />", () => {
const currentStep: PowerOff = {
kind: "power_off",
args: {},
};
const fakeProps = (): StepParams => ({
currentSequence: fakeSequence(),
currentStep: currentStep,
dispatch: jest.fn(),
index: 0,
resources: emptyState().index,
confirmStepDeletion: false,
});
it("renders step", () => {
const block = mount(<TileShutdown {...fakeProps()} />);
expect(block.text()).toContain(Content.SHUTDOWN_STEP);
});
});

View File

@ -18,7 +18,7 @@ import { TileExecuteScript } from "./tile_execute_script";
import { TileTakePhoto } from "./tile_take_photo";
import { overwrite } from "../../api/crud";
import { TileFindHome } from "./tile_find_home";
import { MarkAs } from "./mark_as";
import { TileMarkAs } from "./tile_mark_as";
import { TileUnknown } from "./tile_unknown";
import { forceSetStepTag } from "../../resources/sequence_tagging";
import { compact, assign } from "lodash";
@ -34,6 +34,7 @@ import { TileAssertion } from "./tile_assertion";
import { TileEmergencyStop } from "./tile_emergency_stop";
import { TileReboot } from "./tile_reboot";
import { TileOldMarkAs } from "./tile_old_mark_as";
import { TileShutdown } from "./tile_shutdown";
interface MoveParams {
step: Step;
@ -150,7 +151,7 @@ export function renderCeleryNode(props: StepParams) {
case "take_photo": return <TileTakePhoto {...props} />;
case "wait": return <TileWait {...props} />;
case "write_pin": return <TileWritePin {...props} />;
case "update_resource": return <MarkAs {...props} />;
case "update_resource": return <TileMarkAs {...props} />;
case "resource_update" as LegalSequenceKind:
return <TileOldMarkAs {...props} />;
case "set_servo_angle": return <TileSetServoAngle {...props} />;
@ -161,7 +162,8 @@ export function renderCeleryNode(props: StepParams) {
case "reboot": return <TileReboot {...props} />;
case "emergency_lock": return <TileEmergencyStop {...props} />;
case "assertion": return <TileAssertion {...props} />;
case "sync": case "power_off": case "read_status":
case "power_off": return <TileShutdown {...props} />;
case "sync": case "read_status":
case "emergency_unlock": case "install_first_party_farmware":
return <TileSystemAction {...props} />;
case "check_updates": case "factory_reset":

View File

@ -1,67 +0,0 @@
import { Row, Col, FBSelect, DropDownItem } from "../../ui/index";
import { StepParams } from "../interfaces";
import { StepWrapper, StepHeader, StepContent } from "../step_ui/index";
import { ToolTips } from "../../constants";
import * as React from "react";
import { unpackStep } from "./mark_as/unpack_step";
import { UpdateResource } from "farmbot";
import { resourceList } from "./mark_as/resource_list";
import { actionList } from "./mark_as/action_list";
import { commitStepChanges } from "./mark_as/commit_step_changes";
import { t } from "../../i18next_wrapper";
interface MarkAsState { nextResource: DropDownItem | undefined }
const NONE = (): DropDownItem => ({ label: t("Select one"), value: 0 });
export class MarkAs extends React.Component<StepParams, MarkAsState> {
state: MarkAsState = { nextResource: undefined };
className = "update-resource-step";
commitSelection = (nextAction: DropDownItem) => {
this.props.dispatch(commitStepChanges({
index: this.props.index,
nextAction,
nextResource: this.state.nextResource,
sequence: this.props.currentSequence,
step: this.props.currentStep as UpdateResource,
}));
this.setState({ nextResource: undefined });
};
render() {
const step = this.props.currentStep as UpdateResource;
const { rightSide, leftSide } =
unpackStep({ step, resourceIndex: this.props.resources });
return <StepWrapper>
<StepHeader
className={this.className}
helpText={ToolTips.MARK_AS}
currentSequence={this.props.currentSequence}
currentStep={this.props.currentStep}
dispatch={this.props.dispatch}
index={this.props.index}
confirmStepDeletion={this.props.confirmStepDeletion} />
<StepContent className={this.className}>
<Row>
<Col xs={6}>
<label>{t("Mark")}</label>
<FBSelect
list={resourceList(this.props.resources)}
onChange={(nextResource) => this.setState({ nextResource })}
allowEmpty={false}
selectedItem={this.state.nextResource || leftSide} />
</Col>
<Col xs={6}>
<label>{t("as")}</label>
<FBSelect
list={actionList(this.state.nextResource?.headingId,
step, this.props.resources)}
onChange={this.commitSelection}
key={JSON.stringify(rightSide) + JSON.stringify(this.state)}
selectedItem={this.state.nextResource ? NONE() : rightSide} />
</Col>
</Row>
</StepContent>
</StepWrapper>;
}
}

View File

@ -1,65 +0,0 @@
import { actionList } from "../action_list";
import { updateResource, markAsResourceFixture } from "../test_support";
import {
buildResourceIndex,
} from "../../../../__test_support__/resource_index_builder";
import { PLANT_OPTIONS } from "../constants";
describe("actionList()", () => {
it("uses args.resource_type if DropDownItem is undefined", () => {
const step = updateResource({
kind: "resource",
args: { resource_type: "Plant", resource_id: 0 }
});
const { index } = markAsResourceFixture();
const result = actionList(undefined, step, index);
expect(result).toEqual(PLANT_OPTIONS());
});
it("provides a list of tool mount actions", () => {
const ddi = { label: "test case", value: 1, headingId: "Device" };
const step = updateResource();
const { index } = markAsResourceFixture();
const result = actionList(ddi.headingId, step, index);
expect(result.length).toBe(3);
const labels = result.map(x => x.label);
expect(labels).toContain("Not Mounted");
expect(labels).toContain("Mounted to: T1");
expect(labels).toContain("Mounted to: T2");
});
it("provides a list of generic pointer actions", () => {
const ddi = { label: "test case", value: 1, headingId: "GenericPointer" };
const step = updateResource();
const { index } = markAsResourceFixture();
const result = actionList(ddi.headingId, step, index);
expect(result.length).toBe(1);
const labels = result.map(x => x.label);
expect(labels).toContain("Removed");
});
it("provides a list of weed pointer actions", () => {
const ddi = { label: "test case", value: 1, headingId: "Weed" };
const step = updateResource();
const { index } = markAsResourceFixture();
const result = actionList(ddi.headingId, step, index);
expect(result.length).toBe(1);
const labels = result.map(x => x.label);
expect(labels).toContain("Removed");
});
it("returns an empty list for identifiers", () => {
const ddi = { label: "test case", value: 1, headingId: "USB Cables" };
const step = updateResource();
const { index } = buildResourceIndex([]);
const result = actionList(ddi.headingId, step, index);
expect(result.length).toBe(0);
});
it("returns an empty list for all other options", () => {
const step = updateResource({ kind: "identifier", args: { label: "var" } });
const { index } = buildResourceIndex([]);
const result = actionList("Other", step, index);
expect(result.length).toBe(0);
});
});

View File

@ -1,25 +0,0 @@
import { fakeMarkAsProps } from "../test_support";
import { commitStepChanges } from "../commit_step_changes";
import { UpdateResource, TaggedSequence } from "farmbot";
import { Actions } from "../../../../constants";
import { unpackUUID } from "../../../../util";
describe("commitSelection", () => {
it("commits changes in a <MarkAs/> component", () => {
const p = fakeMarkAsProps();
const results = commitStepChanges({
nextAction: { label: "X", value: "some_action" },
nextResource: undefined,
step: p.currentStep as UpdateResource,
index: p.index,
sequence: p.currentSequence
});
expect(results.type).toBe(Actions.OVERWRITE_RESOURCE);
const { payload } = results;
expect(unpackUUID(payload.uuid).kind).toBe("Sequence");
const s = payload.update as TaggedSequence["body"];
expect(s.kind).toBe("sequence");
const step = (s.body || [])[0] as UpdateResource;
expect(step.body?.[0].args.value).toBe("some_action");
});
});

View File

@ -1,47 +0,0 @@
jest.mock("../commit_step_changes", () => {
return {
commitStepChanges: jest.fn()
};
});
import * as React from "react";
import { shallow, mount } from "enzyme";
import { MarkAs } from "../../mark_as";
import { FBSelect } from "../../../../ui";
import { fakeMarkAsProps } from "../test_support";
import { commitStepChanges } from "../commit_step_changes";
describe("<MarkAs/>", () => {
it("renders the basic parts", () => {
const el = mount(<MarkAs {...fakeMarkAsProps()} />);
const text = el.text();
expect(text).toContain("Tool Mount");
expect(text).toContain("Not Mounted");
});
it("selects a resource", () => {
const el = shallow(<MarkAs {...fakeMarkAsProps()} />);
const wow = el.find(FBSelect).first();
expect(wow).toBeTruthy();
const nextResource = {
label: "fake resource",
value: "fake_resource"
};
wow.simulate("change", nextResource);
expect(el.state()).toEqual({ nextResource });
});
it("triggers callbacks (commitSelection)", () => {
const props = fakeMarkAsProps();
const i = new MarkAs(props);
i.setState = jest.fn((s: typeof i.state) => {
i.state = s;
});
const nextResource = { label: "should be cleared", value: 1 };
i.setState({ nextResource });
expect(i.state.nextResource).toEqual(nextResource);
i.commitSelection({ label: "stub", value: "mock" });
expect(i.state.nextResource).toBe(undefined);
expect(commitStepChanges).toHaveBeenCalled();
expect(i.state.nextResource).toEqual(undefined);
});
});

View File

@ -1,45 +0,0 @@
import { updateResource } from "../test_support";
import { packStep } from "../pack_step";
import { TOP_HALF } from "../constants";
import { Resource, Identifier } from "farmbot";
describe("packStep()", () => {
const plant = updateResource({
kind: "resource",
args: { resource_type: "Plant", resource_id: 6 }
});
it("serializes 'plant_stage' actions", () => {
const actionDDI = { value: "harvested", label: "harvested" };
const { args, body } = packStep(plant, undefined, actionDDI);
expect(body?.[0].args.label).toEqual("plant_stage");
expect(body?.[0].args.value).toEqual("harvested");
expect((args.resource as Resource).args.resource_id).toEqual(6);
expect((args.resource as Resource).args.resource_type).toEqual("Plant");
});
it("serializes 'mounted_tool_id' actions", () => {
const resourceDDI = TOP_HALF[0];
const actionDDI = { value: 23, label: "Mounted to can opener" };
const device = updateResource({
kind: "resource",
args: { resource_type: "Device", resource_id: 7 }
});
const { args, body } = packStep(device, resourceDDI, actionDDI);
expect(body?.[0].args.label).toEqual("mounted_tool_id");
expect((args.resource as Resource).args.resource_type).toEqual("Device");
expect((args.resource as Resource).args.resource_id).toEqual(0);
expect(body?.[0].args.value).toEqual(23);
});
it("serializes 'plant_stage' actions: identifier", () => {
const actionDDI = { value: "harvested", label: "harvested" };
const identifier = updateResource({
kind: "identifier", args: { label: "var" }
});
const { args, body } = packStep(identifier, undefined, actionDDI);
expect(body?.[0].args.label).toEqual("plant_stage");
expect(body?.[0].args.value).toEqual("harvested");
expect((args.resource as Identifier).args.label).toEqual("var");
});
});

View File

@ -1,18 +0,0 @@
import { resourceList } from "../resource_list";
import { markAsResourceFixture } from "../test_support";
describe("resourceList()", () => {
it("lists defaults, plus saved points", () => {
const { index } = markAsResourceFixture();
const result = resourceList(index);
expect(result.length).toBeTruthy();
const headings = result.filter(x => x.heading).map(x => x.label);
expect(headings).toContain("Device");
expect(headings).toContain("Plants");
expect(headings).toContain("Points");
expect(headings).toContain("Weeds");
const weeds = result.filter(x => x.headingId == "Weed");
expect(weeds.length).toEqual(2);
expect(weeds[1].label).toEqual("weed 1 (200, 400, 0)");
});
});

View File

@ -1,144 +0,0 @@
import { fakeResourceIndex } from "../../../locals_list/test_helpers";
import { updateResource } from "../test_support";
import { unpackStep, TOOL_MOUNT, DISMOUNTED } from "../unpack_step";
import {
selectAllPlantPointers,
selectAllTools,
selectAllWeedPointers,
} from "../../../../resources/selectors";
import { DropDownPair } from "../interfaces";
import { fakeTool, fakeWeed } from "../../../../__test_support__/fake_state/resources";
import {
buildResourceIndex,
} from "../../../../__test_support__/resource_index_builder";
describe("unpackStep()", () => {
function assertGoodness(result: DropDownPair,
action_label: string,
action_value: string,
resource_label: string,
resource_value: string | number): void {
expect(result.rightSide.label).toBe(action_label);
expect(result.rightSide.value).toBe(action_value);
expect(result.leftSide.label).toBe(resource_label);
expect(result.leftSide.value).toBe(resource_value);
}
it("unpacks empty tool_ids", () => {
const result = unpackStep({
step: updateResource(undefined, { label: "mounted_tool_id", value: 0 }),
resourceIndex: fakeResourceIndex()
});
expect(result).toEqual(DISMOUNTED());
});
it("unpacks valid tool_ids", () => {
const resourceIndex = fakeResourceIndex();
const { body } = selectAllTools(resourceIndex)[0];
expect(body).toBeTruthy();
const result = unpackStep({
step: updateResource(undefined,
{ label: "mounted_tool_id", value: body.id || NaN }),
resourceIndex
});
const actionLabel = "Mounted to: Generic Tool";
const { label, value } = TOOL_MOUNT();
assertGoodness(result, actionLabel, "mounted", label, value);
});
it("unpacks valid tool_ids with missing names", () => {
const tool = fakeTool();
tool.body.id = 1;
tool.body.name = undefined;
const resourceIndex = buildResourceIndex([tool]).index;
const { body } = selectAllTools(resourceIndex)[0];
expect(body).toBeTruthy();
const result = unpackStep({
step: updateResource(undefined,
{ label: "mounted_tool_id", value: body.id || NaN }),
resourceIndex
});
const actionLabel = "Mounted to: Untitled Tool";
const { label, value } = TOOL_MOUNT();
assertGoodness(result, actionLabel, "mounted", label, value);
});
it("unpacks invalid tool_ids (that may have been valid previously)", () => {
const result = unpackStep({
step: updateResource(undefined,
{ label: "mounted_tool_id", value: Infinity }),
resourceIndex: fakeResourceIndex()
});
const actionLabel = "Mounted to: an unknown tool";
const { label, value } = TOOL_MOUNT();
assertGoodness(result, actionLabel, "mounted", label, value);
});
it("unpacks plant_stage operations: plants", () => {
const resourceIndex = fakeResourceIndex();
const plant = selectAllPlantPointers(resourceIndex)[1];
expect(plant).toBeTruthy();
const result = unpackStep({
step: updateResource({
kind: "resource",
args: { resource_type: "Plant", resource_id: plant.body.id || -1 }
},
{ label: "plant_stage", value: "wilting" }),
resourceIndex
});
const { body } = plant;
const plantName = `${body.name} (${body.x}, ${body.y}, ${body.z})`;
assertGoodness(result, "wilting", "wilting", plantName, body.id || NaN);
});
it("unpacks plant_stage operations: weeds", () => {
const resourceIndex = fakeResourceIndex([fakeWeed()]);
const weed = selectAllWeedPointers(resourceIndex)[1];
expect(weed).toBeTruthy();
const result = unpackStep({
step: updateResource({
kind: "resource",
args: { resource_type: "Weed", resource_id: weed.body.id || -1 }
},
{ label: "plant_stage", value: "removed" }),
resourceIndex
});
const { body } = weed;
const plantName = `${body.name} (${body.x}, ${body.y}, ${body.z})`;
assertGoodness(result, "Removed", "removed", plantName, body.id || NaN);
});
it("unpacks plant_stage operations: identifier", () => {
const resourceIndex = fakeResourceIndex();
const result = unpackStep({
step: updateResource(
{ kind: "identifier", args: { label: "var" } },
{ label: "plant_stage", value: "removed" }),
resourceIndex
});
assertGoodness(result, "Removed", "removed", "var", "var");
});
it("unpacks unknown resource update_resource steps", () => {
const result = unpackStep({
step: updateResource(),
resourceIndex: fakeResourceIndex()
});
assertGoodness(result,
"some_value", "some_value",
"Other 1 some_attr", "some_attr");
});
it("unpacks unknown identifier update_resource steps", () => {
const result = unpackStep({
step: updateResource({ kind: "identifier", args: { label: "var" } }),
resourceIndex: fakeResourceIndex()
});
assertGoodness(result,
"some_value", "some_value",
"variable 0 some_attr", "some_attr");
});
});

View File

@ -1,45 +0,0 @@
import { Dictionary } from "farmbot";
import { DropDownItem } from "../../../ui";
import { ListBuilder } from "./interfaces";
import { ResourceIndex } from "../../../resources/interfaces";
import { UpdateResource } from "farmbot";
import { selectAllTools } from "../../../resources/selectors";
import {
MOUNTED_TO,
DISMOUNT,
PLANT_OPTIONS,
POINT_OPTIONS,
} from "./constants";
const allToolsAsDDI = (i: ResourceIndex) => {
return selectAllTools(i)
.filter(x => !!x.body.id)
.map(x => {
return {
label: `${MOUNTED_TO()} ${x.body.name}`,
value: x.body.id || 0
};
});
};
const DEFAULT = "Default";
const ACTION_LIST: Dictionary<ListBuilder> = {
"Device": (i) => [DISMOUNT(), ...allToolsAsDDI(i)],
"Plant": () => PLANT_OPTIONS(),
"GenericPointer": () => POINT_OPTIONS(),
"Weed": () => POINT_OPTIONS(),
[DEFAULT]: () => []
};
const getList = (t: string): ListBuilder =>
(ACTION_LIST[t] || ACTION_LIST[DEFAULT]);
export const actionList = (d: string | undefined,
r: UpdateResource,
i: ResourceIndex): DropDownItem[] => {
const resourceType = r.args.resource.kind == "identifier"
? DEFAULT
: r.args.resource.args.resource_type;
return getList(d || resourceType)(i);
};

View File

@ -1,20 +0,0 @@
import { UpdateResource } from "farmbot";
import { editStep } from "../../../api/crud";
import { packStep } from "./pack_step";
import { MarkAsEditProps } from "./interfaces";
/** A wrapper for the `editStep()` action creator.
* Isolated from UI for ease of testing. */
export const commitStepChanges = (p: MarkAsEditProps) => {
const { step, nextResource, nextAction, index, sequence } = p;
return editStep({
step,
index,
sequence,
executor(c: UpdateResource) {
const { args, body } = packStep(step, nextResource, nextAction);
c.args = args;
c.body = body;
}
});
};

View File

@ -1,45 +0,0 @@
import { DropDownItem } from "../../../ui";
import { t } from "../../../i18next_wrapper";
import { PLANT_STAGE_LIST } from "../../../farm_designer/plants/edit_plant_status";
export const MOUNTED_TO = () => t("Mounted to:");
export const DISMOUNT = (): DropDownItem =>
({ label: t("Not Mounted"), value: 0 });
/** Legal "actions" for "Mark As.." block when marking Point resources */
export const POINT_OPTIONS = (): DropDownItem[] => [
{ label: t("Removed"), value: "removed" },
];
/** Legal "actions" in the "Mark As.." block when operating on
* a Plant resource. */
export const PLANT_OPTIONS = PLANT_STAGE_LIST;
const value = 0; // Not used in headings.
export const PLANT_HEADER: DropDownItem = {
headingId: "Plant",
label: t("Plants"),
value,
heading: true
};
export const POINT_HEADER: DropDownItem = {
headingId: "GenericPointer",
label: t("Points"),
value,
heading: true
};
export const WEED_HEADER: DropDownItem = {
headingId: "Weed",
label: t("Weeds"),
value,
heading: true
};
export const TOP_HALF = [
{ headingId: "Device", label: t("Device"), value, heading: true },
{ headingId: "Device", label: t("Tool Mount"), value },
];

View File

@ -1,37 +0,0 @@
import { ResourceIndex } from "../../../resources/interfaces";
import { DropDownItem } from "../../../ui";
import { UpdateResource, TaggedSequence, Resource, Identifier } from "farmbot";
/** Function that converts resources into dropdown selections based on
* use-case specific rules */
export type ListBuilder = (i: ResourceIndex) => DropDownItem[];
/** Input data for calls to commitStepChanges() */
export interface MarkAsEditProps {
nextAction: DropDownItem;
nextResource: DropDownItem | undefined;
step: UpdateResource;
index: number;
sequence: TaggedSequence
}
export interface PackedStepWithResourceIndex {
step: UpdateResource;
resourceIndex: ResourceIndex;
}
export interface UnpackedStepWithResourceIndex {
resource: Resource | Identifier;
field: string;
value: string | number | boolean;
resourceIndex: ResourceIndex;
}
/** A pair of DropDownItems used to render the currently selected items in the
* "Mark As.." block. */
export interface DropDownPair {
/** Left side drop down */
leftSide: DropDownItem;
/** Right side drop down */
rightSide: DropDownItem;
}

View File

@ -1,61 +0,0 @@
import { UpdateResource, Resource, Identifier, resource_type } from "farmbot";
import { DropDownItem } from "../../../ui";
/**
* This is a support function for the <MarkAs/> component.
*
* SCENARIO: You are editing a `Mark As..` sequence step. The user has unsaved
* local changes as well as a copy of older data from the API.
*
* PROBLEM: You need to take the component's local state plus the
* shape of the "update_resource" ("Mark As..") block and merge them
* together so that you can render the form in the editor.
*
* SOLUTION: Use the celery node + pieces of the component's state (resourceDDI,
* actionDDI) to properly populate dropdown menus and determine the
* shape of the new "update_resource" step when it is saved.
* */
export const packStep = (
csNode: UpdateResource,
resourceDDI: DropDownItem | undefined,
actionDDI: DropDownItem,
): UpdateResource => {
const resource = resourceDDI?.headingId
? resourceNode(resourceDDI.headingId, resourceDDI.value)
: csNode.args.resource;
if (resource.kind == "identifier") {
return updateResource(resource, "plant_stage", actionDDI.value);
} else {
switch (resource.args.resource_type) {
case "Device":
/* Scenario I: Changing tool mount */
return updateResource(resource, "mounted_tool_id", actionDDI.value);
default:
/* Scenario II: Changing a point */
return updateResource(resource, "plant_stage", actionDDI.value);
}
}
};
const resourceNode = (type: string, id: string | number): Resource => ({
kind: "resource",
args: {
resource_type: type as resource_type,
resource_id: parseInt("" + id),
}
});
const updateResource = (
resource: Resource | Identifier,
field: string,
value: string | number,
): UpdateResource => ({
kind: "update_resource",
args: { resource },
body: [{
kind: "pair", args: {
label: field,
value: value,
}
}]
});

View File

@ -1,54 +0,0 @@
import { ResourceIndex } from "../../../resources/interfaces";
import { DropDownItem } from "../../../ui/fb_select";
import { selectAllPoints } from "../../../resources/selectors";
import { TaggedPoint } from "farmbot";
import { Point } from "farmbot/dist/resources/api_resources";
import { POINT_HEADER, PLANT_HEADER, TOP_HALF, WEED_HEADER } from "./constants";
/** Filter function to remove resources we don't care about,
* such as ToolSlots and unsaved (Plant|Point)'s */
const isRelevant = (x: TaggedPoint) => {
const saved = !!x.body.id;
const notToolSlot = x.body.pointer_type !== "ToolSlot";
return saved && notToolSlot;
};
/** Format DropDownItem["label"] as "Name (1, 2, 3)" */
const labelStr =
(n: string, x: number, y: number, z: number) => `${n} (${x}, ${y}, ${z})`;
/** Convert a Point to a DropDownItem that is formatted appropriately
* for the "Mark As.." step. */
export const point2ddi = (i: Point): DropDownItem => {
const { x, y, z, name, id, pointer_type } = i;
return {
value: id || 0,
label: labelStr(name, x, y, z),
headingId: pointer_type,
};
};
/** GIVEN: mixed list of *SAVED* point types (ToolSlot, Plant, Pointer)
* RETURNS: list of DropDownItems with proper headers and `headerId`s */
const pointList =
(input: TaggedPoint[]): DropDownItem[] => {
const genericPoints: DropDownItem[] = [POINT_HEADER];
const weeds: DropDownItem[] = [WEED_HEADER];
const plants: DropDownItem[] = [PLANT_HEADER];
input
.map(x => x.body)
.forEach(body => {
switch (body.pointer_type) {
case "GenericPointer": return genericPoints.push(point2ddi(body));
case "Weed": return weeds.push(point2ddi(body));
case "Plant": return plants.push(point2ddi(body));
}
});
return [...plants, ...genericPoints, ...weeds];
};
/** Creates a formatted DropDownItem list for the "Resource" (left hand) side of
* the "Mark As" step. */
export const resourceList = (r: ResourceIndex): DropDownItem[] => {
return [...TOP_HALF, ...pointList(selectAllPoints(r).filter(isRelevant))];
};

View File

@ -1,73 +0,0 @@
import {
UpdateResource, TaggedSequence, resource_type, Pair, Resource, Identifier,
} from "farmbot";
import {
buildResourceIndex,
} from "../../../__test_support__/resource_index_builder";
import {
fakeTool,
fakePlant,
fakePoint,
fakeSequence,
fakeWeed,
} from "../../../__test_support__/fake_state/resources";
import { betterMerge } from "../../../util";
import { MarkAs } from "../mark_as";
export function updateResource(
resource?: Resource | Identifier, pairArgs?: Pair["args"]): UpdateResource {
return {
kind: "update_resource",
args: {
resource: resource || {
kind: "resource", args: {
resource_type: "Other" as resource_type,
resource_id: 1,
}
},
},
body: [{
kind: "pair", args: {
label: "some_attr",
value: "some_value",
...pairArgs,
}
}],
};
}
export const markAsResourceFixture = () => buildResourceIndex([
betterMerge(fakeTool(), { body: { name: "T1", id: 1 } }),
fakePlant(),
betterMerge(fakeTool(), { body: { name: "T2", id: 2 } }),
betterMerge(fakePoint(), { body: { name: "my point", id: 7 } }),
betterMerge(fakeWeed(), { body: { name: "weed 1", id: 8 } }),
betterMerge(fakeTool(), { body: { name: "T3", id: undefined } }),
]);
export function fakeMarkAsProps() {
const steps: TaggedSequence["body"]["body"] = [
{
kind: "update_resource",
args: {
resource: {
kind: "resource",
args: { resource_id: 0, resource_type: "Device" }
}
},
body: [{ kind: "pair", args: { label: "mounted_tool_id", value: 0 } }],
},
];
const currentSequence: TaggedSequence =
betterMerge(fakeSequence(), { body: { body: steps } });
const props: MarkAs["props"] = {
currentSequence,
dispatch: jest.fn(),
index: 0,
currentStep: steps[0],
resources: buildResourceIndex([currentSequence]).index,
confirmStepDeletion: false
};
return props;
}

View File

@ -1,94 +0,0 @@
import { DropDownItem } from "../../../ui";
import {
findToolById,
findPointerByTypeAndId,
} from "../../../resources/selectors";
import { point2ddi } from "./resource_list";
import { MOUNTED_TO } from "./constants";
import {
DropDownPair, PackedStepWithResourceIndex, UnpackedStepWithResourceIndex,
} from "./interfaces";
import { t } from "../../../i18next_wrapper";
import {
PLANT_STAGE_DDI_LOOKUP,
} from "../../../farm_designer/plants/edit_plant_status";
export const TOOL_MOUNT = (): DropDownItem => ({
label: t("Tool Mount"), value: "tool_mount"
});
const NOT_IN_USE = (): DropDownItem => ({ label: t("Not Mounted"), value: 0 });
export const DISMOUNTED = (): DropDownPair => ({
leftSide: TOOL_MOUNT(),
rightSide: NOT_IN_USE()
});
const DEFAULT_TOOL_NAME = () => t("Untitled Tool");
const mountedTo = (toolName = DEFAULT_TOOL_NAME()): DropDownItem =>
({ label: `${MOUNTED_TO()} ${toolName}`, value: "mounted" });
/** The user wants to change the `mounted_tool_id` of their Device. */
function mountTool(i: UnpackedStepWithResourceIndex): DropDownPair {
const { value } = i;
if (typeof value === "number" && value > 0) {
try { // Good tool id
const tool = findToolById(i.resourceIndex, value as number);
return { leftSide: TOOL_MOUNT(), rightSide: mountedTo(tool.body.name) };
} catch { // Bad tool ID or app still loading.
return { leftSide: TOOL_MOUNT(), rightSide: mountedTo("an unknown tool") };
}
} else {
// No tool id
return DISMOUNTED();
}
}
/** When we can't properly guess the correct way to to render the screen,
* possibly for legacy reasons or because the user wrote their CeleryScript by
* hand. */
function unknownOption(i: UnpackedStepWithResourceIndex): DropDownPair {
const { resource } = i;
const resource_type =
resource.kind == "resource" ? resource.args.resource_type : "variable";
const resource_id =
resource.kind == "resource" ? resource.args.resource_id : 0;
const { field, value } = i;
const leftLabel = `${resource_type} ${resource_id} ${field}`;
return {
leftSide: { label: leftLabel, value: field },
rightSide: { label: "" + value, value: "" + value }
};
}
/** The user wants to mark a the `plant_stage` attribute of a Plant resource. */
function plantStage(i: UnpackedStepWithResourceIndex): DropDownPair {
const { resource } = i;
const resource_type =
resource.kind == "resource" ? resource.args.resource_type : "";
const resource_id =
resource.kind == "resource" ? resource.args.resource_id : 0;
const { value } = i;
const leftSide = resource.kind == "resource"
? point2ddi(findPointerByTypeAndId(
i.resourceIndex, resource_type, resource_id).body)
: { label: "" + resource.args.label, value: "" + resource.args.label };
return {
leftSide,
rightSide: PLANT_STAGE_DDI_LOOKUP()["" + value]
|| { label: "" + value, value: "" + value },
};
}
/** We can guess how the "Mark As.." UI will be rendered (left and right side
* drop downs) based on the shape of the current step. There are several
* strategies and this function will dispatch the appropriate one. */
export function unpackStep(p: PackedStepWithResourceIndex): DropDownPair {
const { resource } = p.step.args;
const { label, value } = p.step.body?.[0]?.args || { label: "", value: "" };
const field = label;
const unpacked = { resourceIndex: p.resourceIndex, resource, field, value };
switch (field) {
case "mounted_tool_id": return mountTool(unpacked);
case "plant_stage": return plantStage(unpacked);
default: return unknownOption(unpacked);
}
}

View File

@ -7,6 +7,7 @@ import { TypePart } from "./tile_assertion/type_part";
import { LuaPart } from "./tile_assertion/lua_part";
import { SequencePart } from "./tile_assertion/sequence_part";
import { Assertion } from "farmbot/dist/corpus";
import { ToolTips } from "../../constants";
export interface AssertionStepProps extends StepParams {
currentStep: Assertion;
@ -24,7 +25,7 @@ export function TileAssertion(props: StepParams) {
return <StepWrapper>
<StepHeader
className={CLASS_NAME}
helpText={""}
helpText={ToolTips.ASSERTION}
currentSequence={p.currentSequence}
currentStep={p.currentStep}
dispatch={p.dispatch}

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { StepParams } from "../interfaces";
import { ToolTips } from "../../constants";
import { ToolTips, Content } from "../../constants";
import { StepWrapper, StepHeader, StepContent } from "../step_ui";
import { Col, Row } from "../../ui/index";
import { t } from "../../i18next_wrapper";
@ -21,7 +21,7 @@ export function TileEmergencyStop(props: StepParams) {
<Row>
<Col xs={12}>
<p>
{t("Unlocking a device requires user intervention.")}
{t(Content.ESTOP_STEP)}
</p>
</Col>
</Row>

View File

@ -14,6 +14,6 @@ export function TileIf(props: StepParams) {
confirmStepDeletion={props.confirmStepDeletion}
showPins={props.showPins} />;
} else {
return <p> Expected "_if" node</p>;
return <p>{"Expected `_if` node"}</p>;
}
}

View File

@ -76,8 +76,23 @@ describe("LHSOptions()", () => {
describe("<InnerIf />", () => {
it("renders", () => {
const wrapper = mount(<InnerIf {...fakeProps()} />);
["Variable", "Operator", "Value", "Then Execute", "Else Execute"].map(string =>
expect(wrapper.text()).toContain(string));
const inputs = wrapper.find("input");
const labels = wrapper.find("label");
const buttons = wrapper.find("button");
expect(inputs.length).toEqual(2);
expect(labels.length).toEqual(5);
expect(buttons.length).toEqual(4);
expect(inputs.first().props().placeholder).toEqual("If ...");
expect(labels.at(0).text()).toEqual("Variable");
expect(buttons.at(0).text()).toEqual("Pin 0");
expect(labels.at(1).text()).toEqual("Operator");
expect(buttons.at(1).text()).toEqual("is");
expect(labels.at(2).text()).toEqual("Value");
expect(inputs.at(1).props().value).toEqual(0);
expect(labels.at(3).text()).toEqual("Then Execute");
expect(buttons.at(2).text()).toEqual("None");
expect(labels.at(4).text()).toEqual("Else Execute");
expect(buttons.at(3).text()).toEqual("None");
});
it("is recursive", () => {

View File

@ -0,0 +1,17 @@
import * as React from "react";
import { StepParams } from "../interfaces";
import { MarkAs } from "./tile_mark_as/component";
export function TileMarkAs(props: StepParams) {
if (props.currentStep.kind === "update_resource") {
return <MarkAs
currentSequence={props.currentSequence}
currentStep={props.currentStep}
dispatch={props.dispatch}
index={props.index}
resources={props.resources}
confirmStepDeletion={props.confirmStepDeletion} />;
} else {
return <p>{"Expected `update_resource` node"}</p>;
}
}

View File

@ -0,0 +1,195 @@
const mockEditStep = jest.fn();
jest.mock("../../../../api/crud", () => ({ editStep: mockEditStep }));
import * as React from "react";
import { mount } from "enzyme";
import { MarkAs } from "../component";
import { MarkAsProps, UpdateResourceValue } from "../interfaces";
import { UpdateResource, Identifier, Resource, resource_type } from "farmbot";
import {
fakeSequence, fakePlant, fakeWeed,
} from "../../../../__test_support__/fake_state/resources";
import {
buildResourceIndex,
} from "../../../../__test_support__/resource_index_builder";
import { editStep } from "../../../../api/crud";
import { NOTHING_SELECTED } from "../../../locals_list/handle_select";
describe("<MarkAs/>", () => {
const plant = fakePlant();
plant.body.id = 1;
const weed = fakeWeed();
weed.body.id = 2;
const fakeProps = (): MarkAsProps => ({
currentSequence: fakeSequence(),
dispatch: jest.fn(),
index: 0,
currentStep: ResourceUpdateResourceStep("Device", 1, "mounted_tool_id", 0),
resources: buildResourceIndex([plant, weed]).index,
confirmStepDeletion: false
});
it("renders the basic parts", () => {
const wrapper = mount(<MarkAs {...fakeProps()} />);
["Mark", "Tool Mount", "field", "Mounted Tool", "as", "None"].map(string =>
expect(wrapper.text()).toContain(string));
});
it("resets step", () => {
const p = fakeProps();
const wrapper = mount<MarkAs>(<MarkAs {...p} />);
wrapper.instance().resetStep();
expect(editStep).toHaveBeenCalled();
mockEditStep.mock.calls[0][0].executor(p.currentStep);
expect(p.currentStep).toEqual({
kind: "update_resource",
args: { resource: NOTHING_SELECTED },
body: [],
});
});
it("edits step", () => {
const p = fakeProps();
const wrapper = mount<MarkAs>(<MarkAs {...p} />);
wrapper.setState({
resource: {
kind: "resource",
args: { resource_type: "Plant", resource_id: 1 }
},
fieldsAndValues: [{ field: "plant_stage", value: "planted" }],
});
wrapper.instance().commitSelection();
expect(editStep).toHaveBeenCalled();
mockEditStep.mock.calls[0][0].executor(p.currentStep);
expect(p.currentStep).toEqual(
ResourceUpdateResourceStep("Plant", 1, "plant_stage", "planted"));
});
it("doesn't edit step", () => {
const p = fakeProps();
const wrapper = mount<MarkAs>(<MarkAs {...p} />);
wrapper.setState({
resource: { kind: "nothing", args: {} },
fieldsAndValues: [{ field: "plant_stage", value: "planted" }],
});
wrapper.instance().commitSelection();
expect(editStep).toHaveBeenCalled();
mockEditStep.mock.calls[0][0].executor(p.currentStep);
expect(p.currentStep).toEqual(
ResourceUpdateResourceStep("Device", 1, "mounted_tool_id", 0));
});
it("doesn't save partial pairs", () => {
const p = fakeProps();
const wrapper = mount<MarkAs>(<MarkAs {...p} />);
wrapper.setState({
resource: {
kind: "resource",
args: { resource_type: "Plant", resource_id: 1 }
},
fieldsAndValues: [
{ field: "plant_stage", value: "planted" },
{ field: "x", value: 1 },
{ field: "y", value: undefined },
],
});
wrapper.instance().commitSelection();
expect(editStep).toHaveBeenCalled();
mockEditStep.mock.calls[0][0].executor(p.currentStep);
const expectedStep =
ResourceUpdateResourceStep("Plant", 1, "plant_stage", "planted");
expectedStep.body && expectedStep.body.push({
kind: "pair", args: { label: "x", value: 1 }
});
expect(p.currentStep).toEqual(expectedStep);
});
it("edits step to use identifier", () => {
const p = fakeProps();
const wrapper = mount<MarkAs>(<MarkAs {...p} />);
wrapper.setState({
resource: { kind: "identifier", args: { label: "var" } },
fieldsAndValues: [{ field: "plant_stage", value: "planted" }],
});
wrapper.instance().commitSelection();
expect(editStep).toHaveBeenCalled();
mockEditStep.mock.calls[0][0].executor(p.currentStep);
expect(p.currentStep).toEqual(
IdentifierUpdateResourceStep("var", "plant_stage", "planted"));
});
it("updates resource", () => {
const p = fakeProps();
const wrapper = mount<MarkAs>(<MarkAs {...p} />);
expect(wrapper.state().resource).toEqual(p.currentStep.args.resource);
expect(wrapper.state().fieldsAndValues)
.toEqual([{ field: "mounted_tool_id", value: 0 }]);
const newResource: Resource =
({ kind: "resource", args: { resource_type: "Weed", resource_id: 2 } });
wrapper.instance().updateResource(newResource);
expect(wrapper.state().resource).toEqual(newResource);
expect(wrapper.state().fieldsAndValues)
.toEqual([{ field: undefined, value: undefined }]);
});
it("updates field", () => {
const p = fakeProps();
p.currentStep.body = undefined;
const wrapper = mount<MarkAs>(<MarkAs {...p} />);
expect(wrapper.state().fieldsAndValues)
.toEqual([{ field: undefined, value: undefined }]);
wrapper.instance().updateFieldOrValue(0)({ field: "plant_stage" });
expect(wrapper.state().fieldsAndValues)
.toEqual([{ field: "plant_stage", value: undefined }]);
expect(p.dispatch).toHaveBeenCalled();
});
it("updates value", () => {
const p = fakeProps();
p.currentStep.body && p.currentStep.body.push({
kind: "pair", args: { label: "plant_stage", value: "planned" }
});
const wrapper = mount<MarkAs>(<MarkAs {...p} />);
expect(wrapper.state().fieldsAndValues).toEqual([
{ field: "mounted_tool_id", value: 0 },
{ field: "plant_stage", value: "planned" },
]);
const callback = jest.fn();
wrapper.instance().updateFieldOrValue(1)({ value: "planted" }, callback);
expect(wrapper.state().fieldsAndValues).toEqual([
{ field: "mounted_tool_id", value: 0 },
{ field: "plant_stage", value: "planted" },
]);
expect(callback).toHaveBeenCalled();
expect(p.dispatch).not.toHaveBeenCalled();
});
});
const BaseUpdateResourceStep =
(resource: Resource | Identifier,
field: string,
value: UpdateResourceValue,
): UpdateResource => ({
kind: "update_resource",
args: { resource },
body: [{ kind: "pair", args: { label: field, value } }],
});
const ResourceUpdateResourceStep = (
resourceType: resource_type,
resourceId: number,
field: string,
value: UpdateResourceValue,
): UpdateResource =>
BaseUpdateResourceStep({
kind: "resource",
args: { resource_id: resourceId, resource_type: resourceType }
}, field, value);
const IdentifierUpdateResourceStep = (
label: string,
field: string,
value: UpdateResourceValue,
): UpdateResource =>
BaseUpdateResourceStep({ kind: "identifier", args: { label } }, field, value);

View File

@ -0,0 +1,172 @@
import * as React from "react";
import { mount, shallow } from "enzyme";
import { FieldSelection, isCustomMetaField } from "../field_selection";
import { FieldSelectionProps } from "../interfaces";
import {
buildResourceIndex,
} from "../../../../__test_support__/resource_index_builder";
describe("<FieldSelection />", () => {
const fakeProps = (): FieldSelectionProps => ({
resource: { kind: "nothing", args: {} },
field: undefined,
resources: buildResourceIndex().index,
update: jest.fn(),
});
it("renders disabled none field", () => {
const p = fakeProps();
p.field = undefined;
const wrapper = mount(<FieldSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual([]);
expect(wrapper.text()).toContain("field");
expect(wrapper.text()).toContain("Select one");
expect(wrapper.find(".reset-custom-field").length).toEqual(0);
});
it("renders none field", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "Plant", resource_id: 1 }
};
p.field = undefined;
const wrapper = mount(<FieldSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "Plant stage", value: "plant_stage" },
{ label: "Custom Meta Field", value: "" },
]);
expect(wrapper.text()).toContain("field");
expect(wrapper.text()).toContain("Select one");
expect(wrapper.find(".reset-custom-field").length).toEqual(0);
});
it("renders custom meta field", () => {
const p = fakeProps();
p.field = "custom";
const wrapper = mount(<FieldSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(0);
expect(wrapper.text()).toContain("field");
expect(wrapper.find("input").props().value).toEqual("custom");
expect(wrapper.find(".reset-custom-field").length).toEqual(1);
});
it("changes custom meta field", () => {
const p = fakeProps();
p.field = "custom_field";
const wrapper = mount(<FieldSelection {...p} />);
const input = shallow(wrapper.find("input").getElement());
input.simulate("change", { currentTarget: { value: "1" } });
input.simulate("blur", { currentTarget: { value: "1" } });
expect(p.update).toHaveBeenCalledWith({ field: "1" });
});
it("clears custom meta field", () => {
const p = fakeProps();
p.field = "custom_field";
const wrapper = mount(<FieldSelection {...p} />);
wrapper.find(".reset-custom-field").simulate("click");
expect(p.update).toHaveBeenCalledWith({
field: undefined, value: undefined
});
});
it("renders field list for identifier", () => {
const p = fakeProps();
p.resource = { kind: "identifier", args: { label: "var" } };
p.field = "plant_stage";
const wrapper = mount(<FieldSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "Status", value: "plant_stage" },
{ label: "Custom Meta Field", value: "" },
]);
expect(wrapper.text()).toContain("field");
expect(wrapper.text()).toContain("Status");
expect(wrapper.find(".reset-custom-field").length).toEqual(0);
});
it("renders known weed field", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "Weed", resource_id: 1 }
};
p.field = "plant_stage";
const wrapper = mount(<FieldSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "Weed status", value: "plant_stage" },
{ label: "Custom Meta Field", value: "" },
]);
expect(wrapper.text()).toContain("field");
expect(wrapper.text()).toContain("Weed status");
expect(wrapper.find(".reset-custom-field").length).toEqual(0);
});
it("renders known point field", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "GenericPointer", resource_id: 3 }
};
p.field = "plant_stage";
const wrapper = mount(<FieldSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "Status", value: "plant_stage" },
{ label: "Custom Meta Field", value: "" },
]);
expect(wrapper.text()).toContain("field");
expect(wrapper.text()).toContain("Status");
expect(wrapper.find(".reset-custom-field").length).toEqual(0);
});
it("changes known weed field", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "Weed", resource_id: 1 }
};
p.field = undefined;
const wrapper = mount(<FieldSelection {...p} />);
const select = shallow(<div>{wrapper.find("FBSelect").getElement()}</div>);
select.find("FBSelect").simulate("change", {
label: "", value: "plant_stage"
});
expect(p.update).toHaveBeenCalledWith({ field: "plant_stage" });
});
it("renders known device field", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "Device", resource_id: 1 }
};
p.field = "mounted_tool_id";
const wrapper = mount(<FieldSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "Mounted Tool", value: "mounted_tool_id" },
{ label: "Custom Meta Field", value: "" },
]);
expect(wrapper.text()).toContain("field");
expect(wrapper.text()).toContain("Mounted Tool");
expect(wrapper.find(".reset-custom-field").length).toEqual(0);
});
});
describe("isCustomMetaField()", () => {
it("is custom meta field", () => {
expect(isCustomMetaField("")).toBeTruthy();
expect(isCustomMetaField("custom")).toBeTruthy();
});
it("is not custom meta field", () => {
expect(isCustomMetaField(undefined)).toBeFalsy();
expect(isCustomMetaField("plant_stage")).toBeFalsy();
expect(isCustomMetaField("mounted_tool_id")).toBeFalsy();
});
});

View File

@ -0,0 +1,101 @@
import * as React from "react";
import { mount, shallow } from "enzyme";
import { ResourceSelection } from "../resource_selection";
import { ResourceSelectionProps } from "../interfaces";
import {
buildResourceIndex, fakeDevice,
} from "../../../../__test_support__/resource_index_builder";
import { fakePlant } from "../../../../__test_support__/fake_state/resources";
describe("<ResourceSelection />", () => {
const plant = fakePlant();
plant.body.id = 1;
const fakeProps = (): ResourceSelectionProps => ({
resource: { kind: "nothing", args: {} },
resources: buildResourceIndex([plant]).index,
updateResource: jest.fn(),
sequenceUuid: "fake Sequence UUID",
});
it("renders", () => {
const p = fakeProps();
const device = fakeDevice();
device.body.id = 1;
p.resources = buildResourceIndex([device]).index;
const wrapper = mount(<ResourceSelection {...p} />);
expect(wrapper.text()).toContain("Mark");
expect(wrapper.text()).toContain("Select one");
});
it("renders resource", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "Plant", resource_id: 1 }
};
const wrapper = mount(<ResourceSelection {...p} />);
expect(wrapper.text()).toContain("Mark");
expect(wrapper.text()).toContain("Strawberry plant 1 (100, 200, 0)");
});
it("renders identifier", () => {
const p = fakeProps();
p.resource = {
kind: "identifier",
args: { label: "var" }
};
const wrapper = mount(<ResourceSelection {...p} />);
expect(wrapper.text()).toContain("Mark");
expect(wrapper.text()).toContain("Variable - Add new");
});
it("renders identifier with label", () => {
const p = fakeProps();
p.resources.sequenceMetas["fake uuid"] = {
parent: {
celeryNode: {
kind: "parameter_declaration", args: {
label: "parent", default_value: {
kind: "coordinate", args: { x: 0, y: 0, z: 0 }
}
}
},
dropdown: { label: "Parent", value: "parent" },
vector: undefined,
}
};
p.sequenceUuid = "fake uuid";
p.resource = {
kind: "identifier",
args: { label: "parent" }
};
const wrapper = mount(<ResourceSelection {...p} />);
expect(wrapper.text()).toContain("Mark");
expect(wrapper.text()).toContain("Variable - Parent");
});
it("changes resource", () => {
const p = fakeProps();
const wrapper = shallow(<ResourceSelection {...p} />);
wrapper.find("FBSelect").simulate("change", {
label: "", value: "1", headingId: "Plant",
});
expect(p.updateResource).toHaveBeenCalledWith({
kind: "resource",
args: { resource_type: "Plant", resource_id: 1 }
});
});
it("changes resource to identifier", () => {
const p = fakeProps();
const wrapper = shallow(<ResourceSelection {...p} />);
wrapper.find("FBSelect").simulate("change", {
label: "Variable", value: "parent", headingId: "Identifier",
});
expect(p.updateResource).toHaveBeenCalledWith({
kind: "identifier",
args: { label: "parent" }
});
});
});

View File

@ -0,0 +1,256 @@
let mockDev = false;
jest.mock("../../../../account/dev/dev_support", () => ({
DevSettings: { futureFeaturesEnabled: () => mockDev }
}));
import * as React from "react";
import { mount, shallow } from "enzyme";
import { ValueSelection } from "../value_selection";
import { ValueSelectionProps } from "../interfaces";
import {
buildResourceIndex,
} from "../../../../__test_support__/resource_index_builder";
import {
PLANT_STAGE_LIST,
} from "../../../../farm_designer/plants/edit_plant_status";
import { fakeTool } from "../../../../__test_support__/fake_state/resources";
import { resource_type, Resource } from "farmbot";
describe("<ValueSelection />", () => {
const fakeProps = (): ValueSelectionProps => ({
resource: { kind: "nothing", args: {} },
field: undefined,
value: undefined,
resources: buildResourceIndex().index,
update: jest.fn(),
add: jest.fn(),
commitSelection: jest.fn(),
});
it("renders none value", () => {
const p = fakeProps();
p.field = undefined;
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("Select one");
});
it("renders custom meta value", () => {
const p = fakeProps();
p.field = "custom_field";
p.value = "custom_value";
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(0);
expect(wrapper.text()).toContain("as");
expect(wrapper.find("input").props().value).toEqual("custom_value");
});
it("renders missing custom meta value", () => {
const p = fakeProps();
p.field = "custom_field";
p.value = undefined;
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(0);
expect(wrapper.text()).toContain("as");
expect(wrapper.find("input").props().value).toEqual("");
});
it("changes custom meta value", () => {
const p = fakeProps();
p.field = "custom_field";
p.value = "custom_value";
const wrapper = mount(<ValueSelection {...p} />);
const input = shallow(wrapper.find("input").getElement());
input.simulate("change", { currentTarget: { value: "1" } });
input.simulate("blur", { currentTarget: { value: "1" } });
expect(p.update).toHaveBeenCalledWith({ value: "1" },
expect.any(Function));
});
it("adds row", () => {
const p = fakeProps();
const wrapper = mount(<ValueSelection {...p} />);
wrapper.find("label").simulate("click");
expect(p.add).not.toHaveBeenCalled();
mockDev = true;
wrapper.find("label").simulate("click");
expect(p.add).toHaveBeenCalledWith({});
});
it("renders known plant value", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "Plant", resource_id: 1 }
};
p.field = "plant_stage";
p.value = "planted";
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual(PLANT_STAGE_LIST());
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("Planted");
});
it("renders plant value", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "Plant", resource_id: 1 }
};
p.field = "plant_stage";
p.value = "other";
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual(PLANT_STAGE_LIST());
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("other");
});
it("renders known weed value", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "Weed", resource_id: 1 }
};
p.field = "plant_stage";
p.value = "removed";
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "Removed", value: "removed" },
]);
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("Removed");
});
it("changes known weed value", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "Weed", resource_id: 1 }
};
p.field = "plant_stage";
p.value = undefined;
const wrapper = mount(<ValueSelection {...p} />);
const select = shallow(<div>{wrapper.find("FBSelect").getElement()}</div>);
select.find("FBSelect").simulate("change", {
label: "", value: "removed"
});
expect(p.update).toHaveBeenCalledWith({ value: "removed" },
expect.any(Function));
});
it("renders known point value", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "GenericPointer", resource_id: 1 }
};
p.field = "plant_stage";
p.value = "removed";
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "Removed", value: "removed" },
]);
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("Removed");
});
it("renders other value", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "Other" as resource_type, resource_id: 1 }
};
p.field = "plant_stage";
p.value = "removed";
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual(PLANT_STAGE_LIST());
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("Removed");
});
const TOOL_OPTIONS = [
{ label: "None", value: 0 },
{ label: "Trench Digging Tool", value: 14 },
{ label: "Berry Picking Tool", value: 15 },
];
const DeviceResource: Resource = {
kind: "resource",
args: { resource_type: "Device", resource_id: 1 }
};
it("renders known tool value: not mounted", () => {
const p = fakeProps();
p.resource = DeviceResource;
p.field = "mounted_tool_id";
p.value = 0;
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual(TOOL_OPTIONS);
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("None");
});
it("renders known tool value: mounted", () => {
const p = fakeProps();
p.resource = DeviceResource;
p.field = "mounted_tool_id";
p.value = 14;
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual(TOOL_OPTIONS);
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("Trench Digging Tool");
});
it("renders known tool value: unknown tool", () => {
const p = fakeProps();
p.resource = DeviceResource;
p.field = "mounted_tool_id";
p.value = 123;
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual(TOOL_OPTIONS);
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("Unknown tool");
});
it("renders known tool value: untitled tool", () => {
const p = fakeProps();
p.resource = DeviceResource;
p.field = "mounted_tool_id";
p.value = 1;
const tool = fakeTool();
tool.body.id = 1;
tool.body.name = undefined;
p.resources = buildResourceIndex([tool]).index;
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "None", value: 0 },
{ label: "Untitled tool", value: 1 },
]);
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("Untitled tool");
});
it("renders known identifier value", () => {
const p = fakeProps();
p.resource = {
kind: "identifier", args: { label: "var" }
};
p.field = "plant_stage";
p.value = "planted";
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual(PLANT_STAGE_LIST());
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("Planted");
});
});

View File

@ -0,0 +1,119 @@
import * as React from "react";
import { editStep } from "../../../api/crud";
import { Row, Col } from "../../../ui";
import { StepWrapper, StepHeader, StepContent } from "../../step_ui/index";
import { ToolTips } from "../../../constants";
import { UpdateResource, Resource, Identifier } from "farmbot";
import { MarkAsState, MarkAsProps, FieldAndValue } from "./interfaces";
import { ResourceSelection } from "./resource_selection";
import { FieldSelection } from "./field_selection";
import { ValueSelection } from "./value_selection";
import { isUndefined } from "lodash";
import { NOTHING_SELECTED } from "../../locals_list/handle_select";
export class MarkAs extends React.Component<MarkAsProps, MarkAsState> {
state: MarkAsState = {
resource: this.step.args.resource,
fieldsAndValues: this.step.body?.length
? this.step.body.map(pair =>
({ field: pair.args.label, value: pair.args.value }))
: [{ field: undefined, value: undefined }],
};
get step() { return this.props.currentStep; }
editStep = (executor: (s: UpdateResource) => void) =>
this.props.dispatch(editStep({
step: this.step,
index: this.props.index,
sequence: this.props.currentSequence,
executor,
}));
resetStep = () =>
this.editStep(s => {
s.args = { resource: NOTHING_SELECTED };
s.body = [];
});
commitSelection = () => {
const { resource, fieldsAndValues } = this.state;
this.editStep(s => {
if (fieldsAndValues.length > 0 && resource.kind != "nothing") {
s.args = { resource };
s.body = [];
fieldsAndValues.map(({ field, value }) => {
if (s.body && !isUndefined(field) && !isUndefined(value)) {
s.body.push({ kind: "pair", args: { label: field, value: value } });
}
});
}
});
};
updateResource = (resource: Resource | Identifier) => {
this.setState({
resource,
fieldsAndValues: [{ field: undefined, value: undefined }],
});
this.resetStep();
};
updateFieldOrValue = (index: number) =>
(update: Partial<FieldAndValue>, callback?: () => void) => {
const { fieldsAndValues } = this.state;
const old = fieldsAndValues[index];
fieldsAndValues[index] = { ...old, ...update };
this.setState({ fieldsAndValues: fieldsAndValues }, callback);
if (isUndefined(update.value) && fieldsAndValues.length < 2) {
this.resetStep();
}
};
render() {
const commonProps = {
key: JSON.stringify(this.state)
+ JSON.stringify(this.props.currentSequence.body.args.locals),
resource: this.state.resource,
resources: this.props.resources,
};
const className = "update-resource-step";
return <StepWrapper>
<StepHeader
className={className}
helpText={ToolTips.MARK_AS}
currentSequence={this.props.currentSequence}
currentStep={this.props.currentStep}
dispatch={this.props.dispatch}
index={this.props.index}
confirmStepDeletion={this.props.confirmStepDeletion} />
<StepContent className={className}>
<Row>
<Col xs={12}>
<ResourceSelection {...commonProps}
sequenceUuid={this.props.currentSequence.uuid}
updateResource={this.updateResource} />
</Col>
</Row>
{this.state.fieldsAndValues.map((fieldAndValue, index) =>
<div className={"update-resource-pair"} key={index}>
<Row>
<Col xs={6}>
<FieldSelection {...commonProps}
field={fieldAndValue.field}
update={this.updateFieldOrValue(index)} />
</Col>
<Col xs={6}>
<ValueSelection {...commonProps}
field={fieldAndValue.field}
value={fieldAndValue.value}
update={this.updateFieldOrValue(index)}
add={this.updateFieldOrValue(this.state.fieldsAndValues.length)}
commitSelection={this.commitSelection} />
</Col>
</Row>
</div>)}
</StepContent>
</StepWrapper>;
}
}

View File

@ -0,0 +1,99 @@
import * as React from "react";
import { t } from "../../../i18next_wrapper";
import { FBSelect, DropDownItem, BlurableInput } from "../../../ui";
import { Resource, Identifier, Nothing } from "farmbot";
import { isUndefined } from "lodash";
import { FieldSelectionProps, CustomFieldSelectionProps } from "./interfaces";
export const FieldSelection = (props: FieldSelectionProps) =>
<div className={"update-resource-step-field"}>
<label>{t("field")}</label>
{(isCustomMetaField(props.field) && !isUndefined(props.field))
? <CustomMetaField {...props} field={props.field} />
: <KnownFieldSelection {...props} />}
</div>;
const KnownFieldSelection = (props: FieldSelectionProps) =>
<FBSelect
extraClass={props.resource.kind == "nothing" ? "disabled" : ""}
list={props.resource.kind == "nothing"
? []
: fieldList(props.resource)
.concat([{ label: t("Custom Meta Field"), value: "" }])}
onChange={ddi => props.update({
field: "" + ddi.value,
value: undefined
})}
allowEmpty={false}
selectedItem={getSelectedField(
props.resource, knownField(props.field))} />;
const CustomMetaField = (props: CustomFieldSelectionProps) =>
<div className="custom-meta-field">
<BlurableInput type="text" name="field"
onCommit={e => props.update({
field: e.currentTarget.value,
value: undefined
})}
allowEmpty={true}
value={props.field} />
<i className={"reset-custom-field fa fa-undo"}
title={t("reset")}
onClick={() => props.update({ field: undefined, value: undefined })} />
</div>;
export enum KnownField {
plant_stage = "plant_stage",
mounted_tool_id = "mounted_tool_id",
}
const isKnownField = (x: string | undefined): x is KnownField =>
!!(x && Object.keys(KnownField).includes(x));
export const knownField =
(field: string | undefined): KnownField | undefined =>
isKnownField(field) ? field : undefined;
export const isCustomMetaField = (field: string | undefined): boolean =>
!(isUndefined(field) || knownField(field));
const fieldList = (resource: Resource | Identifier) => {
if (resource.kind == "identifier") {
return [{ label: t("Status"), value: "plant_stage" }];
}
switch (resource.args.resource_type) {
case "Device":
return [{ label: t("Mounted Tool"), value: "mounted_tool_id" }];
case "Weed":
return [{ label: t("Weed status"), value: "plant_stage" }];
case "GenericPointer":
return [{ label: t("Status"), value: "plant_stage" }];
default:
return [{ label: t("Plant stage"), value: "plant_stage" }];
}
};
const getSelectedField = (
resource: Resource | Identifier | Nothing,
field: KnownField | undefined,
): DropDownItem => {
if (isUndefined(field) || resource.kind == "nothing") {
return { label: t("Select one"), value: "" };
}
if (resource.kind == "identifier") {
return { label: t("Status"), value: "plant_stage" };
}
const resourceType = resource.args.resource_type;
switch (field) {
case KnownField.mounted_tool_id:
return { label: t("Mounted Tool"), value: "tool" };
case KnownField.plant_stage:
if (resourceType == "Weed") {
return { label: t("Weed status"), value: "plant_stage" };
}
if (resourceType == "GenericPointer") {
return { label: t("Status"), value: "plant_stage" };
}
return { label: t("Plant stage"), value: "plant_stage" };
}
};

View File

@ -0,0 +1,64 @@
import { ResourceIndex, UUID } from "../../../resources/interfaces";
import {
UpdateResource, TaggedSequence, Resource, Identifier, Nothing, Pair,
} from "farmbot";
import { KnownField } from "./field_selection";
export interface MarkAsProps {
currentSequence: TaggedSequence;
currentStep: UpdateResource;
dispatch: Function;
index: number;
resources: ResourceIndex;
confirmStepDeletion: boolean;
}
export type UpdateResourceValue = Pair["args"]["value"];
export interface FieldAndValue {
field: string | undefined;
value: UpdateResourceValue | undefined;
}
export interface MarkAsState {
resource: Resource | Identifier | Nothing;
fieldsAndValues: FieldAndValue[];
}
export interface GetSelectedValueProps {
resource: Resource | Identifier | Nothing;
field: KnownField | undefined;
value: UpdateResourceValue | undefined;
resourceIndex: ResourceIndex;
}
interface SelectionPropsBase {
resource: Resource | Identifier | Nothing;
resources: ResourceIndex;
}
export interface ResourceSelectionProps extends SelectionPropsBase {
updateResource(resource: Resource | Identifier): void;
sequenceUuid: UUID;
}
type UpdateFieldOrValue =
(update: Partial<FieldAndValue>, callback?: () => void) => void;
export interface FieldSelectionProps extends SelectionPropsBase {
field: string | undefined;
update: UpdateFieldOrValue;
}
export interface CustomFieldSelectionProps extends SelectionPropsBase {
field: string;
update: UpdateFieldOrValue;
}
export interface ValueSelectionProps extends SelectionPropsBase {
field: string | undefined;
value: UpdateResourceValue | undefined;
update: UpdateFieldOrValue;
add: UpdateFieldOrValue;
commitSelection(): void;
}

View File

@ -0,0 +1,91 @@
import * as React from "react";
import { t } from "../../../i18next_wrapper";
import { FBSelect } from "../../../ui";
import {
resource_type as RESOURCE_TYPE, Identifier, Resource, Nothing,
} from "farmbot";
import { ResourceSelectionProps } from "./interfaces";
import { ResourceIndex, UUID } from "../../../resources/interfaces";
import { DropDownItem } from "../../../ui/fb_select";
import {
selectAllPoints, maybeGetDevice, findPointerByTypeAndId,
} from "../../../resources/selectors";
import { formatPoint } from "../../locals_list/location_form_list";
import {
maybeFindVariable, SequenceMeta,
} from "../../../resources/sequence_meta";
export const ResourceSelection = (props: ResourceSelectionProps) =>
<div className={"update-resource-step-resource"}>
<label>{t("Mark")}</label>
<FBSelect
list={resourceList(props.resources, props.sequenceUuid)}
onChange={ddi => props.updateResource(prepareResource(ddi))}
selectedItem={getSelectedResource(
props.resource, props.resources, props.sequenceUuid)} />
</div>;
const prepareResource = (ddi: DropDownItem): Resource | Identifier => {
switch (ddi.headingId) {
case "Identifier":
return { kind: "identifier", args: { label: "" + ddi.value } };
default:
return {
kind: "resource",
args: {
resource_type: ddi.headingId as RESOURCE_TYPE,
resource_id: parseInt("" + ddi.value)
}
};
}
};
const resourceList =
(resources: ResourceIndex, sequenceUuid: UUID): DropDownItem[] => {
const deviceId = maybeGetDevice(resources)?.body.id || 0;
const points = selectAllPoints(resources).filter(p => !!p.body.id);
const mapPoints = points.filter(p => p.body.pointer_type == "GenericPointer");
const weeds = points.filter(p => p.body.pointer_type == "Weed");
const plants = points.filter(p => p.body.pointer_type == "Plant");
const headingCommon = { heading: true, value: 0 };
const varLabel = resourceVariableLabel(maybeFindVariable(
"parent", resources, sequenceUuid));
return [
{ headingId: "Identifier", label: varLabel, value: "parent" },
{ headingId: "Device", label: t("Device"), ...headingCommon },
{ headingId: "Device", label: t("Tool Mount"), value: deviceId },
{ headingId: "Plant", label: t("Plants"), ...headingCommon },
...plants.map(formatPoint),
{ headingId: "GenericPointer", label: t("Points"), ...headingCommon },
...mapPoints.map(formatPoint),
{ headingId: "Weed", label: t("Weeds"), ...headingCommon },
...weeds.map(formatPoint),
];
};
const getSelectedResource = (
resource: Resource | Identifier | Nothing,
resources: ResourceIndex,
sequenceUuid: UUID,
): DropDownItem => {
switch (resource.kind) {
case "resource":
const { resource_type, resource_id } = resource.args;
if (resource_type == "Device") {
return { label: t("Tool Mount"), value: resource_id };
}
return formatPoint(
findPointerByTypeAndId(resources, resource_type, resource_id));
case "identifier":
const variable =
maybeFindVariable(resource.args.label, resources, sequenceUuid);
return {
label: resourceVariableLabel(variable),
value: resource.args.label,
};
case "nothing": return { label: t("Select one"), value: "" };
}
};
const resourceVariableLabel = (variable: SequenceMeta | undefined) =>
`${t("Variable")} - ${variable?.dropdown.label || t("Add new")}`;

View File

@ -0,0 +1,93 @@
import * as React from "react";
import { t } from "../../../i18next_wrapper";
import { FBSelect, BlurableInput } from "../../../ui";
import { isUndefined } from "lodash";
import { ValueSelectionProps, GetSelectedValueProps } from "./interfaces";
import { Identifier, Resource } from "farmbot";
import { DropDownItem } from "../../../ui";
import { ResourceIndex } from "../../../resources/interfaces";
import { selectAllTools, maybeFindToolById } from "../../../resources/selectors";
import {
PLANT_STAGE_LIST, PLANT_STAGE_DDI_LOOKUP,
} from "../../../farm_designer/plants/edit_plant_status";
import { isCustomMetaField, KnownField, knownField } from "./field_selection";
import { DevSettings } from "../../../account/dev/dev_support";
export const ValueSelection = (props: ValueSelectionProps) =>
<div className={"update-resource-step-value"}>
<label onClick={() => DevSettings.futureFeaturesEnabled() && props.add({})}>
{t("as")}
</label>
{isCustomMetaField(props.field)
? <CustomMetaValue {...props} />
: <KnownValue {...props} />}
</div>;
const KnownValue = (props: ValueSelectionProps) =>
<FBSelect
extraClass={isUndefined(props.field) ? "disabled" : ""}
list={props.resource.kind == "nothing"
? []
: valuesList(props.resource, props.resources)}
onChange={ddi => {
props.update({ value: ddi.value },
props.commitSelection);
}}
selectedItem={getSelectedValue({
resourceIndex: props.resources,
resource: props.resource,
field: knownField(props.field),
value: props.value,
})} />;
const CustomMetaValue = (props: ValueSelectionProps) =>
<div className="custom-meta-field">
<BlurableInput type="text" name="value"
value={isUndefined(props.value) ? "" : "" + props.value}
onCommit={e => {
props.update({ value: e.currentTarget.value },
props.commitSelection);
}} />
</div>;
const valuesList = (
resource: Resource | Identifier,
resources: ResourceIndex): DropDownItem[] => {
const stepResourceType =
resource.kind == "identifier" ? undefined : resource.args.resource_type;
switch (stepResourceType) {
case "Device": return [
{ label: t("None"), value: 0 },
...selectAllTools(resources).filter(x => !!x.body.id)
.map(x => ({ toolName: x.body.name, toolId: x.body.id }))
.map(({ toolName, toolId }:
{ toolName: string | undefined, toolId: number }) =>
({ label: toolName || t("Untitled tool"), value: toolId })),
];
case "GenericPointer": return [{ label: t("Removed"), value: "removed" }];
case "Weed": return [{ label: t("Removed"), value: "removed" }];
case "Plant":
default: return PLANT_STAGE_LIST();
}
};
const getSelectedValue = (props: GetSelectedValueProps): DropDownItem => {
if (isUndefined(props.field) || isUndefined(props.value)
|| props.resource.kind == "nothing") {
return { label: t("Select one"), value: "" };
}
switch (props.field) {
case KnownField.mounted_tool_id:
const toolId = parseInt("" + props.value);
if (toolId == 0) { return { label: t("None"), value: 0 }; }
const tool = maybeFindToolById(props.resourceIndex, toolId);
if (!tool) { return { label: t("Unknown tool"), value: toolId }; }
return {
label: tool.body.name || t("Untitled tool"),
value: toolId
};
case KnownField.plant_stage:
return PLANT_STAGE_DDI_LOOKUP()["" + props.value]
|| { label: "" + props.value, value: "" + props.value };
}
};

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { StepParams } from "../interfaces";
import { ToolTips } from "../../constants";
import { Content } from "../../constants";
import { StepWrapper, StepHeader, StepContent } from "../step_ui/index";
import { t } from "../../i18next_wrapper";
import { ALLOWED_PACKAGES, SequenceBodyItem, Reboot } from "farmbot";
@ -44,7 +44,7 @@ export function TileReboot(props: StepParams) {
return <StepWrapper>
<StepHeader
className={className}
helpText={ToolTips.REBOOT}
helpText={Content.RESTART_FARMBOT}
currentSequence={currentSequence}
currentStep={currentStep}
dispatch={dispatch}
@ -52,7 +52,7 @@ export function TileReboot(props: StepParams) {
confirmStepDeletion={props.confirmStepDeletion} />
<StepContent className={className}>
<p>
{t(ToolTips.REBOOT)}
{t(Content.REBOOT_STEP)}
</p>
{/* <StepRadio
choices={Object.keys(PACKAGE_CHOICES())}

View File

@ -0,0 +1,23 @@
import * as React from "react";
import { StepParams } from "../interfaces";
import { Content } from "../../constants";
import { StepWrapper, StepHeader, StepContent } from "../step_ui/index";
import { t } from "../../i18next_wrapper";
export function TileShutdown(props: StepParams) {
const { dispatch, currentStep, index, currentSequence } = props;
const className = "shutdown-step";
return <StepWrapper>
<StepHeader
className={className}
helpText={Content.SHUTDOWN_FARMBOT}
currentSequence={currentSequence}
currentStep={currentStep}
dispatch={dispatch}
index={index}
confirmStepDeletion={props.confirmStepDeletion} />
<StepContent className={className}>
<p>{t(Content.SHUTDOWN_STEP)}</p>
</StepContent>
</StepWrapper>;
}

View File

@ -104,10 +104,19 @@ export enum FbosVersionFallback {
NULL = "0.0.0",
}
const fallbackData: MinOsFeatureLookup = {
[Feature.api_farmware_env]: "8.0.0",
[Feature.api_farmware_installations]: "8.0.0",
[Feature.criteria_groups]: "9.2.2",
[Feature.update_resource]: MinVersionOverride.NEVER,
[Feature.boot_sequence]: MinVersionOverride.NEVER,
};
/**
* Determine whether a feature should be displayed based on
* the user's current FBOS version. Min FBOS version feature data is pulled
* from an external source to allow App and FBOS development flexibility.
* Device-less accounts can use features compatible with supported versions.
*
* @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"}
@ -120,7 +129,7 @@ export function createShouldDisplayFn(
const fallback = globalConfig.FBOS_END_OF_LIFE_VERSION ||
FbosVersionFallback.NULL;
const target = override || current || fallback;
const table = lookupData || {};
const table = lookupData || fallbackData;
const min = table[feature] || MinVersionOverride.NEVER;
switch (semverCompare(target, min)) {
case SemverResult.LEFT_IS_GREATER:

View File

@ -44,7 +44,9 @@ end
def get_base_branch(pull_data)
current_branch = BASE_BRANCHES.empty? ||
BASE_BRANCHES.include?(CURRENT_BRANCH) ? CURRENT_BRANCH : "staging"
pull_data.dig("base", "ref") || current_branch
provided_base_branch =
CURRENT_BRANCH.start_with?("master-hotfix/") ? "master" : nil;
pull_data.dig("base", "ref") || provided_base_branch || current_branch
end
# Gather relevant coverage data.

View File

@ -25,35 +25,35 @@
"license": "MIT",
"dependencies": {
"@babel/core": "7.9.0",
"@blueprintjs/core": "3.25.0",
"@blueprintjs/datetime": "3.16.0",
"@blueprintjs/select": "3.12.1",
"@blueprintjs/core": "3.26.0",
"@blueprintjs/datetime": "3.16.1",
"@blueprintjs/select": "3.12.2",
"@types/enzyme": "3.10.5",
"@types/jest": "25.2.1",
"@types/lodash": "4.14.149",
"@types/markdown-it": "10.0.0",
"@types/lodash": "4.14.150",
"@types/markdown-it": "10.0.1",
"@types/moxios": "0.4.9",
"@types/node": "13.11.1",
"@types/node": "13.13.4",
"@types/promise-timeout": "1.3.0",
"@types/react": "16.9.34",
"@types/react-color": "3.0.1",
"@types/react-dom": "16.9.6",
"@types/react-dom": "16.9.7",
"@types/react-redux": "7.1.7",
"axios": "0.19.2",
"boxed_value": "1.0.0",
"browser-speech": "1.1.1",
"coveralls": "3.0.11",
"coveralls": "3.1.0",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.2",
"farmbot": "10.0.0-rc1",
"i18next": "19.4.1",
"i18next": "19.4.4",
"install": "0.13.0",
"lodash": "4.17.15",
"markdown-it": "10.0.0",
"markdown-it-emoji": "1.4.0",
"moment": "2.24.0",
"moxios": "0.4.0",
"mqtt": "3.0.0",
"mqtt": "4.0.0",
"npm": "6.14.4",
"parcel-bundler": "1.12.4",
"promise-timeout": "1.3.0",
@ -71,19 +71,19 @@
"redux-thunk": "2.3.0",
"sass-lint": "1.13.1",
"takeme": "0.11.3",
"ts-jest": "25.3.1",
"ts-jest": "25.4.0",
"ts-lint": "4.5.1",
"tslint": "6.1.1",
"tslint": "6.1.2",
"typescript": "3.8.3",
"which": "2.0.2"
},
"devDependencies": {
"jest": "25.3.0",
"jest-cli": "25.3.0",
"jest": "25.4.0",
"jest-cli": "25.4.0",
"jest-junit": "10.0.0",
"jest-skipped-reporter": "0.0.5",
"jshint": "2.11.0",
"madge": "3.8.0",
"sass": "1.26.3"
"sass": "1.26.5"
}
}

View File

@ -9,8 +9,8 @@ describe Api::PointsController do
end
let(:auth_token) do
params = { email: user.email,
password: "password123",
fbos_version: Gem::Version.new("999.9.9") }
password: "password123",
fbos_version: Gem::Version.new("999.9.9") }
Auth::CreateToken.run!(params)[:token].encoded
end
@ -130,6 +130,7 @@ describe Api::PointsController do
get :index
expect(response.status).to eq(200)
expect(json.length).to eq(3)
json.map { |json| expect(json[:created_at]).to eq(json[:planted_at]) }
end
it "lists all tool slots" do
Point.destroy_all