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, } from "lodash"; import { t } from "../i18next_wrapper"; export const colors: Array = [ "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(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(d: T): T { console.log(Object .keys(d) .map(key => [key, (d as Dictionary)[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 = Dictionary; /** 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; /** Better than Array.proto.filter and compact() because the type checker * knows what's going on. */ export function betterCompact(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(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(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 = (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); }; /** 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 : undefined; } /** * Return FbosConfig if the data is valid. */ export function validFbosConfig( config: TaggedFbosConfig | undefined): TaggedFbosConfig["body"] | undefined { return config ? 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?.hour24) ? "H:mm" : "h:mma";