Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type fields #15706

Merged
merged 15 commits into from
Mar 13, 2025
19 changes: 12 additions & 7 deletions packages/client/src/components/app/forms/BBReferenceField.svelte
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
<script>
<script lang="ts">
import { sdk } from "@budibase/shared-core"
import { FieldType } from "@budibase/types"
import RelationshipField from "./RelationshipField.svelte"

export let defaultValue
export let defaultValue: string
export let type = FieldType.BB_REFERENCE

function updateUserIDs(value) {
function updateUserIDs(value: string | string[]) {
if (Array.isArray(value)) {
return value.map(val => sdk.users.getGlobalUserID(val))
return value.map(val => sdk.users.getGlobalUserID(val)!)
} else {
return sdk.users.getGlobalUserID(value)
}
}

function updateReferences(value) {
function updateReferences(value: string) {
if (sdk.users.containsUserID(value)) {
return updateUserIDs(value)
}
return value
}

$: updatedDefaultValue = updateReferences(defaultValue)

// This cannot be typed, as svelte does not provide typed inheritance
$: allProps = $$props as any
</script>

<RelationshipField
{...$$props}
{...allProps}
{type}
datasourceType={"user"}
primaryDisplay={"email"}
defaultValue={updateReferences(defaultValue)}
defaultValue={updatedDefaultValue}
/>
63 changes: 41 additions & 22 deletions packages/client/src/components/app/forms/Field.svelte
Original file line number Diff line number Diff line change
@@ -1,43 +1,56 @@
<script lang="ts">
import { getContext, onDestroy } from "svelte"
import type { Readable } from "svelte/store"
import { writable } from "svelte/store"
import { Icon } from "@budibase/bbui"
import { memo } from "@budibase/frontend-core"
import Placeholder from "../Placeholder.svelte"
import InnerForm from "./InnerForm.svelte"
import type { FieldApi } from "."
import type { FieldApi, FieldState } from "."
import type { FieldSchema, FieldType } from "@budibase/types"
import type { FieldValidation, FormField } from "@/index"

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 field: string | undefined = undefined
export let fieldState: any
export let fieldApi: FieldApi
export let fieldSchema: any
export let fieldState: FieldState | undefined
export let fieldApi: FieldApi | undefined
export let fieldSchema: FieldSchema | undefined
export let defaultValue: string | undefined = undefined
export let type: any
export let type: FieldType
export let disabled = false
export let readonly = false
export let validation: any
export let validation: FieldValidation | undefined
export let span = 6
export let helpText: string | undefined = undefined

// Get contexts
const formContext: any = getContext("form")
const formStepContext: any = getContext("form-step")
const fieldGroupContext: any = getContext("field-group")
const formContext = getContext("form")
const formStepContext = getContext("form-step")
const fieldGroupContext = getContext("field-group")
const { styleable, builderStore, Provider } = getContext("sdk")
const component: any = getContext("component")
const component = getContext("component")

// Register field with form
const formApi = formContext?.formApi
const labelPos = fieldGroupContext?.labelPosition || "above"

let formField: any
let formField: Readable<FormField> | undefined
let touched = false
let labelNode: any
let labelNode: HTMLElement | undefined

// Memoize values required to register the field to avoid loops
const formStep = formStepContext || writable(1)
const fieldInfo = memo({
const fieldInfo = memo<FieldInfo>({
field: field || $component.name,
type,
defaultValue,
Expand Down Expand Up @@ -66,16 +79,22 @@
$: $component.editing && labelNode?.focus()

// Update form properties in parent component on every store change
$: unsubscribe = formField?.subscribe((value: any) => {
fieldState = value?.fieldState
fieldApi = value?.fieldApi
fieldSchema = value?.fieldSchema
})
$: unsubscribe = formField?.subscribe(
(value?: {
fieldState: FieldState
fieldApi: FieldApi
fieldSchema: FieldSchema
}) => {
fieldState = value?.fieldState
fieldApi = value?.fieldApi
fieldSchema = value?.fieldSchema
}
)

// Determine label class from position
$: labelClass = labelPos === "above" ? "" : `spectrum-FieldLabel--${labelPos}`

const registerField = (info: any) => {
const registerField = (info: FieldInfo) => {
formField = formApi?.registerField(
info.field,
info.type,
Expand All @@ -87,10 +106,10 @@
)
}

const updateLabel = (e: any) => {
const updateLabel = (e: Event) => {
if (touched) {
// @ts-expect-error and TODO updateProp isn't recognised - need builder TS conversion
builderStore.actions.updateProp("label", e.target.textContent)
const label = e.target as HTMLLabelElement
builderStore.actions.updateProp("label", label.textContent)
}
touched = false
}
Expand Down
19 changes: 11 additions & 8 deletions packages/client/src/components/app/forms/RelationshipField.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@
Row,
} from "@budibase/types"
import type { FieldApi, FieldState } from "."
import type { FieldValidation } from "@/index"

type ValueType = string | string[]

export let field: string | undefined = undefined
export let label: string | undefined = undefined
export let placeholder: string | undefined = undefined
export let disabled: boolean = false
export let readonly: boolean = false
export let validation: any
export let validation: FieldValidation | undefined = undefined
export let autocomplete: boolean = true
export let defaultValue: string | string[] | undefined = undefined
export let onChange: any
export let defaultValue: ValueType | undefined = undefined
export let onChange: (_props: { value: ValueType }) => void
export let filter: SearchFilter[]
export let datasourceType: "table" | "user" = "table"
export let primaryDisplay: string | undefined = undefined
Expand Down Expand Up @@ -88,14 +91,14 @@
// Ensure backwards compatibility
$: enrichedDefaultValue = enrichDefaultValue(defaultValue)

$: emptyValue = multiselect ? [] : undefined
// 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
// rows are passed down. Not sure how this can be an object to begin with?
const parseSelectedValue = (
value: any,
value: ValueType | undefined,
multiselect: boolean
): undefined | string | string[] => {
return multiselect ? flatten(value) : flatten(value)[0]
Expand Down Expand Up @@ -140,7 +143,7 @@

// Builds a map of all available options, in a consistent structure
const processOptions = (
realValue: any | any[],
realValue: ValueType | undefined,
rows: Row[],
primaryDisplay?: string
) => {
Expand Down Expand Up @@ -171,7 +174,7 @@

// Parses a row-like structure into a properly shaped option
const parseOption = (
option: any | BasicRelatedRow | Row,
option: string | BasicRelatedRow | Row,
primaryDisplay?: string
): BasicRelatedRow | null => {
if (!option || typeof option !== "object" || !option?._id) {
Expand Down
3 changes: 2 additions & 1 deletion packages/client/src/components/app/forms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ export interface FieldApi {
deregister(): void
}

export interface FieldState<T> {
export interface FieldState<T = any> {
value: T
fieldId: string
disabled: boolean
readonly: boolean
error?: string
}
6 changes: 5 additions & 1 deletion packages/client/src/context.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Component, Context, SDK } from "."
import { Writable } from "svelte"
import { Component, Context, FieldGroupContext, FormContext, SDK } from "."

declare module "svelte" {
export function getContext(key: "sdk"): SDK
export function getContext(key: "component"): Component
export function getContext(key: "context"): Context
export function getContext(key: "form"): FormContext | undefined
export function getContext(key: "form-step"): Writable<number> | undefined
export function getContext(key: "field-group"): FieldGroupContext | undefined
}
32 changes: 32 additions & 0 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import {
Snippet,
UIComponentError,
CustomComponent,
FieldType,
FieldSchema,
} from "@budibase/types"

// Provide svelte and svelte/internal as globals for custom components
Expand All @@ -39,6 +41,7 @@ window.svelte = svelte
// Initialise spectrum icons
// eslint-disable-next-line local-rules/no-budibase-imports
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
import { FieldApi, FieldState } from "./components/app"
loadSpectrumIcons()

// Extend global window scope
Expand Down Expand Up @@ -89,18 +92,47 @@ export interface SDK {
componentId: string,
fullAncestorType: string
) => void
updateProp: (key: string, value: any) => void
}
}
}

export type Component = Readable<{
id: string
name: string
styles: any
editing: boolean
errorState: boolean
}>

export type Context = Readable<Record<string, any>>

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 FieldGroupContext {
labelPosition: string
}

let app: ClientApp

const loadBudibase = async () => {
Expand Down
Loading