WIP component management and definition refactor

This commit is contained in:
Andrew Kingston 2021-01-12 20:00:35 +00:00
parent e8f18e2853
commit 9b035df33e
25 changed files with 589 additions and 700 deletions

View File

@ -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)
}
)

View File

@ -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
}

View File

@ -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)
},
},
},

View File

@ -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

View File

@ -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
}

View File

@ -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()

View File

@ -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) => {

View File

@ -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

View File

@ -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()
},
}

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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)
}

View File

@ -0,0 +1,11 @@
[
{
"name": "Category",
"icon": "ri-file-edit-line",
"children": [
"container"
]
},
"grid",
"screenslot"
]

View File

@ -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 },
// ],
// },
// },
],
},
{

View File

@ -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.

View File

@ -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],
}
}

View File

@ -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",

View File

@ -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

View File

@ -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",

View File

@ -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
}
}

View File

@ -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",

View File

@ -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>

View File

@ -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"

View File

@ -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"