Farmbot-Web-App/frontend/util/util.ts

236 lines
6.6 KiB
TypeScript

import { ResourceColor, TimeSettings } from "../interfaces";
import { box } from "boxed_value";
import {
TaggedResource,
TaggedFirmwareConfig,
TaggedFbosConfig,
Dictionary,
ResourceName,
} from "farmbot";
import { BotLocationData } from "../devices/interfaces";
import {
sample,
padStart,
sortBy,
merge,
isNumber,
isUndefined as lodashIsUndefined
} from "lodash";
import { t } from "../i18next_wrapper";
export const colors: Array<ResourceColor> = [
"blue",
"green",
"yellow",
"orange",
"purple",
"pink",
"gray",
"red"
];
/** Picks a color that is compliant with sequence / regimen color codes */
export function randomColor(): ResourceColor {
return sample(colors) as typeof colors[0];
}
export function defensiveClone<T>(target: T): T {
const jsonString = JSON.stringify(target);
return JSON.parse(jsonString || "null");
}
/** USAGE: DYNAMICALLY plucks `obj[key]`.
* * `undefined` becomes `""`
* * `number` types are coerced to strings (Eg: "5").
* * `boolean` is converted to "true" and "false" (a string).
* * All other types raise a runtime exception (Objects, functions,
* Array, Symbol, etc)
*/
export function safeStringFetch(obj: {}, key: string): string {
const boxed = box((obj as Dictionary<{}>)[key]);
switch (boxed.kind) {
case "undefined":
case "null":
return "";
case "number":
case "string":
return boxed.value.toString();
case "boolean":
return (boxed.value) ? "true" : "false";
default:
const msg = t(`Numbers strings and null only (got ${boxed.kind}).`);
throw new Error(msg);
}
}
/** Fancy debug */
export function fancyDebug<T extends {}>(d: T): T {
console.log(Object
.keys(d)
.map(key => [key, (d as Dictionary<string>)[key]])
.map((x) => {
const key = padStart(x[0], 20, " ");
const val = (JSON.stringify(x[1]) || "Nothing").slice(0, 52);
return `${key} => ${val}`;
})
.join("\n"));
return d;
}
export type CowardlyDictionary<T> = Dictionary<T | undefined>;
/** Sometimes, you are forced to pass a number type even though
* the resource has no ID (usually for rendering purposes).
* Example:
* farmEvent.id || 0
*
* In those cases, you can use this constant to indicate intent.
*/
export const NOT_SAVED = -1;
export function isUndefined(x: object | undefined): x is undefined {
return lodashIsUndefined(x);
}
/** Better than Array.proto.filter and compact() because the type checker
* knows what's going on.
*/
export function betterCompact<T>(input: (T | undefined)[]): T[] {
const output: T[] = [];
input.forEach(x => x ? output.push(x) : "");
return output;
}
/** Sorts a list of tagged resources. Unsaved resource get put on the end. */
export function sortResourcesById<T extends TaggedResource>(input: T[]): T[] {
return sortBy(input, (x) => x.body.id || Infinity);
}
/**
* Light wrapper around merge() to prevent common type errors / mistakes.
*
* NOTE: If you rely solely on `betterMerge()` to combine array-bearing
* CeleryScript nodes, the API will reject them because they contain
* extra properties. The CS Corpus does not allow extra nodes for
* safety reasons.
*/
export function betterMerge<T, U>(target: T, update: U): T & U {
return merge({}, target, update);
}
/** Like parseFloat, but allows you to control fallback value instead of
* returning NaN. */
export function betterParseNum(num: string | undefined,
fallback: number): number {
try {
const maybe = JSON.parse("" + num);
if (isNumber(maybe) && !isNaN(maybe)) {
return maybe;
}
} catch (_) {
}
return fallback;
}
/** Determine if a string contains one of multiple values. */
export function oneOf(list: string[], target: string) {
let matches = 0;
list.map(x => target.includes(x) ? matches++ : "");
return !!matches;
}
export type Primitive = boolean | string | number;
export function shortRevision() {
return (globalConfig.SHORT_REVISION || "NONE").slice(0, 8);
}
export * from "./urls";
export const trim = (i: string): string => i.replace(/\s+/g, " ");
/** When you have a ridiculously long chain of flags and need to convert it
* into a binary integer. */
export function bitArray(...values: boolean[]) {
return values
.map((x): number => x ? 1 : 0)
.reduce((res, x) => {
// tslint:disable-next-line:no-bitwise
return res << 1 | x;
});
}
/** Performs deep object comparison. ONLY WORKS ON JSON-y DATA TYPES. */
export const equals = <T>(a: T, b: T): boolean => {
// Some benchmarks claim that this is slower than `_.isEqual`.
// For whatever reason, this is not true for our application.
return JSON.stringify(a) === JSON.stringify(b);
};
export const timestamp = (date = new Date()) => Math.round(date.getTime());
/** Used to scroll to the bottom of a sequence after adding a step. */
export function scrollToBottom(elementId: string) {
const elToScroll = document.getElementById(elementId);
if (!elToScroll) { return; }
// Wait for the new element height and scroll to the bottom.
setTimeout(() => elToScroll.scrollTop = elToScroll.scrollHeight, 1);
}
export function validBotLocationData(
botLocationData: BotLocationData | undefined): BotLocationData {
return betterMerge({
position: { x: undefined, y: undefined, z: undefined },
scaled_encoders: { x: undefined, y: undefined, z: undefined },
raw_encoders: { x: undefined, y: undefined, z: undefined },
}, botLocationData);
}
/**
* Return FirmwareConfig if the data is valid.
*/
export function validFwConfig(config: TaggedFirmwareConfig | undefined):
TaggedFirmwareConfig["body"] | undefined {
return (config && config.body.api_migrated)
? config.body
: undefined;
}
/**
* Return FbosConfig if the data is valid.
*/
export function validFbosConfig(
config: TaggedFbosConfig | undefined): TaggedFbosConfig["body"] | undefined {
return (config && config.body.api_migrated)
? config.body
: undefined;
}
interface BetterUUID {
kind: ResourceName;
localId: number;
remoteId?: number;
}
export function unpackUUID(uuid: string): BetterUUID {
const [kind, remoteId, localId] = uuid.split(".");
const id = parseInt(remoteId, 10);
return {
kind: (kind as ResourceName),
localId: parseInt(localId, 10),
remoteId: id > 0 ? id : undefined
};
}
/**
* Integer parsed from float
* since number type inputs allow floating point notation.
*/
export const parseIntInput = (input: string): number => {
const int = parseInt("" + parseFloat(input).toFixed(1), 10);
return int === 0 ? 0 : int;
};
export const timeFormatString =
(timeSettings: TimeSettings | undefined): string =>
(timeSettings && timeSettings.hour24) ? "H:mm" : "h:mma";