Merge branch 'master' into state-and-bindings-panels

This commit is contained in:
Andrew Kingston 2025-01-24 09:44:24 +00:00 committed by GitHub
commit 899e75869a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 200 additions and 26 deletions

View File

@ -41,11 +41,12 @@ module.exports = {
if ( if (
/^@budibase\/[^/]+\/.*$/.test(importPath) && /^@budibase\/[^/]+\/.*$/.test(importPath) &&
importPath !== "@budibase/backend-core/tests" && importPath !== "@budibase/backend-core/tests" &&
importPath !== "@budibase/string-templates/test/utils" importPath !== "@budibase/string-templates/test/utils" &&
importPath !== "@budibase/client/manifest.json"
) { ) {
context.report({ context.report({
node, 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

@ -293,7 +293,7 @@
type: RowSelector, type: RowSelector,
props: { props: {
row: inputData["oldRow"] || { row: inputData["oldRow"] || {
tableId: inputData["row"].tableId, tableId: inputData["row"]?.tableId,
}, },
meta: { meta: {
fields: inputData["meta"]?.oldFields || {}, fields: inputData["meta"]?.oldFields || {},

View File

@ -7,8 +7,21 @@
export let dataSet export let dataSet
export let value export let value
export let onSelect export let onSelect
export let identifiers = ["resourceId"]
$: displayDatasourceName = $datasources.list.length > 1 $: 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
}
</script> </script>
{#if dividerState} {#if dividerState}
@ -24,8 +37,7 @@
{#each dataSet as data} {#each dataSet as data}
<li <li
class="spectrum-Menu-item" class="spectrum-Menu-item"
class:is-selected={value?.resourceId === data.resourceId && class:is-selected={isSelected(data) && value?.type === data.type}
value?.type === data.type}
role="option" role="option"
aria-selected="true" aria-selected="true"
tabindex="0" tabindex="0"

View File

@ -291,6 +291,7 @@
dataSet={views} dataSet={views}
{value} {value}
onSelect={handleSelected} onSelect={handleSelected}
identifiers={["tableId", "name"]}
/> />
{/if} {/if}
{#if queries?.length} {#if queries?.length}
@ -300,6 +301,7 @@
dataSet={queries} dataSet={queries}
{value} {value}
onSelect={handleSelected} onSelect={handleSelected}
identifiers={["_id"]}
/> />
{/if} {/if}
{#if links?.length} {#if links?.length}
@ -309,6 +311,7 @@
dataSet={links} dataSet={links}
{value} {value}
onSelect={handleSelected} onSelect={handleSelected}
identifiers={["tableId", "fieldName"]}
/> />
{/if} {/if}
{#if fields?.length} {#if fields?.length}
@ -318,6 +321,7 @@
dataSet={fields} dataSet={fields}
{value} {value}
onSelect={handleSelected} onSelect={handleSelected}
identifiers={["providerId", "tableId", "fieldName"]}
/> />
{/if} {/if}
{#if jsonArrays?.length} {#if jsonArrays?.length}
@ -327,6 +331,7 @@
dataSet={jsonArrays} dataSet={jsonArrays}
{value} {value}
onSelect={handleSelected} onSelect={handleSelected}
identifiers={["providerId", "tableId", "fieldName"]}
/> />
{/if} {/if}
{#if showDataProviders && dataProviders?.length} {#if showDataProviders && dataProviders?.length}
@ -336,6 +341,7 @@
dataSet={dataProviders} dataSet={dataProviders}
{value} {value}
onSelect={handleSelected} onSelect={handleSelected}
identifiers={["providerId"]}
/> />
{/if} {/if}
<DataSourceCategory <DataSourceCategory

View File

@ -9,3 +9,4 @@ export {
lowercase, lowercase,
isBuilderInputFocused, isBuilderInputFocused,
} from "./helpers" } from "./helpers"
export * as featureFlag from "./featureFlags"

View File

@ -0,0 +1,46 @@
import { Component, Screen, ScreenProps } from "@budibase/types"
import clientManifest from "@budibase/client/manifest.json"
export function findComponentsBySettingsType(
screen: Screen,
type: string | string[]
) {
const typesArray = Array.isArray(type) ? type : [type]
const result: {
component: Component
setting: {
type: string
key: string
}
}[] = []
function recurseFieldComponentsInChildren(component: ScreenProps) {
if (!component) {
return
}
const definition = getManifestDefinition(component)
const setting =
"settings" in definition &&
definition.settings.find((s: any) => 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
}

View File

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

View File

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

View File

@ -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<TItem extends {}, TKey extends keyof TItem>(
key: TKey,
list: TItem[]
) {
return list.reduce(
(result, item) => ({
...result,
[item[key] as string]: item,
}),
{}
)
}
const friendlyNameByType: Partial<Record<UIDatasourceType, string>> = {
viewV2: "view",
}
const validationKeyByType: Record<UIDatasourceType, string | null> = {
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<string, any>
) {
const result: Record<string, string[]> = {}
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)
}
)

View File

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

View File

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

View File

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

View File

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

View File

@ -42,6 +42,14 @@ const createScreenStore = () => {
if ($builderStore.layout) { if ($builderStore.layout) {
activeLayout = $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 { } else {
// Find the correct screen by matching the current route // Find the correct screen by matching the current route
screens = $appStore.screens || [] screens = $appStore.screens || []

View File

@ -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 { derived, get, Readable, Writable } from "svelte/store"
import { import {
DataFetchDefinition, DataFetchDefinition,
@ -10,12 +8,10 @@ import { enrichSchemaWithRelColumns, memo } from "../../../utils"
import { cloneDeep } from "lodash" import { cloneDeep } from "lodash"
import { import {
SaveRowRequest, SaveRowRequest,
SaveTableRequest,
UIDatasource, UIDatasource,
UIFieldMutation, UIFieldMutation,
UIFieldSchema, UIFieldSchema,
UIRow, UIRow,
UpdateViewRequest,
ViewV2Type, ViewV2Type,
} from "@budibase/types" } from "@budibase/types"
import { Store as StoreContext, BaseStoreProps } from "." import { Store as StoreContext, BaseStoreProps } from "."
@ -79,7 +75,7 @@ export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
const schema = derived(definition, $definition => { const schema = derived(definition, $definition => {
const schema: Record<string, any> | undefined = getDatasourceSchema({ const schema: Record<string, any> | undefined = getDatasourceSchema({
API, API,
datasource: get(datasource) as any, // TODO: see line 1 datasource: get(datasource),
definition: $definition ?? undefined, definition: $definition ?? undefined,
}) })
if (!schema) { if (!schema) {
@ -137,7 +133,7 @@ export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
let type = $datasource?.type let type = $datasource?.type
// @ts-expect-error // @ts-expect-error
if (type === "provider") { if (type === "provider") {
type = ($datasource as any).value?.datasource?.type // TODO: see line 1 type = ($datasource as any).value?.datasource?.type
} }
// Handle calculation views // Handle calculation views
if ( if (
@ -196,15 +192,13 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
const refreshDefinition = async () => { const refreshDefinition = async () => {
const def = await getDatasourceDefinition({ const def = await getDatasourceDefinition({
API, 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 // Saves the datasource definition
const saveDefinition = async ( const saveDefinition = async (newDefinition: DataFetchDefinition) => {
newDefinition: SaveTableRequest | UpdateViewRequest
) => {
// Update local state // Update local state
const originalDefinition = get(definition) const originalDefinition = get(definition)
definition.set(newDefinition) definition.set(newDefinition)
@ -245,7 +239,7 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
delete newDefinition.schema[column].default 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 // Adds a schema mutation for a single field
@ -321,7 +315,7 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
await saveDefinition({ await saveDefinition({
...$definition, ...$definition,
schema: newSchema, schema: newSchema,
} as any) // TODO: see line 1 })
resetSchemaMutations() resetSchemaMutations()
} }

View File

@ -21,7 +21,7 @@ export default class ViewFetch extends BaseDataFetch<ViewV1Datasource, Table> {
getSchema(definition: Table) { getSchema(definition: Table) {
const { datasource } = this.options const { datasource } = this.options
return definition?.views?.[datasource.name]?.schema return definition?.views?.[datasource?.name]?.schema
} }
async getData() { async getData() {

View File

@ -101,12 +101,12 @@ export const fetchData = <
// Creates an empty fetch instance with no datasource configured, so no data // Creates an empty fetch instance with no datasource configured, so no data
// will initially be loaded // will initially be loaded
const createEmptyFetchInstance = ({ const createEmptyFetchInstance = <T extends DataFetchDatasource>({
API, API,
datasource, datasource,
}: { }: {
API: APIClient API: APIClient
datasource: DataFetchDatasource datasource: T
}) => { }) => {
const handler = DataFetchMap[datasource?.type] const handler = DataFetchMap[datasource?.type]
if (!handler) { if (!handler) {
@ -114,7 +114,7 @@ const createEmptyFetchInstance = ({
} }
return new handler({ return new handler({
API, API,
datasource: null as never, datasource: datasource as any,
query: null as any, query: null as any,
}) })
} }

View File

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

View File

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

View File

@ -1,5 +1,6 @@
export enum FeatureFlag { export enum FeatureFlag {
USE_ZOD_VALIDATOR = "USE_ZOD_VALIDATOR", USE_ZOD_VALIDATOR = "USE_ZOD_VALIDATOR",
CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS = "CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS",
// Account-portal // Account-portal
DIRECT_LOGIN_TO_ACCOUNT_PORTAL = "DIRECT_LOGIN_TO_ACCOUNT_PORTAL", DIRECT_LOGIN_TO_ACCOUNT_PORTAL = "DIRECT_LOGIN_TO_ACCOUNT_PORTAL",
@ -7,6 +8,7 @@ export enum FeatureFlag {
export const FeatureFlagDefaults = { export const FeatureFlagDefaults = {
[FeatureFlag.USE_ZOD_VALIDATOR]: false, [FeatureFlag.USE_ZOD_VALIDATOR]: false,
[FeatureFlag.CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS]: false,
// Account-portal // Account-portal
[FeatureFlag.DIRECT_LOGIN_TO_ACCOUNT_PORTAL]: false, [FeatureFlag.DIRECT_LOGIN_TO_ACCOUNT_PORTAL]: false,

View File

@ -0,0 +1 @@
export type UIDatasourceType = "table" | "view" | "viewV2" | "query" | "custom"

View File

@ -2,3 +2,4 @@ export * from "./stores"
export * from "./bindings" export * from "./bindings"
export * from "./components" export * from "./components"
export * from "./dataFetch" export * from "./dataFetch"
export * from "./datasource"