Farmbot-Web-App/frontend/api/crud.ts

343 lines
11 KiB
TypeScript
Raw Normal View History

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";
import axios from "axios";
import {
updateNO, destroyOK, destroyNO, GeneralizedError, saveOK
} 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";
import { Actions } from "../constants";
2017-11-21 07:55:51 -07:00
import { maybeStartTracking } from "./maybe_start_tracking";
import { newTaggedResource } from "../sync/actions";
import { arrayUnwrap } from "../resources/util";
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";
2019-08-01 07:01:15 -06:00
import { appIsReadonly } from "../read_only_mode/app_is_read_only";
2017-06-29 12:54:02 -06:00
export function edit(tr: TaggedResource, changes: Partial<typeof tr.body>):
2017-06-29 12:54:02 -06:00
ReduxAction<EditResourceParams> {
return {
type: Actions.EDIT_RESOURCE,
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"],
specialStatus = SpecialStatus.DIRTY):
2017-06-29 12:54:02 -06:00
ReduxAction<EditResourceParams> {
return {
type: Actions.OVERWRITE_RESOURCE,
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. */
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> {
const resource = arrayUnwrap(newTaggedResource(kind, body));
resource.specialStatus = SpecialStatus[clean ? "SAVED" : "DIRTY"];
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);
});
};
export function initSave<T extends TaggedResource>(kind: T["kind"],
body: T["body"]) {
2018-10-31 17:49:05 -06:00
return function (dispatch: Function) {
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-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);
const oldStatus = resource.specialStatus;
2017-08-30 15:17:52 -06:00
dispatch({ type: Actions.SAVE_RESOURCE_START, payload: resource });
return dispatch(update(uuid, oldStatus));
};
2017-06-29 12:54:02 -06:00
}
export function refresh(resource: TaggedResource, urlNeedsId = false) {
return function (dispatch: Function) {
dispatch(refreshStart(resource.uuid));
const endPart = "" + urlNeedsId ? resource.body.id : "";
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 {
const action = refreshNO({
err: { message: "Unable to refresh" },
uuid: resource.uuid,
statusBeforeError
});
dispatch(action);
2017-08-30 15:17:52 -06:00
}
});
};
}
export function refreshStart(uuid: string): ReduxAction<string> {
return { type: Actions.REFRESH_RESOURCE_START, payload: uuid };
}
export function refreshOK(payload: TaggedResource): ReduxAction<TaggedResource> {
return { type: Actions.REFRESH_RESOURCE_OK, payload };
}
export function refreshNO(payload: GeneralizedError): ReduxAction<GeneralizedError> {
return { type: Actions.REFRESH_RESOURCE_NO, payload };
}
2017-08-30 15:17:52 -06: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) {
const { index } = getState().resources;
const payl: AjaxUpdatePayload = { index, uuid, dispatch, statusBeforeError };
return updateViaAjax(payl);
};
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);
};
2019-08-01 10:55:25 -06:00
/** We need this to detect read-only deletion attempts */
function destroyStart() {
return { type: Actions.DESTROY_RESOURCE_START, payload: {} };
}
export function destroy(uuid: string, force = false) {
2017-06-29 12:54:02 -06:00
return function (dispatch: Function, getState: GetState) {
2019-08-01 10:55:25 -06:00
dispatch(destroyStart());
2019-07-30 06:56:04 -06:00
/** Stop user from deleting resources if app is read only. */
if (appIsReadonly(getState().resources.index)) {
return Promise.reject("Application is in read-only mode.");
}
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(() => {
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);
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 {
dispatch(destroyOK(resource));
2017-06-29 12:54:02 -06:00
return Promise.resolve("");
}
}) || Promise.reject("User pressed cancel");
};
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) {
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-29 12:54:02 -06:00
}
export function urlFor(tag: ResourceName) {
const OPTIONS: Partial<Record<ResourceName, string>> = {
2019-08-05 15:45:19 -06:00
Alert: API.current.alertPath,
Device: API.current.devicePath,
DiagnosticDump: API.current.diagnosticDumpsPath,
FarmEvent: API.current.farmEventsPath,
2019-08-05 15:45:19 -06:00
FarmwareEnv: API.current.farmwareEnvPath,
FarmwareInstallation: API.current.farmwareInstallationPath,
FbosConfig: API.current.fbosConfigPath,
FirmwareConfig: API.current.firmwareConfigPath,
Image: API.current.imagesPath,
2019-08-26 12:20:46 -06:00
Log: API.current.logsPath,
Peripheral: API.current.peripheralsPath,
2018-03-13 18:34:30 -06:00
PinBinding: API.current.pinBindingPath,
2019-08-05 15:45:19 -06:00
PlantTemplate: API.current.plantTemplatePath,
Point: API.current.pointsPath,
2019-08-05 15:45:19 -06:00
PointGroup: API.current.pointGroupsPath,
Regimen: API.current.regimensPath,
SavedGarden: API.current.savedGardensPath,
Sensor: API.current.sensorPath,
Sequence: API.current.sequencesPath,
Tool: API.current.toolsPath,
User: API.current.usersPath,
2018-03-08 21:03:02 -07:00
WebAppConfig: API.current.webAppConfigPath,
2019-08-05 15:45:19 -06:00
WebcamFeed: API.current.webcamFeedPath,
};
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"];
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) {
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)) {
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)) {
dispatch(saveOK(newTR));
2017-06-29 12:54:02 -06:00
} else {
throw new Error("Just saved a malformed TR.");
}
})
.catch(function (err: UnsafeError) {
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
];
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();
};