add destroyAll feature
parent
314baf3854
commit
f9f21f6e5c
|
@ -1,8 +1,8 @@
|
|||
# Api::FarmwareEnvController is the RESTful endpoint for managing key/value
|
||||
# configuration pairs.
|
||||
module Api
|
||||
# Device configs controller handles CRUD for user definable key/value pairs.
|
||||
# Usually used by 3rd party Farmware devs. Not used often as of May 2018
|
||||
# Farmware envs controller handles CRUD for user definable key/value pairs.
|
||||
# Usually used for Farmware settings and data.
|
||||
class FarmwareEnvsController < Api::AbstractController
|
||||
|
||||
def create
|
||||
|
@ -22,7 +22,11 @@ module Api
|
|||
end
|
||||
|
||||
def destroy
|
||||
render json: (farmware_env.destroy! && "")
|
||||
if params[:id] == "all"
|
||||
render json: (current_device.farmware_envs.destroy_all && "")
|
||||
else
|
||||
render json: (farmware_env.destroy! && "")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -13,7 +13,7 @@ module FarmwareEnvs
|
|||
if device.farmware_envs.length >= LIMIT
|
||||
add_error :configs,
|
||||
:configs,
|
||||
"You are over the limit of #{LIMIT} configs."
|
||||
"You are over the limit of #{LIMIT} Farmware Envs."
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ describe Api::FarmwareEnvsController do
|
|||
|
||||
include Devise::Test::ControllerHelpers
|
||||
|
||||
it 'creates a device config' do
|
||||
it 'creates a farmware env' do
|
||||
sign_in user
|
||||
b4 = FarmwareEnv.count
|
||||
input = { key: "Coffee Emoji", value: "☕" }
|
||||
|
@ -57,6 +57,15 @@ describe Api::FarmwareEnvsController do
|
|||
expect(FarmwareEnv.exists?(id)).to be false
|
||||
end
|
||||
|
||||
it 'deletes all' do
|
||||
sign_in user
|
||||
FarmwareEnv.destroy_all
|
||||
FactoryBot.create_list(:farmware_env, 3, device: device)
|
||||
delete :destroy, params: { id: "all" }
|
||||
expect(response.status).to be(200)
|
||||
expect(FarmwareEnv.count).to eq(0)
|
||||
end
|
||||
|
||||
it 'shows' do
|
||||
sign_in user
|
||||
fe = FactoryBot.create(:farmware_env, device: device)
|
|
@ -13,14 +13,14 @@ jest.mock("../maybe_start_tracking", () => ({
|
|||
maybeStartTracking: jest.fn()
|
||||
}));
|
||||
|
||||
let mockDelete: Promise<{}> = Promise.resolve({});
|
||||
let mockDelete: Promise<{} | void> = Promise.resolve({});
|
||||
jest.mock("axios", () => ({
|
||||
default: {
|
||||
delete: jest.fn(() => mockDelete)
|
||||
}
|
||||
}));
|
||||
|
||||
import { destroy } from "../crud";
|
||||
import { destroy, destroyAll } from "../crud";
|
||||
import { API } from "../api";
|
||||
import axios from "axios";
|
||||
import { destroyOK, destroyNO } from "../../resources/actions";
|
||||
|
@ -99,3 +99,37 @@ describe("destroy", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("destroyAll", () => {
|
||||
it("confirmed", async () => {
|
||||
window.confirm = () => true;
|
||||
mockDelete = Promise.resolve();
|
||||
await expect(destroyAll("FarmwareEnv")).resolves.toEqual(undefined);
|
||||
expect(axios.delete)
|
||||
.toHaveBeenCalledWith("http://localhost:3000/api/farmware_envs/all");
|
||||
});
|
||||
|
||||
it("confirmation overridden", async () => {
|
||||
window.confirm = () => false;
|
||||
mockDelete = Promise.resolve();
|
||||
await expect(destroyAll("FarmwareEnv", true)).resolves.toEqual(undefined);
|
||||
expect(axios.delete)
|
||||
.toHaveBeenCalledWith("http://localhost:3000/api/farmware_envs/all");
|
||||
});
|
||||
|
||||
it("cancelled", async () => {
|
||||
window.confirm = () => false;
|
||||
mockDelete = Promise.resolve();
|
||||
await expect(destroyAll("FarmwareEnv"))
|
||||
.rejects.toEqual("User pressed cancel");
|
||||
expect(axios.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejected", async () => {
|
||||
window.confirm = () => true;
|
||||
mockDelete = Promise.reject("error");
|
||||
await expect(destroyAll("FarmwareEnv")).rejects.toEqual("error");
|
||||
expect(axios.delete)
|
||||
.toHaveBeenCalledWith("http://localhost:3000/api/farmware_envs/all");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -171,7 +171,7 @@ export const destroyCatch = (p: DestroyNoProps) => (err: UnsafeError) => {
|
|||
export function destroy(uuid: string, force = false) {
|
||||
return function (dispatch: Function, getState: GetState) {
|
||||
const resource = findByUuid(getState().resources.index, uuid);
|
||||
const maybeProceed = confirmationChecker(resource, force);
|
||||
const maybeProceed = confirmationChecker(resource.kind, force);
|
||||
return maybeProceed(() => {
|
||||
const statusBeforeError = resource.specialStatus;
|
||||
if (resource.body.id) {
|
||||
|
@ -190,6 +190,14 @@ export function destroy(uuid: string, force = false) {
|
|||
};
|
||||
}
|
||||
|
||||
export function destroyAll(resourceName: ResourceName, force = false) {
|
||||
if (force || confirm(t("Are you sure you want to delete all items?"))) {
|
||||
return axios.delete(urlFor(resourceName) + "all");
|
||||
} else {
|
||||
return Promise.reject("User pressed cancel");
|
||||
}
|
||||
}
|
||||
|
||||
export function saveAll(input: TaggedResource[],
|
||||
callback: () => void = _.noop,
|
||||
errBack: (err: UnsafeError) => void = _.noop) {
|
||||
|
@ -282,9 +290,9 @@ const MUST_CONFIRM_LIST: ResourceName[] = [
|
|||
"SavedGarden",
|
||||
];
|
||||
|
||||
const confirmationChecker = (resource: TaggedResource, force = false) =>
|
||||
const confirmationChecker = (resourceName: ResourceName, force = false) =>
|
||||
<T>(proceed: () => T): T | undefined => {
|
||||
if (MUST_CONFIRM_LIST.includes(resource.kind)) {
|
||||
if (MUST_CONFIRM_LIST.includes(resourceName)) {
|
||||
if (force || confirm(t("Are you sure you want to delete this item?"))) {
|
||||
return proceed();
|
||||
} else {
|
||||
|
|
|
@ -757,6 +757,12 @@ ul {
|
|||
float: right !important;
|
||||
}
|
||||
|
||||
.farmware-settings-menu-contents {
|
||||
label {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.logs {
|
||||
.row {
|
||||
@media screen and (max-width: 974px) {
|
||||
|
|
|
@ -5,30 +5,33 @@ const mockDevice = {
|
|||
execScript: jest.fn(() => Promise.resolve()),
|
||||
installFirstPartyFarmware: jest.fn(() => Promise.resolve())
|
||||
};
|
||||
|
||||
jest.mock("../../device", () => ({
|
||||
getDevice: () => (mockDevice)
|
||||
}));
|
||||
jest.mock("../../device", () => ({ getDevice: () => mockDevice }));
|
||||
|
||||
jest.mock("../../config_storage/actions", () => ({
|
||||
toggleWebAppBool: jest.fn()
|
||||
}));
|
||||
|
||||
let mockDestroyAllPromise: Promise<void | never> = Promise.reject("error");
|
||||
jest.mock("../../api/crud", () => ({
|
||||
destroyAll: jest.fn(() => mockDestroyAllPromise)
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { FarmwareConfigMenu } from "../farmware_config_menu";
|
||||
import { FarmwareConfigMenuProps } from "../interfaces";
|
||||
import { getDevice } from "../../device";
|
||||
import { toggleWebAppBool } from "../../config_storage/actions";
|
||||
import { destroyAll } from "../../api/crud";
|
||||
import { success, error } from "farmbot-toastr";
|
||||
|
||||
describe("<FarmwareConfigMenu />", () => {
|
||||
function fakeProps(): FarmwareConfigMenuProps {
|
||||
return {
|
||||
show: true,
|
||||
dispatch: jest.fn(),
|
||||
firstPartyFwsInstalled: false
|
||||
};
|
||||
}
|
||||
const fakeProps = (): FarmwareConfigMenuProps => ({
|
||||
show: true,
|
||||
dispatch: jest.fn(),
|
||||
firstPartyFwsInstalled: false,
|
||||
shouldDisplay: () => false,
|
||||
});
|
||||
|
||||
it("calls install 1st party farmwares", () => {
|
||||
const wrapper = mount(<FarmwareConfigMenu {...fakeProps()} />);
|
||||
|
@ -40,8 +43,7 @@ describe("<FarmwareConfigMenu />", () => {
|
|||
it("1st party farmwares all installed", () => {
|
||||
const p = fakeProps();
|
||||
p.firstPartyFwsInstalled = true;
|
||||
const wrapper = mount(
|
||||
<FarmwareConfigMenu {...p} />);
|
||||
const wrapper = mount(<FarmwareConfigMenu {...p} />);
|
||||
const button = wrapper.find("button").first();
|
||||
expect(button.hasClass("fa-download")).toBeTruthy();
|
||||
button.simulate("click");
|
||||
|
@ -49,8 +51,7 @@ describe("<FarmwareConfigMenu />", () => {
|
|||
});
|
||||
|
||||
it("toggles 1st party farmware display", () => {
|
||||
const wrapper = mount(
|
||||
<FarmwareConfigMenu {...fakeProps()} />);
|
||||
const wrapper = mount(<FarmwareConfigMenu {...fakeProps()} />);
|
||||
const button = wrapper.find("button").last();
|
||||
expect(button.hasClass("green")).toBeTruthy();
|
||||
expect(button.hasClass("fb-toggle-button")).toBeTruthy();
|
||||
|
@ -61,9 +62,28 @@ describe("<FarmwareConfigMenu />", () => {
|
|||
it("1st party farmware display is disabled", () => {
|
||||
const p = fakeProps();
|
||||
p.show = false;
|
||||
const wrapper = mount(
|
||||
<FarmwareConfigMenu {...p} />);
|
||||
const wrapper = mount(<FarmwareConfigMenu {...p} />);
|
||||
const button = wrapper.find("button").last();
|
||||
expect(button.hasClass("red")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("destroys all FarmwareEnvs", async () => {
|
||||
mockDestroyAllPromise = Promise.resolve();
|
||||
const p = fakeProps();
|
||||
p.shouldDisplay = () => true;
|
||||
const wrapper = mount(<FarmwareConfigMenu {...p} />);
|
||||
wrapper.find("button").last().simulate("click");
|
||||
await expect(destroyAll).toHaveBeenCalledWith("FarmwareEnv");
|
||||
expect(success).toHaveBeenCalledWith(expect.stringContaining("deleted"));
|
||||
});
|
||||
|
||||
it("fails to destroy all FarmwareEnvs", async () => {
|
||||
mockDestroyAllPromise = Promise.reject("error");
|
||||
const p = fakeProps();
|
||||
p.shouldDisplay = () => true;
|
||||
const wrapper = mount(<FarmwareConfigMenu {...p} />);
|
||||
await wrapper.find("button").last().simulate("click");
|
||||
await expect(destroyAll).toHaveBeenCalledWith("FarmwareEnv");
|
||||
expect(error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,6 +21,7 @@ describe("<FarmwareList />", () => {
|
|||
farmwares: fakeFarmwares(),
|
||||
showFirstParty: false,
|
||||
firstPartyFarmwareNames: [],
|
||||
shouldDisplay: () => false,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -4,11 +4,14 @@ import { getDevice } from "../device";
|
|||
import { FarmwareConfigMenuProps } from "./interfaces";
|
||||
import { commandErr } from "../devices/actions";
|
||||
import { toggleWebAppBool } from "../config_storage/actions";
|
||||
import { destroyAll } from "../api/crud";
|
||||
import { success, error } from "farmbot-toastr";
|
||||
import { Feature } from "../devices/interfaces";
|
||||
|
||||
/** First-party Farmware settings. */
|
||||
export function FarmwareConfigMenu(props: FarmwareConfigMenuProps) {
|
||||
const listBtnColor = props.show ? "green" : "red";
|
||||
return <div>
|
||||
return <div className="farmware-settings-menu-contents">
|
||||
<label>
|
||||
{t("First-party Farmware")}
|
||||
</label>
|
||||
|
@ -33,5 +36,16 @@ export function FarmwareConfigMenu(props: FarmwareConfigMenuProps) {
|
|||
onClick={() =>
|
||||
props.dispatch(toggleWebAppBool("show_first_party_farmware"))} />
|
||||
</fieldset>
|
||||
{props.shouldDisplay(Feature.api_farmware_env) &&
|
||||
<fieldset>
|
||||
<label>
|
||||
{t("Delete all Farmware data")}
|
||||
</label>
|
||||
<button
|
||||
className={"fb-button red fa fa-trash"}
|
||||
onClick={() => destroyAll("FarmwareEnv")
|
||||
.then(() => success(t("Farmware data successfully deleted.")))
|
||||
.catch(() => error(t("Error deleting Farmware data")))} />
|
||||
</fieldset>}
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { FarmwareConfigMenu } from "./farmware_config_menu";
|
|||
import { every, Dictionary } from "lodash";
|
||||
import { Popover, Position } from "@blueprintjs/core";
|
||||
import { Link } from "../link";
|
||||
import { ShouldDisplay } from "../devices/interfaces";
|
||||
|
||||
const DISPLAY_NAMES: Dictionary<string> = {
|
||||
"Photos": t("Photos"),
|
||||
|
@ -45,6 +46,7 @@ export interface FarmwareListProps {
|
|||
farmwares: Farmwares;
|
||||
showFirstParty: boolean;
|
||||
firstPartyFarmwareNames: string[];
|
||||
shouldDisplay: ShouldDisplay;
|
||||
}
|
||||
|
||||
interface FarmwareListState {
|
||||
|
@ -94,6 +96,7 @@ export class FarmwareList
|
|||
<i className="fa fa-gear dark" />
|
||||
<FarmwareConfigMenu
|
||||
show={this.props.showFirstParty}
|
||||
shouldDisplay={this.props.shouldDisplay}
|
||||
dispatch={this.props.dispatch}
|
||||
firstPartyFwsInstalled={
|
||||
this.firstPartyFarmwaresPresent(
|
||||
|
|
|
@ -159,6 +159,7 @@ export class FarmwarePage extends React.Component<FarmwareProps, {}> {
|
|||
<FarmwareList
|
||||
current={this.current}
|
||||
dispatch={this.props.dispatch}
|
||||
shouldDisplay={this.props.shouldDisplay}
|
||||
farmwares={this.props.farmwares}
|
||||
firstPartyFarmwareNames={this.props.firstPartyFarmwareNames}
|
||||
showFirstParty={!!this.props.webAppConfig.show_first_party_farmware} />
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Dictionary, FarmwareManifest, SyncStatus } from "farmbot/dist";
|
||||
import { NetworkState } from "../connectivity/interfaces";
|
||||
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
|
||||
import { ShouldDisplay } from "../devices/interfaces";
|
||||
|
||||
export interface FWState {
|
||||
selectedFarmware: string | undefined;
|
||||
|
@ -28,6 +29,7 @@ export interface FarmwareConfigMenuProps {
|
|||
show: boolean | undefined;
|
||||
dispatch: Function;
|
||||
firstPartyFwsInstalled: boolean;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
}
|
||||
|
||||
export type Farmwares = Dictionary<FarmwareManifest | undefined>;
|
||||
|
|
Loading…
Reference in New Issue