Merge branch 'master' into 98-builtin-slot

This commit is contained in:
Michael Shanks 2020-02-18 17:11:46 +00:00 committed by GitHub
commit 83953922d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1404 additions and 257 deletions

View File

@ -0,0 +1,17 @@
export const insertCodeMetadata = props => {
if (props._code && props._code.length > 0) {
props._codeMeta = codeMetaData(props._code)
}
if (!props._children || props._children.length === 0) return
for (let child of props._children) {
insertCodeMetadata(child)
}
}
const codeMetaData = code => {
return {
dependsOnStore: RegExp(/(store.)/g).test(code),
}
}

View File

@ -40,6 +40,7 @@ import {
} from "./loadComponentLibraries"
import { buildCodeForScreens } from "./buildCodeForScreens"
import { generate_screen_css } from "./generate_css"
import { insertCodeMetadata } from "./insertCodeMetadata"
// import { uuid } from "./uuid"
let appname = ""
@ -115,6 +116,7 @@ export const getStore = () => {
store.setComponentStyle = setComponentStyle(store)
store.setComponentCode = setComponentCode(store)
store.setScreenType = setScreenType(store)
store.deleteComponent = deleteComponent(store)
return store
}
@ -824,6 +826,8 @@ const setCurrentScreenFunctions = s => {
s.currentPreviewItem === "screen"
? buildCodeForScreens([s.currentPreviewItem])
: "({});"
insertCodeMetadata(s.currentPreviewItem.props)
}
const setScreenType = store => type => {
@ -840,3 +844,39 @@ const setScreenType = store => type => {
return s
})
}
const deleteComponent = store => component => {
store.update(s => {
let parent
walkProps(s.currentPreviewItem.props, (p, breakWalk) => {
if (p._children.includes(component)) {
parent = p
breakWalk()
}
})
if (parent) {
parent._children = parent._children.filter(c => c !== component)
}
s.currentFrontEndType === "page"
? _savePage(s)
: _saveScreenApi(s.currentPreviewItem, s)
return s
})
}
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)
}
}
}

View File

@ -0,0 +1,55 @@
<script>
import Button from "./Button.svelte"
import ButtonGroup from "./ButtonGroup.svelte"
import UIkit from "uikit"
export let title=""
export let body=""
export let okText = "OK"
export let cancelText = "Cancel"
export let onOk = ()=> {}
export let onCancel = ()=> {}
export const show = () => {
UIkit.modal(theModal).show()
}
export const hide = () => {
UIkit.modal(theModal).hide()
}
let theModal;
const cancel = () => {
hide()
onCancel()
}
const ok = () => {
hide()
onOk()
}
</script>
<div id="my-id" uk-modal bind:this={theModal}>
<div class="uk-modal-dialog">
<button class="uk-modal-close-default" type="button" uk-close></button>
<div class="uk-modal-header">
<h2 class="uk-modal-title">{title}</h2>
</div>
<div class="uk-modal-body">{body}</div>
<div class="uk-modal-footer">
<ButtonGroup>
<Button grouped color="primary" on:click={ok}>
{okText}
</Button>
<Button grouped color="secondary" on:click={cancel}>
{cancelText}
</Button>
</ButtonGroup>
</div>
</div>
</div>

View File

@ -0,0 +1,23 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-x-circle">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
<style>
svg {
height: 100%;
width: 100%;
}
</style>

After

Width:  |  Height:  |  Size: 454 B

View File

@ -7,3 +7,4 @@ export { default as ArrowDownIcon } from "./ArrowDown.svelte"
export { default as CircleIndicator } from "./CircleIndicator.svelte"
export { default as PencilIcon } from "./Pencil.svelte"
export { default as EventsIcon } from "./Events.svelte"
export { default as XCircleIcon } from "./XCircle.svelte"

View File

@ -2,13 +2,16 @@
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
import { last, sortBy, map, trimCharsStart, trimChars, join } from "lodash/fp"
import ConfirmDialog from "../common/ConfirmDialog.svelte"
import { pipe } from "../common/core"
import { store } from "../builderStore"
import { ArrowDownIcon } from "../common/Icons/"
export let screens = []
let confirmDeleteDialog
let componentToDelete = ""
const joinPath = join("/")
const normalizedName = name =>
@ -23,6 +26,7 @@
)
const lastPartOfName = c =>
c &&
last(c.name ? c.name.split("/") : c._component.split("/"))
const isComponentSelected = (current, comp) => current === comp
@ -38,6 +42,12 @@
component.component &&
$store.currentPreviewItem &&
component.component.name === $store.currentPreviewItem.name
const confirmDeleteComponent = component => {
componentToDelete = component
confirmDeleteDialog.show()
}
</script>
<div class="root">
@ -63,12 +73,20 @@
<ComponentsHierarchyChildren
components={screen.component.props._children}
currentComponent={$store.currentComponentInfo}
onSelect={store.selectComponent} />
onSelect={store.selectComponent}
onDeleteComponent={confirmDeleteComponent}/>
{/if}
{/each}
</div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete._component)}' component`}
okText="Delete Component"
onOk={() => store.deleteComponent(componentToDelete)}/>
<style>
.root {
font-weight: 500;

View File

@ -1,20 +1,24 @@
<script>
import { last } from "lodash/fp"
import { pipe } from "../common/core"
import { XCircleIcon } from "../common/Icons"
export let components = []
export let currentComponent
export let onSelect = () => {}
export let level = 0
export let onDeleteComponent
const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
const get_name = s => last(s.split("/"))
const get_name = s => !s ? "" : last(s.split("/"))
const get_capitalised_name = name =>
pipe(
name,
[get_name, capitalise]
)
</script>
<ul>
@ -24,7 +28,12 @@
class="item"
class:selected={currentComponent === component}
style="padding-left: {level * 20 + 67}px">
{get_capitalised_name(component._component)}
<span class="item-name">{get_capitalised_name(component._component)}</span>
<button
class="delete-component"
on:click={() => onDeleteComponent(component)}>
<XCircleIcon />
</button>
</span>
{#if component._children}
@ -32,7 +41,8 @@
components={component._children}
{currentComponent}
{onSelect}
level={level + 1} />
level={level + 1}
{onDeleteComponent} />
{/if}
</li>
{/each}
@ -45,14 +55,39 @@
margin: 0;
}
.item {
display: block;
padding: 11px 67px;
display: flex;
flex-direction: row;
padding: 11px 5px 11px 67px;
border-radius: 3px;
}
.item > span {
width: 1px;
flex: 1 1 auto;
}
.item > button {
display: none;
height: 20px;
color: var(--slate)
}
.item:hover {
background: #fafafa;
cursor: pointer;
}
.item:hover > button {
border-style: none;
background: rgba(0,0,0,0);
display: block;
cursor: pointer;
}
.item:hover > button:hover {
color: var(--button-text);
}
.selected {
color: var(--button-text);
background: var(--background-button) !important;

View File

@ -11,15 +11,17 @@
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers"
export let event
export let eventOptions
export let eventOptions = []
export let open
export let onClose
export let onPropChanged
let eventType = "onClick"
let eventType = ""
let draftEventHandler = { parameters: [] }
$: eventData = event || { handlers: [] }
$: if (!eventOptions.includes(eventType) && eventOptions.length > 0)
eventType = eventOptions[0].name
const closeModal = () => {
onClose()
@ -74,7 +76,7 @@
<h5>Event Type</h5>
{@html getIcon('info', 20)}
</header>
<Select :value={eventType}>
<Select bind:value={eventType}>
{#each eventOptions as option}
<option value={option.name}>{option.name}</option>
{/each}

View File

@ -34,16 +34,10 @@
let selectedEvent = null
$: {
events = Object.keys(component)
.filter(key => findType(key) === EVENT_TYPE)
.map(key => ({ name: key, handlers: component[key] }))
}
function findType(propName) {
if (!component._component) return
return components.find(({ name }) => name === component._component).props[
propName
]
const componentDefinition = components.find(c => c.name === component._component)
events = Object.keys(componentDefinition.props)
.filter(propName => componentDefinition.props[propName].type === EVENT_TYPE)
.map(propName => ({ name: propName, handlers: (component[propName] || []) }))
}
const openModal = event => {

View File

@ -10,8 +10,12 @@
import SettingsView from "./SettingsView.svelte"
import PageView from "./PageView.svelte"
import ComponentsPaneSwitcher from "./ComponentsPaneSwitcher.svelte"
import ConfirmDialog from "../common/ConfirmDialog.svelte"
import { last } from "lodash/fp"
let newComponentPicker
let confirmDeleteDialog
let componentToDelete = ""
const newComponent = () => {
newComponentPicker.show()
@ -21,6 +25,14 @@
const settings = () => {
settingsView.show()
}
const confirmDeleteComponent = component => {
componentToDelete = component
confirmDeleteDialog.show()
}
const lastPartOfName = c => c ? c.split("/") : ""
</script>
<div class="root">
@ -53,6 +65,7 @@
<ComponentsHierarchyChildren
components={$store.currentPreviewItem.props._children}
currentComponent={$store.currentComponentInfo}
onDeleteComponent={confirmDeleteComponent}
onSelect={store.selectComponent}
level={-2} />
{/if}
@ -101,6 +114,13 @@
<NewComponent bind:this={newComponentPicker} />
<SettingsView bind:this={settingsView} />
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete._component)}' component`}
okText="Delete Component"
onOk={() => store.deleteComponent(componentToDelete)}/>
<style>
button {
cursor: pointer;

View File

@ -36,6 +36,7 @@
"dependencies": {
"@nx-js/compiler-util": "^2.0.0",
"bcryptjs": "^2.4.3",
"deep-equal": "^2.0.1",
"lodash": "^4.17.15",
"lunr": "^2.3.5",
"regexparam": "^1.3.0",

View File

@ -1,74 +1,46 @@
import { writable } from "svelte/store"
import { createCoreApi } from "./core"
import { getStateOrValue } from "./state/getState"
import { setState, setStateFromBinding } from "./state/setState"
import { trimSlash } from "./common/trimSlash"
import { isBound } from "./state/isState"
import { attachChildren } from "./render/attachChildren"
import { createTreeNode } from "./render/renderComponent"
import { createTreeNode } from "./render/prepareRenderComponent"
import { screenRouter } from "./render/screenRouter"
import { createStateManager } from "./state/stateManager"
export const createApp = (
document,
componentLibraries,
frontendDefinition,
backendDefinition,
user,
uiFunctions
uiFunctions,
window
) => {
const coreApi = createCoreApi(backendDefinition, user)
backendDefinition.hierarchy = coreApi.templateApi.constructHierarchy(
backendDefinition.hierarchy
)
const pageStore = writable({
_bbuser: user,
})
const relativeUrl = url =>
frontendDefinition.appRootPath
? frontendDefinition.appRootPath + "/" + trimSlash(url)
: url
const apiCall = method => (url, body) =>
fetch(relativeUrl(url), {
method: method,
headers: {
"Content-Type": "application/json",
},
body: body && JSON.stringify(body),
})
const api = {
post: apiCall("POST"),
get: apiCall("GET"),
patch: apiCall("PATCH"),
delete: apiCall("DELETE"),
}
const safeCallEvent = (event, context) => {
const isFunction = obj =>
!!(obj && obj.constructor && obj.call && obj.apply)
if (isFunction(event)) event(context)
}
let routeTo
let currentScreenStore
let currentScreenUbsubscribe
let currentUrl
let screenStateManager
const onScreenSlotRendered = screenSlotNode => {
const onScreenSelected = (screen, store, url) => {
const { getInitialiseParams, unsubscribe } = attachChildrenParams(store)
const stateManager = createStateManager({
store,
coreApi,
frontendDefinition,
componentLibraries,
uiFunctions,
onScreenSlotRendered: () => {},
})
const getAttchChildrenParams = attachChildrenParams(stateManager)
screenSlotNode.props._children = [screen.props]
const initialiseChildParams = getInitialiseParams(screenSlotNode)
const initialiseChildParams = getAttchChildrenParams(screenSlotNode)
attachChildren(initialiseChildParams)(screenSlotNode.rootElement, {
hydrate: true,
force: true,
})
if (currentScreenUbsubscribe) currentScreenUbsubscribe()
currentScreenUbsubscribe = unsubscribe
currentScreenStore = store
if (screenStateManager) screenStateManager.destroy()
screenStateManager = stateManager
currentUrl = url
}
@ -76,46 +48,28 @@ export const createApp = (
routeTo(currentUrl || window.location.pathname)
}
const attachChildrenParams = store => {
let currentState = null
const unsubscribe = store.subscribe(s => {
currentState = s
})
const attachChildrenParams = stateManager => {
const getInitialiseParams = treeNode => ({
bb: getBbClientApi,
coreApi,
store,
document,
componentLibraries,
frontendDefinition,
uiFunctions,
treeNode,
onScreenSlotRendered,
setupState: stateManager.setup,
getCurrentState: stateManager.getCurrentState,
})
const getBbClientApi = (treeNode, componentProps) => {
return {
attachChildren: attachChildren(getInitialiseParams(treeNode)),
context: treeNode.context,
props: componentProps,
call: safeCallEvent,
setStateFromBinding: (binding, value) =>
setStateFromBinding(store, binding, value),
setState: (path, value) => setState(store, path, value),
getStateOrValue: (prop, currentContext) =>
getStateOrValue(currentState, prop, currentContext),
store,
relativeUrl,
api,
isBound,
parent,
}
}
return { getInitialiseParams, unsubscribe }
return getInitialiseParams
}
let rootTreeNode
const pageStateManager = createStateManager({
store: writable({ _bbuser: user }),
coreApi,
frontendDefinition,
componentLibraries,
uiFunctions,
onScreenSlotRendered,
})
const initialisePage = (page, target, urlPath) => {
currentUrl = urlPath
@ -125,7 +79,7 @@ export const createApp = (
_children: [page.props],
}
rootTreeNode.rootElement = target
const { getInitialiseParams } = attachChildrenParams(pageStore)
const getInitialiseParams = attachChildrenParams(pageStateManager)
const initChildParams = getInitialiseParams(rootTreeNode)
attachChildren(initChildParams)(target, {
@ -137,8 +91,8 @@ export const createApp = (
}
return {
initialisePage,
screenStore: () => currentScreenStore,
pageStore: () => pageStore,
screenStore: () => screenStateManager.store,
pageStore: () => pageStateManager.store,
routeTo: () => routeTo,
rootNode: () => rootTreeNode,
}

View File

@ -43,12 +43,12 @@ export const loadBudibase = async (opts) => {
componentLibraries[builtinLibName] = builtins(_window)
const { initialisePage, screenStore, pageStore, routeTo, rootNode } = createApp(
_window.document,
componentLibraries,
frontendDefinition,
backendDefinition,
user,
uiFunctions || {}
uiFunctions || {},
_window
)
const route = _window.location

View File

@ -1,19 +1,17 @@
import { setupBinding } from "../state/stateBinding"
import { split, last } from "lodash/fp"
import { $ } from "../core/common"
import { renderComponent } from "./renderComponent"
import { prepareRenderComponent } from "./prepareRenderComponent"
import { isScreenSlot } from "./builtinComponents"
import deepEqual from "deep-equal"
export const attachChildren = initialiseOpts => (htmlElement, options) => {
const {
uiFunctions,
bb,
coreApi,
store,
componentLibraries,
treeNode,
frontendDefinition,
onScreenSlotRendered,
setupState,
getCurrentState,
} = initialiseOpts
const anchor = options && options.anchor ? options.anchor : null
@ -34,50 +32,46 @@ export const attachChildren = initialiseOpts => (htmlElement, options) => {
htmlElement.classList.add(`lay-${treeNode.props._id}`)
const renderedComponents = []
const childNodes = []
for (let childProps of treeNode.props._children) {
const { componentName, libName } = splitName(childProps._component)
if (!componentName || !libName) return
const { initialProps, bind } = setupBinding(
store,
childProps,
coreApi,
frontendDefinition.appRootPath
)
const componentConstructor = componentLibraries[libName][componentName]
const renderedComponentsThisIteration = renderComponent({
const childNodesThisIteration = prepareRenderComponent({
props: childProps,
parentNode: treeNode,
componentConstructor,
uiFunctions,
htmlElement,
anchor,
initialProps,
bb,
getCurrentState
})
if (
onScreenSlotRendered &&
isScreenSlot(childProps._component) &&
renderedComponentsThisIteration.length > 0
) {
for (let childNode of childNodesThisIteration) {
childNodes.push(childNode)
}
}
if (areTreeNodesEqual(treeNode.children, childNodes)) return treeNode.children
for (let node of childNodes) {
const initialProps = setupState(node)
node.render(initialProps)
}
const screenSlot = childNodes.find(n => isScreenSlot(n.props._component))
if (onScreenSlotRendered && screenSlot) {
// assuming there is only ever one screen slot
onScreenSlotRendered(renderedComponentsThisIteration[0])
onScreenSlotRendered(screenSlot)
}
for (let comp of renderedComponentsThisIteration) {
comp.unsubscribe = bind(comp.component)
renderedComponents.push(comp)
}
}
treeNode.children = childNodes
treeNode.children = renderedComponents
return renderedComponents
return childNodes
}
const splitName = fullname => {
@ -90,3 +84,19 @@ const splitName = fullname => {
return { libName, componentName }
}
const areTreeNodesEqual = (children1, children2) => {
if (children1.length !== children2.length) return false
if (children1 === children2) return true
let isEqual = false
for (let i = 0; i < children1.length; i++) {
isEqual = deepEqual(children1[i].context, children2[i].context)
if (!isEqual) return false
if (isScreenSlot(children1[i].parentNode.props._component)) {
isEqual = deepEqual(children1[i].props, children2[i].props)
}
if (!isEqual) return false
}
return true
}

View File

@ -1,22 +1,21 @@
export const renderComponent = ({
export const prepareRenderComponent = ({
componentConstructor,
uiFunctions,
htmlElement,
anchor,
props,
initialProps,
bb,
parentNode,
getCurrentState,
}) => {
const func = initialProps._id ? uiFunctions[initialProps._id] : undefined
const func = props._id ? uiFunctions[props._id] : undefined
const parentContext = (parentNode && parentNode.context) || {}
let renderedNodes = []
const render = context => {
let nodesToRender = []
const createNodeAndRender = context => {
let componentContext = parentContext
if (context) {
componentContext = { ...componentContext }
componentContext = { ...context }
componentContext.$parent = parentContext
}
@ -24,33 +23,31 @@ export const renderComponent = ({
thisNode.context = componentContext
thisNode.parentNode = parentNode
thisNode.props = props
nodesToRender.push(thisNode)
parentNode.children.push(thisNode)
renderedNodes.push(thisNode)
initialProps._bb = bb(thisNode, props)
thisNode.render = initialProps => {
thisNode.component = new componentConstructor({
target: htmlElement,
props: initialProps,
hydrate: false,
anchor,
})
thisNode.rootElement =
htmlElement.children[htmlElement.children.length - 1]
thisNode.rootElement = htmlElement.children[htmlElement.children.length - 1]
if (initialProps._id && thisNode.rootElement) {
thisNode.rootElement.classList.add(`pos-${initialProps._id}`)
if (props._id && thisNode.rootElement) {
thisNode.rootElement.classList.add(`pos-${props._id}`)
}
}
}
if (func) {
func(render, parentContext)
func(createNodeAndRender, parentContext, getCurrentState())
} else {
render()
createNodeAndRender()
}
return renderedNodes
return nodesToRender
}
export const createTreeNode = () => ({
@ -59,8 +56,10 @@ export const createTreeNode = () => ({
rootElement: null,
parentNode: null,
children: [],
bindings: [],
component: null,
unsubscribe: () => {},
render: () => {},
get destroy() {
const node = this
return () => {
@ -71,6 +70,10 @@ export const createTreeNode = () => ({
child.destroy()
}
}
for (let onDestroyItem of node.onDestroy) {
onDestroyItem()
}
}
},
onDestroy: [],
})

View File

@ -0,0 +1,70 @@
import { getStateOrValue } from "./getState"
import { setState, setStateFromBinding } from "./setState"
import { trimSlash } from "../common/trimSlash"
import { isBound } from "./isState"
import { attachChildren } from "../render/attachChildren"
export const bbFactory = ({
store,
getCurrentState,
frontendDefinition,
componentLibraries,
uiFunctions,
onScreenSlotRendered,
}) => {
const relativeUrl = url =>
frontendDefinition.appRootPath
? frontendDefinition.appRootPath + "/" + trimSlash(url)
: url
const apiCall = method => (url, body) =>
fetch(relativeUrl(url), {
method: method,
headers: {
"Content-Type": "application/json",
},
body: body && JSON.stringify(body),
})
const api = {
post: apiCall("POST"),
get: apiCall("GET"),
patch: apiCall("PATCH"),
delete: apiCall("DELETE"),
}
const safeCallEvent = (event, context) => {
const isFunction = obj =>
!!(obj && obj.constructor && obj.call && obj.apply)
if (isFunction(event)) event(context)
}
return (treeNode, setupState) => {
const attachParams = {
componentLibraries,
uiFunctions,
treeNode,
onScreenSlotRendered,
setupState,
getCurrentState,
}
return {
attachChildren: attachChildren(attachParams),
context: treeNode.context,
props: treeNode.props,
call: safeCallEvent,
setStateFromBinding: (binding, value) =>
setStateFromBinding(store, binding, value),
setState: (path, value) => setState(store, path, value),
getStateOrValue: (prop, currentContext) =>
getStateOrValue(getCurrentState(), prop, currentContext),
store: store,
relativeUrl,
api,
isBound,
parent,
}
}
}

View File

@ -23,7 +23,8 @@ const isMetaProp = propName =>
propName === "_component" ||
propName === "_children" ||
propName === "_id" ||
propName === "_style"
propName === "_style" ||
propName === "_code"
export const setupBinding = (store, rootProps, coreApi, context, rootPath) => {
const rootInitialProps = { ...rootProps }

View File

@ -0,0 +1,288 @@
import {
isEventType,
eventHandlers,
EVENT_TYPE_MEMBER_NAME,
} from "./eventHandlers"
import { bbFactory } from "./bbComponentApi"
import { getState } from "./getState"
import { attachChildren } from "../render/attachChildren"
import {
isBound,
takeStateFromStore,
takeStateFromContext,
takeStateFromEventParameters,
BB_STATE_FALLBACK,
BB_STATE_BINDINGPATH,
BB_STATE_BINDINGSOURCE,
} from "./isState"
const doNothing = () => {}
doNothing.isPlaceholder = true
const isMetaProp = propName =>
propName === "_component" ||
propName === "_children" ||
propName === "_id" ||
propName === "_style" ||
propName === "_code" ||
propName === "_codeMeta"
export const createStateManager = ({
store,
coreApi,
rootPath,
frontendDefinition,
componentLibraries,
uiFunctions,
onScreenSlotRendered,
}) => {
let handlerTypes = eventHandlers(store, coreApi, rootPath)
let currentState
// any nodes that have props that are bound to the store
let nodesBoundByProps = []
// any node whose children depend on code, that uses the store
let nodesWithCodeBoundChildren = []
const getCurrentState = () => currentState
const registerBindings = _registerBindings(
nodesBoundByProps,
nodesWithCodeBoundChildren
)
const bb = bbFactory({
store,
getCurrentState,
frontendDefinition,
componentLibraries,
uiFunctions,
onScreenSlotRendered,
})
const setup = _setup(handlerTypes, getCurrentState, registerBindings, bb)
const unsubscribe = store.subscribe(
onStoreStateUpdated({
setCurrentState: s => (currentState = s),
getCurrentState,
nodesWithCodeBoundChildren,
nodesBoundByProps,
uiFunctions,
componentLibraries,
onScreenSlotRendered,
setupState: setup,
})
)
return {
setup,
destroy: () => unsubscribe(),
getCurrentState,
store,
}
}
const onStoreStateUpdated = ({
setCurrentState,
getCurrentState,
nodesWithCodeBoundChildren,
nodesBoundByProps,
uiFunctions,
componentLibraries,
onScreenSlotRendered,
setupState,
}) => s => {
setCurrentState(s)
// the original array gets changed by components' destroy()
// so we make a clone and check if they are still in the original
const nodesWithBoundChildren_clone = [...nodesWithCodeBoundChildren]
for (let node of nodesWithBoundChildren_clone) {
if (!nodesWithCodeBoundChildren.includes(node)) continue
attachChildren({
uiFunctions,
componentLibraries,
treeNode: node,
onScreenSlotRendered,
setupState,
getCurrentState,
})(node.rootElement, { hydrate: true, force: true })
}
for (let node of nodesBoundByProps) {
setNodeState(s, node)
}
}
const _registerBindings = (nodesBoundByProps, nodesWithCodeBoundChildren) => (
node,
bindings
) => {
if (bindings.length > 0) {
node.bindings = bindings
nodesBoundByProps.push(node)
const onDestroy = () => {
nodesBoundByProps = nodesBoundByProps.filter(n => n === node)
node.onDestroy = node.onDestroy.filter(d => d === onDestroy)
}
node.onDestroy.push(onDestroy)
}
if (
node.props._children &&
node.props._children.filter(c => c._codeMeta && c._codeMeta.dependsOnStore)
.length > 0
) {
nodesWithCodeBoundChildren.push(node)
const onDestroy = () => {
nodesWithCodeBoundChildren = nodesWithCodeBoundChildren.filter(
n => n === node
)
node.onDestroy = node.onDestroy.filter(d => d === onDestroy)
}
node.onDestroy.push(onDestroy)
}
}
const setNodeState = (storeState, node) => {
if (!node.component) return
const newProps = { ...node.bindings.initialProps }
for (let binding of node.bindings) {
const val = getState(storeState, binding.path, binding.fallback)
if (val === undefined && newProps[binding.propName] !== undefined) {
delete newProps[binding.propName]
}
if (val !== undefined) {
newProps[binding.propName] = val
}
}
node.component.$set(newProps)
}
const _setup = (
handlerTypes,
getCurrentState,
registerBindings,
bb
) => node => {
const props = node.props
const context = node.context || {}
const initialProps = { ...props }
const storeBoundProps = []
const currentStoreState = getCurrentState()
for (let propName in props) {
if (isMetaProp(propName)) continue
const val = props[propName]
if (isBound(val) && takeStateFromStore(val)) {
const path = BindingPath(val)
const source = BindingSource(val)
const fallback = BindingFallback(val)
storeBoundProps.push({
path,
fallback,
propName,
source,
})
initialProps[propName] = !currentStoreState
? fallback
: getState(
currentStoreState,
BindingPath(val),
BindingFallback(val),
BindingSource(val)
)
} else if (isBound(val) && takeStateFromContext(val)) {
initialProps[propName] = !context
? val
: getState(
context,
BindingPath(val),
BindingFallback(val),
BindingSource(val)
)
} else if (isEventType(val)) {
const handlersInfos = []
for (let e of val) {
const handlerInfo = {
handlerType: e[EVENT_TYPE_MEMBER_NAME],
parameters: e.parameters,
}
const resolvedParams = {}
for (let paramName in handlerInfo.parameters) {
const paramValue = handlerInfo.parameters[paramName]
if (!isBound(paramValue)) {
resolvedParams[paramName] = () => paramValue
continue
} else if (takeStateFromContext(paramValue)) {
const val = getState(
context,
paramValue[BB_STATE_BINDINGPATH],
paramValue[BB_STATE_FALLBACK]
)
resolvedParams[paramName] = () => val
} else if (takeStateFromStore(paramValue)) {
resolvedParams[paramName] = () =>
getState(
getCurrentState(),
paramValue[BB_STATE_BINDINGPATH],
paramValue[BB_STATE_FALLBACK]
)
continue
} else if (takeStateFromEventParameters(paramValue)) {
resolvedParams[paramName] = eventContext => {
getState(
eventContext,
paramValue[BB_STATE_BINDINGPATH],
paramValue[BB_STATE_FALLBACK]
)
}
}
}
handlerInfo.parameters = resolvedParams
handlersInfos.push(handlerInfo)
}
if (handlersInfos.length === 0) initialProps[propName] = doNothing
else {
initialProps[propName] = async context => {
for (let handlerInfo of handlersInfos) {
const handler = makeHandler(handlerTypes, handlerInfo)
await handler(context)
}
}
}
}
}
registerBindings(node, storeBoundProps)
const setup = _setup(handlerTypes, getCurrentState, registerBindings, bb)
initialProps._bb = bb(node, setup)
return initialProps
}
const makeHandler = (handlerTypes, handlerInfo) => {
const handlerType = handlerTypes[handlerInfo.handlerType]
return context => {
const parameters = {}
for (let p in handlerInfo.parameters) {
parameters[p] = handlerInfo.parameters[p](context)
}
handlerType.execute(parameters)
}
}
const BindingPath = prop => prop[BB_STATE_BINDINGPATH]
const BindingFallback = prop => prop[BB_STATE_FALLBACK]
const BindingSource = prop => prop[BB_STATE_BINDINGSOURCE]

View File

@ -1,4 +1,5 @@
import { load, makePage, makeScreen } from "./testAppDef"
import { EVENT_TYPE_MEMBER_NAME } from "../src/state/eventHandlers"
describe("initialiseApp (binding)", () => {
it("should populate root element prop from store value", async () => {
@ -169,4 +170,177 @@ describe("initialiseApp (binding)", () => {
"header 2 - new val"
)
})
it("should fire events", async () => {
const { dom, app } = await load(
makePage({
_component: "testlib/button",
onClick: [
event("Set State", {
path: "address",
value: "123 Main Street",
}),
],
})
)
const button = dom.window.document.body.children[0]
expect(button.tagName).toBe("BUTTON")
let storeAddress
app.pageStore().subscribe(s => {
storeAddress = s.address
})
button.dispatchEvent(new dom.window.Event("click"))
expect(storeAddress).toBe("123 Main Street")
})
it("should alter event parameters based on store values", async () => {
const { dom, app } = await load(
makePage({
_component: "testlib/button",
onClick: [
event("Set State", {
path: "address",
value: {
"##bbstate": "sourceaddress",
"##bbsource": "store",
"##bbstatefallback": "fallback address",
},
}),
],
})
)
const button = dom.window.document.body.children[0]
expect(button.tagName).toBe("BUTTON")
let storeAddress
app.pageStore().subscribe(s => {
storeAddress = s.address
})
button.dispatchEvent(new dom.window.Event("click"))
expect(storeAddress).toBe("fallback address")
app.pageStore().update(s => {
s.sourceaddress = "new address"
return s
})
button.dispatchEvent(new dom.window.Event("click"))
expect(storeAddress).toBe("new address")
})
it("should take event parameters from context values", async () => {
const { dom, app } = await load(
makePage({
_component: "testlib/button",
_id: "with_context",
onClick: [
event("Set State", {
path: "address",
value: {
"##bbstate": "testKey",
"##bbsource": "context",
"##bbstatefallback": "fallback address",
},
}),
],
})
)
const button = dom.window.document.body.children[0]
expect(button.tagName).toBe("BUTTON")
let storeAddress
app.pageStore().subscribe(s => {
storeAddress = s.address
})
button.dispatchEvent(new dom.window.Event("click"))
expect(storeAddress).toBe("test value")
})
})
it("should rerender components when their code is bound to the store ", async () => {
const { dom, app } = await load(
makePage({
_component: "testlib/div",
_children: [
{
_component: "testlib/div",
_id: "n_clones_based_on_store",
className: "child_div",
},
],
})
)
const rootDiv = dom.window.document.body.children[0]
expect(rootDiv.tagName).toBe("DIV")
expect(rootDiv.children.length).toBe(0)
app.pageStore().update(s => {
s.componentCount = 3
return s
})
expect(rootDiv.children.length).toBe(3)
expect(rootDiv.children[0].className.includes("child_div")).toBe(true)
app.pageStore().update(s => {
s.componentCount = 5
return s
})
expect(rootDiv.children.length).toBe(5)
expect(rootDiv.children[0].className.includes("child_div")).toBe(true)
app.pageStore().update(s => {
s.componentCount = 0
return s
})
expect(rootDiv.children.length).toBe(0)
})
it("should be able to read value from context, passed fromm parent, through code", async () => {
const { dom, app } = await load(
makePage({
_component: "testlib/div",
_children: [
{
_component: "testlib/div",
_id: "n_clones_based_on_store",
className: {
"##bbstate": "index",
"##bbsource": "context",
"##bbstatefallback": "nothing",
},
},
],
})
)
const rootDiv = dom.window.document.body.children[0]
expect(rootDiv.tagName).toBe("DIV")
expect(rootDiv.children.length).toBe(0)
app.pageStore().update(s => {
s.componentCount = 3
return s
})
expect(rootDiv.children.length).toBe(3)
expect(rootDiv.children[0].className.includes("index_0")).toBe(true)
expect(rootDiv.children[1].className.includes("index_1")).toBe(true)
expect(rootDiv.children[2].className.includes("index_2")).toBe(true)
})
const event = (handlerType, parameters) => {
const e = {}
e[EVENT_TYPE_MEMBER_NAME] = handlerType
e.parameters = parameters
return e
}

View File

@ -15,6 +15,7 @@ export const load = async (page, screens = [], url = "/") => {
actions: [],
triggers: [],
})
setComponentCodeMeta(page, screens)
const app = await loadBudibase({
componentLibraries: allLibs(dom.window),
window: dom.window,
@ -47,8 +48,11 @@ export const timeout = ms => new Promise(resolve => setTimeout(resolve, ms))
export const walkComponentTree = (node, action) => {
action(node)
if (node.children) {
for (let child of node.children) {
// works for nodes or props
const children = node.children || node._children
if (children) {
for (let child of children) {
walkComponentTree(child, action)
}
}
@ -68,6 +72,22 @@ const autoAssignIds = (props, count = 0) => {
}
}
// any component with an id that include "based_on_store" is
// assumed to have code that depends on store value
const setComponentCodeMeta = (page, screens) => {
const setComponentCodeMeta_single = props => {
walkComponentTree(props, c => {
if (c._id.indexOf("based_on_store") >= 0) {
c._codeMeta = { dependsOnStore: true }
}
})
}
setComponentCodeMeta_single(page.props)
for (let s of screens || []) {
setComponentCodeMeta_single(s.props)
}
}
const setAppDef = (window, page, screens) => {
window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
componentLibraries: [],
@ -148,6 +168,29 @@ const maketestlib = window => ({
set(opts.props)
opts.target.appendChild(node)
},
button: function(opts) {
const node = window.document.createElement("BUTTON")
let currentProps = { ...opts.props }
const set = props => {
currentProps = Object.assign(currentProps, props)
if (currentProps.onClick) {
node.addEventListener("click", () => {
const testText = currentProps.testText || "hello"
currentProps._bb.call(props.onClick, { testText })
})
}
}
this.$destroy = () => opts.target.removeChild(node)
this.$set = set
this._element = node
set(opts.props)
opts.target.appendChild(node)
},
})
const uiFunctions = {
@ -162,4 +205,15 @@ const uiFunctions = {
render()
}
},
with_context: render => {
render({ testKey: "test value" })
},
n_clones_based_on_store: (render, _, state) => {
const n = state.componentCount || 0
for (let i = 0; i < n; i++) {
render({ index: `index_${i}` })
}
},
}

View File

@ -1,20 +1,189 @@
{
"_lib": "./dist/index.js",
"h1": {
"name": "H1",
"description": "An HTML H1 tag",
"_generators": {},
"Body1": {
"name": "Body1",
"description": "Sets the font properties as Roboto Body 1",
"props": {
"text": "string",
"className": "string"
"text": "string"
},
"tags": []
},
"button": {
"name": "Button",
"description": "A button",
"Body2": {
"name": "Body2",
"description": "Sets the font properties as Roboto Body 2",
"props": {
"raised": "bool"
"text": "string"
},
"tags": []
},
"Button": {
"name": "Button",
"description": "A Material Design button with different variations. It renders as an anchor if href is passed to it.",
"props": {
"onClick": "event",
"variant": "string",
"colour": "string",
"size": "string",
"href": "string",
"icon": "string",
"trailingIcon": "bool",
"fullwidth": "bool",
"text": "string",
"disabled": "bool"
},
"tags": []
},
"Caption": {
"name": "Caption",
"description": "Sets the font properties as Roboto Caption",
"props": {
"text": "string"
},
"tags": []
},
"Checkbox": {
"name": "Checkbox",
"description": "A Material Design checkbox. Supports aligning label before or after checkbox.",
"props": {
"onClick": "event",
"id": "string",
"label": "string",
"disabled": "bool",
"alignEnd": "bool",
"indeterminate": "bool",
"checked": "bool"
},
"tags": []
},
"Datatable": {
"name": "Datatable",
"description": "A Material Design component to represent tabular data.",
"props": {},
"tags": []
},
"H1": {
"name": "H1",
"description": "Sets the font properties as Roboto Headline1",
"props": {
"text": "string"
},
"tags": []
},
"H2": {
"name": "H2",
"description": "Sets the font properties as Roboto Headline2",
"props": {
"text": "string"
},
"tags": []
},
"H3": {
"name": "H3",
"description": "Sets the font properties as Roboto Headline3",
"props": {
"text": "string"
},
"tags": []
},
"H4": {
"name": "H4",
"description": "Sets the font properties as Roboto Headline4",
"props": {
"text": "string"
},
"tags": []
},
"H5": {
"name": "H5",
"description": "Sets the font properties as Roboto Headline5",
"props": {
"text": "string"
},
"tags": []
},
"H6": {
"name": "H6",
"description": "Sets the font properties as Roboto Headline6",
"props": {
"text": "string"
},
"tags": []
},
"Label": {
"name": "Label",
"description": "A simple label component that displays its text in the standard Roboto Material Design font",
"props": {
"bold": "bool"
},
"tags": []
},
"Overline": {
"name": "Overline",
"description": "Sets the font properties as Roboto Overline",
"props": {
"text": "string"
},
"tags": []
},
"Radiobutton": {
"name": "Radiobutton",
"description": "A Material Design radiobutton. Supports aligning label before or after radiobutton.",
"props": {
"onClick": "event",
"id": "string",
"label": "string",
"names": "string",
"name": "string",
"checked": "bool",
"disabled": "bool",
"alignEnd": "bool"
},
"tags": []
},
"Sub1": {
"name": "Sub1",
"description": "Sets the font properties as Roboto Subtitle1",
"props": {
"text": "string"
},
"tags": []
},
"Sub2": {
"name": "Sub2",
"description": "Sets the font properties as Roboto Subtitle2",
"props": {
"text": "string"
},
"tags": []
},
"Textfield": {
"name": "Textfield",
"description": "A Material Design textfield with multiple variants. Can also be converted to a text area / multine text field.",
"props": {
"onChange": "event",
"label": "string",
"variant": "string",
"disabled": "bool",
"fullwidth": "bool",
"colour":"string",
"size":"string",
"type": "string",
"required": "bool",
"minLength": "number",
"maxLength": "number",
"helperText": "string",
"errorText": "string",
"placeholder": "string",
"icon": "string",
"trailingIcon": "bool",
"textarea": "bool",
"rows": "number",
"cols": "number",
"validation": "bool",
"persistent": "bool"
},
"tags": []
}
}

View File

@ -41,6 +41,7 @@
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072",
"dependencies": {
"@material/checkbox": "^4.0.0",
"@material/data-table": "4.0.0",
"@material/form-field": "^4.0.0",
"@material/radio": "^4.0.0",
"@material/textfield": "^4.0.0"

View File

@ -1,12 +0,0 @@
<script>
import "@material/button/mdc-button.scss"
export let raised = false
let c = raised ? "mdc-button mdc-button--raised" : "mdc-button"
</script>
<button class={c}>
<div class="mdc-button__ripple" />
<span class="mdc-button__label">Button</span>
</button>

View File

@ -1,7 +1,7 @@
<script>
import { setContext, getContext } from "svelte"
import Icon from "../Icon.svelte"
import ripple from "../Ripple.js"
import Icon from "../Common/Icon.svelte"
import ripple from "../Common/Ripple.js"
import ClassBuilder from "../ClassBuilder.js"
const cb = new ClassBuilder("button", ["primary", "medium"])

View File

@ -1,2 +1,2 @@
import "./_index.scss"
export { default as button } from "./Button.svelte"
export { default as Button } from "./Button.svelte"

View File

@ -1,4 +1,4 @@
import "./_style.scss";
export { default as checkbox } from "./Checkbox.svelte";
export { default as checkboxgroup } from "./CheckboxGroup.svelte";
export { default as Checkbox } from "./Checkbox.svelte";
export { default as Checkboxgroup } from "./CheckboxGroup.svelte";

View File

@ -0,0 +1,67 @@
<script>
import { onMount, setContext } from "svelte"
import { MDCDataTable } from "@material/data-table"
import Row from "./DatatableRow.svelte"
import Cell from "./DatatableCell.svelte"
import { Button } from "../Button"
import ClassBuilder from "../ClassBuilder.js"
const cb = new ClassBuilder("data-table")
setContext("BBMD:data-table:cb", cb)
let datatable = null
let instance = null
onMount(() => {
if (!!datatable) instance = new MDCDataTable(datatable)
return () => {
!!instance && instance.destroy()
instance = null
}
})
</script>
<div bind:this={datatable} class={cb.build()}>
<table class={cb.elem`table`} aria-label="Material Design Datatable">
<thead>
<Row isHeader>
<Cell isHeader>Id</Cell>
<Cell isHeader>First Name</Cell>
<Cell isHeader>Second Name</Cell>
<Cell isHeader>Gender</Cell>
<Cell isHeader>Address</Cell>
<Cell isHeader>Actions</Cell>
</Row>
</thead>
<tbody class={cb.elem`content`}>
<Row>
<Cell>123456</Cell>
<Cell>Conor</Cell>
<Cell>McKeown</Cell>
<Cell>Male</Cell>
<Cell>1 Cool Street</Cell>
<Cell>
<Button
text="Select"
variant="unelevated"
colour="secondary"
size="small" />
</Cell>
</Row>
<Row>
<Cell>789101</Cell>
<Cell>Joe</Cell>
<Cell>Bloggs</Cell>
<Cell>Male</Cell>
<Cell>2 Cool Street</Cell>
<Cell>
<Button
text="Select"
variant="unelevated"
colour="secondary"
size="small" />
</Cell>
</Row>
</tbody>
</table>
</div>

View File

@ -0,0 +1,23 @@
<script>
import { getContext } from "svelte"
export let isHeader = false
export let numeric = false
const cb = getContext("BBMD:data-table:cb")
let elementName = isHeader ? "header-cell" : "cell"
let modifiers = { numeric }
let props = { modifiers }
let cellClass = cb.build({ elementName, props })
</script>
{#if isHeader}
<th class={cellClass} role="columnheader" scope="col">
<slot />
</th>
{:else}
<td class={cellClass}>
<slot />
</td>
{/if}

View File

@ -0,0 +1,26 @@
<script>
import { getContext } from "svelte";
export let onSelect = () => {};
export let isHeader = false;
let row = null;
let selected = false;
const cb = getContext("BBMD:data-table:cb");
let elementName = isHeader ? "header-row" : "row";
let modifiers = {};
$: modifiers = { selected };
$: props = { modifiers };
$: rowClass = cb.build({ elementName, props });
function rowSelected() {
selected = !selected;
onSelect();
}
</script>
<tr bind:this={row} class={rowClass} on:click={rowSelected}>
<slot />
</tr>

View File

@ -0,0 +1 @@
@import "@material/data-table/mdc-data-table";

View File

@ -0,0 +1,2 @@
import "./_style.scss";
export { default as Datatable } from "./Datatable.svelte";

View File

@ -1,8 +0,0 @@
<script>
export let text = ""
export let className = ""
export let _bb
</script>
<h1 class={className}>{text}</h1>

View File

@ -8,7 +8,7 @@
export let id = ""
export let label = ""
export let names = "radios"
export let name = "radios"
export let checked = false
export let disabled = false
export let alignEnd = false
@ -37,7 +37,7 @@
{id}
class={cb.elem`native-control`}
type="radio"
{names}
{name}
{checked}
{disabled}
on:click={onClick} />

View File

@ -1,3 +1,3 @@
import "./_style.scss";
export { default as radiobutton } from "./Radiobutton.svelte";
export { default as radiobuttongroup } from "./RadiobuttonGroup.svelte";
export { default as Radiobutton } from "./Radiobutton.svelte";
export { default as Radiobuttongroup } from "./RadiobuttonGroup.svelte";

View File

@ -3,14 +3,15 @@
import { props } from "./props"
let _bb
const {
h1,
overline,
button,
textfield,
checkbox,
checkboxgroup,
radiobutton,
radiobuttongroup,
H1,
Overline,
Button,
Textfield,
Checkbox,
Checkboxgroup,
Radiobutton,
Radiobuttongroup,
Datatable,
} = props
let currentComponent
@ -22,14 +23,15 @@
props: {
_component: "testcomponents/rootComponent",
_children: [
h1,
overline,
button,
textfield,
checkbox,
checkboxgroup,
radiobutton,
radiobuttongroup,
H1,
Overline,
Button,
Textfield,
Checkbox,
Checkboxgroup,
Radiobutton,
Radiobuttongroup,
Datatable,
],
},
}

View File

@ -1,17 +1,17 @@
export const props = {
h1: {
H1: {
_component: "@budibase/materialdesign-components/H1",
_children: [],
text: "Im a big header",
},
overline: {
Overline: {
_component: "@budibase/materialdesign-components/Overline",
_children: [],
text: "Im a wee overline",
},
button: {
_component: "@budibase/materialdesign-components/button",
Button: {
_component: "@budibase/materialdesign-components/Button",
_children: [],
variant: "raised",
colour: "secondary",
@ -24,13 +24,13 @@ export const props = {
disabled: false,
onClick: () => alert`Button Clicked`,
},
icon: {
_component: "@budibase/materialdesign-components/icon",
Icon: {
_component: "@budibase/materialdesign-components/Icon",
_children: [],
icon: "",
},
textfield: {
_component: "@budibase/materialdesign-components/textfield",
Textfield: {
_component: "@budibase/materialdesign-components/Textfield",
_children: [],
label: "First",
colour: "secondary",
@ -39,15 +39,15 @@ export const props = {
helperText: "Add Surname",
onChange: text => console.log("Text: ", text),
},
checkbox: {
_component: "@budibase/materialdesign-components/checkbox",
Checkbox: {
_component: "@budibase/materialdesign-components/Checkbox",
_children: [],
id: "test-check",
label: "Check Yo Self",
onClick: () => alert`Before ya reck yo'self`,
},
checkboxgroup: {
_component: "@budibase/materialdesign-components/checkboxgroup",
Checkboxgroup: {
_component: "@budibase/materialdesign-components/Checkboxgroup",
_children: [],
label: "Whats your favourite?",
items: [
@ -57,15 +57,15 @@ export const props = {
],
onChange: selectedItems => console.log(selectedItems),
},
radiobutton: {
_component: "@budibase/materialdesign-components/radiobutton",
Radiobutton: {
_component: "@budibase/materialdesign-components/Radiobutton",
_children: [],
label: "Hi radio",
alignEnd: true,
onClick: () => alert`Roger That`,
},
radiobuttongroup: {
_component: "@budibase/materialdesign-components/radiobuttongroup",
Radiobuttongroup: {
_component: "@budibase/materialdesign-components/Radiobuttongroup",
_children: [],
label: "Preferred method of contact: ",
orientation: "column",
@ -75,5 +75,9 @@ export const props = {
{ label: "Social Media", value: 3 },
],
onChange: selected => console.log(selected),
}
},
Datatable: {
_component: "@budibase/materialdesign-components/Datatable",
_children: [],
},
}

View File

@ -1,23 +1,25 @@
import {
H1,
Overline,
button,
icon,
textfield,
checkbox,
checkboxgroup,
radiobutton,
radiobuttongroup,
Button,
Icon,
Textfield,
Checkbox,
Checkboxgroup,
Radiobutton,
Radiobuttongroup,
Datatable,
} from "@BBMD"
export default {
H1,
Overline,
button,
icon,
textfield,
checkbox,
checkboxgroup,
radiobutton,
radiobuttongroup,
Button,
Icon,
Textfield,
Checkbox,
Checkboxgroup,
Radiobutton,
Radiobuttongroup,
Datatable,
}

View File

@ -8,7 +8,7 @@
import FloatingLabel from "../Common/FloatingLabel.svelte"
import HelperText from "./HelperText.svelte"
import CharacterCounter from "./CharacterCounter.svelte"
import Icon from "../Icon.svelte"
import Icon from "../Common/Icon.svelte"
const cb = new ClassBuilder("text-field", ["primary", "medium"])

View File

@ -1,2 +1,2 @@
import "./_index.scss"
export { default as textfield } from "./Textfield.svelte"
export { default as Textfield } from "./Textfield.svelte"

View File

@ -1,13 +1,13 @@
import "./_style.scss";
export { default as Body1 } from "./Body1.svelte";
export { default as Body2 } from "./Body2.svelte";
export { default as Caption } from "./Caption.svelte";
export { default as H1 } from "./H1.svelte";
export { default as H2 } from "./H2.svelte";
export { default as H3 } from "./H3.svelte";
export { default as H4 } from "./H4.svelte";
export { default as H5 } from "./H5.svelte";
export { default as H6 } from "./H6.svelte";
export { default as Body1 } from "./Body1.svelte";
export { default as Body2 } from "./Body2.svelte";
export { default as Overline } from "./Overline.svelte";
export { default as Sub1 } from "./Sub1.svelte";
export { default as Sub2 } from "./Sub2.svelte";
export { default as Caption } from "./Caption.svelte";
export { default as Overline } from "./Overline.svelte";

View File

@ -1,9 +1,11 @@
import "@material/theme/mdc-theme.scss";
export { button } from "./Button"
export { default as icon } from "./Icon.svelte"
export { textfield } from "./Textfield"
export { Button } from "./Button"
export { default as Icon } from "./Common/Icon.svelte"
export { Textfield } from "./Textfield"
export * from "./Typography"
export { checkbox, checkboxgroup } from "./Checkbox"
export { radiobutton, radiobuttongroup } from "./Radiobutton"
export { Checkbox, Checkboxgroup } from "./Checkbox"
export { Radiobutton, Radiobuttongroup } from "./Radiobutton"
export { default as Label } from "./Common/Label.svelte"
export { Datatable } from "./Datatable"

View File

@ -0,0 +1,92 @@
const { readdir, stat, copyFile } = require("fs-extra")
const { constants } = require("fs")
const { join, basename } = require("path")
const serverConfig = require("../../../server/config")()
const packagesFolder = ".."
const jsFile = dir => join(dir, "index.js")
const generatorsFile = dir => join(dir, "generators.js")
const jsMapFile = dir => join(dir, "index.js.map")
const sourceJs = jsFile("dist")
const sourceJsMap = jsMapFile("dist")
const componentsFile = "components.json"
const sourceGenerators = generatorsFile("dist")
const appPackages = join(
packagesFolder,
"server",
serverConfig.latestPackagesFolder
)
const publicMain = appName =>
join(
appPackages,
appName,
"public",
"main",
"lib",
"node_modules",
"@budibase",
"standard-components"
)
const publicUnauth = appName =>
join(
appPackages,
appName,
"public",
"unauthenticated",
"lib",
"node_modules",
"@budibase",
"standard-components"
)
const nodeModulesDist = appName =>
join(
appPackages,
appName,
"node_modules",
"@budibase",
"standard-components",
"dist"
)
const nodeModules = appName =>
join(appPackages, appName, "node_modules", "@budibase", "standard-components")
;(async () => {
const apps = await readdir(appPackages)
const copySource = file => async toDir => {
const dest = join(toDir, basename(file))
try {
await copyFile(file, dest, constants.COPYFILE_FICLONE)
console.log(`COPIED ${file} to ${dest}`)
} catch (e) {
console.log(`COPY FAILED ${file} to ${dest}: ${e}`)
}
}
const copySourceJs = copySource(sourceJs)
const copySourceJsMap = copySource(sourceJsMap)
const copyGenerators = copySource(sourceGenerators)
const copyComponentsJson = copySource(componentsFile)
for (let app of apps) {
if (app === ".data") continue
if (!(await stat(join(appPackages, app))).isDirectory()) continue
await copySourceJs(nodeModulesDist(app))
await copySourceJsMap(nodeModulesDist(app))
await copyGenerators(nodeModulesDist(app))
await copyComponentsJson(nodeModules(app))
await copySourceJs(join(publicMain(app), "dist"))
await copySourceJsMap(join(publicMain(app), "dist"))
await copyGenerators(join(publicMain(app), "dist"))
await copySourceJs(join(publicUnauth(app), "dist"))
await copySourceJsMap(join(publicUnauth(app), "dist"))
await copyGenerators(join(publicUnauth(app), "dist"))
}
})()

View File

@ -14,6 +14,7 @@ const { join, resolve, dirname } = require("path")
const sqrl = require("squirrelly")
const { convertCssToFiles } = require("./convertCssToFiles")
const publicPath = require("./publicPath")
const deleteCodeMeta = require("./deleteCodeMeta")
module.exports = async (config, appname, pageName, pkg) => {
const appPath = appPackageFolder(config, appname)
@ -155,6 +156,8 @@ const savePageJson = async (appPath, pageName, pkg) => {
delete pkg.page._screens
}
deleteCodeMeta(pkg.page.props)
await writeJSON(pageFile, pkg.page, {
spaces: 2,
})

View File

@ -0,0 +1,9 @@
module.exports = props => {
if (props._codeMeta) {
delete props._codeMeta
}
for (let child of props._children || []) {
module.exports(child)
}
}

View File

@ -18,6 +18,7 @@ const buildPage = require("./buildPage")
const getPages = require("./getPages")
const listScreens = require("./listScreens")
const saveBackend = require("./saveBackend")
const deleteCodeMeta = require("./deleteCodeMeta")
module.exports.buildPage = buildPage
module.exports.listScreens = listScreens
@ -58,12 +59,15 @@ module.exports.saveScreen = async (config, appname, pagename, screen) => {
if (screen._css) {
delete screen._css
}
deleteCodeMeta(screen.props)
await writeJSON(compPath, screen, {
encoding: "utf8",
flag: "w",
spaces: 2,
})
return screen;
return screen
}
module.exports.renameScreen = async (

View File

@ -34,7 +34,7 @@
return all
}
$: if (_bb.props._children.length > 0)
$: if(_bb.props._children && _bb.props._children.length > 0)
theButton && _bb.attachChildren(theButton)
$: {
@ -74,7 +74,7 @@
disabled={disabled || false}
on:click={clickHandler}
style={buttonStyles}>
{#if _bb.props._children.length === 0}{contentText}{/if}
{#if !_bb.props._children || _bb.props._children.length === 0}{contentText}{/if}
</button>
<style>