Merge pull request #15414 from Budibase/BUDI-8986/validate-datasource-setting-on-components
Validate datasource setting on components
This commit is contained in:
commit
b3197807c3
|
@ -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.`,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
|
@ -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}>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -19,6 +19,7 @@ const createBuilderStore = () => {
|
|||
eventResolvers: {},
|
||||
metadata: null,
|
||||
snippets: null,
|
||||
componentErrors: {},
|
||||
|
||||
// Legacy - allow the builder to specify a layout
|
||||
layout: null,
|
||||
|
|
|
@ -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 || []
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue