src/utilities.tsx
import { useEffect, KeyboardEvent } from 'react';
import {
cloneDeep, get, has, identity, includes, intersection, isArray, isEmpty, isEqual,
isFunction, isNumber, isObject, isString, keys, lowerCase, merge as merge$, omit, orderBy,
padStart, pick, pickBy, split, startCase, take, toLower, toString, transform, uniqBy
} from 'lodash';
import queryString from 'query-string'; //upgraded package
// import swal from '@sweetalert/with-react'; //need to check if this is compatible with typescript
import Fuse from 'fuse.js';
import shortid from 'shortid';
import Bowser from 'bowser';
import Scroll from 'react-scroll';
import numeral from 'numeral';
import { distanceInWords, format } from 'date-fns';
import { Bid, Grade, Region, Waiver } from './types';
import { LOGIN_REDIRECT, LOGIN_ROUTE, LOGOUT_ROUTE } from './login/routes';
import { NO_BID_CYCLE, NO_POST } from './Constants/SystemMessages';
import { VALID_PARAMS, VALID_TANDEM_PARAMS } from './Constants/EndpointParams';
import FLAG_COLORS from './Constants/FlagColors';
const scroll = Scroll.animateScroll;
export function localStorageFetchValue(key: string, value: any): { exists: boolean; count: number } {
const saved = { exists: true, count: 0 };
const retrievedKey = localStorage.getItem(key);
let parsedKey = JSON.parse(retrievedKey ?? '');
const arrayExists = Array.isArray(parsedKey);
if (!arrayExists) {
localStorage.setItem(key, JSON.stringify([]));
parsedKey = JSON.parse(localStorage.getItem(key) ?? '');
}
saved.count = parsedKey.length;
const refIsSaved = parsedKey.indexOf(value);
if (refIsSaved !== -1) {
saved.exists = true;
} else {
saved.exists = false;
}
return saved;
}
const dispatchLs = (key: string): void => {
// create, initialize, and dispatch event
const event = document.createEvent('Event');
event.initEvent(`${key}-ls`, true, true);
document.dispatchEvent(event);
};
export function localStorageSetKey(key: string, value: string): void {
localStorage.setItem(key, value);
dispatchLs(key);
}
// toggling a specific value in an array
// useDispatch: only dispatch an event if true.
// onlyDelete: don't add, only delete from the array
export function localStorageToggleValue(key: string, value: any, useDispatch: boolean = true, onlyDelete: boolean = false): void {
const existingArray = JSON.parse(localStorage.getItem(key) ?? '') || [];
// check if the value matches, either as a string or as a number
let indexOfId = existingArray.indexOf(value);
if (indexOfId <= -1) {
indexOfId = existingArray.indexOf(Number(value));
}
if (indexOfId <= -1) {
indexOfId = existingArray.indexOf(toString(value));
}
if (indexOfId !== -1) {
existingArray.splice(indexOfId, 1);
localStorage.setItem(key,
JSON.stringify(existingArray));
if (useDispatch) {
dispatchLs(key);
}
} else if (!onlyDelete) {
existingArray.push(value);
localStorage.setItem(key,
JSON.stringify(existingArray));
if (useDispatch) {
dispatchLs(key);
}
}
}
export function validStateEmail(email: string): boolean {
return /.+@state.gov$/.test(email.trim());
}
export function hasValidToken(): boolean {
try {
/* eslint-disable no-unused-vars */
const token = JSON.parse(localStorage.getItem('token') ?? '');
/* eslint-enable no-unused-vars */
return true;
} catch (error) {
// If token exists and is bad (maybe user injected)
// Drop the token anyways just so we can have the container
// render login directly
localStorage.removeItem('token');
return false;
}
}
export function fetchUserToken(): (string | null) {
const key = JSON.parse(localStorage.getItem('token') ?? '');
if (key) {
return `Token ${key}`;
}
return null;
}
export function fetchJWT(): (string | null) {
const key = sessionStorage.getItem('jwt');
if (key) {
return key;
}
return null;
}
export const sortTods = (data: any): any => {
const sortingArray = ['T', 'C', 'H', 'O', 'V', '1', '2', 'U', 'A', 'B', 'E', 'N', 'S', 'G', 'D', 'F', 'R', 'Q', 'J', 'I', 'P', 'W', 'L', 'K', 'M', 'Y', 'Z', 'X'];
// eslint-disable-next-line no-confusing-arrow
return orderBy(data, o => o ? sortingArray.indexOf(o.code) : sortingArray.length);
};
export const propSort = (propName: string, nestedPropName?: string) => (a: any, b: any): number => {
let A = get(a, `${propName}.${nestedPropName}`) || get(a, propName);
A = lowerCase(toString(A));
let B = get(b, `${propName}.${nestedPropName}`) || get(b, propName);
B = lowerCase(toString(B));
if (A < B) { // sort string ascending
return -1;
}
if (A > B) { return 1; }
return 0; // default return value (no sorting)
};
// Custom grade sorting
export const sortGrades = (a: Grade, b: Grade): number => {
const sortingArray = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '00', 'CM', 'MC', 'OC', 'OM'];
const A = a.code;
const B = b.code;
// if grade is not in sortingArray, push to bottom of list.
const indexOfA = sortingArray.indexOf(A) >= 0 ? sortingArray.indexOf(A) : sortingArray.length;
const indexOfB = sortingArray.indexOf(B) >= 0 ? sortingArray.indexOf(B) : sortingArray.length;
if (indexOfA < indexOfB) {
return -1;
}
if (indexOfA > indexOfB) { return 1; }
return 0;
};
// function to find the Region filters
export const formExploreRegionDropdown = (filters: any[]): Region[] => {
function filterRegion(filterItem: { item: { title: string; }; }) {
return (filterItem.item && filterItem.item.title === 'Bureau');
}
// set an array so we can render in case we don't find Region
let regions: Region[] = [];
// find the Region filters
const foundRegion = filters.find(filterRegion);
// if found, set foundRegion to a copy of the data
if (foundRegion && foundRegion.data) { regions = foundRegion.data.slice(); }
if (regions.length) {
regions.forEach((region, i) => {
// set up our prop names so that SelectForm can read them
regions[i].text = region.long_description ?? '';
regions[i].value = region.code;
});
// also add a placeholder to the top
regions.unshift(
{
text: 'Select a Bureau',
value: '',
disabled: true,
},
);
}
return regions;
};
// see all props at https://github.com/fisshy/react-scroll#propsoptions
const defaultScrollConfig = {
duration: 900,
delay: 270,
smooth: 'easeOutQuad',
};
export const scrollTo = (num: number, config = {}): void => {
scroll.scrollTo(num, { ...defaultScrollConfig, ...config });
};
export const scrollToTop = (config = {}): void => {
scroll.scrollToTop({ ...defaultScrollConfig, ...config });
};
export const scrollToId = ({ el, config = {} }: any): void => {
// Get an element's distance from the top of the page
const getElemDistance = (elem: HTMLElement) => {
let location = 0;
if (elem.offsetParent) {
// eslint-disable-next-line no-loops/no-loops
do {
location += elem.offsetTop;
elem = elem.offsetParent as HTMLElement; // eslint-disable-line
} while (elem);
}
return location >= 0 ? location : 0;
};
const elem = document.querySelector(el);
const location = getElemDistance(elem);
scrollTo(location, config);
};
// When we want to grab a label, but aren't sure which one exists.
// We set custom ones first in the list.
export const getItemLabel = (itemData: any): string =>
itemData.custom_description || itemData.long_description ||
itemData.description || itemData.code || itemData.name;
// abcde 4 // a...
// Shortens strings to varying lengths
export const shortenString = (string: string, shortenTo: number = 250, suffix: (string | null) = '...'): string => {
let newString = string;
let newSuffix = suffix;
if (!newSuffix) {
newSuffix = '';
}
// return the suffix even if the shortenTo is less than its length, empty string if null
if (shortenTo < newSuffix.length) {
return suffix ?? '';
}
if (string && string.length > shortenTo) {
// shorten to the shortenTo param, less the length of our suffix
newString = string.slice(0, shortenTo - newSuffix.length);
// in case the last character(s) was whitespace
newString = newString.trim();
// append suffix
newString += newSuffix;
}
// return the string
return newString;
};
// TODO: Clarify type of array, is a variety of objects that have the id attribute or only one
// for checking if a favorite_position exists in the user's profile
export const existsInArray = (ref: any, array: any[]): boolean => {
let found = false;
array.forEach((i) => {
if (get(i, 'id') && ref && `${i.id}` === `${ref}`) {
found = true;
}
});
return found;
};
// Check if there is an object in the array with a value in the nested prop
// strictly equal to the given ref value
// Used for checking if a position is in the user's bid list
export const existsInNestedObject = (ref: any, array: any[], prop: string = 'position_info', nestedProp: string = 'id'): boolean => {
const array$ = isArray(array) ? array : [];
let found = false;
array$.some((i) => {
if (i[prop] && i[prop][nestedProp] === ref) {
found = i;
return true;
}
return false;
});
return found;
};
// clean our query object for use with the saved search endpoint
// make sure query object only uses real parameters (no extras that may have been added to the URL)
// we also want to get rid of page and limit,
// since those aren't valid params in the saved search endpoint
export const cleanQueryParams = (q: any): any => {
let object = Object.assign({}, q);
object = omit(object, ['count']);
Object.keys(object).forEach((key) => {
if (VALID_PARAMS.indexOf(key) <= -1) {
delete object[key];
}
});
return object;
};
export const cleanTandemQueryParams = (q: any): any => {
const object = Object.assign({}, q);
Object.keys(object).forEach((key) => {
if (VALID_TANDEM_PARAMS.indexOf(key) <= -1 && VALID_PARAMS.indexOf(key) <= -1) {
delete object[key];
}
});
return object;
};
export const ifEnter = (event: KeyboardEvent): boolean => {
if (event.keyCode === 13) {
return true;
}
return false;
};
// convert a query object to a query string
export const formQueryString = (queryObject: any): string => queryString.stringify(queryObject);
// remove duplicates from an array by object property
export const removeDuplicates = (myArr: any[], props: string[] = ['']): any => (
uniqBy(myArr, elem => props.map(m => elem[m]).join())
);
// Format date for notifications.
// We want to use minutes for recent notifications, but days for older ones.
export const getTimeDistanceInWords = (dateToCompare: Date, date: Date = new Date(), options = {}): string =>
`${distanceInWords(dateToCompare, date, options)} ago`;
// Format the date into our preferred format.
// We can take any valid date and convert it into M.D.YYYY format, or any
// format provided with the dateFormat param.
export const formatDate = (date: string | null, dateFormat: string = 'MM/DD/YYYY'): (string | null) => {
if (date) {
// then format the date with dateFormat
const formattedDate = format(date, dateFormat);
// and finally return the formatted date
return formattedDate;
}
return null;
};
// Prefix asset paths with the PUBLIC_URL
export const getAssetPath = (strAssetPath: string): string =>
`${process.env.PUBLIC_URL}${strAssetPath}`.replace('//', '/');
// Filter by objects that contain a specified prop(s) that match a string.
// Check if any of "array"'s objects' "props" contain "keyword"
export const filterByProps = (keyword: string, props: string[] = [], array: any[] = []): any[] => {
// keyword should have length
if (keyword.length) {
// filter the array and return its value
return array.filter((data) => {
let doesMatch = true;
// iterate through props and see if keyword is found in their values
keyword.split(' ').filter(f => f.length).forEach((k) => {
let doesMatch$ = false;
props.forEach((prop) => {
if (doesMatch) {
// if so, doesMatch = true
if (lowerCase(toString(data[prop])).indexOf(lowerCase(toString(k))) !== -1) {
doesMatch$ = true;
}
}
});
doesMatch = doesMatch$;
});
// if keyword was found in at least one of the props, doesMatch should be true
return doesMatch;
});
}
// if keyword length === 0, return the unfiltered array
return array;
};
// Focus an element on the page based on its ID. Pass an optional, positive timeout number to
// execute the focus within a timeout.
export const focusById = (id: string, timeout?: number, config: { preventScroll?: boolean } = {}): void => {
const config$ = {
preventScroll: true,
...config,
};
let element = document.getElementById(id);
if (typeof timeout !== 'number') {
if (element) { element.focus(config$); }
} else {
setTimeout(() => {
element = document.getElementById(id);
if (element) {
element.focus(config$);
}
}, timeout);
}
};
// Determine which header type to focus. We always have a page title h1, so we
// search for 1. The second h1, 2. the first h2, 3. the first h3, and focus which ever
// is found first.
export const focusByFirstOfHeader = (timeout: number = 1): void => {
setTimeout(() => {
let element: HTMLCollectionOf<HTMLHeadingElement> | HTMLHeadingElement = document.getElementsByTagName('h1');
element = (element && element[1]) || document.getElementsByTagName('h2')[0] || document.getElementsByTagName('h3')[0];
if (element) {
element.setAttribute('tabindex', '-1');
element.focus();
}
}, timeout);
};
// Give objects in an array the necessary value and label props needed when
// they're used in a multi-select list.
export const wrapForMultiSelect = (options: any[], valueProp: string, labelProp: string): any[] => options.slice().map((f) => {
const newObj = { ...f };
newObj.value = f[valueProp];
newObj.label = f[labelProp];
return newObj;
});
// Provide two arrays, a sourceArray and a compareArray, and a property to check (propToCheck),
// and this function will return objects from the sourceArray where a given propToCheck value exists
// in at least one object in both arrays.
export const returnObjectsWherePropMatches = (sourceArray: any[] = [], compareArray: any[] = [], propToCheck: string): any[] =>
sourceArray.filter(o1 => compareArray.some(o2 => o1[propToCheck] === o2[propToCheck]));
// Convert a numerator and a denominator to a percentage.
export const numbersToPercentString = (numerator: number, denominator: number, percentFormat: string = '0.0%'): string => {
const fraction = numerator / denominator;
const percentage = numeral(fraction).format(percentFormat);
return percentage;
};
export const formatBidTitle = (bid: Bid): string => `${bid.position.title} (${bid.position.position_number})`;
export const formatWaiverTitle = (waiver: Waiver): string => `${waiver.position} - ${waiver.category.toUpperCase()}`;
// for traversing nested objects.
// obj should be an object, such as { a: { b: 1, c: { d: 2 } } }
// path should be a string to the desired path - "a.b.c.d"
// defaultToReturn should be the default value you want to return if the traversal fails
export const propOrDefault = (obj: any, path: string, defaultToReturn: any = null): any =>
get(obj, path, defaultToReturn);
// Return the correct object from the bidStatistics array/object.
// If it doesn't exist, return an empty object.
export const getBidStatisticsObject = (bidStatistics: any | any[]): any => {
if (Array.isArray(bidStatistics) && bidStatistics.length) {
return bidStatistics[0];
} else if (isObject(bidStatistics)) {
return bidStatistics;
}
return {};
};
// replace spaces with hyphens so that id attributes are valid
export const formatIdSpacing = (id?: string | number | null): string => {
if (id && toString(id)) {
let idString = toString(id);
idString = split(idString, ' ').join('-');
// remove any non-alphanumeric character, excluding hyphen
idString = idString.replace(/[^a-zA-Z0-9 -]/g, '');
return idString;
}
// if id is not defined, return a shortid
return shortid.generate();
};
// provide an array of permissions to check if they all exist in an array of user permissions
export const userHasPermissions = (permissionsToCheck: string[] = [], userPermissions: string[] = []): boolean =>
permissionsToCheck.every(val => userPermissions.indexOf(val) >= 0);
// provide an array of permissions to check if at least one exists in an array of user permissions
export const userHasSomePermissions = (permissionsToCheck: string[] = [], userPermissions: string[] = []): boolean =>
!!intersection(permissionsToCheck, userPermissions).length;
// Takes multiple saved search objects and combines them into one object,
// where the value for each property is an array of all individual values
// found across the different saved search objects.
// See Constants/PropTypes SAVED_SEARCH_OBJECT
export const mapSavedSearchesToSingleQuery = (savedSearchesObject: any): any => {
const clonedSavedSearchesObject = cloneDeep(savedSearchesObject);
const clonedSavedSearches = clonedSavedSearchesObject.results;
const mappedSearchTerms = clonedSavedSearches.slice().map((s: any) => s.filters);
const mappedSearchTermsFormatted = mappedSearchTerms.map((m: any) => {
const filtered: any = m;
Object.keys(m).forEach((k) => {
if (!Array.isArray(filtered[k])) {
filtered[k] = filtered[k].split(',');
}
});
return filtered;
});
function merge(...rest: any[]): any {
return [].reduce.call(rest, (acc: any, x: any) => {
const acc$ = merge$({}, acc);
Object.keys(x).forEach((k) => {
acc$[k] = (acc$[k] || []).concat(x[k]);
acc$[k] = acc$[k].filter((item: any, index: any, self: any[]) => self.indexOf(item) === index);
});
return acc$;
}, {});
}
const mergedFilters = mappedSearchTermsFormatted.length ? merge(...mappedSearchTermsFormatted) : {};
const mergedFiltersWithoutArrays: any = { ...mergedFilters };
Object.keys(mergedFilters).forEach((f) => {
if (Array.isArray(mergedFilters[f])) {
mergedFiltersWithoutArrays[f] = mergedFilters[f].join();
}
});
const newQuery = mergedFiltersWithoutArrays;
return newQuery;
};
// Maps a saved search object against the full filter objects its related to, so that
// we can return an array of descriptions based on the codes in the savedSearchObject.
// See Constants/PropTypes SAVED_SEARCH_OBJECT and MAPPED_PARAM_ARRAY
export const mapSavedSearchToDescriptions = (savedSearchObject: any, mappedParams: any[]): any[] => {
const clonedSearchObject = cloneDeep(savedSearchObject);
const searchKeys = Object.keys(clonedSearchObject);
searchKeys.forEach((s) => { clonedSearchObject[s] = clonedSearchObject[s].split(','); });
const arrayToReturn: any[] = [];
// Push the keyword search, since it won't match up with a real filter
if (savedSearchObject.q) {
arrayToReturn.push(
{
description: savedSearchObject.q,
isTandem: undefined,
isCommon: true,
isToggle: undefined,
}
);
}
searchKeys.forEach((s) => {
clonedSearchObject[s].forEach((c: any) => {
const foundParam = mappedParams.find(m => m.selectionRef === s && m.codeRef === c);
if (foundParam && foundParam.description) {
arrayToReturn.push(pick(foundParam, ['description', 'isTandem', 'isCommon', 'isToggle']));
}
});
});
return arrayToReturn;
};
export const getPostName = (post: any, defaultValue: any = null): string => {
let valueToReturn: any = defaultValue;
if (propOrDefault(post, 'location.city') &&
includes(['United States', 'USA'], get(post, 'location.country'))) {
valueToReturn = `${post.location.city}, ${post.location.state}`;
} else if (propOrDefault(post, 'location.city')) {
valueToReturn = `${post.location.city}${post.location.country ? `, ${post.location.country}` : ''}`;
} else if (propOrDefault(post, 'code')) {
valueToReturn = post.code;
}
return valueToReturn;
};
// returns the base application path,
// ie, https://hostname:8080/PUBLIC_URL/
export const getApplicationPath = () => `${window.location.origin}${process.env.PUBLIC_URL}`;
// Adds spaces between position number characters so that it's accessible for screen readers.
// Based on this accessibility feedback:
// When a letter is used in the position number, such as S7250404,
// the screen reader reads the number as a full-length numeral
// (i.e., "S. 7 million two hundred thousand ….). This can confuse or disorient the user as
// they navigate and search for positions.
export const getAccessiblePositionNumber = (positionNumber?: string | null) => {
if (positionNumber) {
return positionNumber.split('').join(' ');
}
return null;
};
// returns a percentage string for differential data.
export const getDifferentialPercentage = (differential: number | null, defaultValue: string = ''): string => {
if (isNumber(differential)) {
return `${differential}%`;
}
return defaultValue;
};
// redirect to express /login route
export const redirectToLogin = () => {
const prefix: string = process.env.PUBLIC_URL || '';
window.location.assign(`${prefix}${LOGIN_ROUTE}`);
};
// redirect to react /loginRedirect route
export const redirectToLoginRedirect = () => {
const prefix: string = process.env.PUBLIC_URL || '';
window.location.assign(`${prefix}${LOGIN_REDIRECT}`);
};
// redirect to express /logout route
export const redirectToLogout = () => {
const prefix: string = process.env.PUBLIC_URL || '';
window.location.assign(`${prefix}${LOGOUT_ROUTE}`);
};
/**
* ~ Returns a Deep Diff Object Between 2 Objects (First parameter as base) ~
* base = { param1: true, param2: 'loading' };
* object = { param1: false, param2: 'loading' };
*
* difference(base, object) => { param1: false }
* difference(object, base) => { param1: true }
*/
export const difference = (base: any, object: any): any => {
return transform(object, (result: any, value: any, key: any) => {
/* eslint-disable no-param-reassign */
if (!isEqual(value, base[key])) {
result[key] = (isObject(value) && isObject(base[key]) && difference(base[key], value)) || value;
}
/* eslint-enable no-param-reassign */
});
};
/* returns true/false whether url is a valid url that contains http/https/ftp */
export const isUrl = (url: string): RegExpMatchArray | null => {
const expression = /(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])?/;
const regex = new RegExp(expression);
return url.match(regex);
};
export const getScrollDistanceFromBottom = (): number => {
const scrollPosition: number = window.pageYOffset;
const windowSize: number = window.innerHeight;
const bodyHeight: number = document.body.offsetHeight;
return Math.max(bodyHeight - (scrollPosition + windowSize), 0);
};
// eslint-disable-next-line no-confusing-arrow
export const getFormattedNumCSV = (v: any): string => {
if (v === null || v === undefined) {
return '';
}
// else
return !isNaN(v) ? `=${v}` : v;
};
export const spliceStringForCSV = (v: any): string => {
if (v[1] === '=' && typeof v === 'string') {
return `=${v.slice(0, 1)}${v.slice(2)}`;
}
return v;
};
// Returns a paginated array based on page size and the desired page number
export const paginate = (array: any[], pageSize: number, pageNumber: number): any[] => {
// because pages logically start with 1, but technically with 0
const pageNumber$ = pageNumber - 1;
return array.slice(pageNumber$ * pageSize, (pageNumber$ + 1) * pageSize);
};
// Looks for duplicates in a data set by property, and adds a "hasDuplicateDescription" property
// to any objects that are duplicates.
export const mapDuplicates = (
data: any[] = [],
propToCheck: string = 'custom_description',
transformFunc?: (item: any) => any
): any[] => {
return data.slice().map((p) => {
let p$ = { ...p };
const matching = data.filter(f => f[propToCheck] === p$[propToCheck]) || [];
if (matching.length >= 2) {
p$.hasDuplicateDescription = true;
if (typeof transformFunc === 'function') {
transformFunc = (e: any) => ({
...e,
name: e.code ? `${e.name} (${e.code})` : e.name
});
p$ = transformFunc(p$);
}
}
return p$;
});
};
export const termInGlossary = (term: string): boolean => {
const id: string = `${formatIdSpacing(term)}-button`;
return document.getElementById(id) !== null;
};
// scroll to a specific glossary term
export const scrollToGlossaryTerm = (term: string): void => {
// id formatting used for glossary accordion buttons
const id: string = `${formatIdSpacing(term)}-button`;
const el: HTMLElement | null = document.getElementById(id);
if (el) {
setTimeout(() => {
el.scrollIntoView();
focusById(id, 0, { preventScroll: false });
if (el.getAttribute('aria-expanded') !== 'true') {
el.click();
}
}, 300);
}
};
export const getBrowserName = () => Bowser.getParser(window.navigator.userAgent).getBrowserName();
export const getBrowser = () => Bowser.getParser(window.navigator.userAgent).getBrowser();
// Convert values used in aria-* attributes to 'true'/'false' string.
// Perform a string check, if for some reason the value was already a string.
// https://github.com/cerner/terra-core/wiki/React-16-Migration-Guide#noted-changes
export const getAriaValue = (e: string | number | boolean | null) => {
if (e === 'true') {
return e;
} else if (e === 'false') {
return e;
} else if (e) {
return 'true';
}
return 'false';
};
export const downloadFromResponse = (
response: any,
fileNameAlt: string = '',
type: string = 'text/csv'
): void => {
const cd: string = get(response, 'headers.content-disposition', '');
const filename: string = cd.replace('attachment; filename=', '') || fileNameAlt;
const a: HTMLAnchorElement = document.createElement('a');
const url: string = window.URL.createObjectURL(new Blob([response.data]));
a.href = url;
a.setAttribute('download', filename);
document.body.appendChild(a);
const _win = window.navigator as any;
if (_win.msSaveBlob) {
a.onclick = (() => {
const BOM: string = '\uFEFF';
const blobObject: Blob = new Blob([BOM + response.data], { type: ` type: "${type}; charset=utf-8"` });
_win.msSaveOrOpenBlob(blobObject, filename);
});
a.click();
} else {
a.click();
}
};
export const downloadPdfBlob = (
response: any,
filename: string = 'employee-profile.pdf'
): void => {
const _win = window.navigator as any;
if (_win.msSaveBlob) {
// const BOM = '\uFEFF';
const blobObject: Blob = new Blob([response], { type: 'application/pdf; charset=utf-8' });
_win.msSaveOrOpenBlob(blobObject, filename);
} else {
const blob: Blob = new Blob([response], { type: 'application/pdf' });
const win: Window | null = window.open('', '_blank');
const URL = window.URL || window.webkitURL;
const dataUrl: string = URL.createObjectURL(blob);
if (win) {
win.location.href = dataUrl;
}
}
};
const saveByteArray = (reportName: string, byte: Uint8Array): void => {
const blob: Blob = new Blob([byte], { type: 'application/pdf' });
const _win = window.navigator as any;
if (_win && _win.msSaveOrOpenBlob) { // if IE
_win.msSaveOrOpenBlob(blob);
} else {
const link: HTMLAnchorElement = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
const fileName: string = reportName;
link.download = fileName;
link.click();
}
}
export const downloadPdfStream = (response: any, filename: string = 'employee-profile.pdf') => {
saveByteArray(filename, response);
};
//Create a tsx file for the constants
export const getBidCycleName = (bidcycle: string | object): string => {
// TODO: Reference bidcycle type once determined instead of any
let text: string = isObject(bidcycle) && has(bidcycle, 'name') ? (bidcycle as any).name : bidcycle;
if (!isString(text) || !text) { text = NO_BID_CYCLE; }
return text;
};
export const anyToTitleCase = (str: string = '') => startCase(toLower(str));
export const loadImg = (src: string, callback: () => void): void => {
const sprite: HTMLImageElement = new Image();
sprite.onload = callback;
sprite.onerror = callback;
sprite.src = src;
};
export const isNumeric = (value: any): boolean => isNumber(value) || (!isEmpty(value) && !isNaN(value));
// BEGIN FUSE SEARCH //
interface FuseOptions {
shouldSort?: boolean;
tokenize?: boolean;
includeScore?: boolean;
threshold?: number;
location?: number;
distance?: number;
maxPatternLength?: number;
minMatchCharLength?: number;
keys?: string[];
}
const fuseOptions: FuseOptions = {
shouldSort: true,
tokenize: true,
includeScore: true,
threshold: 0.5,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 3,
keys: [
'name',
],
};
const flagFuse = new Fuse(FLAG_COLORS, fuseOptions);
export const getFlagColorsByTextSearch = (t: string = '', limit: number = 5): string[] | false => {
let value: string[] | false = false;
if (t && isString(t)) {
const result = flagFuse.search(t).map(({ item }) => item);
const colors = get(result, '[0].item.colors', false);
value = colors;
}
if (value) {
value = take(value, limit);
}
return value;
};
// END FUSE SEARCH //
export const stopProp = (event: Event | {}): void => {
const e: Event = get(event, 'target') || event;
if (e && e.stopPropagation && isFunction(e.stopPropagation)) {
e.stopPropagation();
}
};
export const getContrastYIQ = (hexcolor: string): 'black' | 'white' => {
const r: number = parseInt(hexcolor.substr(0, 2), 16);
const g: number = parseInt(hexcolor.substr(2, 2), 16);
const b: number = parseInt(hexcolor.substr(4, 2), 16);
const yiq: number = ((r * 299) + (g * 587) + (b * 114)) / 1000;
return (yiq >= 128) ? 'black' : 'white';
};
// Supply a user's full name
// Returns background color and text color
// Supply a user's full name
// Returns background color and text color
export const getAvatarColor = (str: string, hashAdjuster: number = 0): { backgroundColor: string, color: 'black' | 'white' } | null => {
if (str) {
let hash: number = Math.floor(Math.random() * hashAdjuster);
//replaced this loop with the for loop below
// [...str].forEach((s: string, i: number) => {
// if (i) {
// hash = str.charCodeAt(i) + ((hash << 5) - hash);
// }
// });
for (let i = 0; i < str.length; i++) {
if (i) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
}
const c: string = (hash & 0x00FFFFFF).toString(16).toUpperCase();
const backgroundColor: string = '00000'.substring(0, 6 - c.length) + c;
const color: 'black' | 'white' = getContrastYIQ(backgroundColor);
const backgroundColorWithHash: string = `#${backgroundColor}`;
return { backgroundColor: backgroundColorWithHash, color };
}
return null;
};
export function getBidListStats(bidList: any[], statuses: string[], padWithZero?: boolean): number | string {
let numBids: number = 0;
bidList.forEach((b: any) => {
if (includes(statuses, b.status)) numBids += 1;
});
if (padWithZero) {
return padStart(toString(numBids), 2, '0').toString();
}
return numBids;
}
export const isOnProxy = (): boolean => !!includes(get(window, 'location.host'), 'msappproxy');
export function move<T>(arr: T[], fromIndex: number, toIndex: number): T[] {
const element: T = arr[fromIndex];
arr.splice(fromIndex, 1);
arr.splice(toIndex, 0, element);
return arr;
}
export function getCustomLocation(loc: any, org: string): string {
if (!loc) return NO_POST;
// DC Post - org ex. GTM/EX/SDD
if (get(loc, 'state') === 'DC') return org || NO_POST;
// Domestic outside of DC - City, State
if (get(loc, 'country') === 'USA') return `${get(loc, 'city')}, ${get(loc, 'state')}`;
if (!get(loc, 'city') && !get(loc, 'country')) return '';
// Foreign posts - City, Country
let x: string = `${get(loc, 'city')}, ${get(loc, 'country')}`;
if (!get(loc, 'city')) { x = get(loc, 'country'); }
if (!get(loc, 'country')) { x = get(loc, 'city'); }
return x;
}
//These will be added back after the swal package is updated to be compatible with typescript
// export const closeSwal = (): void | null => {
// try {
// swal.close();
// } catch { return null; }
// return null;
// };
// export const useCloseSwalOnUnmount = (): void =>
// useEffect(() => () => {
// closeSwal();
// }, []);
export const splitByLineBreak = (text: string | null) => (text || '').split('\n\n\n');
export const convertQueryToString = (query: Record<string, any>): string | Record<string, any> => {
let q: any = pickBy(query, identity);
Object.keys(q).forEach((queryk: string) => {
if (isArray(q[queryk])) {
q[queryk] = q[queryk].join();
}
if (isString(q[queryk]) && !q[queryk]) {
q[queryk] = undefined;
}
});
q = queryString.stringify(q);
return q;
};
export const determineEnv = (url: string): string => {
const expression = /(dev1|dev2|tst1|tst2|asb|ivv1|uat|prd|cpy|localhost|metaphasedev)/i;
const regex = new RegExp(expression);
const match = url.match(regex);
if (!match) {
console.log('no valid env found');
throw new Error('No valid env found');
}
return match[0];
};
export const formatLang = (langArr: any[]): string => langArr.map(lang => (
`${lang.code} ${lang.spoken_proficiency}/${lang.reading_proficiency}`
)).join(', ');
// Search Tags: common.js, helper file, helper functions, common helper file, common file