Merge pull request #15706 from Budibase/BUDI-9077/type-fields
Type fields
This commit is contained in:
commit
b063fab09b
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Readable } from "svelte/store"
|
||||||
|
|
||||||
|
export type Component = Readable<{
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
styles: any
|
||||||
|
editing: boolean
|
||||||
|
errorState: boolean
|
||||||
|
}>
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface FieldGroupContext {
|
||||||
|
labelPosition: string
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from "./components"
|
||||||
|
export * from "./fields"
|
||||||
|
export * from "./forms"
|
||||||
|
export * from "./sdk"
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue