Page Layout & Screen restructure (#87)

* refactoring server for screens & page layout restructure

* Disable API calls, UI placeholders.

* buildPropsHierarchy is gone & screen has url

* Recent changes.

* router

* router

* updated git-ignore to reinclude server/utilities/builder

* modified cli - budi new create new file structure

* Fix uuid import.

* prettier fixes

* prettier fixes

* prettier fixes

* page/screen restructure.. broken tests

* all tests passing at last

* screen routing tests

* Working screen editor and preview.

* Render page previews to the screen.

* Key input lists to ensure new array references when updating styles.

* Ensure the iframe html and body fills the container.

* Save screens via the API.

* Get all save APIs almost working.

* Write pages.json to disk.

* Use correct API endpoint for saving styles.

* Differentiate between saving properties of screens and pages.

* Add required fields to default pages layouts.

* Add _css default property to newly created screens.

* Add default code property.

* page layout / screens - app output

Co-authored-by: pngwn <pnda007@gmail.com>
This commit is contained in:
Michael Shanks 2020-02-10 15:51:09 +00:00 committed by GitHub
parent f59eedc21f
commit 8a80d8801a
98 changed files with 135342 additions and 176761 deletions

File diff suppressed because one or more lines are too long

View File

@ -7,7 +7,6 @@ import {
last, last,
keys, keys,
concat, concat,
keyBy,
find, find,
isEmpty, isEmpty,
values, values,
@ -21,7 +20,6 @@ import {
} from "../common/core" } from "../common/core"
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { defaultPagesObject } from "../userInterface/pagesParsing/defaultPagesObject" import { defaultPagesObject } from "../userInterface/pagesParsing/defaultPagesObject"
import { buildPropsHierarchy } from "../userInterface/pagesParsing/buildPropsHierarchy"
import api from "./api" import api from "./api"
import { import {
isRootComponent, isRootComponent,
@ -29,8 +27,8 @@ import {
} from "../userInterface/pagesParsing/searchComponents" } from "../userInterface/pagesParsing/searchComponents"
import { rename } from "../userInterface/pagesParsing/renameScreen" import { rename } from "../userInterface/pagesParsing/renameScreen"
import { import {
getNewComponentInfo, getNewScreen,
getScreenInfo, createProps,
} from "../userInterface/pagesParsing/createProps" } from "../userInterface/pagesParsing/createProps"
import { import {
loadLibs, loadLibs,
@ -38,8 +36,8 @@ import {
loadGeneratorLibs, loadGeneratorLibs,
} from "./loadComponentLibraries" } from "./loadComponentLibraries"
import { buildCodeForScreens } from "./buildCodeForScreens" import { buildCodeForScreens } from "./buildCodeForScreens"
import { uuid } from "./uuid"
import { generate_screen_css } from "./generate_css" import { generate_screen_css } from "./generate_css"
// import { uuid } from "./uuid"
let appname = "" let appname = ""
@ -54,7 +52,7 @@ export const getStore = () => {
mainUi: {}, mainUi: {},
unauthenticatedUi: {}, unauthenticatedUi: {},
components: [], components: [],
currentFrontEndItem: null, currentPreviewItem: null,
currentComponentInfo: null, currentComponentInfo: null,
currentFrontEndType: "none", currentFrontEndType: "none",
currentPageName: "", currentPageName: "",
@ -113,6 +111,7 @@ export const getStore = () => {
store.setComponentProp = setComponentProp(store) store.setComponentProp = setComponentProp(store)
store.setComponentStyle = setComponentStyle(store) store.setComponentStyle = setComponentStyle(store)
store.setComponentCode = setComponentCode(store) store.setComponentCode = setComponentCode(store)
store.setScreenType = setScreenType(store)
return store return store
} }
@ -134,6 +133,26 @@ const initialise = (store, initial) => async () => {
.get(`/_builder/api/${appname}/appPackage`) .get(`/_builder/api/${appname}/appPackage`)
.then(r => r.json()) .then(r => r.json())
const [main_screens, unauth_screens] = await Promise.all([
api.get(`/_builder/api/${appname}/pages/main/screens`).then(r => r.json()),
api
.get(`/_builder/api/${appname}/pages/unauthenticated/screens`)
.then(r => r.json()),
])
pkg.pages = {
componentLibraries: ["@budibase/standard-components"],
stylesheets: [],
main: {
...pkg.pages.main,
_screens: Object.values(main_screens),
},
unauthenticated: {
...pkg.pages.unauthenticated,
_screens: Object.values(unauth_screens),
},
}
initial.libraries = await loadLibs(appname, pkg) initial.libraries = await loadLibs(appname, pkg)
initial.generatorLibraries = await loadGeneratorLibs(appname, pkg) initial.generatorLibraries = await loadGeneratorLibs(appname, pkg)
initial.loadLibraryUrls = () => loadLibUrls(appname, pkg) initial.loadLibraryUrls = () => loadLibUrls(appname, pkg)
@ -156,20 +175,21 @@ const initialise = (store, initial) => async () => {
} }
store.set(initial) store.set(initial)
return initial return initial
} }
const generatorsArray = generators => const generatorsArray = generators =>
pipe(generators, [keys, filter(k => k !== "_lib"), map(k => generators[k])]) pipe(generators, [keys, filter(k => k !== "_lib"), map(k => generators[k])])
const showSettings = store => show => { const showSettings = store => () => {
store.update(s => { store.update(s => {
s.showSettings = !s.showSettings s.showSettings = !s.showSettings
return s return s
}) })
} }
const useAnalytics = store => useAnalytics => { const useAnalytics = store => () => {
store.update(s => { store.update(s => {
s.useAnalytics = !s.useAnalytics s.useAnalytics = !s.useAnalytics
return s return s
@ -194,7 +214,7 @@ const newRecord = (store, useRoot) => () => {
store.update(s => { store.update(s => {
s.currentNodeIsNew = true s.currentNodeIsNew = true
const shadowHierarchy = createShadowHierarchy(s.hierarchy) const shadowHierarchy = createShadowHierarchy(s.hierarchy)
parent = useRoot const parent = useRoot
? shadowHierarchy ? shadowHierarchy
: getNode(shadowHierarchy, s.currentNode.nodeId) : getNode(shadowHierarchy, s.currentNode.nodeId)
s.errors = [] s.errors = []
@ -223,7 +243,7 @@ const newIndex = (store, useRoot) => () => {
s.currentNodeIsNew = true s.currentNodeIsNew = true
s.errors = [] s.errors = []
const shadowHierarchy = createShadowHierarchy(s.hierarchy) const shadowHierarchy = createShadowHierarchy(s.hierarchy)
parent = useRoot const parent = useRoot
? shadowHierarchy ? shadowHierarchy
: getNode(shadowHierarchy, s.currentNode.nodeId) : getNode(shadowHierarchy, s.currentNode.nodeId)
@ -442,17 +462,21 @@ const saveScreen = store => screen => {
} }
const _saveScreen = (store, s, screen) => { const _saveScreen = (store, s, screen) => {
const screens = pipe(s.screens, [ const screens = pipe(s.pages[s.currentPageName]._screens, [
filter(c => c.name !== screen.name), filter(c => c.name !== screen.name),
concat([screen]), concat([screen]),
]) ])
// console.log('saveScreen', screens, screen)
s.screens = screens s.pages[s.currentPageName]._screens = screens
s.currentFrontEndItem = screen s.screens = s.pages[s.currentPageName]._screens
s.currentComponentInfo = getScreenInfo(s.components, screen) // s.currentPreviewItem = screen
// s.currentComponentInfo = screen.props
api api
.post(`/_builder/api/${s.appname}/screen`, screen) .post(
`/_builder/api/${s.appname}/pages/${s.currentPageName}/screen`,
screen
)
.then(() => savePackage(store, s)) .then(() => savePackage(store, s))
return s return s
@ -460,22 +484,39 @@ const _saveScreen = (store, s, screen) => {
const _save = (appname, screen, store, s) => const _save = (appname, screen, store, s) =>
api api
.post(`/_builder/api/${appname}/screen`, screen) .post(
`/_builder/api/${s.appname}/pages/${s.currentPageName}/screen`,
screen
)
.then(() => savePackage(store, s)) .then(() => savePackage(store, s))
const createScreen = store => (screenName, layoutComponentName) => { const createScreen = store => (screenName, route, layoutComponentName) => {
store.update(s => { store.update(s => {
const newComponentInfo = getNewComponentInfo( const newScreen = getNewScreen(
s.components, s.components,
layoutComponentName, layoutComponentName,
screenName screenName
) )
s.currentFrontEndItem = newComponentInfo.component newScreen.route = route
s.currentComponentInfo = newComponentInfo s.currentPreviewItem = newScreen
s.currentComponentInfo = newScreen.props
s.currentFrontEndType = "screen" s.currentFrontEndType = "screen"
return _saveScreen(store, s, newComponentInfo.component) return _saveScreen(store, s, newScreen)
})
}
const setCurrentScreen = store => screenName => {
store.update(s => {
const screen = getExactComponent(s.screens, screenName)
s.currentPreviewItem = screen
s.currentFrontEndType = "screen"
s.currentComponentInfo = screen.props
setCurrentScreenFunctions(s)
return s
}) })
} }
@ -506,8 +547,8 @@ const deleteScreen = store => name => {
s.components = components s.components = components
s.screens = screens s.screens = screens
if (s.currentFrontEndItem.name === name) { if (s.currentPreviewItem.name === name) {
s.currentFrontEndItem = null s.currentPreviewItem = null
s.currentFrontEndType = "" s.currentFrontEndType = ""
} }
@ -533,8 +574,8 @@ const renameScreen = store => (oldname, newname) => {
s.screens = screens s.screens = screens
s.pages = pages s.pages = pages
if (s.currentFrontEndItem.name === oldname) if (s.currentPreviewItem.name === oldname)
s.currentFrontEndItem.name = newname s.currentPreviewItem.name = newname
const saveAllChanged = async () => { const saveAllChanged = async () => {
for (let screenName of changedScreens) { for (let screenName of changedScreens) {
@ -578,13 +619,6 @@ const addComponentLibrary = store => async lib => {
const success = response.status === 200 const success = response.status === 200
const error =
response.status === 404
? `Could not find library ${lib}`
: success
? ""
: response.statusText
const components = success ? await response.json() : [] const components = success ? await response.json() : []
store.update(s => { store.update(s => {
@ -654,89 +688,50 @@ const refreshComponents = store => async () => {
}) })
} }
const savePackage = (store, s) => { const savePackage = async (store, s) => {
const appDefinition = { const page = s.pages[s.currentPageName]
hierarchy: s.hierarchy,
triggers: s.triggers, await api.post(`/_builder/api/${appname}/pages/${s.currentPageName}`, {
actions: keyBy("name")(s.actions), appDefinition: {
props: { hierarchy: s.hierarchy,
main: buildPropsHierarchy(s.components, s.screens, s.pages.main.appBody), actions: s.actions,
unauthenticated: buildPropsHierarchy( triggers: s.triggers,
s.components,
s.screens,
s.pages.unauthenticated.appBody
),
}, },
uiFunctions: buildCodeForScreens(s.screens),
}
const data = {
appDefinition,
accessLevels: s.accessLevels, accessLevels: s.accessLevels,
pages: s.pages, page: { componentLibraries: s.pages.componentLibraries, ...page },
} uiFunctions: "{'1234':() => 'test return'}",
props: page.props,
return api.post(`/_builder/api/${s.appname}/appPackage`, data) screens: page.screens,
}
const setCurrentScreen = store => screenName => {
store.update(s => {
const screen = getExactComponent(s.screens, screenName)
s.currentFrontEndItem = screen
s.currentFrontEndType = "screen"
s.currentComponentInfo = getScreenInfo(s.components, screen)
setCurrentScreenFunctions(s)
return s
}) })
} }
const setCurrentPage = store => pageName => { const setCurrentPage = store => pageName => {
store.update(s => { store.update(s => {
const current_screens = s.pages[pageName]._screens
s.currentFrontEndType = "page" s.currentFrontEndType = "page"
s.currentPageName = pageName s.currentPageName = pageName
s.screens = Array.isArray(current_screens)
? current_screens
: Object.values(current_screens)
s.currentComponentInfo = s.pages[pageName].props
s.currentPreviewItem = s.pages[pageName]
setCurrentScreenFunctions(s) setCurrentScreenFunctions(s)
return s return s
}) })
} }
const addChildComponent = store => component => { const addChildComponent = store => componentName => {
store.update(s => { store.update(s => {
const newComponent = getNewComponentInfo(s.components, component) const component = s.components.find(c => c.name === componentName)
const newComponent = createProps(component)
let children = s.currentComponentInfo.component s.currentComponentInfo._children = s.currentComponentInfo._children.concat(
? s.currentComponentInfo.component.props._children newComponent.props
: s.currentComponentInfo._children
const component_definition = Object.assign(
cloneDeep(newComponent.fullProps),
{
_component: component,
_styles: { position: {}, layout: {} },
_id: uuid(),
}
) )
if (children) { savePackage(store, s)
if (s.currentComponentInfo.component) {
s.currentComponentInfo.component.props._children = children.concat(
component_definition
)
} else {
s.currentComponentInfo._children = children.concat(component_definition)
}
} else {
if (s.currentComponentInfo.component) {
s.currentComponentInfo.component.props._children = [
component_definition,
]
} else {
s.currentComponentInfo._children = [component_definition]
}
}
_saveScreen(store, s, s.currentFrontEndItem)
_saveScreen(store, s, s.currentFrontEndItem)
return s return s
}) })
@ -753,7 +748,11 @@ const setComponentProp = store => (name, value) => {
store.update(s => { store.update(s => {
const current_component = s.currentComponentInfo const current_component = s.currentComponentInfo
s.currentComponentInfo[name] = value s.currentComponentInfo[name] = value
_saveScreen(store, s, s.currentFrontEndItem)
s.currentFrontEndType === "page"
? savePackage(store, s, s.currentPreviewItem)
: _saveScreen(store, s, s.currentPreviewItem)
s.currentComponentInfo = current_component s.currentComponentInfo = current_component
return s return s
}) })
@ -765,13 +764,14 @@ const setComponentStyle = store => (type, name, value) => {
s.currentComponentInfo._styles = {} s.currentComponentInfo._styles = {}
} }
s.currentComponentInfo._styles[type][name] = value s.currentComponentInfo._styles[type][name] = value
s.currentFrontEndItem._css = generate_screen_css( s.currentPreviewItem._css = generate_screen_css([
s.currentFrontEndItem.props._children s.currentPreviewItem.props,
) ])
// save without messing with the store // save without messing with the store
_save(s.appname, s.currentFrontEndItem, store, s) s.currentFrontEndType === "page"
? savePackage(store, s, s.currentPreviewItem)
: _save(s.appname, s.currentPreviewItem, store, s)
return s return s
}) })
} }
@ -782,7 +782,7 @@ const setComponentCode = store => code => {
setCurrentScreenFunctions(s) setCurrentScreenFunctions(s)
// save without messing with the store // save without messing with the store
_save(s.appname, s.currentFrontEndItem, store, s) _save(s.appname, s.currentPreviewItem, store, s)
return s return s
}) })
@ -790,7 +790,22 @@ const setComponentCode = store => code => {
const setCurrentScreenFunctions = s => { const setCurrentScreenFunctions = s => {
s.currentScreenFunctions = s.currentScreenFunctions =
s.currentFrontEndItem === "screen" s.currentPreviewItem === "screen"
? buildCodeForScreens([s.currentFrontEndItem]) ? buildCodeForScreens([s.currentPreviewItem])
: "({});" : "({});"
} }
const setScreenType = store => type => {
store.update(s => {
s.currentFrontEndType = type
const pageOrScreen =
type === "page"
? s.pages[s.currentPageName]
: s.pages[s.currentPageName]._screens[0]
s.currentComponentInfo = pageOrScreen ? pageOrScreen.props : null
s.currentPreviewItem = pageOrScreen
return s
})
}

View File

@ -1,4 +1,6 @@
<script> <script>
import { onMount } from "svelte"
export let meta = [] export let meta = []
export let size = "" export let size = ""
export let values = [] export let values = []

View File

@ -20,7 +20,6 @@
$: originalName = component.name $: originalName = component.name
$: name = component.name $: name = component.name
$: description = component.description $: description = component.description
$: componentInfo = $store.currentComponentInfo
$: components = $store.components $: components = $store.components
const onPropChanged = store.setComponentProp const onPropChanged = store.setComponentProp
@ -47,7 +46,7 @@
<button <button
class:selected={current_view === 'code'} class:selected={current_view === 'code'}
on:click={() => codeEditor && codeEditor.show()}> on:click={() => codeEditor && codeEditor.show()}>
{#if componentInfo._code && componentInfo._code.trim().length > 0} {#if component._code && component._code.trim().length > 0}
<div class="button-indicator"> <div class="button-indicator">
<CircleIndicator /> <CircleIndicator />
</div> </div>
@ -63,27 +62,26 @@
</button> </button>
</li> </li>
</ul> </ul>
{$store.currentFrontEndType}
{#if !componentInfo.component} <div class="component-props-container">
<div class="component-props-container">
{#if current_view === 'props'} {#if current_view === 'props'}
<PropsView {componentInfo} {components} {onPropChanged} /> <PropsView {component} {components} {onPropChanged} />
{:else if current_view === 'layout'} {:else if current_view === 'layout'}
<LayoutEditor {onStyleChanged} {componentInfo} /> <LayoutEditor {onStyleChanged} {component} />
{:else if current_view === 'events'} {:else if current_view === 'events'}
<EventsEditor {componentInfo} {components} {onPropChanged} /> <EventsEditor {component} {components} {onPropChanged} />
{/if} {/if}
<CodeEditor <CodeEditor
bind:this={codeEditor} bind:this={codeEditor}
code={$store.currentComponentInfo._code} code={component._code}
onCodeChanged={store.setComponentCode} /> onCodeChanged={store.setComponentCode} />
</div> </div>
{:else}
<h1>This is a screen, this will be dealt with later</h1>
{/if}
</div> </div>

View File

@ -7,69 +7,65 @@
import { store } from "../builderStore" import { store } from "../builderStore"
import { ArrowDownIcon } from "../common/Icons/" import { ArrowDownIcon } from "../common/Icons/"
export let components = [] export let screens = []
const joinPath = join("/") const joinPath = join("/")
const normalizedName = name => const normalizedName = name =>
pipe(name, [ pipe(
trimCharsStart("./"), name,
trimCharsStart("~/"), [
trimCharsStart("../"), trimCharsStart("./"),
trimChars(" "), trimCharsStart("~/"),
]) trimCharsStart("../"),
trimChars(" "),
]
)
const lastPartOfName = c => const lastPartOfName = c =>
last(c.name ? c.name.split("/") : c._component.split("/")) last(c.name ? c.name.split("/") : c._component.split("/"))
const isComponentSelected = (current, comp) => const isComponentSelected = (current, comp) => current === comp
current &&
current.component &&
comp.component &&
current.component.name === comp.component.name
const isFolderSelected = (current, folder) => isInSubfolder(current, folder) const isFolderSelected = (current, folder) => isInSubfolder(current, folder)
$: _components = pipe(components, [ $: _screens = pipe(
map(c => ({ component: c, title: lastPartOfName(c) })), screens,
sortBy("title"), [map(c => ({ component: c, title: lastPartOfName(c) })), sortBy("title")]
]) )
function select_component(screen, component) {
store.setCurrentScreen(screen)
store.selectComponent(component)
}
const isScreenSelected = component => const isScreenSelected = component =>
component.component && component.component &&
$store.currentFrontEndItem && $store.currentPreviewItem &&
component.component.name === $store.currentFrontEndItem.name component.component.name === $store.currentPreviewItem.name
$: console.log(_screens)
</script> </script>
<div class="root"> <div class="root">
{#each _components as component} {#each _screens as screen}
<div <div
class="hierarchy-item component" class="hierarchy-item component"
class:selected={isComponentSelected($store.currentComponentInfo, component)} class:selected={$store.currentPreviewItem.name === screen.title}
on:click|stopPropagation={() => store.setCurrentScreen(component.component.name)}> on:click|stopPropagation={() => store.setCurrentScreen(screen.title)}>
<span <span
class="icon" class="icon"
style="transform: rotate({isScreenSelected(component) ? 0 : -90}deg);"> style="transform: rotate({$store.currentPreviewItem.name === screen.title ? 0 : -90}deg);">
{#if component.component.props && component.component.props._children} {#if screen.component.props._children.length}
<ArrowDownIcon /> <ArrowDownIcon />
{/if} {/if}
</span> </span>
<span class="title">{component.title}</span> <span class="title">{screen.title}</span>
</div> </div>
{#if isScreenSelected(component) && component.component.props && component.component.props._children} {#if $store.currentPreviewItem.name === screen.title && screen.component.props._children}
<ComponentsHierarchyChildren <ComponentsHierarchyChildren
components={component.component.props._children} components={screen.component.props._children}
currentComponent={$store.currentComponentInfo} currentComponent={$store.currentComponentInfo}
onSelect={child => select_component(component.component.name, child)} /> onSelect={store.selectComponent} />
{/if} {/if}
{/each} {/each}

View File

@ -1,13 +1,20 @@
<script> <script>
import { last } from "lodash/fp" import { last } from "lodash/fp"
import { pipe } from "../common/core" import { pipe } from "../common/core"
export let components = [] export let components = []
export let currentComponent export let currentComponent
export let onSelect = () => {} export let onSelect = () => {}
export let level = 0 export let level = 0
const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1) const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
const get_name = s => last(s.split("/")) const get_name = s => last(s.split("/"))
const get_capitalised_name = name => pipe(name, [get_name, capitalise])
const get_capitalised_name = name =>
pipe(
name,
[get_name, capitalise]
)
</script> </script>
<ul> <ul>

View File

@ -1,4 +1,5 @@
<script> <script>
import { store } from "../builderStore/"
import ComponentPanel from "./ComponentPanel.svelte" import ComponentPanel from "./ComponentPanel.svelte"
import ComponentsList from "./ComponentsList.svelte" import ComponentsList from "./ComponentsList.svelte"
@ -10,32 +11,36 @@
</script> </script>
<div class="root"> <div class="root">
{#if $store.currentFrontEndType === 'page' || $store.screens.length}
<div class="switcher">
<div class="switcher"> <button
class:selected={selected === 'properties'}
on:click={() => selectTab('properties')}>
Properties
</button>
<button <button
class:selected={selected === 'properties'} class:selected={selected === 'components'}
on:click={() => selectTab('properties')}> on:click={() => selectTab('components')}>
Properties Components
</button> </button>
<button </div>
class:selected={selected === 'components'}
on:click={() => selectTab('components')}>
Components
</button>
</div> <div class="panel">
{#if selected === 'properties'}
<ComponentPanel />
{/if}
<div class="panel"> {#if selected === 'components'}
{#if selected === 'properties'} <ComponentsList />
<ComponentPanel /> {/if}
{/if}
{#if selected === 'components'} </div>
<ComponentsList /> {:else}
{/if} <p>Please create a new screen</p>
</div> {/if}
</div> </div>

View File

@ -2,50 +2,70 @@
import { store } from "../builderStore" import { store } from "../builderStore"
import { map, join } from "lodash/fp" import { map, join } from "lodash/fp"
import { pipe } from "../common/core" import { pipe } from "../common/core"
import { buildPropsHierarchy } from "./pagesParsing/buildPropsHierarchy"
let iframe let iframe
function transform_component(comp) {
const props = comp.props || comp
if (props && props._children && props._children.length) {
props._children = props._children.map(transform_component)
}
return props
}
$: iframe && $: iframe &&
console.log( console.log(
iframe.contentDocument.head.insertAdjacentHTML( iframe.contentDocument.head.insertAdjacentHTML(
"beforeend", "beforeend",
'<style prettier:content=""></style>' `<\style></style>`
) )
) )
$: hasComponent = !!$store.currentFrontEndItem $: hasComponent = !!$store.currentPreviewItem
$: styles = hasComponent ? $store.currentFrontEndItem._css : "" $: styles = hasComponent ? $store.currentPreviewItem._css : ""
$: stylesheetLinks = pipe($store.pages.stylesheets, [ $: stylesheetLinks = pipe(
map(s => `<link rel="stylesheet" href="${s}"/>`), $store.pages.stylesheets,
join("\n"), [map(s => `<link rel="stylesheet" href="${s}"/>`), join("\n")]
]) )
$: appDefinition = { $: appDefinition = {
componentLibraries: $store.loadLibraryUrls(), componentLibraries: $store.loadLibraryUrls(),
props: buildPropsHierarchy( props:
$store.components, $store.currentPreviewItem &&
$store.screens, transform_component($store.currentPreviewItem, true),
$store.currentFrontEndItem
),
hierarchy: $store.hierarchy, hierarchy: $store.hierarchy,
appRootPath: "", appRootPath: "",
} }
</script> </script>
<div class="component-container"> <div class="component-container">
{#if hasComponent} {#if hasComponent && $store.currentPreviewItem}
<iframe <iframe
style="height: 100%; width: 100%" style="height: 100%; width: 100%"
title="componentPreview" title="componentPreview"
bind:this={iframe} bind:this={iframe}
srcdoc={`<html> srcdoc={`<html>
<head>
<head>
${stylesheetLinks} ${stylesheetLinks}
<script prettier:content="CiAgICAgICAgd2luZG93WyIjI0JVRElCQVNFX0FQUERFRklOSVRJT04jIyJdID0gJHtKU09OLnN0cmluZ2lmeShhcHBEZWZpbml0aW9uKX07CiAgICAgICAgd2luZG93WyIjI0JVRElCQVNFX1VJRlVOQ1RJT05TIl0gPSAkeyRzdG9yZS5jdXJyZW50U2NyZWVuRnVuY3Rpb25zfTsKICAgICAgICAKICAgICAgICBpbXBvcnQoJy9fYnVpbGRlci9idWRpYmFzZS1jbGllbnQuZXNtLm1qcycpCiAgICAgICAgLnRoZW4obW9kdWxlID0+IHsKICAgICAgICAgICAgbW9kdWxlLmxvYWRCdWRpYmFzZSh7IHdpbmRvdywgbG9jYWxTdG9yYWdlIH0pOwogICAgICAgIH0pCiAgICA=">{}</script> <style ✂prettier:content✂="CgogICAgICAgIGJvZHkgewogICAgICAgICAgICBib3gtc2l6aW5nOiBib3JkZXItYm94OwogICAgICAgICAgICBwYWRkaW5nOiAyMHB4OwogICAgICAgIH0KICAgICR7c3R5bGVzfQogICAg"></style></head> <style>
<body> ${styles || ''}
</body> body, html {
height: 100%!important;
}
<\/style>
<\script>
window["##BUDIBASE_APPDEFINITION##"] = ${JSON.stringify(appDefinition)};
window["##BUDIBASE_UIFUNCTIONS"] = ${$store.currentScreenFunctions};
import('/_builder/budibase-client.esm.mjs')
.then(module => {
module.loadBudibase({ window, localStorage });
})
<\/script>
</head>
<body>
</body>
</html>`} /> </html>`} />
{/if} {/if}
</div> </div>

View File

@ -9,7 +9,7 @@
import { cloneDeep, join, split, last } from "lodash/fp" import { cloneDeep, join, split, last } from "lodash/fp"
import { assign } from "lodash" import { assign } from "lodash"
$: component = $store.currentFrontEndItem $: component = $store.currentPreviewItem
$: componentInfo = $store.currentComponentInfo $: componentInfo = $store.currentComponentInfo
$: components = $store.components $: components = $store.components

View File

@ -2,7 +2,7 @@
import InputGroup from "../common/Inputs/InputGroup.svelte" import InputGroup from "../common/Inputs/InputGroup.svelte"
export let onStyleChanged = () => {} export let onStyleChanged = () => {}
export let componentInfo export let component
const tbrl = [ const tbrl = [
{ placeholder: "T" }, { placeholder: "T" },
@ -16,8 +16,8 @@
const single = [{ placeholder: "" }] const single = [{ placeholder: "" }]
$: layout = { $: layout = {
...componentInfo._styles.position, ...component._styles.position,
...componentInfo._styles.layout, ...component._styles.layout,
} }
$: layouts = { $: layouts = {
@ -46,7 +46,7 @@
<h4>Positioning</h4> <h4>Positioning</h4>
<div class="layout-pos"> <div class="layout-pos">
{#each Object.entries(layouts) as [key, [name, meta, size]]} {#each Object.entries(layouts) as [key, [name, meta, size]] (component._id + key)}
<div class="grid"> <div class="grid">
<h5>{name}:</h5> <h5>{name}:</h5>
<InputGroup <InputGroup
@ -61,7 +61,7 @@
<h4>Positioning</h4> <h4>Positioning</h4>
<div class="layout-pos"> <div class="layout-pos">
{#each Object.entries(positions) as [key, [name, meta, size]]} {#each Object.entries(positions) as [key, [name, meta, size]] (component._id + key)}
<div class="grid"> <div class="grid">
<h5>{name}:</h5> <h5>{name}:</h5>
<InputGroup <InputGroup
@ -75,7 +75,7 @@
<h4>Spacing</h4> <h4>Spacing</h4>
<div class="layout-spacing"> <div class="layout-spacing">
{#each Object.entries(spacing) as [key, [name, meta, size]]} {#each Object.entries(spacing) as [key, [name, meta, size]] (component._id + key)}
<div class="grid"> <div class="grid">
<h5>{name}:</h5> <h5>{name}:</h5>
<InputGroup <InputGroup
@ -89,7 +89,7 @@
<h4>Z-Index</h4> <h4>Z-Index</h4>
<div class="layout-layer"> <div class="layout-layer">
{#each Object.entries(zindex) as [key, [name, meta, size]]} {#each Object.entries(zindex) as [key, [name, meta, size]] (component._id + key)}
<div class="grid"> <div class="grid">
<h5>{name}:</h5> <h5>{name}:</h5>
<InputGroup <InputGroup

View File

@ -22,13 +22,17 @@
let layoutComponent let layoutComponent
let screens let screens
let name = "" let name = ""
let route = ""
let saveAttempted = false let saveAttempted = false
store.subscribe(s => { store.subscribe(s => {
layoutComponents = pipe(s.components, [ layoutComponents = pipe(
filter(c => c.container), s.components,
map(c => ({ name: c.name, ...splitName(c.name) })), [
]) filter(c => c.container),
map(c => ({ name: c.name, ...splitName(c.name) })),
]
)
layoutComponent = layoutComponent layoutComponent = layoutComponent
? find(c => c.name === layoutComponent.name)(layoutComponents) ? find(c => c.name === layoutComponent.name)(layoutComponents)
@ -45,7 +49,7 @@
if (!isValid) return if (!isValid) return
store.createScreen(name, layoutComponent.name) store.createScreen(name, route, layoutComponent.name)
UIkit.modal(componentSelectorModal).hide() UIkit.modal(componentSelectorModal).hide()
} }
@ -53,8 +57,11 @@
UIkit.modal(componentSelectorModal).hide() UIkit.modal(componentSelectorModal).hide()
} }
const screenNameExists = name => const screenNameExists = name => {
some(s => s.name.toLowerCase() === name.toLowerCase())(screens) return some(s => {
return s.name.toLowerCase() === name.toLowerCase()
})(screens)
}
</script> </script>
<div bind:this={componentSelectorModal} id="new-component-modal" uk-modal> <div bind:this={componentSelectorModal} id="new-component-modal" uk-modal>
@ -73,6 +80,14 @@
class:uk-form-danger={saveAttempted && (name.length === 0 || screenNameExists(name))} class:uk-form-danger={saveAttempted && (name.length === 0 || screenNameExists(name))}
bind:value={name} /> bind:value={name} />
</div> </div>
<label class="uk-form-label">Route (Url)</label>
<div class="uk-form-controls">
<input
class="uk-input uk-form-small"
class:uk-form-danger={saveAttempted && route.length === 0}
bind:value={route} />
</div>
</div> </div>
<div class="uk-margin"> <div class="uk-margin">

View File

@ -0,0 +1,3 @@
<script>
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
</script>

View File

@ -5,25 +5,25 @@
import PropControl from "./PropControl.svelte" import PropControl from "./PropControl.svelte"
import IconButton from "../common/IconButton.svelte" import IconButton from "../common/IconButton.svelte"
export let componentInfo export let component
export let onPropChanged = () => {} export let onPropChanged = () => {}
export let components export let components
let errors = [] let errors = []
let props = {} let props = {}
const props_to_ignore = ["_component", "_children", "_styles", "_code", "_id"] const props_to_ignore = ["_component", "_children", "_styles", "_code", "_id"]
$: propDefs = $: propDefs =
componentInfo && component &&
Object.entries(componentInfo).filter( Object.entries(component).filter(
([name]) => !props_to_ignore.includes(name) ([name]) => !props_to_ignore.includes(name)
) )
function find_type(prop_name) { function find_type(prop_name) {
if (!componentInfo._component) return if (!component._component) return
return components.find(({ name }) => name === componentInfo._component) return components.find(({ name }) => name === component._component).props[
.props[prop_name] prop_name
]
} }
let setProp = (name, value) => { let setProp = (name, value) => {

View File

@ -1,5 +1,6 @@
<script> <script>
import ComponentsHierarchy from "./ComponentsHierarchy.svelte" import ComponentsHierarchy from "./ComponentsHierarchy.svelte"
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
import PagesList from "./PagesList.svelte" import PagesList from "./PagesList.svelte"
import { store } from "../builderStore" import { store } from "../builderStore"
import IconButton from "../common/IconButton.svelte" import IconButton from "../common/IconButton.svelte"
@ -37,29 +38,59 @@
</div> </div>
<div class="components-list-container"> <div class="components-list-container">
<div class="nav-group-header"> <div class="nav-group-header">
<span class="components-nav-header">Screens</span> <span
<div> on:click={() => store.setScreenType('page')}
<button on:click={newComponent}>+</button> class="components-nav-header"
</div> class:active={$store.currentFrontEndType === 'page'}>
Page
</span>
</div> </div>
<div class="nav-items-container"> <div class="nav-items-container">
<ComponentsHierarchy components={$store.screens} /> {#if $store.currentFrontEndType === 'page'}
<ComponentsHierarchyChildren
components={$store.currentPreviewItem.props._children}
currentComponent={$store.currentComponentInfo}
onSelect={store.selectComponent}
level={-2} />
{/if}
</div>
</div>
<div class="components-list-container">
<div class="nav-group-header">
<span
on:click={() => store.setScreenType('screen')}
class="components-nav-header"
class:active={$store.currentFrontEndType === 'screen'}>
Screens
</span>
{#if $store.currentFrontEndType === 'screen'}
<div>
<button on:click={newComponent}>+</button>
</div>
{/if}
</div>
<div class="nav-items-container">
{#if $store.currentFrontEndType === 'screen'}
<ComponentsHierarchy screens={$store.screens} />
{/if}
</div> </div>
</div> </div>
</div> </div>
<div class="preview-pane"> <div class="preview-pane">
{#if $store.currentFrontEndType === 'screen'} <CurrentItemPreview />
<CurrentItemPreview />
{:else if $store.currentFrontEndType === 'page'}
<PageView />
{/if}
</div> </div>
{#if $store.currentFrontEndType === 'screen'} {#if $store.currentFrontEndType === 'screen' || $store.currentFrontEndType === 'page'}
<div class="components-pane"> <div class="components-pane">
<ComponentsPaneSwitcher /> <ComponentsPaneSwitcher />
</div> </div>
@ -152,7 +183,7 @@
margin-right: 5px; margin-right: 5px;
} }
.nav-group-header > span:nth-child(2) { .nav-group-header > span:nth-child(3) {
margin-left: 5px; margin-left: 5px;
vertical-align: bottom; vertical-align: bottom;
grid-column-start: title; grid-column-start: title;
@ -175,4 +206,8 @@
font-weight: 400; font-weight: 400;
color: #999; color: #999;
} }
.active {
color: #333;
}
</style> </style>

View File

@ -1,57 +0,0 @@
import { getComponentInfo, createProps, getInstanceProps } from "./createProps"
export const buildPropsHierarchy = (components, screens, baseComponent) => {
const allComponents = [...components, ...screens]
const buildProps = (componentDefinition, derivedFromProps) => {
const { props } = createProps(componentDefinition, derivedFromProps)
const propsDefinition = componentDefinition.props
props._component = componentDefinition.name
for (let propName in props) {
if (propName === "_component") continue
const propDef = propsDefinition[propName]
if (!propDef) continue
if (propName === "_children") {
const childrenProps = props[propName]
if (!childrenProps || childrenProps.length === 0) {
continue
}
props[propName] = []
for (let child of childrenProps) {
const propComponentInfo = getComponentInfo(
allComponents,
child._component
)
const subComponentInstanceProps = getInstanceProps(
propComponentInfo,
child
)
props[propName].push(
buildProps(
propComponentInfo.rootComponent.name,
propComponentInfo.propsDefinition,
subComponentInstanceProps
)
)
}
}
}
return props
}
if (!baseComponent) return {}
const baseComponentInfo = getComponentInfo(allComponents, baseComponent)
return buildProps(
baseComponentInfo.rootComponent,
baseComponentInfo.fullProps
)
}

View File

@ -1,86 +1,17 @@
import { import { isString, isUndefined } from "lodash/fp"
isString, import { types } from "./types"
isUndefined,
find,
keys,
uniq,
some,
filter,
reduce,
cloneDeep,
includes,
last,
} from "lodash/fp"
import { types, expandComponentDefinition } from "./types"
import { assign } from "lodash" import { assign } from "lodash"
import { pipe } from "../../common/core" import { uuid } from "../../builderStore/uuid"
import { isRootComponent } from "./searchComponents"
import { ensureShardNameIsInShardMap } from "../../../../core/src/indexing/sharding"
export const getInstanceProps = (componentInfo, props) => { export const getNewScreen = (components, rootComponentName, name) => {
const finalProps = cloneDeep(componentInfo.fullProps) const rootComponent = components.find(c => c.name === rootComponentName)
return {
for (let p in props) {
finalProps[p] = props[p]
}
return finalProps
}
export const getNewComponentInfo = (components, rootComponent, name) => {
const component = {
name: name || "", name: name || "",
description: "", description: "",
props: { url: "",
_component: rootComponent, _css: "",
}, uiFunctions: "",
} props: createProps(rootComponent).props,
return getComponentInfo(components, component)
}
export const getScreenInfo = (components, screen) => {
return getComponentInfo(components, screen)
}
export const getComponentInfo = (components, comp) => {
const targetComponent = isString(comp)
? find(c => c.name === comp)(components)
: comp
let component
let subComponent
if (isRootComponent(targetComponent)) {
component = targetComponent
} else {
subComponent = targetComponent
component = find(
c =>
c.name ===
(subComponent.props
? subComponent.props._component
: subComponent._component)
)(components)
}
const subComponentProps = subComponent ? subComponent.props : {}
const p = createProps(component, subComponentProps)
const rootProps = createProps(component)
const unsetProps = pipe(p.props, [
keys,
filter(k => !includes(k)(keys(subComponentProps)) && k !== "_component"),
])
const fullProps = cloneDeep(p.props)
fullProps._component = targetComponent.name
return {
propsDefinition: expandComponentDefinition(component),
rootDefaultProps: rootProps.props,
unsetProps,
fullProps: fullProps,
errors: p.errors,
component: targetComponent,
rootComponent: component,
} }
} }
@ -89,6 +20,9 @@ export const createProps = (componentDefinition, derivedFromProps) => {
const props = { const props = {
_component: componentDefinition.name, _component: componentDefinition.name,
_styles: { position: {}, layout: {} },
_id: uuid(),
_code: "",
} }
const errors = [] const errors = []

View File

@ -1,11 +1,15 @@
export const defaultPagesObject = () => ({ export const defaultPagesObject = () => ({
main: { main: {
_props: {},
_screens: {},
index: { index: {
_component: "./components/indexHtml", _component: "./components/indexHtml",
}, },
appBody: "bbapp.main.json", appBody: "bbapp.main.json",
}, },
unauthenticated: { unauthenticated: {
_props: {},
_screens: {},
index: { index: {
_component: "./components/indexHtml", _component: "./components/indexHtml",
}, },

View File

@ -1,25 +0,0 @@
import { componentsAndScreens } from "./testData"
import { find } from "lodash/fp"
import { buildPropsHierarchy } from "../src/userInterface/pagesParsing/buildPropsHierarchy"
describe("buildPropsHierarchy", () => {
it("should build a complex component children", () => {
const { components, screens } = componentsAndScreens()
const allprops = buildPropsHierarchy(components, screens, "ButtonGroup")
expect(allprops._component).toEqual("budibase-components/div")
const primaryButtonProps = () => ({
_component: "budibase-components/Button",
})
const button1 = primaryButtonProps()
button1.contentText = "Button 1"
expect(allprops._children[0]).toEqual(button1)
const button2 = primaryButtonProps()
button2.contentText = "Button 2"
expect(allprops._children[1]).toEqual(button2)
})
})

View File

@ -22,20 +22,7 @@ describe("component dependencies", () => {
) )
}) })
it("should include component that nests", () => { it("should include components in page apbody", () => {
const { components, screens } = componentsAndScreens()
const result = componentDependencies(
{},
screens,
components,
get([...components, ...screens], "budibase-components/Button")
)
expect(contains(result.dependantComponents, "ButtonGroup")).toBe(true)
})
it("should include components n page apbody", () => {
const { components, screens } = componentsAndScreens() const { components, screens } = componentsAndScreens()
const pages = { const pages = {
main: { main: {

View File

@ -1,6 +1,7 @@
import { createProps } from "../src/userInterface/pagesParsing/createProps" import { createProps } from "../src/userInterface/pagesParsing/createProps"
import { keys, some } from "lodash/fp" import { keys, some } from "lodash/fp"
import { BB_STATE_BINDINGPATH } from "@budibase/client/src/state/isState" import { BB_STATE_BINDINGPATH } from "@budibase/client/src/state/isState"
import { stripStandardProps } from "./testData"
describe("createDefaultProps", () => { describe("createDefaultProps", () => {
const getcomponent = () => ({ const getcomponent = () => ({
@ -16,6 +17,7 @@ describe("createDefaultProps", () => {
expect(errors).toEqual([]) expect(errors).toEqual([])
expect(props.fieldName).toBeDefined() expect(props.fieldName).toBeDefined()
expect(props.fieldName).toBe("something") expect(props.fieldName).toBe("something")
stripStandardProps(props)
expect(keys(props).length).toBe(3) expect(keys(props).length).toBe(3)
}) })
@ -190,11 +192,6 @@ describe("createDefaultProps", () => {
}) })
it("should merge in derived props", () => { it("should merge in derived props", () => {
const propDef = {
fieldName: "string",
fieldLength: { type: "number", default: 500 },
}
const comp = getcomponent() const comp = getcomponent()
comp.props.fieldName = "string" comp.props.fieldName = "string"
comp.props.fieldLength = { type: "number", default: 500 } comp.props.fieldLength = { type: "number", default: 500 }
@ -209,4 +206,13 @@ describe("createDefaultProps", () => {
expect(props.fieldName).toBe("surname") expect(props.fieldName).toBe("surname")
expect(props.fieldLength).toBe(500) expect(props.fieldLength).toBe(500)
}) })
it("should create standard props", () => {
const comp = getcomponent()
comp.props.fieldName = { type: "string", default: 1 }
const { props } = createProps(comp)
expect(props._code).toBeDefined()
expect(props._styles).toBeDefined()
expect(props._code).toBeDefined()
})
}) })

View File

@ -1,91 +0,0 @@
import {
getInstanceProps,
getScreenInfo,
getComponentInfo,
} from "../src/userInterface/pagesParsing/createProps"
import { keys, some, find } from "lodash/fp"
import { componentsAndScreens } from "./testData"
describe("getComponentInfo", () => {
it("should return default props for root component", () => {
const result = getComponentInfo(
componentsAndScreens().components,
"budibase-components/TextBox"
)
expect(result.errors).toEqual([])
expect(result.fullProps).toEqual({
_component: "budibase-components/TextBox",
size: "",
isPassword: false,
placeholder: "",
label: "",
})
})
it("getInstanceProps should set supplied props on top of default props", () => {
const result = getInstanceProps(
getComponentInfo(
componentsAndScreens().components,
"budibase-components/TextBox"
),
{ size: "small" }
)
expect(result).toEqual({
_component: "budibase-components/TextBox",
size: "small",
isPassword: false,
placeholder: "",
label: "",
})
})
})
describe("getScreenInfo", () => {
const getScreen = (screens, name) => find(s => s.name === name)(screens)
it("should return correct props for screen", () => {
const { components, screens } = componentsAndScreens()
const result = getScreenInfo(
components,
getScreen(screens, "common/SmallTextbox")
)
expect(result.errors).toEqual([])
expect(result.fullProps).toEqual({
_component: "common/SmallTextbox",
size: "small",
isPassword: false,
placeholder: "",
label: "",
})
})
it("should return correct props for twice derived component", () => {
const { components, screens } = componentsAndScreens()
const result = getScreenInfo(
components,
getScreen(screens, "common/PasswordBox")
)
expect(result.errors).toEqual([])
expect(result.fullProps).toEqual({
_component: "common/PasswordBox",
size: "small",
isPassword: true,
placeholder: "",
label: "",
})
})
it("should list unset props as those that are only defined in root", () => {
const { components, screens } = componentsAndScreens()
const result = getScreenInfo(
components,
getScreen(screens, "common/PasswordBox")
)
expect(result.unsetProps).toEqual(["placeholder", "label"])
})
})

View File

@ -0,0 +1,30 @@
import { getNewScreen } from "../src/userInterface/pagesParsing/createProps"
import { componentsAndScreens, stripStandardProps } from "./testData"
describe("geNewScreen", () => {
it("should return correct props for screen", () => {
const { components } = componentsAndScreens()
const result = getNewScreen(
components,
"budibase-components/TextBox",
"newscreen"
)
expect(result.props._code).toBeDefined()
expect(result.props._id).toBeDefined()
expect(result.props._styles).toBeDefined()
stripStandardProps(result.props)
const expectedProps = {
_component: "budibase-components/TextBox",
size: "",
isPassword: false,
placeholder: "",
label: "",
}
expect(result.props).toEqual(expectedProps)
expect(result.name).toBe("newscreen")
expect(result.url).toBeDefined()
})
})

View File

@ -64,7 +64,8 @@ export const componentsAndScreens = () => ({
}, },
{ {
name: "ButtonGroup", name: "Screen 1",
route: "",
props: { props: {
_component: "budibase-components/div", _component: "budibase-components/div",
width: 100, width: 100,
@ -99,3 +100,9 @@ export const componentsAndScreens = () => ({
}, },
], ],
}) })
export const stripStandardProps = props => {
delete props._code
delete props._id
delete props._styles
}

View File

@ -0,0 +1,18 @@
{
"title": "Test App",
"favicon": "./_shared/favicon.png",
"stylesheets": [],
"componentLibraries": ["@budibase/standard-components"],
"props" : {
"_component": "@budibase/standard-components/div",
"_children": [],
"_id": 0,
"_styles": {
"layout": {},
"positions": {}
},
"_code": ""
},
"_css": "",
"uiFunctions": ""
}

View File

@ -0,0 +1,18 @@
{
"title": "Test App",
"favicon": "./_shared/favicon.png",
"stylesheets": [],
"componentLibraries": ["@budibase/standard-components"],
"props" : {
"_component": "@budibase/standard-components/div",
"_children": [],
"_id": 1,
"_styles": {
"layout": {},
"positions": {}
},
"_code": ""
},
"_css": "",
"uiFunctions": ""
}

View File

@ -0,0 +1 @@
whats the craic big lawd ?

View File

@ -54,7 +54,8 @@ const createEmptyAppPackage = async opts => {
await remove(join(destinationFolder, ...args, "placeholder")) await remove(join(destinationFolder, ...args, "placeholder"))
} }
await removePlaceholder("components") await removePlaceholder("pages", "main", "screens")
await removePlaceholder("pages", "unauthenticated", "screens")
await removePlaceholder("public", "shared") await removePlaceholder("public", "shared")
await removePlaceholder("public", "main") await removePlaceholder("public", "main")
await removePlaceholder("public", "unauthenticated") await removePlaceholder("public", "unauthenticated")

View File

@ -38,6 +38,7 @@
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"lunr": "^2.3.5", "lunr": "^2.3.5",
"regexparam": "^1.3.0",
"shortid": "^2.2.8", "shortid": "^2.2.8",
"svelte": "^3.9.2" "svelte": "^3.9.2"
}, },

View File

@ -4,29 +4,26 @@ import { getStateOrValue } from "./state/getState"
import { setState, setStateFromBinding } from "./state/setState" import { setState, setStateFromBinding } from "./state/setState"
import { trimSlash } from "./common/trimSlash" import { trimSlash } from "./common/trimSlash"
import { isBound } from "./state/isState" import { isBound } from "./state/isState"
import { _initialiseChildren } from "./render/initialiseChildren" import { initialiseChildren } from "./render/initialiseChildren"
import { createTreeNode } from "./render/renderComponent" import { createTreeNode } from "./render/renderComponent"
import { screenRouter } from "./render/screenRouter"
export const createApp = ( export const createApp = (
document, document,
componentLibraries, componentLibraries,
appDefinition, appDefinition,
user, user,
uiFunctions uiFunctions,
screens
) => { ) => {
const coreApi = createCoreApi(appDefinition, user) const coreApi = createCoreApi(appDefinition, user)
appDefinition.hierarchy = coreApi.templateApi.constructHierarchy( appDefinition.hierarchy = coreApi.templateApi.constructHierarchy(
appDefinition.hierarchy appDefinition.hierarchy
) )
const store = writable({ const pageStore = writable({
_bbuser: user, _bbuser: user,
}) })
let globalState = null
store.subscribe(s => {
globalState = s
})
const relativeUrl = url => const relativeUrl = url =>
appDefinition.appRootPath appDefinition.appRootPath
? appDefinition.appRootPath + "/" + trimSlash(url) ? appDefinition.appRootPath + "/" + trimSlash(url)
@ -55,45 +52,100 @@ export const createApp = (
if (isFunction(event)) event(context) if (isFunction(event)) event(context)
} }
const initialiseChildrenParams = (hydrate, treeNode) => ({ let routeTo
bb, let currentScreenStore
coreApi, let currentScreenUbsubscribe
store, let currentUrl
document,
componentLibraries,
appDefinition,
hydrate,
uiFunctions,
treeNode,
})
const bb = (treeNode, componentProps) => ({ const onScreenSlotRendered = screenSlotNode => {
hydrateChildren: _initialiseChildren( const onScreenSelected = (screen, store, url) => {
initialiseChildrenParams(true, treeNode) const { getInitialiseParams, unsubscribe } = initialiseChildrenParams(
), store
appendChildren: _initialiseChildren( )
initialiseChildrenParams(false, treeNode) const initialiseChildParams = getInitialiseParams(true, screenSlotNode)
), initialiseChildren(initialiseChildParams)(
insertChildren: (props, htmlElement, anchor) => [screen.props],
_initialiseChildren(initialiseChildrenParams(false, treeNode))( screenSlotNode.rootElement
props, )
htmlElement, if (currentScreenUbsubscribe) currentScreenUbsubscribe()
anchor currentScreenUbsubscribe = unsubscribe
), currentScreenStore = store
context: treeNode.context, currentUrl = url
props: componentProps, }
call: safeCallEvent,
setStateFromBinding: (binding, value) =>
setStateFromBinding(store, binding, value),
setState: (path, value) => setState(store, path, value),
getStateOrValue: (prop, currentContext) =>
getStateOrValue(globalState, prop, currentContext),
store,
relativeUrl,
api,
isBound,
parent,
})
return bb(createTreeNode()) routeTo = screenRouter(screens, onScreenSelected)
routeTo(currentUrl || window.location.pathname)
}
const initialiseChildrenParams = store => {
let currentState = null
const unsubscribe = store.subscribe(s => {
currentState = s
})
const getInitialiseParams = (hydrate, treeNode) => ({
bb: getBbClientApi,
coreApi,
store,
document,
componentLibraries,
appDefinition,
hydrate,
uiFunctions,
treeNode,
onScreenSlotRendered,
})
const getBbClientApi = (treeNode, componentProps) => {
return {
hydrateChildren: initialiseChildren(
getInitialiseParams(true, treeNode)
),
appendChildren: initialiseChildren(
getInitialiseParams(false, treeNode)
),
insertChildren: (props, htmlElement, anchor) =>
initialiseChildren(getInitialiseParams(false, treeNode))(
props,
htmlElement,
anchor
),
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 }
}
let rootTreeNode
const initialisePage = (page, target, urlPath) => {
currentUrl = urlPath
rootTreeNode = createTreeNode()
const { getInitialiseParams } = initialiseChildrenParams(pageStore)
const initChildParams = getInitialiseParams(true, rootTreeNode)
initialiseChildren(initChildParams)([page.props], target)
return rootTreeNode
}
return {
initialisePage,
screenStore: () => currentScreenStore,
pageStore: () => pageStore,
routeTo: () => routeTo,
rootNode: () => rootTreeNode,
}
} }

View File

@ -1,15 +1,17 @@
import { createApp } from "./createApp" import { createApp } from "./createApp"
import { trimSlash } from "./common/trimSlash" import { trimSlash } from "./common/trimSlash"
import { builtins, builtinLibName } from "./render/builtinComponents"
export const loadBudibase = async ({ export const loadBudibase = async ({
componentLibraries, componentLibraries,
props, page,
screens,
window, window,
localStorage, localStorage,
uiFunctions, uiFunctions,
}) => { }) => {
const appDefinition = window["##BUDIBASE_APPDEFINITION##"] const appDefinition = window["##BUDIBASE_APPDEFINITION##"]
const uiFunctionsFromWindow = window["##BUDIBASE_APPDEFINITION##"] const uiFunctionsFromWindow = window["##BUDIBASE_UIFUNCTIONS##"]
uiFunctions = uiFunctionsFromWindow || uiFunctions uiFunctions = uiFunctionsFromWindow || uiFunctions
const userFromStorage = localStorage.getItem("budibase:user") const userFromStorage = localStorage.getItem("budibase:user")
@ -23,11 +25,13 @@ export const loadBudibase = async ({
temp: false, temp: false,
} }
const rootPath =
appDefinition.appRootPath === ""
? ""
: "/" + trimSlash(appDefinition.appRootPath)
if (!componentLibraries) { if (!componentLibraries) {
const rootPath =
appDefinition.appRootPath === ""
? ""
: "/" + trimSlash(appDefinition.appRootPath)
const componentLibraryUrl = lib => rootPath + "/" + trimSlash(lib) const componentLibraryUrl = lib => rootPath + "/" + trimSlash(lib)
componentLibraries = {} componentLibraries = {}
@ -38,20 +42,36 @@ export const loadBudibase = async ({
} }
} }
if (!props) { componentLibraries[builtinLibName] = builtins(window)
props = appDefinition.props
if (!page) {
page = appDefinition.page
} }
const app = createApp( if (!screens) {
screens = appDefinition.screens
}
const { initialisePage, screenStore, pageStore, routeTo, rootNode } = createApp(
window.document, window.document,
componentLibraries, componentLibraries,
appDefinition, appDefinition,
user, user,
uiFunctions || {} uiFunctions || {},
screens
) )
app.hydrateChildren([props], window.document.body)
return app const route = window.location
? window.location.pathname.replace(rootPath, "")
: "";
return {
rootNode: initialisePage(page, window.document.body, route),
screenStore,
pageStore,
routeTo,
rootNode
}
} }
if (window) { if (window) {

View File

@ -0,0 +1,10 @@
import { screenSlotComponent } from "./screenSlotComponent"
export const builtinLibName = "##builtin"
export const isScreenSlot = componentName =>
componentName === "##builtin/screenslot"
export const builtins = window => ({
screenslot: screenSlotComponent(window),
})

View File

@ -2,8 +2,9 @@ import { setupBinding } from "../state/stateBinding"
import { split, last } from "lodash/fp" import { split, last } from "lodash/fp"
import { $ } from "../core/common" import { $ } from "../core/common"
import { renderComponent } from "./renderComponent" import { renderComponent } from "./renderComponent"
import { isScreenSlot } from "./builtinComponents"
export const _initialiseChildren = initialiseOpts => ( export const initialiseChildren = initialiseOpts => (
childrenProps, childrenProps,
htmlElement, htmlElement,
anchor = null anchor = null
@ -16,13 +17,12 @@ export const _initialiseChildren = initialiseOpts => (
componentLibraries, componentLibraries,
treeNode, treeNode,
appDefinition, appDefinition,
document,
hydrate, hydrate,
onScreenSlotRendered,
} = initialiseOpts } = initialiseOpts
for (let childNode of treeNode.children) { for (let childNode of treeNode.children) {
if (childNode.unsubscribe) childNode.unsubscribe() childNode.destroy()
if (childNode.component) childNode.component.$destroy()
} }
if (hydrate) { if (hydrate) {
@ -59,6 +59,15 @@ export const _initialiseChildren = initialiseOpts => (
bb, bb,
}) })
if (
onScreenSlotRendered &&
isScreenSlot(childProps._component) &&
renderedComponentsThisIteration.length > 0
) {
// assuming there is only ever one screen slot
onScreenSlotRendered(renderedComponentsThisIteration[0])
}
for (let comp of renderedComponentsThisIteration) { for (let comp of renderedComponentsThisIteration) {
comp.unsubscribe = bind(comp.component) comp.unsubscribe = bind(comp.component)
renderedComponents.push(comp) renderedComponents.push(comp)

View File

@ -61,4 +61,16 @@ export const createTreeNode = () => ({
children: [], children: [],
component: null, component: null,
unsubscribe: () => {}, unsubscribe: () => {},
get destroy() {
const node = this
return () => {
if (node.unsubscribe) node.unsubscribe()
if (node.component && node.component.$destroy) node.component.$destroy()
if (node.children) {
for (let child of node.children) {
child.destroy()
}
}
}
},
}) })

View File

@ -0,0 +1,73 @@
import regexparam from "regexparam"
import { writable } from "svelte/store"
export const screenRouter = (screens, onScreenSelected) => {
const routes = screens.map(s => s.route)
let fallback = routes.findIndex(([p]) => p === "*")
if (fallback < 0) fallback = 0
let current
function route(url) {
const _url = url.state || url
current = routes.findIndex(
p => p !== "*" && new RegExp("^" + p + "$").test(_url)
)
const params = {}
if (current === -1) {
routes.forEach(([p], i) => {
const pm = regexparam(p)
const matches = pm.pattern.exec(_url)
if (!matches) return
let j = 0
while (j < pm.keys.length) {
params[pm.keys[j]] = matches[++j] || null
}
current = i
})
}
const storeInitial = {}
storeInitial["##routeParams"]
const store = writable(storeInitial)
if (current !== -1) {
onScreenSelected(screens[current], store, _url)
} else if (fallback) {
onScreenSelected(screens[fallback], store, _url)
}
!url.state && history.pushState(_url, null, _url)
}
function click(e) {
const x = e.target.closest("a")
const y = x && x.getAttribute("href")
if (
e.ctrlKey ||
e.metaKey ||
e.altKey ||
e.shiftKey ||
e.button ||
e.defaultPrevented
)
return
if (!y || x.target || x.host !== location.host) return
e.preventDefault()
route(y)
}
addEventListener("popstate", route)
addEventListener("pushstate", route)
addEventListener("click", click)
return route
}

View File

@ -0,0 +1,14 @@
export const screenSlotComponent = window => {
return function(opts) {
const node = window.document.createElement("DIV")
const $set = props => {
props._bb.hydrateChildren(props._children, node)
}
const $destroy = () => {
if (opts.target && node) opts.target.removeChild(node)
}
this.$set = $set
this.$destroy = $destroy
opts.target.appendChild(node)
}
}

View File

@ -1,61 +1,67 @@
import { load } from "./testAppDef" import { load, makePage, makeScreen } from "./testAppDef"
describe("initialiseApp", () => { describe("initialiseApp (binding)", () => {
it("should populate root element prop from store value", async () => { it("should populate root element prop from store value", async () => {
const { dom } = await load({ const { dom } = await load(
_component: "testlib/div", makePage({
className: { _component: "testlib/div",
"##bbstate": "divClassName", className: {
"##bbsource": "store", "##bbstate": "divClassName",
"##bbstatefallback": "default", "##bbsource": "store",
}, "##bbstatefallback": "default",
}) },
})
)
const rootDiv = dom.window.document.body.children[0] const rootDiv = dom.window.document.body.children[0]
expect(rootDiv.className).toBe("default") expect(rootDiv.className.includes("default")).toBe(true)
}) })
it("should update root element from store", async () => { it("should update root element from store", async () => {
const { dom, app } = await load({ const { dom, app } = await load(
_component: "testlib/div", makePage({
className: { _component: "testlib/div",
"##bbstate": "divClassName", className: {
"##bbsource": "store", "##bbstate": "divClassName",
"##bbstatefallback": "default", "##bbsource": "store",
}, "##bbstatefallback": "default",
}) },
})
)
app.store.update(s => { app.pageStore().update(s => {
s.divClassName = "newvalue" s.divClassName = "newvalue"
return s return s
}) })
const rootDiv = dom.window.document.body.children[0] const rootDiv = dom.window.document.body.children[0]
expect(rootDiv.className).toBe("newvalue") expect(rootDiv.className.includes("newvalue")).toBe(true)
}) })
it("should populate child component with store value", async () => { it("should populate child component with store value", async () => {
const { dom } = await load({ const { dom } = await load(
_component: "testlib/div", makePage({
_children: [ _component: "testlib/div",
{ _children: [
_component: "testlib/h1", {
text: { _component: "testlib/h1",
"##bbstate": "headerOneText", text: {
"##bbsource": "store", "##bbstate": "headerOneText",
"##bbstatefallback": "header one", "##bbsource": "store",
"##bbstatefallback": "header one",
},
}, },
}, {
{ _component: "testlib/h1",
_component: "testlib/h1", text: {
text: { "##bbstate": "headerTwoText",
"##bbstate": "headerTwoText", "##bbsource": "store",
"##bbsource": "store", "##bbstatefallback": "header two",
"##bbstatefallback": "header two", },
}, },
}, ],
], })
}) )
const rootDiv = dom.window.document.body.children[0] const rootDiv = dom.window.document.body.children[0]
@ -67,29 +73,31 @@ describe("initialiseApp", () => {
}) })
it("should populate child component with store value", async () => { it("should populate child component with store value", async () => {
const { dom, app } = await load({ const { dom, app } = await load(
_component: "testlib/div", makePage({
_children: [ _component: "testlib/div",
{ _children: [
_component: "testlib/h1", {
text: { _component: "testlib/h1",
"##bbstate": "headerOneText", text: {
"##bbsource": "store", "##bbstate": "headerOneText",
"##bbstatefallback": "header one", "##bbsource": "store",
"##bbstatefallback": "header one",
},
}, },
}, {
{ _component: "testlib/h1",
_component: "testlib/h1", text: {
text: { "##bbstate": "headerTwoText",
"##bbstate": "headerTwoText", "##bbsource": "store",
"##bbsource": "store", "##bbstatefallback": "header two",
"##bbstatefallback": "header two", },
}, },
}, ],
], })
}) )
app.store.update(s => { app.pageStore().update(s => {
s.headerOneText = "header 1 - new val" s.headerOneText = "header 1 - new val"
s.headerTwoText = "header 2 - new val" s.headerTwoText = "header 2 - new val"
return s return s
@ -103,4 +111,62 @@ describe("initialiseApp", () => {
expect(rootDiv.children[1].tagName).toBe("H1") expect(rootDiv.children[1].tagName).toBe("H1")
expect(rootDiv.children[1].innerText).toBe("header 2 - new val") expect(rootDiv.children[1].innerText).toBe("header 2 - new val")
}) })
it("should populate screen child with store value", async () => {
const { dom, app } = await load(
makePage({
_component: "testlib/div",
_children: [
{
_component: "##builtin/screenslot",
text: "header one",
},
],
}),
[
makeScreen("/", {
_component: "testlib/div",
className: "screen-class",
_children: [
{
_component: "testlib/h1",
text: {
"##bbstate": "headerOneText",
"##bbsource": "store",
"##bbstatefallback": "header one",
},
},
{
_component: "testlib/h1",
text: {
"##bbstate": "headerTwoText",
"##bbsource": "store",
"##bbstatefallback": "header two",
},
},
],
}),
]
)
app.screenStore().update(s => {
s.headerOneText = "header 1 - new val"
s.headerTwoText = "header 2 - new val"
return s
})
const rootDiv = dom.window.document.body.children[0]
expect(rootDiv.children.length).toBe(1)
const screenRoot = rootDiv.children[0]
expect(screenRoot.children.length).toBe(1)
expect(screenRoot.children[0].children.length).toBe(2)
expect(screenRoot.children[0].children[0].innerText).toBe(
"header 1 - new val"
)
expect(screenRoot.children[0].children[1].innerText).toBe(
"header 2 - new val"
)
})
}) })

View File

@ -1,66 +1,74 @@
import { load } from "./testAppDef" import { load, makePage } from "./testAppDef"
describe("controlFlow", () => { describe("controlFlow", () => {
it("should display simple div, with always true render function", async () => { it("should display simple div, with always true render function", async () => {
const { dom } = await load({ const { dom } = await load(
_component: "testlib/div", makePage({
className: "my-test-class", _component: "testlib/div",
_id: "always_render", className: "my-test-class",
}) _id: "always_render",
})
)
expect(dom.window.document.body.children.length).toBe(1) expect(dom.window.document.body.children.length).toBe(1)
const child = dom.window.document.body.children[0] const child = dom.window.document.body.children[0]
expect(child.className).toBe("my-test-class") expect(child.className.includes("my-test-class")).toBeTruthy()
}) })
it("should not display div, with always false render function", async () => { it("should not display div, with always false render function", async () => {
const { dom } = await load({ const { dom } = await load(
_component: "testlib/div", makePage({
className: "my-test-class", _component: "testlib/div",
_id: "never_render", className: "my-test-class",
}) _id: "never_render",
})
)
expect(dom.window.document.body.children.length).toBe(0) expect(dom.window.document.body.children.length).toBe(0)
}) })
it("should display 3 divs in a looped render function", async () => { it("should display 3 divs in a looped render function", async () => {
const { dom } = await load({ const { dom } = await load(
_component: "testlib/div", makePage({
className: "my-test-class", _component: "testlib/div",
_id: "three_clones", className: "my-test-class",
}) _id: "three_clones",
})
)
expect(dom.window.document.body.children.length).toBe(3) expect(dom.window.document.body.children.length).toBe(3)
const child0 = dom.window.document.body.children[0] const child0 = dom.window.document.body.children[0]
expect(child0.className).toBe("my-test-class") expect(child0.className.includes("my-test-class")).toBeTruthy()
const child1 = dom.window.document.body.children[1] const child1 = dom.window.document.body.children[1]
expect(child1.className).toBe("my-test-class") expect(child1.className.includes("my-test-class")).toBeTruthy()
const child2 = dom.window.document.body.children[2] const child2 = dom.window.document.body.children[2]
expect(child2.className).toBe("my-test-class") expect(child2.className.includes("my-test-class")).toBeTruthy()
}) })
it("should display 3 div, in a looped render, as children", async () => { it("should display 3 div, in a looped render, as children", async () => {
const { dom } = await load({ const { dom } = await load(
_component: "testlib/div", makePage({
_children: [ _component: "testlib/div",
{ _children: [
_component: "testlib/div", {
className: "my-test-class", _component: "testlib/div",
_id: "three_clones", className: "my-test-class",
}, _id: "three_clones",
], },
}) ],
})
)
expect(dom.window.document.body.children.length).toBe(1) expect(dom.window.document.body.children.length).toBe(1)
const rootDiv = dom.window.document.body.children[0] const rootDiv = dom.window.document.body.children[0]
expect(rootDiv.children.length).toBe(3) expect(rootDiv.children.length).toBe(3)
expect(rootDiv.children[0].className).toBe("my-test-class") expect(rootDiv.children[0].className.includes("my-test-class")).toBeTruthy()
expect(rootDiv.children[1].className).toBe("my-test-class") expect(rootDiv.children[1].className.includes("my-test-class")).toBeTruthy()
expect(rootDiv.children[2].className).toBe("my-test-class") expect(rootDiv.children[2].className.includes("my-test-class")).toBeTruthy()
}) })
}) })

View File

@ -1,31 +1,35 @@
import { load } from "./testAppDef" import { load, makePage, makeScreen } from "./testAppDef"
describe("initialiseApp", () => { describe("initialiseApp", () => {
it("should populate simple div with initial props", async () => { it("should populate simple div with initial props", async () => {
const { dom } = await load({ const { dom } = await load(
_component: "testlib/div", makePage({
className: "my-test-class", _component: "testlib/div",
}) className: "my-test-class",
})
)
expect(dom.window.document.body.children.length).toBe(1) expect(dom.window.document.body.children.length).toBe(1)
const child = dom.window.document.body.children[0] const child = dom.window.document.body.children[0]
expect(child.className).toBe("my-test-class") expect(child.className.includes("my-test-class")).toBeTruthy()
}) })
it("should populate child component with props", async () => { it("should populate child component with props", async () => {
const { dom } = await load({ const { dom } = await load(
_component: "testlib/div", makePage({
_children: [ _component: "testlib/div",
{ _children: [
_component: "testlib/h1", {
text: "header one", _component: "testlib/h1",
}, text: "header one",
{ },
_component: "testlib/h1", {
text: "header two", _component: "testlib/h1",
}, text: "header two",
], },
}) ],
})
)
const rootDiv = dom.window.document.body.children[0] const rootDiv = dom.window.document.body.children[0]
@ -37,20 +41,22 @@ describe("initialiseApp", () => {
}) })
it("should append children when told to do so", async () => { it("should append children when told to do so", async () => {
const { dom } = await load({ const { dom } = await load(
_component: "testlib/div", makePage({
_children: [ _component: "testlib/div",
{ _children: [
_component: "testlib/h1", {
text: "header one", _component: "testlib/h1",
}, text: "header one",
{ },
_component: "testlib/h1", {
text: "header two", _component: "testlib/h1",
}, text: "header two",
], },
append: true, ],
}) append: true,
})
)
const rootDiv = dom.window.document.body.children[0] const rootDiv = dom.window.document.body.children[0]
@ -62,4 +68,71 @@ describe("initialiseApp", () => {
expect(rootDiv.children[2].tagName).toBe("H1") expect(rootDiv.children[2].tagName).toBe("H1")
expect(rootDiv.children[2].innerText).toBe("header two") expect(rootDiv.children[2].innerText).toBe("header two")
}) })
it("should populate page with correct screen", async () => {
const { dom } = await load(
makePage({
_component: "testlib/div",
_children: [
{
_component: "##builtin/screenslot",
},
],
}),
[
makeScreen("/", {
_component: "testlib/div",
className: "screen-class",
}),
]
)
const rootDiv = dom.window.document.body.children[0]
expect(rootDiv.children.length).toBe(1)
expect(rootDiv.children[0].children.length).toBe(1)
expect(
rootDiv.children[0].children[0].className.includes("screen-class")
).toBeTruthy()
})
it("should populate screen with children", async () => {
const { dom } = await load(
makePage({
_component: "testlib/div",
_children: [
{
_component: "##builtin/screenslot",
text: "header one",
},
],
}),
[
makeScreen("/", {
_component: "testlib/div",
className: "screen-class",
_children: [
{
_component: "testlib/h1",
text: "header one",
},
{
_component: "testlib/h1",
text: "header two",
},
],
}),
]
)
const rootDiv = dom.window.document.body.children[0]
expect(rootDiv.children.length).toBe(1)
const screenRoot = rootDiv.children[0]
expect(screenRoot.children.length).toBe(1)
expect(screenRoot.children[0].children.length).toBe(2)
expect(screenRoot.children[0].children[0].innerText).toBe("header one")
expect(screenRoot.children[0].children[1].innerText).toBe("header two")
})
}) })

View File

@ -0,0 +1,137 @@
import { load, makePage, makeScreen, walkComponentTree } from "./testAppDef"
import { isScreenSlot } from "../src/render/builtinComponents"
describe("screenRouting", () => {
it("should load correct screen, for initial URL", async () => {
const { page, screens } = pageWith3Screens()
const { dom } = await load(page, screens, "/screen2")
const rootDiv = dom.window.document.body.children[0]
expect(rootDiv.children.length).toBe(1)
const screenRoot = rootDiv.children[0]
expect(screenRoot.children.length).toBe(1)
expect(screenRoot.children[0].children.length).toBe(1)
expect(screenRoot.children[0].children[0].innerText).toBe("screen 2")
})
it("should be able to route to the correct screen", async () => {
const { page, screens } = pageWith3Screens()
const { dom, app } = await load(page, screens, "/screen2")
app.routeTo()("/screen3")
const rootDiv = dom.window.document.body.children[0]
expect(rootDiv.children.length).toBe(1)
const screenRoot = rootDiv.children[0]
expect(screenRoot.children.length).toBe(1)
expect(screenRoot.children[0].children.length).toBe(1)
expect(screenRoot.children[0].children[0].innerText).toBe("screen 3")
})
it("should destroy and unsubscribe all components on a screen whe screen is changed", async () => {
const { page, screens } = pageWith3Screens()
const { app } = await load(page, screens, "/screen2")
const nodes = createTrackerNodes(app)
app.routeTo()("/screen3")
expect(nodes.length > 0).toBe(true)
expect(
nodes.some(n => n.isDestroyed === false && isUnderScreenSlot(n.node))
).toBe(false)
expect(
nodes.some(n => n.isUnsubscribed === false && isUnderScreenSlot(n.node))
).toBe(false)
})
it("should not destroy and unsubscribe page and screenslot components when screen is changed", async () => {
const { page, screens } = pageWith3Screens()
const { app } = await load(page, screens, "/screen2")
const nodes = createTrackerNodes(app)
app.routeTo()("/screen3")
expect(nodes.length > 0).toBe(true)
expect(
nodes.some(n => n.isDestroyed === true && !isUnderScreenSlot(n.node))
).toBe(false)
})
})
const createTrackerNodes = app => {
const nodes = []
walkComponentTree(app.rootNode(), n => {
if (!n.component) return
const tracker = { node: n, isDestroyed: false, isUnsubscribed: false }
const _destroy = n.component.$destroy
n.component.$destroy = () => {
_destroy()
tracker.isDestroyed = true
}
const _unsubscribe = n.unsubscribe
if (!_unsubscribe) {
tracker.isUnsubscribed = undefined
} else {
n.unsubscribe = () => {
_unsubscribe()
tracker.isUnsubscribed = true
}
}
nodes.push(tracker)
})
return nodes
}
const isUnderScreenSlot = node =>
node.parentNode &&
(isScreenSlot(node.parentNode.props._component) ||
isUnderScreenSlot(node.parentNode))
const pageWith3Screens = () => ({
page: makePage({
_component: "testlib/div",
_children: [
{
_component: "##builtin/screenslot",
text: "header one",
},
],
}),
screens: [
makeScreen("/", {
_component: "testlib/div",
className: "screen-class",
_children: [
{
_component: "testlib/h1",
text: "screen 1",
},
],
}),
makeScreen("/screen2", {
_component: "testlib/div",
className: "screen-class",
_children: [
{
_component: "testlib/h1",
text: "screen 2",
},
],
}),
makeScreen("/screen3", {
_component: "testlib/div",
className: "screen-class",
_children: [
{
_component: "testlib/h1",
text: "screen 3",
},
],
}),
],
})

View File

@ -1,20 +1,41 @@
import { JSDOM } from "jsdom" import { JSDOM } from "jsdom"
import { loadBudibase } from "../src/index" import { loadBudibase } from "../src/index"
export const load = async props => { export const load = async (page, screens = [], url = "/") => {
const dom = new JSDOM(`<!DOCTYPE html><html><body></body><html>`) const dom = new JSDOM("<!DOCTYPE html><html><body></body><html>", {
autoAssignIds(props) url: `http://test${url}`,
setAppDef(dom.window, props) })
autoAssignIds(page.props)
for (let s of screens) {
autoAssignIds(s.props)
}
setAppDef(dom.window, page, screens)
const app = await loadBudibase({ const app = await loadBudibase({
componentLibraries: allLibs(dom.window), componentLibraries: allLibs(dom.window),
window: dom.window, window: dom.window,
localStorage: createLocalStorage(), localStorage: createLocalStorage(),
props, page,
screens,
uiFunctions, uiFunctions,
}) })
return { dom, app } return { dom, app }
} }
export const makePage = props => ({ props })
export const makeScreen = (route, props) => ({ props, route })
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) {
walkComponentTree(child, action)
}
}
}
// this happens for real by the builder... // this happens for real by the builder...
// ..this only assigns _ids when missing // ..this only assigns _ids when missing
const autoAssignIds = (props, count = 0) => { const autoAssignIds = (props, count = 0) => {
@ -29,10 +50,11 @@ const autoAssignIds = (props, count = 0) => {
} }
} }
const setAppDef = (window, props) => { const setAppDef = (window, page, screens) => {
window["##BUDIBASE_APPDEFINITION##"] = { window["##BUDIBASE_APPDEFINITION##"] = {
componentLibraries: [], componentLibraries: [],
props, page,
screens,
hierarchy: {}, hierarchy: {},
appRootPath: "", appRootPath: "",
} }
@ -79,6 +101,8 @@ const maketestlib = window => ({
} }
} }
this.$destroy = () => opts.target.removeChild(node)
this.$set = set this.$set = set
this._element = node this._element = node
set(opts.props) set(opts.props)
@ -97,6 +121,8 @@ const maketestlib = window => ({
} }
} }
this.$destroy = () => opts.target.removeChild(node)
this.$set = set this.$set = set
this._element = node this._element = node
set(opts.props) set(opts.props)
@ -105,13 +131,13 @@ const maketestlib = window => ({
}) })
const uiFunctions = { const uiFunctions = {
never_render: (render, parentContext) => {}, never_render: () => {},
always_render: (render, parentContext) => { always_render: render => {
render() render()
}, },
three_clones: (render, parentContext) => { three_clones: render => {
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
render() render()
} }

View File

@ -120,7 +120,7 @@ export default {
// we'll extract any component CSS out into // we'll extract any component CSS out into
// a separate file — better for performance // a separate file — better for performance
css: css => { css: css => {
css.write("public/build/bundle.css"); css.write("public/build/bundle.css")
}, },
hydratable: true, hydratable: true,

View File

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

View File

@ -1,70 +1,38 @@
export default class ClassBuilder { export default class ClassBuilder {
constructor(block, defaultIgnoreList) { constructor(block, customDefaults) {
this.block = `mdc-${block}`; this.block = `mdc-${block}`
this.defaultIgnoreList = defaultIgnoreList; //will be ignored when building custom classes this.customDefaults = customDefaults //will be ignored when building custom classes
} }
/* // classParams: {modifiers:[] (mdc), custom:[] (bbmd), extra:[] (any)}
handles both blocks and elementss (BEM MD Notation) blocks(classParams) {
params = {elementName: string, props: {modifiers{}, customs:{}, extras: []}} let base = this.block
All are optional if (classParams == undefined) return base
*/ return this.buildClass(base, classParams)
build(params) {
if (!params) return this.block; //return block if nothing passed
const { props, elementName } = params;
let base = !!elementName ? `${this.block}__${elementName}` : this.block;
if (!props) return base;
return this._handleProps(base, props);
} }
//Easily grab a simple element class //elementName: string, classParams: {}
elem(elementName) { elements(elementName, classParams) {
return this.build({ elementName }); let base = `${this.block}__${elementName}`
if (classParams == undefined) return base
return this.buildClass(base, classParams)
} }
//use if a different base is needed than whats defined by this.block buildClass(base, classParams) {
debase(base, elementProps) { let cls = base
if (!elementProps) return base; const { modifiers, customs, extras } = classParams
return this._handleProps(base, elementProps); if (modifiers) cls += modifiers.map(m => ` ${base}--${m}`).join(" ")
} if (customs)
cls += Object.entries(customs)
//proxies bindProps and checks for which elementProps exist before binding .map(([property, value]) => {
_handleProps(base, elementProps) { //disregard falsy and values set by customDefaults constructor param
let cls = base; if (!!value && !this.customDefaults.includes(value)) {
const { modifiers, customs, extras } = elementProps; //custom scss name convention = bbmd-[block | element]--[property]-[value]
if (!!modifiers) cls += this._bindProps(modifiers, base); return ` bbmd-${base}--${property}-${value}`
if (!!customs) cls += this._bindProps(customs, base, true);
if (!!extras) cls += ` ${extras.join(" ")}`;
return cls.trim();
}
/*
Handles both modifiers and customs. Use property, value or both depending
on whether it is passsed props for custom or modifiers
if custom uses the following convention for scss mixins:
bbmd-{this.block}--{property}-{value}
bbmd-mdc-button--size-large
*/
_bindProps(elementProps, base, isCustom = false) {
return Object.entries(elementProps)
.map(([property, value]) => {
//disregard falsy and values set by defaultIgnoreList constructor param
if (
!!value &&
(!this.defaultIgnoreList || !this.defaultIgnoreList.includes(value))
) {
let classBase = isCustom ? `bbmd-${base}` : `${base}`;
let valueType = typeof value;
if (valueType == "string" || valueType == "number") {
return isCustom
? ` ${classBase}--${property}-${value}`
: ` ${classBase}--${value}`;
} else if (valueType == "boolean") {
return ` ${classBase}--${property}`;
} }
} })
}) .join("")
.join(""); if (extras) cls += ` ${extras.join(" ")}`
return cls.trim()
} }
} }

View File

@ -1,28 +1,28 @@
import { MDCRipple } from "@material/ripple"; import { MDCRipple } from "@material/ripple"
export default function ripple( export default function ripple(
node, node,
props = { colour: "primary", unbounded: false } props = { colour: "primary", unbounded: false }
) { ) {
node.classList.add("mdc-ripple-surface"); node.classList.add("mdc-ripple-surface")
const component = new MDCRipple(node); const component = new MDCRipple(node)
component.unbounded = props.unbounded; component.unbounded = props.unbounded
if (props.colour === "secondary") { if (props.colour === "secondary") {
node.classList.remove("mdc-ripple-surface--primary"); node.classList.remove("mdc-ripple-surface--primary")
node.classList.add("mdc-ripple-surface--accent"); node.classList.add("mdc-ripple-surface--accent")
} else { } else {
node.classList.add("mdc-ripple-surface--primary"); node.classList.add("mdc-ripple-surface--primary")
node.classList.remove("mdc-ripple-surface--accent"); node.classList.remove("mdc-ripple-surface--accent")
} }
return { return {
destroy() { destroy() {
component.destroy(); component.destroy()
node.classList.remove("mdc-ripple-surface"); node.classList.remove("mdc-ripple-surface")
node.classList.remove("mdc-ripple-surface--primary"); node.classList.remove("mdc-ripple-surface--primary")
node.classList.remove("mdc-ripple-surface--accent"); node.classList.remove("mdc-ripple-surface--accent")
component = null; component = null
} },
}; }
} }

View File

@ -23,12 +23,12 @@ export const props = {
trailingIcon: true, trailingIcon: true,
fullwidth: false, fullwidth: false,
text: "I am button", text: "I am button",
disabled: false disabled: false,
}, },
icon: { icon: {
_component: "@budibase/materialdesign-components/icon", _component: "@budibase/materialdesign-components/icon",
_children: [], _children: [],
icon: "" icon: "",
}, },
textfield: { textfield: {
_component: "@budibase/materialdesign-components/textfield", _component: "@budibase/materialdesign-components/textfield",
@ -39,6 +39,4 @@ export const props = {
fullwidth:true, fullwidth:true,
helperText: "Add Surname", helperText: "Add Surname",
useCharCounter: true useCharCounter: true
} }
};

View File

@ -1,3 +1,4 @@
import { button, icon, textfield, H1, Overline } from "@BBMD"; import h1 from "../H1.svelte"
export default { H1, Overline, button, icon, textfield }; import { button, icon } from "@BBMD"
export default { h1, button, icon }

View File

@ -1,6 +1,3 @@
// export { default as h1 } from "./H1.svelte"; export { default as h1 } from "./H1.svelte"
export { default as icon } from "./Icon.svelte"
export { default as icon } from "./Icon.svelte"; export { button } from "./Button"
export { button } from "./Button";
export { textfield } from "./Textfield";
export * from "./Typography"

View File

@ -1,8 +1,4 @@
myapps/ myapps/
config.js config.js
<<<<<<< HEAD
/builder/* /builder/*
!/builder/assets/ !/builder/assets/
=======
builder/
>>>>>>> ee5a4e8c962b29242152cbbd8065d8f3ccf65eaf

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,25 +0,0 @@
{
"main": {
"index": {},
"appBody": "./main.app.json"
},
"unauthenticated": {
"index": {
"_component": "budibase-components/indexHtml",
"title": "Test App 1 - Login",
"customScripts": [
"MyCustomComponents.js"
]
},
"appBody": "./unauthenticated.app.json"
},
"componentLibraries": [
"./customComponents",
"./moreCustomComponents",
"@budibase/standard-components"
],
"stylesheets": [
"https://css-r-us.com/myawesomestyles.css",
"/local.css"
]
}

View File

@ -0,0 +1,14 @@
{
"title": "Test App",
"favicon": "./_shared/favicon.png",
"stylesheets": [
"my-styles.css"
],
"componentLibraries": [
"./customComponents",
"./moreCustomComponents"
],
"props": {
"_component": "@budibase/standard-components/div"
}
}

View File

@ -0,0 +1,8 @@
{
"name": "screen1",
"description": "",
"props": {
"_component": "@budibase/standard-components/div",
"className": ""
}
}

View File

@ -0,0 +1,8 @@
{
"name": "screen2",
"description": "",
"props": {
"_component": "@budibase/standard-components/div",
"className": ""
}
}

View File

@ -0,0 +1,9 @@
{
"title": "Test App",
"favicon": "./_shared/favicon.png",
"stylesheets": ["my-styles.css"],
"componentLibraries": ["./customComponents","./moreCustomComponents"],
"props" : {
"_component": "@budibase/standard-components/div"
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,79 +1,2 @@
window["##BUDIBASE_APPDEFINITION##"] = { window['##BUDIBASE_APPDEFINITION##'] = {"hierarchy":{"name":"root","type":"root","children":[{"name":"customer","type":"record","fields":[{"name":"name","type":"string","typeOptions":{"maxLength":1000,"values":null,"allowDeclaredValuesOnly":false},"label":"name","getInitialValue":"default","getUndefinedValue":"default"}],"children":[{"name":"invoiceyooo","type":"record","fields":[{"name":"amount","type":"number","typeOptions":{"minValue":99999999999,"maxValue":99999999999,"decimalPlaces":2},"label":"amount","getInitialValue":"default","getUndefinedValue":"default"}],"children":[],"validationRules":[],"nodeId":2,"indexes":[],"allidsShardFactor":1,"collectionName":"invoices","isSingle":false}],"validationRules":[],"nodeId":1,"indexes":[],"allidsShardFactor":64,"collectionName":"customers","isSingle":false}],"pathMaps":[],"indexes":[],"nodeId":0},"componentLibraries":[{"importPath":"/lib/customComponents/index.js","libName":"./customComponents"},{"importPath":"/lib/moreCustomComponents/index.js","libName":"./moreCustomComponents"}],"appRootPath":"","page":{"title":"Test App","favicon":"./_shared/favicon.png","stylesheets":["my-styles.css"],"componentLibraries":["./customComponents","./moreCustomComponents"],"props":{"_component":"@budibase/standard-components/div"}},"screens":[{"name":"screen1","description":"","props":{"_component":"@budibase/standard-components/div","className":""},"_css":"/css/d121e1ecc6cf44f433213222e9ff5d40.css"},{"name":"screen2","description":"","props":{"_component":"@budibase/standard-components/div","className":""},"_css":"/css/7b7c05b78e05c06eb8d69475caadfea3.css"}]};
hierarchy: { window['##BUDIBASE_UIFUNCTIONS##'] = {'1234':() => 'test return'}
name: "root",
type: "root",
children: [
{
name: "customer",
type: "record",
fields: [
{
name: "name",
type: "string",
typeOptions: {
maxLength: 1000,
values: null,
allowDeclaredValuesOnly: false,
},
label: "name",
getInitialValue: "default",
getUndefinedValue: "default",
},
],
children: [
{
name: "invoiceyooo",
type: "record",
fields: [
{
name: "amount",
type: "number",
typeOptions: {
minValue: 99999999999,
maxValue: 99999999999,
decimalPlaces: 2,
},
label: "amount",
getInitialValue: "default",
getUndefinedValue: "default",
},
],
children: [],
validationRules: [],
nodeId: 2,
indexes: [],
allidsShardFactor: 1,
collectionName: "invoices",
isSingle: false,
},
],
validationRules: [],
nodeId: 1,
indexes: [],
allidsShardFactor: 64,
collectionName: "customers",
isSingle: false,
},
],
pathMaps: [],
indexes: [],
nodeId: 0,
},
componentLibraries: [
{
importPath: "/lib/customComponents/index.js",
libName: "./customComponents",
},
{
importPath: "/lib/moreCustomComponents/index.js",
libName: "./moreCustomComponents",
},
{
importPath:
"/lib/node_modules/@budibase/standard-components/dist/index.js",
libName: "@budibase/standard-components",
},
],
appRootPath: "",
props: { _component: "some_component" },
}

View File

@ -4,8 +4,8 @@
<meta charset='utf8'> <meta charset='utf8'>
<meta name='viewport' content='width=device-width'> <meta name='viewport' content='width=device-width'>
<title>Budibase App</title> <title>Test App</title>
<link rel='icon' type='image/png' href='//_shared/favicon.png'> <link rel='icon' type='image/png' href='/./_shared/favicon.png'>
<style> <style>
html, body { html, body {
@ -14,9 +14,21 @@
} }
</style> </style>
<link rel='stylesheet' href='https://css-r-us.com/myawesomestyles.css'>
<link rel='stylesheet' href='///local.css'>
<link rel='stylesheet' href='//my-styles.css'>
<link rel='stylesheet' href='/css/d121e1ecc6cf44f433213222e9ff5d40.css'>
<link rel='stylesheet' href='/css/7b7c05b78e05c06eb8d69475caadfea3.css'>
<link rel='stylesheet' href='/css/f66fc2928f7d850c946e619c1a1f3096.css'>
<script src='/clientAppDefinition.js'></script> <script src='/clientAppDefinition.js'></script>
<script src='/budibase-client.js'></script> <script src='/budibase-client.js'></script>
<script> <script>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -7,12 +7,13 @@ const send = require("koa-send")
const { const {
getPackageForBuilder, getPackageForBuilder,
getComponents, getComponents,
savePackage,
getApps, getApps,
saveScreen, saveScreen,
renameScreen, renameScreen,
deleteScreen, deleteScreen,
savePagePackage,
componentLibraryInfo, componentLibraryInfo,
listScreens,
} = require("../utilities/builder") } = require("../utilities/builder")
const builderPath = resolve(__dirname, "../builder") const builderPath = resolve(__dirname, "../builder")
@ -20,8 +21,6 @@ const builderPath = resolve(__dirname, "../builder")
module.exports = (config, app) => { module.exports = (config, app) => {
const router = new Router() const router = new Router()
const prependSlash = path => (path.startsWith("/") ? path : `/${path}`)
router router
.use(session(config, app)) .use(session(config, app))
.use(async (ctx, next) => { .use(async (ctx, next) => {
@ -95,7 +94,7 @@ module.exports = (config, app) => {
await send(ctx, path, { root: builderPath }) await send(ctx, path, { root: builderPath })
} }
}) })
.post("/:appname/api/authenticate", async (ctx, next) => { .post("/:appname/api/authenticate", async ctx => {
const user = await ctx.master.authenticate( const user = await ctx.master.authenticate(
ctx.sessionId, ctx.sessionId,
ctx.params.appname, ctx.params.appname,
@ -149,10 +148,6 @@ module.exports = (config, app) => {
ctx.body = await getPackageForBuilder(config, ctx.params.appname) ctx.body = await getPackageForBuilder(config, ctx.params.appname)
ctx.response.status = StatusCodes.OK ctx.response.status = StatusCodes.OK
}) })
.post("/_builder/api/:appname/appPackage", async ctx => {
ctx.body = await savePackage(config, ctx.params.appname, ctx.request.body)
ctx.response.status = StatusCodes.OK
})
.get("/_builder/api/:appname/components", async ctx => { .get("/_builder/api/:appname/components", async ctx => {
try { try {
ctx.body = getComponents(config, ctx.params.appname, ctx.query.lib) ctx.body = getComponents(config, ctx.params.appname, ctx.query.lib)
@ -184,26 +179,55 @@ module.exports = (config, app) => {
ctx.body = info.generators ctx.body = info.generators
ctx.response.status = StatusCodes.OK ctx.response.status = StatusCodes.OK
}) })
.post("/_builder/api/:appname/screen", async ctx => { .post("/_builder/api/:appname/pages/:pageName", async ctx => {
await saveScreen(config, ctx.params.appname, ctx.request.body) await savePagePackage(
config,
ctx.params.appname,
ctx.params.pageName,
ctx.request.body
)
ctx.response.status = StatusCodes.OK ctx.response.status = StatusCodes.OK
}) })
.patch("/_builder/api/:appname/screen", async ctx => { .get("/_builder/api/:appname/pages/:pagename/screens", async ctx => {
ctx.body = await listScreens(
config,
ctx.params.appname,
ctx.params.pagename
)
ctx.response.status = StatusCodes.OK
})
.post("/_builder/api/:appname/pages/:pagename/screen", async ctx => {
await saveScreen(
config,
ctx.params.appname,
ctx.params.pagename,
ctx.request.body
)
ctx.response.status = StatusCodes.OK
})
.patch("/_builder/api/:appname/pages/:pagename/screen", async ctx => {
await renameScreen( await renameScreen(
config, config,
ctx.params.appname, ctx.params.appname,
ctx.params.pagename,
ctx.request.body.oldname, ctx.request.body.oldname,
ctx.request.body.newname ctx.request.body.newname
) )
ctx.response.status = StatusCodes.OK ctx.response.status = StatusCodes.OK
}) })
.delete("/_builder/api/:appname/screen/*", async ctx => { .delete("/_builder/api/:appname/pages/:pagename/screen/*", async ctx => {
const name = ctx.request.path.replace( const name = ctx.request.path.replace(
`/_builder/api/${ctx.params.appname}/screen/`, `/_builder/api/${ctx.params.appname}/pages/${ctx.params.pagename}/screen/`,
"" ""
) )
await deleteScreen(config, ctx.params.appname, decodeURI(name)) await deleteScreen(
config,
ctx.params.appname,
ctx.params.pagename,
decodeURI(name)
)
ctx.response.status = StatusCodes.OK ctx.response.status = StatusCodes.OK
}) })
.get("/:appname", async ctx => { .get("/:appname", async ctx => {

View File

@ -1,25 +1,26 @@
const testAppDef = require("../appPackages/testApp/appDefinition.json") const testAppDef = require("../appPackages/testApp/appDefinition.json")
const testAccessLevels = require("../appPackages/testApp/access_levels.json") const testAccessLevels = require("../appPackages/testApp/access_levels.json")
const testPages = require("../appPackages/testApp/pages.json") const mainPage = require("../appPackages/testApp/pages/main/page.json")
const unauthenticatedPage = require("../appPackages/testApp/pages/unauthenticated/page.json")
const testComponents = require("../appPackages/testApp/customComponents/components.json") const testComponents = require("../appPackages/testApp/customComponents/components.json")
const testMoreComponents = require("../appPackages/testApp/moreCustomComponents/components.json") const testMoreComponents = require("../appPackages/testApp/moreCustomComponents/components.json")
const statusCodes = require("../utilities/statusCodes") const statusCodes = require("../utilities/statusCodes")
const screen1 = require("../appPackages/testApp/components/myTextBox.json") const screen1 = require("../appPackages/testApp/pages/main/screens/screen1.json")
const screen2 = require("../appPackages/testApp/components/subfolder/otherTextBox.json") const screen2 = require("../appPackages/testApp/pages/main/screens/screen2.json")
const { readJSON, pathExists, unlink } = require("fs-extra") const { readJSON, pathExists, unlink, readFile } = require("fs-extra")
const { getHashedCssPaths } = require("../utilities/builder/convertCssToFiles")
const app = require("./testApp")() const app = require("./testApp")()
testComponents.textbox.name = `./customComponents/textbox` testComponents.textbox.name = `./customComponents/textbox`
testMoreComponents.textbox.name = `./moreCustomComponents/textbox` testMoreComponents.textbox.name = `./moreCustomComponents/textbox`
beforeAll(async () => { beforeAll(async () => {
const testComponent = "./appPackages/testApp/components/newTextBox.json" const testScreen = "./appPackages/testApp/pages/main/screens/newscreen.json"
const testComponentAfterMove = const testScreenAfterMove =
"./appPackages/testApp/components/anotherSubFolder/newTextBox.json" "./appPackages/testApp/pages/main/screens/anotherscreen.json"
if (await pathExists(testComponent)) await unlink(testComponent) if (await pathExists(testScreen)) await unlink(testScreen)
if (await pathExists(testComponentAfterMove)) if (await pathExists(testScreenAfterMove)) await unlink(testScreenAfterMove)
await unlink(testComponentAfterMove)
await app.start() await app.start()
}) })
@ -45,7 +46,10 @@ it("/apppackage should get pages", async () => {
const { body } = await app const { body } = await app
.get("/_builder/api/testApp/appPackage") .get("/_builder/api/testApp/appPackage")
.expect(statusCodes.OK) .expect(statusCodes.OK)
expect(body.pages).toEqual(testPages) expect(body.pages).toEqual({
main: mainPage,
unauthenticated: unauthenticatedPage,
})
}) })
it("/apppackage should get components", async () => { it("/apppackage should get components", async () => {
@ -53,123 +57,127 @@ it("/apppackage should get components", async () => {
.get("/_builder/api/testApp/appPackage") .get("/_builder/api/testApp/appPackage")
.expect(statusCodes.OK) .expect(statusCodes.OK)
expect(body.components["./customComponents/textbox"]).toBeDefined() expect(body.components.components["./customComponents/textbox"]).toBeDefined()
expect(body.components["./moreCustomComponents/textbox"]).toBeDefined() expect(
body.components.components["./moreCustomComponents/textbox"]
).toBeDefined()
expect(body.components["./customComponents/textbox"]).toEqual( expect(body.components.components["./customComponents/textbox"]).toEqual(
testComponents.textbox testComponents.textbox
) )
expect(body.components["./moreCustomComponents/textbox"]).toEqual( expect(body.components.components["./moreCustomComponents/textbox"]).toEqual(
testMoreComponents.textbox testMoreComponents.textbox
) )
}) })
it("/apppackage should get screens", async () => { it("/pages/:pageName/screens should get screens", async () => {
const { body } = await app const { body } = await app
.get("/_builder/api/testApp/appPackage") .get("/_builder/api/testApp/pages/main/screens")
.expect(statusCodes.OK) .expect(statusCodes.OK)
const expectedComponents = { const expectedComponents = {
myTextBox: { ...screen1, name: "myTextBox" }, screen1: { ...screen1, name: "screen1" },
"subfolder/otherTextBox": { ...screen2, name: "subfolder/otherTextBox" }, screen2: { ...screen2, name: "screen2" },
} }
expect(body.screens).toEqual(expectedComponents) expect(body).toEqual(expectedComponents)
}) })
it("should be able to create new derived component", async () => { it("should be able to create new screen", async () => {
const newscreen = { const newscreen = {
name: "newTextBox", name: "newscreen",
inherits: "./customComponents/textbox",
props: { props: {
label: "something", _component: "@budibase/standard-component/div",
className: "something",
}, },
} }
await app await app
.post("/_builder/api/testApp/screen", newscreen) .post("/_builder/api/testApp/pages/main/screen", newscreen)
.expect(statusCodes.OK) .expect(statusCodes.OK)
const componentFile = "./appPackages/testApp/components/newTextBox.json" const screenFile = "./appPackages/testApp/pages/main/screens/newscreen.json"
expect(await pathExists(componentFile)).toBe(true) expect(await pathExists(screenFile)).toBe(true)
expect(await readJSON(componentFile)).toEqual(newscreen) expect(await readJSON(screenFile)).toEqual(newscreen)
}) })
it("should be able to update derived component", async () => { it("should be able to update screen", async () => {
const updatedscreen = { const updatedscreen = {
name: "newTextBox", name: "newscreen",
inherits: "./customComponents/textbox",
props: { props: {
label: "something else", _component: "@budibase/standard-component/div",
className: "something else",
}, },
} }
await app await app
.post("/_builder/api/testApp/screen", updatedscreen) .post("/_builder/api/testApp/pages/main/screen", updatedscreen)
.expect(statusCodes.OK) .expect(statusCodes.OK)
const componentFile = "./appPackages/testApp/components/newTextBox.json" const screenFile = "./appPackages/testApp/pages/main/screens/newscreen.json"
expect(await readJSON(componentFile)).toEqual(updatedscreen) expect(await readJSON(screenFile)).toEqual(updatedscreen)
}) })
it("should be able to rename derived component", async () => { it("should be able to rename screen", async () => {
await app await app
.patch("/_builder/api/testApp/screen", { .patch("/_builder/api/testApp/pages/main/screen", {
oldname: "newTextBox", oldname: "newscreen",
newname: "anotherSubFolder/newTextBox", newname: "anotherscreen",
}) })
.expect(statusCodes.OK) .expect(statusCodes.OK)
const oldcomponentFile = "./appPackages/testApp/components/newTextBox.json" const oldcomponentFile =
"./appPackages/testApp/pages/main/screens/newscreen.json"
const newcomponentFile = const newcomponentFile =
"./appPackages/testApp/components/anotherSubFolder/newTextBox.json" "./appPackages/testApp/pages/main/screens/anotherscreen.json"
expect(await pathExists(oldcomponentFile)).toBe(false) expect(await pathExists(oldcomponentFile)).toBe(false)
expect(await pathExists(newcomponentFile)).toBe(true) expect(await pathExists(newcomponentFile)).toBe(true)
}) })
it("should be able to delete derived component", async () => { it("should be able to delete screen", async () => {
await app await app
.delete("/_builder/api/testApp/screen/anotherSubFolder/newTextBox") .delete("/_builder/api/testApp/pages/main/screen/anotherscreen")
.expect(statusCodes.OK) .expect(statusCodes.OK)
const componentFile = const componentFile =
"./appPackages/testApp/components/anotherSubFolder/newTextBox.json" "./appPackages/testApp/pages/main/screens/anotherscreen.json"
const componentDir = "./appPackages/testApp/components/anotherSubFolder"
expect(await pathExists(componentFile)).toBe(false) expect(await pathExists(componentFile)).toBe(false)
expect(await pathExists(componentDir)).toBe(false)
}) })
it("/savePackage should prepare all necessary client files", async () => { it("/savePage should prepare all necessary client files", async () => {
const mainCss = "/*main page css*/"
mainPage._css = mainCss
const screen1Css = "/*screen1 css*/"
screen1._css = screen1Css
const screen2Css = "/*screen2 css*/"
screen2._css = screen2Css
await app await app
.post("/_builder/api/testApp/appPackage", { .post("/_builder/api/testApp/pages/main", {
appDefinition: testAppDef, appDefinition: testAppDef,
accessLevels: testAccessLevels, accessLevels: testAccessLevels,
pages: testPages, page: mainPage,
uiFunctions: "{'1234':() => 'test return'}",
screens: [screen1, screen2],
}) })
.expect(statusCodes.OK) .expect(statusCodes.OK)
const publicFolderMain = relative => const publicFolderMain = relative =>
"./appPackages/testApp/public/main" + relative "./appPackages/testApp/public/main" + relative
const publicFolderUnauth = relative =>
"./appPackages/testApp/public/unauthenticated" + relative const cssDir = publicFolderMain("/css")
expect(await pathExists(publicFolderMain("/index.html"))).toBe(true) expect(await pathExists(publicFolderMain("/index.html"))).toBe(true)
expect(await pathExists(publicFolderUnauth("/index.html"))).toBe(true)
expect( expect(
await pathExists(publicFolderMain("/lib/customComponents/index.js")) await pathExists(publicFolderMain("/lib/customComponents/index.js"))
).toBe(true) ).toBe(true)
expect(
await pathExists(publicFolderUnauth("/lib/customComponents/index.js"))
).toBe(true)
expect( expect(
await pathExists(publicFolderMain("/lib/moreCustomComponents/index.js")) await pathExists(publicFolderMain("/lib/moreCustomComponents/index.js"))
).toBe(true) ).toBe(true)
expect(
await pathExists(publicFolderUnauth("/lib/moreCustomComponents/index.js"))
).toBe(true)
expect( expect(
await pathExists( await pathExists(
@ -178,16 +186,34 @@ it("/savePackage should prepare all necessary client files", async () => {
) )
) )
).toBe(true) ).toBe(true)
expect(
await pathExists(
publicFolderUnauth(
"/lib/node_modules/@budibase/standard-components/dist/index.js"
)
)
).toBe(true)
expect(await pathExists(publicFolderUnauth("/budibase-client.js"))).toBe(true) const indexHtmlMain = await readFile(publicFolderMain("/index.html"), "utf8")
expect(await pathExists(publicFolderUnauth("/clientAppDefinition.js"))).toBe(
true const pageCssPaths = getHashedCssPaths(cssDir, mainCss)
const screen1CssPaths = getHashedCssPaths(cssDir, screen1Css)
const screen2CssPaths = getHashedCssPaths(cssDir, screen2Css)
expect(await pathExists(publicFolderMain(pageCssPaths.url))).toBe(true)
const savedPageCss = await readFile(
publicFolderMain(pageCssPaths.url),
"utf8"
) )
expect(savedPageCss).toEqual(mainCss)
expect(indexHtmlMain.includes(pageCssPaths.url)).toBe(true)
expect(await pathExists(publicFolderMain(screen1CssPaths.url))).toBe(true)
const savedScreen1Css = await readFile(
publicFolderMain(screen1CssPaths.url),
"utf8"
)
expect(savedScreen1Css).toEqual(screen1Css)
expect(indexHtmlMain.includes(screen1CssPaths.url)).toBe(true)
expect(await pathExists(publicFolderMain(screen2CssPaths.url))).toBe(true)
const savedScreen2Css = await readFile(
publicFolderMain(screen2CssPaths.url),
"utf8"
)
expect(savedScreen2Css).toEqual(screen2Css)
expect(indexHtmlMain.includes(screen2CssPaths.url)).toBe(true)
}) })

View File

@ -11,34 +11,18 @@ const {
} = require("fs-extra") } = require("fs-extra")
const { join, resolve, dirname } = require("path") const { join, resolve, dirname } = require("path")
const sqrl = require("squirrelly") const sqrl = require("squirrelly")
const { convertCssToFiles } = require("./convertCssToFiles")
module.exports = async (config, appname, pages, appdefinition) => { module.exports = async (config, appname, pkg) => {
const appPath = appPackageFolder(config, appname) const appPath = appPackageFolder(config, appname)
await buildClientAppDefinition( await convertCssToFiles(publicPath(appPath, pkg.pageName), pkg)
config,
appname,
appdefinition,
appPath,
pages,
"main"
)
await buildClientAppDefinition( await buildIndexHtml(config, appname, appPath, pkg)
config,
appname,
appdefinition,
appPath,
pages,
"unauthenticated"
)
await buildIndexHtml(config, appname, appPath, pages, "main") await buildClientAppDefinition(config, appname, pkg, appPath)
await buildIndexHtml(config, appname, appPath, pages, "unauthenticated") await copyClientLib(appPath, pkg.pageName)
await copyClientLib(appPath, "main")
await copyClientLib(appPath, "unauthenticated")
} }
const publicPath = (appPath, pageName) => join(appPath, "public", pageName) const publicPath = (appPath, pageName) => join(appPath, "public", pageName)
@ -46,10 +30,11 @@ const rootPath = (config, appname) =>
config.useAppRootPath ? `/${appname}` : "" config.useAppRootPath ? `/${appname}` : ""
const copyClientLib = async (appPath, pageName) => { const copyClientLib = async (appPath, pageName) => {
var sourcepath = require.resolve("@budibase/client") const sourcepath = require.resolve("@budibase/client")
var destPath = join(publicPath(appPath, pageName), "budibase-client.js") const destPath = join(publicPath(appPath, pageName), "budibase-client.js")
await copyFile(sourcepath, destPath, constants.COPYFILE_FICLONE) await copyFile(sourcepath, destPath, constants.COPYFILE_FICLONE)
await copyFile( await copyFile(
sourcepath + ".map", sourcepath + ".map",
destPath + ".map", destPath + ".map",
@ -57,8 +42,8 @@ const copyClientLib = async (appPath, pageName) => {
) )
} }
const buildIndexHtml = async (config, appname, appPath, pages, pageName) => { const buildIndexHtml = async (config, appname, appPath, pkg) => {
const appPublicPath = publicPath(appPath, pageName) const appPublicPath = publicPath(appPath, pkg.pageName)
const appRootPath = rootPath(config, appname) const appRootPath = rootPath(config, appname)
const stylesheetUrl = s => const stylesheetUrl = s =>
@ -67,10 +52,11 @@ const buildIndexHtml = async (config, appname, appPath, pages, pageName) => {
: `/${rootPath(config, appname)}/${s}` : `/${rootPath(config, appname)}/${s}`
const templateObj = { const templateObj = {
title: pages[pageName].index.title || "Budibase App", title: pkg.page.title || "Budibase App",
favicon: `${appRootPath}/${pages[pageName].index.favicon || favicon: `${appRootPath}/${pkg.page.favicon || "/_shared/favicon.png"}`,
"/_shared/favicon.png"}`, stylesheets: (pkg.page.stylesheets || []).map(stylesheetUrl),
stylesheets: (pages.stylesheets || []).map(stylesheetUrl), screenStyles: pkg.screens.filter(s => s._css).map(s => s._css),
pageStyle: pkg.page._css,
appRootPath, appRootPath,
} }
@ -86,20 +72,14 @@ const buildIndexHtml = async (config, appname, appPath, pages, pageName) => {
await writeFile(indexHtmlPath, indexHtml, { flag: "w+" }) await writeFile(indexHtmlPath, indexHtml, { flag: "w+" })
} }
const buildClientAppDefinition = async ( const buildClientAppDefinition = async (config, appname, pkg) => {
config, const appPath = appPackageFolder(config, appname)
appname, const appPublicPath = publicPath(appPath, pkg.pageName)
appdefinition,
appPath,
pages,
pageName
) => {
const appPublicPath = publicPath(appPath, pageName)
const appRootPath = rootPath(config, appname) const appRootPath = rootPath(config, appname)
const componentLibraries = [] const componentLibraries = []
for (let lib of pages.componentLibraries) { for (let lib of pkg.page.componentLibraries) {
const info = await componentLibraryInfo(appPath, lib) const info = await componentLibraryInfo(appPath, lib)
const libFile = info.components._lib || "index.js" const libFile = info.components._lib || "index.js"
const source = join(info.libDir, libFile) const source = join(info.libDir, libFile)
@ -131,16 +111,27 @@ const buildClientAppDefinition = async (
const filename = join(appPublicPath, "clientAppDefinition.js") const filename = join(appPublicPath, "clientAppDefinition.js")
if (pkg.page._css) {
delete pkg.page._css
}
for (let screen of pkg.screens) {
if (screen._css) {
delete pkg.page._css
}
}
const clientAppDefObj = { const clientAppDefObj = {
hierarchy: appdefinition.hierarchy, hierarchy: pkg.appDefinition.hierarchy,
componentLibraries: componentLibraries, componentLibraries: componentLibraries,
appRootPath: appRootPath, appRootPath: appRootPath,
props: appdefinition.props[pageName], page: pkg.page,
screens: pkg.screens,
} }
await writeFile( await writeFile(
filename, filename,
`window['##BUDIBASE_APPDEFINITION##'] = ${JSON.stringify(clientAppDefObj)}; `window['##BUDIBASE_APPDEFINITION##'] = ${JSON.stringify(clientAppDefObj)};
window['##BUDIBASE_UIFUNCTIONS##'] = ${appdefinition.uiFunctions}` window['##BUDIBASE_UIFUNCTIONS##'] = ${pkg.uiFunctions}`
) )
} }

View File

@ -0,0 +1,43 @@
const crypto = require("crypto")
const { ensureDir, emptyDir, writeFile } = require("fs-extra")
const { join } = require("path")
module.exports.convertCssToFiles = async (publicPagePath, pkg) => {
const cssDir = join(publicPagePath, "css")
await ensureDir(cssDir)
await emptyDir(cssDir)
for (let screen of pkg.screens) {
if (!screen._css) continue
if (screen._css.trim().length === 0) {
delete screen._css
continue
}
screen._css = await createCssFile(cssDir, screen._css)
}
if (pkg.page._css) {
pkg.page._css = await createCssFile(cssDir, pkg.page._css)
}
}
module.exports.getHashedCssPaths = (cssDir, _css) => {
const fileName =
crypto
.createHash("md5")
.update(_css)
.digest("hex") + ".css"
const filePath = join(cssDir, fileName)
const url = `/css/${fileName}`
return { filePath, url }
}
const createCssFile = async (cssDir, _css) => {
const { filePath, url } = module.exports.getHashedCssPaths(cssDir, _css)
await writeFile(filePath, _css)
return url
}

View File

@ -11,16 +11,15 @@ const {
} = require("fs-extra") } = require("fs-extra")
const { join, dirname } = require("path") const { join, dirname } = require("path")
const { $ } = require("@budibase/core").common const { $ } = require("@budibase/core").common
const { keyBy, intersection, map } = require("lodash/fp") const { keyBy, intersection, map, values, flatten } = require("lodash/fp")
const { merge } = require("lodash") const { merge } = require("lodash")
const { componentLibraryInfo } = require("./componentLibraryInfo") const { componentLibraryInfo } = require("./componentLibraryInfo")
const savePackage = require("./savePackage") const savePagePackage = require("./savePagePackage")
const buildApp = require("./buildApp") const buildPage = require("./buildPage")
module.exports.savePackage = savePackage module.exports.savePagePackage = savePagePackage
const getPages = async appPath => await readJSON(`${appPath}/pages.json`)
const getAppDefinition = async appPath => const getAppDefinition = async appPath =>
await readJSON(`${appPath}/appDefinition.json`) await readJSON(`${appPath}/appDefinition.json`)
@ -37,8 +36,6 @@ module.exports.getPackageForBuilder = async (config, appname) => {
pages, pages,
components: await getComponents(appPath, pages), components: await getComponents(appPath, pages),
screens: keyBy("name")(await fetchscreens(appPath)),
} }
} }
@ -48,34 +45,65 @@ module.exports.getApps = async (config, master) => {
return $(master.listApplications(), [map(a => a.name), intersection(dirs)]) return $(master.listApplications(), [map(a => a.name), intersection(dirs)])
} }
const componentPath = (appPath, name) => const getPages = async appPath => {
join(appPath, "components", name + ".json") const pages = {}
module.exports.saveScreen = async (config, appname, component) => { const pageFolders = await readdir(join(appPath, "pages"))
for (let pageFolder of pageFolders) {
try {
pages[pageFolder] = await readJSON(
join(appPath, "pages", pageFolder, "page.json")
)
} catch (_) {
// ignore error
}
}
return pages
}
const screenPath = (appPath, pageName, name) =>
join(appPath, "pages", pageName, "screens", name + ".json")
module.exports.listScreens = async (config, appname, pagename) => {
const appPath = appPackageFolder(config, appname) const appPath = appPackageFolder(config, appname)
const compPath = componentPath(appPath, component.name) return keyBy("name")(await fetchscreens(appPath, pagename))
}
module.exports.saveScreen = async (config, appname, pagename, screen) => {
const appPath = appPackageFolder(config, appname)
const compPath = screenPath(appPath, pagename, screen.name)
await ensureDir(dirname(compPath)) await ensureDir(dirname(compPath))
await writeJSON(compPath, component, { if (screen._css) {
delete screen._css
}
await writeJSON(compPath, screen, {
encoding: "utf8", encoding: "utf8",
flag: "w", flag: "w",
spaces: 2, spaces: 2,
}) })
} }
module.exports.renameScreen = async (config, appname, oldName, newName) => { module.exports.renameScreen = async (
config,
appname,
pagename,
oldName,
newName
) => {
const appPath = appPackageFolder(config, appname) const appPath = appPackageFolder(config, appname)
const oldComponentPath = componentPath(appPath, oldName) const oldComponentPath = screenPath(appPath, pagename, oldName)
const newComponentPath = componentPath(appPath, newName) const newComponentPath = screenPath(appPath, pagename, newName)
await ensureDir(dirname(newComponentPath)) await ensureDir(dirname(newComponentPath))
await rename(oldComponentPath, newComponentPath) await rename(oldComponentPath, newComponentPath)
} }
module.exports.deleteScreen = async (config, appname, name) => { module.exports.deleteScreen = async (config, appname, pagename, name) => {
const appPath = appPackageFolder(config, appname) const appPath = appPackageFolder(config, appname)
const componentFile = componentPath(appPath, name) const componentFile = screenPath(appPath, pagename, name)
await unlink(componentFile) await unlink(componentFile)
const dir = dirname(componentFile) const dir = dirname(componentFile)
@ -84,6 +112,20 @@ module.exports.deleteScreen = async (config, appname, name) => {
} }
} }
module.exports.savePage = async (config, appname, pagename, page) => {
const appPath = appPackageFolder(config, appname)
const pageDir = join(appPath, "pages", pagename)
await ensureDir(pageDir)
await writeJSON(join(pageDir, "page.json"), page, {
encoding: "utf8",
flag: "w",
space: 2,
})
const appDefinition = await getAppDefinition(appPath)
await buildPage(config, appname, appDefinition, pagename, page)
}
module.exports.componentLibraryInfo = async (config, appname, lib) => { module.exports.componentLibraryInfo = async (config, appname, lib) => {
const appPath = appPackageFolder(config, appname) const appPath = appPackageFolder(config, appname)
return await componentLibraryInfo(appPath, lib) return await componentLibraryInfo(appPath, lib)
@ -92,11 +134,11 @@ module.exports.componentLibraryInfo = async (config, appname, lib) => {
const getComponents = async (appPath, pages, lib) => { const getComponents = async (appPath, pages, lib) => {
let libs let libs
if (!lib) { if (!lib) {
pages = pages || (await readJSON(`${appPath}/pages.json`)) pages = pages || (await getPages(appPath))
if (!pages.componentLibraries) return [] if (!pages) return []
libs = pages.componentLibraries libs = $(pages, [values, map(p => p.componentLibraries), flatten])
} else { } else {
libs = [lib] libs = [lib]
} }
@ -116,12 +158,12 @@ const getComponents = async (appPath, pages, lib) => {
return { components, generators } return { components, generators }
} }
const fetchscreens = async (appPath, relativePath = "") => { const fetchscreens = async (appPath, pagename, relativePath = "") => {
const currentDir = join(appPath, "components", relativePath) const currentDir = join(appPath, "pages", pagename, "screens", relativePath)
const contents = await readdir(currentDir) const contents = await readdir(currentDir)
const components = [] const screens = []
for (let item of contents) { for (let item of contents) {
const itemRelativePath = join(relativePath, item) const itemRelativePath = join(relativePath, item)
@ -139,7 +181,7 @@ const fetchscreens = async (appPath, relativePath = "") => {
component.props = component.props || {} component.props = component.props || {}
components.push(component) screens.push(component)
} else { } else {
const childComponents = await fetchscreens( const childComponents = await fetchscreens(
appPath, appPath,
@ -147,12 +189,12 @@ const fetchscreens = async (appPath, relativePath = "") => {
) )
for (let c of childComponents) { for (let c of childComponents) {
components.push(c) screens.push(c)
} }
} }
} }
return components return screens
} }
module.exports.getComponents = getComponents module.exports.getComponents = getComponents

View File

@ -18,6 +18,15 @@
<link rel='stylesheet' href='{{ @this }}'> <link rel='stylesheet' href='{{ @this }}'>
{{ /each }} {{ /each }}
{{ each(options.screenStyles) }}
<link rel='stylesheet' href='{{ @this }}'>
{{ /each }}
{{ if(options.pageStyle) }}
<link rel='stylesheet' href='{{ pageStyle }}'>
{{ /if }}
<script src='{{ appRootPath }}/clientAppDefinition.js'></script> <script src='{{ appRootPath }}/clientAppDefinition.js'></script>
<script src='{{ appRootPath }}/budibase-client.js'></script> <script src='{{ appRootPath }}/budibase-client.js'></script>
<script> <script>

View File

@ -1,18 +0,0 @@
const { appPackageFolder } = require("../createAppPackage")
const { writeJSON } = require("fs-extra")
const buildApp = require("./buildApp")
module.exports = async (config, appname, pkg) => {
const appPath = appPackageFolder(config, appname)
await writeJSON(`${appPath}/appDefinition.json`, pkg.appDefinition, {
spaces: 2,
})
await writeJSON(`${appPath}/access_levels.json`, pkg.accessLevels, {
spaces: 2,
})
await writeJSON(`${appPath}/pages.json`, pkg.pages, { spaces: 2 })
await buildApp(config, appname, pkg.pages, pkg.appDefinition)
}

View File

@ -0,0 +1,30 @@
const { appPackageFolder } = require("../createAppPackage")
const { writeJSON } = require("fs-extra")
const { join } = require("path")
const buildPage = require("./buildPage")
module.exports = async (config, appname, pageName, pkg) => {
const appPath = appPackageFolder(config, appname)
pkg.pageName = pageName
await writeJSON(`${appPath}/appDefinition.json`, pkg.appDefinition, {
spaces: 2,
})
await writeJSON(`${appPath}/access_levels.json`, pkg.accessLevels, {
spaces: 2,
})
await buildPage(config, appname, pkg)
const pageFile = join(appPath, "pages", pageName, "page.json")
if (pkg.page._css) {
delete pkg.page._css
}
await writeJSON(pageFile, pkg.page, {
spaces: 2,
})
}

File diff suppressed because one or more lines are too long