FarmBot/Farmbot-Web-App

View on GitHub
frontend/util/util.ts

Summary

Maintainability
A
1 hr
Test Coverage
import { ResourceColor, TimeSettings } from "../interfaces";
import {
  TaggedResource,
  TaggedFirmwareConfig,
  TaggedFbosConfig,
  ResourceName,
} from "farmbot";
import {
  sortBy,
  merge,
  isNumber,
} from "lodash";
import moment from "moment";
import { BotLocationData } from "../devices/interfaces";

export const colors: Array<ResourceColor> = [
  "blue",
  "green",
  "yellow",
  "orange",
  "purple",
  "pink",
  "gray",
  "red",
];

export function defensiveClone<T>(target: T): T {
  const jsonString = JSON.stringify(target);
  return JSON.parse(jsonString || "null");
}

/** 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;
    }
    // eslint-disable-next-line no-empty
  } 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 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) => {
      // eslint-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);
};

/** 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);
}

/**
 * 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 => {
    const subHour = timeSettings?.seconds ? "mm:ss" : "mm";
    return timeSettings?.hour24 ? `H:${subHour}` : `h:${subHour}a`;
  };

export const formatTime =
  (momentTime: moment.Moment,
    timeSettings: TimeSettings,
    dateFormat = "",
  ) => {
    const datetimeSeparator = dateFormat ? ", " : "";
    const separator = dateFormat.includes("Y") ? " " : datetimeSeparator;
    const timeFormat = timeFormatString(timeSettings);
    return momentTime
      .utcOffset(timeSettings.utcOffset)
      .format(`${dateFormat}${separator}${timeFormat}`);
  };

export const fullLocationData = (botLocationData: BotLocationData | undefined) =>
  betterMerge({
    position: { x: undefined, y: undefined, z: undefined },
    scaled_encoders: { x: undefined, y: undefined, z: undefined },
    raw_encoders: { x: undefined, y: undefined, z: undefined },
    load: { x: undefined, y: undefined, z: undefined },
    axis_states: { x: undefined, y: undefined, z: undefined },
  }, botLocationData);