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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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", () => { it("should include components in page apbody", () => {
const { components, screens } = componentsAndScreens()
const result = componentDependencies(
{},
screens,
components,
get([...components, ...screens], "budibase-components/Button")
)
expect(contains(result.dependantComponents, "ButtonGroup")).toBe(true)
})
it("should include components n page apbody", () => {
const { components, screens } = componentsAndScreens() const { components, screens } = componentsAndScreens()
const pages = { const pages = {
main: { main: {

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

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: { props: {
_component: "budibase-components/div", _component: "budibase-components/div",
width: 100, width: 100,
@ -99,3 +100,9 @@ export const componentsAndScreens = () => ({
}, },
], ],
}) })
export const stripStandardProps = props => {
delete props._code
delete props._id
delete props._styles
}

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 remove(join(destinationFolder, ...args, "placeholder"))
} }
await removePlaceholder("components") await removePlaceholder("pages", "main", "screens")
await removePlaceholder("pages", "unauthenticated", "screens")
await removePlaceholder("public", "shared") await removePlaceholder("public", "shared")
await removePlaceholder("public", "main") await removePlaceholder("public", "main")
await removePlaceholder("public", "unauthenticated") await removePlaceholder("public", "unauthenticated")

1
packages/client/package.json

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

146
packages/client/src/createApp.js

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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( export default function ripple(
node, node,
props = { colour: "primary", unbounded: false } props = { colour: "primary", unbounded: false }
) { ) {
node.classList.add("mdc-ripple-surface"); node.classList.add("mdc-ripple-surface")
const component = new MDCRipple(node); const component = new MDCRipple(node)
component.unbounded = props.unbounded; component.unbounded = props.unbounded
if (props.colour === "secondary") { if (props.colour === "secondary") {
node.classList.remove("mdc-ripple-surface--primary"); node.classList.remove("mdc-ripple-surface--primary")
node.classList.add("mdc-ripple-surface--accent"); node.classList.add("mdc-ripple-surface--accent")
} else { } else {
node.classList.add("mdc-ripple-surface--primary"); node.classList.add("mdc-ripple-surface--primary")
node.classList.remove("mdc-ripple-surface--accent"); node.classList.remove("mdc-ripple-surface--accent")
} }
return { return {
destroy() { destroy() {
component.destroy(); component.destroy()
node.classList.remove("mdc-ripple-surface"); node.classList.remove("mdc-ripple-surface")
node.classList.remove("mdc-ripple-surface--primary"); node.classList.remove("mdc-ripple-surface--primary")
node.classList.remove("mdc-ripple-surface--accent"); node.classList.remove("mdc-ripple-surface--accent")
component = null; component = null
} },
}; }
} }

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

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

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

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

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

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

6
packages/server/.gitignore

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

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

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

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

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

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

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

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

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

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

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

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