Merge pull request #15414 from Budibase/BUDI-8986/validate-datasource-setting-on-components

Validate datasource setting on components
This commit is contained in:
Adria Navarro 2025-01-23 11:40:29 +01:00 committed by GitHub
commit b3197807c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 138 additions and 5 deletions

View File

@ -41,11 +41,12 @@ module.exports = {
if (
/^@budibase\/[^/]+\/.*$/.test(importPath) &&
importPath !== "@budibase/backend-core/tests" &&
importPath !== "@budibase/string-templates/test/utils"
importPath !== "@budibase/string-templates/test/utils" &&
importPath !== "@budibase/client/manifest.json"
) {
context.report({
node,
message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests and @budibase/string-templates/test/utils.`,
message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests, @budibase/string-templates/test/utils and @budibase/client/manifest.json.`,
})
}
},

View File

@ -0,0 +1,44 @@
import { Component, Screen, ScreenProps } from "@budibase/types"
import clientManifest from "@budibase/client/manifest.json"
export function findComponentsBySettingsType(screen: Screen, type: string) {
const result: {
component: Component
setting: {
type: string
key: string
}
}[] = []
function recurseFieldComponentsInChildren(
component: ScreenProps,
type: string
) {
if (!component) {
return
}
const definition = getManifestDefinition(component)
const setting =
"settings" in definition &&
definition.settings.find((s: any) => s.type === type)
if (setting && "type" in setting) {
result.push({
component,
setting: { type: setting.type!, key: setting.key! },
})
}
component._children?.forEach(child => {
recurseFieldComponentsInChildren(child, type)
})
}
recurseFieldComponentsInChildren(screen?.props, type)
return result
}
function getManifestDefinition(component: Component) {
const componentType = component._component.split("/").slice(-1)[0]
const definition =
clientManifest[componentType as keyof typeof clientManifest]
return definition
}

View File

@ -11,6 +11,7 @@
selectedScreen,
hoverStore,
componentTreeNodesStore,
screenComponentErrors,
snippets,
} from "@/stores/builder"
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
@ -68,6 +69,7 @@
port: window.location.port,
},
snippets: $snippets,
componentErrors: $screenComponentErrors,
}
// Refresh the preview when required

View File

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

View File

@ -0,0 +1,61 @@
import { derived } from "svelte/store"
import { tables } from "./tables"
import { selectedScreen } from "./screens"
import { viewsV2 } from "./viewsV2"
import { findComponentsBySettingsType } from "@/helpers/screen"
import { Screen, Table, ViewV2 } from "@budibase/types"
export const screenComponentErrors = derived(
[selectedScreen, tables, viewsV2],
([$selectedScreen, $tables, $viewsV2]): Record<string, string[]> => {
function flattenTablesAndViews(tables: Table[], views: ViewV2[]) {
return {
...tables.reduce(
(list, table) => ({
...list,
[table._id!]: table,
}),
{}
),
...views.reduce(
(list, view) => ({
...list,
[view.id]: view,
}),
{}
),
}
}
function getInvalidDatasources(
screen: Screen,
datasources: Record<string, any>
) {
const friendlyNameByType = {
table: "table",
view: "view",
viewV2: "view",
}
const result: Record<string, string[]> = {}
for (const { component, setting } of findComponentsBySettingsType(
screen,
"table"
)) {
const { resourceId, type, label } = component[setting.key]
if (!datasources[resourceId]) {
const friendlyTypeName =
friendlyNameByType[type as keyof typeof friendlyNameByType]
result[component._id!] = [
`The ${friendlyTypeName} named "${label}" does not exist`,
]
}
}
return result
}
const datasources = flattenTablesAndViews($tables.list, $viewsV2.list)
return getInvalidDatasources($selectedScreen, datasources)
}
)

View File

@ -103,6 +103,7 @@
let settingsDefinition
let settingsDefinitionMap
let missingRequiredSettings = false
let componentErrors = false
// Temporary styles which can be added in the app preview for things like
// DND. We clear these whenever a new instance is received.
@ -137,16 +138,21 @@
// Derive definition properties which can all be optional, so need to be
// coerced to booleans
$: componentErrors = instance?._meta?.errors
$: hasChildren = !!definition?.hasChildren
$: showEmptyState = definition?.showEmptyState !== false
$: hasMissingRequiredSettings = missingRequiredSettings?.length > 0
$: editable = !!definition?.editable && !hasMissingRequiredSettings
$: hasComponentErrors = componentErrors?.length > 0
$: requiredAncestors = definition?.requiredAncestors || []
$: missingRequiredAncestors = requiredAncestors.filter(
ancestor => !$component.ancestors.includes(`${BudibasePrefix}${ancestor}`)
)
$: hasMissingRequiredAncestors = missingRequiredAncestors?.length > 0
$: errorState = hasMissingRequiredSettings || hasMissingRequiredAncestors
$: errorState =
hasMissingRequiredSettings ||
hasMissingRequiredAncestors ||
hasComponentErrors
// Interactive components can be selected, dragged and highlighted inside
// the builder preview
@ -692,6 +698,7 @@
<ComponentErrorState
{missingRequiredSettings}
{missingRequiredAncestors}
{componentErrors}
/>
{:else}
<svelte:component this={constructor} bind:this={ref} {...initialSettings}>

View File

@ -8,6 +8,7 @@
| { key: string; label: string }[]
| undefined
export let missingRequiredAncestors: string[] | undefined
export let componentErrors: string[] | undefined
const component = getContext("component")
const { styleable, builderStore } = getContext("sdk")
@ -15,6 +16,7 @@
$: styles = { ...$component.styles, normal: {}, custom: null, empty: true }
$: requiredSetting = missingRequiredSettings?.[0]
$: requiredAncestor = missingRequiredAncestors?.[0]
$: errorMessage = componentErrors?.[0]
</script>
{#if $builderStore.inBuilder}
@ -23,6 +25,8 @@
<Icon name="Alert" color="var(--spectrum-global-color-static-red-600)" />
{#if requiredAncestor}
<MissingRequiredAncestor {requiredAncestor} />
{:else if errorMessage}
{errorMessage}
{:else if requiredSetting}
<MissingRequiredSetting {requiredSetting} />
{/if}

View File

@ -43,6 +43,7 @@ const loadBudibase = async () => {
usedPlugins: window["##BUDIBASE_USED_PLUGINS##"],
location: window["##BUDIBASE_LOCATION##"],
snippets: window["##BUDIBASE_SNIPPETS##"],
componentErrors: window["##BUDIBASE_COMPONENT_ERRORS##"],
})
// Set app ID - this window flag is set by both the preview and the real

View File

@ -19,6 +19,7 @@ const createBuilderStore = () => {
eventResolvers: {},
metadata: null,
snippets: null,
componentErrors: {},
// Legacy - allow the builder to specify a layout
layout: null,

View File

@ -42,6 +42,14 @@ const createScreenStore = () => {
if ($builderStore.layout) {
activeLayout = $builderStore.layout
}
// Attach meta
const errors = $builderStore.componentErrors || {}
const attachComponentMeta = component => {
component._meta = { errors: errors[component._id] || [] }
component._children?.forEach(attachComponentMeta)
}
attachComponentMeta(activeScreen.props)
} else {
// Find the correct screen by matching the current route
screens = $appStore.screens || []

View File

@ -7,7 +7,7 @@
"../shared-core/src",
"../string-templates/src"
],
"ext": "js,ts,json,svelte",
"ext": "js,ts,json,svelte,hbs",
"ignore": [
"**/*.spec.ts",
"**/*.spec.js",

View File

@ -73,7 +73,8 @@
hiddenComponentIds,
usedPlugins,
location,
snippets
snippets,
componentErrors
} = parsed
// Set some flags so the app knows we're in the builder
@ -91,6 +92,7 @@
window["##BUDIBASE_USED_PLUGINS##"] = usedPlugins
window["##BUDIBASE_LOCATION##"] = location
window["##BUDIBASE_SNIPPETS##"] = snippets
window['##BUDIBASE_COMPONENT_ERRORS##'] = componentErrors
// Initialise app
try {