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/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
index 16183ea59a..e713d9bc85 100644
--- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
+++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
@@ -293,7 +293,7 @@
type: RowSelector,
props: {
row: inputData["oldRow"] || {
- tableId: inputData["row"].tableId,
+ tableId: inputData["row"]?.tableId,
},
meta: {
fields: inputData["meta"]?.oldFields || {},
diff --git a/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte b/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte
index 72e2fbf638..4ea8c63087 100644
--- a/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte
+++ b/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte
@@ -7,8 +7,21 @@
export let dataSet
export let value
export let onSelect
+ export let identifiers = ["resourceId"]
$: displayDatasourceName = $datasources.list.length > 1
+
+ function isSelected(entry) {
+ if (!identifiers.length) {
+ return false
+ }
+ for (const identifier of identifiers) {
+ if (entry[identifier] !== value?.[identifier]) {
+ return false
+ }
+ }
+ return true
+ }
{#if dividerState}
@@ -24,8 +37,7 @@
{#each dataSet as data}
{/if}
{#if queries?.length}
@@ -300,6 +301,7 @@
dataSet={queries}
{value}
onSelect={handleSelected}
+ identifiers={["_id"]}
/>
{/if}
{#if links?.length}
@@ -309,6 +311,7 @@
dataSet={links}
{value}
onSelect={handleSelected}
+ identifiers={["tableId", "fieldName"]}
/>
{/if}
{#if fields?.length}
@@ -318,6 +321,7 @@
dataSet={fields}
{value}
onSelect={handleSelected}
+ identifiers={["providerId", "tableId", "fieldName"]}
/>
{/if}
{#if jsonArrays?.length}
@@ -327,6 +331,7 @@
dataSet={jsonArrays}
{value}
onSelect={handleSelected}
+ identifiers={["providerId", "tableId", "fieldName"]}
/>
{/if}
{#if showDataProviders && dataProviders?.length}
@@ -336,6 +341,7 @@
dataSet={dataProviders}
{value}
onSelect={handleSelected}
+ identifiers={["providerId"]}
/>
{/if}
typesArray.includes(s.type))
+ if (setting && "type" in setting) {
+ result.push({
+ component,
+ setting: { type: setting.type!, key: setting.key! },
+ })
+ }
+ component._children?.forEach(child => {
+ recurseFieldComponentsInChildren(child)
+ })
+ }
+
+ recurseFieldComponentsInChildren(screen?.props)
+ 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..d8169fdedb
--- /dev/null
+++ b/packages/builder/src/stores/builder/screenComponent.ts
@@ -0,0 +1,83 @@
+import { derived } from "svelte/store"
+import { tables } from "./tables"
+import { selectedScreen } from "./screens"
+import { viewsV2 } from "./viewsV2"
+import { findComponentsBySettingsType } from "@/helpers/screen"
+import { UIDatasourceType, Screen } from "@budibase/types"
+import { queries } from "./queries"
+import { views } from "./views"
+import { featureFlag } from "@/helpers"
+
+function reduceBy(
+ key: TKey,
+ list: TItem[]
+) {
+ return list.reduce(
+ (result, item) => ({
+ ...result,
+ [item[key] as string]: item,
+ }),
+ {}
+ )
+}
+
+const friendlyNameByType: Partial> = {
+ viewV2: "view",
+}
+
+const validationKeyByType: Record = {
+ table: "tableId",
+ view: "name",
+ viewV2: "id",
+ query: "_id",
+ custom: null,
+}
+
+export const screenComponentErrors = derived(
+ [selectedScreen, tables, views, viewsV2, queries],
+ ([$selectedScreen, $tables, $views, $viewsV2, $queries]): Record<
+ string,
+ string[]
+ > => {
+ if (!featureFlag.isEnabled("CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS")) {
+ return {}
+ }
+ function getInvalidDatasources(
+ screen: Screen,
+ datasources: Record
+ ) {
+ const result: Record = {}
+ for (const { component, setting } of findComponentsBySettingsType(
+ screen,
+ ["table", "dataSource"]
+ )) {
+ const componentSettings = component[setting.key]
+ const { label } = componentSettings
+ const type = componentSettings.type as UIDatasourceType
+
+ const validationKey = validationKeyByType[type]
+ if (!validationKey) {
+ continue
+ }
+ const resourceId = componentSettings[validationKey]
+ if (!datasources[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),
+ ...reduceBy("name", $views.list),
+ ...reduceBy("id", $viewsV2.list),
+ ...reduceBy("_id", $queries.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..7069b7a431 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}
@@ -34,7 +38,7 @@
.component-placeholder {
display: flex;
flex-direction: row;
- justify-content: flex-start;
+ justify-content: center;
align-items: center;
color: var(--spectrum-global-color-gray-600);
font-size: var(--font-size-s);
diff --git a/packages/client/src/index.js b/packages/client/src/index.js
index e3f5c34d3e..19e2f84e57 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 7261ca375a..a7277ca2b5 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/frontend-core/src/components/grid/stores/datasource.ts b/packages/frontend-core/src/components/grid/stores/datasource.ts
index 588f373152..5934c7c636 100644
--- a/packages/frontend-core/src/components/grid/stores/datasource.ts
+++ b/packages/frontend-core/src/components/grid/stores/datasource.ts
@@ -1,5 +1,3 @@
-// TODO: datasource and defitions are unions of the different implementations. At this point, the datasource does not know what type is being used, and the assignations will cause TS exceptions. Casting it "as any" for now. This should be fixed improving the type usages.
-
import { derived, get, Readable, Writable } from "svelte/store"
import {
DataFetchDefinition,
@@ -10,12 +8,10 @@ import { enrichSchemaWithRelColumns, memo } from "../../../utils"
import { cloneDeep } from "lodash"
import {
SaveRowRequest,
- SaveTableRequest,
UIDatasource,
UIFieldMutation,
UIFieldSchema,
UIRow,
- UpdateViewRequest,
ViewV2Type,
} from "@budibase/types"
import { Store as StoreContext, BaseStoreProps } from "."
@@ -79,7 +75,7 @@ export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
const schema = derived(definition, $definition => {
const schema: Record | undefined = getDatasourceSchema({
API,
- datasource: get(datasource) as any, // TODO: see line 1
+ datasource: get(datasource),
definition: $definition ?? undefined,
})
if (!schema) {
@@ -137,7 +133,7 @@ export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
let type = $datasource?.type
// @ts-expect-error
if (type === "provider") {
- type = ($datasource as any).value?.datasource?.type // TODO: see line 1
+ type = ($datasource as any).value?.datasource?.type
}
// Handle calculation views
if (
@@ -196,15 +192,13 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
const refreshDefinition = async () => {
const def = await getDatasourceDefinition({
API,
- datasource: get(datasource) as any, // TODO: see line 1
+ datasource: get(datasource),
})
- definition.set(def as any) // TODO: see line 1
+ definition.set(def ?? null)
}
// Saves the datasource definition
- const saveDefinition = async (
- newDefinition: SaveTableRequest | UpdateViewRequest
- ) => {
+ const saveDefinition = async (newDefinition: DataFetchDefinition) => {
// Update local state
const originalDefinition = get(definition)
definition.set(newDefinition)
@@ -245,7 +239,7 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
delete newDefinition.schema[column].default
}
}
- return await saveDefinition(newDefinition as any) // TODO: see line 1
+ return await saveDefinition(newDefinition)
}
// Adds a schema mutation for a single field
@@ -321,7 +315,7 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
await saveDefinition({
...$definition,
schema: newSchema,
- } as any) // TODO: see line 1
+ })
resetSchemaMutations()
}
diff --git a/packages/frontend-core/src/fetch/ViewFetch.ts b/packages/frontend-core/src/fetch/ViewFetch.ts
index 6555896ae8..720f91eaab 100644
--- a/packages/frontend-core/src/fetch/ViewFetch.ts
+++ b/packages/frontend-core/src/fetch/ViewFetch.ts
@@ -21,7 +21,7 @@ export default class ViewFetch extends BaseDataFetch {
getSchema(definition: Table) {
const { datasource } = this.options
- return definition?.views?.[datasource.name]?.schema
+ return definition?.views?.[datasource?.name]?.schema
}
async getData() {
diff --git a/packages/frontend-core/src/fetch/index.ts b/packages/frontend-core/src/fetch/index.ts
index e12e74340c..547043145d 100644
--- a/packages/frontend-core/src/fetch/index.ts
+++ b/packages/frontend-core/src/fetch/index.ts
@@ -101,12 +101,12 @@ export const fetchData = <
// Creates an empty fetch instance with no datasource configured, so no data
// will initially be loaded
-const createEmptyFetchInstance = ({
+const createEmptyFetchInstance = ({
API,
datasource,
}: {
API: APIClient
- datasource: DataFetchDatasource
+ datasource: T
}) => {
const handler = DataFetchMap[datasource?.type]
if (!handler) {
@@ -114,7 +114,7 @@ const createEmptyFetchInstance = ({
}
return new handler({
API,
- datasource: null as never,
+ datasource: datasource as any,
query: null as any,
})
}
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 {
diff --git a/packages/types/src/sdk/featureFlag.ts b/packages/types/src/sdk/featureFlag.ts
index 996d3bba8d..d9f092c80a 100644
--- a/packages/types/src/sdk/featureFlag.ts
+++ b/packages/types/src/sdk/featureFlag.ts
@@ -1,5 +1,6 @@
export enum FeatureFlag {
USE_ZOD_VALIDATOR = "USE_ZOD_VALIDATOR",
+ CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS = "CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS",
// Account-portal
DIRECT_LOGIN_TO_ACCOUNT_PORTAL = "DIRECT_LOGIN_TO_ACCOUNT_PORTAL",
@@ -7,6 +8,7 @@ export enum FeatureFlag {
export const FeatureFlagDefaults = {
[FeatureFlag.USE_ZOD_VALIDATOR]: false,
+ [FeatureFlag.CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS]: false,
// Account-portal
[FeatureFlag.DIRECT_LOGIN_TO_ACCOUNT_PORTAL]: false,
diff --git a/packages/types/src/ui/datasource.ts b/packages/types/src/ui/datasource.ts
new file mode 100644
index 0000000000..53740e8c4d
--- /dev/null
+++ b/packages/types/src/ui/datasource.ts
@@ -0,0 +1 @@
+export type UIDatasourceType = "table" | "view" | "viewV2" | "query" | "custom"
diff --git a/packages/types/src/ui/index.ts b/packages/types/src/ui/index.ts
index 907f4ec0b5..6e5f37608c 100644
--- a/packages/types/src/ui/index.ts
+++ b/packages/types/src/ui/index.ts
@@ -2,3 +2,4 @@ export * from "./stores"
export * from "./bindings"
export * from "./components"
export * from "./dataFetch"
+export * from "./datasource"