Add functional bindings panel
This commit is contained in:
parent
cc765ef6b4
commit
1048a50627
|
@ -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>
|
|
@ -1,14 +1,102 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { ActionButton, Modal, ModalContent } from "@budibase/bbui"
|
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
|
enum ValueType {
|
||||||
let modal
|
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>
|
</script>
|
||||||
|
|
||||||
<ActionButton on:click={modal.show}>Bindings</ActionButton>
|
<ActionButton on:click={show}>Bindings</ActionButton>
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<ModalContent title="Bindings" showConfirmButton={false} cancelText="Close">
|
<ModalContent
|
||||||
Some awesome bindings content.
|
title="Bindings"
|
||||||
|
showConfirmButton={false}
|
||||||
|
cancelText="Close"
|
||||||
|
size="M"
|
||||||
|
>
|
||||||
|
<BindingNode value={context} />
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
Loading…
Reference in New Issue