diff --git a/frontend/constants.ts b/frontend/constants.ts
index 747697abba..ab188de610 100644
--- a/frontend/constants.ts
+++ b/frontend/constants.ts
@@ -11,11 +11,10 @@ export namespace ToolTips {
every time, so the ordering shown below will only be representative.`);
export const CRITERIA_SELECTION_COUNT =
- trim(`Filter additions can only be removed by changing filters.
- Click and drag in the map to modify selection filters.
- Filters will be applied at the time of sequence execution. The final
- selection at that time may differ from the selection currently
- displayed.`);
+ trim(`Manually add group members by clicking in the map. Group members
+ selected by filters can only be removed by changing the filters.
+ Filters will be applied at the time of sequence execution.
+ Group members at that time may differ from those currently displayed.`);
export const DOT_NOTATION_TIP =
trim(`Tip: Use dot notation (i.e., 'meta.color') to access meta fields.`);
diff --git a/frontend/css/farm_designer/farm_designer_panels.scss b/frontend/css/farm_designer/farm_designer_panels.scss
index 4ad02f932d..67718b9c20 100644
--- a/frontend/css/farm_designer/farm_designer_panels.scss
+++ b/frontend/css/farm_designer/farm_designer_panels.scss
@@ -1670,6 +1670,20 @@ li {
}
.group-detail-panel {
+ .panel-header-icon-group {
+ display: flex;
+ float: right;
+ padding: 1.5rem;
+ .fa-sort {
+ margin-top: 0.25rem;
+ color: $white;
+ }
+ .fa-trash {
+ margin-top: 0.25rem;
+ margin-left: 1rem;
+ color: $dark_red;
+ }
+ }
.panel-content {
max-height: calc(100vh - 14rem);
overflow-y: auto;
@@ -1915,6 +1929,7 @@ li {
.lt-gt-criteria,
.location-criteria {
display: inline-block;
+ position: relative;
.row {
margin-left: 0;
margin-right: -2.5rem;
@@ -1937,7 +1952,9 @@ li {
margin-top: 2rem !important;
}
.edit-in-map {
- float: right;
+ position: absolute;
+ top: 0;
+ right: 0;
button {
margin: 1rem !important;
width: 5rem !important;
@@ -2440,6 +2457,7 @@ li {
}
}
+.group-detail-panel,
.designer-regimen-editor-panel,
.designer-sequence-editor-panel {
.panel-header {
diff --git a/frontend/point_groups/__tests__/actions_test.ts b/frontend/point_groups/__tests__/actions_test.ts
index 91334a8672..a29bcfc187 100644
--- a/frontend/point_groups/__tests__/actions_test.ts
+++ b/frontend/point_groups/__tests__/actions_test.ts
@@ -40,7 +40,7 @@ describe("createGroup()", () => {
expect(init).toHaveBeenCalledWith("PointGroup", expect.objectContaining({
name: "Name123",
point_ids: [1, 2],
- sort_type: "xy_ascending",
+ sort_type: "nn",
criteria: DEFAULT_CRITERIA,
}));
expect(save).toHaveBeenCalledWith("???");
@@ -62,7 +62,7 @@ describe("createGroup()", () => {
expect(init).toHaveBeenCalledWith("PointGroup", expect.objectContaining({
name: "Untitled Group",
point_ids: [4],
- sort_type: "xy_ascending",
+ sort_type: "nn",
criteria: DEFAULT_CRITERIA,
}));
expect(save).toHaveBeenCalledWith("???");
diff --git a/frontend/point_groups/__tests__/group_detail_active_test.tsx b/frontend/point_groups/__tests__/group_detail_active_test.tsx
index 62417d9331..a5fef57500 100644
--- a/frontend/point_groups/__tests__/group_detail_active_test.tsx
+++ b/frontend/point_groups/__tests__/group_detail_active_test.tsx
@@ -1,9 +1,3 @@
-jest.mock("../../api/crud", () => ({
- save: jest.fn(),
- overwrite: jest.fn(),
- edit: jest.fn(),
-}));
-
jest.mock("../../farm_designer/map/actions", () => ({
setHoveredPlant: jest.fn(),
}));
@@ -15,26 +9,25 @@ jest.mock("../../plants/select_plants", () => ({
}));
jest.mock("../../ui/help", () => ({
- Help: jest.fn(props =>
{props.text}
),
+ Help: jest.fn(props => {props.text}{props.customIcon}
),
}));
import React from "react";
import {
- GroupDetailActive, GroupDetailActiveProps,
+ GroupDetailActive, GroupDetailActiveProps, GroupSortSelection,
+ GroupSortSelectionProps,
} from "../group_detail_active";
-import { mount, shallow } from "enzyme";
+import { mount } from "enzyme";
import {
fakePointGroup, fakePlant, fakePoint,
} from "../../__test_support__/fake_state/resources";
-import { edit } from "../../api/crud";
import { SpecialStatus } from "farmbot";
import { DEFAULT_CRITERIA } from "../criteria/interfaces";
import { setSelectionPointType } from "../../plants/select_plants";
import { fakeToolTransformProps } from "../../__test_support__/fake_tool_info";
-import { ToolTips } from "../../constants";
import { cloneDeep } from "lodash";
-describe("", () => {
+describe("", () => {
const fakeProps = (): GroupDetailActiveProps => {
const plant = fakePlant();
plant.body.id = 1;
@@ -73,7 +66,6 @@ describe("", () => {
const p = fakeProps();
p.group.specialStatus = SpecialStatus.SAVED;
const wrapper = mount();
- expect(wrapper.find("input").first().prop("defaultValue")).toContain("XYZ");
expect(wrapper.find(".group-member-display").length).toEqual(1);
});
@@ -85,58 +77,29 @@ describe("", () => {
expect(setSelectionPointType).toHaveBeenCalledWith(undefined);
});
- it("changes group name", () => {
- const p = fakeProps();
- const parentWrapper = shallow();
- const wrapper = shallow(parentWrapper.find("GroupNameInput").getElement());
- wrapper.find("input").first().simulate("blur", {
- currentTarget: { value: "new group name" }
- });
- expect(edit).toHaveBeenCalledWith(p.group, { name: "new group name" });
- });
-
- it("doesn't change group name", () => {
- const p = fakeProps();
- const parentWrapper = shallow();
- const wrapper = shallow(parentWrapper.find("GroupNameInput").getElement());
- wrapper.find("input").first().simulate("blur", {
- currentTarget: { value: "" }
- });
- expect(edit).not.toHaveBeenCalled();
- });
-
- it("shows paths", () => {
- const p = fakeProps();
- const wrapper = mount();
- expect(wrapper.text().toLowerCase()).toContain("0m");
- });
-
- it("shows random warning text", () => {
- const p = fakeProps();
- p.group.body.sort_type = "random";
- const wrapper = mount();
- expect(wrapper.text()).toContain(ToolTips.SORT_DESCRIPTION);
- });
-
it("doesn't show icons", () => {
const wrapper = mount();
wrapper.setState({ iconDisplay: false });
expect(wrapper.find(".groups-list-wrapper").length).toEqual(0);
});
+});
+
+describe("", () => {
+ const fakeProps = (): GroupSortSelectionProps => ({
+ group: fakePointGroup(),
+ dispatch: jest.fn(),
+ pointsSelectedByGroup: [fakePoint()],
+ });
- it("doesn't show filters tooltip addition", () => {
- const wrapper = mount();
- expect(wrapper.text()).not.toContain(ToolTips.CRITERIA_SELECTION_COUNT);
+ it("renders", () => {
+ const wrapper = mount();
+ expect(wrapper.text().toLowerCase()).toContain("ascending");
});
- it("shows filters tooltip addition", () => {
+ it("renders random notice", () => {
const p = fakeProps();
- const point = fakePoint();
- point.body.x = 0;
- p.allPoints = [point];
- p.group.body.point_ids = [];
- p.group.body.criteria.number_eq = { x: [0] };
- const wrapper = mount();
- expect(wrapper.text()).toContain(ToolTips.CRITERIA_SELECTION_COUNT);
+ p.group.body.sort_type = "random";
+ const wrapper = mount();
+ expect(wrapper.html()).toContain("exclamation-triangle");
});
});
diff --git a/frontend/point_groups/__tests__/group_detail_test.tsx b/frontend/point_groups/__tests__/group_detail_test.tsx
index fe2834b648..3c63e0af50 100644
--- a/frontend/point_groups/__tests__/group_detail_test.tsx
+++ b/frontend/point_groups/__tests__/group_detail_test.tsx
@@ -7,6 +7,11 @@ jest.mock("../../history", () => ({
jest.mock("../group_detail_active", () => ({
GroupDetailActive: () => ,
+ GroupSortSelection: () => ,
+}));
+
+jest.mock("../../api/crud", () => ({
+ destroy: jest.fn(),
}));
import React from "react";
@@ -27,6 +32,7 @@ import {
fakePointGroup, fakeWebAppConfig,
} from "../../__test_support__/fake_state/resources";
import { PointType } from "farmbot";
+import { destroy } from "../../api/crud";
describe("", () => {
const fakeProps = (): GroupDetailProps => {
@@ -76,16 +82,23 @@ describe("", () => {
});
it.each<[string, PointType]>([
- ["plant", "Plant"],
- ["weed", "Weed"],
- ["point", "GenericPointer"],
- ["slot", "ToolSlot"],
+ ["plants", "Plant"],
+ ["weeds", "Weed"],
+ ["points", "GenericPointer"],
+ ["tools", "ToolSlot"],
])("renders %s group", (title, pointerType) => {
mockPath = Path.mock(Path.groups(1));
const p = fakeProps();
p.group && (p.group.body.criteria.string_eq = { pointer_type: [pointerType] });
const wrapper = mount();
- expect(wrapper.text().toLowerCase()).toContain(`edit ${title} group`);
+ expect(wrapper.html()).toContain("go back to " + title);
+ });
+
+ it("deletes group", () => {
+ mockPath = Path.mock(Path.groups(1));
+ const wrapper = mount();
+ wrapper.find(".fa-trash").first().simulate("click");
+ expect(destroy).toHaveBeenCalled();
});
});
diff --git a/frontend/point_groups/actions.ts b/frontend/point_groups/actions.ts
index 9af8128b5e..c782fe8c31 100644
--- a/frontend/point_groups/actions.ts
+++ b/frontend/point_groups/actions.ts
@@ -27,7 +27,7 @@ export const createGroup = (props: CreateGroupProps) =>
const group: PointGroup = {
name: groupName || t("Untitled Group"),
point_ids,
- sort_type: "xy_ascending",
+ sort_type: "nn",
criteria: criteria || DEFAULT_CRITERIA,
};
const action = init("PointGroup", group);
diff --git a/frontend/point_groups/criteria/show.tsx b/frontend/point_groups/criteria/show.tsx
index 628f02d792..5c3a3ba092 100644
--- a/frontend/point_groups/criteria/show.tsx
+++ b/frontend/point_groups/criteria/show.tsx
@@ -192,6 +192,20 @@ export const NumberLtGtInput = (props: NumberLtGtInputProps) => {
export const LocationSelection = (props: LocationSelectionProps) =>
{t("Location")}
+
+
+ props.dispatch({
+ type: Actions.EDIT_GROUP_AREA_IN_MAP,
+ payload: !props.editGroupAreaInMap
+ })} />
+
+
criteriaKey={axis}
group={props.group}
dispatch={props.dispatch} />)}
-
-
- props.dispatch({
- type: Actions.EDIT_GROUP_AREA_IN_MAP,
- payload: !props.editGroupAreaInMap
- })} />
-
-
;
diff --git a/frontend/point_groups/group_detail.tsx b/frontend/point_groups/group_detail.tsx
index b5a4355138..2fdd4c7801 100644
--- a/frontend/point_groups/group_detail.tsx
+++ b/frontend/point_groups/group_detail.tsx
@@ -7,7 +7,7 @@ import {
selectAllTools,
} from "../resources/selectors";
import { getPathArray } from "../history";
-import { GroupDetailActive } from "./group_detail_active";
+import { GroupDetailActive, GroupSortSelection } from "./group_detail_active";
import { uniq } from "lodash";
import { UUID } from "../resources/interfaces";
import {
@@ -23,6 +23,10 @@ import { BooleanSetting, NumericSetting } from "../session_keys";
import { isBotOriginQuadrant } from "../farm_designer/interfaces";
import { validPointTypes } from "../plants/select_plants";
import { Path } from "../internal_urls";
+import { destroy } from "../api/crud";
+import { ResourceTitle } from "../sequences/panel/editor";
+import { Popover } from "../ui";
+import { pointsSelectedByGroup } from "./criteria/apply";
export interface GroupDetailProps {
dispatch: Function;
@@ -95,8 +99,28 @@ export class RawGroupDetail extends React.Component {
+ titleElement={}
+ backTo={panelInfo(group).backTo}>
+
+ {group &&
+
}
+ content={
+
} />}
+ {group &&
+
this.props.dispatch(destroy(group.uuid))} />}
+
+
{group
?
diff --git a/frontend/point_groups/group_detail_active.tsx b/frontend/point_groups/group_detail_active.tsx
index fdc47bd27e..d59dc1b511 100644
--- a/frontend/point_groups/group_detail_active.tsx
+++ b/frontend/point_groups/group_detail_active.tsx
@@ -1,8 +1,6 @@
import React from "react";
import { t } from "../i18next_wrapper";
import { TaggedPointGroup, TaggedPoint, PointType, TaggedTool } from "farmbot";
-import { DeleteButton } from "../ui/delete_button";
-import { save, edit } from "../api/crud";
import { Paths } from "./paths";
import { ErrorBoundary } from "../error_boundary";
import {
@@ -48,9 +46,6 @@ export class GroupDetailActive
render() {
const { group, dispatch } = this.props;
return
-
-
-
- {t("DELETE GROUP")}
-
;
}
}
-interface GroupNameInputProps {
- group: TaggedPointGroup;
- dispatch: Function;
-}
-
-const GroupNameInput = (props: GroupNameInputProps) => {
- const { dispatch, group } = props;
- return
-
- {
- const newGroupName = e.currentTarget.value;
- if (newGroupName != "" && newGroupName != group.body.name) {
- dispatch(edit(group, { name: newGroupName }));
- dispatch(save(group.uuid));
- }
- }} />
-
;
-};
-
-interface GroupSortSelectionProps {
+export interface GroupSortSelectionProps {
group: TaggedPointGroup;
dispatch: Function;
pointsSelectedByGroup: TaggedPoint[];
}
/** Choose and view group point sort method. */
-const GroupSortSelection = (props: GroupSortSelectionProps) =>
+export const GroupSortSelection = (props: GroupSortSelectionProps) =>
-
+
{
export type ExtendedPointGroupSortType = PointGroupSortType;
const SORT_TYPES: ExtendedPointGroupSortType[] = [
- "random", "xy_ascending", "xy_descending", "yx_ascending", "yx_descending"];
+ "random", "yx_descending", "yx_ascending", "xy_descending", "xy_ascending"];
export interface PathInfoBarProps {
sortTypeKey: ExtendedPointGroupSortType;
@@ -152,9 +152,10 @@ export class Paths extends React.Component {
return
{SORT_TYPES
.concat(shouldDisplayFeature(Feature.sort_type_alternating)
- ? ["xy_alternating", "yx_alternating"]
+ ? ["yx_alternating", "xy_alternating"]
: [])
.concat(shouldDisplayFeature(Feature.sort_type_optimized) ? ["nn"] : [])
+ .reverse()
.map(sortType =>
{
}
}
}, buildResourceIndex([pg]).index);
- expect(r.label).toEqual(pg.body.name);
+ expect(r.label).toEqual(pg.body.name + " (0)");
expect(r.value).toEqual(pg.body.id);
});
diff --git a/frontend/resources/selectors_by_kind.ts b/frontend/resources/selectors_by_kind.ts
index 7c9803083e..afd62e16f5 100644
--- a/frontend/resources/selectors_by_kind.ts
+++ b/frontend/resources/selectors_by_kind.ts
@@ -58,7 +58,7 @@ export const findSequence = resourceFinder("Sequence");
export const findRegimen = resourceFinder("Regimen");
export const findFarmEvent = resourceFinder("FarmEvent");
export const findPoints = resourceFinder("Point");
-export const findPointGroup = resourceFinder("Point");
+export const findPointGroup = resourceFinder("PointGroup");
export const findSavedGarden = resourceFinder("SavedGarden");
export const selectAllCrops =
diff --git a/frontend/resources/sequence_meta.ts b/frontend/resources/sequence_meta.ts
index 76c7ae4610..d5b2eb4805 100644
--- a/frontend/resources/sequence_meta.ts
+++ b/frontend/resources/sequence_meta.ts
@@ -5,7 +5,9 @@ import {
ScopeDeclarationBodyItem,
} from "farmbot";
import { DropDownItem } from "../ui";
-import { findPointerByTypeAndId, findPointGroup, findUuid } from "./selectors";
+import {
+ findPointerByTypeAndId, findPointGroup, findUuid, selectAllActivePoints,
+} from "./selectors";
import {
findSlotByToolId, findToolById, maybeFindPeripheralById, maybeFindSensorById,
maybeFindSequenceById,
@@ -19,6 +21,7 @@ import {
import { VariableNode } from "../sequences/locals_list/locals_list_support";
import { t } from "../i18next_wrapper";
import { get } from "lodash";
+import { pointsSelectedByGroup } from "../point_groups/criteria/apply";
export interface Vector3Plus extends Vector3 {
gantry_mounted: boolean;
@@ -185,8 +188,10 @@ export const determineDropdown =
const value = data_value.args.point_group_id;
const uuid2 = findUuid(resources, "PointGroup", value);
const group = findPointGroup(resources, uuid2);
+ const allPoints = selectAllActivePoints(resources);
+ const count = pointsSelectedByGroup(group, allPoints).length;
return {
- label: group.body.name,
+ label: `${group.body.name} (${count})`,
value
};
case "nothing" as unknown:
diff --git a/frontend/sequences/locals_list/__tests__/variable_form_list_test.ts b/frontend/sequences/locals_list/__tests__/variable_form_list_test.ts
index 633f18fcd3..9429cde212 100644
--- a/frontend/sequences/locals_list/__tests__/variable_form_list_test.ts
+++ b/frontend/sequences/locals_list/__tests__/variable_form_list_test.ts
@@ -42,7 +42,7 @@ describe("variableFormList()", () => {
},
{
headingId: "PointGroup",
- label: "Fake",
+ label: "Fake (0)",
value: "1"
},
{
@@ -166,9 +166,9 @@ describe("groups2Ddi", () => {
const fakes = [fakePointGroup(), fakePointGroup()];
fakes[0].body.id = 1;
fakes[1].body.id = undefined;
- const result = groups2Ddi(fakes);
+ const result = groups2Ddi(fakes, []);
expect(result.length).toEqual(1);
- expect(result[0].label).toEqual(fakes[0].body.name);
+ expect(result[0].label).toEqual(fakes[0].body.name + " (0)");
expect(result[0].value).toEqual("1");
});
});
diff --git a/frontend/sequences/locals_list/variable_form_list.ts b/frontend/sequences/locals_list/variable_form_list.ts
index 46156488bd..09185c6c6a 100644
--- a/frontend/sequences/locals_list/variable_form_list.ts
+++ b/frontend/sequences/locals_list/variable_form_list.ts
@@ -18,6 +18,7 @@ import { Point } from "farmbot/dist/resources/api_resources";
import { t } from "../../i18next_wrapper";
import { SequenceMeta } from "../../resources/sequence_meta";
import { VariableType } from "./locals_list_support";
+import { pointsSelectedByGroup } from "../../point_groups/criteria/apply";
/** Return tool and location for all tools currently in tool slots. */
export function activeToolDDIs(resources: ResourceIndex): DropDownItem[] {
@@ -67,11 +68,16 @@ const points2ddi = (allPoints: TaggedPoint[], pointerType: PointerTypeName) =>
.map(formatPoint)
.filter(x => parseInt("" + x.value) > 0);
-export const groups2Ddi = (groups: TaggedPointGroup[]): DropDownItem[] => {
+export const groups2Ddi = (
+ groups: TaggedPointGroup[],
+ allPoints: TaggedPoint[],
+): DropDownItem[] => {
return groups
- .filter(x => x.body.id)
- .map(x => {
- return { label: x.body.name, value: "" + x.body.id, headingId: "PointGroup" };
+ .filter(group => group.body.id)
+ .map(group => {
+ const count = pointsSelectedByGroup(group, allPoints).length;
+ const label = `${group.body.name} (${count})`;
+ return { label, value: "" + group.body.id, headingId: "PointGroup" };
});
};
@@ -130,12 +136,13 @@ export function variableFormList(
if (variableType == VariableType.Resource) {
return [];
}
+ const allGroups = selectAllPointGroups(resources);
return [COORDINATE_DDI()]
.concat(addItems)
.concat(heading("Tool"))
.concat(toolDDI)
.concat(displayGroups ? heading("PointGroup") : [])
- .concat(displayGroups ? groups2Ddi(selectAllPointGroups(resources)) : [])
+ .concat(displayGroups ? groups2Ddi(allGroups, allPoints) : [])
.concat(heading("Plant"))
.concat(plantDDI)
.concat(heading("GenericPointer"))
diff --git a/frontend/sequences/panel/__tests__/editor_test.tsx b/frontend/sequences/panel/__tests__/editor_test.tsx
index b23a4a0269..44837618ee 100644
--- a/frontend/sequences/panel/__tests__/editor_test.tsx
+++ b/frontend/sequences/panel/__tests__/editor_test.tsx
@@ -4,6 +4,7 @@ jest.mock("../../../sequences/set_active_sequence_by_name", () => ({
jest.mock("../../../api/crud", () => ({
edit: jest.fn(),
+ save: jest.fn(),
}));
import React from "react";
@@ -29,7 +30,7 @@ import { push } from "../../../history";
import { Path } from "../../../internal_urls";
import { sequencesPanelState } from "../../../__test_support__/panel_state";
import { Color } from "farmbot";
-import { edit } from "../../../api/crud";
+import { edit, save } from "../../../api/crud";
describe("", () => {
const fakeProps = (): SequencesProps => ({
@@ -95,5 +96,19 @@ describe("", () => {
wrapper.find("input").first().simulate("change",
{ currentTarget: { value: "abc" } });
wrapper.find("input").first().simulate("blur");
+ expect(edit).toHaveBeenCalled();
+ expect(save).not.toHaveBeenCalled();
+ });
+
+ it("saves change", () => {
+ const p = fakeProps();
+ p.save = true;
+ const wrapper = mount();
+ wrapper.find("span").first().simulate("click");
+ wrapper.find("input").first().simulate("change",
+ { currentTarget: { value: "abc" } });
+ wrapper.find("input").first().simulate("blur");
+ expect(edit).toHaveBeenCalled();
+ expect(save).toHaveBeenCalled();
});
});
diff --git a/frontend/sequences/panel/editor.tsx b/frontend/sequences/panel/editor.tsx
index 7bab359eb9..183cc07f48 100644
--- a/frontend/sequences/panel/editor.tsx
+++ b/frontend/sequences/panel/editor.tsx
@@ -18,8 +18,10 @@ import {
} from "../set_active_sequence_by_name";
import { push } from "../../history";
import { urlFriendly } from "../../util";
-import { edit } from "../../api/crud";
-import { TaggedPoint, TaggedRegimen, TaggedSequence } from "farmbot";
+import { edit, save } from "../../api/crud";
+import {
+ TaggedPoint, TaggedPointGroup, TaggedRegimen, TaggedSequence,
+} from "farmbot";
import { Path } from "../../internal_urls";
export class RawDesignerSequenceEditor extends React.Component {
@@ -82,9 +84,14 @@ export class RawDesignerSequenceEditor extends React.Component {
export interface ResourceTitleProps {
dispatch: Function;
- resource: TaggedSequence | TaggedRegimen | TaggedPoint | undefined;
+ resource: TaggedSequence
+ | TaggedRegimen
+ | TaggedPoint
+ | TaggedPointGroup
+ | undefined;
readOnly?: boolean;
fallback: string;
+ save?: boolean;
}
export const ResourceTitle = (props: ResourceTitleProps) => {
@@ -97,6 +104,7 @@ export const ResourceTitle = (props: ResourceTitleProps) => {
onBlur={() => {
setIsEditing(false);
props.resource && props.dispatch(edit(props.resource, { name: nameValue }));
+ props.save && props.resource && props.dispatch(save(props.resource.uuid));
}}
onChange={e => {
setNameValue(e.currentTarget.value);