Merge branch 'master' of github.com:RickCarlino/farmbot-web-app

This commit is contained in:
MrChristofferson 2017-07-07 12:02:49 -05:00
commit 8114cef2bd
14 changed files with 134 additions and 31 deletions

View file

@ -1,3 +1,3 @@
web: rails s -e development -p 3000 -b 0.0.0.0
worker: rake jobs:work
webpack: npm run webpack
api: rails s -e development -p 3000 -b 0.0.0.0
wrk: rake jobs:work
fe: npm run webpack

View file

@ -21,8 +21,8 @@ init();
* If the sync object takes more than 10s to load, the user will be granted
* access into the app, but still warned.
*/
const TIMEOUT_MESSAGE = `App could not be fully loaded, we recommend you try 
refreshing the page.`;
const TIMEOUT_MESSAGE = `App could not be fully loaded, we recommend you try
refreshing the page.`;
interface AppProps {
dispatch: Function;
@ -68,7 +68,6 @@ export default class App extends React.Component<AppProps, {}> {
componentDidMount() {
setTimeout(() => {
if (!this.isLoaded) {
this.props.dispatch({ type: "SYNC_TIMEOUT_EXCEEDED" });
error(TIMEOUT_MESSAGE, "Warning");
}
}, 10000);

View file

@ -275,6 +275,7 @@ export enum Actions {
OVERWRITE_RESOURCE = "OVERWRITE_RESOURCE",
SAVE_RESOURCE_START = "SAVE_RESOURCE_START",
RESOURCE_READY = "RESOURCE_READY",
_RESOURCE_NO = "*_RESOURCE_NO",
// Auth
LOGIN_OK = "LOGIN_OK",

View file

@ -1,5 +1,6 @@
import { ReduxAction } from "./interfaces";
import { defensiveClone } from "../util";
import { Actions } from "../constants";
export function generateReducer<State>(initialState: State,
/** For passing state down to children. */
@ -20,7 +21,7 @@ export function generateReducer<State>(initialState: State,
interface GeneratedReducer extends ActionHandler {
/** Adds action handler for current reducer. */
add: <T>(name: string, fn: GenericActionHandler<T>) => GeneratedReducer;
add: <T>(name: Actions, fn: GenericActionHandler<T>) => GeneratedReducer;
// Calms the type checker.
}

View file

@ -160,7 +160,7 @@ export let resourceReducer = generateReducer
throw new Error("BAD UUID IN UPDATE_RESOURCE_OK");
}
})
.add<TaggedResource>("*_RESOURCE_NO", (s, { payload }) => {
.add<TaggedResource>(Actions._RESOURCE_NO, (s, { payload }) => {
let uuid = payload.uuid;
let tr = _.merge(findByUuid(s.index, uuid), payload);
tr.dirty = true;
@ -178,7 +178,7 @@ export let resourceReducer = generateReducer
payload && isTaggedResource(source);
return s;
})
.add<EditResourceParams>("OVERWRITE_RESOURCE", (s, { payload }) => {
.add<EditResourceParams>(Actions.OVERWRITE_RESOURCE, (s, { payload }) => {
let uuid = payload.uuid;
let original = findByUuid(s.index, uuid);
original.body = payload.update as typeof original.body;
@ -188,7 +188,7 @@ export let resourceReducer = generateReducer
if (original.kind === "sequences") { setStepUuid(original); }
return s;
})
.add<TaggedResource>("INIT_RESOURCE", (s, { payload }) => {
.add<TaggedResource>(Actions.INIT_RESOURCE, (s, { payload }) => {
let tr = payload;
let uuid = tr.uuid;
// TEMPORARY STUB:
@ -200,6 +200,9 @@ export let resourceReducer = generateReducer
if (tr.kind === "logs" && (typeof tr.body.created_at === "string")) {
tr.body.created_at = moment(tr.body.created_at).unix();
}
if (tr.kind == "sequences") {
setStepUuid(tr);
}
reindexResource(s.index, tr);
if (tr.kind === "logs") {
// Since logs don't come from the API all the time, they are the only
@ -269,8 +272,8 @@ function removeFromIndex(index: ResourceIndex, tr: TaggedResource) {
let id = tr.body.id;
index.all = index.all.filter(filterOutUuid(tr));
index.byKind[tr.kind] = index.byKind[tr.kind].filter(filterOutUuid(tr));
delete index.byKindAndId[joinKindAndId(kind, id)]
delete index.byKindAndId[joinKindAndId(kind, 0)]
delete index.byKindAndId[joinKindAndId(kind, id)];
delete index.byKindAndId[joinKindAndId(kind, 0)];
delete index.references[tr.uuid];
}

View file

@ -0,0 +1,67 @@
import * as React from "react";
import { TestButton, TestBtnProps } from "../test_button";
import { TaggedSequence } from "../../resources/tagged_resources";
import { mount } from "enzyme";
describe("<TestButton/>", () => {
function fakeSequence(): TaggedSequence {
return {
"kind": "sequences",
"body": {
"name": "Goto 0, 0, 0",
"color": "gray",
"body": [],
"args": { "version": 4 },
"kind": "sequence"
},
"uuid": "sequences.23.47"
};
}
function fakeProps(): TestBtnProps {
jest.clearAllMocks();
return {
onClick: jest.fn(),
onFail: jest.fn(),
sequence: fakeSequence(),
syncStatus: "synced"
};
}
it("doesnt fire if unsaved", () => {
let props = fakeProps();
props.sequence.dirty = true;
let result = mount(<TestButton {...props} />);
let btn = result.find("button");
btn.simulate("click");
expect(btn.hasClass("gray")).toBeTruthy();
expect(props.onFail).toHaveBeenCalled();
expect(props.onClick).not.toHaveBeenCalled();
});
it("doesnt fire if unsynced", () => {
let props = fakeProps();
props.syncStatus = "sync_now";
props.sequence.dirty = false;
props.sequence.body.id = 1;
let result = mount(<TestButton {...props} />);
let btn = result.find("button");
btn.simulate("click");
expect(btn.hasClass("gray")).toBeTruthy();
expect(props.onFail).toHaveBeenCalled();
expect(props.onClick).not.toHaveBeenCalled();
});
it("does fire if saved and synced", () => {
let props = fakeProps();
props.syncStatus = "synced";
props.sequence.dirty = false;
props.sequence.body.id = 1;
let result = mount(<TestButton {...props} />);
let btn = result.find("button");
btn.simulate("click");
expect(btn.hasClass("orange")).toBeTruthy();
expect(props.onFail).not.toHaveBeenCalled();
expect(props.onClick).toHaveBeenCalled();
});
});

View file

@ -3,7 +3,8 @@ import { AuthState } from "../auth/interfaces";
import {
Sequence as CeleryScriptSequence,
SequenceBodyItem,
LegalArgString
LegalArgString,
SyncStatus
} from "farmbot";
import { StepMoveDataXfer, StepSpliceDataXfer } from "../draggable/interfaces";
import {
@ -22,6 +23,7 @@ export interface Props {
sequence: TaggedSequence | undefined;
auth: AuthState | undefined;
resources: ResourceIndex;
syncStatus: SyncStatus;
}
export interface SequenceEditorMiddleProps {
@ -34,10 +36,12 @@ export interface SequenceEditorMiddleProps {
/** @deprecated Use props.resources now. */
slots: TaggedToolSlotPointer[];
resources: ResourceIndex;
syncStatus: SyncStatus;
}
export interface ActiveMiddleProps extends SequenceEditorMiddleProps {
sequence: TaggedSequence;
syncStatus: SyncStatus;
}
export type CHANNEL_NAME = "toast" | "ticker";

View file

@ -22,11 +22,11 @@ export let sequenceReducer = generateReducer<SequenceReducerState>(initialState)
}
return s;
})
.add<string>("SELECT_SEQUENCE", function (s, { payload }) {
.add<string>(Actions.SELECT_SEQUENCE, function (s, { payload }) {
s.current = payload;
return s;
})
.add<void>("RESOURCE_READY", function (s, a) {
.add<void>(Actions.RESOURCE_READY, function (s, a) {
s.current = undefined;
return s;
});

View file

@ -13,7 +13,8 @@ export class SequenceEditorMiddle
sequences,
tools,
slots,
resources
resources,
syncStatus
} = this.props;
if (sequence && isTaggedSequence(sequence)) {
return <SequenceEditorMiddleActive
@ -22,7 +23,8 @@ export class SequenceEditorMiddle
sequence={sequence}
sequences={sequences}
tools={tools}
resources={resources} />;
resources={resources}
syncStatus={syncStatus} />;
} else {
return <SequenceEditorMiddleInactive />;
}

View file

@ -17,6 +17,8 @@ import { save, edit, destroy } from "../api/crud";
import { GetState } from "../redux/interfaces";
import { ToolTips } from "../constants";
import { get } from "lodash";
import { TestButton } from "./test_button";
import { warning } from "farmbot-toastr";
let onDrop = (index: number, dispatch1: Function, sequence: TaggedSequence) =>
(key: string) => {
@ -41,12 +43,6 @@ let copy = function (dispatch: Function, sequence: TaggedSequence) {
dispatch(copySequence(sequence));
};
export let performSeq = (dispatch: Function, s: TaggedSequence) => {
return () => {
dispatch(save(s.uuid)).then(() => execSequence(s.body));
};
};
export class SequenceEditorMiddleActive
extends React.Component<ActiveMiddleProps, {}> {
render() {
@ -76,12 +72,11 @@ export class SequenceEditorMiddleActive
isSaved={isSaved}
onClick={() => { dispatch(save(sequence.uuid)); }}
/>
<button
className="fb-button orange"
onClick={performSeq(dispatch, sequence)}
>
{t("Save & Run")}
</button>
<TestButton
syncStatus={this.props.syncStatus}
sequence={sequence}
onFail={warning}
onClick={() => execSequence(sequence.body)} />
<button
className="fb-button red"
onClick={() => dispatch(destroy(sequence.uuid))}

View file

@ -20,6 +20,7 @@ export class Sequences extends React.Component<Props, {}> {
</Col>
<Col sm={6}>
<SequenceEditorMiddle
syncStatus={this.props.syncStatus}
dispatch={this.props.dispatch}
sequences={this.props.sequences}
sequence={this.props.sequence}

View file

@ -9,6 +9,8 @@ import {
export function mapStateToProps(props: Everything): Props {
let uuid = props.resources.consumers.sequences.current;
let syncStatus =
props.bot.hardware.informational_settings.sync_status || "unknown";
return {
dispatch: props.dispatch,
sequences: selectAllSequences(props.resources.index),
@ -16,6 +18,7 @@ export function mapStateToProps(props: Everything): Props {
slots: selectAllToolSlotPointers(props.resources.index),
sequence: (uuid) ? findSequence(props.resources.index, uuid) : undefined,
auth: props.auth,
resources: props.resources.index
resources: props.resources.index,
syncStatus
};
}

View file

@ -0,0 +1,27 @@
import * as React from "react";
import { t } from "i18next";
import { SyncStatus } from "farmbot/dist";
import { TaggedSequence } from "../resources/tagged_resources";
export interface TestBtnProps {
/** Callback fired ONLY if synced. */
onClick(): void;
/** Callback fired is NOT synced. */
onFail(message: string): void;
syncStatus: SyncStatus;
sequence: TaggedSequence;
}
export function TestButton({ onClick, onFail, syncStatus, sequence }: TestBtnProps) {
let isSynced = syncStatus === "synced";
let isSaved = !sequence.dirty;
let canTest = isSynced && isSaved;
let className = canTest ? "orange" : "gray";
let clickHandler = () => (canTest) ?
onClick() : onFail(t("Sync device before running."));
return <button className={`fb-button ${className}`} onClick={clickHandler} >
{t("Test")}
</button>;
}

View file

@ -417,7 +417,7 @@ export type JSXChildren = JSXChild[] | JSXChild;
* This is a work around until then. */
export function hardRefresh() {
// Change this string to trigger a force cache reset.
let HARD_RESET = "NEED_HARD_REFRESH4";
let HARD_RESET = "CACHE5";
if (localStorage && sessionStorage) {
if (!localStorage.getItem(HARD_RESET)) {
console.warn("Performing hard reset of localstorage and JS cookies.");