Merge conflicts
commit
6583cd2708
|
@ -63,7 +63,7 @@ GEM
|
||||||
builder (3.2.3)
|
builder (3.2.3)
|
||||||
bunny (2.12.0)
|
bunny (2.12.0)
|
||||||
amq-protocol (~> 2.3, >= 2.3.0)
|
amq-protocol (~> 2.3, >= 2.3.0)
|
||||||
capybara (3.10.1)
|
capybara (3.11.0)
|
||||||
addressable
|
addressable
|
||||||
mini_mime (>= 0.1.3)
|
mini_mime (>= 0.1.3)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
|
@ -77,7 +77,7 @@ GEM
|
||||||
ffi (~> 1.0, >= 1.0.11)
|
ffi (~> 1.0, >= 1.0.11)
|
||||||
choice (0.2.0)
|
choice (0.2.0)
|
||||||
climate_control (0.2.0)
|
climate_control (0.2.0)
|
||||||
codecov (0.1.13)
|
codecov (0.1.14)
|
||||||
json
|
json
|
||||||
simplecov
|
simplecov
|
||||||
url
|
url
|
||||||
|
@ -266,7 +266,7 @@ GEM
|
||||||
thor (>= 0.19.0, < 2.0)
|
thor (>= 0.19.0, < 2.0)
|
||||||
rake (12.3.1)
|
rake (12.3.1)
|
||||||
redis (4.0.3)
|
redis (4.0.3)
|
||||||
regexp_parser (1.2.0)
|
regexp_parser (1.3.0)
|
||||||
representable (3.0.4)
|
representable (3.0.4)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
declarative-option (< 0.2.0)
|
declarative-option (< 0.2.0)
|
||||||
|
|
|
@ -127,4 +127,44 @@ namespace :api do
|
||||||
rebuild_deps if user_typed?("build")
|
rebuild_deps if user_typed?("build")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
VERSION = "tag_name"
|
||||||
|
TIMESTAMP = "created_at"
|
||||||
|
|
||||||
|
desc "Update GlobalConfig to deprecate old FBOS versions"
|
||||||
|
task deprecate: :environment do
|
||||||
|
# Get current version
|
||||||
|
version_str = GlobalConfig.dump.fetch("FBOS_END_OF_LIFE_VERSION")
|
||||||
|
# Convert it to Gem::Version for easy comparisons (>, <, ==, etc)
|
||||||
|
current_version = Gem::Version::new(version_str)
|
||||||
|
# 60 days is the current policy.
|
||||||
|
cutoff = 60.days.ago
|
||||||
|
# Download release data from github
|
||||||
|
stringio = open("https://api.github.com/repos/farmbot/farmbot_os/releases")
|
||||||
|
string = stringio.read
|
||||||
|
data = JSON
|
||||||
|
.parse(string)
|
||||||
|
.map { |x| x.slice(VERSION, TIMESTAMP) } # Only grab keys that matter
|
||||||
|
.reject { |x| x.fetch(VERSION).include?("-") } # Remove RC/Beta releases
|
||||||
|
.map do |x|
|
||||||
|
# Convert string-y version/timestamps to Real ObjectsTM
|
||||||
|
version = Gem::Version::new(x.fetch(VERSION).gsub("v", ""))
|
||||||
|
time = DateTime.parse(x.fetch(TIMESTAMP))
|
||||||
|
Pair.new(version, time)
|
||||||
|
end
|
||||||
|
.select do |pair|
|
||||||
|
# Grab versions that are > current version and outside of cutoff window
|
||||||
|
(pair.head > current_version) && (pair.tail < cutoff)
|
||||||
|
end
|
||||||
|
.sort_by { |p| p.tail } # Sort by release date
|
||||||
|
.last(2) # Grab 2 latest versions (closest to cuttof)
|
||||||
|
.first # Give 'em some leeway, grabbing the 2nd most outdated version.
|
||||||
|
.try(:head) # We might already be up-to-date?
|
||||||
|
if data # ...or not
|
||||||
|
puts "Setting new support target to #{data.to_s}"
|
||||||
|
GlobalConfig # Set the new oldest support version.
|
||||||
|
.find_by(key: "FBOS_END_OF_LIFE_VERSION")
|
||||||
|
.update_attributes!(value: data.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -39,8 +39,8 @@
|
||||||
"@types/lodash": "^4.14.118",
|
"@types/lodash": "^4.14.118",
|
||||||
"@types/markdown-it": "0.0.7",
|
"@types/markdown-it": "0.0.7",
|
||||||
"@types/moxios": "^0.4.5",
|
"@types/moxios": "^0.4.5",
|
||||||
"@types/node": "^10.12.6",
|
"@types/node": "^10.12.8",
|
||||||
"@types/react": "^16.7.4",
|
"@types/react": "^16.7.6",
|
||||||
"@types/react-color": "2.13.6",
|
"@types/react-color": "2.13.6",
|
||||||
"@types/react-dom": "^16.0.9",
|
"@types/react-dom": "^16.0.9",
|
||||||
"@types/react-joyride": "^2.0.1",
|
"@types/react-joyride": "^2.0.1",
|
||||||
|
|
|
@ -13,7 +13,7 @@ jest.mock("axios", () => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
import { destroy, saveAll, initSave } from "../crud";
|
import { destroy, saveAll, initSave, initSaveGetId } from "../crud";
|
||||||
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
|
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
|
||||||
import { createStore, applyMiddleware } from "redux";
|
import { createStore, applyMiddleware } from "redux";
|
||||||
import { resourceReducer } from "../../resources/reducer";
|
import { resourceReducer } from "../../resources/reducer";
|
||||||
|
@ -67,4 +67,14 @@ describe("AJAX data tracking", () => {
|
||||||
store.dispatch(action);
|
store.dispatch(action);
|
||||||
expect(maybeStartTracking).toHaveBeenCalled();
|
expect(maybeStartTracking).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sets consistency when calling initSaveGetId()", () => {
|
||||||
|
// tslint:disable-next-line:no-any
|
||||||
|
const action: any = initSaveGetId("User", {
|
||||||
|
name: "tester123",
|
||||||
|
email: "test@test.com"
|
||||||
|
});
|
||||||
|
store.dispatch(action);
|
||||||
|
expect(maybeStartTracking).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
let mockPost = Promise.resolve({ data: { id: 1 } });
|
||||||
jest.mock("axios", () => ({
|
jest.mock("axios", () => ({
|
||||||
default: {
|
default: {
|
||||||
get: () => Promise.resolve({
|
get: () => Promise.resolve({
|
||||||
|
@ -7,11 +8,12 @@ jest.mock("axios", () => ({
|
||||||
"timezone": "America/Chicago",
|
"timezone": "America/Chicago",
|
||||||
"last_saw_api": "2017-08-30T20:42:35.854Z"
|
"last_saw_api": "2017-08-30T20:42:35.854Z"
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
post: () => mockPost,
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { refresh } from "../crud";
|
import { refresh, initSaveGetId } from "../crud";
|
||||||
import { TaggedDevice, SpecialStatus } from "farmbot";
|
import { TaggedDevice, SpecialStatus } from "farmbot";
|
||||||
import { API } from "../index";
|
import { API } from "../index";
|
||||||
import { Actions } from "../../constants";
|
import { Actions } from "../../constants";
|
||||||
|
@ -62,3 +64,33 @@ describe("successful refresh()", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("initSaveGetId()", () => {
|
||||||
|
it("returns id", async () => {
|
||||||
|
const dispatch = jest.fn();
|
||||||
|
const result = await initSaveGetId("SavedGarden", {})(dispatch);
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
type: Actions.SAVE_RESOURCE_START,
|
||||||
|
payload: expect.objectContaining({ kind: "SavedGarden" })
|
||||||
|
});
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
type: Actions.INIT_RESOURCE,
|
||||||
|
payload: expect.objectContaining({ kind: "SavedGarden" })
|
||||||
|
});
|
||||||
|
await expect(result).toEqual(1);
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
type: Actions.SAVE_RESOURCE_OK,
|
||||||
|
payload: expect.objectContaining({ kind: "SavedGarden" })
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("catches errors", async () => {
|
||||||
|
mockPost = Promise.reject("error");
|
||||||
|
const dispatch = jest.fn();
|
||||||
|
await initSaveGetId("SavedGarden", {})(dispatch).catch(() => { });
|
||||||
|
await expect(dispatch).toHaveBeenLastCalledWith({
|
||||||
|
type: Actions._RESOURCE_NO,
|
||||||
|
payload: expect.objectContaining({ err: "error" })
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -77,10 +77,36 @@ export function init<T extends TaggedResource>(kind: T["kind"],
|
||||||
clean = false): ReduxAction<TaggedResource> {
|
clean = false): ReduxAction<TaggedResource> {
|
||||||
const resource = arrayUnwrap(newTaggedResource(kind, body));
|
const resource = arrayUnwrap(newTaggedResource(kind, body));
|
||||||
resource.specialStatus = SpecialStatus[clean ? "SAVED" : "DIRTY"];
|
resource.specialStatus = SpecialStatus[clean ? "SAVED" : "DIRTY"];
|
||||||
// /** Don't touch this- very important! */
|
|
||||||
return { type: Actions.INIT_RESOURCE, payload: resource };
|
return { type: Actions.INIT_RESOURCE, payload: resource };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Initialize and save a new resource, returning the `id`.
|
||||||
|
* If you don't need the `id` returned, use `initSave` instead.
|
||||||
|
*/
|
||||||
|
export const initSaveGetId =
|
||||||
|
<T extends TaggedResource>(kind: T["kind"], body: T["body"]) =>
|
||||||
|
(dispatch: Function) => {
|
||||||
|
const resource = arrayUnwrap(newTaggedResource(kind, body));
|
||||||
|
resource.specialStatus = SpecialStatus.DIRTY;
|
||||||
|
dispatch({ type: Actions.INIT_RESOURCE, payload: resource });
|
||||||
|
dispatch({ type: Actions.SAVE_RESOURCE_START, payload: resource });
|
||||||
|
maybeStartTracking(resource.uuid);
|
||||||
|
return axios.post<typeof resource.body>(
|
||||||
|
urlFor(resource.kind), resource.body)
|
||||||
|
.then(resp => {
|
||||||
|
dispatch(saveOK(resource));
|
||||||
|
return resp.data.id;
|
||||||
|
})
|
||||||
|
.catch((err: UnsafeError) => {
|
||||||
|
dispatch(updateNO({
|
||||||
|
err,
|
||||||
|
uuid: resource.uuid,
|
||||||
|
statusBeforeError: resource.specialStatus
|
||||||
|
}));
|
||||||
|
return Promise.reject(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export function initSave<T extends TaggedResource>(kind: T["kind"],
|
export function initSave<T extends TaggedResource>(kind: T["kind"],
|
||||||
body: T["body"]) {
|
body: T["body"]) {
|
||||||
return function (dispatch: Function) {
|
return function (dispatch: Function) {
|
||||||
|
|
|
@ -558,6 +558,11 @@ export namespace Content {
|
||||||
trim(`Drag a box around the plants you would like to select.
|
trim(`Drag a box around the plants you would like to select.
|
||||||
Press the back arrow to exit.`);
|
Press the back arrow to exit.`);
|
||||||
|
|
||||||
|
export const SAVED_GARDENS =
|
||||||
|
trim(`Create new gardens from scratch or by copying plants from the
|
||||||
|
current garden. View and edit saved gardens, and, when ready, apply them
|
||||||
|
to the main garden.`);
|
||||||
|
|
||||||
// Farm Events
|
// Farm Events
|
||||||
export const REGIMEN_TODAY_SKIPPED_ITEM_RISK =
|
export const REGIMEN_TODAY_SKIPPED_ITEM_RISK =
|
||||||
trim(`You are scheduling a regimen to run today. Be aware that
|
trim(`You are scheduling a regimen to run today. Be aware that
|
||||||
|
|
|
@ -312,7 +312,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.saved-garden-panel-content {
|
.saved-garden-panel-content {
|
||||||
margin-top: 3rem;
|
margin-top: 5rem;
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
.row {
|
.row {
|
||||||
|
|
|
@ -57,37 +57,42 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.photos {
|
.photos-footer {
|
||||||
.photos-footer {
|
display: flex;
|
||||||
display: flex;
|
position: relative;
|
||||||
position: relative;
|
bottom: 1rem;
|
||||||
left: 2.5rem;
|
justify-content: space-between;
|
||||||
bottom: -1rem;
|
label {
|
||||||
padding-left: 0.5rem;
|
font-weight: normal;
|
||||||
padding-right: 0.5rem;
|
|
||||||
width: 90%;
|
|
||||||
justify-content: space-between;
|
|
||||||
label {
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
span {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.image-metadatas {
|
span {
|
||||||
display: flex;
|
font-weight: bold;
|
||||||
label {
|
|
||||||
margin-left: 1rem;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.image-created-at {
|
}
|
||||||
span {
|
|
||||||
white-space: nowrap;
|
.image-metadata {
|
||||||
}
|
display: flex;
|
||||||
label {
|
label {
|
||||||
margin-right: 0.5rem;
|
margin-left: 1rem;
|
||||||
}
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-created-at {
|
||||||
|
span {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.farmware-button {
|
||||||
|
p {
|
||||||
|
float: right;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
color: $medium_gray;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ const isWorking = (job: JobProgress | undefined) =>
|
||||||
job && (job.status == "working");
|
job && (job.status == "working");
|
||||||
|
|
||||||
/** FBOS update download progress. */
|
/** FBOS update download progress. */
|
||||||
function downloadProgress(job: JobProgress | undefined) {
|
export function downloadProgress(job: JobProgress | undefined) {
|
||||||
if (job && isWorking(job)) {
|
if (job && isWorking(job)) {
|
||||||
switch (job.unit) {
|
switch (job.unit) {
|
||||||
case "bytes":
|
case "bytes":
|
||||||
|
|
|
@ -8,7 +8,8 @@ import {
|
||||||
TaggedSensor,
|
TaggedSensor,
|
||||||
TaggedDiagnosticDump,
|
TaggedDiagnosticDump,
|
||||||
TaggedUser,
|
TaggedUser,
|
||||||
TaggedFarmwareInstallation
|
TaggedFarmwareInstallation,
|
||||||
|
JobProgress,
|
||||||
} from "farmbot";
|
} from "farmbot";
|
||||||
import { ResourceIndex } from "../resources/interfaces";
|
import { ResourceIndex } from "../resources/interfaces";
|
||||||
import { WD_ENV } from "../farmware/weed_detector/remote_env/interfaces";
|
import { WD_ENV } from "../farmware/weed_detector/remote_env/interfaces";
|
||||||
|
@ -223,6 +224,7 @@ export interface FarmwareProps {
|
||||||
shouldDisplay: ShouldDisplay;
|
shouldDisplay: ShouldDisplay;
|
||||||
saveFarmwareEnv: SaveFarmwareEnv;
|
saveFarmwareEnv: SaveFarmwareEnv;
|
||||||
taggedFarmwareInstallations: TaggedFarmwareInstallation[];
|
taggedFarmwareInstallations: TaggedFarmwareInstallation[];
|
||||||
|
imageJobs: JobProgress[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HardwareSettingsProps {
|
export interface HardwareSettingsProps {
|
||||||
|
|
|
@ -10,17 +10,22 @@ jest.mock("../../../history", () => ({ history: { push: jest.fn() } }));
|
||||||
jest.mock("../../../api/crud", () => ({
|
jest.mock("../../../api/crud", () => ({
|
||||||
destroy: jest.fn(),
|
destroy: jest.fn(),
|
||||||
initSave: jest.fn(),
|
initSave: jest.fn(),
|
||||||
|
initSaveGetId: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { API } from "../../../api";
|
import { API } from "../../../api";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {
|
import {
|
||||||
snapshotGarden, applyGarden, destroySavedGarden, closeSavedGarden,
|
snapshotGarden, applyGarden, destroySavedGarden, closeSavedGarden,
|
||||||
openSavedGarden, openOrCloseGarden, newSavedGarden
|
openSavedGarden, openOrCloseGarden, newSavedGarden, unselectSavedGarden,
|
||||||
|
copySavedGarden
|
||||||
} from "../actions";
|
} from "../actions";
|
||||||
import { history } from "../../../history";
|
import { history } from "../../../history";
|
||||||
import { Actions } from "../../../constants";
|
import { Actions } from "../../../constants";
|
||||||
import { destroy, initSave } from "../../../api/crud";
|
import { destroy, initSave, initSaveGetId } from "../../../api/crud";
|
||||||
|
import {
|
||||||
|
fakeSavedGarden, fakePlantTemplate
|
||||||
|
} from "../../../__test_support__/fake_state/resources";
|
||||||
|
|
||||||
describe("snapshotGarden", () => {
|
describe("snapshotGarden", () => {
|
||||||
it("calls the API and lets auto-sync do the rest", () => {
|
it("calls the API and lets auto-sync do the rest", () => {
|
||||||
|
@ -43,10 +48,7 @@ describe("applyGarden", () => {
|
||||||
await applyGarden(4)(dispatch);
|
await applyGarden(4)(dispatch);
|
||||||
expect(axios.patch).toHaveBeenCalledWith(API.current.applyGardenPath(4));
|
expect(axios.patch).toHaveBeenCalledWith(API.current.applyGardenPath(4));
|
||||||
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
|
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
|
||||||
expect(dispatch).toHaveBeenCalledWith({
|
expect(dispatch).toHaveBeenCalledWith(unselectSavedGarden);
|
||||||
type: Actions.CHOOSE_SAVED_GARDEN,
|
|
||||||
payload: undefined
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -55,6 +57,7 @@ describe("destroySavedGarden", () => {
|
||||||
const dispatch = jest.fn(() => Promise.resolve());
|
const dispatch = jest.fn(() => Promise.resolve());
|
||||||
destroySavedGarden("SavedGardenUuid")(dispatch);
|
destroySavedGarden("SavedGardenUuid")(dispatch);
|
||||||
expect(destroy).toHaveBeenCalledWith("SavedGardenUuid");
|
expect(destroy).toHaveBeenCalledWith("SavedGardenUuid");
|
||||||
|
expect(dispatch).toHaveBeenLastCalledWith(unselectSavedGarden);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -63,10 +66,7 @@ describe("closeSavedGarden", () => {
|
||||||
const dispatch = jest.fn();
|
const dispatch = jest.fn();
|
||||||
closeSavedGarden()(dispatch);
|
closeSavedGarden()(dispatch);
|
||||||
expect(history.push).toHaveBeenCalledWith("/app/designer/saved_gardens");
|
expect(history.push).toHaveBeenCalledWith("/app/designer/saved_gardens");
|
||||||
expect(dispatch).toHaveBeenCalledWith({
|
expect(dispatch).toHaveBeenCalledWith(unselectSavedGarden);
|
||||||
type: Actions.CHOOSE_SAVED_GARDEN,
|
|
||||||
payload: undefined
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -111,4 +111,40 @@ describe("newSavedGarden", () => {
|
||||||
expect(initSave).toHaveBeenCalledWith(
|
expect(initSave).toHaveBeenCalledWith(
|
||||||
"SavedGarden", { name: "my saved garden" });
|
"SavedGarden", { name: "my saved garden" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("creates a new saved garden with default name", () => {
|
||||||
|
newSavedGarden("")(jest.fn());
|
||||||
|
expect(initSave).toHaveBeenCalledWith(
|
||||||
|
"SavedGarden", { name: "Untitled Garden" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("copySavedGarden", () => {
|
||||||
|
const fakeProps = () => {
|
||||||
|
const fakeSG = fakeSavedGarden();
|
||||||
|
fakeSG.body.id = 1;
|
||||||
|
const fakePT = fakePlantTemplate();
|
||||||
|
fakePT.body.saved_garden_id = fakeSG.body.id;
|
||||||
|
return {
|
||||||
|
newSGName: "",
|
||||||
|
savedGarden: fakeSG,
|
||||||
|
plantTemplates: [fakePT],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
it("creates copy", async () => {
|
||||||
|
await copySavedGarden(fakeProps())(jest.fn(() => Promise.resolve(5)));
|
||||||
|
expect(initSaveGetId).toHaveBeenCalledWith("SavedGarden",
|
||||||
|
{ name: "Saved Garden 1 (copy)" });
|
||||||
|
await expect(initSave).toHaveBeenCalledWith("PlantTemplate",
|
||||||
|
expect.objectContaining({ saved_garden_id: 5 }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates copy with provided name", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.newSGName = "New copy";
|
||||||
|
copySavedGarden(p)(jest.fn(() => Promise.resolve()));
|
||||||
|
expect(initSaveGetId).toHaveBeenCalledWith("SavedGarden",
|
||||||
|
{ name: p.newSGName });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,19 +8,20 @@ jest.mock("../../../api/crud", () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { shallow } from "enzyme";
|
import { shallow, mount } from "enzyme";
|
||||||
import {
|
import {
|
||||||
fakeSavedGarden
|
fakeSavedGarden, fakePlantTemplate
|
||||||
} from "../../../__test_support__/fake_state/resources";
|
} from "../../../__test_support__/fake_state/resources";
|
||||||
import { edit } from "../../../api/crud";
|
import { edit } from "../../../api/crud";
|
||||||
import { GardenInfo } from "../garden_list";
|
import { GardenInfo, SavedGardenList } from "../garden_list";
|
||||||
|
import { SavedGardenInfoProps, SavedGardensProps } from "../interfaces";
|
||||||
|
|
||||||
describe("<GardenInfo />", () => {
|
describe("<GardenInfo />", () => {
|
||||||
const fakeProps = () => ({
|
const fakeProps = (): SavedGardenInfoProps => ({
|
||||||
dispatch: jest.fn(),
|
dispatch: jest.fn(),
|
||||||
savedGarden: fakeSavedGarden(),
|
savedGarden: fakeSavedGarden(),
|
||||||
gardenIsOpen: false,
|
gardenIsOpen: false,
|
||||||
plantCount: 1,
|
plantTemplateCount: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
it("edits garden name", () => {
|
it("edits garden name", () => {
|
||||||
|
@ -30,3 +31,28 @@ describe("<GardenInfo />", () => {
|
||||||
expect(edit).toHaveBeenCalledWith(expect.any(Object), { name: "new name" });
|
expect(edit).toHaveBeenCalledWith(expect.any(Object), { name: "new name" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("<SavedGardenList />", () => {
|
||||||
|
const fakeProps = (): SavedGardensProps => {
|
||||||
|
const fakeSG = fakeSavedGarden();
|
||||||
|
return {
|
||||||
|
dispatch: jest.fn(),
|
||||||
|
plantPointerCount: 1,
|
||||||
|
savedGardens: [fakeSG],
|
||||||
|
plantTemplates: [fakePlantTemplate(), fakePlantTemplate()],
|
||||||
|
openedSavedGarden: fakeSG.uuid,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
it("renders open garden", () => {
|
||||||
|
const wrapper = mount(<SavedGardenList {...fakeProps()} />);
|
||||||
|
expect(wrapper.text()).toContain("exit");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders gardens closed", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.openedSavedGarden = undefined;
|
||||||
|
const wrapper = mount(<SavedGardenList {...p} />);
|
||||||
|
expect(wrapper.text()).not.toContain("exit");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -5,19 +5,18 @@ jest.mock("axios", () => {
|
||||||
jest.mock("../actions", () => ({
|
jest.mock("../actions", () => ({
|
||||||
snapshotGarden: jest.fn(),
|
snapshotGarden: jest.fn(),
|
||||||
newSavedGarden: jest.fn(),
|
newSavedGarden: jest.fn(),
|
||||||
|
copySavedGarden: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { mount, shallow } from "enzyme";
|
import { mount, shallow } from "enzyme";
|
||||||
import { GardenSnapshotProps, GardenSnapshot } from "../garden_snapshot";
|
import { GardenSnapshotProps, GardenSnapshot } from "../garden_snapshot";
|
||||||
import { clickButton } from "../../../__test_support__/helpers";
|
import { clickButton } from "../../../__test_support__/helpers";
|
||||||
import { error } from "farmbot-toastr";
|
import { snapshotGarden, newSavedGarden, copySavedGarden } from "../actions";
|
||||||
import { snapshotGarden, newSavedGarden } from "../actions";
|
|
||||||
import { fakeSavedGarden } from "../../../__test_support__/fake_state/resources";
|
import { fakeSavedGarden } from "../../../__test_support__/fake_state/resources";
|
||||||
|
|
||||||
describe("<GardenSnapshot />", () => {
|
describe("<GardenSnapshot />", () => {
|
||||||
const fakeProps = (): GardenSnapshotProps => ({
|
const fakeProps = (): GardenSnapshotProps => ({
|
||||||
plantsInGarden: true,
|
|
||||||
currentSavedGarden: undefined,
|
currentSavedGarden: undefined,
|
||||||
plantTemplates: [],
|
plantTemplates: [],
|
||||||
dispatch: jest.fn(),
|
dispatch: jest.fn(),
|
||||||
|
@ -29,24 +28,17 @@ describe("<GardenSnapshot />", () => {
|
||||||
expect(snapshotGarden).toHaveBeenCalledWith("");
|
expect(snapshotGarden).toHaveBeenCalledWith("");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't snapshot saved garden", () => {
|
it("copies saved garden", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.currentSavedGarden = fakeSavedGarden();
|
p.currentSavedGarden = fakeSavedGarden();
|
||||||
const wrapper = mount(<GardenSnapshot {...p} />);
|
const wrapper = mount(<GardenSnapshot {...p} />);
|
||||||
clickButton(wrapper, 0, "snapshot current garden");
|
clickButton(wrapper, 0, "snapshot current garden");
|
||||||
expect(snapshotGarden).not.toHaveBeenCalled();
|
expect(snapshotGarden).not.toHaveBeenCalled();
|
||||||
expect(error).toHaveBeenCalledWith(
|
expect(copySavedGarden).toHaveBeenCalledWith({
|
||||||
expect.stringContaining("while saved garden is open"));
|
newSGName: "",
|
||||||
});
|
plantTemplates: [],
|
||||||
|
savedGarden: p.currentSavedGarden
|
||||||
it("no garden to save", () => {
|
});
|
||||||
const p = fakeProps();
|
|
||||||
p.plantsInGarden = false;
|
|
||||||
const wrapper = mount(<GardenSnapshot {...p} />);
|
|
||||||
clickButton(wrapper, 0, "snapshot current garden");
|
|
||||||
expect(snapshotGarden).not.toHaveBeenCalled();
|
|
||||||
expect(error).toHaveBeenCalledWith(expect.stringContaining(
|
|
||||||
"No plants in garden"));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("changes name", () => {
|
it("changes name", () => {
|
||||||
|
|
|
@ -36,7 +36,7 @@ import { Actions } from "../../../constants";
|
||||||
describe("<SavedGardens />", () => {
|
describe("<SavedGardens />", () => {
|
||||||
const fakeProps = (): SavedGardensProps => ({
|
const fakeProps = (): SavedGardensProps => ({
|
||||||
dispatch: jest.fn(),
|
dispatch: jest.fn(),
|
||||||
plantsInGarden: true,
|
plantPointerCount: 1,
|
||||||
savedGardens: [fakeSavedGarden()],
|
savedGardens: [fakeSavedGarden()],
|
||||||
plantTemplates: [fakePlantTemplate(), fakePlantTemplate()],
|
plantTemplates: [fakePlantTemplate(), fakePlantTemplate()],
|
||||||
openedSavedGarden: undefined,
|
openedSavedGarden: undefined,
|
||||||
|
@ -51,7 +51,7 @@ describe("<SavedGardens />", () => {
|
||||||
it("applies garden", () => {
|
it("applies garden", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.savedGardens[0].uuid = "SavedGarden.1.0";
|
p.savedGardens[0].uuid = "SavedGarden.1.0";
|
||||||
p.plantsInGarden = false;
|
p.plantPointerCount = 0;
|
||||||
const wrapper = mount(<SavedGardens {...p} />);
|
const wrapper = mount(<SavedGardens {...p} />);
|
||||||
clickButton(wrapper, 3, "apply");
|
clickButton(wrapper, 3, "apply");
|
||||||
expect(applyGarden).toHaveBeenCalledWith(1);
|
expect(applyGarden).toHaveBeenCalledWith(1);
|
||||||
|
@ -91,12 +91,12 @@ describe("mapStateToProps()", () => {
|
||||||
const state = fakeState();
|
const state = fakeState();
|
||||||
state.resources = buildResourceIndex([]);
|
state.resources = buildResourceIndex([]);
|
||||||
const result = mapStateToProps(state);
|
const result = mapStateToProps(state);
|
||||||
expect(result.plantsInGarden).toEqual(false);
|
expect(result.plantPointerCount).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("has plants in garden", () => {
|
it("has plants in garden", () => {
|
||||||
const result = mapStateToProps(fakeState());
|
const result = mapStateToProps(fakeState());
|
||||||
expect(result.plantsInGarden).toEqual(true);
|
expect(result.plantPointerCount).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,31 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { API } from "../../api";
|
import { API } from "../../api";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { success } from "farmbot-toastr";
|
import { success, info } from "farmbot-toastr";
|
||||||
import { history } from "../../history";
|
import { history } from "../../history";
|
||||||
import { Actions } from "../../constants";
|
import { Actions } from "../../constants";
|
||||||
import { destroy, initSave } from "../../api/crud";
|
import { destroy, initSave, initSaveGetId } from "../../api/crud";
|
||||||
import { unpackUUID } from "../../util";
|
import { unpackUUID } from "../../util";
|
||||||
import { isString } from "lodash";
|
import { isString } from "lodash";
|
||||||
|
import { TaggedSavedGarden, TaggedPlantTemplate } from "farmbot";
|
||||||
|
|
||||||
|
/** Save all Plant to PlantTemplates in a new SavedGarden. */
|
||||||
export const snapshotGarden = (name?: string | undefined) =>
|
export const snapshotGarden = (name?: string | undefined) =>
|
||||||
axios.post<void>(API.current.snapshotPath, name ? { name } : {})
|
axios.post<void>(API.current.snapshotPath, name ? { name } : {})
|
||||||
.then(() => success(t("Garden Saved.")));
|
.then(() => success(t("Garden Saved.")));
|
||||||
|
|
||||||
const unselectSavedGarden = {
|
export const unselectSavedGarden = {
|
||||||
type: Actions.CHOOSE_SAVED_GARDEN,
|
type: Actions.CHOOSE_SAVED_GARDEN,
|
||||||
payload: undefined
|
payload: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Save a SavedGarden's PlantTemplates as Plants. */
|
||||||
export const applyGarden = (gardenId: number) => (dispatch: Function) => axios
|
export const applyGarden = (gardenId: number) => (dispatch: Function) => axios
|
||||||
.patch<void>(API.current.applyGardenPath(gardenId))
|
.patch<void>(API.current.applyGardenPath(gardenId))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
history.push("/app/designer/plants");
|
history.push("/app/designer/plants");
|
||||||
dispatch(unselectSavedGarden);
|
dispatch(unselectSavedGarden);
|
||||||
|
info(t("while your garden is applied."), t("Please wait"), "blue");
|
||||||
});
|
});
|
||||||
|
|
||||||
export const destroySavedGarden = (uuid: string) => (dispatch: Function) => {
|
export const destroySavedGarden = (uuid: string) => (dispatch: Function) => {
|
||||||
|
@ -42,6 +46,7 @@ export const openSavedGarden = (savedGarden: string) => {
|
||||||
dispatch({ type: Actions.CHOOSE_SAVED_GARDEN, payload: savedGarden });
|
dispatch({ type: Actions.CHOOSE_SAVED_GARDEN, payload: savedGarden });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Open a SavedGarden if it is closed, otherwise close it. */
|
||||||
export const openOrCloseGarden = (props: {
|
export const openOrCloseGarden = (props: {
|
||||||
savedGarden: string | undefined,
|
savedGarden: string | undefined,
|
||||||
gardenIsOpen: boolean,
|
gardenIsOpen: boolean,
|
||||||
|
@ -51,7 +56,36 @@ export const openOrCloseGarden = (props: {
|
||||||
? props.dispatch(openSavedGarden(props.savedGarden))
|
? props.dispatch(openSavedGarden(props.savedGarden))
|
||||||
: props.dispatch(closeSavedGarden());
|
: props.dispatch(closeSavedGarden());
|
||||||
|
|
||||||
|
/** Create a new SavedGarden with the chosen name. */
|
||||||
export const newSavedGarden = (name: string) =>
|
export const newSavedGarden = (name: string) =>
|
||||||
(dispatch: Function) => {
|
(dispatch: Function) => {
|
||||||
dispatch(initSave("SavedGarden", { name: name || "Untitled Garden" }));
|
dispatch(initSave("SavedGarden", { name: name || "Untitled Garden" }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Create a copy of a PlantTemplate body and assign it a new SavedGarden. */
|
||||||
|
const newPTBody =
|
||||||
|
(source: TaggedPlantTemplate, newSGId: number): TaggedPlantTemplate["body"] =>
|
||||||
|
({
|
||||||
|
name: source.body.name,
|
||||||
|
openfarm_slug: source.body.openfarm_slug,
|
||||||
|
saved_garden_id: newSGId,
|
||||||
|
radius: source.body.radius,
|
||||||
|
x: source.body.x,
|
||||||
|
y: source.body.y,
|
||||||
|
z: source.body.z,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Copy a SavedGarden and all of its PlantTemplates. */
|
||||||
|
export const copySavedGarden = ({ newSGName, savedGarden, plantTemplates }: {
|
||||||
|
newSGName: string,
|
||||||
|
savedGarden: TaggedSavedGarden,
|
||||||
|
plantTemplates: TaggedPlantTemplate[]
|
||||||
|
}) =>
|
||||||
|
(dispatch: Function) => {
|
||||||
|
const sourceSavedGardenId = savedGarden.body.id;
|
||||||
|
const name = newSGName || `${savedGarden.body.name} (${t("copy")})`;
|
||||||
|
dispatch(initSaveGetId(savedGarden.kind, { name }))
|
||||||
|
.then((newSGId: number) => plantTemplates
|
||||||
|
.filter(x => x.body.saved_garden_id === sourceSavedGardenId)
|
||||||
|
.map(x => dispatch(initSave(x.kind, newPTBody(x, newSGId)))));
|
||||||
|
};
|
||||||
|
|
|
@ -5,17 +5,13 @@ import { error } from "farmbot-toastr";
|
||||||
import { isNumber, isString } from "lodash";
|
import { isNumber, isString } from "lodash";
|
||||||
import { openOrCloseGarden, applyGarden, destroySavedGarden } from "./actions";
|
import { openOrCloseGarden, applyGarden, destroySavedGarden } from "./actions";
|
||||||
import {
|
import {
|
||||||
SavedGardensProps, GardenViewButtonProps, SavedGardenItemProps
|
SavedGardensProps, GardenViewButtonProps, SavedGardenItemProps,
|
||||||
|
SavedGardenInfoProps
|
||||||
} from "./interfaces";
|
} from "./interfaces";
|
||||||
import { TaggedSavedGarden } from "farmbot";
|
|
||||||
import { edit, save } from "../../api/crud";
|
import { edit, save } from "../../api/crud";
|
||||||
|
|
||||||
export const GardenInfo = (props: {
|
/** Name input and PlantTemplate count for a single SavedGarden. */
|
||||||
savedGarden: TaggedSavedGarden,
|
export const GardenInfo = (props: SavedGardenInfoProps) => {
|
||||||
gardenIsOpen: boolean,
|
|
||||||
plantCount: number,
|
|
||||||
dispatch: Function
|
|
||||||
}) => {
|
|
||||||
const { savedGarden, gardenIsOpen, dispatch } = props;
|
const { savedGarden, gardenIsOpen, dispatch } = props;
|
||||||
return <div className="saved-garden-info"
|
return <div className="saved-garden-info"
|
||||||
onClick={openOrCloseGarden({
|
onClick={openOrCloseGarden({
|
||||||
|
@ -30,11 +26,12 @@ export const GardenInfo = (props: {
|
||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={2}>
|
<Col xs={2}>
|
||||||
<p style={{ textAlign: "center" }}>{props.plantCount}</p>
|
<p style={{ textAlign: "center" }}>{props.plantTemplateCount}</p>
|
||||||
</Col>
|
</Col>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Open or close a SavedGarden. */
|
||||||
const GardenViewButton = (props: GardenViewButtonProps) => {
|
const GardenViewButton = (props: GardenViewButtonProps) => {
|
||||||
const { dispatch, savedGarden, gardenIsOpen } = props;
|
const { dispatch, savedGarden, gardenIsOpen } = props;
|
||||||
const onClick = openOrCloseGarden({ savedGarden, gardenIsOpen, dispatch });
|
const onClick = openOrCloseGarden({ savedGarden, gardenIsOpen, dispatch });
|
||||||
|
@ -48,12 +45,14 @@ const GardenViewButton = (props: GardenViewButtonProps) => {
|
||||||
</button>;
|
</button>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Apply a SavedGarden after checking that the current garden is empty. */
|
||||||
const ApplyGardenButton =
|
const ApplyGardenButton =
|
||||||
(props: { plantsInGarden: boolean, gardenId: number, dispatch: Function }) =>
|
(props: { plantPointerCount: number, gardenId: number, dispatch: Function }) =>
|
||||||
<button
|
<button
|
||||||
className="fb-button green"
|
className="fb-button green"
|
||||||
onClick={() => props.plantsInGarden
|
onClick={() => props.plantPointerCount > 0
|
||||||
? error(t("Please clear current garden first."))
|
? error(t("Please clear current garden first. ({{plantCount}} plants)",
|
||||||
|
{ plantCount: props.plantPointerCount }))
|
||||||
: props.dispatch(applyGarden(props.gardenId))}>
|
: props.dispatch(applyGarden(props.gardenId))}>
|
||||||
{t("apply")}
|
{t("apply")}
|
||||||
</button>;
|
</button>;
|
||||||
|
@ -66,6 +65,7 @@ const DestroyGardenButton =
|
||||||
<i className="fa fa-times" />
|
<i className="fa fa-times" />
|
||||||
</button>;
|
</button>;
|
||||||
|
|
||||||
|
/** Info and actions for a single SavedGarden. */
|
||||||
const SavedGardenItem = (props: SavedGardenItemProps) => {
|
const SavedGardenItem = (props: SavedGardenItemProps) => {
|
||||||
return <div
|
return <div
|
||||||
className={`saved-garden-row ${props.gardenIsOpen ? "selected" : ""}`}>
|
className={`saved-garden-row ${props.gardenIsOpen ? "selected" : ""}`}>
|
||||||
|
@ -73,7 +73,7 @@ const SavedGardenItem = (props: SavedGardenItemProps) => {
|
||||||
<GardenInfo
|
<GardenInfo
|
||||||
savedGarden={props.savedGarden}
|
savedGarden={props.savedGarden}
|
||||||
gardenIsOpen={props.gardenIsOpen}
|
gardenIsOpen={props.gardenIsOpen}
|
||||||
plantCount={props.plantCount}
|
plantTemplateCount={props.plantTemplateCount}
|
||||||
dispatch={props.dispatch} />
|
dispatch={props.dispatch} />
|
||||||
<Col xs={6}>
|
<Col xs={6}>
|
||||||
<DestroyGardenButton
|
<DestroyGardenButton
|
||||||
|
@ -81,7 +81,7 @@ const SavedGardenItem = (props: SavedGardenItemProps) => {
|
||||||
gardenUuid={props.savedGarden.uuid} />
|
gardenUuid={props.savedGarden.uuid} />
|
||||||
<ApplyGardenButton
|
<ApplyGardenButton
|
||||||
dispatch={props.dispatch}
|
dispatch={props.dispatch}
|
||||||
plantsInGarden={props.plantsInGarden}
|
plantPointerCount={props.plantPointerCount}
|
||||||
gardenId={props.savedGarden.body.id || -1} />
|
gardenId={props.savedGarden.body.id || -1} />
|
||||||
<GardenViewButton
|
<GardenViewButton
|
||||||
dispatch={props.dispatch}
|
dispatch={props.dispatch}
|
||||||
|
@ -92,6 +92,7 @@ const SavedGardenItem = (props: SavedGardenItemProps) => {
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Info and action list for all SavedGardens. */
|
||||||
export const SavedGardenList = (props: SavedGardensProps) =>
|
export const SavedGardenList = (props: SavedGardensProps) =>
|
||||||
<div className="saved-garden-list">
|
<div className="saved-garden-list">
|
||||||
<Row>
|
<Row>
|
||||||
|
@ -112,9 +113,9 @@ export const SavedGardenList = (props: SavedGardensProps) =>
|
||||||
savedGarden={sg}
|
savedGarden={sg}
|
||||||
gardenIsOpen={sg.uuid === props.openedSavedGarden}
|
gardenIsOpen={sg.uuid === props.openedSavedGarden}
|
||||||
dispatch={props.dispatch}
|
dispatch={props.dispatch}
|
||||||
plantCount={props.plantTemplates.filter(pt =>
|
plantPointerCount={props.plantPointerCount}
|
||||||
pt.body.saved_garden_id === sg.body.id).length}
|
plantTemplateCount={props.plantTemplates.filter(pt =>
|
||||||
plantsInGarden={props.plantsInGarden} />;
|
pt.body.saved_garden_id === sg.body.id).length} />;
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { error } from "farmbot-toastr";
|
import { snapshotGarden, newSavedGarden, copySavedGarden } from "./actions";
|
||||||
import { snapshotGarden, newSavedGarden } from "./actions";
|
|
||||||
import { TaggedPlantTemplate, TaggedSavedGarden } from "farmbot";
|
import { TaggedPlantTemplate, TaggedSavedGarden } from "farmbot";
|
||||||
|
|
||||||
export interface GardenSnapshotProps {
|
export interface GardenSnapshotProps {
|
||||||
plantsInGarden: boolean;
|
|
||||||
currentSavedGarden: TaggedSavedGarden | undefined;
|
currentSavedGarden: TaggedSavedGarden | undefined;
|
||||||
plantTemplates: TaggedPlantTemplate[];
|
plantTemplates: TaggedPlantTemplate[];
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
|
@ -15,23 +13,21 @@ interface GardenSnapshotState {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GARDEN_OPEN_ERROR = t("Can't snapshot while saved garden is open.");
|
/** New SavedGarden name input and snapshot/create buttons. */
|
||||||
const NO_PLANTS_ERROR = t("No plants in garden. Create some plants first.");
|
|
||||||
|
|
||||||
export class GardenSnapshot
|
export class GardenSnapshot
|
||||||
extends React.Component<GardenSnapshotProps, GardenSnapshotState> {
|
extends React.Component<GardenSnapshotProps, GardenSnapshotState> {
|
||||||
state = { name: "" };
|
state = { name: "" };
|
||||||
|
|
||||||
snapshot = () => {
|
snapshot = () => {
|
||||||
const { currentSavedGarden } = this.props;
|
const { currentSavedGarden, plantTemplates } = this.props;
|
||||||
if (!currentSavedGarden) {
|
!currentSavedGarden
|
||||||
this.props.plantsInGarden
|
? snapshotGarden(this.state.name)
|
||||||
? snapshotGarden(this.state.name)
|
: this.props.dispatch(copySavedGarden({
|
||||||
: error(NO_PLANTS_ERROR);
|
newSGName: this.state.name,
|
||||||
this.setState({ name: "" });
|
savedGarden: currentSavedGarden,
|
||||||
} else {
|
plantTemplates
|
||||||
error(GARDEN_OPEN_ERROR);
|
}));
|
||||||
}
|
this.setState({ name: "" });
|
||||||
}
|
}
|
||||||
|
|
||||||
new = () => {
|
new = () => {
|
||||||
|
@ -40,18 +36,13 @@ export class GardenSnapshot
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const disabledClassName =
|
|
||||||
this.props.currentSavedGarden ? "pseudo-disabled" : "";
|
|
||||||
return <div className="garden-snapshot">
|
return <div className="garden-snapshot">
|
||||||
<label>{t("garden name")}</label>
|
<label>{t("new garden name")}</label>
|
||||||
<input
|
<input
|
||||||
onChange={e => this.setState({ name: e.currentTarget.value })}
|
onChange={e => this.setState({ name: e.currentTarget.value })}
|
||||||
value={this.state.name} />
|
value={this.state.name} />
|
||||||
<button
|
<button
|
||||||
title={this.props.currentSavedGarden
|
className={"fb-button gray wide"}
|
||||||
? GARDEN_OPEN_ERROR
|
|
||||||
: ""}
|
|
||||||
className={`fb-button gray wide ${disabledClassName}`}
|
|
||||||
onClick={this.snapshot}>
|
onClick={this.snapshot}>
|
||||||
{t("Snapshot current garden")}
|
{t("Snapshot current garden")}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -4,7 +4,7 @@ export interface SavedGardensProps {
|
||||||
savedGardens: TaggedSavedGarden[];
|
savedGardens: TaggedSavedGarden[];
|
||||||
plantTemplates: TaggedPlantTemplate[];
|
plantTemplates: TaggedPlantTemplate[];
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
plantsInGarden: boolean;
|
plantPointerCount: number;
|
||||||
openedSavedGarden: string | undefined;
|
openedSavedGarden: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,13 @@ export interface SavedGardenItemProps {
|
||||||
savedGarden: TaggedSavedGarden;
|
savedGarden: TaggedSavedGarden;
|
||||||
gardenIsOpen: boolean;
|
gardenIsOpen: boolean;
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
plantCount: number;
|
plantPointerCount: number;
|
||||||
plantsInGarden: boolean;
|
plantTemplateCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SavedGardenInfoProps {
|
||||||
|
savedGarden: TaggedSavedGarden;
|
||||||
|
gardenIsOpen: boolean;
|
||||||
|
plantTemplateCount: number;
|
||||||
|
dispatch: Function;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,16 +12,15 @@ import { SavedGardenList } from "./garden_list";
|
||||||
import { SavedGardensProps } from "./interfaces";
|
import { SavedGardensProps } from "./interfaces";
|
||||||
import { closeSavedGarden } from "./actions";
|
import { closeSavedGarden } from "./actions";
|
||||||
import { TaggedSavedGarden } from "farmbot";
|
import { TaggedSavedGarden } from "farmbot";
|
||||||
|
import { Content } from "../../constants";
|
||||||
|
|
||||||
export function mapStateToProps(props: Everything): SavedGardensProps {
|
export const mapStateToProps = (props: Everything): SavedGardensProps => ({
|
||||||
return {
|
savedGardens: selectAllSavedGardens(props.resources.index),
|
||||||
savedGardens: selectAllSavedGardens(props.resources.index),
|
plantTemplates: selectAllPlantTemplates(props.resources.index),
|
||||||
plantTemplates: selectAllPlantTemplates(props.resources.index),
|
dispatch: props.dispatch,
|
||||||
dispatch: props.dispatch,
|
plantPointerCount: selectAllPlantPointers(props.resources.index).length,
|
||||||
plantsInGarden: selectAllPlantPointers(props.resources.index).length > 0,
|
openedSavedGarden: props.resources.consumers.farm_designer.openedSavedGarden,
|
||||||
openedSavedGarden: props.resources.consumers.farm_designer.openedSavedGarden,
|
});
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@connect(mapStateToProps)
|
@connect(mapStateToProps)
|
||||||
export class SavedGardens extends React.Component<SavedGardensProps, {}> {
|
export class SavedGardens extends React.Component<SavedGardensProps, {}> {
|
||||||
|
@ -46,13 +45,12 @@ export class SavedGardens extends React.Component<SavedGardensProps, {}> {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="panel-header-description">
|
<div className="panel-header-description">
|
||||||
{t("Save or load a garden.")}
|
{t(Content.SAVED_GARDENS)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="panel-content saved-garden-panel-content">
|
<div className="panel-content saved-garden-panel-content">
|
||||||
<GardenSnapshot
|
<GardenSnapshot
|
||||||
plantsInGarden={this.props.plantsInGarden}
|
|
||||||
currentSavedGarden={this.currentSavedGarden}
|
currentSavedGarden={this.currentSavedGarden}
|
||||||
plantTemplates={this.props.plantTemplates}
|
plantTemplates={this.props.plantTemplates}
|
||||||
dispatch={this.props.dispatch} />
|
dispatch={this.props.dispatch} />
|
||||||
|
@ -65,6 +63,7 @@ export class SavedGardens extends React.Component<SavedGardensProps, {}> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Link to SavedGardens panel for garden map legend. */
|
||||||
export const SavedGardensLink = () =>
|
export const SavedGardensLink = () =>
|
||||||
<button className="fb-button green"
|
<button className="fb-button green"
|
||||||
hidden={!(localStorage.getItem("FUTURE_FEATURES"))}
|
hidden={!(localStorage.getItem("FUTURE_FEATURES"))}
|
||||||
|
@ -72,10 +71,12 @@ export const SavedGardensLink = () =>
|
||||||
{t("Saved Gardens")}
|
{t("Saved Gardens")}
|
||||||
</button>;
|
</button>;
|
||||||
|
|
||||||
|
/** Check if a SavedGarden is currently open (URL approach). */
|
||||||
export const savedGardenOpen = (pathArray: string[]) =>
|
export const savedGardenOpen = (pathArray: string[]) =>
|
||||||
pathArray[3] === "saved_gardens" && parseInt(pathArray[4]) > 0
|
pathArray[3] === "saved_gardens" && parseInt(pathArray[4]) > 0
|
||||||
? parseInt(pathArray[4]) : false;
|
? parseInt(pathArray[4]) : false;
|
||||||
|
|
||||||
|
/** Sticky an indicator and actions menu when a SavedGarden is open. */
|
||||||
export const SavedGardenHUD = (props: { dispatch: Function }) =>
|
export const SavedGardenHUD = (props: { dispatch: Function }) =>
|
||||||
<div className="saved-garden-indicator">
|
<div className="saved-garden-indicator">
|
||||||
<label>{t("Viewing saved garden")}</label>
|
<label>{t("Viewing saved garden")}</label>
|
||||||
|
|
|
@ -42,6 +42,7 @@ describe("<FarmwarePage />", () => {
|
||||||
shouldDisplay: () => false,
|
shouldDisplay: () => false,
|
||||||
saveFarmwareEnv: jest.fn(),
|
saveFarmwareEnv: jest.fn(),
|
||||||
taggedFarmwareInstallations: [],
|
taggedFarmwareInstallations: [],
|
||||||
|
imageJobs: [],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
} from "../../__test_support__/fake_state/resources";
|
} from "../../__test_support__/fake_state/resources";
|
||||||
import { edit, initSave, save } from "../../api/crud";
|
import { edit, initSave, save } from "../../api/crud";
|
||||||
import { fakeFarmware } from "../../__test_support__/fake_farmwares";
|
import { fakeFarmware } from "../../__test_support__/fake_farmwares";
|
||||||
|
import { JobProgress } from "farmbot";
|
||||||
|
|
||||||
describe("mapStateToProps()", () => {
|
describe("mapStateToProps()", () => {
|
||||||
|
|
||||||
|
@ -79,6 +80,44 @@ describe("mapStateToProps()", () => {
|
||||||
[botFarmwareName]: botFarmware
|
[botFarmwareName]: botFarmware
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns image upload job list", () => {
|
||||||
|
const state = fakeState();
|
||||||
|
state.bot.hardware.jobs = {
|
||||||
|
"img1.png": {
|
||||||
|
status: "working",
|
||||||
|
percent: 20,
|
||||||
|
unit: "percent",
|
||||||
|
time: "2018-11-15 18:13:21.167440Z",
|
||||||
|
} as JobProgress,
|
||||||
|
"FBOS_OTA": {
|
||||||
|
status: "working",
|
||||||
|
percent: 10,
|
||||||
|
unit: "percent",
|
||||||
|
time: "2018-11-15 17:13:21.167440Z",
|
||||||
|
} as JobProgress,
|
||||||
|
"img2.png": {
|
||||||
|
status: "working",
|
||||||
|
percent: 10,
|
||||||
|
unit: "percent",
|
||||||
|
time: "2018-11-15 19:13:21.167440Z",
|
||||||
|
} as JobProgress,
|
||||||
|
};
|
||||||
|
const props = mapStateToProps(state);
|
||||||
|
expect(props.imageJobs).toEqual([
|
||||||
|
{
|
||||||
|
status: "working",
|
||||||
|
percent: 10,
|
||||||
|
unit: "percent",
|
||||||
|
time: "2018-11-15 19:13:21.167440Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: "working",
|
||||||
|
percent: 20,
|
||||||
|
unit: "percent",
|
||||||
|
time: "2018-11-15 18:13:21.167440Z"
|
||||||
|
}]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("saveOrEditFarmwareEnv()", () => {
|
describe("saveOrEditFarmwareEnv()", () => {
|
||||||
|
|
|
@ -35,6 +35,7 @@ describe("<CameraCalibration/>", () => {
|
||||||
syncStatus: "synced",
|
syncStatus: "synced",
|
||||||
shouldDisplay: () => false,
|
shouldDisplay: () => false,
|
||||||
saveFarmwareEnv: jest.fn(),
|
saveFarmwareEnv: jest.fn(),
|
||||||
|
timeOffset: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
|
|
|
@ -45,6 +45,7 @@ export class CameraCalibration extends
|
||||||
images={this.props.images}
|
images={this.props.images}
|
||||||
currentImage={this.props.currentImage}
|
currentImage={this.props.currentImage}
|
||||||
onChange={this.change}
|
onChange={this.change}
|
||||||
|
timeOffset={this.props.timeOffset}
|
||||||
iteration={this.props.iteration}
|
iteration={this.props.iteration}
|
||||||
morph={this.props.morph}
|
morph={this.props.morph}
|
||||||
blur={this.props.blur}
|
blur={this.props.blur}
|
||||||
|
|
|
@ -21,4 +21,5 @@ export interface CameraCalibrationProps {
|
||||||
syncStatus: SyncStatus | undefined;
|
syncStatus: SyncStatus | undefined;
|
||||||
shouldDisplay: ShouldDisplay;
|
shouldDisplay: ShouldDisplay;
|
||||||
saveFarmwareEnv: SaveFarmwareEnv;
|
saveFarmwareEnv: SaveFarmwareEnv;
|
||||||
|
timeOffset: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import "../../../__test_support__/unmock_i18next";
|
import "../../../__test_support__/unmock_i18next";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { shallow, mount } from "enzyme";
|
import { shallow, mount } from "enzyme";
|
||||||
import { ImageFlipper } from "../image_flipper";
|
import { ImageFlipper, PLACEHOLDER_FARMBOT } from "../image_flipper";
|
||||||
import { fakeImages } from "../../../__test_support__/fake_state/images";
|
import { fakeImages } from "../../../__test_support__/fake_state/images";
|
||||||
import { TaggedImage } from "farmbot";
|
import { TaggedImage } from "farmbot";
|
||||||
import { defensiveClone } from "../../../util";
|
import { defensiveClone } from "../../../util";
|
||||||
|
import { ImageFlipperProps } from "../interfaces";
|
||||||
|
|
||||||
describe("<ImageFlipper/>", () => {
|
describe("<ImageFlipper/>", () => {
|
||||||
function prepareImages(data: TaggedImage[]): TaggedImage[] {
|
function prepareImages(data: TaggedImage[]): TaggedImage[] {
|
||||||
|
@ -17,114 +18,104 @@ describe("<ImageFlipper/>", () => {
|
||||||
return images;
|
return images;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fakeProps = (): ImageFlipperProps => ({
|
||||||
|
images: prepareImages(fakeImages),
|
||||||
|
currentImage: undefined,
|
||||||
|
onFlip: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
it("defaults to index 0 and flips up", () => {
|
it("defaults to index 0 and flips up", () => {
|
||||||
const onFlip = jest.fn();
|
const p = fakeProps();
|
||||||
const currentImage = undefined;
|
const x = shallow(<ImageFlipper {...p} />);
|
||||||
const images = prepareImages(fakeImages);
|
|
||||||
const props = { images, currentImage, onFlip };
|
|
||||||
const x = shallow(<ImageFlipper {...props} />);
|
|
||||||
const up = (x.instance() as ImageFlipper).go(1);
|
const up = (x.instance() as ImageFlipper).go(1);
|
||||||
up();
|
up();
|
||||||
expect(onFlip).toHaveBeenCalledWith(images[1].uuid);
|
expect(p.onFlip).toHaveBeenCalledWith(p.images[1].uuid);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("flips down", () => {
|
it("flips down", () => {
|
||||||
const onFlip = jest.fn();
|
const p = fakeProps();
|
||||||
const images = prepareImages(fakeImages);
|
p.currentImage = p.images[1];
|
||||||
const currentImage = images[1];
|
const x = shallow(<ImageFlipper {...p} />);
|
||||||
const props = { images, currentImage, onFlip };
|
|
||||||
const x = shallow(<ImageFlipper {...props} />);
|
|
||||||
const down = (x.instance() as ImageFlipper).go(-1);
|
const down = (x.instance() as ImageFlipper).go(-1);
|
||||||
down();
|
down();
|
||||||
expect(onFlip).toHaveBeenCalledWith(images[0].uuid);
|
expect(p.onFlip).toHaveBeenCalledWith(p.images[0].uuid);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("stops at upper end", () => {
|
it("stops at upper end", () => {
|
||||||
const onFlip = jest.fn();
|
const p = fakeProps();
|
||||||
const images = prepareImages(fakeImages);
|
p.currentImage = p.images[2];
|
||||||
const currentImage = images[2];
|
const x = shallow(<ImageFlipper {...p} />);
|
||||||
const props = { images, currentImage, onFlip };
|
|
||||||
const x = shallow(<ImageFlipper {...props} />);
|
|
||||||
const up = (x.instance() as ImageFlipper).go(1);
|
const up = (x.instance() as ImageFlipper).go(1);
|
||||||
up();
|
up();
|
||||||
expect(onFlip).not.toHaveBeenCalled();
|
expect(p.onFlip).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("stops at lower end", () => {
|
it("stops at lower end", () => {
|
||||||
const images = prepareImages(fakeImages);
|
const p = fakeProps();
|
||||||
const props = {
|
p.currentImage = p.images[0];
|
||||||
images,
|
const x = shallow(<ImageFlipper {...p} />);
|
||||||
currentImage: images[0],
|
|
||||||
onFlip: jest.fn()
|
|
||||||
};
|
|
||||||
const x = shallow(<ImageFlipper {...props} />);
|
|
||||||
const down = (x.instance() as ImageFlipper).go(-1);
|
const down = (x.instance() as ImageFlipper).go(-1);
|
||||||
down();
|
down();
|
||||||
expect(props.onFlip).not.toHaveBeenCalled();
|
expect(p.onFlip).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disables flippers when no images", () => {
|
it("disables flippers when no images", () => {
|
||||||
const onFlip = jest.fn();
|
const p = fakeProps();
|
||||||
const images = prepareImages([]);
|
p.images = prepareImages([]);
|
||||||
const currentImage = undefined;
|
const wrapper = shallow(<ImageFlipper {...p} />);
|
||||||
const props = { images, currentImage, onFlip };
|
|
||||||
const wrapper = shallow(<ImageFlipper {...props} />);
|
|
||||||
expect(wrapper.find("button").first().props().disabled).toBeTruthy();
|
expect(wrapper.find("button").first().props().disabled).toBeTruthy();
|
||||||
expect(wrapper.find("button").last().props().disabled).toBeTruthy();
|
expect(wrapper.find("button").last().props().disabled).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disables flippers when only one image", () => {
|
it("disables flippers when only one image", () => {
|
||||||
const onFlip = jest.fn();
|
const p = fakeProps();
|
||||||
const images = prepareImages([fakeImages[0]]);
|
p.images = prepareImages([fakeImages[0]]);
|
||||||
const currentImage = undefined;
|
const wrapper = shallow(<ImageFlipper {...p} />);
|
||||||
const props = { images, currentImage, onFlip };
|
|
||||||
const wrapper = shallow(<ImageFlipper {...props} />);
|
|
||||||
expect(wrapper.find("button").first().props().disabled).toBeTruthy();
|
expect(wrapper.find("button").first().props().disabled).toBeTruthy();
|
||||||
expect(wrapper.find("button").last().props().disabled).toBeTruthy();
|
expect(wrapper.find("button").last().props().disabled).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disables next flipper on load", () => {
|
it("disables next flipper on load", () => {
|
||||||
const onFlip = jest.fn();
|
const wrapper = shallow(<ImageFlipper {...fakeProps()} />);
|
||||||
const images = prepareImages(fakeImages);
|
|
||||||
const currentImage = undefined;
|
|
||||||
const props = { images, currentImage, onFlip };
|
|
||||||
const wrapper = shallow(<ImageFlipper {...props} />);
|
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
expect(wrapper.find("button").first().props().disabled).toBeFalsy();
|
expect(wrapper.find("button").first().props().disabled).toBeFalsy();
|
||||||
expect(wrapper.find("button").last().props().disabled).toBeTruthy();
|
expect(wrapper.find("button").last().props().disabled).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disables flipper at lower end", () => {
|
it("disables flipper at lower end", () => {
|
||||||
const onFlip = jest.fn();
|
const p = fakeProps();
|
||||||
const images = prepareImages(fakeImages);
|
p.currentImage = p.images[1];
|
||||||
const currentImage = images[1];
|
const wrapper = shallow(<ImageFlipper {...p} />);
|
||||||
const props = { images, currentImage, onFlip };
|
|
||||||
const wrapper = shallow(<ImageFlipper {...props} />);
|
|
||||||
wrapper.setState({ disableNext: false });
|
wrapper.setState({ disableNext: false });
|
||||||
const nextButton = wrapper.render().find("button").last();
|
const nextButton = wrapper.render().find("button").last();
|
||||||
expect(nextButton.text().toLowerCase()).toBe("next");
|
expect(nextButton.text().toLowerCase()).toBe("next");
|
||||||
expect(nextButton.prop("disabled")).toBeFalsy();
|
expect(nextButton.prop("disabled")).toBeFalsy();
|
||||||
wrapper.find("button").last().simulate("click");
|
wrapper.find("button").last().simulate("click");
|
||||||
expect(onFlip).toHaveBeenLastCalledWith(images[0].uuid);
|
expect(p.onFlip).toHaveBeenLastCalledWith(p.images[0].uuid);
|
||||||
expect(wrapper.find("button").last().render().prop("disabled")).toBeTruthy();
|
expect(wrapper.find("button").last().render().prop("disabled")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disables flipper at upper end", () => {
|
it("disables flipper at upper end", () => {
|
||||||
const onFlip = jest.fn();
|
const p = fakeProps();
|
||||||
const images = prepareImages(fakeImages);
|
p.currentImage = p.images[1];
|
||||||
const currentImage = images[1];
|
const wrapper = mount(<ImageFlipper {...p} />);
|
||||||
const props = { images, currentImage, onFlip };
|
|
||||||
const wrapper = mount(<ImageFlipper {...props} />);
|
|
||||||
const prevButton = wrapper.find("button").first();
|
const prevButton = wrapper.find("button").first();
|
||||||
expect(prevButton.text().toLowerCase()).toBe("prev");
|
expect(prevButton.text().toLowerCase()).toBe("prev");
|
||||||
expect(prevButton.props().disabled).toBeFalsy();
|
expect(prevButton.props().disabled).toBeFalsy();
|
||||||
prevButton.simulate("click");
|
prevButton.simulate("click");
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
// FAILED
|
// FAILED
|
||||||
expect(onFlip).toHaveBeenCalledWith(images[2].uuid);
|
expect(p.onFlip).toHaveBeenCalledWith(p.images[2].uuid);
|
||||||
expect(wrapper.find("button").first().render().prop("disabled")).toBeTruthy();
|
expect(wrapper.find("button").first().render().prop("disabled")).toBeTruthy();
|
||||||
prevButton.simulate("click");
|
prevButton.simulate("click");
|
||||||
expect(onFlip).toHaveBeenCalledTimes(1);
|
expect(p.onFlip).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders placeholder", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.images[0].body.attachment_processed_at = undefined;
|
||||||
|
p.currentImage = p.images[0];
|
||||||
|
const wrapper = mount(<ImageFlipper {...p} />);
|
||||||
|
expect(wrapper.find("img").last().props().src).toEqual(PLACEHOLDER_FARMBOT);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,60 +1,135 @@
|
||||||
jest.mock("../../../api/crud", () => ({
|
const mockDevice = { takePhoto: jest.fn(() => Promise.resolve({})) };
|
||||||
destroy: jest.fn(),
|
jest.mock("../../../device", () => ({ getDevice: () => mockDevice }));
|
||||||
}));
|
|
||||||
|
jest.mock("../../../api/crud", () => ({ destroy: jest.fn() }));
|
||||||
|
|
||||||
|
jest.mock("../actions", () => ({ selectImage: jest.fn() }));
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { mount } from "enzyme";
|
import { mount, shallow } from "enzyme";
|
||||||
import { Photos } from "../photos";
|
import { Photos } from "../photos";
|
||||||
import { TaggedImage } from "farmbot";
|
import { JobProgress } from "farmbot";
|
||||||
import { fakeImages } from "../../../__test_support__/fake_state/images";
|
import { fakeImages } from "../../../__test_support__/fake_state/images";
|
||||||
import { defensiveClone } from "../../../util";
|
|
||||||
import { destroy } from "../../../api/crud";
|
import { destroy } from "../../../api/crud";
|
||||||
import { clickButton } from "../../../__test_support__/helpers";
|
import { clickButton } from "../../../__test_support__/helpers";
|
||||||
|
import { PhotosProps } from "../interfaces";
|
||||||
|
import { success, error } from "farmbot-toastr";
|
||||||
|
import { selectImage } from "../actions";
|
||||||
|
|
||||||
describe("<Photos/>", () => {
|
describe("<Photos/>", () => {
|
||||||
function prepareImages(data: TaggedImage[]): TaggedImage[] {
|
const fakeProps = (): PhotosProps => ({
|
||||||
const images: TaggedImage[] = [];
|
images: [],
|
||||||
data.forEach((item, index) => {
|
currentImage: undefined,
|
||||||
const image = defensiveClone(item);
|
dispatch: jest.fn(),
|
||||||
image.uuid = `Position ${index}`;
|
timeOffset: 0,
|
||||||
images.push(image);
|
imageJobs: [],
|
||||||
});
|
});
|
||||||
return images;
|
|
||||||
}
|
|
||||||
|
|
||||||
it("shows photo", () => {
|
it("shows photo", () => {
|
||||||
const dispatch = jest.fn();
|
const p = fakeProps();
|
||||||
const images = prepareImages(fakeImages);
|
const images = fakeImages;
|
||||||
const currentImage = images[1];
|
p.currentImage = images[1];
|
||||||
const props = { images, currentImage, dispatch, timeOffset: 0 };
|
const wrapper = mount(<Photos {...p} />);
|
||||||
const wrapper = mount(<Photos {...props} />);
|
|
||||||
expect(wrapper.text()).toContain("Created At:June 1st, 2017");
|
expect(wrapper.text()).toContain("Created At:June 1st, 2017");
|
||||||
expect(wrapper.text()).toContain("X:632Y:347Z:164");
|
expect(wrapper.text()).toContain("X:632Y:347Z:164");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("no photos", () => {
|
it("no photos", () => {
|
||||||
const props = {
|
const wrapper = mount(<Photos {...fakeProps()} />);
|
||||||
images: [],
|
|
||||||
currentImage: undefined,
|
|
||||||
dispatch: jest.fn(),
|
|
||||||
timeOffset: 0
|
|
||||||
};
|
|
||||||
const wrapper = mount(<Photos {...props} />);
|
|
||||||
expect(wrapper.text()).toContain("Image:No meta data.");
|
expect(wrapper.text()).toContain("Image:No meta data.");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("deletes photo", () => {
|
it("takes photo", async () => {
|
||||||
const dispatch = jest.fn(() => { return Promise.resolve(); });
|
const wrapper = mount(<Photos {...fakeProps()} />);
|
||||||
const images = prepareImages(fakeImages);
|
await clickButton(wrapper, 0, "take photo");
|
||||||
const currentImage = images[1];
|
expect(mockDevice.takePhoto).toHaveBeenCalled();
|
||||||
const props = {
|
await expect(success).toHaveBeenCalled();
|
||||||
images,
|
});
|
||||||
currentImage,
|
|
||||||
dispatch,
|
it("fails to take photo", async () => {
|
||||||
timeOffset: 0
|
mockDevice.takePhoto = jest.fn(() => Promise.reject());
|
||||||
};
|
const wrapper = mount(<Photos {...fakeProps()} />);
|
||||||
const wrapper = mount(<Photos {...props} />);
|
await clickButton(wrapper, 0, "take photo");
|
||||||
|
expect(mockDevice.takePhoto).toHaveBeenCalled();
|
||||||
|
await expect(error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deletes photo", async () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.dispatch = jest.fn(() => Promise.resolve());
|
||||||
|
const images = fakeImages;
|
||||||
|
p.currentImage = images[1];
|
||||||
|
const wrapper = mount(<Photos {...p} />);
|
||||||
|
await clickButton(wrapper, 1, "delete photo");
|
||||||
|
expect(destroy).toHaveBeenCalledWith(p.currentImage.uuid);
|
||||||
|
await expect(success).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails to delete photo", async () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.dispatch = jest.fn(() => Promise.reject("error"));
|
||||||
|
const images = fakeImages;
|
||||||
|
p.currentImage = images[1];
|
||||||
|
const wrapper = mount(<Photos {...p} />);
|
||||||
|
await clickButton(wrapper, 1, "delete photo");
|
||||||
|
await expect(destroy).toHaveBeenCalledWith(p.currentImage.uuid);
|
||||||
|
await expect(error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deletes most recent photo", async () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.dispatch = jest.fn(() => Promise.resolve());
|
||||||
|
p.images = fakeImages;
|
||||||
|
const wrapper = mount(<Photos {...p} />);
|
||||||
|
await clickButton(wrapper, 1, "delete photo");
|
||||||
|
expect(destroy).toHaveBeenCalledWith(p.images[0].uuid);
|
||||||
|
await expect(success).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("no photos to delete", () => {
|
||||||
|
const wrapper = mount(<Photos {...fakeProps()} />);
|
||||||
clickButton(wrapper, 1, "delete photo");
|
clickButton(wrapper, 1, "delete photo");
|
||||||
expect(destroy).toHaveBeenCalledWith("Position 1");
|
expect(destroy).not.toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows image download progress", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.imageJobs = [{
|
||||||
|
status: "working",
|
||||||
|
percent: 15,
|
||||||
|
unit: "percent",
|
||||||
|
time: "2018-11-15 19:13:21.167440Z"
|
||||||
|
} as JobProgress];
|
||||||
|
const wrapper = mount(<Photos {...p} />);
|
||||||
|
expect(wrapper.text()).toContain("uploading photo...15%");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't show image download progress", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.imageJobs = [{
|
||||||
|
status: "complete",
|
||||||
|
percent: 15,
|
||||||
|
unit: "percent",
|
||||||
|
time: "2018-11-15 19:13:21.167440Z"
|
||||||
|
} as JobProgress];
|
||||||
|
const wrapper = mount(<Photos {...p} />);
|
||||||
|
expect(wrapper.text()).not.toContain("uploading");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can't find meta field data", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.images = fakeImages;
|
||||||
|
p.images[0].body.meta.x = undefined;
|
||||||
|
p.currentImage = p.images[0];
|
||||||
|
const wrapper = mount(<Photos {...p} />);
|
||||||
|
expect(wrapper.text()).toContain("X:unknown");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flips photo", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.images = fakeImages;
|
||||||
|
const wrapper = shallow(<Photos {...p} />);
|
||||||
|
wrapper.find("ImageFlipper").simulate("flip", 1);
|
||||||
|
expect(selectImage).toHaveBeenCalledWith(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,6 +5,15 @@ import { Content } from "../../constants";
|
||||||
|
|
||||||
export const PLACEHOLDER_FARMBOT = "/placeholder_farmbot.jpg";
|
export const PLACEHOLDER_FARMBOT = "/placeholder_farmbot.jpg";
|
||||||
|
|
||||||
|
/** Placeholder image with text overlay. */
|
||||||
|
const PlaceholderImg = ({ textOverlay }: { textOverlay: string }) =>
|
||||||
|
<div className="no-flipper-image-container">
|
||||||
|
<p>{t(textOverlay)}</p>
|
||||||
|
<img
|
||||||
|
className="image-flipper-image"
|
||||||
|
src={PLACEHOLDER_FARMBOT} />
|
||||||
|
</div>;
|
||||||
|
|
||||||
export class ImageFlipper extends
|
export class ImageFlipper extends
|
||||||
React.Component<ImageFlipperProps, Partial<ImageFlipperState>> {
|
React.Component<ImageFlipperProps, Partial<ImageFlipperState>> {
|
||||||
|
|
||||||
|
@ -16,30 +25,22 @@ export class ImageFlipper extends
|
||||||
|
|
||||||
imageJSX = () => {
|
imageJSX = () => {
|
||||||
if (this.props.images.length > 0) {
|
if (this.props.images.length > 0) {
|
||||||
const i = this.props.currentImage || this.props.images[0];
|
const image = this.props.currentImage || this.props.images[0];
|
||||||
let url: string;
|
const url = image.body.attachment_processed_at
|
||||||
url = (i.body.attachment_processed_at) ?
|
? image.body.attachment_url
|
||||||
i.body.attachment_url : PLACEHOLDER_FARMBOT;
|
: PLACEHOLDER_FARMBOT;
|
||||||
return <div>
|
return <div>
|
||||||
{!this.state.isLoaded && (
|
{!this.state.isLoaded &&
|
||||||
<div className="no-flipper-image-container">
|
<PlaceholderImg
|
||||||
<p>{t(`Image loading (try refreshing)`)}</p>
|
textOverlay={t("Image loading (try refreshing)")} />}
|
||||||
<img
|
|
||||||
className="image-flipper-image"
|
|
||||||
src={PLACEHOLDER_FARMBOT} />
|
|
||||||
</div>)}
|
|
||||||
<img
|
<img
|
||||||
onLoad={() => this.setState({ isLoaded: true })}
|
onLoad={() => this.setState({ isLoaded: true })}
|
||||||
className={`image-flipper-image is-loaded-${this.state.isLoaded}`}
|
className={`image-flipper-image is-loaded-${this.state.isLoaded}`}
|
||||||
src={url} />
|
src={url} />
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
return <div className="no-flipper-image-container">
|
return <PlaceholderImg
|
||||||
<p>{t(Content.NO_IMAGES_YET)}</p>
|
textOverlay={Content.NO_IMAGES_YET} />;
|
||||||
<img
|
|
||||||
className="image-flipper-image"
|
|
||||||
src={PLACEHOLDER_FARMBOT} />
|
|
||||||
</div>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,10 +62,9 @@ export class ImageFlipper extends
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const image = this.imageJSX();
|
|
||||||
const multipleImages = this.props.images.length > 1;
|
const multipleImages = this.props.images.length > 1;
|
||||||
return <div className="image-flipper">
|
return <div className="image-flipper">
|
||||||
{image}
|
<this.imageJSX />
|
||||||
<button
|
<button
|
||||||
onClick={this.go(1)}
|
onClick={this.go(1)}
|
||||||
disabled={!multipleImages || this.state.disablePrev}
|
disabled={!multipleImages || this.state.disablePrev}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { TaggedImage } from "farmbot";
|
import { TaggedImage, JobProgress } from "farmbot";
|
||||||
|
|
||||||
export interface ImageFlipperProps {
|
export interface ImageFlipperProps {
|
||||||
onFlip(uuid: string | undefined): void;
|
onFlip(uuid: string | undefined): void;
|
||||||
|
@ -17,4 +17,5 @@ export interface PhotosProps {
|
||||||
images: TaggedImage[];
|
images: TaggedImage[];
|
||||||
currentImage: TaggedImage | undefined;
|
currentImage: TaggedImage | undefined;
|
||||||
timeOffset: number;
|
timeOffset: number;
|
||||||
|
imageJobs: JobProgress[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,10 @@ import { Content } from "../../constants";
|
||||||
import { selectImage } from "./actions";
|
import { selectImage } from "./actions";
|
||||||
import { safeStringFetch } from "../../util";
|
import { safeStringFetch } from "../../util";
|
||||||
import { destroy } from "../../api/crud";
|
import { destroy } from "../../api/crud";
|
||||||
|
import {
|
||||||
|
downloadProgress
|
||||||
|
} from "../../devices/components/fbos_settings/os_update_button";
|
||||||
|
import { JobProgress, TaggedImage } from "farmbot";
|
||||||
|
|
||||||
interface MetaInfoProps {
|
interface MetaInfoProps {
|
||||||
/** Default conversion is `attr_name ==> Attr Name`.
|
/** Default conversion is `attr_name ==> Attr Name`.
|
||||||
|
@ -30,6 +34,67 @@ function MetaInfo({ obj, attr, label }: MetaInfoProps) {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PhotoMetaData = ({ image }: { image: TaggedImage | undefined }) =>
|
||||||
|
<div className="image-metadata">
|
||||||
|
{image
|
||||||
|
? Object.keys(image.body.meta)
|
||||||
|
.filter(key => ["x", "y", "z"].includes(key))
|
||||||
|
.sort()
|
||||||
|
.map((key, index) =>
|
||||||
|
<MetaInfo key={index} attr={key} obj={image.body.meta} />)
|
||||||
|
: <MetaInfo
|
||||||
|
label={t("Image")}
|
||||||
|
attr={"image"}
|
||||||
|
obj={{ image: t("No meta data.") }} />}
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
const PhotoButtons = (props: {
|
||||||
|
takePhoto: () => void,
|
||||||
|
deletePhoto: () => void,
|
||||||
|
imageJobs: JobProgress[]
|
||||||
|
}) => {
|
||||||
|
const imageUploadJobProgress = downloadProgress(props.imageJobs[0]);
|
||||||
|
return <div className="farmware-button">
|
||||||
|
<button
|
||||||
|
className="fb-button green"
|
||||||
|
onClick={props.takePhoto}>
|
||||||
|
{t("Take Photo")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="fb-button red"
|
||||||
|
onClick={props.deletePhoto}>
|
||||||
|
{t("Delete Photo")}
|
||||||
|
</button>
|
||||||
|
<p>
|
||||||
|
{imageUploadJobProgress &&
|
||||||
|
`${t("uploading photo")}...${imageUploadJobProgress}`}
|
||||||
|
</p>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PhotoFooter = ({ image, timeOffset }: {
|
||||||
|
image: TaggedImage | undefined,
|
||||||
|
timeOffset: number
|
||||||
|
}) => {
|
||||||
|
const created_at = image
|
||||||
|
? moment(image.body.created_at)
|
||||||
|
.utcOffset(timeOffset)
|
||||||
|
.format("MMMM Do, YYYY h:mma")
|
||||||
|
: "";
|
||||||
|
return <div className="photos-footer">
|
||||||
|
{/** Separated from <MetaInfo /> for stylistic purposes. */}
|
||||||
|
{image ?
|
||||||
|
<div className="image-created-at">
|
||||||
|
<label>{t("Created At:")}</label>
|
||||||
|
<span>
|
||||||
|
{created_at}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
: ""}
|
||||||
|
<PhotoMetaData image={image} />
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
export class Photos extends React.Component<PhotosProps, {}> {
|
export class Photos extends React.Component<PhotosProps, {}> {
|
||||||
|
|
||||||
takePhoto = () => {
|
takePhoto = () => {
|
||||||
|
@ -38,22 +103,7 @@ export class Photos extends React.Component<PhotosProps, {}> {
|
||||||
getDevice().takePhoto().then(ok, no);
|
getDevice().takePhoto().then(ok, no);
|
||||||
}
|
}
|
||||||
|
|
||||||
metaDatas() {
|
deletePhoto = () => {
|
||||||
const i = this.props.currentImage;
|
|
||||||
if (i) {
|
|
||||||
const { meta } = i.body;
|
|
||||||
return Object.keys(meta)
|
|
||||||
.filter(key => ["x", "y", "z"].includes(key))
|
|
||||||
.sort()
|
|
||||||
.map((key, index) => {
|
|
||||||
return <MetaInfo key={index} attr={key} obj={meta} />;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return <MetaInfo attr={t("image")} obj={{ image: t("No meta data.") }} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy = () => {
|
|
||||||
const img = this.props.currentImage || this.props.images[0];
|
const img = this.props.currentImage || this.props.images[0];
|
||||||
if (img && img.uuid) {
|
if (img && img.uuid) {
|
||||||
this.props.dispatch(destroy(img.uuid))
|
this.props.dispatch(destroy(img.uuid))
|
||||||
|
@ -63,43 +113,18 @@ export class Photos extends React.Component<PhotosProps, {}> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const image = this.props.currentImage;
|
|
||||||
const created_at = image
|
|
||||||
? moment(image.body.created_at)
|
|
||||||
.utcOffset(this.props.timeOffset)
|
|
||||||
.format("MMMM Do, YYYY h:mma")
|
|
||||||
: "";
|
|
||||||
return <div className="photos">
|
return <div className="photos">
|
||||||
<div className="farmware-button">
|
<PhotoButtons
|
||||||
<button
|
takePhoto={this.takePhoto}
|
||||||
className="fb-button green"
|
deletePhoto={this.deletePhoto}
|
||||||
onClick={this.takePhoto}>
|
imageJobs={this.props.imageJobs} />
|
||||||
{t("Take Photo")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="fb-button red"
|
|
||||||
onClick={() => this.destroy()}>
|
|
||||||
{t("Delete Photo")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ImageFlipper
|
<ImageFlipper
|
||||||
onFlip={id => { this.props.dispatch(selectImage(id)); }}
|
onFlip={id => this.props.dispatch(selectImage(id))}
|
||||||
currentImage={this.props.currentImage}
|
currentImage={this.props.currentImage}
|
||||||
images={this.props.images} />
|
images={this.props.images} />
|
||||||
<div className="photos-footer">
|
<PhotoFooter
|
||||||
{/** Separated from <MetaInfo /> for stylistic purposes. */}
|
image={this.props.currentImage}
|
||||||
{image ?
|
timeOffset={this.props.timeOffset} />
|
||||||
<div className="image-created-at">
|
|
||||||
<label>{t("Created At:")}</label>
|
|
||||||
<span>
|
|
||||||
{created_at}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
: ""}
|
|
||||||
<div className="image-metadatas">
|
|
||||||
{this.metaDatas()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,7 +122,8 @@ export class FarmwarePage extends React.Component<FarmwareProps, {}> {
|
||||||
timeOffset={this.props.timeOffset}
|
timeOffset={this.props.timeOffset}
|
||||||
dispatch={this.props.dispatch}
|
dispatch={this.props.dispatch}
|
||||||
images={this.props.images}
|
images={this.props.images}
|
||||||
currentImage={this.props.currentImage} />;
|
currentImage={this.props.currentImage}
|
||||||
|
imageJobs={this.props.imageJobs} />;
|
||||||
case "camera_calibration":
|
case "camera_calibration":
|
||||||
return <CameraCalibration
|
return <CameraCalibration
|
||||||
syncStatus={this.props.syncStatus}
|
syncStatus={this.props.syncStatus}
|
||||||
|
@ -140,6 +141,7 @@ export class FarmwarePage extends React.Component<FarmwareProps, {}> {
|
||||||
H_HI={envGet("CAMERA_CALIBRATION_H_HI", this.props.env)}
|
H_HI={envGet("CAMERA_CALIBRATION_H_HI", this.props.env)}
|
||||||
S_HI={envGet("CAMERA_CALIBRATION_S_HI", this.props.env)}
|
S_HI={envGet("CAMERA_CALIBRATION_S_HI", this.props.env)}
|
||||||
V_HI={envGet("CAMERA_CALIBRATION_V_HI", this.props.env)}
|
V_HI={envGet("CAMERA_CALIBRATION_V_HI", this.props.env)}
|
||||||
|
timeOffset={this.props.timeOffset}
|
||||||
shouldDisplay={this.props.shouldDisplay}
|
shouldDisplay={this.props.shouldDisplay}
|
||||||
botToMqttStatus={this.props.botToMqttStatus} />;
|
botToMqttStatus={this.props.botToMqttStatus} />;
|
||||||
case "plant_detection":
|
case "plant_detection":
|
||||||
|
|
|
@ -13,10 +13,11 @@ import {
|
||||||
import {
|
import {
|
||||||
determineInstalledOsVersion,
|
determineInstalledOsVersion,
|
||||||
shouldDisplay as shouldDisplayFunc,
|
shouldDisplay as shouldDisplayFunc,
|
||||||
trim
|
trim,
|
||||||
|
betterCompact
|
||||||
} from "../util";
|
} from "../util";
|
||||||
import { ResourceIndex } from "../resources/interfaces";
|
import { ResourceIndex } from "../resources/interfaces";
|
||||||
import { TaggedFarmwareEnv, FarmwareManifest } from "farmbot";
|
import { TaggedFarmwareEnv, FarmwareManifest, JobProgress } from "farmbot";
|
||||||
import { save, edit, initSave } from "../api/crud";
|
import { save, edit, initSave } from "../api/crud";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
|
||||||
|
@ -97,6 +98,14 @@ export function mapStateToProps(props: Everything): FarmwareProps {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { jobs } = props.bot.hardware;
|
||||||
|
const imageJobNames = Object.keys(jobs).filter(x => x != "FBOS_OTA");
|
||||||
|
const imageJobs: JobProgress[] =
|
||||||
|
_(betterCompact(imageJobNames.map(x => jobs[x])))
|
||||||
|
.sortBy("time")
|
||||||
|
.reverse()
|
||||||
|
.value();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
timeOffset: maybeGetTimeOffset(props.resources.index),
|
timeOffset: maybeGetTimeOffset(props.resources.index),
|
||||||
currentFarmware,
|
currentFarmware,
|
||||||
|
@ -113,5 +122,6 @@ export function mapStateToProps(props: Everything): FarmwareProps {
|
||||||
shouldDisplay,
|
shouldDisplay,
|
||||||
saveFarmwareEnv: saveOrEditFarmwareEnv(props.resources.index),
|
saveFarmwareEnv: saveOrEditFarmwareEnv(props.resources.index),
|
||||||
taggedFarmwareInstallations,
|
taggedFarmwareInstallations,
|
||||||
|
imageJobs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,25 @@
|
||||||
import { ImageWorkspace } from "../image_workspace";
|
import { ImageWorkspace, ImageWorkspaceProps } from "../image_workspace";
|
||||||
import { fakeImage } from "../../../__test_support__/fake_state/resources";
|
import { fakeImage } from "../../../__test_support__/fake_state/resources";
|
||||||
import { TaggedImage } from "farmbot";
|
import { TaggedImage } from "farmbot";
|
||||||
|
|
||||||
describe("<Body/>", () => {
|
describe("<Body/>", () => {
|
||||||
function fakeProps() {
|
const fakeProps = (): ImageWorkspaceProps => ({
|
||||||
return {
|
onFlip: jest.fn(),
|
||||||
onFlip: jest.fn(),
|
onProcessPhoto: jest.fn(),
|
||||||
onProcessPhoto: jest.fn(),
|
onChange: jest.fn(),
|
||||||
onChange: jest.fn(),
|
currentImage: undefined as TaggedImage | undefined,
|
||||||
currentImage: undefined as TaggedImage | undefined,
|
images: [] as TaggedImage[],
|
||||||
images: [] as TaggedImage[],
|
iteration: 9,
|
||||||
iteration: 9,
|
morph: 9,
|
||||||
morph: 9,
|
blur: 9,
|
||||||
blur: 9,
|
H_LO: 2,
|
||||||
H_LO: 2,
|
S_LO: 4,
|
||||||
S_LO: 4,
|
V_LO: 6,
|
||||||
V_LO: 6,
|
H_HI: 8,
|
||||||
H_HI: 8,
|
S_HI: 10,
|
||||||
S_HI: 10,
|
V_HI: 12,
|
||||||
V_HI: 12
|
timeOffset: 0,
|
||||||
};
|
});
|
||||||
}
|
|
||||||
|
|
||||||
it("triggers onChange() event", () => {
|
it("triggers onChange() event", () => {
|
||||||
const props = fakeProps();
|
const props = fakeProps();
|
||||||
|
|
|
@ -42,6 +42,7 @@ describe("<WeedDetector />", () => {
|
||||||
shouldDisplay: () => false,
|
shouldDisplay: () => false,
|
||||||
saveFarmwareEnv: jest.fn(),
|
saveFarmwareEnv: jest.fn(),
|
||||||
taggedFarmwareInstallations: [],
|
taggedFarmwareInstallations: [],
|
||||||
|
imageJobs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { HSV } from "./interfaces";
|
||||||
import { WeedDetectorSlider } from "./slider";
|
import { WeedDetectorSlider } from "./slider";
|
||||||
import { TaggedImage } from "farmbot";
|
import { TaggedImage } from "farmbot";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
import { PhotoFooter } from "../images/photos";
|
||||||
|
|
||||||
const RANGES = {
|
const RANGES = {
|
||||||
H: { LOWEST: 0, HIGHEST: 179 },
|
H: { LOWEST: 0, HIGHEST: 179 },
|
||||||
|
@ -31,13 +32,14 @@ export interface NumericValues {
|
||||||
|
|
||||||
type NumericKeyName = keyof NumericValues;
|
type NumericKeyName = keyof NumericValues;
|
||||||
|
|
||||||
interface Props extends NumericValues {
|
export interface ImageWorkspaceProps extends NumericValues {
|
||||||
onFlip(uuid: string | undefined): void;
|
onFlip(uuid: string | undefined): void;
|
||||||
onProcessPhoto(image_id: number): void;
|
onProcessPhoto(image_id: number): void;
|
||||||
currentImage: TaggedImage | undefined;
|
currentImage: TaggedImage | undefined;
|
||||||
images: TaggedImage[];
|
images: TaggedImage[];
|
||||||
onChange(key: NumericKeyName, value: number): void;
|
onChange(key: NumericKeyName, value: number): void;
|
||||||
invertHue?: boolean;
|
invertHue?: boolean;
|
||||||
|
timeOffset: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mapping of HSV values to FBOS Env variables. */
|
/** Mapping of HSV values to FBOS Env variables. */
|
||||||
|
@ -47,7 +49,7 @@ const CHANGE_MAP: Record<HSV, [NumericKeyName, NumericKeyName]> = {
|
||||||
V: ["V_LO", "V_HI"]
|
V: ["V_LO", "V_HI"]
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ImageWorkspace extends React.Component<Props, {}> {
|
export class ImageWorkspace extends React.Component<ImageWorkspaceProps, {}> {
|
||||||
/** Generates a function to handle changes to blur/morph/iteration. */
|
/** Generates a function to handle changes to blur/morph/iteration. */
|
||||||
numericChange = (key: NumericKeyName) =>
|
numericChange = (key: NumericKeyName) =>
|
||||||
(e: React.SyntheticEvent<HTMLInputElement>) => {
|
(e: React.SyntheticEvent<HTMLInputElement>) => {
|
||||||
|
@ -160,6 +162,9 @@ export class ImageWorkspace extends React.Component<Props, {}> {
|
||||||
onFlip={this.props.onFlip}
|
onFlip={this.props.onFlip}
|
||||||
images={this.props.images}
|
images={this.props.images}
|
||||||
currentImage={this.props.currentImage} />
|
currentImage={this.props.currentImage} />
|
||||||
|
<PhotoFooter
|
||||||
|
image={this.props.currentImage}
|
||||||
|
timeOffset={this.props.timeOffset} />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,6 +79,7 @@ export class WeedDetector
|
||||||
currentImage={this.props.currentImage}
|
currentImage={this.props.currentImage}
|
||||||
images={this.props.images}
|
images={this.props.images}
|
||||||
onChange={this.change}
|
onChange={this.change}
|
||||||
|
timeOffset={this.props.timeOffset}
|
||||||
iteration={envGet(this.namespace("iteration"), this.props.env)}
|
iteration={envGet(this.namespace("iteration"), this.props.env)}
|
||||||
morph={envGet(this.namespace("morph"), this.props.env)}
|
morph={envGet(this.namespace("morph"), this.props.env)}
|
||||||
blur={envGet(this.namespace("blur"), this.props.env)}
|
blur={envGet(this.namespace("blur"), this.props.env)}
|
||||||
|
|
Loading…
Reference in New Issue