Merge pull request #15706 from Budibase/BUDI-9077/type-fields

Type fields
This commit is contained in:
Adria Navarro 2025-03-13 12:22:16 +01:00 committed by GitHub
commit b063fab09b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 153 additions and 80 deletions

View File

@ -1,31 +1,36 @@
<script> <script lang="ts">
import { sdk } from "@budibase/shared-core" import { sdk } from "@budibase/shared-core"
import { FieldType } from "@budibase/types" import { FieldType } from "@budibase/types"
import RelationshipField from "./RelationshipField.svelte" import RelationshipField from "./RelationshipField.svelte"
export let defaultValue export let defaultValue: string
export let type = FieldType.BB_REFERENCE export let type = FieldType.BB_REFERENCE
function updateUserIDs(value) { function updateUserIDs(value: string | string[]) {
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value.map(val => sdk.users.getGlobalUserID(val)) return value.map(val => sdk.users.getGlobalUserID(val)!)
} else { } else {
return sdk.users.getGlobalUserID(value) return sdk.users.getGlobalUserID(value)
} }
} }
function updateReferences(value) { function updateReferences(value: string) {
if (sdk.users.containsUserID(value)) { if (sdk.users.containsUserID(value)) {
return updateUserIDs(value) return updateUserIDs(value)
} }
return value return value
} }
$: updatedDefaultValue = updateReferences(defaultValue)
// This cannot be typed, as svelte does not provide typed inheritance
$: allProps = $$props as any
</script> </script>
<RelationshipField <RelationshipField
{...$$props} {...allProps}
{type} {type}
datasourceType={"user"} datasourceType={"user"}
primaryDisplay={"email"} primaryDisplay={"email"}
defaultValue={updateReferences(defaultValue)} defaultValue={updatedDefaultValue}
/> />

View File

@ -1,43 +1,60 @@
<script lang="ts"> <script lang="ts">
import { getContext, onDestroy } from "svelte" import { getContext, onDestroy } from "svelte"
import type { Readable } from "svelte/store"
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
import { memo } from "@budibase/frontend-core" import { memo } from "@budibase/frontend-core"
import Placeholder from "../Placeholder.svelte" import Placeholder from "../Placeholder.svelte"
import InnerForm from "./InnerForm.svelte" import InnerForm from "./InnerForm.svelte"
import type { FieldApi } from "." import type { FieldSchema, FieldType } from "@budibase/types"
import type {
FieldApi,
FieldState,
FieldValidation,
FormField,
} from "@/types"
interface FieldInfo {
field: string
type: FieldType
defaultValue: string | undefined
disabled: boolean
readonly: boolean
validation?: FieldValidation
formStep: number
}
export let label: string | undefined = undefined export let label: string | undefined = undefined
export let field: string | undefined = undefined export let field: string | undefined = undefined
export let fieldState: any export let fieldState: FieldState | undefined
export let fieldApi: FieldApi export let fieldApi: FieldApi | undefined
export let fieldSchema: any export let fieldSchema: FieldSchema | undefined
export let defaultValue: string | undefined = undefined export let defaultValue: string | undefined = undefined
export let type: any export let type: FieldType
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let validation: any export let validation: FieldValidation | undefined
export let span = 6 export let span = 6
export let helpText: string | undefined = undefined export let helpText: string | undefined = undefined
// Get contexts // Get contexts
const formContext: any = getContext("form") const formContext = getContext("form")
const formStepContext: any = getContext("form-step") const formStepContext = getContext("form-step")
const fieldGroupContext: any = getContext("field-group") const fieldGroupContext = getContext("field-group")
const { styleable, builderStore, Provider } = getContext("sdk") const { styleable, builderStore, Provider } = getContext("sdk")
const component: any = getContext("component") const component = getContext("component")
// Register field with form // Register field with form
const formApi = formContext?.formApi const formApi = formContext?.formApi
const labelPos = fieldGroupContext?.labelPosition || "above" const labelPos = fieldGroupContext?.labelPosition || "above"
let formField: any let formField: Readable<FormField> | undefined
let touched = false let touched = false
let labelNode: any let labelNode: HTMLElement | undefined
// Memoize values required to register the field to avoid loops // Memoize values required to register the field to avoid loops
const formStep = formStepContext || writable(1) const formStep = formStepContext || writable(1)
const fieldInfo = memo({ const fieldInfo = memo<FieldInfo>({
field: field || $component.name, field: field || $component.name,
type, type,
defaultValue, defaultValue,
@ -66,16 +83,22 @@
$: $component.editing && labelNode?.focus() $: $component.editing && labelNode?.focus()
// Update form properties in parent component on every store change // Update form properties in parent component on every store change
$: unsubscribe = formField?.subscribe((value: any) => { $: unsubscribe = formField?.subscribe(
fieldState = value?.fieldState (value?: {
fieldApi = value?.fieldApi fieldState: FieldState
fieldSchema = value?.fieldSchema fieldApi: FieldApi
}) fieldSchema: FieldSchema
}) => {
fieldState = value?.fieldState
fieldApi = value?.fieldApi
fieldSchema = value?.fieldSchema
}
)
// Determine label class from position // Determine label class from position
$: labelClass = labelPos === "above" ? "" : `spectrum-FieldLabel--${labelPos}` $: labelClass = labelPos === "above" ? "" : `spectrum-FieldLabel--${labelPos}`
const registerField = (info: any) => { const registerField = (info: FieldInfo) => {
formField = formApi?.registerField( formField = formApi?.registerField(
info.field, info.field,
info.type, info.type,
@ -87,10 +110,10 @@
) )
} }
const updateLabel = (e: any) => { const updateLabel = (e: Event) => {
if (touched) { if (touched) {
// @ts-expect-error and TODO updateProp isn't recognised - need builder TS conversion const label = e.target as HTMLLabelElement
builderStore.actions.updateProp("label", e.target.textContent) builderStore.actions.updateProp("label", label.textContent)
} }
touched = false touched = false
} }

View File

@ -9,17 +9,19 @@
RelationshipFieldMetadata, RelationshipFieldMetadata,
Row, Row,
} from "@budibase/types" } from "@budibase/types"
import type { FieldApi, FieldState } from "." import type { FieldApi, FieldState, FieldValidation } from "@/types"
type ValueType = string | string[]
export let field: string | undefined = undefined export let field: string | undefined = undefined
export let label: string | undefined = undefined export let label: string | undefined = undefined
export let placeholder: string | undefined = undefined export let placeholder: string | undefined = undefined
export let disabled: boolean = false export let disabled: boolean = false
export let readonly: boolean = false export let readonly: boolean = false
export let validation: any export let validation: FieldValidation | undefined = undefined
export let autocomplete: boolean = true export let autocomplete: boolean = true
export let defaultValue: string | string[] | undefined = undefined export let defaultValue: ValueType | undefined = undefined
export let onChange: any export let onChange: (_props: { value: ValueType }) => void
export let filter: SearchFilter[] export let filter: SearchFilter[]
export let datasourceType: "table" | "user" = "table" export let datasourceType: "table" | "user" = "table"
export let primaryDisplay: string | undefined = undefined export let primaryDisplay: string | undefined = undefined
@ -88,14 +90,14 @@
// Ensure backwards compatibility // Ensure backwards compatibility
$: enrichedDefaultValue = enrichDefaultValue(defaultValue) $: enrichedDefaultValue = enrichDefaultValue(defaultValue)
$: emptyValue = multiselect ? [] : undefined
// We need to cast value to pass it down, as those components aren't typed // We need to cast value to pass it down, as those components aren't typed
$: emptyValue = multiselect ? [] : null $: displayValue = (missingIDs.length ? emptyValue : selectedValue) as any
$: displayValue = missingIDs.length ? emptyValue : (selectedValue as any)
// Ensures that we flatten any objects so that only the IDs of the selected // Ensures that we flatten any objects so that only the IDs of the selected
// rows are passed down. Not sure how this can be an object to begin with? // rows are passed down. Not sure how this can be an object to begin with?
const parseSelectedValue = ( const parseSelectedValue = (
value: any, value: ValueType | undefined,
multiselect: boolean multiselect: boolean
): undefined | string | string[] => { ): undefined | string | string[] => {
return multiselect ? flatten(value) : flatten(value)[0] return multiselect ? flatten(value) : flatten(value)[0]
@ -140,7 +142,7 @@
// Builds a map of all available options, in a consistent structure // Builds a map of all available options, in a consistent structure
const processOptions = ( const processOptions = (
realValue: any | any[], realValue: ValueType | undefined,
rows: Row[], rows: Row[],
primaryDisplay?: string primaryDisplay?: string
) => { ) => {
@ -171,7 +173,7 @@
// Parses a row-like structure into a properly shaped option // Parses a row-like structure into a properly shaped option
const parseOption = ( const parseOption = (
option: any | BasicRelatedRow | Row, option: string | BasicRelatedRow | Row,
primaryDisplay?: string primaryDisplay?: string
): BasicRelatedRow | null => { ): BasicRelatedRow | null => {
if (!option || typeof option !== "object" || !option?._id) { if (!option || typeof option !== "object" || !option?._id) {

View File

@ -19,15 +19,3 @@ export { default as codescanner } from "./CodeScannerField.svelte"
export { default as signaturesinglefield } from "./SignatureField.svelte" export { default as signaturesinglefield } from "./SignatureField.svelte"
export { default as bbreferencefield } from "./BBReferenceField.svelte" export { default as bbreferencefield } from "./BBReferenceField.svelte"
export { default as bbreferencesinglefield } from "./BBReferenceSingleField.svelte" export { default as bbreferencesinglefield } from "./BBReferenceSingleField.svelte"
export interface FieldApi {
setValue(value: any): boolean
deregister(): void
}
export interface FieldState<T> {
value: T
fieldId: string
disabled: boolean
readonly: boolean
}

View File

@ -1,7 +1,12 @@
import { Component, Context, SDK } from "." import { Writable } from "svelte"
import { Component, FieldGroupContext, FormContext, SDK } from "@/types"
import { Readable } from "svelte/store"
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"): Context export function getContext(key: "context"): Readable<Record<string, any>>
export function getContext(key: "form"): FormContext | undefined
export function getContext(key: "form-step"): Writable<number> | undefined
export function getContext(key: "field-group"): FieldGroupContext | undefined
} }

View File

@ -14,8 +14,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 { APIClient } from "@budibase/frontend-core"
import type { ActionTypes } from "@/constants"
import { Readable } from "svelte/store" import { Readable } from "svelte/store"
import { import {
Screen, Screen,
@ -39,6 +37,7 @@ window.svelte = svelte
// Initialise spectrum icons // Initialise spectrum icons
// eslint-disable-next-line local-rules/no-budibase-imports // eslint-disable-next-line local-rules/no-budibase-imports
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js" import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
loadSpectrumIcons() loadSpectrumIcons()
// Extend global window scope // Extend global window scope
@ -73,32 +72,6 @@ declare global {
} }
} }
export interface SDK {
API: APIClient
styleable: any
Provider: any
ActionTypes: typeof ActionTypes
fetchDatasourceSchema: any
generateGoldenSample: any
builderStore: Readable<{
inBuilder: boolean
}> & {
actions: {
highlightSetting: (key: string) => void
addParentComponent: (
componentId: string,
fullAncestorType: string
) => void
}
}
}
export type Component = Readable<{
id: string
styles: any
errorState: boolean
}>
export type Context = Readable<Record<string, any>> export type Context = Readable<Record<string, any>>
let app: ClientApp let app: ClientApp

View File

@ -0,0 +1,9 @@
import { Readable } from "svelte/store"
export type Component = Readable<{
id: string
name: string
styles: any
editing: boolean
errorState: boolean
}>

View File

@ -0,0 +1,3 @@
export interface FieldGroupContext {
labelPosition: string
}

View File

@ -0,0 +1,37 @@
import { Readable } from "svelte/store"
import { FieldSchema, FieldType } from "@budibase/types"
export interface FormContext {
formApi?: {
registerField: (
field: string,
type: FieldType,
defaultValue: string | undefined,
disabled: boolean,
readonly: boolean,
validation: FieldValidation | undefined,
formStep: number
) => Readable<FormField>
}
}
export type FieldValidation = () => string | undefined
export interface FormField {
fieldState: FieldState
fieldApi: FieldApi
fieldSchema: FieldSchema
}
export interface FieldApi {
setValue(value: any): boolean
deregister(): void
}
export interface FieldState<T = any> {
value: T
fieldId: string
disabled: boolean
readonly: boolean
error?: string
}

View File

@ -0,0 +1,4 @@
export * from "./components"
export * from "./fields"
export * from "./forms"
export * from "./sdk"

View File

@ -0,0 +1,24 @@
import { ActionTypes } from "@/constants"
import { APIClient } from "@budibase/frontend-core"
import { Readable } from "svelte/store"
export interface SDK {
API: APIClient
styleable: any
Provider: any
ActionTypes: typeof ActionTypes
fetchDatasourceSchema: any
generateGoldenSample: any
builderStore: Readable<{
inBuilder: boolean
}> & {
actions: {
highlightSetting: (key: string) => void
addParentComponent: (
componentId: string,
fullAncestorType: string
) => void
updateProp: (key: string, value: any) => void
}
}
}