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);