mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
130 changed files with 2790 additions and 4653 deletions
@ -1,4 +1,4 @@ |
|||
packages/builder/src/userInterface/CurrentItemPreview.svelte |
|||
packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte |
|||
public |
|||
dist |
|||
packages/server/builder |
|||
|
|||
@ -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 |
|||
} |
|||
@ -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,80 +1,105 @@ |
|||
import { getBuiltin } from "components/userInterface/assetParsing/createProps" |
|||
import { uuid } from "./uuid" |
|||
import getNewComponentName from "./getNewComponentName" |
|||
|
|||
/** |
|||
* 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 ID |
|||
*/ |
|||
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 findComponent = (rootComponent, id) => { |
|||
return searchComponentTree(rootComponent, comp => comp._id === id) |
|||
} |
|||
|
|||
export const walkProps = (props, action, cancelToken = null) => { |
|||
cancelToken = cancelToken || { cancelled: false } |
|||
action(props, () => { |
|||
cancelToken.cancelled = true |
|||
}) |
|||
/** |
|||
* Recursively searches for a specific component type |
|||
*/ |
|||
export const findComponentType = (rootComponent, type) => { |
|||
return searchComponentTree(rootComponent, comp => comp._component === type) |
|||
} |
|||
|
|||
if (props._children) { |
|||
for (let child of props._children) { |
|||
if (cancelToken.cancelled) return |
|||
walkProps(child, action, cancelToken) |
|||
/** |
|||
* Recursively searches for the parent component of a specific component ID |
|||
*/ |
|||
export const findComponentParent = (rootComponent, id, parentComponent) => { |
|||
if (!rootComponent || !id) { |
|||
return null |
|||
} |
|||
if (rootComponent._id === id) { |
|||
return parentComponent |
|||
} |
|||
if (!rootComponent._children) { |
|||
return null |
|||
} |
|||
for (const child of rootComponent._children) { |
|||
const childResult = findComponentParent(child, id, rootComponent) |
|||
if (childResult) { |
|||
return childResult |
|||
} |
|||
} |
|||
return null |
|||
} |
|||
|
|||
export const generateNewIdsForComponent = ( |
|||
component, |
|||
state, |
|||
changeName = true |
|||
) => |
|||
walkProps(component, prop => { |
|||
prop._id = uuid() |
|||
if (changeName) prop._instanceName = getNewComponentName(prop, state) |
|||
}) |
|||
/** |
|||
* 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 [] |
|||
} |
|||
|
|||
export const getComponentDefinition = (state, name) => |
|||
name.startsWith("##") ? getBuiltin(name) : state.components[name] |
|||
/** |
|||
* 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() |
|||
} |
|||
|
|||
export const findChildComponentType = (node, typeToFind) => { |
|||
// Stop recursion if invalid props
|
|||
if (!node || !typeToFind) { |
|||
/** |
|||
* 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 |
|||
} |
|||
|
|||
// Stop recursion if this element matches
|
|||
if (node._component === typeToFind) { |
|||
return node |
|||
if (matchComponent(rootComponent)) { |
|||
return rootComponent |
|||
} |
|||
|
|||
// 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 = searchComponentTree(child, matchComponent) |
|||
if (childResult) { |
|||
return childResult |
|||
} |
|||
} |
|||
|
|||
// If we reach here then no children were valid
|
|||
return null |
|||
} |
|||
|
|||
@ -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() |
|||
@ -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" |
|||
] |
|||
} |
|||
] |
|||
@ -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 /> |
|||
@ -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, |
|||
@ -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> |
|||
@ -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 |
|||
@ -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 = "" |
|||
@ -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} |
|||
@ -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,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
@ -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,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() |
|||
}) |
|||
}) |
|||
@ -1,253 +0,0 @@ |
|||
import fetchBindableProperties from "../src/builderStore/fetchBindableProperties" |
|||
|
|||
describe("fetch bindable properties", () => { |
|||
it("should return bindable properties from screen components", () => { |
|||
const result = fetchBindableProperties({ |
|||
componentInstanceId: "heading-id", |
|||
...testData(), |
|||
}) |
|||
const componentBinding = result.find( |
|||
r => r.instance._id === "search-input-id" && r.type === "instance" |
|||
) |
|||
expect(componentBinding).toBeDefined() |
|||
expect(componentBinding.type).toBe("instance") |
|||
expect(componentBinding.runtimeBinding).toBe("search-input-id") |
|||
}) |
|||
|
|||
it("should not return bindable components when not in their context", () => { |
|||
const result = fetchBindableProperties({ |
|||
componentInstanceId: "heading-id", |
|||
...testData(), |
|||
}) |
|||
const componentBinding = result.find( |
|||
r => r.instance._id === "list-item-input-id" |
|||
) |
|||
expect(componentBinding).not.toBeDefined() |
|||
}) |
|||
|
|||
it("should return table schema, when inside a context", () => { |
|||
const result = fetchBindableProperties({ |
|||
componentInstanceId: "list-item-input-id", |
|||
...testData(), |
|||
}) |
|||
const contextBindings = result.filter( |
|||
r => r.instance._id === "list-id" && r.type === "context" |
|||
) |
|||
// 2 fields + _id + _rev
|
|||
expect(contextBindings.length).toBe(4) |
|||
|
|||
const namebinding = contextBindings.find( |
|||
b => b.runtimeBinding === "list-id.name" |
|||
) |
|||
expect(namebinding).toBeDefined() |
|||
expect(namebinding.readableBinding).toBe("list-name.Test Table.name") |
|||
|
|||
const descriptionbinding = contextBindings.find( |
|||
b => b.runtimeBinding === "list-id.description" |
|||
) |
|||
expect(descriptionbinding).toBeDefined() |
|||
expect(descriptionbinding.readableBinding).toBe( |
|||
"list-name.Test Table.description" |
|||
) |
|||
|
|||
const idbinding = contextBindings.find( |
|||
b => b.runtimeBinding === "list-id._id" |
|||
) |
|||
expect(idbinding).toBeDefined() |
|||
expect(idbinding.readableBinding).toBe("list-name.Test Table._id") |
|||
}) |
|||
|
|||
it("should return table schema, for grantparent context", () => { |
|||
const result = fetchBindableProperties({ |
|||
componentInstanceId: "child-list-item-input-id", |
|||
...testData(), |
|||
}) |
|||
const contextBindings = result.filter(r => r.type === "context") |
|||
// 2 fields + _id + _rev ... x 2 tables
|
|||
expect(contextBindings.length).toBe(8) |
|||
|
|||
const namebinding_parent = contextBindings.find( |
|||
b => b.runtimeBinding === "list-id.name" |
|||
) |
|||
expect(namebinding_parent).toBeDefined() |
|||
expect(namebinding_parent.readableBinding).toBe("list-name.Test Table.name") |
|||
|
|||
const descriptionbinding_parent = contextBindings.find( |
|||
b => b.runtimeBinding === "list-id.description" |
|||
) |
|||
expect(descriptionbinding_parent).toBeDefined() |
|||
expect(descriptionbinding_parent.readableBinding).toBe( |
|||
"list-name.Test Table.description" |
|||
) |
|||
|
|||
const namebinding_own = contextBindings.find( |
|||
b => b.runtimeBinding === "child-list-id.name" |
|||
) |
|||
expect(namebinding_own).toBeDefined() |
|||
expect(namebinding_own.readableBinding).toBe( |
|||
"child-list-name.Test Table.name" |
|||
) |
|||
|
|||
const descriptionbinding_own = contextBindings.find( |
|||
b => b.runtimeBinding === "child-list-id.description" |
|||
) |
|||
expect(descriptionbinding_own).toBeDefined() |
|||
expect(descriptionbinding_own.readableBinding).toBe( |
|||
"child-list-name.Test Table.description" |
|||
) |
|||
}) |
|||
|
|||
it("should return bindable component props, from components in same context", () => { |
|||
const result = fetchBindableProperties({ |
|||
componentInstanceId: "list-item-heading-id", |
|||
...testData(), |
|||
}) |
|||
const componentBinding = result.find( |
|||
r => r.instance._id === "list-item-input-id" && r.type === "instance" |
|||
) |
|||
expect(componentBinding).toBeDefined() |
|||
expect(componentBinding.runtimeBinding).toBe("list-item-input-id") |
|||
}) |
|||
|
|||
it("should not return components from child context", () => { |
|||
const result = fetchBindableProperties({ |
|||
componentInstanceId: "list-item-heading-id", |
|||
...testData(), |
|||
}) |
|||
const componentBinding = result.find( |
|||
r => |
|||
r.instance._id === "child-list-item-input-id" && r.type === "instance" |
|||
) |
|||
expect(componentBinding).not.toBeDefined() |
|||
}) |
|||
|
|||
it("should return bindable component props, from components in same context (when nested context)", () => { |
|||
const result = fetchBindableProperties({ |
|||
componentInstanceId: "child-list-item-heading-id", |
|||
...testData(), |
|||
}) |
|||
const componentBinding = result.find( |
|||
r => |
|||
r.instance._id === "child-list-item-input-id" && r.type === "instance" |
|||
) |
|||
expect(componentBinding).toBeDefined() |
|||
}) |
|||
}) |
|||
|
|||
const testData = () => { |
|||
const screen = { |
|||
instanceName: "test screen", |
|||
name: "screen-id", |
|||
route: "/", |
|||
props: { |
|||
_id: "screent-root-id", |
|||
_component: "@budibase/standard-components/container", |
|||
_children: [ |
|||
{ |
|||
_id: "heading-id", |
|||
_instanceName: "list item heading", |
|||
_component: "@budibase/standard-components/heading", |
|||
text: "Screen Title", |
|||
}, |
|||
{ |
|||
_id: "search-input-id", |
|||
_instanceName: "Search Input", |
|||
_component: "@budibase/standard-components/input", |
|||
value: "search phrase", |
|||
}, |
|||
{ |
|||
_id: "list-id", |
|||
_component: "@budibase/standard-components/list", |
|||
_instanceName: "list-name", |
|||
table: { |
|||
type: "table", |
|||
tableId: "test-table-id", |
|||
label: "Test Table", |
|||
name: "all_test-table-id", |
|||
}, |
|||
_children: [ |
|||
{ |
|||
_id: "list-item-heading-id", |
|||
_instanceName: "list item heading", |
|||
_component: "@budibase/standard-components/heading", |
|||
text: "hello", |
|||
}, |
|||
{ |
|||
_id: "list-item-input-id", |
|||
_instanceName: "List Item Input", |
|||
_component: "@budibase/standard-components/input", |
|||
value: "list item", |
|||
}, |
|||
{ |
|||
_id: "child-list-id", |
|||
_component: "@budibase/standard-components/list", |
|||
_instanceName: "child-list-name", |
|||
table: { |
|||
type: "table", |
|||
tableId: "test-table-id", |
|||
label: "Test Table", |
|||
name: "all_test-table-id", |
|||
}, |
|||
_children: [ |
|||
{ |
|||
_id: "child-list-item-heading-id", |
|||
_instanceName: "child list item heading", |
|||
_component: "@budibase/standard-components/heading", |
|||
text: "hello", |
|||
}, |
|||
{ |
|||
_id: "child-list-item-input-id", |
|||
_instanceName: "Child List Item Input", |
|||
_component: "@budibase/standard-components/input", |
|||
value: "child list item", |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}, |
|||
} |
|||
|
|||
const tables = [ |
|||
{ |
|||
_id: "test-table-id", |
|||
name: "Test Table", |
|||
schema: { |
|||
name: { |
|||
type: "string", |
|||
}, |
|||
description: { |
|||
type: "string", |
|||
}, |
|||
}, |
|||
}, |
|||
] |
|||
|
|||
const queries = [] |
|||
|
|||
const components = { |
|||
"@budibase/standard-components/container": { |
|||
props: {}, |
|||
}, |
|||
"@budibase/standard-components/list": { |
|||
context: "table", |
|||
props: { |
|||
table: "string", |
|||
}, |
|||
}, |
|||
"@budibase/standard-components/input": { |
|||
bindable: "value", |
|||
props: { |
|||
value: "string", |
|||
}, |
|||
}, |
|||
"@budibase/standard-components/heading": { |
|||
props: { |
|||
text: "string", |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
return { screen, tables, components, queries } |
|||
} |
|||
@ -1,37 +0,0 @@ |
|||
import { |
|||
searchAllComponents, |
|||
getExactComponent, |
|||
getAncestorProps, |
|||
} from "../src/components/userInterface/assetParsing/searchComponents" |
|||
import { componentsAndScreens } from "./testData" |
|||
|
|||
|
|||
describe("getExactComponent", () => { |
|||
it("should get component by name", () => { |
|||
const { components } = componentsAndScreens() |
|||
const result = getExactComponent( |
|||
components, |
|||
"TextBox" |
|||
) |
|||
|
|||
expect(result).toBeDefined() |
|||
expect(result._instanceName).toBe("TextBox") |
|||
}) |
|||
|
|||
test("Should not find as isScreen is not specified", () => { |
|||
const { screens } = componentsAndScreens() |
|||
const result = getExactComponent(screens, "SmallTextbox") |
|||
|
|||
expect(result).not.toBeDefined() |
|||
}) |
|||
|
|||
test("Should find as isScreen is specified", () => { |
|||
const { screens } = componentsAndScreens() |
|||
const result = getExactComponent(screens, "SmallTextbox", true) |
|||
|
|||
expect(result).toBeDefined() |
|||
expect(result.props._instanceName).toBe("SmallTextbox") |
|||
|
|||
}) |
|||
}) |
|||
|
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue