panel updates

pull/1389/head
gabrielburnworth 2019-08-23 14:18:28 -07:00
parent 3ae676ef54
commit 538ef0f1e4
49 changed files with 1601 additions and 147 deletions

View File

@ -40,4 +40,16 @@ describe("<Link/>", () => {
expect(el.html()).toContain("Hey!");
el.unmount();
});
it("navigates", () => {
const wrapper = shallow(<Link to="/tools" />);
wrapper.simulate("click", { preventDefault: jest.fn() });
expect(navigate).toHaveBeenCalledWith("/tools");
});
it("doesn't navigate when disabled", () => {
const wrapper = shallow(<Link to="/tools" disabled={true} />);
wrapper.simulate("click", { preventDefault: jest.fn() });
expect(navigate).not.toHaveBeenCalledWith();
});
});

View File

@ -747,6 +747,15 @@ export namespace Content {
export const NO_PLANTS =
trim(`Press "+" to add a plant to your garden.`);
export const NO_GARDENS =
trim(`Press "CREATE NEW GARDEN" to add a garden.`);
export const NO_POINTS =
trim(`Press "+" to add a point to your garden.`);
export const NO_GROUPS =
trim(`Press "+" to add a point group.`);
export const ENTER_CROP_SEARCH_TERM =
trim(`Search for a crop to add to your garden.`);
@ -756,6 +765,9 @@ export namespace Content {
export const CROP_NOT_FOUND_LINK =
trim(`add this crop on OpenFarm?`);
export const NO_TOOLS =
trim(`Press "+" to add a new tool.`);
// Farm Events
export const NOTHING_SCHEDULED =
trim(`Press "+" to schedule an event.`);
@ -945,6 +957,7 @@ export enum Actions {
SEARCH_QUERY_CHANGE = "SEARCH_QUERY_CHANGE",
SELECT_PLANT = "SELECT_PLANT",
TOGGLE_HOVERED_PLANT = "TOGGLE_HOVERED_PLANT",
TOGGLE_HOVERED_POINT = "TOGGLE_HOVERED_POINT",
HOVER_PLANT_LIST_ITEM = "HOVER_PLANT_LIST_ITEM",
OF_SEARCH_RESULTS_START = "OF_SEARCH_RESULTS_START",
OF_SEARCH_RESULTS_OK = "OF_SEARCH_RESULTS_OK",

View File

@ -211,6 +211,38 @@
text-overflow: ellipsis;
margin-left: 1rem;
}
.point-search-item {
cursor: pointer;
padding: 0.5rem 1rem;
&:hover,
&.hovered {
background: darken($light_brown, 10%);
transition: background 0.2s ease;
}
.saucer {
display: inline-block;
margin: 0 1rem 0 0;
height: 2rem;
width: 2rem;
vertical-align: middle;
}
}
.point-search-item-info {
text-align: right;
font-size: 1rem;
padding-top: 0.6rem;
padding-right: 1rem;
float: right;
}
.point-search-item-name {
display: inline-block;
vertical-align: middle;
white-space: nowrap;
width: 8em;
overflow: hidden;
text-overflow: ellipsis;
margin-left: 1rem;
}
}
.thin-search {

View File

@ -187,6 +187,7 @@
}
}
.point-inventory-panel,
.plant-inventory-panel {
.panel-content {
padding: 0;
@ -197,6 +198,18 @@
}
}
.points-panel-tabs {
i {
padding-left: 1rem;
padding-right: 1rem;
}
label {
padding-left: 10rem;
padding-right: 10rem;
margin-top: 0 !important;
}
}
.plant-selection-panel {
.panel-action-buttons {
position: absolute;
@ -393,6 +406,12 @@
}
}
.tools-panel-content {
button {
margin-top: 1rem;
}
}
.settings-panel-content {
max-height: calc(100vh - 15rem);
overflow-y: auto;
@ -450,3 +469,9 @@
min-width: 7rem;
}
}
.point-panel-content {
.point-search-item-name {
width: 40%;
}
}

View File

@ -829,6 +829,13 @@ ul {
color: $panel_green;
}
}
&.gardens {
p,
h5,
a {
color: $panel_green;
}
}
&.events {
p,
h5,
@ -839,6 +846,33 @@ ul {
filter: sepia(1) contrast(1.2) saturate(1.2);
}
}
&.points {
p,
h5,
a {
color: $panel_gray;
}
.empty-state-graphic {
filter: saturate(0);
}
}
&.tools {
p,
h5,
a {
color: $panel_gray;
}
}
&.groups {
p,
h5,
a {
color: $panel_blue;
}
.empty-state-graphic {
filter: hue-rotate(60deg) saturate(0.6);
}
}
}
.farmware-selection-panel {

View File

@ -2,28 +2,14 @@ import { designer } from "../reducer";
import { Actions } from "../../constants";
import { ReduxAction } from "../../redux/interfaces";
import {
DesignerState, HoveredPlantPayl, CurrentPointPayl, CropLiveSearchResult
HoveredPlantPayl, CurrentPointPayl, CropLiveSearchResult
} from "../interfaces";
import { BotPosition } from "../../devices/interfaces";
import { fakeCropLiveSearchResult } from "../../__test_support__/fake_crop_search_result";
import { fakeDesignerState } from "../../__test_support__/fake_designer_state";
describe("designer reducer", () => {
const oldState = (): DesignerState => {
return {
selectedPlants: undefined,
hoveredPlant: {
plantUUID: undefined,
icon: ""
},
hoveredPlantListItem: undefined,
cropSearchQuery: "",
cropSearchResults: [],
cropSearchInProgress: false,
chosenLocation: { x: undefined, y: undefined, z: undefined },
currentPoint: undefined,
openedSavedGarden: undefined,
};
};
const oldState = fakeDesignerState;
it("sets search query", () => {
const action: ReduxAction<string> = {

View File

@ -264,6 +264,7 @@ export interface CameraCalibrationData {
}
export interface CurrentPointPayl {
name?: string;
cx: number;
cy: number;
r: number;

View File

@ -337,10 +337,12 @@ describe("getMode()", () => {
expect(getMode()).toEqual(Mode.addPlant);
mockPath = "/app/designer/move_to";
expect(getMode()).toEqual(Mode.moveTo);
mockPath = "/app/designer/plants/create_point";
mockPath = "/app/designer/points/add";
expect(getMode()).toEqual(Mode.createPoint);
mockPath = "/app/designer/saved_gardens";
expect(getMode()).toEqual(Mode.templateView);
mockPath = "/app/designer/groups/1";
expect(getMode()).toEqual(Mode.addPointToGroup);
});
});

View File

@ -18,7 +18,7 @@ export function DrawnPoint(props: DrawnPointProps) {
stroke={color ? color : "green"}
strokeOpacity={0.75}
strokeWidth={3}
fill="none">
fill={"none"}>
<circle id="point-radius" cx={qx} cy={qy} r={r} strokeDasharray="4 5" />
<circle id="point-center" cx={qx} cy={qy} r={2} />
</g>;

View File

@ -126,6 +126,7 @@ export class GardenMap extends
switch (getMode()) {
case Mode.boxSelect:
case Mode.moveTo:
case Mode.points:
case Mode.createPoint:
return undefined; // For modes without plant interaction
default:

View File

@ -72,6 +72,9 @@ export interface GardenPointProps {
point: TaggedGenericPointer;
}
export interface GardenPointState {
}
interface DragHelpersBaseProps {
dragging: boolean;
mapTransformProps: MapTransformProps;
@ -140,6 +143,7 @@ export enum Mode {
editPlant = "editPlant",
addPlant = "addPlant",
moveTo = "moveTo",
points = "points",
createPoint = "createPoint",
templateView = "templateView",
addPointToGroup = "addPointToGroup",

View File

@ -29,7 +29,9 @@ import {
fakeMapTransformProps
} from "../../../../../__test_support__/map_transform_props";
import { movePlant } from "../../../../actions";
import { fakeCropLiveSearchResult } from "../../../../../__test_support__/fake_crop_search_result";
import {
fakeCropLiveSearchResult
} from "../../../../../__test_support__/fake_crop_search_result";
import { error } from "../../../../../toast/toast";
describe("newPlantKindAndBody()", () => {

View File

@ -25,7 +25,8 @@ export function PlantLayer(props: PlantLayerProps) {
const selected = !!(currentPlant && (p.uuid === currentPlant.uuid));
const grayscale = !!(selectedForDel && (selectedForDel.includes(p.uuid)));
const plantCategory = unpackUUID(p.uuid).kind === "PlantTemplate"
? "saved_gardens/templates" : "plants";
? "saved_gardens/templates"
: "plants";
const plant = <GardenPlant
uuid={p.uuid}
mapTransformProps={mapTransformProps}
@ -42,16 +43,12 @@ export function PlantLayer(props: PlantLayerProps) {
style: maybeNoPointer(p.body.id ? {} : { pointerEvents: "none" }),
key: p.uuid,
};
if (getMode() === Mode.addPointToGroup) {
return <g {...wrapperProps}>
{plant}
</g>;
} else {
return <Link {...wrapperProps}
return getMode() === Mode.addPointToGroup
? <g {...wrapperProps}>{plant}</g>
: <Link {...wrapperProps}
to={`/app/designer/${plantCategory}/${"" + p.body.id}`}>
{plant}
</Link>;
}
})}
</g>;
}

View File

@ -113,12 +113,12 @@ describe("<SpreadOverlapHelper/>", () => {
describe("SpreadOverlapHelper functions", () => {
it("getDiscreteColor()", () => {
expect(getDiscreteColor(0, 100)).toEqual("none");
expect(getDiscreteColor(10, 100)).toEqual("green");
expect(getDiscreteColor(20, 100)).toEqual("green");
expect(getDiscreteColor(91, 100)).toEqual("red");
expect(getDiscreteColor(61, 100)).toEqual("orange");
expect(getDiscreteColor(31, 100)).toEqual("yellow");
expect(getDiscreteColor(-2, 100)).toEqual("none");
expect(getDiscreteColor(40, 100)).toEqual("yellow");
expect(getDiscreteColor(70, 100)).toEqual("orange");
expect(getDiscreteColor(100, 100)).toEqual("red");
});
it("getContinuousColor()", () => {

View File

@ -101,7 +101,7 @@ describe("<PointsSubMenu />", () => {
getConfigValue={jest.fn()} />);
clickButton(wrapper, 0, "point creator");
expect(history.push).toHaveBeenCalledWith(
"/app/designer/plants/create_point");
"/app/designer/points/add");
});
it("shows historic points", () => {

View File

@ -40,10 +40,11 @@ export const PointsSubMenu = ({ toggle, getConfigValue }: {
getConfigValue: GetWebAppConfigValue
}) =>
<div className="map-points-submenu">
<button className={"fb-button green"}
onClick={() => history.push("/app/designer/plants/create_point")}>
{t("Point Creator")}
</button>
{!DevSettings.futureFeaturesEnabled() &&
<button className={"fb-button green"}
onClick={() => history.push("/app/designer/points/add")}>
{t("Point Creator")}
</button>}
<LayerToggle
value={!!getConfigValue(BooleanSetting.show_historic_points)}
label={t("Historic Points?")}

View File

@ -299,7 +299,10 @@ export const getMode = (): Mode => {
if (pathArray[4] === "select") { return Mode.boxSelect; }
if (pathArray[4] === "crop_search") { return Mode.addPlant; }
if (pathArray[3] === "move_to") { return Mode.moveTo; }
if (pathArray[4] === "create_point") { return Mode.createPoint; }
if (pathArray[3] === "points") {
if (pathArray[4] === "add") { return Mode.createPoint; }
return Mode.points;
}
if (savedGardenOpen(pathArray)) { return Mode.templateView; }
}
return Mode.none;
@ -337,6 +340,7 @@ export const maybeNoPointer =
case Mode.boxSelect:
case Mode.clickToAdd:
case Mode.moveTo:
case Mode.points:
case Mode.createPoint:
return { "pointerEvents": "none" };
default:

View File

@ -9,8 +9,10 @@ export enum Panel {
Plants = "Plants",
FarmEvents = "FarmEvents",
SavedGardens = "SavedGardens",
Tools = "Tools",
Settings = "Settings",
Groups = "Groups"
Points = "Points",
Groups = "Groups",
}
type Tabs = keyof typeof Panel;
@ -20,7 +22,9 @@ export const TAB_COLOR: { [key in Panel]: string } = {
[Panel.Plants]: "green",
[Panel.FarmEvents]: "yellow",
[Panel.SavedGardens]: "green",
[Panel.Tools]: "gray",
[Panel.Settings]: "gray",
[Panel.Points]: "gray",
[Panel.Groups]: "blue",
};
@ -31,8 +35,10 @@ export const TAB_ICON: { [key in Panel]: string } = {
[Panel.Plants]: iconFile("plant"),
[Panel.FarmEvents]: iconFile("calendar"),
[Panel.SavedGardens]: iconFile("gardens"),
[Panel.Settings]: iconFile("gardens"),
[Panel.Groups]: iconFile("groups")
[Panel.Tools]: iconFile("tool"),
[Panel.Settings]: iconFile("settings"),
[Panel.Points]: iconFile("point"),
[Panel.Groups]: iconFile("groups"),
};
const getCurrentTab = (): Tabs => {
@ -43,8 +49,12 @@ const getCurrentTab = (): Tabs => {
return Panel.FarmEvents;
} else if (pathArray.includes("saved_gardens")) {
return Panel.SavedGardens;
} else if (pathArray.includes("tools")) {
return Panel.Tools;
} else if (pathArray.includes("settings")) {
return Panel.Settings;
} else if (pathArray.includes("points")) {
return Panel.Points;
} else if (pathArray.includes("groups")) {
return Panel.Groups;
} else {
@ -84,9 +94,15 @@ export function DesignerNavTabs(props: { hidden?: boolean }) {
{DevSettings.futureFeaturesEnabled() &&
<NavTab panel={Panel.SavedGardens}
linkTo={"/app/designer/saved_gardens"} title={t("Gardens")} />}
{DevSettings.futureFeaturesEnabled() &&
<NavTab panel={Panel.Points}
linkTo={"/app/designer/points"} title={t("Points")} />}
{DevSettings.futureFeaturesEnabled() &&
<NavTab panel={Panel.Groups}
linkTo={"/app/designer/groups"} title={t("Groups")} />}
{DevSettings.futureFeaturesEnabled() &&
<NavTab panel={Panel.Tools}
linkTo={"/app/designer/tools"} title={t("Tools")} />}
<NavTab panel={Panel.Settings} icon={"fa fa-gear"}
linkTo={"/app/designer/settings"} title={t("Settings")} />
</div>

View File

@ -103,7 +103,7 @@ describe("<CreatePoints />", () => {
const wrapper = shallow<CreatePoints>(<CreatePoints {...fakeProps()} />);
wrapper.instance().getPointData();
expect(wrapper.instance().state).toEqual({
color: "green", cx: 0, cy: 0, r: 1
color: "green", cx: 0, cy: 0, r: 1, name: "Created Point"
});
});

View File

@ -1,7 +1,11 @@
import { mapStateToProps } from "../map_state_to_props";
import { fakeState } from "../../../__test_support__/fake_state";
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
import { fakePlant } from "../../../__test_support__/fake_state/resources";
import {
buildResourceIndex
} from "../../../__test_support__/resource_index_builder";
import {
fakePlant, fakePlantTemplate
} from "../../../__test_support__/fake_state/resources";
describe("mapStateToProps()", () => {
it("returns findPlant()", () => {
@ -15,4 +19,16 @@ describe("mapStateToProps()", () => {
expect(result.findPlant("10")).toEqual(
expect.objectContaining({ uuid }));
});
it("finds plant template", () => {
const state = fakeState();
const template = fakePlantTemplate();
template.body.id = 10;
state.resources = buildResourceIndex([template]);
const uuid = Object.keys(state.resources.index.all)[0];
state.resources.consumers.farm_designer.openedSavedGarden = "uuid";
const result = mapStateToProps(state);
expect(result.findPlant("10")).toEqual(
expect.objectContaining({ uuid }));
});
});

View File

@ -0,0 +1,44 @@
jest.mock("react-redux", () => ({ connect: jest.fn() }));
let mockPath = "/app/designer/points/1";
jest.mock("../../../history", () => ({
getPathArray: jest.fn(() => mockPath.split("/")),
history: { push: jest.fn() }
}));
import * as React from "react";
import { mount } from "enzyme";
import { EditPoint, EditPointProps, mapStateToProps } from "../point_info";
import { fakePoint } from "../../../__test_support__/fake_state/resources";
import { fakeState } from "../../../__test_support__/fake_state";
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
describe("<EditPoint />", () => {
const fakeProps = (): EditPointProps => ({
findPoint: fakePoint,
dispatch: jest.fn(),
});
it("renders redirect", () => {
mockPath = "/app/designer/points";
const wrapper = mount(<EditPoint {...fakeProps()} />);
expect(wrapper.text()).toContain("Redirecting...");
});
it("renders with points", () => {
mockPath = "/app/designer/points/1";
const wrapper = mount(<EditPoint {...fakeProps()} />);
expect(wrapper.text()).toContain("Edit Point 1");
});
});
describe("mapStateToProps()", () => {
it("returns props", () => {
const state = fakeState();
const point = fakePoint();
point.body.id = 1;
state.resources = buildResourceIndex([point]);
const props = mapStateToProps(state);
expect(props.findPoint(1)).toEqual(point);
});
});

View File

@ -0,0 +1,78 @@
jest.mock("react-redux", () => ({ connect: jest.fn() }));
jest.mock("../../../history", () => ({
push: jest.fn(),
getPathArray: () => [],
}));
import * as React from "react";
import { mount, shallow } from "enzyme";
import { Points, PointsProps } from "../point_inventory";
import { fakePoint } from "../../../__test_support__/fake_state/resources";
import { push } from "../../../history";
import { fakeState } from "../../../__test_support__/fake_state";
import {
buildResourceIndex
} from "../../../__test_support__/resource_index_builder";
import { mapStateToProps } from "../point_inventory";
describe("<Points />", () => {
const fakeProps = (): PointsProps => ({
points: [],
dispatch: jest.fn(),
});
it("renders no points", () => {
const wrapper = mount(<Points {...fakeProps()} />);
expect(wrapper.text()).toContain("No points yet.");
});
it("renders points", () => {
const p = fakeProps();
p.points = [fakePoint()];
const wrapper = mount(<Points {...p} />);
expect(wrapper.text()).toContain("Point 1");
});
it("navigates to point info", () => {
const p = fakeProps();
p.points = [fakePoint()];
p.points[0].body.id = 1;
const wrapper = mount(<Points {...p} />);
wrapper.find(".point-search-item").first().simulate("click");
expect(push).toHaveBeenCalledWith("/app/designer/points/1");
});
it("changes search term", () => {
const p = fakeProps();
p.points = [fakePoint(), fakePoint()];
p.points[0].body.name = "point 0";
p.points[1].body.name = "point 1";
const wrapper = shallow<Points>(<Points {...p} />);
wrapper.find("input").first().simulate("change",
{ currentTarget: { value: "0" } });
expect(wrapper.state().searchTerm).toEqual("0");
});
it("filters points", () => {
const p = fakeProps();
p.points = [fakePoint(), fakePoint()];
p.points[0].body.name = "point 0";
p.points[1].body.name = "point 1";
const wrapper = mount(<Points {...p} />);
wrapper.setState({ searchTerm: "0" });
expect(wrapper.text()).not.toContain("point 1");
});
});
describe("mapStateToProps()", () => {
it("returns props", () => {
const state = fakeState();
const point = fakePoint();
const discarded = fakePoint();
discarded.body.discarded_at = "2016-05-22T05:00:00.000Z";
state.resources = buildResourceIndex([point, discarded]);
const props = mapStateToProps(state);
expect(props.points).toEqual([point]);
});
});

View File

@ -1,5 +1,4 @@
import * as React from "react";
import { connect } from "react-redux";
import { Everything, ResourceColor } from "../../interfaces";
import { initSave } from "../../api/crud";
@ -30,6 +29,7 @@ export interface CreatePointsProps {
}
interface CreatePointsState {
name: string;
cx: number;
cy: number;
r: number;
@ -51,6 +51,7 @@ export class CreatePoints
getPointData = () => {
const point = this.props.currentPoint;
this.setState({
name: point ? point.name : "Created Point",
cx: point ? point.cx : 0,
cy: point ? point.cy : 0,
r: point ? point.r : 1,
@ -63,7 +64,9 @@ export class CreatePoints
type: Actions.SET_CURRENT_POINT_DATA,
payload: undefined
});
this.setState({ cx: undefined, cy: undefined, r: undefined, color: undefined });
this.setState({
cx: undefined, cy: undefined, r: undefined, color: undefined
});
}
componentWillUnmount() {
@ -77,14 +80,21 @@ export class CreatePoints
});
}
/** Update number fields. */
updateNumberValue = (key: keyof Omit<CreatePointsState, "color">) => {
/** Update fields. */
updateValue = (key: keyof CreatePointsState) => {
return (e: React.SyntheticEvent<HTMLInputElement>) => {
const value = parseIntInput(e.currentTarget.value);
const { value } = e.currentTarget;
this.setState({ [key]: value });
if (this.props.currentPoint) {
const point = clone(this.props.currentPoint);
point[key] = value;
switch (key) {
case "name":
case "color":
point[key] = value;
break;
default:
point[key] = parseIntInput(value);
}
this.props.dispatch({
type: Actions.SET_CURRENT_POINT_DATA,
payload: point
@ -96,30 +106,42 @@ export class CreatePoints
changeColor = (color: ResourceColor) => {
this.setState({ color });
if (this.props.currentPoint) {
const { cx, cy, r } = this.props.currentPoint;
const { cx, cy, r, name } = this.props.currentPoint;
this.props.dispatch({
type: Actions.SET_CURRENT_POINT_DATA,
payload: { cx, cy, r, color }
payload: { cx, cy, r, color, name }
});
}
this.forceUpdate();
}
createPoint = () => {
const { cx, cy, r, color } = this.state;
const { cx, cy, r, color, name } = this.state;
const body: GenericPointer = {
pointer_type: "GenericPointer",
name: "Created Point",
name: name || "Created Point",
meta: { color, created_by: "farm-designer" },
x: (cx || 0),
y: (cy || 0),
x: cx || 0,
y: cy || 0,
z: 0,
radius: (r || 1),
radius: r || 1,
};
this.props.dispatch(initSave("Point", body));
this.cancel();
}
PointName = () =>
<Row>
<Col xs={12}>
<label>{t("Name")}</label>
<BlurableInput
name="name"
type="text"
onCommit={this.updateValue("name")}
value={this.state.name || "Created Point"} />
</Col>
</Row>;
PointProperties = () => {
const { cx, cy, r, color } = this.state;
return <Row>
@ -128,7 +150,7 @@ export class CreatePoints
<BlurableInput
name="cx"
type="number"
onCommit={this.updateNumberValue("cx")}
onCommit={this.updateValue("cx")}
value={cx || 0} />
</Col>
<Col xs={3}>
@ -136,7 +158,7 @@ export class CreatePoints
<BlurableInput
name="cy"
type="number"
onCommit={this.updateNumberValue("cy")}
onCommit={this.updateValue("cy")}
value={cy || 0} />
</Col>
<Col xs={3}>
@ -144,7 +166,7 @@ export class CreatePoints
<BlurableInput
name="r"
type="number"
onCommit={this.updateNumberValue("r")}
onCommit={this.updateValue("r")}
value={r || 0}
min={0} />
</Col>
@ -196,8 +218,10 @@ export class CreatePoints
panelName={"point-creation"}
panelColor={"brown"}
title={t("Create point")}
backTo={"/app/designer/points"}
description={Content.CREATE_POINTS_DESCRIPTION} />
<DesignerPanelContent panelName={"point-creation"}>
<this.PointName />
<this.PointProperties />
<this.PointActions />
<this.DeleteAllPoints />

View File

@ -167,7 +167,7 @@ interface ListItemProps {
children: React.ReactChild;
}
const ListItem = (props: ListItemProps) =>
export const ListItem = (props: ListItemProps) =>
<li>
<p>
{props.name}

View File

@ -0,0 +1,52 @@
import * as React from "react";
import { connect } from "react-redux";
import {
DesignerPanel, DesignerPanelHeader, DesignerPanelContent
} from "./designer_panel";
import { t } from "../../i18next_wrapper";
import { history, getPathArray } from "../../history";
import { Everything } from "../../interfaces";
import { TaggedPoint } from "farmbot";
import { maybeFindPointById } from "../../resources/selectors";
export interface EditPointProps {
dispatch: Function;
findPoint(id: number): TaggedPoint | undefined;
}
export const mapStateToProps = (props: Everything): EditPointProps => ({
dispatch: props.dispatch,
findPoint: id => maybeFindPointById(props.resources.index, id),
});
@connect(mapStateToProps)
export class EditPoint extends React.Component<EditPointProps, {}> {
get stringyID() { return getPathArray()[4] || ""; }
get point() {
if (this.stringyID) {
return this.props.findPoint(parseInt(this.stringyID));
}
}
fallback = () => {
history.push("/app/designer/points");
return <span>{t("Redirecting...")}</span>;
}
default = (point: TaggedPoint) => {
return <DesignerPanel panelName={"plant-info"} panelColor={"green"}>
<DesignerPanelHeader
panelName={"plant-info"}
panelColor={"gray"}
title={`${t("Edit")} ${point.body.name}`}
backTo={"/app/designer/points"}>
</DesignerPanelHeader>
<DesignerPanelContent panelName={"plants"}>
</DesignerPanelContent>
</DesignerPanel>;
}
render() {
return this.point ? this.default(this.point) : this.fallback();
}
}

View File

@ -0,0 +1,73 @@
import * as React from "react";
import { connect } from "react-redux";
import { PointInventoryItem } from "./point_inventory_item";
import { Everything } from "../../interfaces";
import { DesignerNavTabs, Panel } from "../panel_header";
import {
EmptyStateWrapper, EmptyStateGraphic
} from "../../ui/empty_state_wrapper";
import { Content } from "../../constants";
import {
DesignerPanel, DesignerPanelContent, DesignerPanelTop
} from "./designer_panel";
import { selectAllGenericPointers } from "../../resources/selectors";
import { TaggedGenericPointer } from "farmbot";
import { t } from "../../i18next_wrapper";
export interface PointsProps {
points: TaggedGenericPointer[];
dispatch: Function;
}
interface PointsState {
searchTerm: string;
}
export function mapStateToProps(props: Everything): PointsProps {
return {
points: selectAllGenericPointers(props.resources.index)
.filter(x => !x.body.discarded_at),
dispatch: props.dispatch,
};
}
@connect(mapStateToProps)
export class Points extends React.Component<PointsProps, PointsState> {
state: PointsState = { searchTerm: "" };
update = ({ currentTarget }: React.SyntheticEvent<HTMLInputElement>) => {
this.setState({ searchTerm: currentTarget.value });
}
render() {
return <DesignerPanel panelName={"point-inventory"} panelColor={"brown"}>
<DesignerNavTabs />
<DesignerPanelTop
panel={Panel.Points}
linkTo={"/app/designer/points/add"}
title={t("Add point")}>
<input type="text" onChange={this.update}
placeholder={t("Search your points...")} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"point"}>
<EmptyStateWrapper
notEmpty={this.props.points.length > 0}
graphic={EmptyStateGraphic.no_crop_results}
title={t("No points yet.")}
text={Content.NO_POINTS}
colorScheme={"points"}>
{this.props.points
.filter(p => p.body.name.toLowerCase()
.includes(this.state.searchTerm.toLowerCase()))
.map(p => {
return <PointInventoryItem
key={p.uuid}
tpp={p}
dispatch={this.props.dispatch} />;
})}
</EmptyStateWrapper>
</DesignerPanelContent>
</DesignerPanel>;
}
}

View File

@ -0,0 +1,38 @@
import * as React from "react";
import { TaggedGenericPointer } from "farmbot";
import { Saucer } from "../../ui";
import { push } from "../../history";
export interface PointInventoryItemProps {
tpp: TaggedGenericPointer;
dispatch: Function;
}
// The individual points that show up in the farm designer sub nav.
export class PointInventoryItem extends
React.Component<PointInventoryItemProps, {}> {
render() {
const point = this.props.tpp.body;
const pointId = (point.id || "ERR_NO_POINT_ID").toString();
const click = () => {
push(`/app/designer/points/${pointId}`);
};
const label = point.name || "Unknown point";
return <div
className={`point-search-item`}
key={pointId}
onClick={click}>
<Saucer color={point.meta.color || "green"} />
<span className="point-search-item-name">
{label}
</span>
<p className="point-search-item-info">
{`(${point.x}, ${point.y}) ⌀${point.radius * 2}`}
</p>
</div>;
}
}

View File

@ -1,18 +1,18 @@
jest.mock("../../../api/crud", () => {
return {
save: jest.fn(),
overwrite: jest.fn()
};
});
jest.mock("../../../api/crud", () => ({
save: jest.fn(),
overwrite: jest.fn()
}));
jest.mock("../../actions", () => {
return { toggleHoveredPlant: jest.fn() };
});
jest.mock("../../actions", () => ({
toggleHoveredPlant: jest.fn()
}));
import React from "react";
import { GroupDetailActive, LittleIcon } from "../group_detail_active";
import { mount, shallow } from "enzyme";
import { fakePointGroup, fakePlant } from "../../../__test_support__/fake_state/resources";
import {
fakePointGroup, fakePlant
} from "../../../__test_support__/fake_state/resources";
import { save, overwrite } from "../../../api/crud";
import { toggleHoveredPlant } from "../../actions";

View File

@ -1,4 +1,6 @@
import { fakePointGroup, fakePlant } from "../../../__test_support__/fake_state/resources";
import {
fakePointGroup, fakePlant
} from "../../../__test_support__/fake_state/resources";
const GOOD_ID = 9;
const mockPlant = fakePlant();
@ -24,7 +26,9 @@ import { GroupDetailActive } from "../group_detail_active";
import { GroupDetail } from "../group_detail";
import { fakeState } from "../../../__test_support__/fake_state";
import { createStore } from "redux";
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
import {
buildResourceIndex
} from "../../../__test_support__/resource_index_builder";
import { push } from "../../../history";
describe("<GroupDetail />", () => {

View File

@ -1,24 +1,18 @@
jest.mock("../../../history", () => {
return {
getPathArray: jest.fn(() => ["L", "O", "L"]),
history: {
push: jest.fn(),
}
};
});
jest.mock("react-redux", () => ({ connect: jest.fn() }));
jest.mock("../../../history", () => ({
getPathArray: jest.fn(() => ["L", "O", "L"]),
history: { push: jest.fn() }
}));
import React from "react";
import { mount } from "enzyme";
import { GroupListPanel, newUpdater } from "../group_list_panel";
import { Provider } from "react-redux";
import { createStore, DeepPartial } from "redux";
import { fakeState } from "../../../__test_support__/fake_state";
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
import { mount, shallow } from "enzyme";
import { GroupListPanel, GroupListPanelProps } from "../group_list_panel";
import { fakePointGroup } from "../../../__test_support__/fake_state/resources";
import { history } from "../../../history";
describe("<GroupListPanel />", () => {
const setUpTests = () => {
const fakeProps = (): GroupListPanelProps => {
const fake1 = fakePointGroup();
fake1.body.name = "one";
fake1.body.id = 9;
@ -27,35 +21,34 @@ describe("<GroupListPanel />", () => {
const fake2 = fakePointGroup();
fake2.body.name = "two";
const state = fakeState();
state.resources = buildResourceIndex([fake1, fake2]);
const store = createStore(s => s, state);
return { store, fake1, fake2 };
return { dispatch: jest.fn(), groups: [fake1, fake2] };
};
it("handles the `change` event", () => {
const setState = jest.fn();
const fn = newUpdater(setState, "searchTerm");
type E = React.SyntheticEvent<HTMLInputElement>;
const e: DeepPartial<E> = { currentTarget: { value: "X" } };
fn(e as E);
expect(setState).toHaveBeenCalledWith({ searchTerm: "X" });
it("changes search term", () => {
const p = fakeProps();
const wrapper = shallow<GroupListPanel>(<GroupListPanel {...p} />);
wrapper.find("input").first().simulate("change",
{ currentTarget: { value: "one" } });
expect(wrapper.state().searchTerm).toEqual("one");
});
it("renders relevant group data as a list", () => {
const { store, fake1, fake2 } = setUpTests();
const el = mount(<Provider store={store}>
<GroupListPanel {...({} as GroupListPanel["props"])} />
</Provider>);
el.find(".plant-search-item").first().simulate("click");
const p = fakeProps();
const wrapper = mount(<GroupListPanel {...p} />);
wrapper.find(".plant-search-item").first().simulate("click");
expect(history.push).toHaveBeenCalledWith("/app/designer/groups/9");
const text = el.text();
expect(text).toContain("3 items");
expect(text).toContain("0 items");
expect(text).toContain(fake2.body.name);
expect(text).toContain(fake1.body.name);
["3 items",
"0 items",
p.groups[0].body.name,
p.groups[1].body.name].map(string =>
expect(wrapper.text()).toContain(string));
});
it("renders no groups", () => {
const p = fakeProps();
p.groups = [];
const wrapper = mount(<GroupListPanel {...p} />);
expect(wrapper.text().toLowerCase()).toContain("no groups yet");
});
});

View File

@ -3,13 +3,17 @@ import { connect } from "react-redux";
import { Everything } from "../../interfaces";
import { Panel, DesignerNavTabs } from "../panel_header";
import { t } from "../../i18next_wrapper";
import { DesignerPanel, DesignerPanelTop, DesignerPanelContent } from "../plants/designer_panel";
import {
DesignerPanel, DesignerPanelTop, DesignerPanelContent
} from "../plants/designer_panel";
import { findAll } from "../../resources/find_all";
import { TaggedPointGroup } from "farmbot";
import { history } from "../../history";
import { GroupInventoryItem } from "./group_inventory_item";
import { EmptyStateWrapper, EmptyStateGraphic } from "../../ui/empty_state_wrapper";
import { Content } from "../../constants";
interface GroupListPanelProps {
export interface GroupListPanelProps {
dispatch: Function;
groups: TaggedPointGroup[];
}
@ -23,20 +27,16 @@ function mapStateToProps(props: Everything): GroupListPanelProps {
findAll<TaggedPointGroup>(props.resources.index, "PointGroup");
return { groups, dispatch: props.dispatch };
}
/** I wanted this to be a member method of <GroupListPanel/> but testing was
* too wonky due to @connect(). If anyone knows a way to test this, feel free
* to do a non-curried solution. -RC*/
export const newUpdater =
(cb: Function, key: keyof GroupListPanel["state"]) =>
(e: React.SyntheticEvent<HTMLInputElement>) => {
cb({ [key]: e.currentTarget.value });
};
@connect(mapStateToProps)
export class GroupListPanel extends React.Component<GroupListPanelProps, State> {
state: State = { searchTerm: "" };
update = ({ currentTarget }: React.SyntheticEvent<HTMLInputElement>) => {
this.setState({ searchTerm: currentTarget.value });
}
navigate = (id: number) => history.push(`/app/designer/groups/${id}`);
render() {
@ -47,21 +47,27 @@ export class GroupListPanel extends React.Component<GroupListPanelProps, State>
linkTo={"/app/designer/plants/select"}
title={t("Add Group")}>
<input type="text"
onChange={newUpdater(this.setState, "searchTerm")}
onChange={this.update}
placeholder={t("Search your groups...")} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"groups"}>
{this
.props
.groups
.filter(p => p.body.name.toLowerCase().includes(this.state.searchTerm.toLowerCase()))
.map(group => <GroupInventoryItem
key={group.uuid}
group={group}
hovered={false}
dispatch={this.props.dispatch}
onClick={() => this.navigate(group.body.id || 0)}
/>)}
<EmptyStateWrapper
notEmpty={this.props.groups.length > 0}
title={t("No groups yet.")}
text={t(Content.NO_GROUPS)}
colorScheme="groups"
graphic={EmptyStateGraphic.plants}>
{this.props.groups
.filter(p => p.body.name.toLowerCase()
.includes(this.state.searchTerm.toLowerCase()))
.map(group => <GroupInventoryItem
key={group.uuid}
group={group}
hovered={false}
dispatch={this.props.dispatch}
onClick={() => this.navigate(group.body.id || 0)}
/>)}
</EmptyStateWrapper>
</DesignerPanelContent>
</DesignerPanel>;
}

View File

@ -1,7 +1,6 @@
import * as React from "react";
import { Everything } from "../../interfaces";
import { connect } from "react-redux";
import { history } from "../../history";
import { unselectPlant } from "../actions";
import {
@ -19,6 +18,7 @@ import {
import { DevSettings } from "../../account/dev/dev_support";
import { DesignerNavTabs } from "../panel_header";
import { t } from "../../i18next_wrapper";
import { EmptyStateWrapper, EmptyStateGraphic } from "../../ui/empty_state_wrapper";
export const mapStateToProps = (props: Everything): SavedGardensProps => ({
savedGardens: selectAllSavedGardens(props.resources.index),
@ -58,9 +58,14 @@ export class SavedGardens extends React.Component<SavedGardensProps, {}> {
plantTemplates={this.props.plantTemplates}
dispatch={this.props.dispatch} />
<hr />
{this.props.savedGardens.length > 0
? <SavedGardenList {...this.props} />
: <p>{t("No saved gardens yet.")}</p>}
<EmptyStateWrapper
notEmpty={this.props.savedGardens.length > 0}
title={t("No saved gardens yet.")}
// text={t(Content.NO_GARDENS)}
colorScheme="gardens"
graphic={EmptyStateGraphic.plants}>
<SavedGardenList {...this.props} />
</EmptyStateWrapper>
</DesignerPanelContent>
</DesignerPanel>;
}

View File

@ -0,0 +1,43 @@
jest.mock("react-redux", () => ({ connect: jest.fn() }));
jest.mock("../../../api/crud", () => ({ initSave: jest.fn() }));
import * as React from "react";
import { mount, shallow } from "enzyme";
import { AddTool, AddToolProps, mapStateToProps } from "../add_tool";
import { fakeState } from "../../../__test_support__/fake_state";
import { SaveBtn } from "../../../ui";
import { initSave } from "../../../api/crud";
describe("<AddTool />", () => {
const fakeProps = (): AddToolProps => ({
dispatch: jest.fn(),
});
it("renders", () => {
const wrapper = mount(<AddTool {...fakeProps()} />);
expect(wrapper.text()).toContain("Add new tool");
});
it("edits tool name", () => {
const wrapper = shallow<AddTool>(<AddTool {...fakeProps()} />);
expect(wrapper.state().toolName).toEqual("");
wrapper.find("input").simulate("change",
{ currentTarget: { value: "new name" } });
expect(wrapper.state().toolName).toEqual("new name");
});
it("saves", () => {
const wrapper = shallow(<AddTool {...fakeProps()} />);
wrapper.setState({ toolName: "Foo" });
wrapper.find(SaveBtn).simulate("click");
expect(initSave).toHaveBeenCalledWith("Tool", { name: "Foo" });
});
});
describe("mapStateToProps()", () => {
it("returns props", () => {
const props = mapStateToProps(fakeState());
expect(props.dispatch).toEqual(expect.any(Function));
});
});

View File

@ -0,0 +1,64 @@
jest.mock("react-redux", () => ({ connect: jest.fn() }));
jest.mock("../../../api/crud", () => ({ edit: jest.fn() }));
jest.mock("../../../history", () => ({
history: { push: jest.fn() },
getPathArray: () => "/app/designer/tools/1".split("/"),
}));
import * as React from "react";
import { mount, shallow } from "enzyme";
import { EditTool, EditToolProps, mapStateToProps } from "../edit_tool";
import { fakeTool } from "../../../__test_support__/fake_state/resources";
import { fakeState } from "../../../__test_support__/fake_state";
import {
buildResourceIndex
} from "../../../__test_support__/resource_index_builder";
import { SaveBtn } from "../../../ui";
import { history } from "../../../history";
import { edit } from "../../../api/crud";
describe("<EditTool />", () => {
const fakeProps = (): EditToolProps => ({
findTool: jest.fn(() => fakeTool()),
dispatch: jest.fn(),
});
it("renders", () => {
const wrapper = mount(<EditTool {...fakeProps()} />);
expect(wrapper.text()).toContain("Edit Foo");
});
it("redirects", () => {
const p = fakeProps();
p.findTool = jest.fn(() => undefined);
const wrapper = mount(<EditTool {...p} />);
expect(wrapper.text()).toContain("Redirecting...");
});
it("edits tool name", () => {
const wrapper = shallow<EditTool>(<EditTool {...fakeProps()} />);
wrapper.find("input").simulate("change",
{ currentTarget: { value: "new name" } });
expect(wrapper.state().toolName).toEqual("new name");
});
it("saves", () => {
const wrapper = shallow(<EditTool {...fakeProps()} />);
wrapper.find(SaveBtn).simulate("click");
expect(edit).toHaveBeenCalledWith(expect.any(Object), { name: "Foo" });
expect(history.push).toHaveBeenCalledWith("/app/designer/tools");
});
});
describe("mapStateToProps()", () => {
it("returns props", () => {
const state = fakeState();
const tool = fakeTool();
tool.body.id = 123;
state.resources = buildResourceIndex([tool]);
const props = mapStateToProps(state);
expect(props.findTool("" + tool.body.id)).toEqual(tool);
});
});

View File

@ -0,0 +1,86 @@
jest.mock("react-redux", () => ({ connect: jest.fn() }));
jest.mock("../../../history", () => ({
history: { push: jest.fn() },
getPathArray: () => "/app/designer/tools".split("/"),
}));
import * as React from "react";
import { mount, shallow } from "enzyme";
import { Tools, ToolsProps, mapStateToProps } from "../index";
import {
fakeTool, fakeToolSlot
} from "../../../__test_support__/fake_state/resources";
import { history } from "../../../history";
import { fakeState } from "../../../__test_support__/fake_state";
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
describe("<Tools />", () => {
const fakeProps = (): ToolsProps => ({
tools: [],
toolSlots: [],
dispatch: jest.fn(),
});
it("renders with no tools", () => {
const wrapper = mount(<Tools {...fakeProps()} />);
expect(wrapper.text()).toContain("Add a tool");
});
it("renders with tools", () => {
const p = fakeProps();
p.tools = [fakeTool()];
p.tools[0].body.id = 1;
p.tools[0].body.status = "inactive";
p.toolSlots = [fakeToolSlot()];
p.toolSlots[0].body.x = 1;
const wrapper = mount(<Tools {...p} />);
expect(wrapper.text()).toContain("Foo");
expect(wrapper.text()).toContain("(1, 0, 0)");
});
it("navigates to tool", () => {
const p = fakeProps();
p.tools = [fakeTool()];
p.tools[0].body.id = 1;
p.tools[0].body.status = "inactive";
p.toolSlots = [fakeToolSlot()];
p.toolSlots[0].body.tool_id = 2;
const wrapper = mount(<Tools {...p} />);
wrapper.find("p").first().simulate("click");
expect(history.push).toHaveBeenCalledWith("/app/designer/tools/2");
wrapper.find("p").last().simulate("click");
expect(history.push).toHaveBeenCalledWith("/app/designer/tools/1");
});
it("changes search term", () => {
const p = fakeProps();
p.tools = [fakeTool(), fakeTool()];
p.tools[0].body.name = "tool 0";
p.tools[1].body.name = "tool 1";
const wrapper = shallow<Tools>(<Tools {...p} />);
wrapper.find("input").first().simulate("change",
{ currentTarget: { value: "0" } });
expect(wrapper.state().searchTerm).toEqual("0");
});
it("filters tools", () => {
const p = fakeProps();
p.tools = [fakeTool(), fakeTool()];
p.tools[0].body.name = "tool 0";
p.tools[1].body.name = "tool 1";
const wrapper = mount(<Tools {...p} />);
wrapper.setState({ searchTerm: "0" });
expect(wrapper.text()).not.toContain("tool 1");
});
});
describe("mapStateToProps()", () => {
it("returns props", () => {
const state = fakeState();
const tool = fakeTool();
state.resources = buildResourceIndex([tool]);
const props = mapStateToProps(state);
expect(props.tools).toEqual([tool]);
});
});

View File

@ -0,0 +1,45 @@
import React from "react";
import { connect } from "react-redux";
import {
DesignerPanel, DesignerPanelContent, DesignerPanelHeader
} from "../plants/designer_panel";
import { Everything } from "../../interfaces";
import { t } from "../../i18next_wrapper";
import { SaveBtn } from "../../ui";
import { SpecialStatus } from "farmbot";
import { initSave } from "../../api/crud";
export interface AddToolProps {
dispatch: Function;
}
export interface AddToolState {
toolName: string;
}
export const mapStateToProps = (props: Everything): AddToolProps => ({
dispatch: props.dispatch,
});
@connect(mapStateToProps)
export class AddTool extends React.Component<AddToolProps, AddToolState> {
state: AddToolState = { toolName: "" };
render() {
return <DesignerPanel panelName={"tool"} panelColor={"gray"}>
<DesignerPanelHeader
panelName={"tool"}
title={t("Add new tool")}
backTo={"/app/designer/tools"}
panelColor={"gray"} />
<DesignerPanelContent panelName={"tools"}>
<label>{t("Tool Name")}</label>
<input
onChange={e => this.setState({ toolName: e.currentTarget.value })} />
<SaveBtn
onClick={() =>
this.props.dispatch(initSave("Tool", { name: this.state.toolName }))}
status={SpecialStatus.DIRTY} />
</DesignerPanelContent>
</DesignerPanel>;
}
}

View File

@ -0,0 +1,67 @@
import React from "react";
import { connect } from "react-redux";
import {
DesignerPanel, DesignerPanelContent, DesignerPanelHeader
} from "../plants/designer_panel";
import { Everything } from "../../interfaces";
import { t } from "../../i18next_wrapper";
import { getPathArray } from "../../history";
import { TaggedTool, SpecialStatus } from "farmbot";
import { maybeFindToolById } from "../../resources/selectors";
import { SaveBtn } from "../../ui";
import { edit } from "../../api/crud";
import { history } from "../../history";
export interface EditToolProps {
findTool(id: string): TaggedTool | undefined;
dispatch: Function;
}
export interface EditToolState {
toolName: string;
}
export const mapStateToProps = (props: Everything): EditToolProps => ({
findTool: (id: string) =>
maybeFindToolById(props.resources.index, parseInt(id)),
dispatch: props.dispatch,
});
@connect(mapStateToProps)
export class EditTool extends React.Component<EditToolProps, EditToolState> {
state: EditToolState = { toolName: this.tool ? this.tool.body.name || "" : "" };
get stringyID() { return getPathArray()[4] || ""; }
get tool() { return this.props.findTool(this.stringyID); }
fallback = () => {
history.push("/app/designer/tools");
return <span>{t("Redirecting...")}</span>;
}
default = (tool: TaggedTool) =>
<DesignerPanel panelName={"tool"} panelColor={"gray"}>
<DesignerPanelHeader
panelName={"tool"}
title={`${t("Edit")} ${tool.body.name}`}
backTo={"/app/designer/tools"}
panelColor={"gray"} />
<DesignerPanelContent panelName={"tools"}>
<label>{t("Tool Name")}</label>
<input
value={this.state.toolName}
onChange={e => this.setState({ toolName: e.currentTarget.value })} />
<SaveBtn
onClick={() => {
this.props.dispatch(edit(tool, { name: this.state.toolName }));
history.push("/app/designer/tools");
}}
status={SpecialStatus.DIRTY} />
</DesignerPanelContent>
</DesignerPanel>;
render() {
return this.tool ? this.default(this.tool) : this.fallback();
}
}

View File

@ -0,0 +1,127 @@
import React from "react";
import { connect } from "react-redux";
import {
DesignerPanel, DesignerPanelTop, DesignerPanelContent
} from "../plants/designer_panel";
import { Everything } from "../../interfaces";
import { DesignerNavTabs, Panel } from "../panel_header";
import {
EmptyStateWrapper, EmptyStateGraphic
} from "../../ui/empty_state_wrapper";
import { t } from "../../i18next_wrapper";
import { TaggedTool, TaggedToolSlotPointer } from "farmbot";
import {
selectAllTools, selectAllToolSlotPointers
} from "../../resources/selectors";
import { Content } from "../../constants";
import { history } from "../../history";
import { Row, Col } from "../../ui";
import { botPositionLabel } from "../map/layers/farmbot/bot_position_label";
export interface ToolsProps {
tools: TaggedTool[];
toolSlots: TaggedToolSlotPointer[];
dispatch: Function;
}
export interface ToolsState {
searchTerm: string;
}
export const mapStateToProps = (props: Everything): ToolsProps => ({
tools: selectAllTools(props.resources.index),
toolSlots: selectAllToolSlotPointers(props.resources.index),
dispatch: props.dispatch,
});
@connect(mapStateToProps)
export class Tools extends React.Component<ToolsProps, ToolsState> {
state: ToolsState = { searchTerm: "" };
update = ({ currentTarget }: React.SyntheticEvent<HTMLInputElement>) => {
this.setState({ searchTerm: currentTarget.value });
}
getToolName = (toolId: number | undefined): string | undefined => {
const foundTool = this.props.tools.filter(tool => tool.body.id === toolId)[0];
return foundTool ? foundTool.body.name : undefined;
};
render() {
const panelName = "tools";
return <DesignerPanel
panelName={panelName}
panelColor={"gray"}>
<DesignerNavTabs />
<DesignerPanelTop
panel={Panel.Tools}
linkTo={"/app/designer/tools/add"}
title={t("Add tool")}>
<input type="text" onChange={this.update}
placeholder={t("Search your tools...")} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"tools"}>
<EmptyStateWrapper
notEmpty={this.props.tools.length > 0}
graphic={EmptyStateGraphic.sequences}
title={t("Add a tool")}
text={Content.NO_TOOLS}
colorScheme={"tools"}>
<div>
<label>{t("tool slots")}</label>
{this.props.toolSlots
.filter(p => (this.getToolName(p.body.tool_id) || "").toLowerCase()
.includes(this.state.searchTerm.toLowerCase()))
.map(toolSlot =>
<ToolSlotInventoryItem key={toolSlot.uuid}
toolSlot={toolSlot}
getToolName={this.getToolName} />)}
<br />
<label>{t("inactive tools")}</label>
{this.props.tools
.filter(tool => tool.body.name && tool.body.name.toLowerCase()
.includes(this.state.searchTerm.toLowerCase()))
.filter(tool => tool.body.status === "inactive")
.map(tool =>
<ToolInventoryItem key={tool.uuid}
toolId={tool.body.id}
toolName={tool.body.name || t("Unnammed tool")} />)}
</div>
</EmptyStateWrapper>
</DesignerPanelContent>
</DesignerPanel>;
}
}
interface ToolSlotInventoryItemProps {
toolSlot: TaggedToolSlotPointer;
getToolName(toolId: number | undefined): string | undefined;
}
const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => {
const { x, y, z, tool_id } = props.toolSlot.body;
return <Row>
<Col xs={7}>
<p onClick={() => history.push(`/app/designer/tools/${tool_id}`)}>
{props.getToolName(tool_id) || t("No tool")}
</p>
</Col>
<Col xs={5}>
<p style={{ float: "right" }}>{botPositionLabel({ x, y, z })}</p>
</Col>
</Row>;
};
interface ToolInventoryItemProps {
toolName: string;
toolId: number | undefined;
}
const ToolInventoryItem = (props: ToolInventoryItemProps) =>
<Row>
<Col xs={12}>
<p onClick={() => history.push(`/app/designer/tools/${props.toolId}`)}>
{t(props.toolName)}
</p>
</Col>
</Row>;

View File

@ -1,6 +1,5 @@
import * as React from "react";
import { links } from "./nav/nav_links";
import { getLinks } from "./nav/nav_links";
import { sync } from "./devices/actions";
import { push, getPathArray } from "./history";
import { Row, Col } from "./ui/index";
@ -60,6 +59,7 @@ export class HotKeys extends React.Component<Props, Partial<State>> {
this.setState({ [property]: !this.state[property] });
private hotkeys(dispatch: Function, slug: string) {
const links = getLinks();
const idx = findIndex(links, { slug });
const right = "/app/" + (links[idx + 1] || links[0]).slug;
const left = "/app/" + (links[idx - 1] || links[links.length - 1]).slug;

View File

@ -6,6 +6,7 @@ interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
children?: React.ReactChild | React.ReactChild[];
style?: React.CSSProperties;
className?: string;
disabled?: boolean;
}
export const maybeStripLegacyUrl =
@ -23,6 +24,8 @@ export const clickHandler =
export class Link extends React.Component<LinkProps, {}> {
render() {
const { props } = this;
return <a {...props} href={props.to} onClick={clickHandler(props)} />;
return props.disabled
? <a {...props} />
: <a {...props} href={props.to} onClick={clickHandler(props)} />;
}
}

View File

@ -7,6 +7,8 @@ import {
} from "./compute_editor_url_from_state";
import { Link } from "../link";
import { t } from "../i18next_wrapper";
import { betterCompact } from "../util";
import { DevSettings } from "../account/dev/dev_support";
/** Uses a slug and a child path to compute the `href` of a navbar link. */
export type LinkComputeFn = (slug: string, childPath: string) => string;
@ -24,7 +26,7 @@ interface NavLinkParams {
computeHref?: LinkComputeFn
}
export const links: NavLinkParams[] = [
export const getLinks = (): NavLinkParams[] => betterCompact([
{ name: "Farm Designer", icon: "leaf", slug: "designer" },
{ name: "Controls", icon: "keyboard-o", slug: "controls" },
{ name: "Device", icon: "cog", slug: "device" },
@ -36,19 +38,21 @@ export const links: NavLinkParams[] = [
name: "Regimens", icon: "calendar-check-o", slug: "regimens",
computeHref: computeEditorUrlFromState("Regimen")
},
{ name: "Tools", icon: "wrench", slug: "tools" },
DevSettings.futureFeaturesEnabled()
? undefined
: { name: "Tools", icon: "wrench", slug: "tools" },
{
name: "Farmware", icon: "crosshairs", slug: "farmware",
computeHref: computeFarmwareUrlFromState
},
{ name: "Messages", icon: "list", slug: "messages" },
];
]);
export const NavLinks = (props: NavLinksProps) => {
const currPageSlug = getPathArray()[2];
return <div className="links">
<div className="nav-links">
{links.map(link => {
{getLinks().map(link => {
const isActive = (currPageSlug === link.slug) ? "active" : "";
const childPath = link.slug === "designer" ? "/plants" : "";
const fn = link.computeHref || DEFAULT;

View File

@ -8,6 +8,7 @@ import {
isTaggedToolSlotPointer,
sanityCheck,
isTaggedPlantTemplate,
isTaggedGenericPointer,
} from "./tagged_resources";
import {
ResourceName,
@ -105,6 +106,13 @@ export function maybeFindPlantTemplateById(index: ResourceIndex, id: number) {
if (resource && isTaggedPlantTemplate(resource)) { return resource; }
}
/** Unlike other findById methods, this one allows undefined (missed) values */
export function maybeFindPointById(index: ResourceIndex, id: number) {
const uuid = index.byKindAndId[joinKindAndId("Point", id)];
const resource = index.references[uuid || "nope"];
if (resource && isTaggedGenericPointer(resource)) { return resource; }
}
export let findRegimenById = (ri: ResourceIndex, regimen_id: number) => {
const regimen = byId("Regimen")(ri, regimen_id);
if (regimen && isTaggedRegimen(regimen) && sanityCheck(regimen)) {

View File

@ -91,7 +91,8 @@ const key = "FarmDesigner";
*
* DO NOT RE-ORDER ITEMS FOR READABILITY--they are order-dependent.
* Stuff will break if the route order is changed.
* (e.g., must be "a" then "a/:b/c" then "a/:b", 404 must be last, etc.)
* (e.g., must be ["a", "a/b", "a/b/:c/d", "a/b/:c", "a/:e"],
* 404 must be last, etc.)
*/
export const UNBOUND_ROUTES = [
route({
@ -218,12 +219,28 @@ export const UNBOUND_ROUTES = [
}),
route({
children: true,
$: "/designer/plants/create_point",
$: "/designer/points",
getModule,
key,
getChild: () => import("./farm_designer/plants/point_inventory"),
childKey: "Points"
}),
route({
children: true,
$: "/designer/points/add",
getModule,
key,
getChild: () => import("./farm_designer/plants/create_points"),
childKey: "CreatePoints"
}),
route({
children: true,
$: "/designer/points/:point_id",
getModule,
key,
getChild: () => import("./farm_designer/plants/point_info"),
childKey: "EditPoint"
}),
route({
children: true,
$: "/designer/plants/crop_search",
@ -296,6 +313,30 @@ export const UNBOUND_ROUTES = [
getChild: () => import("./farm_designer/settings"),
childKey: "DesignerSettings"
}),
route({
children: true,
$: "/designer/tools",
getModule,
key,
getChild: () => import("./farm_designer/tools"),
childKey: "Tools"
}),
route({
children: true,
$: "/designer/tools/add",
getModule,
key,
getChild: () => import("./farm_designer/tools/add_tool"),
childKey: "AddTool"
}),
route({
children: true,
$: "/designer/tools/:tool_id",
getModule,
key,
getChild: () => import("./farm_designer/tools/edit_tool"),
childKey: "EditTool"
}),
route({
children: true,
$: "/designer/groups",

View File

@ -100,4 +100,22 @@ describe("FBToast", () => {
i.doPolling();
expect(i.detach).toHaveBeenCalled();
});
it("does polling: large timeout value", () => {
const [i] = newToast();
i.isHovered = false;
i.timeout = 8;
i.detach = jest.fn();
i.doPolling();
expect(i.detach).not.toHaveBeenCalled();
});
it("does polling: hovered", () => {
const [i] = newToast();
i.isHovered = true;
i.timeout = 0;
i.detach = jest.fn();
i.doPolling();
expect(i.detach).not.toHaveBeenCalled();
});
});

View File

@ -26,6 +26,13 @@ describe("toasts", () => {
console.warn);
});
it("pops a warning() toast with different title and color", () => {
warning("test suite msg", "new title", "purple");
expect(createToastOnce)
.toHaveBeenCalledWith("test suite msg", "new title", "purple",
console.warn);
});
it("pops a error() toast", () => {
error("test suite msg 2");
expect(createToastOnce).toHaveBeenCalledWith("test suite msg 2",
@ -34,18 +41,37 @@ describe("toasts", () => {
console.error);
});
it("pops a error() toast with different title and color", () => {
error("test suite msg", "new title", "purple");
expect(createToastOnce)
.toHaveBeenCalledWith("test suite msg", "new title", "purple",
console.error);
});
it("pops a success() toast", () => {
success("test suite msg");
expect(createToast)
.toHaveBeenCalledWith("test suite msg", "Success", "green");
});
it("pops a success() toast with different title and color", () => {
success("test suite msg", "new title", "purple");
expect(createToast)
.toHaveBeenCalledWith("test suite msg", "new title", "purple");
});
it("pops a info() toast", () => {
info("test suite msg");
expect(createToast)
.toHaveBeenCalledWith("test suite msg", "FYI", "blue");
});
it("pops a info() toast with different title and color", () => {
info("test suite msg", "new title", "purple");
expect(createToast)
.toHaveBeenCalledWith("test suite msg", "new title", "purple");
});
it("pops a busy() toast", () => {
busy("test suite msg");
expect(createToast)
@ -64,6 +90,12 @@ describe("toasts", () => {
.toHaveBeenCalledWith("test suite msg", "Did you know?", "dark-blue");
});
it("pops a fun() toast with different title and color", () => {
fun("test suite msg", "new title", "purple");
expect(createToast)
.toHaveBeenCalledWith("test suite msg", "new title", "purple");
});
it("adds the appropriate div to the DOM", () => {
const count1 = document.querySelectorAll(".toast-container").item.length;
expect(count1).toEqual(1);

View File

@ -16,7 +16,7 @@ interface EmptyStateWrapperProps {
text?: string;
textElement?: JSX.Element;
graphic: string;
colorScheme?: "plants" | "events";
colorScheme?: "plants" | "events" | "gardens" | "points" | "tools" | "groups";
children?: React.ReactNode;
}

View File

@ -0,0 +1,140 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-miterlimit:10;}
.st1{fill:#C46F24;}
.st2{opacity:0.5;fill:#C46F24;}
.st3{fill:#FFFFFF;stroke:#000000;stroke-width:7;stroke-miterlimit:10;}
.st4{fill:none;stroke:#000000;stroke-width:7;stroke-linecap:round;stroke-miterlimit:10;}
.st5{fill:#FFFFFF;stroke:#000000;stroke-width:5;stroke-miterlimit:10;}
.st6{fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-miterlimit:10;}
.st7{fill:#F15A24;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st8{fill:none;stroke:#000000;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st9{fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st10{fill:#FFFFFF;}
.st11{fill:#FFFFFF;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st12{fill:none;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:4.088,10.22;}
.st13{fill:#FFFFFF;stroke:#333333;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st14{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st15{fill:none;stroke:#FFFFFF;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st16{fill:none;stroke:#969696;stroke-width:5;stroke-miterlimit:10;}
.st17{fill:#B3B3B3;stroke:#4D4D4D;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st18{fill:#B3B3B3;stroke:#4D4D4D;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st19{fill:none;stroke:#4D4D4D;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st20{fill:#FFFFFF;stroke:#000000;stroke-miterlimit:10;}
.st21{fill:none;stroke:#000000;stroke-width:3;stroke-miterlimit:10;}
.st22{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;}
.st23{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:3.2727,9.8182;}
.st24{fill:none;stroke:#333333;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st25{fill:none;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st26{fill:none;stroke:#333333;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st27{fill:none;stroke:#E6E6E6;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st28{fill:#1EB287;}
.st29{fill:#DEEDCB;}
.st30{fill:#505305;}
.st31{fill:#186435;}
.st32{fill:#A44F79;}
.st33{fill:#2AB188;}
.st34{fill:#A35915;}
.st35{fill:#4D4D4D;}
.st36{fill:#F6B330;}
.st37{fill:#324872;}
.st38{fill:#2BA270;}
.st39{fill:#53A4EA;}
.st40{fill:#3BA2A0;}
.st41{fill:#1792CD;}
.st42{fill:#0C2E3D;}
.st43{fill:#35761B;}
.st44{fill:#0C6364;}
.st45{fill:#F4A519;}
.st46{opacity:0.06;fill:#3B9910;}
.st47{opacity:0.06;fill:#E56200;}
.st48{opacity:0.06;fill:#2E5799;}
.st49{opacity:0.06;fill:#007F7C;}
.st50{opacity:0.06;fill:#00B7FF;}
.st51{opacity:0.06;fill:#FF9D00;}
.st52{opacity:0.06;fill:#00CC8D;}
.st53{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-miterlimit:10;}
.st54{fill:none;stroke:#FFFFFF;stroke-width:3;stroke-linecap:round;stroke-miterlimit:10;}
.st55{fill:#F15A24;}
.st56{fill:#F15A24;stroke:#000000;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st57{fill:none;stroke:#000000;stroke-width:5;stroke-miterlimit:10;}
.st58{fill:#FFFFFF;stroke:#000000;stroke-width:3;stroke-miterlimit:10;}
.st59{opacity:0.8;enable-background:new ;}
.st60{clip-path:url(#SVGID_2_);}
.st61{clip-path:url(#SVGID_4_);}
.st62{clip-path:url(#SVGID_6_);fill:#333333;}
.st63{clip-path:url(#SVGID_8_);}
.st64{clip-path:url(#SVGID_10_);fill:#333333;}
.st65{clip-path:url(#SVGID_12_);}
.st66{clip-path:url(#SVGID_14_);}
.st67{clip-path:url(#SVGID_16_);fill:#333333;}
.st68{clip-path:url(#SVGID_18_);}
.st69{clip-path:url(#SVGID_20_);fill:#333333;}
.st70{clip-path:url(#SVGID_22_);}
.st71{clip-path:url(#SVGID_24_);fill:#333333;}
.st72{clip-path:url(#SVGID_26_);}
.st73{clip-path:url(#SVGID_28_);fill:#333333;}
.st74{clip-path:url(#SVGID_30_);}
.st75{clip-path:url(#SVGID_32_);fill:#333333;}
.st76{clip-path:url(#SVGID_34_);}
.st77{clip-path:url(#SVGID_36_);fill:#333333;}
.st78{clip-path:url(#SVGID_38_);}
.st79{clip-path:url(#SVGID_40_);fill:#333333;}
.st80{clip-path:url(#SVGID_42_);}
.st81{clip-path:url(#SVGID_44_);fill:#333333;}
.st82{clip-path:url(#SVGID_46_);}
.st83{clip-path:url(#SVGID_48_);fill:#333333;}
.st84{clip-path:url(#SVGID_50_);}
.st85{clip-path:url(#SVGID_52_);fill:#333333;}
.st86{clip-path:url(#SVGID_54_);}
.st87{clip-path:url(#SVGID_56_);fill:#333333;}
.st88{clip-path:url(#SVGID_58_);}
.st89{clip-path:url(#SVGID_60_);fill:#333333;}
.st90{clip-path:url(#SVGID_62_);}
.st91{clip-path:url(#SVGID_64_);}
.st92{clip-path:url(#SVGID_66_);enable-background:new ;}
.st93{clip-path:url(#SVGID_68_);}
.st94{clip-path:url(#SVGID_70_);}
.st95{clip-path:url(#SVGID_72_);fill:#333333;}
.st96{clip-path:url(#SVGID_74_);}
.st97{clip-path:url(#SVGID_76_);}
.st98{clip-path:url(#SVGID_78_);fill:#333333;}
.st99{clip-path:url(#SVGID_80_);}
.st100{clip-path:url(#SVGID_82_);fill:#333333;}
.st101{clip-path:url(#SVGID_84_);}
.st102{clip-path:url(#SVGID_86_);}
.st103{clip-path:url(#SVGID_88_);fill:#333333;}
.st104{clip-path:url(#SVGID_90_);}
.st105{clip-path:url(#SVGID_92_);fill:#333333;}
.st106{clip-path:url(#SVGID_94_);}
.st107{clip-path:url(#SVGID_96_);fill:#333333;}
.st108{clip-path:url(#SVGID_98_);}
.st109{clip-path:url(#SVGID_100_);fill:#333333;}
.st110{opacity:0.8;clip-path:url(#SVGID_102_);fill:#333333;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_3">
</g>
<g id="Layer_4">
</g>
<g id="Layer_1">
<g>
<path d="M46.86,22.5l-3.1,0c-0.82,0-1.53-0.57-1.71-1.37c-1.47-6.45-6.53-11.65-13.14-13.17C28.09,7.78,27.5,7.05,27.5,6.2l0-3.06
c0-1.31-0.94-2.5-2.24-2.63C23.76,0.36,22.5,1.53,22.5,3v3.24c0,0.82-0.57,1.52-1.37,1.71C14.68,9.42,9.48,14.48,7.97,21.09
C7.78,21.91,7.05,22.5,6.2,22.5l-3.06,0c-1.31,0-2.5,0.94-2.63,2.24C0.36,26.24,1.53,27.5,3,27.5h3.19c0.84,0,1.58,0.58,1.76,1.4
c0.61,2.69,1.84,5.21,3.67,7.37c2.48,2.94,5.79,4.93,9.48,5.76c0.82,0.19,1.4,0.92,1.4,1.76l0,3.06c0,1.31,0.94,2.5,2.24,2.63
c1.5,0.15,2.76-1.02,2.76-2.49v-3.24c0-0.82,0.57-1.53,1.37-1.71c6.45-1.47,11.66-6.53,13.17-13.14c0.19-0.83,0.92-1.41,1.77-1.41
H47c1.47,0,2.64-1.26,2.49-2.76C49.36,23.44,48.17,22.5,46.86,22.5z M28.31,37.05c-0.91,0.25-1.81-0.41-1.81-1.36v-3.91
c0-0.83-0.67-1.5-1.5-1.5s-1.5,0.67-1.5,1.5v3.9c0,0.91-0.86,1.61-1.73,1.38c-2.46-0.65-4.65-2.03-6.32-4.01
c-1.2-1.43-2.05-3.07-2.52-4.82c-0.23-0.88,0.47-1.73,1.38-1.73h3.92c0.83,0,1.5-0.67,1.5-1.5s-0.67-1.5-1.5-1.5h-3.9
c-0.94,0-1.6-0.91-1.35-1.82c1.17-4.27,4.54-7.57,8.72-8.73c0.91-0.25,1.81,0.41,1.81,1.36v3.91c0,0.83,0.67,1.5,1.5,1.5
s1.5-0.67,1.5-1.5v-3.9c0-0.94,0.91-1.6,1.82-1.35c4.27,1.17,7.57,4.54,8.73,8.72c0.25,0.91-0.41,1.81-1.36,1.81h-3.91
c-0.83,0-1.5,0.67-1.5,1.5s0.67,1.5,1.5,1.5h3.9c0.95,0,1.6,0.91,1.35,1.82C35.86,32.59,32.5,35.89,28.31,37.05z"/>
<circle cx="25" cy="25" r="1.75"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -0,0 +1,169 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-miterlimit:10;}
.st1{fill:#C46F24;}
.st2{opacity:0.5;fill:#C46F24;}
.st3{fill:#C1631E;}
.st4{fill:#FFFFFF;stroke:#000000;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st5{fill:#FFFFFF;stroke:#000000;stroke-width:7;stroke-miterlimit:10;}
.st6{fill:none;stroke:#000000;stroke-width:7;stroke-linecap:round;stroke-miterlimit:10;}
.st7{fill:#FFFFFF;stroke:#000000;stroke-width:5;stroke-miterlimit:10;}
.st8{fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-miterlimit:10;}
.st9{fill:#F15A24;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st10{fill:none;stroke:#000000;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st11{fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st12{fill:#FFFFFF;}
.st13{fill:#FFFFFF;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st14{fill:none;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:4.088,10.22;}
.st15{fill:#FFFFFF;stroke:#333333;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st16{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st17{fill:none;stroke:#FFFFFF;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st18{fill:none;stroke:#969696;stroke-width:5;stroke-miterlimit:10;}
.st19{fill:#B3B3B3;stroke:#4D4D4D;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st20{fill:#B3B3B3;stroke:#4D4D4D;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st21{fill:none;stroke:#4D4D4D;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st22{fill:#FFFFFF;stroke:#000000;stroke-miterlimit:10;}
.st23{fill:none;stroke:#000000;stroke-width:3;stroke-miterlimit:10;}
.st24{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;}
.st25{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:3.2727,9.8182;}
.st26{fill:none;stroke:#333333;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st27{fill:none;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st28{fill:none;stroke:#333333;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st29{fill:none;stroke:#E6E6E6;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st30{fill:#1EB287;}
.st31{fill:#DEEDCB;}
.st32{fill:#505305;}
.st33{fill:#186435;}
.st34{fill:#A44F79;}
.st35{fill:#2AB188;}
.st36{fill:#A35915;}
.st37{fill:#4D4D4D;}
.st38{fill:#F6B330;}
.st39{fill:#324872;}
.st40{fill:#2BA270;}
.st41{fill:#53A4EA;}
.st42{fill:#3BA2A0;}
.st43{fill:#1792CD;}
.st44{fill:#0C2E3D;}
.st45{fill:#35761B;}
.st46{fill:#0C6364;}
.st47{fill:#F4A519;}
.st48{opacity:0.06;fill:#3B9910;}
.st49{opacity:0.06;fill:#E56200;}
.st50{opacity:0.06;fill:#2E5799;}
.st51{opacity:0.06;fill:#007F7C;}
.st52{opacity:0.06;fill:#00B7FF;}
.st53{opacity:0.06;fill:#FF9D00;}
.st54{opacity:0.06;fill:#00CC8D;}
.st55{fill:#91A7B4;}
.st56{fill:#9F6300;}
.st57{fill:#07B386;}
.st58{fill:#FF4D2D;}
.st59{fill:#F9FBFC;}
.st60{fill:#FBF7F0;}
.st61{fill:#F0F8F8;}
.st62{fill:#F1FCF9;}
.st63{fill:#FFF7F6;}
.st64{fill:#AF8761;}
.st65{fill:#FFF8F3;}
.st66{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-miterlimit:10;}
.st67{fill:none;stroke:#FFFFFF;stroke-width:3;stroke-linecap:round;stroke-miterlimit:10;}
.st68{fill:#F15A24;}
.st69{fill:#F15A24;stroke:#000000;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st70{fill:none;stroke:#000000;stroke-width:5;stroke-miterlimit:10;}
.st71{fill:#FFFFFF;stroke:#000000;stroke-width:3;stroke-miterlimit:10;}
.st72{opacity:0.8;enable-background:new ;}
.st73{clip-path:url(#SVGID_2_);}
.st74{clip-path:url(#SVGID_4_);}
.st75{clip-path:url(#SVGID_6_);fill:#333333;}
.st76{clip-path:url(#SVGID_8_);}
.st77{clip-path:url(#SVGID_10_);fill:#333333;}
.st78{clip-path:url(#SVGID_12_);}
.st79{clip-path:url(#SVGID_14_);}
.st80{clip-path:url(#SVGID_16_);fill:#333333;}
.st81{clip-path:url(#SVGID_18_);}
.st82{clip-path:url(#SVGID_20_);fill:#333333;}
.st83{clip-path:url(#SVGID_22_);}
.st84{clip-path:url(#SVGID_24_);fill:#333333;}
.st85{clip-path:url(#SVGID_26_);}
.st86{clip-path:url(#SVGID_28_);fill:#333333;}
.st87{clip-path:url(#SVGID_30_);}
.st88{clip-path:url(#SVGID_32_);fill:#333333;}
.st89{clip-path:url(#SVGID_34_);}
.st90{clip-path:url(#SVGID_36_);fill:#333333;}
.st91{clip-path:url(#SVGID_38_);}
.st92{clip-path:url(#SVGID_40_);fill:#333333;}
.st93{clip-path:url(#SVGID_42_);}
.st94{clip-path:url(#SVGID_44_);fill:#333333;}
.st95{clip-path:url(#SVGID_46_);}
.st96{clip-path:url(#SVGID_48_);fill:#333333;}
.st97{clip-path:url(#SVGID_50_);}
.st98{clip-path:url(#SVGID_52_);fill:#333333;}
.st99{clip-path:url(#SVGID_54_);}
.st100{clip-path:url(#SVGID_56_);fill:#333333;}
.st101{clip-path:url(#SVGID_58_);}
.st102{clip-path:url(#SVGID_60_);fill:#333333;}
.st103{clip-path:url(#SVGID_62_);}
.st104{clip-path:url(#SVGID_64_);}
.st105{clip-path:url(#SVGID_66_);enable-background:new ;}
.st106{clip-path:url(#SVGID_68_);}
.st107{clip-path:url(#SVGID_70_);}
.st108{clip-path:url(#SVGID_72_);fill:#333333;}
.st109{clip-path:url(#SVGID_74_);}
.st110{clip-path:url(#SVGID_76_);}
.st111{clip-path:url(#SVGID_78_);fill:#333333;}
.st112{clip-path:url(#SVGID_80_);}
.st113{clip-path:url(#SVGID_82_);fill:#333333;}
.st114{clip-path:url(#SVGID_84_);}
.st115{clip-path:url(#SVGID_86_);}
.st116{clip-path:url(#SVGID_88_);fill:#333333;}
.st117{clip-path:url(#SVGID_90_);}
.st118{clip-path:url(#SVGID_92_);fill:#333333;}
.st119{clip-path:url(#SVGID_94_);}
.st120{clip-path:url(#SVGID_96_);fill:#333333;}
.st121{clip-path:url(#SVGID_98_);}
.st122{clip-path:url(#SVGID_100_);fill:#333333;}
.st123{opacity:0.8;clip-path:url(#SVGID_102_);fill:#333333;}
.st124{fill:#FFFFFF;stroke:#000000;stroke-width:7;stroke-linecap:round;stroke-miterlimit:10;}
.st125{fill:#FFFFFF;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-miterlimit:10;}
.st126{fill:#FFFFFF;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st127{fill:#FFFFFF;stroke:#000000;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st128{opacity:0.8;}
.st129{opacity:0.8;fill:#FFFFFF;}
.st130{fill:none;stroke:#FFFFFF;stroke-width:6;stroke-miterlimit:10;}
.st131{fill:none;stroke:#FFFFFF;stroke-width:3;stroke-miterlimit:10;}
.st132{opacity:0.2;fill:url(#SVGID_103_);}
.st133{fill:#D8EAD2;}
.st134{opacity:0.2;fill:url(#SVGID_104_);}
.st135{opacity:0.06;fill:#E07127;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_3">
</g>
<g id="Layer_4">
</g>
<g id="Layer_1">
<path d="M41.87,27.98l-0.24-0.08c-0.87-0.28-1.42-1.12-1.37-2.03c0.03-0.5,0.03-1.01,0.01-1.51c-0.04-0.91,0.51-1.73,1.37-2.01
l0.22-0.07c1.29-0.42,2.13-1.76,1.78-3.07c-0.38-1.39-1.84-2.17-3.19-1.73l-0.29,0.09c-0.87,0.28-1.81-0.07-2.31-0.85
c-0.28-0.44-0.58-0.86-0.9-1.27c-0.56-0.71-0.6-1.7-0.07-2.43l0.07-0.1c0.78-1.08,0.72-2.59-0.25-3.46c-1.12-1-2.83-0.8-3.69,0.39
l-0.14,0.2c-0.54,0.74-1.5,1.01-2.35,0.68c-0.5-0.19-1-0.36-1.51-0.49c-0.88-0.24-1.5-1.02-1.5-1.93V8.23
c0-1.32-0.94-2.52-2.24-2.65c-1.5-0.15-2.76,1.02-2.76,2.49l0,0.26c0,0.91-0.62,1.69-1.5,1.93c-0.51,0.14-1.01,0.3-1.5,0.49
c-0.85,0.33-1.81,0.06-2.35-0.68l-0.06-0.08c-0.78-1.07-2.24-1.48-3.37-0.83c-1.3,0.76-1.63,2.45-0.77,3.63l0.16,0.22
c0.54,0.74,0.49,1.73-0.08,2.44c-0.25,0.31-0.57,0.8-0.87,1.27c-0.49,0.78-1.44,1.13-2.32,0.84l-0.17-0.06
c-1.24-0.4-2.67,0.12-3.19,1.32c-0.61,1.38,0.12,2.94,1.51,3.39l0.36,0.12c0.86,0.28,1.41,1.1,1.37,2
c-0.02,0.5-0.02,1.01,0.01,1.51c0.05,0.92-0.5,1.76-1.37,2.04l-0.24,0.08c-1.23,0.4-2.07,1.64-1.81,2.91
c0.25,1.22,1.31,2.01,2.45,2.01c0.26,0,0.52-0.04,0.77-0.12l0.42-0.14c0.86-0.28,1.79,0.06,2.29,0.82c0.27,0.41,0.56,0.8,0.87,1.18
c0.57,0.71,0.62,1.71,0.08,2.45l-0.15,0.21c-0.85,1.17-0.7,2.8,0.42,3.63c0.45,0.33,0.97,0.49,1.49,0.49
c0.77,0,1.54-0.36,2.02-1.03l0.27-0.37c0.54-0.75,1.53-1.01,2.38-0.67c0.48,0.2,0.95,0.36,1.42,0.51c0.85,0.26,1.43,1.03,1.43,1.91
l0,0.22c0,1.35,0.94,2.54,2.24,2.67c1.5,0.15,2.76-1.02,2.76-2.49V40.4c1.67-0.26,3.1-0.81,4.24-1.41
c0.06-0.03,0.11-0.06,0.16-0.09L33,40.4c0.49,0.67,1.25,1.03,2.02,1.03c0.52,0,1.04-0.16,1.49-0.49c1.12-0.83,1.26-2.47,0.45-3.59
l-0.18-0.24c-0.53-0.72-0.51-1.71,0.05-2.41c0.35-0.44,0.65-0.86,0.9-1.23c0.5-0.75,1.43-1.09,2.29-0.81l0.42,0.14
c0.26,0.08,0.52,0.12,0.77,0.12c1.14,0,2.2-0.79,2.45-2.01C43.93,29.62,43.1,28.38,41.87,27.98z M29.41,34.56
c-2.74,1.45-5.83,1.31-9.16-0.43c-5.04-2.62-7-8.85-4.38-13.89c1.83-3.52,5.43-5.54,9.15-5.54c1.6,0,3.22,0.37,4.74,1.16
c5.04,2.62,7,8.85,4.38,13.89C33.33,31.3,31.67,33.37,29.41,34.56z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-miterlimit:10;}
.st1{fill:#C46F24;}
.st2{opacity:0.5;fill:#C46F24;}
.st3{fill:#FFFFFF;stroke:#000000;stroke-width:7;stroke-miterlimit:10;}
.st4{fill:none;stroke:#000000;stroke-width:7;stroke-linecap:round;stroke-miterlimit:10;}
.st5{fill:#FFFFFF;stroke:#000000;stroke-width:5;stroke-miterlimit:10;}
.st6{fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-miterlimit:10;}
.st7{fill:#F15A24;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st8{fill:none;stroke:#000000;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st9{fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st10{fill:#FFFFFF;}
.st11{fill:#FFFFFF;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st12{fill:none;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:4.088,10.22;}
.st13{fill:#FFFFFF;stroke:#333333;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st14{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st15{fill:none;stroke:#FFFFFF;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st16{fill:none;stroke:#969696;stroke-width:5;stroke-miterlimit:10;}
.st17{fill:#B3B3B3;stroke:#4D4D4D;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st18{fill:#B3B3B3;stroke:#4D4D4D;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st19{fill:none;stroke:#4D4D4D;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st20{fill:#FFFFFF;stroke:#000000;stroke-miterlimit:10;}
.st21{fill:none;stroke:#000000;stroke-width:3;stroke-miterlimit:10;}
.st22{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;}
.st23{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:3.2727,9.8182;}
.st24{fill:none;stroke:#333333;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st25{fill:none;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st26{fill:none;stroke:#333333;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st27{fill:none;stroke:#E6E6E6;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st28{fill:#1EB287;}
.st29{fill:#DEEDCB;}
.st30{fill:#505305;}
.st31{fill:#186435;}
.st32{fill:#A44F79;}
.st33{fill:#2AB188;}
.st34{fill:#A35915;}
.st35{fill:#4D4D4D;}
.st36{fill:#F6B330;}
.st37{fill:#324872;}
.st38{fill:#2BA270;}
.st39{fill:#53A4EA;}
.st40{fill:#3BA2A0;}
.st41{fill:#1792CD;}
.st42{fill:#0C2E3D;}
.st43{fill:#35761B;}
.st44{fill:#0C6364;}
.st45{fill:#F4A519;}
.st46{opacity:0.06;fill:#3B9910;}
.st47{opacity:0.06;fill:#E56200;}
.st48{opacity:0.06;fill:#2E5799;}
.st49{opacity:0.06;fill:#007F7C;}
.st50{opacity:0.06;fill:#00B7FF;}
.st51{opacity:0.06;fill:#FF9D00;}
.st52{opacity:0.06;fill:#00CC8D;}
.st53{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-miterlimit:10;}
.st54{fill:none;stroke:#FFFFFF;stroke-width:3;stroke-linecap:round;stroke-miterlimit:10;}
.st55{fill:#F15A24;}
.st56{fill:#F15A24;stroke:#000000;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st57{fill:none;stroke:#000000;stroke-width:5;stroke-miterlimit:10;}
.st58{fill:#FFFFFF;stroke:#000000;stroke-width:3;stroke-miterlimit:10;}
.st59{opacity:0.8;enable-background:new ;}
.st60{clip-path:url(#SVGID_2_);}
.st61{clip-path:url(#SVGID_4_);}
.st62{clip-path:url(#SVGID_6_);fill:#333333;}
.st63{clip-path:url(#SVGID_8_);}
.st64{clip-path:url(#SVGID_10_);fill:#333333;}
.st65{clip-path:url(#SVGID_12_);}
.st66{clip-path:url(#SVGID_14_);}
.st67{clip-path:url(#SVGID_16_);fill:#333333;}
.st68{clip-path:url(#SVGID_18_);}
.st69{clip-path:url(#SVGID_20_);fill:#333333;}
.st70{clip-path:url(#SVGID_22_);}
.st71{clip-path:url(#SVGID_24_);fill:#333333;}
.st72{clip-path:url(#SVGID_26_);}
.st73{clip-path:url(#SVGID_28_);fill:#333333;}
.st74{clip-path:url(#SVGID_30_);}
.st75{clip-path:url(#SVGID_32_);fill:#333333;}
.st76{clip-path:url(#SVGID_34_);}
.st77{clip-path:url(#SVGID_36_);fill:#333333;}
.st78{clip-path:url(#SVGID_38_);}
.st79{clip-path:url(#SVGID_40_);fill:#333333;}
.st80{clip-path:url(#SVGID_42_);}
.st81{clip-path:url(#SVGID_44_);fill:#333333;}
.st82{clip-path:url(#SVGID_46_);}
.st83{clip-path:url(#SVGID_48_);fill:#333333;}
.st84{clip-path:url(#SVGID_50_);}
.st85{clip-path:url(#SVGID_52_);fill:#333333;}
.st86{clip-path:url(#SVGID_54_);}
.st87{clip-path:url(#SVGID_56_);fill:#333333;}
.st88{clip-path:url(#SVGID_58_);}
.st89{clip-path:url(#SVGID_60_);fill:#333333;}
.st90{clip-path:url(#SVGID_62_);}
.st91{clip-path:url(#SVGID_64_);}
.st92{clip-path:url(#SVGID_66_);enable-background:new ;}
.st93{clip-path:url(#SVGID_68_);}
.st94{clip-path:url(#SVGID_70_);}
.st95{clip-path:url(#SVGID_72_);fill:#333333;}
.st96{clip-path:url(#SVGID_74_);}
.st97{clip-path:url(#SVGID_76_);}
.st98{clip-path:url(#SVGID_78_);fill:#333333;}
.st99{clip-path:url(#SVGID_80_);}
.st100{clip-path:url(#SVGID_82_);fill:#333333;}
.st101{clip-path:url(#SVGID_84_);}
.st102{clip-path:url(#SVGID_86_);}
.st103{clip-path:url(#SVGID_88_);fill:#333333;}
.st104{clip-path:url(#SVGID_90_);}
.st105{clip-path:url(#SVGID_92_);fill:#333333;}
.st106{clip-path:url(#SVGID_94_);}
.st107{clip-path:url(#SVGID_96_);fill:#333333;}
.st108{clip-path:url(#SVGID_98_);}
.st109{clip-path:url(#SVGID_100_);fill:#333333;}
.st110{opacity:0.8;clip-path:url(#SVGID_102_);fill:#333333;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_3">
</g>
<g id="Layer_4">
</g>
<g id="Layer_1">
<g>
<path d="M43.89,5.33c-4.71-1.42-11.55-2.24-18.76-2.24S11.07,3.91,6.37,5.33C5.32,5.65,4.63,6.63,4.63,7.72l0,13.76
c0,1.11,0.73,2.08,1.8,2.4c0.83,0.25,1.73,0.48,2.68,0.69c0.59,0.13,1.02,0.65,1.02,1.25v2.55c0,0.83,0.67,1.5,1.5,1.5
s1.5-0.67,1.5-1.5v-1.92c0-0.62,0.54-1.09,1.16-1.01c1.18,0.15,2.41,0.28,3.68,0.38c0.65,0.05,1.16,0.59,1.16,1.25v2.31
c0,0.83,0.67,1.5,1.5,1.5c0.83,0,1.5-0.67,1.5-1.5v-2.09c0-0.68,0.56-1.22,1.23-1.2c0.59,0.01,1.17,0.02,1.77,0.02
s1.18-0.01,1.77-0.02c0.68-0.01,1.23,0.53,1.23,1.2v2.09c0,0.83,0.67,1.5,1.5,1.5c0.83,0,1.5-0.67,1.5-1.5v-2.31
c0-0.66,0.51-1.2,1.16-1.25c1.26-0.1,2.49-0.23,3.68-0.38c0.62-0.08,1.16,0.39,1.16,1.01v1.92c0,0.83,0.67,1.5,1.5,1.5
s1.5-0.67,1.5-1.5v-2.55c0-0.61,0.43-1.12,1.02-1.25c0.95-0.21,1.85-0.44,2.68-0.69c1.06-0.32,1.8-1.29,1.8-2.4l0-13.76
C45.63,6.63,44.93,5.65,43.89,5.33z M25.13,8.09c5.57,0,10.85,0.5,14.91,1.41c0.34,0.08,0.59,0.38,0.59,0.74v0
c0,0.36-0.25,0.67-0.6,0.74c-4.17,0.85-9.47,1.34-15.05,1.34c-5.45,0-10.63-0.46-14.75-1.28c-0.35-0.07-0.6-0.38-0.6-0.74v-0.06
c0-0.35,0.24-0.66,0.59-0.74C14.28,8.6,19.56,8.09,25.13,8.09z M25.13,21.09c-5.18,0-10.1-0.44-14.04-1.22
c-0.85-0.17-1.46-0.92-1.46-1.79v-2.73c0-0.71,0.63-1.25,1.33-1.13c4.08,0.7,8.95,1.09,14.02,1.09c5.19,0,10.17-0.41,14.31-1.14
c0.7-0.12,1.35,0.43,1.35,1.14v2.77c0,0.87-0.61,1.62-1.46,1.79C35.23,20.66,30.3,21.09,25.13,21.09z"/>
<path d="M27.61,38.72c-0.37-0.6-0.66-1.08-0.87-1.44l0,0c-0.36-0.61-1.02-0.94-1.71-0.97c-0.7,0-1.35,0.39-1.7,1
c-0.2,0.35-0.48,0.83-0.85,1.42c-0.07,0.11-0.17,0.25-0.28,0.42c-0.76,1.11-2.17,3.19-2.17,5.49c0,2.83,2.24,5.13,5,5.13
s5-2.3,5-5.13c0-2.31-1.4-4.39-2.16-5.51C27.77,38.97,27.67,38.83,27.61,38.72z M25.58,41.1c0.59,0.89,1.44,2.29,1.44,3.54
c0,1.17-0.9,2.13-2,2.13c-1.1,0-2-0.96-2-2.13c0-1.23,0.86-2.63,1.46-3.52l0.15-0.22c0.2-0.29,0.63-0.29,0.82,0L25.58,41.1z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.0 KiB