All Downloads are FREE. Search and download functionalities are using the official Maven repository.

utils.utils.js Maven / Gradle / Ivy

There is a newer version: 0.80.3
Show newest version
import React                from 'react'
import {
  useCallback,
  useEffect,
  useState }                from 'react'
import {
  useApolloClient }                from '@apollo/client'
import reform               from 'immutability-helper'
import { format, parse }    from 'date-fns'
import _                    from 'lodash'
import Report               from 'helpers/report'
import CenteredLoader       from 'components/common/CenteredLoader'
import { toRegex }          from 'utils/option-utils'
import { nullOrEmpty } from '../components/form/fields/utils/values'
import Types from 'types/types'

// === misc functions === //

const urlRegex =/(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/i
export const isUrl = (str) => urlRegex.test(str) // TODO rename to is*Absolute*Url?

export function addObject(condition, obj) {
  return condition ? [obj] : []
}

export function addPromiseStatus(promise) {
    // Don't modify any promise that has been already modified.
    if (promise.isFulfilled) return promise;

    // Set initial state
    var isPending = true;
    var isRejected = false;
    var isFulfilled = false;

    // Observe the promise, saving the fulfillment in a closure scope.
    var result = promise.then(
        function(v) {
            isFulfilled = true;
            isPending = false;
            return v;
        },
        function(e) {
            isRejected = true;
            isPending = false;
            throw e;
        }
    );

    result.isFulfilled = function() { return isFulfilled; };
    result.isPending = function() { return isPending; };
    result.isRejected = function() { return isRejected; };
    return result;
}

export function env(name) {
  const key = 'REACT_APP_' + name
  return (global._env && global._env[key]) || process.env[key]
}

export function isString(obj) {
  return typeof obj === 'string' || obj instanceof String
}
export const setTimeoutPromise = (cb, delay) =>
  new Promise((resolve) => {
    setTimeout(() => {
      resolve(cb());
    }, delay);
  });

export function asyncSleep(delay = 0) {
  return new Promise((resolve) => {
    setTimeout(resolve, delay);
  });
}

export function sleep(delay = 0) {
  (async () => { await asyncSleep(delay) })()
}

export function toTypeString( obj ) {
  return ({}).toString.call( obj ).match(/\s(\w+)/)[1].toLowerCase();
}

export function isOfType(arg, type) {
  if (typeof type !== "string")
    throw new TypeError("Provide a type specifier, e.g.: 'string!'")

  const simpleType = type.replace(/!/g, '').toLowerCase()
  const actualType = toTypeString(arg)
  return (! /.*!$/.test(type) || arg !== null) && actualType == simpleType;
}

export function assertType(arg, type, msg = "") {
  if (!isOfType(arg, type)) {
    const defaultMsg =  "value '" + arg + "' is not of expected type '" + type + "'."
    const message    = msg ? msg : defaultMsg
    throw new TypeError(message);
  }
}

export function assertTypes( args, types ) {
  args = [].slice.call( args );
  for ( var i = 0; i < types.length; ++i )
    assertType(args[i], types[i], i)
}

export function pruneEmpty(obj) {
  return function prune(current) {
    _.forOwn(current, function (value, key) {
      if (_.isUndefined(value) || _.isNull(value) || _.isNaN(value) ||
        (Types.isObject(value) && _.isEmpty(prune(value))) ||
        (Array.isArray(value) && value.length == 0))
      {
        delete current[key];
      }
    });
    // remove any leftover undefined values from the delete
    // operation on an array
    if (_.isArray(current)) _.pull(current, undefined);

    return current
  }(_.cloneDeep(obj));  // Do not modify the original object, create a clone instead
}

export function hasNonEmptyValue(obj) {
  if (Types.isObject(obj))
    return Object.values(obj).some(hasNonEmptyValue)
  else if (Array.isArray(obj))
    return obj.some(hasNonEmptyValue)
  else
    return !Boolean(isEmptyValue(obj))
}

export function isEmptyValue(value) {
  return value === null || value === undefined || value === "" || (Array.isArray(value) && value.length === 0)
}

export function withDefault(value, fallback) {
  return isEmptyValue(value) ? fallback : value
}

export function mapSet(set, f) {
  var newSet = new Set();
  for (var v of set.values()) newSet.add(f(v));
  return newSet;
};

export function addLinesAt(lines, index, selected, defaultValue) {
  if (!Array.isArray(lines))
    return lines

  var indexes = Array.from(selected).sort();
  const selectedLines = fallback(indexes.map(index => fallback(lines[index], defaultValue)), [defaultValue])
  return reform(lines, { $splice: [[index + 1, 0, ...selectedLines]] })
}

export function deleteLinesAt(lines, index, nr) {
  if (!Array.isArray(lines))
    return lines

  return reform(lines, { $splice: [[index, nr]] })
}

export function fallback(value, fallback) {
  return nullOrEmpty(value) ? fallback : value
}

// this is the opposite of renderPath. given a rpath produces the 'indices' object expected by renderPath
export function extractIndices(rpath) {
  let indices = {}
  if (isString(rpath)) {
    const matches = rpath.matchAll(/(\w+)\[(\d+)\]/g)
    for (const match of matches) {
      const name  = match[1]
      const index = match[2]
      indices[name] = parseInt(index)
    }
  }

  return indices
}

export function renderPath(path, indices) {
  return path.replace(/\$\w+/g, (matched) => {
    const name = matched.substring(1)
    const index = indices[name]
    //console.log("renderPath(%o): name=%o, index=%o", path, name, index)
    return index
  });
}

/** @deprecated no longer used */
export function valuesToFormEntries(values) {
  return new Array(...values).map(([key, value]) => { return { key, value } })
}

// find the first occurance of 'key' recursively, and return the value
export function findVal(object, key) {
  var value;
  const resolve = function(k) {
      if (k === key) {
          value = object[k];
          return true;
      }
      value = findVal(object[k], key);
      return value !== undefined;
  }
  if (Types.isObject(object))
    Object.keys(Types.asObject(object)).some(resolve);

  return value;
}

export function getPath(obj, path, dflt = undefined) {
  if (obj == undefined)
    return dflt
  else if (path == "")
    return obj
  else if (!_.isString(path))
    throw "Function getPath was provided with an invalid path: '" + path + "'"
  else
    return _.get(obj, path, dflt)
}

/* paginate data. starting from index 1! */
export function paginate(array, pageSize, page) {
  if (Array.isArray(array))
    return array.slice(page * pageSize, (page + 1) * pageSize);
  else
    return []
}

export function handleResults(results, notifier, callback, errorCallbacks) {
  const defaultErrorCallback = (error) => {
    console.error("GraphQL error: %o", error)
    const report = Report.error(Report.backend, Report.code.backend.Error)
    report.addToNotifier(notifier)
    return report.toNotification({reload: true})
  }

  if (!Array.isArray(results) || results.some(result => !result.hasOwnProperty('loading'))) {
    notifier.error("Function handleResults only accepts an array of graphql queries")
    return null
  }

  const loading = results.some(result => result.loading)
  if (loading)
    return 

  const errorIndex = results.findIndex(result => Boolean(result.error))
  if (errorIndex >= 0) {
    const error = results[errorIndex].error
    if (errorCallbacks) {
      if (errorCallbacks.length > errorIndex)
        return (errorCallbacks[errorIndex])(error)
      else
        return errorCallbacks(error)
    } else
      return defaultErrorCallback(error)
  }

  const result = results.map(result => result.data)
  console.log('handleResults: data=%o', result)

  return callback(result)
}

export function notifyOnError(result, notifier) {
  const { loading, error } = result

  if (loading)
    return undefined

  if (error) {
    console.error("GraphQL error: %o", error)
    const report = Report.error(Report.backend, Report.code.backend.Error)
    report.addToNotifier(notifier)
  }
}

export function handleError(result, notifier, errorCallback) {
  const defaultErrorCallback = (error) => {
    console.error("GraphQL error: %o", error)
    const report = Report.error(Report.backend, Report.code.backend.Error)
    report.addToNotifier(notifier)
    return report.toNotification({reload: true})
  }
  console.log("error result: %o", result)


  const { loading, error } = result
  if (loading)
    return null

  if (error)
    return errorCallback ? errorCallback(error) : defaultErrorCallback(error)
}

export function handleResult(result, notifier, callback, errorCallback) {
  const results = Array.isArray(result) ? result : [result]

  // handle loading
  if (results.some(result => result.loading))
    return 

  // handle errors
  const errorResult = results.find(result => result.error)
  if (errorResult){
    console.log("handleError: %o", errorResult)
    return handleError(errorResult, notifier, errorCallback)
  }

  // handle data
  const datas = results.map(result => result.data)
  console.log('handleResult: data=%o', datas.length == 1 ? datas[0] : datas)
  if (datas.every(data => !data)) 
    return null

  return callback(datas.length == 1 ? datas[0] : datas)
}

// create a range, i.e., a list with [0,1,...,number-1]
export function range(number) {
  return [...Array(number).keys()];
}

export function updateObject(target, source) {
  for (var prop in source) {
      if (source.hasOwnProperty(prop)) {
          if (target[prop] && typeof source[prop] === 'object') {
              updateObject(target[prop], source[prop]);
          }
          else {
              target[prop] = source[prop];
          }
      }
  }
  return target;
}

export function updateQuery(cache, query, updater) {
  const data = cache.readQuery({ query })
  console.log("updateStore: before=%o", data)
  const newData = updater(data)
  console.log("updateStore: after=%o", newData)
  cache.writeQuery({ query: query, data: newData })
}

export function openInNewTab(url) {
  window.open(url, '_blank', 'noopener,noreferrer');
}

const isFile = input => 'File' in window && input instanceof File;
const isBlob = input => 'Blob' in window && input instanceof Blob;

const arrayFileReducer = ([previousValues, previousFiles], value) => {
  const [newValue, files] = extractFiles(value)
  return [[...previousValues, newValue], previousFiles.concat(files)]
}

const objectFileReducer = ([previousEntries,previousFiles],[key, value]) => {
  const  [newValue, files] = extractFiles(value)
  return [[...previousEntries, [key, newValue]], previousFiles.concat(files)]
}

// this function replaces values of type File (or Blob) with an id, and returns the file seperately,
// e.g. value: {attr: File {name: filename}} => [{ attr: 'filename}, [File {name: 'filename'}]]
export function extractFiles(value) {
  switch (true) {
    case isFile(value) || isBlob(value):
      return [value.path, [value]]
    case Types.isContent(value):
      const content = Types.toContent(value)
      return content.hasData ? [content.path, [content.file]] : [content.toContentRef(), []]
    case Array.isArray(value):
      return value.reduce(arrayFileReducer, [[],[]])
    case Types.isObject(value):
      const [entries, files] = Object.entries(value).reduce(objectFileReducer, [[],[]])
      return [Object.fromEntries(entries), files]
    default:
      return [value,[]]
  }
}

export function reformQuery(cache, query, reformer) {
  const data = cache.readQuery({ query })
  console.log("updateStore: before=%o", data)
  if (data) {
    const newData = reform(data, reformer)
    console.log("updateStore: after=%o", newData)
    cache.writeQuery({ query: query, data: newData })
  } else {
    console.warn("could not update cached graphql result for: ", JSON.stringify(query))
  }
}

export function removerById(id) {
  return elements => elements.filter(elem => elem.id !== id)
}

// === hooks === //

/** @deprecated no longer used */
export function useValue(ctx, path, initial) {
  const [state, setState] = React.useState(initial)
  ctx.setValue(path, state)
  function setValue(value) {
    ctx.setValue(path, value)
    setState(value)
  }
  return [state, setValue]
}

export function useDebouncedState(initial, delay) {
  const [state, setState] = React.useState(initial)

  // eslint-disable-next-line
  const debounce = React.useCallback(
    _.debounce(setState, delay),
    [delay]
  )

  const setDebouncedState = val => debounce(val)
  const setImmediateState = val => {
    debounce.cancel()
    setState(val)
  }

  return [state, setDebouncedState, setImmediateState]
}

// a debouncer, with a shirtcircuit to skip the delay before update. E.g., press Enter to update immediately
export function useDebounce(initialValue, delay) {
  const [value, setValue] = useState(initialValue)
  const [debouncedValue, setDebouncedValue, setDebouncedImmediate] = useDebouncedState(initialValue, delay)

  useEffect(() => {
    setDebouncedValue(value)
  }, [value, delay])

  return [value, setValue, debouncedValue, () => { setDebouncedImmediate(value) }]
}


// see https://github.com/apollographql/react-apollo/issues/3499
export function usePromiseQuery(query) {
  const client = useApolloClient()
  return React.useCallback(
    variables => client.query({ query, variables }),
    [client, query]
  )
}

// === date-related functions === //
// NOTE default JavaScript date format is ECMA-262
// TODO deels dubbel met dt-formats.js?

export const displayFormats = {
  date:     'dd-MM-yyyy',
  time:     'HH:mm:ss',
  dateTime: 'dd-MM-yyyy HH:mm:ss',
}

export const referenceFormats = {
  date:     'yyyy-MM-dd',
  time:     'HH:mm:ss',
  dateTime: 'yyyy-MM-ddTHH:mm:ss',
}

const timezoneConversions = {
  ACDT: "+1030",
  ACST: "+0930",
  ADT: "-0300",
  AEDT: "+1100",
  AEST: "+1000",
  AHDT: "-0900",
  AHST: "-1000",
  AST: "-0400",
  AT: "-0200",
  AWDT: "+0900",
  AWST: "+0800",
  BAT: "+0300",
  BDST: "+0200",
  BET: "-1100",
  BST: "-0300",
  BT: "+0300",
  BZT2: "-0300",
  CADT: "+1030",
  CAST: "+0930",
  CAT: "-1000",
  CCT: "+0800",
  CDT: "-0500",
  CED: "+0200",
  CET: "+0100",
  CEST: "+0200",
  CST: "-0600",
  EAST: "+1000",
  EDT: "-0400",
  EED: "+0300",
  EET: "+0200",
  EEST: "+0300",
  EST: "-0500",
  FST: "+0200",
  FWT: "+0100",
  GMT: "GMT",
  GST: "+1000",
  HDT: "-0900",
  HST: "-1000",
  IDLE: "+1200",
  IDLW: "-1200",
  IST: "+0530",
  IT: "+0330",
  JST: "+0900",
  JT: "+0700",
  MDT: "-0600",
  MED: "+0200",
  MET: "+0100",
  MEST: "+0200",
  MEWT: "+0100",
  MST: "-0700",
  MT: "+0800",
  NDT: "-0230",
  NFT: "-0330",
  NT: "-1100",
  NST: "+0630",
  NZ: "+1100",
  NZST: "+1200",
  NZDT: "+1300",
  NZT: "+1200",
  PDT: "-0700",
  PST: "-0800",
  ROK: "+0900",
  SAD: "+1000",
  SAST: "+0900",
  SAT: "+0900",
  SDT: "+1000",
  SST: "+0200",
  SWT: "+0100",
  USZ3: "+0400",
  USZ4: "+0500",
  USZ5: "+0600",
  USZ6: "+0700",
  UT: "-0000",
  UTC: "-0000",
  UZ10: "+1100",
  WAT: "-0100",
  WET: "-0000",
  WST: "+0800",
  YDT: "-0800",
  YST: "-0900",
  ZP4: "+0400",
  ZP5: "+0500",
  ZP6: "+0600"
}

// convert a string timezone to hour shift, e.g., CET => +0100, CEST => +0200
function withoutTimezone(dateTimeString){
  const reducer = (timeString, [timezone, replacement]) => timeString.replace(timezone, replacement)
  return Object.entries(timezoneConversions).reduce(reducer, dateTimeString)
}

// reference string -> display string
export function convertToDate(value) {
  return value instanceof Date ? value : new Date(withoutTimezone(value))
}

// date -> string
export function formatDate(value) {
  const result = value === null ? '' : format(value, displayFormats.date)
  return result
}
export function formatTime(value) {
  return format(value, displayFormats.time)
}
export function formatDateTime(value) {
  return format(value, displayFormats.date)
}

// string -> date
export function parseDate(value) {
  const result = nullOrEmpty(value) ? null : parse(value, displayFormats.date, new Date())
  return result
}

export function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(
    () => window.localStorage.getItem(key) || initialValue
  )

  const setItem = (newValue) => {
    setValue(newValue);
    if (newValue === null)
      window.localStorage.removeItem(key)
    else
      window.localStorage.setItem(key, newValue)
  }

  useEffect(() => {
    const newValue = window.localStorage.getItem(key)
    if (value !== newValue) {
      setValue(newValue || initialValue)
    }
  })

  const handleStorage = useCallback(
    (event) => {
      if (event.key === key && event.newValue !== value) {
        setValue(event.newValue || initialValue)
      }
    },
    [value]
  );

  useEffect(() => {
    window.addEventListener('storage', handleStorage)
    return () => window.removeEventListener('storage', handleStorage)
  }, [handleStorage])

  return [value, setItem]
}

export function filterTableData(data, filterExpression, toRows, toRow, ...toColumns) {
  const regex = toRegex(filterExpression)

  function matchColumn(data, toColumn) {
    const column = toColumn ? toColumn(data) : data
    return column === null || column === undefined ? false : regex.test(column)
  }

  function filterRow(data) {
    const row = toRow ? toRow(data) : data
    if (Array.isArray(row) && toColumns.length === 0)
      return row.some(matchColumn)
    else
      return toColumns.some(toColumn => matchColumn(row, toColumn))
  }

  function filterRows(data) {
    const rows = toRows ? toRows(data) : data
    return rows.filter(rowData => filterRow(rowData));
  }

  return filterRows(data)
}

export function isEvent(e) {
  return e instanceof Event || _.get(e, "nativeEvent") instanceof Event || _.get(e, "e.originalEvent") instanceof Event;
}

export function isDate(date) {
  return date instanceof Date
}

export function isValidDate(date) {
  return isDate(date) && new Date(date).toString() !== 'Invalid Date';
}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy