Merge pull request #15365 from Budibase/budi-8980-create-state-panel

State explorer panel
This commit is contained in:
Peter Clement 2025-01-29 13:49:10 +00:00 committed by GitHub
commit 5749543ab6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 679 additions and 208 deletions

View File

@ -11,6 +11,7 @@
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import clickOutside from "../../Actions/click_outside" import clickOutside from "../../Actions/click_outside"
import Popover from "../../Popover/Popover.svelte" import Popover from "../../Popover/Popover.svelte"
import { PopoverAlignment } from "../../constants"
export let value: string | undefined = undefined export let value: string | undefined = undefined
export let id: string | undefined = undefined export let id: string | undefined = undefined
@ -97,11 +98,16 @@
<Popover <Popover
{anchor} {anchor}
{open} {open}
align="left" align={PopoverAlignment.Left}
on:close={() => (open = false)} on:close={() => (open = false)}
useAnchorWidth useAnchorWidth
> >
<div class="popover-content" use:clickOutside={() => (open = false)}> <div
class="popover-content"
use:clickOutside={() => {
open = false
}}
>
<ul class="spectrum-Menu" role="listbox"> <ul class="spectrum-Menu" role="listbox">
{#if options && Array.isArray(options)} {#if options && Array.isArray(options)}
{#each options as option} {#each options as option}

View File

@ -1,4 +1,9 @@
<script> <script lang="ts" context="module">
type O = any
type V = any
</script>
<script lang="ts">
import "@spectrum-css/picker/dist/index-vars.css" import "@spectrum-css/picker/dist/index-vars.css"
import "@spectrum-css/popover/dist/index-vars.css" import "@spectrum-css/popover/dist/index-vars.css"
import "@spectrum-css/menu/dist/index-vars.css" import "@spectrum-css/menu/dist/index-vars.css"
@ -11,43 +16,55 @@
import Tags from "../../Tags/Tags.svelte" import Tags from "../../Tags/Tags.svelte"
import Tag from "../../Tags/Tag.svelte" import Tag from "../../Tags/Tag.svelte"
import ProgressCircle from "../../ProgressCircle/ProgressCircle.svelte" import ProgressCircle from "../../ProgressCircle/ProgressCircle.svelte"
import { PopoverAlignment } from "../../constants"
export let id = null export let id: string | undefined = undefined
export let disabled = false export let disabled: boolean = false
export let fieldText = "" export let fieldText: string = ""
export let fieldIcon = "" export let fieldIcon: string = ""
export let fieldColour = "" export let fieldColour: string = ""
export let isPlaceholder = false export let isPlaceholder: boolean = false
export let placeholderOption = null export let placeholderOption: string | undefined | boolean = undefined
export let options = [] export let options: O[] = []
export let isOptionSelected = () => false export let isOptionSelected = (option: O) => option as unknown as boolean
export let isOptionEnabled = () => true export let isOptionEnabled = (option: O, _index?: number) =>
export let onSelectOption = () => {} option as unknown as boolean
export let getOptionLabel = option => option export let onSelectOption: (_value: V) => void = () => {}
export let getOptionValue = option => option export let getOptionLabel = (option: O, _index?: number) => `${option}`
export let getOptionIcon = () => null export let getOptionValue = (option: O, _index?: number) =>
option as unknown as V
export let getOptionIcon = (option: O, _index?: number) =>
option as unknown as O
export let useOptionIconImage = false export let useOptionIconImage = false
export let getOptionColour = () => null export let getOptionColour = (option: O, _index?: number) =>
export let getOptionSubtitle = () => null option as unknown as O
export let open = false export let getOptionSubtitle = (option: O, _index?: number) =>
export let readonly = false option as unknown as O
export let quiet = false export let open: boolean = false
export let autoWidth = false export let readonly: boolean = false
export let autocomplete = false export let quiet: boolean = false
export let sort = false export let autoWidth: boolean | undefined = false
export let searchTerm = null export let autocomplete: boolean = false
export let customPopoverHeight export let sort: boolean = false
export let align = "left" export let searchTerm: string | null = null
export let footer = null export let customPopoverHeight: string | undefined = undefined
export let customAnchor = null export let align: PopoverAlignment | undefined = PopoverAlignment.Left
export let loading export let footer: string | undefined = undefined
export let onOptionMouseenter = () => {} export let customAnchor: HTMLElement | undefined = undefined
export let onOptionMouseleave = () => {} export let loading: boolean = false
export let onOptionMouseenter: (
_e: MouseEvent,
_option: any
) => void = () => {}
export let onOptionMouseleave: (
_e: MouseEvent,
_option: any
) => void = () => {}
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let button let button: any
let component let component: any
$: sortedOptions = getSortedOptions(options, getOptionLabel, sort) $: sortedOptions = getSortedOptions(options, getOptionLabel, sort)
$: filteredOptions = getFilteredOptions( $: filteredOptions = getFilteredOptions(
@ -56,7 +73,7 @@
getOptionLabel getOptionLabel
) )
const onClick = e => { const onClick = (e: MouseEvent) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
dispatch("click") dispatch("click")
@ -67,7 +84,11 @@
open = !open open = !open
} }
const getSortedOptions = (options, getLabel, sort) => { const getSortedOptions = (
options: any[],
getLabel: (_option: any) => string,
sort: boolean
) => {
if (!options?.length || !Array.isArray(options)) { if (!options?.length || !Array.isArray(options)) {
return [] return []
} }
@ -81,17 +102,21 @@
}) })
} }
const getFilteredOptions = (options, term, getLabel) => { const getFilteredOptions = (
options: any[],
term: string | null,
getLabel: (_option: any) => string
) => {
if (autocomplete && term) { if (autocomplete && term) {
const lowerCaseTerm = term.toLowerCase() const lowerCaseTerm = term.toLowerCase()
return options.filter(option => { return options.filter((option: any) => {
return `${getLabel(option)}`.toLowerCase().includes(lowerCaseTerm) return `${getLabel(option)}`.toLowerCase().includes(lowerCaseTerm)
}) })
} }
return options return options
} }
const onScroll = e => { const onScroll = (e: any) => {
const scrollPxThreshold = 100 const scrollPxThreshold = 100
const scrollPositionFromBottom = const scrollPositionFromBottom =
e.target.scrollHeight - e.target.clientHeight - e.target.scrollTop e.target.scrollHeight - e.target.clientHeight - e.target.scrollTop
@ -151,18 +176,20 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<Popover <Popover
anchor={customAnchor ? customAnchor : button} anchor={customAnchor ? customAnchor : button}
align={align || "left"} align={align || PopoverAlignment.Left}
{open} {open}
on:close={() => (open = false)} on:close={() => (open = false)}
useAnchorWidth={!autoWidth} useAnchorWidth={!autoWidth}
maxWidth={autoWidth ? 400 : null} maxWidth={autoWidth ? 400 : undefined}
customHeight={customPopoverHeight} customHeight={customPopoverHeight}
maxHeight={360} maxHeight={360}
> >
<div <div
class="popover-content" class="popover-content"
class:auto-width={autoWidth} class:auto-width={autoWidth}
use:clickOutside={() => (open = false)} use:clickOutside={() => {
open = false
}}
> >
{#if autocomplete} {#if autocomplete}
<Search <Search

View File

@ -1,19 +1,19 @@
<script> <script lang="ts">
import "@spectrum-css/search/dist/index-vars.css" import "@spectrum-css/search/dist/index-vars.css"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
export let value = null export let value: any = null
export let placeholder = null export let placeholder: string | undefined = undefined
export let disabled = false export let disabled = false
export let id = null export let id = null
export let updateOnChange = true export let updateOnChange = true
export let quiet = false export let quiet = false
export let inputRef export let inputRef: HTMLElement | undefined = undefined
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let focus = false let focus = false
const updateValue = value => { const updateValue = (value: any) => {
dispatch("change", value) dispatch("change", value)
} }
@ -21,19 +21,19 @@
focus = true focus = true
} }
const onBlur = event => { const onBlur = (event: any) => {
focus = false focus = false
updateValue(event.target.value) updateValue(event.target.value)
} }
const onInput = event => { const onInput = (event: any) => {
if (!updateOnChange) { if (!updateOnChange) {
return return
} }
updateValue(event.target.value) updateValue(event.target.value)
} }
const updateValueOnEnter = event => { const updateValueOnEnter = (event: any) => {
if (event.key === "Enter") { if (event.key === "Enter") {
updateValue(event.target.value) updateValue(event.target.value)
} }

View File

@ -1,33 +1,44 @@
<script> <script lang="ts" context="module">
type O = any
type V = any
</script>
<script lang="ts">
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import Picker from "./Picker.svelte" import Picker from "./Picker.svelte"
import { PopoverAlignment } from "../../constants"
export let value = null export let value: V | null = null
export let id = null export let id: string | undefined = undefined
export let placeholder = "Choose an option" export let placeholder: string | boolean = "Choose an option"
export let disabled = false export let disabled: boolean = false
export let options = [] export let options: O[] = []
export let getOptionLabel = option => option export let getOptionLabel = (option: O, _index?: number) => `${option}`
export let getOptionValue = option => option export let getOptionValue = (option: O, _index?: number) =>
export let getOptionIcon = () => null option as unknown as V
export let getOptionColour = () => null export let getOptionIcon = (option: O, _index?: number) =>
export let getOptionSubtitle = () => null option as unknown as string
export let compare = null export let getOptionColour = (option: O, _index?: number) =>
option as unknown as string
export let getOptionSubtitle = (option: O, _index?: number) =>
option as unknown as string
export let compare = (option: O, value: V) => option === value
export let useOptionIconImage = false export let useOptionIconImage = false
export let isOptionEnabled export let isOptionEnabled = (option: O, _index?: number) =>
export let readonly = false option as unknown as boolean
export let quiet = false export let readonly: boolean = false
export let autoWidth = false export let quiet: boolean = false
export let autocomplete = false export let autoWidth: boolean = false
export let sort = false export let autocomplete: boolean = false
export let align export let sort: boolean = false
export let footer = null export let align: PopoverAlignment | undefined = PopoverAlignment.Left
export let open = false export let footer: string | undefined = undefined
export let tag = null export let open: boolean = false
export let searchTerm = null export let searchTerm: string | undefined = undefined
export let loading export let loading: boolean | undefined = undefined
export let onOptionMouseenter = () => {} export let onOptionMouseenter = () => {}
export let onOptionMouseleave = () => {} export let onOptionMouseleave = () => {}
export let customPopoverHeight: string | undefined = undefined
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -35,24 +46,28 @@
$: fieldIcon = getFieldAttribute(getOptionIcon, value, options) $: fieldIcon = getFieldAttribute(getOptionIcon, value, options)
$: fieldColour = getFieldAttribute(getOptionColour, value, options) $: fieldColour = getFieldAttribute(getOptionColour, value, options)
function compareOptionAndValue(option, value) { function compareOptionAndValue(option: O, value: V) {
return typeof compare === "function" return typeof compare === "function"
? compare(option, value) ? compare(option, value)
: option === value : option === value
} }
const getFieldAttribute = (getAttribute, value, options) => { const getFieldAttribute = (getAttribute: any, value: V[], options: O[]) => {
// Wait for options to load if there is a value but no options // Wait for options to load if there is a value but no options
if (!options?.length) { if (!options?.length) {
return "" return ""
} }
const index = options.findIndex((option, idx) => const index = options.findIndex((option: any, idx: number) =>
compareOptionAndValue(getOptionValue(option, idx), value) compareOptionAndValue(getOptionValue(option, idx), value)
) )
return index !== -1 ? getAttribute(options[index], index) : null return index !== -1 ? getAttribute(options[index], index) : null
} }
const getFieldText = (value, options, placeholder) => { const getFieldText = (
value: any,
options: any,
placeholder: boolean | string
) => {
if (value == null || value === "") { if (value == null || value === "") {
// Explicit false means use no placeholder and allow an empty fields // Explicit false means use no placeholder and allow an empty fields
if (placeholder === false) { if (placeholder === false) {
@ -67,7 +82,7 @@
) )
} }
const selectOption = value => { const selectOption = (value: V) => {
dispatch("change", value) dispatch("change", value)
open = false open = false
} }
@ -98,14 +113,14 @@
{isOptionEnabled} {isOptionEnabled}
{autocomplete} {autocomplete}
{sort} {sort}
{tag}
{onOptionMouseenter} {onOptionMouseenter}
{onOptionMouseleave} {onOptionMouseleave}
isPlaceholder={value == null || value === ""} isPlaceholder={value == null || value === ""}
placeholderOption={placeholder === false placeholderOption={placeholder === false
? null ? undefined
: placeholder || "Choose an option"} : placeholder || "Choose an option"}
isOptionSelected={option => compareOptionAndValue(option, value)} isOptionSelected={option => compareOptionAndValue(option, value)}
onSelectOption={selectOption} onSelectOption={selectOption}
{loading} {loading}
{customPopoverHeight}
/> />

View File

@ -1,25 +1,25 @@
<script> <script lang="ts">
import "@spectrum-css/textfield/dist/index-vars.css" import "@spectrum-css/textfield/dist/index-vars.css"
import { createEventDispatcher, onMount, tick } from "svelte" import { createEventDispatcher, onMount, tick } from "svelte"
export let value = null export let value = null
export let placeholder = null export let placeholder: string | undefined = undefined
export let type = "text" export let type = "text"
export let disabled = false export let disabled = false
export let id = null export let id = null
export let readonly = false export let readonly = false
export let updateOnChange = true export let updateOnChange = true
export let quiet = false export let quiet = false
export let align export let align: "left" | "right" | "center" | undefined = undefined
export let autofocus = false export let autofocus = false
export let autocomplete = null export let autocomplete: boolean | undefined
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let field let field: any
let focus = false let focus = false
const updateValue = newValue => { const updateValue = (newValue: any) => {
if (readonly || disabled) { if (readonly || disabled) {
return return
} }
@ -37,7 +37,7 @@
focus = true focus = true
} }
const onBlur = event => { const onBlur = (event: any) => {
if (readonly || disabled) { if (readonly || disabled) {
return return
} }
@ -45,14 +45,14 @@
updateValue(event.target.value) updateValue(event.target.value)
} }
const onInput = event => { const onInput = (event: any) => {
if (readonly || !updateOnChange || disabled) { if (readonly || !updateOnChange || disabled) {
return return
} }
updateValue(event.target.value) updateValue(event.target.value)
} }
const updateValueOnEnter = event => { const updateValueOnEnter = (event: any) => {
if (readonly || disabled) { if (readonly || disabled) {
return return
} }
@ -61,13 +61,20 @@
} }
} }
const getInputMode = type => { const getInputMode = (type: any) => {
if (type === "bigint") { if (type === "bigint") {
return "numeric" return "numeric"
} }
return type === "number" ? "decimal" : "text" return type === "number" ? "decimal" : "text"
} }
$: autocompleteValue =
typeof autocomplete === "boolean"
? autocomplete
? "on"
: "off"
: undefined
onMount(async () => { onMount(async () => {
if (disabled) return if (disabled) return
focus = autofocus focus = autofocus
@ -104,7 +111,7 @@
class="spectrum-Textfield-input" class="spectrum-Textfield-input"
style={align ? `text-align: ${align};` : ""} style={align ? `text-align: ${align};` : ""}
inputmode={getInputMode(type)} inputmode={getInputMode(type)}
{autocomplete} autocomplete={autocompleteValue}
/> />
</div> </div>

View File

@ -1,14 +1,14 @@
<script> <script lang="ts">
import "@spectrum-css/fieldlabel/dist/index-vars.css" import "@spectrum-css/fieldlabel/dist/index-vars.css"
import FieldLabel from "./FieldLabel.svelte" import FieldLabel from "./FieldLabel.svelte"
import Icon from "../Icon/Icon.svelte" import Icon from "../Icon/Icon.svelte"
export let id = null export let id: string | undefined = undefined
export let label = null export let label: string | undefined = undefined
export let labelPosition = "above" export let labelPosition: string = "above"
export let error = null export let error: string | undefined = undefined
export let helpText = null export let helpText: string | undefined = undefined
export let tooltip = "" export let tooltip: string | undefined = undefined
</script> </script>
<div class="spectrum-Form-item" class:above={labelPosition === "above"}> <div class="spectrum-Form-item" class:above={labelPosition === "above"}>

View File

@ -1,24 +1,24 @@
<script> <script lang="ts">
import Field from "./Field.svelte" import Field from "./Field.svelte"
import TextField from "./Core/TextField.svelte" import TextField from "./Core/TextField.svelte"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
export let value = null export let value: any = undefined
export let label = null export let label: string | undefined = undefined
export let labelPosition = "above" export let labelPosition = "above"
export let placeholder = null export let placeholder: string | undefined = undefined
export let type = "text" export let type = "text"
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let error = null export let error: string | undefined = undefined
export let updateOnChange = true export let updateOnChange = true
export let quiet = false export let quiet = false
export let autofocus export let autofocus: boolean | undefined = undefined
export let autocomplete export let autocomplete: boolean | undefined = undefined
export let helpText = null export let helpText: string | undefined = undefined
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = (e: any) => {
value = e.detail value = e.detail
dispatch("change", e.detail) dispatch("change", e.detail)
} }
@ -27,7 +27,6 @@
<Field {helpText} {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<TextField <TextField
{updateOnChange} {updateOnChange}
{error}
{disabled} {disabled}
{readonly} {readonly}
{value} {value}

View File

@ -1,44 +1,54 @@
<script> <script lang="ts" context="module">
type O = any
type V = any
</script>
<script lang="ts">
import Field from "./Field.svelte" import Field from "./Field.svelte"
import Select from "./Core/Select.svelte" import Select from "./Core/Select.svelte"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { PopoverAlignment } from "../constants"
export let value = null export let value: V | undefined = undefined
export let label = undefined export let label: string | undefined = undefined
export let disabled = false export let disabled: boolean = false
export let readonly = false export let readonly: boolean = false
export let labelPosition = "above" export let labelPosition: string = "above"
export let error = null export let error: string | undefined = undefined
export let placeholder = "Choose an option" export let placeholder: string | boolean = "Choose an option"
export let options = [] export let options: O[] = []
export let getOptionLabel = option => extractProperty(option, "label") export let getOptionLabel = (option: O, _index?: number) =>
export let getOptionValue = option => extractProperty(option, "value") extractProperty(option, "label")
export let getOptionSubtitle = option => option?.subtitle export let getOptionValue = (option: O, _index?: number) =>
export let getOptionIcon = option => option?.icon extractProperty(option, "value")
export let getOptionColour = option => option?.colour export let getOptionSubtitle = (option: O, _index?: number) =>
option?.subtitle
export let getOptionIcon = (option: O, _index?: number) => option?.icon
export let getOptionColour = (option: O, _index?: number) => option?.colour
export let useOptionIconImage = false export let useOptionIconImage = false
export let isOptionEnabled = undefined export let isOptionEnabled:
export let quiet = false | ((_option: O, _index?: number) => boolean)
export let autoWidth = false | undefined = undefined
export let sort = false export let quiet: boolean = false
export let tooltip = "" export let autoWidth: boolean = false
export let autocomplete = false export let sort: boolean = false
export let customPopoverHeight = undefined export let tooltip: string | undefined = undefined
export let align = undefined export let autocomplete: boolean = false
export let footer = null export let customPopoverHeight: string | undefined = undefined
export let tag = null export let align: PopoverAlignment | undefined = PopoverAlignment.Left
export let helpText = null export let footer: string | undefined = undefined
export let compare = undefined export let helpText: string | undefined = undefined
export let compare: any = undefined
export let onOptionMouseenter = () => {} export let onOptionMouseenter = () => {}
export let onOptionMouseleave = () => {} export let onOptionMouseleave = () => {}
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = (e: CustomEvent<any>) => {
value = e.detail value = e.detail
dispatch("change", e.detail) dispatch("change", e.detail)
} }
const extractProperty = (value, property) => { const extractProperty = (value: any, property: any) => {
if (value && typeof value === "object") { if (value && typeof value === "object") {
return value[property] return value[property]
} }
@ -49,7 +59,6 @@
<Field {helpText} {label} {labelPosition} {error} {tooltip}> <Field {helpText} {label} {labelPosition} {error} {tooltip}>
<Select <Select
{quiet} {quiet}
{error}
{disabled} {disabled}
{readonly} {readonly}
{value} {value}
@ -68,7 +77,6 @@
{isOptionEnabled} {isOptionEnabled}
{autocomplete} {autocomplete}
{customPopoverHeight} {customPopoverHeight}
{tag}
{compare} {compare}
{onOptionMouseenter} {onOptionMouseenter}
{onOptionMouseleave} {onOptionMouseleave}

View File

@ -1,25 +1,25 @@
<script> <script lang="ts">
import "@spectrum-css/statuslight" import "@spectrum-css/statuslight"
export let size = "M" export let size: string = "M"
export let celery = false export let celery: boolean = false
export let yellow = false export let yellow: boolean = false
export let fuchsia = false export let fuchsia: boolean = false
export let indigo = false export let indigo: boolean = false
export let seafoam = false export let seafoam: boolean = false
export let chartreuse = false export let chartreuse: boolean = false
export let magenta = false export let magenta: boolean = false
export let purple = false export let purple: boolean = false
export let neutral = false export let neutral: boolean = false
export let info = false export let info: boolean = false
export let positive = false export let positive: boolean = false
export let notice = false export let notice: boolean = false
export let negative = false export let negative: boolean = false
export let disabled = false export let disabled: boolean = false
export let active = false export let active: boolean = false
export let color = null export let color: string | undefined = undefined
export let square = false export let square: boolean = false
export let hoverable = false export let hoverable: boolean = false
</script> </script>
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->

View File

@ -1,4 +1,4 @@
<script> <script lang="ts">
import { Icon, Input, Drawer, Button } from "@budibase/bbui" import { Icon, Input, Drawer, Button } from "@budibase/bbui"
import { import {
readableToRuntimeBinding, readableToRuntimeBinding,
@ -10,25 +10,25 @@
import { builderStore } from "@/stores/builder" import { builderStore } from "@/stores/builder"
export let panel = ClientBindingPanel export let panel = ClientBindingPanel
export let value = "" export let value: any = ""
export let bindings = [] export let bindings: any[] = []
export let title export let title: string | undefined = undefined
export let placeholder export let placeholder: string | undefined = undefined
export let label export let label: string | undefined = undefined
export let disabled = false export let disabled: boolean = false
export let allowHBS = true export let allowHBS: boolean = true
export let allowJS = true export let allowJS: boolean = true
export let allowHelpers = true export let allowHelpers: boolean = true
export let updateOnChange = true export let updateOnChange: boolean = true
export let key export let key: string | null = null
export let disableBindings = false export let disableBindings: boolean = false
export let forceModal = false export let forceModal: boolean = false
export let context = null export let context = null
export let autocomplete export let autocomplete: boolean | undefined = undefined
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let bindingDrawer let bindingDrawer: any
let currentVal = value let currentVal = value
$: readableValue = runtimeToReadableBinding(bindings, value) $: readableValue = runtimeToReadableBinding(bindings, value)
@ -38,7 +38,7 @@
const saveBinding = () => { const saveBinding = () => {
onChange(tempValue) onChange(tempValue)
onBlur() onBlur()
builderStore.propertyFocus() builderStore.propertyFocus(null)
bindingDrawer.hide() bindingDrawer.hide()
} }
@ -46,7 +46,7 @@
save: saveBinding, save: saveBinding,
}) })
const onChange = value => { const onChange = (value: any) => {
currentVal = readableToRuntimeBinding(bindings, value) currentVal = readableToRuntimeBinding(bindings, value)
dispatch("change", currentVal) dispatch("change", currentVal)
} }
@ -55,8 +55,8 @@
dispatch("blur", currentVal) dispatch("blur", currentVal)
} }
const onDrawerHide = e => { const onDrawerHide = (e: any) => {
builderStore.propertyFocus() builderStore.propertyFocus(null)
dispatch("drawerHide", e.detail) dispatch("drawerHide", e.detail)
} }
</script> </script>

View File

@ -5,7 +5,6 @@
runtimeToReadableBinding, runtimeToReadableBinding,
} from "@/dataBinding" } from "@/dataBinding"
import { builderStore } from "@/stores/builder" import { builderStore } from "@/stores/builder"
import { onDestroy } from "svelte"
export let label = "" export let label = ""
export let labelHidden = false export let labelHidden = false
@ -32,7 +31,7 @@
$: safeValue = getSafeValue(value, defaultValue, allBindings) $: safeValue = getSafeValue(value, defaultValue, allBindings)
$: replaceBindings = val => readableToRuntimeBinding(allBindings, val) $: replaceBindings = val => readableToRuntimeBinding(allBindings, val)
$: if (!Array.isArray(value)) { $: if (value) {
highlightType = highlightType =
highlightedProp?.key === key ? `highlighted-${highlightedProp?.type}` : "" highlightedProp?.key === key ? `highlighted-${highlightedProp?.type}` : ""
} }
@ -75,12 +74,6 @@
? defaultValue ? defaultValue
: enriched : enriched
} }
onDestroy(() => {
if (highlightedProp) {
builderStore.highlightSetting(null)
}
})
</script> </script>
<div <div

View File

@ -1159,10 +1159,17 @@ export const buildFormSchema = (component, asset) => {
* Returns an array of the keys of any state variables which are set anywhere * Returns an array of the keys of any state variables which are set anywhere
* in the app. * in the app.
*/ */
export const getAllStateVariables = () => { export const getAllStateVariables = screen => {
// Find all button action settings in all components let assets = []
if (screen) {
// only include state variables from a specific screen
assets.push(screen)
} else {
// otherwise include state variables from all screens
assets = getAllAssets()
}
let eventSettings = [] let eventSettings = []
getAllAssets().forEach(asset => { assets.forEach(asset => {
findAllMatchingComponents(asset.props, component => { findAllMatchingComponents(asset.props, component => {
const settings = componentStore.getComponentSettings(component._component) const settings = componentStore.getComponentSettings(component._component)
const nestedTypes = [ const nestedTypes = [
@ -1214,11 +1221,17 @@ export const getAllStateVariables = () => {
}) })
// Add on load settings from screens // Add on load settings from screens
get(screenStore).screens.forEach(screen => { if (screen) {
if (screen.onLoad) { if (screen.onLoad) {
eventSettings.push(screen.onLoad) eventSettings.push(screen.onLoad)
} }
}) } else {
get(screenStore).screens.forEach(screen => {
if (screen.onLoad) {
eventSettings.push(screen.onLoad)
}
})
}
// Extract all state keys from any "update state" actions in each setting // Extract all state keys from any "update state" actions in each setting
let bindingSet = new Set() let bindingSet = new Set()

View File

@ -16,6 +16,7 @@
} from "@/dataBinding" } from "@/dataBinding"
import { ActionButton, notifications } from "@budibase/bbui" import { ActionButton, notifications } from "@budibase/bbui"
import { capitalise } from "@/helpers" import { capitalise } from "@/helpers"
import { builderStore } from "@/stores/builder"
import TourWrap from "@/components/portal/onboarding/TourWrap.svelte" import TourWrap from "@/components/portal/onboarding/TourWrap.svelte"
import { TOUR_STEP_KEYS } from "@/components/portal/onboarding/tours.js" import { TOUR_STEP_KEYS } from "@/components/portal/onboarding/tours.js"
@ -55,6 +56,17 @@
$: id = $selectedComponent?._id $: id = $selectedComponent?._id
$: id, (section = tabs[0]) $: id, (section = tabs[0])
$: componentName = getComponentName(componentInstance) $: componentName = getComponentName(componentInstance)
$: highlightedSetting = $builderStore.highlightedSetting
$: if (highlightedSetting) {
if (highlightedSetting.key === "_conditions") {
section = "conditions"
} else if (highlightedSetting.key === "_styles") {
section = "styles"
} else if (highlightedSetting.key === "_settings") {
section = "settings"
}
}
</script> </script>
{#if $selectedComponent} {#if $selectedComponent}

View File

@ -9,6 +9,7 @@
import { componentStore } from "@/stores/builder" import { componentStore } from "@/stores/builder"
import ConditionalUIDrawer from "./ConditionalUIDrawer.svelte" import ConditionalUIDrawer from "./ConditionalUIDrawer.svelte"
import ComponentSettingsSection from "./ComponentSettingsSection.svelte" import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
import { builderStore } from "@/stores/builder"
export let componentInstance export let componentInstance
export let componentDefinition export let componentDefinition
@ -18,6 +19,8 @@
let tempValue let tempValue
let drawer let drawer
$: highlighted = $builderStore.highlightedSetting?.key === "_conditions"
const openDrawer = () => { const openDrawer = () => {
tempValue = JSON.parse(JSON.stringify(componentInstance?._conditions ?? [])) tempValue = JSON.parse(JSON.stringify(componentInstance?._conditions ?? []))
drawer.show() drawer.show()
@ -52,7 +55,9 @@
/> />
<DetailSummary name={"Conditions"} collapsible={false}> <DetailSummary name={"Conditions"} collapsible={false}>
<ActionButton on:click={openDrawer}>{conditionText}</ActionButton> <div class:highlighted>
<ActionButton fullWidth on:click={openDrawer}>{conditionText}</ActionButton>
</div>
</DetailSummary> </DetailSummary>
<Drawer bind:this={drawer} title="Conditions"> <Drawer bind:this={drawer} title="Conditions">
<svelte:fragment slot="description"> <svelte:fragment slot="description">
@ -61,3 +66,17 @@
<Button cta slot="buttons" on:click={() => save()}>Save</Button> <Button cta slot="buttons" on:click={() => save()}>Save</Button>
<ConditionalUIDrawer slot="body" bind:conditions={tempValue} {bindings} /> <ConditionalUIDrawer slot="body" bind:conditions={tempValue} {bindings} />
</Drawer> </Drawer>
<style>
.highlighted {
background: var(--spectrum-global-color-gray-300);
border-left: 4px solid var(--spectrum-semantic-informative-color-background);
transition: background 130ms ease-out, border-color 130ms ease-out;
margin-top: -3.5px;
margin-bottom: -3.5px;
padding-bottom: 3.5px;
padding-top: 3.5px;
padding-left: 5px;
padding-right: 5px;
}
</style>

View File

@ -16,6 +16,7 @@
readableToRuntimeBinding, readableToRuntimeBinding,
runtimeToReadableBinding, runtimeToReadableBinding,
} from "@/dataBinding" } from "@/dataBinding"
import { builderStore } from "@/stores/builder"
export let componentInstance export let componentInstance
export let componentDefinition export let componentDefinition
@ -32,6 +33,8 @@
$: icon = componentDefinition?.icon $: icon = componentDefinition?.icon
$: highlighted = $builderStore.highlightedSetting?.key === "_styles"
const openDrawer = () => { const openDrawer = () => {
tempValue = runtimeToReadableBinding( tempValue = runtimeToReadableBinding(
bindings, bindings,
@ -55,7 +58,7 @@
name={`Custom CSS${componentInstance?._styles?.custom ? " *" : ""}`} name={`Custom CSS${componentInstance?._styles?.custom ? " *" : ""}`}
collapsible={false} collapsible={false}
> >
<div> <div class:highlighted>
<ActionButton on:click={openDrawer}>Edit custom CSS</ActionButton> <ActionButton on:click={openDrawer}>Edit custom CSS</ActionButton>
</div> </div>
</DetailSummary> </DetailSummary>
@ -97,4 +100,16 @@
align-items: center; align-items: center;
gap: var(--spacing-m); gap: var(--spacing-m);
} }
.highlighted {
background: var(--spectrum-global-color-gray-300);
border-left: 4px solid var(--spectrum-semantic-informative-color-background);
transition: background 130ms ease-out, border-color 130ms ease-out;
margin-top: -3.5px;
margin-bottom: -3.5px;
padding-bottom: 3.5px;
padding-top: 3.5px;
padding-left: 5px;
padding-right: 5px;
}
</style> </style>

View File

@ -3,6 +3,7 @@
import ComponentList from "./ComponentList/index.svelte" import ComponentList from "./ComponentList/index.svelte"
import { getHorizontalResizeActions } from "@/components/common/resizable" import { getHorizontalResizeActions } from "@/components/common/resizable"
import { ActionButton } from "@budibase/bbui" import { ActionButton } from "@budibase/bbui"
import StatePanel from "./StatePanel.svelte"
import BindingsPanel from "./BindingsPanel.svelte" import BindingsPanel from "./BindingsPanel.svelte"
import ComponentKeyHandler from "./ComponentKeyHandler.svelte" import ComponentKeyHandler from "./ComponentKeyHandler.svelte"
@ -36,7 +37,7 @@
{:else if activeTab === Tabs.Bindings} {:else if activeTab === Tabs.Bindings}
<BindingsPanel /> <BindingsPanel />
{:else if activeTab === Tabs.State} {:else if activeTab === Tabs.State}
<div class="tab-content">State</div> <div class="tab-content"><StatePanel /></div>
{/if} {/if}
</div> </div>
<div class="divider"> <div class="divider">

View File

@ -1,13 +1,342 @@
<script> <script lang="ts">
import { ActionButton, Modal, ModalContent } from "@budibase/bbui" import { onMount } from "svelte"
import { Select } from "@budibase/bbui"
import type { Component } from "@budibase/types"
import { getAllStateVariables, getBindableProperties } from "@/dataBinding"
import {
componentStore,
selectedScreen,
builderStore,
previewStore,
} from "@/stores/builder"
import {
decodeJSBinding,
findHBSBlocks,
isJSBinding,
processStringSync,
} from "@budibase/string-templates"
import DrawerBindableInput from "@/components/common/bindings/DrawerBindableInput.svelte"
let modal interface ComponentUsingState {
id: string
name: string
settings: string[]
}
$: keyOptions = getAllStateVariables($selectedScreen)
$: bindings = getBindableProperties(
$selectedScreen,
$componentStore.selectedComponentId
)
let selectedKey: string | undefined = undefined
let componentsUsingState: ComponentUsingState[] = []
let componentsUpdatingState: ComponentUsingState[] = []
let editorValue: string = ""
let previousScreenId: string | undefined = undefined
$: {
const screenChanged =
$selectedScreen && $selectedScreen._id !== previousScreenId
const previewContext = $previewStore.selectedComponentContext || {}
if (screenChanged) {
selectedKey = keyOptions[0]
componentsUsingState = []
componentsUpdatingState = []
editorValue = ""
previousScreenId = $selectedScreen._id
}
if (keyOptions.length > 0 && !keyOptions.includes(selectedKey)) {
selectedKey = keyOptions[0]
}
if (selectedKey) {
searchComponents(selectedKey)
editorValue = previewContext.state?.[selectedKey] ?? ""
}
}
const findComponentsUpdatingState = (
component: Component,
stateKey: string
): ComponentUsingState[] => {
let foundComponents: ComponentUsingState[] = []
const eventHandlerProps = [
"onClick",
"onChange",
"onRowClick",
"onChange",
"buttonOnClick",
]
eventHandlerProps.forEach(eventType => {
const handlers = component[eventType]
if (Array.isArray(handlers)) {
handlers.forEach(handler => {
if (
handler["##eventHandlerType"] === "Update State" &&
handler.parameters?.key === stateKey
) {
foundComponents.push({
id: component._id!,
name: component._instanceName,
settings: [eventType],
})
}
})
}
})
if (component._children) {
for (let child of component._children) {
foundComponents = [
...foundComponents,
...findComponentsUpdatingState(child, stateKey),
]
}
}
return foundComponents
}
const hasStateBinding = (value: string, stateKey: string): boolean => {
const bindings = findHBSBlocks(value).map(binding => {
const sanitizedBinding = binding.replace(/\\"/g, '"')
return isJSBinding(sanitizedBinding)
? decodeJSBinding(sanitizedBinding)
: sanitizedBinding
})
return bindings.join(" ").includes(stateKey)
}
const getSettingsWithState = (component: any, stateKey: string): string[] => {
const settingsWithState: string[] = []
for (const [setting, value] of Object.entries(component)) {
if (typeof value === "string" && hasStateBinding(value, stateKey)) {
settingsWithState.push(setting)
}
}
return settingsWithState
}
const checkConditions = (conditions: any[], stateKey: string): boolean => {
return conditions.some(condition =>
[condition.referenceValue, condition.newValue].some(
value => typeof value === "string" && hasStateBinding(value, stateKey)
)
)
}
const checkStyles = (styles: any, stateKey: string): boolean => {
return (
typeof styles?.custom === "string" &&
hasStateBinding(styles.custom, stateKey)
)
}
const findComponentsUsingState = (
component: any,
stateKey: string
): ComponentUsingState[] => {
let componentsUsingState: ComponentUsingState[] = []
const { _children, _styles, _conditions, ...componentSettings } = component
const settingsWithState = getSettingsWithState(componentSettings, stateKey)
settingsWithState.forEach(setting => {
componentsUsingState.push({
id: component._id,
name: `${component._instanceName} (${setting})`,
settings: [setting],
})
})
if (_conditions?.length > 0 && checkConditions(_conditions, stateKey)) {
componentsUsingState.push({
id: component._id,
name: `${component._instanceName} (conditions)`,
settings: ["_conditions"],
})
}
if (_styles && checkStyles(_styles, stateKey)) {
componentsUsingState.push({
id: component._id,
name: `${component._instanceName} (styles)`,
settings: ["_styles"],
})
}
if (_children) {
for (let child of _children) {
componentsUsingState = [
...componentsUsingState,
...findComponentsUsingState(child, stateKey),
]
}
}
return componentsUsingState
}
const searchComponents = (stateKey: string | undefined) => {
if (!stateKey || !$selectedScreen?.props) {
return
}
const componentStateUpdates = findComponentsUpdatingState(
$selectedScreen.props,
stateKey
)
componentsUsingState = findComponentsUsingState(
$selectedScreen.props,
stateKey
)
const screenStateUpdates =
$selectedScreen?.onLoad
?.filter(
(handler: any) =>
handler["##eventHandlerType"] === "Update State" &&
handler.parameters?.key === stateKey
)
.map(() => ({
id: $selectedScreen._id!,
name: "Screen onLoad",
settings: ["onLoad"],
})) || []
componentsUpdatingState = [...componentStateUpdates, ...screenStateUpdates]
}
const handleStateKeySelect = (key: CustomEvent) => {
if (!key.detail && keyOptions.length > 0) {
throw new Error("No state key selected")
}
searchComponents(key.detail)
}
const onClickComponentLink = (component: ComponentUsingState) => {
componentStore.select(component.id)
component.settings.forEach(setting => {
builderStore.highlightSetting(setting)
})
}
const handleStateInspectorChange = (e: CustomEvent) => {
if (!selectedKey || !$previewStore.selectedComponentContext) {
return
}
const stateUpdate = {
[selectedKey]: processStringSync(
e.detail,
$previewStore.selectedComponentContext
),
}
previewStore.updateState(stateUpdate)
editorValue = e.detail
}
onMount(() => {
previewStore.requestComponentContext()
})
</script> </script>
<ActionButton on:click={modal.show}>State</ActionButton> <div class="state-panel">
<Select
label="State variables"
bind:value={selectedKey}
placeholder={keyOptions.length > 0 ? false : "No state variables found"}
options={keyOptions}
on:change={handleStateKeySelect}
/>
{#if selectedKey && keyOptions.length > 0}
<DrawerBindableInput
value={editorValue}
title={`Set value for "${selectedKey}"`}
placeholder="Enter a value"
label="Set temporary value for design preview"
on:change={e => handleStateInspectorChange(e)}
{bindings}
/>
{/if}
{#if componentsUsingState.length > 0}
<div class="section">
<span class="text">Updates</span>
<div class="updates-section">
{#each componentsUsingState as component}
<button
class="component-link updates-colour"
on:click={() => onClickComponentLink(component)}
>
{component.name}
</button>
{/each}
</div>
</div>
{/if}
{#if componentsUpdatingState.length > 0}
<div class="section">
<span class="text">Controlled by</span>
<div class="updates-section">
{#each componentsUpdatingState as component}
<button
class="component-link controlled-by-colour"
on:click={() => onClickComponentLink(component)}
>
{component.name}
</button>
{/each}
</div>
</div>
{/if}
</div>
<Modal bind:this={modal}> <style>
<ModalContent title="State" showConfirmButton={false} cancelText="Close"> .state-panel {
Some awesome state content. background-color: var(--spectrum-alias-background-color-primary);
</ModalContent> display: flex;
</Modal> flex-direction: column;
gap: var(--spacing-m);
}
.section {
display: flex;
flex-direction: column;
gap: var(--spacing-s);
margin-top: var(--spacing-s);
}
.text {
color: var(--spectrum-global-color-gray-700);
font-size: 12px;
}
.updates-colour {
color: var(--bb-indigo-light);
}
.controlled-by-colour {
color: var(--spectrum-global-color-orange-700);
}
.component-link {
display: inline-block;
border: none;
background: none;
text-decoration: underline;
cursor: pointer;
padding: 0;
white-space: nowrap;
font-size: 12px;
}
.component-link:hover {
text-decoration: underline;
}
.updates-section {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-s);
}
</style>

View File

@ -20,6 +20,7 @@ import {
previewStore, previewStore,
tables, tables,
componentTreeNodesStore, componentTreeNodesStore,
builderStore,
} from "@/stores/builder" } from "@/stores/builder"
import { buildFormSchema, getSchemaForDatasource } from "@/dataBinding" import { buildFormSchema, getSchemaForDatasource } from "@/dataBinding"
import { import {
@ -736,10 +737,16 @@ export class ComponentStore extends BudiStore<ComponentState> {
* *
* @param {string} componentId * @param {string} componentId
*/ */
select(componentId: string) { select(id: string) {
this.update(state => { this.update(state => {
state.selectedComponentId = componentId // Only clear highlights if selecting a different component
return state if (!id.includes(state.selectedComponentId!)) {
builderStore.highlightSetting()
}
return {
...state,
selectedComponentId: id,
}
}) })
} }

View File

@ -82,6 +82,10 @@ export class PreviewStore extends BudiStore<PreviewState> {
})) }))
} }
updateState(data: Record<string, any>) {
this.sendEvent("builder-state", data)
}
requestComponentContext() { requestComponentContext() {
this.sendEvent("request-context") this.sendEvent("request-context")
} }

View File

@ -9,6 +9,7 @@ import {
dndStore, dndStore,
eventStore, eventStore,
hoverStore, hoverStore,
stateStore,
} from "./stores" } from "./stores"
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js" import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
import { get } from "svelte/store" import { get } from "svelte/store"
@ -104,6 +105,9 @@ const loadBudibase = async () => {
hoverStore.actions.hoverComponent(data, false) hoverStore.actions.hoverComponent(data, false)
} else if (type === "builder-meta") { } else if (type === "builder-meta") {
builderStore.actions.setMetadata(data) builderStore.actions.setMetadata(data)
} else if (type === "builder-state") {
const [[key, value]] = Object.entries(data)
stateStore.actions.setValue(key, value)
} }
} }

View File

@ -23,6 +23,7 @@ export interface Screen extends Document {
props: ScreenProps props: ScreenProps
name?: string name?: string
pluginAdded?: boolean pluginAdded?: boolean
onLoad?: EventHandler[]
} }
export interface ScreenRoutesViewOutput extends Document { export interface ScreenRoutesViewOutput extends Document {
@ -36,3 +37,14 @@ export type ScreenRoutingJson = Record<
subpaths: Record<string, any> subpaths: Record<string, any>
} }
> >
export interface EventHandler {
parameters: {
key: string
type: string
value: string
persist: any | null
}
eventHandlerType: string
id: string
}