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 (
/^@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

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

View File

@ -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
}
</script>
{#if dividerState}
@ -24,8 +37,7 @@
{#each dataSet as data}
<li
class="spectrum-Menu-item"
class:is-selected={value?.resourceId === data.resourceId &&
value?.type === data.type}
class:is-selected={isSelected(data) && value?.type === data.type}
role="option"
aria-selected="true"
tabindex="0"

View File

@ -291,6 +291,7 @@
dataSet={views}
{value}
onSelect={handleSelected}
identifiers={["tableId", "name"]}
/>
{/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}
<DataSourceCategory

View File

@ -9,3 +9,4 @@ export {
lowercase,
isBuilderInputFocused,
} 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,
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,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 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}
@ -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);

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

@ -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<string, any> | 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()
}

View File

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

View File

@ -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 = <T extends DataFetchDatasource>({
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,
})
}

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 {

View File

@ -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,

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 "./components"
export * from "./dataFetch"
export * from "./datasource"