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
|
||||
dist
|
||||
packages/server/builder
|
||||
|
|
|
@ -103,13 +103,9 @@ The Budibase builder runs in Electron, on Mac, PC and Linux. Follow the steps be
|
|||
|
||||
## 🤖 Self-hosting
|
||||
|
||||
<p align="center">
|
||||
<img src="https://i.imgur.com/Z52cEvT.png?1" />
|
||||
</p>
|
||||
|
||||
Budibase wants to make sure anyone can use the tools we develop and we know a lot of people need to be able to host the apps they make on their own systems - that is why we've decided to try and make self hosting as easy as possible!
|
||||
|
||||
Currently, you can host your apps using Docker. The documentation for self-hosting can be found [here](https://docs.budibase.com/self-hosting/introduction-to-self-hosting).
|
||||
Currently, you can host your apps using Docker or Digital Ocean. The documentation for self-hosting can be found [here](https://docs.budibase.com/self-hosting/introduction-to-self-hosting).
|
||||
|
||||
|
||||
## 🎓 Learning Budibase
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.54.0",
|
||||
"@budibase/bbui": "^1.54.1",
|
||||
"@budibase/client": "^0.5.3",
|
||||
"@budibase/colorpicker": "^1.0.1",
|
||||
"@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 analytics from "analytics"
|
||||
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
|
||||
import { makePropsSafe } from "components/userInterface/assetParsing/createProps"
|
||||
import { findComponent } from "./storeUtils"
|
||||
|
||||
export const store = getFrontendStore()
|
||||
export const backendUiStore = getBackendUiStore()
|
||||
|
@ -28,31 +28,10 @@ export const currentAsset = derived(store, $store => {
|
|||
export const selectedComponent = derived(
|
||||
[store, currentAsset],
|
||||
([$store, $currentAsset]) => {
|
||||
if (!$currentAsset || !$store.selectedComponentId) return null
|
||||
|
||||
function traverse(node, callback) {
|
||||
if (node._id === $store.selectedComponentId) return callback(node)
|
||||
|
||||
if (node._children) {
|
||||
node._children.forEach(child => traverse(child, callback))
|
||||
if (!$currentAsset || !$store.selectedComponentId) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (node.props) {
|
||||
traverse(node.props, callback)
|
||||
}
|
||||
}
|
||||
|
||||
let component
|
||||
traverse($currentAsset, found => {
|
||||
const componentIdentifier = found._component ?? found.props._component
|
||||
const componentDef = componentIdentifier.startsWith("##")
|
||||
? found
|
||||
: $store.components[componentIdentifier]
|
||||
|
||||
component = makePropsSafe(componentDef, found)
|
||||
})
|
||||
|
||||
return component
|
||||
return findComponent($currentAsset.props, $store.selectedComponentId)
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -21,12 +21,9 @@ export const fetchComponentLibDefinitions = async appId => {
|
|||
*/
|
||||
export const fetchComponentLibModules = async application => {
|
||||
const allLibraries = {}
|
||||
|
||||
for (let libraryName of application.componentLibraries) {
|
||||
const LIBRARY_URL = `/${application._id}/componentlibrary?library=${libraryName}`
|
||||
const libraryModule = await import(LIBRARY_URL)
|
||||
allLibraries[libraryName] = libraryModule
|
||||
allLibraries[libraryName] = await import(LIBRARY_URL)
|
||||
}
|
||||
|
||||
return allLibraries
|
||||
}
|
||||
|
|
|
@ -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 { cloneDeep } from "lodash/fp"
|
||||
import {
|
||||
createProps,
|
||||
getBuiltin,
|
||||
} from "components/userInterface/assetParsing/createProps"
|
||||
import {
|
||||
allScreens,
|
||||
backendUiStore,
|
||||
|
@ -15,15 +11,10 @@ import {
|
|||
} from "builderStore"
|
||||
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
|
||||
import api from "../api"
|
||||
import { FrontendTypes } from "../../constants"
|
||||
import getNewComponentName from "../getNewComponentName"
|
||||
import { FrontendTypes } from "constants"
|
||||
import analytics from "analytics"
|
||||
import {
|
||||
findChildComponentType,
|
||||
generateNewIdsForComponent,
|
||||
getComponentDefinition,
|
||||
findParent,
|
||||
} from "../storeUtils"
|
||||
import { findComponentType, findComponentParent } from "../storeUtils"
|
||||
import { uuid } from "../uuid"
|
||||
|
||||
const INITIAL_FRONTEND_STATE = {
|
||||
apps: [],
|
||||
|
@ -50,37 +41,27 @@ export const getFrontendStore = () => {
|
|||
store.actions = {
|
||||
initialise: async pkg => {
|
||||
const { layouts, screens, application } = pkg
|
||||
|
||||
store.update(state => {
|
||||
state.appId = application._id
|
||||
return state
|
||||
})
|
||||
|
||||
const components = await fetchComponentLibDefinitions(pkg.application._id)
|
||||
|
||||
const components = await fetchComponentLibDefinitions(application._id)
|
||||
store.update(state => ({
|
||||
...state,
|
||||
libraries: pkg.application.componentLibraries,
|
||||
libraries: application.componentLibraries,
|
||||
components,
|
||||
name: pkg.application.name,
|
||||
url: pkg.application.url,
|
||||
description: pkg.application.description,
|
||||
appId: pkg.application._id,
|
||||
name: application.name,
|
||||
description: application.description,
|
||||
appId: application._id,
|
||||
url: application.url,
|
||||
layouts,
|
||||
screens,
|
||||
hasAppPackage: true,
|
||||
builtins: [getBuiltin("##builtin/screenslot")],
|
||||
appInstance: pkg.application.instance,
|
||||
appInstance: application.instance,
|
||||
}))
|
||||
|
||||
await hostingStore.actions.fetch()
|
||||
await backendUiStore.actions.database.select(pkg.application.instance)
|
||||
await backendUiStore.actions.database.select(application.instance)
|
||||
},
|
||||
routing: {
|
||||
fetch: async () => {
|
||||
const response = await api.get("/api/routing")
|
||||
const json = await response.json()
|
||||
|
||||
store.update(state => {
|
||||
state.routes = json.routes
|
||||
return state
|
||||
|
@ -243,128 +224,231 @@ export const getFrontendStore = () => {
|
|||
},
|
||||
components: {
|
||||
select: component => {
|
||||
if (!component) {
|
||||
return
|
||||
}
|
||||
|
||||
// If this is the root component, select the asset instead
|
||||
const asset = get(currentAsset)
|
||||
const parent = findComponentParent(asset.props, component._id)
|
||||
if (parent == null) {
|
||||
const state = get(store)
|
||||
const isLayout = state.currentFrontEndType === FrontendTypes.LAYOUT
|
||||
if (isLayout) {
|
||||
store.actions.layouts.select(asset._id)
|
||||
} else {
|
||||
store.actions.screens.select(asset._id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise select the component
|
||||
store.update(state => {
|
||||
state.selectedComponentId = component._id
|
||||
state.currentView = "component"
|
||||
return state
|
||||
})
|
||||
},
|
||||
create: (componentToAdd, presetProps) => {
|
||||
const selectedAsset = get(currentAsset)
|
||||
|
||||
store.update(state => {
|
||||
function findSlot(component_array) {
|
||||
if (!component_array) {
|
||||
return false
|
||||
getDefinition: componentName => {
|
||||
if (!componentName) {
|
||||
return null
|
||||
}
|
||||
for (let component of component_array) {
|
||||
if (component._component === "##builtin/screenslot") {
|
||||
return true
|
||||
if (!componentName.startsWith("@budibase")) {
|
||||
componentName = `@budibase/standard-components/${componentName}`
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
const currentComponentDefinition =
|
||||
state.components[selected._component]
|
||||
|
||||
const allowsChildren = currentComponentDefinition.children
|
||||
|
||||
// Determine where to put the new component.
|
||||
let targetParent
|
||||
if (allowsChildren) {
|
||||
// Child of the selected component
|
||||
targetParent = selected
|
||||
} else {
|
||||
// Sibling of selected component
|
||||
targetParent = findParent(selectedAsset.props, selected)
|
||||
}
|
||||
|
||||
// Don't continue if there's no parent
|
||||
if (!targetParent) return state
|
||||
|
||||
// Push the new component
|
||||
targetParent._children.push(newComponent.props)
|
||||
|
||||
store.actions.preview.saveSelected()
|
||||
|
||||
state.currentView = "component"
|
||||
state.selectedComponentId = newComponent.props._id
|
||||
|
||||
analytics.captureEvent("Added Component", {
|
||||
name: newComponent.props._component,
|
||||
})
|
||||
return state
|
||||
})
|
||||
return get(store).components[componentName]
|
||||
},
|
||||
copy: (component, cut = false) => {
|
||||
const selectedAsset = get(currentAsset)
|
||||
createInstance: (componentName, presetProps) => {
|
||||
const definition = store.actions.components.getDefinition(componentName)
|
||||
if (!definition) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Generate default props
|
||||
let props = { ...presetProps }
|
||||
if (definition.settings) {
|
||||
definition.settings.forEach(setting => {
|
||||
if (setting.defaultValue !== undefined) {
|
||||
props[setting.key] = setting.defaultValue
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Add any extra properties the component needs
|
||||
let extras = {}
|
||||
if (definition.hasChildren) {
|
||||
extras._children = []
|
||||
}
|
||||
|
||||
return {
|
||||
_id: uuid(),
|
||||
_component: definition.component,
|
||||
_styles: { normal: {}, hover: {}, active: {} },
|
||||
_instanceName: `New ${definition.name}`,
|
||||
...cloneDeep(props),
|
||||
...extras,
|
||||
}
|
||||
},
|
||||
create: (componentName, presetProps) => {
|
||||
const selected = get(selectedComponent)
|
||||
const asset = get(currentAsset)
|
||||
const state = get(store)
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// Create new component
|
||||
const componentInstance = store.actions.components.createInstance(
|
||||
componentName,
|
||||
presetProps
|
||||
)
|
||||
if (!componentInstance) {
|
||||
return
|
||||
}
|
||||
|
||||
// Find parent node to attach this component to
|
||||
let parentComponent
|
||||
|
||||
if (!asset) {
|
||||
return
|
||||
}
|
||||
if (selected) {
|
||||
// Use current screen or layout as parent if no component is selected
|
||||
const definition = store.actions.components.getDefinition(
|
||||
selected._component
|
||||
)
|
||||
if (definition?.hasChildren) {
|
||||
// Use selected component if it allows children
|
||||
parentComponent = selected
|
||||
} else {
|
||||
// Otherwise we need to use the parent of this component
|
||||
parentComponent = findComponentParent(asset.props, selected._id)
|
||||
}
|
||||
} else {
|
||||
// Use screen or layout if no component is selected
|
||||
parentComponent = asset.props
|
||||
}
|
||||
|
||||
// Attach component
|
||||
if (!parentComponent) {
|
||||
return
|
||||
}
|
||||
if (!parentComponent._children) {
|
||||
parentComponent._children = []
|
||||
}
|
||||
parentComponent._children.push(componentInstance)
|
||||
|
||||
// Save components and update UI
|
||||
store.actions.preview.saveSelected()
|
||||
store.update(state => {
|
||||
state.componentToPaste = cloneDeep(component)
|
||||
state.componentToPaste.isCut = cut
|
||||
if (cut) {
|
||||
const parent = findParent(selectedAsset.props, component._id)
|
||||
state.currentView = "component"
|
||||
state.selectedComponentId = componentInstance._id
|
||||
return state
|
||||
})
|
||||
|
||||
// Log event
|
||||
analytics.captureEvent("Added Component", {
|
||||
name: componentInstance._component,
|
||||
})
|
||||
|
||||
return componentInstance
|
||||
},
|
||||
delete: component => {
|
||||
if (!component) {
|
||||
return
|
||||
}
|
||||
const asset = get(currentAsset)
|
||||
if (!asset) {
|
||||
return
|
||||
}
|
||||
const parent = findComponentParent(asset.props, component._id)
|
||||
if (parent) {
|
||||
parent._children = parent._children.filter(
|
||||
child => child._id !== component._id
|
||||
)
|
||||
store.actions.components.select(parent)
|
||||
}
|
||||
store.actions.preview.saveSelected()
|
||||
},
|
||||
copy: (component, cut = false) => {
|
||||
const selectedAsset = get(currentAsset)
|
||||
if (!selectedAsset) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Update store with copied component
|
||||
store.update(state => {
|
||||
state.componentToPaste = cloneDeep(component)
|
||||
state.componentToPaste.isCut = cut
|
||||
return state
|
||||
})
|
||||
|
||||
// Remove the component from its parent if we're cutting
|
||||
if (cut) {
|
||||
const parent = findComponentParent(selectedAsset.props, component._id)
|
||||
if (parent) {
|
||||
parent._children = parent._children.filter(
|
||||
child => child._id !== component._id
|
||||
)
|
||||
store.actions.components.select(parent)
|
||||
}
|
||||
}
|
||||
},
|
||||
paste: async (targetComponent, mode) => {
|
||||
const selectedAsset = get(currentAsset)
|
||||
let promises = []
|
||||
store.update(state => {
|
||||
if (!state.componentToPaste) return state
|
||||
|
||||
const componentToPaste = cloneDeep(state.componentToPaste)
|
||||
// retain the same ids as things may be referencing this component
|
||||
if (componentToPaste.isCut) {
|
||||
// in case we paste a second time
|
||||
state.componentToPaste.isCut = false
|
||||
} else {
|
||||
generateNewIdsForComponent(componentToPaste, state)
|
||||
}
|
||||
delete componentToPaste.isCut
|
||||
|
||||
if (mode === "inside") {
|
||||
targetComponent._children.push(componentToPaste)
|
||||
// Stop if we have nothing to paste
|
||||
if (!state.componentToPaste) {
|
||||
return state
|
||||
}
|
||||
|
||||
const parent = findParent(selectedAsset.props, targetComponent)
|
||||
// Clone the component to paste
|
||||
// Retain the same ID if cutting as things may be referencing this component
|
||||
const cut = state.componentToPaste.isCut
|
||||
delete state.componentToPaste.isCut
|
||||
let componentToPaste = cloneDeep(state.componentToPaste)
|
||||
if (cut) {
|
||||
state.componentToPaste = null
|
||||
} else {
|
||||
componentToPaste._id = uuid()
|
||||
}
|
||||
|
||||
if (mode === "inside") {
|
||||
// Paste inside target component if chosen
|
||||
if (!targetComponent._children) {
|
||||
targetComponent._children = []
|
||||
}
|
||||
targetComponent._children.push(componentToPaste)
|
||||
} else {
|
||||
// Otherwise find the parent so we can paste in the correct order
|
||||
// in the parents child components
|
||||
const selectedAsset = get(currentAsset)
|
||||
if (!selectedAsset) {
|
||||
return state
|
||||
}
|
||||
const parent = findComponentParent(
|
||||
selectedAsset.props,
|
||||
targetComponent._id
|
||||
)
|
||||
if (!parent) {
|
||||
return state
|
||||
}
|
||||
|
||||
// Insert the component in the correct position
|
||||
const targetIndex = parent._children.indexOf(targetComponent)
|
||||
const index = mode === "above" ? targetIndex : targetIndex + 1
|
||||
parent._children.splice(index, 0, cloneDeep(componentToPaste))
|
||||
}
|
||||
|
||||
// Save and select the new component
|
||||
promises.push(store.actions.preview.saveSelected())
|
||||
store.actions.components.select(componentToPaste)
|
||||
|
||||
return state
|
||||
})
|
||||
await Promise.all(promises)
|
||||
|
@ -389,90 +473,56 @@ export const getFrontendStore = () => {
|
|||
await store.actions.preview.saveSelected()
|
||||
},
|
||||
updateProp: (name, value) => {
|
||||
let component = get(selectedComponent)
|
||||
if (!name || !component) {
|
||||
return
|
||||
}
|
||||
component[name] = value
|
||||
store.update(state => {
|
||||
let current_component = get(selectedComponent)
|
||||
current_component[name] = value
|
||||
|
||||
state.selectedComponentId = current_component._id
|
||||
store.actions.preview.saveSelected()
|
||||
state.selectedComponentId = component._id
|
||||
return state
|
||||
})
|
||||
},
|
||||
findRoute: component => {
|
||||
// Gets all the components to needed to construct a path.
|
||||
const selectedAsset = get(currentAsset)
|
||||
let pathComponents = []
|
||||
let parent = component
|
||||
let root = false
|
||||
while (!root) {
|
||||
parent = findParent(selectedAsset.props, parent)
|
||||
if (!parent) {
|
||||
root = true
|
||||
} else {
|
||||
pathComponents.push(parent)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove root entry since it's the screen or layout.
|
||||
// Reverse array since we need the correct order of the IDs
|
||||
const reversedComponents = pathComponents.reverse().slice(1)
|
||||
|
||||
// Add component
|
||||
const allComponents = [...reversedComponents, component]
|
||||
|
||||
// Map IDs
|
||||
const IdList = allComponents.map(c => c._id)
|
||||
|
||||
// Construct ID Path:
|
||||
return IdList.join("/")
|
||||
store.actions.preview.saveSelected()
|
||||
},
|
||||
links: {
|
||||
save: async (url, title) => {
|
||||
let promises = []
|
||||
const layout = get(mainLayout)
|
||||
store.update(state => {
|
||||
// Try to extract a nav component from the master layout
|
||||
const nav = findChildComponentType(
|
||||
layout,
|
||||
if (!layout) {
|
||||
return
|
||||
}
|
||||
|
||||
// Find a nav bar in the main layout
|
||||
const nav = findComponentType(
|
||||
layout.props,
|
||||
"@budibase/standard-components/navigation"
|
||||
)
|
||||
if (nav) {
|
||||
let newLink
|
||||
if (!nav) {
|
||||
return
|
||||
}
|
||||
|
||||
// Clone an existing link if one exists
|
||||
let newLink
|
||||
if (nav._children && nav._children.length) {
|
||||
// Clone existing link style
|
||||
// Clone an existing link if one exists
|
||||
newLink = cloneDeep(nav._children[0])
|
||||
|
||||
// Manipulate IDs to ensure uniqueness
|
||||
generateNewIdsForComponent(newLink, state, false)
|
||||
|
||||
// Set our new props
|
||||
newLink._id = uuid()
|
||||
newLink._instanceName = `${title} Link`
|
||||
newLink.url = url
|
||||
newLink.text = title
|
||||
} else {
|
||||
// Otherwise create vanilla new link
|
||||
const component = getComponentDefinition(
|
||||
state,
|
||||
"@budibase/standard-components/link"
|
||||
)
|
||||
const instanceId = get(backendUiStore).selectedDatabase._id
|
||||
newLink = createProps(component, {
|
||||
newLink = {
|
||||
...store.actions.components.createInstance("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)
|
||||
await store.actions.layouts.save(layout)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -4,8 +4,6 @@ import rowListScreen from "./rowListScreen"
|
|||
import emptyNewRowScreen from "./emptyNewRowScreen"
|
||||
import createFromScratchScreen from "./createFromScratchScreen"
|
||||
import emptyRowDetailScreen from "./emptyRowDetailScreen"
|
||||
import { generateNewIdsForComponent } from "../../storeUtils"
|
||||
import { uuid } from "builderStore/uuid"
|
||||
|
||||
const allTemplates = tables => [
|
||||
createFromScratchScreen,
|
||||
|
@ -16,13 +14,9 @@ const allTemplates = tables => [
|
|||
emptyRowDetailScreen,
|
||||
]
|
||||
|
||||
// allows us to apply common behaviour to all create() functions
|
||||
// Allows us to apply common behaviour to all create() functions
|
||||
const createTemplateOverride = (frontendState, create) => () => {
|
||||
const screen = create()
|
||||
for (let component of screen.props._children) {
|
||||
generateNewIdsForComponent(component, frontendState, false)
|
||||
}
|
||||
screen.props._id = uuid()
|
||||
screen.name = screen.props._id
|
||||
screen.routing.route = screen.routing.route.toLowerCase()
|
||||
return screen
|
||||
|
|
|
@ -21,26 +21,29 @@ export default function(tables) {
|
|||
export const newRowUrl = table => sanitizeUrl(`/${table.name}/new/row`)
|
||||
export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE"
|
||||
|
||||
function generateTitleContainer(table) {
|
||||
return makeTitleContainer("New Row").addChild(makeSaveButton(table))
|
||||
function generateTitleContainer(table, providerId) {
|
||||
return makeTitleContainer("New Row").addChild(
|
||||
makeSaveButton(table, providerId)
|
||||
)
|
||||
}
|
||||
|
||||
const createScreen = table => {
|
||||
const dataform = new Component(
|
||||
"@budibase/standard-components/dataformwide"
|
||||
).instanceName("Form")
|
||||
|
||||
const container = makeMainContainer()
|
||||
.addChild(makeBreadcrumbContainer(table.name, "New"))
|
||||
.addChild(generateTitleContainer(table))
|
||||
.addChild(dataform)
|
||||
|
||||
return new Screen()
|
||||
const screen = new Screen()
|
||||
.component("@budibase/standard-components/newrow")
|
||||
.table(table._id)
|
||||
.route(newRowUrl(table))
|
||||
.instanceName(`${table.name} - New`)
|
||||
.name("")
|
||||
.addChild(container)
|
||||
.json()
|
||||
|
||||
const dataform = new Component(
|
||||
"@budibase/standard-components/dataformwide"
|
||||
).instanceName("Form")
|
||||
|
||||
const providerId = screen._json.props._id
|
||||
const container = makeMainContainer()
|
||||
.addChild(makeBreadcrumbContainer(table.name, "New"))
|
||||
.addChild(generateTitleContainer(table, providerId))
|
||||
.addChild(dataform)
|
||||
|
||||
return screen.addChild(container).json()
|
||||
}
|
||||
|
|
|
@ -25,9 +25,9 @@ export default function(tables) {
|
|||
export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE"
|
||||
export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`)
|
||||
|
||||
function generateTitleContainer(table, title) {
|
||||
function generateTitleContainer(table, title, providerId) {
|
||||
// have to override style for this, its missing margin
|
||||
const saveButton = makeSaveButton(table).normalStyle({
|
||||
const saveButton = makeSaveButton(table, providerId).normalStyle({
|
||||
background: "#000000",
|
||||
"border-width": "0",
|
||||
"border-style": "None",
|
||||
|
@ -60,8 +60,8 @@ function generateTitleContainer(table, title) {
|
|||
onClick: [
|
||||
{
|
||||
parameters: {
|
||||
rowId: "{{ data._id }}",
|
||||
revId: "{{ data._rev }}",
|
||||
rowId: `{{ ${providerId}._id }}`,
|
||||
revId: `{{ ${providerId}._rev }}`,
|
||||
tableId: table._id,
|
||||
},
|
||||
"##eventHandlerType": "Delete Row",
|
||||
|
@ -82,21 +82,22 @@ function generateTitleContainer(table, title) {
|
|||
}
|
||||
|
||||
const createScreen = (table, heading) => {
|
||||
const dataform = new Component(
|
||||
"@budibase/standard-components/dataformwide"
|
||||
).instanceName("Form")
|
||||
|
||||
const container = makeMainContainer()
|
||||
.addChild(makeBreadcrumbContainer(table.name, heading || "Edit"))
|
||||
.addChild(generateTitleContainer(table, heading || "Edit Row"))
|
||||
.addChild(dataform)
|
||||
|
||||
return new Screen()
|
||||
const screen = new Screen()
|
||||
.component("@budibase/standard-components/rowdetail")
|
||||
.table(table._id)
|
||||
.instanceName(`${table.name} - Detail`)
|
||||
.route(rowDetailUrl(table))
|
||||
.name("")
|
||||
.addChild(container)
|
||||
.json()
|
||||
|
||||
const dataform = new Component(
|
||||
"@budibase/standard-components/dataformwide"
|
||||
).instanceName("Form")
|
||||
|
||||
const providerId = screen._json.props._id
|
||||
const container = makeMainContainer()
|
||||
.addChild(makeBreadcrumbContainer(table.name, heading || "Edit"))
|
||||
.addChild(generateTitleContainer(table, heading || "Edit Row", providerId))
|
||||
.addChild(dataform)
|
||||
|
||||
return screen.addChild(container).json()
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { v4 } from "uuid"
|
||||
import { uuid } from "builderStore/uuid"
|
||||
import { BaseStructure } from "./BaseStructure"
|
||||
|
||||
export class Component extends BaseStructure {
|
||||
|
@ -6,7 +6,7 @@ export class Component extends BaseStructure {
|
|||
super(false)
|
||||
this._children = []
|
||||
this._json = {
|
||||
_id: v4(),
|
||||
_id: uuid(),
|
||||
_component: name,
|
||||
_styles: {
|
||||
normal: {},
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { BaseStructure } from "./BaseStructure"
|
||||
import { uuid } from "builderStore/uuid"
|
||||
|
||||
export class Screen extends BaseStructure {
|
||||
constructor() {
|
||||
|
@ -6,7 +7,7 @@ export class Screen extends BaseStructure {
|
|||
this._json = {
|
||||
layoutId: "layout_private_master",
|
||||
props: {
|
||||
_id: "",
|
||||
_id: uuid(),
|
||||
_component: "",
|
||||
_styles: {
|
||||
normal: {},
|
||||
|
|
|
@ -78,7 +78,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
|||
.addChild(identifierText)
|
||||
}
|
||||
|
||||
export function makeSaveButton(table) {
|
||||
export function makeSaveButton(table, providerId) {
|
||||
return new Component("@budibase/standard-components/button")
|
||||
.normalStyle({
|
||||
background: "#000000",
|
||||
|
@ -100,8 +100,7 @@ export function makeSaveButton(table) {
|
|||
onClick: [
|
||||
{
|
||||
parameters: {
|
||||
contextPath: "data",
|
||||
tableId: table._id,
|
||||
providerId,
|
||||
},
|
||||
"##eventHandlerType": "Save Row",
|
||||
},
|
||||
|
|
|
@ -1,80 +1,105 @@
|
|||
import { getBuiltin } from "components/userInterface/assetParsing/createProps"
|
||||
import { uuid } from "./uuid"
|
||||
import getNewComponentName from "./getNewComponentName"
|
||||
/**
|
||||
* Recursively searches for a specific component ID
|
||||
*/
|
||||
export const findComponent = (rootComponent, id) => {
|
||||
return searchComponentTree(rootComponent, comp => comp._id === id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the parent component of the passed in child.
|
||||
* @param {Object} rootProps - props to search for the parent in
|
||||
* @param {String|Object} child - id of the child or the child itself to find the parent of
|
||||
* Recursively searches for a specific component type
|
||||
*/
|
||||
export const findParent = (rootProps, child) => {
|
||||
let parent
|
||||
walkProps(rootProps, (props, breakWalk) => {
|
||||
if (
|
||||
props._children &&
|
||||
(props._children.includes(child) ||
|
||||
props._children.some(c => c._id === child))
|
||||
) {
|
||||
parent = props
|
||||
breakWalk()
|
||||
}
|
||||
})
|
||||
return parent
|
||||
export const findComponentType = (rootComponent, type) => {
|
||||
return searchComponentTree(rootComponent, comp => comp._component === type)
|
||||
}
|
||||
|
||||
export const walkProps = (props, action, cancelToken = null) => {
|
||||
cancelToken = cancelToken || { cancelled: false }
|
||||
action(props, () => {
|
||||
cancelToken.cancelled = true
|
||||
})
|
||||
|
||||
if (props._children) {
|
||||
for (let child of props._children) {
|
||||
if (cancelToken.cancelled) return
|
||||
walkProps(child, action, cancelToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const generateNewIdsForComponent = (
|
||||
component,
|
||||
state,
|
||||
changeName = true
|
||||
) =>
|
||||
walkProps(component, prop => {
|
||||
prop._id = uuid()
|
||||
if (changeName) prop._instanceName = getNewComponentName(prop, state)
|
||||
})
|
||||
|
||||
export const getComponentDefinition = (state, name) =>
|
||||
name.startsWith("##") ? getBuiltin(name) : state.components[name]
|
||||
|
||||
export const findChildComponentType = (node, typeToFind) => {
|
||||
// Stop recursion if invalid props
|
||||
if (!node || !typeToFind) {
|
||||
/**
|
||||
* Recursively searches for the parent component of a specific component ID
|
||||
*/
|
||||
export const findComponentParent = (rootComponent, id, parentComponent) => {
|
||||
if (!rootComponent || !id) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Stop recursion if this element matches
|
||||
if (node._component === typeToFind) {
|
||||
return node
|
||||
if (rootComponent._id === id) {
|
||||
return parentComponent
|
||||
}
|
||||
|
||||
// Otherwise check if any children match
|
||||
// Stop recursion if no valid children to process
|
||||
const children = node._children || (node.props && node.props._children)
|
||||
if (!children || !children.length) {
|
||||
if (!rootComponent._children) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Recurse and check each child component
|
||||
for (let child of children) {
|
||||
const childResult = findChildComponentType(child, typeToFind)
|
||||
for (const child of rootComponent._children) {
|
||||
const childResult = findComponentParent(child, id, rootComponent)
|
||||
if (childResult) {
|
||||
return childResult
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively searches for a specific component ID and records the component
|
||||
* path to this component
|
||||
*/
|
||||
export const findComponentPath = (rootComponent, id, path = []) => {
|
||||
if (!rootComponent || !id) {
|
||||
return []
|
||||
}
|
||||
if (rootComponent._id === id) {
|
||||
return [...path, rootComponent]
|
||||
}
|
||||
if (!rootComponent._children) {
|
||||
return []
|
||||
}
|
||||
for (const child of rootComponent._children) {
|
||||
const newPath = [...path, rootComponent]
|
||||
const childResult = findComponentPath(child, id, newPath)
|
||||
if (childResult?.length) {
|
||||
return childResult
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Recurses through the component tree and finds all components of a certain
|
||||
* type.
|
||||
*/
|
||||
export const findAllMatchingComponents = (rootComponent, selector) => {
|
||||
if (!rootComponent || !selector) {
|
||||
return []
|
||||
}
|
||||
let components = []
|
||||
if (rootComponent._children) {
|
||||
rootComponent._children.forEach(child => {
|
||||
components = [
|
||||
...components,
|
||||
...findAllMatchingComponents(child, selector),
|
||||
]
|
||||
})
|
||||
}
|
||||
if (selector(rootComponent)) {
|
||||
components.push(rootComponent)
|
||||
}
|
||||
return components.reverse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Recurses through a component tree evaluating a matching function against
|
||||
* components until a match is found
|
||||
*/
|
||||
const searchComponentTree = (rootComponent, matchComponent) => {
|
||||
if (!rootComponent || !matchComponent) {
|
||||
return null
|
||||
}
|
||||
if (matchComponent(rootComponent)) {
|
||||
return rootComponent
|
||||
}
|
||||
if (!rootComponent._children) {
|
||||
return null
|
||||
}
|
||||
for (const child of rootComponent._children) {
|
||||
const childResult = searchComponentTree(child, matchComponent)
|
||||
if (childResult) {
|
||||
return childResult
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach here then no children were valid
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import { Button, Input, Select, Label } from "@budibase/bbui"
|
||||
import { automationStore } from "builderStore"
|
||||
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
|
||||
import BindableInput from "components/userInterface/BindableInput.svelte"
|
||||
import BindableInput from "../../common/BindableInput.svelte"
|
||||
|
||||
export let block
|
||||
export let webhookModal
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { Input, Select, Label } from "@budibase/bbui"
|
||||
import BindableInput from "../../userInterface/BindableInput.svelte"
|
||||
import { Select } from "@budibase/bbui"
|
||||
import BindableInput from "../../common/BindableInput.svelte"
|
||||
|
||||
export let value
|
||||
export let bindings
|
||||
|
|
|
@ -103,6 +103,15 @@
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
.column-header-name {
|
||||
white-space: normal !important;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sort-icon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
|
|
|
@ -43,8 +43,8 @@
|
|||
<div class="datasource-icon" slot="icon">
|
||||
<svelte:component
|
||||
this={ICONS[datasource.source]}
|
||||
height="15"
|
||||
width="15" />
|
||||
height="18"
|
||||
width="18" />
|
||||
</div>
|
||||
<EditDatasourcePopover {datasource} />
|
||||
</NavItem>
|
||||
|
@ -61,3 +61,10 @@
|
|||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.datasource-icon {
|
||||
margin-right: 3px;
|
||||
padding-top: 3px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -7,10 +7,9 @@
|
|||
<form>
|
||||
{#each Object.keys(integration) as configKey}
|
||||
<Input
|
||||
thin
|
||||
type={integration[configKey].type}
|
||||
label={configKey}
|
||||
bind:value={integration[configKey]} />
|
||||
<Spacer medium />
|
||||
<Spacer large />
|
||||
{/each}
|
||||
</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>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import GenericBindingPopover from "./GenericBindingPopover.svelte"
|
||||
import GenericBindingPopover from "../automation/SetupPanel/GenericBindingPopover.svelte"
|
||||
import { Input, Icon } from "@budibase/bbui"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
|
@ -5,12 +5,7 @@
|
|||
export let disabled
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="dropdown-item"
|
||||
class:disabled
|
||||
on:click
|
||||
class:big={subtitle != null}
|
||||
{...$$restProps}>
|
||||
<div class="dropdown-item" class:disabled on:click {...$$restProps}>
|
||||
{#if icon}<i class={icon} />{/if}
|
||||
<div class="content">
|
||||
<div class="title">{title}</div>
|
||||
|
@ -27,7 +22,7 @@
|
|||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
padding: var(--spacing-xs) var(--spacing-l);
|
||||
padding: var(--spacing-s) var(--spacing-l);
|
||||
color: var(--ink);
|
||||
}
|
||||
.dropdown-item.disabled,
|
||||
|
@ -35,9 +30,6 @@
|
|||
pointer-events: none;
|
||||
color: var(--grey-5);
|
||||
}
|
||||
.dropdown-item.big {
|
||||
padding: var(--spacing-s) var(--spacing-l);
|
||||
}
|
||||
.dropdown-item:not(.disabled):hover {
|
||||
background-color: var(--grey-2);
|
||||
cursor: pointer;
|
||||
|
@ -65,10 +57,6 @@
|
|||
}
|
||||
|
||||
i {
|
||||
padding: 0.5rem;
|
||||
background-color: var(--grey-2);
|
||||
font-size: 24px;
|
||||
border-radius: var(--border-radius-s);
|
||||
color: var(--ink);
|
||||
font-size: var(--font-size-m);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -6,47 +6,62 @@
|
|||
selectedComponent,
|
||||
currentAssetId,
|
||||
} from "builderStore"
|
||||
import components from "./temporaryPanelStructure.js"
|
||||
import structure from "./componentStructure.json"
|
||||
import { DropdownMenu } from "@budibase/bbui"
|
||||
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
|
||||
|
||||
const categories = components.categories
|
||||
$: enrichedStructure = enrichStructure(structure, $store.components)
|
||||
|
||||
let selectedIndex
|
||||
let anchors = []
|
||||
let popover
|
||||
$: anchor = selectedIndex === -1 ? null : anchors[selectedIndex]
|
||||
|
||||
const close = () => {
|
||||
popover.hide()
|
||||
const enrichStructure = (structure, definitions) => {
|
||||
let enrichedStructure = []
|
||||
structure.forEach(item => {
|
||||
if (typeof item === "string") {
|
||||
const def = definitions[`@budibase/standard-components/${item}`]
|
||||
if (def) {
|
||||
enrichedStructure.push({
|
||||
...def,
|
||||
isCategory: false,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
enrichedStructure.push({
|
||||
...item,
|
||||
isCategory: true,
|
||||
children: enrichStructure(item.children || [], definitions),
|
||||
})
|
||||
}
|
||||
})
|
||||
return enrichedStructure
|
||||
}
|
||||
|
||||
const onCategoryChosen = (category, idx) => {
|
||||
if (category.isCategory) {
|
||||
const onItemChosen = (item, idx) => {
|
||||
if (item.isCategory) {
|
||||
// Select and open this category
|
||||
selectedIndex = idx
|
||||
popover.show()
|
||||
} else {
|
||||
onComponentChosen(category)
|
||||
// Add this component
|
||||
store.actions.components.create(item.component)
|
||||
popover.hide()
|
||||
}
|
||||
}
|
||||
|
||||
const onComponentChosen = component => {
|
||||
store.actions.components.create(component._component, component.presetProps)
|
||||
const path = store.actions.components.findRoute($selectedComponent)
|
||||
$goto(`./${$currentAssetId}/${path}`)
|
||||
close()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
{#each categories as category, idx}
|
||||
{#each enrichedStructure as item, idx}
|
||||
<div
|
||||
bind:this={anchors[idx]}
|
||||
class="category"
|
||||
on:click={() => onCategoryChosen(category, idx)}
|
||||
on:click={() => onItemChosen(item, idx)}
|
||||
class:active={idx === selectedIndex}>
|
||||
{#if category.icon}<i class={category.icon} />{/if}
|
||||
<span>{category.name}</span>
|
||||
{#if category.isCategory}<i class="ri-arrow-down-s-line arrow" />{/if}
|
||||
{#if item.icon}<i class={item.icon} />{/if}
|
||||
<span>{item.name}</span>
|
||||
{#if item.isCategory}<i class="ri-arrow-down-s-line arrow" />{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -56,12 +71,12 @@
|
|||
{anchor}
|
||||
align="left">
|
||||
<DropdownContainer>
|
||||
{#each categories[selectedIndex].children as item}
|
||||
{#each enrichedStructure[selectedIndex].children as item}
|
||||
{#if !item.showOnAsset || item.showOnAsset.includes($currentAssetName)}
|
||||
<DropdownItem
|
||||
icon={item.icon}
|
||||
title={item.name}
|
||||
on:click={() => onComponentChosen(item)} />
|
||||
on:click={() => onItemChosen(item)} />
|
||||
{/if}
|
||||
{/each}
|
||||
</DropdownContainer>
|
|
@ -14,7 +14,7 @@
|
|||
const screenPlaceholder = new Screen()
|
||||
.name("Screen Placeholder")
|
||||
.route("*")
|
||||
.component("@budibase/standard-components/screenslotplaceholder")
|
||||
.component("@budibase/standard-components/screenslot")
|
||||
.instanceName("Content Placeholder")
|
||||
.json()
|
||||
|
|
@ -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>
|
||||
import { goto } from "@sveltech/routify"
|
||||
import { get } from "svelte/store"
|
||||
import { store, currentAsset } from "builderStore"
|
||||
import { getComponentDefinition } from "builderStore/storeUtils"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { last } from "lodash/fp"
|
||||
import { findParent } from "builderStore/storeUtils"
|
||||
import { findComponentParent } from "builderStore/storeUtils"
|
||||
import { DropdownMenu } from "@budibase/bbui"
|
||||
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
|
||||
|
||||
|
@ -17,7 +15,7 @@
|
|||
|
||||
$: noChildrenAllowed =
|
||||
!component ||
|
||||
!getComponentDefinition($store, component._component)?.children
|
||||
!store.actions.components.getDefinition(component._component)?.hasChildren
|
||||
$: noPaste = !$store.componentToPaste
|
||||
|
||||
const lastPartOfName = c => (c ? last(c._component.split("/")) : "")
|
||||
|
@ -28,48 +26,38 @@
|
|||
|
||||
const selectComponent = component => {
|
||||
store.actions.components.select(component)
|
||||
const path = store.actions.components.findRoute(component)
|
||||
$goto(`./${$store.currentFrontEndType}/${path}`)
|
||||
}
|
||||
|
||||
const moveUpComponent = () => {
|
||||
store.update(state => {
|
||||
const asset = get(currentAsset)
|
||||
const parent = findParent(asset.props, component)
|
||||
|
||||
if (parent) {
|
||||
const parent = findComponentParent(asset.props, component._id)
|
||||
if (!parent) {
|
||||
return
|
||||
}
|
||||
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)
|
||||
parent._children = newChildren
|
||||
}
|
||||
state.selectedComponentId = component._id
|
||||
store.actions.preview.saveSelected()
|
||||
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const moveDownComponent = () => {
|
||||
store.update(state => {
|
||||
const asset = get(currentAsset)
|
||||
const parent = findParent(asset.props, component)
|
||||
|
||||
if (parent) {
|
||||
const parent = findComponentParent(asset.props, component._id)
|
||||
if (!parent) {
|
||||
return
|
||||
}
|
||||
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)
|
||||
parent._children = newChildren
|
||||
}
|
||||
state.selectedComponentId = component._id
|
||||
store.actions.preview.saveSelected()
|
||||
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const duplicateComponent = () => {
|
||||
|
@ -78,18 +66,7 @@
|
|||
}
|
||||
|
||||
const deleteComponent = () => {
|
||||
store.update(state => {
|
||||
const asset = get(currentAsset)
|
||||
const parent = findParent(asset.props, component)
|
||||
|
||||
if (parent) {
|
||||
parent._children = parent._children.filter(child => child !== component)
|
||||
selectComponent(parent)
|
||||
}
|
||||
|
||||
store.actions.preview.saveSelected()
|
||||
return state
|
||||
})
|
||||
store.actions.components.delete(component)
|
||||
}
|
||||
|
||||
const storeComponentForCopy = (cut = false) => {
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import { goto } from "@sveltech/routify"
|
||||
import { store, currentAssetId } from "builderStore"
|
||||
import { getComponentDefinition } from "builderStore/storeUtils"
|
||||
import { DropEffect, DropPosition } from "./dragDropStore"
|
||||
import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
|
@ -12,17 +11,10 @@
|
|||
export let level = 0
|
||||
export let dragDropStore
|
||||
|
||||
const isScreenslot = name => name === "##builtin/screenslot"
|
||||
const isScreenslot = name => name?.endsWith("screenslot")
|
||||
|
||||
const selectComponent = component => {
|
||||
// Set current component
|
||||
store.actions.components.select(component)
|
||||
|
||||
// Get ID path
|
||||
const path = store.actions.components.findRoute(component)
|
||||
|
||||
// Go to correct URL
|
||||
$goto(`./${$currentAssetId}/${path}`)
|
||||
}
|
||||
|
||||
const dragstart = component => e => {
|
||||
|
@ -31,9 +23,11 @@
|
|||
}
|
||||
|
||||
const dragover = (component, index) => e => {
|
||||
const definition = store.actions.components.getDefinition(
|
||||
component._component
|
||||
)
|
||||
const canHaveChildrenButIsEmpty =
|
||||
getComponentDefinition($store, component._component).children &&
|
||||
component._children.length === 0
|
||||
definition?.hasChildren && !component._children?.length
|
||||
|
||||
e.dataTransfer.dropEffect = DropEffect.COPY
|
||||
|
|
@ -24,9 +24,7 @@
|
|||
$: selectedScreen = $currentAsset
|
||||
|
||||
const changeScreen = screenId => {
|
||||
// select the route
|
||||
store.actions.screens.select(screenId)
|
||||
$goto(`./${screenId}`)
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { writable, get } from "svelte/store"
|
||||
import { store as frontendStore } from "builderStore"
|
||||
import { findComponentPath } from "builderStore/storeUtils"
|
||||
|
||||
export const DropEffect = {
|
||||
MOVE: "move",
|
||||
|
@ -72,19 +73,30 @@ export default function() {
|
|||
})
|
||||
},
|
||||
drop: () => {
|
||||
store.update(state => {
|
||||
if (state.targetComponent !== state.dragged) {
|
||||
const state = get(store)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Cut and paste the component
|
||||
frontendStore.actions.components.copy(state.dragged, true)
|
||||
frontendStore.actions.components.paste(
|
||||
state.targetComponent,
|
||||
state.dropPosition
|
||||
)
|
||||
}
|
||||
|
||||
store.actions.reset()
|
||||
|
||||
return state
|
||||
})
|
||||
},
|
||||
}
|
||||
|
|
@ -9,10 +9,10 @@
|
|||
selectedAccessRole,
|
||||
} from "builderStore"
|
||||
import { FrontendTypes } from "constants"
|
||||
import ComponentNavigationTree from "components/userInterface/ComponentNavigationTree/index.svelte"
|
||||
import Layout from "components/userInterface/Layout.svelte"
|
||||
import NewScreenModal from "components/userInterface/NewScreenModal.svelte"
|
||||
import NewLayoutModal from "components/userInterface/NewLayoutModal.svelte"
|
||||
import ComponentNavigationTree from "components/design/NavigationPanel/ComponentNavigationTree/index.svelte"
|
||||
import Layout from "components/design/NavigationPanel/Layout.svelte"
|
||||
import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte"
|
||||
import NewLayoutModal from "components/design/NavigationPanel/NewLayoutModal.svelte"
|
||||
import { Modal, Switcher, Select } from "@budibase/bbui"
|
||||
|
||||
const tabs = [
|
||||
|
@ -28,7 +28,7 @@
|
|||
|
||||
let modal
|
||||
let routes = {}
|
||||
let tab = $params.assetType
|
||||
$: tab = $params.assetType
|
||||
|
||||
const navigate = ({ detail }) => {
|
||||
if (!detail) {
|
|
@ -19,7 +19,6 @@
|
|||
|
||||
const selectLayout = () => {
|
||||
store.actions.layouts.select(layout._id)
|
||||
$goto(`./${layout._id}`)
|
||||
}
|
||||
</script>
|
||||
|
|
@ -8,8 +8,7 @@
|
|||
|
||||
async function save() {
|
||||
try {
|
||||
const layout = await store.actions.layouts.save({ name })
|
||||
$goto(`./${layout._id}`)
|
||||
await store.actions.layouts.save({ name })
|
||||
notifier.success(`Layout ${name} created successfully`)
|
||||
} catch (err) {
|
||||
notifier.danger(`Error creating layout ${name}.`)
|
|
@ -63,7 +63,7 @@
|
|||
draftScreen.props._component = baseComponent
|
||||
draftScreen.routing = { route, roleId }
|
||||
|
||||
const createdScreen = await store.actions.screens.create(draftScreen)
|
||||
await store.actions.screens.create(draftScreen)
|
||||
if (createLink) {
|
||||
await store.actions.components.links.save(route, name)
|
||||
}
|
||||
|
@ -75,8 +75,6 @@
|
|||
template: template.id || template.name,
|
||||
})
|
||||
}
|
||||
|
||||
$goto(`./${createdScreen._id}`)
|
||||
}
|
||||
|
||||
const routeExists = (route, roleId) => {
|
|
@ -9,7 +9,7 @@
|
|||
export let bindingDrawer
|
||||
|
||||
function addToText(readableBinding) {
|
||||
value = value + `{{ ${readableBinding} }}`
|
||||
value = `${value || ""}{{ ${readableBinding} }}`
|
||||
}
|
||||
let originalValue = value
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
<script>
|
||||
import { TextArea, DetailSummary, Button } from "@budibase/bbui"
|
||||
import PropertyGroup from "./PropertyGroup.svelte"
|
||||
import FlatButtonGroup from "./FlatButtonGroup.svelte"
|
||||
import PropertyGroup from "./PropertyControls/PropertyGroup.svelte"
|
||||
import FlatButtonGroup from "./PropertyControls/FlatButtonGroup"
|
||||
import { allStyles } from "./componentStyles"
|
||||
|
||||
export let panelDefinition = {}
|
||||
export let componentDefinition = {}
|
||||
export let componentInstance = {}
|
||||
export let onStyleChanged = () => {}
|
||||
export let onCustomStyleChanged = () => {}
|
||||
export let onResetStyles = () => {}
|
||||
|
||||
let selectedCategory = "normal"
|
||||
let propGroup = null
|
||||
let currentGroup
|
||||
|
||||
function onChange(category) {
|
||||
|
@ -23,7 +23,7 @@
|
|||
{ value: "active", text: "Active" },
|
||||
]
|
||||
|
||||
$: propertyGroupNames = panelDefinition ? Object.keys(panelDefinition) : []
|
||||
$: groups = componentDefinition?.styleable ? Object.keys(allStyles) : []
|
||||
</script>
|
||||
|
||||
<div class="design-view-container">
|
||||
|
@ -32,12 +32,12 @@
|
|||
</div>
|
||||
|
||||
<div class="positioned-wrapper">
|
||||
<div bind:this={propGroup} class="design-view-property-groups">
|
||||
{#if propertyGroupNames.length > 0}
|
||||
{#each propertyGroupNames as groupName}
|
||||
<div class="design-view-property-groups">
|
||||
{#if groups.length > 0}
|
||||
{#each groups as groupName}
|
||||
<PropertyGroup
|
||||
name={groupName}
|
||||
properties={panelDefinition[groupName]}
|
||||
properties={allStyles[groupName]}
|
||||
styleCategory={selectedCategory}
|
||||
{onStyleChanged}
|
||||
{componentInstance}
|
|
@ -2,46 +2,30 @@
|
|||
import { get } from "svelte/store"
|
||||
import { store, selectedComponent, currentAsset } from "builderStore"
|
||||
import { FrontendTypes } from "constants"
|
||||
import panelStructure from "./temporaryPanelStructure.js"
|
||||
import CategoryTab from "./CategoryTab.svelte"
|
||||
import DesignView from "./DesignView.svelte"
|
||||
import SettingsView from "./SettingsView.svelte"
|
||||
import { setWith } from "lodash"
|
||||
|
||||
let flattenedPanel = flattenComponents(panelStructure.categories)
|
||||
let categories = [
|
||||
const categories = [
|
||||
{ value: "settings", name: "Settings" },
|
||||
{ value: "design", name: "Design" },
|
||||
]
|
||||
let selectedCategory = categories[0]
|
||||
|
||||
$: componentInstance =
|
||||
$store.currentView !== "component"
|
||||
? { ...$currentAsset, ...$selectedComponent }
|
||||
: $selectedComponent
|
||||
$: componentDefinition = $store.components[componentInstance._component]
|
||||
$: componentPropDefinition =
|
||||
flattenedPanel.find(
|
||||
// use for getting controls for each component property
|
||||
c => c._component === componentInstance._component
|
||||
) || {}
|
||||
|
||||
$: panelDefinition =
|
||||
componentPropDefinition.properties &&
|
||||
componentPropDefinition.properties[selectedCategory.value]
|
||||
$: definition = store.actions.components.getDefinition(
|
||||
$selectedComponent._component
|
||||
)
|
||||
$: isComponentOrScreen =
|
||||
$store.currentView === "component" ||
|
||||
$store.currentFrontEndType === FrontendTypes.SCREEN
|
||||
$: isNotScreenslot = !$selectedComponent._component.endsWith("screenslot")
|
||||
$: showDisplayName = isComponentOrScreen && isNotScreenslot
|
||||
|
||||
const onStyleChanged = store.actions.components.updateStyle
|
||||
const onCustomStyleChanged = store.actions.components.updateCustomStyle
|
||||
const onResetStyles = store.actions.components.resetStyles
|
||||
|
||||
$: isComponentOrScreen =
|
||||
$store.currentView === "component" ||
|
||||
$store.currentFrontEndType === FrontendTypes.SCREEN
|
||||
$: isNotScreenslot = componentInstance._component !== "##builtin/screenslot"
|
||||
|
||||
$: displayName =
|
||||
isComponentOrScreen && componentInstance._instanceName && isNotScreenslot
|
||||
|
||||
function walkProps(component, action) {
|
||||
action(component)
|
||||
if (component.children) {
|
||||
|
@ -89,24 +73,23 @@
|
|||
{categories}
|
||||
{selectedCategory} />
|
||||
|
||||
{#if displayName}
|
||||
<div class="instance-name">{componentInstance._instanceName}</div>
|
||||
{#if showDisplayName}
|
||||
<div class="instance-name">{$selectedComponent._instanceName}</div>
|
||||
{/if}
|
||||
|
||||
<div class="component-props-container">
|
||||
{#if selectedCategory.value === 'design'}
|
||||
<DesignView
|
||||
{panelDefinition}
|
||||
{componentInstance}
|
||||
componentInstance={$selectedComponent}
|
||||
componentDefinition={definition}
|
||||
{onStyleChanged}
|
||||
{onCustomStyleChanged}
|
||||
{onResetStyles} />
|
||||
{:else if selectedCategory.value === 'settings'}
|
||||
<SettingsView
|
||||
{componentInstance}
|
||||
{componentDefinition}
|
||||
{panelDefinition}
|
||||
displayNameField={displayName}
|
||||
componentInstance={$selectedComponent}
|
||||
componentDefinition={definition}
|
||||
{showDisplayName}
|
||||
onChange={store.actions.components.updateProp}
|
||||
onScreenPropChange={setAssetProps}
|
||||
assetInstance={$store.currentView !== 'component' && $currentAsset} />
|
|
@ -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"
|
||||
|
||||
export let event
|
||||
export let actions
|
||||
|
||||
let addActionButton
|
||||
let addActionDropdown
|
||||
let selectedAction
|
||||
|
||||
$: actions = event || []
|
||||
$: selectedActionComponent =
|
||||
selectedAction &&
|
||||
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_KEY]).component
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let value
|
||||
export let value = []
|
||||
export let name
|
||||
|
||||
let drawer
|
||||
|
@ -57,12 +57,12 @@
|
|||
}
|
||||
</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'}>
|
||||
<heading slot="buttons">
|
||||
<Button thin blue on:click={saveEventData}>Save</Button>
|
||||
</heading>
|
||||
<div slot="body">
|
||||
<EventEditor event={value} eventType={name} />
|
||||
<EventEditor bind:actions={value} eventType={name} />
|
||||
</div>
|
||||
</Drawer>
|
|
@ -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>
|
||||
import { Select, Label, Spacer } from "@budibase/bbui"
|
||||
import { store, backendUiStore, currentAsset } from "builderStore"
|
||||
import fetchBindableProperties from "builderStore/fetchBindableProperties"
|
||||
import ParameterBuilder from "../../../integration/QueryParameterBuilder.svelte"
|
||||
import { getBindableProperties } from "builderStore/dataBinding"
|
||||
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
||||
|
||||
export let parameters
|
||||
|
||||
$: datasource = $backendUiStore.datasources.find(
|
||||
ds => ds._id === parameters.datasourceId
|
||||
)
|
||||
// TODO: binding needs to be centralised
|
||||
$: bindableProperties = fetchBindableProperties({
|
||||
componentInstanceId: $store.selectedComponentId,
|
||||
components: $store.components,
|
||||
screen: $currentAsset,
|
||||
tables: $backendUiStore.tables,
|
||||
queries: $backendUiStore.queries,
|
||||
}).map(property => ({
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset.props,
|
||||
$store.selectedComponentId
|
||||
).map(property => ({
|
||||
...property,
|
||||
category: property.type === "instance" ? "Component" : "Table",
|
||||
label: property.readableBinding,
|
|
@ -1,5 +1,4 @@
|
|||
<script>
|
||||
// accepts an array of field names, and outputs an object of { FieldName: value }
|
||||
import {
|
||||
DataList,
|
||||
Label,
|
||||
|
@ -8,13 +7,13 @@
|
|||
Select,
|
||||
Input,
|
||||
} from "@budibase/bbui"
|
||||
import { store, backendUiStore, currentAsset } from "builderStore"
|
||||
import fetchBindableProperties from "builderStore/fetchBindableProperties"
|
||||
import { CloseCircleIcon, AddIcon } from "components/common/Icons"
|
||||
import { store, currentAsset } from "builderStore"
|
||||
import {
|
||||
getBindableProperties,
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "builderStore/replaceBindings"
|
||||
} from "builderStore/dataBinding"
|
||||
import { CloseCircleIcon, AddIcon } from "components/common/Icons"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@ -25,6 +24,11 @@
|
|||
|
||||
const emptyField = () => ({ name: "", value: "" })
|
||||
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset.props,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
|
||||
// this statement initialises fields from parameters.fields
|
||||
$: fields =
|
||||
fields ||
|
||||
|
@ -39,14 +43,6 @@
|
|||
"",
|
||||
}))
|
||||
|
||||
$: bindableProperties = fetchBindableProperties({
|
||||
componentInstanceId: $store.selectedComponentId,
|
||||
components: $store.components,
|
||||
screen: $currentAsset,
|
||||
tables: $backendUiStore.tables,
|
||||
queries: $backendUiStore.queries,
|
||||
})
|
||||
|
||||
const addField = () => {
|
||||
const newFields = fields.filter(f => f.name)
|
||||
newFields.push(emptyField())
|
|
@ -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}
|
||||
|
||||
<SaveFields
|
||||
parameterFields={parameters.fields}
|
||||
schemaFields={automationStatus === AUTOMATION_STATUS.EXISTING && selectedAutomation && selectedAutomation.schema}
|
||||
fieldLabel="Field"
|
||||
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>
|
||||
import { store, currentAsset } from "builderStore"
|
||||
import { store } from "builderStore"
|
||||
import { Select } from "@budibase/bbui"
|
||||
|
||||
export let value
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
export let options = []
|
||||
export let value = []
|
||||
export let styleBindingProperty
|
||||
export let onChange = () => {}
|
||||
|
||||
let boundValue = getValidOptions(value, options)
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import Portal from "svelte-portal"
|
||||
import { buildStyle } from "../../helpers.js"
|
||||
import { buildStyle } from "../../../../helpers.js"
|
||||
|
||||
export let options = []
|
||||
export let value = ""
|
|
@ -1,83 +1,65 @@
|
|||
<script>
|
||||
import { Button, Icon, Drawer, Body } from "@budibase/bbui"
|
||||
import Input from "./PropertyPanelControls/Input.svelte"
|
||||
import { store, backendUiStore, currentAsset } from "builderStore"
|
||||
import fetchBindableProperties from "builderStore/fetchBindableProperties"
|
||||
import { store, currentAsset } from "builderStore"
|
||||
import {
|
||||
getBindableProperties,
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "builderStore/replaceBindings"
|
||||
import BindingPanel from "components/userInterface/BindingPanel.svelte"
|
||||
} from "builderStore/dataBinding"
|
||||
import BindingPanel from "components/design/PropertiesPanel/BindingPanel.svelte"
|
||||
|
||||
export let label = ""
|
||||
export let bindable = true
|
||||
export let componentInstance = {}
|
||||
export let control = null
|
||||
export let key = ""
|
||||
export let value
|
||||
export let type = ""
|
||||
export let value = null
|
||||
export let props = {}
|
||||
export let onChange = () => {}
|
||||
|
||||
let bindingDrawer
|
||||
|
||||
let temporaryBindableValue = value
|
||||
let bindableProperties = []
|
||||
let anchor
|
||||
|
||||
function handleClose() {
|
||||
handleChange(key, temporaryBindableValue)
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset.props,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
$: safeValue = getSafeValue(value, props.defaultValue, bindableProperties)
|
||||
$: replaceBindings = val => readableToRuntimeBinding(bindableProperties, val)
|
||||
|
||||
const handleClose = () => {
|
||||
handleChange(temporaryBindableValue)
|
||||
bindingDrawer.hide()
|
||||
}
|
||||
|
||||
function getBindableProperties() {
|
||||
// Get all bindableProperties
|
||||
bindableProperties = fetchBindableProperties({
|
||||
componentInstanceId: $store.selectedComponentId,
|
||||
components: $store.components,
|
||||
screen: $currentAsset,
|
||||
tables: $backendUiStore.tables,
|
||||
queries: $backendUiStore.queries,
|
||||
})
|
||||
}
|
||||
|
||||
function replaceBindings(textWithBindings) {
|
||||
getBindableProperties()
|
||||
textWithBindings = readableToRuntimeBinding(
|
||||
bindableProperties,
|
||||
textWithBindings
|
||||
)
|
||||
onChange(key, textWithBindings)
|
||||
}
|
||||
|
||||
function handleChange(key, v) {
|
||||
let innerVal = v
|
||||
if (typeof v === "object") {
|
||||
if ("detail" in v) {
|
||||
innerVal = v.detail
|
||||
} else if ("target" in v) {
|
||||
innerVal = props.valueKey ? v.target[props.valueKey] : v.target.value
|
||||
// Handle a value change of any type
|
||||
// String values have any bindings handled
|
||||
const handleChange = value => {
|
||||
let innerVal = value
|
||||
if (value && typeof value === "object") {
|
||||
if ("detail" in value) {
|
||||
innerVal = value.detail
|
||||
} else if ("target" in value) {
|
||||
innerVal = value.target.value
|
||||
}
|
||||
}
|
||||
if (typeof innerVal === "string") {
|
||||
replaceBindings(innerVal)
|
||||
onChange(replaceBindings(innerVal))
|
||||
} else {
|
||||
onChange(key, innerVal)
|
||||
onChange(innerVal)
|
||||
}
|
||||
}
|
||||
|
||||
const safeValue = () => {
|
||||
getBindableProperties()
|
||||
|
||||
let temp = runtimeToReadableBinding(bindableProperties, value)
|
||||
|
||||
return value == null && props.initialValue !== undefined
|
||||
? props.initialValue
|
||||
: temp
|
||||
// The "safe" value is the value with eny bindings made readable
|
||||
// If there is no value set, any default value is used
|
||||
const getSafeValue = (value, defaultValue, bindableProperties) => {
|
||||
const enriched = runtimeToReadableBinding(bindableProperties, value)
|
||||
return enriched == null && defaultValue !== undefined
|
||||
? defaultValue
|
||||
: enriched
|
||||
}
|
||||
|
||||
// Incase the component has a different value key name
|
||||
const handlevalueKey = value =>
|
||||
props.valueKey ? { [props.valueKey]: safeValue() } : { value: safeValue() }
|
||||
</script>
|
||||
|
||||
<div class="property-control" bind:this={anchor}>
|
||||
|
@ -86,13 +68,13 @@
|
|||
<svelte:component
|
||||
this={control}
|
||||
{componentInstance}
|
||||
{...handlevalueKey(value)}
|
||||
on:change={val => handleChange(key, val)}
|
||||
onChange={val => handleChange(key, val)}
|
||||
value={safeValue}
|
||||
on:change={handleChange}
|
||||
onChange={handleChange}
|
||||
{...props}
|
||||
name={key} />
|
||||
</div>
|
||||
{#if bindable && !key.startsWith('_') && control === Input}
|
||||
{#if bindable && !key.startsWith('_') && type === 'text'}
|
||||
<div
|
||||
class="icon"
|
||||
data-cy={`${key}-binding-button`}
|
||||
|
@ -101,7 +83,6 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Drawer bind:this={bindingDrawer} title="Bindings">
|
||||
<div slot="description">
|
||||
<Body extraSmall grey>
|
||||
|
@ -113,7 +94,7 @@
|
|||
</heading>
|
||||
<div slot="body">
|
||||
<BindingPanel
|
||||
{...handlevalueKey(value)}
|
||||
value={safeValue}
|
||||
close={handleClose}
|
||||
on:update={e => (temporaryBindableValue = e.detail)}
|
||||
{bindableProperties} />
|
|
@ -1,5 +1,4 @@
|
|||
<script>
|
||||
import { excludeProps } from "./propertyCategories.js"
|
||||
import PropertyControl from "./PropertyControl.svelte"
|
||||
import { DetailSummary } from "@budibase/bbui"
|
||||
|
||||
|
@ -10,20 +9,17 @@
|
|||
export let onStyleChanged = () => {}
|
||||
export let open = false
|
||||
|
||||
$: style = componentInstance["_styles"][styleCategory] || {}
|
||||
$: changed = properties.some(prop => hasPropChanged(style, prop))
|
||||
|
||||
const hasPropChanged = (style, prop) => {
|
||||
// TODO: replace color picker with one that works better.
|
||||
// Currently it cannot support null values, so this is a hack which
|
||||
// prevents the color fields from always being marked as changed
|
||||
if (!["color", "background", "border-color"].includes(prop.key)) {
|
||||
if (prop.initialValue !== undefined) {
|
||||
return style[prop.key] !== prop.initialValue
|
||||
}
|
||||
}
|
||||
return style[prop.key] != null && style[prop.key] !== ""
|
||||
}
|
||||
|
||||
$: style = componentInstance["_styles"][styleCategory] || {}
|
||||
$: changed = properties.some(prop => hasPropChanged(style, prop))
|
||||
const getControlProps = props => {
|
||||
const { label, key, control, ...otherProps } = props || {}
|
||||
return otherProps || {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<DetailSummary name={`${name}${changed ? ' *' : ''}`} on:open show={open} thin>
|
||||
|
@ -31,12 +27,13 @@
|
|||
<div>
|
||||
{#each properties as prop}
|
||||
<PropertyControl
|
||||
bindable={false}
|
||||
label={`${prop.label}${hasPropChanged(style, prop) ? ' *' : ''}`}
|
||||
control={prop.control}
|
||||
key={prop.key}
|
||||
value={style[prop.key]}
|
||||
onChange={(key, value) => onStyleChanged(styleCategory, key, value)}
|
||||
props={{ ...excludeProps(prop, ['control', 'label']) }} />
|
||||
onChange={value => onStyleChanged(styleCategory, prop.key, value)}
|
||||
props={getControlProps(prop)} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
|
@ -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>
|
||||
import { getBindableProperties } from "builderStore/dataBinding"
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
|
@ -12,7 +13,6 @@
|
|||
import { notifier } from "builderStore/store/notifications"
|
||||
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||
import fetchBindableProperties from "../../builderStore/fetchBindableProperties"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let anchorRight, dropdownRight
|
||||
|
@ -22,11 +22,9 @@
|
|||
|
||||
$: tables = $backendUiStore.tables.map(m => ({
|
||||
label: m.name,
|
||||
name: `all_${m._id}`,
|
||||
tableId: m._id,
|
||||
type: "table",
|
||||
}))
|
||||
|
||||
$: views = $backendUiStore.tables.reduce((acc, cur) => {
|
||||
let viewsArr = Object.entries(cur.views).map(([key, value]) => ({
|
||||
label: key,
|
||||
|
@ -36,41 +34,38 @@
|
|||
}))
|
||||
return [...acc, ...viewsArr]
|
||||
}, [])
|
||||
|
||||
$: queries = $backendUiStore.queries.map(query => ({
|
||||
label: query.name,
|
||||
name: query.name,
|
||||
tableId: query._id,
|
||||
...query,
|
||||
schema: query.schema,
|
||||
parameters: query.parameters,
|
||||
type: "query",
|
||||
}))
|
||||
|
||||
$: bindableProperties = fetchBindableProperties({
|
||||
componentInstanceId: $store.selectedComponentId,
|
||||
components: $store.components,
|
||||
screen: $currentAsset,
|
||||
tables: $backendUiStore.tables,
|
||||
queries: $backendUiStore.queries,
|
||||
})
|
||||
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset.props,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
$: queryBindableProperties = bindableProperties.map(property => ({
|
||||
...property,
|
||||
category: property.type === "instance" ? "Component" : "Table",
|
||||
label: property.readableBinding,
|
||||
path: property.readableBinding,
|
||||
}))
|
||||
|
||||
$: links = bindableProperties
|
||||
.filter(x => x.fieldSchema?.type === "link")
|
||||
.map(property => {
|
||||
return {
|
||||
providerId: property.instance._id,
|
||||
providerId: property.providerId,
|
||||
label: property.readableBinding,
|
||||
fieldName: property.fieldSchema.name,
|
||||
name: `all_${property.fieldSchema.tableId}`,
|
||||
tableId: property.fieldSchema.tableId,
|
||||
type: "link",
|
||||
// These properties will be enriched by the client library and provide
|
||||
// details of the parent row of the relationship field, from context
|
||||
rowId: `{{ ${property.providerId}._id }}`,
|
||||
rowTableId: `{{ ${property.providerId}.tableId }}`,
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -83,9 +78,7 @@
|
|||
const source = $backendUiStore.datasources.find(
|
||||
ds => ds._id === query.datasourceId
|
||||
).source
|
||||
return $backendUiStore.integrations[source].query[query.queryVerb][
|
||||
query.queryType
|
||||
]
|
||||
return $backendUiStore.integrations[source].query[query.queryVerb]
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -93,12 +86,12 @@
|
|||
class="dropdownbutton"
|
||||
bind:this={anchorRight}
|
||||
on:click={dropdownRight.show}>
|
||||
<span>{value.label ? value.label : 'Table / View / Query'}</span>
|
||||
<span>{value?.label ? value.label : 'Choose option'}</span>
|
||||
<Icon name="arrowdown" />
|
||||
</div>
|
||||
{#if value.type === 'query'}
|
||||
{#if value?.type === 'query'}
|
||||
<i class="ri-settings-5-line" on:click={drawer.show} />
|
||||
<Drawer title={'Query'}>
|
||||
<Drawer title={'Query'} bind:this={drawer}>
|
||||
<div slot="buttons">
|
||||
<Button
|
||||
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 OptionSelect from "./OptionSelect.svelte"
|
||||
import FlatButtonGroup from "./FlatButtonGroup.svelte"
|
||||
import Colorpicker from "@budibase/colorpicker"
|
||||
import Input from "./PropertyControls/Input.svelte"
|
||||
import OptionSelect from "./PropertyControls/OptionSelect.svelte"
|
||||
import FlatButtonGroup from "./PropertyControls/FlatButtonGroup"
|
||||
import Colorpicker from "./PropertyControls/ColorPicker.svelte"
|
||||
|
||||
export const layout = [
|
||||
{
|
||||
|
@ -299,42 +299,36 @@ export const size = [
|
|||
key: "width",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
label: "Height",
|
||||
key: "height",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
label: "Min Width",
|
||||
key: "min-width",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
label: "Max Width",
|
||||
key: "max-width",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
label: "Min Height",
|
||||
key: "min-height",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
label: "Max Height",
|
||||
key: "max-height",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
textAlign: "center",
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -357,28 +351,24 @@ export const position = [
|
|||
key: "top",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
label: "Right",
|
||||
key: "right",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
label: "Bottom",
|
||||
key: "bottom",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
label: "Left",
|
||||
key: "left",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
label: "Z-index",
|
||||
|
@ -458,7 +448,6 @@ export const typography = [
|
|||
{ label: "60px", value: "60px" },
|
||||
{ label: "72px", value: "72px" },
|
||||
],
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
label: "Line H",
|
||||
|
@ -478,7 +467,6 @@ export const typography = [
|
|||
label: "Color",
|
||||
key: "color",
|
||||
control: Colorpicker,
|
||||
initialValue: "#000",
|
||||
},
|
||||
{
|
||||
label: "align",
|
||||
|
@ -522,7 +510,6 @@ export const background = [
|
|||
label: "Color",
|
||||
key: "background",
|
||||
control: Colorpicker,
|
||||
initialValue: "#000",
|
||||
},
|
||||
{
|
||||
label: "Gradient",
|
||||
|
@ -645,7 +632,6 @@ export const border = [
|
|||
label: "Color",
|
||||
key: "border-color",
|
||||
control: Colorpicker,
|
||||
initialValue: "#000",
|
||||
},
|
||||
{
|
||||
label: "Style",
|
||||
|
@ -672,7 +658,6 @@ export const effects = [
|
|||
label: "Opacity",
|
||||
key: "opacity",
|
||||
control: OptionSelect,
|
||||
textAlign: "center",
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "0", value: "0" },
|
||||
|
@ -758,7 +743,6 @@ export const transitions = [
|
|||
label: "Duration",
|
||||
key: "transition-duration",
|
||||
control: OptionSelect,
|
||||
textAlign: "center",
|
||||
placeholder: "sec",
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
|
@ -785,7 +769,7 @@ export const transitions = [
|
|||
},
|
||||
]
|
||||
|
||||
export const all = {
|
||||
export const allStyles = {
|
||||
layout,
|
||||
margin,
|
||||
padding,
|
||||
|
@ -797,13 +781,3 @@ export const all = {
|
|||
effects,
|
||||
transitions,
|
||||
}
|
||||
|
||||
export function excludeProps(props, propsToExclude) {
|
||||
const modifiedProps = {}
|
||||
for (const prop in props) {
|
||||
if (!propsToExclude.includes(prop)) {
|
||||
modifiedProps[prop] = props[prop]
|
||||
}
|
||||
}
|
||||
return modifiedProps
|
||||
}
|
|
@ -155,8 +155,9 @@
|
|||
}
|
||||
|
||||
:global(.CodeMirror) {
|
||||
height: auto !important;
|
||||
border-radius: var(--border-radius-m);
|
||||
font-family: var(--font-sans) !important;
|
||||
height: 500px !important;
|
||||
border-radius: var(--border-radius-s);
|
||||
font-family: monospace !important;
|
||||
line-height: 1.3;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -24,18 +24,18 @@
|
|||
</script>
|
||||
|
||||
<form on:submit|preventDefault>
|
||||
<div class="field">
|
||||
{#each schemaKeys as field}
|
||||
<Label extraSmall grey>{field}</Label>
|
||||
<div class="field">
|
||||
<Input
|
||||
placeholder="Enter {field} name"
|
||||
outline
|
||||
disabled={!editable}
|
||||
type={schema.fields[field]?.type}
|
||||
required={schema.fields[field]?.required}
|
||||
bind:value={fields[field]} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</form>
|
||||
<Label extraSmall grey>Data</Label>
|
||||
{#if schema.customisable}
|
||||
<Editor
|
||||
label="Query"
|
||||
|
@ -49,7 +49,7 @@
|
|||
.field {
|
||||
margin-bottom: var(--spacing-m);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2%;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<script>
|
||||
import { Button, TextArea, Label, Input, Heading } from "@budibase/bbui"
|
||||
import BindableInput from "components/userInterface/BindableInput.svelte"
|
||||
import { Button, Input, Heading, Spacer } from "@budibase/bbui"
|
||||
import BindableInput from "components/common/BindableInput.svelte"
|
||||
import {
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "builderStore/replaceBindings"
|
||||
} from "builderStore/dataBinding"
|
||||
|
||||
export let bindable = true
|
||||
export let parameters = []
|
||||
|
@ -31,23 +31,26 @@
|
|||
|
||||
<section>
|
||||
<Heading extraSmall black>Parameters</Heading>
|
||||
<Spacer large />
|
||||
<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}
|
||||
<Input thin disabled={bindable} bind:value={parameter.name} />
|
||||
<Input thin disabled={bindable} bind:value={parameter.default} />
|
||||
<Input
|
||||
placeholder="Parameter Name"
|
||||
thin
|
||||
disabled={bindable}
|
||||
bind:value={parameter.name} />
|
||||
<Input
|
||||
placeholder="Default"
|
||||
thin
|
||||
disabled={bindable}
|
||||
bind:value={parameter.default} />
|
||||
{#if bindable}
|
||||
<BindableInput
|
||||
placeholder="Value"
|
||||
type="string"
|
||||
thin
|
||||
on:change={evt => onBindingChange(parameter.name, evt.detail)}
|
||||
value={runtimeToReadableBinding(bindings, customParams[parameter.name])}
|
||||
value={runtimeToReadableBinding(bindings, customParams?.[parameter.name])}
|
||||
{bindings} />
|
||||
{:else}
|
||||
<i
|
||||
|
@ -57,9 +60,7 @@
|
|||
{/each}
|
||||
</div>
|
||||
{#if !bindable}
|
||||
<Button thin secondary small on:click={newQueryParameter}>
|
||||
Add Parameter
|
||||
</Button>
|
||||
<Button secondary on:click={newQueryParameter}>Add Parameter</Button>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
import { FIELDS } from "constants/backend"
|
||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
|
||||
import EditQueryParamsPopover from "components/backend/DatasourceNavigator/popovers/EditQueryParamsPopover.svelte"
|
||||
import { backendUiStore } from "builderStore"
|
||||
|
||||
const PREVIEW_HEADINGS = [
|
||||
|
@ -40,6 +41,7 @@
|
|||
let tab = "JSON"
|
||||
let parameters
|
||||
let data = []
|
||||
let popover
|
||||
|
||||
$: datasource = $backendUiStore.datasources.find(
|
||||
ds => ds._id === query.datasourceId
|
||||
|
@ -61,7 +63,7 @@
|
|||
$: config = $backendUiStore.integrations[datasourceType]?.query
|
||||
$: docsLink = $backendUiStore.integrations[datasourceType]?.docs
|
||||
|
||||
$: shouldShowQueryConfig = config && query.queryVerb && query.queryType
|
||||
$: shouldShowQueryConfig = config && query.queryVerb
|
||||
|
||||
function newField() {
|
||||
fields = [...fields, {}]
|
||||
|
@ -129,62 +131,44 @@
|
|||
</script>
|
||||
|
||||
<header>
|
||||
<Heading small>{query.name}</Heading>
|
||||
<div class="input">
|
||||
<Input placeholder="✎ Edit Query Name" bind:value={query.name} />
|
||||
</div>
|
||||
{#if config}
|
||||
<div class="queryVerbs">
|
||||
<div class="props">
|
||||
<div class="query-type">Query type: <span class="query-type-span">{config[query.queryVerb].type}</span></div>
|
||||
<div class="select">
|
||||
<Select primary thin bind:value={query.queryVerb}>
|
||||
{#each Object.keys(config) as queryVerb}
|
||||
<div
|
||||
class="queryVerb"
|
||||
class:selected={queryVerb === query.queryVerb}
|
||||
on:click={() => {
|
||||
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>
|
||||
<option value={queryVerb}>{queryVerb}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{/if}
|
||||
<Spacer medium />
|
||||
<Button primary href={docsLink} target="_blank">
|
||||
<i class="ri-book-2-line" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<EditQueryParamsPopover bind:parameters={query.parameters} bindable={false} />
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<Spacer large />
|
||||
<Spacer extraLarge />
|
||||
|
||||
{#if shouldShowQueryConfig}
|
||||
<section>
|
||||
<div class="config">
|
||||
<Label extraSmall grey>Query Name</Label>
|
||||
<Input thin bind:value={query.name} />
|
||||
|
||||
<Spacer medium />
|
||||
|
||||
<IntegrationQueryEditor
|
||||
{query}
|
||||
schema={config[query.queryVerb][query.queryType]}
|
||||
schema={config[query.queryVerb]}
|
||||
bind:parameters />
|
||||
|
||||
<Spacer medium />
|
||||
<Spacer extraLarge />
|
||||
<Spacer large />
|
||||
|
||||
<div class="viewer-controls">
|
||||
<Button
|
||||
wide
|
||||
thin
|
||||
blue
|
||||
disabled={data.length === 0}
|
||||
on:click={saveQuery}>
|
||||
Save
|
||||
Save Query
|
||||
</Button>
|
||||
<Button wide thin primary on:click={previewQuery}>Run</Button>
|
||||
<Button primary on:click={previewQuery}>Run Query</Button>
|
||||
</div>
|
||||
|
||||
<section class="viewer">
|
||||
|
@ -196,10 +180,11 @@
|
|||
<ExternalDataSourceTable {query} {data} />
|
||||
{:else if tab === 'SCHEMA'}
|
||||
{#each fields as field, idx}
|
||||
<Spacer small />
|
||||
<div class="field">
|
||||
<Input thin type={'text'} bind:value={field.name} />
|
||||
<Select secondary thin bind:value={field.type}>
|
||||
<option value={''}>Select an option</option>
|
||||
<Input outline placeholder="Field Name" type={'text'} bind:value={field.name} />
|
||||
<Select thin border bind:value={field.type}>
|
||||
<option value={''}>Select a field type</option>
|
||||
<option value={'STRING'}>Text</option>
|
||||
<option value={'NUMBER'}>Number</option>
|
||||
<option value={'BOOLEAN'}>Boolean</option>
|
||||
|
@ -210,6 +195,7 @@
|
|||
on:click={() => deleteField(idx)} />
|
||||
</div>
|
||||
{/each}
|
||||
<Spacer small />
|
||||
<Button thin secondary on:click={newField}>Add Field</Button>
|
||||
{/if}
|
||||
</Switcher>
|
||||
|
@ -220,11 +206,28 @@
|
|||
{/if}
|
||||
|
||||
<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 {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: 1fr 1fr 50px;
|
||||
margin-bottom: var(--spacing-m);
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
|
||||
a {
|
||||
|
@ -240,6 +243,16 @@
|
|||
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 {
|
||||
width: 800px;
|
||||
height: 100%;
|
||||
|
@ -253,32 +266,18 @@
|
|||
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 {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-m);
|
||||
grid-auto-flow: column;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: auto;
|
||||
direction: rtl;
|
||||
grid-template-columns: 10% 10% 1fr;
|
||||
margin-bottom: var(--spacing-m);
|
||||
z-index: 5;
|
||||
gap: var(--spacing-m);
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.viewer {
|
||||
margin-top: -28px;
|
||||
z-index: -2;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -20,13 +20,6 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
{#if editable}
|
||||
<ParameterBuilder bind:parameters={query.parameters} bindable={false} />
|
||||
<Spacer large />
|
||||
{/if}
|
||||
|
||||
<Heading extraSmall black>Query</Heading>
|
||||
<Spacer medium />
|
||||
|
||||
{#if schema}
|
||||
{#key query._id}
|
||||
|
@ -38,7 +31,6 @@
|
|||
readOnly={!editable}
|
||||
value={query.fields.sql} />
|
||||
{:else if schema.type === QueryTypes.JSON}
|
||||
<Spacer large />
|
||||
<Editor
|
||||
label="Query"
|
||||
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