add link for each individual setting

This commit is contained in:
Peter Clement 2025-01-23 10:08:34 +00:00
parent c199b681e3
commit 8cab28d5b6
3 changed files with 134 additions and 60 deletions

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,14 @@
$: id = $selectedComponent?._id $: id = $selectedComponent?._id
$: id, (section = tabs[0]) $: id, (section = tabs[0])
$: componentName = getComponentName(componentInstance) $: componentName = getComponentName(componentInstance)
$: highlightedSettings = $builderStore.highlightedSettings
$: if (highlightedSettings?.length) {
const settings = highlightedSettings.map(s => s.key)
if (settings.length === 1 && settings[0] === "_conditions") {
section = "conditions"
}
}
</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,10 @@
let tempValue let tempValue
let drawer let drawer
$: highlightCondition = $builderStore.highlightedSettings?.find(
setting => setting.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 +57,9 @@
/> />
<DetailSummary name={"Conditions"} collapsible={false}> <DetailSummary name={"Conditions"} collapsible={false}>
<ActionButton on:click={openDrawer}>{conditionText}</ActionButton> <div class:highlighted={highlightCondition}>
<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 +68,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

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Divider, Select, Heading } from "@budibase/bbui" import { Select, Link } from "@budibase/bbui"
import { getAllStateVariables } from "@/dataBinding" import { getAllStateVariables } from "@/dataBinding"
import { import {
componentStore, componentStore,
@ -13,7 +13,7 @@
isJSBinding, isJSBinding,
} from "@budibase/string-templates" } from "@budibase/string-templates"
import { onMount } from "svelte" import { onMount } from "svelte"
import CodeMirrorEditor from "@/components/common/CodeMirrorEditor.svelte" import DrawerBindableInput from "@/components/common/bindings/DrawerBindableInput.svelte"
type ComponentUsingState = { type ComponentUsingState = {
id: string id: string
@ -40,9 +40,8 @@
const previewContext = $previewStore.selectedComponentContext || {} const previewContext = $previewStore.selectedComponentContext || {}
if (selectedKey && previewContext.state) { if (selectedKey && previewContext.state) {
// It's unlikely value will ever be populated immediately as state is never populated automatically in preview // It's unlikely value will ever be populated immediately as preview never has state values on load
const value = previewContext.state[selectedKey] ?? null editorValue = previewContext.state[selectedKey] ?? null
editorValue = JSON.stringify(value, null, 2)
editorError = null editorError = null
} else { } else {
editorValue = "" editorValue = ""
@ -50,10 +49,10 @@
} }
} }
function findComponentsUpdatingState( const findComponentsUpdatingState = (
component: any, component: any,
stateKey: string stateKey: string
): ComponentUsingState[] { ): ComponentUsingState[] => {
let foundComponents: ComponentUsingState[] = [] let foundComponents: ComponentUsingState[] = []
const eventHandlerProps = ["onClick", "onChange"] const eventHandlerProps = ["onClick", "onChange"]
@ -87,40 +86,77 @@
return foundComponents return foundComponents
} }
function findComponentsUsingState( const findComponentsUsingState = (
component: any, component: any,
stateKey: string stateKey: string
): ComponentUsingState[] { ): ComponentUsingState[] => {
let componentsUsingState: ComponentUsingState[] = [] let componentsUsingState: ComponentUsingState[] = []
const { _children, ...componentSettings } = component const processValue = (value: string): boolean => {
let settingsWithState: string[] = []
for (const [setting, value] of Object.entries(componentSettings)) {
if (typeof value === "string") {
const bindings = findHBSBlocks(value).map(binding => { const bindings = findHBSBlocks(value).map(binding => {
let sanitizedBinding = binding.replace(/\\"/g, '"') const sanitizedBinding = binding.replace(/\\"/g, '"')
if (isJSBinding(sanitizedBinding)) { return isJSBinding(sanitizedBinding)
return decodeJSBinding(sanitizedBinding) ? decodeJSBinding(sanitizedBinding)
} else { : sanitizedBinding
return sanitizedBinding
}
}) })
const bindingString = bindings.join(" ") return bindings.join(" ").includes(stateKey)
if (bindingString.includes(stateKey)) {
settingsWithState.push(setting)
}
}
} }
if (settingsWithState.length > 0) { const { _children, ...componentSettings } = component
// Check normal settings
for (const [setting, value] of Object.entries(componentSettings)) {
if (typeof value === "string" && processValue(value)) {
componentsUsingState.push({ componentsUsingState.push({
id: component._id, id: component._id,
name: component._instanceName, name: `${component._instanceName} (${setting})`,
settings: settingsWithState, settings: [setting],
}) })
} }
}
// Check conditions
if (component._conditions?.length > 0) {
for (const condition of component._conditions) {
const conditionValues = [condition.referenceValue, condition.newValue]
if (
conditionValues.some(
value => typeof value === "string" && processValue(value)
)
) {
componentsUsingState.push({
id: component._id,
name: `${component._instanceName} (conditions)`,
settings: ["_conditions"],
})
break
}
}
}
// Check styles
if (component._styles) {
const checkStyleObject = (obj: any) => {
for (const [, value] of Object.entries(obj)) {
if (typeof value === "string" && processValue(value)) {
return true
}
}
return false
}
const hasStateInStyles = Object.values(component._styles).some(styleObj =>
checkStyleObject(styleObj)
)
if (hasStateInStyles) {
componentsUsingState.push({
id: component._id,
name: `${component._instanceName} (styles)`,
settings: ["_styles"],
})
}
}
if (_children) { if (_children) {
for (let child of _children) { for (let child of _children) {
@ -134,7 +170,7 @@
return componentsUsingState return componentsUsingState
} }
function searchComponents(stateKey: string) { const searchComponents = (stateKey: string) => {
if (!stateKey || !$selectedScreen?.props) { if (!stateKey || !$selectedScreen?.props) {
return return
} }
@ -148,22 +184,21 @@
) )
} }
function handleStateKeySelect(event: CustomEvent) { const handleStateKeySelect = (key: CustomEvent) => {
selectedKey = event.detail if (!key.detail) {
if (!selectedKey) {
throw new Error("No state key selected") throw new Error("No state key selected")
} }
searchComponents(selectedKey) searchComponents(key.detail)
} }
function onClickComponentLink(component: ComponentUsingState) { const onClickComponentLink = (component: ComponentUsingState) => {
componentStore.select(component.id) componentStore.select(component.id)
component.settings.forEach(setting => { component.settings.forEach(setting => {
builderStore.highlightSetting(setting) builderStore.highlightSetting(setting)
}) })
} }
function handleStateInspectorChange(e: CustomEvent) { const handleStateInspectorChange = (e: CustomEvent) => {
if (!selectedKey || !$previewStore.selectedComponentContext) { if (!selectedKey || !$previewStore.selectedComponentContext) {
throw new Error("No state key selected") throw new Error("No state key selected")
} }
@ -172,9 +207,7 @@
return return
} }
try { const stateUpdate = { [selectedKey]: e.detail }
const value = JSON.parse(e.detail)
const stateUpdate = { [selectedKey]: value }
editorError = null editorError = null
previewStore.updateState(stateUpdate) previewStore.updateState(stateUpdate)
@ -183,37 +216,31 @@
state: stateUpdate, state: stateUpdate,
}) })
previewStore.requestComponentContext() previewStore.requestComponentContext()
} catch (err) {
editorError = "Invalid JSON value"
}
} }
</script> </script>
<div class="state-panel"> <div class="state-panel">
<Heading size="S">State</Heading>
<div>Showing state variables for this screen</div>
<div class="section"> <div class="section">
<Select <Select
label="State variables"
value={selectedKey} value={selectedKey}
placeholder="Type here..." placeholder="Type here..."
options={keyOptions} options={keyOptions}
on:change={handleStateKeySelect} on:change={handleStateKeySelect}
/> />
</div> </div>
<Divider />
<div class="section"> <div class="section">
<CodeMirrorEditor <DrawerBindableInput
height={200}
value={editorValue} value={editorValue}
resize="vertical" title={`Set value for "${selectedKey}"`}
label="State Inspector" placeholder="Enter a value"
label="Set temporary value for design preview"
on:change={handleStateInspectorChange} on:change={handleStateInspectorChange}
/> />
{#if editorError} {#if editorError}
<div class="error">{editorError}</div> <div class="error">{editorError}</div>
{/if} {/if}
</div> </div>
<Divider />
{#if componentsUsingState.length > 0} {#if componentsUsingState.length > 0}
<div class="section"> <div class="section">
@ -221,7 +248,7 @@
{#each componentsUsingState as component, i} {#each componentsUsingState as component, i}
{#if i > 0}{", "}{/if} {#if i > 0}{", "}{/if}
<button <button
class="component-link" class="component-link updates-colour"
on:click={() => onClickComponentLink(component)} on:click={() => onClickComponentLink(component)}
> >
{component.name} {component.name}
@ -235,7 +262,7 @@
{#each componentsUpdatingState as component, i} {#each componentsUpdatingState as component, i}
{#if i > 0}{", "}{/if} {#if i > 0}{", "}{/if}
<button <button
class="component-link" class="component-link controlled-by-colour"
on:click={() => onClickComponentLink(component)} on:click={() => onClickComponentLink(component)}
> >
{component.name} {component.name}
@ -243,6 +270,16 @@
{/each} {/each}
</div> </div>
{/if} {/if}
<div style="opacity: 0.5; ">
<Link
href="https://docs.budibase.com/docs/app-state"
target="_blank"
size={"S"}
secondary
>
Learn more about State within Budibase.
</Link>
</div>
</div> </div>
<style> <style>
@ -258,7 +295,7 @@
} }
.text { .text {
color: var(--spectrum-global-color-gray-700); color: var(--spectrum-global-color-gray-700);
font-size: var(--spectrum-global-dimension-font-size-75); font-size: var(--spectrum-global-dimension-font-size-50);
} }
.error { .error {
color: var( color: var(
@ -268,6 +305,13 @@
font-size: var(--spectrum-global-dimension-font-size-75); font-size: var(--spectrum-global-dimension-font-size-75);
margin-top: var(--spectrum-global-dimension-size-75); margin-top: var(--spectrum-global-dimension-size-75);
} }
.updates-colour {
color: #8488fd;
}
.controlled-by-colour {
color: #e87400;
}
.component-link { .component-link {
display: inline; display: inline;
border: none; border: none;
@ -275,7 +319,7 @@
text-decoration: underline; text-decoration: underline;
color: var(--spectrum-global-color-white); color: var(--spectrum-global-color-white);
cursor: pointer; cursor: pointer;
font-size: var(--spectrum-global-dimension-font-size-75); font-size: var(--spectrum-global-dimension-font-size-50);
padding: 0; padding: 0;
} }
.component-link:hover { .component-link:hover {