generated from alphane/template
Initial commit
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { type RouteLocationNormalized } from "vue-router"
|
||||
import { isEmpty, isNil } from "lodash"
|
||||
|
||||
import useCurrentUser, { type UserAsShow } from "@/use/use-current-user"
|
||||
|
||||
export async function authorizationGuard(to: RouteLocationNormalized) {
|
||||
const guardGroups = to.matched
|
||||
.map((record) => record.meta.guards as ((user: UserAsShow) => boolean)[] | undefined)
|
||||
.filter((g): g is ((user: UserAsShow) => boolean)[] => !isNil(g) && !isEmpty(g))
|
||||
|
||||
if (guardGroups.length === 0) return true
|
||||
|
||||
const { currentUser, fetch } = useCurrentUser()
|
||||
if (isNil(currentUser.value)) {
|
||||
await fetch()
|
||||
}
|
||||
|
||||
if (isNil(currentUser.value)) {
|
||||
throw new Error("No current user")
|
||||
}
|
||||
|
||||
// AND across matched records (every level must pass), OR within each
|
||||
// record's guards array (any guard at that level satisfies it).
|
||||
for (const guards of guardGroups) {
|
||||
const passes = guards.some((guard) => {
|
||||
if (typeof guard !== "function") {
|
||||
throw new Error(`Guard ${guard} is not a function`)
|
||||
}
|
||||
return guard(currentUser.value as UserAsShow)
|
||||
})
|
||||
if (!passes) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export default authorizationGuard
|
||||
@@ -0,0 +1,2 @@
|
||||
export { authorizationGuard } from "./authorization-guard"
|
||||
export { isSystemAdmin } from "./user-guards"
|
||||
@@ -0,0 +1,6 @@
|
||||
import { UserRoles } from "@/api/users-api"
|
||||
import { type UserAsShow } from "@/api/current-user-api"
|
||||
|
||||
export function isSystemAdmin(user: UserAsShow): boolean {
|
||||
return user.roles.includes(UserRoles.SYSTEM_ADMIN) ?? false
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { debounce } from "lodash"
|
||||
|
||||
type DebounceableFunction = Parameters<typeof debounce>["0"]
|
||||
|
||||
/**
|
||||
* @example
|
||||
* ```
|
||||
* const usersApi = {
|
||||
* async get(userId, params = {}) {
|
||||
* console.trace(`usersApi.get: Fetching user: ${userId} with params:`, params)
|
||||
* const { data } = await http.get(`/api/users/${userId}`, { params })
|
||||
* return data
|
||||
* },
|
||||
* }
|
||||
*
|
||||
* usersApi.get = debounceWithArgsCache(usersApi.get, 300, 10)
|
||||
*
|
||||
* usersApi.get(1, { role: "admin" })
|
||||
* usersApi.get(2, { role: "user" })
|
||||
* usersApi.get(1, { role: "admin" })
|
||||
* setTimeout(() => usersApi.get(1, { role: "admin" }), 400)
|
||||
* ```
|
||||
*/
|
||||
export function debounceWithArgsCache(
|
||||
fn: DebounceableFunction,
|
||||
{
|
||||
wait = 300,
|
||||
leading = true,
|
||||
trailing = true,
|
||||
cacheSize = 10,
|
||||
}: { wait?: number; leading?: boolean; trailing?: boolean; cacheSize?: number }
|
||||
) {
|
||||
const invocationCache = new Map()
|
||||
|
||||
return (...args: unknown[]) => {
|
||||
const argsKey = JSON.stringify(args)
|
||||
|
||||
if (invocationCache.has(argsKey)) {
|
||||
const debouncedFn = invocationCache.get(argsKey)
|
||||
const result = debouncedFn(...args)
|
||||
return result
|
||||
}
|
||||
|
||||
if (invocationCache.size >= cacheSize) {
|
||||
const oldestInvocation = invocationCache.keys().next().value
|
||||
invocationCache.delete(oldestInvocation)
|
||||
}
|
||||
|
||||
const debouncedFn = debounce(fn, wait, { leading, trailing })
|
||||
invocationCache.set(argsKey, debouncedFn)
|
||||
|
||||
const result = debouncedFn(...args)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
export default debounceWithArgsCache
|
||||
@@ -0,0 +1,13 @@
|
||||
export function formatBytes(bytes: number, decimals = 2) {
|
||||
if (bytes === 0) return "0 Bytes"
|
||||
|
||||
const k = 1024
|
||||
const dm = decimals < 0 ? 0 : decimals
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]
|
||||
}
|
||||
|
||||
export default formatBytes
|
||||
@@ -0,0 +1,21 @@
|
||||
import { isNil, isEmpty } from "lodash"
|
||||
import { DateTime } from "luxon"
|
||||
|
||||
/**
|
||||
* Note: \u00A0d is a non-breaking space, and permits date to wrap like this:
|
||||
* November 07, 72024
|
||||
* @ 710:21 AM
|
||||
*/
|
||||
export function formatDateLong(input: string | Date | undefined): string {
|
||||
if (isNil(input) || isEmpty(input)) {
|
||||
return ""
|
||||
} else if (typeof input == "string") {
|
||||
return DateTime.fromISO(input).toLocal().toFormat("MMMM\u00A0dd,\u00A0yyyy @\u00A0t")
|
||||
} else if (input instanceof Date) {
|
||||
return DateTime.fromJSDate(input).toLocal().toFormat("MMMM\u00A0dd,\u00A0yyyy @\u00A0t")
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
export default formatDateLong
|
||||
@@ -0,0 +1,16 @@
|
||||
import { isNil, isEmpty } from "lodash"
|
||||
import { DateTime } from "luxon"
|
||||
|
||||
export function formatDateWords(input: string | Date | undefined): string {
|
||||
if (isNil(input) || isEmpty(input)) {
|
||||
return ""
|
||||
} else if (typeof input == "string") {
|
||||
return DateTime.fromISO(input).toLocal().toFormat("MMMM dd, yyyy")
|
||||
} else if (input instanceof Date) {
|
||||
return DateTime.fromJSDate(input).toLocal().toFormat("MMMM dd, yyyy")
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
export default formatDateWords
|
||||
@@ -0,0 +1,18 @@
|
||||
import { isNil, isEmpty } from "lodash"
|
||||
import { DateTime } from "luxon"
|
||||
|
||||
export function formatDate(input: string | Date | undefined): string {
|
||||
if (isNil(input) || isEmpty(input)) {
|
||||
return ""
|
||||
} else if (typeof input == "string") {
|
||||
const parsed = DateTime.fromISO(input, { zone: "utc" })
|
||||
const isMidnight = parsed.hour === 0 && parsed.minute === 0 && parsed.second === 0
|
||||
return isMidnight ? parsed.toFormat("yyyy-MM-dd") : parsed.toLocal().toFormat("yyyy-MM-dd")
|
||||
} else if (input instanceof Date) {
|
||||
return DateTime.fromJSDate(input).toLocal().toFormat("yyyy-MM-dd")
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
export default formatDate
|
||||
@@ -0,0 +1,24 @@
|
||||
import { isNil } from "lodash"
|
||||
|
||||
/**
|
||||
* Extracts the numeric part of a string and formats it to desired decimal places
|
||||
*
|
||||
* Ex. -15.030000000000001 Cycles => -15.03 Cycles
|
||||
*
|
||||
* NOTE: number of decimals defaults to 2
|
||||
*/
|
||||
export function formatNumberInStringToFixedDecimal(
|
||||
input: string | null | undefined,
|
||||
decimals: number = 2
|
||||
): string {
|
||||
if (isNil(input) || decimals < 0) {
|
||||
console.warn(
|
||||
`formatNumberInStringToFixedDecimal received invalid input: input=${input}, decimals=${decimals}`
|
||||
)
|
||||
return ""
|
||||
}
|
||||
|
||||
return input.replace(/([-+]?\d[\d,]*\.?\d*)/, (match) => {
|
||||
return parseFloat(match.replace(/,/g, "")).toFixed(decimals)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { isNil, isEmpty } from "lodash"
|
||||
import { DateTime } from "luxon"
|
||||
|
||||
export function formatRelative(input: string | Date | undefined): string {
|
||||
if (isNil(input) || isEmpty(input)) {
|
||||
return ""
|
||||
} else if (typeof input == "string") {
|
||||
return DateTime.fromISO(input).toLocal().toRelative() ?? ""
|
||||
} else if (input instanceof Date) {
|
||||
return DateTime.fromJSDate(input).toLocal().toRelative() ?? ""
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
export default formatRelative
|
||||
@@ -0,0 +1,6 @@
|
||||
export { formatBytes } from "./format-bytes"
|
||||
export { formatDate } from "./format-date"
|
||||
export { formatDateLong } from "./format-date-long"
|
||||
export { formatRelative } from "./format-relative"
|
||||
export { formatDateWords } from "./format-date-words"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export function safeJsonParse(values: string): unknown | null {
|
||||
try {
|
||||
return JSON.parse(values)
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
return null
|
||||
} else {
|
||||
console.warn(`Unexpected json parsing error: ${error}`, { error })
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default safeJsonParse
|
||||
@@ -0,0 +1,3 @@
|
||||
export function sleep(milliseconds: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, milliseconds))
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function stripTrailingSlash(url: string) {
|
||||
return url.endsWith("/") ? url.slice(0, -1) : url
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { isNil } from "lodash"
|
||||
|
||||
export const booleanTransformer = {
|
||||
get(value: boolean | string | null | undefined): boolean {
|
||||
if (isNil(value)) return false
|
||||
if (typeof value === "string") {
|
||||
return value === "true"
|
||||
}
|
||||
|
||||
return value
|
||||
},
|
||||
set(value: boolean | string | null | undefined): string {
|
||||
if (isNil(value)) return "false"
|
||||
if (typeof value === "boolean") {
|
||||
return value.toString()
|
||||
}
|
||||
|
||||
return value
|
||||
},
|
||||
}
|
||||
|
||||
export default booleanTransformer
|
||||
@@ -0,0 +1,4 @@
|
||||
export { booleanTransformer } from "./boolean-transformer"
|
||||
export { integerTransformer } from "./integer-transformer"
|
||||
export { integerTransformerLegacy } from "./integer-transformer-legacy"
|
||||
export { stringTransformer } from "./string-transformer"
|
||||
@@ -0,0 +1,9 @@
|
||||
import { isNil } from "lodash"
|
||||
|
||||
export function integerTransformerLegacy(value: string | null | undefined): number | null {
|
||||
if (isNil(value)) return null
|
||||
|
||||
return parseInt(value)
|
||||
}
|
||||
|
||||
export default integerTransformerLegacy
|
||||
@@ -0,0 +1,27 @@
|
||||
import { isNil } from "lodash"
|
||||
|
||||
function integerGet<T extends string | number | null | undefined>(
|
||||
value: T
|
||||
): number | (T & undefined) | (T & null) {
|
||||
if (isNil(value)) return value
|
||||
if (typeof value === "number") return value
|
||||
|
||||
return parseInt(value, 10)
|
||||
}
|
||||
|
||||
function integerSet<T extends number | string | null | undefined>(
|
||||
value: T
|
||||
): string | (T & undefined) | (T & null) {
|
||||
if (isNil(value)) return value
|
||||
if (typeof value === "number") return value.toString()
|
||||
if (typeof value === "string") return value
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
export const integerTransformer = {
|
||||
get: integerGet,
|
||||
set: integerSet,
|
||||
} as const
|
||||
|
||||
export default integerTransformer
|
||||
@@ -0,0 +1,16 @@
|
||||
import { isNil } from "lodash"
|
||||
|
||||
export const stringTransformer = {
|
||||
get(value: string | null | undefined) {
|
||||
if (isNil(value)) return ""
|
||||
|
||||
return value
|
||||
},
|
||||
set(value: string | null | undefined) {
|
||||
if (isNil(value)) return ""
|
||||
|
||||
return value
|
||||
},
|
||||
}
|
||||
|
||||
export default stringTransformer
|
||||
@@ -0,0 +1,5 @@
|
||||
export type VueHtmlClass = string | Record<string, boolean> | (string | Record<string, boolean>)[]
|
||||
|
||||
export type SetIntersection<T extends object[]> = {
|
||||
[K in keyof T[number]]: K extends keyof T[0] ? Extract<T[number][K], T[number][K]> : never
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import isEmail from "validator/lib/isEmail"
|
||||
|
||||
export function email(value: string) {
|
||||
if (isEmail(value)) return true
|
||||
|
||||
return "Invalid email"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { email } from "./email"
|
||||
export { minimum } from "./minimum"
|
||||
export { required } from "./required"
|
||||
export { requiredFile } from "./required-file"
|
||||
@@ -0,0 +1,13 @@
|
||||
import { isString } from "lodash"
|
||||
|
||||
export function minimum(length: number) {
|
||||
return (v: unknown): boolean | string => {
|
||||
if (isString(v) && v.length < length) {
|
||||
return `Must be at least ${length} characters`
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export default minimum
|
||||
@@ -0,0 +1,20 @@
|
||||
export function requiredFile(v: unknown): boolean | string {
|
||||
// Handle File object
|
||||
if (v instanceof File) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Handle null or undefined
|
||||
if (v === null || v === undefined) {
|
||||
return "File is required"
|
||||
}
|
||||
|
||||
// Handle empty array (when file input is cleared)
|
||||
if (Array.isArray(v) && v.length === 0) {
|
||||
return "File is required"
|
||||
}
|
||||
|
||||
return "File is required"
|
||||
}
|
||||
|
||||
export default requiredFile
|
||||
@@ -0,0 +1,15 @@
|
||||
import { isArray, isEmpty, isString, isNull, isObject, isUndefined } from "lodash"
|
||||
|
||||
export function required(v: unknown): boolean | string {
|
||||
if (isNull(v) || isUndefined(v)) {
|
||||
return "This field is required"
|
||||
}
|
||||
|
||||
if ((isArray(v) || isString(v) || isObject(v)) && isEmpty(v)) {
|
||||
return "This field is required"
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export default required
|
||||
Reference in New Issue
Block a user