diff --git a/lerna.json b/lerna.json index d0fa1c5436..85a8ed3fcb 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.4.27-alpha.9", + "version": "2.4.27-alpha.12", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 0e3ab5e001..48b5694fa5 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.4.27-alpha.9", + "version": "2.4.27-alpha.12", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -24,7 +24,7 @@ "dependencies": { "@budibase/nano": "10.1.2", "@budibase/pouchdb-replication-stream": "1.2.10", - "@budibase/types": "2.4.27-alpha.9", + "@budibase/types": "2.4.27-alpha.12", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 1160bbc322..cd0bafb66f 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "2.4.27-alpha.9", + "version": "2.4.27-alpha.12", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,8 +38,8 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "1.2.1", - "@budibase/shared-core": "2.4.27-alpha.9", - "@budibase/string-templates": "2.4.27-alpha.9", + "@budibase/shared-core": "2.4.27-alpha.12", + "@budibase/string-templates": "2.4.27-alpha.12", "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", diff --git a/packages/bbui/src/Drawer/Drawer.svelte b/packages/bbui/src/Drawer/Drawer.svelte index 43729cd794..932236bc0c 100644 --- a/packages/bbui/src/Drawer/Drawer.svelte +++ b/packages/bbui/src/Drawer/Drawer.svelte @@ -7,7 +7,7 @@ export let title export let fillWidth export let left = "314px" - export let width = "calc(100% - 576px)" + export let width = "calc(100% - 626px)" let visible = false diff --git a/packages/bbui/src/Form/Core/Select.svelte b/packages/bbui/src/Form/Core/Select.svelte index 6e50cfd1f4..2fad886910 100644 --- a/packages/bbui/src/Form/Core/Select.svelte +++ b/packages/bbui/src/Form/Core/Select.svelte @@ -42,11 +42,12 @@ } const getFieldText = (value, options, placeholder) => { - // Always use placeholder if no value if (value == null || value === "") { + // Explicit false means use no placeholder and allow an empty fields if (placeholder === false) { return "" } + // Otherwise we use the placeholder if possible return placeholder || "Choose an option" } diff --git a/packages/builder/package.json b/packages/builder/package.json index 031bdf2315..781d0221df 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.4.27-alpha.9", + "version": "2.4.27-alpha.12", "license": "GPL-3.0", "private": true, "scripts": { @@ -58,11 +58,11 @@ } }, "dependencies": { - "@budibase/bbui": "2.4.27-alpha.9", - "@budibase/client": "2.4.27-alpha.9", - "@budibase/frontend-core": "2.4.27-alpha.9", - "@budibase/shared-core": "2.4.27-alpha.9", - "@budibase/string-templates": "2.4.27-alpha.9", + "@budibase/bbui": "2.4.27-alpha.12", + "@budibase/client": "2.4.27-alpha.12", + "@budibase/frontend-core": "2.4.27-alpha.12", + "@budibase/shared-core": "2.4.27-alpha.12", + "@budibase/string-templates": "2.4.27-alpha.12", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 51f88add27..3fc0eb769e 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -22,6 +22,7 @@ import { findComponent, getComponentSettings, makeComponentUnique, + findComponentPath, } from "../componentUtils" import { Helpers } from "@budibase/bbui" import { Utils } from "@budibase/frontend-core" @@ -30,7 +31,12 @@ import { DB_TYPE_INTERNAL, DB_TYPE_EXTERNAL, } from "constants/backend" -import { getSchemaForDatasource } from "builderStore/dataBinding" +import { + buildFormSchema, + getSchemaForDatasource, +} from "builderStore/dataBinding" +import { makePropSafe as safe } from "@budibase/string-templates" +import { getComponentFieldOptions } from "helpers/formFields" const INITIAL_FRONTEND_STATE = { apps: [], @@ -63,17 +69,19 @@ const INITIAL_FRONTEND_STATE = { customTheme: {}, previewDevice: "desktop", highlightedSettingKey: null, + builderSidePanel: false, // URL params selectedScreenId: null, selectedComponentId: null, selectedLayoutId: null, - // onboarding + // Client state + selectedComponentInstance: null, + + // Onboarding onboarding: false, tourNodes: null, - - builderSidePanel: false, } export const getFrontendStore = () => { @@ -262,22 +270,27 @@ export const getFrontendStore = () => { } }, save: async screen => { - /* - Temporarily disabled to accomodate migration issues. - store.actions.screens.validate(screen) - */ - const state = get(store) + // Validate screen structure + // Temporarily disabled to accommodate migration issues + // store.actions.screens.validate(screen) + + // Check screen definition for any component settings which need updated + store.actions.screens.enrichEmptySettings(screen) + + // Save screen const creatingNewScreen = screen._id === undefined const savedScreen = await API.saveScreen(screen) const routesResponse = await API.fetchAppRoutes() - let usedPlugins = state.usedPlugins // If plugins changed we need to fetch the latest app metadata + const state = get(store) + let usedPlugins = state.usedPlugins if (savedScreen.pluginAdded) { const { application } = await API.fetchAppPackage(state.appId) usedPlugins = application.usedPlugins || [] } + // Update state store.update(state => { // Update screen object const idx = state.screens.findIndex(x => x._id === savedScreen._id) @@ -298,7 +311,6 @@ export const getFrontendStore = () => { // Update used plugins state.usedPlugins = usedPlugins - return state }) return savedScreen @@ -406,6 +418,17 @@ export const getFrontendStore = () => { } await store.actions.screens.patch(patch, screen._id) }, + enrichEmptySettings: screen => { + // Flatten the recursive component tree + const components = findAllMatchingComponents(screen.props, x => x) + + // Iterate over all components and run checks + components.forEach(component => { + store.actions.components.enrichEmptySettings(component, { + screen, + }) + }) + }, }, preview: { setDevice: device => { @@ -493,65 +516,155 @@ export const getFrontendStore = () => { } return get(store).components[componentName] }, - createInstance: (componentName, presetProps) => { + getDefaultDatasource: () => { + // Ignore users table + const validTables = get(tables).list.filter(x => x._id !== "ta_users") + + // Try to use their own internal table first + let table = validTables.find(table => { + return ( + table.sourceId !== BUDIBASE_INTERNAL_DB_ID && + table.type === DB_TYPE_INTERNAL + ) + }) + if (table) { + return table + } + + // Then try sample data + table = validTables.find(table => { + return ( + table.sourceId === BUDIBASE_INTERNAL_DB_ID && + table.type === DB_TYPE_INTERNAL + ) + }) + if (table) { + return table + } + + // Finally try an external table + return validTables.find(table => table.type === DB_TYPE_EXTERNAL) + }, + enrichEmptySettings: (component, opts) => { + if (!component?._component) { + return + } + const defaultDS = store.actions.components.getDefaultDatasource() + const settings = getComponentSettings(component._component) + const { parent, screen, useDefaultValues } = opts || {} + const treeId = parent?._id || component._id + if (!screen) { + return + } + settings.forEach(setting => { + const value = component[setting.key] + + // Fill empty settings + if (value == null || value === "") { + if (setting.type === "multifield" && setting.selectAllFields) { + // Select all schema fields where required + component[setting.key] = Object.keys(defaultDS?.schema || {}) + } else if ( + (setting.type === "dataSource" || setting.type === "table") && + defaultDS + ) { + // Select default datasource where required + component[setting.key] = { + label: defaultDS.name, + tableId: defaultDS._id, + type: "table", + } + } else if (setting.type === "dataProvider") { + // Pick closest data provider where required + const path = findComponentPath(screen.props, treeId) + const providers = path.filter(component => + component._component?.endsWith("/dataprovider") + ) + if (providers.length) { + const id = providers[providers.length - 1]?._id + component[setting.key] = `{{ literal ${safe(id)} }}` + } + } else if (setting.type.startsWith("field/")) { + // Autofill form field names + // Get all available field names in this form schema + let fieldOptions = getComponentFieldOptions( + screen.props, + treeId, + setting.type, + false + ) + + // Get all currently used fields + const form = findClosestMatchingComponent( + screen.props, + treeId, + x => x._component === "@budibase/standard-components/form" + ) + const usedFields = Object.keys(buildFormSchema(form) || {}) + + // Filter out already used fields + fieldOptions = fieldOptions.filter(x => !usedFields.includes(x)) + + // Set field name and also assume we have a label setting + if (fieldOptions[0]) { + component[setting.key] = fieldOptions[0] + component.label = fieldOptions[0] + } + } else if (useDefaultValues && setting.defaultValue !== undefined) { + // Use default value where required + component[setting.key] = setting.defaultValue + } + } + + // Validate non-empty settings + else { + if (setting.type === "dataProvider") { + // Validate data provider exists, or else clear it + const treeId = parent?._id || component._id + const path = findComponentPath(screen?.props, treeId) + const providers = path.filter(component => + component._component?.endsWith("/dataprovider") + ) + // Validate non-empty values + const valid = providers?.some(dp => value.includes?.(dp._id)) + if (!valid) { + if (providers.length) { + const id = providers[providers.length - 1]?._id + component[setting.key] = `{{ literal ${safe(id)} }}` + } else { + delete component[setting.key] + } + } + } + } + }) + }, + createInstance: (componentName, presetProps, parent) => { const definition = store.actions.components.getDefinition(componentName) if (!definition) { return null } - // Flattened settings - const settings = getComponentSettings(componentName) - - let dataSourceField = settings.find( - setting => setting.type == "dataSource" || setting.type == "table" - ) - - let defaultDatasource - if (dataSourceField) { - const _tables = get(tables) - const filteredTables = _tables.list.filter( - table => table._id != "ta_users" - ) - - const internalTable = filteredTables.find( - table => - table.sourceId === BUDIBASE_INTERNAL_DB_ID && - table.type == DB_TYPE_INTERNAL - ) - - const defaultSourceTable = filteredTables.find( - table => - table.sourceId !== BUDIBASE_INTERNAL_DB_ID && - table.type == DB_TYPE_INTERNAL - ) - - const defaultExternalTable = filteredTables.find( - table => table.type == DB_TYPE_EXTERNAL - ) - - defaultDatasource = - defaultSourceTable || internalTable || defaultExternalTable + // Generate basic component structure + let instance = { + _id: Helpers.uuid(), + _component: definition.component, + _styles: { + normal: {}, + hover: {}, + active: {}, + }, + _instanceName: `New ${definition.friendlyName || definition.name}`, + ...presetProps, } - // Generate default props - let props = { ...presetProps } - settings.forEach(setting => { - if (setting.type === "multifield" && setting.selectAllFields) { - props[setting.key] = Object.keys(defaultDatasource.schema || {}) - } else if (setting.defaultValue !== undefined) { - props[setting.key] = setting.defaultValue - } + // Enrich empty settings + store.actions.components.enrichEmptySettings(instance, { + parent, + screen: get(selectedScreen), + useDefaultValues: true, }) - // Set a default datasource - if (dataSourceField && defaultDatasource) { - props[dataSourceField.key] = { - label: defaultDatasource.name, - tableId: defaultDatasource._id, - type: "table", - } - } - // Add any extra properties the component needs let extras = {} if (definition.hasChildren) { @@ -569,17 +682,8 @@ export const getFrontendStore = () => { extras.step = formSteps.length + 1 extras._instanceName = `Step ${formSteps.length + 1}` } - return { - _id: Helpers.uuid(), - _component: definition.component, - _styles: { - normal: {}, - hover: {}, - active: {}, - }, - _instanceName: `New ${definition.friendlyName || definition.name}`, - ...cloneDeep(props), + ...cloneDeep(instance), ...extras, } }, @@ -587,7 +691,8 @@ export const getFrontendStore = () => { const state = get(store) const componentInstance = store.actions.components.createInstance( componentName, - presetProps + presetProps, + parent ) if (!componentInstance) { return @@ -1123,6 +1228,52 @@ export const getFrontendStore = () => { }) } }, + addParent: async (componentId, parentType) => { + if (!componentId || !parentType) { + return + } + + // Create new parent instance + const newParentDefinition = store.actions.components.createInstance( + parentType, + null, + parent + ) + if (!newParentDefinition) { + return + } + + // Replace component with a version wrapped in a new parent + await store.actions.screens.patch(screen => { + // Get this component definition and parent definition + let definition = findComponent(screen.props, componentId) + let oldParentDefinition = findComponentParent( + screen.props, + componentId + ) + if (!definition || !oldParentDefinition) { + return false + } + + // Replace component with parent + const index = oldParentDefinition._children.findIndex( + component => component._id === componentId + ) + if (index === -1) { + return false + } + oldParentDefinition._children[index] = { + ...newParentDefinition, + _children: [definition], + } + }) + + // Select the new parent + store.update(state => { + state.selectedComponentId = newParentDefinition._id + return state + }) + }, }, links: { save: async (url, title) => { diff --git a/packages/builder/src/components/design/Panel.svelte b/packages/builder/src/components/design/Panel.svelte index 6c8753a99e..dbf42c51a5 100644 --- a/packages/builder/src/components/design/Panel.svelte +++ b/packages/builder/src/components/design/Panel.svelte @@ -3,7 +3,6 @@ export let title export let icon - export let expandable = false export let showAddButton = false export let showBackButton = false export let showCloseButton = false @@ -12,8 +11,8 @@ export let onClickCloseButton export let borderLeft = false export let borderRight = false + export let wide = false - let wide = false $: customHeaderContent = $$slots["panel-header-content"] @@ -28,13 +27,6 @@
{title || ""}
- {#if expandable} - (wide = !wide)} - /> - {/if} {#if showAddButton}
@@ -74,8 +66,8 @@ border-right: var(--border-light); } .panel.wide { - width: 420px; - flex: 0 0 420px; + width: 310px; + flex: 0 0 310px; } .header { flex: 0 0 48px; diff --git a/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte b/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte index 098a8f7ed7..59340d4898 100644 --- a/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte +++ b/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte @@ -27,7 +27,7 @@ : enrichedSchemaFields?.map(field => field.name) $: sanitisedValue = getValidColumns(value, options) $: updateBoundValue(sanitisedValue) - $: enrichedSchemaFields = getFields(Object.values(schema) || [], { + $: enrichedSchemaFields = getFields(Object.values(schema || {}), { allowLinks: true, }) diff --git a/packages/builder/src/components/design/settings/controls/DataProviderSelect.svelte b/packages/builder/src/components/design/settings/controls/DataProviderSelect.svelte index a5b7a08255..83255ec325 100644 --- a/packages/builder/src/components/design/settings/controls/DataProviderSelect.svelte +++ b/packages/builder/src/components/design/settings/controls/DataProviderSelect.svelte @@ -3,23 +3,13 @@ import { makePropSafe } from "@budibase/string-templates" import { currentAsset, store } from "builderStore" import { findComponentPath } from "builderStore/componentUtils" - import { createEventDispatcher, onMount } from "svelte" export let value - const dispatch = createEventDispatcher() const getValue = component => `{{ literal ${makePropSafe(component._id)} }}` $: path = findComponentPath($currentAsset?.props, $store.selectedComponentId) $: providers = path.filter(c => c._component?.endsWith("/dataprovider")) - - // Set initial value to closest data provider - onMount(() => { - const valid = value && providers.find(x => getValue(x) === value) != null - if (!valid && providers.length) { - dispatch("change", getValue(providers[providers.length - 1])) - } - })