Merge pull request #15464 from Budibase/fix/missing-state-usage
Fixes to address state usage misses
This commit is contained in:
commit
0c496ade8a
|
@ -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) {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -124,7 +124,7 @@ export const screenComponentErrors = derived(
|
|||
}
|
||||
)
|
||||
|
||||
function findComponentsBySettingsType(
|
||||
export function findComponentsBySettingsType(
|
||||
screen: Screen,
|
||||
type: string | string[],
|
||||
definitions: Record<string, ComponentDefinition>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -45,6 +45,6 @@ export interface EventHandler {
|
|||
value: string
|
||||
persist: any | null
|
||||
}
|
||||
eventHandlerType: string
|
||||
"##eventHandlerType": string
|
||||
id: string
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue