mirror of https://github.com/Budibase/budibase.git
132 changed files with 2800 additions and 4658 deletions
@ -1,4 +1,4 @@ |
|||||
packages/builder/src/userInterface/CurrentItemPreview.svelte |
packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte |
||||
public |
public |
||||
dist |
dist |
||||
packages/server/builder |
packages/server/builder |
||||
|
|||||
@ -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. |
* Recursively searches for a specific component ID |
||||
* @param {Object} rootProps - props to search for the parent in |
|
||||
* @param {String|Object} child - id of the child or the child itself to find the parent of |
|
||||
*/ |
*/ |
||||
export const findParent = (rootProps, child) => { |
export const findComponent = (rootComponent, id) => { |
||||
let parent |
return searchComponentTree(rootComponent, comp => comp._id === id) |
||||
walkProps(rootProps, (props, breakWalk) => { |
|
||||
if ( |
|
||||
props._children && |
|
||||
(props._children.includes(child) || |
|
||||
props._children.some(c => c._id === child)) |
|
||||
) { |
|
||||
parent = props |
|
||||
breakWalk() |
|
||||
} |
|
||||
}) |
|
||||
return parent |
|
||||
} |
} |
||||
|
|
||||
export const walkProps = (props, action, cancelToken = null) => { |
/** |
||||
cancelToken = cancelToken || { cancelled: false } |
* Recursively searches for a specific component type |
||||
action(props, () => { |
*/ |
||||
cancelToken.cancelled = true |
export const findComponentType = (rootComponent, type) => { |
||||
}) |
return searchComponentTree(rootComponent, comp => comp._component === type) |
||||
|
} |
||||
|
|
||||
if (props._children) { |
/** |
||||
for (let child of props._children) { |
* Recursively searches for the parent component of a specific component ID |
||||
if (cancelToken.cancelled) return |
*/ |
||||
walkProps(child, action, cancelToken) |
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, |
* Recursively searches for a specific component ID and records the component |
||||
state, |
* path to this component |
||||
changeName = true |
*/ |
||||
) => |
export const findComponentPath = (rootComponent, id, path = []) => { |
||||
walkProps(component, prop => { |
if (!rootComponent || !id) { |
||||
prop._id = uuid() |
return [] |
||||
if (changeName) prop._instanceName = getNewComponentName(prop, state) |
} |
||||
}) |
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
|
* Recurses through a component tree evaluating a matching function against |
||||
if (!node || !typeToFind) { |
* components until a match is found |
||||
|
*/ |
||||
|
const searchComponentTree = (rootComponent, matchComponent) => { |
||||
|
if (!rootComponent || !matchComponent) { |
||||
return null |
return null |
||||
} |
} |
||||
|
if (matchComponent(rootComponent)) { |
||||
// Stop recursion if this element matches
|
return rootComponent |
||||
if (node._component === typeToFind) { |
|
||||
return node |
|
||||
} |
} |
||||
|
if (!rootComponent._children) { |
||||
// Otherwise check if any children match
|
|
||||
// Stop recursion if no valid children to process
|
|
||||
const children = node._children || (node.props && node.props._children) |
|
||||
if (!children || !children.length) { |
|
||||
return null |
return null |
||||
} |
} |
||||
|
for (const child of rootComponent._children) { |
||||
// Recurse and check each child component
|
const childResult = searchComponentTree(child, matchComponent) |
||||
for (let child of children) { |
|
||||
const childResult = findChildComponentType(child, typeToFind) |
|
||||
if (childResult) { |
if (childResult) { |
||||
return childResult |
return childResult |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
// If we reach here then no children were valid
|
|
||||
return null |
return null |
||||
} |
} |
||||
|
|||||
@ -1,6 +1,6 @@ |
|||||
<script> |
<script> |
||||
import { createEventDispatcher } from "svelte" |
import { createEventDispatcher } from "svelte" |
||||
import GenericBindingPopover from "./GenericBindingPopover.svelte" |
import GenericBindingPopover from "../automation/SetupPanel/GenericBindingPopover.svelte" |
||||
import { Input, Icon } from "@budibase/bbui" |
import { Input, Icon } from "@budibase/bbui" |
||||
|
|
||||
const dispatch = createEventDispatcher() |
const dispatch = createEventDispatcher() |
||||
@ -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> |
<script> |
||||
import { Select, Label, Spacer } from "@budibase/bbui" |
import { Select, Label, Spacer } from "@budibase/bbui" |
||||
import { store, backendUiStore, currentAsset } from "builderStore" |
import { store, backendUiStore, currentAsset } from "builderStore" |
||||
import fetchBindableProperties from "builderStore/fetchBindableProperties" |
import { getBindableProperties } from "builderStore/dataBinding" |
||||
import ParameterBuilder from "../../../integration/QueryParameterBuilder.svelte" |
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte" |
||||
|
|
||||
export let parameters |
export let parameters |
||||
|
|
||||
$: datasource = $backendUiStore.datasources.find( |
$: datasource = $backendUiStore.datasources.find( |
||||
ds => ds._id === parameters.datasourceId |
ds => ds._id === parameters.datasourceId |
||||
) |
) |
||||
// TODO: binding needs to be centralised |
$: bindableProperties = getBindableProperties( |
||||
$: bindableProperties = fetchBindableProperties({ |
$currentAsset.props, |
||||
componentInstanceId: $store.selectedComponentId, |
$store.selectedComponentId |
||||
components: $store.components, |
).map(property => ({ |
||||
screen: $currentAsset, |
|
||||
tables: $backendUiStore.tables, |
|
||||
queries: $backendUiStore.queries, |
|
||||
}).map(property => ({ |
|
||||
...property, |
...property, |
||||
category: property.type === "instance" ? "Component" : "Table", |
category: property.type === "instance" ? "Component" : "Table", |
||||
label: property.readableBinding, |
label: property.readableBinding, |
||||
@ -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> |
<script> |
||||
import { store, currentAsset } from "builderStore" |
import { store } from "builderStore" |
||||
import { Select } from "@budibase/bbui" |
import { Select } from "@budibase/bbui" |
||||
|
|
||||
export let value |
export let value |
||||
@ -1,7 +1,7 @@ |
|||||
<script> |
<script> |
||||
import { onMount } from "svelte" |
import { onMount } from "svelte" |
||||
import Portal from "svelte-portal" |
import Portal from "svelte-portal" |
||||
import { buildStyle } from "../../helpers.js" |
import { buildStyle } from "../../../../helpers.js" |
||||
|
|
||||
export let options = [] |
export let options = [] |
||||
export let value = "" |
export let value = "" |
||||
@ -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() |
|
||||
}) |
|
||||
}) |
|
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue