diff --git a/eslint-local-rules/index.js b/eslint-local-rules/index.js index d9d894c33e..9348706399 100644 --- a/eslint-local-rules/index.js +++ b/eslint-local-rules/index.js @@ -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.`, }) } }, diff --git a/packages/builder/src/helpers/screen.ts b/packages/builder/src/helpers/screen.ts new file mode 100644 index 0000000000..71623844de --- /dev/null +++ b/packages/builder/src/helpers/screen.ts @@ -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 +} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte index 55a4dc4de4..3951c0e902 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte @@ -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 diff --git a/packages/builder/src/stores/builder/index.js b/packages/builder/src/stores/builder/index.js index 08d87bebf5..892b72c7ab 100644 --- a/packages/builder/src/stores/builder/index.js +++ b/packages/builder/src/stores/builder/index.js @@ -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 = () => { diff --git a/packages/builder/src/stores/builder/screenComponent.ts b/packages/builder/src/stores/builder/screenComponent.ts new file mode 100644 index 0000000000..a061158e6a --- /dev/null +++ b/packages/builder/src/stores/builder/screenComponent.ts @@ -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 => { + 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 + ) { + const friendlyNameByType = { + table: "table", + view: "view", + viewV2: "view", + } + + const result: Record = {} + 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) + } +) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 79b4ca6f68..236bcb7c7e 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -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 @@ {:else} diff --git a/packages/client/src/components/error-states/ComponentErrorState.svelte b/packages/client/src/components/error-states/ComponentErrorState.svelte index d30f4916da..9eace07018 100644 --- a/packages/client/src/components/error-states/ComponentErrorState.svelte +++ b/packages/client/src/components/error-states/ComponentErrorState.svelte @@ -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] {#if $builderStore.inBuilder} @@ -23,6 +25,8 @@ {#if requiredAncestor} + {:else if errorMessage} + {errorMessage} {:else if requiredSetting} {/if} diff --git a/packages/client/src/index.js b/packages/client/src/index.js index 9cef52bb1e..7cb9ed4430 100644 --- a/packages/client/src/index.js +++ b/packages/client/src/index.js @@ -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 diff --git a/packages/client/src/stores/builder.js b/packages/client/src/stores/builder.js index faa37eddca..1ae7d3a670 100644 --- a/packages/client/src/stores/builder.js +++ b/packages/client/src/stores/builder.js @@ -19,6 +19,7 @@ const createBuilderStore = () => { eventResolvers: {}, metadata: null, snippets: null, + componentErrors: {}, // Legacy - allow the builder to specify a layout layout: null, diff --git a/packages/client/src/stores/screens.js b/packages/client/src/stores/screens.js index bc87216660..491d8d4236 100644 --- a/packages/client/src/stores/screens.js +++ b/packages/client/src/stores/screens.js @@ -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 || [] diff --git a/packages/server/nodemon.json b/packages/server/nodemon.json index ac8c38ccb2..9871b9339e 100644 --- a/packages/server/nodemon.json +++ b/packages/server/nodemon.json @@ -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", diff --git a/packages/server/src/api/controllers/static/templates/preview.hbs b/packages/server/src/api/controllers/static/templates/preview.hbs index 87b9ad6ea3..750a780897 100644 --- a/packages/server/src/api/controllers/static/templates/preview.hbs +++ b/packages/server/src/api/controllers/static/templates/preview.hbs @@ -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 {