2023-03-09 09:50:26 +01:00
|
|
|
import { helpers } from "@budibase/shared-core"
|
2024-04-08 09:29:20 +02:00
|
|
|
import dayjs from "dayjs"
|
2023-11-20 21:52:29 +01:00
|
|
|
|
2023-03-09 09:50:26 +01:00
|
|
|
export const deepGet = helpers.deepGet
|
|
|
|
|
2022-01-18 10:39:19 +01:00
|
|
|
/**
|
|
|
|
* Generates a DOM safe UUID.
|
|
|
|
* Starting with a letter is important to make it DOM safe.
|
|
|
|
* @return {string} a random DOM safe UUID
|
|
|
|
*/
|
|
|
|
export function uuid() {
|
|
|
|
return "cxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => {
|
|
|
|
const r = (Math.random() * 16) | 0
|
|
|
|
const v = c === "x" ? r : (r & 0x3) | 0x8
|
|
|
|
return v.toString(16)
|
|
|
|
})
|
|
|
|
}
|
2021-04-20 21:06:27 +02:00
|
|
|
|
2022-01-18 10:39:19 +01:00
|
|
|
/**
|
|
|
|
* Capitalises a string
|
|
|
|
* @param string the string to capitalise
|
|
|
|
* @return {string} the capitalised string
|
|
|
|
*/
|
|
|
|
export const capitalise = string => {
|
|
|
|
if (!string) {
|
|
|
|
return string
|
|
|
|
}
|
|
|
|
return string.substring(0, 1).toUpperCase() + string.substring(1)
|
2021-04-20 21:06:27 +02:00
|
|
|
}
|
2021-06-23 12:47:07 +02:00
|
|
|
|
2022-01-18 10:39:19 +01:00
|
|
|
/**
|
|
|
|
* Computes a short hash of a string
|
|
|
|
* @param string the string to compute a hash of
|
|
|
|
* @return {string} the hash string
|
|
|
|
*/
|
|
|
|
export const hashString = string => {
|
|
|
|
if (!string) {
|
|
|
|
return "0"
|
|
|
|
}
|
|
|
|
let hash = 0
|
|
|
|
for (let i = 0; i < string.length; i++) {
|
|
|
|
let char = string.charCodeAt(i)
|
|
|
|
hash = (hash << 5) - hash + char
|
|
|
|
hash = hash & hash // Convert to 32bit integer
|
|
|
|
}
|
|
|
|
return hash.toString()
|
|
|
|
}
|
2021-12-06 13:37:50 +01:00
|
|
|
|
2021-12-10 15:18:01 +01:00
|
|
|
/**
|
|
|
|
* Sets a key within an object. The key supports dot syntax for retrieving deep
|
|
|
|
* fields - e.g. "a.b.c".
|
|
|
|
* Exact matches of keys with dots in them take precedence over nested keys of
|
|
|
|
* the same path - e.g. setting "a.b" of { "a.b": "foo", a: { b: "bar" } }
|
|
|
|
* will override the value "foo" rather than "bar".
|
2021-12-10 16:27:04 +01:00
|
|
|
* If a deep path is specified and the parent keys don't exist then these will
|
|
|
|
* be created.
|
2021-12-10 15:18:01 +01:00
|
|
|
* @param obj the object
|
|
|
|
* @param key the key
|
|
|
|
* @param value the value
|
|
|
|
*/
|
|
|
|
export const deepSet = (obj, key, value) => {
|
|
|
|
if (!obj || !key) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
|
|
obj[key] = value
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const split = key.split(".")
|
|
|
|
for (let i = 0; i < split.length - 1; i++) {
|
2021-12-10 16:27:04 +01:00
|
|
|
const nextKey = split[i]
|
|
|
|
if (obj && obj[nextKey] == null) {
|
|
|
|
obj[nextKey] = {}
|
|
|
|
}
|
|
|
|
obj = obj?.[nextKey]
|
|
|
|
}
|
|
|
|
if (!obj) {
|
|
|
|
return
|
2021-12-10 15:18:01 +01:00
|
|
|
}
|
|
|
|
obj[split[split.length - 1]] = value
|
2021-12-06 13:37:50 +01:00
|
|
|
}
|
2022-01-31 10:32:06 +01:00
|
|
|
|
2022-01-31 19:58:19 +01:00
|
|
|
/**
|
|
|
|
* Deeply clones an object. Functions are not supported.
|
|
|
|
* @param obj the object to clone
|
|
|
|
*/
|
2022-01-31 10:32:06 +01:00
|
|
|
export const cloneDeep = obj => {
|
2023-02-23 14:55:18 +01:00
|
|
|
if (!obj) {
|
|
|
|
return obj
|
|
|
|
}
|
2022-01-31 10:32:06 +01:00
|
|
|
return JSON.parse(JSON.stringify(obj))
|
|
|
|
}
|
2022-02-24 22:48:23 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Copies a value to the clipboard
|
|
|
|
* @param value the value to copy
|
|
|
|
*/
|
|
|
|
export const copyToClipboard = value => {
|
|
|
|
return new Promise(res => {
|
|
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
|
|
// Try using the clipboard API first
|
|
|
|
navigator.clipboard.writeText(value).then(res)
|
|
|
|
} else {
|
|
|
|
// Fall back to the textarea hack
|
|
|
|
let textArea = document.createElement("textarea")
|
|
|
|
textArea.value = value
|
|
|
|
textArea.style.position = "fixed"
|
|
|
|
textArea.style.left = "-9999px"
|
|
|
|
textArea.style.top = "-9999px"
|
|
|
|
document.body.appendChild(textArea)
|
|
|
|
textArea.focus()
|
|
|
|
textArea.select()
|
|
|
|
document.execCommand("copy")
|
|
|
|
textArea.remove()
|
|
|
|
res()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2024-04-08 09:29:20 +02:00
|
|
|
|
2024-04-23 18:00:15 +02:00
|
|
|
// Parsed a date value. This is usually an ISO string, but can be a
|
|
|
|
// bunch of different formats and shapes depending on schema flags.
|
|
|
|
export const parseDate = (value, { enableTime = true }) => {
|
2024-04-08 09:29:20 +02:00
|
|
|
// If empty then invalid
|
|
|
|
if (!value) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
// Certain string values need transformed
|
2024-04-10 19:46:41 +02:00
|
|
|
if (typeof value === "string") {
|
2024-04-12 10:40:39 +02:00
|
|
|
// Check for time only values
|
|
|
|
if (!isNaN(new Date(`0-${value}`))) {
|
2024-04-08 09:29:20 +02:00
|
|
|
value = `0-${value}`
|
|
|
|
}
|
|
|
|
|
|
|
|
// If date only, check for cases where we received a UTC string
|
2024-04-23 18:00:15 +02:00
|
|
|
else if (!enableTime && value.endsWith("Z")) {
|
2024-04-08 09:29:20 +02:00
|
|
|
value = value.split("Z")[0]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse value and check for validity
|
|
|
|
const parsedDate = dayjs(value)
|
|
|
|
if (!parsedDate.isValid()) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
// By rounding to the nearest second we avoid locking up in an endless
|
|
|
|
// loop in the builder, caused by potentially enriching {{ now }} to every
|
|
|
|
// millisecond.
|
|
|
|
return dayjs(Math.floor(parsedDate.valueOf() / 1000) * 1000)
|
|
|
|
}
|
2024-04-12 10:40:39 +02:00
|
|
|
|
2024-04-23 18:00:15 +02:00
|
|
|
// Stringifies a dayjs object to create an ISO string that respects the various
|
|
|
|
// schema flags
|
|
|
|
export const stringifyDate = (
|
|
|
|
value,
|
2024-04-26 17:25:41 +02:00
|
|
|
{ enableTime = true, timeOnly = false, ignoreTimezones = false } = {}
|
2024-04-23 18:00:15 +02:00
|
|
|
) => {
|
|
|
|
if (!value) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
// Time only fields always ignore timezones, otherwise they make no sense.
|
|
|
|
// For non-timezone-aware fields, create an ISO 8601 timestamp of the exact
|
|
|
|
// time picked, without timezone
|
|
|
|
const offsetForTimezone = (enableTime && ignoreTimezones) || timeOnly
|
|
|
|
if (offsetForTimezone) {
|
|
|
|
// Ensure we use the correct offset for the date
|
2024-05-20 13:09:28 +02:00
|
|
|
const referenceDate = value.toDate()
|
2024-04-23 18:00:15 +02:00
|
|
|
const offset = referenceDate.getTimezoneOffset() * 60000
|
2024-05-23 11:33:50 +02:00
|
|
|
const date = new Date(value.valueOf() - offset)
|
|
|
|
if (timeOnly) {
|
2024-05-23 13:00:50 +02:00
|
|
|
// Extract HH:mm
|
|
|
|
return date.toISOString().slice(11, 16)
|
2024-05-23 11:33:50 +02:00
|
|
|
}
|
|
|
|
return date.toISOString().slice(0, -1)
|
2024-04-23 18:00:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// For date-only fields, construct a manual timestamp string without a time
|
|
|
|
// or time zone
|
|
|
|
else if (!enableTime) {
|
|
|
|
const year = value.year()
|
|
|
|
const month = `${value.month() + 1}`.padStart(2, "0")
|
|
|
|
const day = `${value.date()}`.padStart(2, "0")
|
2024-05-22 14:35:19 +02:00
|
|
|
return `${year}-${month}-${day}`
|
2024-04-23 18:00:15 +02:00
|
|
|
}
|
2024-04-25 15:26:01 +02:00
|
|
|
|
|
|
|
// Otherwise use a normal ISO string with time and timezone
|
|
|
|
else {
|
|
|
|
return value.toISOString()
|
|
|
|
}
|
2024-04-23 18:00:15 +02:00
|
|
|
}
|
|
|
|
|
2024-04-26 13:40:15 +02:00
|
|
|
// Determine the dayjs-compatible format of the browser's default locale
|
|
|
|
const getPatternForPart = part => {
|
|
|
|
switch (part.type) {
|
|
|
|
case "day":
|
|
|
|
return "D".repeat(part.value.length)
|
|
|
|
case "month":
|
|
|
|
return "M".repeat(part.value.length)
|
|
|
|
case "year":
|
|
|
|
return "Y".repeat(part.value.length)
|
|
|
|
case "literal":
|
|
|
|
return part.value
|
|
|
|
default:
|
|
|
|
console.log("Unsupported date part", part)
|
|
|
|
return ""
|
2024-04-26 10:39:17 +02:00
|
|
|
}
|
|
|
|
}
|
2024-04-26 13:40:15 +02:00
|
|
|
const localeDateFormat = new Intl.DateTimeFormat()
|
|
|
|
.formatToParts(new Date("2021-01-01"))
|
|
|
|
.map(getPatternForPart)
|
|
|
|
.join("")
|
2024-04-26 10:39:17 +02:00
|
|
|
|
2024-04-23 18:00:15 +02:00
|
|
|
// Formats a dayjs date according to schema flags
|
|
|
|
export const getDateDisplayValue = (
|
|
|
|
value,
|
2024-04-26 17:25:41 +02:00
|
|
|
{ enableTime = true, timeOnly = false } = {}
|
2024-04-23 18:00:15 +02:00
|
|
|
) => {
|
2024-04-12 10:40:39 +02:00
|
|
|
if (!value?.isValid()) {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
if (timeOnly) {
|
|
|
|
return value.format("HH:mm")
|
|
|
|
} else if (!enableTime) {
|
2024-04-26 10:39:17 +02:00
|
|
|
return value.format(localeDateFormat)
|
2024-04-12 10:40:39 +02:00
|
|
|
} else {
|
2024-04-26 13:40:15 +02:00
|
|
|
return value.format(`${localeDateFormat} HH:mm`)
|
2024-04-12 10:40:39 +02:00
|
|
|
}
|
|
|
|
}
|