Merge pull request #15655 from Budibase/ts/form

Client form to TS
This commit is contained in:
Adria Navarro 2025-03-20 12:46:40 +01:00 committed by GitHub
commit 0c35c2612f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 218 additions and 139 deletions

View File

@ -1,38 +1,39 @@
<script> <script lang="ts">
import { getContext } from "svelte" import { getContext } from "svelte"
import InnerForm from "./InnerForm.svelte" import InnerForm from "./InnerForm.svelte"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { writable } from "svelte/store" import { writable } from "svelte/store"
import type { DataFetchDatasource, Table, TableSchema } from "@budibase/types"
export let dataSource export let dataSource: DataFetchDatasource
export let size export let size: "Medium" | "Large"
export let disabled = false export let disabled: boolean = false
export let readonly = false export let readonly: boolean = false
export let actionType = "Create" export let actionType: "Create" = "Create"
export let initialFormStep = 1 export let initialFormStep: string | number = 1
// Not exposed as a builder setting. Used internally to disable validation // Not exposed as a builder setting. Used internally to disable validation
// for fields rendered in things like search blocks. // for fields rendered in things like search blocks.
export let disableSchemaValidation = false export let disableSchemaValidation: boolean = false
// Not exposed as a builder setting. Used internally to allow searching on // Not exposed as a builder setting. Used internally to allow searching on
// auto columns. // auto columns.
export let editAutoColumns = false export let editAutoColumns: boolean = false
const context = getContext("context") const context = getContext("context")
const component = getContext("component") const component = getContext("component")
const { fetchDatasourceSchema, fetchDatasourceDefinition } = getContext("sdk") const { fetchDatasourceSchema, fetchDatasourceDefinition } = getContext("sdk")
const getInitialFormStep = () => { const getInitialFormStep = () => {
const parsedFormStep = parseInt(initialFormStep) const parsedFormStep = parseInt(initialFormStep.toString())
if (isNaN(parsedFormStep)) { if (isNaN(parsedFormStep)) {
return 1 return 1
} }
return parsedFormStep return parsedFormStep
} }
let definition let definition: Table | undefined
let schema let schema: TableSchema | undefined
let loaded = false let loaded = false
let currentStep = getContext("current-step") || writable(getInitialFormStep()) let currentStep = getContext("current-step") || writable(getInitialFormStep())
@ -49,7 +50,12 @@
) )
// Returns the closes data context which isn't a built in context // Returns the closes data context which isn't a built in context
const getInitialValues = (type, dataSource, path, context) => { const getInitialValues = (
type: string,
dataSource: DataFetchDatasource,
path: string[],
context: Record<string, any>
) => {
// Only inherit values for update forms // Only inherit values for update forms
if (type !== "Update") { if (type !== "Update") {
return {} return {}
@ -82,11 +88,11 @@
} }
// Fetches the form schema from this form's dataSource // Fetches the form schema from this form's dataSource
const fetchSchema = async dataSource => { const fetchSchema = async (dataSource: DataFetchDatasource) => {
try { try {
definition = await fetchDatasourceDefinition(dataSource) definition = await fetchDatasourceDefinition(dataSource)
} catch (error) { } catch (error) {
definition = null definition = undefined
} }
const res = await fetchDatasourceSchema(dataSource) const res = await fetchDatasourceSchema(dataSource)
schema = res || {} schema = res || {}
@ -98,7 +104,7 @@
// Generates a predictable string that uniquely identifies a schema. We can't // Generates a predictable string that uniquely identifies a schema. We can't
// simply stringify the whole schema as there are array fields which have // simply stringify the whole schema as there are array fields which have
// random order. // random order.
const generateSchemaKey = schema => { const generateSchemaKey = (schema: TableSchema | undefined) => {
if (!schema) { if (!schema) {
return null return null
} }

View File

@ -1,31 +1,62 @@
<script> <script lang="ts">
import { setContext, getContext } from "svelte" import { setContext, getContext } from "svelte"
import type { Readable, Writable } from "svelte/store"
import { derived, get, writable } from "svelte/store" import { derived, get, writable } from "svelte/store"
import { createValidatorFromConstraints } from "./validation" import { createValidatorFromConstraints } from "./validation"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import type {
DataFetchDatasource,
FieldSchema,
FieldType,
Table,
TableSchema,
UIFieldValidationRule,
} from "@budibase/types"
export let dataSource = undefined type FieldInfo<T = any> = {
export let disabled = false name: string
export let readonly = false step: number
export let initialValues = undefined type: `${FieldType}`
export let size = undefined fieldState: {
export let schema = undefined fieldId: string
export let definition = undefined value: T
export let disableSchemaValidation = false defaultValue: T
export let editAutoColumns = false disabled: boolean
readonly: boolean
validator: ((_value: T) => string | null) | null
error: string | null | undefined
lastUpdate: number
}
fieldApi: {
setValue(_value: T): void
validate(): boolean
reset(): void
}
fieldSchema: FieldSchema | {}
}
export let dataSource: DataFetchDatasource | undefined = undefined
export let disabled: boolean = false
export let readonly: boolean = false
export let initialValues: Record<string, any> | undefined = undefined
export let size: "Medium" | "Large" | undefined = undefined
export let schema: TableSchema | undefined = undefined
export let definition: Table | undefined = undefined
export let disableSchemaValidation: boolean = false
export let editAutoColumns: boolean = false
// For internal use only, to disable context when being used with standalone // For internal use only, to disable context when being used with standalone
// fields // fields
export let provideContext = true export let provideContext: boolean = true
// We export this store so that when we remount the inner form we can still // We export this store so that when we remount the inner form we can still
// persist what step we're on // persist what step we're on
export let currentStep export let currentStep: Writable<number>
const component = getContext("component") const component = getContext("component")
const { styleable, Provider, ActionTypes } = getContext("sdk") const { styleable, Provider, ActionTypes } = getContext("sdk")
let fields = [] let fields: Writable<FieldInfo>[] = []
const formState = writable({ const formState = writable({
values: {}, values: {},
errors: {}, errors: {},
@ -75,19 +106,24 @@
// Generates a derived store from an array of fields, comprised of a map of // Generates a derived store from an array of fields, comprised of a map of
// extracted values from the field array // extracted values from the field array
const deriveFieldProperty = (fieldStores, getProp) => { const deriveFieldProperty = (
fieldStores: Readable<FieldInfo>[],
getProp: (_field: FieldInfo) => any
) => {
return derived(fieldStores, fieldValues => { return derived(fieldStores, fieldValues => {
const reducer = (map, field) => ({ ...map, [field.name]: getProp(field) }) return fieldValues.reduce(
return fieldValues.reduce(reducer, {}) (map, field) => ({ ...map, [field.name]: getProp(field) }),
{}
)
}) })
} }
// Derives any enrichments which need to be made so that bindings work for // Derives any enrichments which need to be made so that bindings work for
// special data types like attachments. Relationships are currently not // special data types like attachments. Relationships are currently not
// handled as we don't have the primaryDisplay field that is required. // handled as we don't have the primaryDisplay field that is required.
const deriveBindingEnrichments = fieldStores => { const deriveBindingEnrichments = (fieldStores: Readable<FieldInfo>[]) => {
return derived(fieldStores, fieldValues => { return derived(fieldStores, fieldValues => {
let enrichments = {} const enrichments: Record<string, string> = {}
fieldValues.forEach(field => { fieldValues.forEach(field => {
if (field.type === "attachment") { if (field.type === "attachment") {
const value = field.fieldState.value const value = field.fieldState.value
@ -104,7 +140,11 @@
// Derive the overall form value and deeply set all field paths so that we // Derive the overall form value and deeply set all field paths so that we
// can support things like JSON fields. // can support things like JSON fields.
const deriveFormValue = (initialValues, values, enrichments) => { const deriveFormValue = (
initialValues: Record<string, any> | undefined,
values: Record<string, any>,
enrichments: Record<string, string>
) => {
let formValue = Helpers.cloneDeep(initialValues || {}) let formValue = Helpers.cloneDeep(initialValues || {})
// We need to sort the keys to avoid a JSON field overwriting a nested field // We need to sort the keys to avoid a JSON field overwriting a nested field
@ -118,7 +158,7 @@
} }
}) })
.sort((a, b) => { .sort((a, b) => {
return a.lastUpdate > b.lastUpdate return a.lastUpdate - b.lastUpdate
}) })
// Merge all values and enrichments into a single value // Merge all values and enrichments into a single value
@ -132,12 +172,16 @@
} }
// Searches the field array for a certain field // Searches the field array for a certain field
const getField = name => { const getField = (name: string) => {
return fields.find(field => get(field).name === name) return fields.find(field => get(field).name === name)!
} }
// Sanitises a value by ensuring it doesn't contain any invalid data // Sanitises a value by ensuring it doesn't contain any invalid data
const sanitiseValue = (value, schema, type) => { const sanitiseValue = (
value: any,
schema: FieldSchema | undefined,
type: `${FieldType}`
) => {
// Check arrays - remove any values not present in the field schema and // Check arrays - remove any values not present in the field schema and
// convert any values supplied to strings // convert any values supplied to strings
if (Array.isArray(value) && type === "array" && schema) { if (Array.isArray(value) && type === "array" && schema) {
@ -149,13 +193,13 @@
const formApi = { const formApi = {
registerField: ( registerField: (
field, field: string,
type, type: FieldType,
defaultValue = null, defaultValue: string | null = null,
fieldDisabled = false, fieldDisabled: boolean = false,
fieldReadOnly = false, fieldReadOnly: boolean = false,
validationRules, validationRules: UIFieldValidationRule[],
step = 1 step: number = 1
) => { ) => {
if (!field) { if (!field) {
return return
@ -200,7 +244,7 @@
const isAutoColumn = !!schema?.[field]?.autocolumn const isAutoColumn = !!schema?.[field]?.autocolumn
// Construct field info // Construct field info
const fieldInfo = writable({ const fieldInfo = writable<FieldInfo>({
name: field, name: field,
type, type,
step: step || 1, step: step || 1,
@ -210,7 +254,8 @@
error: initialError, error: initialError,
disabled: disabled:
disabled || fieldDisabled || (isAutoColumn && !editAutoColumns), disabled || fieldDisabled || (isAutoColumn && !editAutoColumns),
readonly: readonly || fieldReadOnly || schema?.[field]?.readonly, readonly:
readonly || fieldReadOnly || (schema?.[field] as any)?.readonly,
defaultValue, defaultValue,
validator, validator,
lastUpdate: Date.now(), lastUpdate: Date.now(),
@ -254,7 +299,13 @@
get(field).fieldApi.reset() get(field).fieldApi.reset()
}) })
}, },
changeStep: ({ type, number }) => { changeStep: ({
type,
number,
}: {
type: "next" | "prev" | "first" | "specific"
number: any
}) => {
if (type === "next") { if (type === "next") {
currentStep.update(step => step + 1) currentStep.update(step => step + 1)
} else if (type === "prev") { } else if (type === "prev") {
@ -265,12 +316,12 @@
currentStep.set(parseInt(number)) currentStep.set(parseInt(number))
} }
}, },
setStep: step => { setStep: (step: number) => {
if (step) { if (step) {
currentStep.set(step) currentStep.set(step)
} }
}, },
setFieldValue: (fieldName, value) => { setFieldValue: (fieldName: string, value: any) => {
const field = getField(fieldName) const field = getField(fieldName)
if (!field) { if (!field) {
return return
@ -278,7 +329,7 @@
const { fieldApi } = get(field) const { fieldApi } = get(field)
fieldApi.setValue(value) fieldApi.setValue(value)
}, },
resetField: fieldName => { resetField: (fieldName: string) => {
const field = getField(fieldName) const field = getField(fieldName)
if (!field) { if (!field) {
return return
@ -289,9 +340,9 @@
} }
// Creates an API for a specific field // Creates an API for a specific field
const makeFieldApi = field => { const makeFieldApi = (field: string) => {
// Sets the value for a certain field and invokes validation // Sets the value for a certain field and invokes validation
const setValue = (value, skipCheck = false) => { const setValue = (value: any, skipCheck = false) => {
const fieldInfo = getField(field) const fieldInfo = getField(field)
const { fieldState } = get(fieldInfo) const { fieldState } = get(fieldInfo)
const { validator } = fieldState const { validator } = fieldState
@ -328,36 +379,6 @@
}) })
} }
// Updates the validator rules for a certain field
const updateValidation = validationRules => {
const fieldInfo = getField(field)
const { fieldState } = get(fieldInfo)
const { value, error } = fieldState
// Create new validator
const schemaConstraints = disableSchemaValidation
? null
: schema?.[field]?.constraints
const validator = createValidatorFromConstraints(
schemaConstraints,
validationRules,
field,
definition
)
// Update validator
fieldInfo.update(state => {
state.fieldState.validator = validator
return state
})
// If there is currently an error, run the validator again in case
// the error should be cleared by the new validation rules
if (error) {
setValue(value, true)
}
}
// We don't want to actually remove the field state when deregistering, just // We don't want to actually remove the field state when deregistering, just
// remove any errors and validation // remove any errors and validation
const deregister = () => { const deregister = () => {
@ -370,7 +391,7 @@
} }
// Updates the disabled state of a certain field // Updates the disabled state of a certain field
const setDisabled = fieldDisabled => { const setDisabled = (fieldDisabled: boolean) => {
const fieldInfo = getField(field) const fieldInfo = getField(field)
// Auto columns are always disabled // Auto columns are always disabled
@ -386,7 +407,6 @@
return { return {
setValue, setValue,
reset, reset,
updateValidation,
setDisabled, setDisabled,
deregister, deregister,
validate: () => { validate: () => {
@ -412,7 +432,15 @@
// register their fields to step 1 // register their fields to step 1
setContext("form-step", writable(1)) setContext("form-step", writable(1))
const handleUpdateFieldValue = ({ type, field, value }) => { const handleUpdateFieldValue = ({
type,
field,
value,
}: {
type: "set" | "reset"
field: string
value: any
}) => {
if (type === "set") { if (type === "set") {
formApi.setFieldValue(field, value) formApi.setFieldValue(field, value)
} else { } else {
@ -420,16 +448,19 @@
} }
} }
const handleScrollToField = ({ field }) => { const handleScrollToField = (props: { field: FieldInfo | string }) => {
if (!field.fieldState) { let field
field = get(getField(field)) if (typeof props.field === "string") {
field = get(getField(props.field))
} else {
field = props.field
} }
const fieldId = field.fieldState.fieldId const fieldId = field.fieldState.fieldId
const fieldElement = document.getElementById(fieldId) const fieldElement = document.getElementById(fieldId)
if (fieldElement) { if (fieldElement) {
fieldElement.focus({ preventScroll: true }) fieldElement.focus({ preventScroll: true })
} }
const label = document.querySelector(`label[for="${fieldId}"]`) const label = document.querySelector<HTMLElement>(`label[for="${fieldId}"]`)
if (label) { if (label) {
label.style.scrollMargin = "100px" label.style.scrollMargin = "100px"
label.scrollIntoView({ behavior: "smooth", block: "nearest" }) label.scrollIntoView({ behavior: "smooth", block: "nearest" })

View File

@ -1,6 +1,11 @@
import dayjs from "dayjs" import dayjs from "dayjs"
import { FieldTypes } from "../../../constants"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import {
FieldConstraints,
FieldType,
Table,
UIFieldValidationRule,
} from "@budibase/types"
/** /**
* Creates a validation function from a combination of schema-level constraints * Creates a validation function from a combination of schema-level constraints
@ -12,19 +17,19 @@ import { Helpers } from "@budibase/bbui"
* @returns {function} a validator function which accepts test values * @returns {function} a validator function which accepts test values
*/ */
export const createValidatorFromConstraints = ( export const createValidatorFromConstraints = (
schemaConstraints, schemaConstraints: FieldConstraints | null | undefined,
customRules, customRules: UIFieldValidationRule[],
field, field: string,
definition definition: Table | undefined
) => { ) => {
let rules = [] let rules: UIFieldValidationRule[] = []
// Convert schema constraints into validation rules // Convert schema constraints into validation rules
if (schemaConstraints) { if (schemaConstraints) {
// Required constraint // Required constraint
if ( if (
field === definition?.primaryDisplay || field === definition?.primaryDisplay ||
schemaConstraints.presence?.allowEmpty === false || (schemaConstraints.presence as any)?.allowEmpty === false ||
schemaConstraints.presence === true schemaConstraints.presence === true
) { ) {
rules.push({ rules.push({
@ -106,7 +111,7 @@ export const createValidatorFromConstraints = (
rules = rules.concat(customRules || []) rules = rules.concat(customRules || [])
// Evaluate each constraint // Evaluate each constraint
return value => { return (value: any) => {
for (let rule of rules) { for (let rule of rules) {
const error = evaluateRule(rule, value) const error = evaluateRule(rule, value)
if (error) { if (error) {
@ -124,7 +129,7 @@ export const createValidatorFromConstraints = (
* @param value the value to validate against * @param value the value to validate against
* @returns {null|*} an error if validation fails or null if it passes * @returns {null|*} an error if validation fails or null if it passes
*/ */
const evaluateRule = (rule, value) => { const evaluateRule = (rule: UIFieldValidationRule, value: any) => {
if (!rule) { if (!rule) {
return null return null
} }
@ -150,14 +155,14 @@ const evaluateRule = (rule, value) => {
* @param type the type to parse * @param type the type to parse
* @returns {boolean|string|*|number|null|array} the parsed value, or null if invalid * @returns {boolean|string|*|number|null|array} the parsed value, or null if invalid
*/ */
const parseType = (value, type) => { const parseType = (value: any, type: `${FieldType}`) => {
// Treat nulls or empty strings as null // Treat nulls or empty strings as null
if (!exists(value) || !type) { if (!exists(value) || !type) {
return null return null
} }
// Parse as string // Parse as string
if (type === FieldTypes.STRING) { if (type === FieldType.STRING) {
if (typeof value === "string" || Array.isArray(value)) { if (typeof value === "string" || Array.isArray(value)) {
return value return value
} }
@ -168,7 +173,7 @@ const parseType = (value, type) => {
} }
// Parse as number // Parse as number
if (type === FieldTypes.NUMBER) { if (type === FieldType.NUMBER) {
if (isNaN(value)) { if (isNaN(value)) {
return null return null
} }
@ -176,7 +181,7 @@ const parseType = (value, type) => {
} }
// Parse as date // Parse as date
if (type === FieldTypes.DATETIME) { if (type === FieldType.DATETIME) {
if (value instanceof Date) { if (value instanceof Date) {
return value.getTime() return value.getTime()
} }
@ -185,7 +190,7 @@ const parseType = (value, type) => {
} }
// Parse as boolean // Parse as boolean
if (type === FieldTypes.BOOLEAN) { if (type === FieldType.BOOLEAN) {
if (typeof value === "string") { if (typeof value === "string") {
return value.toLowerCase() === "true" return value.toLowerCase() === "true"
} }
@ -193,7 +198,7 @@ const parseType = (value, type) => {
} }
// Parse attachments, treating no elements as null // Parse attachments, treating no elements as null
if (type === FieldTypes.ATTACHMENTS) { if (type === FieldType.ATTACHMENTS) {
if (!Array.isArray(value) || !value.length) { if (!Array.isArray(value) || !value.length) {
return null return null
} }
@ -202,8 +207,8 @@ const parseType = (value, type) => {
// Parse attachment/signature single, treating no key as null // Parse attachment/signature single, treating no key as null
if ( if (
type === FieldTypes.ATTACHMENT_SINGLE || type === FieldType.ATTACHMENT_SINGLE ||
type === FieldTypes.SIGNATURE_SINGLE type === FieldType.SIGNATURE_SINGLE
) { ) {
if (!value?.key) { if (!value?.key) {
return null return null
@ -212,7 +217,7 @@ const parseType = (value, type) => {
} }
// Parse links, treating no elements as null // Parse links, treating no elements as null
if (type === FieldTypes.LINK) { if (type === FieldType.LINK) {
if (!Array.isArray(value) || !value.length) { if (!Array.isArray(value) || !value.length) {
return null return null
} }
@ -220,7 +225,7 @@ const parseType = (value, type) => {
} }
// Parse array, treating no elements as null // Parse array, treating no elements as null
if (type === FieldTypes.ARRAY) { if (type === FieldType.ARRAY) {
if (!Array.isArray(value) || !value.length) { if (!Array.isArray(value) || !value.length) {
return null return null
} }
@ -229,7 +234,7 @@ const parseType = (value, type) => {
// For JSON we don't touch the value at all as we want to verify it in its // For JSON we don't touch the value at all as we want to verify it in its
// raw form // raw form
if (type === FieldTypes.JSON) { if (type === FieldType.JSON) {
return value return value
} }
@ -238,69 +243,74 @@ const parseType = (value, type) => {
} }
// Evaluates a required constraint // Evaluates a required constraint
const requiredHandler = value => { const requiredHandler = (value: any) => {
return value != null return value != null
} }
// Evaluates a min length constraint // Evaluates a min length constraint
const minLengthHandler = (value, rule) => { const minLengthHandler = (value: any, rule: UIFieldValidationRule) => {
const limit = parseType(rule.value, "number") const limit = parseType(rule.value, "number")
return value == null || value.length >= limit return value == null || value.length >= limit
} }
// Evaluates a max length constraint // Evaluates a max length constraint
const maxLengthHandler = (value, rule) => { const maxLengthHandler = (value: any, rule: UIFieldValidationRule) => {
const limit = parseType(rule.value, "number") const limit = parseType(rule.value, "number")
return value == null || value.length <= limit return value == null || value.length <= limit
} }
// Evaluates a max file size (MB) constraint // Evaluates a max file size (MB) constraint
const maxFileSizeHandler = (value, rule) => { const maxFileSizeHandler = (value: any, rule: UIFieldValidationRule) => {
const limit = parseType(rule.value, "number") const limit = parseType(rule.value, "number")
const check = attachment => attachment.size / 1000000 > limit const check = (attachment: { size: number }) =>
attachment.size / 1000000 > limit
return value == null || !(value?.key ? check(value) : value.some(check)) return value == null || !(value?.key ? check(value) : value.some(check))
} }
// Evaluates a max total upload size (MB) constraint // Evaluates a max total upload size (MB) constraint
const maxUploadSizeHandler = (value, rule) => { const maxUploadSizeHandler = (value: any, rule: UIFieldValidationRule) => {
const limit = parseType(rule.value, "number") const limit: number = parseType(rule.value, "number")
return ( return (
value == null || value == null ||
(value?.key (value?.key
? value.size / 1000000 <= limit ? value.size / 1000000 <= limit
: value.reduce((acc, currentItem) => acc + currentItem.size, 0) / : value.reduce(
(acc: number, currentItem: { size: number }) =>
acc + currentItem.size,
0
) /
1000000 <= 1000000 <=
limit) limit)
) )
} }
// Evaluates a min value constraint // Evaluates a min value constraint
const minValueHandler = (value, rule) => { const minValueHandler = (value: any, rule: UIFieldValidationRule) => {
// Use same type as the value so that things can be compared // Use same type as the value so that things can be compared
const limit = parseType(rule.value, rule.type) const limit = parseType(rule.value, rule.type)
return value == null || value >= limit return value == null || value >= limit
} }
// Evaluates a max value constraint // Evaluates a max value constraint
const maxValueHandler = (value, rule) => { const maxValueHandler = (value: any, rule: UIFieldValidationRule) => {
// Use same type as the value so that things can be compared // Use same type as the value so that things can be compared
const limit = parseType(rule.value, rule.type) const limit = parseType(rule.value, rule.type)
return value == null || value <= limit return value == null || value <= limit
} }
// Evaluates an inclusion constraint // Evaluates an inclusion constraint
const inclusionHandler = (value, rule) => { const inclusionHandler = (value: any, rule: UIFieldValidationRule) => {
return value == null || rule.value.includes(value) return value == null || (rule.value as any).includes(value)
} }
// Evaluates an equal constraint // Evaluates an equal constraint
const equalHandler = (value, rule) => { const equalHandler = (value: any, rule: UIFieldValidationRule) => {
const ruleValue = parseType(rule.value, rule.type) const ruleValue = parseType(rule.value, rule.type)
return value === ruleValue return value === ruleValue
} }
// Evaluates a not equal constraint // Evaluates a not equal constraint
const notEqualHandler = (value, rule) => { const notEqualHandler = (value: any, rule: UIFieldValidationRule) => {
const ruleValue = parseType(rule.value, rule.type) const ruleValue = parseType(rule.value, rule.type)
if (value == null && ruleValue == null) { if (value == null && ruleValue == null) {
return true return true
@ -309,7 +319,7 @@ const notEqualHandler = (value, rule) => {
} }
// Evaluates a regex constraint // Evaluates a regex constraint
const regexHandler = (value, rule) => { const regexHandler = (value: any, rule: UIFieldValidationRule) => {
const regex = parseType(rule.value, "string") const regex = parseType(rule.value, "string")
if (!value) { if (!value) {
value = "" value = ""
@ -318,23 +328,23 @@ const regexHandler = (value, rule) => {
} }
// Evaluates a not regex constraint // Evaluates a not regex constraint
const notRegexHandler = (value, rule) => { const notRegexHandler = (value: any, rule: UIFieldValidationRule) => {
return !regexHandler(value, rule) return !regexHandler(value, rule)
} }
// Evaluates a contains constraint // Evaluates a contains constraint
const containsHandler = (value, rule) => { const containsHandler = (value: any, rule: UIFieldValidationRule) => {
const expectedValue = parseType(rule.value, "string") const expectedValue = parseType(rule.value, "string")
return value && value.includes(expectedValue) return value && value.includes(expectedValue)
} }
// Evaluates a not contains constraint // Evaluates a not contains constraint
const notContainsHandler = (value, rule) => { const notContainsHandler = (value: any, rule: UIFieldValidationRule) => {
return !containsHandler(value, rule) return !containsHandler(value, rule)
} }
// Evaluates a constraint that the value must be a valid json object // Evaluates a constraint that the value must be a valid json object
const jsonHandler = value => { const jsonHandler = (value: any) => {
if (typeof value !== "object" || Array.isArray(value)) { if (typeof value !== "object" || Array.isArray(value)) {
return false return false
} }
@ -372,6 +382,6 @@ const handlerMap = {
* @param value the value to test * @param value the value to test
* @returns {boolean} whether the value exists or not * @returns {boolean} whether the value exists or not
*/ */
const exists = value => { const exists = <T = any>(value: T | null | undefined): value is T => {
return value != null && value !== "" return value != null && value !== ""
} }

View File

@ -1,12 +1,12 @@
import { Writable } from "svelte" import { Writable } from "svelte"
import { Component, FieldGroupContext, FormContext } from "@/types" import { Component, Context, FieldGroupContext, FormContext } from "@/types"
import { Readable } from "svelte/store"
import { SDK } from "@/index.ts" import { SDK } from "@/index.ts"
declare module "svelte" { declare module "svelte" {
export function getContext(key: "sdk"): SDK export function getContext(key: "sdk"): SDK
export function getContext(key: "component"): Component export function getContext(key: "component"): Component
export function getContext(key: "context"): Readable<Record<string, any>> export function getContext(key: "current-step"): Writable<number>
export function getContext(key: "context"): Context
export function getContext(key: "form"): FormContext | undefined export function getContext(key: "form"): FormContext | undefined
export function getContext(key: "form-step"): Writable<number> | undefined export function getContext(key: "form-step"): Writable<number> | undefined
export function getContext(key: "field-group"): FieldGroupContext | undefined export function getContext(key: "field-group"): FieldGroupContext | undefined

View File

@ -16,7 +16,6 @@ import {
} from "@/stores" } from "@/stores"
import { get } from "svelte/store" import { get } from "svelte/store"
import { initWebsocket } from "@/websocket" import { initWebsocket } from "@/websocket"
import { Readable } from "svelte/store"
import { import {
Screen, Screen,
Theme, Theme,
@ -27,6 +26,8 @@ import {
Snippet, Snippet,
UIComponentError, UIComponentError,
CustomComponent, CustomComponent,
Table,
DataFetchDatasource,
} from "@budibase/types" } from "@budibase/types"
import { ActionTypes } from "@/constants" import { ActionTypes } from "@/constants"
import { APIClient } from "@budibase/frontend-core" import { APIClient } from "@budibase/frontend-core"
@ -75,14 +76,13 @@ declare global {
} }
} }
export type Context = Readable<Record<string, any>>
export interface SDK { export interface SDK {
API: APIClient API: APIClient
styleable: any styleable: any
Provider: any Provider: any
ActionTypes: typeof ActionTypes ActionTypes: typeof ActionTypes
fetchDatasourceSchema: any fetchDatasourceSchema: any
fetchDatasourceDefinition: (datasource: DataFetchDatasource) => Promise<Table>
generateGoldenSample: any generateGoldenSample: any
builderStore: typeof builderStore builderStore: typeof builderStore
authStore: typeof authStore authStore: typeof authStore

View File

@ -6,4 +6,5 @@ export type Component = Readable<{
styles: any styles: any
editing: boolean editing: boolean
errorState: boolean errorState: boolean
path: string[]
}> }>

View File

@ -1,3 +1,7 @@
import { Readable } from "svelte/store"
export * from "./components" export * from "./components"
export * from "./fields" export * from "./fields"
export * from "./forms" export * from "./forms"
export type Context = Readable<Record<string, any>>

View File

@ -10,7 +10,7 @@ import { User } from "@budibase/types"
* @param key the key * @param key the key
* @return the value or null if a value was not found for this key * @return the value or null if a value was not found for this key
*/ */
export const deepGet = (obj: { [x: string]: any }, key: string) => { export const deepGet = (obj: Record<string, any> | undefined, key: string) => {
if (!obj || !key) { if (!obj || !key) {
return null return null
} }

View File

@ -3,7 +3,7 @@ import {
FieldConstraints, FieldConstraints,
type FieldSchema, type FieldSchema,
type FormulaResponseType, type FormulaResponseType,
} from "../" } from "../../"
export interface UIField { export interface UIField {
name: string name: string

View File

@ -0,0 +1,2 @@
export * from "./fields"
export * from "./validationRules"

View File

@ -0,0 +1,25 @@
import { FieldType } from "../../documents"
export interface UIFieldValidationRule {
type: `${FieldType}`
constraint: FieldValidationRuleType
value?: string | number | string[]
error: string
}
export type FieldValidationRuleType =
| "required"
| "minLength"
| "maxLength"
| "minValue"
| "maxValue"
| "inclusion"
| "equal"
| "notEqual"
| "regex"
| "notRegex"
| "contains"
| "notContains"
| "json"
| "maxFileSize"
| "maxUploadSize"