Merge branch 'cosmetic-fixes' of https://github.com/AscendFB/Farmbot-Web-App into cosmetic-fixes

pull/1505/head
AscendFB 2019-10-20 13:04:13 +02:00
commit 8f593ea098
27 changed files with 124 additions and 139 deletions

View File

@ -1 +1 @@
2.6.3
2.6.5

View File

@ -1,5 +1,5 @@
source "https://rubygems.org"
ruby "~> 2.6.3"
ruby "~> 2.6.5"
gem "active_model_serializers"
gem "bunny"

View File

@ -59,7 +59,7 @@ GEM
arel (9.0.0)
bcrypt (3.1.13)
builder (3.2.3)
bunny (2.14.2)
bunny (2.14.3)
amq-protocol (~> 2.3, >= 2.3.0)
case_transform (0.2)
activesupport
@ -91,13 +91,13 @@ GEM
activerecord (>= 4.2, < 7)
docile (1.3.2)
erubi (1.9.0)
factory_bot (5.1.0)
factory_bot (5.1.1)
activesupport (>= 4.2.0)
factory_bot_rails (5.1.0)
factory_bot_rails (5.1.1)
factory_bot (~> 5.1.0)
railties (>= 4.2.0)
faker (2.4.0)
i18n (~> 1.6.0)
faker (2.6.0)
i18n (>= 1.6, < 1.8)
faraday (0.15.4)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.13.1)
@ -106,7 +106,7 @@ GEM
railties (>= 3.2, < 6.1)
globalid (0.4.2)
activesupport (>= 4.2.0)
google-api-client (0.31.0)
google-api-client (0.32.1)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.5, < 0.10.0)
httpclient (>= 2.8.1, < 3.0)
@ -118,7 +118,7 @@ GEM
google-cloud-env (~> 1.0)
google-cloud-env (1.2.1)
faraday (~> 0.11)
google-cloud-storage (1.21.0)
google-cloud-storage (1.21.1)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-api-client (~> 0.26)
@ -135,12 +135,12 @@ GEM
hashdiff (1.0.0)
hashie (3.6.0)
httpclient (2.8.3)
i18n (1.6.0)
i18n (1.7.0)
concurrent-ruby (~> 1.0)
json (2.2.0)
jsonapi-renderer (0.2.2)
jwt (2.2.1)
loofah (2.2.3)
loofah (2.3.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@ -152,7 +152,7 @@ GEM
mimemagic (0.3.3)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.12.0)
minitest (5.12.2)
multi_json (1.13.1)
multipart-post (2.1.1)
mutations (0.9.0)
@ -178,7 +178,7 @@ GEM
hashie (~> 3.6)
multi_json (~> 1.13.1)
rack (2.0.7)
rack-attack (6.1.0)
rack-attack (6.2.0)
rack (>= 1.0, < 3)
rack-cors (1.0.3)
rack-test (1.1.0)
@ -199,8 +199,8 @@ GEM
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.2.0)
loofah (~> 2.2, >= 2.2.2)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
rails_12factor (0.0.3)
rails_serve_static_assets
rails_stdout_logging
@ -225,32 +225,32 @@ GEM
railties (>= 5.0)
retriable (3.1.2)
rollbar (2.22.1)
rspec (3.8.0)
rspec-core (~> 3.8.0)
rspec-expectations (~> 3.8.0)
rspec-mocks (~> 3.8.0)
rspec-core (3.8.2)
rspec-support (~> 3.8.0)
rspec-expectations (3.8.4)
rspec (3.9.0)
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
rspec-mocks (~> 3.9.0)
rspec-core (3.9.0)
rspec-support (~> 3.9.0)
rspec-expectations (3.9.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.8.0)
rspec-mocks (3.8.1)
rspec-support (~> 3.9.0)
rspec-mocks (3.9.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.8.0)
rspec-rails (3.8.2)
rspec-support (~> 3.9.0)
rspec-rails (3.9.0)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
rspec-core (~> 3.8.0)
rspec-expectations (~> 3.8.0)
rspec-mocks (~> 3.8.0)
rspec-support (~> 3.8.0)
rspec-support (3.8.2)
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
rspec-mocks (~> 3.9.0)
rspec-support (~> 3.9.0)
rspec-support (3.9.0)
scenic (1.5.1)
activerecord (>= 4.0.0)
railties (>= 4.0.0)
secure_headers (6.1.1)
signet (0.11.0)
signet (0.12.0)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
@ -260,7 +260,7 @@ GEM
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
sprockets (3.7.2)
sprockets (4.0.0)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)
@ -327,7 +327,7 @@ DEPENDENCIES
zero_downtime_migrations
RUBY VERSION
ruby 2.6.3p62
ruby 2.6.5p114
BUNDLED WITH
1.17.2

View File

@ -0,0 +1 @@
//= link application.css

View File

@ -7,7 +7,8 @@ class LogService < AbstractServiceRunner
T.new(1.hour) => 0.5 * 10_000,
T.new(1.day) => 0.5 * 100_000
LOG_TPL = "FBOS LOG (device_%s): %s"
LOG_TPL = Rails.env.test? ?
"\e[32m.\e[0m" : "FBOS LOG (device_%s): %s\n"
ERR_TPL = "MALFORMED LOG CAPTURE: %s"
def process(delivery_info, payload)
@ -34,7 +35,7 @@ class LogService < AbstractServiceRunner
dev, log = [data.device, data.payload]
dev.maybe_unthrottle
Log.deliver(dev, Logs::Create.run!(log, device: dev))
puts LOG_TPL % [data.device_id, data.payload["message"] || "??"]
print LOG_TPL % [data.device_id, data.payload["message"] || "??"]
rescue => x
Rollbar.error(x)
end

View File

@ -18,7 +18,8 @@ if Rails.env == "development"
Peripheral,
Log,
PinBinding,
Point,
PointGroupItem,
PointGroup,
Point,
TokenIssuance,
ToolSlot,
@ -27,7 +28,6 @@ if Rails.env == "development"
SavedGarden,
SensorReading,
FarmwareInstallation,
PointGroup,
Tool,
Device,
Delayed::Job,

View File

@ -1,4 +1,4 @@
FROM ruby:2.6.3
FROM ruby:2.6.5
RUN wget -q https://www.postgresql.org/media/keys/ACCC4CF8.asc -O - | apt-key add -
RUN sh -c 'VERSION_CODENAME=stretch; . /etc/os-release; echo "deb http://apt.postgresql.org/pub/repos/apt/ $VERSION_CODENAME-pgdg main" >> /etc/apt/sources.list.d/pgdg.list'
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev \

View File

@ -43,6 +43,7 @@ export let bot: Everything["bot"] = {
busy: false,
locked: false,
commit: "---",
controller_uuid: "123",
target: "---",
env: "---",
node_name: "---",

View File

@ -117,8 +117,16 @@ describe("<App />: NavBar", () => {
const p = fakeProps();
p.loaded = FULLY_LOADED;
const wrapper = mount(<App {...p} />);
expect(wrapper.text())
.toContain("Farm DesignerControlsDeviceSequencesRegimensToolsFarmware");
const t = wrapper.text();
const strings = [
"Farm Designer",
"Controls",
"Device",
"Sequences",
"Regimens",
"Farmware"
];
strings.map(string => expect(t).toContain(string));
wrapper.unmount();
});

View File

@ -1442,7 +1442,7 @@ ul {
padding-right: 15px;
}
.input-line{
width: -webkit-fill-available;
flex: auto;
}
}

View File

@ -95,8 +95,6 @@ describe("<FarmDesigner/>", () => {
it("renders nav titles", () => {
mockPath = "/app/designer/plants";
const wrapper = mount(<FarmDesigner {...fakeProps()} />);
["Map", "Plants", "Events"].map(string =>
expect(wrapper.text()).toContain(string));
expect(wrapper.find(".panel-nav").first().hasClass("hidden"))
.toBeTruthy();
expect(wrapper.find(".farm-designer-panels").hasClass("panel-open"))
@ -108,8 +106,6 @@ describe("<FarmDesigner/>", () => {
it("hides panel", () => {
mockPath = "/app/designer";
const wrapper = mount(<FarmDesigner {...fakeProps()} />);
["Map", "Plants", "Events"].map(string =>
expect(wrapper.text()).toContain(string));
expect(wrapper.find(".panel-nav").first().hasClass("hidden"))
.toBeFalsy();
expect(wrapper.find(".farm-designer-panels").hasClass("panel-open"))

View File

@ -15,12 +15,6 @@ describe("<PureFarmEvents/>", () => {
calendarRows,
});
it("renders nav", () => {
const wrapper = render(<PureFarmEvents {...fakeProps()} />);
["Map", "Plants", "Events"].map(string =>
expect(wrapper.text()).toContain(string));
});
it("sorts items correctly", () => {
const results = render(<PureFarmEvents {...fakeProps()} />);
const rows = results

View File

@ -1,8 +1,8 @@
import * as React from "react";
import { getPathArray } from "../history";
import { Link } from "../link";
import { DevSettings } from "../account/dev/dev_support";
import { t } from "../i18next_wrapper";
import { DevSettings } from "../account/dev/dev_support";
export enum Panel {
Map = "Map",
@ -72,13 +72,10 @@ interface NavTabProps {
}
const NavTab = (props: NavTabProps) =>
<Link to={props.linkTo} style={(props.icon
&& !DevSettings.futureFeaturesEnabled()) ? { flex: 0.3 } : {}}
<Link to={props.linkTo} style={{ flex: 0.3 }}
className={getCurrentTab() === props.panel ? "active" : ""}>
{DevSettings.futureFeaturesEnabled()
? <img {...common}
src={TAB_ICON[props.panel]} title={props.title} />
: props.icon ? <i className={props.icon} /> : props.title}
<img {...common}
src={TAB_ICON[props.panel]} title={props.title} />
</Link>;
export function DesignerNavTabs(props: { hidden?: boolean }) {
@ -93,14 +90,14 @@ export function DesignerNavTabs(props: { hidden?: boolean }) {
panel={Panel.Plants}
linkTo={"/app/designer/plants"}
title={t("Plants")} />
{DevSettings.futureFeaturesEnabled() && <NavTab
<NavTab
panel={Panel.Groups}
linkTo={"/app/designer/groups"}
title={t("Groups")} />}
{DevSettings.futureFeaturesEnabled() && <NavTab
title={t("Groups")} />
<NavTab
panel={Panel.SavedGardens}
linkTo={"/app/designer/saved_gardens"}
title={t("Gardens")} />}
title={t("Gardens")} />
<NavTab
panel={Panel.FarmEvents}
linkTo={"/app/designer/events"}

View File

@ -12,12 +12,8 @@ describe("<PlantInventory />", () => {
it("renders", () => {
const wrapper = mount(<Plants {...fakeProps()} />);
["Map",
"Plants",
"Events",
"Strawberry Plant",
"11 days old"
].map(string => expect(wrapper.text()).toContain(string));
["Strawberry Plant",
"11 days old"].map(string => expect(wrapper.text()).toContain(string));
expect(wrapper.find("input").props().placeholder)
.toEqual("Search your plants...");
});

View File

@ -13,7 +13,6 @@ import {
} from "./designer_panel";
import { t } from "../../i18next_wrapper";
import { createGroup } from "../point_groups/actions";
import { DevSettings } from "../../account/dev/dev_support";
export function mapStateToProps(props: Everything) {
return {
@ -73,11 +72,6 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
onClick={() => this.destroySelected(this.props.selected)}>
{t("Delete")}
</button>
{DevSettings.futureFeaturesEnabled() &&
<button className="fb-button green"
onClick={() => { throw new Error("WIP"); }}>
{t("Create garden")}
</button>}
<button className="fb-button blue"
onClick={() => this.props.dispatch(createGroup({
points: this.props.selected

View File

@ -80,12 +80,6 @@ describe("<SavedGardens />", () => {
expect(destroySavedGarden).toHaveBeenCalledWith(p.savedGardens[0].uuid);
});
it("goes back", () => {
const wrapper = mount(<SavedGardens {...fakeProps()} />);
wrapper.find("i").first().simulate("click");
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
});
it("has no saved gardens yet", () => {
const p = fakeProps();
p.savedGardens = [];

View File

@ -13,9 +13,9 @@ import { closeSavedGarden } from "./actions";
import { TaggedSavedGarden } from "farmbot";
import { Content } from "../../constants";
import {
DesignerPanel, DesignerPanelContent, DesignerPanelHeader
DesignerPanel,
DesignerPanelContent
} from "../plants/designer_panel";
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";
@ -40,18 +40,11 @@ export class RawSavedGardens extends React.Component<SavedGardensProps, {}> {
}
render() {
const alt = DevSettings.futureFeaturesEnabled();
return <DesignerPanel panelName={"saved-garden"} panelColor={"green"}>
{alt ? <DesignerNavTabs /> :
<DesignerPanelHeader
panelName={"saved-garden"}
panelColor={"green"}
title={t("Saved Gardens")}
description={Content.SAVED_GARDENS}
backTo={"/app/designer/plants"} />}
<DesignerNavTabs />
<DesignerPanelContent panelName={"saved-garden"}
className={`${alt ? "with-nav" : ""}`}>
{alt && <p>{t(Content.SAVED_GARDENS)}</p>}
className={"with-nav"}>
<p>{t(Content.SAVED_GARDENS)}</p>
<GardenSnapshot
currentSavedGarden={this.currentSavedGarden}
plantTemplates={this.props.plantTemplates}

View File

@ -3,7 +3,6 @@ import { AccountMenuProps } from "./interfaces";
import { docLink } from "../ui/doc_link";
import { Link } from "../link";
import { shortRevision } from "../util";
import { DevSettings } from "../account/dev/dev_support";
import { t } from "../i18next_wrapper";
export const AdditionalMenu = (props: AccountMenuProps) => {
@ -24,13 +23,12 @@ export const AdditionalMenu = (props: AccountMenuProps) => {
<i className="fa fa-question-circle"></i>
{t("Help")}
</Link>
{!DevSettings.futureFeaturesEnabled() &&
<div>
<a href={docLink("the-farmbot-web-app")}
target="_blank">
<i className="fa fa-file-text-o"></i>{t("Documentation")}
</a>
</div>}
<div>
<a href={docLink("the-farmbot-web-app")}
target="_blank">
<i className="fa fa-file-text-o"></i>{t("Documentation")}
</a>
</div>
<div>
<a onClick={props.logout}>
<i className="fa fa-sign-out"></i>

View File

@ -7,7 +7,6 @@ import {
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;
@ -37,9 +36,7 @@ export const getLinks = (): NavLinkParams[] => betterCompact([
name: "Regimens", icon: "calendar-check-o", slug: "regimens",
computeHref: computeEditorUrlFromState("Regimen")
},
DevSettings.futureFeaturesEnabled()
? undefined
: { name: "Tools", icon: "wrench", slug: "tools" },
{ name: "Tools", icon: "wrench", slug: "tools" },
{
name: "Farmware", icon: "crosshairs", slug: "farmware",
computeHref: computeFarmwareUrlFromState

View File

@ -116,7 +116,7 @@ describe("<LocationForm/>", () => {
it("shows groups in dropdown", () => {
const p = fakeProps();
p.shouldDisplay = () => true;
p.disallowGroups = false;
p.hideGroups = false;
const wrapper = shallow(<LocationForm {...p} />);
expect(wrapper.find(FBSelect).first().props().list).toContainEqual({
headingId: "Coordinate",

View File

@ -17,9 +17,9 @@ export interface DefaultValueFormProps {
onChange: (v: ParameterDeclaration) => void;
}
export const DefaultValueForm = (props: DefaultValueFormProps) =>
props.variableNode.kind === "parameter_declaration"
? <div className="default-value-form">
export const DefaultValueForm = (props: DefaultValueFormProps) => {
if (props.variableNode.kind === "parameter_declaration") {
return <div className="default-value-form">
<div className="default-value-tooltip">
<Help text={ToolTips.DEFAULT_VALUE} position={Position.TOP_LEFT} />
</div>
@ -32,9 +32,13 @@ export const DefaultValueForm = (props: DefaultValueFormProps) =>
shouldDisplay={() => true}
allowedVariableNodes={AllowedVariableNodes.variable}
hideTypeLabel={true}
hideGroups={true}
onChange={change(props.onChange, props.variableNode)} />
</div>
: <div />;
</div>;
} else {
return <div />;
}
};
const change =
(onChange: (v: ParameterDeclaration) => void, variable: VariableNode) =>

View File

@ -49,8 +49,9 @@ interface CommonProps {
* chooses between reassignment vs. creation for new variables,
* and determines which variables to display in the form. */
allowedVariableNodes: AllowedVariableNodes;
/** Don't display group dropdown items. */
disallowGroups?: boolean;
/** Do not show `groups` as an option. Eg: Don't allow the user to pick
* "group123" in the sequence editor header. */
hideGroups?: boolean;
/** Add ability to collapse the form content. */
collapsible?: boolean;
collapsed?: boolean;

View File

@ -1,5 +1,5 @@
import * as React from "react";
import { Row, Col, FBSelect } from "../../ui";
import { Row, Col, FBSelect, DropDownItem } from "../../ui";
import { locationFormList, NO_VALUE_SELECTED_DDI } from "./location_form_list";
import { convertDDItoVariable } from "../locals_list/handle_select";
import {
@ -38,6 +38,8 @@ const maybeUseStepData = ({ resources, bodyVariables, variable, uuid }: {
return variable;
};
const hideGroups = (x: DropDownItem) => x.headingId !== "PointGroup";
const allowAll = (_: unknown) => true;
/**
* Form with an "import from" dropdown and coordinate input boxes.
* Can be used to set a specific value, import a value, or declare a variable.
@ -45,7 +47,7 @@ const maybeUseStepData = ({ resources, bodyVariables, variable, uuid }: {
export const LocationForm =
(props: LocationFormProps) => {
const { sequenceUuid, resources, bodyVariables, variable,
allowedVariableNodes, disallowGroups } = props;
allowedVariableNodes } = props;
const { celeryNode, dropdown, vector } = maybeUseStepData({
resources, bodyVariables, variable, uuid: sequenceUuid
});
@ -55,8 +57,8 @@ export const LocationForm =
const variableListItems = displayVariables ? [PARENT(determineVarDDILabel({
label: "parent", resources, uuid: sequenceUuid, forceExternal: headerForm
}))] : [];
const displayGroups = props.shouldDisplay(Feature.groups) && !disallowGroups;
const list = locationFormList(resources, variableListItems, displayGroups);
const list = locationFormList(resources, variableListItems)
.filter(props.hideGroups ? hideGroups : allowAll);
/** Variable name. */
const { label } = celeryNode.args;
if (variable.default) {

View File

@ -75,7 +75,7 @@ export const SequenceSetting = (props: SequenceSettingProps) => {
<label>
{t(props.label)}
</label>
<Help text={t(props.description)} requireClick={true}/>
<Help text={t(props.description)} requireClick={true} />
<ToggleButton
toggleValue={value}
toggleAction={() => proceed() &&
@ -123,7 +123,12 @@ interface SequenceBtnGroupProps {
}
const SequenceBtnGroup = ({
dispatch, sequence, syncStatus, resources, shouldDisplay, menuOpen,
dispatch,
sequence,
syncStatus,
resources,
shouldDisplay,
menuOpen,
getWebAppConfigValue
}: SequenceBtnGroupProps) =>
<div className="button-group">

View File

@ -118,7 +118,8 @@ export class RefactoredExecuteBlock
onChange={assignVariable(this.props)(currentStep.body || [])}
locationDropdownKey={JSON.stringify(currentSequence)}
allowedVariableNodes={AllowedVariableNodes.identifier}
shouldDisplay={this.props.shouldDisplay} />
shouldDisplay={this.props.shouldDisplay}
hideGroups={false} />
</Col>}
</Row>
</StepContent>

View File

@ -96,7 +96,7 @@ export class TileMoveAbsolute extends React.Component<StepParams, MoveAbsState>
hideHeader={true}
locationDropdownKey={JSON.stringify(this.props.currentSequence)}
allowedVariableNodes={AllowedVariableNodes.identifier}
disallowGroups={true}
hideGroups={true}
width={3} />
SpeedInput = () =>

View File

@ -24,57 +24,59 @@
"author": "farmbot.io",
"license": "MIT",
"dependencies": {
"@babel/core": "7.6.2",
"@blueprintjs/core": "3.18.1",
"@blueprintjs/datetime": "3.13.0",
"@blueprintjs/select": "3.10.0",
"@babel/core": "7.6.4",
"@blueprintjs/core": "3.19.1",
"@blueprintjs/datetime": "3.14.0",
"@blueprintjs/select": "3.11.1",
"@types/enzyme": "3.10.3",
"@types/jest": "24.0.18",
"@types/lodash": "4.14.141",
"@types/lodash": "4.14.144",
"@types/markdown-it": "0.0.9",
"@types/moxios": "0.4.9",
"@types/node": "12.7.8",
"@types/node": "12.7.12",
"@types/promise-timeout": "1.3.0",
"@types/react": "16.9.3",
"@types/react": "16.9.5",
"@types/react-color": "3.0.1",
"@types/react-dom": "16.9.1",
"@types/react-redux": "7.1.4",
"axios": "0.19.0",
"boxed_value": "1.0.0",
"browser-speech": "1.1.1",
"coveralls": "3.0.6",
"coveralls": "3.0.7",
"enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.14.0",
"farmbot": "8.3.0-rc5",
"i18next": "17.0.16",
"enzyme-adapter-react-16": "1.15.1",
"farmbot": "8.3.0-rc6",
"i18next": "17.2.0",
"install": "0.13.0",
"lodash": "4.17.15",
"markdown-it": "10.0.0",
"markdown-it-emoji": "1.4.0",
"moment": "2.24.0",
"moxios": "0.4.0",
"mqtt": "3.0.0",
"parcel-bundler": "1.12.3",
"npm": "6.12.0",
"parcel-bundler": "1.12.4",
"promise-timeout": "1.3.0",
"raf": "3.4.1",
"react": "16.10.1",
"react": "16.10.2",
"react-addons-test-utils": "15.6.2",
"react-color": "2.17.3",
"react-dom": "16.10.1",
"react-dom": "16.10.2",
"react-joyride": "2.1.1",
"react-redux": "7.1.1",
"react-test-renderer": "16.10.1",
"react-test-renderer": "16.10.2",
"react-transition-group": "4.3.0",
"redux": "4.0.4",
"redux-immutable-state-invariant": "2.1.0",
"redux-thunk": "2.3.0",
"sass": "1.22.12",
"sass": "1.23.0",
"sass-lint": "1.13.1",
"takeme": "0.11.3",
"ts-jest": "24.1.0",
"ts-lint": "4.5.1",
"tslint": "5.20.0",
"typescript": "3.6.3",
"which": "1.3.1"
"typescript": "3.6.4",
"which": "2.0.1"
},
"devDependencies": {
"jest": "24.9.0",