Merge pull request #15362 from Budibase/ts/builder-helpers

Typing builder helpers
This commit is contained in:
Adria Navarro 2025-01-14 11:41:11 +01:00 committed by GitHub
commit 9ae483ca30
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 205 additions and 154 deletions

View File

@ -28,7 +28,9 @@
let loading = false
let deleteConfirmationDialog
$: defaultName = getSequentialName($snippets, "MySnippet", x => x.name)
$: defaultName = getSequentialName($snippets, "MySnippet", {
getName: x => x.name,
})
$: key = snippet?.name
$: name = snippet?.name || defaultName
$: code = snippet?.code ? encodeJSBinding(snippet.code) : ""

View File

@ -16,7 +16,10 @@ export {
export const AUTO_COLUMN_SUB_TYPES = AutoFieldSubType
export const AUTO_COLUMN_DISPLAY_NAMES = {
export const AUTO_COLUMN_DISPLAY_NAMES: Record<
keyof typeof AUTO_COLUMN_SUB_TYPES,
string
> = {
AUTO_ID: "Auto ID",
CREATED_BY: "Created By",
CREATED_AT: "Created At",
@ -209,13 +212,6 @@ export const Roles = {
BUILDER: "BUILDER",
}
export function isAutoColumnUserRelationship(subtype) {
return (
subtype === AUTO_COLUMN_SUB_TYPES.CREATED_BY ||
subtype === AUTO_COLUMN_SUB_TYPES.UPDATED_BY
)
}
export const PrettyRelationshipDefinitions = {
MANY: "Many rows",
ONE: "One row",

View File

@ -10,13 +10,13 @@
*
* Repl
*/
export const duplicateName = (name, allNames) => {
export const duplicateName = (name: string, allNames: string[]) => {
const duplicatePattern = new RegExp(`\\s(\\d+)$`)
const baseName = name.split(duplicatePattern)[0]
const isDuplicate = new RegExp(`${baseName}\\s(\\d+)$`)
// get the sequence from matched names
const sequence = []
const sequence: number[] = []
allNames.filter(n => {
if (n === baseName) {
return true
@ -70,12 +70,18 @@ export const duplicateName = (name, allNames) => {
* @param getName optional function to extract the name for an item, if not a
* flat array of strings
*/
export const getSequentialName = (
items,
prefix,
{ getName = x => x, numberFirstItem = false } = {}
export const getSequentialName = <T extends any>(
items: T[] | null,
prefix: string | null,
{
getName,
numberFirstItem,
}: {
getName?: (item: T) => string
numberFirstItem?: boolean
} = {}
) => {
if (!prefix?.length || !getName) {
if (!prefix?.length) {
return null
}
const trimmedPrefix = prefix.trim()
@ -85,7 +91,7 @@ export const getSequentialName = (
}
let max = 0
items.forEach(item => {
const name = getName(item)
const name = getName?.(item) ?? item
if (typeof name !== "string" || !name.startsWith(trimmedPrefix)) {
return
}

View File

@ -1,7 +1,8 @@
import { FeatureFlag } from "@budibase/types"
import { auth } from "../stores/portal"
import { get } from "svelte/store"
export const isEnabled = featureFlag => {
export const isEnabled = (featureFlag: FeatureFlag | `${FeatureFlag}`) => {
const user = get(auth).user
return !!user?.flags?.[featureFlag]
}

View File

@ -1,13 +1,21 @@
import { writable } from "svelte/store"
import { API } from "@/api"
export default function (url) {
const store = writable({ status: "LOADING", data: {}, error: {} })
export default function (url: string) {
const store = writable<{
status: "LOADING" | "SUCCESS" | "ERROR"
data: object
error?: unknown
}>({
status: "LOADING",
data: {},
error: {},
})
async function get() {
store.update(u => ({ ...u, status: "LOADING" }))
try {
const data = await API.get({ url })
const data = await API.get<object>({ url })
store.set({ data, status: "SUCCESS" })
} catch (e) {
store.set({ data: {}, error: e, status: "ERROR" })

View File

@ -1,46 +0,0 @@
import { last, flow } from "lodash/fp"
export const buildStyle = styles => {
let str = ""
for (let s in styles) {
if (styles[s]) {
let key = convertCamel(s)
str += `${key}: ${styles[s]}; `
}
}
return str
}
export const convertCamel = str => {
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`)
}
export const pipe = (arg, funcs) => flow(funcs)(arg)
export const capitalise = s => {
if (!s) {
return s
}
return s.substring(0, 1).toUpperCase() + s.substring(1)
}
export const lowercase = s => s.substring(0, 1).toLowerCase() + s.substring(1)
export const lowercaseExceptFirst = s =>
s.charAt(0) + s.substring(1).toLowerCase()
export const get_name = s => (!s ? "" : last(s.split("/")))
export const get_capitalised_name = name => pipe(name, [get_name, capitalise])
export const isBuilderInputFocused = e => {
const activeTag = document.activeElement?.tagName.toLowerCase()
const inCodeEditor = document.activeElement?.classList?.contains("cm-content")
if (
(inCodeEditor || ["input", "textarea"].indexOf(activeTag) !== -1) &&
e.key !== "Escape"
) {
return true
}
return false
}

View File

@ -0,0 +1,50 @@
import type { Many } from "lodash"
import { last, flow } from "lodash/fp"
export const buildStyle = (styles: Record<string, any>) => {
let str = ""
for (let s in styles) {
if (styles[s]) {
let key = convertCamel(s)
str += `${key}: ${styles[s]}; `
}
}
return str
}
export const convertCamel = (str: string) => {
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`)
}
export const pipe = (arg: string, funcs: Many<(...args: any[]) => any>) =>
flow(funcs)(arg)
export const capitalise = (s: string) => {
if (!s) {
return s
}
return s.substring(0, 1).toUpperCase() + s.substring(1)
}
export const lowercase = (s: string) =>
s.substring(0, 1).toLowerCase() + s.substring(1)
export const lowercaseExceptFirst = (s: string) =>
s.charAt(0) + s.substring(1).toLowerCase()
export const get_name = (s: string) => (!s ? "" : last(s.split("/")))
export const get_capitalised_name = (name: string) =>
pipe(name, [get_name, capitalise])
export const isBuilderInputFocused = (e: KeyboardEvent) => {
const activeTag = document.activeElement?.tagName.toLowerCase()
const inCodeEditor = document.activeElement?.classList?.contains("cm-content")
if (
(inCodeEditor || ["input", "textarea"].indexOf(activeTag!) !== -1) &&
e.key !== "Escape"
) {
return true
}
return false
}

View File

@ -1,7 +0,0 @@
function handleEnter(fnc) {
return e => e.key === "Enter" && fnc()
}
export const keyUtils = {
handleEnter,
}

View File

@ -0,0 +1,7 @@
function handleEnter(fnc: () => void) {
return (e: KeyboardEvent) => e.key === "Enter" && fnc()
}
export const keyUtils = {
handleEnter,
}

View File

@ -1,6 +1,16 @@
import { writable } from "svelte/store"
function defaultValue() {
interface PaginationStore {
nextPage: string | null | undefined
page: string | null | undefined
hasPrevPage: boolean
hasNextPage: boolean
loading: boolean
pageNumber: number
pages: string[]
}
function defaultValue(): PaginationStore {
return {
nextPage: null,
page: undefined,
@ -29,13 +39,13 @@ export function createPaginationStore() {
update(state => {
state.pageNumber++
state.page = state.nextPage
state.pages.push(state.page)
state.pages.push(state.page!)
state.hasPrevPage = state.pageNumber > 1
return state
})
}
function fetched(hasNextPage, nextPage) {
function fetched(hasNextPage: boolean, nextPage: string) {
update(state => {
state.hasNextPage = hasNextPage
state.nextPage = nextPage

View File

@ -1,6 +1,6 @@
import { PlanType } from "@budibase/types"
export function getFormattedPlanName(userPlanType) {
export function getFormattedPlanName(userPlanType: PlanType) {
let planName
switch (userPlanType) {
case PlanType.PRO:
@ -29,6 +29,6 @@ export function getFormattedPlanName(userPlanType) {
return `${planName} Plan`
}
export function isPremiumOrAbove(userPlanType) {
export function isPremiumOrAbove(userPlanType: PlanType) {
return ![PlanType.PRO, PlanType.TEAM, PlanType.FREE].includes(userPlanType)
}

View File

@ -1,4 +1,4 @@
export default function (url) {
export default function (url: string) {
return url
.split("/")
.map(part => {

View File

@ -1,72 +0,0 @@
import { FieldType } from "@budibase/types"
import { ActionStepID } from "@/constants/backend/automations"
import { TableNames } from "@/constants"
import {
AUTO_COLUMN_DISPLAY_NAMES,
AUTO_COLUMN_SUB_TYPES,
FIELDS,
isAutoColumnUserRelationship,
} from "@/constants/backend"
export function getAutoColumnInformation(enabled = true) {
let info = {}
for (const [key, subtype] of Object.entries(AUTO_COLUMN_SUB_TYPES)) {
// Because it's possible to replicate the functionality of CREATED_AT and
// CREATED_BY columns with user column default values, we disable their creation
if (
subtype === AUTO_COLUMN_SUB_TYPES.CREATED_AT ||
subtype === AUTO_COLUMN_SUB_TYPES.CREATED_BY
) {
continue
}
info[subtype] = { enabled, name: AUTO_COLUMN_DISPLAY_NAMES[key] }
}
return info
}
export function buildAutoColumn(tableName, name, subtype) {
let type, constraints
switch (subtype) {
case AUTO_COLUMN_SUB_TYPES.UPDATED_BY:
case AUTO_COLUMN_SUB_TYPES.CREATED_BY:
type = FieldType.LINK
constraints = FIELDS.LINK.constraints
break
case AUTO_COLUMN_SUB_TYPES.AUTO_ID:
type = FieldType.NUMBER
constraints = FIELDS.NUMBER.constraints
break
case AUTO_COLUMN_SUB_TYPES.UPDATED_AT:
case AUTO_COLUMN_SUB_TYPES.CREATED_AT:
type = FieldType.DATETIME
constraints = FIELDS.DATETIME.constraints
break
default:
type = FieldType.STRING
constraints = FIELDS.STRING.constraints
break
}
if (Object.values(AUTO_COLUMN_SUB_TYPES).indexOf(subtype) === -1) {
throw "Cannot build auto column with supplied subtype"
}
const base = {
name,
type,
subtype,
icon: "ri-magic-line",
autocolumn: true,
constraints,
}
if (isAutoColumnUserRelationship(subtype)) {
base.tableId = TableNames.USERS
base.fieldName = `${tableName}-${name}`
}
return base
}
export function checkForCollectStep(automation) {
return automation.definition.steps.some(
step => step.stepId === ActionStepID.COLLECT
)
}

View File

@ -0,0 +1,96 @@
import {
AutoFieldSubType,
Automation,
DateFieldMetadata,
FieldType,
NumberFieldMetadata,
RelationshipFieldMetadata,
RelationshipType,
} from "@budibase/types"
import { ActionStepID } from "@/constants/backend/automations"
import { TableNames } from "@/constants"
import {
AUTO_COLUMN_DISPLAY_NAMES,
AUTO_COLUMN_SUB_TYPES,
FIELDS,
} from "@/constants/backend"
import { utils } from "@budibase/shared-core"
type AutoColumnInformation = Partial<
Record<AutoFieldSubType, { enabled: boolean; name: string }>
>
export function getAutoColumnInformation(
enabled = true
): AutoColumnInformation {
const info: AutoColumnInformation = {}
for (const [key, subtype] of Object.entries(AUTO_COLUMN_SUB_TYPES)) {
// Because it's possible to replicate the functionality of CREATED_AT and
// CREATED_BY columns with user column default values, we disable their creation
if (
subtype === AUTO_COLUMN_SUB_TYPES.CREATED_AT ||
subtype === AUTO_COLUMN_SUB_TYPES.CREATED_BY
) {
continue
}
const typedKey = key as keyof typeof AUTO_COLUMN_SUB_TYPES
info[subtype] = {
enabled,
name: AUTO_COLUMN_DISPLAY_NAMES[typedKey],
}
}
return info
}
export function buildAutoColumn(
tableName: string,
name: string,
subtype: AutoFieldSubType
): RelationshipFieldMetadata | NumberFieldMetadata | DateFieldMetadata {
const base = {
name,
icon: "ri-magic-line",
autocolumn: true,
}
switch (subtype) {
case AUTO_COLUMN_SUB_TYPES.UPDATED_BY:
case AUTO_COLUMN_SUB_TYPES.CREATED_BY:
return {
...base,
type: FieldType.LINK,
subtype,
constraints: FIELDS.LINK.constraints,
tableId: TableNames.USERS,
fieldName: `${tableName}-${name}`,
relationshipType: RelationshipType.MANY_TO_ONE,
}
case AUTO_COLUMN_SUB_TYPES.AUTO_ID:
return {
...base,
type: FieldType.NUMBER,
subtype,
constraints: FIELDS.NUMBER.constraints,
}
case AUTO_COLUMN_SUB_TYPES.UPDATED_AT:
case AUTO_COLUMN_SUB_TYPES.CREATED_AT:
return {
...base,
type: FieldType.DATETIME,
subtype,
constraints: FIELDS.DATETIME.constraints,
}
default:
throw utils.unreachable(subtype, {
message: "Cannot build auto column with supplied subtype",
})
}
}
export function checkForCollectStep(automation: Automation) {
return automation.definition.steps.some(
step => step.stepId === ActionStepID.COLLECT
)
}

View File

@ -1,4 +1,4 @@
export const suppressWarnings = warnings => {
export const suppressWarnings = (warnings: string[]) => {
if (!warnings?.length) {
return
}

View File

@ -62,7 +62,7 @@ export class RowActionStore extends BudiStore<RowActionState> {
const existingRowActions = get(this)[tableId] || []
name = getSequentialName(existingRowActions, "New row action ", {
getName: x => x.name,
})
})!
}
if (!name) {