mon-entreprise/site/source/utils.ts

243 lines
6.5 KiB
TypeScript
Raw Normal View History

import Engine, {
formatValue,
PublicodesExpression,
Rule,
RuleNode,
} from 'publicodes'
export function capitalise0(name: undefined): undefined
export function capitalise0(name: string): string
export function capitalise0(name?: string) {
return name && name[0].toUpperCase() + name.slice(1)
}
2018-01-18 14:53:20 +00:00
export const debounce = <T>(waitFor: number, fn: (...args: T[]) => void) => {
let timeoutId: ReturnType<typeof setTimeout>
2022-03-28 12:57:11 +00:00
return (...args: T[]) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => fn(...args), waitFor)
}
}
export const fetcher = (url: RequestInfo) => fetch(url).then((r) => r.json())
export function inIframe(): boolean {
try {
return window.self !== window.top
} catch (e) {
return true
}
}
🔥 Migration vers TypeScript Outils ====== Ce commit retire le tooling de Flow, et ajoute le support de TypeScript pour les fichiers .ts et .tsx. Il n'est pas nécessaire de tout migrer d'un coup ce qui facilite la transition. On garde en effet le compilateur Babel avec un preset TypeScript (ce qui permet donc de retirer à la fois les types Flow et TypeScript) plutôt que d'utiliser le compilateur standard pour la conversion du code. Cela permet aussi de mieux s'intégrer avec d'autres outils, notamment les test-runners. Ajout d'une nouvelle commande `yarn run type-check`, intégrée dans CircleCI. Par ailleurs ajout du support de l'opérateur ?? pour donner des valeurs par défaut (nullish-coalescing-operator). Typage des libraires tierces ============================ Les principales libraires que nous utilisons ont un typage TypeScript de bon niveau, ce qui facilite l'intégration. J'ai mis à jour react-i18next et i18next afin de corriger un problème de typage. Typage du code ============== Le typage est loin d'être complet dans ce commit, en particulier il manque les types relatifs au state Redux, ainsi qu'au moteur (règle, explication). Néanmoins le typage des contextes fonctionne, en particulier sitePaths (avec un type récursif non trivial !) qui a déjà permis de détecter un lien mort. Le typage des "paths" (Components/, Règles/, etc.) fonctionne bien, y compris avec l'auto-complétion automatique des import par Typescript. TypeScript se révèle déjà bien agréable dans VSCode (auto-complétion, refacto, etc.) ! Reste à migrer progressivement le reste du code !
2019-10-26 16:21:09 +00:00
export function softCatch<ArgType, ReturnType>(
fn: (arg: ArgType) => ReturnType
): (arg: ArgType) => ReturnType | null {
return function (...args) {
try {
return fn(...args)
} catch (e) {
// eslint-disable-next-line no-console
console.warn(e)
2022-03-28 12:57:11 +00:00
return null
}
}
}
export function getSessionStorage() {
// In some browsers like Brave, even just reading the variable sessionStorage
// is throwing an error in the iframe, so we can't do things if sessionStorage !== undefined
// and we need to wrap it in a try { } catch { } logic
try {
return window.sessionStorage
} catch (e) {
return undefined
}
}
export const currencyFormat = (language: string) => ({
isCurrencyPrefixed: !!formatValue(12, { language, displayedUnit: '€' }).match(
/^€/
),
thousandSeparator: formatValue(1000, { language }).charAt(1),
decimalSeparator: formatValue(0.1, { language }).charAt(1),
})
export function hash(str: string): number {
let hash = 0
let chr
for (let i = 0; i < str.length; i++) {
chr = str.charCodeAt(i)
hash = (hash << 5) - hash + chr
hash |= 0 // Convert to 32bit integer
}
2022-03-28 12:57:11 +00:00
return hash
}
export function omit<T, K extends keyof T>(obj: T, key: K): Omit<T, K> {
const { [key]: _ignore, ...returnObject } = obj
2022-03-28 12:57:11 +00:00
return returnObject
}
2022-05-26 16:44:01 +00:00
// TODO: This is will be included in the ES spec soon. Remove our custom
// implementation and rely on browser native support and polyfill when it is
// available.
// https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Global_Objects/Array/groupBy
// https://caniuse.com/?search=groupby
export function groupBy<E, G extends string>(
arr: Array<E>,
callback: (elm: E, index: number, array: Array<E>) => G
): Record<G, Array<E>> {
return arr.reduce((result, item, currentIndex) => {
const key = callback(item, currentIndex, arr)
result[key] = result[key] || []
result[key].push(item)
return result
}, {} as Record<G, Array<E>>)
}
export function isIterable<T>(obj: unknown): obj is Iterable<T> {
return Symbol.iterator in Object(obj)
}
/**
* Check if a key exists in the object and return its value or undefined,
* usefull for type check
* @param obj any object
* @param key key to get value from object
* @returns value or undefined
*/
export const getValueFrom = <
T extends Record<string | number | symbol, unknown>,
K extends string | number | symbol
>(
obj: T,
key: K
): Extract<T, { [k in K]?: unknown }>[K] | undefined =>
key in obj ? obj[key] : undefined
const isMeta = <T>(rule: Rule): rule is Rule & { meta?: T } => 'meta' in rule
/**
* Return typed meta property from a rule
* @param rule
* @param defaultValue
* @returns
*/
export const getMeta = <T>(rule: Rule, defaultValue: T) =>
(isMeta<T>(rule) ? getValueFrom(rule, 'meta') : null) ?? defaultValue
/**
2022-03-17 18:19:40 +00:00
* Wraps each event function specified in eventsToWrap (default onPress) with an
* asynchronous function that waits x ms before executing the original function
* Use this function on button props to avoid ghost click after onPress event
* See this issue https://github.com/betagouv/mon-entreprise/issues/1872
* Maybe the next version of the react-spectrum will handle that natively (issue https://github.com/adobe/react-spectrum/issues/1513)
* @param props
* @param options ms (time of debounce) and eventsToWrap (array of events to wrap with debounce)
* @returns props
*/
export const wrapperDebounceEvents = <T>(
props: T,
2022-03-17 19:07:53 +00:00
{ ms = 25, eventsToWrap = ['onPress'] } = {}
2022-03-17 18:19:40 +00:00
): T => {
if (props && typeof props === 'object') {
const castedProps = props as Record<string, unknown>
eventsToWrap.forEach((event: string) => {
if (event in castedProps) {
const original = castedProps[event]
if (typeof original === 'function') {
const debouncedFunction = async (...params: unknown[]) => {
2022-04-04 15:06:21 +00:00
await new Promise((resolve) =>
setTimeout(() => resolve(original(...params)), ms)
2022-03-17 18:19:40 +00:00
)
}
castedProps[event] = debouncedFunction
}
}
})
}
return props
}
/**
* Return git branch name
* @returns string
*/
export const getBranch = () => {
let branch: string | undefined = import.meta.env.VITE_GITHUB_REF?.split(
'/'
)?.slice(-1)?.[0]
if (branch === 'merge') {
branch = import.meta.env.VITE_GITHUB_HEAD_REF
}
return branch ?? ''
}
/**
* We use this function to hide some features in production while keeping them
* in feature-branches. In case we do A/B testing with several branches served
* in production, we should add the public faced branch names in the test below.
* This is different from the import.meta.env.MODE in that a feature branch may
* be build in production mode (with the NODE_ENV) but we may still want to show
* or hide some features.
* @returns boolean
*/
2022-03-17 14:48:47 +00:00
export const isProduction = () => {
return import.meta.env.PROD && ['master', 'next'].includes(getBranch())
}
/**
* Is a feature branche
* @returns boolean
*/
2022-03-17 14:48:47 +00:00
export const isStaging = () => {
return import.meta.env.PROD && !isProduction()
}
2022-03-17 14:48:47 +00:00
export const isDevelopment = () => {
return import.meta.env.DEV
}
export async function getIframeOffset(): Promise<number> {
return new Promise<number>((resolve, reject) => {
const returnOffset = (evt: MessageEvent) => {
if (evt.data.kind !== 'offset') {
return
}
window.removeEventListener('message', returnOffset)
resolve(evt.data.value)
}
if (!window.parent.postMessage) {
2022-04-04 15:06:21 +00:00
reject(new Error('No parent window'))
}
window.parent?.postMessage({ kind: 'get-offset' }, '*')
window.addEventListener('message', returnOffset)
})
}
export function evaluateQuestion(
engine: Engine,
rule: RuleNode
): string | undefined {
const question = rule.rawNode.question as Exclude<
number,
PublicodesExpression
>
if (question && typeof question === 'object') {
return engine.evaluate(question as PublicodesExpression).nodeValue as string
}
return question
}