WIP component management and definition refactor
This commit is contained in:
parent
e8f18e2853
commit
9b035df33e
|
@ -5,7 +5,7 @@ import { getThemeStore } from "./store/theme"
|
|||
import { derived, writable } from "svelte/store"
|
||||
import analytics from "analytics"
|
||||
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
|
||||
import { makePropsSafe } from "components/userInterface/assetParsing/createProps"
|
||||
import { findComponent } from "./storeUtils"
|
||||
|
||||
export const store = getFrontendStore()
|
||||
export const backendUiStore = getBackendUiStore()
|
||||
|
@ -25,31 +25,10 @@ export const currentAsset = derived(store, $store => {
|
|||
export const selectedComponent = derived(
|
||||
[store, currentAsset],
|
||||
([$store, $currentAsset]) => {
|
||||
if (!$currentAsset || !$store.selectedComponentId) return null
|
||||
|
||||
function traverse(node, callback) {
|
||||
if (node._id === $store.selectedComponentId) return callback(node)
|
||||
|
||||
if (node._children) {
|
||||
node._children.forEach(child => traverse(child, callback))
|
||||
}
|
||||
|
||||
if (node.props) {
|
||||
traverse(node.props, callback)
|
||||
}
|
||||
if (!$currentAsset || !$store.selectedComponentId) {
|
||||
return null
|
||||
}
|
||||
|
||||
let component
|
||||
traverse($currentAsset, found => {
|
||||
const componentIdentifier = found._component ?? found.props._component
|
||||
const componentDef = componentIdentifier.startsWith("##")
|
||||
? found
|
||||
: $store.components[componentIdentifier]
|
||||
|
||||
component = makePropsSafe(componentDef, found)
|
||||
})
|
||||
|
||||
return component
|
||||
return findComponent($currentAsset.props, $store.selectedComponentId)
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -21,12 +21,9 @@ export const fetchComponentLibDefinitions = async appId => {
|
|||
*/
|
||||
export const fetchComponentLibModules = async application => {
|
||||
const allLibraries = {}
|
||||
|
||||
for (let libraryName of application.componentLibraries) {
|
||||
const LIBRARY_URL = `/${application._id}/componentlibrary?library=${libraryName}`
|
||||
const libraryModule = await import(LIBRARY_URL)
|
||||
allLibraries[libraryName] = libraryModule
|
||||
allLibraries[libraryName] = await import(LIBRARY_URL)
|
||||
}
|
||||
|
||||
return allLibraries
|
||||
}
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
import { get, writable } from "svelte/store"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import {
|
||||
createProps,
|
||||
getBuiltin,
|
||||
} from "components/userInterface/assetParsing/createProps"
|
||||
import {
|
||||
allScreens,
|
||||
backendUiStore,
|
||||
|
@ -15,14 +11,13 @@ import {
|
|||
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
|
||||
import api from "../api"
|
||||
import { FrontendTypes } from "../../constants"
|
||||
import getNewComponentName from "../getNewComponentName"
|
||||
import analytics from "analytics"
|
||||
import {
|
||||
findChildComponentType,
|
||||
generateNewIdsForComponent,
|
||||
getComponentDefinition,
|
||||
findParent,
|
||||
findComponentType,
|
||||
findComponentParent,
|
||||
findComponentPath,
|
||||
} from "../storeUtils"
|
||||
import { uuid } from "../uuid"
|
||||
|
||||
const INITIAL_FRONTEND_STATE = {
|
||||
apps: [],
|
||||
|
@ -48,14 +43,7 @@ export const getFrontendStore = () => {
|
|||
store.actions = {
|
||||
initialise: async pkg => {
|
||||
const { layouts, screens, application } = pkg
|
||||
|
||||
store.update(state => {
|
||||
state.appId = application._id
|
||||
return state
|
||||
})
|
||||
|
||||
const components = await fetchComponentLibDefinitions(pkg.application._id)
|
||||
|
||||
store.update(state => ({
|
||||
...state,
|
||||
libraries: pkg.application.componentLibraries,
|
||||
|
@ -66,17 +54,14 @@ export const getFrontendStore = () => {
|
|||
layouts,
|
||||
screens,
|
||||
hasAppPackage: true,
|
||||
builtins: [getBuiltin("##builtin/screenslot")],
|
||||
appInstance: pkg.application.instance,
|
||||
}))
|
||||
|
||||
await backendUiStore.actions.database.select(pkg.application.instance)
|
||||
},
|
||||
routing: {
|
||||
fetch: async () => {
|
||||
const response = await api.get("/api/routing")
|
||||
const json = await response.json()
|
||||
|
||||
store.update(state => {
|
||||
state.routes = json.routes
|
||||
return state
|
||||
|
@ -245,122 +230,194 @@ export const getFrontendStore = () => {
|
|||
return state
|
||||
})
|
||||
},
|
||||
create: (componentToAdd, presetProps) => {
|
||||
const selectedAsset = get(currentAsset)
|
||||
getDefinition: componentName => {
|
||||
if (!componentName) {
|
||||
return null
|
||||
}
|
||||
const name = componentName.startsWith("@budibase")
|
||||
? componentName
|
||||
: `@budibase/standard-components/${componentName}`
|
||||
return get(store).components[name]
|
||||
},
|
||||
createInstance: (componentName, presetProps) => {
|
||||
const definition = store.actions.components.getDefinition(componentName)
|
||||
if (!definition) {
|
||||
return null
|
||||
}
|
||||
|
||||
store.update(state => {
|
||||
function findSlot(component_array) {
|
||||
if (!component_array) {
|
||||
return false
|
||||
// Generate default props
|
||||
let props = { ...presetProps }
|
||||
if (definition.settings) {
|
||||
definition.settings.forEach(setting => {
|
||||
if (setting.defaultValue !== undefined) {
|
||||
props[setting.key] = setting.defaultValue
|
||||
}
|
||||
for (let component of component_array) {
|
||||
if (component._component === "##builtin/screenslot") {
|
||||
return true
|
||||
}
|
||||
|
||||
if (component._children) findSlot(component)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
componentToAdd.startsWith("##") &&
|
||||
findSlot(selectedAsset?.props._children)
|
||||
) {
|
||||
return state
|
||||
}
|
||||
|
||||
const component = getComponentDefinition(state, componentToAdd)
|
||||
|
||||
const instanceId = get(backendUiStore).selectedDatabase._id
|
||||
const instanceName = getNewComponentName(component, state)
|
||||
|
||||
const newComponent = createProps(component, {
|
||||
...presetProps,
|
||||
_instanceId: instanceId,
|
||||
_instanceName: instanceName,
|
||||
})
|
||||
}
|
||||
|
||||
const selected = get(selectedComponent)
|
||||
// Add any extra properties the component needs
|
||||
let extras = {}
|
||||
if (definition.hasChildren) {
|
||||
extras._children = []
|
||||
}
|
||||
|
||||
const currentComponentDefinition =
|
||||
state.components[selected._component]
|
||||
return {
|
||||
_id: uuid(),
|
||||
_component: definition.component,
|
||||
_styles: { normal: {}, hover: {}, active: {} },
|
||||
_instanceName: `New ${definition.component.split("/")[2]}`,
|
||||
...cloneDeep(props),
|
||||
...extras,
|
||||
}
|
||||
},
|
||||
create: (componentName, presetProps) => {
|
||||
// Create new component
|
||||
const componentInstance = store.actions.components.createInstance(
|
||||
componentName,
|
||||
presetProps
|
||||
)
|
||||
if (!componentInstance) {
|
||||
return
|
||||
}
|
||||
|
||||
const allowsChildren = currentComponentDefinition.children
|
||||
|
||||
// Determine where to put the new component.
|
||||
let targetParent
|
||||
if (allowsChildren) {
|
||||
// Child of the selected component
|
||||
targetParent = selected
|
||||
// Find parent node to attach this component to
|
||||
let parentComponent
|
||||
const selected = get(selectedComponent)
|
||||
const asset = get(currentAsset)
|
||||
if (!asset) {
|
||||
return
|
||||
}
|
||||
if (selected) {
|
||||
// Use current screen or layout as parent if no component is selected
|
||||
const definition = store.actions.components.getDefinition(
|
||||
selected._component
|
||||
)
|
||||
if (definition?.hasChildren) {
|
||||
// Use selected component if it allows children
|
||||
parentComponent = selected
|
||||
} else {
|
||||
// Sibling of selected component
|
||||
targetParent = findParent(selectedAsset.props, selected)
|
||||
// Otherwise we need to use the parent of this component
|
||||
parentComponent = findComponentParent(asset.props, selected._id)
|
||||
}
|
||||
} else {
|
||||
// Use screen or layout if no component is selected
|
||||
parentComponent = asset.props
|
||||
}
|
||||
|
||||
// Don't continue if there's no parent
|
||||
if (!targetParent) return state
|
||||
|
||||
// Push the new component
|
||||
targetParent._children.push(newComponent.props)
|
||||
|
||||
store.actions.preview.saveSelected()
|
||||
// Attach component
|
||||
if (!parentComponent) {
|
||||
return
|
||||
}
|
||||
if (!parentComponent._children) {
|
||||
parentComponent._children = []
|
||||
}
|
||||
parentComponent._children.push(componentInstance)
|
||||
|
||||
// Save components and update UI
|
||||
store.actions.preview.saveSelected()
|
||||
store.update(state => {
|
||||
state.currentView = "component"
|
||||
state.selectedComponentId = newComponent.props._id
|
||||
|
||||
analytics.captureEvent("Added Component", {
|
||||
name: newComponent.props._component,
|
||||
})
|
||||
state.selectedComponentId = componentInstance._id
|
||||
return state
|
||||
})
|
||||
|
||||
// Log event
|
||||
analytics.captureEvent("Added Component", {
|
||||
name: componentInstance._component,
|
||||
})
|
||||
|
||||
return componentInstance
|
||||
},
|
||||
delete: component => {
|
||||
if (!component) {
|
||||
return
|
||||
}
|
||||
const asset = get(currentAsset)
|
||||
if (!asset) {
|
||||
return
|
||||
}
|
||||
const parent = findComponentParent(asset.props, component._id)
|
||||
if (parent) {
|
||||
parent._children = parent._children.filter(
|
||||
child => child._id !== component._id
|
||||
)
|
||||
store.actions.components.select(parent)
|
||||
}
|
||||
store.actions.preview.saveSelected()
|
||||
},
|
||||
copy: (component, cut = false) => {
|
||||
const selectedAsset = get(currentAsset)
|
||||
if (!selectedAsset) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Update store with copied component
|
||||
store.update(state => {
|
||||
state.componentToPaste = cloneDeep(component)
|
||||
state.componentToPaste.isCut = cut
|
||||
if (cut) {
|
||||
const parent = findParent(selectedAsset.props, component._id)
|
||||
return state
|
||||
})
|
||||
|
||||
// Remove the component from its parent if we're cutting
|
||||
if (cut) {
|
||||
const parent = findComponentParent(selectedAsset.props, component._id)
|
||||
if (parent) {
|
||||
parent._children = parent._children.filter(
|
||||
child => child._id !== component._id
|
||||
)
|
||||
store.actions.components.select(parent)
|
||||
}
|
||||
|
||||
return state
|
||||
})
|
||||
}
|
||||
},
|
||||
paste: async (targetComponent, mode) => {
|
||||
const selectedAsset = get(currentAsset)
|
||||
let promises = []
|
||||
store.update(state => {
|
||||
if (!state.componentToPaste) return state
|
||||
|
||||
const componentToPaste = cloneDeep(state.componentToPaste)
|
||||
// retain the same ids as things may be referencing this component
|
||||
if (componentToPaste.isCut) {
|
||||
// in case we paste a second time
|
||||
state.componentToPaste.isCut = false
|
||||
} else {
|
||||
generateNewIdsForComponent(componentToPaste, state)
|
||||
}
|
||||
delete componentToPaste.isCut
|
||||
|
||||
if (mode === "inside") {
|
||||
targetComponent._children.push(componentToPaste)
|
||||
// Stop if we have nothing to paste
|
||||
if (!state.componentToPaste) {
|
||||
return state
|
||||
}
|
||||
|
||||
const parent = findParent(selectedAsset.props, targetComponent)
|
||||
// Clone the component to paste
|
||||
// Retain the same ID if cutting as things may be referencing this component
|
||||
const cut = state.componentToPaste.isCut
|
||||
delete state.componentToPaste.isCut
|
||||
let componentToPaste = cloneDeep(state.componentToPaste)
|
||||
if (cut) {
|
||||
state.componentToPaste = null
|
||||
} else {
|
||||
componentToPaste._id = uuid()
|
||||
}
|
||||
|
||||
const targetIndex = parent._children.indexOf(targetComponent)
|
||||
const index = mode === "above" ? targetIndex : targetIndex + 1
|
||||
parent._children.splice(index, 0, cloneDeep(componentToPaste))
|
||||
if (mode === "inside") {
|
||||
// Paste inside target component if chosen
|
||||
if (!targetComponent._children) {
|
||||
targetComponent._children = []
|
||||
}
|
||||
targetComponent._children.push(componentToPaste)
|
||||
} else {
|
||||
// Otherwise find the parent so we can paste in the correct order
|
||||
// in the parents child components
|
||||
const selectedAsset = get(currentAsset)
|
||||
if (!selectedAsset) {
|
||||
return state
|
||||
}
|
||||
const parent = findComponentParent(
|
||||
selectedAsset.props,
|
||||
targetComponent._id
|
||||
)
|
||||
if (!parent) {
|
||||
return state
|
||||
}
|
||||
|
||||
// Insert the component in the correct position
|
||||
const targetIndex = parent._children.indexOf(targetComponent)
|
||||
const index = mode === "above" ? targetIndex : targetIndex + 1
|
||||
parent._children.splice(index, 0, cloneDeep(componentToPaste))
|
||||
}
|
||||
|
||||
// Save and select the new component
|
||||
promises.push(store.actions.preview.saveSelected())
|
||||
store.actions.components.select(componentToPaste)
|
||||
|
||||
return state
|
||||
})
|
||||
await Promise.all(promises)
|
||||
|
@ -385,90 +442,68 @@ export const getFrontendStore = () => {
|
|||
await store.actions.preview.saveSelected()
|
||||
},
|
||||
updateProp: (name, value) => {
|
||||
let component = get(selectedComponent)
|
||||
if (!name || !component) {
|
||||
return
|
||||
}
|
||||
component[name] = value
|
||||
store.update(state => {
|
||||
let current_component = get(selectedComponent)
|
||||
current_component[name] = value
|
||||
|
||||
state.selectedComponentId = current_component._id
|
||||
store.actions.preview.saveSelected()
|
||||
state.selectedComponentId = component._id
|
||||
return state
|
||||
})
|
||||
store.actions.preview.saveSelected()
|
||||
},
|
||||
findRoute: component => {
|
||||
// Gets all the components to needed to construct a path.
|
||||
const selectedAsset = get(currentAsset)
|
||||
let pathComponents = []
|
||||
let parent = component
|
||||
let root = false
|
||||
while (!root) {
|
||||
parent = findParent(selectedAsset.props, parent)
|
||||
if (!parent) {
|
||||
root = true
|
||||
} else {
|
||||
pathComponents.push(parent)
|
||||
}
|
||||
if (!component || !selectedAsset) {
|
||||
return "/"
|
||||
}
|
||||
|
||||
// Remove root entry since it's the screen or layout.
|
||||
// Reverse array since we need the correct order of the IDs
|
||||
const reversedComponents = pathComponents.reverse().slice(1)
|
||||
// Get the path to this component
|
||||
const path = findComponentPath(selectedAsset.props, component._id) || []
|
||||
|
||||
// Add component
|
||||
const allComponents = [...reversedComponents, component]
|
||||
|
||||
// Map IDs
|
||||
const IdList = allComponents.map(c => c._id)
|
||||
|
||||
// Construct ID Path:
|
||||
return IdList.join("/")
|
||||
// Remove root entry since it's the screen or layout
|
||||
return path.slice(1).join("/")
|
||||
},
|
||||
links: {
|
||||
save: async (url, title) => {
|
||||
let promises = []
|
||||
const layout = get(mainLayout)
|
||||
store.update(state => {
|
||||
// Try to extract a nav component from the master layout
|
||||
const nav = findChildComponentType(
|
||||
layout,
|
||||
"@budibase/standard-components/navigation"
|
||||
)
|
||||
if (nav) {
|
||||
let newLink
|
||||
if (!layout) {
|
||||
return
|
||||
}
|
||||
|
||||
// Clone an existing link if one exists
|
||||
if (nav._children && nav._children.length) {
|
||||
// Clone existing link style
|
||||
newLink = cloneDeep(nav._children[0])
|
||||
// Find a nav bar in the main layout
|
||||
const nav = findComponentType(
|
||||
layout,
|
||||
"@budibase/standard-components/navigation"
|
||||
)
|
||||
if (!nav) {
|
||||
return
|
||||
}
|
||||
|
||||
// Manipulate IDs to ensure uniqueness
|
||||
generateNewIdsForComponent(newLink, state, false)
|
||||
let newLink
|
||||
if (nav._children && nav._children.length) {
|
||||
// Clone an existing link if one exists
|
||||
newLink = cloneDeep(nav._children[0])
|
||||
|
||||
// Set our new props
|
||||
newLink._instanceName = `${title} Link`
|
||||
newLink.url = url
|
||||
newLink.text = title
|
||||
} else {
|
||||
// Otherwise create vanilla new link
|
||||
const component = getComponentDefinition(
|
||||
state,
|
||||
"@budibase/standard-components/link"
|
||||
)
|
||||
const instanceId = get(backendUiStore).selectedDatabase._id
|
||||
newLink = createProps(component, {
|
||||
url,
|
||||
text: title,
|
||||
_instanceName: `${title} Link`,
|
||||
_instanceId: instanceId,
|
||||
}).props
|
||||
}
|
||||
|
||||
// Save layout
|
||||
nav._children = [...nav._children, newLink]
|
||||
promises.push(store.actions.layouts.save(layout))
|
||||
// Set our new props
|
||||
newLink._id = uuid()
|
||||
newLink._instanceName = `${title} Link`
|
||||
newLink.url = url
|
||||
newLink.text = title
|
||||
} else {
|
||||
// Otherwise create vanilla new link
|
||||
newLink = {
|
||||
...store.actions.components.createInstance("link"),
|
||||
url,
|
||||
text: title,
|
||||
_instanceName: `${title} Link`,
|
||||
}
|
||||
return state
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
// Save layout
|
||||
nav._children = [...nav._children, newLink]
|
||||
await store.actions.layouts.save(layout)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -4,7 +4,6 @@ import rowListScreen from "./rowListScreen"
|
|||
import emptyNewRowScreen from "./emptyNewRowScreen"
|
||||
import createFromScratchScreen from "./createFromScratchScreen"
|
||||
import emptyRowDetailScreen from "./emptyRowDetailScreen"
|
||||
import { generateNewIdsForComponent } from "../../storeUtils"
|
||||
import { uuid } from "builderStore/uuid"
|
||||
|
||||
const allTemplates = tables => [
|
||||
|
@ -16,13 +15,21 @@ const allTemplates = tables => [
|
|||
emptyRowDetailScreen,
|
||||
]
|
||||
|
||||
// allows us to apply common behaviour to all create() functions
|
||||
// Recurses through a component tree and generates new unique ID's
|
||||
const makeUniqueIds = component => {
|
||||
if (!component) {
|
||||
return
|
||||
}
|
||||
component._id = uuid()
|
||||
if (component._children) {
|
||||
component._children.forEach(makeUniqueIds)
|
||||
}
|
||||
}
|
||||
|
||||
// Allows us to apply common behaviour to all create() functions
|
||||
const createTemplateOverride = (frontendState, create) => () => {
|
||||
const screen = create()
|
||||
for (let component of screen.props._children) {
|
||||
generateNewIdsForComponent(component, frontendState, false)
|
||||
}
|
||||
screen.props._id = uuid()
|
||||
makeUniqueIds(screen.props)
|
||||
screen.name = screen.props._id
|
||||
screen.routing.route = screen.routing.route.toLowerCase()
|
||||
return screen
|
||||
|
|
|
@ -1,80 +1,82 @@
|
|||
import { getBuiltin } from "components/userInterface/assetParsing/createProps"
|
||||
import { uuid } from "./uuid"
|
||||
import getNewComponentName from "./getNewComponentName"
|
||||
/**
|
||||
* Recursively searches for a specific component ID
|
||||
*/
|
||||
export const findComponent = (rootComponent, id) => {
|
||||
return searchComponentTree(rootComponent, comp => comp._id === id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the parent component of the passed in child.
|
||||
* @param {Object} rootProps - props to search for the parent in
|
||||
* @param {String|Object} child - id of the child or the child itself to find the parent of
|
||||
* Recursively searches for a specific component type
|
||||
*/
|
||||
export const findParent = (rootProps, child) => {
|
||||
let parent
|
||||
walkProps(rootProps, (props, breakWalk) => {
|
||||
if (
|
||||
props._children &&
|
||||
(props._children.includes(child) ||
|
||||
props._children.some(c => c._id === child))
|
||||
) {
|
||||
parent = props
|
||||
breakWalk()
|
||||
}
|
||||
})
|
||||
return parent
|
||||
export const findComponentType = (rootComponent, type) => {
|
||||
return searchComponentTree(rootComponent, comp => comp._component === type)
|
||||
}
|
||||
|
||||
export const walkProps = (props, action, cancelToken = null) => {
|
||||
cancelToken = cancelToken || { cancelled: false }
|
||||
action(props, () => {
|
||||
cancelToken.cancelled = true
|
||||
})
|
||||
|
||||
if (props._children) {
|
||||
for (let child of props._children) {
|
||||
if (cancelToken.cancelled) return
|
||||
walkProps(child, action, cancelToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const generateNewIdsForComponent = (
|
||||
component,
|
||||
state,
|
||||
changeName = true
|
||||
) =>
|
||||
walkProps(component, prop => {
|
||||
prop._id = uuid()
|
||||
if (changeName) prop._instanceName = getNewComponentName(prop, state)
|
||||
})
|
||||
|
||||
export const getComponentDefinition = (state, name) =>
|
||||
name.startsWith("##") ? getBuiltin(name) : state.components[name]
|
||||
|
||||
export const findChildComponentType = (node, typeToFind) => {
|
||||
// Stop recursion if invalid props
|
||||
if (!node || !typeToFind) {
|
||||
/**
|
||||
* Recursively searches for the parent component of a specific component ID
|
||||
*/
|
||||
export const findComponentParent = (rootComponent, id, parentComponent) => {
|
||||
if (!rootComponent || !id) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Stop recursion if this element matches
|
||||
if (node._component === typeToFind) {
|
||||
return node
|
||||
if (rootComponent._id === id) {
|
||||
return parentComponent
|
||||
}
|
||||
|
||||
// Otherwise check if any children match
|
||||
// Stop recursion if no valid children to process
|
||||
const children = node._children || (node.props && node.props._children)
|
||||
if (!children || !children.length) {
|
||||
if (!rootComponent._children) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Recurse and check each child component
|
||||
for (let child of children) {
|
||||
const childResult = findChildComponentType(child, typeToFind)
|
||||
for (const child of rootComponent._children) {
|
||||
const childResult = findComponentParent(child, id, rootComponent)
|
||||
if (childResult) {
|
||||
return childResult
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively searches for a specific component ID and records the component
|
||||
* path to this component
|
||||
*/
|
||||
export const findComponentPath = (rootComponent, id, path = []) => {
|
||||
if (!rootComponent || !id) {
|
||||
return null
|
||||
}
|
||||
if (rootComponent._id === id) {
|
||||
return [...path, id]
|
||||
}
|
||||
if (!rootComponent._children) {
|
||||
return null
|
||||
}
|
||||
for (const child of rootComponent._children) {
|
||||
const newPath = [...path, rootComponent._id]
|
||||
const childResult = findComponentPath(child, id, newPath)
|
||||
if (childResult != null) {
|
||||
return childResult
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Recurses through a component tree evaluating a matching function against
|
||||
* components until a match is found
|
||||
*/
|
||||
const searchComponentTree = (rootComponent, matchComponent) => {
|
||||
if (!rootComponent || !matchComponent) {
|
||||
return null
|
||||
}
|
||||
if (matchComponent(rootComponent)) {
|
||||
return rootComponent
|
||||
}
|
||||
if (!rootComponent._children) {
|
||||
return null
|
||||
}
|
||||
for (const child of rootComponent._children) {
|
||||
const childResult = searchComponentTree(child, matchComponent)
|
||||
if (childResult) {
|
||||
return childResult
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach here then no children were valid
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
const screenPlaceholder = new Screen()
|
||||
.name("Screen Placeholder")
|
||||
.route("*")
|
||||
.component("@budibase/standard-components/screenslotplaceholder")
|
||||
.component("@budibase/standard-components/screenslot")
|
||||
.instanceName("Content Placeholder")
|
||||
.json()
|
||||
|
||||
|
|
|
@ -2,10 +2,9 @@
|
|||
import { goto } from "@sveltech/routify"
|
||||
import { get } from "svelte/store"
|
||||
import { store, currentAsset } from "builderStore"
|
||||
import { getComponentDefinition } from "builderStore/storeUtils"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { last } from "lodash/fp"
|
||||
import { findParent } from "builderStore/storeUtils"
|
||||
import { findComponentParent } from "builderStore/storeUtils"
|
||||
import { DropdownMenu } from "@budibase/bbui"
|
||||
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
|
||||
|
||||
|
@ -17,7 +16,7 @@
|
|||
|
||||
$: noChildrenAllowed =
|
||||
!component ||
|
||||
!getComponentDefinition($store, component._component)?.children
|
||||
!store.actions.components.getDefinition(component._component)?.hasChildren
|
||||
$: noPaste = !$store.componentToPaste
|
||||
|
||||
const lastPartOfName = c => (c ? last(c._component.split("/")) : "")
|
||||
|
@ -35,7 +34,7 @@
|
|||
const moveUpComponent = () => {
|
||||
store.update(state => {
|
||||
const asset = get(currentAsset)
|
||||
const parent = findParent(asset.props, component)
|
||||
const parent = findComponentParent(asset.props, component)
|
||||
|
||||
if (parent) {
|
||||
const currentIndex = parent._children.indexOf(component)
|
||||
|
@ -55,7 +54,7 @@
|
|||
const moveDownComponent = () => {
|
||||
store.update(state => {
|
||||
const asset = get(currentAsset)
|
||||
const parent = findParent(asset.props, component)
|
||||
const parent = findComponentParent(asset.props, component)
|
||||
|
||||
if (parent) {
|
||||
const currentIndex = parent._children.indexOf(component)
|
||||
|
@ -78,18 +77,7 @@
|
|||
}
|
||||
|
||||
const deleteComponent = () => {
|
||||
store.update(state => {
|
||||
const asset = get(currentAsset)
|
||||
const parent = findParent(asset.props, component)
|
||||
|
||||
if (parent) {
|
||||
parent._children = parent._children.filter(child => child !== component)
|
||||
selectComponent(parent)
|
||||
}
|
||||
|
||||
store.actions.preview.saveSelected()
|
||||
return state
|
||||
})
|
||||
store.actions.components.delete(component)
|
||||
}
|
||||
|
||||
const storeComponentForCopy = (cut = false) => {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import { goto } from "@sveltech/routify"
|
||||
import { store, currentAssetId } from "builderStore"
|
||||
import { getComponentDefinition } from "builderStore/storeUtils"
|
||||
import { DropEffect, DropPosition } from "./dragDropStore"
|
||||
import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
|
@ -12,16 +11,11 @@
|
|||
export let level = 0
|
||||
export let dragDropStore
|
||||
|
||||
const isScreenslot = name => name === "##builtin/screenslot"
|
||||
const isScreenslot = name => name?.endsWith("screenslot")
|
||||
|
||||
const selectComponent = component => {
|
||||
// Set current component
|
||||
store.actions.components.select(component)
|
||||
|
||||
// Get ID path
|
||||
const path = store.actions.components.findRoute(component)
|
||||
|
||||
// Go to correct URL
|
||||
$goto(`./${$currentAssetId}/${path}`)
|
||||
}
|
||||
|
||||
|
@ -31,9 +25,11 @@
|
|||
}
|
||||
|
||||
const dragover = (component, index) => e => {
|
||||
const definition = store.actions.components.getDefinition(
|
||||
component._component
|
||||
)
|
||||
const canHaveChildrenButIsEmpty =
|
||||
getComponentDefinition($store, component._component).children &&
|
||||
component._children.length === 0
|
||||
definition?.hasChildren && !component._children?.length
|
||||
|
||||
e.dataTransfer.dropEffect = DropEffect.COPY
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { writable, get } from "svelte/store"
|
||||
import { store as frontendStore } from "builderStore"
|
||||
import { findComponentPath } from "builderStore/storeUtils"
|
||||
|
||||
export const DropEffect = {
|
||||
MOVE: "move",
|
||||
|
@ -72,19 +73,32 @@ export default function() {
|
|||
})
|
||||
},
|
||||
drop: () => {
|
||||
store.update(state => {
|
||||
if (state.targetComponent !== state.dragged) {
|
||||
frontendStore.actions.components.copy(state.dragged, true)
|
||||
frontendStore.actions.components.paste(
|
||||
state.targetComponent,
|
||||
state.dropPosition
|
||||
)
|
||||
}
|
||||
const state = get(store)
|
||||
|
||||
store.actions.reset()
|
||||
// Stop if the target and source are the same
|
||||
if (state.targetComponent === state.dragged) {
|
||||
console.log("same component")
|
||||
return
|
||||
}
|
||||
// Stop if the target or source are null
|
||||
if (!state.targetComponent || !state.dragged) {
|
||||
console.log("null component")
|
||||
return
|
||||
}
|
||||
// Stop if the target is a child of source
|
||||
const path = findComponentPath(state.dragged, state.targetComponent._id)
|
||||
if (path?.includes(state.targetComponent._id)) {
|
||||
console.log("target is child of course")
|
||||
return
|
||||
}
|
||||
|
||||
return state
|
||||
})
|
||||
// Cut and paste the component
|
||||
frontendStore.actions.components.copy(state.dragged, true)
|
||||
frontendStore.actions.components.paste(
|
||||
state.targetComponent,
|
||||
state.dropPosition
|
||||
)
|
||||
store.actions.reset()
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,9 @@
|
|||
$store.currentView !== "component"
|
||||
? { ...$currentAsset, ...$selectedComponent }
|
||||
: $selectedComponent
|
||||
$: componentDefinition = $store.components[componentInstance._component]
|
||||
$: componentDefinition = store.actions.components.getDefinition(
|
||||
componentInstance._component
|
||||
)
|
||||
$: componentPropDefinition =
|
||||
flattenedPanel.find(
|
||||
// use for getting controls for each component property
|
||||
|
@ -37,7 +39,7 @@
|
|||
$: isComponentOrScreen =
|
||||
$store.currentView === "component" ||
|
||||
$store.currentFrontEndType === FrontendTypes.SCREEN
|
||||
$: isNotScreenslot = componentInstance._component !== "##builtin/screenslot"
|
||||
$: isNotScreenslot = !componentInstance._component.endsWith("screenslot")
|
||||
|
||||
$: displayName =
|
||||
isComponentOrScreen && componentInstance._instanceName && isNotScreenslot
|
||||
|
|
|
@ -6,47 +6,66 @@
|
|||
selectedComponent,
|
||||
currentAssetId,
|
||||
} from "builderStore"
|
||||
import components from "./temporaryPanelStructure.js"
|
||||
import structure from "./componentStructure.json"
|
||||
import { DropdownMenu } from "@budibase/bbui"
|
||||
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
|
||||
|
||||
const categories = components.categories
|
||||
$: enrichedStructure = enrichStructure(structure, $store.components)
|
||||
|
||||
let selectedIndex
|
||||
let anchors = []
|
||||
let popover
|
||||
$: anchor = selectedIndex === -1 ? null : anchors[selectedIndex]
|
||||
|
||||
const close = () => {
|
||||
popover.hide()
|
||||
const enrichStructure = (structure, definitions) => {
|
||||
let enrichedStructure = []
|
||||
structure.forEach(item => {
|
||||
if (typeof item === "string") {
|
||||
const def = definitions[`@budibase/standard-components/${item}`]
|
||||
if (def) {
|
||||
enrichedStructure.push({
|
||||
...def,
|
||||
isCategory: false,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
enrichedStructure.push({
|
||||
...item,
|
||||
isCategory: true,
|
||||
children: enrichStructure(item.children || [], definitions),
|
||||
})
|
||||
}
|
||||
})
|
||||
return enrichedStructure
|
||||
}
|
||||
|
||||
const onCategoryChosen = (category, idx) => {
|
||||
if (category.isCategory) {
|
||||
const onItemChosen = (item, idx) => {
|
||||
if (item.isCategory) {
|
||||
// Select and open this category
|
||||
selectedIndex = idx
|
||||
popover.show()
|
||||
} else {
|
||||
onComponentChosen(category)
|
||||
// Add this component
|
||||
const newComponent = store.actions.components.create(item.component)
|
||||
if (newComponent) {
|
||||
const path = store.actions.components.findRoute(newComponent)
|
||||
$goto(`./${$currentAssetId}/${path}`)
|
||||
}
|
||||
popover.hide()
|
||||
}
|
||||
}
|
||||
|
||||
const onComponentChosen = component => {
|
||||
store.actions.components.create(component._component, component.presetProps)
|
||||
const path = store.actions.components.findRoute($selectedComponent)
|
||||
$goto(`./${$currentAssetId}/${path}`)
|
||||
close()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
{#each categories as category, idx}
|
||||
{#each enrichedStructure as item, idx}
|
||||
<div
|
||||
bind:this={anchors[idx]}
|
||||
class="category"
|
||||
on:click={() => onCategoryChosen(category, idx)}
|
||||
on:click={() => onItemChosen(item, idx)}
|
||||
class:active={idx === selectedIndex}>
|
||||
{#if category.icon}<i class={category.icon} />{/if}
|
||||
<span>{category.name}</span>
|
||||
{#if category.isCategory}<i class="ri-arrow-down-s-line arrow" />{/if}
|
||||
{#if item.icon}<i class={item.icon} />{/if}
|
||||
<span>{item.name}</span>
|
||||
{#if item.isCategory}<i class="ri-arrow-down-s-line arrow" />{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -56,12 +75,12 @@
|
|||
{anchor}
|
||||
align="left">
|
||||
<DropdownContainer>
|
||||
{#each categories[selectedIndex].children as item}
|
||||
{#each enrichedStructure[selectedIndex].children as item}
|
||||
{#if !item.showOnAsset || item.showOnAsset.includes($currentAssetName)}
|
||||
<DropdownItem
|
||||
icon={item.icon}
|
||||
title={item.name}
|
||||
on:click={() => onComponentChosen(item)} />
|
||||
on:click={() => onItemChosen(item)} />
|
||||
{/if}
|
||||
{/each}
|
||||
</DropdownContainer>
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
<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 = {}
|
||||
|
@ -28,11 +25,7 @@
|
|||
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)
|
||||
}
|
||||
assetProps.includes(prop) || prop in (componentDefinition?.props ?? {})
|
||||
|
||||
const screenDefinition = [
|
||||
{ key: "description", label: "Description", control: Input },
|
||||
|
@ -44,8 +37,6 @@
|
|||
const layoutDefinition = []
|
||||
|
||||
const canRenderControl = (key, dependsOn) => {
|
||||
let test = !isEmpty(componentInstance[dependsOn])
|
||||
|
||||
return (
|
||||
propExistsOnComponentDef(key) &&
|
||||
(!dependsOn || !isEmpty(componentInstance[dependsOn]))
|
||||
|
@ -55,41 +46,8 @@
|
|||
$: 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)
|
||||
}
|
||||
onChange("_instanceName", name)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
[
|
||||
{
|
||||
"name": "Category",
|
||||
"icon": "ri-file-edit-line",
|
||||
"children": [
|
||||
"container"
|
||||
]
|
||||
},
|
||||
"grid",
|
||||
"screenslot"
|
||||
]
|
|
@ -20,93 +20,6 @@ import { all } from "./propertyCategories.js"
|
|||
|
||||
export default {
|
||||
categories: [
|
||||
{
|
||||
_component: "@budibase/standard-components/container",
|
||||
name: "Container",
|
||||
description: "This component contains things within itself",
|
||||
icon: "ri-layout-column-line",
|
||||
commonProps: {},
|
||||
children: [],
|
||||
properties: {
|
||||
design: { ...all },
|
||||
settings: [
|
||||
{
|
||||
key: "type",
|
||||
label: "Type",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
"article",
|
||||
"aside",
|
||||
"details",
|
||||
"div",
|
||||
"figure",
|
||||
"figcaption",
|
||||
"footer",
|
||||
"header",
|
||||
"main",
|
||||
"mark",
|
||||
"nav",
|
||||
"paragraph",
|
||||
"summary",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Grid",
|
||||
_component: "@budibase/standard-components/datagrid",
|
||||
description:
|
||||
"a datagrid component with functionality to add, remove and edit rows.",
|
||||
icon: "ri-grid-line",
|
||||
properties: {
|
||||
design: { ...all },
|
||||
settings: [
|
||||
{
|
||||
label: "Source",
|
||||
key: "datasource",
|
||||
control: TableViewSelect,
|
||||
},
|
||||
{
|
||||
label: "Detail URL",
|
||||
key: "detailUrl",
|
||||
control: DetailScreenSelect,
|
||||
},
|
||||
{
|
||||
label: "Editable",
|
||||
key: "editable",
|
||||
valueKey: "checked",
|
||||
control: Checkbox,
|
||||
},
|
||||
{
|
||||
label: "Theme",
|
||||
key: "theme",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
"alpine",
|
||||
"alpine-dark",
|
||||
"balham",
|
||||
"balham-dark",
|
||||
"material",
|
||||
],
|
||||
placeholder: "alpine",
|
||||
},
|
||||
{
|
||||
label: "Height",
|
||||
key: "height",
|
||||
defaultValue: "500",
|
||||
control: Input,
|
||||
},
|
||||
{
|
||||
label: "Pagination",
|
||||
key: "pagination",
|
||||
valueKey: "checked",
|
||||
control: Checkbox,
|
||||
},
|
||||
],
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
name: "Repeater",
|
||||
_component: "@budibase/standard-components/list",
|
||||
|
@ -152,8 +65,8 @@ export default {
|
|||
isCategory: true,
|
||||
children: [
|
||||
{
|
||||
_component: "@budibase/standard-components/dataform",
|
||||
name: "Form Basic",
|
||||
_component: "@budibase/standard-components/form",
|
||||
name: "Form",
|
||||
icon: "ri-file-edit-line",
|
||||
properties: {
|
||||
design: { ...all },
|
||||
|
@ -161,17 +74,8 @@ export default {
|
|||
},
|
||||
},
|
||||
{
|
||||
_component: "@budibase/standard-components/dataformwide",
|
||||
name: "Form Wide",
|
||||
icon: "ri-file-edit-line",
|
||||
properties: {
|
||||
design: { ...all },
|
||||
settings: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
_component: "@budibase/standard-components/input",
|
||||
name: "Textfield",
|
||||
_component: "@budibase/standard-components/textfield",
|
||||
name: "Text Field",
|
||||
description:
|
||||
"A textfield component that allows the user to input text.",
|
||||
icon: "ri-edit-box-line",
|
||||
|
@ -199,19 +103,19 @@ export default {
|
|||
// settings: [],
|
||||
// },
|
||||
// },
|
||||
{
|
||||
_component: "@budibase/standard-components/datepicker",
|
||||
name: "Date Picker",
|
||||
description: "A basic date picker component",
|
||||
icon: "ri-calendar-line",
|
||||
children: [],
|
||||
properties: {
|
||||
design: { ...all },
|
||||
settings: [
|
||||
{ label: "Placeholder", key: "placeholder", control: Input },
|
||||
],
|
||||
},
|
||||
},
|
||||
// {
|
||||
// _component: "@budibase/standard-components/datepicker",
|
||||
// name: "Date Picker",
|
||||
// description: "A basic date picker component",
|
||||
// icon: "ri-calendar-line",
|
||||
// children: [],
|
||||
// properties: {
|
||||
// design: { ...all },
|
||||
// settings: [
|
||||
// { label: "Placeholder", key: "placeholder", control: Input },
|
||||
// ],
|
||||
// },
|
||||
// },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -35,7 +35,10 @@
|
|||
const getComponentConstructor = component => {
|
||||
const split = component?.split("/")
|
||||
const name = split?.[split.length - 1]
|
||||
return name === "screenslot" ? Router : ComponentLibrary[name]
|
||||
if (name === "screenslot" && $builderStore.previewType !== "layout") {
|
||||
return Router
|
||||
}
|
||||
return ComponentLibrary[name]
|
||||
}
|
||||
|
||||
// Returns a unique key to let svelte know when to remount components.
|
||||
|
|
|
@ -21,17 +21,18 @@ exports.fetchAppComponentDefinitions = async function(ctx) {
|
|||
appDirectory,
|
||||
componentLibrary,
|
||||
ctx.isDev ? "" : "package",
|
||||
"components.json"
|
||||
"manifest.json"
|
||||
))
|
||||
console.log(componentJson)
|
||||
|
||||
const result = {}
|
||||
|
||||
// map over the components.json and add the library identifier as a key
|
||||
// button -> @budibase/standard-components/button
|
||||
for (let key of Object.keys(componentJson)) {
|
||||
const fullComponentName = `${componentLibrary}/${key}`
|
||||
const fullComponentName = `${componentLibrary}/${key}`.toLowerCase()
|
||||
result[fullComponentName] = {
|
||||
_component: fullComponentName,
|
||||
component: fullComponentName,
|
||||
...componentJson[key],
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ const EMPTY_LAYOUT = {
|
|||
_children: [
|
||||
{
|
||||
_id: "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967",
|
||||
_component: "##builtin/screenslot",
|
||||
_component: "@budibase/standard-components/screenslot",
|
||||
_styles: {
|
||||
normal: {
|
||||
flex: "1 1 auto",
|
||||
|
@ -151,7 +151,7 @@ const BASE_LAYOUTS = [
|
|||
},
|
||||
{
|
||||
_id: "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967",
|
||||
_component: "##builtin/screenslot",
|
||||
_component: "@budibase/standard-components/screenslot",
|
||||
_styles: {
|
||||
normal: {
|
||||
flex: "1 1 auto",
|
||||
|
@ -206,7 +206,7 @@ const BASE_LAYOUTS = [
|
|||
_children: [
|
||||
{
|
||||
_id: "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967",
|
||||
_component: "##builtin/screenslot",
|
||||
_component: "@budibase/standard-components/screenslot",
|
||||
_styles: {
|
||||
normal: {
|
||||
flex: "1 1 auto",
|
||||
|
|
|
@ -1,33 +1,43 @@
|
|||
*Psst — looking for an app template? Go here --> [sveltejs/template](https://github.com/sveltejs/template)*
|
||||
## Manifest
|
||||
|
||||
---
|
||||
|
||||
# component-template
|
||||
|
||||
A base for building shareable Svelte components. Clone it with [degit](https://github.com/Rich-Harris/degit):
|
||||
|
||||
```bash
|
||||
npx degit sveltejs/component-template my-new-component
|
||||
cd my-new-component
|
||||
npm install # or yarn
|
||||
```
|
||||
|
||||
Your component's source code lives in `src/index.html`.
|
||||
|
||||
TODO
|
||||
|
||||
* [ ] some firm opinions about the best way to test components
|
||||
* [ ] update `degit` so that it automates some of the setup work
|
||||
The `manifest.json` file exports the definitions of all components available in this version
|
||||
of the client library. The manifest is used by the builder to correctly display components and
|
||||
their settings, and know how to correctly interact with them.
|
||||
|
||||
|
||||
## Setting up
|
||||
|
||||
* Run `npm init` (or `yarn init`)
|
||||
* Replace this README with your own
|
||||
### Component Definitions
|
||||
|
||||
The object key is the name of the component, as exported by `index.js`.
|
||||
|
||||
- **name** - the name displayed in the builder
|
||||
- **description** - not currently used
|
||||
- **icon** - the icon displayed in the builder
|
||||
- **hasChildren** - whether the component accepts children or not
|
||||
- **styleable** - whether the component accepts design props or not
|
||||
- **dataProvider** - whether the component provides a data context or not
|
||||
- **bindable** - whether the components provides a bindable value or not
|
||||
- **settings** - array of settings displayed in the builder
|
||||
|
||||
###Settings Definitions
|
||||
|
||||
The `type` field in each setting is used by the builder to know which component to use to display
|
||||
the setting, so it's important that this field is correct. The valid options are:
|
||||
|
||||
- **text** - A text field
|
||||
- **select** - A select dropdown. Accompany these with an `options` field to provide options
|
||||
- **datasource** - A datasource (e.g. a table or a view)
|
||||
- **boolean** - A boolean field
|
||||
- **number** - A numeric text field
|
||||
- **detailURL** - A URL to a page which displays details about a row.
|
||||
Exclusively used for grids which link to row details.
|
||||
|
||||
|
||||
## Consuming components
|
||||
The available fields in each setting definition are:
|
||||
|
||||
Your package.json has a `"svelte"` field pointing to `src/index.html`, which allows Svelte apps to import the source code directly, if they are using a bundler plugin like [rollup-plugin-svelte](https://github.com/rollup/rollup-plugin-svelte) or [svelte-loader](https://github.com/sveltejs/svelte-loader) (where [`resolve.mainFields`](https://webpack.js.org/configuration/resolve/#resolve-mainfields) in your webpack config includes `"svelte"`). **This is recommended.**
|
||||
|
||||
For everyone else, `npm run build` will bundle your component's source code into a plain JavaScript module (`index.mjs`) and a UMD script (`index.js`). This will happen automatically when you publish your component to npm, courtesy of the `prepublishOnly` hook in package.json.
|
||||
- **type** - the type of field which determines which component the builder will use
|
||||
to display the setting
|
||||
- **key** - the key of this setting in the component
|
||||
- **label** - the label displayed in the builder
|
||||
- **defaultValue** - the default value of the setting
|
||||
- **placeholder** - the placeholder for the setting
|
||||
|
|
|
@ -141,35 +141,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"datagrid": {
|
||||
"name": "Grid",
|
||||
"description": "a datagrid component with functionality to add, remove and edit rows.",
|
||||
"data": true,
|
||||
"props": {
|
||||
"datasource": "tables",
|
||||
"editable": "bool",
|
||||
"theme": {
|
||||
"type": "options",
|
||||
"default": "alpine",
|
||||
"options": [
|
||||
"alpine",
|
||||
"alpine-dark",
|
||||
"balham",
|
||||
"balham-dark",
|
||||
"material"
|
||||
]
|
||||
},
|
||||
"height": {
|
||||
"type": "number",
|
||||
"default": "540"
|
||||
},
|
||||
"pagination": {
|
||||
"type": "bool",
|
||||
"default": true
|
||||
},
|
||||
"detailUrl": "string"
|
||||
}
|
||||
},
|
||||
|
||||
"dataform": {
|
||||
"name": "Form",
|
||||
"description": "an HTML table that fetches data from a table or view and displays it.",
|
||||
|
@ -623,38 +595,7 @@
|
|||
"url": "string"
|
||||
}
|
||||
},
|
||||
"container": {
|
||||
"name": "Container",
|
||||
"children": true,
|
||||
"description": "An element that contains and lays out other elements. e.g. <div>, <header> etc",
|
||||
"props": {
|
||||
"type": {
|
||||
"type": "options",
|
||||
"options": [
|
||||
"article",
|
||||
"aside",
|
||||
"details",
|
||||
"div",
|
||||
"firgure",
|
||||
"figcaption",
|
||||
"footer",
|
||||
"header",
|
||||
"main",
|
||||
"mark",
|
||||
"nav",
|
||||
"paragraph",
|
||||
"summary"
|
||||
],
|
||||
"default": "div"
|
||||
}
|
||||
},
|
||||
"baseComponent": true,
|
||||
"tags": [
|
||||
"div",
|
||||
"container",
|
||||
"layout"
|
||||
]
|
||||
},
|
||||
|
||||
"heading": {
|
||||
"name": "Heading",
|
||||
"description": "An HTML H1 - H6 tag",
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
{
|
||||
"container": {
|
||||
"name": "Container",
|
||||
"description": "This component contains things within itself",
|
||||
"icon": "ri-layout-column-line",
|
||||
"hasChildren": true,
|
||||
"styleable": true,
|
||||
"settings": [
|
||||
{
|
||||
"type": "select",
|
||||
"key": "type",
|
||||
"label": "Type",
|
||||
"defaultValue": "div",
|
||||
"options": [
|
||||
"article",
|
||||
"aside",
|
||||
"details",
|
||||
"div",
|
||||
"figure",
|
||||
"figcaption",
|
||||
"footer",
|
||||
"header",
|
||||
"main",
|
||||
"mark",
|
||||
"nav",
|
||||
"paragraph",
|
||||
"summary"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"grid": {
|
||||
"name": "Grid",
|
||||
"description":
|
||||
"A datagrid component with functionality to add, remove and edit rows.",
|
||||
"icon": "ri-grid-line",
|
||||
"styleable": true,
|
||||
"settings": [
|
||||
{
|
||||
"type": "datasource",
|
||||
"label": "Source",
|
||||
"key": "datasource"
|
||||
},
|
||||
{
|
||||
"type": "detailURL",
|
||||
"label": "Detail URL",
|
||||
"key": "detailUrl"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Editable",
|
||||
"key": "editable"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Theme",
|
||||
"key": "theme",
|
||||
"options": ["alpine", "alpine-dark", "balham", "balham-dark", "material"],
|
||||
"defaultValue": "alpine"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"label": "Height",
|
||||
"key": "height",
|
||||
"defaultValue": "500"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Pagination",
|
||||
"key": "pagination"
|
||||
}
|
||||
]
|
||||
},
|
||||
"screenslot": {
|
||||
"name": "Screenslot",
|
||||
"description": "Contains your app screens",
|
||||
"styleable": true
|
||||
}
|
||||
}
|
|
@ -36,6 +36,11 @@
|
|||
"@budibase/bbui": "^1.52.4",
|
||||
"@budibase/svelte-ag-grid": "^0.0.16",
|
||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||
"@spectrum-css/button": "^3.0.0-beta.6",
|
||||
"@spectrum-css/icon": "^3.0.0-beta.2",
|
||||
"@spectrum-css/page": "^3.0.0-beta.0",
|
||||
"@spectrum-css/typography": "^3.0.0-beta.1",
|
||||
"@spectrum-css/vars": "^3.0.0-beta.2",
|
||||
"apexcharts": "^3.22.1",
|
||||
"flatpickr": "^4.6.6",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
<script>
|
||||
// This component is overridden when running in a real app.
|
||||
// This simply serves as a placeholder component for the real screen router.
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h1>Screen Slot</h1>
|
||||
<span>
|
|
@ -1,28 +1,31 @@
|
|||
import "@budibase/bbui/dist/bbui.css"
|
||||
import "flatpickr/dist/flatpickr.css"
|
||||
|
||||
import "@spectrum-css/vars/dist/spectrum-global.css"
|
||||
import "@spectrum-css/vars/dist/spectrum-medium.css"
|
||||
import "@spectrum-css/vars/dist/spectrum-light.css"
|
||||
|
||||
export { default as container } from "./Container.svelte"
|
||||
export { default as text } from "./Text.svelte"
|
||||
export { default as heading } from "./Heading.svelte"
|
||||
export { default as input } from "./Input.svelte"
|
||||
export { default as richtext } from "./RichText.svelte"
|
||||
export { default as button } from "./Button.svelte"
|
||||
export { default as login } from "./Login.svelte"
|
||||
export { default as link } from "./Link.svelte"
|
||||
export { default as image } from "./Image.svelte"
|
||||
export { default as navigation } from "./Navigation.svelte"
|
||||
export { default as datagrid } from "./grid/Component.svelte"
|
||||
export { default as dataform } from "./DataForm.svelte"
|
||||
export { default as dataformwide } from "./DataFormWide.svelte"
|
||||
export { default as list } from "./List.svelte"
|
||||
export { default as embed } from "./Embed.svelte"
|
||||
export { default as stackedlist } from "./StackedList.svelte"
|
||||
export { default as card } from "./Card.svelte"
|
||||
export { default as cardhorizontal } from "./CardHorizontal.svelte"
|
||||
export { default as cardstat } from "./CardStat.svelte"
|
||||
export { default as rowdetail } from "./RowDetail.svelte"
|
||||
export { default as newrow } from "./NewRow.svelte"
|
||||
export { default as datepicker } from "./DatePicker.svelte"
|
||||
export { default as icon } from "./Icon.svelte"
|
||||
export { default as screenslotplaceholder } from "./ScreenSlotPlaceholder.svelte"
|
||||
export * from "./charts"
|
||||
export { default as grid } from "./grid/Component.svelte"
|
||||
export { default as screenslot } from "./ScreenSlot.svelte"
|
||||
|
||||
// export { default as text } from "./Text.svelte"
|
||||
// export { default as heading } from "./Heading.svelte"
|
||||
// export { default as input } from "./Input.svelte"
|
||||
// export { default as richtext } from "./RichText.svelte"
|
||||
// export { default as button } from "./Button.svelte"
|
||||
// export { default as login } from "./Login.svelte"
|
||||
// export { default as link } from "./Link.svelte"
|
||||
// export { default as image } from "./Image.svelte"
|
||||
// export { default as navigation } from "./Navigation.svelte"
|
||||
// export { default as list } from "./List.svelte"
|
||||
// export { default as embed } from "./Embed.svelte"
|
||||
// export { default as stackedlist } from "./StackedList.svelte"
|
||||
// export { default as card } from "./Card.svelte"
|
||||
// export { default as cardhorizontal } from "./CardHorizontal.svelte"
|
||||
// export { default as cardstat } from "./CardStat.svelte"
|
||||
// export { default as rowdetail } from "./RowDetail.svelte"
|
||||
// export { default as newrow } from "./NewRow.svelte"
|
||||
// export { default as datepicker } from "./DatePicker.svelte"
|
||||
// export { default as icon } from "./Icon.svelte"
|
||||
// export * from "./charts"
|
||||
|
|
|
@ -132,6 +132,33 @@
|
|||
estree-walker "^1.0.1"
|
||||
picomatch "^2.2.2"
|
||||
|
||||
"@spectrum-css/button@^3.0.0-beta.6":
|
||||
version "3.0.0-beta.6"
|
||||
resolved "https://registry.yarnpkg.com/@spectrum-css/button/-/button-3.0.0-beta.6.tgz#007919d3e7a6692e506dc9addcd46aee6b203b1a"
|
||||
integrity sha512-ZoJxezt5Pc006RR7SMG7PfC0VAdWqaGDpd21N8SEykGuz/KmNulqGW8RiSZQGMVX/jk5ZCAthPrH8cI/qtKbMg==
|
||||
|
||||
"@spectrum-css/icon@^3.0.0-beta.2":
|
||||
version "3.0.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/@spectrum-css/icon/-/icon-3.0.0-beta.2.tgz#2dd7258ded74501b56e5fc42d0b6f0a3f4936aeb"
|
||||
integrity sha512-BEHJ68YIXSwsNAqTdq/FrS4A+jtbKzqYrsGKXdDf93ql+fHWYXRCh1EVYGHx/1696mY73DhM4snMpKGIFtXGFA==
|
||||
|
||||
"@spectrum-css/page@^3.0.0-beta.0":
|
||||
version "3.0.0-beta.0"
|
||||
resolved "https://registry.yarnpkg.com/@spectrum-css/page/-/page-3.0.0-beta.0.tgz#885ea41b44861c5dc3aac904536f9e93c9109b58"
|
||||
integrity sha512-+OD+l3aLisykxJnHfLkdkxMS1Uj1vKGYpKil7W0r5lSWU44eHyRgb8ZK5Vri1+sUO5SSf/CTybeVwtXME9wMLA==
|
||||
dependencies:
|
||||
"@spectrum-css/vars" "^3.0.0-beta.2"
|
||||
|
||||
"@spectrum-css/typography@^3.0.0-beta.1":
|
||||
version "3.0.0-beta.1"
|
||||
resolved "https://registry.yarnpkg.com/@spectrum-css/typography/-/typography-3.0.0-beta.1.tgz#c2c2097c49e2711e8d048afcbaa5ccfe1a6ea7f1"
|
||||
integrity sha512-NnRvEnrTdt53ZUYh42v+ff6bUTUjG1qHctVJrIv8XrivFSc4L475x3lJgHmSVQeDoDLAsVOtISlJBXKrqK5eRQ==
|
||||
|
||||
"@spectrum-css/vars@^3.0.0-beta.2":
|
||||
version "3.0.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-3.0.0-beta.2.tgz#f0b3a2db44aa57b1a82e47ab392c716a3056a157"
|
||||
integrity sha512-HpcRDUkSjKVWUi7+jf6zp33YszXs3qFljaaNVTVOf0m0mqjWWXHxgLrvYlFFlHp5ITbNXds5Cb7EgiXCKmVIpA==
|
||||
|
||||
"@types/color-name@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
|
||||
|
|
Loading…
Reference in New Issue