Merge branch 'master' into BUDI-9016/extract-componenterrors-from-client

This commit is contained in:
Adria Navarro 2025-01-31 12:57:15 +01:00
commit 884895060b
9 changed files with 117 additions and 91 deletions

View File

@ -41,12 +41,11 @@ 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, @budibase/string-templates/test/utils and @budibase/client/manifest.json.`, message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests and @budibase/string-templates/test/utils.`,
}) })
} }
}, },

View File

@ -1,6 +1,6 @@
{ {
"$schema": "node_modules/lerna/schemas/lerna-schema.json", "$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "3.3.5", "version": "3.3.6",
"npmClient": "yarn", "npmClient": "yarn",
"concurrency": 20, "concurrency": 20,
"command": { "command": {

View File

@ -76,13 +76,15 @@ export const getSequentialName = <T extends any>(
{ {
getName, getName,
numberFirstItem, numberFirstItem,
separator = "",
}: { }: {
getName?: (item: T) => string getName?: (item: T) => string
numberFirstItem?: boolean numberFirstItem?: boolean
separator?: string
} = {} } = {}
) => { ) => {
if (!prefix?.length) { if (!prefix?.length) {
return null return ""
} }
const trimmedPrefix = prefix.trim() const trimmedPrefix = prefix.trim()
const firstName = numberFirstItem ? `${prefix}1` : trimmedPrefix const firstName = numberFirstItem ? `${prefix}1` : trimmedPrefix
@ -107,5 +109,5 @@ export const getSequentialName = <T extends any>(
max = num max = num
} }
}) })
return max === 0 ? firstName : `${prefix}${max + 1}` return max === 0 ? firstName : `${prefix}${separator}${max + 1}`
} }

View File

@ -1,49 +0,0 @@
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
}
export function getManifestDefinition(component: Component | string) {
const componentType =
typeof component === "string"
? component
: component._component.split("/").slice(-1)[0]
const definition =
clientManifest[componentType as keyof typeof clientManifest]
return definition
}

View File

@ -49,7 +49,7 @@ describe("getSequentialName", () => {
it("handles nullish prefix", async () => { it("handles nullish prefix", async () => {
const name = getSequentialName([], null) const name = getSequentialName([], null)
expect(name).toBe(null) expect(name).toBe("")
}) })
it("handles just the prefix", async () => { it("handles just the prefix", async () => {

View File

@ -20,6 +20,7 @@ import {
previewStore, previewStore,
tables, tables,
componentTreeNodesStore, componentTreeNodesStore,
screenComponents,
} from "@/stores/builder" } from "@/stores/builder"
import { buildFormSchema, getSchemaForDatasource } from "@/dataBinding" import { buildFormSchema, getSchemaForDatasource } from "@/dataBinding"
import { import {
@ -37,6 +38,7 @@ import {
Table, Table,
} from "@budibase/types" } from "@budibase/types"
import { utils } from "@budibase/shared-core" import { utils } from "@budibase/shared-core"
import { getSequentialName } from "@/helpers/duplicate"
interface Component extends ComponentType { interface Component extends ComponentType {
_id: string _id: string
@ -60,6 +62,7 @@ export interface ComponentDefinition {
features?: Record<string, boolean> features?: Record<string, boolean>
typeSupportPresets?: Record<string, any> typeSupportPresets?: Record<string, any>
legalDirectChildren: string[] legalDirectChildren: string[]
requiredAncestors?: string[]
illegalChildren: string[] illegalChildren: string[]
} }
@ -452,7 +455,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
* @returns * @returns
*/ */
createInstance( createInstance(
componentName: string, componentType: string,
presetProps: any, presetProps: any,
parent: any parent: any
): Component | null { ): Component | null {
@ -461,11 +464,20 @@ export class ComponentStore extends BudiStore<ComponentState> {
throw "A valid screen must be selected" throw "A valid screen must be selected"
} }
const definition = this.getDefinition(componentName) const definition = this.getDefinition(componentType)
if (!definition) { if (!definition) {
return null return null
} }
const componentName = getSequentialName(
get(screenComponents),
`New ${definition.friendlyName || definition.name}`,
{
getName: c => c._instanceName,
separator: " ",
}
)
// Generate basic component structure // Generate basic component structure
let instance: Component = { let instance: Component = {
_id: Helpers.uuid(), _id: Helpers.uuid(),
@ -475,7 +487,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
hover: {}, hover: {},
active: {}, active: {},
}, },
_instanceName: `New ${definition.friendlyName || definition.name}`, _instanceName: componentName,
...presetProps, ...presetProps,
} }
@ -500,7 +512,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
} }
// Add step name to form steps // Add step name to form steps
if (componentName.endsWith("/formstep")) { if (componentType.endsWith("/formstep")) {
const parentForm = findClosestMatchingComponent( const parentForm = findClosestMatchingComponent(
screen.props, screen.props,
get(selectedComponent)?._id, get(selectedComponent)?._id,
@ -529,14 +541,14 @@ export class ComponentStore extends BudiStore<ComponentState> {
* @returns * @returns
*/ */
async create( async create(
componentName: string, componentType: string,
presetProps: any, presetProps: any,
parent: Component, parent: Component,
index: number index: number
) { ) {
const state = get(this.store) const state = get(this.store)
const componentInstance = this.createInstance( const componentInstance = this.createInstance(
componentName, componentType,
presetProps, presetProps,
parent parent
) )

View File

@ -16,7 +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" import { screenComponents, screenComponentErrors } from "./screenComponent"
// Backend // Backend
import { tables } from "./tables" import { tables } from "./tables"
@ -68,6 +68,7 @@ export {
snippets, snippets,
rowActions, rowActions,
appPublished, appPublished,
screenComponents,
screenComponentErrors, screenComponentErrors,
} }

View File

@ -2,21 +2,19 @@ import { derived } from "svelte/store"
import { tables } from "./tables" import { tables } from "./tables"
import { selectedScreen } from "./screens" import { selectedScreen } from "./screens"
import { viewsV2 } from "./viewsV2" import { viewsV2 } from "./viewsV2"
import {
findComponentsBySettingsType,
getManifestDefinition,
} from "@/helpers/screen"
import { import {
UIDatasourceType, UIDatasourceType,
Screen, Screen,
Component, Component,
UIComponentError, UIComponentError,
ScreenProps,
} from "@budibase/types" } from "@budibase/types"
import { queries } from "./queries" import { queries } from "./queries"
import { views } from "./views" import { views } from "./views"
import { findAllComponents } from "@/helpers/components" import { findAllComponents } from "@/helpers/components"
import { bindings, featureFlag } from "@/helpers" import { bindings, featureFlag } from "@/helpers"
import { getBindableProperties } from "@/dataBinding" import { getBindableProperties } from "@/dataBinding"
import { componentStore, ComponentDefinition } from "./components"
function reduceBy<TItem extends {}, TKey extends keyof TItem>( function reduceBy<TItem extends {}, TKey extends keyof TItem>(
key: TKey, key: TKey,
@ -47,13 +45,17 @@ const validationKeyByType: Record<UIDatasourceType, string | null> = {
} }
export const screenComponentErrors = derived( export const screenComponentErrors = derived(
[selectedScreen, tables, views, viewsV2, queries], [selectedScreen, tables, views, viewsV2, queries, componentStore],
([$selectedScreen, $tables, $views, $viewsV2, $queries]): Record< ([
string, $selectedScreen,
UIComponentError[] $tables,
> => { $views,
$viewsV2,
$queries,
$componentStore,
]): Record<string, UIComponentError[]> => {
if ( if (
!featureFlag.isEnabled("CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS") || !featureFlag.isEnabled("CHECK_COMPONENT_SETTINGS_ERRORS") ||
!$selectedScreen !$selectedScreen
) { ) {
return {} return {}
@ -66,10 +68,12 @@ export const screenComponentErrors = derived(
...reduceBy("_id", $queries.list), ...reduceBy("_id", $queries.list),
} }
const { components: definitions } = $componentStore
const errors = { const errors = {
...getInvalidDatasources($selectedScreen, datasources), ...getInvalidDatasources($selectedScreen, datasources, definitions),
...getMissingAncestors($selectedScreen), ...getMissingAncestors($selectedScreen, definitions),
...getMissingRequiredSettings($selectedScreen), ...getMissingRequiredSettings($selectedScreen, definitions),
} }
return errors return errors
} }
@ -77,13 +81,15 @@ export const screenComponentErrors = derived(
function getInvalidDatasources( function getInvalidDatasources(
screen: Screen, screen: Screen,
datasources: Record<string, any> datasources: Record<string, any>,
definitions: Record<string, ComponentDefinition>
) { ) {
const result: Record<string, UIComponentError[]> = {} const result: Record<string, UIComponentError[]> = {}
for (const { component, setting } of findComponentsBySettingsType(screen, [ for (const { component, setting } of findComponentsBySettingsType(
"table", screen,
"dataSource", ["table", "dataSource"],
])) { definitions
)) {
const componentSettings = component[setting.key] const componentSettings = component[setting.key]
if (!componentSettings) { if (!componentSettings) {
continue continue
@ -121,17 +127,20 @@ function getInvalidDatasources(
return result return result
} }
function getMissingRequiredSettings(screen: Screen) { function getMissingRequiredSettings(
screen: Screen,
definitions: Record<string, ComponentDefinition>
) {
const allComponents = findAllComponents(screen.props) as Component[] const allComponents = findAllComponents(screen.props) as Component[]
const result: Record<string, UIComponentError[]> = {} const result: Record<string, UIComponentError[]> = {}
for (const component of allComponents) { for (const component of allComponents) {
const definition = getManifestDefinition(component) const definition = definitions[component._component]
if (!("settings" in definition)) { if (!("settings" in definition)) {
continue continue
} }
const missingRequiredSettings = definition.settings.filter( const missingRequiredSettings = definition.settings?.filter(
(setting: any) => { (setting: any) => {
let empty = let empty =
component[setting.key] == null || component[setting.key] === "" component[setting.key] == null || component[setting.key] === ""
@ -167,7 +176,7 @@ function getMissingRequiredSettings(screen: Screen) {
} }
) )
if (missingRequiredSettings.length) { if (missingRequiredSettings?.length) {
result[component._id!] = missingRequiredSettings.map((s: any) => ({ result[component._id!] = missingRequiredSettings.map((s: any) => ({
key: s.key, key: s.key,
message: `Add the <mark>${s.label}</mark> setting to start using your component`, message: `Add the <mark>${s.label}</mark> setting to start using your component`,
@ -180,7 +189,10 @@ function getMissingRequiredSettings(screen: Screen) {
} }
const BudibasePrefix = "@budibase/standard-components/" const BudibasePrefix = "@budibase/standard-components/"
function getMissingAncestors(screen: Screen) { function getMissingAncestors(
screen: Screen,
definitions: Record<string, ComponentDefinition>
) {
const result: Record<string, UIComponentError[]> = {} const result: Record<string, UIComponentError[]> = {}
function checkMissingAncestors(component: Component, ancestors: string[]) { function checkMissingAncestors(component: Component, ancestors: string[]) {
@ -188,9 +200,9 @@ function getMissingAncestors(screen: Screen) {
checkMissingAncestors(child, [...ancestors, component._component]) checkMissingAncestors(child, [...ancestors, component._component])
} }
const definition = getManifestDefinition(component) const definition = definitions[component._component]
if (!("requiredAncestors" in definition)) { if (!definition?.requiredAncestors?.length) {
return return
} }
@ -204,7 +216,7 @@ function getMissingAncestors(screen: Screen) {
} }
result[component._id!] = missingAncestors.map(ancestor => { result[component._id!] = missingAncestors.map(ancestor => {
const ancestorDefinition: any = getManifestDefinition(ancestor) const ancestorDefinition = definitions[`${BudibasePrefix}${ancestor}`]
return { return {
message: `${pluralise(definition.name)} need to be inside a message: `${pluralise(definition.name)} need to be inside a
<mark>${ancestorDefinition.name}</mark>`, <mark>${ancestorDefinition.name}</mark>`,
@ -221,3 +233,52 @@ function getMissingAncestors(screen: Screen) {
checkMissingAncestors(screen.props, []) checkMissingAncestors(screen.props, [])
return result return result
} }
function findComponentsBySettingsType(
screen: Screen,
type: string | string[],
definitions: Record<string, ComponentDefinition>
) {
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 = definitions[component._component]
const setting = 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
}
export const screenComponents = derived(
[selectedScreen],
([$selectedScreen]) => {
if (!$selectedScreen) {
return []
}
return findAllComponents($selectedScreen.props) as Component[]
}
)

View File

@ -1,6 +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", CHECK_COMPONENT_SETTINGS_ERRORS = "CHECK_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",
@ -8,7 +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, [FeatureFlag.CHECK_COMPONENT_SETTINGS_ERRORS]: false,
// Account-portal // Account-portal
[FeatureFlag.DIRECT_LOGIN_TO_ACCOUNT_PORTAL]: false, [FeatureFlag.DIRECT_LOGIN_TO_ACCOUNT_PORTAL]: false,