Merge branch 'master' of github.com:Budibase/budibase into mysql-connector
This commit is contained in:
commit
fcbb27b628
|
@ -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
|
||||
|
|
|
@ -0,0 +1,209 @@
|
|||
import { cloneDeep } from "lodash/fp"
|
||||
import { get } from "svelte/store"
|
||||
import { backendUiStore, store } from "builderStore"
|
||||
import { findAllMatchingComponents, findComponentPath } from "./storeUtils"
|
||||
|
||||
// Regex to match all instances of template strings
|
||||
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
||||
|
||||
/**
|
||||
* Gets all bindable data context fields and instance fields.
|
||||
*/
|
||||
export const getBindableProperties = (rootComponent, componentId) => {
|
||||
const contextBindings = getContextBindings(rootComponent, componentId)
|
||||
const componentBindings = getComponentBindings(rootComponent)
|
||||
return [...contextBindings, ...componentBindings]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all data provider components above a component.
|
||||
*/
|
||||
export const getDataProviderComponents = (rootComponent, componentId) => {
|
||||
if (!rootComponent || !componentId) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Get the component tree leading up to this component, ignoring the component
|
||||
// itself
|
||||
const path = findComponentPath(rootComponent, componentId)
|
||||
path.pop()
|
||||
|
||||
// Filter by only data provider components
|
||||
return path.filter(component => {
|
||||
const def = store.actions.components.getDefinition(component._component)
|
||||
return def?.dataProvider
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a datasource object for a certain data provider component
|
||||
*/
|
||||
export const getDatasourceForProvider = component => {
|
||||
const def = store.actions.components.getDefinition(component?._component)
|
||||
if (!def) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Extract datasource from component instance
|
||||
const datasourceSetting = def.settings.find(setting => {
|
||||
return setting.key === def.datasourceSetting
|
||||
})
|
||||
if (!datasourceSetting) {
|
||||
return null
|
||||
}
|
||||
|
||||
// There are different types of setting which can be a datasource, for
|
||||
// example an actual datasource object, or a table ID string.
|
||||
// Convert the datasource setting into a proper datasource object so that
|
||||
// we can use it properly
|
||||
if (datasourceSetting.type === "datasource") {
|
||||
return component[datasourceSetting?.key]
|
||||
} else if (datasourceSetting.type === "table") {
|
||||
return {
|
||||
tableId: component[datasourceSetting?.key],
|
||||
type: "table",
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all bindable data contexts. These are fields of schemas of data contexts
|
||||
* provided by data provider components, such as lists or row detail components.
|
||||
*/
|
||||
export const getContextBindings = (rootComponent, componentId) => {
|
||||
// Extract any components which provide data contexts
|
||||
const dataProviders = getDataProviderComponents(rootComponent, componentId)
|
||||
let contextBindings = []
|
||||
dataProviders.forEach(component => {
|
||||
const datasource = getDatasourceForProvider(component)
|
||||
if (!datasource) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get schema and add _id and _rev fields for certain types
|
||||
let { schema, table } = getSchemaForDatasource(datasource)
|
||||
if (!schema || !table) {
|
||||
return
|
||||
}
|
||||
if (datasource.type === "table" || datasource.type === "link") {
|
||||
schema["_id"] = { type: "string" }
|
||||
schema["_rev"] = { type: "string " }
|
||||
}
|
||||
const keys = Object.keys(schema).sort()
|
||||
|
||||
// Create bindable properties for each schema field
|
||||
keys.forEach(key => {
|
||||
const fieldSchema = schema[key]
|
||||
// Replace certain bindings with a new property to help display components
|
||||
let runtimeBoundKey = key
|
||||
if (fieldSchema.type === "link") {
|
||||
runtimeBoundKey = `${key}_count`
|
||||
} else if (fieldSchema.type === "attachment") {
|
||||
runtimeBoundKey = `${key}_first`
|
||||
}
|
||||
|
||||
contextBindings.push({
|
||||
type: "context",
|
||||
runtimeBinding: `${component._id}.${runtimeBoundKey}`,
|
||||
readableBinding: `${component._instanceName}.${table.name}.${key}`,
|
||||
fieldSchema,
|
||||
providerId: component._id,
|
||||
tableId: datasource.tableId,
|
||||
field: key,
|
||||
})
|
||||
})
|
||||
})
|
||||
return contextBindings
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all bindable components. These are form components which allow their
|
||||
* values to be bound to.
|
||||
*/
|
||||
export const getComponentBindings = rootComponent => {
|
||||
if (!rootComponent) {
|
||||
return []
|
||||
}
|
||||
const componentSelector = component => {
|
||||
const type = component._component
|
||||
const definition = store.actions.components.getDefinition(type)
|
||||
return definition?.bindable
|
||||
}
|
||||
const components = findAllMatchingComponents(rootComponent, componentSelector)
|
||||
return components.map(component => {
|
||||
return {
|
||||
type: "instance",
|
||||
providerId: component._id,
|
||||
runtimeBinding: `${component._id}`,
|
||||
readableBinding: `${component._instanceName}`,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a schema for a datasource object.
|
||||
*/
|
||||
export const getSchemaForDatasource = datasource => {
|
||||
let schema, table
|
||||
if (datasource) {
|
||||
const { type } = datasource
|
||||
if (type === "query") {
|
||||
const queries = get(backendUiStore).queries
|
||||
table = queries.find(query => query._id === datasource._id)
|
||||
} else {
|
||||
const tables = get(backendUiStore).tables
|
||||
table = tables.find(table => table._id === datasource.tableId)
|
||||
}
|
||||
if (table) {
|
||||
if (type === "view") {
|
||||
schema = cloneDeep(table.views?.[datasource.name]?.schema)
|
||||
} else {
|
||||
schema = cloneDeep(table.schema)
|
||||
}
|
||||
}
|
||||
}
|
||||
return { schema, table }
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a readable data binding into a runtime data binding
|
||||
*/
|
||||
export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
|
||||
if (typeof textWithBindings !== "string") {
|
||||
return textWithBindings
|
||||
}
|
||||
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_TEMPLATE) || []
|
||||
let result = textWithBindings
|
||||
boundValues.forEach(boundValue => {
|
||||
const binding = bindableProperties.find(({ readableBinding }) => {
|
||||
return boundValue === `{{ ${readableBinding} }}`
|
||||
})
|
||||
if (binding) {
|
||||
result = result.replace(boundValue, `{{ ${binding.runtimeBinding} }}`)
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a runtime data binding into a readable data binding
|
||||
*/
|
||||
export function runtimeToReadableBinding(bindableProperties, textWithBindings) {
|
||||
if (typeof textWithBindings !== "string") {
|
||||
return textWithBindings
|
||||
}
|
||||
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_TEMPLATE) || []
|
||||
let result = textWithBindings
|
||||
boundValues.forEach(boundValue => {
|
||||
const binding = bindableProperties.find(({ runtimeBinding }) => {
|
||||
return boundValue === `{{ ${runtimeBinding} }}`
|
||||
})
|
||||
// Show invalid bindings as invalid rather than a long ID
|
||||
result = result.replace(
|
||||
boundValue,
|
||||
`{{ ${binding?.readableBinding ?? "Invalid binding"} }}`
|
||||
)
|
||||
})
|
||||
return result
|
||||
}
|
|
@ -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 (node.props) {
|
||||
traverse(node.props, callback)
|
||||
}
|
||||
if (!$currentAsset || !$store.selectedComponentId) {
|
||||
return null
|
||||
}
|
||||
|
||||
let component
|
||||
traverse($currentAsset, found => {
|
||||
const componentIdentifier = found._component ?? found.props._component
|
||||
const componentDef = componentIdentifier.startsWith("##")
|
||||
? found
|
||||
: $store.components[componentIdentifier]
|
||||
|
||||
component = makePropsSafe(componentDef, found)
|
||||
})
|
||||
|
||||
return component
|
||||
return findComponent($currentAsset.props, $store.selectedComponentId)
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
getDefinition: componentName => {
|
||||
if (!componentName) {
|
||||
return null
|
||||
}
|
||||
if (!componentName.startsWith("@budibase")) {
|
||||
componentName = `@budibase/standard-components/${componentName}`
|
||||
}
|
||||
return get(store).components[componentName]
|
||||
},
|
||||
createInstance: (componentName, presetProps) => {
|
||||
const definition = store.actions.components.getDefinition(componentName)
|
||||
if (!definition) {
|
||||
return null
|
||||
}
|
||||
|
||||
store.update(state => {
|
||||
function findSlot(component_array) {
|
||||
if (!component_array) {
|
||||
return false
|
||||
// Generate default props
|
||||
let props = { ...presetProps }
|
||||
if (definition.settings) {
|
||||
definition.settings.forEach(setting => {
|
||||
if (setting.defaultValue !== undefined) {
|
||||
props[setting.key] = setting.defaultValue
|
||||
}
|
||||
for (let component of component_array) {
|
||||
if (component._component === "##builtin/screenslot") {
|
||||
return true
|
||||
}
|
||||
|
||||
if (component._children) findSlot(component)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
componentToAdd.startsWith("##") &&
|
||||
findSlot(selectedAsset?.props._children)
|
||||
) {
|
||||
return state
|
||||
}
|
||||
|
||||
const component = getComponentDefinition(state, componentToAdd)
|
||||
|
||||
const instanceId = get(backendUiStore).selectedDatabase._id
|
||||
const instanceName = getNewComponentName(component, state)
|
||||
|
||||
const newComponent = createProps(component, {
|
||||
...presetProps,
|
||||
_instanceId: instanceId,
|
||||
_instanceName: instanceName,
|
||||
})
|
||||
}
|
||||
|
||||
const selected = get(selectedComponent)
|
||||
// Add any extra properties the component needs
|
||||
let extras = {}
|
||||
if (definition.hasChildren) {
|
||||
extras._children = []
|
||||
}
|
||||
|
||||
const currentComponentDefinition =
|
||||
state.components[selected._component]
|
||||
return {
|
||||
_id: uuid(),
|
||||
_component: definition.component,
|
||||
_styles: { normal: {}, hover: {}, active: {} },
|
||||
_instanceName: `New ${definition.name}`,
|
||||
...cloneDeep(props),
|
||||
...extras,
|
||||
}
|
||||
},
|
||||
create: (componentName, presetProps) => {
|
||||
const selected = get(selectedComponent)
|
||||
const asset = get(currentAsset)
|
||||
const state = get(store)
|
||||
|
||||
const allowsChildren = currentComponentDefinition.children
|
||||
// Only allow one screen slot, and in the layout
|
||||
if (componentName.endsWith("screenslot")) {
|
||||
const isLayout = state.currentFrontEndType === FrontendTypes.LAYOUT
|
||||
const slot = findComponentType(asset.props, componentName)
|
||||
if (!isLayout || slot != null) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Determine where to put the new component.
|
||||
let targetParent
|
||||
if (allowsChildren) {
|
||||
// Child of the selected component
|
||||
targetParent = selected
|
||||
// Create new component
|
||||
const componentInstance = store.actions.components.createInstance(
|
||||
componentName,
|
||||
presetProps
|
||||
)
|
||||
if (!componentInstance) {
|
||||
return
|
||||
}
|
||||
|
||||
// Find parent node to attach this component to
|
||||
let parentComponent
|
||||
|
||||
if (!asset) {
|
||||
return
|
||||
}
|
||||
if (selected) {
|
||||
// Use current screen or layout as parent if no component is selected
|
||||
const definition = store.actions.components.getDefinition(
|
||||
selected._component
|
||||
)
|
||||
if (definition?.hasChildren) {
|
||||
// Use selected component if it allows children
|
||||
parentComponent = selected
|
||||
} else {
|
||||
// Sibling of selected component
|
||||
targetParent = findParent(selectedAsset.props, selected)
|
||||
// Otherwise we need to use the parent of this component
|
||||
parentComponent = findComponentParent(asset.props, selected._id)
|
||||
}
|
||||
} else {
|
||||
// Use screen or layout if no component is selected
|
||||
parentComponent = asset.props
|
||||
}
|
||||
|
||||
// Don't continue if there's no parent
|
||||
if (!targetParent) return state
|
||||
|
||||
// Push the new component
|
||||
targetParent._children.push(newComponent.props)
|
||||
|
||||
store.actions.preview.saveSelected()
|
||||
// Attach component
|
||||
if (!parentComponent) {
|
||||
return
|
||||
}
|
||||
if (!parentComponent._children) {
|
||||
parentComponent._children = []
|
||||
}
|
||||
parentComponent._children.push(componentInstance)
|
||||
|
||||
// Save components and update UI
|
||||
store.actions.preview.saveSelected()
|
||||
store.update(state => {
|
||||
state.currentView = "component"
|
||||
state.selectedComponentId = newComponent.props._id
|
||||
|
||||
analytics.captureEvent("Added Component", {
|
||||
name: newComponent.props._component,
|
||||
})
|
||||
state.selectedComponentId = componentInstance._id
|
||||
return state
|
||||
})
|
||||
|
||||
// Log event
|
||||
analytics.captureEvent("Added Component", {
|
||||
name: componentInstance._component,
|
||||
})
|
||||
|
||||
return componentInstance
|
||||
},
|
||||
delete: component => {
|
||||
if (!component) {
|
||||
return
|
||||
}
|
||||
const asset = get(currentAsset)
|
||||
if (!asset) {
|
||||
return
|
||||
}
|
||||
const parent = findComponentParent(asset.props, component._id)
|
||||
if (parent) {
|
||||
parent._children = parent._children.filter(
|
||||
child => child._id !== component._id
|
||||
)
|
||||
store.actions.components.select(parent)
|
||||
}
|
||||
store.actions.preview.saveSelected()
|
||||
},
|
||||
copy: (component, cut = false) => {
|
||||
const selectedAsset = get(currentAsset)
|
||||
if (!selectedAsset) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Update store with copied component
|
||||
store.update(state => {
|
||||
state.componentToPaste = cloneDeep(component)
|
||||
state.componentToPaste.isCut = cut
|
||||
if (cut) {
|
||||
const parent = findParent(selectedAsset.props, component._id)
|
||||
return state
|
||||
})
|
||||
|
||||
// Remove the component from its parent if we're cutting
|
||||
if (cut) {
|
||||
const parent = findComponentParent(selectedAsset.props, component._id)
|
||||
if (parent) {
|
||||
parent._children = parent._children.filter(
|
||||
child => child._id !== component._id
|
||||
)
|
||||
store.actions.components.select(parent)
|
||||
}
|
||||
|
||||
return state
|
||||
})
|
||||
}
|
||||
},
|
||||
paste: async (targetComponent, mode) => {
|
||||
const selectedAsset = get(currentAsset)
|
||||
let promises = []
|
||||
store.update(state => {
|
||||
if (!state.componentToPaste) return state
|
||||
|
||||
const componentToPaste = cloneDeep(state.componentToPaste)
|
||||
// retain the same ids as things may be referencing this component
|
||||
if (componentToPaste.isCut) {
|
||||
// in case we paste a second time
|
||||
state.componentToPaste.isCut = false
|
||||
} else {
|
||||
generateNewIdsForComponent(componentToPaste, state)
|
||||
}
|
||||
delete componentToPaste.isCut
|
||||
|
||||
if (mode === "inside") {
|
||||
targetComponent._children.push(componentToPaste)
|
||||
// Stop if we have nothing to paste
|
||||
if (!state.componentToPaste) {
|
||||
return state
|
||||
}
|
||||
|
||||
const parent = findParent(selectedAsset.props, targetComponent)
|
||||
// Clone the component to paste
|
||||
// Retain the same ID if cutting as things may be referencing this component
|
||||
const cut = state.componentToPaste.isCut
|
||||
delete state.componentToPaste.isCut
|
||||
let componentToPaste = cloneDeep(state.componentToPaste)
|
||||
if (cut) {
|
||||
state.componentToPaste = null
|
||||
} else {
|
||||
componentToPaste._id = uuid()
|
||||
}
|
||||
|
||||
const targetIndex = parent._children.indexOf(targetComponent)
|
||||
const index = mode === "above" ? targetIndex : targetIndex + 1
|
||||
parent._children.splice(index, 0, cloneDeep(componentToPaste))
|
||||
if (mode === "inside") {
|
||||
// Paste inside target component if chosen
|
||||
if (!targetComponent._children) {
|
||||
targetComponent._children = []
|
||||
}
|
||||
targetComponent._children.push(componentToPaste)
|
||||
} else {
|
||||
// Otherwise find the parent so we can paste in the correct order
|
||||
// in the parents child components
|
||||
const selectedAsset = get(currentAsset)
|
||||
if (!selectedAsset) {
|
||||
return state
|
||||
}
|
||||
const parent = findComponentParent(
|
||||
selectedAsset.props,
|
||||
targetComponent._id
|
||||
)
|
||||
if (!parent) {
|
||||
return state
|
||||
}
|
||||
|
||||
// Insert the component in the correct position
|
||||
const targetIndex = parent._children.indexOf(targetComponent)
|
||||
const index = mode === "above" ? targetIndex : targetIndex + 1
|
||||
parent._children.splice(index, 0, cloneDeep(componentToPaste))
|
||||
}
|
||||
|
||||
// Save and select the new component
|
||||
promises.push(store.actions.preview.saveSelected())
|
||||
store.actions.components.select(componentToPaste)
|
||||
|
||||
return state
|
||||
})
|
||||
await Promise.all(promises)
|
||||
|
@ -389,90 +473,56 @@ export const getFrontendStore = () => {
|
|||
await store.actions.preview.saveSelected()
|
||||
},
|
||||
updateProp: (name, value) => {
|
||||
let component = get(selectedComponent)
|
||||
if (!name || !component) {
|
||||
return
|
||||
}
|
||||
component[name] = value
|
||||
store.update(state => {
|
||||
let current_component = get(selectedComponent)
|
||||
current_component[name] = value
|
||||
|
||||
state.selectedComponentId = current_component._id
|
||||
store.actions.preview.saveSelected()
|
||||
state.selectedComponentId = component._id
|
||||
return state
|
||||
})
|
||||
},
|
||||
findRoute: component => {
|
||||
// Gets all the components to needed to construct a path.
|
||||
const selectedAsset = get(currentAsset)
|
||||
let pathComponents = []
|
||||
let parent = component
|
||||
let root = false
|
||||
while (!root) {
|
||||
parent = findParent(selectedAsset.props, parent)
|
||||
if (!parent) {
|
||||
root = true
|
||||
} else {
|
||||
pathComponents.push(parent)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove root entry since it's the screen or layout.
|
||||
// Reverse array since we need the correct order of the IDs
|
||||
const reversedComponents = pathComponents.reverse().slice(1)
|
||||
|
||||
// Add component
|
||||
const allComponents = [...reversedComponents, component]
|
||||
|
||||
// Map IDs
|
||||
const IdList = allComponents.map(c => c._id)
|
||||
|
||||
// Construct ID Path:
|
||||
return IdList.join("/")
|
||||
store.actions.preview.saveSelected()
|
||||
},
|
||||
links: {
|
||||
save: async (url, title) => {
|
||||
let promises = []
|
||||
const layout = get(mainLayout)
|
||||
store.update(state => {
|
||||
// Try to extract a nav component from the master layout
|
||||
const nav = findChildComponentType(
|
||||
layout,
|
||||
"@budibase/standard-components/navigation"
|
||||
)
|
||||
if (nav) {
|
||||
let newLink
|
||||
if (!layout) {
|
||||
return
|
||||
}
|
||||
|
||||
// Clone an existing link if one exists
|
||||
if (nav._children && nav._children.length) {
|
||||
// Clone existing link style
|
||||
newLink = cloneDeep(nav._children[0])
|
||||
// Find a nav bar in the main layout
|
||||
const nav = findComponentType(
|
||||
layout.props,
|
||||
"@budibase/standard-components/navigation"
|
||||
)
|
||||
if (!nav) {
|
||||
return
|
||||
}
|
||||
|
||||
// Manipulate IDs to ensure uniqueness
|
||||
generateNewIdsForComponent(newLink, state, false)
|
||||
let newLink
|
||||
if (nav._children && nav._children.length) {
|
||||
// Clone an existing link if one exists
|
||||
newLink = cloneDeep(nav._children[0])
|
||||
|
||||
// Set our new props
|
||||
newLink._instanceName = `${title} Link`
|
||||
newLink.url = url
|
||||
newLink.text = title
|
||||
} else {
|
||||
// Otherwise create vanilla new link
|
||||
const component = getComponentDefinition(
|
||||
state,
|
||||
"@budibase/standard-components/link"
|
||||
)
|
||||
const instanceId = get(backendUiStore).selectedDatabase._id
|
||||
newLink = createProps(component, {
|
||||
url,
|
||||
text: title,
|
||||
_instanceName: `${title} Link`,
|
||||
_instanceId: instanceId,
|
||||
}).props
|
||||
}
|
||||
|
||||
// Save layout
|
||||
nav._children = [...nav._children, newLink]
|
||||
promises.push(store.actions.layouts.save(layout))
|
||||
// Set our new props
|
||||
newLink._id = uuid()
|
||||
newLink._instanceName = `${title} Link`
|
||||
newLink.url = url
|
||||
newLink.text = title
|
||||
} else {
|
||||
// Otherwise create vanilla new link
|
||||
newLink = {
|
||||
...store.actions.components.createInstance("link"),
|
||||
url,
|
||||
text: title,
|
||||
_instanceName: `${title} Link`,
|
||||
}
|
||||
return state
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
// Save layout
|
||||
nav._children = [...nav._children, newLink]
|
||||
await store.actions.layouts.save(layout)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 currentIndex = parent._children.indexOf(component)
|
||||
if (currentIndex === 0) return state
|
||||
|
||||
const newChildren = parent._children.filter(c => c !== component)
|
||||
newChildren.splice(currentIndex - 1, 0, component)
|
||||
parent._children = newChildren
|
||||
}
|
||||
state.selectedComponentId = component._id
|
||||
store.actions.preview.saveSelected()
|
||||
|
||||
return state
|
||||
})
|
||||
const asset = get(currentAsset)
|
||||
const parent = findComponentParent(asset.props, component._id)
|
||||
if (!parent) {
|
||||
return
|
||||
}
|
||||
const currentIndex = parent._children.indexOf(component)
|
||||
if (currentIndex === 0) {
|
||||
return
|
||||
}
|
||||
const newChildren = parent._children.filter(c => c !== component)
|
||||
newChildren.splice(currentIndex - 1, 0, component)
|
||||
parent._children = newChildren
|
||||
store.actions.preview.saveSelected()
|
||||
}
|
||||
|
||||
const moveDownComponent = () => {
|
||||
store.update(state => {
|
||||
const asset = get(currentAsset)
|
||||
const parent = findParent(asset.props, component)
|
||||
|
||||
if (parent) {
|
||||
const currentIndex = parent._children.indexOf(component)
|
||||
if (currentIndex === parent._children.length - 1) return state
|
||||
|
||||
const newChildren = parent._children.filter(c => c !== component)
|
||||
newChildren.splice(currentIndex + 1, 0, component)
|
||||
parent._children = newChildren
|
||||
}
|
||||
state.selectedComponentId = component._id
|
||||
store.actions.preview.saveSelected()
|
||||
|
||||
return state
|
||||
})
|
||||
const asset = get(currentAsset)
|
||||
const parent = findComponentParent(asset.props, component._id)
|
||||
if (!parent) {
|
||||
return
|
||||
}
|
||||
const currentIndex = parent._children.indexOf(component)
|
||||
if (currentIndex === parent._children.length - 1) {
|
||||
return
|
||||
}
|
||||
const newChildren = parent._children.filter(c => c !== component)
|
||||
newChildren.splice(currentIndex + 1, 0, component)
|
||||
parent._children = newChildren
|
||||
store.actions.preview.saveSelected()
|
||||
}
|
||||
|
||||
const duplicateComponent = () => {
|
||||
|
@ -78,18 +66,7 @@
|
|||
}
|
||||
|
||||
const deleteComponent = () => {
|
||||
store.update(state => {
|
||||
const asset = get(currentAsset)
|
||||
const parent = findParent(asset.props, component)
|
||||
|
||||
if (parent) {
|
||||
parent._children = parent._children.filter(child => child !== component)
|
||||
selectComponent(parent)
|
||||
}
|
||||
|
||||
store.actions.preview.saveSelected()
|
||||
return state
|
||||
})
|
||||
store.actions.components.delete(component)
|
||||
}
|
||||
|
||||
const storeComponentForCopy = (cut = false) => {
|
|
@ -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) {
|
||||
frontendStore.actions.components.copy(state.dragged, true)
|
||||
frontendStore.actions.components.paste(
|
||||
state.targetComponent,
|
||||
state.dropPosition
|
||||
)
|
||||
}
|
||||
const state = get(store)
|
||||
|
||||
store.actions.reset()
|
||||
// Stop if the target and source are the same
|
||||
if (state.targetComponent === state.dragged) {
|
||||
return
|
||||
}
|
||||
// Stop if the target or source are null
|
||||
if (!state.targetComponent || !state.dragged) {
|
||||
return
|
||||
}
|
||||
// Stop if the target is a child of source
|
||||
const path = findComponentPath(state.dragged, state.targetComponent._id)
|
||||
const ids = path.map(component => component._id)
|
||||
if (ids.includes(state.targetComponent._id)) {
|
||||
return
|
||||
}
|
||||
|
||||
return state
|
||||
})
|
||||
// Cut and paste the component
|
||||
frontendStore.actions.components.copy(state.dragged, true)
|
||||
frontendStore.actions.components.paste(
|
||||
state.targetComponent,
|
||||
state.dropPosition
|
||||
)
|
||||
store.actions.reset()
|
||||
},
|
||||
}
|
||||
|
|
@ -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
|
||||
|
@ -63,6 +63,6 @@
|
|||
<Button thin blue on:click={saveEventData}>Save</Button>
|
||||
</heading>
|
||||
<div slot="body">
|
||||
<EventEditor event={value} eventType={name} />
|
||||
<EventEditor bind:actions={value} eventType={name} />
|
||||
</div>
|
||||
</Drawer>
|
|
@ -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)
|
||||
$: 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 }}`,
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -91,39 +86,39 @@
|
|||
class="dropdownbutton"
|
||||
bind:this={anchorRight}
|
||||
on:click={dropdownRight.show}>
|
||||
<span>{value.label ? value.label : 'Table / View / Query'}</span>
|
||||
<span>{value?.label ? value.label : 'Choose option'}</span>
|
||||
<Icon name="arrowdown" />
|
||||
</div>
|
||||
{#if value.type === 'query'}
|
||||
{#if value?.type === 'query'}
|
||||
<i class="ri-settings-5-line" on:click={drawer.show} />
|
||||
<Drawer title={'Query'} bind:this={drawer}>
|
||||
<div slot="buttons">
|
||||
<Button
|
||||
blue
|
||||
thin
|
||||
on:click={() => {
|
||||
notifier.success('Query parameters saved.')
|
||||
handleSelected(value)
|
||||
drawer.hide()
|
||||
}}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<div class="drawer-contents" slot="body">
|
||||
<IntegrationQueryEditor
|
||||
query={value}
|
||||
schema={fetchDatasourceSchema(value)}
|
||||
editable={false} />
|
||||
<Spacer large />
|
||||
{#if value.parameters.length > 0}
|
||||
<ParameterBuilder
|
||||
bind:customParams={value.queryParams}
|
||||
parameters={queries.find(query => query._id === value._id).parameters}
|
||||
bindings={queryBindableProperties} />
|
||||
{/if}
|
||||
</div>
|
||||
</Drawer>
|
||||
{/if}
|
||||
<Drawer title={'Query'} bind:this={drawer}>
|
||||
<div slot="buttons">
|
||||
<Button
|
||||
blue
|
||||
thin
|
||||
on:click={() => {
|
||||
notifier.success('Query parameters saved.')
|
||||
handleSelected(value)
|
||||
drawer.hide()
|
||||
}}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<div class="drawer-contents" slot="body">
|
||||
<IntegrationQueryEditor
|
||||
query={value}
|
||||
schema={fetchDatasourceSchema(value)}
|
||||
editable={false} />
|
||||
<Spacer large />
|
||||
{#if value.parameters.length > 0}
|
||||
<ParameterBuilder
|
||||
bind:customParams={value.queryParams}
|
||||
parameters={queries.find(query => query._id === value._id).parameters}
|
||||
bindings={queryBindableProperties} />
|
||||
{/if}
|
||||
</div>
|
||||
</Drawer>
|
||||
{/if}
|
||||
<DropdownMenu bind:this={dropdownRight} anchor={anchorRight}>
|
||||
<div class="dropdown">
|
||||
<div class="title">
|
|
@ -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
|
||||
}
|
|
@ -1,17 +1,10 @@
|
|||
<script>
|
||||
import {
|
||||
Button,
|
||||
TextArea,
|
||||
Label,
|
||||
Input,
|
||||
Heading,
|
||||
Spacer,
|
||||
} from "@budibase/bbui"
|
||||
import BindableInput from "components/userInterface/BindableInput.svelte"
|
||||
import { Button, Input, Heading, Spacer } from "@budibase/bbui"
|
||||
import BindableInput from "components/common/BindableInput.svelte"
|
||||
import {
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "builderStore/replaceBindings"
|
||||
} from "builderStore/dataBinding"
|
||||
|
||||
export let bindable = true
|
||||
export let parameters = []
|
||||
|
@ -57,7 +50,7 @@
|
|||
type="string"
|
||||
thin
|
||||
on:change={evt => onBindingChange(parameter.name, evt.detail)}
|
||||
value={runtimeToReadableBinding(bindings, customParams[parameter.name])}
|
||||
value={runtimeToReadableBinding(bindings, customParams?.[parameter.name])}
|
||||
{bindings} />
|
||||
{:else}
|
||||
<i
|
||||
|
|
|
@ -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 }
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
export const TYPE_MAP = {
|
||||
string: {
|
||||
default: "",
|
||||
},
|
||||
bool: {
|
||||
default: false,
|
||||
},
|
||||
number: {
|
||||
default: 0,
|
||||
},
|
||||
options: {
|
||||
default: [],
|
||||
},
|
||||
event: {
|
||||
default: [],
|
||||
},
|
||||
state: {
|
||||
default: {
|
||||
"##bbstate": "",
|
||||
},
|
||||
},
|
||||
tables: {
|
||||
default: {},
|
||||
},
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -3,7 +3,7 @@
|
|||
import { Button } from "@budibase/bbui"
|
||||
import SettingsLink from "components/settings/Link.svelte"
|
||||
import ThemeEditorDropdown from "components/settings/ThemeEditorDropdown.svelte"
|
||||
import FeedbackNavLink from "components/userInterface/Feedback/FeedbackNavLink.svelte"
|
||||
import FeedbackNavLink from "components/feedback/FeedbackNavLink.svelte"
|
||||
import { get } from "builderStore/api"
|
||||
import { isActive, goto, layout } from "@sveltech/routify"
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import Spinner from "components/common/Spinner.svelte"
|
||||
import DeploymentHistory from "components/deploy/DeploymentHistory.svelte"
|
||||
import analytics from "analytics"
|
||||
import FeedbackIframe from "components/userInterface/Feedback/FeedbackIframe.svelte"
|
||||
import FeedbackIframe from "components/feedback/FeedbackIframe.svelte"
|
||||
|
||||
let loading = false
|
||||
let deployments = []
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
<script>
|
||||
import { params, leftover, goto } from "@sveltech/routify"
|
||||
import { FrontendTypes } from "constants"
|
||||
import { store, allScreens } from "builderStore"
|
||||
|
||||
// Get any leftover params not caught by Routifys params store.
|
||||
const componentIds = $leftover.split("/").filter(id => id !== "")
|
||||
|
||||
const currentAssetId = decodeURI($params.asset)
|
||||
|
||||
let assetList
|
||||
let actions
|
||||
|
||||
// Determine screens or layouts based on the URL
|
||||
if ($params.assetType === FrontendTypes.SCREEN) {
|
||||
assetList = $allScreens
|
||||
actions = store.actions.screens
|
||||
} else {
|
||||
assetList = $store.layouts
|
||||
actions = store.actions.layouts
|
||||
}
|
||||
|
||||
// select the screen or layout in the UI
|
||||
actions.select(currentAssetId)
|
||||
|
||||
// There are leftover stuff, like IDs, so navigate the components and find the ID and select it.
|
||||
if ($leftover) {
|
||||
// Get the correct screen children.
|
||||
const assetChildren =
|
||||
assetList.find(
|
||||
asset =>
|
||||
asset._id === $params.asset ||
|
||||
asset._id === decodeURIComponent($params.asset)
|
||||
)?.props._children ?? []
|
||||
findComponent(componentIds, assetChildren)
|
||||
}
|
||||
// }
|
||||
|
||||
// Find Component with ID and continue
|
||||
function findComponent(ids, children) {
|
||||
// Setup stuff
|
||||
let componentToSelect
|
||||
let currentChildren = children
|
||||
|
||||
// Loop through each ID
|
||||
ids.forEach(id => {
|
||||
// Find ID
|
||||
const component = currentChildren.find(child => child._id === id)
|
||||
|
||||
// If it does not exist, ignore (use last valid route)
|
||||
if (!component) return
|
||||
|
||||
componentToSelect = component
|
||||
|
||||
// Update childrens array to selected components children
|
||||
currentChildren = componentToSelect._children
|
||||
})
|
||||
|
||||
// Select Component!
|
||||
if (componentToSelect) store.actions.components.select(componentToSelect)
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -1,34 +1,134 @@
|
|||
<script>
|
||||
import {
|
||||
store,
|
||||
backendUiStore,
|
||||
currentAsset,
|
||||
selectedComponent,
|
||||
allScreens,
|
||||
} from "builderStore"
|
||||
import { onMount } from "svelte"
|
||||
import CurrentItemPreview from "components/userInterface/AppPreview"
|
||||
import ComponentPropertiesPanel from "components/userInterface/ComponentPropertiesPanel.svelte"
|
||||
import ComponentSelectionList from "components/userInterface/ComponentSelectionList.svelte"
|
||||
import FrontendNavigatePane from "components/userInterface/FrontendNavigatePane.svelte"
|
||||
import CurrentItemPreview from "components/design/AppPreview"
|
||||
import PropertiesPanel from "components/design/PropertiesPanel/PropertiesPanel.svelte"
|
||||
import ComponentSelectionList from "components/design/AppPreview/ComponentSelectionList.svelte"
|
||||
import FrontendNavigatePane from "components/design/NavigationPanel/FrontendNavigatePane.svelte"
|
||||
import { goto, leftover, params } from "@sveltech/routify"
|
||||
import { FrontendTypes } from "constants"
|
||||
import { findComponent, findComponentPath } from "builderStore/storeUtils"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
$: instance = $store.appInstance
|
||||
// Cache previous values so we don't update the URL more than necessary
|
||||
let previousType
|
||||
let previousAsset
|
||||
let previousComponentId
|
||||
|
||||
async function selectDatabase(database) {
|
||||
backendUiStore.actions.database.select(database)
|
||||
// Hydrate state from URL params
|
||||
$: hydrateStateFromURL($params, $leftover)
|
||||
|
||||
// Keep URL in sync with state
|
||||
$: updateURLFromState(
|
||||
$store.currentFrontEndType,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
|
||||
const hydrateStateFromURL = (params, leftover) => {
|
||||
// Do nothing if no asset type, as that means we've left the page
|
||||
if (!params.assetType) {
|
||||
return
|
||||
}
|
||||
|
||||
const state = get(store)
|
||||
const selectedAsset = get(currentAsset)
|
||||
|
||||
// Hydrate asset type
|
||||
let assetType = params.assetType
|
||||
if (![FrontendTypes.LAYOUT, FrontendTypes.SCREEN].includes(assetType)) {
|
||||
assetType = FrontendTypes.SCREEN
|
||||
}
|
||||
if (assetType !== state.currentFrontEndType) {
|
||||
store.update(state => {
|
||||
state.currentFrontEndType = assetType
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
// Hydrate asset
|
||||
const assetId = decodeURI(params.asset)
|
||||
let asset
|
||||
if (assetId) {
|
||||
let assetList
|
||||
let actions
|
||||
|
||||
// Determine screens or layouts based on the URL
|
||||
if (assetType === FrontendTypes.SCREEN) {
|
||||
assetList = get(allScreens)
|
||||
actions = store.actions.screens
|
||||
} else {
|
||||
assetList = state.layouts
|
||||
actions = store.actions.layouts
|
||||
}
|
||||
|
||||
// Find and select the current asset
|
||||
asset = assetList.find(asset => asset._id === assetId)
|
||||
if (asset && asset._id !== selectedAsset?._id) {
|
||||
actions.select(assetId)
|
||||
}
|
||||
}
|
||||
|
||||
// Hydrate component ID if one is present in the URL
|
||||
const selectedComponentId = leftover.split("/").pop()
|
||||
if (asset && selectedComponentId) {
|
||||
const component = findComponent(asset.props, selectedComponentId)
|
||||
if (component && component._id !== state.selectedComponentId) {
|
||||
store.actions.components.select(component)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if ($store.appInstance && !$backendUiStore.database) {
|
||||
await selectDatabase($store.appInstance)
|
||||
// Updates the route params in the URL to the specified values
|
||||
const updateURLFromState = (assetType, asset, componentId) => {
|
||||
// Check we have different params than last invocation
|
||||
if (
|
||||
assetType === previousType &&
|
||||
asset === previousAsset &&
|
||||
componentId === previousComponentId
|
||||
) {
|
||||
return
|
||||
} else {
|
||||
previousType = assetType
|
||||
previousAsset = asset
|
||||
previousComponentId = componentId
|
||||
}
|
||||
})
|
||||
|
||||
let confirmDeleteDialog
|
||||
let componentToDelete = ""
|
||||
// Extract current URL params
|
||||
const currentParams = get(params)
|
||||
const currentLeftover = get(leftover)
|
||||
const paramAssetType = currentParams.assetType
|
||||
const paramAssetId = currentParams.asset
|
||||
const paramComponentId = currentLeftover.split("/").pop()
|
||||
|
||||
let settingsView
|
||||
const settings = () => {
|
||||
settingsView.show()
|
||||
// Only update params if the params actually changed
|
||||
if (
|
||||
assetType !== paramAssetType ||
|
||||
asset?._id !== paramAssetId ||
|
||||
componentId !== paramComponentId
|
||||
) {
|
||||
// Build and navigate to a valid URL
|
||||
let url = "../"
|
||||
if ([FrontendTypes.SCREEN, FrontendTypes.LAYOUT].includes(assetType)) {
|
||||
url += `${assetType}`
|
||||
if (asset?._id) {
|
||||
url += `/${asset._id}`
|
||||
if (componentId) {
|
||||
const componentPath = findComponentPath(asset.props, componentId)
|
||||
const componentURL = componentPath
|
||||
.slice(1)
|
||||
.map(comp => comp._id)
|
||||
.join("/")
|
||||
url += `/${componentURL}`
|
||||
}
|
||||
}
|
||||
}
|
||||
$goto(url)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -49,7 +149,7 @@
|
|||
|
||||
{#if $selectedComponent != null}
|
||||
<div class="components-pane">
|
||||
<ComponentPropertiesPanel />
|
||||
<PropertiesPanel />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -1,36 +1,50 @@
|
|||
<script>
|
||||
import { store, allScreens } from "builderStore"
|
||||
import { get } from "svelte/store"
|
||||
import { store, allScreens, selectedAccessRole } from "builderStore"
|
||||
import { FrontendTypes } from "constants"
|
||||
import { goto, params } from "@sveltech/routify"
|
||||
import { params } from "@sveltech/routify"
|
||||
|
||||
// Go to first layout
|
||||
if ($params.assetType === FrontendTypes.LAYOUT) {
|
||||
// Try to use previously selected layout first
|
||||
let id
|
||||
if (
|
||||
$store.selectedLayoutId &&
|
||||
$store.layouts.find(layout => layout._id === $store.selectedLayoutId)
|
||||
) {
|
||||
id = $store.selectedLayoutId
|
||||
} else {
|
||||
id = $store.layouts[0]?._id
|
||||
}
|
||||
$goto(`../${id}`)
|
||||
}
|
||||
$: selectValidAsset($params.assetType)
|
||||
|
||||
// Go to first screen
|
||||
if ($params.assetType === FrontendTypes.SCREEN) {
|
||||
// Try to use previously selected layout first
|
||||
// If we ever land on this index page we want to correctly update state
|
||||
// to select a valid asset. The layout page will in turn update the URL
|
||||
// to reflect state.
|
||||
const selectValidAsset = assetType => {
|
||||
let id
|
||||
if (
|
||||
$store.selectedScreenId &&
|
||||
$allScreens.find(screen => screen._id === $store.selectedScreenId)
|
||||
) {
|
||||
id = $store.selectedScreenId
|
||||
} else {
|
||||
id = $allScreens[0]?._id
|
||||
const state = get(store)
|
||||
const screens = get(allScreens)
|
||||
const role = get(selectedAccessRole)
|
||||
|
||||
// Get ID or first correct asset type and select it
|
||||
if (assetType === FrontendTypes.LAYOUT) {
|
||||
if (
|
||||
state.selectedLayoutId &&
|
||||
state.layouts.find(layout => layout._id === state.selectedLayoutId)
|
||||
) {
|
||||
id = state.selectedLayoutId
|
||||
} else {
|
||||
id = state.layouts[0]?._id
|
||||
}
|
||||
if (id) {
|
||||
store.actions.layouts.select(id)
|
||||
}
|
||||
} else if (assetType === FrontendTypes.SCREEN) {
|
||||
if (
|
||||
state.selectedScreenId &&
|
||||
screens.find(screen => screen._id === state.selectedScreenId)
|
||||
) {
|
||||
id = state.selectedScreenId
|
||||
} else {
|
||||
// Select the first screen matching the selected role ID
|
||||
const filteredScreens = screens.filter(screen => {
|
||||
return screen.routing?.roleId === role
|
||||
})
|
||||
id = filteredScreens[0]?._id
|
||||
}
|
||||
if (id) {
|
||||
store.actions.screens.select(id)
|
||||
}
|
||||
}
|
||||
$goto(`../${id}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,163 +0,0 @@
|
|||
import { createProps } from "../src/components/userInterface/assetParsing/createProps"
|
||||
import { keys, some } from "lodash/fp"
|
||||
import { stripStandardProps } from "./testData"
|
||||
|
||||
describe("createDefaultProps", () => {
|
||||
const getcomponent = () => ({
|
||||
_component: "some_component",
|
||||
name: "some_component",
|
||||
props: {
|
||||
fieldName: { type: "string", default: "something" },
|
||||
},
|
||||
})
|
||||
|
||||
it("should create a object with single string value, when default string field set", () => {
|
||||
const { props, errors } = createProps(getcomponent())
|
||||
|
||||
expect(errors).toEqual([])
|
||||
expect(props.fieldName).toBeDefined()
|
||||
expect(props.fieldName).toBe("something")
|
||||
stripStandardProps(props)
|
||||
expect(keys(props).length).toBe(3)
|
||||
})
|
||||
|
||||
it("should set component _component", () => {
|
||||
const { props, errors } = createProps(getcomponent())
|
||||
|
||||
expect(errors).toEqual([])
|
||||
expect(props._component).toBe("some_component")
|
||||
})
|
||||
|
||||
it("should create a object with single blank string value, when prop definition is 'string' ", () => {
|
||||
const comp = getcomponent()
|
||||
comp.props.fieldName = "string"
|
||||
|
||||
const { props, errors } = createProps(comp)
|
||||
|
||||
expect(errors).toEqual([])
|
||||
expect(props.fieldName).toBeDefined()
|
||||
expect(props.fieldName).toBe("")
|
||||
})
|
||||
|
||||
it("should create a object with single fals value, when prop definition is 'bool' ", () => {
|
||||
const comp = getcomponent()
|
||||
comp.props.isVisible = "bool"
|
||||
|
||||
const { props, errors } = createProps(comp)
|
||||
|
||||
expect(errors).toEqual([])
|
||||
expect(props.isVisible).toBeDefined()
|
||||
expect(props.isVisible).toBe(false)
|
||||
})
|
||||
|
||||
it("should create a object with single 0 value, when prop definition is 'number' ", () => {
|
||||
const comp = getcomponent()
|
||||
comp.props.width = "number"
|
||||
|
||||
const { props, errors } = createProps(comp)
|
||||
|
||||
expect(errors).toEqual([])
|
||||
expect(props.width).toBeDefined()
|
||||
expect(props.width).toBe(0)
|
||||
})
|
||||
|
||||
it("should create a object with empty _children array, when children===true ", () => {
|
||||
const comp = getcomponent()
|
||||
comp.children = true
|
||||
|
||||
const { props, errors } = createProps(comp)
|
||||
|
||||
expect(errors).toEqual([])
|
||||
expect(props._children).toBeDefined()
|
||||
expect(props._children).toEqual([])
|
||||
})
|
||||
|
||||
it("should create a object with single empty array, when prop definition is 'event' ", () => {
|
||||
const comp = getcomponent()
|
||||
comp.props.onClick = "event"
|
||||
|
||||
const { props, errors } = createProps(comp)
|
||||
|
||||
expect(errors).toEqual([])
|
||||
expect(props.onClick).toBeDefined()
|
||||
expect(props.onClick).toEqual([])
|
||||
})
|
||||
|
||||
it("should create a object children array when children == true ", () => {
|
||||
const comp = getcomponent()
|
||||
comp.children = true
|
||||
|
||||
const { props, errors } = createProps(comp)
|
||||
|
||||
expect(errors).toEqual([])
|
||||
expect(props._children).toBeDefined()
|
||||
expect(props._children).toEqual([])
|
||||
})
|
||||
|
||||
it("should always create _children ", () => {
|
||||
const comp = getcomponent()
|
||||
comp.children = false
|
||||
|
||||
const createRes1 = createProps(comp)
|
||||
|
||||
expect(createRes1.errors).toEqual([])
|
||||
expect(createRes1.props._children).toBeDefined()
|
||||
|
||||
const comp2 = getcomponent()
|
||||
comp2.children = true
|
||||
|
||||
const createRes2 = createProps(comp)
|
||||
|
||||
expect(createRes2.errors).toEqual([])
|
||||
expect(createRes2.props._children).toBeDefined()
|
||||
})
|
||||
|
||||
it("should create an object with multiple prop names", () => {
|
||||
const comp = getcomponent()
|
||||
comp.props.fieldName = "string"
|
||||
comp.props.fieldLength = { type: "number", default: 500 }
|
||||
|
||||
const { props, errors } = createProps(comp)
|
||||
|
||||
expect(errors).toEqual([])
|
||||
expect(props.fieldName).toBeDefined()
|
||||
expect(props.fieldName).toBe("")
|
||||
expect(props.fieldLength).toBeDefined()
|
||||
expect(props.fieldLength).toBe(500)
|
||||
})
|
||||
|
||||
it("should return error when invalid type", () => {
|
||||
const comp = getcomponent()
|
||||
comp.props.fieldName = "invalid type name"
|
||||
comp.props.fieldLength = { type: "invalid type name " }
|
||||
|
||||
const { errors } = createProps(comp)
|
||||
|
||||
expect(errors.length).toBe(2)
|
||||
expect(some(e => e.propName === "fieldName")(errors)).toBeTruthy()
|
||||
expect(some(e => e.propName === "fieldLength")(errors)).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should merge in derived props", () => {
|
||||
const comp = getcomponent()
|
||||
comp.props.fieldName = "string"
|
||||
comp.props.fieldLength = { type: "number", default: 500 }
|
||||
|
||||
const derivedFrom = {
|
||||
fieldName: "surname",
|
||||
}
|
||||
|
||||
const { props, errors } = createProps(comp, derivedFrom)
|
||||
|
||||
expect(errors.length).toBe(0)
|
||||
expect(props.fieldName).toBe("surname")
|
||||
expect(props.fieldLength).toBe(500)
|
||||
})
|
||||
|
||||
it("should create standard props", () => {
|
||||
const comp = getcomponent()
|
||||
comp.props.fieldName = { type: "string", default: 1 }
|
||||
const { props } = createProps(comp)
|
||||
expect(props._styles).toBeDefined()
|
||||
})
|
||||
})
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue