diff --git a/eslint-local-rules/index.js b/eslint-local-rules/index.js index 9348706399..d9d894c33e 100644 --- a/eslint-local-rules/index.js +++ b/eslint-local-rules/index.js @@ -41,12 +41,11 @@ module.exports = { if ( /^@budibase\/[^/]+\/.*$/.test(importPath) && importPath !== "@budibase/backend-core/tests" && - importPath !== "@budibase/string-templates/test/utils" && - importPath !== "@budibase/client/manifest.json" + importPath !== "@budibase/string-templates/test/utils" ) { context.report({ 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.`, }) } }, diff --git a/lerna.json b/lerna.json index c16b958d24..15f405c847 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.3.5", + "version": "3.3.6", "npmClient": "yarn", "concurrency": 20, "command": { diff --git a/packages/builder/src/helpers/duplicate.ts b/packages/builder/src/helpers/duplicate.ts index b4740b3e52..eb705c6525 100644 --- a/packages/builder/src/helpers/duplicate.ts +++ b/packages/builder/src/helpers/duplicate.ts @@ -76,13 +76,15 @@ export const getSequentialName = <T extends any>( { getName, numberFirstItem, + separator = "", }: { getName?: (item: T) => string numberFirstItem?: boolean + separator?: string } = {} ) => { if (!prefix?.length) { - return null + return "" } const trimmedPrefix = prefix.trim() const firstName = numberFirstItem ? `${prefix}1` : trimmedPrefix @@ -107,5 +109,5 @@ export const getSequentialName = <T extends any>( max = num } }) - return max === 0 ? firstName : `${prefix}${max + 1}` + return max === 0 ? firstName : `${prefix}${separator}${max + 1}` } diff --git a/packages/builder/src/helpers/screen.ts b/packages/builder/src/helpers/screen.ts deleted file mode 100644 index 24988c2195..0000000000 --- a/packages/builder/src/helpers/screen.ts +++ /dev/null @@ -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 -} diff --git a/packages/builder/src/helpers/tests/duplicate.test.ts b/packages/builder/src/helpers/tests/duplicate.test.ts index 131e76a6c2..5e956d7b1c 100644 --- a/packages/builder/src/helpers/tests/duplicate.test.ts +++ b/packages/builder/src/helpers/tests/duplicate.test.ts @@ -49,7 +49,7 @@ describe("getSequentialName", () => { it("handles nullish prefix", async () => { const name = getSequentialName([], null) - expect(name).toBe(null) + expect(name).toBe("") }) it("handles just the prefix", async () => { diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index 90e1abfecf..f0dbc560c1 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -20,6 +20,7 @@ import { previewStore, tables, componentTreeNodesStore, + screenComponents, } from "@/stores/builder" import { buildFormSchema, getSchemaForDatasource } from "@/dataBinding" import { @@ -37,6 +38,7 @@ import { Table, } from "@budibase/types" import { utils } from "@budibase/shared-core" +import { getSequentialName } from "@/helpers/duplicate" interface Component extends ComponentType { _id: string @@ -60,6 +62,7 @@ export interface ComponentDefinition { features?: Record<string, boolean> typeSupportPresets?: Record<string, any> legalDirectChildren: string[] + requiredAncestors?: string[] illegalChildren: string[] } @@ -452,7 +455,7 @@ export class ComponentStore extends BudiStore<ComponentState> { * @returns */ createInstance( - componentName: string, + componentType: string, presetProps: any, parent: any ): Component | null { @@ -461,11 +464,20 @@ export class ComponentStore extends BudiStore<ComponentState> { throw "A valid screen must be selected" } - const definition = this.getDefinition(componentName) + const definition = this.getDefinition(componentType) if (!definition) { return null } + const componentName = getSequentialName( + get(screenComponents), + `New ${definition.friendlyName || definition.name}`, + { + getName: c => c._instanceName, + separator: " ", + } + ) + // Generate basic component structure let instance: Component = { _id: Helpers.uuid(), @@ -475,7 +487,7 @@ export class ComponentStore extends BudiStore<ComponentState> { hover: {}, active: {}, }, - _instanceName: `New ${definition.friendlyName || definition.name}`, + _instanceName: componentName, ...presetProps, } @@ -500,7 +512,7 @@ export class ComponentStore extends BudiStore<ComponentState> { } // Add step name to form steps - if (componentName.endsWith("/formstep")) { + if (componentType.endsWith("/formstep")) { const parentForm = findClosestMatchingComponent( screen.props, get(selectedComponent)?._id, @@ -529,14 +541,14 @@ export class ComponentStore extends BudiStore<ComponentState> { * @returns */ async create( - componentName: string, + componentType: string, presetProps: any, parent: Component, index: number ) { const state = get(this.store) const componentInstance = this.createInstance( - componentName, + componentType, presetProps, parent ) diff --git a/packages/builder/src/stores/builder/index.js b/packages/builder/src/stores/builder/index.js index 54c1aa39a2..7dd7f67828 100644 --- a/packages/builder/src/stores/builder/index.js +++ b/packages/builder/src/stores/builder/index.js @@ -16,7 +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" +import { screenComponents, screenComponentErrors } from "./screenComponent" // Backend import { tables } from "./tables" @@ -68,6 +68,7 @@ export { snippets, rowActions, appPublished, + screenComponents, screenComponentErrors, } diff --git a/packages/builder/src/stores/builder/screenComponent.ts b/packages/builder/src/stores/builder/screenComponent.ts index 47ac11a2c4..3338796283 100644 --- a/packages/builder/src/stores/builder/screenComponent.ts +++ b/packages/builder/src/stores/builder/screenComponent.ts @@ -2,21 +2,19 @@ import { derived } from "svelte/store" import { tables } from "./tables" import { selectedScreen } from "./screens" import { viewsV2 } from "./viewsV2" -import { - findComponentsBySettingsType, - getManifestDefinition, -} from "@/helpers/screen" import { UIDatasourceType, Screen, Component, UIComponentError, + ScreenProps, } from "@budibase/types" import { queries } from "./queries" import { views } from "./views" import { findAllComponents } from "@/helpers/components" import { bindings, featureFlag } from "@/helpers" import { getBindableProperties } from "@/dataBinding" +import { componentStore, ComponentDefinition } from "./components" function reduceBy<TItem extends {}, TKey extends keyof TItem>( key: TKey, @@ -47,13 +45,17 @@ const validationKeyByType: Record<UIDatasourceType, string | null> = { } export const screenComponentErrors = derived( - [selectedScreen, tables, views, viewsV2, queries], - ([$selectedScreen, $tables, $views, $viewsV2, $queries]): Record< - string, - UIComponentError[] - > => { + [selectedScreen, tables, views, viewsV2, queries, componentStore], + ([ + $selectedScreen, + $tables, + $views, + $viewsV2, + $queries, + $componentStore, + ]): Record<string, UIComponentError[]> => { if ( - !featureFlag.isEnabled("CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS") || + !featureFlag.isEnabled("CHECK_COMPONENT_SETTINGS_ERRORS") || !$selectedScreen ) { return {} @@ -66,10 +68,12 @@ export const screenComponentErrors = derived( ...reduceBy("_id", $queries.list), } + const { components: definitions } = $componentStore + const errors = { - ...getInvalidDatasources($selectedScreen, datasources), - ...getMissingAncestors($selectedScreen), - ...getMissingRequiredSettings($selectedScreen), + ...getInvalidDatasources($selectedScreen, datasources, definitions), + ...getMissingAncestors($selectedScreen, definitions), + ...getMissingRequiredSettings($selectedScreen, definitions), } return errors } @@ -77,13 +81,15 @@ export const screenComponentErrors = derived( function getInvalidDatasources( screen: Screen, - datasources: Record<string, any> + datasources: Record<string, any>, + definitions: Record<string, ComponentDefinition> ) { const result: Record<string, UIComponentError[]> = {} - for (const { component, setting } of findComponentsBySettingsType(screen, [ - "table", - "dataSource", - ])) { + for (const { component, setting } of findComponentsBySettingsType( + screen, + ["table", "dataSource"], + definitions + )) { const componentSettings = component[setting.key] if (!componentSettings) { continue @@ -121,17 +127,20 @@ function getInvalidDatasources( return result } -function getMissingRequiredSettings(screen: Screen) { +function getMissingRequiredSettings( + screen: Screen, + definitions: Record<string, ComponentDefinition> +) { const allComponents = findAllComponents(screen.props) as Component[] const result: Record<string, UIComponentError[]> = {} for (const component of allComponents) { - const definition = getManifestDefinition(component) + const definition = definitions[component._component] if (!("settings" in definition)) { continue } - const missingRequiredSettings = definition.settings.filter( + const missingRequiredSettings = definition.settings?.filter( (setting: any) => { let empty = 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) => ({ key: s.key, 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/" -function getMissingAncestors(screen: Screen) { +function getMissingAncestors( + screen: Screen, + definitions: Record<string, ComponentDefinition> +) { const result: Record<string, UIComponentError[]> = {} function checkMissingAncestors(component: Component, ancestors: string[]) { @@ -188,9 +200,9 @@ function getMissingAncestors(screen: Screen) { checkMissingAncestors(child, [...ancestors, component._component]) } - const definition = getManifestDefinition(component) + const definition = definitions[component._component] - if (!("requiredAncestors" in definition)) { + if (!definition?.requiredAncestors?.length) { return } @@ -204,7 +216,7 @@ function getMissingAncestors(screen: Screen) { } result[component._id!] = missingAncestors.map(ancestor => { - const ancestorDefinition: any = getManifestDefinition(ancestor) + const ancestorDefinition = definitions[`${BudibasePrefix}${ancestor}`] return { message: `${pluralise(definition.name)} need to be inside a <mark>${ancestorDefinition.name}</mark>`, @@ -221,3 +233,52 @@ function getMissingAncestors(screen: Screen) { checkMissingAncestors(screen.props, []) 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[] + } +) diff --git a/packages/types/src/sdk/featureFlag.ts b/packages/types/src/sdk/featureFlag.ts index d9f092c80a..97893a1b5e 100644 --- a/packages/types/src/sdk/featureFlag.ts +++ b/packages/types/src/sdk/featureFlag.ts @@ -1,6 +1,6 @@ export enum FeatureFlag { 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 DIRECT_LOGIN_TO_ACCOUNT_PORTAL = "DIRECT_LOGIN_TO_ACCOUNT_PORTAL", @@ -8,7 +8,7 @@ export enum FeatureFlag { export const FeatureFlagDefaults = { [FeatureFlag.USE_ZOD_VALIDATOR]: false, - [FeatureFlag.CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS]: false, + [FeatureFlag.CHECK_COMPONENT_SETTINGS_ERRORS]: false, // Account-portal [FeatureFlag.DIRECT_LOGIN_TO_ACCOUNT_PORTAL]: false,