diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte index ac0b6adcd0..522ab9adc3 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte @@ -4,6 +4,7 @@ import type { Component, ComponentCondition, + ComponentSetting, EventHandler, Screen, } from "@budibase/types" @@ -21,7 +22,6 @@ processStringSync, } from "@budibase/string-templates" import DrawerBindableInput from "@/components/common/bindings/DrawerBindableInput.svelte" - import { type ComponentSetting } from "@/stores/builder/components" interface ComponentUsingState { id: string diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index 5b8b455e5a..a8f52e2660 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -33,6 +33,8 @@ import { import { BudiStore } from "../BudiStore" import { Utils } from "@budibase/frontend-core" import { + ComponentDefinition, + ComponentSetting, Component as ComponentType, ComponentCondition, FieldType, @@ -55,30 +57,6 @@ export interface ComponentState { selectedScreenId?: string | null } -export interface ComponentDefinition { - component: string - name: string - friendlyName?: string - hasChildren?: boolean - settings?: ComponentSetting[] - features?: Record - typeSupportPresets?: Record - legalDirectChildren: string[] - illegalChildren: string[] -} - -export interface ComponentSetting { - key: string - type: string - label?: string - section?: string - name?: string - defaultValue?: any - selectAllFields?: boolean - resetOn?: string | string[] - settings?: ComponentSetting[] -} - export const INITIAL_COMPONENTS_STATE: ComponentState = { components: {}, customComponents: [], diff --git a/packages/builder/src/stores/builder/screenComponent.ts b/packages/builder/src/stores/builder/screenComponent.ts index 446d0f31d6..62bc97cc27 100644 --- a/packages/builder/src/stores/builder/screenComponent.ts +++ b/packages/builder/src/stores/builder/screenComponent.ts @@ -6,14 +6,17 @@ import { UIDatasourceType, Screen, Component, + UIComponentError, ScreenProps, + ComponentDefinition, } from "@budibase/types" import { queries } from "./queries" import { views } from "./views" -import { bindings, featureFlag } from "@/helpers" -import { getBindableProperties } from "@/dataBinding" -import { componentStore, ComponentDefinition } from "./components" import { findAllComponents } from "@/helpers/components" +import { bindings } from "@/helpers" +import { getBindableProperties } from "@/dataBinding" +import { componentStore } from "./components" +import { getSettingsDefinition } from "@budibase/frontend-core" function reduceBy( key: TKey, @@ -52,61 +55,10 @@ export const screenComponentErrors = derived( $viewsV2, $queries, $componentStore, - ]): Record => { - if (!featureFlag.isEnabled("CHECK_COMPONENT_SETTINGS_ERRORS")) { + ]): Record => { + if (!$selectedScreen) { return {} } - function getInvalidDatasources( - screen: Screen, - datasources: Record - ) { - const result: Record = {} - - for (const { component, setting } of findComponentsBySettingsType( - screen, - ["table", "dataSource"], - $componentStore.components - )) { - const componentSettings = component[setting.key] - if (!componentSettings) { - continue - } - const { label } = componentSettings - const type = componentSettings.type as UIDatasourceType - - const validationKey = validationKeyByType[type] - if (!validationKey) { - continue - } - - const componentBindings = getBindableProperties( - $selectedScreen, - component._id - ) - - const componentDatasources = { - ...reduceBy( - "rowId", - bindings.extractRelationships(componentBindings) - ), - ...reduceBy("value", bindings.extractFields(componentBindings)), - ...reduceBy( - "value", - bindings.extractJSONArrayFields(componentBindings) - ), - } - - const resourceId = componentSettings[validationKey] - if (!{ ...datasources, ...componentDatasources }[resourceId]) { - const friendlyTypeName = friendlyNameByType[type] ?? type - result[component._id!] = [ - `The ${friendlyTypeName} named "${label}" could not be found`, - ] - } - } - - return result - } const datasources = { ...reduceBy("_id", $tables.list), @@ -115,15 +67,169 @@ export const screenComponentErrors = derived( ...reduceBy("_id", $queries.list), } - if (!$selectedScreen) { - // Skip validation if a screen is not selected. - return {} - } + const { components: definitions } = $componentStore - return getInvalidDatasources($selectedScreen, datasources) + const errors = { + ...getInvalidDatasources($selectedScreen, datasources, definitions), + ...getMissingAncestors($selectedScreen, definitions), + ...getMissingRequiredSettings($selectedScreen, definitions), + } + return errors } ) +function getInvalidDatasources( + screen: Screen, + datasources: Record, + definitions: Record +) { + const result: Record = {} + for (const { component, setting } of findComponentsBySettingsType( + screen, + ["table", "dataSource"], + definitions + )) { + const componentSettings = component[setting.key] + if (!componentSettings) { + continue + } + + const { label } = componentSettings + const type = componentSettings.type as UIDatasourceType + + const validationKey = validationKeyByType[type] + if (!validationKey) { + continue + } + + const componentBindings = getBindableProperties(screen, component._id) + + const componentDatasources = { + ...reduceBy("rowId", bindings.extractRelationships(componentBindings)), + ...reduceBy("value", bindings.extractFields(componentBindings)), + ...reduceBy("value", bindings.extractJSONArrayFields(componentBindings)), + } + + const resourceId = componentSettings[validationKey] + if (!{ ...datasources, ...componentDatasources }[resourceId]) { + const friendlyTypeName = friendlyNameByType[type] ?? type + result[component._id!] = [ + { + key: setting.key, + message: `The ${friendlyTypeName} named "${label}" could not be found`, + errorType: "setting", + }, + ] + } + } + + return result +} + +function getMissingRequiredSettings( + screen: Screen, + definitions: Record +) { + const allComponents = findAllComponents(screen.props) as Component[] + + const result: Record = {} + for (const component of allComponents) { + const definition = definitions[component._component] + + const settings = getSettingsDefinition(definition) + + const missingRequiredSettings = settings.filter((setting: any) => { + let empty = + component[setting.key] == null || component[setting.key] === "" + let missing = setting.required && empty + + // Check if this setting depends on another, as it may not be required + if (setting.dependsOn) { + const dependsOnKey = setting.dependsOn.setting || setting.dependsOn + const dependsOnValue = setting.dependsOn.value + const realDependentValue = component[dependsOnKey] + + const sectionDependsOnKey = + setting.sectionDependsOn?.setting || setting.sectionDependsOn + const sectionDependsOnValue = setting.sectionDependsOn?.value + const sectionRealDependentValue = component[sectionDependsOnKey] + + if (dependsOnValue == null && realDependentValue == null) { + return false + } + if (dependsOnValue != null && dependsOnValue !== realDependentValue) { + return false + } + + if ( + sectionDependsOnValue != null && + sectionDependsOnValue !== sectionRealDependentValue + ) { + return false + } + } + + return missing + }) + + if (missingRequiredSettings?.length) { + result[component._id!] = missingRequiredSettings.map((s: any) => ({ + key: s.key, + message: `Add the ${s.label} setting to start using your component`, + errorType: "setting", + })) + } + } + + return result +} + +const BudibasePrefix = "@budibase/standard-components/" +function getMissingAncestors( + screen: Screen, + definitions: Record +) { + const result: Record = {} + + function checkMissingAncestors(component: Component, ancestors: string[]) { + for (const child of component._children || []) { + checkMissingAncestors(child, [...ancestors, component._component]) + } + + const definition = definitions[component._component] + + if (!definition?.requiredAncestors?.length) { + return + } + + const missingAncestors = definition.requiredAncestors.filter( + ancestor => !ancestors.includes(`${BudibasePrefix}${ancestor}`) + ) + + if (missingAncestors.length) { + const pluralise = (name: string) => { + return name.endsWith("s") ? `${name}'` : `${name}s` + } + + result[component._id!] = missingAncestors.map(ancestor => { + const ancestorDefinition = definitions[`${BudibasePrefix}${ancestor}`] + return { + message: `${pluralise(definition.name)} need to be inside a +${ancestorDefinition.name}`, + errorType: "ancestor-setting", + ancestor: { + name: ancestorDefinition.name, + fullType: `${BudibasePrefix}${ancestor}`, + }, + } + }) + } + } + + checkMissingAncestors(screen.props, []) + return result +} + export function findComponentsBySettingsType( screen: Screen, type: string | string[], @@ -149,10 +255,10 @@ export function findComponentsBySettingsType( const setting = definition?.settings?.find((s: any) => typesArray.includes(s.type) ) - if (setting && "type" in setting) { + if (setting) { result.push({ component, - setting: { type: setting.type!, key: setting.key! }, + setting: { type: setting.type, key: setting.key }, }) } component._children?.forEach(child => { diff --git a/packages/builder/src/stores/builder/screens.ts b/packages/builder/src/stores/builder/screens.ts index 64fe31752d..3f098334f0 100644 --- a/packages/builder/src/stores/builder/screens.ts +++ b/packages/builder/src/stores/builder/screens.ts @@ -19,8 +19,8 @@ import { Screen, Component, SaveScreenResponse, + ComponentDefinition, } from "@budibase/types" -import { ComponentDefinition } from "./components" interface ScreenState { screens: Screen[] diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 236bcb7c7e..3e22ffada1 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -11,11 +11,8 @@ @@ -23,12 +17,10 @@ {#if $component.errorState}
- {#if requiredAncestor} - - {:else if errorMessage} - {errorMessage} - {:else if requiredSetting} - + {#if errorMessage} + + {@html errorMessage.message} + {/if}
{/if} diff --git a/packages/client/src/components/error-states/ComponentErrorStateCTA.svelte b/packages/client/src/components/error-states/ComponentErrorStateCTA.svelte new file mode 100644 index 0000000000..ef22f307f7 --- /dev/null +++ b/packages/client/src/components/error-states/ComponentErrorStateCTA.svelte @@ -0,0 +1,40 @@ + + +{#if error} + {#if error.errorType === "setting"} + - + + + { + builderStore.actions.highlightSetting(error.key) + }} + > + Show me + + {:else if error.errorType === "ancestor-setting"} + - + + + { + builderStore.actions.addParentComponent( + $component.id, + error.ancestor.fullType + ) + }} + > + Add {error.ancestor.name} + + {/if} +{/if} diff --git a/packages/client/src/components/error-states/MissingRequiredAncestor.svelte b/packages/client/src/components/error-states/MissingRequiredAncestor.svelte deleted file mode 100644 index c251b1be4e..0000000000 --- a/packages/client/src/components/error-states/MissingRequiredAncestor.svelte +++ /dev/null @@ -1,43 +0,0 @@ - - - - {pluralName} need to be inside a - {ancestorName} - -- - - - { - builderStore.actions.addParentComponent($component.id, fullAncestorType) - }} -> - Add {ancestorName} - diff --git a/packages/client/src/components/error-states/MissingRequiredSetting.svelte b/packages/client/src/components/error-states/MissingRequiredSetting.svelte deleted file mode 100644 index 37e99a3cd1..0000000000 --- a/packages/client/src/components/error-states/MissingRequiredSetting.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - Add the {requiredSetting.label} setting to start using your component - -- - - - { - builderStore.actions.highlightSetting(requiredSetting.key) - }} -> - Show me - diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 2a435c2f8c..08330a60fa 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -11,7 +11,15 @@ export interface SDK { generateGoldenSample: any builderStore: Readable<{ inBuilder: boolean - }> + }> & { + actions: { + highlightSetting: (key: string) => void + addParentComponent: ( + componentId: string, + fullAncestorType: string + ) => void + } + } } export type Component = Readable<{ diff --git a/packages/client/src/utils/componentProps.js b/packages/client/src/utils/componentProps.js index bdf74c8014..eec284e3d8 100644 --- a/packages/client/src/utils/componentProps.js +++ b/packages/client/src/utils/componentProps.js @@ -97,26 +97,3 @@ export const propsUseBinding = (props, bindingKey) => { } return false } - -/** - * Gets the definition of this component's settings from the manifest - */ -export const getSettingsDefinition = definition => { - if (!definition) { - return [] - } - let settings = [] - definition.settings?.forEach(setting => { - if (setting.section) { - settings = settings.concat( - (setting.settings || [])?.map(childSetting => ({ - ...childSetting, - sectionDependsOn: setting.dependsOn, - })) - ) - } else { - settings.push(setting) - } - }) - return settings -} diff --git a/packages/frontend-core/src/utils/components.ts b/packages/frontend-core/src/utils/components.ts new file mode 100644 index 0000000000..60eb520258 --- /dev/null +++ b/packages/frontend-core/src/utils/components.ts @@ -0,0 +1,26 @@ +import { ComponentDefinition, ComponentSetting } from "@budibase/types" + +/** + * Gets the definition of this component's settings from the manifest + */ +export const getSettingsDefinition = ( + definition: ComponentDefinition +): ComponentSetting[] => { + if (!definition) { + return [] + } + let settings: ComponentSetting[] = [] + definition.settings?.forEach(setting => { + if (setting.section) { + settings = settings.concat( + (setting.settings || [])?.map(childSetting => ({ + ...childSetting, + sectionDependsOn: setting.dependsOn, + })) + ) + } else { + settings.push(setting) + } + }) + return settings +} diff --git a/packages/frontend-core/src/utils/index.ts b/packages/frontend-core/src/utils/index.ts index efc694d268..3fa9f66b22 100644 --- a/packages/frontend-core/src/utils/index.ts +++ b/packages/frontend-core/src/utils/index.ts @@ -13,3 +13,4 @@ export * from "./download" export * from "./settings" export * from "./relatedColumns" export * from "./table" +export * from "./components" diff --git a/packages/types/src/sdk/featureFlag.ts b/packages/types/src/sdk/featureFlag.ts index 97893a1b5e..996d3bba8d 100644 --- a/packages/types/src/sdk/featureFlag.ts +++ b/packages/types/src/sdk/featureFlag.ts @@ -1,6 +1,5 @@ export enum FeatureFlag { USE_ZOD_VALIDATOR = "USE_ZOD_VALIDATOR", - CHECK_COMPONENT_SETTINGS_ERRORS = "CHECK_COMPONENT_SETTINGS_ERRORS", // Account-portal DIRECT_LOGIN_TO_ACCOUNT_PORTAL = "DIRECT_LOGIN_TO_ACCOUNT_PORTAL", @@ -8,7 +7,6 @@ export enum FeatureFlag { export const FeatureFlagDefaults = { [FeatureFlag.USE_ZOD_VALIDATOR]: false, - [FeatureFlag.CHECK_COMPONENT_SETTINGS_ERRORS]: false, // Account-portal [FeatureFlag.DIRECT_LOGIN_TO_ACCOUNT_PORTAL]: false, diff --git a/packages/types/src/ui/components/errors.ts b/packages/types/src/ui/components/errors.ts new file mode 100644 index 0000000000..9725ed8a33 --- /dev/null +++ b/packages/types/src/ui/components/errors.ts @@ -0,0 +1,20 @@ +interface BaseUIComponentError { + message: string +} + +interface UISettingComponentError extends BaseUIComponentError { + errorType: "setting" + key: string +} + +interface UIAncestorComponentError extends BaseUIComponentError { + errorType: "ancestor-setting" + ancestor: { + name: string + fullType: string + } +} + +export type UIComponentError = + | UISettingComponentError + | UIAncestorComponentError diff --git a/packages/types/src/ui/components/index.ts b/packages/types/src/ui/components/index.ts index 8dc1638f8c..283ceb47da 100644 --- a/packages/types/src/ui/components/index.ts +++ b/packages/types/src/ui/components/index.ts @@ -1,2 +1,34 @@ export * from "./sidepanel" export * from "./codeEditor" +export * from "./errors" + +export interface ComponentDefinition { + component: string + name: string + friendlyName?: string + hasChildren?: boolean + settings?: ComponentSetting[] + features?: Record + typeSupportPresets?: Record + legalDirectChildren: string[] + requiredAncestors?: string[] + illegalChildren: string[] +} + +export interface ComponentSetting { + key: string + type: string + label?: string + section?: string + name?: string + defaultValue?: any + selectAllFields?: boolean + resetOn?: string | string[] + settings?: ComponentSetting[] + dependsOn?: + | string + | { + setting: string + value: string + } +}