commit
6821070a24
22
Gemfile.lock
22
Gemfile.lock
|
@ -30,7 +30,7 @@ GEM
|
|||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
||||
active_model_serializers (0.10.7)
|
||||
active_model_serializers (0.10.8)
|
||||
actionpack (>= 4.1, < 6)
|
||||
activemodel (>= 4.1, < 6)
|
||||
case_transform (>= 0.2)
|
||||
|
@ -63,13 +63,14 @@ GEM
|
|||
builder (3.2.3)
|
||||
bunny (2.12.0)
|
||||
amq-protocol (~> 2.3, >= 2.3.0)
|
||||
capybara (3.9.0)
|
||||
capybara (3.10.0)
|
||||
addressable
|
||||
mini_mime (>= 0.1.3)
|
||||
nokogiri (~> 1.8)
|
||||
rack (>= 1.6.0)
|
||||
rack-test (>= 0.6.3)
|
||||
xpath (~> 3.1)
|
||||
regexp_parser (~> 1.2)
|
||||
xpath (~> 3.2)
|
||||
case_transform (0.2)
|
||||
activesupport
|
||||
childprocess (0.9.0)
|
||||
|
@ -174,7 +175,7 @@ GEM
|
|||
actionpack (>= 3.0)
|
||||
activerecord (>= 3.0)
|
||||
railties (>= 3.0)
|
||||
loofah (2.2.2)
|
||||
loofah (2.2.3)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
|
@ -224,7 +225,7 @@ GEM
|
|||
hashie (~> 3.5)
|
||||
multi_json (~> 1.12)
|
||||
rack (2.0.5)
|
||||
rack-attack (5.4.1)
|
||||
rack-attack (5.4.2)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-cors (1.0.2)
|
||||
rack-test (1.1.0)
|
||||
|
@ -264,7 +265,8 @@ GEM
|
|||
rake (>= 0.8.7)
|
||||
thor (>= 0.19.0, < 2.0)
|
||||
rake (12.3.1)
|
||||
redis (4.0.2)
|
||||
redis (4.0.3)
|
||||
regexp_parser (1.2.0)
|
||||
representable (3.0.4)
|
||||
declarative (< 0.1.0)
|
||||
declarative-option (< 0.2.0)
|
||||
|
@ -304,7 +306,7 @@ GEM
|
|||
activerecord (>= 4.0.0)
|
||||
railties (>= 4.0.0)
|
||||
secure_headers (6.0.0)
|
||||
selenium-webdriver (3.14.1)
|
||||
selenium-webdriver (3.141.0)
|
||||
childprocess (~> 0.5)
|
||||
rubyzip (~> 1.2, >= 1.2.2)
|
||||
signet (0.11.0)
|
||||
|
@ -317,9 +319,9 @@ GEM
|
|||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.2)
|
||||
skylight (3.1.0)
|
||||
skylight-core (= 3.1.0)
|
||||
skylight-core (3.1.0)
|
||||
skylight (3.1.1)
|
||||
skylight-core (= 3.1.1)
|
||||
skylight-core (3.1.1)
|
||||
activesupport (>= 4.2.0)
|
||||
sprockets (3.7.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
|
|
|
@ -7,7 +7,12 @@ module CeleryScript
|
|||
BODY_HAS_NON_NODES = "The `body` of a node can only contain nodes- " \
|
||||
"no leaves here."
|
||||
LEAVES_NEED_KEYS = "Tried to initialize a leaf without a key."
|
||||
def initialize(parent = nil, args:, body: nil, comment: "", kind:)
|
||||
def initialize(parent = nil,
|
||||
args:,
|
||||
body: nil,
|
||||
comment: "",
|
||||
kind:,
|
||||
uuid: nil)
|
||||
@comment, @kind, @parent = comment, kind, parent
|
||||
|
||||
@args = args.map do |key, value|
|
||||
|
|
28
package.json
28
package.json
|
@ -30,16 +30,16 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@blueprintjs/core": "^3.7.0",
|
||||
"@blueprintjs/datetime": "^3.3.0",
|
||||
"@blueprintjs/select": "^3.2.0",
|
||||
"@blueprintjs/datetime": "^3.3.1",
|
||||
"@blueprintjs/select": "^3.2.1",
|
||||
"@types/enzyme": "3.1.14",
|
||||
"@types/fastclick": "^1.0.28",
|
||||
"@types/i18next": "^11.9.3",
|
||||
"@types/jest": "^23.3.5",
|
||||
"@types/jest": "^23.3.9",
|
||||
"@types/lodash": "^4.14.117",
|
||||
"@types/markdown-it": "0.0.6",
|
||||
"@types/markdown-it": "0.0.7",
|
||||
"@types/moxios": "^0.4.5",
|
||||
"@types/node": "^10.12.0",
|
||||
"@types/node": "^10.12.2",
|
||||
"@types/react": "^16.4.18",
|
||||
"@types/react-color": "2.13.6",
|
||||
"@types/react-dom": "^16.0.9",
|
||||
|
@ -49,7 +49,7 @@
|
|||
"boxed_value": "^1.0.0",
|
||||
"browser-speech": "1.1.1",
|
||||
"coveralls": "3.0.2",
|
||||
"css-loader": "1.0.0",
|
||||
"css-loader": "^1.0.1",
|
||||
"enzyme": "^3.7.0",
|
||||
"enzyme-adapter-react-16": "^1.6.0",
|
||||
"farmbot": "^6.5.7",
|
||||
|
@ -68,14 +68,14 @@
|
|||
"moxios": "^0.4.0",
|
||||
"node-sass": "^4.9.4",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.1",
|
||||
"raf": "^3.4.0",
|
||||
"react": "16.5.2",
|
||||
"raf": "^3.4.1",
|
||||
"react": "^16.6.0",
|
||||
"react-addons-test-utils": "^15.6.2",
|
||||
"react-color": "2.14.1",
|
||||
"react-dom": "16.5.2",
|
||||
"react-dom": "^16.6.0",
|
||||
"react-joyride": "^2.0.0-15",
|
||||
"react-redux": "^5.0.6",
|
||||
"react-test-renderer": "16.5.2",
|
||||
"react-redux": "^5.1.0",
|
||||
"react-test-renderer": "^16.6.0",
|
||||
"react-transition-group": "2.5.0",
|
||||
"redux": "^4.0.1",
|
||||
"redux-immutable-state-invariant": "^2.1.0",
|
||||
|
@ -88,11 +88,11 @@
|
|||
"takeme": "^0.11.1",
|
||||
"ts-jest": "^23.10.4",
|
||||
"ts-lint": "^4.5.1",
|
||||
"ts-loader": "^5.2.2",
|
||||
"ts-loader": "^5.3.0",
|
||||
"tslint": "5.11.0",
|
||||
"typescript": "^3.1.3",
|
||||
"typescript": "^3.1.6",
|
||||
"url-loader": "^1.1.2",
|
||||
"webpack": "^4.22.0",
|
||||
"webpack": "^4.24.0",
|
||||
"webpack-uglify-js-plugin": "1.1.9",
|
||||
"which": "1.3.1"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { Edge } from "./interfaces";
|
||||
/** throttle calls to these functions to avoid unnecessary re-paints. */
|
||||
const SLOWDOWN_TIME = 1500;
|
||||
|
||||
const lastCalledAt: Record<Edge, number> = {
|
||||
"user.api": 0, "user.mqtt": 0, "bot.mqtt": 0
|
||||
};
|
||||
|
||||
export function bumpThrottle(edge: Edge, now: number) {
|
||||
lastCalledAt[edge] = now;
|
||||
}
|
||||
|
||||
export function shouldThrottle(edge: Edge, now: number): boolean {
|
||||
const then = lastCalledAt[edge];
|
||||
const diff = then - now;
|
||||
|
||||
return diff > SLOWDOWN_TIME;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { selectImage } from "../actions";
|
||||
import { Actions } from "../../../constants";
|
||||
|
||||
describe("selectImage", () => {
|
||||
it("selects one image", () => {
|
||||
const payload = "my uuid";
|
||||
const result = selectImage(payload);
|
||||
expect(result.type).toEqual(Actions.SELECT_IMAGE);
|
||||
expect(result.payload).toEqual(payload);
|
||||
});
|
||||
|
||||
it("selects no image", () => {
|
||||
const payload = undefined;
|
||||
const result = selectImage(payload);
|
||||
expect(result.type).toEqual(Actions.SELECT_IMAGE);
|
||||
expect(result.payload).toEqual(payload);
|
||||
});
|
||||
});
|
|
@ -6,8 +6,13 @@ jest.mock("../../util/stop_ie", () => {
|
|||
return { stopIE: jest.fn() };
|
||||
});
|
||||
|
||||
jest.mock("../../util",
|
||||
() => ({ attachToRoot: jest.fn(), trim: (s: string) => s }));
|
||||
|
||||
import { detectLanguage } from "../../i18n";
|
||||
import { stopIE } from "../../util/stop_ie";
|
||||
import { attachToRoot } from "../../util";
|
||||
import { FrontPage, attachFrontPage } from "../front_page";
|
||||
|
||||
describe("index", () => {
|
||||
it("Attaches to the DOM", async () => {
|
||||
|
@ -15,4 +20,9 @@ describe("index", () => {
|
|||
expect(detectLanguage).toHaveBeenCalled();
|
||||
expect(stopIE).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("attaches FrontPage to DOM specifically", () => {
|
||||
attachFrontPage();
|
||||
expect(attachToRoot).toHaveBeenCalledWith(FrontPage, {});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@ import { t } from "i18next";
|
|||
import * as _ from "lodash";
|
||||
import { error as log, success, init as logInit } from "farmbot-toastr";
|
||||
import { AuthState } from "../auth/interfaces";
|
||||
import { prettyPrintApiErrors } from "../util";
|
||||
import { prettyPrintApiErrors, attachToRoot } from "../util";
|
||||
import { API } from "../api";
|
||||
import { Session } from "../session";
|
||||
import { FrontPageState, SetterCB } from "./interfaces";
|
||||
|
@ -15,6 +15,10 @@ import { ResendVerification } from "./resend_verification";
|
|||
import { CreateAccount } from "./create_account";
|
||||
import { Content } from "../constants";
|
||||
|
||||
export const attachFrontPage = () => {
|
||||
attachToRoot(FrontPage, {});
|
||||
};
|
||||
|
||||
const showFor = (size: string[], extraClass?: string): string => {
|
||||
const ALL_SIZES = ["xs", "sm", "md", "lg", "xl"];
|
||||
const HIDDEN_SIZES = ALL_SIZES.filter(x => !size.includes(x));
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
import { detectLanguage } from "../i18n";
|
||||
import { FrontPage } from "./front_page";
|
||||
import * as i18next from "i18next";
|
||||
import "../css/_index.scss";
|
||||
import { attachToRoot } from "../util";
|
||||
import { stopIE } from "../util/stop_ie";
|
||||
import { attachFrontPage } from "./front_page";
|
||||
|
||||
stopIE();
|
||||
|
||||
detectLanguage().then((config) => {
|
||||
i18next.init(config, () => {
|
||||
attachToRoot(FrontPage, {});
|
||||
});
|
||||
});
|
||||
detectLanguage().then((config) => i18next.init(config, attachFrontPage));
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
import { findByUuid, joinKindAndId } from "../reducer_support";
|
||||
import { fakeState } from "../../__test_support__/fake_state";
|
||||
import { overwrite, refreshStart, refreshOK, refreshNO } from "../../api/crud";
|
||||
import { SpecialStatus, TaggedSequence, TaggedDevice, ResourceName, TaggedResource } from "farmbot";
|
||||
import {
|
||||
SpecialStatus,
|
||||
TaggedSequence,
|
||||
TaggedDevice,
|
||||
ResourceName,
|
||||
TaggedResource,
|
||||
TaggedTool
|
||||
} from "farmbot";
|
||||
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
|
||||
import { GeneralizedError } from "../actions";
|
||||
import { Actions } from "../../constants";
|
||||
import { fakeResource } from "../../__test_support__/fake_resource";
|
||||
import { resourceReducer } from "../reducer";
|
||||
import { resourceReady } from "../../sync/actions";
|
||||
import { EditResourceParams } from "../../api/interfaces";
|
||||
|
||||
describe("resource reducer", () => {
|
||||
it("marks resources as DIRTY when reducing OVERWRITE_RESOURCE", () => {
|
||||
|
@ -72,6 +80,36 @@ describe("resource reducer", () => {
|
|||
TEST_RESOURCE_NAMES.map(kind => testResource(kind));
|
||||
});
|
||||
|
||||
it("EDITs a _RESOURCE", () => {
|
||||
const startingState = fakeState().resources;
|
||||
const { index } = startingState;
|
||||
const uuid = Object.keys(index.byKind.Tool)[0];
|
||||
const update: Partial<TaggedTool["body"]> = { name: "after" };
|
||||
const payload: EditResourceParams = {
|
||||
uuid,
|
||||
update,
|
||||
specialStatus: SpecialStatus.SAVED
|
||||
};
|
||||
const action = { type: Actions.EDIT_RESOURCE, payload };
|
||||
const newState = resourceReducer(startingState, action);
|
||||
const oldTool = index.references[uuid] as TaggedTool;
|
||||
const newTool = newState.index.references[uuid] as TaggedTool;
|
||||
expect(oldTool.body.name).not.toEqual("after");
|
||||
expect(newTool.body.name).toEqual("after");
|
||||
});
|
||||
|
||||
it("handles resource failures", () => {
|
||||
const startingState = fakeState().resources;
|
||||
const uuid = Object.keys(startingState.index.byKind.Tool)[0];
|
||||
const action = {
|
||||
type: Actions._RESOURCE_NO,
|
||||
payload: { uuid, err: "Whatever", statusBeforeError: SpecialStatus.DIRTY }
|
||||
};
|
||||
const newState = resourceReducer(startingState, action);
|
||||
const tool = newState.index.references[uuid] as TaggedTool;
|
||||
expect(tool.specialStatus).toBe(SpecialStatus.DIRTY);
|
||||
});
|
||||
|
||||
it("covers destroy resource branches", () => {
|
||||
const testResourceDestroy = (kind: ResourceName) => {
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { TaggedSequence, SpecialStatus } from "farmbot";
|
||||
import { get } from "lodash";
|
||||
import { maybeTagSteps, getStepTag } from "../sequence_tagging";
|
||||
import { getStepTag, setStepTag } from "../sequence_tagging";
|
||||
|
||||
describe("maybeTagSteps()", () => {
|
||||
describe("tagAllSteps()", () => {
|
||||
const UNTAGGED_SEQUENCE: TaggedSequence = {
|
||||
"kind": "Sequence",
|
||||
"uuid": "whatever",
|
||||
|
@ -36,7 +36,7 @@ describe("maybeTagSteps()", () => {
|
|||
expect(() => {
|
||||
getStepTag(body[0]);
|
||||
}).toThrow();
|
||||
maybeTagSteps(UNTAGGED_SEQUENCE);
|
||||
setStepTag(body[0]);
|
||||
expect(get(body[0], "uuid")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { fakeTool } from "../../__test_support__/fake_state/resources";
|
||||
import { getArrayStatus } from "../tagged_resources";
|
||||
import { getArrayStatus, sanityCheck } from "../tagged_resources";
|
||||
import { SpecialStatus } from "farmbot";
|
||||
|
||||
describe("getArrayStatus()", () => {
|
||||
|
@ -28,3 +28,9 @@ describe("getArrayStatus()", () => {
|
|||
expect(getArrayStatus(arr)).toBe(SpecialStatus.SAVED);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sanityCheck", () => {
|
||||
it("crashes on malformed TaggedResources", () => {
|
||||
expect(() => sanityCheck({})).toThrow("Bad kind, uuid, or body: {}");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,11 +15,17 @@ import { HelpState } from "../help/reducer";
|
|||
|
||||
type UUID = string;
|
||||
|
||||
export interface SequenceVariableMeta {
|
||||
label: string;
|
||||
kind: "Point"
|
||||
}
|
||||
|
||||
export interface ResourceIndex {
|
||||
all: Record<UUID, UUID>;
|
||||
all: Record<UUID, true>;
|
||||
byKind: Record<ResourceName, Record<UUID, UUID>>;
|
||||
byKindAndId: CowardlyDictionary<UUID>;
|
||||
references: Dictionary<TaggedResource | undefined>;
|
||||
sequenceMeta: Record<UUID, SequenceVariableMeta[]>;
|
||||
}
|
||||
|
||||
export interface RestResources {
|
||||
|
|
|
@ -76,7 +76,8 @@ export const emptyState = (): RestResources => {
|
|||
DiagnosticDump: {}
|
||||
},
|
||||
byKindAndId: {},
|
||||
references: {}
|
||||
references: {},
|
||||
sequenceMeta: {}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { ResourceIndex } from "./interfaces";
|
||||
import { TaggedResource } from "farmbot";
|
||||
import { joinKindAndId } from "./reducer_support";
|
||||
import { maybeTagSteps } from "./sequence_tagging";
|
||||
import {
|
||||
recomputeLocalVarDeclaration
|
||||
performAllTransformsOnSequence
|
||||
} from "../sequences/step_tiles/tile_move_absolute/variables_support";
|
||||
|
||||
type IndexDirection = "up" | "down";
|
||||
|
@ -16,7 +15,7 @@ const REFERENCES: Indexer = {
|
|||
};
|
||||
|
||||
const ALL: Indexer = {
|
||||
up: (r, s) => s.all[r.uuid] = r.uuid,
|
||||
up: (r, s) => s.all[r.uuid] = true,
|
||||
down: (r, i) => delete i.all[r.uuid],
|
||||
};
|
||||
|
||||
|
@ -37,15 +36,24 @@ const BY_KIND_AND_ID: Indexer = {
|
|||
};
|
||||
|
||||
const SEQUENCE_STUFF: Indexer = {
|
||||
up(r) {
|
||||
up(r, _i) {
|
||||
if (r.kind === "Sequence") {
|
||||
const recomputed = recomputeLocalVarDeclaration(r.body);
|
||||
r.body.args = recomputed.args;
|
||||
r.body.body = recomputed.body;
|
||||
maybeTagSteps(r);
|
||||
performAllTransformsOnSequence(r.body);
|
||||
// const locals = (r.body.args.locals.body || []);
|
||||
// i.sequenceMeta[r.uuid] = locals.map((local): SequenceVariableMeta => {
|
||||
// switch (local.kind) {
|
||||
// case "parameter_declaration":
|
||||
// case "variable_declaration":
|
||||
// return {
|
||||
// label: local.args.label,
|
||||
// kind: local.args.data_type
|
||||
// };
|
||||
// }
|
||||
// });
|
||||
}
|
||||
},
|
||||
down(_r, _i) {
|
||||
down(r, i) {
|
||||
delete i.sequenceMeta[r.uuid];
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import {
|
||||
TaggedResource,
|
||||
TaggedToolSlotPointer,
|
||||
TaggedSequence,
|
||||
TaggedRegimen,
|
||||
TaggedFarmEvent,
|
||||
TaggedTool,
|
||||
TaggedPoint
|
||||
} from "farmbot";
|
||||
import { CowardlyDictionary } from "../util";
|
||||
import {
|
||||
|
@ -13,7 +11,6 @@ import {
|
|||
SlotWithTool
|
||||
} from "./interfaces";
|
||||
import {
|
||||
selectAllTools,
|
||||
selectAllToolSlotPointers,
|
||||
maybeFindToolById
|
||||
} from "./selectors";
|
||||
|
@ -28,8 +25,6 @@ interface Indexer<T extends TaggedResource> {
|
|||
|
||||
interface MapperFn<T extends TaggedResource> { (item: T): T | undefined; }
|
||||
|
||||
type StringMap = CowardlyDictionary<string>;
|
||||
|
||||
/** Build a function,
|
||||
* that returns a function,
|
||||
* that returns a dictionary,
|
||||
|
@ -57,26 +52,11 @@ export const buildIndexer =
|
|||
};
|
||||
};
|
||||
|
||||
const slotMapper = (i: TaggedPoint): TaggedToolSlotPointer | undefined => {
|
||||
if (i.kind == "Point" && (i.body.pointer_type === "ToolSlot")) {
|
||||
return i as TaggedToolSlotPointer;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const indexBySlotId =
|
||||
buildIndexer<TaggedToolSlotPointer>("Point", slotMapper);
|
||||
export const indexSequenceById = buildIndexer<TaggedSequence>("Sequence");
|
||||
export const indexRegimenById = buildIndexer<TaggedRegimen>("Regimen");
|
||||
export const indexFarmEventById = buildIndexer<TaggedFarmEvent>("FarmEvent");
|
||||
export const indexByToolId = buildIndexer<TaggedTool>("Tool");
|
||||
|
||||
export function mapToolIdToName(input: ResourceIndex) {
|
||||
return selectAllTools(input)
|
||||
.map(x => ({ key: "" + x.body.id, val: x.body.name }))
|
||||
.reduce((x, y) => ({ ...{ [y.key]: y.val, ...x } }), {} as StringMap);
|
||||
}
|
||||
|
||||
/** For those times that you need to ref a tool and slot together. */
|
||||
export function joinToolsAndSlot(index: ResourceIndex): SlotWithTool[] {
|
||||
return selectAllToolSlotPointers(index)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { get, set } from "lodash";
|
||||
import { SequenceBodyItem, uuid } from "farmbot/dist";
|
||||
import { TaggedResource } from "farmbot";
|
||||
import {
|
||||
Traversable
|
||||
} from "../sequences/step_tiles/tile_move_absolute/variables_support";
|
||||
|
||||
/** HISTORICAL NOTES:
|
||||
* This file is the result of some very subtle bugs relating to dynamic
|
||||
|
@ -50,6 +52,9 @@ export type StepTag = string;
|
|||
/** Property name where a unique ID is stored in a step. */
|
||||
const TAG_PROP = "uuid";
|
||||
|
||||
export const setStepTag =
|
||||
(node: Traversable) => !get(node, TAG_PROP) && set(node, TAG_PROP, uuid());
|
||||
|
||||
/** VERY IMPORTANT FUNCTION.
|
||||
* SEE HEADER AT TOP OF FILE.
|
||||
* Retrieves tag from a step object. Assumes that all steps have a tag.
|
||||
|
@ -59,18 +64,3 @@ export function getStepTag(i: SequenceBodyItem): StepTag {
|
|||
if (tag) { return tag; }
|
||||
throw new Error("No tag on step: " + i.kind);
|
||||
}
|
||||
|
||||
/** Idempotently add a `uuid` property to a step. */
|
||||
export let setStepTag = (i: SequenceBodyItem) => {
|
||||
set(i, TAG_PROP, uuid());
|
||||
};
|
||||
|
||||
/** Idempotently add a `uuid` property to all steps in an array. */
|
||||
export let tagAllSteps = (i: SequenceBodyItem[]) => i.map(setStepTag);
|
||||
|
||||
/** REALLY IMPORTANT SEE FILE HEADER FOR MORE INFO! -RC
|
||||
* Used by Redux within the `resource` reducer. Given a TaggedResource,
|
||||
* idempotently adds `UUID` property to all steps in all sequences. */
|
||||
export function maybeTagSteps(x: TaggedResource) {
|
||||
if (x && (x.kind === "Sequence")) { tagAllSteps(x.body.body || []); }
|
||||
}
|
||||
|
|
|
@ -3,14 +3,12 @@ import { betterCompact } from "../util";
|
|||
import * as _ from "lodash";
|
||||
import { assertUuid } from "./util";
|
||||
import {
|
||||
TaggedCrop,
|
||||
TaggedResource,
|
||||
ResourceName,
|
||||
TaggedRegimen,
|
||||
TaggedSequence,
|
||||
TaggedTool,
|
||||
TaggedFarmEvent,
|
||||
TaggedLog,
|
||||
TaggedToolSlotPointer,
|
||||
TaggedPlantPointer,
|
||||
TaggedGenericPointer,
|
||||
|
@ -87,12 +85,8 @@ export let isTaggedSequence =
|
|||
(x: object): x is TaggedSequence => is("Sequence")(x);
|
||||
export let isTaggedTool =
|
||||
(x: object): x is TaggedTool => is("Tool")(x);
|
||||
export let isTaggedCrop =
|
||||
(x: object): x is TaggedCrop => is("Crop")(x);
|
||||
export let isTaggedFarmEvent =
|
||||
(x: object): x is TaggedFarmEvent => is("FarmEvent")(x);
|
||||
export let isTaggedLog =
|
||||
(x: object): x is TaggedLog => is("Log")(x);
|
||||
export let isTaggedToolSlotPointer =
|
||||
(x: object): x is TaggedToolSlotPointer => {
|
||||
return isTaggedPoint(x) && (x.body.pointer_type === "ToolSlot");
|
||||
|
|
|
@ -3,16 +3,16 @@ import { AllSteps } from "../all_steps";
|
|||
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
|
||||
import { shallow } from "enzyme";
|
||||
import { TaggedSequence, SpecialStatus } from "farmbot";
|
||||
import { maybeTagSteps } from "../../resources/sequence_tagging";
|
||||
import { TileMoveRelative } from "../step_tiles/tile_move_relative";
|
||||
import { TileReadPin } from "../step_tiles/tile_read_pin";
|
||||
import { TileWritePin } from "../step_tiles/tile_write_pin";
|
||||
import { performAllTransformsOnSequence } from "../step_tiles/tile_move_absolute/variables_support";
|
||||
|
||||
describe("<AllSteps/>", () => {
|
||||
const TEST_CASE = {
|
||||
const TEST_CASE: TaggedSequence = {
|
||||
"kind": "Sequence",
|
||||
"specialStatus": SpecialStatus.SAVED,
|
||||
"body": {
|
||||
"body": performAllTransformsOnSequence({
|
||||
"id": 8,
|
||||
"name": "Goto 0, 0, 0",
|
||||
"color": "gray",
|
||||
|
@ -45,16 +45,12 @@ describe("<AllSteps/>", () => {
|
|||
],
|
||||
"args": {
|
||||
"locals": { kind: "scope_declaration", args: {} },
|
||||
"is_outdated": false,
|
||||
"version": 4,
|
||||
"label": "WIP"
|
||||
},
|
||||
"kind": "sequence"
|
||||
},
|
||||
}),
|
||||
"uuid": "Sequence.8.52"
|
||||
} as TaggedSequence;
|
||||
|
||||
maybeTagSteps(TEST_CASE);
|
||||
};
|
||||
|
||||
it("uses index as a key", () => {
|
||||
const el = shallow(<AllSteps
|
||||
|
|
|
@ -1,150 +1,35 @@
|
|||
import { Sequence, MoveAbsolute } from "farmbot";
|
||||
import { collectAllVariables, recomputeLocalVarDeclaration } from "../variables_support";
|
||||
import { fakeSequence } from "../../../../__test_support__/fake_state/resources";
|
||||
import { MoveAbsolute } from "farmbot";
|
||||
import { performAllTransformsOnSequence } from "../variables_support";
|
||||
import { get } from "lodash";
|
||||
|
||||
const PARENT: MoveAbsolute = {
|
||||
kind: "move_absolute",
|
||||
args: {
|
||||
location: { kind: "identifier", args: { label: "parent" } },
|
||||
offset: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } },
|
||||
speed: 100
|
||||
}
|
||||
};
|
||||
describe("performAllIndexesOnSequence", () => {
|
||||
const move_abs: MoveAbsolute = {
|
||||
kind: "move_absolute",
|
||||
args: {
|
||||
location: { kind: "identifier", args: { label: "parent" } },
|
||||
offset: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } },
|
||||
speed: 800
|
||||
}
|
||||
};
|
||||
|
||||
const PARENT2: MoveAbsolute = {
|
||||
kind: "move_absolute",
|
||||
args: {
|
||||
location: { kind: "identifier", args: { label: "parent2" } },
|
||||
offset: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } },
|
||||
speed: 100
|
||||
}
|
||||
};
|
||||
|
||||
/** Another random variable. */
|
||||
const Q: MoveAbsolute = {
|
||||
kind: "move_absolute",
|
||||
args: {
|
||||
location: { kind: "identifier", args: { label: "q" } },
|
||||
offset: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } },
|
||||
speed: 100
|
||||
}
|
||||
};
|
||||
|
||||
const EMPTY_SEQ_ARGS: Sequence["args"] = {
|
||||
version: 6,
|
||||
locals: { kind: "scope_declaration", args: {} }
|
||||
};
|
||||
|
||||
// USE CASE 1: Empty / new sequence.
|
||||
const sequence1: Sequence = { kind: "sequence", args: EMPTY_SEQ_ARGS, body: [] };
|
||||
|
||||
// USE CASE 2: Single variable
|
||||
const sequence2: Sequence = { kind: "sequence", args: EMPTY_SEQ_ARGS, body: [PARENT] };
|
||||
|
||||
// USE CASE 3: Multiple unique variables
|
||||
const sequence3: Sequence = {
|
||||
kind: "sequence",
|
||||
args: EMPTY_SEQ_ARGS,
|
||||
body: [PARENT, PARENT2]
|
||||
};
|
||||
|
||||
// USE CASE 4: Duplicate variables
|
||||
const sequence4: Sequence = {
|
||||
kind: "sequence",
|
||||
args: EMPTY_SEQ_ARGS,
|
||||
body: [PARENT, PARENT]
|
||||
};
|
||||
|
||||
// USE CASE 5: preexisting variables
|
||||
const sequence5: Sequence = {
|
||||
kind: "sequence",
|
||||
args: {
|
||||
version: 6,
|
||||
locals: {
|
||||
it("fills in missing information related to variables", () => {
|
||||
const missing_declaration = fakeSequence().body;
|
||||
expect(missing_declaration.args.locals.body).toBeUndefined();
|
||||
missing_declaration.body = [move_abs];
|
||||
missing_declaration.args.locals = {
|
||||
kind: "scope_declaration",
|
||||
args: {},
|
||||
body: [
|
||||
{
|
||||
kind: "parameter_declaration",
|
||||
args: { label: "foo", data_type: "point" }
|
||||
}
|
||||
]
|
||||
body: []
|
||||
};
|
||||
performAllTransformsOnSequence(missing_declaration);
|
||||
const locals = missing_declaration.args.locals.body;
|
||||
if (locals) {
|
||||
expect(locals[0]).toBeDefined();
|
||||
expect(get(locals[0], "uuid")).toBeDefined();
|
||||
expect(locals[0].args.label).toEqual("parent");
|
||||
} else {
|
||||
fail("Expected performAllIndexesOnSequence to fill in missing declarations");
|
||||
}
|
||||
},
|
||||
body: [PARENT, Q]
|
||||
};
|
||||
|
||||
// USE CASE 6: Empty scope_declaration
|
||||
const sequence6: Sequence = {
|
||||
kind: "sequence",
|
||||
args: { version: 6, locals: { kind: "scope_declaration", args: {} } },
|
||||
body: [PARENT, Q]
|
||||
};
|
||||
|
||||
// USE CASE 7:
|
||||
const sequence7: Sequence = {
|
||||
kind: "sequence",
|
||||
args: { version: 6, locals: { kind: "scope_declaration", args: {} } },
|
||||
body: []
|
||||
};
|
||||
|
||||
describe("collectIdentifiers", () => {
|
||||
it("doesnt blow up on empty sequences", () => {
|
||||
const results = collectAllVariables(sequence1);
|
||||
expect(results.length).toBe(0);
|
||||
});
|
||||
|
||||
it("finds a single identifier", () => {
|
||||
const results = collectAllVariables(sequence2);
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].args.label).toBe("parent");
|
||||
});
|
||||
|
||||
it("finds multiple identifiers", () => {
|
||||
const results = collectAllVariables(sequence3);
|
||||
expect(results.length).toBe(2);
|
||||
expect(results[0].args.label).toBe("parent");
|
||||
expect(results[1].args.label).toBe("parent2");
|
||||
});
|
||||
|
||||
it("removes duplicates", () => {
|
||||
const results = collectAllVariables(sequence4);
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].args.label).toBe("parent");
|
||||
});
|
||||
});
|
||||
|
||||
describe("recomputeLocals", () => {
|
||||
it("recomputes locals (multiple use of same var)", () => {
|
||||
const result = recomputeLocalVarDeclaration(sequence4);
|
||||
expect(result.args.locals.kind).toBe("scope_declaration");
|
||||
expect((result.args.locals.body || []).length).toBe(1);
|
||||
});
|
||||
|
||||
it("recomputes locals (multiple variables)", () => {
|
||||
const result = recomputeLocalVarDeclaration(sequence3);
|
||||
expect(result.args.locals.kind).toBe("scope_declaration");
|
||||
expect((result.args.locals.body || []).length).toBe(2);
|
||||
});
|
||||
|
||||
it("recomputes locals (squash old variables)", () => {
|
||||
const result = recomputeLocalVarDeclaration(sequence5);
|
||||
expect(result.args.locals.kind).toBe("scope_declaration");
|
||||
expect((result.args.locals.body || []).length).toBe(2);
|
||||
const labels = (result.args.locals.body || []).map(x => x.args.label);
|
||||
expect(labels).toContain("q");
|
||||
});
|
||||
|
||||
it("Doesn't crash on empty arrays in `locals`", () => {
|
||||
const result = recomputeLocalVarDeclaration(sequence6);
|
||||
expect(result.args.locals.kind).toBe("scope_declaration");
|
||||
expect((result.args.locals.body || []).length).toBe(2);
|
||||
const labels = (result.args.locals.body || []).map(x => x.args.label);
|
||||
expect(labels).toContain("q");
|
||||
});
|
||||
|
||||
it("Doesn't crash on variable-less sequences", () => {
|
||||
const result = recomputeLocalVarDeclaration(sequence7);
|
||||
expect(result.args.locals.kind).toBe("scope_declaration");
|
||||
expect((result.args.locals.body || []).length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,105 +1,102 @@
|
|||
import { get } from "lodash";
|
||||
import {
|
||||
Identifier,
|
||||
Dictionary,
|
||||
Sequence,
|
||||
Nothing,
|
||||
ScopeDeclarationBodyItem
|
||||
Identifier,
|
||||
ScopeDeclarationBodyItem,
|
||||
VariableDeclaration
|
||||
} from "farmbot";
|
||||
import { get, uniqBy } from "lodash";
|
||||
import { defensiveClone } from "../../../util";
|
||||
import {
|
||||
SequenceResource as Sequence
|
||||
} from "farmbot/dist/resources/api_resources";
|
||||
import { setStepTag } from "../../../resources/sequence_tagging";
|
||||
|
||||
/** A less strict version of a CeleryScript node used for
|
||||
* the sake of recursion. */
|
||||
interface Traversable {
|
||||
kind: string;
|
||||
args: Args;
|
||||
body?: Body;
|
||||
}
|
||||
|
||||
/** Junk that we don't care about on a celery script node. */
|
||||
type Other = string | number | object;
|
||||
/** Less strict version of CeleryScript `args`-
|
||||
* It's either traversable, or we don't care. */
|
||||
type Args = Dictionary<Traversable | Other>;
|
||||
// ======= TYPE DECLARATIONS =======
|
||||
/** Less strict version of CeleryScript args. It's traversable, or unknown. */
|
||||
type Args = Dictionary<Traversable | unknown>;
|
||||
type Body = Traversable[] | undefined;
|
||||
/** Accumulator for collecting identifiers found while recursing. */
|
||||
type Accum = Identifier[];
|
||||
/** Less strict CeleryScript node used for the sake of recursion. */
|
||||
export interface Traversable { kind: string; args: Args; body?: Body; }
|
||||
type TreeClimberCB = (item: Traversable) => void;
|
||||
// ======= END TYPE DECLARATIONS =======
|
||||
|
||||
export const NOTHING: Nothing = { kind: "nothing", args: {} };
|
||||
|
||||
/** Is it a variable (identifier)? */
|
||||
const isIdentifier =
|
||||
(x: Traversable): x is Identifier => (x.kind === "identifier");
|
||||
// ======= CONST / LITERAL / DYNAMIC KEY DECLARATIONS =======
|
||||
const ARGS = "args";
|
||||
const IDENTIFIER = "identifier";
|
||||
const KIND = "kind";
|
||||
const OBJECT = "object";
|
||||
const STRING = "string";
|
||||
// ======= END CONST / LITERAL DECLARATIONS =======
|
||||
|
||||
/** Is it a fully-formed CeleryScript node? Can we continue recursing? */
|
||||
const isTraversable = (x: unknown): x is Traversable => {
|
||||
const kind: string | undefined = get(x, "kind");
|
||||
const args: object | undefined = get(x, "args");
|
||||
|
||||
return !!((typeof kind == "string") && args && typeof args == "object");
|
||||
const hasKind = typeof get(x, KIND, -1) == STRING;
|
||||
const hasArgs = typeof get(x, ARGS, -1) == OBJECT;
|
||||
return hasKind && hasArgs;
|
||||
};
|
||||
|
||||
/** Is it an _identifier_ node? Put it in the array if so.
|
||||
* If it is some other node type, continue recursion. */
|
||||
const maybeCollect =
|
||||
(x: Traversable, y: Accum) => isIdentifier(x) ? y.push(x) : traverse(y)(x);
|
||||
/** Is it a variable (identifier)? */
|
||||
const isIdentifier =
|
||||
(x: Traversable): x is Identifier => (x.kind === IDENTIFIER);
|
||||
|
||||
/** Maybe recurse into each leg of node.args */
|
||||
const traverseArgs = (input: Args, accumulator: Accum) => {
|
||||
const keys = Object.keys(input);
|
||||
keys.map(key => {
|
||||
const value = input[key];
|
||||
isTraversable(value) && maybeCollect(value, accumulator);
|
||||
});
|
||||
};
|
||||
|
||||
/** Iterate over node.body and perform recursion on each child node. */
|
||||
const traverseBody = (input: Body, accumulator: Accum) => {
|
||||
input && input.map(traverse(accumulator));
|
||||
};
|
||||
|
||||
/** Recurse into every leg of node.args and node.body, pushing all `identifier`
|
||||
* nodes into the `acc` array. */
|
||||
const traverse = (acc: Accum = []) => (input: unknown): Accum => {
|
||||
if (isTraversable(input)) {
|
||||
traverseArgs(input.args, acc);
|
||||
traverseBody(input.body, acc);
|
||||
const newVar = (label: string): VariableDeclaration => ({
|
||||
kind: "variable_declaration",
|
||||
args: {
|
||||
label,
|
||||
data_value: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } }
|
||||
}
|
||||
});
|
||||
|
||||
return acc;
|
||||
};
|
||||
/** This function currently has two responsibilities:
|
||||
* 1. Recursively tag all CeleryScript nodes with a `uuid` property to
|
||||
* prevent subtle React issues.
|
||||
* 2. "fill in the blanks" for variables. Example: If a move abs. step
|
||||
* references `parent`, but sequence.args.locals.body does not contain a
|
||||
* `parent` declaration, that could be very bad.
|
||||
*
|
||||
* SEE ALSO: Huge explanation in `sequence_tagging.ts` */
|
||||
export const performAllTransformsOnSequence = (input: Sequence): Sequence => {
|
||||
// Ideally, we want to be able to blindly insert identifiers into any part of
|
||||
// a sequence and have said identifier show up in the `scope_declaration`.
|
||||
|
||||
/** Used to remove duplicates */
|
||||
const iteratee = (x: Identifier) => x.args["label"];
|
||||
const actuallyUsed: Dictionary<Identifier> = {};
|
||||
const declared: Dictionary<ScopeDeclarationBodyItem> = {};
|
||||
const body = (input.args.locals.body = input.args.locals.body || []);
|
||||
input.args.locals.body.map(item => declared[item.args.label] = item);
|
||||
const updateDeclarations = (node: Identifier) => {
|
||||
const varName = node.args.label;
|
||||
// STEP 1: Collect all the stuff thats been _declared_.
|
||||
actuallyUsed[varName] = node;
|
||||
|
||||
/** Collect ever `identifier` CeleryScript node in a sequence. */
|
||||
export const collectAllVariables =
|
||||
(sequence: Sequence) => uniqBy(traverse([])(sequence), iteratee);
|
||||
|
||||
const generateDeclarationsFromIdentifiers = (s: Sequence) => {
|
||||
const { locals } = s.args;
|
||||
const lookup: Dictionary<ScopeDeclarationBodyItem | undefined> = {};
|
||||
(locals.body || []).map(x => (lookup[x.args.label] = x));
|
||||
|
||||
return (identifier: Identifier): ScopeDeclarationBodyItem => {
|
||||
return lookup[identifier.args.label] || {
|
||||
kind: "variable_declaration",
|
||||
args: {
|
||||
label: identifier.args.label,
|
||||
data_value: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } }
|
||||
}
|
||||
};
|
||||
/** Scenario: You referenced a variable, but it does not
|
||||
* exist in `seq.args.locals`. */
|
||||
if (!declared[varName]) {
|
||||
// STEP 2: Collect all stuff that's been _referenced_.
|
||||
// If it's not already in the sequence.args, declare it as an empty var.
|
||||
const declaration = newVar(varName);
|
||||
declared[varName] = declaration;
|
||||
setStepTag(declaration);
|
||||
body.push(declaration);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/** Calculate the next value of sequence.arg.locals given a *new* list of
|
||||
* variables for a sequence. */
|
||||
export const recomputeLocalVarDeclaration = (input: Sequence): Sequence => {
|
||||
const output = defensiveClone(input);
|
||||
const identifiers = collectAllVariables(output);
|
||||
|
||||
const body = identifiers.map(generateDeclarationsFromIdentifiers(input));
|
||||
input.args.locals = { kind: "scope_declaration", args: {}, body };
|
||||
climb(input, node => {
|
||||
setStepTag(node);
|
||||
isIdentifier(node) && updateDeclarations(node);
|
||||
});
|
||||
|
||||
return input;
|
||||
};
|
||||
|
||||
export function climb(t: Traversable | unknown, cb: TreeClimberCB) {
|
||||
const climbArgs = /** RECURSION ALERT! */
|
||||
(a: Args) => Object.keys(a).map(arg => climb(a[arg], cb));
|
||||
const climbBody = /** WEE OOO WEE OO */
|
||||
(body: Body = []) => body.map(item => climb(item, cb));
|
||||
|
||||
if (isTraversable(t)) {
|
||||
t.body = t.body || [];
|
||||
climbArgs(t.args);
|
||||
climbBody(t.body);
|
||||
cb(t);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue