ICTU/quality-time

View on GitHub
components/frontend/src/hooks/url_search_query.js

Summary

Maintainability
A
1 hr
Test Coverage
import history from "history/browser"
import { useState } from "react"

const registeredURLSearchQueryKeys = new Set(["report_date", "report_url", "hide_toasts"])

export function registeredURLSearchParams() {
    // Return registered URL search parameters only; to prevent untrusted URL redirection by the client.
    // (Reported by CodeQL as js/client-side-unvalidated-url-redirection)
    let parsed = new URLSearchParams(history.location.search)
    for (let key of parsed.keys()) {
        if (!registeredURLSearchQueryKeys.has(key)) {
            parsed.delete(key)
        }
    }
    return parsed
}

function equals(value1, value2) {
    return JSON.stringify(value1) === JSON.stringify(value2)
}

function parseURLSearchQuery() {
    return new URLSearchParams(history.location.search)
}

function setURLSearchQuery(key, newValue, defaultValue, setValue) {
    let parsed = parseURLSearchQuery()
    if (equals(newValue, defaultValue)) {
        parsed.delete(key)
    } else {
        parsed.set(key, newValue)
    }
    const search = parsed.toString().replace(/%2C/g, ",") // No need to encode commas
    history.replace({ search: search.length > 0 ? "?" + search : "" })
    setValue(newValue)
}

function createHook(key, value, defaultValue, setValue) {
    registeredURLSearchQueryKeys.add(key)
    let hook = {}
    hook.equals = (otherValue) => equals(value, otherValue)
    hook.isDefault = () => equals(value, defaultValue)
    hook.reset = () => setURLSearchQuery(key, defaultValue, defaultValue, setValue)
    hook.set = (newValue) => setURLSearchQuery(key, newValue, defaultValue, setValue)
    hook.value = value
    return hook
}

export function useArrayURLSearchQuery(key) {
    const parsedValue = parseURLSearchQuery().get(key)?.split(",") ?? []
    const [value, setValue] = useState(parsedValue)
    const defaultValue = []

    function toggleURLSearchQuery(...items) {
        const newValue = []
        value.forEach((item) => {
            if (!items.includes(item)) {
                newValue.push(item)
            }
        })
        items.forEach((item) => {
            if (!value.includes(item)) {
                newValue.push(item)
            }
        })
        setURLSearchQuery(key, newValue, defaultValue, setValue)
    }

    let hook = createHook(key, value, defaultValue, setValue)
    hook.excludes = (item) => !value.includes(item)
    hook.includes = (item) => value.includes(item)
    hook.toggle = toggleURLSearchQuery
    return hook
}

export function useBooleanURLSearchQuery(key) {
    const parsedValue = parseURLSearchQuery().get(key) === "true"
    const [value, setValue] = useState(parsedValue)
    return createHook(key, value, false, setValue)
}

export function useIntegerURLSearchQuery(key, defaultValue) {
    const searchQueryValue = parseURLSearchQuery().get(key)
    const parsedValue = typeof searchQueryValue === "string" ? parseInt(searchQueryValue, 10) : defaultValue
    const [value, setValue] = useState(parsedValue)
    return createHook(key, value, defaultValue, setValue)
}

export function useStringURLSearchQuery(key, defaultValue) {
    const searchQueryValue = parseURLSearchQuery().get(key)
    const [value, setValue] = useState(searchQueryValue ?? defaultValue)
    return createHook(key, value, defaultValue, setValue)
}