Merge branch 'master' into fix/attachment-issues
This commit is contained in:
commit
958efd2303
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: {},
|
||||||
|
|
|
@ -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: {},
|
||||||
|
|
|
@ -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",
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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()
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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()
|
||||||
|
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
|
@ -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) => {
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
|
@ -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>
|
||||||
|
|
|
@ -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}.`)
|
|
@ -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) => {
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
|
@ -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} />
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script>
|
||||||
|
import Checkbox from "components/common/Checkbox.svelte"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Checkbox checked={value} on:change />
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script>
|
||||||
|
import Colorpicker from "@budibase/colorpicker"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Colorpicker value={value || '#000'} on:change />
|
|
@ -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
|
|
@ -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>
|
|
@ -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>
|
|
@ -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,
|
|
@ -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())
|
|
@ -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>
|
|
@ -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} />
|
|
@ -0,0 +1,2 @@
|
||||||
|
import EventsEditor from "./EventPropertyControl.svelte"
|
||||||
|
export default EventsEditor
|
|
@ -0,0 +1,2 @@
|
||||||
|
import FlatButtonGroup from "./FlatButtonGroup.svelte"
|
||||||
|
export default FlatButtonGroup
|
|
@ -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
|
|
@ -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)
|
|
@ -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 = ""
|
|
@ -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} />
|
|
@ -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}
|
|
@ -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>
|
|
@ -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}
|
|
@ -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
|
|
@ -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>
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
])
|
|
||||||
}
|
|
|
@ -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
Loading…
Reference in New Issue