add regimen variables UI
parent
acd764ab1e
commit
540422df0e
|
@ -51,7 +51,7 @@
|
|||
"css-loader": "2.1.0",
|
||||
"enzyme": "3.8.0",
|
||||
"enzyme-adapter-react-16": "1.7.1",
|
||||
"farmbot": "6.6.3-rc5",
|
||||
"farmbot": "6.6.3-rc6",
|
||||
"farmbot-toastr": "1.0.3",
|
||||
"fastclick": "1.0.6",
|
||||
"file-loader": "3.0.1",
|
||||
|
|
|
@ -50,7 +50,8 @@ export function fakeRegimen(): TaggedRegimen {
|
|||
return fakeResource("Regimen", {
|
||||
name: "Foo",
|
||||
color: "red",
|
||||
regimen_items: []
|
||||
regimen_items: [],
|
||||
body: [],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -279,7 +279,8 @@ const tr12: TaggedResource = {
|
|||
"sequence_id": 23,
|
||||
"time_offset": 345900000
|
||||
}
|
||||
]
|
||||
],
|
||||
body: [],
|
||||
},
|
||||
"uuid": "Regimen.11.46"
|
||||
};
|
||||
|
@ -406,7 +407,8 @@ const blankReg: TaggedRegimen = {
|
|||
"id": 11,
|
||||
"name": "Repair Sequence",
|
||||
"color": "gray",
|
||||
"regimen_items": []
|
||||
"regimen_items": [],
|
||||
body: [],
|
||||
},
|
||||
"uuid": "Regimen.11.46"
|
||||
};
|
||||
|
|
|
@ -546,10 +546,6 @@ export namespace Content {
|
|||
trim(`Click one in the Regimens panel to edit, or click "+" to create
|
||||
a new one.`);
|
||||
|
||||
export const NO_PARAMETERS = trim(`Can't directly use this sequence in a
|
||||
regimen. Consider wrapping it in a parent sequence that calls it via
|
||||
"execute" instead.`);
|
||||
|
||||
// Farm Designer
|
||||
export const OUTSIDE_PLANTING_AREA =
|
||||
trim(`Outside of planting area. Plants must be placed within the grid.`);
|
||||
|
|
|
@ -32,7 +32,9 @@ describe("<Regimens />", () => {
|
|||
weeks: [],
|
||||
bot,
|
||||
calendar: [],
|
||||
regimenUsageStats: {}
|
||||
regimenUsageStats: {},
|
||||
shouldDisplay: () => false,
|
||||
variableData: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ import { TaggedResource } from "farmbot";
|
|||
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
|
||||
import { newTaggedResource } from "../../sync/actions";
|
||||
import { selectAllRegimens } from "../../resources/selectors";
|
||||
import { fakeVariableNameSet } from "../../__test_support__/fake_variables";
|
||||
import { fakeRegimen, fakeSequence } from "../../__test_support__/fake_state/resources";
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
it("returns props: no regimen selected", () => {
|
||||
|
@ -42,4 +44,17 @@ describe("mapStateToProps()", () => {
|
|||
props.current ? expect(props.current.uuid).toEqual(uuid) : fail;
|
||||
expect(props.calendar[0].items[0].item.time_offset).toEqual(1000);
|
||||
});
|
||||
|
||||
it("returns variableData", () => {
|
||||
const reg = fakeRegimen();
|
||||
const seq = fakeSequence();
|
||||
reg.body.regimen_items = [{ sequence_id: seq.body.id || 0, time_offset: 1000 }];
|
||||
const state = fakeState();
|
||||
state.resources = buildResourceIndex([reg, seq]);
|
||||
state.resources.consumers.regimens.currentRegimen = reg.uuid;
|
||||
const varData = fakeVariableNameSet();
|
||||
state.resources.index.sequenceMetas[seq.uuid] = varData;
|
||||
const props = mapStateToProps(state);
|
||||
expect(props.variableData).toEqual(varData);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,7 +22,8 @@ describe("commitBulkEditor()", () => {
|
|||
"color": "gray",
|
||||
"regimen_items": [
|
||||
{ regimen_id, sequence_id, time_offset: 1000 }
|
||||
]
|
||||
],
|
||||
body: [],
|
||||
};
|
||||
const reg = newTaggedResource("Regimen", regBody)[0];
|
||||
const seqBody: TaggedSequence["body"] = {
|
||||
|
|
|
@ -29,7 +29,7 @@ describe("<BulkScheduler />", () => {
|
|||
weeks,
|
||||
sequences: [fakeSequence(), fakeSequence()],
|
||||
resources: buildResourceIndex([]).index,
|
||||
dispatch: jest.fn()
|
||||
dispatch: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,8 @@ describe("<BulkScheduler />", () => {
|
|||
it("changes time", () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn();
|
||||
const wrapper = shallow(<BulkScheduler {...p} />);
|
||||
const panel = shallow<BulkScheduler>(<BulkScheduler {...p} />);
|
||||
const wrapper = shallow(panel.instance().TimeSelection());
|
||||
const timeInput = wrapper.find("BlurableInput").first();
|
||||
expect(timeInput.props().value).toEqual("01:00");
|
||||
timeInput.simulate("commit", { currentTarget: { value: "02:00" } });
|
||||
|
@ -66,7 +67,8 @@ describe("<BulkScheduler />", () => {
|
|||
it("sets current time", () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn();
|
||||
const wrapper = shallow(<BulkScheduler {...p} />);
|
||||
const panel = shallow<BulkScheduler>(<BulkScheduler {...p} />);
|
||||
const wrapper = shallow(panel.instance().TimeSelection());
|
||||
const currentTimeBtn = wrapper.find(".fa-clock-o").first();
|
||||
currentTimeBtn.simulate("click");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
|
@ -78,7 +80,8 @@ describe("<BulkScheduler />", () => {
|
|||
it("changes sequence", () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn();
|
||||
const wrapper = shallow(<BulkScheduler {...p} />);
|
||||
const panel = shallow<BulkScheduler>(<BulkScheduler {...p} />);
|
||||
const wrapper = shallow(panel.instance().SequenceSelectBox());
|
||||
const sequenceInput = wrapper.find("FBSelect").first();
|
||||
sequenceInput.simulate("change", { value: "Sequence" });
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
|
@ -90,7 +93,8 @@ describe("<BulkScheduler />", () => {
|
|||
it("doesn't change sequence", () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn();
|
||||
const wrapper = shallow(<BulkScheduler {...p} />);
|
||||
const panel = shallow<BulkScheduler>(<BulkScheduler {...p} />);
|
||||
const wrapper = shallow(panel.instance().SequenceSelectBox());
|
||||
const sequenceInput = wrapper.find("FBSelect").first();
|
||||
const change = () => sequenceInput.simulate("change", { value: 4 });
|
||||
expect(change).toThrowError("WARNING: Not a sequence UUID.");
|
||||
|
|
|
@ -1,19 +1,4 @@
|
|||
import { msToTime, maybeWarnAboutParameters } from "../utils";
|
||||
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
|
||||
import { error } from "farmbot-toastr";
|
||||
import { Content } from "../../../constants";
|
||||
|
||||
describe("maybeWarnAboutParameters", () => {
|
||||
it("calls `error()` if the sequence uses params", () => {
|
||||
const s = fakeSequence();
|
||||
s.body.args.locals.body = [{
|
||||
kind: "parameter_declaration",
|
||||
args: { label: "parent", data_type: "point" }
|
||||
}];
|
||||
maybeWarnAboutParameters(s);
|
||||
expect(error).toHaveBeenCalledWith(Content.NO_PARAMETERS);
|
||||
});
|
||||
});
|
||||
import { msToTime } from "../utils";
|
||||
|
||||
describe("msToTime", () => {
|
||||
it("handles bad inputs", () => {
|
||||
|
|
|
@ -9,6 +9,7 @@ import { defensiveClone } from "../../util";
|
|||
import { overwrite } from "../../api/crud";
|
||||
import { Actions } from "../../constants";
|
||||
import { assertUuid } from "../../resources/util";
|
||||
import { mergeVariableDeclarations } from "../../sequences/locals_list/declaration_support";
|
||||
|
||||
export function pushWeek() {
|
||||
return {
|
||||
|
@ -87,6 +88,8 @@ export function commitBulkEditor(): Thunk {
|
|||
const regimen = findRegimen(resources.index, currentRegimen);
|
||||
const clonedRegimen = defensiveClone(regimen).body;
|
||||
clonedRegimen.regimen_items = clonedRegimen.regimen_items.concat(groupedItems);
|
||||
const varData = resources.index.sequenceMetas[selectedSequenceUUID];
|
||||
clonedRegimen.body = mergeVariableDeclarations(varData, regimen.body.body);
|
||||
dispatch(overwrite(regimen, clonedRegimen));
|
||||
} else {
|
||||
return error(t("No day(s) selected."));
|
||||
|
|
|
@ -8,16 +8,18 @@ import {
|
|||
} from "../../ui/index";
|
||||
import * as moment from "moment";
|
||||
import { t } from "i18next";
|
||||
import * as _ from "lodash";
|
||||
import { isString } from "lodash";
|
||||
import { betterCompact, bail } from "../../util";
|
||||
import { maybeWarnAboutParameters, msToTime, timeToMs } from "./utils";
|
||||
import { msToTime, timeToMs } from "./utils";
|
||||
|
||||
const BAD_UUID = "WARNING: Not a sequence UUID.";
|
||||
|
||||
export class BulkScheduler extends React.Component<BulkEditorProps, {}> {
|
||||
selected = (): DropDownItem => {
|
||||
const s = this.props.selectedSequence;
|
||||
return (s && s.body.id) ? { label: s.body.name, value: s.uuid } : NULL_CHOICE;
|
||||
return (s && s.body.id)
|
||||
? { label: s.body.name, value: s.uuid }
|
||||
: NULL_CHOICE;
|
||||
};
|
||||
|
||||
all = (): DropDownItem[] => {
|
||||
|
@ -29,52 +31,50 @@ export class BulkScheduler extends React.Component<BulkEditorProps, {}> {
|
|||
};
|
||||
|
||||
commitChange = (uuid: string) => {
|
||||
const s = this.props.sequences.filter(x => x.uuid == uuid)[0];
|
||||
maybeWarnAboutParameters(s);
|
||||
this.props.dispatch(setSequence(uuid));
|
||||
}
|
||||
|
||||
onChange = (event: DropDownItem) => {
|
||||
const uuid = event.value;
|
||||
_.isString(uuid) ? this.commitChange(uuid) : bail(BAD_UUID);
|
||||
isString(uuid) ? this.commitChange(uuid) : bail(BAD_UUID);
|
||||
}
|
||||
|
||||
SequenceSelectBox = () =>
|
||||
<Col xs={6}>
|
||||
<div>
|
||||
<label>{t("Sequence")}</label>
|
||||
<FBSelect onChange={this.onChange}
|
||||
selectedItem={this.selected()}
|
||||
list={this.all()}
|
||||
placeholder="Pick a sequence (or save a new one)" />
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
TimeSelection = () =>
|
||||
<Col xs={6}>
|
||||
<div>
|
||||
<label>{t("Time")}</label>
|
||||
<i className="fa fa-clock-o" onClick={() =>
|
||||
this.props.dispatch(setTimeOffset(timeToMs(
|
||||
moment().add(3, "minutes").format("HH:mm"))))} />
|
||||
<BlurableInput type="time"
|
||||
value={msToTime(this.props.dailyOffsetMs)}
|
||||
onCommit={({ currentTarget }) => {
|
||||
this.props.dispatch(setTimeOffset(timeToMs(currentTarget.value)));
|
||||
}} />
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatch,
|
||||
dailyOffsetMs,
|
||||
weeks,
|
||||
sequences
|
||||
} = this.props;
|
||||
const { dispatch, weeks, sequences } = this.props;
|
||||
const active = !!(sequences && sequences.length);
|
||||
return <div>
|
||||
<AddButton
|
||||
active={active}
|
||||
click={() => { dispatch(commitBulkEditor()); }} />
|
||||
click={() => dispatch(commitBulkEditor())} />
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<div>
|
||||
<label>{t("Sequence")}</label>
|
||||
<FBSelect onChange={this.onChange}
|
||||
selectedItem={this.selected()}
|
||||
list={this.all()}
|
||||
placeholder="Pick a sequence (or save a new one)" />
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<div>
|
||||
<label>{t("Time")}</label>
|
||||
<i className="fa fa-clock-o" onClick={() =>
|
||||
this.props.dispatch(setTimeOffset(timeToMs(
|
||||
moment().add(3, "minutes").format("HH:mm"))))
|
||||
} />
|
||||
<BlurableInput type="time"
|
||||
value={msToTime(dailyOffsetMs)}
|
||||
onCommit={({ currentTarget }) => {
|
||||
dispatch(setTimeOffset(timeToMs(currentTarget.value)));
|
||||
}} />
|
||||
</div>
|
||||
</Col>
|
||||
<this.SequenceSelectBox />
|
||||
<this.TimeSelection />
|
||||
</Row>
|
||||
<WeekGrid weeks={weeks} dispatch={dispatch} />
|
||||
</div>;
|
||||
|
|
|
@ -1,14 +1,5 @@
|
|||
import { TaggedSequence } from "farmbot";
|
||||
import { isParameterized } from "../../sequences/locals_list/is_parameterized";
|
||||
import { error } from "farmbot-toastr";
|
||||
import { t } from "i18next";
|
||||
import * as moment from "moment";
|
||||
import * as _ from "lodash";
|
||||
import { Content } from "../../constants";
|
||||
|
||||
export function maybeWarnAboutParameters(s: undefined | TaggedSequence) {
|
||||
s && isParameterized(s.body) && error(t(Content.NO_PARAMETERS));
|
||||
}
|
||||
|
||||
export function msToTime(ms: number) {
|
||||
if (_.isNumber(ms)) {
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
jest.mock("../../../api/crud", () => ({
|
||||
overwrite: jest.fn(),
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { ActiveEditor } from "../active_editor";
|
||||
import { ActiveEditor, editRegimenDeclarations } from "../active_editor";
|
||||
import { fakeRegimen } from "../../../__test_support__/fake_state/resources";
|
||||
import { ActiveEditorProps } from "../interfaces";
|
||||
import { Actions } from "../../../constants";
|
||||
import { SpecialStatus } from "farmbot";
|
||||
import { VariableDeclaration } from "farmbot";
|
||||
import {
|
||||
buildResourceIndex
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
import { overwrite } from "../../../api/crud";
|
||||
|
||||
describe("<ActiveEditor />", () => {
|
||||
const props: ActiveEditorProps = {
|
||||
const fakeProps = (): ActiveEditorProps => ({
|
||||
dispatch: jest.fn(),
|
||||
regimen: fakeRegimen(),
|
||||
calendar: [{
|
||||
|
@ -24,25 +31,44 @@ describe("<ActiveEditor />", () => {
|
|||
sequence_id: 0, time_offset: 1000
|
||||
}
|
||||
}]
|
||||
}]
|
||||
};
|
||||
}],
|
||||
resources: buildResourceIndex([]).index,
|
||||
shouldDisplay: () => false,
|
||||
variableData: {},
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<ActiveEditor {...props} />);
|
||||
const wrapper = mount(<ActiveEditor {...fakeProps()} />);
|
||||
["Day", "Item 0", "10:00"].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
|
||||
it("removes regimen item", () => {
|
||||
const wrapper = mount(<ActiveEditor {...props} />);
|
||||
const keptItem = { sequence_id: 1, time_offset: 1000 };
|
||||
const p = fakeProps();
|
||||
p.calendar[0].items[0].regimen.body.regimen_items =
|
||||
[p.calendar[0].items[0].item, keptItem];
|
||||
const wrapper = mount(<ActiveEditor {...p} />);
|
||||
wrapper.find("i").simulate("click");
|
||||
expect(props.dispatch).toHaveBeenCalledWith({
|
||||
payload: {
|
||||
update: expect.objectContaining({ regimen_items: [] }),
|
||||
uuid: expect.stringContaining("Regimen"),
|
||||
specialStatus: SpecialStatus.DIRTY
|
||||
},
|
||||
type: Actions.OVERWRITE_RESOURCE
|
||||
});
|
||||
expect(overwrite).toHaveBeenCalledWith(expect.any(Object),
|
||||
expect.objectContaining({ regimen_items: [keptItem] }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("editRegimenDeclarations()", () => {
|
||||
const declaration: VariableDeclaration = {
|
||||
kind: "variable_declaration",
|
||||
args: {
|
||||
label: "label", data_value: {
|
||||
kind: "identifier", args: { label: "new_var" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
it("updates declarations", () => {
|
||||
const regimen = fakeRegimen();
|
||||
editRegimenDeclarations({ dispatch: jest.fn(), regimen })([])(declaration);
|
||||
expect(overwrite).toHaveBeenCalledWith(regimen,
|
||||
expect.objectContaining({ body: [declaration] }));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -34,7 +34,7 @@ describe("<CopyButton />", () => {
|
|||
wrapper.simulate("click");
|
||||
expect(p.dispatch).toHaveBeenCalled();
|
||||
expect(init).toHaveBeenCalledWith("Regimen", {
|
||||
color: "red", name: "Foo copy 1", regimen_items
|
||||
color: "red", name: "Foo copy 1", regimen_items, body: []
|
||||
});
|
||||
expect(push).toHaveBeenCalledWith("/app/regimens/foo_copy_1");
|
||||
expect(setActiveRegimenByName).toHaveBeenCalled();
|
||||
|
|
|
@ -14,6 +14,9 @@ import { RegimenEditorProps } from "../interfaces";
|
|||
import { destroy, save } from "../../../api/crud";
|
||||
import { clickButton } from "../../../__test_support__/helpers";
|
||||
import { SpecialStatus } from "farmbot";
|
||||
import {
|
||||
buildResourceIndex
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
|
||||
describe("<RegimenEditor />", () => {
|
||||
function fakeProps(): RegimenEditorProps {
|
||||
|
@ -36,7 +39,10 @@ describe("<RegimenEditor />", () => {
|
|||
sequence_id: 0, time_offset: 1000
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}],
|
||||
resources: buildResourceIndex([]).index,
|
||||
shouldDisplay: () => false,
|
||||
variableData: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +1,27 @@
|
|||
jest.mock("../../actions", () => ({ editRegimen: jest.fn() }));
|
||||
|
||||
import { write } from "../regimen_name_input";
|
||||
import * as React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { write, RegimenNameInput } from "../regimen_name_input";
|
||||
import { fakeRegimen } from "../../../__test_support__/fake_state/resources";
|
||||
import { editRegimen } from "../../actions";
|
||||
import { inputEvent } from "../../../__test_support__/fake_input_event";
|
||||
|
||||
const fakeProps = () => ({ regimen: fakeRegimen(), dispatch: jest.fn() });
|
||||
|
||||
describe("write()", () => {
|
||||
it("crashes without a regimen", () => {
|
||||
const input = { regimen: undefined, dispatch: jest.fn() };
|
||||
expect(() => write(input)).toThrowError();
|
||||
});
|
||||
|
||||
it("calls dispatch", () => {
|
||||
const input = { regimen: fakeRegimen(), dispatch: jest.fn() };
|
||||
const callback = write(input);
|
||||
expect(callback).toBeInstanceOf(Function);
|
||||
const value = "FOO";
|
||||
// tslint:disable-next-line:no-any
|
||||
callback({ currentTarget: { value } } as any);
|
||||
expect(input.dispatch).toHaveBeenCalled();
|
||||
expect(editRegimen).toHaveBeenCalled();
|
||||
const p = fakeProps();
|
||||
write(p)(inputEvent("foo"));
|
||||
expect(editRegimen).toHaveBeenCalledWith(p.regimen, { name: "foo" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("<RegimenNameInput />", () => {
|
||||
it("changes color", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<RegimenNameInput {...p} />);
|
||||
wrapper.find("ColorPicker").simulate("change", "red");
|
||||
expect(editRegimen).toHaveBeenCalledWith(p.regimen, { color: "red" });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,58 +3,97 @@ import { RegimenNameInput } from "./regimen_name_input";
|
|||
import { ActiveEditorProps } from "./interfaces";
|
||||
import { t } from "i18next";
|
||||
import { push } from "../../history";
|
||||
import { RegimenItem } from "../interfaces";
|
||||
import { TaggedRegimen } from "farmbot";
|
||||
import {
|
||||
RegimenItem, CalendarRow, RegimenItemCalendarRow, RegimenProps
|
||||
} from "../interfaces";
|
||||
import { TaggedRegimen, VariableDeclaration } from "farmbot";
|
||||
import { defensiveClone } from "../../util";
|
||||
import { overwrite, save, destroy } from "../../api/crud";
|
||||
import { SaveBtn } from "../../ui";
|
||||
import { CopyButton } from "./copy_button";
|
||||
import { LocalsList } from "../../sequences/locals_list/locals_list";
|
||||
import {
|
||||
addOrEditVarDeclaration
|
||||
} from "../../sequences/locals_list/declaration_support";
|
||||
import {
|
||||
AllowedDeclaration
|
||||
} from "../../sequences/locals_list/locals_list_support";
|
||||
|
||||
/**
|
||||
* The bottom half of the regimen editor panel (when there's something to
|
||||
* actually edit).
|
||||
*/
|
||||
export function ActiveEditor(props: ActiveEditorProps) {
|
||||
const regimenProps = { regimen: props.regimen, dispatch: props.dispatch };
|
||||
return <div className="regimen-editor-content">
|
||||
<div className="regimen-editor-tools">
|
||||
<div className="button-group">
|
||||
<SaveBtn
|
||||
status={props.regimen.specialStatus}
|
||||
onClick={() => props.dispatch(save(props.regimen.uuid))} />
|
||||
<CopyButton regimen={props.regimen} dispatch={props.dispatch} />
|
||||
<button className="fb-button red"
|
||||
onClick={() => {
|
||||
props.dispatch(destroy(props.regimen.uuid)).then(
|
||||
() => push("/app/regimens/"));
|
||||
}}>
|
||||
{t("Delete")}
|
||||
</button>
|
||||
</div>
|
||||
<RegimenNameInput regimen={props.regimen} dispatch={props.dispatch} />
|
||||
<RegimenButtonGroup {...regimenProps} />
|
||||
<RegimenNameInput {...regimenProps} />
|
||||
<LocalsList
|
||||
locationDropdownKey={JSON.stringify(props.regimen)}
|
||||
declarations={props.regimen.body.body}
|
||||
variableData={props.variableData}
|
||||
sequenceUuid={props.regimen.uuid}
|
||||
resources={props.resources}
|
||||
onChange={editRegimenDeclarations(regimenProps)(props.regimen.body.body)}
|
||||
allowedDeclarations={AllowedDeclaration.variable}
|
||||
shouldDisplay={props.shouldDisplay} />
|
||||
<hr />
|
||||
</div>
|
||||
<div className="regimen">
|
||||
{props.calendar.map(function (group, index1) {
|
||||
return <div className="regimen-day" key={index1}>
|
||||
<label> {t("Day {{day}}", { day: group.day })} </label>
|
||||
{group.items.map(function (row, index2) {
|
||||
const { item, regimen } = row;
|
||||
const click = () => props.dispatch(removeRegimenItem(item, regimen));
|
||||
const klass = `${row.color} regimen-event`;
|
||||
return <div className={klass} key={`${index1}.${index2}`}>
|
||||
<span className="regimen-event-title">{row.name}</span>
|
||||
<span className="regimen-event-time">{row.hhmm}</span>
|
||||
<i className="fa fa-trash regimen-control" onClick={click} />
|
||||
</div>;
|
||||
})}
|
||||
</div>;
|
||||
})}
|
||||
</div>
|
||||
<RegimenRows calendar={props.calendar} dispatch={props.dispatch} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
function removeRegimenItem(item: RegimenItem, r: TaggedRegimen) {
|
||||
export const editRegimenDeclarations = (props: RegimenProps) =>
|
||||
(declarations: VariableDeclaration[]) =>
|
||||
(declaration: VariableDeclaration) => {
|
||||
const copy = defensiveClone(props.regimen);
|
||||
copy.body.body = addOrEditVarDeclaration(declarations, declaration);
|
||||
props.dispatch(overwrite(props.regimen, copy.body));
|
||||
};
|
||||
|
||||
const RegimenButtonGroup = (props: RegimenProps) =>
|
||||
<div className="button-group">
|
||||
<SaveBtn
|
||||
status={props.regimen.specialStatus}
|
||||
onClick={() => props.dispatch(save(props.regimen.uuid))} />
|
||||
<CopyButton regimen={props.regimen} dispatch={props.dispatch} />
|
||||
<button className="fb-button red"
|
||||
onClick={() => props.dispatch(destroy(props.regimen.uuid))
|
||||
.then(() => push("/app/regimens/"))}>
|
||||
{t("Delete")}
|
||||
</button>
|
||||
</div>;
|
||||
|
||||
interface RegimenRowsProps {
|
||||
calendar: CalendarRow[];
|
||||
dispatch: Function;
|
||||
}
|
||||
|
||||
const RegimenRows = (props: RegimenRowsProps) =>
|
||||
<div className="regimen">
|
||||
{props.calendar.map(regimenDay(props.dispatch))}
|
||||
</div>;
|
||||
|
||||
const regimenDay = (dispatch: Function) =>
|
||||
(group: CalendarRow, dayIndex: number) =>
|
||||
<div className="regimen-day" key={dayIndex}>
|
||||
<label> {t("Day {{day}}", { day: group.day })} </label>
|
||||
{group.items.map(regimenItemRow(dispatch, dayIndex))}
|
||||
</div>;
|
||||
|
||||
const regimenItemRow = (dispatch: Function, dayIndex: number) =>
|
||||
(row: RegimenItemCalendarRow, itemIndex: number) =>
|
||||
<div className={`${row.color} regimen-event`}
|
||||
key={`${dayIndex}.${itemIndex}`}>
|
||||
<span className="regimen-event-title">{row.name}</span>
|
||||
<span className="regimen-event-time">{row.hhmm}</span>
|
||||
<i className="fa fa-trash regimen-control" onClick={() =>
|
||||
dispatch(removeRegimenItem(row.item, row.regimen))} />
|
||||
</div>;
|
||||
|
||||
const removeRegimenItem = (item: RegimenItem, r: TaggedRegimen) => {
|
||||
const copy = defensiveClone(r);
|
||||
copy.body.regimen_items = r.body.regimen_items.filter(x => x !== item);
|
||||
return overwrite(r, copy.body);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -19,6 +19,9 @@ export function RegimenEditor(props: RegimenEditorProps) {
|
|||
{regimen && <ActiveEditor
|
||||
dispatch={dispatch}
|
||||
regimen={regimen}
|
||||
calendar={calendar} />}
|
||||
calendar={calendar}
|
||||
resources={props.resources}
|
||||
variableData={props.variableData}
|
||||
shouldDisplay={props.shouldDisplay} />}
|
||||
</EmptyStateWrapper>;
|
||||
}
|
||||
|
|
|
@ -5,11 +5,16 @@ import {
|
|||
} from "../interfaces";
|
||||
import { TaggedRegimen } from "farmbot";
|
||||
import { Actions } from "../../constants";
|
||||
import { ResourceIndex, VariableNameSet } from "../../resources/interfaces";
|
||||
import { ShouldDisplay } from "../../devices/interfaces";
|
||||
|
||||
export interface ActiveEditorProps {
|
||||
regimen: TaggedRegimen;
|
||||
calendar: CalendarRow[];
|
||||
dispatch: Function;
|
||||
resources: ResourceIndex;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
variableData: VariableNameSet;
|
||||
}
|
||||
|
||||
export interface RegimenItemListProps {
|
||||
|
@ -31,6 +36,9 @@ export interface RegimenEditorProps {
|
|||
current: TaggedRegimen | undefined;
|
||||
dispatch: Function;
|
||||
calendar: CalendarRow[];
|
||||
resources: ResourceIndex;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
variableData: VariableNameSet;
|
||||
}
|
||||
|
||||
export interface CopyButtonProps {
|
||||
|
|
|
@ -4,19 +4,13 @@ import { t } from "i18next";
|
|||
import { Row, Col, ColorPicker } from "../../ui/index";
|
||||
import { editRegimen } from "../actions";
|
||||
|
||||
export function write({ dispatch, regimen }: RegimenProps):
|
||||
React.EventHandler<React.FormEvent<{}>> {
|
||||
if (regimen) {
|
||||
return (event: React.FormEvent<HTMLInputElement>) => {
|
||||
dispatch(editRegimen(regimen, { name: event.currentTarget.value }));
|
||||
};
|
||||
} else {
|
||||
throw new Error("Regimen is required");
|
||||
}
|
||||
}
|
||||
export const write = ({ dispatch, regimen }: RegimenProps):
|
||||
React.EventHandler<React.FormEvent<{}>> =>
|
||||
(event: React.FormEvent<HTMLInputElement>) =>
|
||||
dispatch(editRegimen(regimen, { name: event.currentTarget.value }));
|
||||
|
||||
export function RegimenNameInput({ regimen, dispatch }: RegimenProps) {
|
||||
const value = (regimen && regimen.body.name) || "";
|
||||
const value = regimen.body.name || "";
|
||||
return <Row>
|
||||
<Col xs={11}>
|
||||
<input
|
||||
|
@ -27,8 +21,8 @@ export function RegimenNameInput({ regimen, dispatch }: RegimenProps) {
|
|||
</Col>
|
||||
<Col xs={1} className="color-picker-col">
|
||||
<ColorPicker
|
||||
current={(regimen && regimen.body.color) || "gray"}
|
||||
onChange={(color) => dispatch(editRegimen(regimen, { color }))} />
|
||||
current={regimen.body.color || "gray"}
|
||||
onChange={color => dispatch(editRegimen(regimen, { color }))} />
|
||||
</Col>
|
||||
</Row>;
|
||||
}
|
||||
|
|
|
@ -40,7 +40,10 @@ export class Regimens extends React.Component<Props, {}> {
|
|||
<RegimenEditor
|
||||
dispatch={this.props.dispatch}
|
||||
calendar={this.props.calendar}
|
||||
current={this.props.current} />
|
||||
current={this.props.current}
|
||||
resources={this.props.resources}
|
||||
variableData={this.props.variableData}
|
||||
shouldDisplay={this.props.shouldDisplay} />
|
||||
</CenterPanel>
|
||||
<RightPanel
|
||||
className="bulk-scheduler"
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Color } from "../interfaces";
|
||||
import { Week } from "./bulk_scheduler/interfaces";
|
||||
import { AuthState } from "../auth/interfaces";
|
||||
import { BotState } from "../devices/interfaces";
|
||||
import { BotState, ShouldDisplay } from "../devices/interfaces";
|
||||
import { TaggedRegimen, TaggedSequence } from "farmbot";
|
||||
import { ResourceIndex, UUID } from "../resources/interfaces";
|
||||
import { ResourceIndex, UUID, VariableNameSet } from "../resources/interfaces";
|
||||
|
||||
export interface CalendarRow {
|
||||
day: string;
|
||||
|
@ -13,6 +13,7 @@ export interface CalendarRow {
|
|||
export interface Props {
|
||||
dispatch: Function;
|
||||
sequences: TaggedSequence[];
|
||||
variableData: VariableNameSet;
|
||||
auth: AuthState | undefined;
|
||||
bot: BotState;
|
||||
current: TaggedRegimen | undefined;
|
||||
|
@ -22,7 +23,8 @@ export interface Props {
|
|||
dailyOffsetMs: number;
|
||||
weeks: Week[];
|
||||
calendar: CalendarRow[];
|
||||
regimenUsageStats: Record<UUID, boolean | undefined>
|
||||
regimenUsageStats: Record<UUID, boolean | undefined>;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
}
|
||||
|
||||
export interface RegimenItemCalendarRow {
|
||||
|
@ -39,7 +41,7 @@ export interface RegimenItemCalendarRow {
|
|||
|
||||
/** Used by UI widgets that modify a regimen */
|
||||
export interface RegimenProps {
|
||||
regimen?: TaggedRegimen;
|
||||
regimen: TaggedRegimen;
|
||||
dispatch: Function;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
let mockPath = "/app/regimens";
|
||||
jest.mock("../../../history", () => ({
|
||||
history: { getCurrentLocation: () => ({ pathname: mockPath }) },
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { RegimenListItemProps } from "../../interfaces";
|
||||
import { RegimenListItem } from "../regimen_list_item";
|
||||
|
@ -5,6 +10,7 @@ import { render, shallow } from "enzyme";
|
|||
import { fakeRegimen } from "../../../__test_support__/fake_state/resources";
|
||||
import { SpecialStatus } from "farmbot";
|
||||
import { Actions } from "../../../constants";
|
||||
import { urlFriendly } from "../../../util";
|
||||
|
||||
describe("<RegimenListItem/>", () => {
|
||||
const fakeProps = (): RegimenListItemProps => {
|
||||
|
@ -54,4 +60,18 @@ describe("<RegimenListItem/>", () => {
|
|||
payload: props.regimen.uuid
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't set regimen as active", () => {
|
||||
const p = fakeProps();
|
||||
mockPath = "/app/regimens";
|
||||
const wrapper = render(<RegimenListItem {...p} />);
|
||||
expect(wrapper.find(".active").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("sets active regimen", () => {
|
||||
const p = fakeProps();
|
||||
mockPath = "/app/regimens/" + urlFriendly(p.regimen.body.name);
|
||||
const wrapper = render(<RegimenListItem {...p} />);
|
||||
expect(wrapper.find(".active").length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,7 +10,8 @@ import { urlFriendly } from "../../util";
|
|||
const emptyRegimenBody = (length: number): TaggedRegimen["body"] => ({
|
||||
name: (t("New regimen ") + (length++)),
|
||||
color: "gray",
|
||||
regimen_items: []
|
||||
regimen_items: [],
|
||||
body: [],
|
||||
});
|
||||
|
||||
export function AddRegimen(props: AddRegimenProps) {
|
||||
|
|
|
@ -8,14 +8,14 @@ import { Link } from "../../link";
|
|||
|
||||
export function RegimenListItem({ regimen, dispatch, inUse }: RegimenListItemProps) {
|
||||
const name = (regimen.body.name || "") + (regimen.specialStatus ? " *" : "");
|
||||
const color = (regimen.body.color) || "gray";
|
||||
const style = [`block`, `full-width`, `fb-button`, `${color}`];
|
||||
lastUrlChunk() === urlFriendly(regimen.body.name) && style.push("active");
|
||||
const color = regimen.body.color || "gray";
|
||||
const classNames = [`block`, `full-width`, `fb-button`, `${color}`];
|
||||
lastUrlChunk() === urlFriendly(regimen.body.name) && classNames.push("active");
|
||||
return <Link
|
||||
to={`/app/regimens/${urlFriendly(regimen.body.name)}`}
|
||||
key={regimen.uuid}>
|
||||
<button
|
||||
className={style.join(" ")}
|
||||
className={classNames.join(" ")}
|
||||
onClick={() => dispatch(selectRegimen(regimen.uuid))}>
|
||||
<label>{name}</label>
|
||||
{inUse && <i className="in-use fa fa-hdd-o" title={t(Content.IN_USE)} />}
|
||||
|
|
|
@ -1,18 +1,25 @@
|
|||
import { Everything } from "../interfaces";
|
||||
import { Props, RegimenItem, RegimenItemCalendarRow, CalendarRow } from "./interfaces";
|
||||
import {
|
||||
Props, RegimenItem, RegimenItemCalendarRow, CalendarRow
|
||||
} from "./interfaces";
|
||||
import {
|
||||
selectAllSequences,
|
||||
selectAllRegimens,
|
||||
maybeGetSequence,
|
||||
maybeGetRegimen,
|
||||
findId,
|
||||
findSequence
|
||||
findSequence,
|
||||
maybeGetDevice,
|
||||
findSequenceById
|
||||
} from "../resources/selectors";
|
||||
import { TaggedRegimen } from "farmbot";
|
||||
import { duration } from "moment";
|
||||
import * as moment from "moment";
|
||||
import { ResourceIndex } from "../resources/interfaces";
|
||||
import { randomColor } from "../util";
|
||||
import { ResourceIndex, UUID, VariableNameSet } from "../resources/interfaces";
|
||||
import {
|
||||
randomColor, determineInstalledOsVersion,
|
||||
shouldDisplay as shouldDisplayFunc
|
||||
} from "../util";
|
||||
import * as _ from "lodash";
|
||||
import { resourceUsageList } from "../resources/in_use";
|
||||
|
||||
|
@ -25,9 +32,29 @@ export function mapStateToProps(props: Everything): Props {
|
|||
const calendar = current ?
|
||||
generateCalendar(current, index, dispatch) : [];
|
||||
|
||||
const installedOsVersion = determineInstalledOsVersion(
|
||||
props.bot, maybeGetDevice(props.resources.index));
|
||||
const shouldDisplay = shouldDisplayFunc(
|
||||
installedOsVersion, props.bot.minOsFeatureData);
|
||||
|
||||
const calledSequences = (): UUID[] => {
|
||||
if (current) {
|
||||
const sequenceIds = current.body.regimen_items.map(x => x.sequence_id);
|
||||
return Array.from(new Set(sequenceIds)).map(x =>
|
||||
findSequenceById(props.resources.index, x).uuid);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const variableData: VariableNameSet = {};
|
||||
calledSequences().map(uuid =>
|
||||
Object.entries(props.resources.index.sequenceMetas[uuid] || {})
|
||||
.map(([key, value]) => !variableData[key] && (variableData[key] = value)));
|
||||
|
||||
return {
|
||||
dispatch: props.dispatch,
|
||||
sequences: selectAllSequences(index),
|
||||
variableData,
|
||||
resources: index,
|
||||
auth: props.auth,
|
||||
current,
|
||||
|
@ -37,7 +64,8 @@ export function mapStateToProps(props: Everything): Props {
|
|||
weeks,
|
||||
bot,
|
||||
calendar,
|
||||
regimenUsageStats: resourceUsageList(props.resources.index.inUse)
|
||||
regimenUsageStats: resourceUsageList(props.resources.index.inUse),
|
||||
shouldDisplay,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -109,7 +109,8 @@ describe("in_use tracking at reducer level", () => {
|
|||
color: "red",
|
||||
regimen_items: [
|
||||
{ sequence_id, time_offset: 12 }
|
||||
]
|
||||
],
|
||||
body: [],
|
||||
})[0];
|
||||
const ri = buildResourceIndex([regimen, sequence]);
|
||||
expect(resourceUsageList(ri.index.inUse)[sequence.uuid]).toBe(true);
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import { declarationList } from "../declaration_support";
|
||||
import { declarationList, mergeVariableDeclarations } from "../declaration_support";
|
||||
import { fakeVariableNameSet } from "../../../__test_support__/fake_variables";
|
||||
import { VariableDeclaration, ParameterDeclaration } from "farmbot";
|
||||
import { cloneDeep } from "lodash";
|
||||
|
||||
describe("declarationList()", () => {
|
||||
it("returns undefined", () => {
|
||||
|
@ -38,3 +41,57 @@ describe("declarationList()", () => {
|
|||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mergeVariableDeclarations()", () => {
|
||||
const declarations: VariableDeclaration[] = [
|
||||
{
|
||||
kind: "variable_declaration", args: {
|
||||
label: "parent1", data_value: {
|
||||
kind: "coordinate", args: { x: 1, y: 2, z: 3 }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
kind: "variable_declaration", args: {
|
||||
label: "parent2", data_value: {
|
||||
kind: "coordinate", args: { x: 1, y: 2, z: 3 }
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
it("doesn't overwrite declarations", () => {
|
||||
const varData = fakeVariableNameSet("parent1");
|
||||
const result = mergeVariableDeclarations(varData, declarations);
|
||||
expect(result).toEqual(declarations);
|
||||
});
|
||||
|
||||
it("adds new declarations", () => {
|
||||
// "parent2" will not be added: already exists
|
||||
const varData = fakeVariableNameSet("parent2");
|
||||
|
||||
// "parent3" will not be added: already defined
|
||||
const notAdded = "parent3";
|
||||
varData[notAdded] = fakeVariableNameSet(notAdded)[notAdded];
|
||||
|
||||
// "parent4" will be added to the existing declarations
|
||||
const label = "parent4";
|
||||
const add = fakeVariableNameSet(label)[label];
|
||||
const addedNewDecl: ParameterDeclaration = {
|
||||
kind: "parameter_declaration", args: { label, data_type: "point" }
|
||||
};
|
||||
add && (add.celeryNode = addedNewDecl);
|
||||
varData[label] = add;
|
||||
const expected = cloneDeep(declarations);
|
||||
expected.push({
|
||||
kind: "variable_declaration", args: {
|
||||
label, data_value: {
|
||||
kind: "coordinate", args: { x: 0, y: 0, z: 0 }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const result = mergeVariableDeclarations(varData, declarations);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -40,3 +40,19 @@ const reduceVarDeclarations = (declarations: VariableDeclaration[]):
|
|||
declarations.map(d => items[d.args.label] = d);
|
||||
return items;
|
||||
};
|
||||
|
||||
/** Add new variable declarations if they don't already exist. */
|
||||
export const mergeVariableDeclarations = (
|
||||
varData: VariableNameSet | undefined,
|
||||
declarations: VariableDeclaration[]
|
||||
): VariableDeclaration[] => {
|
||||
/** New variables required by the chosen sequence. */
|
||||
const newVars = reduceVarDeclarations(declarationList(varData) || []);
|
||||
const regimenVars = reduceVarDeclarations(declarations);
|
||||
Object.entries(newVars)
|
||||
/** Filter out variables already in the Regimen. */
|
||||
.filter(([k, _]) => !Object.keys(regimenVars).includes(k))
|
||||
/** Add the remaining new variables to the Regimen body. */
|
||||
.map(([k, v]) => regimenVars[k] = v);
|
||||
return Object.values(regimenVars);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue