Browse Source

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>
pull/4023/head
Michael Shanks 6 years ago
committed by GitHub
parent
commit
8a80d8801a
  1. 71
      packages/bootstrap-components/dist/generators.js
  2. 229
      packages/builder/src/builderStore/store.js
  3. 2
      packages/builder/src/common/Inputs/InputGroup.svelte
  4. 42
      packages/builder/src/userInterface/ComponentPanel.svelte
  5. 60
      packages/builder/src/userInterface/ComponentsHierarchy.svelte
  6. 9
      packages/builder/src/userInterface/ComponentsHierarchyChildren.svelte
  7. 57
      packages/builder/src/userInterface/ComponentsPaneSwitcher.svelte
  8. 58
      packages/builder/src/userInterface/CurrentItemPreview.svelte
  9. 2
      packages/builder/src/userInterface/EditComponentProps.svelte
  10. 14
      packages/builder/src/userInterface/LayoutEditor.svelte
  11. 29
      packages/builder/src/userInterface/NewComponent.svelte
  12. 3
      packages/builder/src/userInterface/PagesComponents.svelte
  13. 14
      packages/builder/src/userInterface/PropsView.svelte
  14. 59
      packages/builder/src/userInterface/UserInterfaceRoot.svelte
  15. 57
      packages/builder/src/userInterface/pagesParsing/buildPropsHierarchy.js
  16. 92
      packages/builder/src/userInterface/pagesParsing/createProps.js
  17. 4
      packages/builder/src/userInterface/pagesParsing/defaultPagesObject.js
  18. 25
      packages/builder/tests/buildPropsHierarchy.spec.js
  19. 15
      packages/builder/tests/componentDependencies.spec.js
  20. 16
      packages/builder/tests/createProps.spec.js
  21. 91
      packages/builder/tests/getComponentInfo.spec.js
  22. 30
      packages/builder/tests/getNewScreen.spec.js
  23. 9
      packages/builder/tests/testData.js
  24. 18
      packages/cli/src/commands/new/appPackageTemplate/pages/main/page.json
  25. 0
      packages/cli/src/commands/new/appPackageTemplate/pages/main/screens/placeholder
  26. 18
      packages/cli/src/commands/new/appPackageTemplate/pages/unauthenticated/page.json
  27. 1
      packages/cli/src/commands/new/appPackageTemplate/pages/unauthenticated/screens/placeholder
  28. 3
      packages/cli/src/commands/new/newHandler.js
  29. 1
      packages/client/package.json
  30. 146
      packages/client/src/createApp.js
  31. 44
      packages/client/src/index.js
  32. 10
      packages/client/src/render/builtinComponents.js
  33. 17
      packages/client/src/render/initialiseChildren.js
  34. 12
      packages/client/src/render/renderComponent.js
  35. 73
      packages/client/src/render/screenRouter.js
  36. 14
      packages/client/src/render/screenSlotComponent.js
  37. 186
      packages/client/tests/bindingDom.spec.js
  38. 74
      packages/client/tests/domControlFlow.spec.js
  39. 139
      packages/client/tests/initialiseApp.spec.js
  40. 137
      packages/client/tests/screenRouting.spec.js
  41. 46
      packages/client/tests/testAppDef.js
  42. 2
      packages/materialdesign-components/rollup.testconfig.js
  43. 4
      packages/materialdesign-components/src/Button/index.js
  44. 88
      packages/materialdesign-components/src/ClassBuilder.js
  45. 30
      packages/materialdesign-components/src/Ripple.js
  46. 8
      packages/materialdesign-components/src/Test/props.js
  47. 5
      packages/materialdesign-components/src/Test/testComponents.js
  48. 9
      packages/materialdesign-components/src/index.js
  49. 6
      packages/server/.gitignore
  50. 51684
      packages/server/appPackages/_master/public/main/budibase-client.js
  51. 2
      packages/server/appPackages/_master/public/main/budibase-client.js.map
  52. 51684
      packages/server/appPackages/_master/public/unauthenticated/budibase-client.js
  53. 2
      packages/server/appPackages/_master/public/unauthenticated/budibase-client.js.map
  54. 25
      packages/server/appPackages/testApp/pages.json
  55. 14
      packages/server/appPackages/testApp/pages/main/page.json
  56. 8
      packages/server/appPackages/testApp/pages/main/screens/screen1.json
  57. 8
      packages/server/appPackages/testApp/pages/main/screens/screen2.json
  58. 9
      packages/server/appPackages/testApp/pages/unauthenticated/page.json
  59. 0
      packages/server/appPackages/testApp/pages/unauthenticated/screens/Button.json
  60. 0
      packages/server/appPackages/testApp/pages/unauthenticated/screens/LoginForm.json
  61. 0
      packages/server/appPackages/testApp/pages/unauthenticated/screens/joeTextBox.json
  62. 0
      packages/server/appPackages/testApp/pages/unauthenticated/screens/myTextBox.json
  63. 0
      packages/server/appPackages/testApp/pages/unauthenticated/screens/subfolder/otherTextBox.json
  64. 51376
      packages/server/appPackages/testApp/public/main/budibase-client.js
  65. 2
      packages/server/appPackages/testApp/public/main/budibase-client.js.map
  66. 81
      packages/server/appPackages/testApp/public/main/clientAppDefinition.js
  67. 1
      packages/server/appPackages/testApp/public/main/css/7b7c05b78e05c06eb8d69475caadfea3.css
  68. 1
      packages/server/appPackages/testApp/public/main/css/d121e1ecc6cf44f433213222e9ff5d40.css
  69. 1
      packages/server/appPackages/testApp/public/main/css/f66fc2928f7d850c946e619c1a1f3096.css
  70. 20
      packages/server/appPackages/testApp/public/main/index.html
  71. 51684
      packages/server/appPackages/testApp/public/unauthenticated/budibase-client.js
  72. 2
      packages/server/appPackages/testApp/public/unauthenticated/budibase-client.js.map
  73. 51684
      packages/server/appPackages/testApp2/public/main/budibase-client.js
  74. 2
      packages/server/appPackages/testApp2/public/main/budibase-client.js.map
  75. 51684
      packages/server/appPackages/testApp2/public/unauthenticated/budibase-client.js
  76. 2
      packages/server/appPackages/testApp2/public/unauthenticated/budibase-client.js.map
  77. BIN
      packages/server/builder/assets/budibase-logo-only.png
  78. BIN
      packages/server/builder/assets/budibase-logo-white.png
  79. BIN
      packages/server/builder/assets/budibase-logo.png
  80. BIN
      packages/server/builder/assets/roboto-v20-latin-ext_latin-300.woff
  81. BIN
      packages/server/builder/assets/roboto-v20-latin-ext_latin-300.woff2
  82. BIN
      packages/server/builder/assets/roboto-v20-latin-ext_latin-500.woff
  83. BIN
      packages/server/builder/assets/roboto-v20-latin-ext_latin-500.woff2
  84. BIN
      packages/server/builder/assets/roboto-v20-latin-ext_latin-700.woff
  85. BIN
      packages/server/builder/assets/roboto-v20-latin-ext_latin-700.woff2
  86. BIN
      packages/server/builder/assets/roboto-v20-latin-ext_latin-900.woff
  87. BIN
      packages/server/builder/assets/roboto-v20-latin-ext_latin-900.woff2
  88. BIN
      packages/server/builder/assets/roboto-v20-latin-ext_latin-regular.woff
  89. BIN
      packages/server/builder/assets/roboto-v20-latin-ext_latin-regular.woff2
  90. 52
      packages/server/middleware/routers.js
  91. 162
      packages/server/tests/builder.spec.js
  92. 77
      packages/server/utilities/builder/buildPage.js
  93. 43
      packages/server/utilities/builder/convertCssToFiles.js
  94. 94
      packages/server/utilities/builder/index.js
  95. 9
      packages/server/utilities/builder/index.template.html
  96. 18
      packages/server/utilities/builder/savePackage.js
  97. 30
      packages/server/utilities/builder/savePagePackage.js
  98. 35
      packages/standard-components/dist/generators.js

71
packages/bootstrap-components/dist/generators.js

File diff suppressed because one or more lines are too long

229
packages/builder/src/builderStore/store.js

@ -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, newScreen)
})
}
const setCurrentScreen = store => screenName => {
store.update(s => {
const screen = getExactComponent(s.screens, screenName)
s.currentPreviewItem = screen
s.currentFrontEndType = "screen"
s.currentComponentInfo = screen.props
return _saveScreen(store, s, newComponentInfo.component)
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
),
},
uiFunctions: buildCodeForScreens(s.screens),
}
const savePackage = async (store, s) => {
const page = s.pages[s.currentPageName]
const data = {
appDefinition,
await api.post(`/_builder/api/${appname}/pages/${s.currentPageName}`, {
appDefinition: {
hierarchy: s.hierarchy,
actions: s.actions,
triggers: s.triggers,
},
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
})
}

2
packages/builder/src/common/Inputs/InputGroup.svelte

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

42
packages/builder/src/userInterface/ComponentPanel.svelte

@ -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">
{#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}
<CodeEditor
bind:this={codeEditor}
code={$store.currentComponentInfo._code}
onCodeChanged={store.setComponentCode} />
</div>
{:else}
<h1>This is a screen, this will be dealt with later</h1>
{/if}
<div class="component-props-container">
{#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={component._code}
onCodeChanged={store.setComponentCode} />
</div>
</div>

60
packages/builder/src/userInterface/ComponentsHierarchy.svelte

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

9
packages/builder/src/userInterface/ComponentsHierarchyChildren.svelte

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

57
packages/builder/src/userInterface/ComponentsPaneSwitcher.svelte

@ -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">
<div class="switcher">
<button
class:selected={selected === 'properties'}
on:click={() => selectTab('properties')}>
Properties
</button>
<button
class:selected={selected === 'components'}
on:click={() => selectTab('components')}>
Components
</button>
</div>
<div class="panel">
{#if selected === 'properties'}
<ComponentPanel />
{/if}
{#if selected === 'components'}
<ComponentsList />
{/if}
</div>
{#if $store.currentFrontEndType === 'page' || $store.screens.length}
<div class="switcher">
<button
class:selected={selected === 'properties'}
on:click={() => selectTab('properties')}>
Properties
</button>
<button
class:selected={selected === 'components'}
on:click={() => selectTab('components')}>
Components
</button>
</div>
<div class="panel">
{#if selected === 'properties'}
<ComponentPanel />
{/if}
{#if selected === 'components'}
<ComponentsList />
{/if}
</div>
{:else}
<p>Please create a new screen</p>
{/if}
</div>

58
packages/builder/src/userInterface/CurrentItemPreview.svelte

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

2
packages/builder/src/userInterface/EditComponentProps.svelte

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

14
packages/builder/src/userInterface/LayoutEditor.svelte

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

29
packages/builder/src/userInterface/NewComponent.svelte

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

3
packages/builder/src/userInterface/PagesComponents.svelte

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

14
packages/builder/src/userInterface/PropsView.svelte

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

59
packages/builder/src/userInterface/UserInterfaceRoot.svelte

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

57
packages/builder/src/userInterface/pagesParsing/buildPropsHierarchy.js

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

92
packages/builder/src/userInterface/pagesParsing/createProps.js

@ -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 = []

4
packages/builder/src/userInterface/pagesParsing/defaultPagesObject.js

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

25
packages/builder/tests/buildPropsHierarchy.spec.js

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

15
packages/builder/tests/componentDependencies.spec.js

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

16
packages/builder/tests/createDefaultProps.spec.js → packages/builder/tests/createProps.spec.js

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

91
packages/builder/tests/getComponentInfo.spec.js

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

30
packages/builder/tests/getNewScreen.spec.js

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

9
packages/builder/tests/testData.js

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

18
packages/cli/src/commands/new/appPackageTemplate/pages/main/page.json

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

0
packages/cli/src/commands/new/appPackageTemplate/components/placeholder → packages/cli/src/commands/new/appPackageTemplate/pages/main/screens/placeholder

18
packages/cli/src/commands/new/appPackageTemplate/pages/unauthenticated/page.json

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

1
packages/cli/src/commands/new/appPackageTemplate/pages/unauthenticated/screens/placeholder

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

3
packages/cli/src/commands/new/newHandler.js

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

1
packages/client/package.json

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

146
packages/client/src/createApp.js

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

44
packages/client/src/index.js

@ -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
}
if (!screens) {
screens = appDefinition.screens
}
const app = createApp(
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) {

10
packages/client/src/render/builtinComponents.js

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

17
packages/client/src/render/initialiseChildren.js

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

12
packages/client/src/render/renderComponent.js

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

73
packages/client/src/render/screenRouter.js

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

14
packages/client/src/render/screenSlotComponent.js

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

186
packages/client/tests/bindingDom.spec.js

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

74
packages/client/tests/domControlFlow.spec.js

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

139
packages/client/tests/initialiseApp.spec.js

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

137
packages/client/tests/screenRouting.spec.js

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

46
packages/client/tests/testAppDef.js

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

2
packages/materialdesign-components/rollup.testconfig.js

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

4
packages/materialdesign-components/src/Button/index.js

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

88
packages/materialdesign-components/src/ClassBuilder.js

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

30
packages/materialdesign-components/src/Ripple.js

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

8
packages/materialdesign-components/src/Test/props.js

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

5
packages/materialdesign-components/src/Test/testComponents.js

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

9
packages/materialdesign-components/src/index.js

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

6
packages/server/.gitignore

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

51684
packages/server/appPackages/_master/public/main/budibase-client.js

File diff suppressed because it is too large

2
packages/server/appPackages/_master/public/main/budibase-client.js.map

File diff suppressed because one or more lines are too long

51684
packages/server/appPackages/_master/public/unauthenticated/budibase-client.js

File diff suppressed because it is too large

2
packages/server/appPackages/_master/public/unauthenticated/budibase-client.js.map

File diff suppressed because one or more lines are too long

25
packages/server/appPackages/testApp/pages.json

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

14
packages/server/appPackages/testApp/pages/main/page.json

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

8
packages/server/appPackages/testApp/pages/main/screens/screen1.json

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

8
packages/server/appPackages/testApp/pages/main/screens/screen2.json

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

9
packages/server/appPackages/testApp/pages/unauthenticated/page.json

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

0
packages/server/appPackages/testApp/components/Button.json → packages/server/appPackages/testApp/pages/unauthenticated/screens/Button.json

0
packages/server/appPackages/testApp/components/LoginForm.json → packages/server/appPackages/testApp/pages/unauthenticated/screens/LoginForm.json

0
packages/server/appPackages/testApp/components/joeTextBox.json → packages/server/appPackages/testApp/pages/unauthenticated/screens/joeTextBox.json

0
packages/server/appPackages/testApp/components/myTextBox.json → packages/server/appPackages/testApp/pages/unauthenticated/screens/myTextBox.json

0
packages/server/appPackages/testApp/components/subfolder/otherTextBox.json → packages/server/appPackages/testApp/pages/unauthenticated/screens/subfolder/otherTextBox.json

51376
packages/server/appPackages/testApp/public/main/budibase-client.js

File diff suppressed because it is too large

2
packages/server/appPackages/testApp/public/main/budibase-client.js.map

File diff suppressed because one or more lines are too long

81
packages/server/appPackages/testApp/public/main/clientAppDefinition.js

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

1
packages/server/appPackages/testApp/public/main/css/7b7c05b78e05c06eb8d69475caadfea3.css

@ -0,0 +1 @@
/*screen2 css*/

1
packages/server/appPackages/testApp/public/main/css/d121e1ecc6cf44f433213222e9ff5d40.css

@ -0,0 +1 @@
/*screen1 css*/

1
packages/server/appPackages/testApp/public/main/css/f66fc2928f7d850c946e619c1a1f3096.css

@ -0,0 +1 @@
/*main page css*/

20
packages/server/appPackages/testApp/public/main/index.html

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

51684
packages/server/appPackages/testApp/public/unauthenticated/budibase-client.js

File diff suppressed because it is too large

2
packages/server/appPackages/testApp/public/unauthenticated/budibase-client.js.map

File diff suppressed because one or more lines are too long

51684
packages/server/appPackages/testApp2/public/main/budibase-client.js

File diff suppressed because it is too large

2
packages/server/appPackages/testApp2/public/main/budibase-client.js.map

File diff suppressed because one or more lines are too long

51684
packages/server/appPackages/testApp2/public/unauthenticated/budibase-client.js

File diff suppressed because it is too large

2
packages/server/appPackages/testApp2/public/unauthenticated/budibase-client.js.map

File diff suppressed because one or more lines are too long

BIN
packages/server/builder/assets/budibase-logo-only.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
packages/server/builder/assets/budibase-logo-white.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
packages/server/builder/assets/budibase-logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
packages/server/builder/assets/roboto-v20-latin-ext_latin-300.woff

Binary file not shown.

BIN
packages/server/builder/assets/roboto-v20-latin-ext_latin-300.woff2

Binary file not shown.

BIN
packages/server/builder/assets/roboto-v20-latin-ext_latin-500.woff

Binary file not shown.

BIN
packages/server/builder/assets/roboto-v20-latin-ext_latin-500.woff2

Binary file not shown.

BIN
packages/server/builder/assets/roboto-v20-latin-ext_latin-700.woff

Binary file not shown.

BIN
packages/server/builder/assets/roboto-v20-latin-ext_latin-700.woff2

Binary file not shown.

BIN
packages/server/builder/assets/roboto-v20-latin-ext_latin-900.woff

Binary file not shown.

BIN
packages/server/builder/assets/roboto-v20-latin-ext_latin-900.woff2

Binary file not shown.

BIN
packages/server/builder/assets/roboto-v20-latin-ext_latin-regular.woff

Binary file not shown.

BIN
packages/server/builder/assets/roboto-v20-latin-ext_latin-regular.woff2

Binary file not shown.

52
packages/server/middleware/routers.js

@ -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
})
.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/screen", async ctx => {
.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 => {

162
packages/server/tests/builder.spec.js

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

77
packages/server/utilities/builder/buildApp.js → packages/server/utilities/builder/buildPage.js

@ -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 buildClientAppDefinition(
config,
appname,
appdefinition,
appPath,
pages,
"unauthenticated"
)
await convertCssToFiles(publicPath(appPath, pkg.pageName), pkg)
await buildIndexHtml(config, appname, appPath, pages, "main")
await buildIndexHtml(config, appname, appPath, pkg)
await buildIndexHtml(config, appname, appPath, pages, "unauthenticated")
await buildClientAppDefinition(config, appname, pkg, appPath)
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}`
)
}

43
packages/server/utilities/builder/convertCssToFiles.js

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

94
packages/server/utilities/builder/index.js

@ -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 = {}
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.saveScreen = async (config, appname, component) => {
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

9
packages/server/utilities/builder/index.template.html

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

18
packages/server/utilities/builder/savePackage.js

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

30
packages/server/utilities/builder/savePagePackage.js

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

35
packages/standard-components/dist/generators.js

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save