Merge branch 'master' of github.com:Budibase/budibase into mysql-connector

This commit is contained in:
Martin McKeaveney 2021-01-26 11:10:08 +00:00
commit fcbb27b628
132 changed files with 2819 additions and 4677 deletions

View File

@ -1,4 +1,4 @@
packages/builder/src/userInterface/CurrentItemPreview.svelte
packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte
public
dist
packages/server/builder

View File

@ -103,13 +103,9 @@ The Budibase builder runs in Electron, on Mac, PC and Linux. Follow the steps be
## 🤖 Self-hosting
<p align="center">
<img src="https://i.imgur.com/Z52cEvT.png?1" />
</p>
Budibase wants to make sure anyone can use the tools we develop and we know a lot of people need to be able to host the apps they make on their own systems - that is why we've decided to try and make self hosting as easy as possible!
Currently, you can host your apps using Docker. The documentation for self-hosting can be found [here](https://docs.budibase.com/self-hosting/introduction-to-self-hosting).
Currently, you can host your apps using Docker or Digital Ocean. The documentation for self-hosting can be found [here](https://docs.budibase.com/self-hosting/introduction-to-self-hosting).
## 🎓 Learning Budibase

View File

@ -0,0 +1,209 @@
import { cloneDeep } from "lodash/fp"
import { get } from "svelte/store"
import { backendUiStore, store } from "builderStore"
import { findAllMatchingComponents, findComponentPath } from "./storeUtils"
// Regex to match all instances of template strings
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
/**
* Gets all bindable data context fields and instance fields.
*/
export const getBindableProperties = (rootComponent, componentId) => {
const contextBindings = getContextBindings(rootComponent, componentId)
const componentBindings = getComponentBindings(rootComponent)
return [...contextBindings, ...componentBindings]
}
/**
* Gets all data provider components above a component.
*/
export const getDataProviderComponents = (rootComponent, componentId) => {
if (!rootComponent || !componentId) {
return []
}
// Get the component tree leading up to this component, ignoring the component
// itself
const path = findComponentPath(rootComponent, componentId)
path.pop()
// Filter by only data provider components
return path.filter(component => {
const def = store.actions.components.getDefinition(component._component)
return def?.dataProvider
})
}
/**
* Gets a datasource object for a certain data provider component
*/
export const getDatasourceForProvider = component => {
const def = store.actions.components.getDefinition(component?._component)
if (!def) {
return null
}
// Extract datasource from component instance
const datasourceSetting = def.settings.find(setting => {
return setting.key === def.datasourceSetting
})
if (!datasourceSetting) {
return null
}
// There are different types of setting which can be a datasource, for
// example an actual datasource object, or a table ID string.
// Convert the datasource setting into a proper datasource object so that
// we can use it properly
if (datasourceSetting.type === "datasource") {
return component[datasourceSetting?.key]
} else if (datasourceSetting.type === "table") {
return {
tableId: component[datasourceSetting?.key],
type: "table",
}
}
return null
}
/**
* Gets all bindable data contexts. These are fields of schemas of data contexts
* provided by data provider components, such as lists or row detail components.
*/
export const getContextBindings = (rootComponent, componentId) => {
// Extract any components which provide data contexts
const dataProviders = getDataProviderComponents(rootComponent, componentId)
let contextBindings = []
dataProviders.forEach(component => {
const datasource = getDatasourceForProvider(component)
if (!datasource) {
return
}
// Get schema and add _id and _rev fields for certain types
let { schema, table } = getSchemaForDatasource(datasource)
if (!schema || !table) {
return
}
if (datasource.type === "table" || datasource.type === "link") {
schema["_id"] = { type: "string" }
schema["_rev"] = { type: "string " }
}
const keys = Object.keys(schema).sort()
// Create bindable properties for each schema field
keys.forEach(key => {
const fieldSchema = schema[key]
// Replace certain bindings with a new property to help display components
let runtimeBoundKey = key
if (fieldSchema.type === "link") {
runtimeBoundKey = `${key}_count`
} else if (fieldSchema.type === "attachment") {
runtimeBoundKey = `${key}_first`
}
contextBindings.push({
type: "context",
runtimeBinding: `${component._id}.${runtimeBoundKey}`,
readableBinding: `${component._instanceName}.${table.name}.${key}`,
fieldSchema,
providerId: component._id,
tableId: datasource.tableId,
field: key,
})
})
})
return contextBindings
}
/**
* Gets all bindable components. These are form components which allow their
* values to be bound to.
*/
export const getComponentBindings = rootComponent => {
if (!rootComponent) {
return []
}
const componentSelector = component => {
const type = component._component
const definition = store.actions.components.getDefinition(type)
return definition?.bindable
}
const components = findAllMatchingComponents(rootComponent, componentSelector)
return components.map(component => {
return {
type: "instance",
providerId: component._id,
runtimeBinding: `${component._id}`,
readableBinding: `${component._instanceName}`,
}
})
}
/**
* Gets a schema for a datasource object.
*/
export const getSchemaForDatasource = datasource => {
let schema, table
if (datasource) {
const { type } = datasource
if (type === "query") {
const queries = get(backendUiStore).queries
table = queries.find(query => query._id === datasource._id)
} else {
const tables = get(backendUiStore).tables
table = tables.find(table => table._id === datasource.tableId)
}
if (table) {
if (type === "view") {
schema = cloneDeep(table.views?.[datasource.name]?.schema)
} else {
schema = cloneDeep(table.schema)
}
}
}
return { schema, table }
}
/**
* Converts a readable data binding into a runtime data binding
*/
export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
if (typeof textWithBindings !== "string") {
return textWithBindings
}
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_TEMPLATE) || []
let result = textWithBindings
boundValues.forEach(boundValue => {
const binding = bindableProperties.find(({ readableBinding }) => {
return boundValue === `{{ ${readableBinding} }}`
})
if (binding) {
result = result.replace(boundValue, `{{ ${binding.runtimeBinding} }}`)
}
})
return result
}
/**
* Converts a runtime data binding into a readable data binding
*/
export function runtimeToReadableBinding(bindableProperties, textWithBindings) {
if (typeof textWithBindings !== "string") {
return textWithBindings
}
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_TEMPLATE) || []
let result = textWithBindings
boundValues.forEach(boundValue => {
const binding = bindableProperties.find(({ runtimeBinding }) => {
return boundValue === `{{ ${runtimeBinding} }}`
})
// Show invalid bindings as invalid rather than a long ID
result = result.replace(
boundValue,
`{{ ${binding?.readableBinding ?? "Invalid binding"} }}`
)
})
return result
}

View File

@ -1,205 +0,0 @@
import { cloneDeep, difference } from "lodash/fp"
/**
* parameter for fetchBindableProperties function
* @typedef {Object} fetchBindablePropertiesParameter
* @property {string} componentInstanceId - an _id of a component that has been added to a screen, which you want to fetch bindable props for
* @propperty {Object} screen - current screen - where componentInstanceId lives
* @property {Object} components - dictionary of component definitions
* @property {Array} tables - array of all tables
*/
/**
*
* @typedef {Object} BindableProperty
* @property {string} type - either "instance" (binding to a component instance) or "context" (binding to data in context e.g. List Item)
* @property {Object} instance - relevant component instance. If "context" type, this instance is the component that provides the context... e.g. the List
* @property {string} runtimeBinding - a binding string that is a) saved against the string, and b) used at runtime to read/write the value
* @property {string} readableBinding - a binding string that is displayed to the user, in the builder
*/
/**
* Generates all allowed bindings from within any particular component instance
* @param {fetchBindablePropertiesParameter} param
* @returns {Array.<BindableProperty>}
*/
export default function({
componentInstanceId,
screen,
components,
tables,
queries,
}) {
const result = walk({
// cloning so we are free to mutate props (e.g. by adding _contexts)
instance: cloneDeep(screen.props),
targetId: componentInstanceId,
components,
tables,
queries,
})
return [
...result.bindableInstances
.filter(isInstanceInSharedContext(result))
.map(componentInstanceToBindable),
...(result.target?._contexts.map(contextToBindables(tables)).flat() ?? []),
...(result.target?._contexts
.map(context => queriesToBindables(queries, context))
.flat() ?? []),
]
}
const isInstanceInSharedContext = walkResult => i =>
// should cover
// - neither are in any context
// - both in same context
// - instance is in ancestor context of target
i.instance._contexts.length <= walkResult.target._contexts.length &&
difference(i.instance._contexts, walkResult.target._contexts).length === 0
// turns a component instance prop into binding expressions
// used by the UI
const componentInstanceToBindable = i => {
return {
type: "instance",
instance: i.instance,
// how the binding expression persists, and is used in the app at runtime
runtimeBinding: `${i.instance._id}`,
// how the binding exressions looks to the user of the builder
readableBinding: `${i.instance._instanceName}`,
}
}
const queriesToBindables = (queries, context) => {
let queryId = context.table._id
const query = queries.find(query => query._id === queryId)
let schema = query?.schema
// Avoid crashing whenever no data source has been selected
if (!schema) {
return []
}
const queryBindings = Object.entries(schema).map(([key, value]) => ({
type: "context",
fieldSchema: value,
instance: context.instance,
// how the binding expression persists, and is used in the app at runtime
runtimeBinding: `${context.instance._id}.${key}`,
// how the binding expressions looks to the user of the builder
readableBinding: `${context.instance._instanceName}.${query.name}.${key}`,
// table / view info
table: context.table,
}))
return queryBindings
}
const contextToBindables = tables => context => {
let tableId = context.table?.tableId ?? context.table
const table = tables.find(table => table._id === tableId || context.table._id)
let schema =
context.table?.type === "view"
? table?.views?.[context.table.name]?.schema
: table?.schema
// Avoid crashing whenever no data source has been selected
if (!schema) {
return []
}
const newBindable = ([key, fieldSchema]) => {
// Replace certain bindings with a new property to help display components
let runtimeBoundKey = key
if (fieldSchema.type === "link") {
runtimeBoundKey = `${key}_count`
} else if (fieldSchema.type === "attachment") {
runtimeBoundKey = `${key}_first`
}
return {
type: "context",
fieldSchema,
instance: context.instance,
// how the binding expression persists, and is used in the app at runtime
runtimeBinding: `${context.instance._id}.${runtimeBoundKey}`,
// how the binding expressions looks to the user of the builder
readableBinding: `${context.instance._instanceName}.${table.name}.${key}`,
// table / view info
table: context.table,
}
}
const stringType = { type: "string" }
return (
Object.entries(schema)
.map(newBindable)
// add _id and _rev fields - not part of schema, but always valid
.concat([
newBindable(["_id", stringType]),
newBindable(["_rev", stringType]),
])
)
}
const walk = ({ instance, targetId, components, tables, result }) => {
if (!result) {
result = {
target: null,
bindableInstances: [],
allContexts: [],
currentContexts: [],
}
}
if (!instance._contexts) instance._contexts = []
// "component" is the component definition (object in component.json)
const component = components[instance._component]
if (instance._id === targetId) {
// found it
result.target = instance
} else {
if (component && component.bindable) {
// pushing all components in here initially
// but this will not be correct, as some of
// these components will be in another context
// but we dont know this until the end of the walk
// so we will filter in another method
result.bindableInstances.push({
instance,
prop: component.bindable,
})
}
}
// a component that provides context to it's children
const contextualInstance =
component && component.context && instance[component.context]
if (contextualInstance) {
// add to currentContexts (ancestory of context)
// before walking children
const table = instance[component.context]
result.currentContexts.push({ instance, table })
}
const currentContexts = [...result.currentContexts]
for (let child of instance._children || []) {
// attaching _contexts of components, for easy comparison later
// these have been deep cloned above, so shouldn't modify the
// original component instances
child._contexts = currentContexts
walk({ instance: child, targetId, components, tables, result })
}
if (contextualInstance) {
// child walk done, remove from currentContexts
result.currentContexts.pop()
}
return result
}

View File

@ -1,48 +0,0 @@
import { walkProps } from "./storeUtils"
import { get_capitalised_name } from "../helpers"
import { get } from "svelte/store"
import { allScreens } from "builderStore"
import { FrontendTypes } from "../constants"
import { currentAsset } from "."
export default function(component, state) {
const capitalised = get_capitalised_name(
component.name || component._component
)
const matchingComponents = []
const findMatches = props => {
walkProps(props, c => {
const thisInstanceName = get_capitalised_name(c._instanceName)
if ((thisInstanceName || "").startsWith(capitalised)) {
matchingComponents.push(thisInstanceName)
}
})
}
// check layouts first
for (let layout of state.layouts) {
findMatches(layout.props)
}
// if viewing screen, check current screen for duplicate
if (state.currentFrontEndType === FrontendTypes.SCREEN) {
findMatches(get(currentAsset).props)
} else {
// viewing a layout - need to find against all screens
for (let screen of get(allScreens)) {
findMatches(screen.props)
}
}
let index = 1
let name
while (!name) {
const tryName = `${capitalised || "Copy"} ${index}`
if (!matchingComponents.includes(tryName)) name = tryName
index++
}
return name
}

View File

@ -7,7 +7,7 @@ import { getThemeStore } from "./store/theme"
import { derived, writable } from "svelte/store"
import analytics from "analytics"
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
import { makePropsSafe } from "components/userInterface/assetParsing/createProps"
import { findComponent } from "./storeUtils"
export const store = getFrontendStore()
export const backendUiStore = getBackendUiStore()
@ -28,31 +28,10 @@ export const currentAsset = derived(store, $store => {
export const selectedComponent = derived(
[store, currentAsset],
([$store, $currentAsset]) => {
if (!$currentAsset || !$store.selectedComponentId) return null
function traverse(node, callback) {
if (node._id === $store.selectedComponentId) return callback(node)
if (node._children) {
node._children.forEach(child => traverse(child, callback))
}
if (node.props) {
traverse(node.props, callback)
}
if (!$currentAsset || !$store.selectedComponentId) {
return null
}
let component
traverse($currentAsset, found => {
const componentIdentifier = found._component ?? found.props._component
const componentDef = componentIdentifier.startsWith("##")
? found
: $store.components[componentIdentifier]
component = makePropsSafe(componentDef, found)
})
return component
return findComponent($currentAsset.props, $store.selectedComponentId)
}
)

View File

@ -21,12 +21,9 @@ export const fetchComponentLibDefinitions = async appId => {
*/
export const fetchComponentLibModules = async application => {
const allLibraries = {}
for (let libraryName of application.componentLibraries) {
const LIBRARY_URL = `/${application._id}/componentlibrary?library=${libraryName}`
const libraryModule = await import(LIBRARY_URL)
allLibraries[libraryName] = libraryModule
allLibraries[libraryName] = await import(LIBRARY_URL)
}
return allLibraries
}

View File

@ -1,39 +0,0 @@
export const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
// Find all instances of template strings
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_TEMPLATE)
let result = textWithBindings
// Replace readableBindings with runtimeBindings
boundValues &&
boundValues.forEach(boundValue => {
const binding = bindableProperties.find(({ readableBinding }) => {
return boundValue === `{{ ${readableBinding} }}`
})
if (binding) {
result = result.replace(boundValue, `{{ ${binding.runtimeBinding} }}`)
}
})
return result
}
export function runtimeToReadableBinding(bindableProperties, textWithBindings) {
let temp = textWithBindings
const boundValues =
(typeof textWithBindings === "string" &&
textWithBindings.match(CAPTURE_VAR_INSIDE_TEMPLATE)) ||
[]
// Replace runtimeBindings with readableBindings:
boundValues.forEach(v => {
const binding = bindableProperties.find(({ runtimeBinding }) => {
return v === `{{ ${runtimeBinding} }}`
})
if (binding) {
temp = temp.replace(v, `{{ ${binding.readableBinding} }}`)
}
})
return temp
}

View File

@ -1,9 +1,5 @@
import { get, writable } from "svelte/store"
import { cloneDeep } from "lodash/fp"
import {
createProps,
getBuiltin,
} from "components/userInterface/assetParsing/createProps"
import {
allScreens,
backendUiStore,
@ -15,15 +11,10 @@ import {
} from "builderStore"
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
import api from "../api"
import { FrontendTypes } from "../../constants"
import getNewComponentName from "../getNewComponentName"
import { FrontendTypes } from "constants"
import analytics from "analytics"
import {
findChildComponentType,
generateNewIdsForComponent,
getComponentDefinition,
findParent,
} from "../storeUtils"
import { findComponentType, findComponentParent } from "../storeUtils"
import { uuid } from "../uuid"
const INITIAL_FRONTEND_STATE = {
apps: [],
@ -50,37 +41,27 @@ export const getFrontendStore = () => {
store.actions = {
initialise: async pkg => {
const { layouts, screens, application } = pkg
store.update(state => {
state.appId = application._id
return state
})
const components = await fetchComponentLibDefinitions(pkg.application._id)
const components = await fetchComponentLibDefinitions(application._id)
store.update(state => ({
...state,
libraries: pkg.application.componentLibraries,
libraries: application.componentLibraries,
components,
name: pkg.application.name,
url: pkg.application.url,
description: pkg.application.description,
appId: pkg.application._id,
name: application.name,
description: application.description,
appId: application._id,
url: application.url,
layouts,
screens,
hasAppPackage: true,
builtins: [getBuiltin("##builtin/screenslot")],
appInstance: pkg.application.instance,
appInstance: application.instance,
}))
await hostingStore.actions.fetch()
await backendUiStore.actions.database.select(pkg.application.instance)
await backendUiStore.actions.database.select(application.instance)
},
routing: {
fetch: async () => {
const response = await api.get("/api/routing")
const json = await response.json()
store.update(state => {
state.routes = json.routes
return state
@ -243,128 +224,231 @@ export const getFrontendStore = () => {
},
components: {
select: component => {
if (!component) {
return
}
// If this is the root component, select the asset instead
const asset = get(currentAsset)
const parent = findComponentParent(asset.props, component._id)
if (parent == null) {
const state = get(store)
const isLayout = state.currentFrontEndType === FrontendTypes.LAYOUT
if (isLayout) {
store.actions.layouts.select(asset._id)
} else {
store.actions.screens.select(asset._id)
}
return
}
// Otherwise select the component
store.update(state => {
state.selectedComponentId = component._id
state.currentView = "component"
return state
})
},
create: (componentToAdd, presetProps) => {
const selectedAsset = get(currentAsset)
getDefinition: componentName => {
if (!componentName) {
return null
}
if (!componentName.startsWith("@budibase")) {
componentName = `@budibase/standard-components/${componentName}`
}
return get(store).components[componentName]
},
createInstance: (componentName, presetProps) => {
const definition = store.actions.components.getDefinition(componentName)
if (!definition) {
return null
}
store.update(state => {
function findSlot(component_array) {
if (!component_array) {
return false
// Generate default props
let props = { ...presetProps }
if (definition.settings) {
definition.settings.forEach(setting => {
if (setting.defaultValue !== undefined) {
props[setting.key] = setting.defaultValue
}
for (let component of component_array) {
if (component._component === "##builtin/screenslot") {
return true
}
if (component._children) findSlot(component)
}
return false
}
if (
componentToAdd.startsWith("##") &&
findSlot(selectedAsset?.props._children)
) {
return state
}
const component = getComponentDefinition(state, componentToAdd)
const instanceId = get(backendUiStore).selectedDatabase._id
const instanceName = getNewComponentName(component, state)
const newComponent = createProps(component, {
...presetProps,
_instanceId: instanceId,
_instanceName: instanceName,
})
}
const selected = get(selectedComponent)
// Add any extra properties the component needs
let extras = {}
if (definition.hasChildren) {
extras._children = []
}
const currentComponentDefinition =
state.components[selected._component]
return {
_id: uuid(),
_component: definition.component,
_styles: { normal: {}, hover: {}, active: {} },
_instanceName: `New ${definition.name}`,
...cloneDeep(props),
...extras,
}
},
create: (componentName, presetProps) => {
const selected = get(selectedComponent)
const asset = get(currentAsset)
const state = get(store)
const allowsChildren = currentComponentDefinition.children
// Only allow one screen slot, and in the layout
if (componentName.endsWith("screenslot")) {
const isLayout = state.currentFrontEndType === FrontendTypes.LAYOUT
const slot = findComponentType(asset.props, componentName)
if (!isLayout || slot != null) {
return
}
}
// Determine where to put the new component.
let targetParent
if (allowsChildren) {
// Child of the selected component
targetParent = selected
// Create new component
const componentInstance = store.actions.components.createInstance(
componentName,
presetProps
)
if (!componentInstance) {
return
}
// Find parent node to attach this component to
let parentComponent
if (!asset) {
return
}
if (selected) {
// Use current screen or layout as parent if no component is selected
const definition = store.actions.components.getDefinition(
selected._component
)
if (definition?.hasChildren) {
// Use selected component if it allows children
parentComponent = selected
} else {
// Sibling of selected component
targetParent = findParent(selectedAsset.props, selected)
// Otherwise we need to use the parent of this component
parentComponent = findComponentParent(asset.props, selected._id)
}
} else {
// Use screen or layout if no component is selected
parentComponent = asset.props
}
// Don't continue if there's no parent
if (!targetParent) return state
// Push the new component
targetParent._children.push(newComponent.props)
store.actions.preview.saveSelected()
// Attach component
if (!parentComponent) {
return
}
if (!parentComponent._children) {
parentComponent._children = []
}
parentComponent._children.push(componentInstance)
// Save components and update UI
store.actions.preview.saveSelected()
store.update(state => {
state.currentView = "component"
state.selectedComponentId = newComponent.props._id
analytics.captureEvent("Added Component", {
name: newComponent.props._component,
})
state.selectedComponentId = componentInstance._id
return state
})
// Log event
analytics.captureEvent("Added Component", {
name: componentInstance._component,
})
return componentInstance
},
delete: component => {
if (!component) {
return
}
const asset = get(currentAsset)
if (!asset) {
return
}
const parent = findComponentParent(asset.props, component._id)
if (parent) {
parent._children = parent._children.filter(
child => child._id !== component._id
)
store.actions.components.select(parent)
}
store.actions.preview.saveSelected()
},
copy: (component, cut = false) => {
const selectedAsset = get(currentAsset)
if (!selectedAsset) {
return null
}
// Update store with copied component
store.update(state => {
state.componentToPaste = cloneDeep(component)
state.componentToPaste.isCut = cut
if (cut) {
const parent = findParent(selectedAsset.props, component._id)
return state
})
// Remove the component from its parent if we're cutting
if (cut) {
const parent = findComponentParent(selectedAsset.props, component._id)
if (parent) {
parent._children = parent._children.filter(
child => child._id !== component._id
)
store.actions.components.select(parent)
}
return state
})
}
},
paste: async (targetComponent, mode) => {
const selectedAsset = get(currentAsset)
let promises = []
store.update(state => {
if (!state.componentToPaste) return state
const componentToPaste = cloneDeep(state.componentToPaste)
// retain the same ids as things may be referencing this component
if (componentToPaste.isCut) {
// in case we paste a second time
state.componentToPaste.isCut = false
} else {
generateNewIdsForComponent(componentToPaste, state)
}
delete componentToPaste.isCut
if (mode === "inside") {
targetComponent._children.push(componentToPaste)
// Stop if we have nothing to paste
if (!state.componentToPaste) {
return state
}
const parent = findParent(selectedAsset.props, targetComponent)
// Clone the component to paste
// Retain the same ID if cutting as things may be referencing this component
const cut = state.componentToPaste.isCut
delete state.componentToPaste.isCut
let componentToPaste = cloneDeep(state.componentToPaste)
if (cut) {
state.componentToPaste = null
} else {
componentToPaste._id = uuid()
}
const targetIndex = parent._children.indexOf(targetComponent)
const index = mode === "above" ? targetIndex : targetIndex + 1
parent._children.splice(index, 0, cloneDeep(componentToPaste))
if (mode === "inside") {
// Paste inside target component if chosen
if (!targetComponent._children) {
targetComponent._children = []
}
targetComponent._children.push(componentToPaste)
} else {
// Otherwise find the parent so we can paste in the correct order
// in the parents child components
const selectedAsset = get(currentAsset)
if (!selectedAsset) {
return state
}
const parent = findComponentParent(
selectedAsset.props,
targetComponent._id
)
if (!parent) {
return state
}
// Insert the component in the correct position
const targetIndex = parent._children.indexOf(targetComponent)
const index = mode === "above" ? targetIndex : targetIndex + 1
parent._children.splice(index, 0, cloneDeep(componentToPaste))
}
// Save and select the new component
promises.push(store.actions.preview.saveSelected())
store.actions.components.select(componentToPaste)
return state
})
await Promise.all(promises)
@ -389,90 +473,56 @@ export const getFrontendStore = () => {
await store.actions.preview.saveSelected()
},
updateProp: (name, value) => {
let component = get(selectedComponent)
if (!name || !component) {
return
}
component[name] = value
store.update(state => {
let current_component = get(selectedComponent)
current_component[name] = value
state.selectedComponentId = current_component._id
store.actions.preview.saveSelected()
state.selectedComponentId = component._id
return state
})
},
findRoute: component => {
// Gets all the components to needed to construct a path.
const selectedAsset = get(currentAsset)
let pathComponents = []
let parent = component
let root = false
while (!root) {
parent = findParent(selectedAsset.props, parent)
if (!parent) {
root = true
} else {
pathComponents.push(parent)
}
}
// Remove root entry since it's the screen or layout.
// Reverse array since we need the correct order of the IDs
const reversedComponents = pathComponents.reverse().slice(1)
// Add component
const allComponents = [...reversedComponents, component]
// Map IDs
const IdList = allComponents.map(c => c._id)
// Construct ID Path:
return IdList.join("/")
store.actions.preview.saveSelected()
},
links: {
save: async (url, title) => {
let promises = []
const layout = get(mainLayout)
store.update(state => {
// Try to extract a nav component from the master layout
const nav = findChildComponentType(
layout,
"@budibase/standard-components/navigation"
)
if (nav) {
let newLink
if (!layout) {
return
}
// Clone an existing link if one exists
if (nav._children && nav._children.length) {
// Clone existing link style
newLink = cloneDeep(nav._children[0])
// Find a nav bar in the main layout
const nav = findComponentType(
layout.props,
"@budibase/standard-components/navigation"
)
if (!nav) {
return
}
// Manipulate IDs to ensure uniqueness
generateNewIdsForComponent(newLink, state, false)
let newLink
if (nav._children && nav._children.length) {
// Clone an existing link if one exists
newLink = cloneDeep(nav._children[0])
// Set our new props
newLink._instanceName = `${title} Link`
newLink.url = url
newLink.text = title
} else {
// Otherwise create vanilla new link
const component = getComponentDefinition(
state,
"@budibase/standard-components/link"
)
const instanceId = get(backendUiStore).selectedDatabase._id
newLink = createProps(component, {
url,
text: title,
_instanceName: `${title} Link`,
_instanceId: instanceId,
}).props
}
// Save layout
nav._children = [...nav._children, newLink]
promises.push(store.actions.layouts.save(layout))
// Set our new props
newLink._id = uuid()
newLink._instanceName = `${title} Link`
newLink.url = url
newLink.text = title
} else {
// Otherwise create vanilla new link
newLink = {
...store.actions.components.createInstance("link"),
url,
text: title,
_instanceName: `${title} Link`,
}
return state
})
await Promise.all(promises)
}
// Save layout
nav._children = [...nav._children, newLink]
await store.actions.layouts.save(layout)
},
},
},

View File

@ -4,8 +4,6 @@ import rowListScreen from "./rowListScreen"
import emptyNewRowScreen from "./emptyNewRowScreen"
import createFromScratchScreen from "./createFromScratchScreen"
import emptyRowDetailScreen from "./emptyRowDetailScreen"
import { generateNewIdsForComponent } from "../../storeUtils"
import { uuid } from "builderStore/uuid"
const allTemplates = tables => [
createFromScratchScreen,
@ -16,13 +14,9 @@ const allTemplates = tables => [
emptyRowDetailScreen,
]
// allows us to apply common behaviour to all create() functions
// Allows us to apply common behaviour to all create() functions
const createTemplateOverride = (frontendState, create) => () => {
const screen = create()
for (let component of screen.props._children) {
generateNewIdsForComponent(component, frontendState, false)
}
screen.props._id = uuid()
screen.name = screen.props._id
screen.routing.route = screen.routing.route.toLowerCase()
return screen

View File

@ -21,26 +21,29 @@ export default function(tables) {
export const newRowUrl = table => sanitizeUrl(`/${table.name}/new/row`)
export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE"
function generateTitleContainer(table) {
return makeTitleContainer("New Row").addChild(makeSaveButton(table))
function generateTitleContainer(table, providerId) {
return makeTitleContainer("New Row").addChild(
makeSaveButton(table, providerId)
)
}
const createScreen = table => {
const dataform = new Component(
"@budibase/standard-components/dataformwide"
).instanceName("Form")
const container = makeMainContainer()
.addChild(makeBreadcrumbContainer(table.name, "New"))
.addChild(generateTitleContainer(table))
.addChild(dataform)
return new Screen()
const screen = new Screen()
.component("@budibase/standard-components/newrow")
.table(table._id)
.route(newRowUrl(table))
.instanceName(`${table.name} - New`)
.name("")
.addChild(container)
.json()
const dataform = new Component(
"@budibase/standard-components/dataformwide"
).instanceName("Form")
const providerId = screen._json.props._id
const container = makeMainContainer()
.addChild(makeBreadcrumbContainer(table.name, "New"))
.addChild(generateTitleContainer(table, providerId))
.addChild(dataform)
return screen.addChild(container).json()
}

View File

@ -25,9 +25,9 @@ export default function(tables) {
export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE"
export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`)
function generateTitleContainer(table, title) {
function generateTitleContainer(table, title, providerId) {
// have to override style for this, its missing margin
const saveButton = makeSaveButton(table).normalStyle({
const saveButton = makeSaveButton(table, providerId).normalStyle({
background: "#000000",
"border-width": "0",
"border-style": "None",
@ -60,8 +60,8 @@ function generateTitleContainer(table, title) {
onClick: [
{
parameters: {
rowId: "{{ data._id }}",
revId: "{{ data._rev }}",
rowId: `{{ ${providerId}._id }}`,
revId: `{{ ${providerId}._rev }}`,
tableId: table._id,
},
"##eventHandlerType": "Delete Row",
@ -82,21 +82,22 @@ function generateTitleContainer(table, title) {
}
const createScreen = (table, heading) => {
const dataform = new Component(
"@budibase/standard-components/dataformwide"
).instanceName("Form")
const container = makeMainContainer()
.addChild(makeBreadcrumbContainer(table.name, heading || "Edit"))
.addChild(generateTitleContainer(table, heading || "Edit Row"))
.addChild(dataform)
return new Screen()
const screen = new Screen()
.component("@budibase/standard-components/rowdetail")
.table(table._id)
.instanceName(`${table.name} - Detail`)
.route(rowDetailUrl(table))
.name("")
.addChild(container)
.json()
const dataform = new Component(
"@budibase/standard-components/dataformwide"
).instanceName("Form")
const providerId = screen._json.props._id
const container = makeMainContainer()
.addChild(makeBreadcrumbContainer(table.name, heading || "Edit"))
.addChild(generateTitleContainer(table, heading || "Edit Row", providerId))
.addChild(dataform)
return screen.addChild(container).json()
}

View File

@ -1,4 +1,4 @@
import { v4 } from "uuid"
import { uuid } from "builderStore/uuid"
import { BaseStructure } from "./BaseStructure"
export class Component extends BaseStructure {
@ -6,7 +6,7 @@ export class Component extends BaseStructure {
super(false)
this._children = []
this._json = {
_id: v4(),
_id: uuid(),
_component: name,
_styles: {
normal: {},

View File

@ -1,4 +1,5 @@
import { BaseStructure } from "./BaseStructure"
import { uuid } from "builderStore/uuid"
export class Screen extends BaseStructure {
constructor() {
@ -6,7 +7,7 @@ export class Screen extends BaseStructure {
this._json = {
layoutId: "layout_private_master",
props: {
_id: "",
_id: uuid(),
_component: "",
_styles: {
normal: {},

View File

@ -78,7 +78,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
.addChild(identifierText)
}
export function makeSaveButton(table) {
export function makeSaveButton(table, providerId) {
return new Component("@budibase/standard-components/button")
.normalStyle({
background: "#000000",
@ -100,8 +100,7 @@ export function makeSaveButton(table) {
onClick: [
{
parameters: {
contextPath: "data",
tableId: table._id,
providerId,
},
"##eventHandlerType": "Save Row",
},

View File

@ -1,80 +1,105 @@
import { getBuiltin } from "components/userInterface/assetParsing/createProps"
import { uuid } from "./uuid"
import getNewComponentName from "./getNewComponentName"
/**
* Recursively searches for a specific component ID
*/
export const findComponent = (rootComponent, id) => {
return searchComponentTree(rootComponent, comp => comp._id === id)
}
/**
* Find the parent component of the passed in child.
* @param {Object} rootProps - props to search for the parent in
* @param {String|Object} child - id of the child or the child itself to find the parent of
* Recursively searches for a specific component type
*/
export const findParent = (rootProps, child) => {
let parent
walkProps(rootProps, (props, breakWalk) => {
if (
props._children &&
(props._children.includes(child) ||
props._children.some(c => c._id === child))
) {
parent = props
breakWalk()
}
})
return parent
export const findComponentType = (rootComponent, type) => {
return searchComponentTree(rootComponent, comp => comp._component === type)
}
export const walkProps = (props, action, cancelToken = null) => {
cancelToken = cancelToken || { cancelled: false }
action(props, () => {
cancelToken.cancelled = true
})
if (props._children) {
for (let child of props._children) {
if (cancelToken.cancelled) return
walkProps(child, action, cancelToken)
}
}
}
export const generateNewIdsForComponent = (
component,
state,
changeName = true
) =>
walkProps(component, prop => {
prop._id = uuid()
if (changeName) prop._instanceName = getNewComponentName(prop, state)
})
export const getComponentDefinition = (state, name) =>
name.startsWith("##") ? getBuiltin(name) : state.components[name]
export const findChildComponentType = (node, typeToFind) => {
// Stop recursion if invalid props
if (!node || !typeToFind) {
/**
* Recursively searches for the parent component of a specific component ID
*/
export const findComponentParent = (rootComponent, id, parentComponent) => {
if (!rootComponent || !id) {
return null
}
// Stop recursion if this element matches
if (node._component === typeToFind) {
return node
if (rootComponent._id === id) {
return parentComponent
}
// Otherwise check if any children match
// Stop recursion if no valid children to process
const children = node._children || (node.props && node.props._children)
if (!children || !children.length) {
if (!rootComponent._children) {
return null
}
// Recurse and check each child component
for (let child of children) {
const childResult = findChildComponentType(child, typeToFind)
for (const child of rootComponent._children) {
const childResult = findComponentParent(child, id, rootComponent)
if (childResult) {
return childResult
}
}
return null
}
/**
* Recursively searches for a specific component ID and records the component
* path to this component
*/
export const findComponentPath = (rootComponent, id, path = []) => {
if (!rootComponent || !id) {
return []
}
if (rootComponent._id === id) {
return [...path, rootComponent]
}
if (!rootComponent._children) {
return []
}
for (const child of rootComponent._children) {
const newPath = [...path, rootComponent]
const childResult = findComponentPath(child, id, newPath)
if (childResult?.length) {
return childResult
}
}
return []
}
/**
* Recurses through the component tree and finds all components of a certain
* type.
*/
export const findAllMatchingComponents = (rootComponent, selector) => {
if (!rootComponent || !selector) {
return []
}
let components = []
if (rootComponent._children) {
rootComponent._children.forEach(child => {
components = [
...components,
...findAllMatchingComponents(child, selector),
]
})
}
if (selector(rootComponent)) {
components.push(rootComponent)
}
return components.reverse()
}
/**
* Recurses through a component tree evaluating a matching function against
* components until a match is found
*/
const searchComponentTree = (rootComponent, matchComponent) => {
if (!rootComponent || !matchComponent) {
return null
}
if (matchComponent(rootComponent)) {
return rootComponent
}
if (!rootComponent._children) {
return null
}
for (const child of rootComponent._children) {
const childResult = searchComponentTree(child, matchComponent)
if (childResult) {
return childResult
}
}
// If we reach here then no children were valid
return null
}

View File

@ -5,7 +5,7 @@
import { Button, Input, Select, Label } from "@budibase/bbui"
import { automationStore } from "builderStore"
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
import BindableInput from "components/userInterface/BindableInput.svelte"
import BindableInput from "../../common/BindableInput.svelte"
export let block
export let webhookModal

View File

@ -1,7 +1,7 @@
<script>
import { backendUiStore } from "builderStore"
import { Input, Select, Label } from "@budibase/bbui"
import BindableInput from "../../userInterface/BindableInput.svelte"
import { Select } from "@budibase/bbui"
import BindableInput from "../../common/BindableInput.svelte"
export let value
export let bindings

View File

@ -103,6 +103,15 @@
opacity: 1;
}
.column-header-name {
white-space: normal !important;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
}
.sort-icon {
position: relative;
top: 2px;

View File

@ -1,6 +1,6 @@
<script>
import { createEventDispatcher } from "svelte"
import GenericBindingPopover from "./GenericBindingPopover.svelte"
import GenericBindingPopover from "../automation/SetupPanel/GenericBindingPopover.svelte"
import { Input, Icon } from "@budibase/bbui"
const dispatch = createEventDispatcher()

View File

@ -5,12 +5,7 @@
export let disabled
</script>
<div
class="dropdown-item"
class:disabled
on:click
class:big={subtitle != null}
{...$$restProps}>
<div class="dropdown-item" class:disabled on:click {...$$restProps}>
{#if icon}<i class={icon} />{/if}
<div class="content">
<div class="title">{title}</div>
@ -27,7 +22,7 @@
justify-content: flex-start;
align-items: center;
gap: var(--spacing-m);
padding: var(--spacing-xs) var(--spacing-l);
padding: var(--spacing-s) var(--spacing-l);
color: var(--ink);
}
.dropdown-item.disabled,
@ -35,9 +30,6 @@
pointer-events: none;
color: var(--grey-5);
}
.dropdown-item.big {
padding: var(--spacing-s) var(--spacing-l);
}
.dropdown-item:not(.disabled):hover {
background-color: var(--grey-2);
cursor: pointer;
@ -65,10 +57,6 @@
}
i {
padding: 0.5rem;
background-color: var(--grey-2);
font-size: 24px;
border-radius: var(--border-radius-s);
color: var(--ink);
font-size: var(--font-size-m);
}
</style>

View File

@ -6,47 +6,62 @@
selectedComponent,
currentAssetId,
} from "builderStore"
import components from "./temporaryPanelStructure.js"
import structure from "./componentStructure.json"
import { DropdownMenu } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
const categories = components.categories
$: enrichedStructure = enrichStructure(structure, $store.components)
let selectedIndex
let anchors = []
let popover
$: anchor = selectedIndex === -1 ? null : anchors[selectedIndex]
const close = () => {
popover.hide()
const enrichStructure = (structure, definitions) => {
let enrichedStructure = []
structure.forEach(item => {
if (typeof item === "string") {
const def = definitions[`@budibase/standard-components/${item}`]
if (def) {
enrichedStructure.push({
...def,
isCategory: false,
})
}
} else {
enrichedStructure.push({
...item,
isCategory: true,
children: enrichStructure(item.children || [], definitions),
})
}
})
return enrichedStructure
}
const onCategoryChosen = (category, idx) => {
if (category.isCategory) {
const onItemChosen = (item, idx) => {
if (item.isCategory) {
// Select and open this category
selectedIndex = idx
popover.show()
} else {
onComponentChosen(category)
// Add this component
store.actions.components.create(item.component)
popover.hide()
}
}
const onComponentChosen = component => {
store.actions.components.create(component._component, component.presetProps)
const path = store.actions.components.findRoute($selectedComponent)
$goto(`./${$currentAssetId}/${path}`)
close()
}
</script>
<div class="container">
{#each categories as category, idx}
{#each enrichedStructure as item, idx}
<div
bind:this={anchors[idx]}
class="category"
on:click={() => onCategoryChosen(category, idx)}
on:click={() => onItemChosen(item, idx)}
class:active={idx === selectedIndex}>
{#if category.icon}<i class={category.icon} />{/if}
<span>{category.name}</span>
{#if category.isCategory}<i class="ri-arrow-down-s-line arrow" />{/if}
{#if item.icon}<i class={item.icon} />{/if}
<span>{item.name}</span>
{#if item.isCategory}<i class="ri-arrow-down-s-line arrow" />{/if}
</div>
{/each}
</div>
@ -56,12 +71,12 @@
{anchor}
align="left">
<DropdownContainer>
{#each categories[selectedIndex].children as item}
{#each enrichedStructure[selectedIndex].children as item}
{#if !item.showOnAsset || item.showOnAsset.includes($currentAssetName)}
<DropdownItem
icon={item.icon}
title={item.name}
on:click={() => onComponentChosen(item)} />
on:click={() => onItemChosen(item)} />
{/if}
{/each}
</DropdownContainer>

View File

@ -14,7 +14,7 @@
const screenPlaceholder = new Screen()
.name("Screen Placeholder")
.route("*")
.component("@budibase/standard-components/screenslotplaceholder")
.component("@budibase/standard-components/screenslot")
.instanceName("Content Placeholder")
.json()

View File

@ -0,0 +1,62 @@
[
"container",
"datagrid",
"list",
"button",
{
"name": "Form",
"icon": "ri-file-edit-line",
"children": [
"dataform",
"dataformwide",
"input",
"richtext",
"datepicker"
]
},
{
"name": "Card",
"icon": "ri-archive-drawer-line",
"children": [
"stackedlist",
"card",
"cardhorizontal",
"cardstat"
]
},
{
"name": "Chart",
"icon": "ri-bar-chart-2-line",
"children": [
"bar",
"line",
"area",
"pie",
"donut",
"candlestick"
]
},
{
"name": "Elements",
"icon": "ri-paragraph",
"children": [
"heading",
"text",
"image",
"link",
"icon",
"embed"
]
},
{
"name": "Other",
"icon": "ri-more-2-line",
"children": [
"screenslot",
"navigation",
"login",
"rowdetail",
"newrow"
]
}
]

View File

@ -1,11 +1,9 @@
<script>
import { goto } from "@sveltech/routify"
import { get } from "svelte/store"
import { store, currentAsset } from "builderStore"
import { getComponentDefinition } from "builderStore/storeUtils"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { last } from "lodash/fp"
import { findParent } from "builderStore/storeUtils"
import { findComponentParent } from "builderStore/storeUtils"
import { DropdownMenu } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
@ -17,7 +15,7 @@
$: noChildrenAllowed =
!component ||
!getComponentDefinition($store, component._component)?.children
!store.actions.components.getDefinition(component._component)?.hasChildren
$: noPaste = !$store.componentToPaste
const lastPartOfName = c => (c ? last(c._component.split("/")) : "")
@ -28,48 +26,38 @@
const selectComponent = component => {
store.actions.components.select(component)
const path = store.actions.components.findRoute(component)
$goto(`./${$store.currentFrontEndType}/${path}`)
}
const moveUpComponent = () => {
store.update(state => {
const asset = get(currentAsset)
const parent = findParent(asset.props, component)
if (parent) {
const currentIndex = parent._children.indexOf(component)
if (currentIndex === 0) return state
const newChildren = parent._children.filter(c => c !== component)
newChildren.splice(currentIndex - 1, 0, component)
parent._children = newChildren
}
state.selectedComponentId = component._id
store.actions.preview.saveSelected()
return state
})
const asset = get(currentAsset)
const parent = findComponentParent(asset.props, component._id)
if (!parent) {
return
}
const currentIndex = parent._children.indexOf(component)
if (currentIndex === 0) {
return
}
const newChildren = parent._children.filter(c => c !== component)
newChildren.splice(currentIndex - 1, 0, component)
parent._children = newChildren
store.actions.preview.saveSelected()
}
const moveDownComponent = () => {
store.update(state => {
const asset = get(currentAsset)
const parent = findParent(asset.props, component)
if (parent) {
const currentIndex = parent._children.indexOf(component)
if (currentIndex === parent._children.length - 1) return state
const newChildren = parent._children.filter(c => c !== component)
newChildren.splice(currentIndex + 1, 0, component)
parent._children = newChildren
}
state.selectedComponentId = component._id
store.actions.preview.saveSelected()
return state
})
const asset = get(currentAsset)
const parent = findComponentParent(asset.props, component._id)
if (!parent) {
return
}
const currentIndex = parent._children.indexOf(component)
if (currentIndex === parent._children.length - 1) {
return
}
const newChildren = parent._children.filter(c => c !== component)
newChildren.splice(currentIndex + 1, 0, component)
parent._children = newChildren
store.actions.preview.saveSelected()
}
const duplicateComponent = () => {
@ -78,18 +66,7 @@
}
const deleteComponent = () => {
store.update(state => {
const asset = get(currentAsset)
const parent = findParent(asset.props, component)
if (parent) {
parent._children = parent._children.filter(child => child !== component)
selectComponent(parent)
}
store.actions.preview.saveSelected()
return state
})
store.actions.components.delete(component)
}
const storeComponentForCopy = (cut = false) => {

View File

@ -1,7 +1,6 @@
<script>
import { goto } from "@sveltech/routify"
import { store, currentAssetId } from "builderStore"
import { getComponentDefinition } from "builderStore/storeUtils"
import { DropEffect, DropPosition } from "./dragDropStore"
import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte"
import NavItem from "components/common/NavItem.svelte"
@ -12,17 +11,10 @@
export let level = 0
export let dragDropStore
const isScreenslot = name => name === "##builtin/screenslot"
const isScreenslot = name => name?.endsWith("screenslot")
const selectComponent = component => {
// Set current component
store.actions.components.select(component)
// Get ID path
const path = store.actions.components.findRoute(component)
// Go to correct URL
$goto(`./${$currentAssetId}/${path}`)
}
const dragstart = component => e => {
@ -31,9 +23,11 @@
}
const dragover = (component, index) => e => {
const definition = store.actions.components.getDefinition(
component._component
)
const canHaveChildrenButIsEmpty =
getComponentDefinition($store, component._component).children &&
component._children.length === 0
definition?.hasChildren && !component._children?.length
e.dataTransfer.dropEffect = DropEffect.COPY

View File

@ -24,9 +24,7 @@
$: selectedScreen = $currentAsset
const changeScreen = screenId => {
// select the route
store.actions.screens.select(screenId)
$goto(`./${screenId}`)
}
</script>

View File

@ -1,5 +1,6 @@
import { writable } from "svelte/store"
import { writable, get } from "svelte/store"
import { store as frontendStore } from "builderStore"
import { findComponentPath } from "builderStore/storeUtils"
export const DropEffect = {
MOVE: "move",
@ -72,19 +73,30 @@ export default function() {
})
},
drop: () => {
store.update(state => {
if (state.targetComponent !== state.dragged) {
frontendStore.actions.components.copy(state.dragged, true)
frontendStore.actions.components.paste(
state.targetComponent,
state.dropPosition
)
}
const state = get(store)
store.actions.reset()
// Stop if the target and source are the same
if (state.targetComponent === state.dragged) {
return
}
// Stop if the target or source are null
if (!state.targetComponent || !state.dragged) {
return
}
// Stop if the target is a child of source
const path = findComponentPath(state.dragged, state.targetComponent._id)
const ids = path.map(component => component._id)
if (ids.includes(state.targetComponent._id)) {
return
}
return state
})
// Cut and paste the component
frontendStore.actions.components.copy(state.dragged, true)
frontendStore.actions.components.paste(
state.targetComponent,
state.dropPosition
)
store.actions.reset()
},
}

View File

@ -9,10 +9,10 @@
selectedAccessRole,
} from "builderStore"
import { FrontendTypes } from "constants"
import ComponentNavigationTree from "components/userInterface/ComponentNavigationTree/index.svelte"
import Layout from "components/userInterface/Layout.svelte"
import NewScreenModal from "components/userInterface/NewScreenModal.svelte"
import NewLayoutModal from "components/userInterface/NewLayoutModal.svelte"
import ComponentNavigationTree from "components/design/NavigationPanel/ComponentNavigationTree/index.svelte"
import Layout from "components/design/NavigationPanel/Layout.svelte"
import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte"
import NewLayoutModal from "components/design/NavigationPanel/NewLayoutModal.svelte"
import { Modal, Switcher, Select } from "@budibase/bbui"
const tabs = [
@ -28,7 +28,7 @@
let modal
let routes = {}
let tab = $params.assetType
$: tab = $params.assetType
const navigate = ({ detail }) => {
if (!detail) {

View File

@ -19,7 +19,6 @@
const selectLayout = () => {
store.actions.layouts.select(layout._id)
$goto(`./${layout._id}`)
}
</script>

View File

@ -8,8 +8,7 @@
async function save() {
try {
const layout = await store.actions.layouts.save({ name })
$goto(`./${layout._id}`)
await store.actions.layouts.save({ name })
notifier.success(`Layout ${name} created successfully`)
} catch (err) {
notifier.danger(`Error creating layout ${name}.`)

View File

@ -63,7 +63,7 @@
draftScreen.props._component = baseComponent
draftScreen.routing = { route, roleId }
const createdScreen = await store.actions.screens.create(draftScreen)
await store.actions.screens.create(draftScreen)
if (createLink) {
await store.actions.components.links.save(route, name)
}
@ -75,8 +75,6 @@
template: template.id || template.name,
})
}
$goto(`./${createdScreen._id}`)
}
const routeExists = (route, roleId) => {

View File

@ -9,7 +9,7 @@
export let bindingDrawer
function addToText(readableBinding) {
value = value + `{{ ${readableBinding} }}`
value = `${value || ""}{{ ${readableBinding} }}`
}
let originalValue = value

View File

@ -1,16 +1,16 @@
<script>
import { TextArea, DetailSummary, Button } from "@budibase/bbui"
import PropertyGroup from "./PropertyGroup.svelte"
import FlatButtonGroup from "./FlatButtonGroup.svelte"
import PropertyGroup from "./PropertyControls/PropertyGroup.svelte"
import FlatButtonGroup from "./PropertyControls/FlatButtonGroup"
import { allStyles } from "./componentStyles"
export let panelDefinition = {}
export let componentDefinition = {}
export let componentInstance = {}
export let onStyleChanged = () => {}
export let onCustomStyleChanged = () => {}
export let onResetStyles = () => {}
let selectedCategory = "normal"
let propGroup = null
let currentGroup
function onChange(category) {
@ -23,7 +23,7 @@
{ value: "active", text: "Active" },
]
$: propertyGroupNames = panelDefinition ? Object.keys(panelDefinition) : []
$: groups = componentDefinition?.styleable ? Object.keys(allStyles) : []
</script>
<div class="design-view-container">
@ -32,12 +32,12 @@
</div>
<div class="positioned-wrapper">
<div bind:this={propGroup} class="design-view-property-groups">
{#if propertyGroupNames.length > 0}
{#each propertyGroupNames as groupName}
<div class="design-view-property-groups">
{#if groups.length > 0}
{#each groups as groupName}
<PropertyGroup
name={groupName}
properties={panelDefinition[groupName]}
properties={allStyles[groupName]}
styleCategory={selectedCategory}
{onStyleChanged}
{componentInstance}

View File

@ -2,46 +2,30 @@
import { get } from "svelte/store"
import { store, selectedComponent, currentAsset } from "builderStore"
import { FrontendTypes } from "constants"
import panelStructure from "./temporaryPanelStructure.js"
import CategoryTab from "./CategoryTab.svelte"
import DesignView from "./DesignView.svelte"
import SettingsView from "./SettingsView.svelte"
import { setWith } from "lodash"
let flattenedPanel = flattenComponents(panelStructure.categories)
let categories = [
const categories = [
{ value: "settings", name: "Settings" },
{ value: "design", name: "Design" },
]
let selectedCategory = categories[0]
$: componentInstance =
$store.currentView !== "component"
? { ...$currentAsset, ...$selectedComponent }
: $selectedComponent
$: componentDefinition = $store.components[componentInstance._component]
$: componentPropDefinition =
flattenedPanel.find(
// use for getting controls for each component property
c => c._component === componentInstance._component
) || {}
$: panelDefinition =
componentPropDefinition.properties &&
componentPropDefinition.properties[selectedCategory.value]
$: definition = store.actions.components.getDefinition(
$selectedComponent._component
)
$: isComponentOrScreen =
$store.currentView === "component" ||
$store.currentFrontEndType === FrontendTypes.SCREEN
$: isNotScreenslot = !$selectedComponent._component.endsWith("screenslot")
$: showDisplayName = isComponentOrScreen && isNotScreenslot
const onStyleChanged = store.actions.components.updateStyle
const onCustomStyleChanged = store.actions.components.updateCustomStyle
const onResetStyles = store.actions.components.resetStyles
$: isComponentOrScreen =
$store.currentView === "component" ||
$store.currentFrontEndType === FrontendTypes.SCREEN
$: isNotScreenslot = componentInstance._component !== "##builtin/screenslot"
$: displayName =
isComponentOrScreen && componentInstance._instanceName && isNotScreenslot
function walkProps(component, action) {
action(component)
if (component.children) {
@ -89,24 +73,23 @@
{categories}
{selectedCategory} />
{#if displayName}
<div class="instance-name">{componentInstance._instanceName}</div>
{#if showDisplayName}
<div class="instance-name">{$selectedComponent._instanceName}</div>
{/if}
<div class="component-props-container">
{#if selectedCategory.value === 'design'}
<DesignView
{panelDefinition}
{componentInstance}
componentInstance={$selectedComponent}
componentDefinition={definition}
{onStyleChanged}
{onCustomStyleChanged}
{onResetStyles} />
{:else if selectedCategory.value === 'settings'}
<SettingsView
{componentInstance}
{componentDefinition}
{panelDefinition}
displayNameField={displayName}
componentInstance={$selectedComponent}
componentDefinition={definition}
{showDisplayName}
onChange={store.actions.components.updateProp}
onScreenPropChange={setAssetProps}
assetInstance={$store.currentView !== 'component' && $currentAsset} />

View File

@ -0,0 +1,7 @@
<script>
import Checkbox from "components/common/Checkbox.svelte"
export let value
</script>
<Checkbox checked={value} on:change />

View File

@ -0,0 +1,7 @@
<script>
import Colorpicker from "@budibase/colorpicker"
export let value
</script>
<Colorpicker value={value || '#000'} on:change />

View File

@ -13,13 +13,12 @@
const EVENT_TYPE_KEY = "##eventHandlerType"
export let event
export let actions
let addActionButton
let addActionDropdown
let selectedAction
$: actions = event || []
$: selectedActionComponent =
selectedAction &&
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_KEY]).component

View File

@ -7,7 +7,7 @@
const dispatch = createEventDispatcher()
export let value
export let value = []
export let name
let drawer
@ -63,6 +63,6 @@
<Button thin blue on:click={saveEventData}>Save</Button>
</heading>
<div slot="body">
<EventEditor event={value} eventType={name} />
<EventEditor bind:actions={value} eventType={name} />
</div>
</Drawer>

View File

@ -0,0 +1,72 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { store, currentAsset } from "builderStore"
import {
getDataProviderComponents,
getDatasourceForProvider,
getSchemaForDatasource,
} from "builderStore/dataBinding"
export let parameters
$: dataProviderComponents = getDataProviderComponents(
$currentAsset.props,
$store.selectedComponentId
)
$: {
// Automatically set rev and table ID based on row ID
if (parameters.rowId) {
parameters.revId = parameters.rowId.replace("_id", "_rev")
const providerComponent = dataProviderComponents.find(
provider => provider._id === parameters.providerId
)
const datasource = getDatasourceForProvider(providerComponent)
const { table } = getSchemaForDatasource(datasource)
if (table) {
parameters.tableId = table._id
}
}
}
</script>
<div class="root">
{#if dataProviderComponents.length === 0}
<div class="cannot-use">
Delete row can only be used within a component that provides data, such as
a List
</div>
{:else}
<Label size="m" color="dark">Datasource</Label>
<Select secondary bind:value={parameters.rowId}>
<option value="" />
{#each dataProviderComponents as provider}
<option value={`{{ ${provider._id}._id }}`}>
{provider._instanceName}
</option>
{/each}
</Select>
{/if}
</div>
<style>
.root {
display: grid;
column-gap: var(--spacing-s);
row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto;
align-items: baseline;
}
.root :global(> div:nth-child(2)) {
grid-column-start: 2;
grid-column-end: 6;
}
.cannot-use {
color: var(--red);
font-size: var(--font-size-s);
text-align: center;
width: 70%;
margin: auto;
}
</style>

View File

@ -1,22 +1,18 @@
<script>
import { Select, Label, Spacer } from "@budibase/bbui"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import ParameterBuilder from "../../../integration/QueryParameterBuilder.svelte"
import { getBindableProperties } from "builderStore/dataBinding"
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
export let parameters
$: datasource = $backendUiStore.datasources.find(
ds => ds._id === parameters.datasourceId
)
// TODO: binding needs to be centralised
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $currentAsset,
tables: $backendUiStore.tables,
queries: $backendUiStore.queries,
}).map(property => ({
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$store.selectedComponentId
).map(property => ({
...property,
category: property.type === "instance" ? "Component" : "Table",
label: property.readableBinding,

View File

@ -1,5 +1,4 @@
<script>
// accepts an array of field names, and outputs an object of { FieldName: value }
import {
DataList,
Label,
@ -8,13 +7,13 @@
Select,
Input,
} from "@budibase/bbui"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import { CloseCircleIcon, AddIcon } from "components/common/Icons"
import { store, currentAsset } from "builderStore"
import {
getBindableProperties,
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/replaceBindings"
} from "builderStore/dataBinding"
import { CloseCircleIcon, AddIcon } from "components/common/Icons"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
@ -25,6 +24,11 @@
const emptyField = () => ({ name: "", value: "" })
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$store.selectedComponentId
)
// this statement initialises fields from parameters.fields
$: fields =
fields ||
@ -39,14 +43,6 @@
"",
}))
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $currentAsset,
tables: $backendUiStore.tables,
queries: $backendUiStore.queries,
})
const addField = () => {
const newFields = fields.filter(f => f.name)
newFields.push(emptyField())

View File

@ -0,0 +1,78 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { store, currentAsset } from "builderStore"
import {
getDataProviderComponents,
getDatasourceForProvider,
getSchemaForDatasource,
} from "builderStore/dataBinding"
import SaveFields from "./SaveFields.svelte"
export let parameters
$: dataProviderComponents = getDataProviderComponents(
$currentAsset.props,
$store.selectedComponentId
)
$: providerComponent = dataProviderComponents.find(
provider => provider._id === parameters.providerId
)
$: schemaFields = getSchemaFields(providerComponent)
const getSchemaFields = component => {
const datasource = getDatasourceForProvider(component)
const { schema } = getSchemaForDatasource(datasource)
return Object.values(schema || {})
}
const onFieldsChanged = e => {
parameters.fields = e.detail
}
</script>
<div class="root">
{#if !dataProviderComponents.length}
<div class="cannot-use">
Save Row can only be used within a component that provides data, such as a
Repeater
</div>
{:else}
<Label size="m" color="dark">Datasource</Label>
<Select secondary bind:value={parameters.providerId}>
<option value="" />
{#each dataProviderComponents as provider}
<option value={provider._id}>{provider._instanceName}</option>
{/each}
</Select>
{#if parameters.providerId}
<SaveFields
parameterFields={parameters.fields}
{schemaFields}
on:fieldschanged={onFieldsChanged} />
{/if}
{/if}
</div>
<style>
.root {
display: grid;
column-gap: var(--spacing-s);
row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto;
align-items: baseline;
}
.root :global(> div:nth-child(2)) {
grid-column-start: 2;
grid-column-end: 6;
}
.cannot-use {
color: var(--red);
font-size: var(--font-size-s);
text-align: center;
width: 70%;
margin: auto;
}
</style>

View File

@ -91,7 +91,6 @@
{/if}
<SaveFields
parameterFields={parameters.fields}
schemaFields={automationStatus === AUTOMATION_STATUS.EXISTING && selectedAutomation && selectedAutomation.schema}
fieldLabel="Field"
on:fieldschanged={onFieldsChanged} />

View File

@ -0,0 +1,2 @@
import EventsEditor from "./EventPropertyControl.svelte"
export default EventsEditor

View File

@ -0,0 +1,2 @@
import FlatButtonGroup from "./FlatButtonGroup.svelte"
export default FlatButtonGroup

View File

@ -1,5 +1,5 @@
<script>
import { store, currentAsset } from "builderStore"
import { store } from "builderStore"
import { Select } from "@budibase/bbui"
export let value

View File

@ -3,7 +3,6 @@
export let options = []
export let value = []
export let styleBindingProperty
export let onChange = () => {}
let boundValue = getValidOptions(value, options)

View File

@ -1,7 +1,7 @@
<script>
import { onMount } from "svelte"
import Portal from "svelte-portal"
import { buildStyle } from "../../helpers.js"
import { buildStyle } from "../../../../helpers.js"
export let options = []
export let value = ""

View File

@ -1,83 +1,65 @@
<script>
import { Button, Icon, Drawer, Body } from "@budibase/bbui"
import Input from "./PropertyPanelControls/Input.svelte"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import { store, currentAsset } from "builderStore"
import {
getBindableProperties,
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/replaceBindings"
import BindingPanel from "components/userInterface/BindingPanel.svelte"
} from "builderStore/dataBinding"
import BindingPanel from "components/design/PropertiesPanel/BindingPanel.svelte"
export let label = ""
export let bindable = true
export let componentInstance = {}
export let control = null
export let key = ""
export let value
export let type = ""
export let value = null
export let props = {}
export let onChange = () => {}
let bindingDrawer
let temporaryBindableValue = value
let bindableProperties = []
let anchor
function handleClose() {
handleChange(key, temporaryBindableValue)
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$store.selectedComponentId
)
$: safeValue = getSafeValue(value, props.defaultValue, bindableProperties)
$: replaceBindings = val => readableToRuntimeBinding(bindableProperties, val)
const handleClose = () => {
handleChange(temporaryBindableValue)
bindingDrawer.hide()
}
function getBindableProperties() {
// Get all bindableProperties
bindableProperties = fetchBindableProperties({
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $currentAsset,
tables: $backendUiStore.tables,
queries: $backendUiStore.queries,
})
}
function replaceBindings(textWithBindings) {
getBindableProperties()
textWithBindings = readableToRuntimeBinding(
bindableProperties,
textWithBindings
)
onChange(key, textWithBindings)
}
function handleChange(key, v) {
let innerVal = v
if (typeof v === "object") {
if ("detail" in v) {
innerVal = v.detail
} else if ("target" in v) {
innerVal = props.valueKey ? v.target[props.valueKey] : v.target.value
// Handle a value change of any type
// String values have any bindings handled
const handleChange = value => {
let innerVal = value
if (value && typeof value === "object") {
if ("detail" in value) {
innerVal = value.detail
} else if ("target" in value) {
innerVal = value.target.value
}
}
if (typeof innerVal === "string") {
replaceBindings(innerVal)
onChange(replaceBindings(innerVal))
} else {
onChange(key, innerVal)
onChange(innerVal)
}
}
const safeValue = () => {
getBindableProperties()
let temp = runtimeToReadableBinding(bindableProperties, value)
return value == null && props.initialValue !== undefined
? props.initialValue
: temp
// The "safe" value is the value with eny bindings made readable
// If there is no value set, any default value is used
const getSafeValue = (value, defaultValue, bindableProperties) => {
const enriched = runtimeToReadableBinding(bindableProperties, value)
return enriched == null && defaultValue !== undefined
? defaultValue
: enriched
}
// Incase the component has a different value key name
const handlevalueKey = value =>
props.valueKey ? { [props.valueKey]: safeValue() } : { value: safeValue() }
</script>
<div class="property-control" bind:this={anchor}>
@ -86,13 +68,13 @@
<svelte:component
this={control}
{componentInstance}
{...handlevalueKey(value)}
on:change={val => handleChange(key, val)}
onChange={val => handleChange(key, val)}
value={safeValue}
on:change={handleChange}
onChange={handleChange}
{...props}
name={key} />
</div>
{#if bindable && !key.startsWith('_') && control === Input}
{#if bindable && !key.startsWith('_') && type === 'text'}
<div
class="icon"
data-cy={`${key}-binding-button`}
@ -101,7 +83,6 @@
</div>
{/if}
</div>
<Drawer bind:this={bindingDrawer} title="Bindings">
<div slot="description">
<Body extraSmall grey>
@ -113,7 +94,7 @@
</heading>
<div slot="body">
<BindingPanel
{...handlevalueKey(value)}
value={safeValue}
close={handleClose}
on:update={e => (temporaryBindableValue = e.detail)}
{bindableProperties} />

View File

@ -1,5 +1,4 @@
<script>
import { excludeProps } from "./propertyCategories.js"
import PropertyControl from "./PropertyControl.svelte"
import { DetailSummary } from "@budibase/bbui"
@ -10,20 +9,17 @@
export let onStyleChanged = () => {}
export let open = false
$: style = componentInstance["_styles"][styleCategory] || {}
$: changed = properties.some(prop => hasPropChanged(style, prop))
const hasPropChanged = (style, prop) => {
// TODO: replace color picker with one that works better.
// Currently it cannot support null values, so this is a hack which
// prevents the color fields from always being marked as changed
if (!["color", "background", "border-color"].includes(prop.key)) {
if (prop.initialValue !== undefined) {
return style[prop.key] !== prop.initialValue
}
}
return style[prop.key] != null && style[prop.key] !== ""
}
$: style = componentInstance["_styles"][styleCategory] || {}
$: changed = properties.some(prop => hasPropChanged(style, prop))
const getControlProps = props => {
const { label, key, control, ...otherProps } = props || {}
return otherProps || {}
}
</script>
<DetailSummary name={`${name}${changed ? ' *' : ''}`} on:open show={open} thin>
@ -31,12 +27,13 @@
<div>
{#each properties as prop}
<PropertyControl
bindable={false}
label={`${prop.label}${hasPropChanged(style, prop) ? ' *' : ''}`}
control={prop.control}
key={prop.key}
value={style[prop.key]}
onChange={(key, value) => onStyleChanged(styleCategory, key, value)}
props={{ ...excludeProps(prop, ['control', 'label']) }} />
onChange={value => onStyleChanged(styleCategory, prop.key, value)}
props={getControlProps(prop)} />
{/each}
</div>
{/if}

View File

@ -0,0 +1,80 @@
<script>
import { DataList } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { store, allScreens, currentAsset } from "builderStore"
import { getBindableProperties } from "builderStore/dataBinding"
export let value = ""
$: urls = getUrls($allScreens, $currentAsset, $store.selectedComponentId)
// Update value on blur
const dispatch = createEventDispatcher()
const handleBlur = () => dispatch("change", value)
// Get all valid screen URL, as well as detail screens which can be used in
// the current data context
const getUrls = (screens, asset, componentId) => {
// Get all screens which aren't detail screens
let urls = screens
.filter(screen => !screen.props._component.endsWith("/rowdetail"))
.map(screen => ({
name: screen.props._instanceName,
url: screen.routing.route,
sort: screen.props._component,
}))
// Add detail screens enriched with the current data context
const bindableProperties = getBindableProperties(asset.props, componentId)
screens
.filter(screen => screen.props._component.endsWith("/rowdetail"))
.forEach(detailScreen => {
// Find any _id bindings that match the detail screen's table
const binding = bindableProperties.find(p => {
return (
p.type === "context" &&
p.runtimeBinding.endsWith("._id") &&
p.tableId === detailScreen.props.table
)
})
if (binding) {
urls.push({
name: detailScreen.props._instanceName,
url: detailScreen.routing.route.replace(
":id",
`{{ ${binding.runtimeBinding} }}`
),
sort: detailScreen.props._component,
})
}
})
return urls
}
</script>
<div>
<DataList
editable
secondary
extraThin
on:blur={handleBlur}
on:change
bind:value>
<option value="" />
{#each urls as url}
<option value={url.url}>{url.name}</option>
{/each}
</DataList>
</div>
<style>
div {
flex: 1 1 auto;
display: flex;
flex-direction: row;
}
div :global(> div) {
flex: 1 1 auto;
}
</style>

View File

@ -0,0 +1,23 @@
<script>
import OptionSelect from "./OptionSelect.svelte"
import MultiOptionSelect from "./MultiOptionSelect.svelte"
import {
getDatasourceForProvider,
getSchemaForDatasource,
} from "builderStore/dataBinding"
export let componentInstance = {}
export let value = ""
export let onChange = () => {}
export let multiselect = false
$: datasource = getDatasourceForProvider(componentInstance)
$: schema = getSchemaForDatasource(datasource)
$: options = Object.keys(schema || {})
</script>
{#if multiselect}
<MultiOptionSelect {value} {onChange} {options} />
{:else}
<OptionSelect {value} {onChange} {options} />
{/if}

View File

@ -1,4 +1,5 @@
<script>
import { getBindableProperties } from "builderStore/dataBinding"
import {
Button,
Icon,
@ -12,7 +13,6 @@
import { notifier } from "builderStore/store/notifications"
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
import IntegrationQueryEditor from "components/integration/index.svelte"
import fetchBindableProperties from "../../builderStore/fetchBindableProperties"
const dispatch = createEventDispatcher()
let anchorRight, dropdownRight
@ -22,11 +22,9 @@
$: tables = $backendUiStore.tables.map(m => ({
label: m.name,
name: `all_${m._id}`,
tableId: m._id,
type: "table",
}))
$: views = $backendUiStore.tables.reduce((acc, cur) => {
let viewsArr = Object.entries(cur.views).map(([key, value]) => ({
label: key,
@ -36,41 +34,38 @@
}))
return [...acc, ...viewsArr]
}, [])
$: queries = $backendUiStore.queries.map(query => ({
label: query.name,
name: query.name,
tableId: query._id,
...query,
schema: query.schema,
parameters: query.parameters,
type: "query",
}))
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $currentAsset,
tables: $backendUiStore.tables,
queries: $backendUiStore.queries,
})
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$store.selectedComponentId
)
$: queryBindableProperties = bindableProperties.map(property => ({
...property,
category: property.type === "instance" ? "Component" : "Table",
label: property.readableBinding,
path: property.readableBinding,
}))
$: links = bindableProperties
.filter(x => x.fieldSchema?.type === "link")
.map(property => {
return {
providerId: property.instance._id,
providerId: property.providerId,
label: property.readableBinding,
fieldName: property.fieldSchema.name,
name: `all_${property.fieldSchema.tableId}`,
tableId: property.fieldSchema.tableId,
type: "link",
// These properties will be enriched by the client library and provide
// details of the parent row of the relationship field, from context
rowId: `{{ ${property.providerId}._id }}`,
rowTableId: `{{ ${property.providerId}.tableId }}`,
}
})
@ -91,39 +86,39 @@
class="dropdownbutton"
bind:this={anchorRight}
on:click={dropdownRight.show}>
<span>{value.label ? value.label : 'Table / View / Query'}</span>
<span>{value?.label ? value.label : 'Choose option'}</span>
<Icon name="arrowdown" />
</div>
{#if value.type === 'query'}
{#if value?.type === 'query'}
<i class="ri-settings-5-line" on:click={drawer.show} />
<Drawer title={'Query'} bind:this={drawer}>
<div slot="buttons">
<Button
blue
thin
on:click={() => {
notifier.success('Query parameters saved.')
handleSelected(value)
drawer.hide()
}}>
Save
</Button>
</div>
<div class="drawer-contents" slot="body">
<IntegrationQueryEditor
query={value}
schema={fetchDatasourceSchema(value)}
editable={false} />
<Spacer large />
{#if value.parameters.length > 0}
<ParameterBuilder
bind:customParams={value.queryParams}
parameters={queries.find(query => query._id === value._id).parameters}
bindings={queryBindableProperties} />
{/if}
</div>
</Drawer>
{/if}
<Drawer title={'Query'} bind:this={drawer}>
<div slot="buttons">
<Button
blue
thin
on:click={() => {
notifier.success('Query parameters saved.')
handleSelected(value)
drawer.hide()
}}>
Save
</Button>
</div>
<div class="drawer-contents" slot="body">
<IntegrationQueryEditor
query={value}
schema={fetchDatasourceSchema(value)}
editable={false} />
<Spacer large />
{#if value.parameters.length > 0}
<ParameterBuilder
bind:customParams={value.queryParams}
parameters={queries.find(query => query._id === value._id).parameters}
bindings={queryBindableProperties} />
{/if}
</div>
</Drawer>
{/if}
<DropdownMenu bind:this={dropdownRight} anchor={anchorRight}>
<div class="dropdown">
<div class="title">

View File

@ -0,0 +1,140 @@
<script>
import { get } from "lodash"
import { isEmpty } from "lodash/fp"
import PropertyControl from "./PropertyControls/PropertyControl.svelte"
import Input from "./PropertyControls/Input.svelte"
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
import RoleSelect from "./PropertyControls/RoleSelect.svelte"
import OptionSelect from "./PropertyControls/OptionSelect.svelte"
import MultiTableViewFieldSelect from "./PropertyControls/MultiTableViewFieldSelect.svelte"
import Checkbox from "./PropertyControls/Checkbox.svelte"
import TableSelect from "./PropertyControls/TableSelect.svelte"
import TableViewSelect from "./PropertyControls/TableViewSelect.svelte"
import TableViewFieldSelect from "./PropertyControls/TableViewFieldSelect.svelte"
import EventsEditor from "./PropertyControls/EventsEditor"
import ScreenSelect from "./PropertyControls/ScreenSelect.svelte"
import DetailScreenSelect from "./PropertyControls/DetailScreenSelect.svelte"
import { IconSelect } from "./PropertyControls/IconSelect"
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
export let componentDefinition = {}
export let componentInstance = {}
export let assetInstance
export let onChange = () => {}
export let onScreenPropChange = () => {}
export let showDisplayName = false
const layoutDefinition = []
const screenDefinition = [
{ key: "description", label: "Description", control: Input },
{ key: "routing.route", label: "Route", control: Input },
{ key: "routing.roleId", label: "Access", control: RoleSelect },
{ key: "layoutId", label: "Layout", control: LayoutSelect },
]
const assetProps = [
"title",
"description",
"routing.route",
"layoutId",
"routing.roleId",
]
$: settings = componentDefinition?.settings ?? []
$: isLayout = assetInstance && assetInstance.favicon
$: assetDefinition = isLayout ? layoutDefinition : screenDefinition
const controlMap = {
text: Input,
select: OptionSelect,
datasource: TableViewSelect,
screen: ScreenSelect,
detailScreen: DetailScreenSelect,
boolean: Checkbox,
number: Input,
event: EventsEditor,
table: TableSelect,
color: ColorPicker,
icon: IconSelect,
field: TableViewFieldSelect,
multifield: MultiTableViewFieldSelect,
}
const getControl = type => {
return controlMap[type]
}
const canRenderControl = setting => {
const control = getControl(setting?.type)
if (!control) {
return false
}
if (setting.dependsOn && isEmpty(componentInstance[setting.dependsOn])) {
return false
}
return true
}
const onInstanceNameChange = name => {
onChange("_instanceName", name)
}
</script>
<div class="settings-view-container">
{#if assetInstance}
{#each assetDefinition as def (`${componentInstance._id}-${def.key}`)}
<PropertyControl
bindable={false}
control={def.control}
label={def.label}
key={def.key}
value={get(assetInstance, def.key)}
onChange={val => onScreenPropChange(def.key, val)} />
{/each}
{/if}
{#if showDisplayName}
<PropertyControl
bindable={false}
control={Input}
label="Name"
key="_instanceName"
value={componentInstance._instanceName}
onChange={onInstanceNameChange} />
{/if}
{#if settings && settings.length > 0}
{#each settings as setting (`${componentInstance._id}-${setting.key}`)}
{#if canRenderControl(setting)}
<PropertyControl
type={setting.type}
control={getControl(setting.type)}
label={setting.label}
key={setting.key}
value={componentInstance[setting.key] ?? componentInstance[setting.key]?.defaultValue}
{componentInstance}
onChange={val => onChange(setting.key, val)}
props={{ options: setting.options }} />
{/if}
{/each}
{:else}
<div class="empty">
This component doesn't have any additional settings.
</div>
{/if}
</div>
<style>
.settings-view-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
gap: var(--spacing-s);
}
.empty {
font-size: var(--font-size-xs);
margin-top: var(--spacing-m);
color: var(--grey-5);
}
</style>

View File

@ -1,7 +1,7 @@
import Input from "./PropertyPanelControls/Input.svelte"
import OptionSelect from "./OptionSelect.svelte"
import FlatButtonGroup from "./FlatButtonGroup.svelte"
import Colorpicker from "@budibase/colorpicker"
import Input from "./PropertyControls/Input.svelte"
import OptionSelect from "./PropertyControls/OptionSelect.svelte"
import FlatButtonGroup from "./PropertyControls/FlatButtonGroup"
import Colorpicker from "./PropertyControls/ColorPicker.svelte"
export const layout = [
{
@ -299,42 +299,36 @@ export const size = [
key: "width",
control: Input,
placeholder: "px",
textAlign: "center",
},
{
label: "Height",
key: "height",
control: Input,
placeholder: "px",
textAlign: "center",
},
{
label: "Min Width",
key: "min-width",
control: Input,
placeholder: "px",
textAlign: "center",
},
{
label: "Max Width",
key: "max-width",
control: Input,
placeholder: "px",
textAlign: "center",
},
{
label: "Min Height",
key: "min-height",
control: Input,
placeholder: "px",
textAlign: "center",
},
{
label: "Max Height",
key: "max-height",
control: Input,
placeholder: "px",
textAlign: "center",
},
]
@ -357,28 +351,24 @@ export const position = [
key: "top",
control: Input,
placeholder: "px",
textAlign: "center",
},
{
label: "Right",
key: "right",
control: Input,
placeholder: "px",
textAlign: "center",
},
{
label: "Bottom",
key: "bottom",
control: Input,
placeholder: "px",
textAlign: "center",
},
{
label: "Left",
key: "left",
control: Input,
placeholder: "px",
textAlign: "center",
},
{
label: "Z-index",
@ -458,7 +448,6 @@ export const typography = [
{ label: "60px", value: "60px" },
{ label: "72px", value: "72px" },
],
textAlign: "center",
},
{
label: "Line H",
@ -478,7 +467,6 @@ export const typography = [
label: "Color",
key: "color",
control: Colorpicker,
initialValue: "#000",
},
{
label: "align",
@ -522,7 +510,6 @@ export const background = [
label: "Color",
key: "background",
control: Colorpicker,
initialValue: "#000",
},
{
label: "Gradient",
@ -645,7 +632,6 @@ export const border = [
label: "Color",
key: "border-color",
control: Colorpicker,
initialValue: "#000",
},
{
label: "Style",
@ -672,7 +658,6 @@ export const effects = [
label: "Opacity",
key: "opacity",
control: OptionSelect,
textAlign: "center",
options: [
{ label: "Choose option", value: "" },
{ label: "0", value: "0" },
@ -758,7 +743,6 @@ export const transitions = [
label: "Duration",
key: "transition-duration",
control: OptionSelect,
textAlign: "center",
placeholder: "sec",
options: [
{ label: "Choose option", value: "" },
@ -785,7 +769,7 @@ export const transitions = [
},
]
export const all = {
export const allStyles = {
layout,
margin,
padding,
@ -797,13 +781,3 @@ export const all = {
effects,
transitions,
}
export function excludeProps(props, propsToExclude) {
const modifiedProps = {}
for (const prop in props) {
if (!propsToExclude.includes(prop)) {
modifiedProps[prop] = props[prop]
}
}
return modifiedProps
}

View File

@ -1,17 +1,10 @@
<script>
import {
Button,
TextArea,
Label,
Input,
Heading,
Spacer,
} from "@budibase/bbui"
import BindableInput from "components/userInterface/BindableInput.svelte"
import { Button, Input, Heading, Spacer } from "@budibase/bbui"
import BindableInput from "components/common/BindableInput.svelte"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/replaceBindings"
} from "builderStore/dataBinding"
export let bindable = true
export let parameters = []
@ -57,7 +50,7 @@
type="string"
thin
on:change={evt => onBindingChange(parameter.name, evt.detail)}
value={runtimeToReadableBinding(bindings, customParams[parameter.name])}
value={runtimeToReadableBinding(bindings, customParams?.[parameter.name])}
{bindings} />
{:else}
<i

View File

@ -1,64 +0,0 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import SaveFields from "./SaveFields.svelte"
export let parameters
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $currentAsset,
tables: $backendUiStore.tables,
queries: $backendUiStore.queries,
})
const tableFields = tableId => {
const table = $backendUiStore.tables.find(m => m._id === tableId)
return Object.keys(table.schema).map(k => ({
name: k,
type: table.schema[k].type,
}))
}
$: schemaFields =
parameters && parameters.tableId ? tableFields(parameters.tableId) : []
const onFieldsChanged = e => {
parameters.fields = e.detail
}
</script>
<div class="root">
<Label size="m" color="dark">Table</Label>
<Select secondary bind:value={parameters.tableId}>
<option value="" />
{#each $backendUiStore.tables as table}
<option value={table._id}>{table.name}</option>
{/each}
</Select>
{#if parameters.tableId}
<SaveFields
parameterFields={parameters.fields}
{schemaFields}
on:fieldschanged={onFieldsChanged} />
{/if}
</div>
<style>
.root {
display: grid;
column-gap: var(--spacing-s);
row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto;
align-items: baseline;
}
.root :global(> div:nth-child(2)) {
grid-column-start: 2;
grid-column-end: 6;
}
</style>

View File

@ -1,90 +0,0 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
export let parameters
let idFields
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $currentAsset,
tables: $backendUiStore.tables,
queries: $backendUiStore.queries,
})
$: idFields = bindableProperties.filter(
bindable =>
bindable.type === "context" && bindable.runtimeBinding.endsWith("._id")
)
$: {
if (parameters.rowId) {
// Set rev ID
parameters.revId = parameters.rowId.replace("_id", "_rev")
// Set table ID
const idBinding = bindableProperties.find(
prop =>
prop.runtimeBinding ===
parameters.rowId
.replace("{{", "")
.replace("}}", "")
.trim()
)
if (idBinding) {
const { instance } = idBinding
const component = $store.components[instance._component]
const tableInfo = instance[component.context]
if (tableInfo) {
parameters.tableId =
typeof tableInfo === "string" ? tableInfo : tableInfo.tableId
}
}
}
}
</script>
<div class="root">
{#if idFields.length === 0}
<div class="cannot-use">
Delete row can only be used within a component that provides data, such as
a List
</div>
{:else}
<Label size="m" color="dark">Datasource</Label>
<Select secondary bind:value={parameters.rowId}>
<option value="" />
{#each idFields as idField}
<option value={`{{ ${idField.runtimeBinding} }}`}>
{idField.instance._instanceName}
</option>
{/each}
</Select>
{/if}
</div>
<style>
.root {
display: grid;
column-gap: var(--spacing-s);
row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto;
align-items: baseline;
}
.root :global(> div:nth-child(2)) {
grid-column-start: 2;
grid-column-end: 6;
}
.cannot-use {
color: var(--red);
font-size: var(--font-size-s);
text-align: center;
width: 70%;
margin: auto;
}
</style>

View File

@ -1,136 +0,0 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import SaveFields from "./SaveFields.svelte"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/replaceBindings"
// parameters.contextPath used in the client handler to determine which row to save
// this could be "data" or "data.parent", "data.parent.parent" etc
export let parameters
let idFields
let schemaFields
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $currentAsset,
tables: $backendUiStore.tables,
queries: $backendUiStore.queries,
})
$: {
if (parameters && parameters.contextPath) {
schemaFields = schemaFromContextPath(parameters.contextPath)
} else {
schemaFields = []
}
}
const idBindingToContextPath = id => id.substring(0, id.length - 4)
const contextPathToId = path => `${path}._id`
$: {
idFields = bindableProperties.filter(
bindable =>
bindable.type === "context" && bindable.runtimeBinding.endsWith("._id")
)
// ensure contextPath is always defaulted - there is usually only one option
if (idFields.length > 0 && !parameters.contextPath) {
parameters.contextPath = idBindingToContextPath(
idFields[0].runtimeBinding
)
parameters = parameters
}
}
// just wraps binding in {{ ... }}
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}`
// finds the selected idBinding, then reads the table/view
// from the component instance that it belongs to.
// then returns the field names for that schema
const schemaFromContextPath = contextPath => {
if (!contextPath) return []
const idBinding = bindableProperties.find(
prop => prop.runtimeBinding === contextPathToId(contextPath)
)
if (!idBinding) return []
const { instance } = idBinding
const component = $store.components[instance._component]
// component.context is the name of the prop that holds the tableId
const tableInfo = instance[component.context]
const tableId =
typeof tableInfo === "string" ? tableInfo : tableInfo.tableId
if (!tableInfo) return []
const table = $backendUiStore.tables.find(m => m._id === tableId)
parameters.tableId = tableId
return Object.keys(table.schema).map(k => ({
name: k,
type: table.schema[k].type,
}))
}
const onFieldsChanged = e => {
parameters.fields = e.detail
}
</script>
<div class="root">
{#if idFields.length === 0}
<div class="cannot-use">
Update row can only be used within a component that provides data, such as
a List
</div>
{:else}
<Label size="m" color="dark">Datasource</Label>
<Select secondary bind:value={parameters.contextPath}>
<option value="" />
{#each idFields as idField}
<option value={idBindingToContextPath(idField.runtimeBinding)}>
{idField.instance._instanceName}
</option>
{/each}
</Select>
{/if}
{#if parameters.contextPath}
<SaveFields
parameterFields={parameters.fields}
{schemaFields}
on:fieldschanged={onFieldsChanged} />
{/if}
</div>
<style>
.root {
display: grid;
column-gap: var(--spacing-s);
row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto;
align-items: baseline;
}
.root :global(> div:nth-child(2)) {
grid-column-start: 2;
grid-column-end: 6;
}
.cannot-use {
color: var(--red);
font-size: var(--font-size-s);
text-align: center;
width: 70%;
margin: auto;
}
</style>

View File

@ -1,134 +0,0 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import SaveFields from "./SaveFields.svelte"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/replaceBindings"
export let parameters
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $currentAsset,
tables: $backendUiStore.tables,
queries: $backendUiStore.queries,
})
let idFields
let rowId
$: {
idFields = bindableProperties.filter(
bindable =>
bindable.type === "context" && bindable.runtimeBinding.endsWith("._id")
)
// ensure rowId is always defaulted - there is usually only one option
if (idFields.length > 0 && !parameters._id) {
rowId = idFields[0].runtimeBinding
parameters = parameters
} else if (!rowId && parameters._id) {
rowId = parameters._id
.replace("{{", "")
.replace("}}", "")
.trim()
}
}
$: parameters._id = `{{ ${rowId} }}`
// just wraps binding in {{ ... }}
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}`
// finds the selected idBinding, then reads the table/view
// from the component instance that it belongs to.
// then returns the field names for that schema
const schemaFromIdBinding = rowId => {
if (!rowId) return []
const idBinding = bindableProperties.find(
prop => prop.runtimeBinding === rowId
)
if (!idBinding) return []
const { instance } = idBinding
const component = $store.components[instance._component]
// component.context is the name of the prop that holds the tableId
const tableInfo = instance[component.context]
if (!tableInfo) return []
const table = $backendUiStore.tables.find(m => m._id === tableInfo.tableId)
parameters.tableId = tableInfo.tableId
return Object.keys(table.schema).map(k => ({
name: k,
type: table.schema[k].type,
}))
}
let schemaFields
$: {
if (parameters && rowId) {
schemaFields = schemaFromIdBinding(rowId)
} else {
schemaFields = []
}
}
const onFieldsChanged = e => {
parameters.fields = e.detail
}
</script>
<div class="root">
{#if idFields.length === 0}
<div class="cannot-use">
Update row can only be used within a component that provides data, such as
a List
</div>
{:else}
<Label size="m" color="dark">Row Id</Label>
<Select secondary bind:value={rowId}>
<option value="" />
{#each idFields as idField}
<option value={idField.runtimeBinding}>
{idField.readableBinding}
</option>
{/each}
</Select>
{/if}
{#if rowId}
<SaveFields
parameterFields={parameters.fields}
{schemaFields}
on:fieldschanged={onFieldsChanged} />
{/if}
</div>
<style>
.root {
display: grid;
column-gap: var(--spacing-s);
row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto;
align-items: baseline;
}
.root :global(> div:nth-child(2)) {
grid-column-start: 2;
grid-column-end: 6;
}
.cannot-use {
color: var(--red);
font-size: var(--font-size-s);
text-align: center;
width: 70%;
margin: auto;
}
</style>

View File

@ -1,78 +0,0 @@
<script>
export let meta = []
export let size = ""
export let values = []
export let propertyName
export let onStyleChanged = () => {}
let selectedLayoutValues = values.map(v => v)
$: onStyleChanged(selectedLayoutValues)
const PROPERTY_OPTIONS = {
Direction: {
vertical: ["column", "ri-arrow-up-down-line"],
horizontal: ["row", "ri-arrow-left-right-line"],
},
Align: {
left: ["flex-start", "ri-layout-bottom-line"],
center: ["center", "ri-layout-row-line"],
right: ["flex-end", "ri-layout-top-line"],
space: ["space-between", "ri-space"],
},
Justify: {
left: ["flex-start", "ri-layout-left-line"],
center: ["center", "ri-layout-column-line"],
right: ["flex-end", "ri-layout-right-line"],
space: ["space-between", "ri-space"],
},
}
$: propertyChoices = Object.entries(PROPERTY_OPTIONS[propertyName])
</script>
<div class="inputs {size}">
{#each meta as { placeholder }, i}
{#each propertyChoices as [displayName, [cssPropValue, icon]]}
<button
class:selected={cssPropValue === selectedLayoutValues[i]}
on:click={() => {
const newPropertyValue = cssPropValue === selectedLayoutValues[i] ? '' : cssPropValue
selectedLayoutValues[i] = newPropertyValue
}}>
<i class={icon} />
</button>
{/each}
{/each}
</div>
<style>
.selected {
color: var(--blue);
background: var(--grey-1);
opacity: 1;
}
button {
cursor: pointer;
outline: none;
border: none;
border-radius: 3px;
min-width: 1.6rem;
min-height: 1.6rem;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 1.2rem;
font-weight: 500;
color: var(--ink);
}
.inputs {
display: flex;
justify-content: space-between;
}
</style>

View File

@ -1,95 +0,0 @@
<script>
import { DataList } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { store, allScreens, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
const dispatch = createEventDispatcher()
export let value = ""
$: urls = getUrls()
const handleBlur = () => dispatch("change", value)
// this will get urls of all screens, but only
// choose detail screens that are usable in the current context
// and substitute the :id param for the actual {{ ._id }} binding
const getUrls = () => {
const urls = [
...$allScreens
.filter(screen => !screen.props._component.endsWith("/rowdetail"))
.map(screen => ({
name: screen.props._instanceName,
url: screen.routing.route,
sort: screen.props._component,
})),
]
const bindableProperties = fetchBindableProperties({
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $currentAsset,
tables: $backendUiStore.tables,
queries: $backendUiStore.queries,
})
const detailScreens = $allScreens.filter(screen =>
screen.props._component.endsWith("/rowdetail")
)
for (let detailScreen of detailScreens) {
const idBinding = bindableProperties.find(p => {
if (
p.type === "context" &&
p.runtimeBinding.endsWith("._id") &&
p.table
) {
const tableId =
typeof p.table === "string" ? p.table : p.table.tableId
return tableId === detailScreen.props.table
}
return false
})
if (idBinding) {
urls.push({
name: detailScreen.props._instanceName,
url: detailScreen.routing.route.replace(
":id",
`{{ ${idBinding.runtimeBinding} }}`
),
sort: detailScreen.props._component,
})
}
}
return urls
}
</script>
<div>
<DataList
editable
secondary
extraThin
on:blur={handleBlur}
on:change
bind:value>
<option value="" />
{#each urls as url}
<option value={url.url}>{url.name}</option>
{/each}
</DataList>
</div>
<style>
div {
flex: 1 1 auto;
display: flex;
flex-direction: row;
}
div :global(> div) {
flex: 1 1 auto;
}
</style>

View File

@ -1,163 +0,0 @@
<script>
import { get } from "lodash"
import { isEmpty } from "lodash/fp"
import { FrontendTypes } from "constants"
import PropertyControl from "./PropertyControl.svelte"
import LayoutSelect from "./LayoutSelect.svelte"
import RoleSelect from "./RoleSelect.svelte"
import Input from "./PropertyPanelControls/Input.svelte"
import { excludeProps } from "./propertyCategories.js"
import { store, allScreens, currentAsset } from "builderStore"
import { walkProps } from "builderStore/storeUtils"
export let panelDefinition = []
export let componentDefinition = {}
export let componentInstance = {}
export let onChange = () => {}
export let onScreenPropChange = () => {}
export let displayNameField = false
export let assetInstance
let assetProps = [
"title",
"description",
"routing.route",
"layoutId",
"routing.roleId",
]
let duplicateName = false
const propExistsOnComponentDef = prop =>
assetProps.includes(prop) || prop in componentDefinition.props
function handleChange(key, data) {
data.target ? onChange(key, data.target.value) : onChange(key, data)
}
const screenDefinition = [
{ key: "description", label: "Description", control: Input },
{ key: "routing.route", label: "Route", control: Input },
{ key: "routing.roleId", label: "Access", control: RoleSelect },
{ key: "layoutId", label: "Layout", control: LayoutSelect },
]
const layoutDefinition = []
const canRenderControl = (key, dependsOn) => {
let test = !isEmpty(componentInstance[dependsOn])
return (
propExistsOnComponentDef(key) &&
(!dependsOn || !isEmpty(componentInstance[dependsOn]))
)
}
$: isLayout = assetInstance && assetInstance.favicon
$: assetDefinition = isLayout ? layoutDefinition : screenDefinition
const isDuplicateName = name => {
let duplicate = false
const lookForDuplicate = rootProps => {
walkProps(rootProps, (inst, cancel) => {
if (inst._instanceName === name && inst._id !== componentInstance._id) {
duplicate = true
cancel()
}
})
}
// check against layouts
for (let layout of $store.layouts) {
lookForDuplicate(layout.props)
}
// if viewing screen, check current screen for duplicate
if ($store.currentFrontEndType === FrontendTypes.SCREEN) {
lookForDuplicate($currentAsset.props)
} else {
// need to dedupe against all screens
for (let screen of $allScreens) {
lookForDuplicate(screen.props)
}
}
return duplicate
}
const onInstanceNameChange = (_, name) => {
if (isDuplicateName(name)) {
duplicateName = true
} else {
duplicateName = false
onChange("_instanceName", name)
}
}
</script>
<div class="settings-view-container">
{#if assetInstance}
{#each assetDefinition as def}
<PropertyControl
bindable={false}
control={def.control}
label={def.label}
key={def.key}
value={get(assetInstance, def.key)}
onChange={onScreenPropChange}
props={{ ...excludeProps(def, ['control', 'label']) }} />
{/each}
{/if}
{#if displayNameField}
<PropertyControl
control={Input}
label="Name"
key="_instanceName"
value={componentInstance._instanceName}
onChange={onInstanceNameChange} />
{#if duplicateName}
<span class="duplicate-name">Name must be unique</span>
{/if}
{/if}
{#if !isLayout && panelDefinition && panelDefinition.length > 0}
{#each panelDefinition as definition}
{#if canRenderControl(definition.key, definition.dependsOn)}
<PropertyControl
control={definition.control}
label={definition.label}
key={definition.key}
value={componentInstance[definition.key] ?? componentInstance[definition.key]?.defaultValue}
{componentInstance}
{onChange}
props={{ ...excludeProps(definition, ['control', 'label']) }} />
{/if}
{/each}
{:else}
<div class="empty">
This component doesn't have any additional settings.
</div>
{/if}
</div>
<style>
.settings-view-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
gap: var(--spacing-s);
}
.empty {
font-size: var(--font-size-xs);
margin-top: var(--spacing-m);
color: var(--grey-5);
}
.duplicate-name {
color: var(--red);
font-size: var(--font-size-xs);
position: relative;
top: -10px;
}
</style>

View File

@ -1,35 +0,0 @@
<script>
import OptionSelect from "./OptionSelect.svelte"
import { backendUiStore } from "builderStore"
import MultiOptionSelect from "./MultiOptionSelect.svelte"
export let componentInstance = {}
export let value = ""
export let onChange = () => {}
export let multiselect = false
const tables = $backendUiStore.tables
const queries = $backendUiStore.queries
let options = []
$: table =
componentInstance.datasource?.type === "table"
? tables.find(m => m._id === componentInstance.datasource.tableId)
: queries.find(query => query._id === componentInstance.datasource._id)
$: type = componentInstance.datasource.type
$: if (table) {
options =
type === "table" || type === "link" || type === "query"
? Object.keys(table.schema)
: Object.keys(table.views[componentInstance.datasource.name].schema)
}
</script>
{#if multiselect}
<MultiOptionSelect {value} {onChange} {options} />
{:else}
<OptionSelect {value} {onChange} {options} />
{/if}

View File

@ -1,97 +0,0 @@
import { isString, isUndefined, cloneDeep } from "lodash/fp"
import { TYPE_MAP } from "./types"
import { assign } from "lodash"
import { uuid } from "builderStore/uuid"
export const getBuiltin = _component => {
const { props } = createProps({ _component })
return {
_component,
name: "Screenslot",
props,
}
}
/**
* @param {object} componentDefinition - component definition from a component library
* @param {object} derivedFromProps - extra props derived from a components given props.
* @return {object} the fully created properties for the component, and any property parsing errors
*/
export const createProps = (componentDefinition, derivedFromProps) => {
const errorOccurred = (propName, error) => errors.push({ propName, error })
const props = {
_id: uuid(),
_component: componentDefinition._component,
_styles: { normal: {}, hover: {}, active: {} },
}
const errors = []
if (!componentDefinition._component) {
errorOccurred("_component", "Component name not supplied")
}
for (let propName in componentDefinition.props) {
const parsedPropDef = parsePropDef(componentDefinition.props[propName])
if (parsedPropDef.error) {
errors.push({ propName, error: parsedPropDef.error })
} else {
props[propName] = parsedPropDef
}
}
if (derivedFromProps) {
assign(props, derivedFromProps)
}
if (isUndefined(props._children)) {
props._children = []
}
return {
props,
errors,
}
}
export const makePropsSafe = (componentDefinition, props) => {
if (!componentDefinition) {
console.error(
"No component definition passed to makePropsSafe. Please check the component definition is being passed correctly."
)
}
const safeProps = createProps(componentDefinition, props).props
for (let propName in safeProps) {
props[propName] = safeProps[propName]
}
for (let propName in props) {
if (safeProps[propName] === undefined) {
delete props[propName]
}
}
if (!props._styles) {
props._styles = { normal: {}, hover: {}, active: {} }
}
return props
}
const parsePropDef = propDef => {
const error = message => ({ error: message, propDef })
if (isString(propDef)) {
if (!TYPE_MAP[propDef]) return error(`Type ${propDef} is not recognised.`)
return cloneDeep(TYPE_MAP[propDef].default)
}
const type = TYPE_MAP[propDef.type]
if (!type) return error(`Type ${propDef.type} is not recognised.`)
return cloneDeep(propDef.default)
}

View File

@ -1,10 +0,0 @@
import { isRootComponent } from "./searchComponents"
import { find } from "lodash/fp"
export const getRootComponent = (componentName, components) => {
const component = find(c => c.name === componentName)(components)
if (isRootComponent(component)) return component
return getRootComponent(component.props._component, components)
}

View File

@ -1,46 +0,0 @@
import { isUndefined, filter, some, includes } from "lodash/fp"
import { pipe } from "../../../helpers"
const normalString = s => (s || "").trim().toLowerCase()
export const isRootComponent = c =>
isComponent(c) && isUndefined(c.props._component)
export const isComponent = c => {
const hasProp = n => !isUndefined(c[n])
return hasProp("name") && hasProp("props")
}
export const searchAllComponents = (components, phrase) => {
const hasPhrase = (...vals) =>
pipe(vals, [some(v => includes(normalString(phrase))(normalString(v)))])
const componentMatches = c => {
if (hasPhrase(c._instanceName, ...(c.tags || []))) return true
if (isRootComponent(c)) return false
const parent = getExactComponent(components, c.props._component)
return componentMatches(parent)
}
return filter(componentMatches)(components)
}
export const getExactComponent = (components, name, isScreen = false) => {
return components.find(comp =>
isScreen ? comp.props._instanceName === name : comp._instanceName === name
)
}
export const getAncestorProps = (components, name, found = []) => {
const thisComponent = getExactComponent(components, name)
if (isRootComponent(thisComponent)) return [thisComponent.props, ...found]
return getAncestorProps(components, thisComponent.props._component, [
{ ...thisComponent.props },
...found,
])
}

View File

@ -1,13 +0,0 @@
import { split, last } from "lodash/fp"
import { pipe } from "../../../helpers"
export const splitName = fullname => {
const componentName = pipe(fullname, [split("/"), last])
const libName = fullname.substring(
0,
fullname.length - componentName.length - 1
)
return { libName, componentName }
}

View File

@ -1,25 +0,0 @@
export const TYPE_MAP = {
string: {
default: "",
},
bool: {
default: false,
},
number: {
default: 0,
},
options: {
default: [],
},
event: {
default: [],
},
state: {
default: {
"##bbstate": "",
},
},
tables: {
default: {},
},
}

View File

@ -3,7 +3,7 @@
import { Button } from "@budibase/bbui"
import SettingsLink from "components/settings/Link.svelte"
import ThemeEditorDropdown from "components/settings/ThemeEditorDropdown.svelte"
import FeedbackNavLink from "components/userInterface/Feedback/FeedbackNavLink.svelte"
import FeedbackNavLink from "components/feedback/FeedbackNavLink.svelte"
import { get } from "builderStore/api"
import { isActive, goto, layout } from "@sveltech/routify"

View File

@ -7,7 +7,7 @@
import Spinner from "components/common/Spinner.svelte"
import DeploymentHistory from "components/deploy/DeploymentHistory.svelte"
import analytics from "analytics"
import FeedbackIframe from "components/userInterface/Feedback/FeedbackIframe.svelte"
import FeedbackIframe from "components/feedback/FeedbackIframe.svelte"
let loading = false
let deployments = []

View File

@ -1,64 +0,0 @@
<script>
import { params, leftover, goto } from "@sveltech/routify"
import { FrontendTypes } from "constants"
import { store, allScreens } from "builderStore"
// Get any leftover params not caught by Routifys params store.
const componentIds = $leftover.split("/").filter(id => id !== "")
const currentAssetId = decodeURI($params.asset)
let assetList
let actions
// Determine screens or layouts based on the URL
if ($params.assetType === FrontendTypes.SCREEN) {
assetList = $allScreens
actions = store.actions.screens
} else {
assetList = $store.layouts
actions = store.actions.layouts
}
// select the screen or layout in the UI
actions.select(currentAssetId)
// There are leftover stuff, like IDs, so navigate the components and find the ID and select it.
if ($leftover) {
// Get the correct screen children.
const assetChildren =
assetList.find(
asset =>
asset._id === $params.asset ||
asset._id === decodeURIComponent($params.asset)
)?.props._children ?? []
findComponent(componentIds, assetChildren)
}
// }
// Find Component with ID and continue
function findComponent(ids, children) {
// Setup stuff
let componentToSelect
let currentChildren = children
// Loop through each ID
ids.forEach(id => {
// Find ID
const component = currentChildren.find(child => child._id === id)
// If it does not exist, ignore (use last valid route)
if (!component) return
componentToSelect = component
// Update childrens array to selected components children
currentChildren = componentToSelect._children
})
// Select Component!
if (componentToSelect) store.actions.components.select(componentToSelect)
}
</script>
<slot />

View File

@ -1,34 +1,134 @@
<script>
import {
store,
backendUiStore,
currentAsset,
selectedComponent,
allScreens,
} from "builderStore"
import { onMount } from "svelte"
import CurrentItemPreview from "components/userInterface/AppPreview"
import ComponentPropertiesPanel from "components/userInterface/ComponentPropertiesPanel.svelte"
import ComponentSelectionList from "components/userInterface/ComponentSelectionList.svelte"
import FrontendNavigatePane from "components/userInterface/FrontendNavigatePane.svelte"
import CurrentItemPreview from "components/design/AppPreview"
import PropertiesPanel from "components/design/PropertiesPanel/PropertiesPanel.svelte"
import ComponentSelectionList from "components/design/AppPreview/ComponentSelectionList.svelte"
import FrontendNavigatePane from "components/design/NavigationPanel/FrontendNavigatePane.svelte"
import { goto, leftover, params } from "@sveltech/routify"
import { FrontendTypes } from "constants"
import { findComponent, findComponentPath } from "builderStore/storeUtils"
import { get } from "svelte/store"
$: instance = $store.appInstance
// Cache previous values so we don't update the URL more than necessary
let previousType
let previousAsset
let previousComponentId
async function selectDatabase(database) {
backendUiStore.actions.database.select(database)
// Hydrate state from URL params
$: hydrateStateFromURL($params, $leftover)
// Keep URL in sync with state
$: updateURLFromState(
$store.currentFrontEndType,
$currentAsset,
$store.selectedComponentId
)
const hydrateStateFromURL = (params, leftover) => {
// Do nothing if no asset type, as that means we've left the page
if (!params.assetType) {
return
}
const state = get(store)
const selectedAsset = get(currentAsset)
// Hydrate asset type
let assetType = params.assetType
if (![FrontendTypes.LAYOUT, FrontendTypes.SCREEN].includes(assetType)) {
assetType = FrontendTypes.SCREEN
}
if (assetType !== state.currentFrontEndType) {
store.update(state => {
state.currentFrontEndType = assetType
return state
})
}
// Hydrate asset
const assetId = decodeURI(params.asset)
let asset
if (assetId) {
let assetList
let actions
// Determine screens or layouts based on the URL
if (assetType === FrontendTypes.SCREEN) {
assetList = get(allScreens)
actions = store.actions.screens
} else {
assetList = state.layouts
actions = store.actions.layouts
}
// Find and select the current asset
asset = assetList.find(asset => asset._id === assetId)
if (asset && asset._id !== selectedAsset?._id) {
actions.select(assetId)
}
}
// Hydrate component ID if one is present in the URL
const selectedComponentId = leftover.split("/").pop()
if (asset && selectedComponentId) {
const component = findComponent(asset.props, selectedComponentId)
if (component && component._id !== state.selectedComponentId) {
store.actions.components.select(component)
}
}
}
onMount(async () => {
if ($store.appInstance && !$backendUiStore.database) {
await selectDatabase($store.appInstance)
// Updates the route params in the URL to the specified values
const updateURLFromState = (assetType, asset, componentId) => {
// Check we have different params than last invocation
if (
assetType === previousType &&
asset === previousAsset &&
componentId === previousComponentId
) {
return
} else {
previousType = assetType
previousAsset = asset
previousComponentId = componentId
}
})
let confirmDeleteDialog
let componentToDelete = ""
// Extract current URL params
const currentParams = get(params)
const currentLeftover = get(leftover)
const paramAssetType = currentParams.assetType
const paramAssetId = currentParams.asset
const paramComponentId = currentLeftover.split("/").pop()
let settingsView
const settings = () => {
settingsView.show()
// Only update params if the params actually changed
if (
assetType !== paramAssetType ||
asset?._id !== paramAssetId ||
componentId !== paramComponentId
) {
// Build and navigate to a valid URL
let url = "../"
if ([FrontendTypes.SCREEN, FrontendTypes.LAYOUT].includes(assetType)) {
url += `${assetType}`
if (asset?._id) {
url += `/${asset._id}`
if (componentId) {
const componentPath = findComponentPath(asset.props, componentId)
const componentURL = componentPath
.slice(1)
.map(comp => comp._id)
.join("/")
url += `/${componentURL}`
}
}
}
$goto(url)
}
}
</script>
@ -49,7 +149,7 @@
{#if $selectedComponent != null}
<div class="components-pane">
<ComponentPropertiesPanel />
<PropertiesPanel />
</div>
{/if}
</div>

View File

@ -1,36 +1,50 @@
<script>
import { store, allScreens } from "builderStore"
import { get } from "svelte/store"
import { store, allScreens, selectedAccessRole } from "builderStore"
import { FrontendTypes } from "constants"
import { goto, params } from "@sveltech/routify"
import { params } from "@sveltech/routify"
// Go to first layout
if ($params.assetType === FrontendTypes.LAYOUT) {
// Try to use previously selected layout first
let id
if (
$store.selectedLayoutId &&
$store.layouts.find(layout => layout._id === $store.selectedLayoutId)
) {
id = $store.selectedLayoutId
} else {
id = $store.layouts[0]?._id
}
$goto(`../${id}`)
}
$: selectValidAsset($params.assetType)
// Go to first screen
if ($params.assetType === FrontendTypes.SCREEN) {
// Try to use previously selected layout first
// If we ever land on this index page we want to correctly update state
// to select a valid asset. The layout page will in turn update the URL
// to reflect state.
const selectValidAsset = assetType => {
let id
if (
$store.selectedScreenId &&
$allScreens.find(screen => screen._id === $store.selectedScreenId)
) {
id = $store.selectedScreenId
} else {
id = $allScreens[0]?._id
const state = get(store)
const screens = get(allScreens)
const role = get(selectedAccessRole)
// Get ID or first correct asset type and select it
if (assetType === FrontendTypes.LAYOUT) {
if (
state.selectedLayoutId &&
state.layouts.find(layout => layout._id === state.selectedLayoutId)
) {
id = state.selectedLayoutId
} else {
id = state.layouts[0]?._id
}
if (id) {
store.actions.layouts.select(id)
}
} else if (assetType === FrontendTypes.SCREEN) {
if (
state.selectedScreenId &&
screens.find(screen => screen._id === state.selectedScreenId)
) {
id = state.selectedScreenId
} else {
// Select the first screen matching the selected role ID
const filteredScreens = screens.filter(screen => {
return screen.routing?.roleId === role
})
id = filteredScreens[0]?._id
}
if (id) {
store.actions.screens.select(id)
}
}
$goto(`../${id}`)
}
</script>

View File

@ -1,163 +0,0 @@
import { createProps } from "../src/components/userInterface/assetParsing/createProps"
import { keys, some } from "lodash/fp"
import { stripStandardProps } from "./testData"
describe("createDefaultProps", () => {
const getcomponent = () => ({
_component: "some_component",
name: "some_component",
props: {
fieldName: { type: "string", default: "something" },
},
})
it("should create a object with single string value, when default string field set", () => {
const { props, errors } = createProps(getcomponent())
expect(errors).toEqual([])
expect(props.fieldName).toBeDefined()
expect(props.fieldName).toBe("something")
stripStandardProps(props)
expect(keys(props).length).toBe(3)
})
it("should set component _component", () => {
const { props, errors } = createProps(getcomponent())
expect(errors).toEqual([])
expect(props._component).toBe("some_component")
})
it("should create a object with single blank string value, when prop definition is 'string' ", () => {
const comp = getcomponent()
comp.props.fieldName = "string"
const { props, errors } = createProps(comp)
expect(errors).toEqual([])
expect(props.fieldName).toBeDefined()
expect(props.fieldName).toBe("")
})
it("should create a object with single fals value, when prop definition is 'bool' ", () => {
const comp = getcomponent()
comp.props.isVisible = "bool"
const { props, errors } = createProps(comp)
expect(errors).toEqual([])
expect(props.isVisible).toBeDefined()
expect(props.isVisible).toBe(false)
})
it("should create a object with single 0 value, when prop definition is 'number' ", () => {
const comp = getcomponent()
comp.props.width = "number"
const { props, errors } = createProps(comp)
expect(errors).toEqual([])
expect(props.width).toBeDefined()
expect(props.width).toBe(0)
})
it("should create a object with empty _children array, when children===true ", () => {
const comp = getcomponent()
comp.children = true
const { props, errors } = createProps(comp)
expect(errors).toEqual([])
expect(props._children).toBeDefined()
expect(props._children).toEqual([])
})
it("should create a object with single empty array, when prop definition is 'event' ", () => {
const comp = getcomponent()
comp.props.onClick = "event"
const { props, errors } = createProps(comp)
expect(errors).toEqual([])
expect(props.onClick).toBeDefined()
expect(props.onClick).toEqual([])
})
it("should create a object children array when children == true ", () => {
const comp = getcomponent()
comp.children = true
const { props, errors } = createProps(comp)
expect(errors).toEqual([])
expect(props._children).toBeDefined()
expect(props._children).toEqual([])
})
it("should always create _children ", () => {
const comp = getcomponent()
comp.children = false
const createRes1 = createProps(comp)
expect(createRes1.errors).toEqual([])
expect(createRes1.props._children).toBeDefined()
const comp2 = getcomponent()
comp2.children = true
const createRes2 = createProps(comp)
expect(createRes2.errors).toEqual([])
expect(createRes2.props._children).toBeDefined()
})
it("should create an object with multiple prop names", () => {
const comp = getcomponent()
comp.props.fieldName = "string"
comp.props.fieldLength = { type: "number", default: 500 }
const { props, errors } = createProps(comp)
expect(errors).toEqual([])
expect(props.fieldName).toBeDefined()
expect(props.fieldName).toBe("")
expect(props.fieldLength).toBeDefined()
expect(props.fieldLength).toBe(500)
})
it("should return error when invalid type", () => {
const comp = getcomponent()
comp.props.fieldName = "invalid type name"
comp.props.fieldLength = { type: "invalid type name " }
const { errors } = createProps(comp)
expect(errors.length).toBe(2)
expect(some(e => e.propName === "fieldName")(errors)).toBeTruthy()
expect(some(e => e.propName === "fieldLength")(errors)).toBeTruthy()
})
it("should merge in derived props", () => {
const comp = getcomponent()
comp.props.fieldName = "string"
comp.props.fieldLength = { type: "number", default: 500 }
const derivedFrom = {
fieldName: "surname",
}
const { props, errors } = createProps(comp, derivedFrom)
expect(errors.length).toBe(0)
expect(props.fieldName).toBe("surname")
expect(props.fieldLength).toBe(500)
})
it("should create standard props", () => {
const comp = getcomponent()
comp.props.fieldName = { type: "string", default: 1 }
const { props } = createProps(comp)
expect(props._styles).toBeDefined()
})
})

Some files were not shown because too many files have changed in this diff Show More