Merge pull request #15464 from Budibase/fix/missing-state-usage

Fixes to address state usage misses
This commit is contained in:
Andrew Kingston 2025-02-03 13:02:35 +00:00 committed by GitHub
commit 0c496ade8a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 199 additions and 199 deletions

View File

@ -143,10 +143,10 @@
.property-control.highlighted { .property-control.highlighted {
background: var(--spectrum-global-color-gray-300); background: var(--spectrum-global-color-gray-300);
border-color: var(--spectrum-global-color-static-red-600); border-color: var(--spectrum-global-color-static-red-600);
margin-top: -3.5px; margin-top: -4px;
margin-bottom: -3.5px; margin-bottom: -4px;
padding-bottom: 3.5px; padding-bottom: 4px;
padding-top: 3.5px; padding-top: 4px;
} }
.property-control.property-focus :global(input) { .property-control.property-focus :global(input) {

View File

@ -63,7 +63,7 @@
section = "conditions" section = "conditions"
} else if (highlightedSetting.key === "_styles") { } else if (highlightedSetting.key === "_styles") {
section = "styles" section = "styles"
} else if (highlightedSetting.key === "_settings") { } else {
section = "settings" section = "settings"
} }
} }
@ -110,7 +110,7 @@
{/each} {/each}
</div> </div>
</span> </span>
{#if section == "settings"} {#if section === "settings"}
<TourWrap <TourWrap
stepKeys={[ stepKeys={[
BUILDER_FORM_CREATE_STEPS, BUILDER_FORM_CREATE_STEPS,
@ -127,7 +127,7 @@
/> />
</TourWrap> </TourWrap>
{/if} {/if}
{#if section == "styles"} {#if section === "styles"}
<DesignSection <DesignSection
{componentInstance} {componentInstance}
{componentBindings} {componentBindings}
@ -142,7 +142,7 @@
componentTitle={title} componentTitle={title}
/> />
{/if} {/if}
{#if section == "conditions"} {#if section === "conditions"}
<ConditionalUISection <ConditionalUISection
{componentInstance} {componentInstance}
{componentDefinition} {componentDefinition}

View File

@ -190,7 +190,7 @@
<Icon name="DragHandle" size="XL" /> <Icon name="DragHandle" size="XL" />
</div> </div>
<Select <Select
placeholder={null} placeholder={false}
options={actionOptions} options={actionOptions}
bind:value={condition.action} bind:value={condition.action}
/> />
@ -227,7 +227,7 @@
on:change={e => (condition.newValue = e.detail)} on:change={e => (condition.newValue = e.detail)}
/> />
<Select <Select
placeholder={null} placeholder={false}
options={getOperatorOptions(condition)} options={getOperatorOptions(condition)}
bind:value={condition.operator} bind:value={condition.operator}
on:change={e => onOperatorChange(condition, e.detail)} on:change={e => onOperatorChange(condition, e.detail)}
@ -236,7 +236,7 @@
disabled={condition.noValue || condition.operator === "oneOf"} disabled={condition.noValue || condition.operator === "oneOf"}
options={valueTypeOptions} options={valueTypeOptions}
bind:value={condition.valueType} bind:value={condition.valueType}
placeholder={null} placeholder={false}
on:change={e => onValueTypeChange(condition, e.detail)} on:change={e => onValueTypeChange(condition, e.detail)}
/> />
{#if ["string", "number"].includes(condition.valueType)} {#if ["string", "number"].includes(condition.valueType)}

View File

@ -72,11 +72,7 @@
background: var(--spectrum-global-color-gray-300); background: var(--spectrum-global-color-gray-300);
border-left: 4px solid var(--spectrum-semantic-informative-color-background); border-left: 4px solid var(--spectrum-semantic-informative-color-background);
transition: background 130ms ease-out, border-color 130ms ease-out; transition: background 130ms ease-out, border-color 130ms ease-out;
margin-top: -3.5px; margin: -4px calc(-1 * var(--spacing-xl));
margin-bottom: -3.5px; padding: 4px var(--spacing-xl) 4px calc(var(--spacing-xl) - 4px);
padding-bottom: 3.5px;
padding-top: 3.5px;
padding-left: 5px;
padding-right: 5px;
} }
</style> </style>

View File

@ -105,11 +105,7 @@
background: var(--spectrum-global-color-gray-300); background: var(--spectrum-global-color-gray-300);
border-left: 4px solid var(--spectrum-semantic-informative-color-background); border-left: 4px solid var(--spectrum-semantic-informative-color-background);
transition: background 130ms ease-out, border-color 130ms ease-out; transition: background 130ms ease-out, border-color 130ms ease-out;
margin-top: -3.5px; margin: -4px calc(-1 * var(--spacing-xl));
margin-bottom: -3.5px; padding: 4px var(--spacing-xl) 4px calc(var(--spacing-xl) - 4px);
padding-bottom: 3.5px;
padding-top: 3.5px;
padding-left: 5px;
padding-right: 5px;
} }
</style> </style>

View File

@ -33,7 +33,7 @@
{/each} {/each}
</div> </div>
</div> </div>
<Layout gap="XS" paddingX="L" paddingY="XL"> <Layout gap="XS" paddingX="XL" paddingY="XL">
{#if activeTab === "theme"} {#if activeTab === "theme"}
<ThemePanel /> <ThemePanel />
{:else} {:else}

View File

@ -1,7 +1,12 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte" import { onMount } from "svelte"
import { Select } from "@budibase/bbui" import { Select } from "@budibase/bbui"
import type { Component } from "@budibase/types" import type {
Component,
ComponentCondition,
EventHandler,
Screen,
} from "@budibase/types"
import { getAllStateVariables, getBindableProperties } from "@/dataBinding" import { getAllStateVariables, getBindableProperties } from "@/dataBinding"
import { import {
componentStore, componentStore,
@ -16,91 +21,191 @@
processStringSync, processStringSync,
} from "@budibase/string-templates" } from "@budibase/string-templates"
import DrawerBindableInput from "@/components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "@/components/common/bindings/DrawerBindableInput.svelte"
import { type ComponentSetting } from "@/stores/builder/components"
interface ComponentUsingState { interface ComponentUsingState {
id: string id: string
name: string name: string
settings: string[] setting: string
} }
let selectedKey: string | undefined = undefined
let componentsUsingState: ComponentUsingState[] = []
let componentsUpdatingState: ComponentUsingState[] = []
let editorValue: string = ""
$: selectStateKey($selectedScreen, selectedKey)
$: keyOptions = getAllStateVariables($selectedScreen) $: keyOptions = getAllStateVariables($selectedScreen)
$: bindings = getBindableProperties( $: bindings = getBindableProperties(
$selectedScreen, $selectedScreen,
$componentStore.selectedComponentId $componentStore.selectedComponentId
) )
let selectedKey: string | undefined = undefined // Auto-select first valid state key
let componentsUsingState: ComponentUsingState[] = []
let componentsUpdatingState: ComponentUsingState[] = []
let editorValue: string = ""
let previousScreenId: string | undefined = undefined
$: { $: {
const screenChanged = if (keyOptions.length && !keyOptions.includes(selectedKey)) {
$selectedScreen && $selectedScreen._id !== previousScreenId
const previewContext = $previewStore.selectedComponentContext || {}
if (screenChanged) {
selectedKey = keyOptions[0] selectedKey = keyOptions[0]
} else if (!keyOptions.length) {
selectedKey = undefined
}
}
const selectStateKey = (
screen: Screen | undefined,
key: string | undefined
) => {
if (screen && key) {
searchComponents(screen, key)
editorValue = $previewStore.selectedComponentContext?.state?.[key] ?? ""
} else {
editorValue = ""
componentsUsingState = [] componentsUsingState = []
componentsUpdatingState = [] componentsUpdatingState = []
editorValue = ""
previousScreenId = $selectedScreen._id
} }
}
if (keyOptions.length > 0 && !keyOptions.includes(selectedKey)) { const searchComponents = (screen: Screen, stateKey: string) => {
selectedKey = keyOptions[0] const { props, onLoad, _id } = screen
} componentsUsingState = findComponentsUsingState(props, stateKey)
componentsUpdatingState = findComponentsUpdatingState(props, stateKey)
if (selectedKey) { // Check screen load actions which are outside the component hierarchy
searchComponents(selectedKey) if (eventUpdatesState(onLoad, stateKey)) {
editorValue = previewContext.state?.[selectedKey] ?? "" componentsUpdatingState.push({
id: _id!,
name: "Screen - On load",
setting: "onLoad",
})
} }
} }
// Checks if an event setting updates a certain state key
const eventUpdatesState = (
handlers: EventHandler[] | undefined,
stateKey: string
) => {
return handlers?.some(handler => {
return (
handler["##eventHandlerType"] === "Update State" &&
handler.parameters?.key === stateKey
)
})
}
// Checks if a setting for the given component updates a certain state key
const settingUpdatesState = (
component: Record<string, any>,
setting: ComponentSetting,
stateKey: string
) => {
if (setting.type === "event") {
return eventUpdatesState(component[setting.key], stateKey)
} else if (setting.type === "buttonConfiguration") {
const buttons = component[setting.key]
if (Array.isArray(buttons)) {
for (let button of buttons) {
if (eventUpdatesState(button.onClick, stateKey)) {
return true
}
}
}
}
return false
}
// Checks if a condition updates a certain state key
const conditionUpdatesState = (
condition: ComponentCondition,
settings: ComponentSetting[],
stateKey: string
) => {
const setting = settings.find(s => s.key === condition.setting)
if (!setting) {
return false
}
const component = { [setting.key]: condition.settingValue }
return settingUpdatesState(component, setting, stateKey)
}
const findComponentsUpdatingState = ( const findComponentsUpdatingState = (
component: Component, component: Component,
stateKey: string stateKey: string,
foundComponents: ComponentUsingState[] = []
): ComponentUsingState[] => { ): ComponentUsingState[] => {
let foundComponents: ComponentUsingState[] = [] const { _children, _conditions, _component, _instanceName, _id } = component
const settings = componentStore
.getComponentSettings(_component)
.filter(s => s.type === "event" || s.type === "buttonConfiguration")
const eventHandlerProps = [ // Check all settings of this component
"onClick", settings.forEach(setting => {
"onChange", if (settingUpdatesState(component, setting, stateKey)) {
"onRowClick", const label = setting.label || setting.key
"onChange", foundComponents.push({
"buttonOnClick", id: _id!,
] name: `${_instanceName} - ${label}`,
setting: setting.key,
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) { // Check if conditions update these settings to update this state key
for (let child of component._children) { if (_conditions?.some(c => conditionUpdatesState(c, settings, stateKey))) {
foundComponents = [ foundComponents.push({
...foundComponents, id: _id!,
...findComponentsUpdatingState(child, stateKey), name: `${_instanceName} - Conditions`,
] setting: "_conditions",
} })
} }
// Check children
_children?.forEach(child => {
findComponentsUpdatingState(child, stateKey, foundComponents)
})
return foundComponents return foundComponents
} }
const findComponentsUsingState = (
component: Component,
stateKey: string,
componentsUsingState: ComponentUsingState[] = []
): ComponentUsingState[] => {
const settings = componentStore.getComponentSettings(component._component)
// Check all settings of this component
const settingsWithState = getSettingsUsingState(component, stateKey)
settingsWithState.forEach(setting => {
// Get readable label for this setting
let label = settings.find(s => s.key === setting)?.label || setting
if (setting === "_conditions") {
label = "Conditions"
} else if (setting === "_styles") {
label = "Styles"
}
componentsUsingState.push({
id: component._id!,
name: `${component._instanceName} - ${label}`,
setting,
})
})
// Check children
component._children?.forEach(child => {
findComponentsUsingState(child, stateKey, componentsUsingState)
})
return componentsUsingState
}
const getSettingsUsingState = (
component: Component,
stateKey: string
): string[] => {
return Object.entries(component)
.filter(([key]) => key !== "_children")
.filter(([_, value]) => hasStateBinding(JSON.stringify(value), stateKey))
.map(([key]) => key)
}
const hasStateBinding = (value: string, stateKey: string): boolean => { const hasStateBinding = (value: string, stateKey: string): boolean => {
const bindings = findHBSBlocks(value).map(binding => { const bindings = findHBSBlocks(value).map(binding => {
const sanitizedBinding = binding.replace(/\\"/g, '"') const sanitizedBinding = binding.replace(/\\"/g, '"')
@ -111,125 +216,15 @@
return bindings.join(" ").includes(stateKey) 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) => { const onClickComponentLink = (component: ComponentUsingState) => {
componentStore.select(component.id) componentStore.select(component.id)
component.settings.forEach(setting => { builderStore.highlightSetting(component.setting)
builderStore.highlightSetting(setting)
})
} }
const handleStateInspectorChange = (e: CustomEvent) => { const handleStateInspectorChange = (e: CustomEvent) => {
if (!selectedKey || !$previewStore.selectedComponentContext) { if (!selectedKey || !$previewStore.selectedComponentContext) {
return return
} }
const stateUpdate = { const stateUpdate = {
[selectedKey]: processStringSync( [selectedKey]: processStringSync(
e.detail, e.detail,
@ -247,11 +242,10 @@
<div class="state-panel"> <div class="state-panel">
<Select <Select
label="State variables" label="State variable"
bind:value={selectedKey} bind:value={selectedKey}
placeholder={keyOptions.length > 0 ? false : "No state variables found"} placeholder={keyOptions.length > 0 ? false : "No state variables found"}
options={keyOptions} options={keyOptions}
on:change={handleStateKeySelect}
/> />
{#if selectedKey && keyOptions.length > 0} {#if selectedKey && keyOptions.length > 0}
<DrawerBindableInput <DrawerBindableInput
@ -312,7 +306,6 @@
color: var(--spectrum-global-color-gray-700); color: var(--spectrum-global-color-gray-700);
font-size: 12px; font-size: 12px;
} }
.updates-colour { .updates-colour {
color: var(--bb-indigo-light); color: var(--bb-indigo-light);
} }
@ -332,7 +325,6 @@
.component-link:hover { .component-link:hover {
text-decoration: underline; text-decoration: underline;
} }
.updates-section { .updates-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -34,6 +34,7 @@ import { BudiStore } from "../BudiStore"
import { Utils } from "@budibase/frontend-core" import { Utils } from "@budibase/frontend-core"
import { import {
Component as ComponentType, Component as ComponentType,
ComponentCondition,
FieldType, FieldType,
Screen, Screen,
Table, Table,
@ -69,6 +70,7 @@ export interface ComponentDefinition {
export interface ComponentSetting { export interface ComponentSetting {
key: string key: string
type: string type: string
label?: string
section?: string section?: string
name?: string name?: string
defaultValue?: any defaultValue?: any
@ -744,10 +746,6 @@ export class ComponentStore extends BudiStore<ComponentState> {
} }
} }
/**
*
* @param {string} componentId
*/
select(id: string) { select(id: string) {
this.update(state => { this.update(state => {
// Only clear highlights if selecting a different component // Only clear highlights if selecting a different component
@ -1139,7 +1137,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
}) })
} }
async updateConditions(conditions: Record<string, any>) { async updateConditions(conditions: ComponentCondition[]) {
await this.patch((component: Component) => { await this.patch((component: Component) => {
component._conditions = conditions component._conditions = conditions
}) })

View File

@ -16,7 +16,11 @@ import { userStore, userSelectedResourceMap, isOnlyUser } from "./users.js"
import { deploymentStore } from "./deployments.js" import { deploymentStore } from "./deployments.js"
import { contextMenuStore } from "./contextMenu.js" import { contextMenuStore } from "./contextMenu.js"
import { snippets } from "./snippets" import { snippets } from "./snippets"
import { screenComponents, screenComponentErrors } from "./screenComponent" import {
screenComponents,
screenComponentErrors,
findComponentsBySettingsType,
} from "./screenComponent"
// Backend // Backend
import { tables } from "./tables" import { tables } from "./tables"
@ -70,6 +74,7 @@ export {
appPublished, appPublished,
screenComponents, screenComponents,
screenComponentErrors, screenComponentErrors,
findComponentsBySettingsType,
} }
export const reset = () => { export const reset = () => {

View File

@ -124,7 +124,7 @@ export const screenComponentErrors = derived(
} }
) )
function findComponentsBySettingsType( export function findComponentsBySettingsType(
screen: Screen, screen: Screen,
type: string | string[], type: string | string[],
definitions: Record<string, ComponentDefinition> definitions: Record<string, ComponentDefinition>

View File

@ -1,9 +1,22 @@
import { Document } from "../document" import { Document } from "../document"
import { BasicOperator } from "../../sdk"
export interface Component extends Document { export interface Component extends Document {
_instanceName: string _instanceName: string
_styles: { [key: string]: any } _styles: { [key: string]: any }
_component: string _component: string
_children?: Component[] _children?: Component[]
_conditions?: ComponentCondition[]
[key: string]: any [key: string]: any
} }
export interface ComponentCondition {
id: string
operator: BasicOperator
action: "update" | "show" | "hide"
valueType: "string" | "number" | "datetime" | "boolean"
newValue?: any
referenceValue?: any
setting?: string
settingValue?: any
}

View File

@ -45,6 +45,6 @@ export interface EventHandler {
value: string value: string
persist: any | null persist: any | null
} }
eventHandlerType: string "##eventHandlerType": string
id: string id: string
} }