Browse Source

Merge pull request #826 from Budibase/tidy-up-store

Pages and Screens to couch as well as general store tidy up
pull/828/head
Michael Drury 6 years ago
committed by GitHub
parent
commit
960ca9df8e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 33
      packages/builder/src/builderStore/buildCodeForScreens.js
  2. 6
      packages/builder/src/builderStore/getNewComponentName.js
  3. 26
      packages/builder/src/builderStore/index.js
  4. 17
      packages/builder/src/builderStore/insertCodeMetadata.js
  5. 529
      packages/builder/src/builderStore/store/frontend.js
  6. 607
      packages/builder/src/builderStore/store/index.js
  7. 74
      packages/builder/src/builderStore/storeUtils.js
  8. 4
      packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte
  9. 6
      packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte
  10. 10
      packages/builder/src/components/start/CreateAppModal.svelte
  11. 40
      packages/builder/src/components/userInterface/ComponentDropdownMenu.svelte
  12. 18
      packages/builder/src/components/userInterface/ComponentPropertiesPanel.svelte
  13. 4
      packages/builder/src/components/userInterface/ComponentSelectionList.svelte
  14. 2
      packages/builder/src/components/userInterface/ComponentsHierarchy.svelte
  15. 8
      packages/builder/src/components/userInterface/ComponentsHierarchyChildren.svelte
  16. 4
      packages/builder/src/components/userInterface/ComponentsPaneSwitcher.svelte
  17. 4
      packages/builder/src/components/userInterface/DetailScreenSelect.svelte
  18. 4
      packages/builder/src/components/userInterface/EventsEditor/StateBindingCascader.svelte
  19. 4
      packages/builder/src/components/userInterface/EventsEditor/actions/NavigateTo.svelte
  20. 4
      packages/builder/src/components/userInterface/FrontendNavigatePane.svelte
  21. 10
      packages/builder/src/components/userInterface/NewScreenModal.svelte
  22. 2
      packages/builder/src/components/userInterface/PageLayout.svelte
  23. 8
      packages/builder/src/components/userInterface/PagesList.svelte
  24. 6
      packages/builder/src/components/userInterface/ScreenDropdownMenu.svelte
  25. 6
      packages/builder/src/components/userInterface/ScreenSelect.svelte
  26. 4
      packages/builder/src/components/userInterface/SettingsView.svelte
  27. 5
      packages/builder/src/components/userInterface/pagesParsing/createProps.js
  28. 4
      packages/builder/src/components/userInterface/pagesParsing/searchComponents.js
  29. 16
      packages/builder/src/constants/index.js
  30. 2
      packages/builder/src/pages/[application]/_reset.svelte
  31. 2
      packages/builder/src/pages/[application]/automate/[automation]/_layout.svelte
  32. 12
      packages/builder/src/pages/[application]/design/[page]/[screen]/_layout.svelte
  33. 2
      packages/builder/src/pages/[application]/design/[page]/_layout.svelte
  34. 4
      packages/builder/src/pages/_layout.svelte
  35. 63
      packages/builder/tests/buildCodeForScreen.spec.js
  36. 6
      packages/server/scripts/exportAppTemplate.js
  37. 140
      packages/server/src/api/controllers/application.js
  38. 10
      packages/server/src/api/controllers/deploy/aws.js
  39. 19
      packages/server/src/api/controllers/page.js
  40. 47
      packages/server/src/api/controllers/screen.js
  41. 21
      packages/server/src/api/controllers/templates.js
  42. 25
      packages/server/src/api/controllers/user.js
  43. 4
      packages/server/src/api/index.js
  44. 2
      packages/server/src/api/routes/index.js
  45. 111
      packages/server/src/api/routes/pages.js
  46. 34
      packages/server/src/api/routes/screen.js
  47. 221
      packages/server/src/constants/pages.js
  48. 103
      packages/server/src/constants/screens.js
  49. 14
      packages/server/src/db/client.js
  50. 32
      packages/server/src/db/utils.js
  51. 1
      packages/server/src/environment.js
  52. 144
      packages/server/src/utilities/appDirectoryTemplate/pages/main/page.json
  53. 0
      packages/server/src/utilities/appDirectoryTemplate/pages/main/screens/.gitkeep
  54. 102
      packages/server/src/utilities/appDirectoryTemplate/pages/main/screens/d834fea2-1b3e-4320-ab34-f9009f5ecc59.json
  55. 68
      packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/page.json
  56. 0
      packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/screens/.gitkeep
  57. 0
      packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/screens/placeholder
  58. 1
      packages/server/src/utilities/appDirectoryTemplate/plugins.js
  59. 58
      packages/server/src/utilities/builder/compileStaticAssetsForPage.js
  60. 9
      packages/server/src/utilities/builder/deleteCodeMeta.js
  61. 20
      packages/server/src/utilities/builder/getPages.js
  62. 98
      packages/server/src/utilities/builder/index.js
  63. 48
      packages/server/src/utilities/builder/listScreens.js
  64. 1
      packages/server/src/utilities/builder/setBuilderToken.js
  65. 5
      packages/server/src/utilities/createAppPackage.js
  66. 54
      packages/server/src/utilities/templates.js

33
packages/builder/src/builderStore/buildCodeForScreens.js

@ -1,33 +0,0 @@
const buildCodeForSingleScreen = screen => {
let code = ""
const walkProps = props => {
if (props._code && props._code.trim().length > 0) {
code += buildComponentCode(props)
}
if (!props._children) return
for (let child of props._children) {
walkProps(child)
}
}
walkProps(screen.props)
return code
}
export const buildCodeForScreens = screens => {
let allfunctions = ""
for (let screen of screens) {
allfunctions += buildCodeForSingleScreen(screen)
}
return `({ ${allfunctions} });`
}
const buildComponentCode = componentProps =>
`"${componentProps._id}" : (render, context, state, route) => {
${componentProps._code}
},
`

6
packages/builder/src/builderStore/getNewComponentName.js

@ -1,5 +1,7 @@
import { walkProps } from "./storeUtils"
import { get_capitalised_name } from "../helpers"
import { get } from "svelte/store"
import { allScreens } from "builderStore"
export default function(component, state) {
const capitalised = get_capitalised_name(
@ -25,7 +27,7 @@ export default function(component, state) {
findMatches(state.currentPreviewItem.props)
} else {
// viewing master page - need to find against all screens
for (let screen of state.screens) {
for (let screen of get(allScreens)) {
findMatches(screen.props)
}
}
@ -33,7 +35,7 @@ export default function(component, state) {
let index = 1
let name
while (!name) {
const tryName = `${capitalised} ${index}`
const tryName = `${capitalised || "Copy"} ${index}`
if (!matchingComponents.includes(tryName)) name = tryName
index++
}

26
packages/builder/src/builderStore/index.js

@ -1,14 +1,36 @@
import { getStore } from "./store"
import { getFrontendStore } from "./store/frontend"
import { getBackendUiStore } from "./store/backend"
import { getAutomationStore } from "./store/automation/"
import { getThemeStore } from "./store/theme"
import { derived } from "svelte/store"
import analytics from "analytics"
export const store = getStore()
export const store = getFrontendStore()
export const backendUiStore = getBackendUiStore()
export const automationStore = getAutomationStore()
export const themeStore = getThemeStore()
export const allScreens = derived(store, $store => {
let screens = []
if ($store.pages == null) {
return screens
}
for (let page of Object.values($store.pages)) {
screens = screens.concat(page._screens)
}
return screens
})
export const currentScreens = derived(store, $store => {
const currentScreens = $store.pages[$store.currentPageName]?._screens
if (currentScreens == null) {
return []
}
return Array.isArray(currentScreens)
? currentScreens
: Object.values(currentScreens)
})
export const initialise = async () => {
try {
await analytics.activate()

17
packages/builder/src/builderStore/insertCodeMetadata.js

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

529
packages/builder/src/builderStore/store/frontend.js

@ -0,0 +1,529 @@
import { get, writable } from "svelte/store"
import { cloneDeep } from "lodash/fp"
import {
createProps,
getBuiltin,
makePropsSafe,
} from "components/userInterface/pagesParsing/createProps"
import { getExactComponent } from "components/userInterface/pagesParsing/searchComponents"
import { allScreens, backendUiStore } from "builderStore"
import { generate_screen_css } from "../generate_css"
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
import api from "../api"
import { DEFAULT_PAGES_OBJECT } from "../../constants"
import getNewComponentName from "../getNewComponentName"
import analytics from "analytics"
import {
findChildComponentType,
generateNewIdsForComponent,
getComponentDefinition,
getParent,
} from "../storeUtils"
const INITIAL_FRONTEND_STATE = {
apps: [],
name: "",
description: "",
pages: DEFAULT_PAGES_OBJECT,
mainUi: {},
unauthenticatedUi: {},
components: [],
currentPreviewItem: null,
currentComponentInfo: null,
currentFrontEndType: "none",
currentPageName: "",
currentComponentProps: null,
errors: [],
hasAppPackage: false,
libraries: null,
appId: "",
}
export const getFrontendStore = () => {
const store = writable({ ...INITIAL_FRONTEND_STATE })
store.actions = {
// TODO: REFACTOR
initialise: async pkg => {
store.update(state => {
state.appId = pkg.application._id
return state
})
const screens = await api.get("/api/screens").then(r => r.json())
const mainScreens = screens.filter(screen =>
screen._id.includes(pkg.pages.main._id)
),
unauthScreens = screens.filter(screen =>
screen._id.includes(pkg.pages.unauthenticated._id)
)
pkg.pages = {
main: {
...pkg.pages.main,
_screens: mainScreens,
},
unauthenticated: {
...pkg.pages.unauthenticated,
_screens: unauthScreens,
},
}
// if the app has just been created
// we need to build the CSS and save
if (pkg.justCreated) {
for (let pageName of ["main", "unauthenticated"]) {
const page = pkg.pages[pageName]
store.actions.screens.regenerateCss(page)
for (let screen of page._screens) {
store.actions.screens.regenerateCss(screen)
}
await api.post(`/api/pages/${page._id}`, {
page: {
componentLibraries: pkg.application.componentLibraries,
...page,
},
screens: page._screens,
})
}
}
pkg.justCreated = false
const components = await fetchComponentLibDefinitions(pkg.application._id)
store.update(state => ({
...state,
libraries: pkg.application.componentLibraries,
components,
name: pkg.application.name,
description: pkg.application.description,
appId: pkg.application._id,
pages: pkg.pages,
hasAppPackage: true,
currentScreens: [],
builtins: [getBuiltin("##builtin/screenslot")],
appInstance: pkg.application.instance,
}))
await backendUiStore.actions.database.select(pkg.application.instance)
},
selectPageOrScreen: type => {
store.update(state => {
state.currentFrontEndType = type
const pageOrScreen =
type === "page"
? state.pages[state.currentPageName]
: state.pages[state.currentPageName]._screens[0]
state.currentComponentInfo = pageOrScreen ? pageOrScreen.props : null
state.currentPreviewItem = pageOrScreen
state.currentView = "detail"
return state
})
},
screens: {
select: screenName => {
store.update(state => {
const screen = getExactComponent(get(allScreens), screenName, true)
state.currentPreviewItem = screen
state.currentFrontEndType = "screen"
state.currentView = "detail"
store.actions.screens.regenerateCssForCurrentScreen()
// this.regenerateCssForCurrentScreen()
// regenerateCssForCurrentScreen(s)
const safeProps = makePropsSafe(
state.components[screen.props._component],
screen.props
)
screen.props = safeProps
state.currentComponentInfo = safeProps
return state
})
},
create: async screen => {
let savePromise
store.update(state => {
state.currentPreviewItem = screen
state.currentComponentInfo = screen.props
state.currentFrontEndType = "screen"
if (state.currentPreviewItem) {
store.actions.screens.regenerateCss(state.currentPreviewItem)
}
savePromise = store.actions.screens.save(screen)
return state
})
await savePromise
},
save: async screen => {
const storeContents = get(store)
const pageName = storeContents.currentPageName || "main"
const currentPage = storeContents.pages[pageName]
const currentPageScreens = currentPage._screens
let savePromise
const response = await api.post(
`/api/screens/${currentPage._id}`,
screen
)
const json = await response.json()
screen._rev = json.rev
screen._id = json.id
const foundScreen = currentPageScreens.findIndex(
el => el._id === screen._id
)
if (currentPageScreens !== -1) {
currentPageScreens.splice(foundScreen, 1)
}
currentPageScreens.push(screen)
// TODO: should carry out all server updates to screen in a single call
store.update(state => {
state.pages[pageName]._screens = currentPageScreens
state.currentPreviewItem = screen
const safeProps = makePropsSafe(
state.components[screen.props._component],
screen.props
)
state.currentComponentInfo = safeProps
screen.props = safeProps
savePromise = store.actions.pages.save()
return state
})
await savePromise
},
regenerateCss: screen => {
screen._css = generate_screen_css([screen.props])
},
regenerateCssForCurrentScreen: () => {
const { currentPreviewItem } = get(store)
if (currentPreviewItem) {
store.actions.screens.regenerateCss(currentPreviewItem)
}
},
delete: async (screensToDelete, pageName) => {
let deletePromise
store.update(state => {
if (pageName == null) {
pageName = state.pages.main.name
}
for (let screenToDelete of Array.isArray(screenToDelete)
? screenToDelete
: [screenToDelete]) {
// Remove screen from current page as well
// TODO: Should be done server side
state.pages[pageName]._screens = state.pages[
pageName
]._screens.filter(scr => scr.name !== screenToDelete.name)
deletePromise = api.delete(
`/api/screens/${screenToDelete._id}/${screenToDelete._rev}`
)
}
return state
})
await deletePromise
},
},
preview: {
// _saveCurrentPreviewItem
saveSelected: () => {
const state = get(store)
state.currentFrontEndType === "page"
? store.actions.pages.save()
: store.actions.screens.save(state.currentPreviewItem)
},
},
pages: {
select: pageName => {
store.update(state => {
const currentPage = state.pages[pageName]
state.currentScreens = currentPage._screens
state.currentFrontEndType = "page"
state.currentView = "detail"
state.currentPageName = pageName
// This is the root of many problems.
// Uncaught (in promise) TypeError: Cannot read property '_component' of undefined
// it appears that the currentPage sometimes has _props instead of props
// why
const safeProps = makePropsSafe(
state.components[currentPage.props._component],
currentPage.props
)
state.currentComponentInfo = safeProps
currentPage.props = safeProps
state.currentPreviewItem = state.pages[pageName]
store.actions.screens.regenerateCssForCurrentScreen()
for (let screen of get(allScreens)) {
screen._css = generate_screen_css([screen.props])
}
return state
})
},
save: async page => {
const storeContents = get(store)
const pageName = storeContents.currentPageName || "main"
const pageToSave = page || storeContents.pages[pageName]
// TODO: revisit. This sends down a very weird payload
const response = await api
.post(`/api/pages/${pageToSave._id}`, {
page: {
componentLibraries: storeContents.pages.componentLibraries,
...pageToSave,
},
screens: pageToSave._screens,
})
.then(response => response.json())
store.update(state => {
state.pages[pageName]._rev = response.rev
return state
})
},
},
components: {
select: component => {
store.update(state => {
const componentDef = component._component.startsWith("##")
? component
: state.components[component._component]
state.currentComponentInfo = makePropsSafe(componentDef, component)
state.currentView = "component"
return state
})
},
// addChildComponent
create: (componentToAdd, presetProps) => {
store.update(state => {
function findSlot(component_array) {
for (let i = 0; i < component_array.length; i += 1) {
if (component_array[i]._component === "##builtin/screenslot") {
return true
}
if (component_array[i]._children) findSlot(component_array[i])
}
return false
}
if (
componentToAdd.startsWith("##") &&
findSlot(state.pages[state.currentPageName].props._children)
) {
return state
}
const component = getComponentDefinition(state, componentToAdd)
const instanceId = get(backendUiStore).selectedDatabase._id
const instanceName = getNewComponentName(component, state)
const newComponent = createProps(component, {
...presetProps,
_instanceId: instanceId,
_instanceName: instanceName,
})
const currentComponent =
state.components[state.currentComponentInfo._component]
const targetParent = currentComponent.children
? state.currentComponentInfo
: getParent(
state.currentPreviewItem.props,
state.currentComponentInfo
)
// Don't continue if there's no parent
if (!targetParent) {
return state
}
targetParent._children = targetParent._children.concat(
newComponent.props
)
store.actions.preview.saveSelected()
state.currentView = "component"
state.currentComponentInfo = newComponent.props
analytics.captureEvent("Added Component", {
name: newComponent.props._component,
})
return state
})
},
copy: (component, cut = false) => {
store.update(state => {
state.componentToPaste = cloneDeep(component)
state.componentToPaste.isCut = cut
if (cut) {
const parent = getParent(
state.currentPreviewItem.props,
component._id
)
parent._children = parent._children.filter(
c => c._id !== component._id
)
store.actions.components.select(parent)
}
return state
})
},
paste: (targetComponent, mode) => {
store.update(state => {
if (!state.componentToPaste) return state
const componentToPaste = cloneDeep(state.componentToPaste)
// retain the same ids as things may be referencing this component
if (componentToPaste.isCut) {
// in case we paste a second time
state.componentToPaste.isCut = false
} else {
generateNewIdsForComponent(componentToPaste, state)
}
delete componentToPaste.isCut
if (mode === "inside") {
targetComponent._children.push(componentToPaste)
return state
}
const parent = getParent(
state.currentPreviewItem.props,
targetComponent
)
const targetIndex = parent._children.indexOf(targetComponent)
const index = mode === "above" ? targetIndex : targetIndex + 1
parent._children.splice(index, 0, cloneDeep(componentToPaste))
store.actions.screens.regenerateCssForCurrentScreen()
store.actions.preview.saveSelected()
store.actions.components.select(componentToPaste)
return state
})
},
updateStyle: (type, name, value) => {
store.update(state => {
if (!state.currentComponentInfo._styles) {
state.currentComponentInfo._styles = {}
}
state.currentComponentInfo._styles[type][name] = value
store.actions.screens.regenerateCssForCurrentScreen()
// save without messing with the store
store.actions.preview.saveSelected()
return state
})
},
updateProp: (name, value) => {
store.update(state => {
let current_component = state.currentComponentInfo
current_component[name] = value
state.currentComponentInfo = current_component
store.actions.preview.saveSelected()
return state
})
},
findRoute: component => {
// Gets all the components to needed to construct a path.
const tempStore = get(store)
let pathComponents = []
let parent = component
let root = false
while (!root) {
parent = getParent(tempStore.currentPreviewItem.props, parent)
if (!parent) {
root = true
} else {
pathComponents.push(parent)
}
}
// Remove root entry since it's the screen or page layout.
// Reverse array since we need the correct order of the IDs
const reversedComponents = pathComponents.reverse().slice(1)
// Add component
const allComponents = [...reversedComponents, component]
// Map IDs
const IdList = allComponents.map(c => c._id)
// Construct ID Path:
return IdList.join("/")
},
links: {
save: async (url, title) => {
let savePromise
store.update(state => {
// Try to extract a nav component from the master screen
const nav = findChildComponentType(
state.pages.main,
"@budibase/standard-components/Navigation"
)
if (nav) {
let newLink
// Clone an existing link if one exists
if (nav._children && nav._children.length) {
// Clone existing link style
newLink = cloneDeep(nav._children[0])
// Manipulate IDs to ensure uniqueness
generateNewIdsForComponent(newLink, state, false)
// Set our new props
newLink._instanceName = `${title} Link`
newLink.url = url
newLink.text = title
} else {
// Otherwise create vanilla new link
const component = getComponentDefinition(
state,
"@budibase/standard-components/link"
)
const instanceId = get(backendUiStore).selectedDatabase._id
newLink = createProps(component, {
url,
text: title,
_instanceName: `${title} Link`,
_instanceId: instanceId,
}).props
}
// Save page and regenerate all CSS because otherwise weird things happen
nav._children = [...nav._children, newLink]
state.currentPageName = "main"
store.actions.screens.regenerateCss(state.pages.main)
for (let screen of state.pages.main._screens) {
store.actions.screens.regenerateCss(screen)
}
savePromise = store.actions.pages.save()
}
return state
})
await savePromise
},
},
},
}
return store
}

607
packages/builder/src/builderStore/store/index.js

@ -1,607 +0,0 @@
import { cloneDeep } from "lodash/fp"
import getNewComponentName from "../getNewComponentName"
import { backendUiStore } from "builderStore"
import { writable, get } from "svelte/store"
import api from "../api"
import { DEFAULT_PAGES_OBJECT } from "../../constants"
import { getExactComponent } from "components/userInterface/pagesParsing/searchComponents"
import {
createProps,
makePropsSafe,
getBuiltin,
} from "components/userInterface/pagesParsing/createProps"
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
import { buildCodeForScreens } from "../buildCodeForScreens"
import { generate_screen_css } from "../generate_css"
import { insertCodeMetadata } from "../insertCodeMetadata"
import analytics from "analytics"
import { uuid } from "../uuid"
import {
selectComponent as _selectComponent,
getParent,
walkProps,
savePage as _savePage,
saveCurrentPreviewItem as _saveCurrentPreviewItem,
saveScreenApi as _saveScreenApi,
regenerateCssForCurrentScreen,
regenerateCssForScreen,
generateNewIdsForComponent,
getComponentDefinition,
findChildComponentType,
} from "../storeUtils"
export const getStore = () => {
const initial = {
apps: [],
name: "",
description: "",
pages: DEFAULT_PAGES_OBJECT,
mainUi: {},
unauthenticatedUi: {},
components: [],
currentPreviewItem: null,
currentComponentInfo: null,
currentFrontEndType: "none",
currentPageName: "",
currentComponentProps: null,
errors: [],
hasAppPackage: false,
libraries: null,
appId: "",
}
const store = writable(initial)
store.setPackage = setPackage(store, initial)
store.saveScreen = saveScreen(store)
store.setCurrentScreen = setCurrentScreen(store)
store.deleteScreens = deleteScreens(store)
store.setCurrentPage = setCurrentPage(store)
store.createLink = createLink(store)
store.createScreen = createScreen(store)
store.addStylesheet = addStylesheet(store)
store.removeStylesheet = removeStylesheet(store)
store.savePage = savePage(store)
store.addChildComponent = addChildComponent(store)
store.selectComponent = selectComponent(store)
store.setComponentProp = setComponentProp(store)
store.setPageOrScreenProp = setPageOrScreenProp(store)
store.setComponentStyle = setComponentStyle(store)
store.setComponentCode = setComponentCode(store)
store.setScreenType = setScreenType(store)
store.getPathToComponent = getPathToComponent(store)
store.addTemplatedComponent = addTemplatedComponent(store)
store.setMetadataProp = setMetadataProp(store)
store.editPageOrScreen = editPageOrScreen(store)
store.pasteComponent = pasteComponent(store)
store.storeComponentForCopy = storeComponentForCopy(store)
return store
}
export default getStore
const setPackage = (store, initial) => async pkg => {
const [main_screens, unauth_screens] = await Promise.all([
api
.get(`/_builder/api/${pkg.application._id}/pages/main/screens`)
.then(r => r.json()),
api
.get(`/_builder/api/${pkg.application._id}/pages/unauthenticated/screens`)
.then(r => r.json()),
])
pkg.pages = {
main: {
...pkg.pages.main,
_screens: Object.values(main_screens),
},
unauthenticated: {
...pkg.pages.unauthenticated,
_screens: Object.values(unauth_screens),
},
}
// if the app has just been created
// we need to build the CSS and save
if (pkg.justCreated) {
const generateInitialPageCss = async name => {
const page = pkg.pages[name]
regenerateCssForScreen(page)
for (let screen of page._screens) {
regenerateCssForScreen(screen)
}
await api.post(`/_builder/api/${pkg.application._id}/pages/${name}`, {
page: {
componentLibraries: pkg.application.componentLibraries,
...page,
},
screens: page._screens,
})
}
generateInitialPageCss("main")
generateInitialPageCss("unauthenticated")
pkg.justCreated = false
}
initial.libraries = pkg.application.componentLibraries
initial.components = await fetchComponentLibDefinitions(pkg.application._id)
initial.name = pkg.application.name
initial.description = pkg.application.description
initial.appId = pkg.application._id
initial.pages = pkg.pages
initial.hasAppPackage = true
initial.screens = [
...Object.values(main_screens),
...Object.values(unauth_screens),
]
initial.builtins = [getBuiltin("##builtin/screenslot")]
initial.appInstance = pkg.application.instance
initial.appId = pkg.application._id
store.set(initial)
await backendUiStore.actions.database.select(initial.appInstance)
return initial
}
const saveScreen = store => screen => {
store.update(state => {
return _saveScreen(store, state, screen)
})
}
const _saveScreen = async (store, s, screen) => {
const pageName = s.currentPageName || "main"
const currentPageScreens = s.pages[pageName]._screens
await api
.post(`/_builder/api/${s.appId}/pages/${pageName}/screen`, screen)
.then(() => {
if (currentPageScreens.includes(screen)) return
const screens = [...currentPageScreens, screen]
store.update(innerState => {
innerState.pages[pageName]._screens = screens
innerState.screens = screens
innerState.currentPreviewItem = screen
const safeProps = makePropsSafe(
innerState.components[screen.props._component],
screen.props
)
innerState.currentComponentInfo = safeProps
screen.props = safeProps
_savePage(innerState)
return innerState
})
})
return s
}
const createScreen = store => async screen => {
let savePromise
store.update(state => {
state.currentPreviewItem = screen
state.currentComponentInfo = screen.props
state.currentFrontEndType = "screen"
regenerateCssForCurrentScreen(state)
savePromise = _saveScreen(store, state, screen)
return state
})
await savePromise
}
const createLink = store => async (url, title) => {
let savePromise
store.update(state => {
// Try to extract a nav component from the master screen
const nav = findChildComponentType(
state.pages.main,
"@budibase/standard-components/Navigation"
)
if (nav) {
let newLink
// Clone an existing link if one exists
if (nav._children && nav._children.length) {
// Clone existing link style
newLink = cloneDeep(nav._children[0])
// Manipulate IDs to ensure uniqueness
generateNewIdsForComponent(newLink, state, false)
// Set our new props
newLink._instanceName = `${title} Link`
newLink.url = url
newLink.text = title
} else {
// Otherwise create vanilla new link
const component = getComponentDefinition(
state,
"@budibase/standard-components/link"
)
const instanceId = get(backendUiStore).selectedDatabase._id
newLink = createProps(component, {
url,
text: title,
_instanceName: `${title} Link`,
_instanceId: instanceId,
}).props
}
// Save page and regenerate all CSS because otherwise weird things happen
nav._children = [...nav._children, newLink]
state.currentPageName = "main"
regenerateCssForScreen(state.pages.main)
for (let screen of state.pages.main._screens) {
regenerateCssForScreen(screen)
}
savePromise = _savePage(state)
}
return state
})
await savePromise
}
const setCurrentScreen = store => screenName => {
store.update(s => {
const screen = getExactComponent(s.screens, screenName, true)
s.currentPreviewItem = screen
s.currentFrontEndType = "screen"
s.currentView = "detail"
regenerateCssForCurrentScreen(s)
const safeProps = makePropsSafe(
s.components[screen.props._component],
screen.props
)
screen.props = safeProps
s.currentComponentInfo = safeProps
setCurrentPageFunctions(s)
return s
})
}
const deleteScreens = store => (screens, pageName = null) => {
if (!(screens instanceof Array)) {
screens = [screens]
}
store.update(state => {
if (pageName == null) {
pageName = state.pages.main.name
}
for (let screen of screens) {
state.screens = state.screens.filter(c => c.name !== screen.name)
// Remove screen from current page as well
state.pages[pageName]._screens = state.pages[pageName]._screens.filter(
scr => scr.name !== screen.name
)
api.delete(`/_builder/api/pages/${pageName}/screens/${screen.name}`)
}
return state
})
}
const savePage = store => async page => {
store.update(state => {
if (state.currentFrontEndType !== "page" || !state.currentPageName) {
return state
}
state.pages[state.currentPageName] = page
_savePage(state)
return state
})
}
const addStylesheet = store => stylesheet => {
store.update(s => {
s.pages.stylesheets.push(stylesheet)
_savePage(s)
return s
})
}
const removeStylesheet = store => stylesheet => {
store.update(state => {
state.pages.stylesheets = state.pages.stylesheets.filter(
s => s !== stylesheet
)
_savePage(state)
return state
})
}
const setCurrentPage = store => pageName => {
store.update(state => {
const current_screens = state.pages[pageName]._screens
const currentPage = state.pages[pageName]
state.currentFrontEndType = "page"
state.currentView = "detail"
state.currentPageName = pageName
state.screens = Array.isArray(current_screens)
? current_screens
: Object.values(current_screens)
const safeProps = makePropsSafe(
state.components[currentPage.props._component],
currentPage.props
)
state.currentComponentInfo = safeProps
currentPage.props = safeProps
state.currentPreviewItem = state.pages[pageName]
regenerateCssForCurrentScreen(state)
for (let screen of state.screens) {
screen._css = generate_screen_css([screen.props])
}
setCurrentPageFunctions(state)
return state
})
}
/**
* @param {string} componentToAdd - name of the component to add to the application
* @param {string} presetName - name of the component preset if defined
*/
const addChildComponent = store => (componentToAdd, presetProps = {}) => {
store.update(state => {
function findSlot(component_array) {
for (let i = 0; i < component_array.length; i += 1) {
if (component_array[i]._component === "##builtin/screenslot") {
return true
}
if (component_array[i]._children) findSlot(component_array[i])
}
return false
}
if (
componentToAdd.startsWith("##") &&
findSlot(state.pages[state.currentPageName].props._children)
) {
return state
}
const component = getComponentDefinition(state, componentToAdd)
const instanceId = get(backendUiStore).selectedDatabase._id
const instanceName = getNewComponentName(component, state)
const newComponent = createProps(
component,
{
...presetProps,
_instanceId: instanceId,
_instanceName: instanceName,
},
state
)
const currentComponent =
state.components[state.currentComponentInfo._component]
const targetParent = currentComponent.children
? state.currentComponentInfo
: getParent(state.currentPreviewItem.props, state.currentComponentInfo)
// Don't continue if there's no parent
if (!targetParent) {
return state
}
targetParent._children = targetParent._children.concat(newComponent.props)
state.currentFrontEndType === "page"
? _savePage(state)
: _saveScreenApi(state.currentPreviewItem, state)
state.currentView = "component"
state.currentComponentInfo = newComponent.props
analytics.captureEvent("Added Component", {
name: newComponent.props._component,
})
return state
})
}
/**
* @param {string} props - props to add, as child of current component
*/
const addTemplatedComponent = store => props => {
store.update(state => {
walkProps(props, p => {
p._id = uuid()
})
state.currentComponentInfo._children = state.currentComponentInfo._children.concat(
props
)
regenerateCssForCurrentScreen(state)
setCurrentPageFunctions(state)
_saveCurrentPreviewItem(state)
return state
})
}
const selectComponent = store => component => {
store.update(state => {
return _selectComponent(state, component)
})
}
const setComponentProp = store => (name, value) => {
store.update(state => {
let current_component = state.currentComponentInfo
current_component[name] = value
state.currentComponentInfo = current_component
_saveCurrentPreviewItem(state)
return state
})
}
const setPageOrScreenProp = store => (name, value) => {
store.update(state => {
if (name === "_instanceName" && state.currentFrontEndType === "screen") {
state.currentPreviewItem.props[name] = value
} else {
state.currentPreviewItem[name] = value
}
_saveCurrentPreviewItem(state)
return state
})
}
const setComponentStyle = store => (type, name, value) => {
store.update(state => {
if (!state.currentComponentInfo._styles) {
state.currentComponentInfo._styles = {}
}
state.currentComponentInfo._styles[type][name] = value
regenerateCssForCurrentScreen(state)
// save without messing with the store
_saveCurrentPreviewItem(state)
return state
})
}
const setComponentCode = store => code => {
store.update(state => {
state.currentComponentInfo._code = code
setCurrentPageFunctions(state)
// save without messing with the store
_saveScreenApi(state.currentPreviewItem, state)
return state
})
}
const setCurrentPageFunctions = s => {
s.currentPageFunctions = buildPageCode(s.screens, s.pages[s.currentPageName])
insertCodeMetadata(s.currentPreviewItem.props)
}
const buildPageCode = (screens, page) => buildCodeForScreens([page, ...screens])
const setScreenType = store => type => {
store.update(state => {
state.currentFrontEndType = type
const pageOrScreen =
type === "page"
? state.pages[state.currentPageName]
: state.pages[state.currentPageName]._screens[0]
state.currentComponentInfo = pageOrScreen ? pageOrScreen.props : null
state.currentPreviewItem = pageOrScreen
state.currentView = "detail"
return state
})
}
const editPageOrScreen = store => (key, value, setOnComponent = false) => {
store.update(state => {
setOnComponent
? (state.currentPreviewItem.props[key] = value)
: (state.currentPreviewItem[key] = value)
_saveCurrentPreviewItem(state)
return state
})
}
const getPathToComponent = store => component => {
// Gets all the components to needed to construct a path.
const tempStore = get(store)
let pathComponents = []
let parent = component
let root = false
while (!root) {
parent = getParent(tempStore.currentPreviewItem.props, parent)
if (!parent) {
root = true
} else {
pathComponents.push(parent)
}
}
// Remove root entry since it's the screen or page layout.
// Reverse array since we need the correct order of the IDs
const reversedComponents = pathComponents.reverse().slice(1)
// Add component
const allComponents = [...reversedComponents, component]
// Map IDs
const IdList = allComponents.map(c => c._id)
// Construct ID Path:
const path = IdList.join("/")
return path
}
const setMetadataProp = store => (name, prop) => {
store.update(s => {
s.currentPreviewItem[name] = prop
return s
})
}
const storeComponentForCopy = store => (component, cut = false) => {
store.update(s => {
const copiedComponent = cloneDeep(component)
s.componentToPaste = copiedComponent
s.componentToPaste.isCut = cut
if (cut) {
const parent = getParent(s.currentPreviewItem.props, component._id)
parent._children = parent._children.filter(c => c._id !== component._id)
selectComponent(s, parent)
}
return s
})
}
const pasteComponent = store => (targetComponent, mode) => {
store.update(s => {
if (!s.componentToPaste) return s
const componentToPaste = cloneDeep(s.componentToPaste)
// retain the same ids as things may be referencing this component
if (componentToPaste.isCut) {
// in case we paste a second time
s.componentToPaste.isCut = false
} else {
generateNewIdsForComponent(componentToPaste, s)
}
delete componentToPaste.isCut
if (mode === "inside") {
targetComponent._children.push(componentToPaste)
return s
}
const parent = getParent(s.currentPreviewItem.props, targetComponent)
const targetIndex = parent._children.indexOf(targetComponent)
const index = mode === "above" ? targetIndex : targetIndex + 1
parent._children.splice(index, 0, cloneDeep(componentToPaste))
regenerateCssForCurrentScreen(s)
_saveCurrentPreviewItem(s)
selectComponent(s, componentToPaste)
return s
})
}

74
packages/builder/src/builderStore/storeUtils.js

@ -1,21 +1,7 @@
import {
makePropsSafe,
getBuiltin,
} from "components/userInterface/pagesParsing/createProps"
import api from "./api"
import { generate_screen_css } from "./generate_css"
import { getBuiltin } from "components/userInterface/pagesParsing/createProps"
import { uuid } from "./uuid"
import getNewComponentName from "./getNewComponentName"
export const selectComponent = (state, component) => {
const componentDef = component._component.startsWith("##")
? component
: state.components[component._component]
state.currentComponentInfo = makePropsSafe(componentDef, component)
state.currentView = "component"
return state
}
export const getParent = (rootProps, child) => {
let parent
walkProps(rootProps, (p, breakWalk) => {
@ -30,41 +16,6 @@ export const getParent = (rootProps, child) => {
return parent
}
export const saveCurrentPreviewItem = s =>
s.currentFrontEndType === "page"
? savePage(s)
: saveScreenApi(s.currentPreviewItem, s)
export const savePage = async s => {
const pageName = s.currentPageName || "main"
const page = s.pages[pageName]
await api.post(`/_builder/api/${s.appId}/pages/${pageName}`, {
page: { componentLibraries: s.pages.componentLibraries, ...page },
uiFunctions: s.currentPageFunctions,
screens: page._screens,
})
}
export const saveScreenApi = (screen, s) => {
api
.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}/screen`, screen)
.then(() => savePage(s))
}
export const renameCurrentScreen = (newname, state) => {
const oldname = state.currentPreviewItem.props._instanceName
state.currentPreviewItem.props._instanceName = newname
api.patch(
`/_builder/api/${state.appId}/pages/${state.currentPageName}/screen`,
{
oldname,
newname,
}
)
return state
}
export const walkProps = (props, action, cancelToken = null) => {
cancelToken = cancelToken || { cancelled: false }
action(props, () => {
@ -79,21 +30,14 @@ export const walkProps = (props, action, cancelToken = null) => {
}
}
export const regenerateCssForScreen = screen => {
screen._css = generate_screen_css([screen.props])
}
export const regenerateCssForCurrentScreen = state => {
if (state.currentPreviewItem) {
regenerateCssForScreen(state.currentPreviewItem)
}
return state
}
export const generateNewIdsForComponent = (c, state, changeName = true) =>
walkProps(c, p => {
p._id = uuid()
if (changeName) p._instanceName = getNewComponentName(p._component, state)
export const generateNewIdsForComponent = (
component,
state,
changeName = true
) =>
walkProps(component, prop => {
prop._id = uuid()
if (changeName) prop._instanceName = getNewComponentName(prop, state)
})
export const getComponentDefinition = (state, name) =>

4
packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte

@ -55,7 +55,7 @@
// Record the table that created this screen so we can link it later
screen.autoTableId = table._id
try {
await store.createScreen(screen)
await store.actions.screens.create(screen)
} catch (_) {
// TODO: this is temporary
// a cypress test is failing, because I added the
@ -70,7 +70,7 @@
const listPage = screens.find(screen =>
screen.props._instanceName.endsWith("List")
)
await store.createLink(listPage.route, table.name)
await store.actions.components.links.save(listPage.route, table.name)
// Navigate to new table
$goto(`./table/${table._id}`)

6
packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte

@ -1,5 +1,5 @@
<script>
import { backendUiStore, store } from "builderStore"
import { backendUiStore, store, allScreens } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Input } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -28,7 +28,7 @@
}
function showModal() {
const screens = $store.screens
const screens = $allScreens
templateScreens = screens.filter(screen => screen.autoTableId === table._id)
willBeDeleted = ["All table data"].concat(
templateScreens.map(screen => `Screen ${screen.props._instanceName}`)
@ -39,7 +39,7 @@
async function deleteTable() {
await backendUiStore.actions.tables.delete(table)
store.deleteScreens(templateScreens)
store.store.actions.screens.delete(templateScreens)
await backendUiStore.actions.tables.fetch()
notifier.success("Table deleted")
hideEditor()

10
packages/builder/src/components/start/CreateAppModal.svelte

@ -128,7 +128,7 @@
}
}
async function signUp() {
async function createNewApp() {
submitting = true
try {
// Add API key if there is none.
@ -154,7 +154,7 @@
if (applicationPkg.ok) {
backendUiStore.actions.reset()
pkg.justCreated = true
await store.setPackage(pkg)
await store.actions.initialise(pkg)
automationStore.actions.fetch()
} else {
throw new Error(pkg)
@ -193,10 +193,6 @@
$: checkValidity($createAppStore.values, $createAppStore.currentStep)
let onChange = () => {}
async function _onOkay() {
await createNewApp()
}
</script>
<div class="container">
@ -239,7 +235,7 @@
<Button
medium
blue
on:click={signUp}
on:click={createNewApp}
disabled={!fullFormIsValid || submitting}>
{submitting ? 'Loading...' : 'Submit'}
</Button>

40
packages/builder/src/components/userInterface/ComponentDropdownMenu.svelte

@ -4,7 +4,7 @@
import { getComponentDefinition } from "builderStore/storeUtils"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { last } from "lodash/fp"
import { getParent, saveCurrentPreviewItem } from "builderStore/storeUtils"
import { getParent } from "builderStore/storeUtils"
import { DropdownMenu } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
@ -25,50 +25,50 @@
}
const selectComponent = component => {
store.selectComponent(component)
const path = store.getPathToComponent(component)
store.actions.components.select(component)
const path = store.actions.components.findRoute(component)
$goto(`./:page/:screen/${path}`)
}
const moveUpComponent = () => {
store.update(s => {
const parent = getParent(s.currentPreviewItem.props, component)
store.update(state => {
const parent = getParent(state.currentPreviewItem.props, component)
if (parent) {
const currentIndex = parent._children.indexOf(component)
if (currentIndex === 0) return s
if (currentIndex === 0) return state
const newChildren = parent._children.filter(c => c !== component)
newChildren.splice(currentIndex - 1, 0, component)
parent._children = newChildren
}
s.currentComponentInfo = component
saveCurrentPreviewItem(s)
state.currentComponentInfo = component
store.actions.preview.saveSelected()
return s
return state
})
}
const moveDownComponent = () => {
store.update(s => {
const parent = getParent(s.currentPreviewItem.props, component)
store.update(state => {
const parent = getParent(state.currentPreviewItem.props, component)
if (parent) {
const currentIndex = parent._children.indexOf(component)
if (currentIndex === parent._children.length - 1) return s
if (currentIndex === parent._children.length - 1) return state
const newChildren = parent._children.filter(c => c !== component)
newChildren.splice(currentIndex + 1, 0, component)
parent._children = newChildren
}
s.currentComponentInfo = component
saveCurrentPreviewItem(s)
state.currentComponentInfo = component
store.actions.preview.saveSelected()
return s
return state
})
}
const copyComponent = () => {
const duplicateComponent = () => {
storeComponentForCopy(false)
pasteComponent("below")
}
@ -82,19 +82,19 @@
selectComponent(parent)
}
saveCurrentPreviewItem(state)
store.actions.preview.saveSelected()
return state
})
}
const storeComponentForCopy = (cut = false) => {
// lives in store - also used by drag drop
store.storeComponentForCopy(component, cut)
store.actions.components.copy(component, cut)
}
const pasteComponent = mode => {
// lives in store - also used by drag drop
store.pasteComponent(component, mode)
store.actions.components.paste(component, mode)
}
</script>
@ -117,7 +117,7 @@
<DropdownItem
icon="ri-repeat-one-line"
title="Duplicate"
on:click={copyComponent} />
on:click={duplicateComponent} />
<DropdownItem
icon="ri-scissors-cut-line"
title="Cut"

18
packages/builder/src/components/userInterface/ComponentPropertiesPanel.svelte

@ -27,7 +27,7 @@
componentPropDefinition.properties &&
componentPropDefinition.properties[selectedCategory.value]
const onStyleChanged = store.setComponentStyle
const onStyleChanged = store.actions.components.updateStyle
$: isComponentOrScreen =
$store.currentView === "component" ||
@ -58,6 +58,18 @@
return components
}
function setPageOrScreenProp(name, value) {
store.update(state => {
if (name === "_instanceName" && state.currentFrontEndType === "screen") {
state.currentPreviewItem.props[name] = value
} else {
state.currentPreviewItem[name] = value
}
store.actions.preview.saveSelected()
return state
})
}
function getProps(obj, keys) {
return keys.map((key, i) => [key, obj[key], obj.props._id + i])
}
@ -81,8 +93,8 @@
{componentDefinition}
{panelDefinition}
displayNameField={displayName}
onChange={store.setComponentProp}
onScreenPropChange={store.setPageOrScreenProp}
onChange={store.actions.components.updateProp}
onScreenPropChange={setPageOrScreenProp}
screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} />
{/if}
</div>

4
packages/builder/src/components/userInterface/ComponentSelectionList.svelte

@ -25,8 +25,8 @@
}
const onComponentChosen = component => {
store.addChildComponent(component._component, component.presetProps)
const path = store.getPathToComponent($store.currentComponentInfo)
store.actions.components.create(component._component, component.presetProps)
const path = store.actions.components.findRoute($store.currentComponentInfo)
$goto(`./:page/:screen/${path}`)
close()
}

2
packages/builder/src/components/userInterface/ComponentsHierarchy.svelte

@ -32,7 +32,7 @@
])
const changeScreen = screen => {
store.setCurrentScreen(screen.props._instanceName)
store.actions.screens.select(screen.props._instanceName)
$goto(`./:page/${screen.props._instanceName}`)
}
</script>

8
packages/builder/src/components/userInterface/ComponentsHierarchyChildren.svelte

@ -40,10 +40,10 @@
const selectComponent = component => {
// Set current component
store.selectComponent(component)
store.actions.components.select(component)
// Get ID path
const path = store.getPathToComponent(component)
const path = store.actions.components.findRoute(component)
// Go to correct URL
$goto(`./:page/:screen/${path}`)
@ -96,8 +96,8 @@
const drop = () => {
if ($dragDropStore.targetComponent !== $dragDropStore.componentToDrop) {
store.storeComponentForCopy($dragDropStore.componentToDrop, true)
store.pasteComponent(
store.actions.components.copy($dragDropStore.componentToDrop, true)
store.actions.components.paste(
$dragDropStore.targetComponent,
$dragDropStore.dropPosition
)

4
packages/builder/src/components/userInterface/ComponentsPaneSwitcher.svelte

@ -1,5 +1,5 @@
<script>
import { store } from "builderStore/"
import { store, allScreens } from "builderStore"
import ComponentPropertiesPanel from "./ComponentPropertiesPanel.svelte"
import ComponentSelectionList from "./ComponentSelectionList.svelte"
@ -18,7 +18,7 @@
</script>
<div class="root">
{#if $store.currentFrontEndType === 'page' || $store.screens.length}
{#if $store.currentFrontEndType === 'page' || $allScreens.length}
<div class="switcher">
<button
class:selected={selected === COMPONENT_SELECTION_TAB}

4
packages/builder/src/components/userInterface/DetailScreenSelect.svelte

@ -1,7 +1,7 @@
<script>
import { DataList } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { store } from "builderStore"
import { allScreens } from "builderStore"
const dispatch = createEventDispatcher()
@ -13,7 +13,7 @@
const getUrls = () => {
return [
...$store.screens
...$allScreens
.filter(
screen =>
screen.props._component.endsWith("/rowdetail") ||

4
packages/builder/src/components/userInterface/EventsEditor/StateBindingCascader.svelte

@ -1,6 +1,6 @@
<script>
import { Input, DataList, Select } from "@budibase/bbui"
import { store, automationStore } from "builderStore"
import { automationStore, allScreens } from "builderStore"
export let parameter
@ -24,7 +24,7 @@
{:else if parameter.name === 'url'}
<DataList on:change bind:value={parameter.value}>
<option value="" />
{#each $store.screens as screen}
{#each $allScreens as screen}
<option value={screen.route}>{screen.props._instanceName}</option>
{/each}
</DataList>

4
packages/builder/src/components/userInterface/EventsEditor/actions/NavigateTo.svelte

@ -1,6 +1,6 @@
<script>
import { DataList, Label } from "@budibase/bbui"
import { store } from "builderStore"
import { allScreens } from "builderStore"
export let parameters
</script>
@ -9,7 +9,7 @@
<Label size="m" color="dark">Screen</Label>
<DataList secondary bind:value={parameters.url}>
<option value="" />
{#each $store.screens as screen}
{#each $allScreens as screen}
<option value={screen.route}>{screen.props._instanceName}</option>
{/each}
</DataList>

4
packages/builder/src/components/userInterface/FrontendNavigatePane.svelte

@ -1,5 +1,5 @@
<script>
import { store } from "builderStore"
import { store, currentScreens } from "builderStore"
import ComponentsHierarchy from "components/userInterface/ComponentsHierarchy.svelte"
import PageLayout from "components/userInterface/PageLayout.svelte"
import PagesList from "components/userInterface/PagesList.svelte"
@ -16,7 +16,7 @@
<PagesList />
<div class="nav-items-container">
<PageLayout layout={$store.pages[$store.currentPageName]} />
<ComponentsHierarchy screens={$store.screens} />
<ComponentsHierarchy screens={$currentScreens} />
</div>
<Modal bind:this={modal}>
<NewScreenModal />

10
packages/builder/src/components/userInterface/NewScreenModal.svelte

@ -1,6 +1,6 @@
<script>
import { goto } from "@sveltech/routify"
import { store, backendUiStore } from "builderStore"
import { store, backendUiStore, allScreens } from "builderStore"
import {
Input,
Button,
@ -24,7 +24,7 @@
$: templates = getTemplates($store, $backendUiStore.tables)
$: route = !route && $store.screens.length === 0 ? "*" : route
$: route = !route && $allScreens.length === 0 ? "*" : route
$: baseComponents = Object.values($store.components)
.filter(componentDefinition => componentDefinition.baseComponent)
@ -71,9 +71,9 @@
draftScreen.props._component = baseComponent
draftScreen.route = route
await store.createScreen(draftScreen)
await store.actions.screens.create(draftScreen)
if (createLink) {
await store.createLink(route, name)
await store.actions.components.links.save(route, name)
}
if (templateIndex !== undefined) {
@ -87,7 +87,7 @@
}
const routeNameExists = route => {
return $store.screens.some(
return $allScreens.some(
screen => screen.route.toLowerCase() === route.toLowerCase()
)
}

2
packages/builder/src/components/userInterface/PageLayout.svelte

@ -22,7 +22,7 @@
}
const setCurrentScreenToLayout = () => {
store.setScreenType("page")
store.actions.selectPageOrScreen("page")
$goto("./:page/page-layout")
}
</script>

8
packages/builder/src/components/userInterface/PagesList.svelte

@ -2,8 +2,8 @@
import { params, goto } from "@sveltech/routify"
import { store } from "builderStore"
const getPage = (s, name) => {
const props = s.pages[name]
const getPage = (state, name) => {
const props = state.pages[name]
return { name, props }
}
@ -19,10 +19,10 @@
]
if (!$store.currentPageName)
store.setCurrentPage($params.page ? $params.page : "main")
store.actions.pages.select($params.page ? $params.page : "main")
const changePage = id => {
store.setCurrentPage(id)
store.actions.pages.select(id)
$goto(`./${id}/page-layout`)
}
</script>

6
packages/builder/src/components/userInterface/ScreenDropdownMenu.svelte

@ -1,6 +1,7 @@
<script>
import { goto } from "@sveltech/routify"
import { store } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { DropdownMenu } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
@ -12,11 +13,12 @@
let anchor
const deleteScreen = () => {
store.deleteScreens(screen, $store.currentPageName)
store.actions.screens.delete(screen, $store.currentPageName)
// update the page if required
store.update(state => {
if (state.currentPreviewItem.name === screen.name) {
store.setCurrentPage($store.currentPageName)
store.actions.pages.select($store.currentPageName)
notifier.success(`Screen ${screen.name} deleted successfully.`)
$goto(`./:page/page-layout`)
}
return state

6
packages/builder/src/components/userInterface/ScreenSelect.svelte

@ -1,7 +1,7 @@
<script>
import { DataList } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { store, backendUiStore } from "builderStore"
import { store, allScreens, backendUiStore } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
const dispatch = createEventDispatcher()
@ -17,7 +17,7 @@
// and substitute the :id param for the actual {{ ._id }} binding
const getUrls = () => {
const urls = [
...$store.screens
...$allScreens
.filter(screen => !screen.props._component.endsWith("/rowdetail"))
.map(screen => ({
name: screen.props._instanceName,
@ -33,7 +33,7 @@
tables: $backendUiStore.tables,
})
const detailScreens = $store.screens.filter(screen =>
const detailScreens = $allScreens.filter(screen =>
screen.props._component.endsWith("/rowdetail")
)

4
packages/builder/src/components/userInterface/SettingsView.svelte

@ -4,7 +4,7 @@
import Input from "./PropertyPanelControls/Input.svelte"
import { goto } from "@sveltech/routify"
import { excludeProps } from "./propertyCategories.js"
import { store } from "builderStore"
import { store, allScreens } from "builderStore"
import { walkProps } from "builderStore/storeUtils"
export let panelDefinition = []
@ -67,7 +67,7 @@
lookForDuplicate($store.currentPreviewItem.props)
} else {
// viewing master page - need to dedupe against all screens
for (let screen of $store.screens) {
for (let screen of $allScreens) {
lookForDuplicate(screen.props)
}
}

5
packages/builder/src/components/userInterface/pagesParsing/createProps.js

@ -59,6 +59,11 @@ export const createProps = (componentDefinition, derivedFromProps) => {
}
export const makePropsSafe = (componentDefinition, props) => {
if (!componentDefinition) {
console.error(
"No component definition passed to makePropsSafe. Please check the component definition is being passed correctly."
)
}
const safeProps = createProps(componentDefinition, props).props
for (let propName in safeProps) {
props[propName] = safeProps[propName]

4
packages/builder/src/components/userInterface/pagesParsing/searchComponents.js

@ -29,8 +29,8 @@ export const searchAllComponents = (components, phrase) => {
}
export const getExactComponent = (components, name, isScreen = false) => {
return components.find(c =>
isScreen ? c.props._instanceName === name : c._instanceName === name
return components.find(comp =>
isScreen ? comp.props._instanceName === name : comp._instanceName === name
)
}

16
packages/builder/src/constants/index.js

@ -1,19 +1,15 @@
export const DEFAULT_PAGES_OBJECT = {
main: {
_props: {},
_screens: {},
index: {
_component: "./components/indexHtml",
props: {
_component: "@budibase/standard-components/container",
},
appBody: "bbapp.main.json",
_screens: {},
},
unauthenticated: {
_props: {},
_screens: {},
index: {
_component: "./components/indexHtml",
props: {
_component: "@budibase/standard-components/container",
},
appBody: "bbapp.unauthenticated.json",
_screens: {},
},
componentLibraries: [],
stylesheets: [],

2
packages/builder/src/pages/[application]/_reset.svelte

@ -18,7 +18,7 @@
if (res.ok) {
backendUiStore.actions.reset()
await store.setPackage(pkg)
await store.actions.initialise(pkg)
await automationStore.actions.fetch()
return pkg
} else {

2
packages/builder/src/pages/[application]/automate/[automation]/_layout.svelte

@ -1,4 +1,4 @@
<script>
import { params } from "@sveltech/routify"
store.setCurrentPage($params.page)
store.actions.pages.select($params.page)
</script>

12
packages/builder/src/pages/[application]/design/[page]/[screen]/_layout.svelte

@ -1,7 +1,7 @@
<script>
import { onMount } from "svelte"
import { params, leftover, goto } from "@sveltech/routify"
import { store } from "builderStore"
import { store, allScreens } from "builderStore"
// Get any leftover params not caught by Routifys params store.
const componentIds = $leftover.split("/").filter(id => id !== "")
@ -10,17 +10,17 @@
if ($params.screen !== "page-layout") {
const currentScreenName = decodeURI($params.screen)
const validScreen =
$store.screens.findIndex(
$allScreens.findIndex(
screen => screen.props._instanceName === currentScreenName
) !== -1
if (!validScreen) {
// Go to main layout if URL set to invalid screen
store.setCurrentPage("main")
store.actions.pages.select("main")
$goto("../../main")
} else {
// Otherwise proceed to set screen
store.setCurrentScreen(currentScreenName)
store.actions.screens.select(currentScreenName)
// There are leftover stuff, like IDs, so navigate the components and find the ID and select it.
if ($leftover) {
@ -35,7 +35,7 @@
}
} else {
// It's a page, so set the screentype to page.
store.setScreenType("page")
store.actions.selectPageOrScreen("page")
// There are leftover stuff, like IDs, so navigate the components and find the ID and select it.
if ($leftover) {
@ -64,7 +64,7 @@
})
// Select Component!
if (componentToSelect) store.selectComponent(componentToSelect)
if (componentToSelect) store.actions.components.select(componentToSelect)
}
</script>

2
packages/builder/src/pages/[application]/design/[page]/_layout.svelte

@ -2,7 +2,7 @@
import { params } from "@sveltech/routify"
import { store } from "builderStore"
store.setCurrentPage($params.page)
store.actions.pages.select($params.page)
</script>
<slot />

4
packages/builder/src/pages/_layout.svelte

@ -28,12 +28,12 @@
<Link
icon={CommunityIcon}
title="Community"
href="https://forum.budibase.com/" />
href="https://github.com/Budibase/budibase/discussions" />
<Link
icon={BugIcon}
title="Raise an issue"
href="https://github.com/Budibase/budibase" />
href="https://github.com/Budibase/budibase/issues/new/choose" />
</div>
</div>

63
packages/builder/tests/buildCodeForScreen.spec.js

@ -1,63 +0,0 @@
import { buildCodeForScreens } from "../src/builderStore/buildCodeForScreens"
describe("buildCodeForScreen", () => {
it("should package _code into runnable function, for simple screen props", () => {
const screen = {
props: {
_id: "1234",
_code: "render('render argument');",
},
}
let renderArg
const render = arg => {
renderArg = arg
}
const uiFunctions = getFunctions(screen)
const targetfunction = uiFunctions[screen.props._id]
expect(targetfunction).toBeDefined()
targetfunction(render)
expect(renderArg).toBe("render argument")
})
it("should package _code into runnable function, for _children ", () => {
const screen = {
props: {
_id: "parent",
_code: "render('parent argument');",
_children: [
{
_id: "child1",
_code: "render('child 1 argument');",
},
{
_id: "child2",
_code: "render('child 2 argument');",
},
],
},
}
let renderArg
const render = arg => {
renderArg = arg
}
const uiFunctions = getFunctions(screen)
const targetfunction = uiFunctions["child2"]
expect(targetfunction).toBeDefined()
targetfunction(render)
expect(renderArg).toBe("child 2 argument")
})
})
const getFunctions = screen => {
const code = buildCodeForScreens([screen])
const func = new Function(`return ${code}`)()
return func
}

6
packages/server/scripts/exportAppTemplate.js

@ -23,6 +23,12 @@ yargs
},
async args => {
console.log("Exporting app..")
if (args.name == null || args.appId == null) {
console.error(
"Unable to export without a name and app ID being specified, check help for more info."
)
return
}
const exportPath = await exportTemplateFromApp({
templateName: args.name,
appId: args.appId,

140
packages/server/src/api/controllers/application.js

@ -1,21 +1,29 @@
const CouchDB = require("../../db")
const { getPackageForBuilder, buildPage } = require("../../utilities/builder")
const compileStaticAssetsForPage = require("../../utilities/builder/compileStaticAssetsForPage")
const env = require("../../environment")
const { copy, existsSync, readFile, writeFile } = require("fs-extra")
const { existsSync } = require("fs-extra")
const { budibaseAppsDir } = require("../../utilities/budibaseDir")
const sqrl = require("squirrelly")
const setBuilderToken = require("../../utilities/builder/setBuilderToken")
const fs = require("fs-extra")
const { join, resolve } = require("../../utilities/centralPath")
const { promisify } = require("util")
const chmodr = require("chmodr")
const packageJson = require("../../../package.json")
const { createLinkView } = require("../../db/linkedRows")
const { downloadTemplate } = require("../../utilities/templates")
const { generateAppID, DocumentTypes, SEPARATOR } = require("../../db/utils")
const {
generateAppID,
DocumentTypes,
SEPARATOR,
getPageParams,
generatePageID,
generateScreenID,
} = require("../../db/utils")
const {
downloadExtractComponentLibraries,
} = require("../../utilities/createAppPackage")
const { MAIN, UNAUTHENTICATED, PageTypes } = require("../../constants/pages")
const { HOME_SCREEN } = require("../../constants/screens")
const { cloneDeep } = require("lodash/fp")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
async function createInstance(template) {
@ -60,13 +68,31 @@ exports.fetch = async function(ctx) {
exports.fetchAppPackage = async function(ctx) {
const db = new CouchDB(ctx.params.appId)
const application = await db.get(ctx.params.appId)
ctx.body = await getPackageForBuilder(ctx.config, application)
let pages = await db.allDocs(
getPageParams(null, {
include_docs: true,
})
)
pages = pages.rows.map(row => row.doc)
const mainPage = pages.find(page => page.name === PageTypes.MAIN)
const unauthPage = pages.find(page => page.name === PageTypes.UNAUTHENTICATED)
ctx.body = {
application,
pages: {
main: mainPage,
unauthenticated: unauthPage,
},
}
await setBuilderToken(ctx, ctx.params.appId, application.version)
}
exports.create = async function(ctx) {
const instance = await createInstance(ctx.request.body.template)
const appId = instance._id
const version = packageJson.version
const newApplication = {
_id: appId,
type: "app",
@ -84,6 +110,7 @@ exports.create = async function(ctx) {
await downloadExtractComponentLibraries(newAppFolder)
}
await setBuilderToken(ctx, appId, version)
ctx.status = 200
ctx.body = newApplication
ctx.message = `Application ${ctx.request.body.name} created successfully`
@ -120,99 +147,38 @@ exports.delete = async function(ctx) {
}
const createEmptyAppPackage = async (ctx, app) => {
const templateFolder = resolve(
__dirname,
"..",
"..",
"utilities",
"appDirectoryTemplate"
)
const appsFolder = budibaseAppsDir()
const newAppFolder = resolve(appsFolder, app._id)
const db = new CouchDB(app._id)
if (existsSync(newAppFolder)) {
ctx.throw(400, "App folder already exists for this application")
}
await fs.ensureDir(join(newAppFolder, "pages", "main", "screens"), 0o777)
await fs.ensureDir(
join(newAppFolder, "pages", "unauthenticated", "screens"),
0o777
)
await copy(templateFolder, newAppFolder)
fs.mkdirpSync(newAppFolder)
// this line allows full permission on copied files
// we have an unknown problem without this, whereby the
// files get weird permissions and cant be written to :(
const chmodrPromise = promisify(chmodr)
await chmodrPromise(newAppFolder, 0o777)
const mainPage = cloneDeep(MAIN)
mainPage._id = generatePageID()
mainPage.title = app.name
await updateJsonFile(join(appsFolder, app._id, "package.json"), {
name: npmFriendlyAppName(app.name),
})
// if this app is being created from a template,
// copy the frontend page definition files from
// the template directory.
if (app.template) {
const templatePageDefinitions = join(
appsFolder,
"templates",
app.template.key,
"pages"
)
await copy(templatePageDefinitions, join(appsFolder, app._id, "pages"))
}
const unauthPage = cloneDeep(UNAUTHENTICATED)
unauthPage._id = generatePageID()
unauthPage.title = app.name
unauthPage.props._children[0].title = `Log in to ${app.name}`
const mainJson = await updateJsonFile(
join(appsFolder, app._id, "pages", "main", "page.json"),
app
)
const homeScreen = cloneDeep(HOME_SCREEN)
homeScreen._id = generateScreenID(mainPage._id)
await db.bulkDocs([mainPage, unauthPage, homeScreen])
await buildPage(ctx.config, app._id, "main", {
page: mainJson,
screens: await loadScreens(newAppFolder, "main"),
await compileStaticAssetsForPage(app._id, "main", {
page: mainPage,
screens: [homeScreen],
})
const unauthenticatedJson = await updateJsonFile(
join(appsFolder, app._id, "pages", "unauthenticated", "page.json"),
app
)
await buildPage(ctx.config, app._id, "unauthenticated", {
page: unauthenticatedJson,
screens: await loadScreens(newAppFolder, "unauthenticated"),
await compileStaticAssetsForPage(app._id, "unauthenticated", {
page: unauthPage,
screens: [],
})
return newAppFolder
}
const loadScreens = async (appFolder, page) => {
const screensFolder = join(appFolder, "pages", page, "screens")
const screenFiles = (await fs.readdir(screensFolder)).filter(s =>
s.endsWith(".json")
)
let screens = []
for (let file of screenFiles) {
screens.push(await fs.readJSON(join(screensFolder, file)))
}
return screens
}
const updateJsonFile = async (filePath, app) => {
const json = await readFile(filePath, "utf8")
const newJson = sqrl.Render(json, app)
await writeFile(filePath, newJson, "utf8")
return JSON.parse(newJson)
}
const npmFriendlyAppName = name =>
name
.replace(/_/g, "")
.replace(/./g, "")
.replace(/ /g, "")
.toLowerCase()

10
packages/server/src/api/controllers/deploy/aws.js

@ -42,6 +42,13 @@ exports.isInvalidationComplete = async function(
return resp.Invalidation.Status === "Completed"
}
/**
* Finalises the deployment, updating the quota for the user API key
* The verification process returns the levels to update to.
* Calls the "deployment-success" lambda.
* @param {object} quota The usage quota levels returned from the verifyDeploy
* @returns {Promise<object>} The usage has been updated against the user API key.
*/
exports.updateDeploymentQuota = async function(quota) {
const DEPLOYMENT_SUCCESS_URL =
env.DEPLOYMENT_CREDENTIALS_URL + "deploy/success"
@ -67,7 +74,8 @@ exports.updateDeploymentQuota = async function(quota) {
/**
* Verifies the users API key and
* Verifies that the deployment fits within the quota of the user,
* Verifies that the deployment fits within the quota of the user
* Links to the "check-api-key" lambda.
* @param {String} appId - appId being deployed
* @param {String} appId - appId being deployed
* @param {quota} quota - current quota being changed with this application

19
packages/server/src/api/controllers/page.js

@ -0,0 +1,19 @@
const CouchDB = require("../../db/client")
const { generatePageID } = require("../../db/utils")
const compileStaticAssetsForPage = require("../../utilities/builder/compileStaticAssetsForPage")
exports.save = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const appPackage = ctx.request.body
const page = await db.get(ctx.params.pageId)
await compileStaticAssetsForPage(ctx.user.appId, page.name, ctx.request.body)
// remove special doc props which couch will complain about
delete appPackage.page._css
delete appPackage.page._screens
appPackage.page._id = appPackage.page._id || generatePageID()
ctx.body = await db.put(appPackage.page)
ctx.status = 200
}

47
packages/server/src/api/controllers/screen.js

@ -1,17 +1,48 @@
/**
* This controller is not currently fully implemented. Screens are
* currently managed as part of the pages API, please look in api/routes/page.js
* for routes and controllers.
*/
const CouchDB = require("../../db")
const { getScreenParams, generateScreenID } = require("../../db/utils")
exports.fetch = async ctx => {
ctx.throw(501)
const db = new CouchDB(ctx.user.appId)
const screens = await db.allDocs(
getScreenParams(null, {
include_docs: true,
})
)
ctx.body = screens.rows.map(element => element.doc)
}
exports.find = async ctx => {
const db = new CouchDB(ctx.user.appId)
const screens = await db.allDocs(
getScreenParams(ctx.params.pageId, {
include_docs: true,
})
)
ctx.body = screens.response.rows
}
exports.save = async ctx => {
ctx.throw(501)
const appId = ctx.user.appId
const db = new CouchDB(appId)
const screen = ctx.request.body
if (!screen._id) {
screen._id = generateScreenID(ctx.params.pageId)
}
delete screen._css
const response = await db.put(screen)
ctx.message = `Screen ${screen.name} saved.`
ctx.body = response
}
exports.destroy = async ctx => {
ctx.throw(501)
const db = new CouchDB(ctx.user.appId)
await db.remove(ctx.params.screenId, ctx.params.revId)
ctx.message = "Screen deleted successfully"
ctx.status = 200
}

21
packages/server/src/api/controllers/templates.js

@ -2,25 +2,34 @@ const fetch = require("node-fetch")
const {
downloadTemplate,
exportTemplateFromApp,
getLocalTemplates,
} = require("../../utilities/templates")
const env = require("../../environment")
// development flag, can be used to test against templates exported locally
const DEFAULT_TEMPLATES_BUCKET =
"prod-budi-templates.s3-eu-west-1.amazonaws.com"
exports.fetch = async function(ctx) {
const { type = "app" } = ctx.query
const response = await fetch(
`https://${DEFAULT_TEMPLATES_BUCKET}/manifest.json`
)
const json = await response.json()
ctx.body = Object.values(json.templates[type])
if (env.LOCAL_TEMPLATES) {
ctx.body = Object.values(getLocalTemplates()[type])
} else {
const response = await fetch(
`https://${DEFAULT_TEMPLATES_BUCKET}/manifest.json`
)
const json = await response.json()
ctx.body = Object.values(json.templates[type])
}
}
exports.downloadTemplate = async function(ctx) {
const { type, name } = ctx.params
await downloadTemplate(type, name)
if (!env.LOCAL_TEMPLATES) {
await downloadTemplate(type, name)
}
ctx.body = {
message: `template ${type}:${name} downloaded successfully.`,

25
packages/server/src/api/controllers/user.js

@ -37,15 +37,22 @@ exports.create = async function(ctx) {
accessLevelId,
}
const response = await db.post(user)
ctx.status = 200
ctx.message = "User created successfully."
ctx.userId = response._id
ctx.body = {
_rev: response.rev,
username,
name,
try {
const response = await db.post(user)
ctx.status = 200
ctx.message = "User created successfully."
ctx.userId = response._id
ctx.body = {
_rev: response.rev,
username,
name,
}
} catch (err) {
if (err.status === 409) {
ctx.throw(400, "User exists already")
} else {
ctx.throw(err.status, err)
}
}
}

4
packages/server/src/api/index.js

@ -7,6 +7,7 @@ const { isDev } = require("../utilities")
const {
authRoutes,
pageRoutes,
screenRoutes,
userRoutes,
deployRoutes,
applicationRoutes,
@ -97,6 +98,9 @@ router.use(templatesRoutes.allowedMethods())
router.use(pageRoutes.routes())
router.use(pageRoutes.allowedMethods())
router.use(screenRoutes.routes())
router.use(screenRoutes.allowedMethods())
router.use(applicationRoutes.routes())
router.use(applicationRoutes.allowedMethods())

2
packages/server/src/api/routes/index.js

@ -1,5 +1,6 @@
const authRoutes = require("./auth")
const pageRoutes = require("./pages")
const screenRoutes = require("./screen")
const userRoutes = require("./user")
const applicationRoutes = require("./application")
const tableRoutes = require("./table")
@ -19,6 +20,7 @@ module.exports = {
deployRoutes,
authRoutes,
pageRoutes,
screenRoutes,
userRoutes,
applicationRoutes,
rowRoutes,

111
packages/server/src/api/routes/pages.js

@ -1,117 +1,10 @@
const Router = require("@koa/router")
const StatusCodes = require("../../utilities/statusCodes")
const joiValidator = require("../../middleware/joi-validator")
const Joi = require("joi")
const {
listScreens,
saveScreen,
buildPage,
renameScreen,
deleteScreen,
} = require("../../utilities/builder")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const controller = require("../controllers/page")
const router = Router()
function generateSaveValidation() {
// prettier-ignore
return joiValidator.body(Joi.object({
_css: Joi.string().allow(""),
name: Joi.string().required(),
route: Joi.string().required(),
props: Joi.object({
_id: Joi.string().required(),
_component: Joi.string().required(),
_children: Joi.array().required(),
_instanceName: Joi.string().required(),
_styles: Joi.object().required(),
type: Joi.string().optional(),
table: Joi.string().optional(),
}).required().unknown(true),
}).unknown(true))
}
function generatePatchValidation() {
return joiValidator.body(
Joi.object({
oldname: Joi.string().required(),
newname: Joi.string().required(),
}).unknown(true)
)
}
router.post(
"/_builder/api/:appId/pages/:pageName",
authorized(BUILDER),
async ctx => {
await buildPage(
ctx.config,
ctx.params.appId,
ctx.params.pageName,
ctx.request.body
)
ctx.response.status = StatusCodes.OK
}
)
router.get(
"/_builder/api/:appId/pages/:pagename/screens",
authorized(BUILDER),
async ctx => {
ctx.body = await listScreens(
ctx.config,
ctx.params.appId,
ctx.params.pagename
)
ctx.response.status = StatusCodes.OK
}
)
router.post(
"/_builder/api/:appId/pages/:pagename/screen",
authorized(BUILDER),
generateSaveValidation(),
async ctx => {
ctx.body = await saveScreen(
ctx.config,
ctx.params.appId,
ctx.params.pagename,
ctx.request.body
)
ctx.response.status = StatusCodes.OK
}
)
router.patch(
"/_builder/api/:appname/pages/:pagename/screen",
authorized(BUILDER),
generatePatchValidation(),
async ctx => {
await renameScreen(
ctx.config,
ctx.params.appname,
ctx.params.pagename,
ctx.request.body.oldname,
ctx.request.body.newname
)
ctx.response.status = StatusCodes.OK
}
)
router.delete(
"/_builder/api/pages/:pagename/screens/:id",
authorized(BUILDER),
async ctx => {
await deleteScreen(
ctx.config,
ctx.user.appId,
ctx.params.pagename,
ctx.params.id
)
ctx.response.status = StatusCodes.OK
}
)
router.post("/api/pages/:pageId", authorized(BUILDER), controller.save)
module.exports = router

34
packages/server/src/api/routes/screen.js

@ -2,12 +2,42 @@ const Router = require("@koa/router")
const controller = require("../controllers/screen")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const joiValidator = require("../../middleware/joi-validator")
const Joi = require("joi")
const router = Router()
function generateSaveValidation() {
// prettier-ignore
return joiValidator.body(Joi.object({
_css: Joi.string().allow(""),
name: Joi.string().required(),
route: Joi.string().required(),
props: Joi.object({
_id: Joi.string().required(),
_component: Joi.string().required(),
_children: Joi.array().required(),
_instanceName: Joi.string().required(),
_styles: Joi.object().required(),
type: Joi.string().optional(),
table: Joi.string().optional(),
}).required().unknown(true),
}).unknown(true))
}
router
.get("/api/screens", authorized(BUILDER), controller.fetch)
.post("/api/screens", authorized(BUILDER), controller.save)
.delete("/api/:screenId/:revId", authorized(BUILDER), controller.destroy)
.get("/api/screens/:pageId", authorized(BUILDER), controller.find)
.post(
"/api/screens/:pageId",
authorized(BUILDER),
generateSaveValidation(),
controller.save
)
.delete(
"/api/screens/:screenId/:revId",
authorized(BUILDER),
controller.destroy
)
module.exports = router

221
packages/server/src/constants/pages.js

@ -0,0 +1,221 @@
const PageTypes = {
MAIN: "main",
UNAUTHENTICATED: "unauthenticated",
}
const MAIN = {
componentLibraries: ["@budibase/standard-components"],
title: "{{ name }}",
favicon: "./_shared/favicon.png",
stylesheets: [],
name: PageTypes.MAIN,
props: {
_id: "private-master-root",
_component: "@budibase/standard-components/container",
_children: [
{
_id: "c74f07266980c4b6eafc33e2a6caa783d",
_component: "@budibase/standard-components/container",
_styles: {
normal: {
display: "flex",
"flex-direction": "row",
"justify-content": "flex-start",
"align-items": "flex-start",
background: "#fff",
width: "100%",
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
onLoad: [],
type: "div",
_appId: "inst_app_80b_f158d4057d2c4bedb0042d42fda8abaf",
_instanceName: "Header",
_children: [
{
_id: "49e0e519-9e5e-4127-885a-ee6a0a49e2c1",
_component: "@budibase/standard-components/Navigation",
_styles: {
normal: {
"max-width": "1400px",
"margin-left": "auto",
"margin-right": "auto",
padding: "20px",
color: "#757575",
"font-weight": "400",
"font-size": "16px",
flex: "1 1 auto",
},
hover: {},
active: {},
selected: {},
},
_code: "",
logoUrl:
"https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg",
title: "",
backgroundColor: "",
color: "",
borderWidth: "",
borderColor: "",
borderStyle: "",
_appId: "inst_cf8ace4_69efc0d72e6f443db2d4c902c14d9394",
_instanceName: "Navigation",
_children: [
{
_id: "48b35328-4c91-4343-a6a3-1a1fd77b3386",
_component: "@budibase/standard-components/link",
_styles: {
normal: {
"font-family": "Inter",
"font-weight": "500",
color: "#000000",
"text-decoration-line": "none",
"font-size": "16px",
},
hover: {
color: "#4285f4",
},
active: {},
selected: {},
},
_code: "",
url: "/",
openInNewTab: false,
text: "Home",
color: "",
hoverColor: "",
underline: false,
fontSize: "",
fontFamily: "initial",
_appId: "inst_cf8ace4_69efc0d72e6f443db2d4c902c14d9394",
_instanceName: "Home Link",
_children: [],
},
],
},
],
},
{
_id: "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967",
_component: "##builtin/screenslot",
_styles: {
normal: {
flex: "1 1 auto",
display: "flex",
"flex-direction": "column",
"justify-content": "flex-start",
"align-items": "stretch",
"max-width": "100%",
"margin-left": "20px",
"margin-right": "20px",
width: "1400px",
padding: "20px",
},
hover: {},
active: {},
selected: {},
},
_code: "",
_children: [],
},
],
type: "div",
_styles: {
active: {},
hover: {},
normal: {
display: "flex",
"flex-direction": "column",
"align-items": "center",
"justify-content": "flex-start",
"margin-right": "auto",
"margin-left": "auto",
"min-height": "100%",
"background-image":
"linear-gradient(135deg, rgba(252,215,212,1) 20%, rgba(207,218,255,1) 100%);",
},
selected: {},
},
_code: "",
className: "",
onLoad: [],
},
}
const UNAUTHENTICATED = {
componentLibraries: ["@budibase/standard-components"],
title: "{{ name }}",
favicon: "./_shared/favicon.png",
stylesheets: [],
name: PageTypes.UNAUTHENTICATED,
props: {
_id: "public-master-root",
_component: "@budibase/standard-components/container",
_children: [
{
_id: "686c252d-dbf2-4e28-9078-414ba4719759",
_component: "@budibase/standard-components/login",
_styles: {
normal: {
padding: "64px",
background: "rgba(255, 255, 255, 0.4)",
"border-radius": "0.5rem",
"margin-top": "0px",
margin: "0px",
"line-height": "1",
"box-shadow":
"0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)",
"font-size": "16px",
"font-family": "Inter",
flex: "0 1 auto",
transform: "0",
},
hover: {},
active: {},
selected: {},
},
_code: "",
loginRedirect: "",
usernameLabel: "Username",
passwordLabel: "Password",
loginButtonLabel: "Login",
buttonClass: "",
_instanceName: "Login",
inputClass: "",
_children: [],
title: "Log in to {{ name }}",
buttonText: "Log In",
logo:
"https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg",
},
],
type: "div",
_styles: {
active: {},
hover: {},
normal: {
display: "flex",
"flex-direction": "column",
"align-items": "center",
"justify-content": "center",
"margin-right": "auto",
"margin-left": "auto",
"min-height": "100%",
"background-image":
"linear-gradient(135deg, rgba(252,215,212,1) 20%, rgba(207,218,255,1) 100%);",
},
selected: {},
},
_code: "",
className: "",
onLoad: [],
},
}
module.exports = { MAIN, UNAUTHENTICATED, PageTypes }

103
packages/server/src/constants/screens.js

@ -0,0 +1,103 @@
exports.HOME_SCREEN = {
description: "",
url: "",
props: {
_id: "d834fea2-1b3e-4320-ab34-f9009f5ecc59",
_component: "@budibase/standard-components/container",
_styles: {
normal: {
flex: "1 1 auto",
display: "flex",
"flex-direction": "column",
"justify-content": "flex-start",
"align-items": "stretch",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
onLoad: [],
type: "div",
_children: [
{
_id: "ef60083f-4a02-4df3-80f3-a0d3d16847e7",
_component: "@budibase/standard-components/heading",
_styles: {
normal: {
"text-align": "left",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
text: "Welcome to your Budibase App 👋",
type: "h2",
_appId: "inst_cf8ace4_69efc0d72e6f443db2d4c902c14d9394",
_instanceName: "Heading",
_children: [],
},
{
_id: "cbbf41b27c2b44d1abba38bb694880c6a",
_component: "@budibase/standard-components/container",
_styles: {
normal: {
display: "flex",
"flex-direction": "column",
"justify-content": "center",
"align-items": "stretch",
flex: "1 1 auto",
"border-width": "4px",
"border-style": "Dashed",
"margin-bottom": "32px",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
onLoad: [],
type: "div",
_appId: "inst_app_2cc_ca3383f896034e9295345c05f7dfca0c",
_instanceName: "Video Container",
_children: [
{
_id: "c07d752cb3e544b418088fa9be84ba2e4",
_component: "@budibase/standard-components/embed",
_styles: {
normal: {
width: "100%",
flex: "1 1 auto",
opacity: "0",
"transition-property": "Opacity",
"transition-duration": "1s",
"transition-timing-function:": "ease-in",
},
hover: {
"transition-property": "Opacity",
"transition-duration": "1s",
"transition-timing-function:": "ease-out",
opacity: "1",
},
active: {},
selected: {},
},
_code: "",
embed:
'<iframe width="560" height="315" src="https://www.youtube.com/embed/dQw4w9WgXcQ" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>',
_appId: "inst_app_2cc_ca3383f896034e9295345c05f7dfca0c",
_instanceName: "Rick Astley Video",
_children: [],
},
],
},
],
_instanceName: "Home",
},
route: "/",
name: "d834fea2-1b3e-4320-ab34-f9009f5ecc59",
}

14
packages/server/src/db/client.js

@ -26,4 +26,18 @@ const Pouch = PouchDB.defaults(POUCH_DB_DEFAULTS)
allDbs(Pouch)
// replicate your local levelDB pouch to a running HTTP compliant couch or pouchdb server.
// eslint-disable-next-line no-unused-vars
function replicateLocal() {
Pouch.allDbs().then(dbs => {
for (let db of dbs) {
new Pouch(db).sync(
new PouchDB(`http://127.0.0.1:5984/${db}`, { live: true })
)
}
})
}
replicateLocal()
module.exports = Pouch

32
packages/server/src/db/utils.js

@ -13,6 +13,8 @@ const DocumentTypes = {
ACCESS_LEVEL: "ac",
WEBHOOK: "wh",
INSTANCE: "inst",
PAGE: "page",
SCREEN: "screen",
}
exports.DocumentTypes = DocumentTypes
@ -175,6 +177,36 @@ exports.generateWebhookID = () => {
return `${DocumentTypes.WEBHOOK}${SEPARATOR}${newid()}`
}
/**
* Generates a new page ID.
* @returns {string} The new page ID which the page doc can be stored under.
*/
exports.generatePageID = () => {
return `${DocumentTypes.PAGE}${SEPARATOR}${newid()}`
}
/**
* Gets parameters for retrieving pages, this is a utility function for the getDocParams function.
*/
exports.getPageParams = (pageId = null, otherProps = {}) => {
return getDocParams(DocumentTypes.PAGE, pageId, otherProps)
}
/**
* Generates a new screen ID.
* @returns {string} The new screen ID which the screen doc can be stored under.
*/
exports.generateScreenID = pageId => {
return `${DocumentTypes.SCREEN}${SEPARATOR}${pageId}${SEPARATOR}${newid()}`
}
/**
* Gets parameters for retrieving screens for a particular page, this is a utility function for the getDocParams function.
*/
exports.getScreenParams = (pageId = null, otherProps = {}) => {
return getDocParams(DocumentTypes.SCREEN, pageId, otherProps)
}
/**
* Gets parameters for retrieving a webhook, this is a utility function for the getDocParams function.
*/

1
packages/server/src/environment.js

@ -34,6 +34,7 @@ module.exports = {
USERID_API_KEY: process.env.USERID_API_KEY,
ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS,
DEPLOYMENT_DB_URL: process.env.DEPLOYMENT_DB_URL,
LOCAL_TEMPLATES: process.env.LOCAL_TEMPLATES,
_set(key, value) {
process.env[key] = value
module.exports[key] = value

144
packages/server/src/utilities/appDirectoryTemplate/pages/main/page.json

@ -1,144 +0,0 @@
{
"componentLibraries": [
"@budibase/standard-components"
],
"title": "{{ name }}",
"favicon": "./_shared/favicon.png",
"stylesheets": [],
"props": {
"_id": "private-master-root",
"_component": "@budibase/standard-components/container",
"_children": [
{
"_id": "c74f07266980c4b6eafc33e2a6caa783d",
"_component": "@budibase/standard-components/container",
"_styles": {
"normal": {
"display": "flex",
"flex-direction": "row",
"justify-content": "flex-start",
"align-items": "flex-start",
"background": "#fff",
"width": "100%",
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)"
},
"hover": {},
"active": {},
"selected": {}
},
"_code": "",
"className": "",
"onLoad": [],
"type": "div",
"_appId": "inst_app_80b_f158d4057d2c4bedb0042d42fda8abaf",
"_instanceName": "Header",
"_children": [
{
"_id": "49e0e519-9e5e-4127-885a-ee6a0a49e2c1",
"_component": "@budibase/standard-components/Navigation",
"_styles": {
"normal": {
"max-width": "1400px",
"margin-left": "auto",
"margin-right": "auto",
"padding": "20px",
"color": "#757575",
"font-weight": "400",
"font-size": "16px",
"flex": "1 1 auto"
},
"hover": {},
"active": {},
"selected": {}
},
"_code": "",
"logoUrl": "https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg",
"title": "",
"backgroundColor": "",
"color": "",
"borderWidth": "",
"borderColor": "",
"borderStyle": "",
"_appId": "inst_cf8ace4_69efc0d72e6f443db2d4c902c14d9394",
"_instanceName": "Navigation",
"_children": [
{
"_id": "48b35328-4c91-4343-a6a3-1a1fd77b3386",
"_component": "@budibase/standard-components/link",
"_styles": {
"normal": {
"font-family": "Inter",
"font-weight": "500",
"color": "#000000",
"text-decoration-line": "none",
"font-size": "16px"
},
"hover": {
"color": "#4285f4"
},
"active": {},
"selected": {}
},
"_code": "",
"url": "/",
"openInNewTab": false,
"text": "Home",
"color": "",
"hoverColor": "",
"underline": false,
"fontSize": "",
"fontFamily": "initial",
"_appId": "inst_cf8ace4_69efc0d72e6f443db2d4c902c14d9394",
"_instanceName": "Home Link",
"_children": []
}
]
}
]
},
{
"_id": "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967",
"_component": "##builtin/screenslot",
"_styles": {
"normal": {
"flex": "1 1 auto",
"display": "flex",
"flex-direction": "column",
"justify-content": "flex-start",
"align-items": "stretch",
"max-width": "100%",
"margin-left": "20px",
"margin-right": "20px",
"width": "1400px",
"padding": "20px"
},
"hover": {},
"active": {},
"selected": {}
},
"_code": "",
"_children": []
}
],
"type": "div",
"_styles": {
"active": {},
"hover": {},
"normal": {
"display": "flex",
"flex-direction": "column",
"align-items": "center",
"justify-content": "flex-start",
"margin-right": "auto",
"margin-left": "auto",
"min-height": "100%",
"background-image": "linear-gradient(135deg, rgba(252,215,212,1) 20%, rgba(207,218,255,1) 100%);"
},
"selected": {}
},
"_code": "",
"className": "",
"onLoad": []
},
"uiFunctions": ""
}

0
packages/server/src/utilities/appDirectoryTemplate/pages/main/screens/.gitkeep

102
packages/server/src/utilities/appDirectoryTemplate/pages/main/screens/d834fea2-1b3e-4320-ab34-f9009f5ecc59.json

@ -1,102 +0,0 @@
{
"description": "",
"url": "",
"props": {
"_id": "d834fea2-1b3e-4320-ab34-f9009f5ecc59",
"_component": "@budibase/standard-components/container",
"_styles": {
"normal": {
"flex": "1 1 auto",
"display": "flex",
"flex-direction": "column",
"justify-content": "flex-start",
"align-items": "stretch"
},
"hover": {},
"active": {},
"selected": {}
},
"_code": "",
"className": "",
"onLoad": [],
"type": "div",
"_children": [
{
"_id": "ef60083f-4a02-4df3-80f3-a0d3d16847e7",
"_component": "@budibase/standard-components/heading",
"_styles": {
"normal": {
"text-align": "left"
},
"hover": {},
"active": {},
"selected": {}
},
"_code": "",
"className": "",
"text": "Welcome to your Budibase App 👋",
"type": "h2",
"_appId": "inst_cf8ace4_69efc0d72e6f443db2d4c902c14d9394",
"_instanceName": "Heading",
"_children": []
},
{
"_id": "cbbf41b27c2b44d1abba38bb694880c6a",
"_component": "@budibase/standard-components/container",
"_styles": {
"normal": {
"display": "flex",
"flex-direction": "column",
"justify-content": "center",
"align-items": "stretch",
"flex": "1 1 auto",
"border-width": "4px",
"border-style": "Dashed",
"margin-bottom": "32px"
},
"hover": {},
"active": {},
"selected": {}
},
"_code": "",
"className": "",
"onLoad": [],
"type": "div",
"_appId": "inst_app_2cc_ca3383f896034e9295345c05f7dfca0c",
"_instanceName": "Video Container",
"_children": [
{
"_id": "c07d752cb3e544b418088fa9be84ba2e4",
"_component": "@budibase/standard-components/embed",
"_styles": {
"normal": {
"width": "100%",
"flex": "1 1 auto",
"opacity": "0",
"transition-property": "Opacity",
"transition-duration": "1s",
"transition-timing-function:": "ease-in"
},
"hover": {
"transition-property": "Opacity",
"transition-duration": "1s",
"transition-timing-function:": "ease-out",
"opacity": "1"
},
"active": {},
"selected": {}
},
"_code": "",
"embed": "<iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/dQw4w9WgXcQ\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>",
"_appId": "inst_app_2cc_ca3383f896034e9295345c05f7dfca0c",
"_instanceName": "Rick Astley Video",
"_children": []
}
]
}
],
"_instanceName": "Home"
},
"route": "/",
"name": "d834fea2-1b3e-4320-ab34-f9009f5ecc59"
}

68
packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/page.json

@ -1,68 +0,0 @@
{
"componentLibraries": [
"@budibase/standard-components"
],
"title": "{{ name }}",
"favicon": "./_shared/favicon.png",
"stylesheets": [],
"props": {
"_id": "public-master-root",
"_component": "@budibase/standard-components/container",
"_children": [
{
"_id": "686c252d-dbf2-4e28-9078-414ba4719759",
"_component": "@budibase/standard-components/login",
"_styles": {
"normal": {
"padding": "64px",
"background": "rgba(255, 255, 255, 0.4)",
"border-radius": "0.5rem",
"margin-top": "0px",
"margin": "0px",
"line-height": "1",
"box-shadow": "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)",
"font-size": "16px",
"font-family": "Inter",
"flex": "0 1 auto",
"transform": "0"
},
"hover": {},
"active": {},
"selected": {}
},
"_code": "",
"loginRedirect": "",
"usernameLabel": "Username",
"passwordLabel": "Password",
"loginButtonLabel": "Login",
"buttonClass": "",
"_instanceName": "Login",
"inputClass": "",
"_children": [],
"title": "Log in to {{ name }}",
"buttonText": "Log In",
"logo": "https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg"
}
],
"type": "div",
"_styles": {
"active": {},
"hover": {},
"normal": {
"display": "flex",
"flex-direction": "column",
"align-items": "center",
"justify-content": "center",
"margin-right": "auto",
"margin-left": "auto",
"min-height": "100%",
"background-image": "linear-gradient(135deg, rgba(252,215,212,1) 20%, rgba(207,218,255,1) 100%);"
},
"selected": {}
},
"_code": "",
"className": "",
"onLoad": []
},
"uiFunctions": ""
}

0
packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/screens/.gitkeep

0
packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/screens/placeholder

1
packages/server/src/utilities/appDirectoryTemplate/plugins.js

@ -1 +0,0 @@
module.exports = () => ({})

58
packages/server/src/utilities/builder/buildPage.js → packages/server/src/utilities/builder/compileStaticAssetsForPage.js

@ -1,35 +1,24 @@
const { appPackageFolder } = require("../createAppPackage")
const {
constants,
copyFile,
writeFile,
readFile,
writeJSON,
} = require("fs-extra")
const { constants, copyFile, writeFile, readFile } = require("fs-extra")
const { join, resolve } = require("../centralPath")
const sqrl = require("squirrelly")
const { convertCssToFiles } = require("./convertCssToFiles")
const publicPath = require("./publicPath")
const deleteCodeMeta = require("./deleteCodeMeta")
const { budibaseAppsDir } = require("../budibaseDir")
module.exports = async (config, appId, pageName, pkg) => {
const appPath = appPackageFolder(config, appId)
module.exports = async (appId, pageName, pkg) => {
const appPath = join(budibaseAppsDir(), appId)
pkg.screens = pkg.screens || []
await convertCssToFiles(publicPath(appPath, pageName), pkg)
await buildIndexHtml(config, appId, pageName, appPath, pkg)
await buildIndexHtml(appId, pageName, appPath, pkg)
await buildFrontendAppDefinition(config, appId, pageName, pkg, appPath)
await buildFrontendAppDefinition(appId, pageName, pkg, appPath)
await copyClientLib(appPath, pageName)
await savePageJson(appPath, pageName, pkg)
}
const rootPath = (config, appId) => (config.useAppRootPath ? `/${appId}` : "")
const copyClientLib = async (appPath, pageName) => {
const sourcepath = require.resolve("@budibase/client")
const destPath = join(publicPath(appPath, pageName), "budibase-client.js")
@ -43,11 +32,10 @@ const copyClientLib = async (appPath, pageName) => {
)
}
const buildIndexHtml = async (config, appId, pageName, appPath, pkg) => {
const buildIndexHtml = async (appId, pageName, appPath, pkg) => {
const appPublicPath = publicPath(appPath, pageName)
const stylesheetUrl = s =>
s.startsWith("http") ? s : `/${rootPath(config, appId)}/${s}`
const stylesheetUrl = s => (s.startsWith("http") ? s : `/${appId}/${s}`)
const templateObj = {
title: pkg.page.title || "Budibase App",
@ -77,15 +65,13 @@ const buildIndexHtml = async (config, appId, pageName, appPath, pkg) => {
await writeFile(deployableHtmlPath, deployableHtml, { flag: "w+" })
}
const buildFrontendAppDefinition = async (config, appId, pageName, pkg) => {
const appPath = appPackageFolder(config, appId)
const buildFrontendAppDefinition = async (appId, pageName, pkg) => {
const appPath = join(budibaseAppsDir(), appId)
const appPublicPath = publicPath(appPath, pageName)
const filename = join(appPublicPath, "clientFrontendDefinition.js")
if (pkg.page._css) {
delete pkg.page._css
}
delete pkg.page._css
for (let screen of pkg.screens) {
if (screen._css) {
@ -106,25 +92,3 @@ const buildFrontendAppDefinition = async (config, appId, pageName, pkg) => {
`
)
}
const savePageJson = async (appPath, pageName, pkg) => {
const pageFile = join(appPath, "pages", pageName, "page.json")
if (pkg.page._css) {
delete pkg.page._css
}
if (pkg.page.name) {
delete pkg.page.name
}
if (pkg.page._screens) {
delete pkg.page._screens
}
deleteCodeMeta(pkg.page.props)
await writeJSON(pageFile, pkg.page, {
spaces: 2,
})
}

9
packages/server/src/utilities/builder/deleteCodeMeta.js

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

20
packages/server/src/utilities/builder/getPages.js

@ -1,20 +0,0 @@
const { readJSON, readdir } = require("fs-extra")
const { join } = require("../centralPath")
module.exports = async appPath => {
const pages = {}
const pageFolders = await readdir(join(appPath, "pages"))
for (let pageFolder of pageFolders) {
try {
pages[pageFolder] = await readJSON(
join(appPath, "pages", pageFolder, "page.json")
)
pages[pageFolder].name = pageFolder
} catch (_) {
// ignore error
}
}
return pages
}

98
packages/server/src/utilities/builder/index.js

@ -1,98 +0,0 @@
const { appPackageFolder } = require("../createAppPackage")
const {
readJSON,
writeJSON,
readdir,
ensureDir,
rename,
unlink,
rmdir,
} = require("fs-extra")
const { join, resolve } = require("../centralPath")
const { dirname } = require("path")
const buildPage = require("./buildPage")
const getPages = require("./getPages")
const listScreens = require("./listScreens")
const deleteCodeMeta = require("./deleteCodeMeta")
module.exports.buildPage = buildPage
module.exports.listScreens = listScreens
const getAppDefinition = async appPath =>
await readJSON(`${appPath}/appDefinition.json`)
module.exports.getPackageForBuilder = async (config, application) => {
const appPath = resolve(config.latestPackagesFolder, application._id)
const pages = await getPages(appPath)
return {
pages,
application,
}
}
const screenPath = (appPath, pageName, name) =>
join(appPath, "pages", pageName, "screens", name + ".json")
module.exports.saveScreen = async (config, appname, pagename, screen) => {
const appPath = appPackageFolder(config, appname)
const compPath = screenPath(appPath, pagename, screen.props._id)
await ensureDir(dirname(compPath))
if (screen._css) {
delete screen._css
}
deleteCodeMeta(screen.props)
await writeJSON(compPath, screen, {
encoding: "utf8",
flag: "w",
spaces: 2,
})
return screen
}
module.exports.renameScreen = async (
config,
appname,
pagename,
oldName,
newName
) => {
const appPath = appPackageFolder(config, appname)
const oldComponentPath = screenPath(appPath, pagename, oldName)
const newComponentPath = screenPath(appPath, pagename, newName)
await ensureDir(dirname(newComponentPath))
await rename(oldComponentPath, newComponentPath)
}
module.exports.deleteScreen = async (config, appId, pagename, name) => {
const appPath = appPackageFolder(config, appId)
const componentFile = screenPath(appPath, pagename, name)
await unlink(componentFile)
const dir = dirname(componentFile)
if ((await readdir(dir)).length === 0) {
await rmdir(dir)
}
}
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)
}

48
packages/server/src/utilities/builder/listScreens.js

@ -1,48 +0,0 @@
const { appPackageFolder } = require("../createAppPackage")
const { readJSON, readdir, stat } = require("fs-extra")
const { join } = require("../centralPath")
const { keyBy } = require("lodash/fp")
module.exports = async (config, appname, pagename) => {
const appPath = appPackageFolder(config, appname)
return keyBy("name")(await fetchscreens(appPath, pagename))
}
const fetchscreens = async (appPath, pagename, relativePath = "") => {
const currentDir = join(appPath, "pages", pagename, "screens", relativePath)
const contents = await readdir(currentDir)
const screens = []
for (let item of contents) {
const itemRelativePath = join(relativePath, item)
const itemFullPath = join(currentDir, item)
const stats = await stat(itemFullPath)
if (stats.isFile()) {
if (!item.endsWith(".json")) continue
const component = await readJSON(itemFullPath)
component.name = itemRelativePath
.substring(0, itemRelativePath.length - 5)
.replace(/\\/g, "/")
component.props = component.props || {}
screens.push(component)
} else {
const childComponents = await fetchscreens(
appPath,
join(relativePath, item)
)
for (let c of childComponents) {
screens.push(c)
}
}
}
return screens
}

1
packages/server/src/utilities/builder/setBuilderToken.js

@ -21,6 +21,7 @@ module.exports = async (ctx, appId, version) => {
// set the builder token
setCookie(ctx, "builder", token)
setCookie(ctx, "currentapp", appId)
// need to clear all app tokens or else unable to use the app in the builder
let allDbNames = await CouchDB.allDbs()
allDbNames.map(dbName => {

5
packages/server/src/utilities/createAppPackage.js

@ -1,5 +1,3 @@
const { resolve } = require("./centralPath")
const { cwd } = require("process")
const stream = require("stream")
const fetch = require("node-fetch")
const tar = require("tar-fs")
@ -9,9 +7,6 @@ const packageJson = require("../../package.json")
const streamPipeline = promisify(stream.pipeline)
exports.appPackageFolder = (config, appname) =>
resolve(cwd(), config.latestPackagesFolder, appname)
exports.downloadExtractComponentLibraries = async appFolder => {
const LIBRARIES = ["standard-components"]

54
packages/server/src/utilities/templates.js

@ -8,12 +8,35 @@ const zlib = require("zlib")
const { promisify } = require("util")
const streamPipeline = promisify(stream.pipeline)
const { budibaseAppsDir } = require("./budibaseDir")
const env = require("../environment")
const CouchDB = require("../db")
const { DocumentTypes } = require("../db/utils")
const DEFAULT_TEMPLATES_BUCKET =
"prod-budi-templates.s3-eu-west-1.amazonaws.com"
exports.getLocalTemplates = function() {
const templatesDir = join(os.homedir(), ".budibase", "templates", "app")
const templateObj = { app: {} }
fs.ensureDirSync(templatesDir)
const templateNames = fs.readdirSync(templatesDir)
for (let name of templateNames) {
templateObj.app[name] = {
name,
category: "local",
description: "local template",
type: "app",
key: `app/${name}`,
}
}
return templateObj
}
exports.downloadTemplate = async function(type, name) {
const dirName = join(budibaseAppsDir(), "templates", type, name)
if (env.LOCAL_TEMPLATES) {
return dirName
}
const templateUrl = `https://${DEFAULT_TEMPLATES_BUCKET}/templates/${type}/${name}.tar.gz`
const response = await fetch(templateUrl)
@ -30,26 +53,27 @@ exports.downloadTemplate = async function(type, name) {
tar.extract(join(budibaseAppsDir(), "templates", type))
)
return join(budibaseAppsDir(), "templates", type, name)
return dirName
}
exports.exportTemplateFromApp = async function({ templateName, appId }) {
// Copy frontend files
const appToExport = join(os.homedir(), ".budibase", appId, "pages")
const templatesDir = join(os.homedir(), ".budibase", "templates")
fs.ensureDirSync(templatesDir)
const templateOutputPath = join(templatesDir, templateName)
fs.copySync(appToExport, join(templateOutputPath, "pages"))
fs.ensureDirSync(join(templateOutputPath, "db"))
const writeStream = fs.createWriteStream(
join(templateOutputPath, "db", "dump.txt")
const templatesDir = join(
os.homedir(),
".budibase",
"templates",
"app",
templateName,
"db"
)
fs.ensureDirSync(templatesDir)
const writeStream = fs.createWriteStream(join(templatesDir, "dump.txt"))
// perform couch dump
const instanceDb = new CouchDB(appId)
await instanceDb.dump(writeStream)
return templateOutputPath
await instanceDb.dump(writeStream, {
filter: doc => {
return !doc._id.startsWith(DocumentTypes.USER)
},
})
return templatesDir
}

Loading…
Cancel
Save