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

Pages and Screens to couch as well as general store tidy up
This commit is contained in:
Michael Drury 2020-11-06 14:54:54 +00:00 committed by GitHub
commit bd65dd5992
66 changed files with 1289 additions and 1661 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: [],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
)
fs.mkdirpSync(newAppFolder)
await copy(templateFolder, newAppFolder)
const mainPage = cloneDeep(MAIN)
mainPage._id = generatePageID()
mainPage.title = app.name
// 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 unauthPage = cloneDeep(UNAUTHENTICATED)
unauthPage._id = generatePageID()
unauthPage.title = app.name
unauthPage.props._children[0].title = `Log in to ${app.name}`
await updateJsonFile(join(appsFolder, app._id, "package.json"), {
name: npmFriendlyAppName(app.name),
const homeScreen = cloneDeep(HOME_SCREEN)
homeScreen._id = generateScreenID(mainPage._id)
await db.bulkDocs([mainPage, unauthPage, homeScreen])
await compileStaticAssetsForPage(app._id, "main", {
page: mainPage,
screens: [homeScreen],
})
// 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 mainJson = await updateJsonFile(
join(appsFolder, app._id, "pages", "main", "page.json"),
app
)
await buildPage(ctx.config, app._id, "main", {
page: mainJson,
screens: await loadScreens(newAppFolder, "main"),
})
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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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