1
0
Fork 0
mirror of synced 2024-06-30 03:50:37 +12:00

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

View file

@ -20,7 +20,6 @@
$: originalName = component.name
$: name = component.name
$: description = component.description
$: componentInfo = $store.currentComponentInfo
$: components = $store.components
const onPropChanged = store.setComponentProp
@ -47,7 +46,7 @@
<button
class:selected={current_view === 'code'}
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">
<CircleIndicator />
</div>
@ -63,27 +62,26 @@
</button>
</li>
</ul>
{$store.currentFrontEndType}
{#if !componentInfo.component}
<div class="component-props-container">
<div class="component-props-container">
{#if current_view === 'props'}
<PropsView {componentInfo} {components} {onPropChanged} />
{:else if current_view === 'layout'}
<LayoutEditor {onStyleChanged} {componentInfo} />
{:else if current_view === 'events'}
<EventsEditor {componentInfo} {components} {onPropChanged} />
{/if}
{#if current_view === 'props'}
<PropsView {component} {components} {onPropChanged} />
{:else if current_view === 'layout'}
<LayoutEditor {onStyleChanged} {component} />
{:else if current_view === 'events'}
<EventsEditor {component} {components} {onPropChanged} />
{/if}
<CodeEditor
bind:this={codeEditor}
code={$store.currentComponentInfo._code}
onCodeChanged={store.setComponentCode} />
<CodeEditor
bind:this={codeEditor}
code={component._code}
onCodeChanged={store.setComponentCode} />
</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 { ArrowDownIcon } from "../common/Icons/"
export let components = []
export let screens = []
const joinPath = join("/")
const normalizedName = name =>
pipe(name, [
trimCharsStart("./"),
trimCharsStart("~/"),
trimCharsStart("../"),
trimChars(" "),
])
pipe(
name,
[
trimCharsStart("./"),
trimCharsStart("~/"),
trimCharsStart("../"),
trimChars(" "),
]
)
const lastPartOfName = c =>
last(c.name ? c.name.split("/") : c._component.split("/"))
const isComponentSelected = (current, comp) =>
current &&
current.component &&
comp.component &&
current.component.name === comp.component.name
const isComponentSelected = (current, comp) => current === comp
const isFolderSelected = (current, folder) => isInSubfolder(current, folder)
$: _components = pipe(components, [
map(c => ({ component: c, title: lastPartOfName(c) })),
sortBy("title"),
])
function select_component(screen, component) {
store.setCurrentScreen(screen)
store.selectComponent(component)
}
$: _screens = pipe(
screens,
[map(c => ({ component: c, title: lastPartOfName(c) })), sortBy("title")]
)
const isScreenSelected = component =>
component.component &&
$store.currentFrontEndItem &&
component.component.name === $store.currentFrontEndItem.name
$store.currentPreviewItem &&
component.component.name === $store.currentPreviewItem.name
$: console.log(_screens)
</script>
<div class="root">
{#each _components as component}
{#each _screens as screen}
<div
class="hierarchy-item component"
class:selected={isComponentSelected($store.currentComponentInfo, component)}
on:click|stopPropagation={() => store.setCurrentScreen(component.component.name)}>
class:selected={$store.currentPreviewItem.name === screen.title}
on:click|stopPropagation={() => store.setCurrentScreen(screen.title)}>
<span
class="icon"
style="transform: rotate({isScreenSelected(component) ? 0 : -90}deg);">
{#if component.component.props && component.component.props._children}
style="transform: rotate({$store.currentPreviewItem.name === screen.title ? 0 : -90}deg);">
{#if screen.component.props._children.length}
<ArrowDownIcon />
{/if}
</span>
<span class="title">{component.title}</span>
<span class="title">{screen.title}</span>
</div>
{#if isScreenSelected(component) && component.component.props && component.component.props._children}
{#if $store.currentPreviewItem.name === screen.title && screen.component.props._children}
<ComponentsHierarchyChildren
components={component.component.props._children}
components={screen.component.props._children}
currentComponent={$store.currentComponentInfo}
onSelect={child => select_component(component.component.name, child)} />
onSelect={store.selectComponent} />
{/if}
{/each}

View file

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

View file

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

View file

@ -2,50 +2,70 @@
import { store } from "../builderStore"
import { map, join } from "lodash/fp"
import { pipe } from "../common/core"
import { buildPropsHierarchy } from "./pagesParsing/buildPropsHierarchy"
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 &&
console.log(
iframe.contentDocument.head.insertAdjacentHTML(
"beforeend",
'<style prettier:content=""></style>'
`<\style></style>`
)
)
$: hasComponent = !!$store.currentFrontEndItem
$: styles = hasComponent ? $store.currentFrontEndItem._css : ""
$: hasComponent = !!$store.currentPreviewItem
$: styles = hasComponent ? $store.currentPreviewItem._css : ""
$: stylesheetLinks = pipe($store.pages.stylesheets, [
map(s => `<link rel="stylesheet" href="${s}"/>`),
join("\n"),
])
$: stylesheetLinks = pipe(
$store.pages.stylesheets,
[map(s => `<link rel="stylesheet" href="${s}"/>`), join("\n")]
)
$: appDefinition = {
componentLibraries: $store.loadLibraryUrls(),
props: buildPropsHierarchy(
$store.components,
$store.screens,
$store.currentFrontEndItem
),
props:
$store.currentPreviewItem &&
transform_component($store.currentPreviewItem, true),
hierarchy: $store.hierarchy,
appRootPath: "",
}
</script>
<div class="component-container">
{#if hasComponent}
{#if hasComponent && $store.currentPreviewItem}
<iframe
style="height: 100%; width: 100%"
title="componentPreview"
bind:this={iframe}
srcdoc={`<html>
<head>
<head>
${stylesheetLinks}
<script prettier:content="CiAgICAgICAgd2luZG93WyIjI0JVRElCQVNFX0FQUERFRklOSVRJT04jIyJdID0gJHtKU09OLnN0cmluZ2lmeShhcHBEZWZpbml0aW9uKX07CiAgICAgICAgd2luZG93WyIjI0JVRElCQVNFX1VJRlVOQ1RJT05TIl0gPSAkeyRzdG9yZS5jdXJyZW50U2NyZWVuRnVuY3Rpb25zfTsKICAgICAgICAKICAgICAgICBpbXBvcnQoJy9fYnVpbGRlci9idWRpYmFzZS1jbGllbnQuZXNtLm1qcycpCiAgICAgICAgLnRoZW4obW9kdWxlID0+IHsKICAgICAgICAgICAgbW9kdWxlLmxvYWRCdWRpYmFzZSh7IHdpbmRvdywgbG9jYWxTdG9yYWdlIH0pOwogICAgICAgIH0pCiAgICA=">{}</script> <style ✂prettier:content✂="CgogICAgICAgIGJvZHkgewogICAgICAgICAgICBib3gtc2l6aW5nOiBib3JkZXItYm94OwogICAgICAgICAgICBwYWRkaW5nOiAyMHB4OwogICAgICAgIH0KICAgICR7c3R5bGVzfQogICAg"></style></head>
<body>
</body>
<style>
${styles || ''}
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>`} />
{/if}
</div>

View file

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

View file

@ -2,7 +2,7 @@
import InputGroup from "../common/Inputs/InputGroup.svelte"
export let onStyleChanged = () => {}
export let componentInfo
export let component
const tbrl = [
{ placeholder: "T" },
@ -16,8 +16,8 @@
const single = [{ placeholder: "" }]
$: layout = {
...componentInfo._styles.position,
...componentInfo._styles.layout,
...component._styles.position,
...component._styles.layout,
}
$: layouts = {
@ -46,7 +46,7 @@
<h4>Positioning</h4>
<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">
<h5>{name}:</h5>
<InputGroup
@ -61,7 +61,7 @@
<h4>Positioning</h4>
<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">
<h5>{name}:</h5>
<InputGroup
@ -75,7 +75,7 @@
<h4>Spacing</h4>
<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">
<h5>{name}:</h5>
<InputGroup
@ -89,7 +89,7 @@
<h4>Z-Index</h4>
<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">
<h5>{name}:</h5>
<InputGroup

View file

@ -22,13 +22,17 @@
let layoutComponent
let screens
let name = ""
let route = ""
let saveAttempted = false
store.subscribe(s => {
layoutComponents = pipe(s.components, [
filter(c => c.container),
map(c => ({ name: c.name, ...splitName(c.name) })),
])
layoutComponents = pipe(
s.components,
[
filter(c => c.container),
map(c => ({ name: c.name, ...splitName(c.name) })),
]
)
layoutComponent = layoutComponent
? find(c => c.name === layoutComponent.name)(layoutComponents)
@ -45,7 +49,7 @@
if (!isValid) return
store.createScreen(name, layoutComponent.name)
store.createScreen(name, route, layoutComponent.name)
UIkit.modal(componentSelectorModal).hide()
}
@ -53,8 +57,11 @@
UIkit.modal(componentSelectorModal).hide()
}
const screenNameExists = name =>
some(s => s.name.toLowerCase() === name.toLowerCase())(screens)
const screenNameExists = name => {
return some(s => {
return s.name.toLowerCase() === name.toLowerCase()
})(screens)
}
</script>
<div bind:this={componentSelectorModal} id="new-component-modal" uk-modal>
@ -73,6 +80,14 @@
class:uk-form-danger={saveAttempted && (name.length === 0 || screenNameExists(name))}
bind:value={name} />
</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 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 IconButton from "../common/IconButton.svelte"
export let componentInfo
export let component
export let onPropChanged = () => {}
export let components
let errors = []
let props = {}
const props_to_ignore = ["_component", "_children", "_styles", "_code", "_id"]
$: propDefs =
componentInfo &&
Object.entries(componentInfo).filter(
component &&
Object.entries(component).filter(
([name]) => !props_to_ignore.includes(name)
)
function find_type(prop_name) {
if (!componentInfo._component) return
return components.find(({ name }) => name === componentInfo._component)
.props[prop_name]
if (!component._component) return
return components.find(({ name }) => name === component._component).props[
prop_name
]
}
let setProp = (name, value) => {

View file

@ -1,5 +1,6 @@
<script>
import ComponentsHierarchy from "./ComponentsHierarchy.svelte"
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
import PagesList from "./PagesList.svelte"
import { store } from "../builderStore"
import IconButton from "../common/IconButton.svelte"
@ -37,29 +38,59 @@
</div>
<div class="components-list-container">
<div class="nav-group-header">
<span class="components-nav-header">Screens</span>
<div>
<button on:click={newComponent}>+</button>
</div>
<span
on:click={() => store.setScreenType('page')}
class="components-nav-header"
class:active={$store.currentFrontEndType === 'page'}>
Page
</span>
</div>
<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 class="preview-pane">
{#if $store.currentFrontEndType === 'screen'}
<CurrentItemPreview />
{:else if $store.currentFrontEndType === 'page'}
<PageView />
{/if}
<CurrentItemPreview />
</div>
{#if $store.currentFrontEndType === 'screen'}
{#if $store.currentFrontEndType === 'screen' || $store.currentFrontEndType === 'page'}
<div class="components-pane">
<ComponentsPaneSwitcher />
</div>
@ -152,7 +183,7 @@
margin-right: 5px;
}
.nav-group-header > span:nth-child(2) {
.nav-group-header > span:nth-child(3) {
margin-left: 5px;
vertical-align: bottom;
grid-column-start: title;
@ -175,4 +206,8 @@
font-weight: 400;
color: #999;
}
.active {
color: #333;
}
</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 {
isString,
isUndefined,
find,
keys,
uniq,
some,
filter,
reduce,
cloneDeep,
includes,
last,
} from "lodash/fp"
import { types, expandComponentDefinition } from "./types"
import { isString, isUndefined } from "lodash/fp"
import { types } from "./types"
import { assign } from "lodash"
import { pipe } from "../../common/core"
import { isRootComponent } from "./searchComponents"
import { ensureShardNameIsInShardMap } from "../../../../core/src/indexing/sharding"
import { uuid } from "../../builderStore/uuid"
export const getInstanceProps = (componentInfo, props) => {
const finalProps = cloneDeep(componentInfo.fullProps)
for (let p in props) {
finalProps[p] = props[p]
}
return finalProps
}
export const getNewComponentInfo = (components, rootComponent, name) => {
const component = {
export const getNewScreen = (components, rootComponentName, name) => {
const rootComponent = components.find(c => c.name === rootComponentName)
return {
name: name || "",
description: "",
props: {
_component: rootComponent,
},
}
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,
url: "",
_css: "",
uiFunctions: "",
props: createProps(rootComponent).props,
}
}
@ -89,6 +20,9 @@ export const createProps = (componentDefinition, derivedFromProps) => {
const props = {
_component: componentDefinition.name,
_styles: { position: {}, layout: {} },
_id: uuid(),
_code: "",
}
const errors = []

View file

@ -1,11 +1,15 @@
export const defaultPagesObject = () => ({
main: {
_props: {},
_screens: {},
index: {
_component: "./components/indexHtml",
},
appBody: "bbapp.main.json",
},
unauthenticated: {
_props: {},
_screens: {},
index: {
_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", () => {
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", () => {
it("should include components in page apbody", () => {
const { components, screens } = componentsAndScreens()
const pages = {
main: {

View file

@ -1,6 +1,7 @@
import { createProps } from "../src/userInterface/pagesParsing/createProps"
import { keys, some } from "lodash/fp"
import { BB_STATE_BINDINGPATH } from "@budibase/client/src/state/isState"
import { stripStandardProps } from "./testData"
describe("createDefaultProps", () => {
const getcomponent = () => ({
@ -16,6 +17,7 @@ describe("createDefaultProps", () => {
expect(errors).toEqual([])
expect(props.fieldName).toBeDefined()
expect(props.fieldName).toBe("something")
stripStandardProps(props)
expect(keys(props).length).toBe(3)
})
@ -190,11 +192,6 @@ describe("createDefaultProps", () => {
})
it("should merge in derived props", () => {
const propDef = {
fieldName: "string",
fieldLength: { type: "number", default: 500 },
}
const comp = getcomponent()
comp.props.fieldName = "string"
comp.props.fieldLength = { type: "number", default: 500 }
@ -209,4 +206,13 @@ describe("createDefaultProps", () => {
expect(props.fieldName).toBe("surname")
expect(props.fieldLength).toBe(500)
})
it("should create standard props", () => {
const comp = getcomponent()
comp.props.fieldName = { type: "string", default: 1 }
const { props } = createProps(comp)
expect(props._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: {
_component: "budibase-components/div",
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 removePlaceholder("components")
await removePlaceholder("pages", "main", "screens")
await removePlaceholder("pages", "unauthenticated", "screens")
await removePlaceholder("public", "shared")
await removePlaceholder("public", "main")
await removePlaceholder("public", "unauthenticated")

View file

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

View file

@ -4,29 +4,26 @@ import { getStateOrValue } from "./state/getState"
import { setState, setStateFromBinding } from "./state/setState"
import { trimSlash } from "./common/trimSlash"
import { isBound } from "./state/isState"
import { _initialiseChildren } from "./render/initialiseChildren"
import { initialiseChildren } from "./render/initialiseChildren"
import { createTreeNode } from "./render/renderComponent"
import { screenRouter } from "./render/screenRouter"
export const createApp = (
document,
componentLibraries,
appDefinition,
user,
uiFunctions
uiFunctions,
screens
) => {
const coreApi = createCoreApi(appDefinition, user)
appDefinition.hierarchy = coreApi.templateApi.constructHierarchy(
appDefinition.hierarchy
)
const store = writable({
const pageStore = writable({
_bbuser: user,
})
let globalState = null
store.subscribe(s => {
globalState = s
})
const relativeUrl = url =>
appDefinition.appRootPath
? appDefinition.appRootPath + "/" + trimSlash(url)
@ -55,45 +52,100 @@ export const createApp = (
if (isFunction(event)) event(context)
}
const initialiseChildrenParams = (hydrate, treeNode) => ({
bb,
coreApi,
store,
document,
componentLibraries,
appDefinition,
hydrate,
uiFunctions,
treeNode,
})
let routeTo
let currentScreenStore
let currentScreenUbsubscribe
let currentUrl
const bb = (treeNode, componentProps) => ({
hydrateChildren: _initialiseChildren(
initialiseChildrenParams(true, treeNode)
),
appendChildren: _initialiseChildren(
initialiseChildrenParams(false, treeNode)
),
insertChildren: (props, htmlElement, anchor) =>
_initialiseChildren(initialiseChildrenParams(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(globalState, prop, currentContext),
store,
relativeUrl,
api,
isBound,
parent,
})
const onScreenSlotRendered = screenSlotNode => {
const onScreenSelected = (screen, store, url) => {
const { getInitialiseParams, unsubscribe } = initialiseChildrenParams(
store
)
const initialiseChildParams = getInitialiseParams(true, screenSlotNode)
initialiseChildren(initialiseChildParams)(
[screen.props],
screenSlotNode.rootElement
)
if (currentScreenUbsubscribe) currentScreenUbsubscribe()
currentScreenUbsubscribe = unsubscribe
currentScreenStore = store
currentUrl = url
}
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 { trimSlash } from "./common/trimSlash"
import { builtins, builtinLibName } from "./render/builtinComponents"
export const loadBudibase = async ({
componentLibraries,
props,
page,
screens,
window,
localStorage,
uiFunctions,
}) => {
const appDefinition = window["##BUDIBASE_APPDEFINITION##"]
const uiFunctionsFromWindow = window["##BUDIBASE_APPDEFINITION##"]
const uiFunctionsFromWindow = window["##BUDIBASE_UIFUNCTIONS##"]
uiFunctions = uiFunctionsFromWindow || uiFunctions
const userFromStorage = localStorage.getItem("budibase:user")
@ -23,11 +25,13 @@ export const loadBudibase = async ({
temp: false,
}
const rootPath =
appDefinition.appRootPath === ""
? ""
: "/" + trimSlash(appDefinition.appRootPath)
if (!componentLibraries) {
const rootPath =
appDefinition.appRootPath === ""
? ""
: "/" + trimSlash(appDefinition.appRootPath)
const componentLibraryUrl = lib => rootPath + "/" + trimSlash(lib)
componentLibraries = {}
@ -38,20 +42,36 @@ export const loadBudibase = async ({
}
}
if (!props) {
props = appDefinition.props
componentLibraries[builtinLibName] = builtins(window)
if (!page) {
page = appDefinition.page
}
const app = createApp(
if (!screens) {
screens = appDefinition.screens
}
const { initialisePage, screenStore, pageStore, routeTo, rootNode } = createApp(
window.document,
componentLibraries,
appDefinition,
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) {

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 { $ } from "../core/common"
import { renderComponent } from "./renderComponent"
import { isScreenSlot } from "./builtinComponents"
export const _initialiseChildren = initialiseOpts => (
export const initialiseChildren = initialiseOpts => (
childrenProps,
htmlElement,
anchor = null
@ -16,13 +17,12 @@ export const _initialiseChildren = initialiseOpts => (
componentLibraries,
treeNode,
appDefinition,
document,
hydrate,
onScreenSlotRendered,
} = initialiseOpts
for (let childNode of treeNode.children) {
if (childNode.unsubscribe) childNode.unsubscribe()
if (childNode.component) childNode.component.$destroy()
childNode.destroy()
}
if (hydrate) {
@ -59,6 +59,15 @@ export const _initialiseChildren = initialiseOpts => (
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) {
comp.unsubscribe = bind(comp.component)
renderedComponents.push(comp)

View file

@ -61,4 +61,16 @@ export const createTreeNode = () => ({
children: [],
component: null,
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 () => {
const { dom } = await load({
_component: "testlib/div",
className: {
"##bbstate": "divClassName",
"##bbsource": "store",
"##bbstatefallback": "default",
},
})
const { dom } = await load(
makePage({
_component: "testlib/div",
className: {
"##bbstate": "divClassName",
"##bbsource": "store",
"##bbstatefallback": "default",
},
})
)
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 () => {
const { dom, app } = await load({
_component: "testlib/div",
className: {
"##bbstate": "divClassName",
"##bbsource": "store",
"##bbstatefallback": "default",
},
})
const { dom, app } = await load(
makePage({
_component: "testlib/div",
className: {
"##bbstate": "divClassName",
"##bbsource": "store",
"##bbstatefallback": "default",
},
})
)
app.store.update(s => {
app.pageStore().update(s => {
s.divClassName = "newvalue"
return s
})
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 () => {
const { dom } = await load({
_component: "testlib/div",
_children: [
{
_component: "testlib/h1",
text: {
"##bbstate": "headerOneText",
"##bbsource": "store",
"##bbstatefallback": "header one",
const { dom } = await load(
makePage({
_component: "testlib/div",
_children: [
{
_component: "testlib/h1",
text: {
"##bbstate": "headerOneText",
"##bbsource": "store",
"##bbstatefallback": "header one",
},
},
},
{
_component: "testlib/h1",
text: {
"##bbstate": "headerTwoText",
"##bbsource": "store",
"##bbstatefallback": "header two",
{
_component: "testlib/h1",
text: {
"##bbstate": "headerTwoText",
"##bbsource": "store",
"##bbstatefallback": "header two",
},
},
},
],
})
],
})
)
const rootDiv = dom.window.document.body.children[0]
@ -67,29 +73,31 @@ describe("initialiseApp", () => {
})
it("should populate child component with store value", async () => {
const { dom, app } = await load({
_component: "testlib/div",
_children: [
{
_component: "testlib/h1",
text: {
"##bbstate": "headerOneText",
"##bbsource": "store",
"##bbstatefallback": "header one",
const { dom, app } = await load(
makePage({
_component: "testlib/div",
_children: [
{
_component: "testlib/h1",
text: {
"##bbstate": "headerOneText",
"##bbsource": "store",
"##bbstatefallback": "header one",
},
},
},
{
_component: "testlib/h1",
text: {
"##bbstate": "headerTwoText",
"##bbsource": "store",
"##bbstatefallback": "header two",
{
_component: "testlib/h1",
text: {
"##bbstate": "headerTwoText",
"##bbsource": "store",
"##bbstatefallback": "header two",
},
},
},
],
})
],
})
)
app.store.update(s => {
app.pageStore().update(s => {
s.headerOneText = "header 1 - new val"
s.headerTwoText = "header 2 - new val"
return s
@ -103,4 +111,62 @@ describe("initialiseApp", () => {
expect(rootDiv.children[1].tagName).toBe("H1")
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", () => {
it("should display simple div, with always true render function", async () => {
const { dom } = await load({
_component: "testlib/div",
className: "my-test-class",
_id: "always_render",
})
const { dom } = await load(
makePage({
_component: "testlib/div",
className: "my-test-class",
_id: "always_render",
})
)
expect(dom.window.document.body.children.length).toBe(1)
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 () => {
const { dom } = await load({
_component: "testlib/div",
className: "my-test-class",
_id: "never_render",
})
const { dom } = await load(
makePage({
_component: "testlib/div",
className: "my-test-class",
_id: "never_render",
})
)
expect(dom.window.document.body.children.length).toBe(0)
})
it("should display 3 divs in a looped render function", async () => {
const { dom } = await load({
_component: "testlib/div",
className: "my-test-class",
_id: "three_clones",
})
const { dom } = await load(
makePage({
_component: "testlib/div",
className: "my-test-class",
_id: "three_clones",
})
)
expect(dom.window.document.body.children.length).toBe(3)
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]
expect(child1.className).toBe("my-test-class")
expect(child1.className.includes("my-test-class")).toBeTruthy()
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 () => {
const { dom } = await load({
_component: "testlib/div",
_children: [
{
_component: "testlib/div",
className: "my-test-class",
_id: "three_clones",
},
],
})
const { dom } = await load(
makePage({
_component: "testlib/div",
_children: [
{
_component: "testlib/div",
className: "my-test-class",
_id: "three_clones",
},
],
})
)
expect(dom.window.document.body.children.length).toBe(1)
const rootDiv = dom.window.document.body.children[0]
expect(rootDiv.children.length).toBe(3)
expect(rootDiv.children[0].className).toBe("my-test-class")
expect(rootDiv.children[1].className).toBe("my-test-class")
expect(rootDiv.children[2].className).toBe("my-test-class")
expect(rootDiv.children[0].className.includes("my-test-class")).toBeTruthy()
expect(rootDiv.children[1].className.includes("my-test-class")).toBeTruthy()
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", () => {
it("should populate simple div with initial props", async () => {
const { dom } = await load({
_component: "testlib/div",
className: "my-test-class",
})
const { dom } = await load(
makePage({
_component: "testlib/div",
className: "my-test-class",
})
)
expect(dom.window.document.body.children.length).toBe(1)
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 () => {
const { dom } = await load({
_component: "testlib/div",
_children: [
{
_component: "testlib/h1",
text: "header one",
},
{
_component: "testlib/h1",
text: "header two",
},
],
})
const { dom } = await load(
makePage({
_component: "testlib/div",
_children: [
{
_component: "testlib/h1",
text: "header one",
},
{
_component: "testlib/h1",
text: "header two",
},
],
})
)
const rootDiv = dom.window.document.body.children[0]
@ -37,20 +41,22 @@ describe("initialiseApp", () => {
})
it("should append children when told to do so", async () => {
const { dom } = await load({
_component: "testlib/div",
_children: [
{
_component: "testlib/h1",
text: "header one",
},
{
_component: "testlib/h1",
text: "header two",
},
],
append: true,
})
const { dom } = await load(
makePage({
_component: "testlib/div",
_children: [
{
_component: "testlib/h1",
text: "header one",
},
{
_component: "testlib/h1",
text: "header two",
},
],
append: true,
})
)
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].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 { loadBudibase } from "../src/index"
export const load = async props => {
const dom = new JSDOM(`<!DOCTYPE html><html><body></body><html>`)
autoAssignIds(props)
setAppDef(dom.window, props)
export const load = async (page, screens = [], url = "/") => {
const dom = new JSDOM("<!DOCTYPE html><html><body></body><html>", {
url: `http://test${url}`,
})
autoAssignIds(page.props)
for (let s of screens) {
autoAssignIds(s.props)
}
setAppDef(dom.window, page, screens)
const app = await loadBudibase({
componentLibraries: allLibs(dom.window),
window: dom.window,
localStorage: createLocalStorage(),
props,
page,
screens,
uiFunctions,
})
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 only assigns _ids when missing
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##"] = {
componentLibraries: [],
props,
page,
screens,
hierarchy: {},
appRootPath: "",
}
@ -79,6 +101,8 @@ const maketestlib = window => ({
}
}
this.$destroy = () => opts.target.removeChild(node)
this.$set = set
this._element = node
set(opts.props)
@ -97,6 +121,8 @@ const maketestlib = window => ({
}
}
this.$destroy = () => opts.target.removeChild(node)
this.$set = set
this._element = node
set(opts.props)
@ -105,13 +131,13 @@ const maketestlib = window => ({
})
const uiFunctions = {
never_render: (render, parentContext) => {},
never_render: () => {},
always_render: (render, parentContext) => {
always_render: render => {
render()
},
three_clones: (render, parentContext) => {
three_clones: render => {
for (let i = 0; i < 3; i++) {
render()
}

View file

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

View file

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

View file

@ -1,70 +1,38 @@
export default class ClassBuilder {
constructor(block, defaultIgnoreList) {
this.block = `mdc-${block}`;
this.defaultIgnoreList = defaultIgnoreList; //will be ignored when building custom classes
constructor(block, customDefaults) {
this.block = `mdc-${block}`
this.customDefaults = customDefaults //will be ignored when building custom classes
}
/*
handles both blocks and elementss (BEM MD Notation)
params = {elementName: string, props: {modifiers{}, customs:{}, extras: []}}
All are optional
*/
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);
// classParams: {modifiers:[] (mdc), custom:[] (bbmd), extra:[] (any)}
blocks(classParams) {
let base = this.block
if (classParams == undefined) return base
return this.buildClass(base, classParams)
}
//Easily grab a simple element class
elem(elementName) {
return this.build({ elementName });
//elementName: string, classParams: {}
elements(elementName, classParams) {
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
debase(base, elementProps) {
if (!elementProps) return base;
return this._handleProps(base, elementProps);
}
//proxies bindProps and checks for which elementProps exist before binding
_handleProps(base, elementProps) {
let cls = base;
const { modifiers, customs, extras } = elementProps;
if (!!modifiers) cls += this._bindProps(modifiers, base);
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}`;
buildClass(base, classParams) {
let cls = base
const { modifiers, customs, extras } = classParams
if (modifiers) cls += modifiers.map(m => ` ${base}--${m}`).join(" ")
if (customs)
cls += Object.entries(customs)
.map(([property, value]) => {
//disregard falsy and values set by customDefaults constructor param
if (!!value && !this.customDefaults.includes(value)) {
//custom scss name convention = bbmd-[block | element]--[property]-[value]
return ` bbmd-${base}--${property}-${value}`
}
}
})
.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(
node,
props = { colour: "primary", unbounded: false }
) {
node.classList.add("mdc-ripple-surface");
const component = new MDCRipple(node);
component.unbounded = props.unbounded;
node.classList.add("mdc-ripple-surface")
const component = new MDCRipple(node)
component.unbounded = props.unbounded
if (props.colour === "secondary") {
node.classList.remove("mdc-ripple-surface--primary");
node.classList.add("mdc-ripple-surface--accent");
node.classList.remove("mdc-ripple-surface--primary")
node.classList.add("mdc-ripple-surface--accent")
} else {
node.classList.add("mdc-ripple-surface--primary");
node.classList.remove("mdc-ripple-surface--accent");
node.classList.add("mdc-ripple-surface--primary")
node.classList.remove("mdc-ripple-surface--accent")
}
return {
destroy() {
component.destroy();
node.classList.remove("mdc-ripple-surface");
node.classList.remove("mdc-ripple-surface--primary");
node.classList.remove("mdc-ripple-surface--accent");
component = null;
}
};
component.destroy()
node.classList.remove("mdc-ripple-surface")
node.classList.remove("mdc-ripple-surface--primary")
node.classList.remove("mdc-ripple-surface--accent")
component = null
},
}
}

View file

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

View file

@ -1,3 +1,4 @@
import { button, icon, textfield, H1, Overline } from "@BBMD";
export default { H1, Overline, button, icon, textfield };
import h1 from "../H1.svelte"
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 icon } from "./Icon.svelte";
export { button } from "./Button";
export { textfield } from "./Textfield";
export * from "./Typography"
export { default as h1 } from "./H1.svelte"
export { default as icon } from "./Icon.svelte"
export { button } from "./Button"

View file

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

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##"] = {
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",
},
{
importPath:
"/lib/node_modules/@budibase/standard-components/dist/index.js",
libName: "@budibase/standard-components",
},
],
appRootPath: "",
props: { _component: "some_component" },
}
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"}]};
window['##BUDIBASE_UIFUNCTIONS##'] = {'1234':() => 'test return'}

View file

@ -4,8 +4,8 @@
<meta charset='utf8'>
<meta name='viewport' content='width=device-width'>
<title>Budibase App</title>
<link rel='icon' type='image/png' href='//_shared/favicon.png'>
<title>Test App</title>
<link rel='icon' type='image/png' href='/./_shared/favicon.png'>
<style>
html, body {
@ -14,9 +14,21 @@
}
</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='/budibase-client.js'></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 {
getPackageForBuilder,
getComponents,
savePackage,
getApps,
saveScreen,
renameScreen,
deleteScreen,
savePagePackage,
componentLibraryInfo,
listScreens,
} = require("../utilities/builder")
const builderPath = resolve(__dirname, "../builder")
@ -20,8 +21,6 @@ const builderPath = resolve(__dirname, "../builder")
module.exports = (config, app) => {
const router = new Router()
const prependSlash = path => (path.startsWith("/") ? path : `/${path}`)
router
.use(session(config, app))
.use(async (ctx, next) => {
@ -95,7 +94,7 @@ module.exports = (config, app) => {
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(
ctx.sessionId,
ctx.params.appname,
@ -149,10 +148,6 @@ module.exports = (config, app) => {
ctx.body = await getPackageForBuilder(config, ctx.params.appname)
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 => {
try {
ctx.body = getComponents(config, ctx.params.appname, ctx.query.lib)
@ -184,26 +179,55 @@ module.exports = (config, app) => {
ctx.body = info.generators
ctx.response.status = StatusCodes.OK
})
.post("/_builder/api/:appname/screen", async ctx => {
await saveScreen(config, ctx.params.appname, ctx.request.body)
.post("/_builder/api/:appname/pages/:pageName", async ctx => {
await savePagePackage(
config,
ctx.params.appname,
ctx.params.pageName,
ctx.request.body
)
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(
config,
ctx.params.appname,
ctx.params.pagename,
ctx.request.body.oldname,
ctx.request.body.newname
)
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(
`/_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
})
.get("/:appname", async ctx => {

View file

@ -1,25 +1,26 @@
const testAppDef = require("../appPackages/testApp/appDefinition.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 testMoreComponents = require("../appPackages/testApp/moreCustomComponents/components.json")
const statusCodes = require("../utilities/statusCodes")
const screen1 = require("../appPackages/testApp/components/myTextBox.json")
const screen2 = require("../appPackages/testApp/components/subfolder/otherTextBox.json")
const { readJSON, pathExists, unlink } = require("fs-extra")
const screen1 = require("../appPackages/testApp/pages/main/screens/screen1.json")
const screen2 = require("../appPackages/testApp/pages/main/screens/screen2.json")
const { readJSON, pathExists, unlink, readFile } = require("fs-extra")
const { getHashedCssPaths } = require("../utilities/builder/convertCssToFiles")
const app = require("./testApp")()
testComponents.textbox.name = `./customComponents/textbox`
testMoreComponents.textbox.name = `./moreCustomComponents/textbox`
beforeAll(async () => {
const testComponent = "./appPackages/testApp/components/newTextBox.json"
const testComponentAfterMove =
"./appPackages/testApp/components/anotherSubFolder/newTextBox.json"
const testScreen = "./appPackages/testApp/pages/main/screens/newscreen.json"
const testScreenAfterMove =
"./appPackages/testApp/pages/main/screens/anotherscreen.json"
if (await pathExists(testComponent)) await unlink(testComponent)
if (await pathExists(testComponentAfterMove))
await unlink(testComponentAfterMove)
if (await pathExists(testScreen)) await unlink(testScreen)
if (await pathExists(testScreenAfterMove)) await unlink(testScreenAfterMove)
await app.start()
})
@ -45,7 +46,10 @@ it("/apppackage should get pages", async () => {
const { body } = await app
.get("/_builder/api/testApp/appPackage")
.expect(statusCodes.OK)
expect(body.pages).toEqual(testPages)
expect(body.pages).toEqual({
main: mainPage,
unauthenticated: unauthenticatedPage,
})
})
it("/apppackage should get components", async () => {
@ -53,123 +57,127 @@ it("/apppackage should get components", async () => {
.get("/_builder/api/testApp/appPackage")
.expect(statusCodes.OK)
expect(body.components["./customComponents/textbox"]).toBeDefined()
expect(body.components["./moreCustomComponents/textbox"]).toBeDefined()
expect(body.components.components["./customComponents/textbox"]).toBeDefined()
expect(
body.components.components["./moreCustomComponents/textbox"]
).toBeDefined()
expect(body.components["./customComponents/textbox"]).toEqual(
expect(body.components.components["./customComponents/textbox"]).toEqual(
testComponents.textbox
)
expect(body.components["./moreCustomComponents/textbox"]).toEqual(
expect(body.components.components["./moreCustomComponents/textbox"]).toEqual(
testMoreComponents.textbox
)
})
it("/apppackage should get screens", async () => {
it("/pages/:pageName/screens should get screens", async () => {
const { body } = await app
.get("/_builder/api/testApp/appPackage")
.get("/_builder/api/testApp/pages/main/screens")
.expect(statusCodes.OK)
const expectedComponents = {
myTextBox: { ...screen1, name: "myTextBox" },
"subfolder/otherTextBox": { ...screen2, name: "subfolder/otherTextBox" },
screen1: { ...screen1, name: "screen1" },
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 = {
name: "newTextBox",
inherits: "./customComponents/textbox",
name: "newscreen",
props: {
label: "something",
_component: "@budibase/standard-component/div",
className: "something",
},
}
await app
.post("/_builder/api/testApp/screen", newscreen)
.post("/_builder/api/testApp/pages/main/screen", newscreen)
.expect(statusCodes.OK)
const componentFile = "./appPackages/testApp/components/newTextBox.json"
expect(await pathExists(componentFile)).toBe(true)
expect(await readJSON(componentFile)).toEqual(newscreen)
const screenFile = "./appPackages/testApp/pages/main/screens/newscreen.json"
expect(await pathExists(screenFile)).toBe(true)
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 = {
name: "newTextBox",
inherits: "./customComponents/textbox",
name: "newscreen",
props: {
label: "something else",
_component: "@budibase/standard-component/div",
className: "something else",
},
}
await app
.post("/_builder/api/testApp/screen", updatedscreen)
.post("/_builder/api/testApp/pages/main/screen", updatedscreen)
.expect(statusCodes.OK)
const componentFile = "./appPackages/testApp/components/newTextBox.json"
expect(await readJSON(componentFile)).toEqual(updatedscreen)
const screenFile = "./appPackages/testApp/pages/main/screens/newscreen.json"
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
.patch("/_builder/api/testApp/screen", {
oldname: "newTextBox",
newname: "anotherSubFolder/newTextBox",
.patch("/_builder/api/testApp/pages/main/screen", {
oldname: "newscreen",
newname: "anotherscreen",
})
.expect(statusCodes.OK)
const oldcomponentFile = "./appPackages/testApp/components/newTextBox.json"
const oldcomponentFile =
"./appPackages/testApp/pages/main/screens/newscreen.json"
const newcomponentFile =
"./appPackages/testApp/components/anotherSubFolder/newTextBox.json"
"./appPackages/testApp/pages/main/screens/anotherscreen.json"
expect(await pathExists(oldcomponentFile)).toBe(false)
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
.delete("/_builder/api/testApp/screen/anotherSubFolder/newTextBox")
.delete("/_builder/api/testApp/pages/main/screen/anotherscreen")
.expect(statusCodes.OK)
const componentFile =
"./appPackages/testApp/components/anotherSubFolder/newTextBox.json"
const componentDir = "./appPackages/testApp/components/anotherSubFolder"
"./appPackages/testApp/pages/main/screens/anotherscreen.json"
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
.post("/_builder/api/testApp/appPackage", {
.post("/_builder/api/testApp/pages/main", {
appDefinition: testAppDef,
accessLevels: testAccessLevels,
pages: testPages,
page: mainPage,
uiFunctions: "{'1234':() => 'test return'}",
screens: [screen1, screen2],
})
.expect(statusCodes.OK)
const publicFolderMain = 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(publicFolderUnauth("/index.html"))).toBe(true)
expect(
await pathExists(publicFolderMain("/lib/customComponents/index.js"))
).toBe(true)
expect(
await pathExists(publicFolderUnauth("/lib/customComponents/index.js"))
).toBe(true)
expect(
await pathExists(publicFolderMain("/lib/moreCustomComponents/index.js"))
).toBe(true)
expect(
await pathExists(publicFolderUnauth("/lib/moreCustomComponents/index.js"))
).toBe(true)
expect(
await pathExists(
@ -178,16 +186,34 @@ it("/savePackage should prepare all necessary client files", async () => {
)
)
).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)
expect(await pathExists(publicFolderUnauth("/clientAppDefinition.js"))).toBe(
true
const indexHtmlMain = await readFile(publicFolderMain("/index.html"), "utf8")
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")
const { join, resolve, dirname } = require("path")
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)
await buildClientAppDefinition(
config,
appname,
appdefinition,
appPath,
pages,
"main"
)
await convertCssToFiles(publicPath(appPath, pkg.pageName), pkg)
await buildClientAppDefinition(
config,
appname,
appdefinition,
appPath,
pages,
"unauthenticated"
)
await buildIndexHtml(config, appname, appPath, pkg)
await buildIndexHtml(config, appname, appPath, pages, "main")
await buildClientAppDefinition(config, appname, pkg, appPath)
await buildIndexHtml(config, appname, appPath, pages, "unauthenticated")
await copyClientLib(appPath, "main")
await copyClientLib(appPath, "unauthenticated")
await copyClientLib(appPath, pkg.pageName)
}
const publicPath = (appPath, pageName) => join(appPath, "public", pageName)
@ -46,10 +30,11 @@ const rootPath = (config, appname) =>
config.useAppRootPath ? `/${appname}` : ""
const copyClientLib = async (appPath, pageName) => {
var sourcepath = require.resolve("@budibase/client")
var destPath = join(publicPath(appPath, pageName), "budibase-client.js")
const sourcepath = require.resolve("@budibase/client")
const destPath = join(publicPath(appPath, pageName), "budibase-client.js")
await copyFile(sourcepath, destPath, constants.COPYFILE_FICLONE)
await copyFile(
sourcepath + ".map",
destPath + ".map",
@ -57,8 +42,8 @@ const copyClientLib = async (appPath, pageName) => {
)
}
const buildIndexHtml = async (config, appname, appPath, pages, pageName) => {
const appPublicPath = publicPath(appPath, pageName)
const buildIndexHtml = async (config, appname, appPath, pkg) => {
const appPublicPath = publicPath(appPath, pkg.pageName)
const appRootPath = rootPath(config, appname)
const stylesheetUrl = s =>
@ -67,10 +52,11 @@ const buildIndexHtml = async (config, appname, appPath, pages, pageName) => {
: `/${rootPath(config, appname)}/${s}`
const templateObj = {
title: pages[pageName].index.title || "Budibase App",
favicon: `${appRootPath}/${pages[pageName].index.favicon ||
"/_shared/favicon.png"}`,
stylesheets: (pages.stylesheets || []).map(stylesheetUrl),
title: pkg.page.title || "Budibase App",
favicon: `${appRootPath}/${pkg.page.favicon || "/_shared/favicon.png"}`,
stylesheets: (pkg.page.stylesheets || []).map(stylesheetUrl),
screenStyles: pkg.screens.filter(s => s._css).map(s => s._css),
pageStyle: pkg.page._css,
appRootPath,
}
@ -86,20 +72,14 @@ const buildIndexHtml = async (config, appname, appPath, pages, pageName) => {
await writeFile(indexHtmlPath, indexHtml, { flag: "w+" })
}
const buildClientAppDefinition = async (
config,
appname,
appdefinition,
appPath,
pages,
pageName
) => {
const appPublicPath = publicPath(appPath, pageName)
const buildClientAppDefinition = async (config, appname, pkg) => {
const appPath = appPackageFolder(config, appname)
const appPublicPath = publicPath(appPath, pkg.pageName)
const appRootPath = rootPath(config, appname)
const componentLibraries = []
for (let lib of pages.componentLibraries) {
for (let lib of pkg.page.componentLibraries) {
const info = await componentLibraryInfo(appPath, lib)
const libFile = info.components._lib || "index.js"
const source = join(info.libDir, libFile)
@ -131,16 +111,27 @@ const buildClientAppDefinition = async (
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 = {
hierarchy: appdefinition.hierarchy,
hierarchy: pkg.appDefinition.hierarchy,
componentLibraries: componentLibraries,
appRootPath: appRootPath,
props: appdefinition.props[pageName],
page: pkg.page,
screens: pkg.screens,
}
await writeFile(
filename,
`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")
const { join, dirname } = require("path")
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 { componentLibraryInfo } = require("./componentLibraryInfo")
const savePackage = require("./savePackage")
const buildApp = require("./buildApp")
const savePagePackage = require("./savePagePackage")
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 =>
await readJSON(`${appPath}/appDefinition.json`)
@ -37,8 +36,6 @@ module.exports.getPackageForBuilder = async (config, appname) => {
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)])
}
const componentPath = (appPath, name) =>
join(appPath, "components", name + ".json")
const getPages = async appPath => {
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 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 writeJSON(compPath, component, {
if (screen._css) {
delete screen._css
}
await writeJSON(compPath, screen, {
encoding: "utf8",
flag: "w",
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 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 rename(oldComponentPath, newComponentPath)
}
module.exports.deleteScreen = async (config, appname, name) => {
module.exports.deleteScreen = async (config, appname, pagename, name) => {
const appPath = appPackageFolder(config, appname)
const componentFile = componentPath(appPath, name)
const componentFile = screenPath(appPath, pagename, name)
await unlink(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) => {
const appPath = appPackageFolder(config, appname)
return await componentLibraryInfo(appPath, lib)
@ -92,11 +134,11 @@ module.exports.componentLibraryInfo = async (config, appname, lib) => {
const getComponents = async (appPath, pages, lib) => {
let libs
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 {
libs = [lib]
}
@ -116,12 +158,12 @@ const getComponents = async (appPath, pages, lib) => {
return { components, generators }
}
const fetchscreens = async (appPath, relativePath = "") => {
const currentDir = join(appPath, "components", relativePath)
const fetchscreens = async (appPath, pagename, relativePath = "") => {
const currentDir = join(appPath, "pages", pagename, "screens", relativePath)
const contents = await readdir(currentDir)
const components = []
const screens = []
for (let item of contents) {
const itemRelativePath = join(relativePath, item)
@ -139,7 +181,7 @@ const fetchscreens = async (appPath, relativePath = "") => {
component.props = component.props || {}
components.push(component)
screens.push(component)
} else {
const childComponents = await fetchscreens(
appPath,
@ -147,12 +189,12 @@ const fetchscreens = async (appPath, relativePath = "") => {
)
for (let c of childComponents) {
components.push(c)
screens.push(c)
}
}
}
return components
return screens
}
module.exports.getComponents = getComponents

View file

@ -18,6 +18,15 @@
<link rel='stylesheet' href='{{ @this }}'>
{{ /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 }}/budibase-client.js'></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