Merge branch 'master' into fix/attachment-issues

This commit is contained in:
Keviin Åberg Kultalahti 2021-01-26 12:49:27 +01:00
commit 958efd2303
155 changed files with 4051 additions and 4938 deletions

View File

@ -1,4 +1,4 @@
packages/builder/src/userInterface/CurrentItemPreview.svelte packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte
public public
dist dist
packages/server/builder 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 ## 🤖 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! 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 ## 🎓 Learning Budibase

View File

@ -63,7 +63,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.54.0", "@budibase/bbui": "^1.54.1",
"@budibase/client": "^0.5.3", "@budibase/client": "^0.5.3",
"@budibase/colorpicker": "^1.0.1", "@budibase/colorpicker": "^1.0.1",
"@budibase/string-templates": "^0.5.3", "@budibase/string-templates": "^0.5.3",

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.type === "datasource" || setting.type === "table"
})
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 { derived, writable } from "svelte/store"
import analytics from "analytics" import analytics from "analytics"
import { FrontendTypes, LAYOUT_NAMES } from "../constants" import { FrontendTypes, LAYOUT_NAMES } from "../constants"
import { makePropsSafe } from "components/userInterface/assetParsing/createProps" import { findComponent } from "./storeUtils"
export const store = getFrontendStore() export const store = getFrontendStore()
export const backendUiStore = getBackendUiStore() export const backendUiStore = getBackendUiStore()
@ -28,31 +28,10 @@ export const currentAsset = derived(store, $store => {
export const selectedComponent = derived( export const selectedComponent = derived(
[store, currentAsset], [store, currentAsset],
([$store, $currentAsset]) => { ([$store, $currentAsset]) => {
if (!$currentAsset || !$store.selectedComponentId) return null 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)
}
} }
return findComponent($currentAsset.props, $store.selectedComponentId)
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
} }
) )

View File

@ -21,12 +21,9 @@ export const fetchComponentLibDefinitions = async appId => {
*/ */
export const fetchComponentLibModules = async application => { export const fetchComponentLibModules = async application => {
const allLibraries = {} const allLibraries = {}
for (let libraryName of application.componentLibraries) { for (let libraryName of application.componentLibraries) {
const LIBRARY_URL = `/${application._id}/componentlibrary?library=${libraryName}` const LIBRARY_URL = `/${application._id}/componentlibrary?library=${libraryName}`
const libraryModule = await import(LIBRARY_URL) allLibraries[libraryName] = await import(LIBRARY_URL)
allLibraries[libraryName] = libraryModule
} }
return allLibraries 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 { get, writable } from "svelte/store"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import {
createProps,
getBuiltin,
} from "components/userInterface/assetParsing/createProps"
import { import {
allScreens, allScreens,
backendUiStore, backendUiStore,
@ -15,15 +11,10 @@ import {
} from "builderStore" } from "builderStore"
import { fetchComponentLibDefinitions } from "../loadComponentLibraries" import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
import api from "../api" import api from "../api"
import { FrontendTypes } from "../../constants" import { FrontendTypes } from "constants"
import getNewComponentName from "../getNewComponentName"
import analytics from "analytics" import analytics from "analytics"
import { import { findComponentType, findComponentParent } from "../storeUtils"
findChildComponentType, import { uuid } from "../uuid"
generateNewIdsForComponent,
getComponentDefinition,
findParent,
} from "../storeUtils"
const INITIAL_FRONTEND_STATE = { const INITIAL_FRONTEND_STATE = {
apps: [], apps: [],
@ -50,37 +41,27 @@ export const getFrontendStore = () => {
store.actions = { store.actions = {
initialise: async pkg => { initialise: async pkg => {
const { layouts, screens, application } = pkg const { layouts, screens, application } = pkg
const components = await fetchComponentLibDefinitions(application._id)
store.update(state => {
state.appId = application._id
return state
})
const components = await fetchComponentLibDefinitions(pkg.application._id)
store.update(state => ({ store.update(state => ({
...state, ...state,
libraries: pkg.application.componentLibraries, libraries: application.componentLibraries,
components, components,
name: pkg.application.name, name: application.name,
url: pkg.application.url, description: application.description,
description: pkg.application.description, appId: application._id,
appId: pkg.application._id, url: application.url,
layouts, layouts,
screens, screens,
hasAppPackage: true, hasAppPackage: true,
builtins: [getBuiltin("##builtin/screenslot")], appInstance: application.instance,
appInstance: pkg.application.instance,
})) }))
await hostingStore.actions.fetch() await hostingStore.actions.fetch()
await backendUiStore.actions.database.select(pkg.application.instance) await backendUiStore.actions.database.select(application.instance)
}, },
routing: { routing: {
fetch: async () => { fetch: async () => {
const response = await api.get("/api/routing") const response = await api.get("/api/routing")
const json = await response.json() const json = await response.json()
store.update(state => { store.update(state => {
state.routes = json.routes state.routes = json.routes
return state return state
@ -243,128 +224,231 @@ export const getFrontendStore = () => {
}, },
components: { components: {
select: component => { 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 => { store.update(state => {
state.selectedComponentId = component._id state.selectedComponentId = component._id
state.currentView = "component" state.currentView = "component"
return state return state
}) })
}, },
create: (componentToAdd, presetProps) => { getDefinition: componentName => {
const selectedAsset = get(currentAsset) 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 => { // Generate default props
function findSlot(component_array) { let props = { ...presetProps }
if (!component_array) { if (definition.settings) {
return false 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 = return {
state.components[selected._component] _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. // Create new component
let targetParent const componentInstance = store.actions.components.createInstance(
if (allowsChildren) { componentName,
// Child of the selected component presetProps
targetParent = selected )
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 { } else {
// Sibling of selected component // Otherwise we need to use the parent of this component
targetParent = findParent(selectedAsset.props, selected) 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 // Attach component
if (!targetParent) return state if (!parentComponent) {
return
// Push the new component }
targetParent._children.push(newComponent.props) if (!parentComponent._children) {
parentComponent._children = []
store.actions.preview.saveSelected() }
parentComponent._children.push(componentInstance)
// Save components and update UI
store.actions.preview.saveSelected()
store.update(state => {
state.currentView = "component" state.currentView = "component"
state.selectedComponentId = newComponent.props._id state.selectedComponentId = componentInstance._id
analytics.captureEvent("Added Component", {
name: newComponent.props._component,
})
return state 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) => { copy: (component, cut = false) => {
const selectedAsset = get(currentAsset) const selectedAsset = get(currentAsset)
if (!selectedAsset) {
return null
}
// Update store with copied component
store.update(state => { store.update(state => {
state.componentToPaste = cloneDeep(component) state.componentToPaste = cloneDeep(component)
state.componentToPaste.isCut = cut state.componentToPaste.isCut = cut
if (cut) { return state
const parent = findParent(selectedAsset.props, component._id) })
// 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( parent._children = parent._children.filter(
child => child._id !== component._id child => child._id !== component._id
) )
store.actions.components.select(parent) store.actions.components.select(parent)
} }
}
return state
})
}, },
paste: async (targetComponent, mode) => { paste: async (targetComponent, mode) => {
const selectedAsset = get(currentAsset)
let promises = [] let promises = []
store.update(state => { store.update(state => {
if (!state.componentToPaste) return state // Stop if we have nothing to paste
if (!state.componentToPaste) {
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)
return state 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) if (mode === "inside") {
const index = mode === "above" ? targetIndex : targetIndex + 1 // Paste inside target component if chosen
parent._children.splice(index, 0, cloneDeep(componentToPaste)) 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()) promises.push(store.actions.preview.saveSelected())
store.actions.components.select(componentToPaste) store.actions.components.select(componentToPaste)
return state return state
}) })
await Promise.all(promises) await Promise.all(promises)
@ -389,90 +473,56 @@ export const getFrontendStore = () => {
await store.actions.preview.saveSelected() await store.actions.preview.saveSelected()
}, },
updateProp: (name, value) => { updateProp: (name, value) => {
let component = get(selectedComponent)
if (!name || !component) {
return
}
component[name] = value
store.update(state => { store.update(state => {
let current_component = get(selectedComponent) state.selectedComponentId = component._id
current_component[name] = value
state.selectedComponentId = current_component._id
store.actions.preview.saveSelected()
return state return state
}) })
}, store.actions.preview.saveSelected()
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("/")
}, },
links: { links: {
save: async (url, title) => { save: async (url, title) => {
let promises = []
const layout = get(mainLayout) const layout = get(mainLayout)
store.update(state => { if (!layout) {
// Try to extract a nav component from the master layout return
const nav = findChildComponentType( }
layout,
"@budibase/standard-components/navigation"
)
if (nav) {
let newLink
// Clone an existing link if one exists // Find a nav bar in the main layout
if (nav._children && nav._children.length) { const nav = findComponentType(
// Clone existing link style layout.props,
newLink = cloneDeep(nav._children[0]) "@budibase/standard-components/navigation"
)
if (!nav) {
return
}
// Manipulate IDs to ensure uniqueness let newLink
generateNewIdsForComponent(newLink, state, false) if (nav._children && nav._children.length) {
// Clone an existing link if one exists
newLink = cloneDeep(nav._children[0])
// Set our new props // Set our new props
newLink._instanceName = `${title} Link` newLink._id = uuid()
newLink.url = url newLink._instanceName = `${title} Link`
newLink.text = title newLink.url = url
} else { newLink.text = title
// Otherwise create vanilla new link } else {
const component = getComponentDefinition( // Otherwise create vanilla new link
state, newLink = {
"@budibase/standard-components/link" ...store.actions.components.createInstance("link"),
) url,
const instanceId = get(backendUiStore).selectedDatabase._id text: title,
newLink = createProps(component, { _instanceName: `${title} Link`,
url,
text: title,
_instanceName: `${title} Link`,
_instanceId: instanceId,
}).props
}
// Save layout
nav._children = [...nav._children, newLink]
promises.push(store.actions.layouts.save(layout))
} }
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 emptyNewRowScreen from "./emptyNewRowScreen"
import createFromScratchScreen from "./createFromScratchScreen" import createFromScratchScreen from "./createFromScratchScreen"
import emptyRowDetailScreen from "./emptyRowDetailScreen" import emptyRowDetailScreen from "./emptyRowDetailScreen"
import { generateNewIdsForComponent } from "../../storeUtils"
import { uuid } from "builderStore/uuid"
const allTemplates = tables => [ const allTemplates = tables => [
createFromScratchScreen, createFromScratchScreen,
@ -16,13 +14,9 @@ const allTemplates = tables => [
emptyRowDetailScreen, 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 createTemplateOverride = (frontendState, create) => () => {
const screen = 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.name = screen.props._id
screen.routing.route = screen.routing.route.toLowerCase() screen.routing.route = screen.routing.route.toLowerCase()
return screen return screen

View File

@ -21,26 +21,29 @@ export default function(tables) {
export const newRowUrl = table => sanitizeUrl(`/${table.name}/new/row`) export const newRowUrl = table => sanitizeUrl(`/${table.name}/new/row`)
export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE" export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE"
function generateTitleContainer(table) { function generateTitleContainer(table, providerId) {
return makeTitleContainer("New Row").addChild(makeSaveButton(table)) return makeTitleContainer("New Row").addChild(
makeSaveButton(table, providerId)
)
} }
const createScreen = table => { const createScreen = table => {
const dataform = new Component( const screen = new Screen()
"@budibase/standard-components/dataformwide"
).instanceName("Form")
const container = makeMainContainer()
.addChild(makeBreadcrumbContainer(table.name, "New"))
.addChild(generateTitleContainer(table))
.addChild(dataform)
return new Screen()
.component("@budibase/standard-components/newrow") .component("@budibase/standard-components/newrow")
.table(table._id) .table(table._id)
.route(newRowUrl(table)) .route(newRowUrl(table))
.instanceName(`${table.name} - New`) .instanceName(`${table.name} - New`)
.name("") .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 ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE"
export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`) 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 // have to override style for this, its missing margin
const saveButton = makeSaveButton(table).normalStyle({ const saveButton = makeSaveButton(table, providerId).normalStyle({
background: "#000000", background: "#000000",
"border-width": "0", "border-width": "0",
"border-style": "None", "border-style": "None",
@ -60,8 +60,8 @@ function generateTitleContainer(table, title) {
onClick: [ onClick: [
{ {
parameters: { parameters: {
rowId: "{{ data._id }}", rowId: `{{ ${providerId}._id }}`,
revId: "{{ data._rev }}", revId: `{{ ${providerId}._rev }}`,
tableId: table._id, tableId: table._id,
}, },
"##eventHandlerType": "Delete Row", "##eventHandlerType": "Delete Row",
@ -82,21 +82,22 @@ function generateTitleContainer(table, title) {
} }
const createScreen = (table, heading) => { const createScreen = (table, heading) => {
const dataform = new Component( const screen = new Screen()
"@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()
.component("@budibase/standard-components/rowdetail") .component("@budibase/standard-components/rowdetail")
.table(table._id) .table(table._id)
.instanceName(`${table.name} - Detail`) .instanceName(`${table.name} - Detail`)
.route(rowDetailUrl(table)) .route(rowDetailUrl(table))
.name("") .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" import { BaseStructure } from "./BaseStructure"
export class Component extends BaseStructure { export class Component extends BaseStructure {
@ -6,7 +6,7 @@ export class Component extends BaseStructure {
super(false) super(false)
this._children = [] this._children = []
this._json = { this._json = {
_id: v4(), _id: uuid(),
_component: name, _component: name,
_styles: { _styles: {
normal: {}, normal: {},

View File

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

View File

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

View File

@ -1,80 +1,105 @@
import { getBuiltin } from "components/userInterface/assetParsing/createProps" /**
import { uuid } from "./uuid" * Recursively searches for a specific component ID
import getNewComponentName from "./getNewComponentName" */
export const findComponent = (rootComponent, id) => {
return searchComponentTree(rootComponent, comp => comp._id === id)
}
/** /**
* Find the parent component of the passed in child. * Recursively searches for a specific component type
* @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
*/ */
export const findParent = (rootProps, child) => { export const findComponentType = (rootComponent, type) => {
let parent return searchComponentTree(rootComponent, comp => comp._component === type)
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 walkProps = (props, action, cancelToken = null) => { /**
cancelToken = cancelToken || { cancelled: false } * Recursively searches for the parent component of a specific component ID
action(props, () => { */
cancelToken.cancelled = true export const findComponentParent = (rootComponent, id, parentComponent) => {
}) if (!rootComponent || !id) {
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) {
return null return null
} }
if (rootComponent._id === id) {
// Stop recursion if this element matches return parentComponent
if (node._component === typeToFind) {
return node
} }
if (!rootComponent._children) {
// 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) {
return null return null
} }
for (const child of rootComponent._children) {
// Recurse and check each child component const childResult = findComponentParent(child, id, rootComponent)
for (let child of children) { if (childResult) {
const childResult = findChildComponentType(child, typeToFind) 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) { if (childResult) {
return childResult return childResult
} }
} }
// If we reach here then no children were valid
return null return null
} }

View File

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

View File

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

View File

@ -103,6 +103,15 @@
opacity: 1; 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 { .sort-icon {
position: relative; position: relative;
top: 2px; top: 2px;

View File

@ -43,8 +43,8 @@
<div class="datasource-icon" slot="icon"> <div class="datasource-icon" slot="icon">
<svelte:component <svelte:component
this={ICONS[datasource.source]} this={ICONS[datasource.source]}
height="15" height="18"
width="15" /> width="18" />
</div> </div>
<EditDatasourcePopover {datasource} /> <EditDatasourcePopover {datasource} />
</NavItem> </NavItem>
@ -61,3 +61,10 @@
{/each} {/each}
</div> </div>
{/if} {/if}
<style>
.datasource-icon {
margin-right: 3px;
padding-top: 3px;
}
</style>

View File

@ -7,10 +7,9 @@
<form> <form>
{#each Object.keys(integration) as configKey} {#each Object.keys(integration) as configKey}
<Input <Input
thin
type={integration[configKey].type} type={integration[configKey].type}
label={configKey} label={configKey}
bind:value={integration[configKey]} /> bind:value={integration[configKey]} />
<Spacer medium /> <Spacer large />
{/each} {/each}
</form> </form>

View File

@ -0,0 +1,42 @@
<script>
import { backendUiStore, store, allScreens } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Input, TextButton, Icon } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
export let bindable
export let parameters
let anchor
let dropdown
let confirmDeleteDialog
function hideEditor() {
dropdown?.hide()
}
</script>
<div on:click|stopPropagation bind:this={anchor}>
<TextButton
text
on:click={dropdown.show}
active={false}>
<Icon name="add" />
Add Parameters
</TextButton>
<DropdownMenu align="right" {anchor} bind:this={dropdown}>
<div class="wrapper">
<ParameterBuilder bind:parameters {bindable} />
</div>
</DropdownMenu>
</div>
<style>
.wrapper {
padding: var(--spacing-xl);
min-width: 600px;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,9 +24,7 @@
$: selectedScreen = $currentAsset $: selectedScreen = $currentAsset
const changeScreen = screenId => { const changeScreen = screenId => {
// select the route
store.actions.screens.select(screenId) store.actions.screens.select(screenId)
$goto(`./${screenId}`)
} }
</script> </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 { store as frontendStore } from "builderStore"
import { findComponentPath } from "builderStore/storeUtils"
export const DropEffect = { export const DropEffect = {
MOVE: "move", MOVE: "move",
@ -72,19 +73,30 @@ export default function() {
}) })
}, },
drop: () => { drop: () => {
store.update(state => { const state = get(store)
if (state.targetComponent !== state.dragged) {
frontendStore.actions.components.copy(state.dragged, true)
frontendStore.actions.components.paste(
state.targetComponent,
state.dropPosition
)
}
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, selectedAccessRole,
} from "builderStore" } from "builderStore"
import { FrontendTypes } from "constants" import { FrontendTypes } from "constants"
import ComponentNavigationTree from "components/userInterface/ComponentNavigationTree/index.svelte" import ComponentNavigationTree from "components/design/NavigationPanel/ComponentNavigationTree/index.svelte"
import Layout from "components/userInterface/Layout.svelte" import Layout from "components/design/NavigationPanel/Layout.svelte"
import NewScreenModal from "components/userInterface/NewScreenModal.svelte" import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte"
import NewLayoutModal from "components/userInterface/NewLayoutModal.svelte" import NewLayoutModal from "components/design/NavigationPanel/NewLayoutModal.svelte"
import { Modal, Switcher, Select } from "@budibase/bbui" import { Modal, Switcher, Select } from "@budibase/bbui"
const tabs = [ const tabs = [
@ -28,7 +28,7 @@
let modal let modal
let routes = {} let routes = {}
let tab = $params.assetType $: tab = $params.assetType
const navigate = ({ detail }) => { const navigate = ({ detail }) => {
if (!detail) { if (!detail) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let value export let value = []
export let name export let name
let drawer let drawer
@ -57,12 +57,12 @@
} }
</script> </script>
<Button secondary small on:click={drawer.show}>Define Actions</Button> <Button secondary wide on:click={drawer.show}>Define Actions</Button>
<Drawer bind:this={drawer} title={'Actions'}> <Drawer bind:this={drawer} title={'Actions'}>
<heading slot="buttons"> <heading slot="buttons">
<Button thin blue on:click={saveEventData}>Save</Button> <Button thin blue on:click={saveEventData}>Save</Button>
</heading> </heading>
<div slot="body"> <div slot="body">
<EventEditor event={value} eventType={name} /> <EventEditor bind:actions={value} eventType={name} />
</div> </div>
</Drawer> </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> <script>
import { Select, Label, Spacer } from "@budibase/bbui" import { Select, Label, Spacer } from "@budibase/bbui"
import { store, backendUiStore, currentAsset } from "builderStore" import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties" import { getBindableProperties } from "builderStore/dataBinding"
import ParameterBuilder from "../../../integration/QueryParameterBuilder.svelte" import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
export let parameters export let parameters
$: datasource = $backendUiStore.datasources.find( $: datasource = $backendUiStore.datasources.find(
ds => ds._id === parameters.datasourceId ds => ds._id === parameters.datasourceId
) )
// TODO: binding needs to be centralised $: bindableProperties = getBindableProperties(
$: bindableProperties = fetchBindableProperties({ $currentAsset.props,
componentInstanceId: $store.selectedComponentId, $store.selectedComponentId
components: $store.components, ).map(property => ({
screen: $currentAsset,
tables: $backendUiStore.tables,
queries: $backendUiStore.queries,
}).map(property => ({
...property, ...property,
category: property.type === "instance" ? "Component" : "Table", category: property.type === "instance" ? "Component" : "Table",
label: property.readableBinding, label: property.readableBinding,

View File

@ -1,5 +1,4 @@
<script> <script>
// accepts an array of field names, and outputs an object of { FieldName: value }
import { import {
DataList, DataList,
Label, Label,
@ -8,13 +7,13 @@
Select, Select,
Input, Input,
} from "@budibase/bbui" } from "@budibase/bbui"
import { store, backendUiStore, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import { CloseCircleIcon, AddIcon } from "components/common/Icons"
import { import {
getBindableProperties,
readableToRuntimeBinding, readableToRuntimeBinding,
runtimeToReadableBinding, runtimeToReadableBinding,
} from "builderStore/replaceBindings" } from "builderStore/dataBinding"
import { CloseCircleIcon, AddIcon } from "components/common/Icons"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -25,6 +24,11 @@
const emptyField = () => ({ name: "", value: "" }) const emptyField = () => ({ name: "", value: "" })
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$store.selectedComponentId
)
// this statement initialises fields from parameters.fields // this statement initialises fields from parameters.fields
$: fields = $: 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 addField = () => {
const newFields = fields.filter(f => f.name) const newFields = fields.filter(f => f.name)
newFields.push(emptyField()) 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} {/if}
<SaveFields <SaveFields
parameterFields={parameters.fields}
schemaFields={automationStatus === AUTOMATION_STATUS.EXISTING && selectedAutomation && selectedAutomation.schema} schemaFields={automationStatus === AUTOMATION_STATUS.EXISTING && selectedAutomation && selectedAutomation.schema}
fieldLabel="Field" fieldLabel="Field"
on:fieldschanged={onFieldsChanged} /> 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> <script>
import { store, currentAsset } from "builderStore" import { store } from "builderStore"
import { Select } from "@budibase/bbui" import { Select } from "@budibase/bbui"
export let value export let value

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
<script> <script>
import { excludeProps } from "./propertyCategories.js"
import PropertyControl from "./PropertyControl.svelte" import PropertyControl from "./PropertyControl.svelte"
import { DetailSummary } from "@budibase/bbui" import { DetailSummary } from "@budibase/bbui"
@ -10,20 +9,17 @@
export let onStyleChanged = () => {} export let onStyleChanged = () => {}
export let open = false export let open = false
$: style = componentInstance["_styles"][styleCategory] || {}
$: changed = properties.some(prop => hasPropChanged(style, prop))
const 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] !== "" return style[prop.key] != null && style[prop.key] !== ""
} }
$: style = componentInstance["_styles"][styleCategory] || {} const getControlProps = props => {
$: changed = properties.some(prop => hasPropChanged(style, prop)) const { label, key, control, ...otherProps } = props || {}
return otherProps || {}
}
</script> </script>
<DetailSummary name={`${name}${changed ? ' *' : ''}`} on:open show={open} thin> <DetailSummary name={`${name}${changed ? ' *' : ''}`} on:open show={open} thin>
@ -31,12 +27,13 @@
<div> <div>
{#each properties as prop} {#each properties as prop}
<PropertyControl <PropertyControl
bindable={false}
label={`${prop.label}${hasPropChanged(style, prop) ? ' *' : ''}`} label={`${prop.label}${hasPropChanged(style, prop) ? ' *' : ''}`}
control={prop.control} control={prop.control}
key={prop.key} key={prop.key}
value={style[prop.key]} value={style[prop.key]}
onChange={(key, value) => onStyleChanged(styleCategory, key, value)} onChange={value => onStyleChanged(styleCategory, prop.key, value)}
props={{ ...excludeProps(prop, ['control', 'label']) }} /> props={getControlProps(prop)} />
{/each} {/each}
</div> </div>
{/if} {/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).schema
$: options = Object.keys(schema || {})
</script>
{#if multiselect}
<MultiOptionSelect {value} {onChange} {options} />
{:else}
<OptionSelect {value} {onChange} {options} />
{/if}

View File

@ -1,4 +1,5 @@
<script> <script>
import { getBindableProperties } from "builderStore/dataBinding"
import { import {
Button, Button,
Icon, Icon,
@ -12,7 +13,6 @@
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte" import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
import IntegrationQueryEditor from "components/integration/index.svelte" import IntegrationQueryEditor from "components/integration/index.svelte"
import fetchBindableProperties from "../../builderStore/fetchBindableProperties"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let anchorRight, dropdownRight let anchorRight, dropdownRight
@ -22,11 +22,9 @@
$: tables = $backendUiStore.tables.map(m => ({ $: tables = $backendUiStore.tables.map(m => ({
label: m.name, label: m.name,
name: `all_${m._id}`,
tableId: m._id, tableId: m._id,
type: "table", type: "table",
})) }))
$: views = $backendUiStore.tables.reduce((acc, cur) => { $: views = $backendUiStore.tables.reduce((acc, cur) => {
let viewsArr = Object.entries(cur.views).map(([key, value]) => ({ let viewsArr = Object.entries(cur.views).map(([key, value]) => ({
label: key, label: key,
@ -36,41 +34,38 @@
})) }))
return [...acc, ...viewsArr] return [...acc, ...viewsArr]
}, []) }, [])
$: queries = $backendUiStore.queries.map(query => ({ $: queries = $backendUiStore.queries.map(query => ({
label: query.name, label: query.name,
name: query.name, name: query.name,
tableId: query._id,
...query, ...query,
schema: query.schema, schema: query.schema,
parameters: query.parameters, parameters: query.parameters,
type: "query", type: "query",
})) }))
$: bindableProperties = getBindableProperties(
$: bindableProperties = fetchBindableProperties({ $currentAsset.props,
componentInstanceId: $store.selectedComponentId, $store.selectedComponentId
components: $store.components, )
screen: $currentAsset,
tables: $backendUiStore.tables,
queries: $backendUiStore.queries,
})
$: queryBindableProperties = bindableProperties.map(property => ({ $: queryBindableProperties = bindableProperties.map(property => ({
...property, ...property,
category: property.type === "instance" ? "Component" : "Table", category: property.type === "instance" ? "Component" : "Table",
label: property.readableBinding, label: property.readableBinding,
path: property.readableBinding, path: property.readableBinding,
})) }))
$: links = bindableProperties $: links = bindableProperties
.filter(x => x.fieldSchema?.type === "link") .filter(x => x.fieldSchema?.type === "link")
.map(property => { .map(property => {
return { return {
providerId: property.instance._id, providerId: property.providerId,
label: property.readableBinding, label: property.readableBinding,
fieldName: property.fieldSchema.name, fieldName: property.fieldSchema.name,
name: `all_${property.fieldSchema.tableId}`,
tableId: property.fieldSchema.tableId, tableId: property.fieldSchema.tableId,
type: "link", 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 }}`,
} }
}) })
@ -83,9 +78,7 @@
const source = $backendUiStore.datasources.find( const source = $backendUiStore.datasources.find(
ds => ds._id === query.datasourceId ds => ds._id === query.datasourceId
).source ).source
return $backendUiStore.integrations[source].query[query.queryVerb][ return $backendUiStore.integrations[source].query[query.queryVerb]
query.queryType
]
} }
</script> </script>
@ -93,12 +86,12 @@
class="dropdownbutton" class="dropdownbutton"
bind:this={anchorRight} bind:this={anchorRight}
on:click={dropdownRight.show}> on:click={dropdownRight.show}>
<span>{value.label ? value.label : 'Table / View / Query'}</span> <span>{value?.label ? value.label : 'Choose option'}</span>
<Icon name="arrowdown" /> <Icon name="arrowdown" />
</div> </div>
{#if value.type === 'query'} {#if value?.type === 'query'}
<i class="ri-settings-5-line" on:click={drawer.show} /> <i class="ri-settings-5-line" on:click={drawer.show} />
<Drawer title={'Query'}> <Drawer title={'Query'} bind:this={drawer}>
<div slot="buttons"> <div slot="buttons">
<Button <Button
blue blue

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

@ -155,8 +155,9 @@
} }
:global(.CodeMirror) { :global(.CodeMirror) {
height: auto !important; height: 500px !important;
border-radius: var(--border-radius-m); border-radius: var(--border-radius-s);
font-family: var(--font-sans) !important; font-family: monospace !important;
line-height: 1.3;
} }
</style> </style>

View File

@ -24,18 +24,18 @@
</script> </script>
<form on:submit|preventDefault> <form on:submit|preventDefault>
<div class="field">
{#each schemaKeys as field} {#each schemaKeys as field}
<Label extraSmall grey>{field}</Label>
<div class="field">
<Input <Input
placeholder="Enter {field} name"
outline
disabled={!editable} disabled={!editable}
type={schema.fields[field]?.type} type={schema.fields[field]?.type}
required={schema.fields[field]?.required} required={schema.fields[field]?.required}
bind:value={fields[field]} /> bind:value={fields[field]} />
</div>
{/each} {/each}
</div>
</form> </form>
<Label extraSmall grey>Data</Label>
{#if schema.customisable} {#if schema.customisable}
<Editor <Editor
label="Query" label="Query"
@ -49,7 +49,7 @@
.field { .field {
margin-bottom: var(--spacing-m); margin-bottom: var(--spacing-m);
display: grid; display: grid;
grid-template-columns: 1fr 2%; grid-template-columns: 1fr 1fr;
grid-gap: var(--spacing-m); grid-gap: var(--spacing-m);
align-items: center; align-items: center;
} }

View File

@ -1,10 +1,10 @@
<script> <script>
import { Button, TextArea, Label, Input, Heading } from "@budibase/bbui" import { Button, Input, Heading, Spacer } from "@budibase/bbui"
import BindableInput from "components/userInterface/BindableInput.svelte" import BindableInput from "components/common/BindableInput.svelte"
import { import {
readableToRuntimeBinding, readableToRuntimeBinding,
runtimeToReadableBinding, runtimeToReadableBinding,
} from "builderStore/replaceBindings" } from "builderStore/dataBinding"
export let bindable = true export let bindable = true
export let parameters = [] export let parameters = []
@ -31,23 +31,26 @@
<section> <section>
<Heading extraSmall black>Parameters</Heading> <Heading extraSmall black>Parameters</Heading>
<Spacer large />
<div class="parameters" class:bindable> <div class="parameters" class:bindable>
<Label extraSmall grey>Parameter Name</Label>
<Label extraSmall grey>Default</Label>
{#if bindable}
<Label extraSmall grey>Value</Label>
{:else}
<div />
{/if}
{#each parameters as parameter, idx} {#each parameters as parameter, idx}
<Input thin disabled={bindable} bind:value={parameter.name} /> <Input
<Input thin disabled={bindable} bind:value={parameter.default} /> placeholder="Parameter Name"
thin
disabled={bindable}
bind:value={parameter.name} />
<Input
placeholder="Default"
thin
disabled={bindable}
bind:value={parameter.default} />
{#if bindable} {#if bindable}
<BindableInput <BindableInput
placeholder="Value"
type="string" type="string"
thin thin
on:change={evt => onBindingChange(parameter.name, evt.detail)} on:change={evt => onBindingChange(parameter.name, evt.detail)}
value={runtimeToReadableBinding(bindings, customParams[parameter.name])} value={runtimeToReadableBinding(bindings, customParams?.[parameter.name])}
{bindings} /> {bindings} />
{:else} {:else}
<i <i
@ -57,9 +60,7 @@
{/each} {/each}
</div> </div>
{#if !bindable} {#if !bindable}
<Button thin secondary small on:click={newQueryParameter}> <Button secondary on:click={newQueryParameter}>Add Parameter</Button>
Add Parameter
</Button>
{/if} {/if}
</section> </section>

View File

@ -16,6 +16,7 @@
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
import IntegrationQueryEditor from "components/integration/index.svelte" import IntegrationQueryEditor from "components/integration/index.svelte"
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte" import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
import EditQueryParamsPopover from "components/backend/DatasourceNavigator/popovers/EditQueryParamsPopover.svelte"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
const PREVIEW_HEADINGS = [ const PREVIEW_HEADINGS = [
@ -40,6 +41,7 @@
let tab = "JSON" let tab = "JSON"
let parameters let parameters
let data = [] let data = []
let popover
$: datasource = $backendUiStore.datasources.find( $: datasource = $backendUiStore.datasources.find(
ds => ds._id === query.datasourceId ds => ds._id === query.datasourceId
@ -61,7 +63,7 @@
$: config = $backendUiStore.integrations[datasourceType]?.query $: config = $backendUiStore.integrations[datasourceType]?.query
$: docsLink = $backendUiStore.integrations[datasourceType]?.docs $: docsLink = $backendUiStore.integrations[datasourceType]?.docs
$: shouldShowQueryConfig = config && query.queryVerb && query.queryType $: shouldShowQueryConfig = config && query.queryVerb
function newField() { function newField() {
fields = [...fields, {}] fields = [...fields, {}]
@ -129,62 +131,44 @@
</script> </script>
<header> <header>
<Heading small>{query.name}</Heading> <div class="input">
<Input placeholder="✎ Edit Query Name" bind:value={query.name} />
</div>
{#if config} {#if config}
<div class="queryVerbs"> <div class="props">
{#each Object.keys(config) as queryVerb} <div class="query-type">Query type: <span class="query-type-span">{config[query.queryVerb].type}</span></div>
<div <div class="select">
class="queryVerb" <Select primary thin bind:value={query.queryVerb}>
class:selected={queryVerb === query.queryVerb} {#each Object.keys(config) as queryVerb}
on:click={() => { <option value={queryVerb}>{queryVerb}</option>
query.queryVerb = queryVerb
}}>
{queryVerb}
</div>
{/each}
</div>
{#if query.queryVerb}
<Select thin secondary bind:value={query.queryType}>
<option value={''}>Select an option</option>
{#each Object.keys(config[query.queryVerb]) as queryType}
<option value={queryType}>{queryType}</option>
{/each} {/each}
</Select> </Select>
{/if} </div>
<Spacer medium /> </div>
<Button primary href={docsLink} target="_blank"> <EditQueryParamsPopover bind:parameters={query.parameters} bindable={false} />
<i class="ri-book-2-line" />
</Button>
{/if} {/if}
</header> </header>
<Spacer extraLarge />
<Spacer large />
{#if shouldShowQueryConfig} {#if shouldShowQueryConfig}
<section> <section>
<div class="config"> <div class="config">
<Label extraSmall grey>Query Name</Label>
<Input thin bind:value={query.name} />
<Spacer medium />
<IntegrationQueryEditor <IntegrationQueryEditor
{query} {query}
schema={config[query.queryVerb][query.queryType]} schema={config[query.queryVerb]}
bind:parameters /> bind:parameters />
<Spacer medium /> <Spacer extraLarge />
<Spacer large />
<div class="viewer-controls"> <div class="viewer-controls">
<Button <Button
wide
thin
blue blue
disabled={data.length === 0} disabled={data.length === 0}
on:click={saveQuery}> on:click={saveQuery}>
Save Save Query
</Button> </Button>
<Button wide thin primary on:click={previewQuery}>Run</Button> <Button primary on:click={previewQuery}>Run Query</Button>
</div> </div>
<section class="viewer"> <section class="viewer">
@ -196,10 +180,11 @@
<ExternalDataSourceTable {query} {data} /> <ExternalDataSourceTable {query} {data} />
{:else if tab === 'SCHEMA'} {:else if tab === 'SCHEMA'}
{#each fields as field, idx} {#each fields as field, idx}
<Spacer small />
<div class="field"> <div class="field">
<Input thin type={'text'} bind:value={field.name} /> <Input outline placeholder="Field Name" type={'text'} bind:value={field.name} />
<Select secondary thin bind:value={field.type}> <Select thin border bind:value={field.type}>
<option value={''}>Select an option</option> <option value={''}>Select a field type</option>
<option value={'STRING'}>Text</option> <option value={'STRING'}>Text</option>
<option value={'NUMBER'}>Number</option> <option value={'NUMBER'}>Number</option>
<option value={'BOOLEAN'}>Boolean</option> <option value={'BOOLEAN'}>Boolean</option>
@ -210,7 +195,8 @@
on:click={() => deleteField(idx)} /> on:click={() => deleteField(idx)} />
</div> </div>
{/each} {/each}
<Button thin secondary on:click={newField}>Add Field</Button> <Spacer small />
<Button thin secondary on:click={newField}>Add Field</Button>
{/if} {/if}
</Switcher> </Switcher>
{/if} {/if}
@ -220,11 +206,28 @@
{/if} {/if}
<style> <style>
.input {
width: 300px;
}
.select {
width: 200px;
margin-right: 40px;
}
.props {
display: flex;
flex-direction: row;
margin-left: auto;
align-items: center;
gap: var(--layout-l);
}
.field { .field {
display: grid; display: grid;
grid-gap: 10px;
grid-template-columns: 1fr 1fr 50px; grid-template-columns: 1fr 1fr 50px;
margin-bottom: var(--spacing-m); gap: var(--spacing-l);
} }
a { a {
@ -240,6 +243,16 @@
cursor: pointer; cursor: pointer;
} }
.query-type {
font-family: var(--font-sans);
color: var(--grey-8);
font-size: var(--font-size-s);
}
.query-type-span {
text-transform: uppercase;
}
.preview { .preview {
width: 800px; width: 800px;
height: 100%; height: 100%;
@ -253,32 +266,18 @@
align-items: center; align-items: center;
} }
.queryVerbs {
display: flex;
flex: 1;
font-size: var(--font-size-m);
align-items: center;
margin-left: var(--spacing-l);
}
.queryVerb {
text-transform: capitalize;
margin-right: var(--spacing-m);
color: var(--grey-5);
cursor: pointer;
}
.selected {
color: var(--white);
font-weight: 500;
}
.viewer-controls { .viewer-controls {
display: grid; display: flex;
grid-gap: var(--spacing-m); flex-direction: row;
grid-auto-flow: column; margin-left: auto;
direction: rtl; direction: rtl;
grid-template-columns: 10% 10% 1fr; z-index: 5;
margin-bottom: var(--spacing-m); gap: var(--spacing-m);
min-width: 150px;
}
.viewer {
margin-top: -28px;
z-index: -2;
} }
</style> </style>

View File

@ -20,13 +20,6 @@
} }
</script> </script>
{#if editable}
<ParameterBuilder bind:parameters={query.parameters} bindable={false} />
<Spacer large />
{/if}
<Heading extraSmall black>Query</Heading>
<Spacer medium />
{#if schema} {#if schema}
{#key query._id} {#key query._id}
@ -38,7 +31,6 @@
readOnly={!editable} readOnly={!editable}
value={query.fields.sql} /> value={query.fields.sql} />
{:else if schema.type === QueryTypes.JSON} {:else if schema.type === QueryTypes.JSON}
<Spacer large />
<Editor <Editor
label="Query" label="Query"
mode="json" mode="json"

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 }
}

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