2017-06-29 12:54:02 -06:00
|
|
|
import {
|
2018-08-01 18:20:50 -06:00
|
|
|
TaggedResource, SpecialStatus, ResourceName, TaggedSequence
|
2018-08-01 07:03:35 -06:00
|
|
|
} from "farmbot";
|
|
|
|
import {
|
2017-06-29 12:54:02 -06:00
|
|
|
isTaggedResource,
|
|
|
|
} from "../resources/tagged_resources";
|
|
|
|
import { GetState, ReduxAction } from "../redux/interfaces";
|
|
|
|
import { API } from "./index";
|
2017-07-05 14:33:06 -06:00
|
|
|
import axios from "axios";
|
2018-01-20 07:46:44 -07:00
|
|
|
import {
|
2018-10-29 06:52:32 -06:00
|
|
|
updateNO, destroyOK, destroyNO, GeneralizedError, saveOK
|
2018-01-20 07:46:44 -07:00
|
|
|
} from "../resources/actions";
|
2017-06-29 12:54:02 -06:00
|
|
|
import { UnsafeError } from "../interfaces";
|
2018-09-12 15:09:40 -06:00
|
|
|
import { defensiveClone, unpackUUID } from "../util";
|
2017-06-29 12:54:02 -06:00
|
|
|
import { EditResourceParams } from "./interfaces";
|
|
|
|
import { ResourceIndex } from "../resources/interfaces";
|
|
|
|
import { SequenceBodyItem } from "farmbot/dist";
|
2017-07-07 07:12:14 -06:00
|
|
|
import { Actions } from "../constants";
|
2017-11-21 07:55:51 -07:00
|
|
|
import { maybeStartTracking } from "./maybe_start_tracking";
|
2019-04-02 13:59:37 -06:00
|
|
|
|
2018-10-31 13:10:36 -06:00
|
|
|
import { newTaggedResource } from "../sync/actions";
|
|
|
|
import { arrayUnwrap } from "../resources/util";
|
2018-11-08 09:41:09 -07:00
|
|
|
import { findByUuid } from "../resources/reducer_support";
|
2019-02-04 07:32:26 -07:00
|
|
|
import { assign, noop } from "lodash";
|
2019-04-02 13:59:37 -06:00
|
|
|
import { t } from "../i18next_wrapper";
|
2017-06-29 12:54:02 -06:00
|
|
|
|
2017-07-07 07:12:14 -06:00
|
|
|
export function edit(tr: TaggedResource, changes: Partial<typeof tr.body>):
|
2017-06-29 12:54:02 -06:00
|
|
|
ReduxAction<EditResourceParams> {
|
|
|
|
return {
|
2017-07-07 07:12:14 -06:00
|
|
|
type: Actions.EDIT_RESOURCE,
|
2017-11-06 16:54:52 -07:00
|
|
|
payload: {
|
|
|
|
uuid: tr.uuid,
|
|
|
|
update: changes,
|
|
|
|
specialStatus: SpecialStatus.DIRTY
|
|
|
|
}
|
2017-06-29 12:54:02 -06:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Rather than update (patch) a TaggedResource, this method will overwrite
|
|
|
|
* everything within the `.body` property. */
|
2018-11-12 10:24:37 -07:00
|
|
|
export function overwrite<T extends TaggedResource>(tr: T,
|
2019-01-10 21:36:06 -07:00
|
|
|
changeset: T["body"],
|
2017-11-07 00:15:00 -07:00
|
|
|
specialStatus = SpecialStatus.DIRTY):
|
2017-06-29 12:54:02 -06:00
|
|
|
ReduxAction<EditResourceParams> {
|
|
|
|
return {
|
2017-07-07 07:12:14 -06:00
|
|
|
type: Actions.OVERWRITE_RESOURCE,
|
2017-11-06 16:54:52 -07:00
|
|
|
payload: { uuid: tr.uuid, update: changeset, specialStatus }
|
2017-06-29 12:54:02 -06:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2018-02-12 15:13:17 -07:00
|
|
|
export interface EditStepProps {
|
2017-06-29 12:54:02 -06:00
|
|
|
step: Readonly<SequenceBodyItem>;
|
|
|
|
sequence: Readonly<TaggedSequence>;
|
|
|
|
index: number;
|
|
|
|
/** Callback provides a fresh, defensively cloned copy of the
|
|
|
|
* original step. Perform modifications to the resource within this
|
|
|
|
* callback */
|
|
|
|
executor(stepCopy: SequenceBodyItem): void;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Editing sequence steps is a tedious process. Use this function in place
|
|
|
|
* of `edit()` or `overwrite`. */
|
|
|
|
export function editStep({ step, sequence, index, executor }: EditStepProps) {
|
|
|
|
// https://en.wikipedia.org/wiki/NeXTSTEP
|
2017-08-28 05:49:13 -06:00
|
|
|
const nextStep = defensiveClone(step);
|
2018-02-13 07:35:05 -07:00
|
|
|
const nextSeq = defensiveClone(sequence);
|
2017-06-29 12:54:02 -06:00
|
|
|
// Let the developer safely perform mutations here:
|
|
|
|
executor(nextStep);
|
|
|
|
nextSeq.body.body = nextSeq.body.body || [];
|
|
|
|
nextSeq.body.body[index] = nextStep;
|
|
|
|
return overwrite(sequence, nextSeq.body);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Initialize (but don't save) an indexed / tagged resource. */
|
2018-10-31 13:10:36 -06:00
|
|
|
export function init<T extends TaggedResource>(kind: T["kind"],
|
|
|
|
body: T["body"],
|
2017-10-30 07:51:21 -06:00
|
|
|
/** Set to "true" when you want an `undefined` SpecialStatus. */
|
|
|
|
clean = false): ReduxAction<TaggedResource> {
|
2018-10-31 13:10:36 -06:00
|
|
|
const resource = arrayUnwrap(newTaggedResource(kind, body));
|
2017-11-06 16:54:52 -07:00
|
|
|
resource.specialStatus = SpecialStatus[clean ? "SAVED" : "DIRTY"];
|
2017-07-07 07:12:14 -06:00
|
|
|
return { type: Actions.INIT_RESOURCE, payload: resource };
|
2017-06-29 12:54:02 -06:00
|
|
|
}
|
|
|
|
|
2018-11-14 17:36:52 -07:00
|
|
|
/** Initialize and save a new resource, returning the `id`.
|
|
|
|
* If you don't need the `id` returned, use `initSave` instead.
|
|
|
|
*/
|
|
|
|
export const initSaveGetId =
|
|
|
|
<T extends TaggedResource>(kind: T["kind"], body: T["body"]) =>
|
|
|
|
(dispatch: Function) => {
|
|
|
|
const resource = arrayUnwrap(newTaggedResource(kind, body));
|
|
|
|
resource.specialStatus = SpecialStatus.DIRTY;
|
|
|
|
dispatch({ type: Actions.INIT_RESOURCE, payload: resource });
|
|
|
|
dispatch({ type: Actions.SAVE_RESOURCE_START, payload: resource });
|
|
|
|
maybeStartTracking(resource.uuid);
|
|
|
|
return axios.post<typeof resource.body>(
|
|
|
|
urlFor(resource.kind), resource.body)
|
|
|
|
.then(resp => {
|
|
|
|
dispatch(saveOK(resource));
|
|
|
|
return resp.data.id;
|
|
|
|
})
|
|
|
|
.catch((err: UnsafeError) => {
|
|
|
|
dispatch(updateNO({
|
|
|
|
err,
|
|
|
|
uuid: resource.uuid,
|
|
|
|
statusBeforeError: resource.specialStatus
|
|
|
|
}));
|
|
|
|
return Promise.reject(err);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2018-10-31 13:10:36 -06:00
|
|
|
export function initSave<T extends TaggedResource>(kind: T["kind"],
|
|
|
|
body: T["body"]) {
|
2018-10-31 17:49:05 -06:00
|
|
|
return function (dispatch: Function) {
|
2018-10-31 13:10:36 -06:00
|
|
|
const action = init(kind, body);
|
2017-06-29 12:54:02 -06:00
|
|
|
dispatch(action);
|
2018-10-31 12:12:45 -06:00
|
|
|
return dispatch(save(action.payload.uuid));
|
2017-06-30 14:24:09 -06:00
|
|
|
};
|
2017-06-29 12:54:02 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
export function save(uuid: string) {
|
|
|
|
return function (dispatch: Function, getState: GetState) {
|
2017-08-28 05:49:13 -06:00
|
|
|
const resource = findByUuid(getState().resources.index, uuid);
|
2017-12-26 09:45:08 -07:00
|
|
|
const oldStatus = resource.specialStatus;
|
2017-08-30 15:17:52 -06:00
|
|
|
dispatch({ type: Actions.SAVE_RESOURCE_START, payload: resource });
|
2017-12-26 09:45:08 -07:00
|
|
|
return dispatch(update(uuid, oldStatus));
|
2017-06-30 14:24:09 -06:00
|
|
|
};
|
2017-06-29 12:54:02 -06:00
|
|
|
}
|
|
|
|
|
2017-08-31 09:40:57 -06:00
|
|
|
export function refresh(resource: TaggedResource, urlNeedsId = false) {
|
2017-08-31 09:15:15 -06:00
|
|
|
return function (dispatch: Function) {
|
|
|
|
dispatch(refreshStart(resource.uuid));
|
2017-08-31 09:40:57 -06:00
|
|
|
const endPart = "" + urlNeedsId ? resource.body.id : "";
|
2017-12-26 08:13:50 -07:00
|
|
|
const statusBeforeError = resource.specialStatus;
|
2017-08-30 15:17:52 -06:00
|
|
|
axios
|
2018-03-09 22:17:16 -07:00
|
|
|
.get<typeof resource.body>(urlFor(resource.kind) + endPart)
|
|
|
|
.then(resp => {
|
2017-08-30 15:17:52 -06:00
|
|
|
const r1 = defensiveClone(resource);
|
|
|
|
const r2 = { body: defensiveClone(resp.data) };
|
2019-02-04 07:32:26 -07:00
|
|
|
const newTR = assign({}, r1, r2);
|
2017-08-30 15:17:52 -06:00
|
|
|
if (isTaggedResource(newTR)) {
|
|
|
|
dispatch(refreshOK(newTR));
|
|
|
|
} else {
|
2017-12-26 08:13:50 -07:00
|
|
|
const action = refreshNO({
|
|
|
|
err: { message: "Unable to refresh" },
|
|
|
|
uuid: resource.uuid,
|
|
|
|
statusBeforeError
|
|
|
|
});
|
2017-09-29 15:00:54 -06:00
|
|
|
dispatch(action);
|
2017-08-30 15:17:52 -06:00
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-08-31 10:47:12 -06:00
|
|
|
export function refreshStart(uuid: string): ReduxAction<string> {
|
2017-08-31 09:15:15 -06:00
|
|
|
return { type: Actions.REFRESH_RESOURCE_START, payload: uuid };
|
|
|
|
}
|
|
|
|
|
2017-08-31 10:47:12 -06:00
|
|
|
export function refreshOK(payload: TaggedResource): ReduxAction<TaggedResource> {
|
2017-08-31 07:20:44 -06:00
|
|
|
return { type: Actions.REFRESH_RESOURCE_OK, payload };
|
|
|
|
}
|
|
|
|
|
2017-08-31 10:47:12 -06:00
|
|
|
export function refreshNO(payload: GeneralizedError): ReduxAction<GeneralizedError> {
|
2017-08-31 07:20:44 -06:00
|
|
|
return { type: Actions.REFRESH_RESOURCE_NO, payload };
|
|
|
|
}
|
2017-08-30 15:17:52 -06:00
|
|
|
|
2017-12-26 09:45:08 -07:00
|
|
|
interface AjaxUpdatePayload {
|
|
|
|
index: ResourceIndex;
|
|
|
|
uuid: string;
|
|
|
|
dispatch: Function;
|
|
|
|
statusBeforeError: SpecialStatus;
|
|
|
|
}
|
|
|
|
|
|
|
|
function update(uuid: string, statusBeforeError: SpecialStatus) {
|
2017-06-29 12:54:02 -06:00
|
|
|
return function (dispatch: Function, getState: GetState) {
|
2017-12-26 09:45:08 -07:00
|
|
|
const { index } = getState().resources;
|
|
|
|
const payl: AjaxUpdatePayload = { index, uuid, dispatch, statusBeforeError };
|
|
|
|
return updateViaAjax(payl);
|
2017-06-30 14:24:09 -06:00
|
|
|
};
|
2017-06-29 12:54:02 -06:00
|
|
|
}
|
|
|
|
|
2017-12-29 16:41:45 -07:00
|
|
|
interface DestroyNoProps {
|
|
|
|
uuid: string;
|
|
|
|
statusBeforeError: SpecialStatus;
|
|
|
|
dispatch: Function;
|
|
|
|
}
|
|
|
|
|
|
|
|
export const destroyCatch = (p: DestroyNoProps) => (err: UnsafeError) => {
|
|
|
|
p.dispatch(destroyNO({
|
|
|
|
err,
|
|
|
|
uuid: p.uuid,
|
|
|
|
statusBeforeError: p.statusBeforeError
|
|
|
|
}));
|
|
|
|
return Promise.reject(err);
|
|
|
|
};
|
|
|
|
|
2017-09-19 16:02:01 -06:00
|
|
|
export function destroy(uuid: string, force = false) {
|
2017-06-29 12:54:02 -06:00
|
|
|
return function (dispatch: Function, getState: GetState) {
|
2017-08-28 05:49:13 -06:00
|
|
|
const resource = findByUuid(getState().resources.index, uuid);
|
2018-11-02 13:53:17 -06:00
|
|
|
const maybeProceed = confirmationChecker(resource.kind, force);
|
2017-06-29 12:54:02 -06:00
|
|
|
return maybeProceed(() => {
|
2017-12-26 08:13:50 -07:00
|
|
|
const statusBeforeError = resource.specialStatus;
|
2017-06-29 12:54:02 -06:00
|
|
|
if (resource.body.id) {
|
2017-11-20 12:44:46 -07:00
|
|
|
maybeStartTracking(uuid);
|
2017-07-05 14:33:06 -06:00
|
|
|
return axios
|
|
|
|
.delete(urlFor(resource.kind) + resource.body.id)
|
2018-03-09 22:17:16 -07:00
|
|
|
.then(function () {
|
2017-06-29 12:54:02 -06:00
|
|
|
dispatch(destroyOK(resource));
|
|
|
|
})
|
2017-12-29 16:41:45 -07:00
|
|
|
.catch(destroyCatch({ dispatch, uuid, statusBeforeError }));
|
2017-06-29 12:54:02 -06:00
|
|
|
} else {
|
2017-06-30 14:24:09 -06:00
|
|
|
dispatch(destroyOK(resource));
|
2017-06-29 12:54:02 -06:00
|
|
|
return Promise.resolve("");
|
|
|
|
}
|
|
|
|
}) || Promise.reject("User pressed cancel");
|
2017-06-30 14:24:09 -06:00
|
|
|
};
|
2017-06-29 12:54:02 -06:00
|
|
|
}
|
|
|
|
|
2018-11-02 13:53:17 -06:00
|
|
|
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");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-29 12:54:02 -06:00
|
|
|
export function saveAll(input: TaggedResource[],
|
2019-02-04 07:32:26 -07:00
|
|
|
callback: () => void = noop,
|
|
|
|
errBack: (err: UnsafeError) => void = noop) {
|
2018-03-13 16:37:24 -06:00
|
|
|
return function (dispatch: Function) {
|
2017-08-28 05:49:13 -06:00
|
|
|
const p = input
|
2017-08-15 14:21:41 -06:00
|
|
|
.filter(x => x.specialStatus === SpecialStatus.DIRTY)
|
2017-11-20 12:44:46 -07:00
|
|
|
.map(tts => tts.uuid)
|
|
|
|
.map(uuid => {
|
|
|
|
maybeStartTracking(uuid);
|
|
|
|
return dispatch(save(uuid));
|
|
|
|
});
|
2017-06-29 12:54:02 -06:00
|
|
|
Promise.all(p).then(callback, errBack);
|
2017-06-30 14:24:09 -06:00
|
|
|
};
|
2017-06-29 12:54:02 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
export function urlFor(tag: ResourceName) {
|
|
|
|
const OPTIONS: Partial<Record<ResourceName, string>> = {
|
2017-10-27 08:48:10 -06:00
|
|
|
Sequence: API.current.sequencesPath,
|
|
|
|
Tool: API.current.toolsPath,
|
|
|
|
FarmEvent: API.current.farmEventsPath,
|
|
|
|
Regimen: API.current.regimensPath,
|
|
|
|
Peripheral: API.current.peripheralsPath,
|
2018-03-10 00:17:53 -07:00
|
|
|
Sensor: API.current.sensorPath,
|
2018-03-13 18:34:30 -06:00
|
|
|
PinBinding: API.current.pinBindingPath,
|
2017-10-27 08:48:10 -06:00
|
|
|
Point: API.current.pointsPath,
|
|
|
|
User: API.current.usersPath,
|
|
|
|
Device: API.current.devicePath,
|
|
|
|
Image: API.current.imagesPath,
|
2018-04-03 15:49:21 -06:00
|
|
|
Log: API.current.filteredLogsPath,
|
2018-01-10 15:28:22 -07:00
|
|
|
WebcamFeed: API.current.webcamFeedPath,
|
2018-01-27 02:29:13 -07:00
|
|
|
FbosConfig: API.current.fbosConfigPath,
|
2018-03-08 21:03:02 -07:00
|
|
|
WebAppConfig: API.current.webAppConfigPath,
|
|
|
|
FirmwareConfig: API.current.firmwareConfigPath,
|
2018-07-10 21:46:22 -06:00
|
|
|
DiagnosticDump: API.current.diagnosticDumpsPath,
|
2018-08-01 18:13:44 -06:00
|
|
|
SavedGarden: API.current.savedGardensPath,
|
2018-09-13 16:00:14 -06:00
|
|
|
PlantTemplate: API.current.plantTemplatePath,
|
2018-11-01 11:17:18 -06:00
|
|
|
FarmwareEnv: API.current.farmwareEnvPath,
|
2018-11-05 18:37:09 -07:00
|
|
|
FarmwareInstallation: API.current.farmwareInstallationPath,
|
2017-06-30 14:24:09 -06:00
|
|
|
};
|
2017-08-28 05:49:13 -06:00
|
|
|
const url = OPTIONS[tag];
|
2017-06-29 12:54:02 -06:00
|
|
|
if (url) {
|
|
|
|
return url;
|
|
|
|
} else {
|
|
|
|
throw new Error(`No resource/URL handler for ${tag} yet.
|
|
|
|
Consider adding one to crud.ts`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-08 21:03:02 -07:00
|
|
|
const SINGULAR_RESOURCE: ResourceName[] =
|
|
|
|
["WebAppConfig", "FbosConfig", "FirmwareConfig"];
|
2018-01-10 15:28:22 -07:00
|
|
|
|
2017-06-29 12:54:02 -06:00
|
|
|
/** Shared functionality in create() and update(). */
|
2018-07-20 18:50:37 -06:00
|
|
|
export function updateViaAjax(payl: AjaxUpdatePayload) {
|
2017-12-26 09:45:08 -07:00
|
|
|
const { uuid, statusBeforeError, dispatch, index } = payl;
|
2017-08-28 05:49:13 -06:00
|
|
|
const resource = findByUuid(index, uuid);
|
|
|
|
const { body, kind } = resource;
|
2017-06-29 12:54:02 -06:00
|
|
|
let verb: "post" | "put";
|
|
|
|
let url = urlFor(kind);
|
|
|
|
if (body.id) {
|
|
|
|
verb = "put";
|
2018-09-12 15:09:40 -06:00
|
|
|
if (!SINGULAR_RESOURCE.includes(unpackUUID(payl.uuid).kind)) {
|
2018-01-10 15:28:22 -07:00
|
|
|
url += body.id;
|
|
|
|
}
|
2017-06-29 12:54:02 -06:00
|
|
|
} else {
|
|
|
|
verb = "post";
|
|
|
|
}
|
2017-11-20 12:44:46 -07:00
|
|
|
maybeStartTracking(uuid);
|
2018-03-09 22:17:16 -07:00
|
|
|
return axios[verb]<typeof resource.body>(url, body)
|
|
|
|
.then(function (resp) {
|
2017-08-28 05:49:13 -06:00
|
|
|
const r1 = defensiveClone(resource);
|
|
|
|
const r2 = { body: defensiveClone(resp.data) };
|
2019-02-04 07:32:26 -07:00
|
|
|
const newTR = assign({}, r1, r2);
|
2017-06-29 12:54:02 -06:00
|
|
|
if (isTaggedResource(newTR)) {
|
2018-10-29 06:52:32 -06:00
|
|
|
dispatch(saveOK(newTR));
|
2017-06-29 12:54:02 -06:00
|
|
|
} else {
|
|
|
|
throw new Error("Just saved a malformed TR.");
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch(function (err: UnsafeError) {
|
2017-12-26 08:13:50 -07:00
|
|
|
dispatch(updateNO({ err, uuid, statusBeforeError }));
|
2017-06-29 12:54:02 -06:00
|
|
|
return Promise.reject(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-08-28 05:49:13 -06:00
|
|
|
const MUST_CONFIRM_LIST: ResourceName[] = [
|
2017-10-27 07:31:25 -06:00
|
|
|
"FarmEvent",
|
|
|
|
"Point",
|
|
|
|
"Sequence",
|
|
|
|
"Regimen",
|
2018-09-13 16:00:14 -06:00
|
|
|
"Image",
|
|
|
|
"SavedGarden",
|
2017-07-02 19:08:19 -06:00
|
|
|
];
|
2017-07-05 14:33:06 -06:00
|
|
|
|
2018-11-02 13:53:17 -06:00
|
|
|
const confirmationChecker = (resourceName: ResourceName, force = false) =>
|
2017-06-29 12:54:02 -06:00
|
|
|
<T>(proceed: () => T): T | undefined => {
|
2018-11-02 13:53:17 -06:00
|
|
|
if (MUST_CONFIRM_LIST.includes(resourceName)) {
|
2018-05-06 03:00:02 -06:00
|
|
|
if (force || confirm(t("Are you sure you want to delete this item?"))) {
|
2017-06-29 12:54:02 -06:00
|
|
|
return proceed();
|
|
|
|
} else {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return proceed();
|
2017-06-30 14:24:09 -06:00
|
|
|
};
|