Component error state improvements (#10136)
* Tidy logic for creating initial component instances * Add initial implementation of enriching empty settings * Fix regression that prevented custom placeholders from working (#9994) * Tidy up * Add automatic naming of form fields when added * Update missing required setting placeholder * Improve error states and add ability to automatically wrap a component in a required parent type * Fix crash in column editor and rename component placeholder to error state * Select the parent component after adding it when wrapping a component with a missing ancestor * Fix blocks and make fields require forms * Improve empty component placeholder * Lint
This commit is contained in:
parent
bd2269d861
commit
f2b12bcf45
|
@ -42,9 +42,13 @@
|
|||
}
|
||||
|
||||
const getFieldText = (value, options, placeholder) => {
|
||||
// Always use placeholder if no value
|
||||
if (value == null || value === "") {
|
||||
return placeholder !== false ? "Choose an option" : ""
|
||||
// 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"
|
||||
}
|
||||
|
||||
return getFieldAttribute(getOptionLabel, value, options)
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
@ -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]))
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Select
|
||||
|
|
|
@ -1,43 +1,17 @@
|
|||
<script>
|
||||
import { Combobox } from "@budibase/bbui"
|
||||
import {
|
||||
getDatasourceForProvider,
|
||||
getSchemaForDatasource,
|
||||
} from "builderStore/dataBinding"
|
||||
import { currentAsset } from "builderStore"
|
||||
import { findClosestMatchingComponent } from "builderStore/componentUtils"
|
||||
import { getComponentFieldOptions } from "helpers/formFields"
|
||||
|
||||
export let componentInstance
|
||||
export let value
|
||||
export let type
|
||||
|
||||
$: form = findClosestMatchingComponent(
|
||||
$: options = getComponentFieldOptions(
|
||||
$currentAsset?.props,
|
||||
componentInstance._id,
|
||||
component => component._component === "@budibase/standard-components/form"
|
||||
componentInstance?._id,
|
||||
type
|
||||
)
|
||||
$: datasource = getDatasourceForProvider($currentAsset, form)
|
||||
$: schema = getSchemaForDatasource($currentAsset, datasource, {
|
||||
formSchema: true,
|
||||
}).schema
|
||||
$: options = getOptions(schema, type)
|
||||
|
||||
const getOptions = (schema, type) => {
|
||||
let entries = Object.entries(schema ?? {})
|
||||
let types = []
|
||||
if (type === "field/options" || type === "field/longform") {
|
||||
// allow options and longform to be used on string fields as well
|
||||
types = [type, "field/string"]
|
||||
} else {
|
||||
types = [type]
|
||||
}
|
||||
|
||||
types = types.map(type => type.slice(type.indexOf("/") + 1))
|
||||
|
||||
entries = entries.filter(entry => types.includes(entry[1].type))
|
||||
|
||||
return entries.map(entry => entry[0])
|
||||
}
|
||||
</script>
|
||||
|
||||
<Combobox on:change {value} {options} />
|
||||
|
|
|
@ -120,7 +120,7 @@
|
|||
}
|
||||
.property-control.highlighted {
|
||||
background: var(--spectrum-global-color-gray-300);
|
||||
border-color: var(--spectrum-global-color-blue-400);
|
||||
border-color: var(--spectrum-global-color-static-red-600);
|
||||
}
|
||||
.control {
|
||||
position: relative;
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import { findClosestMatchingComponent } from "builderStore/componentUtils"
|
||||
import {
|
||||
getDatasourceForProvider,
|
||||
getSchemaForDatasource,
|
||||
} from "builderStore/dataBinding"
|
||||
|
||||
export const getComponentFieldOptions = (asset, id, type, loose = true) => {
|
||||
const form = findClosestMatchingComponent(
|
||||
asset,
|
||||
id,
|
||||
component => component._component === "@budibase/standard-components/form"
|
||||
)
|
||||
const datasource = getDatasourceForProvider(asset, form)
|
||||
const schema = getSchemaForDatasource(asset, datasource, {
|
||||
formSchema: true,
|
||||
}).schema
|
||||
|
||||
// Get valid types for this field
|
||||
let types = [type]
|
||||
if (loose) {
|
||||
if (type === "field/options" || type === "field/longform") {
|
||||
// Allow options and longform to be used on string fields as well
|
||||
types = [type, "field/string"]
|
||||
}
|
||||
}
|
||||
types = types.map(type => type.slice(type.indexOf("/") + 1))
|
||||
|
||||
// Find fields of valid types
|
||||
return Object.entries(schema || {})
|
||||
.filter(entry => types.includes(entry[1].type))
|
||||
.map(entry => entry[0])
|
||||
}
|
|
@ -220,6 +220,9 @@
|
|||
} else if (type === "drop-new-component") {
|
||||
const { component, parent, index } = data
|
||||
await store.actions.components.create(component, null, parent, index)
|
||||
} else if (type === "add-parent-component") {
|
||||
const { componentId, parentType } = data
|
||||
await store.actions.components.addParent(componentId, parentType)
|
||||
} else {
|
||||
console.warn(`Client sent unknown event type: ${type}`)
|
||||
}
|
||||
|
|
|
@ -394,6 +394,7 @@
|
|||
"description": "A configurable data list that attaches to your backend tables.",
|
||||
"icon": "JourneyData",
|
||||
"illegalChildren": ["section"],
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"hasChildren": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -1384,6 +1385,7 @@
|
|||
"name": "Bar Chart",
|
||||
"description": "Bar chart",
|
||||
"icon": "GraphBarVertical",
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"size": {
|
||||
"width": 600,
|
||||
"height": 400
|
||||
|
@ -1546,6 +1548,7 @@
|
|||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -1699,6 +1702,7 @@
|
|||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -1864,6 +1868,7 @@
|
|||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -1993,6 +1998,7 @@
|
|||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -2122,6 +2128,7 @@
|
|||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -2349,6 +2356,7 @@
|
|||
"name": "Text Field",
|
||||
"icon": "Text",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -2438,6 +2446,7 @@
|
|||
"name": "Number Field",
|
||||
"icon": "123",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -2493,6 +2502,7 @@
|
|||
"name": "Password Field",
|
||||
"icon": "LockClosed",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -2548,6 +2558,7 @@
|
|||
"name": "Options Picker",
|
||||
"icon": "Menu",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -2714,6 +2725,7 @@
|
|||
"name": "Multi-select Picker",
|
||||
"icon": "ViewList",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -2874,6 +2886,7 @@
|
|||
"name": "Checkbox",
|
||||
"icon": "SelectBox",
|
||||
"editable": true,
|
||||
"requiredAncestors": ["form"],
|
||||
"size": {
|
||||
"width": 20,
|
||||
"height": 20
|
||||
|
@ -2952,6 +2965,7 @@
|
|||
"name": "Long Form Field",
|
||||
"icon": "TextAlignLeft",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -3029,6 +3043,7 @@
|
|||
"name": "Date Picker",
|
||||
"icon": "Date",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -3162,6 +3177,7 @@
|
|||
"width": 400,
|
||||
"height": 320
|
||||
},
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "dataProvider",
|
||||
|
@ -3269,6 +3285,7 @@
|
|||
"name": "Attachment",
|
||||
"icon": "Attach",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -3331,6 +3348,7 @@
|
|||
"name": "Relationship Picker",
|
||||
"icon": "TaskList",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -3393,6 +3411,7 @@
|
|||
"icon": "Brackets",
|
||||
"styles": ["size"],
|
||||
"editable": true,
|
||||
"requiredAncestors": ["form"],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 100
|
||||
|
@ -3582,6 +3601,7 @@
|
|||
"name": "Table",
|
||||
"icon": "Table",
|
||||
"illegalChildren": ["section"],
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"hasChildren": true,
|
||||
"showEmptyState": false,
|
||||
"size": {
|
||||
|
@ -3666,6 +3686,7 @@
|
|||
"name": "Date Range",
|
||||
"icon": "Calendar",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"hasChildren": false,
|
||||
"size": {
|
||||
"width": 200,
|
||||
|
@ -3773,6 +3794,7 @@
|
|||
"width": 100,
|
||||
"height": 35
|
||||
},
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "dataProvider",
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
|
||||
// Provide contexts
|
||||
setContext("sdk", SDK)
|
||||
setContext("component", writable({}))
|
||||
setContext("component", writable({ id: null, ancestors: [] }))
|
||||
setContext("context", createContextStore())
|
||||
|
||||
let dataLoaded = false
|
||||
|
|
|
@ -26,19 +26,20 @@
|
|||
} from "stores"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { getActiveConditions, reduceConditionActions } from "utils/conditions"
|
||||
import Placeholder from "components/app/Placeholder.svelte"
|
||||
import EmptyPlaceholder from "components/app/EmptyPlaceholder.svelte"
|
||||
import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte"
|
||||
import ComponentPlaceholder from "components/app/ComponentPlaceholder.svelte"
|
||||
import ComponentErrorState from "components/error-states/ComponentErrorState.svelte"
|
||||
import { BudibasePrefix } from "../stores/components.js"
|
||||
|
||||
export let instance = {}
|
||||
export let isLayout = false
|
||||
export let isScreen = false
|
||||
export let isBlock = false
|
||||
export let parent = null
|
||||
|
||||
// Get parent contexts
|
||||
const context = getContext("context")
|
||||
const insideScreenslot = !!getContext("screenslot")
|
||||
const component = getContext("component")
|
||||
|
||||
// Create component context
|
||||
const store = writable({})
|
||||
|
@ -120,6 +121,12 @@
|
|||
$: showEmptyState = definition?.showEmptyState !== false
|
||||
$: hasMissingRequiredSettings = missingRequiredSettings?.length > 0
|
||||
$: editable = !!definition?.editable && !hasMissingRequiredSettings
|
||||
$: requiredAncestors = definition?.requiredAncestors || []
|
||||
$: missingRequiredAncestors = requiredAncestors.filter(
|
||||
ancestor => !$component.ancestors.includes(`${BudibasePrefix}${ancestor}`)
|
||||
)
|
||||
$: hasMissingRequiredAncestors = missingRequiredAncestors?.length > 0
|
||||
$: errorState = hasMissingRequiredSettings || hasMissingRequiredAncestors
|
||||
|
||||
// Interactive components can be selected, dragged and highlighted inside
|
||||
// the builder preview
|
||||
|
@ -183,6 +190,7 @@
|
|||
custom: customCSS,
|
||||
id,
|
||||
empty: emptyState,
|
||||
selected,
|
||||
interactive,
|
||||
draggable,
|
||||
editable,
|
||||
|
@ -193,7 +201,9 @@
|
|||
name,
|
||||
editing,
|
||||
type: instance._component,
|
||||
missingRequiredSettings,
|
||||
errorState,
|
||||
parent: id,
|
||||
ancestors: [...$component?.ancestors, instance._component],
|
||||
})
|
||||
|
||||
const initialise = (instance, force = false) => {
|
||||
|
@ -482,6 +492,7 @@
|
|||
getDataContext: () => get(context),
|
||||
reload: () => initialise(instance, true),
|
||||
setEphemeralStyles: styles => (ephemeralStyles = styles),
|
||||
state: store,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -509,24 +520,28 @@
|
|||
class:pad
|
||||
class:parent={hasChildren}
|
||||
class:block={isBlock}
|
||||
class:error={errorState}
|
||||
data-id={id}
|
||||
data-name={name}
|
||||
data-icon={icon}
|
||||
data-parent={parent}
|
||||
data-parent={$component.id}
|
||||
>
|
||||
{#if hasMissingRequiredSettings}
|
||||
<ComponentPlaceholder />
|
||||
{#if errorState}
|
||||
<ComponentErrorState
|
||||
{missingRequiredSettings}
|
||||
{missingRequiredAncestors}
|
||||
/>
|
||||
{:else}
|
||||
<svelte:component this={constructor} bind:this={ref} {...initialSettings}>
|
||||
{#if children.length}
|
||||
{#each children as child (child._id)}
|
||||
<svelte:self instance={child} parent={id} />
|
||||
<svelte:self instance={child} />
|
||||
{/each}
|
||||
{:else if emptyState}
|
||||
{#if isScreen}
|
||||
<ScreenPlaceholder />
|
||||
{:else}
|
||||
<Placeholder />
|
||||
<EmptyPlaceholder />
|
||||
{/if}
|
||||
{:else if isBlock}
|
||||
<slot />
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { builderStore } from "stores"
|
||||
|
||||
const component = getContext("component")
|
||||
const { styleable } = getContext("sdk")
|
||||
|
||||
$: requiredSetting = $component.missingRequiredSettings?.[0]
|
||||
</script>
|
||||
|
||||
{#if $builderStore.inBuilder && requiredSetting}
|
||||
<div class="component-placeholder" use:styleable={$component.styles}>
|
||||
<span>
|
||||
Add the <mark>{requiredSetting.label}</mark> setting to start using your component
|
||||
-
|
||||
</span>
|
||||
<span
|
||||
class="spectrum-Link"
|
||||
on:click={() => {
|
||||
builderStore.actions.highlightSetting(requiredSetting.key)
|
||||
}}
|
||||
>
|
||||
Show me
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.component-placeholder {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
font-size: var(--font-size-s);
|
||||
padding: var(--spacing-xs);
|
||||
}
|
||||
.component-placeholder mark {
|
||||
background-color: var(--spectrum-global-color-gray-400);
|
||||
padding: 0 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.component-placeholder .spectrum-Link {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,45 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
|
||||
const component = getContext("component")
|
||||
const { builderStore, componentStore } = getContext("sdk")
|
||||
|
||||
$: definition = componentStore.actions.getComponentDefinition($component.type)
|
||||
</script>
|
||||
|
||||
{#if $builderStore.inBuilder}
|
||||
<div class="component-placeholder">
|
||||
<Icon name="Help" color="var(--spectrum-global-color-blue-600)" />
|
||||
<span
|
||||
class="spectrum-Link"
|
||||
on:click={() => {
|
||||
builderStore.actions.requestAddComponent()
|
||||
}}
|
||||
>
|
||||
Add components inside your {definition?.name || $component.type}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.component-placeholder {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
font-size: var(--font-size-s);
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
/* Common styles for all error states to use */
|
||||
.component-placeholder :global(mark) {
|
||||
background-color: var(--spectrum-global-color-gray-400);
|
||||
padding: 0 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.component-placeholder :global(.spectrum-Link) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,52 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import MissingRequiredSetting from "./MissingRequiredSetting.svelte"
|
||||
import MissingRequiredAncestor from "./MissingRequiredAncestor.svelte"
|
||||
|
||||
export let missingRequiredSettings
|
||||
export let missingRequiredAncestors
|
||||
|
||||
const component = getContext("component")
|
||||
const { styleable, builderStore } = getContext("sdk")
|
||||
|
||||
$: styles = { ...$component.styles, normal: {}, custom: null, empty: true }
|
||||
$: requiredSetting = missingRequiredSettings?.[0]
|
||||
$: requiredAncestor = missingRequiredAncestors?.[0]
|
||||
</script>
|
||||
|
||||
{#if $builderStore.inBuilder}
|
||||
{#if $component.errorState}
|
||||
<div class="component-placeholder" use:styleable={styles}>
|
||||
<Icon name="Alert" color="var(--spectrum-global-color-static-red-600)" />
|
||||
{#if requiredAncestor}
|
||||
<MissingRequiredAncestor {requiredAncestor} />
|
||||
{:else if requiredSetting}
|
||||
<MissingRequiredSetting {requiredSetting} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.component-placeholder {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
font-size: var(--font-size-s);
|
||||
padding: var(--spacing-xs);
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
/* Common styles for all error states to use */
|
||||
.component-placeholder :global(mark) {
|
||||
background-color: var(--spectrum-global-color-gray-400);
|
||||
padding: 0 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.component-placeholder :global(.spectrum-Link) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,41 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { BudibasePrefix } from "stores/components"
|
||||
|
||||
export let requiredAncestor
|
||||
|
||||
const component = getContext("component")
|
||||
const { builderStore, componentStore } = getContext("sdk")
|
||||
|
||||
$: definition = componentStore.actions.getComponentDefinition($component.type)
|
||||
$: fullAncestorType = `${BudibasePrefix}${requiredAncestor}`
|
||||
$: ancestorDefinition =
|
||||
componentStore.actions.getComponentDefinition(fullAncestorType)
|
||||
$: pluralName = getPluralName(definition?.name, $component.type)
|
||||
$: ancestorName = getAncestorName(ancestorDefinition?.name, requiredAncestor)
|
||||
|
||||
const getPluralName = (name, type) => {
|
||||
if (!name) {
|
||||
name = type.replace(BudibasePrefix, "")
|
||||
}
|
||||
return name.endsWith("s") ? `${name}'` : `${name}s`
|
||||
}
|
||||
|
||||
const getAncestorName = name => {
|
||||
return name || requiredAncestor
|
||||
}
|
||||
</script>
|
||||
|
||||
<span>
|
||||
{pluralName} need to be inside a
|
||||
<mark>{ancestorName}</mark>
|
||||
</span>
|
||||
<span>-</span>
|
||||
<span
|
||||
class="spectrum-Link"
|
||||
on:click={() => {
|
||||
builderStore.actions.addParentComponent($component.id, fullAncestorType)
|
||||
}}
|
||||
>
|
||||
Add {ancestorName}
|
||||
</span>
|
|
@ -0,0 +1,20 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
|
||||
export let requiredSetting
|
||||
|
||||
const { builderStore } = getContext("sdk")
|
||||
</script>
|
||||
|
||||
<span>
|
||||
Add the <mark>{requiredSetting.label}</mark> setting to start using your component
|
||||
</span>
|
||||
<span>-</span>
|
||||
<span
|
||||
class="spectrum-Link"
|
||||
on:click={() => {
|
||||
builderStore.actions.highlightSetting(requiredSetting.key)
|
||||
}}
|
||||
>
|
||||
Show me
|
||||
</span>
|
|
@ -12,7 +12,8 @@
|
|||
$: id = dragInfo?.id || id
|
||||
|
||||
// Set ephemeral grid styles on the dragged component
|
||||
$: componentStore.actions.getComponentInstance(id)?.setEphemeralStyles({
|
||||
$: instance = componentStore.actions.getComponentInstance(id)
|
||||
$: $instance?.setEphemeralStyles({
|
||||
...gridStyles,
|
||||
...(gridStyles ? { "z-index": 999 } : null),
|
||||
})
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
let text
|
||||
let icon
|
||||
let insideGrid = false
|
||||
let errorState = false
|
||||
|
||||
$: visibleIndicators = indicators.filter(x => x.visible)
|
||||
$: offset = $builderStore.inBuilder ? 0 : 2
|
||||
|
@ -85,6 +86,7 @@
|
|||
icon = parents[0].dataset.icon
|
||||
}
|
||||
}
|
||||
errorState = parents?.[0]?.classList.contains("error")
|
||||
|
||||
// Batch reads to minimize reflow
|
||||
const scrollX = window.scrollX
|
||||
|
@ -152,10 +154,10 @@
|
|||
text={idx === 0 ? text : null}
|
||||
icon={idx === 0 ? icon : null}
|
||||
showResizeAnchors={allowResizeAnchors && insideGrid}
|
||||
color={errorState ? "var(--spectrum-global-color-static-red-600)" : color}
|
||||
{componentId}
|
||||
{transition}
|
||||
{zIndex}
|
||||
{color}
|
||||
/>
|
||||
{/each}
|
||||
{/key}
|
||||
|
|
|
@ -15,17 +15,22 @@
|
|||
let self
|
||||
let measured = false
|
||||
|
||||
$: id = $builderStore.selectedComponentId
|
||||
$: instance = componentStore.actions.getComponentInstance(id)
|
||||
$: state = $instance?.state
|
||||
$: definition = $componentStore.selectedComponentDefinition
|
||||
$: showBar =
|
||||
definition?.showSettingsBar !== false && !$dndIsDragging && definition
|
||||
definition?.showSettingsBar !== false &&
|
||||
!$dndIsDragging &&
|
||||
definition &&
|
||||
!$state?.errorState
|
||||
$: {
|
||||
if (!showBar) {
|
||||
measured = false
|
||||
}
|
||||
}
|
||||
$: settings = getBarSettings(definition)
|
||||
$: isScreen =
|
||||
$builderStore.selectedComponentId === $builderStore.screen?.props?._id
|
||||
$: isScreen = id === $builderStore.screen?.props?._id
|
||||
|
||||
const getBarSettings = definition => {
|
||||
let allSettings = []
|
||||
|
|
|
@ -109,6 +109,12 @@ const createBuilderStore = () => {
|
|||
// Notify the builder so we can reload component definitions
|
||||
eventStore.actions.dispatchEvent("reload-plugin")
|
||||
},
|
||||
addParentComponent: (componentId, parentType) => {
|
||||
eventStore.actions.dispatchEvent("add-parent-component", {
|
||||
componentId,
|
||||
parentType,
|
||||
})
|
||||
},
|
||||
}
|
||||
return {
|
||||
...store,
|
||||
|
|
|
@ -8,7 +8,7 @@ import Router from "../components/Router.svelte"
|
|||
import * as AppComponents from "../components/app/index.js"
|
||||
import { ScreenslotType } from "../constants.js"
|
||||
|
||||
const budibasePrefix = "@budibase/standard-components/"
|
||||
export const BudibasePrefix = "@budibase/standard-components/"
|
||||
|
||||
const createComponentStore = () => {
|
||||
const store = writable({
|
||||
|
@ -107,12 +107,12 @@ const createComponentStore = () => {
|
|||
|
||||
// Screenslot is an edge case
|
||||
if (type === ScreenslotType) {
|
||||
type = `${budibasePrefix}${type}`
|
||||
type = `${BudibasePrefix}${type}`
|
||||
}
|
||||
|
||||
// Handle built-in components
|
||||
if (type.startsWith(budibasePrefix)) {
|
||||
type = type.replace(budibasePrefix, "")
|
||||
if (type.startsWith(BudibasePrefix)) {
|
||||
type = type.replace(BudibasePrefix, "")
|
||||
return type ? Manifest[type] : null
|
||||
}
|
||||
|
||||
|
@ -130,7 +130,7 @@ const createComponentStore = () => {
|
|||
}
|
||||
|
||||
// Handle budibase components
|
||||
if (type.startsWith(budibasePrefix)) {
|
||||
if (type.startsWith(BudibasePrefix)) {
|
||||
const split = type.split("/")
|
||||
const name = split[split.length - 1]
|
||||
return AppComponents[name]
|
||||
|
@ -145,7 +145,7 @@ const createComponentStore = () => {
|
|||
if (!id) {
|
||||
return null
|
||||
}
|
||||
return get(store).mountedComponents[id]
|
||||
return derived(store, $store => $store.mountedComponents[id])
|
||||
}
|
||||
|
||||
const registerCustomComponent = ({ Component, schema, version }) => {
|
||||
|
|
|
@ -29,9 +29,13 @@ export const styleable = (node, styles = {}) => {
|
|||
|
||||
let baseStyles = {}
|
||||
if (newStyles.empty) {
|
||||
baseStyles.border = "2px dashed var(--spectrum-global-color-gray-400)"
|
||||
baseStyles.padding = "var(--spacing-l)"
|
||||
baseStyles.overflow = "hidden"
|
||||
if (newStyles.selected) {
|
||||
baseStyles.border = "2px solid transparent"
|
||||
} else {
|
||||
baseStyles.border = "2px dashed var(--spectrum-global-color-gray-400)"
|
||||
}
|
||||
}
|
||||
|
||||
const componentId = newStyles.id
|
||||
|
|
Loading…
Reference in New Issue