commit
18b29767f7
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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.");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -164,6 +164,7 @@ export interface FarmwareProps {
|
|||
timeOffset: number;
|
||||
syncStatus: SyncStatus | undefined;
|
||||
webAppConfig: Partial<WebAppConfig>;
|
||||
firstPartyFarmwareNames: string[];
|
||||
}
|
||||
|
||||
export interface HardwareSettingsProps {
|
||||
|
|
|
@ -46,6 +46,8 @@ describe("<FarmDesigner/>", () => {
|
|||
origin: undefined,
|
||||
calibrationZ: undefined
|
||||
},
|
||||
tzOffset: 0,
|
||||
getConfigValue: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -25,6 +25,9 @@ describe("<GardenMapLegend />", () => {
|
|||
showSpread: false,
|
||||
showFarmbot: false,
|
||||
showImages: false,
|
||||
dispatch: jest.fn(),
|
||||
tzOffset: 0,
|
||||
getConfigValue: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -85,6 +85,7 @@ function fakeProps(): GardenMapProps {
|
|||
origin: undefined,
|
||||
calibrationZ: undefined
|
||||
},
|
||||
getConfigValue: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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"]
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,8 @@ describe("<FarmwarePage />", () => {
|
|||
images: [],
|
||||
timeOffset: 0,
|
||||
syncStatus: "synced",
|
||||
webAppConfig: {}
|
||||
webAppConfig: {},
|
||||
firstPartyFarmwareNames: []
|
||||
};
|
||||
const wrapper = mount(<FarmwarePage {...props} />);
|
||||
["Take Photo",
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -28,7 +28,8 @@ describe("<WeedDetector />", () => {
|
|||
currentImage: undefined,
|
||||
images: [],
|
||||
syncStatus: "synced",
|
||||
webAppConfig: {}
|
||||
webAppConfig: {},
|
||||
firstPartyFarmwareNames: []
|
||||
};
|
||||
|
||||
it("renders", () => {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,12 @@ describe("<SequenceEditorMiddle/>", () => {
|
|||
syncStatus: "synced",
|
||||
consistent: true,
|
||||
autoSyncEnabled: false,
|
||||
hardwareFlags: fakeHardwareFlags()
|
||||
hardwareFlags: fakeHardwareFlags(),
|
||||
farmwareInfo: {
|
||||
farmwareNames: [],
|
||||
firstPartyFarmwareNames: [],
|
||||
showFirstPartyFarmware: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,12 @@ describe("<Sequences/>", () => {
|
|||
auth,
|
||||
consistent: true,
|
||||
autoSyncEnabled: false,
|
||||
hardwareFlags: fakeHardwareFlags()
|
||||
hardwareFlags: fakeHardwareFlags(),
|
||||
farmwareInfo: {
|
||||
farmwareNames: [],
|
||||
firstPartyFarmwareNames: [],
|
||||
showFirstPartyFarmware: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 />;
|
||||
}
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(" ");
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue