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 {
background: var(--spectrum-global-color-gray-300);
border-color: var(--spectrum-global-color-static-red-600);
margin-top: -3.5px;
margin-bottom: -3.5px;
padding-bottom: 3.5px;
padding-top: 3.5px;
margin-top: -4px;
margin-bottom: -4px;
padding-bottom: 4px;
padding-top: 4px;
}
.property-control.property-focus :global(input) {

View File

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

View File

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

View File

@ -72,11 +72,7 @@
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;
margin: -4px calc(-1 * var(--spacing-xl));
padding: 4px var(--spacing-xl) 4px calc(var(--spacing-xl) - 4px);
}
</style>

View File

@ -105,11 +105,7 @@
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;
margin: -4px calc(-1 * var(--spacing-xl));
padding: 4px var(--spacing-xl) 4px calc(var(--spacing-xl) - 4px);
}
</style>

View File

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

View File

@ -1,7 +1,12 @@
<script lang="ts">
import { onMount } from "svelte"
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 {
componentStore,
@ -16,91 +21,191 @@
processStringSync,
} from "@budibase/string-templates"
import DrawerBindableInput from "@/components/common/bindings/DrawerBindableInput.svelte"
import { type ComponentSetting } from "@/stores/builder/components"
interface ComponentUsingState {
id: 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)
$: bindings = getBindableProperties(
$selectedScreen,
$componentStore.selectedComponentId
)
let selectedKey: string | undefined = undefined
let componentsUsingState: ComponentUsingState[] = []
let componentsUpdatingState: ComponentUsingState[] = []
let editorValue: string = ""
let previousScreenId: string | undefined = undefined
// Auto-select first valid state key
$: {
const screenChanged =
$selectedScreen && $selectedScreen._id !== previousScreenId
const previewContext = $previewStore.selectedComponentContext || {}
if (screenChanged) {
if (keyOptions.length && !keyOptions.includes(selectedKey)) {
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 = []
componentsUpdatingState = []
editorValue = ""
previousScreenId = $selectedScreen._id
}
}
if (keyOptions.length > 0 && !keyOptions.includes(selectedKey)) {
selectedKey = keyOptions[0]
}
const searchComponents = (screen: Screen, stateKey: string) => {
const { props, onLoad, _id } = screen
componentsUsingState = findComponentsUsingState(props, stateKey)
componentsUpdatingState = findComponentsUpdatingState(props, stateKey)
if (selectedKey) {
searchComponents(selectedKey)
editorValue = previewContext.state?.[selectedKey] ?? ""
// Check screen load actions which are outside the component hierarchy
if (eventUpdatesState(onLoad, stateKey)) {
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 = (
component: Component,
stateKey: string
stateKey: string,
foundComponents: 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 = [
"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],
})
}
// Check all settings of this component
settings.forEach(setting => {
if (settingUpdatesState(component, setting, stateKey)) {
const label = setting.label || setting.key
foundComponents.push({
id: _id!,
name: `${_instanceName} - ${label}`,
setting: setting.key,
})
}
})
if (component._children) {
for (let child of component._children) {
foundComponents = [
...foundComponents,
...findComponentsUpdatingState(child, stateKey),
]
}
// Check if conditions update these settings to update this state key
if (_conditions?.some(c => conditionUpdatesState(c, settings, stateKey))) {
foundComponents.push({
id: _id!,
name: `${_instanceName} - Conditions`,
setting: "_conditions",
})
}
// Check children
_children?.forEach(child => {
findComponentsUpdatingState(child, stateKey, 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 bindings = findHBSBlocks(value).map(binding => {
const sanitizedBinding = binding.replace(/\\"/g, '"')
@ -111,125 +216,15 @@
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)
})
builderStore.highlightSetting(component.setting)
}
const handleStateInspectorChange = (e: CustomEvent) => {
if (!selectedKey || !$previewStore.selectedComponentContext) {
return
}
const stateUpdate = {
[selectedKey]: processStringSync(
e.detail,
@ -247,11 +242,10 @@
<div class="state-panel">
<Select
label="State variables"
label="State variable"
bind:value={selectedKey}
placeholder={keyOptions.length > 0 ? false : "No state variables found"}
options={keyOptions}
on:change={handleStateKeySelect}
/>
{#if selectedKey && keyOptions.length > 0}
<DrawerBindableInput
@ -312,7 +306,6 @@
color: var(--spectrum-global-color-gray-700);
font-size: 12px;
}
.updates-colour {
color: var(--bb-indigo-light);
}
@ -332,7 +325,6 @@
.component-link:hover {
text-decoration: underline;
}
.updates-section {
display: flex;
flex-direction: column;

View File

@ -34,6 +34,7 @@ import { BudiStore } from "../BudiStore"
import { Utils } from "@budibase/frontend-core"
import {
Component as ComponentType,
ComponentCondition,
FieldType,
Screen,
Table,
@ -69,6 +70,7 @@ export interface ComponentDefinition {
export interface ComponentSetting {
key: string
type: string
label?: string
section?: string
name?: string
defaultValue?: any
@ -744,10 +746,6 @@ export class ComponentStore extends BudiStore<ComponentState> {
}
}
/**
*
* @param {string} componentId
*/
select(id: string) {
this.update(state => {
// 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) => {
component._conditions = conditions
})

View File

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

View File

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

View File

@ -1,9 +1,22 @@
import { Document } from "../document"
import { BasicOperator } from "../../sdk"
export interface Component extends Document {
_instanceName: string
_styles: { [key: string]: any }
_component: string
_children?: Component[]
_conditions?: ComponentCondition[]
[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
persist: any | null
}
eventHandlerType: string
"##eventHandlerType": string
id: string
}