Merge pull request #662 from gabrielburnworth/master

New Features
pull/663/head
Rick Carlino 2018-02-15 08:06:18 -06:00 committed by GitHub
commit 18b29767f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 732 additions and 146 deletions

View File

@ -0,0 +1,6 @@
class AddPhotoFiltersToWebAppConfigs < ActiveRecord::Migration[5.1]
def change
add_column :web_app_configs, :photo_filter_begin, :string
add_column :web_app_configs, :photo_filter_end, :string
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180213175531) do
ActiveRecord::Schema.define(version: 20180215064728) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -387,6 +387,8 @@ ActiveRecord::Schema.define(version: 20180213175531) do
t.boolean "show_first_party_farmware", default: false
t.boolean "enable_browser_speak", default: false
t.boolean "show_images", default: false
t.string "photo_filter_begin"
t.string "photo_filter_end"
t.index ["device_id"], name: "index_web_app_configs_on_device_id"
end

View File

@ -197,5 +197,7 @@ export function fakeWebAppConfig(): TaggedWebAppConfig {
stub_config: false,
show_first_party_farmware: false,
enable_browser_speak: false,
photo_filter_begin: "2018-01-11T20:20:38.362Z",
photo_filter_end: "2018-01-22T15:32:41.970Z"
});
}

View File

@ -18,6 +18,7 @@ import {
import { Actions } from "../constants";
import { connectDevice } from "../connectivity/connect_device";
import { toastErrors } from "../toast_errors";
import { getFirstPartyFarmwareList } from "../farmware/actions";
export function didLogin(authState: AuthState, dispatch: Function) {
API.setBaseUrl(authState.token.unencoded.iss);
@ -25,6 +26,7 @@ export function didLogin(authState: AuthState, dispatch: Function) {
dispatch(fetchReleases(os_update_server));
beta_os_update_server && beta_os_update_server != "NOT_SET" &&
dispatch(fetchReleases(beta_os_update_server, { beta: true }));
dispatch(getFirstPartyFarmwareList());
dispatch(setToken(authState));
Sync.fetchSyncData(dispatch);
dispatch(connectDevice(authState));

View File

@ -1,4 +1,4 @@
import { toggleWebAppBool, getWebAppConfigValue } from "../actions";
import { toggleWebAppBool, getWebAppConfigValue, setWebAppConfigValue } from "../actions";
import { BooleanSetting, NumericSetting } from "../../session_keys";
import { edit, save } from "../../api/crud";
import { fakeWebAppConfig } from "../../__test_support__/fake_state/resources";
@ -7,7 +7,7 @@ jest.mock("../../api/crud", () => {
return { save: jest.fn(), edit: jest.fn() };
});
const mockConfig = fakeWebAppConfig();
let mockConfig = fakeWebAppConfig();
jest.mock("../../resources/selectors", () => {
return {
getWebAppConfig: () => mockConfig,
@ -40,3 +40,24 @@ describe("getWebAppConfigValue", () => {
expect(getValue(NumericSetting.warn_log)).toEqual(3);
});
});
describe("setWebAppConfigValue", () => {
beforeEach(function () {
jest.clearAllMocks();
});
const getState = jest.fn(() => ({ resources: { index: {} } }));
it("sets a numeric setting value", () => {
setWebAppConfigValue(NumericSetting.fun_log, 2)(jest.fn(), getState);
expect(edit).toHaveBeenCalledWith(mockConfig, { fun_log: 2 });
expect(save).toHaveBeenCalledWith(mockConfig.uuid);
});
it("fails to set a value", () => {
// tslint:disable-next-line:no-any
mockConfig = undefined as any;
const action = () => setWebAppConfigValue(NumericSetting.fun_log, 1)(
jest.fn(), getState);
expect(action).toThrowError("Changed settings before app was loaded.");
});
});

View File

@ -1,6 +1,7 @@
import {
BooleanConfigKey as BooleanWebAppConfigKey,
NumberConfigKey as NumberWebAppConfigKey
NumberConfigKey as NumberWebAppConfigKey,
StringConfigKey as StringWebAppConfigKey
} from "./web_app_configs";
import { GetState } from "../redux/interfaces";
import { getWebAppConfig } from "../resources/selectors";
@ -20,10 +21,31 @@ export function toggleWebAppBool(key: BooleanWebAppConfigKey) {
};
}
type WebAppConfigKey =
BooleanWebAppConfigKey
| NumberWebAppConfigKey
| StringWebAppConfigKey;
type WebAppConfigValue = boolean | number | string | undefined;
export type GetWebAppConfigValue = (k: WebAppConfigKey) => WebAppConfigValue;
export function getWebAppConfigValue(getState: GetState) {
return (key: BooleanWebAppConfigKey | NumberWebAppConfigKey):
boolean | number | undefined => {
return (key: WebAppConfigKey): WebAppConfigValue => {
const conf = getWebAppConfig(getState().resources.index);
return conf && conf.body[key];
};
}
export function setWebAppConfigValue(
key: WebAppConfigKey, value: WebAppConfigValue) {
return (dispatch: Function, getState: GetState) => {
const conf = getWebAppConfig(getState().resources.index);
if (conf) {
dispatch(edit(conf, { [key]: value }));
dispatch(save(conf.uuid));
} else {
throw new Error("Changed settings before app was loaded.");
}
};
}

View File

@ -41,6 +41,8 @@ export interface WebAppConfig {
show_first_party_farmware: boolean;
enable_browser_speak: boolean;
show_images: boolean;
photo_filter_begin: string;
photo_filter_end: string;
}
export type NumberConfigKey = "id"
@ -56,7 +58,9 @@ export type NumberConfigKey = "id"
|"debug_log";
export type StringConfigKey = "created_at"
|"updated_at";
|"updated_at"
|"photo_filter_begin"
|"photo_filter_end";
export type BooleanConfigKey = "confirm_step_deletion"
|"disable_animations"

View File

@ -513,6 +513,7 @@ export enum Actions {
// Farmware
SELECT_IMAGE = "SELECT_IMAGE",
FETCH_FIRST_PARTY_FARMWARE_NAMES_OK = "FETCH_FIRST_PARTY_FARMWARE_NAMES_OK",
// Network
NETWORK_EDGE_CHANGE = "NETWORK_EDGE_CHANGE",

View File

@ -3,6 +3,7 @@ import * as React from "react";
import { DirectionButton } from "./controls/direction_button";
import { Xyz, BotPosition } from "./devices/interfaces";
import { McuParams } from "farmbot";
import { getDevice } from "./device";
export interface State {
isOpen: boolean;
@ -79,6 +80,9 @@ export class ControlsPopup extends React.Component<Props, Partial<State>> {
directionAxisProps={directionAxesProps.x}
steps={this.state.stepSize}
disabled={!isOpen} />
<button
className="i fa fa-camera arrow-button fb-button brown"
onClick={() => getDevice().takePhoto()} />
</div>
</div>
</div>;

View File

@ -313,6 +313,19 @@
display: flex;
flex-direction: column;
align-items: center;
label {
margin-bottom: 0;
}
button {
margin-bottom: 0.5rem;
}
}
.caret-menu-button {
display: inline;
margin-left: 0.5rem;
font-weight: bold;
font-size: medium;
cursor: pointer;
}
.farmbot-origin {
.quadrants {
@ -437,3 +450,9 @@
}
}
}
.image-filter-menu {
th {
text-align: center;
}
}

View File

@ -491,7 +491,7 @@ ul {
}
.controls-popup-menu-outer {
transition: all 0.1s 0s ease-in-out;
width: 27rem;
width: 32rem;
padding: 0.6rem 5rem 0rem 0rem;
}
.controls-popup-menu-inner {
@ -501,6 +501,7 @@ ul {
}
.arrow-button {
margin: 5px;
box-shadow: none !important;
}
}

View File

@ -64,3 +64,27 @@ select {
.filter-search-item-none::after {
content: "*";
}
.fb-checkbox {
input[type="checkbox"] {
border-radius: 0;
-webkit-appearance: none;
border: 0.5px solid $gray;
width: 3rem;
height: 3rem;
background: $white;
position: relative;
margin-top: 0;
cursor: pointer;
&:checked:after {
content: '';
position: absolute;
border: solid $dark_gray;
border-width: 0 4.5px 4.5px 0;
transform: rotate(45deg);
bottom: 0.5rem;
left: 0.8rem;
padding: 0.9rem 0.4rem;
}
}
}

View File

@ -2,9 +2,6 @@
.step-wrapper {
box-shadow: 0px 0px 10px $gray;
border-radius: 3px;
a {
color: $dark_gray;
}
.bottom-content {
display: flex;
fieldset {
@ -21,7 +18,6 @@
width: auto;
cursor: pointer;
display: inline-block;
color: $gray;
}
input {
margin-left: 1rem;
@ -154,16 +150,8 @@
margin: 1.4rem 0 0.4rem;
}
}
// CHRIS HALP!
// Modifications for check box on "if" block.
// -RC 7 June 17
input[type=checkbox] {
width: inherit;
box-shadow: none;
vertical-align: middle;
}
input {
height: 34px;
height: 3rem;
}
}
&.execute-step {
@ -184,4 +172,4 @@
select {
width: 100%;
}
}
}

View File

@ -164,6 +164,7 @@ export interface FarmwareProps {
timeOffset: number;
syncStatus: SyncStatus | undefined;
webAppConfig: Partial<WebAppConfig>;
firstPartyFarmwareNames: string[];
}
export interface HardwareSettingsProps {

View File

@ -46,6 +46,8 @@ describe("<FarmDesigner/>", () => {
origin: undefined,
calibrationZ: undefined
},
tzOffset: 0,
getConfigValue: jest.fn(),
};
}

View File

@ -50,9 +50,9 @@ export interface FarmEventViewModel {
export function destructureFarmEvent(fe: TaggedFarmEvent, timeOffset: number): FarmEventViewModel {
return {
startDate: formatDate((fe.body.start_time).toString()),
startDate: formatDate((fe.body.start_time).toString(), timeOffset),
startTime: formatTime((fe.body.start_time).toString(), timeOffset),
endDate: formatDate((fe.body.end_time || new Date()).toString()),
endDate: formatDate((fe.body.end_time || new Date()).toString(), timeOffset),
endTime: formatTime((fe.body.end_time || new Date()).toString(), timeOffset),
repeat: (fe.body.repeat || 1).toString(),
timeUnit: fe.body.time_unit,
@ -79,7 +79,7 @@ export function recombine(vm: FarmEventViewModel): PartialFE {
};
}
function offsetTime(date: string, time: string, offset: number): string {
export function offsetTime(date: string, time: string, offset: number): string {
const out = moment(date).utcOffset(offset);
const [hrs, min] = time.split(":").map(x => parseInt(x));
out.hours(hrs);

View File

@ -29,9 +29,9 @@ export let formatTime = (input: string, timeOffset: number) => {
return moment(iso).utcOffset(timeOffset).format("HH:mm");
};
export let formatDate = (input: string) => {
export let formatDate = (input: string, timeOffset: number) => {
const iso = new Date(input).toISOString();
return moment(iso).format("YYYY-MM-DD");
return moment(iso).utcOffset(timeOffset).format("YYYY-MM-DD");
};
export let repeatOptions = [

View File

@ -137,7 +137,10 @@ export class FarmDesigner extends React.Component<Props, Partial<State>> {
showPoints={show_points}
showSpread={show_spread}
showFarmbot={show_farmbot}
showImages={show_images} />
showImages={show_images}
dispatch={this.props.dispatch}
tzOffset={this.props.tzOffset}
getConfigValue={this.props.getConfigValue} />
<div className="panel-header gray-panel designer-nav">
<div className="panel-tabs">
@ -183,7 +186,8 @@ export class FarmDesigner extends React.Component<Props, Partial<State>> {
peripherals={this.props.peripherals}
eStopStatus={this.props.eStopStatus}
latestImages={this.props.latestImages}
cameraCalibrationData={this.props.cameraCalibrationData} />
cameraCalibrationData={this.props.cameraCalibrationData}
getConfigValue={this.props.getConfigValue} />
</div>
</div>;
}

View File

@ -18,6 +18,7 @@ import { McuParams } from "farmbot/dist";
import { AxisNumberProperty, BotSize } from "./map/interfaces";
import { SelectionBoxData } from "./map/selection_box";
import { BooleanConfigKey } from "../config_storage/web_app_configs";
import { GetWebAppConfigValue } from "../config_storage/actions";
/** TODO: Use Enums */
export type BotOriginQuadrant = 1 | 2 | 3 | 4;
@ -58,6 +59,8 @@ export interface Props {
eStopStatus: boolean;
latestImages: TaggedImage[];
cameraCalibrationData: CameraCalibrationData;
tzOffset: number;
getConfigValue: GetWebAppConfigValue;
}
export type TimeUnit =
@ -188,6 +191,7 @@ export interface GardenMapProps {
eStopStatus: boolean;
latestImages: TaggedImage[];
cameraCalibrationData: CameraCalibrationData;
getConfigValue: GetWebAppConfigValue;
}
export interface GardenMapState {

View File

@ -25,6 +25,9 @@ describe("<GardenMapLegend />", () => {
showSpread: false,
showFarmbot: false,
showImages: false,
dispatch: jest.fn(),
tzOffset: 0,
getConfigValue: jest.fn(),
};
}

View File

@ -85,6 +85,7 @@ function fakeProps(): GardenMapProps {
origin: undefined,
calibrationZ: undefined
},
getConfigValue: jest.fn(),
};
}

View File

@ -311,7 +311,8 @@ export class GardenMap extends
images={this.props.latestImages}
cameraCalibrationData={this.props.cameraCalibrationData}
visible={!!this.props.showImages}
mapTransformProps={mapTransformProps} />
mapTransformProps={mapTransformProps}
getConfigValue={this.props.getConfigValue} />
<Grid
onClick={closePlantInfo(this.props.dispatch)}
mapTransformProps={mapTransformProps}

View File

@ -4,6 +4,7 @@ import { LayerToggle } from "./layer_toggle";
import { GardenMapLegendProps } from "./interfaces";
import { history } from "../../history";
import { atMaxZoom, atMinZoom } from "./zoom";
import { ImageFilterMenu } from "./layers/image_layer";
export function GardenMapLegend(props: GardenMapLegendProps) {
@ -18,6 +19,9 @@ export function GardenMapLegend(props: GardenMapLegendProps) {
showSpread,
showFarmbot,
showImages,
dispatch,
tzOffset,
getConfigValue,
} = props;
const plusBtnClass = atMaxZoom() ? "disabled" : "";
@ -68,7 +72,11 @@ export function GardenMapLegend(props: GardenMapLegendProps) {
<LayerToggle
value={showImages}
label={t("Photos?")}
onClick={toggle("show_images")} />
onClick={toggle("show_images")}
popover={<ImageFilterMenu
tzOffset={tzOffset}
dispatch={dispatch}
getConfigValue={getConfigValue} />} />
</div>
<div className="farmbot-origin">
<label>

View File

@ -5,6 +5,7 @@ import {
} from "../../resources/tagged_resources";
import { State, BotOriginQuadrant } from "../interfaces";
import { BotPosition, BotLocationData } from "../../devices/interfaces";
import { GetWebAppConfigValue } from "../../config_storage/actions";
export interface PlantLayerProps {
plants: TaggedPlantPointer[];
@ -35,6 +36,9 @@ export interface GardenMapLegendProps {
showSpread: boolean;
showFarmbot: boolean;
showImages: boolean;
dispatch: Function;
tzOffset: number;
getConfigValue: GetWebAppConfigValue;
}
export type MapTransformProps = {

View File

@ -1,18 +1,29 @@
import * as React from "react";
import { Popover, Position } from "@blueprintjs/core";
export interface LayerToggleProps {
label: string;
value: boolean | undefined;
onClick(): void;
popover?: JSX.Element;
}
/** A flipper type switch for showing/hiding the layers of the garden map. */
export function LayerToggle({ label, value, onClick }: LayerToggleProps) {
export function LayerToggle({ label, value, onClick, popover }: LayerToggleProps) {
const klassName = "fb-button fb-toggle-button " + (value ? "green" : "red");
return <fieldset>
<label>
<span>{label}</span>
<button className={klassName} onClick={onClick} />
<span>
{label}
{popover &&
<Popover
position={Position.BOTTOM_RIGHT}
className={"caret-menu-button"}>
<i className="fa fa-caret-down" />
{popover}
</Popover>}
</span>
</label>
<button className={klassName} onClick={onClick} />
</fieldset>;
}

View File

@ -1,7 +1,19 @@
import * as React from "react";
import { ImageLayer, ImageLayerProps } from "../image_layer";
import {
ImageLayer, ImageLayerProps, ImageFilterMenu, ImageFilterMenuProps
} from "../image_layer";
import { shallow } from "enzyme";
import { fakeImage } from "../../../../__test_support__/fake_state/resources";
import { fakeImage, fakeWebAppConfig } from "../../../../__test_support__/fake_state/resources";
import { Actions } from "../../../../constants";
import { StringConfigKey } from "../../../../config_storage/web_app_configs";
const mockConfig = fakeWebAppConfig();
jest.mock("../../../../resources/selectors", () => {
return {
getWebAppConfig: () => mockConfig,
assertUuid: jest.fn()
};
});
describe("<ImageLayer/>", () => {
function fakeProps(): ImageLayerProps {
@ -21,7 +33,8 @@ describe("<ImageLayer/>", () => {
scale: "1",
calibrationZ: "0"
},
sizeOverride: { width: 10, height: 10 }
sizeOverride: { width: 10, height: 10 },
getConfigValue: jest.fn(),
};
}
@ -39,4 +52,97 @@ describe("<ImageLayer/>", () => {
const layer = wrapper.find("#image-layer");
expect(layer.find("MapImage").length).toEqual(0);
});
it("filters old images", () => {
const p = fakeProps();
p.images[0].body.created_at = "2018-01-22T05:00:00.000Z";
p.getConfigValue = () => "2018-01-23T05:00:00.000Z";
const wrapper = shallow(<ImageLayer {...p } />);
const layer = wrapper.find("#image-layer");
expect(layer.find("MapImage").length).toEqual(0);
});
});
describe("<ImageFilterMenu />", () => {
beforeEach(function () {
jest.clearAllMocks();
});
const getState = jest.fn(() => ({ resources: { index: {} } }));
mockConfig.body.photo_filter_begin = "";
mockConfig.body.photo_filter_end = "";
const fakeProps = (): ImageFilterMenuProps => {
return {
tzOffset: 0,
dispatch: jest.fn(),
getConfigValue: jest.fn(x => mockConfig.body[x as StringConfigKey])
};
};
it("renders", () => {
const p = fakeProps();
const wrapper = shallow(<ImageFilterMenu {...p } />);
["Date", "Time", "Newer than", "Older than"].map(string =>
expect(wrapper.text()).toContain(string));
});
const testFilterSetDate =
(filter: "beginDate" | "endDate",
key: "photo_filter_begin" | "photo_filter_end",
i: number) => {
it(`sets filter: ${filter}`, () => {
const p = fakeProps();
const wrapper = shallow(<ImageFilterMenu {...p } />);
wrapper.find("BlurableInput").at(i).simulate("commit", {
currentTarget: { value: "2001-01-03" }
});
expect(wrapper.state()[filter]).toEqual("2001-01-03");
(p.dispatch as jest.Mock).mock.calls[0][0](p.dispatch, getState);
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.EDIT_RESOURCE,
payload: expect.objectContaining({
update: { [key]: "2001-01-03T00:00:00.000Z" }
})
});
});
};
testFilterSetDate("beginDate", "photo_filter_begin", 0);
testFilterSetDate("endDate", "photo_filter_end", 2);
const testFilterSetTime =
(filter: "beginTime" | "endTime",
key: "photo_filter_begin" | "photo_filter_end",
i: number) => {
it(`sets filter: ${filter}`, () => {
const p = fakeProps();
const wrapper = shallow(<ImageFilterMenu {...p } />);
wrapper.setState({ beginDate: "2001-01-03", endDate: "2001-01-03" });
wrapper.find("BlurableInput").at(i).simulate("commit", {
currentTarget: { value: "05:00" }
});
expect(wrapper.state()[filter]).toEqual("05:00");
(p.dispatch as jest.Mock).mock.calls[0][0](p.dispatch, getState);
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.EDIT_RESOURCE,
payload: expect.objectContaining({
update: { [key]: "2001-01-03T05:00:00.000Z" }
})
});
});
};
testFilterSetTime("beginTime", "photo_filter_begin", 1);
testFilterSetTime("endTime", "photo_filter_end", 3);
it("loads values from config", () => {
mockConfig.body.photo_filter_begin = "2001-01-03T05:00:00.000Z";
mockConfig.body.photo_filter_end = "2001-01-03T06:00:00.000Z";
const wrapper = shallow(<ImageFilterMenu {...fakeProps() } />);
expect(wrapper.state()).toEqual({
beginDate: "2001-01-03", beginTime: "05:00",
endDate: "2001-01-03", endTime: "06:00"
});
});
});

View File

@ -4,6 +4,12 @@ import { CameraCalibrationData } from "../../interfaces";
import { TaggedImage } from "../../../resources/tagged_resources";
import { MapImage } from "../map_image";
import { reverse, cloneDeep } from "lodash";
import { BlurableInput } from "../../../ui/index";
import { t } from "i18next";
import { offsetTime } from "../../farm_events/edit_fe_form";
import { setWebAppConfigValue, GetWebAppConfigValue } from "../../../config_storage/actions";
import * as moment from "moment";
import { formatDate, formatTime } from "../../farm_events/map_state_to_props_add_edit";
export interface ImageLayerProps {
visible: boolean;
@ -11,21 +17,166 @@ export interface ImageLayerProps {
mapTransformProps: MapTransformProps;
cameraCalibrationData: CameraCalibrationData;
sizeOverride?: { width: number, height: number };
getConfigValue: GetWebAppConfigValue;
}
export function ImageLayer(props: ImageLayerProps) {
const {
visible, images, mapTransformProps, cameraCalibrationData, sizeOverride
visible, images, mapTransformProps, cameraCalibrationData, sizeOverride,
getConfigValue
} = props;
const imageFilterBegin = getConfigValue("photo_filter_begin");
const imageFilterEnd = getConfigValue("photo_filter_end");
return <g id="image-layer">
{visible &&
reverse(cloneDeep(images)).map(img =>
<MapImage
image={img}
key={"image_" + img.body.id}
cameraCalibrationData={cameraCalibrationData}
sizeOverride={sizeOverride}
mapTransformProps={mapTransformProps} />
)}
reverse(cloneDeep(images))
.filter(x => !imageFilterEnd ||
moment(x.body.created_at).isBefore(imageFilterEnd.toString()))
.filter(x => !imageFilterBegin ||
moment(x.body.created_at).isAfter(imageFilterBegin.toString()))
.map(img =>
<MapImage
image={img}
key={"image_" + img.body.id}
cameraCalibrationData={cameraCalibrationData}
sizeOverride={sizeOverride}
mapTransformProps={mapTransformProps} />
)}
</g>;
}
interface ImageFilterMenuState {
beginDate: string | undefined;
beginTime: string | undefined;
endDate: string | undefined;
endTime: string | undefined;
}
export interface ImageFilterMenuProps {
tzOffset: number;
dispatch: Function;
getConfigValue: GetWebAppConfigValue;
}
export class ImageFilterMenu
extends React.Component<ImageFilterMenuProps, Partial<ImageFilterMenuState>> {
constructor(props: ImageFilterMenuProps) {
super(props);
this.state = {};
}
componentWillMount() {
this.updateState();
}
componentWillReceiveProps() {
this.updateState();
}
updateState = () => {
const beginDatetime = this.props.getConfigValue("photo_filter_begin");
const endDatetime = this.props.getConfigValue("photo_filter_end");
const { tzOffset } = this.props;
this.setState({
beginDate: beginDatetime
? formatDate(beginDatetime.toString(), tzOffset) : undefined,
beginTime: beginDatetime
? formatTime(beginDatetime.toString(), tzOffset) : undefined,
endDate: endDatetime
? formatDate(endDatetime.toString(), tzOffset) : undefined,
endTime: endDatetime
? formatTime(endDatetime.toString(), tzOffset) : undefined,
});
}
setDatetime = (datetime: keyof ImageFilterMenuState) => {
return (e: React.SyntheticEvent<HTMLInputElement>) => {
const input = e.currentTarget.value;
this.setState({ [datetime]: input });
const { beginDate, beginTime, endDate, endTime } = this.state;
const { dispatch, tzOffset } = this.props;
let value = undefined;
switch (datetime) {
case "beginDate":
value = offsetTime(input, beginTime || "00:00", tzOffset);
dispatch(setWebAppConfigValue("photo_filter_begin", value));
break;
case "beginTime":
if (beginDate) {
value = offsetTime(beginDate, input, tzOffset);
dispatch(setWebAppConfigValue("photo_filter_begin", value));
}
break;
case "endDate":
value = offsetTime(input, endTime || "00:00", tzOffset);
dispatch(setWebAppConfigValue("photo_filter_end", value));
break;
case "endTime":
if (endDate) {
value = offsetTime(endDate, input, tzOffset);
dispatch(setWebAppConfigValue("photo_filter_end", value));
}
break;
}
};
};
render() {
const { beginDate, beginTime, endDate, endTime } = this.state;
return <table className={"image-filter-menu"}>
<thead>
<tr>
<th />
<th><label>{t("Date")}</label></th>
<th><label>{t("Time")}</label></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<label>{t("Newer than")}</label>
</td>
<td>
<BlurableInput
type="date"
name="beginDate"
value={beginDate || ""}
allowEmpty={true}
onCommit={this.setDatetime("beginDate")} />
</td>
<td>
<BlurableInput
type="time"
name="beginTime"
value={beginTime || ""}
allowEmpty={true}
disabled={!beginDate}
onCommit={this.setDatetime("beginTime")} />
</td>
</tr>
<tr>
<td>
<label>{t("Older than")}</label>
</td>
<td>
<BlurableInput
type="date"
name="endDate"
value={endDate || ""}
allowEmpty={true}
onCommit={this.setDatetime("endDate")} />
</td>
<td>
<BlurableInput
type="time"
name="endTime"
value={endTime || ""}
allowEmpty={true}
disabled={!endDate}
onCommit={this.setDatetime("endTime")} />
</td>
</tr>
</tbody>
</table>;
}
}

View File

@ -5,12 +5,14 @@ import {
selectAllCrops,
joinToolsAndSlot,
selectAllPeripherals,
selectAllImages
selectAllImages,
maybeGetTimeOffset
} from "../resources/selectors";
import { StepsPerMmXY } from "../devices/interfaces";
import { isNumber } from "lodash";
import * as _ from "lodash";
import { minFwVersionCheck, validBotLocationData } from "../util";
import { getWebAppConfigValue } from "../config_storage/actions";
export function mapStateToProps(props: Everything) {
@ -87,5 +89,7 @@ export function mapStateToProps(props: Everything) {
eStopStatus: props.bot.hardware.informational_settings.locked,
latestImages,
cameraCalibrationData,
tzOffset: maybeGetTimeOffset(props.resources.index),
getConfigValue: getWebAppConfigValue(() => props),
};
}

View File

@ -12,11 +12,15 @@ jest.mock("axios", () => ({
}));
import { getFirstPartyFarmwareList } from "../actions";
import { Actions } from "../../constants";
describe("getFirstPartyFarmwareList()", () => {
it("sets list", async () => {
const setList = jest.fn();
await getFirstPartyFarmwareList(setList);
expect(setList).toHaveBeenCalledWith(["farmware0", "farmware1"]);
const dispatch = jest.fn();
await getFirstPartyFarmwareList()(dispatch);
expect(dispatch).toHaveBeenCalledWith({
type: Actions.FETCH_FIRST_PARTY_FARMWARE_NAMES_OK,
payload: ["farmware0", "farmware1"]
});
});
});

View File

@ -10,12 +10,6 @@ jest.mock("../../device", () => ({
getDevice: () => (mockDevice)
}));
jest.mock("../actions", () => ({
getFirstPartyFarmwareList(setList: (x: string[]) => void) {
setList(["first-party farmware"]);
}
}));
import * as React from "react";
import { mount, shallow } from "enzyme";
import { FarmwarePanel, FarmwareConfigMenu } from "../farmware_panel";
@ -34,7 +28,8 @@ describe("<FarmwarePanel/>: actions", () => {
botToMqttStatus: "up",
syncStatus: "synced",
onToggle: jest.fn(() => showFirstParty = !showFirstParty),
showFirstParty
showFirstParty,
firstPartyFarmwareNames: ["first-party farmware"]
};
}
@ -125,7 +120,8 @@ describe("<FarmwarePanel/>: farmware list", () => {
farmwares: fakeFarmwares(),
syncStatus: "synced",
onToggle: jest.fn(() => showFirstParty = !showFirstParty),
showFirstParty
showFirstParty,
firstPartyFarmwareNames: ["first-party farmware"]
};
}

View File

@ -25,7 +25,8 @@ describe("<FarmwarePage />", () => {
images: [],
timeOffset: 0,
syncStatus: "synced",
webAppConfig: {}
webAppConfig: {},
firstPartyFarmwareNames: []
};
const wrapper = mount(<FarmwarePage {...props} />);
["Take Photo",

View File

@ -5,9 +5,17 @@ import { Actions } from "../../constants";
import { fakeImage } from "../../__test_support__/fake_state/resources";
describe("famrwareReducer", () => {
const fakeState = (): FarmwareState => {
return {
currentImage: undefined,
firstPartyFarmwareNames: []
};
};
it("Removes UUIDs from state on deletion", () => {
const image = fakeImage();
const oldState: FarmwareState = { currentImage: image.uuid };
const oldState = fakeState();
oldState.currentImage = image.uuid;
const newState = famrwareReducer(oldState, {
type: Actions.DESTROY_RESOURCE_OK,
payload: image
@ -18,7 +26,7 @@ describe("famrwareReducer", () => {
it("adds UUID to state on SELECT_IMAGE", () => {
const image = fakeImage();
const oldState: FarmwareState = { currentImage: undefined };
const oldState = fakeState();
const newState = famrwareReducer(oldState, {
type: Actions.SELECT_IMAGE,
payload: image.uuid
@ -30,7 +38,7 @@ describe("famrwareReducer", () => {
it("sets the current image via INIT_RESOURCE", () => {
const image = fakeImage();
const oldState: FarmwareState = { currentImage: undefined };
const oldState = fakeState();
const newState = famrwareReducer(oldState, {
type: Actions.INIT_RESOURCE,
payload: image
@ -39,4 +47,16 @@ describe("famrwareReducer", () => {
expect(newState.currentImage).not.toBeUndefined();
expect(newState.currentImage).toBe(image.uuid);
});
it("sets 1st party farmware list", () => {
const FARMWARE_NAMES = ["1stPartyOne", "1stPartyTwo"];
const oldState = fakeState();
const newState = famrwareReducer(oldState, {
type: Actions.FETCH_FIRST_PARTY_FARMWARE_NAMES_OK,
payload: ["1stPartyOne", "1stPartyTwo"]
});
expect(oldState.firstPartyFarmwareNames)
.not.toEqual(newState.firstPartyFarmwareNames);
expect(newState.firstPartyFarmwareNames).toEqual(FARMWARE_NAMES);
});
});

View File

@ -1,17 +1,23 @@
import axios from "axios";
import { HttpData } from "../util";
import { FarmwareManifestEntry } from "./interfaces";
import { Actions } from "../constants";
const farmwareManifestUrl =
"https://raw.githubusercontent.com/FarmBot-Labs/farmware_manifests" +
"/master/manifest.json";
export function getFirstPartyFarmwareList(setList: (x: string[]) => void) {
axios.get(farmwareManifestUrl)
.then((r: HttpData<FarmwareManifestEntry[]>) => {
const names = r.data.map((fw: FarmwareManifestEntry) => {
return fw.name;
export const getFirstPartyFarmwareList = () => {
return (dispatch: Function) => {
axios.get(farmwareManifestUrl)
.then((r: HttpData<FarmwareManifestEntry[]>) => {
const names = r.data.map((fw: FarmwareManifestEntry) => {
return fw.name;
});
dispatch({
type: Actions.FETCH_FIRST_PARTY_FARMWARE_NAMES_OK,
payload: names
});
});
setList(names);
});
}
};
};

View File

@ -14,7 +14,6 @@ import {
} from "../ui/index";
import { betterCompact } from "../util";
import { Popover, Position } from "@blueprintjs/core";
import { getFirstPartyFarmwareList } from "./actions";
export function FarmwareConfigMenu(props: FarmwareConfigMenuProps) {
const listBtnColor = props.show ? "green" : "red";
@ -48,10 +47,6 @@ export class FarmwarePanel extends React.Component<FWProps, Partial<FWState>> {
this.state = {};
}
componentDidMount() {
getFirstPartyFarmwareList(this.setFirstPartyList);
}
/** Keep null checking DRY for this.state.selectedFarmware */
ifFarmwareSelected = (cb: (label: string) => void) => {
const { selectedFarmware } = this.state;
@ -68,8 +63,9 @@ export class FarmwarePanel extends React.Component<FWProps, Partial<FWState>> {
remove = () => {
this
.ifFarmwareSelected(label => {
const { firstPartyList } = this.state;
const isFirstParty = firstPartyList && firstPartyList.includes(label);
const { firstPartyFarmwareNames } = this.props;
const isFirstParty = firstPartyFarmwareNames &&
firstPartyFarmwareNames.includes(label);
if (!isFirstParty || confirm(Content.FIRST_PARTY_WARNING)) {
getDevice()
.removeFarmware(label)
@ -95,10 +91,6 @@ export class FarmwarePanel extends React.Component<FWProps, Partial<FWState>> {
}
}
setFirstPartyList = (firstPartyList: string[]) => {
this.setState({ firstPartyList });
}
firstPartyFarmwaresPresent = (firstPartyList: string[] | undefined) => {
const fws = this.props.farmwares;
const farmwareList = betterCompact(Object.keys(fws)
@ -109,13 +101,12 @@ export class FarmwarePanel extends React.Component<FWProps, Partial<FWState>> {
}
fwList = () => {
const { farmwares, showFirstParty } = this.props;
const { firstPartyList } = this.state;
const { farmwares, showFirstParty, firstPartyFarmwareNames } = this.props;
const choices = betterCompact(Object
.keys(farmwares)
.map(x => farmwares[x]))
.filter(x => (firstPartyList && !showFirstParty)
? !firstPartyList.includes(x.name) : x)
.filter(x => (firstPartyFarmwareNames && !showFirstParty)
? !firstPartyFarmwareNames.includes(x.name) : x)
.map((fw, i) => ({ value: fw.name, label: (`${fw.name} ${fw.meta.version}`) }));
return choices;
}
@ -149,7 +140,8 @@ export class FarmwarePanel extends React.Component<FWProps, Partial<FWState>> {
show={this.props.showFirstParty}
onToggle={() => this.props.onToggle("show_first_party_farmware")}
firstPartyFwsInstalled={
this.firstPartyFarmwaresPresent(this.state.firstPartyList)} />
this.firstPartyFarmwaresPresent(
this.props.firstPartyFarmwareNames)} />
</Popover>
</WidgetHeader>
<WidgetBody>

View File

@ -37,7 +37,8 @@ export class FarmwarePage extends React.Component<FarmwareProps, {}> {
onToggle={doToggle(this.props.dispatch)}
syncStatus={this.props.syncStatus}
botToMqttStatus={this.props.botToMqttStatus}
farmwares={this.props.farmwares} />
farmwares={this.props.farmwares}
firstPartyFarmwareNames={this.props.firstPartyFarmwareNames} />
</Col>
</Row>
<Row>

View File

@ -5,7 +5,6 @@ import { BooleanConfigKey } from "../config_storage/web_app_configs";
export interface FWState {
selectedFarmware: string | undefined;
packageUrl: string | undefined;
firstPartyList: string[];
}
export interface FWProps {
@ -14,10 +13,12 @@ export interface FWProps {
farmwares: Dictionary<FarmwareManifest | undefined>;
showFirstParty: boolean;
onToggle(key: BooleanConfigKey): void;
firstPartyFarmwareNames: string[];
}
export interface FarmwareState {
currentImage: string | undefined;
firstPartyFarmwareNames: string[];
}
export type FarmwareManifestEntry = Record<"name" | "manifest", string>;

View File

@ -3,7 +3,10 @@ import { FarmwareState } from "./interfaces";
import { TaggedResource } from "../resources/tagged_resources";
import { Actions } from "../constants";
export let farmwareState: FarmwareState = { currentImage: undefined };
export let farmwareState: FarmwareState = {
currentImage: undefined,
firstPartyFarmwareNames: []
};
export let famrwareReducer = generateReducer<FarmwareState>(farmwareState)
.add<TaggedResource>(Actions.INIT_RESOURCE, (s, { payload }) => {
@ -16,6 +19,10 @@ export let famrwareReducer = generateReducer<FarmwareState>(farmwareState)
s.currentImage = payload;
return s;
})
.add<string[]>(Actions.FETCH_FIRST_PARTY_FARMWARE_NAMES_OK, (s, { payload }) => {
s.firstPartyFarmwareNames = payload;
return s;
})
.add<TaggedResource>(Actions.DESTROY_RESOURCE_OK, (s, { payload }) => {
const thatUUID = payload.uuid;
const thisUUID = s.currentImage;

View File

@ -15,6 +15,7 @@ export function mapStateToProps(props: Everything): FarmwareProps {
|| firstImage;
const { farmwares } = props.bot.hardware.process_info;
const conf = getWebAppConfig(props.resources.index);
const { firstPartyFarmwareNames } = props.resources.consumers.farmware;
return {
timeOffset: maybeGetTimeOffset(props.resources.index),
farmwares,
@ -25,6 +26,7 @@ export function mapStateToProps(props: Everything): FarmwareProps {
currentImage,
images,
syncStatus: "synced",
webAppConfig: conf ? conf.body : {}
webAppConfig: conf ? conf.body : {},
firstPartyFarmwareNames
};
}

View File

@ -28,7 +28,8 @@ describe("<WeedDetector />", () => {
currentImage: undefined,
images: [],
syncStatus: "synced",
webAppConfig: {}
webAppConfig: {},
firstPartyFarmwareNames: []
};
it("renders", () => {

View File

@ -27,7 +27,6 @@ import { fakeSequence } from "../../__test_support__/fake_state/resources";
import { destroy } from "../../api/crud";
import { fakeHardwareFlags } from "../../__test_support__/sequence_hardware_settings";
describe("<SequenceEditorMiddleActive/>", () => {
function fakeProps(): ActiveMiddleProps {
return {
@ -37,7 +36,12 @@ describe("<SequenceEditorMiddleActive/>", () => {
syncStatus: "synced",
consistent: true,
autoSyncEnabled: false,
hardwareFlags: fakeHardwareFlags()
hardwareFlags: fakeHardwareFlags(),
farmwareInfo: {
farmwareNames: [],
firstPartyFarmwareNames: [],
showFirstPartyFarmware: false
}
};
}

View File

@ -17,7 +17,12 @@ describe("<SequenceEditorMiddle/>", () => {
syncStatus: "synced",
consistent: true,
autoSyncEnabled: false,
hardwareFlags: fakeHardwareFlags()
hardwareFlags: fakeHardwareFlags(),
farmwareInfo: {
farmwareNames: [],
firstPartyFarmwareNames: [],
showFirstPartyFarmware: false
}
};
}

View File

@ -25,7 +25,12 @@ describe("<Sequences/>", () => {
auth,
consistent: true,
autoSyncEnabled: false,
hardwareFlags: fakeHardwareFlags()
hardwareFlags: fakeHardwareFlags(),
farmwareInfo: {
farmwareNames: [],
firstPartyFarmwareNames: [],
showFirstPartyFarmware: false
}
};
}

View File

@ -6,7 +6,7 @@ import { StepDragger } from "../draggable/step_dragger";
import { renderCeleryNode } from "./step_tiles/index";
import { ResourceIndex } from "../resources/interfaces";
import { getStepTag } from "../resources/sequence_tagging";
import { HardwareFlags } from "./interfaces";
import { HardwareFlags, FarmwareInfo } from "./interfaces";
interface AllStepsProps {
sequence: TaggedSequence;
@ -14,11 +14,14 @@ interface AllStepsProps {
dispatch: Function;
resources: ResourceIndex;
hardwareFlags?: HardwareFlags;
farmwareInfo?: FarmwareInfo;
}
export class AllSteps extends React.Component<AllStepsProps, {}> {
render() {
const { sequence, onDrop, dispatch, hardwareFlags } = this.props;
const {
sequence, onDrop, dispatch, hardwareFlags, farmwareInfo
} = this.props;
const items = (sequence.body.body || [])
.map((currentStep: SequenceBodyItem, index, arr) => {
/** HACK: React's diff algorithm (probably?) can't keep track of steps
@ -42,7 +45,8 @@ export class AllSteps extends React.Component<AllStepsProps, {}> {
dispatch: dispatch,
currentSequence: sequence,
resources: this.props.resources,
hardwareFlags
hardwareFlags,
farmwareInfo
})}
</div>
</StepDragger>

View File

@ -31,6 +31,7 @@ export interface Props {
consistent: boolean;
autoSyncEnabled: boolean;
hardwareFlags: HardwareFlags;
farmwareInfo: FarmwareInfo;
}
export interface SequenceEditorMiddleProps {
@ -41,6 +42,7 @@ export interface SequenceEditorMiddleProps {
consistent: boolean;
autoSyncEnabled: boolean;
hardwareFlags: HardwareFlags;
farmwareInfo: FarmwareInfo;
}
export interface ActiveMiddleProps extends SequenceEditorMiddleProps {
@ -151,6 +153,12 @@ export type DataXferObj = StepMoveDataXfer | StepSpliceDataXfer;
export type dispatcher = (a: Function | { type: string }) => DataXferObj;
export interface FarmwareInfo {
farmwareNames: string[];
firstPartyFarmwareNames: string[];
showFirstPartyFarmware: boolean;
}
export interface StepParams {
currentSequence: TaggedSequence;
currentStep: SequenceBodyItem;
@ -158,4 +166,5 @@ export interface StepParams {
index: number;
resources: ResourceIndex;
hardwareFlags?: HardwareFlags;
farmwareInfo?: FarmwareInfo;
}

View File

@ -12,7 +12,8 @@ export class SequenceEditorMiddle
sequence,
resources,
syncStatus,
hardwareFlags
hardwareFlags,
farmwareInfo
} = this.props;
if (sequence && isTaggedSequence(sequence)) {
return <SequenceEditorMiddleActive
@ -22,7 +23,8 @@ export class SequenceEditorMiddle
syncStatus={syncStatus}
consistent={true}
autoSyncEnabled={false}
hardwareFlags={hardwareFlags} />;
hardwareFlags={hardwareFlags}
farmwareInfo={farmwareInfo} />;
} else {
return <SequenceEditorMiddleInactive />;
}

View File

@ -40,7 +40,8 @@ export class Sequences extends React.Component<Props, {}> {
resources={this.props.resources}
consistent={this.props.consistent}
autoSyncEnabled={this.props.autoSyncEnabled}
hardwareFlags={this.props.hardwareFlags} />
hardwareFlags={this.props.hardwareFlags}
farmwareInfo={this.props.farmwareInfo} />
</div>
</Col>
<Col sm={3}>

View File

@ -2,10 +2,12 @@ import { Everything } from "../interfaces";
import { Props, HardwareFlags } from "./interfaces";
import {
selectAllSequences,
findSequence
findSequence,
getWebAppConfig
} from "../resources/selectors";
import { getStepTag } from "../resources/sequence_tagging";
import { enabledAxisMap } from "../devices/components/axis_tracking_status";
import { betterCompact } from "../util";
export function mapStateToProps(props: Everything): Props {
const uuid = props.resources.consumers.sequences.current;
@ -42,6 +44,15 @@ export function mapStateToProps(props: Everything): Props {
};
};
const { farmwares } = props.bot.hardware.process_info;
const farmwareNames = betterCompact(Object
.keys(farmwares)
.map(x => farmwares[x]))
.map(fw => fw.name);
const { firstPartyFarmwareNames } = props.resources.consumers.farmware;
const conf = getWebAppConfig(props.resources.index);
const showFirstPartyFarmware = !!(conf && conf.body.show_first_party_farmware);
return {
dispatch: props.dispatch,
sequences: selectAllSequences(props.resources.index),
@ -56,5 +67,10 @@ export function mapStateToProps(props: Everything): Props {
consistent: props.bot.consistent,
autoSyncEnabled: !!props.bot.hardware.configuration.auto_sync,
hardwareFlags: hardwareFlags(),
farmwareInfo: {
farmwareNames,
firstPartyFarmwareNames,
showFirstPartyFarmware
}
};
}

View File

@ -1,12 +1,14 @@
import * as React from "react";
import { TileExecuteScript } from "../tile_execute_script";
import { mount } from "enzyme";
import { mount, shallow } from "enzyme";
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { ExecuteScript } from "farmbot/dist";
import { emptyState } from "../../../resources/reducer";
import { StepParams } from "../../interfaces";
import { Actions } from "../../../constants";
describe("<TileExecuteScript/>", () => {
function bootstrapTest() {
const fakeProps = (): StepParams => {
const currentStep: ExecuteScript = {
kind: "execute_script",
args: {
@ -14,23 +16,78 @@ describe("<TileExecuteScript/>", () => {
}
};
return {
component: mount(<TileExecuteScript
currentSequence={fakeSequence()}
currentStep={currentStep}
dispatch={jest.fn()}
index={0}
resources={emptyState().index} />)
currentSequence: fakeSequence(),
currentStep,
dispatch: jest.fn(),
index: 0,
resources: emptyState().index,
farmwareInfo: {
farmwareNames: ["one", "two", "three"],
firstPartyFarmwareNames: ["one"],
showFirstPartyFarmware: false
}
};
}
};
it("renders inputs", () => {
const block = bootstrapTest().component;
const inputs = block.find("input");
const labels = block.find("label");
const wrapper = mount(<TileExecuteScript {...fakeProps() } />);
const inputs = wrapper.find("input");
const labels = wrapper.find("label");
expect(inputs.length).toEqual(2);
expect(labels.length).toEqual(1);
expect(labels.length).toEqual(2);
expect(inputs.first().props().placeholder).toEqual("Run Farmware");
expect(labels.at(0).text()).toEqual("Package Name");
expect(labels.at(1).text()).toEqual("Manual input");
expect(inputs.at(1).props().value).toEqual("farmware-to-execute");
});
it("renders farmware list", () => {
const wrapper = shallow(<TileExecuteScript {...fakeProps() } />);
expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "two", value: "two" },
{ label: "three", value: "three" }]);
});
it("shows 1st party in list", () => {
const p = fakeProps();
p.farmwareInfo && (p.farmwareInfo.showFirstPartyFarmware = true);
const wrapper = shallow(<TileExecuteScript {...p} />);
expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "one", value: "one" },
{ label: "two", value: "two" },
{ label: "three", value: "three" }]);
});
it("doesn't show manual input if installed farmware is selected", () => {
const p = fakeProps();
(p.currentStep as ExecuteScript).args.label = "two";
const wrapper = mount(<TileExecuteScript {...p} />);
expect(wrapper.find("label").length).toEqual(1);
});
it("renders manual input", () => {
const p = fakeProps();
p.farmwareInfo = undefined;
const wrapper = mount(<TileExecuteScript {...p} />);
expect(wrapper.find("button").text()).toEqual("Manual Input");
expect(wrapper.find("label").at(1).text()).toEqual("Manual input");
expect(wrapper.find("input").at(1).props().value).toEqual("farmware-to-execute");
});
it("uses drop-down to update step", () => {
const p = fakeProps();
const wrapper = shallow(<TileExecuteScript {...p} />);
wrapper.find("FBSelect").simulate("change", {
label: "farmware-name",
value: "farmware-name"
});
expect(p.dispatch).toHaveBeenCalledWith({
payload: expect.objectContaining({
update: expect.objectContaining({
body: [{ args: { label: "farmware-name" }, kind: "execute_script" }]
})
}),
type: Actions.OVERWRITE_RESOURCE
});
});
});

View File

@ -30,7 +30,7 @@ describe("<TileReadPeripheral/>", () => {
it("toggles to `read_pin`", () => {
const { component, dispatch } = bootstrapTest();
component.find("a").last().simulate("click");
component.find("input").last().simulate("change");
expect(dispatch).toHaveBeenCalled();
const action = expect
.objectContaining({ "type": Actions.OVERWRITE_RESOURCE });

View File

@ -30,7 +30,7 @@ describe("<TileReadPin/>", () => {
const inputs = block.find("input");
const labels = block.find("label");
const buttons = block.find("button");
expect(inputs.length).toEqual(3);
expect(inputs.length).toEqual(4);
expect(labels.length).toEqual(4);
expect(buttons.length).toEqual(1);
expect(inputs.first().props().placeholder).toEqual("Read Pin");

View File

@ -4,11 +4,47 @@ import { t } from "i18next";
import { ToolTips } from "../../constants";
import { StepInputBox } from "../inputs/step_input_box";
import { StepWrapper, StepHeader, StepContent } from "../step_ui/index";
import { Row, Col } from "../../ui/index";
import { Row, Col, FBSelect, DropDownItem } from "../../ui/index";
import { assign } from "lodash";
import { defensiveClone } from "../../util";
import { overwrite } from "../../api/crud";
const MANUAL_INPUT = { label: "Manual Input", value: "" };
export function TileExecuteScript({
dispatch, currentStep, index, currentSequence }: StepParams) {
dispatch, currentStep, index, currentSequence, farmwareInfo }: StepParams) {
if (currentStep.kind === "execute_script") {
const farmwareList = () => {
if (farmwareInfo) {
const {
farmwareNames, showFirstPartyFarmware, firstPartyFarmwareNames
} = farmwareInfo;
return farmwareNames
.filter(x => (firstPartyFarmwareNames && !showFirstPartyFarmware)
? !firstPartyFarmwareNames.includes(x) : x)
.map(name => ({ value: name, label: name }));
}
return [];
};
const selectedFarmware = () => {
const farmware = currentStep.args.label;
if (farmwareInfo && farmwareInfo.farmwareNames.includes(farmware)) {
return { value: farmware, label: farmware };
}
return MANUAL_INPUT;
};
const updateStep = (item: DropDownItem) => {
const stepCopy = defensiveClone(currentStep);
const seqCopy = defensiveClone(currentSequence).body;
seqCopy.body = seqCopy.body || [];
assign(stepCopy.args, { label: item.value });
seqCopy.body[index] = stepCopy;
dispatch(overwrite(currentSequence, seqCopy));
};
const className = "execute-script-step";
return <StepWrapper>
<StepHeader
@ -22,11 +58,22 @@ export function TileExecuteScript({
<Row>
<Col xs={12}>
<label>{t("Package Name")}</label>
<StepInputBox dispatch={dispatch}
index={index}
step={currentStep}
sequence={currentSequence}
field="label" />
<FBSelect
key={selectedFarmware().label}
list={farmwareList()}
selectedItem={selectedFarmware()}
onChange={updateStep}
allowEmpty={true}
customNullLabel={"Manual Input"} />
{selectedFarmware() === MANUAL_INPUT &&
<div>
<label>{t("Manual input")}</label>
<StepInputBox dispatch={dispatch}
index={index}
step={currentStep}
sequence={currentSequence}
field="label" />
</div>}
</Col>
</Row>
</StepContent>

View File

@ -69,14 +69,14 @@ export function TileReadPeripheral(props: StepParams) {
selectedItem={selectedItem(currentStep.args.peripheral_id, props.resources)} />
</Col>
<PinMode {...props} />
</Row>
<Row>
<Col xs={6} md={6}>
<label>
<a onClick={() => dispatch(payl)}>
{t("Enter peripheral data manually")}
</a>
</label>
<Col xs={6} md={3}>
<label>{t("Peripheral")}</label>
<div className={"fb-checkbox"}>
<input
type="checkbox"
onChange={() => dispatch(payl)}
checked={true} />
</div>
</Col>
</Row>
</StepContent>

View File

@ -53,14 +53,14 @@ export function TileReadPin(props: StepParams) {
field="label" />
</Col>
<PinMode {...props} />
</Row>
<Row>
<Col xs={6} md={6}>
<label>
<a onClick={() => dispatch(payl)}>
{t("Use existing peripheral instead")}
</a>
</label>
<Col xs={6} md={3}>
<label>{t("Peripheral")}</label>
<div className={"fb-checkbox"}>
<input
type="checkbox"
onChange={() => dispatch(payl)}
checked={false} />
</div>
</Col>
</Row>
</StepContent>

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { t } from "i18next";
import { Button, Classes, MenuItem } from "@blueprintjs/core";
import { ISelectItemRendererProps, Select } from "@blueprintjs/labs";
import { DropDownItem, NULL_CHOICE } from "./fb_select";
import { DropDownItem } from "./fb_select";
const SelectComponent = Select.ofType<DropDownItem | undefined>();
@ -22,6 +22,7 @@ interface Props {
selectedItem: DropDownItem;
onChange: (item: DropDownItem) => void;
isASubMenu?: boolean;
nullChoice: DropDownItem;
}
interface State {
@ -61,7 +62,7 @@ export class FilterSearch extends React.Component<Props, Partial<State>> {
styleFor(item: DropDownItem): string {
const styles = ["filter-search-item"];
if (Object.is(item, NULL_CHOICE)) {
if (Object.is(item, this.props.nullChoice)) {
styles.push("filter-search-item-none");
}
return styles.join(" ");

View File

@ -1,5 +1,5 @@
import * as React from "react";
import { DropDownItem, NULL_CHOICE } from "./fb_select";
import { DropDownItem } from "./fb_select";
import { FilterSearch } from "./filter_search";
import { equals } from "../util";
@ -16,14 +16,21 @@ export interface FBSelectProps {
placeholder?: string | undefined;
/** Extra class names to add. */
extraClass?: string;
/** Custom label for NULL_CHOICE instead of "None". */
customNullLabel?: string;
}
export class FBSelect extends React.Component<FBSelectProps, {}> {
get item() { return this.props.selectedItem || NULL_CHOICE; }
NULL_CHOICE = Object.freeze({
label: this.props.customNullLabel || "None",
value: ""
});
get item() { return this.props.selectedItem || this.NULL_CHOICE; }
get list() {
if (this.props.allowEmpty) {
return this.props.list.concat(NULL_CHOICE);
return this.props.list.concat(this.NULL_CHOICE);
} else {
return this.props.list;
}
@ -39,7 +46,8 @@ export class FBSelect extends React.Component<FBSelectProps, {}> {
<FilterSearch
selectedItem={this.item}
items={this.list}
onChange={this.props.onChange} />
onChange={this.props.onChange}
nullChoice={this.NULL_CHOICE} />
</div>;
}
}