Add functional bindings panel

This commit is contained in:
Andrew Kingston 2025-01-15 14:52:27 +00:00
parent cc765ef6b4
commit 1048a50627
No known key found for this signature in database
2 changed files with 282 additions and 6 deletions

View File

@ -0,0 +1,188 @@
<script lang="ts">
import { Icon } from "@budibase/bbui"
export let label: string | undefined
export let value: any
export let root: boolean = true
export let path: (string | number)[] = []
const Colors = {
Undefined: "var(--spectrum-global-color-gray-600)",
Null: "purple",
String: "orange",
Number: "blue",
True: "green",
False: "red",
Date: "pink",
}
let expanded = false
$: isArray = Array.isArray(value)
$: isObject = value?.toString?.() === "[object Object]"
$: keys = isArray || isObject ? Object.keys(value).sort() : []
$: expandable = keys.length > 0
$: displayValue = getDisplayValue(isArray, isObject, keys, value)
$: style = getStyle(expandable, value)
$: readableBinding = `{{ ${path.join(".")} }}`
const pluralise = (text: string, number: number) => {
return number === 1 ? text : text + "s"
}
const getDisplayValue = (
isArray: boolean,
isObject: boolean,
keys: any[],
value: any
) => {
if (isArray) {
return `[] ${keys.length} ${pluralise("item", keys.length)}`
}
if (isObject) {
return `{} ${keys.length} ${pluralise("key", keys.length)}`
}
if (typeof value === "object" && typeof value?.toString === "function") {
return JSON.stringify(value.toString(), null, 2)
} else {
return JSON.stringify(value, null, 2)
}
}
const getStyle = (expandable: boolean, value: any) => {
let style = ""
const color = getColor(expandable, value)
if (color) {
style += `color:${color};`
}
return style
}
const getColor = (expandable: boolean, value: any) => {
if (expandable) {
return
}
switch (value) {
case undefined:
return Colors.Undefined
case null:
return Colors.Null
case true:
return Colors.True
case false:
return Colors.False
}
switch (typeof value) {
case "string":
return Colors.String
case "number":
return Colors.Number
}
if (value instanceof Date) {
return Colors.Date
}
}
$: console.log(path)
</script>
<div class="binding-node">
{#if label}
<div class="binding-text" title={readableBinding}>
<div class="binding-arrow">
{#if expandable}
<Icon
name={expanded ? "ChevronDown" : "ChevronRight"}
hoverable
color="var(--spectrum-global-color-gray-600)"
on:click={() => (expanded = !expanded)}
/>
{/if}
</div>
<div class="binding-label" class:expandable>
{label}
</div>
<div class="binding-value" class:expandable {style}>
{displayValue}
</div>
</div>
{/if}
{#if expandable && (expanded || !label)}
<div class="binding-children" class:root>
{#each keys as key}
<svelte:self
label={key}
value={value[key]}
root={false}
path={[...path, key]}
/>
{/each}
</div>
{/if}
</div>
<style>
.binding-node {
position: relative;
display: flex;
flex-direction: column;
gap: 8px;
overflow: hidden;
}
.binding-arrow {
margin-right: 2px;
flex: 0 0 18px;
}
.binding-text {
display: flex;
flex-direction: row;
align-items: center;
font-family: monospace;
font-size: 12px;
width: 100%;
}
.binding-children {
display: flex;
flex-direction: column;
gap: 8px;
/* border-left: 1px solid var(--spectrum-global-color-gray-400); */
/* margin-left: 20px; */
padding-left: 18px;
}
.binding-children.root {
border-left: none;
margin-left: 0;
padding-left: 0;
}
/* Size label and value according to type */
.binding-label,
.binding-value {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.binding-label {
flex: 0 0 auto;
max-width: 50%;
margin-right: 8px;
}
.binding-value {
flex: 1 1 auto;
color: var(--spectrum-global-color-gray-600);
}
.binding-label.expandable {
flex: 0 1 auto;
max-width: none;
}
.binding-value.expandable {
flex: 0 0 auto;
}
/* Trim spans in the highlighted HTML */
.binding-value :global(span) {
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
}
</style>

View File

@ -1,14 +1,102 @@
<script>
<script lang="ts">
import { ActionButton, Modal, ModalContent } from "@budibase/bbui"
import {
previewStore,
selectedScreen,
componentStore,
snippets,
} from "@/stores/builder"
import { getBindableProperties } from "@/dataBinding"
import { processObjectSync } from "@budibase/string-templates"
import BindingNode from "./BindingExplorer/BindingNode.svelte"
let visible = false
let modal
enum ValueType {
Object = "Object",
Array = "Array",
Primitive = "Primitive",
}
// Minimal typing for the real data binding structure, as none exists
type DataBinding = {
category: string
runtimeBinding: string
readableBinding: string
}
type BindingEntry = {
readableBinding: string
runtimeBinding: string | null
value: any
valueType: ValueType
}
type BindingMap = {
[key: string]: BindingEntry
}
let modal: any
$: context = {
...($previewStore.selectedComponentContext || {}),
date: new Date(),
string: "foo",
number: 1234,
undefined: undefined,
null: null,
true: true,
false: false,
array: [1, 2, 3],
object: { foo: "bar" },
error: new Error(),
}
$: selectedComponentId = $componentStore.selectedComponentId
$: bindings = getBindableProperties($selectedScreen, selectedComponentId)
$: enrichedBindings = enrichBindings(bindings, context, $snippets)
const show = () => {
previewStore.requestComponentContext()
modal.show()
}
const enrichBindings = (
bindings: DataBinding[],
context: Record<string, any>,
snippets: any
) => {
// Create a single big array to enrich in one go
const bindingStrings = bindings.map(binding => {
if (binding.runtimeBinding.startsWith('trim "')) {
// Account for nasty hardcoded HBS bindings for roles, for legacy
// compatibility
return `{{ ${binding.runtimeBinding} }}`
} else {
return `{{ literal ${binding.runtimeBinding} }}`
}
})
const bindingEvauations = processObjectSync(bindingStrings, {
...context,
snippets,
}) as any[]
// Enrich bindings with evaluations and highlighted HTML
const flatBindings = bindings.map((binding, idx) => ({
...binding,
value: bindingEvauations[idx],
}))
return flatBindings
}
</script>
<ActionButton on:click={modal.show}>Bindings</ActionButton>
<ActionButton on:click={show}>Bindings</ActionButton>
<Modal bind:this={modal}>
<ModalContent title="Bindings" showConfirmButton={false} cancelText="Close">
Some awesome bindings content.
<ModalContent
title="Bindings"
showConfirmButton={false}
cancelText="Close"
size="M"
>
<BindingNode value={context} />
</ModalContent>
</Modal>