diff --git a/packages/builder/src/builderStore/buildCodeForScreens.js b/packages/builder/src/builderStore/buildCodeForScreens.js deleted file mode 100644 index 69ef0d6e0b..0000000000 --- a/packages/builder/src/builderStore/buildCodeForScreens.js +++ /dev/null @@ -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} -}, -` diff --git a/packages/builder/src/builderStore/getNewComponentName.js b/packages/builder/src/builderStore/getNewComponentName.js index a4565c2296..a69bec21ad 100644 --- a/packages/builder/src/builderStore/getNewComponentName.js +++ b/packages/builder/src/builderStore/getNewComponentName.js @@ -1,5 +1,7 @@ import { walkProps } from "./storeUtils" import { get_capitalised_name } from "../helpers" +import { get } from "svelte/store" +import { allScreens } from "builderStore" export default function(component, state) { const capitalised = get_capitalised_name( @@ -25,7 +27,7 @@ export default function(component, state) { findMatches(state.currentPreviewItem.props) } else { // viewing master page - need to find against all screens - for (let screen of state.screens) { + for (let screen of get(allScreens)) { findMatches(screen.props) } } @@ -33,7 +35,7 @@ export default function(component, state) { let index = 1 let name while (!name) { - const tryName = `${capitalised} ${index}` + const tryName = `${capitalised || "Copy"} ${index}` if (!matchingComponents.includes(tryName)) name = tryName index++ } diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js index 6317640955..1bd86480a4 100644 --- a/packages/builder/src/builderStore/index.js +++ b/packages/builder/src/builderStore/index.js @@ -1,14 +1,36 @@ -import { getStore } from "./store" +import { getFrontendStore } from "./store/frontend" import { getBackendUiStore } from "./store/backend" import { getAutomationStore } from "./store/automation/" import { getThemeStore } from "./store/theme" +import { derived } from "svelte/store" import analytics from "analytics" -export const store = getStore() +export const store = getFrontendStore() export const backendUiStore = getBackendUiStore() export const automationStore = getAutomationStore() export const themeStore = getThemeStore() +export const allScreens = derived(store, $store => { + let screens = [] + if ($store.pages == null) { + return screens + } + for (let page of Object.values($store.pages)) { + screens = screens.concat(page._screens) + } + return screens +}) + +export const currentScreens = derived(store, $store => { + const currentScreens = $store.pages[$store.currentPageName]?._screens + if (currentScreens == null) { + return [] + } + return Array.isArray(currentScreens) + ? currentScreens + : Object.values(currentScreens) +}) + export const initialise = async () => { try { await analytics.activate() diff --git a/packages/builder/src/builderStore/insertCodeMetadata.js b/packages/builder/src/builderStore/insertCodeMetadata.js deleted file mode 100644 index 9093c4003d..0000000000 --- a/packages/builder/src/builderStore/insertCodeMetadata.js +++ /dev/null @@ -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), - } -} diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js new file mode 100644 index 0000000000..ee0c98ed8d --- /dev/null +++ b/packages/builder/src/builderStore/store/frontend.js @@ -0,0 +1,529 @@ +import { get, writable } from "svelte/store" +import { cloneDeep } from "lodash/fp" +import { + createProps, + getBuiltin, + makePropsSafe, +} from "components/userInterface/pagesParsing/createProps" +import { getExactComponent } from "components/userInterface/pagesParsing/searchComponents" +import { allScreens, backendUiStore } from "builderStore" +import { generate_screen_css } from "../generate_css" +import { fetchComponentLibDefinitions } from "../loadComponentLibraries" +import api from "../api" +import { DEFAULT_PAGES_OBJECT } from "../../constants" +import getNewComponentName from "../getNewComponentName" +import analytics from "analytics" +import { + findChildComponentType, + generateNewIdsForComponent, + getComponentDefinition, + getParent, +} from "../storeUtils" + +const INITIAL_FRONTEND_STATE = { + apps: [], + name: "", + description: "", + pages: DEFAULT_PAGES_OBJECT, + mainUi: {}, + unauthenticatedUi: {}, + components: [], + currentPreviewItem: null, + currentComponentInfo: null, + currentFrontEndType: "none", + currentPageName: "", + currentComponentProps: null, + errors: [], + hasAppPackage: false, + libraries: null, + appId: "", +} + +export const getFrontendStore = () => { + const store = writable({ ...INITIAL_FRONTEND_STATE }) + + store.actions = { + // TODO: REFACTOR + initialise: async pkg => { + store.update(state => { + state.appId = pkg.application._id + return state + }) + const screens = await api.get("/api/screens").then(r => r.json()) + + const mainScreens = screens.filter(screen => + screen._id.includes(pkg.pages.main._id) + ), + unauthScreens = screens.filter(screen => + screen._id.includes(pkg.pages.unauthenticated._id) + ) + pkg.pages = { + main: { + ...pkg.pages.main, + _screens: mainScreens, + }, + unauthenticated: { + ...pkg.pages.unauthenticated, + _screens: unauthScreens, + }, + } + + // if the app has just been created + // we need to build the CSS and save + if (pkg.justCreated) { + for (let pageName of ["main", "unauthenticated"]) { + const page = pkg.pages[pageName] + store.actions.screens.regenerateCss(page) + for (let screen of page._screens) { + store.actions.screens.regenerateCss(screen) + } + + await api.post(`/api/pages/${page._id}`, { + page: { + componentLibraries: pkg.application.componentLibraries, + ...page, + }, + screens: page._screens, + }) + } + } + + pkg.justCreated = false + + const components = await fetchComponentLibDefinitions(pkg.application._id) + + store.update(state => ({ + ...state, + libraries: pkg.application.componentLibraries, + components, + name: pkg.application.name, + description: pkg.application.description, + appId: pkg.application._id, + pages: pkg.pages, + hasAppPackage: true, + currentScreens: [], + builtins: [getBuiltin("##builtin/screenslot")], + appInstance: pkg.application.instance, + })) + + await backendUiStore.actions.database.select(pkg.application.instance) + }, + selectPageOrScreen: type => { + store.update(state => { + state.currentFrontEndType = type + + const pageOrScreen = + type === "page" + ? state.pages[state.currentPageName] + : state.pages[state.currentPageName]._screens[0] + + state.currentComponentInfo = pageOrScreen ? pageOrScreen.props : null + state.currentPreviewItem = pageOrScreen + state.currentView = "detail" + return state + }) + }, + screens: { + select: screenName => { + store.update(state => { + const screen = getExactComponent(get(allScreens), screenName, true) + state.currentPreviewItem = screen + state.currentFrontEndType = "screen" + state.currentView = "detail" + + store.actions.screens.regenerateCssForCurrentScreen() + // this.regenerateCssForCurrentScreen() + // regenerateCssForCurrentScreen(s) + const safeProps = makePropsSafe( + state.components[screen.props._component], + screen.props + ) + screen.props = safeProps + state.currentComponentInfo = safeProps + return state + }) + }, + create: async screen => { + let savePromise + store.update(state => { + state.currentPreviewItem = screen + state.currentComponentInfo = screen.props + state.currentFrontEndType = "screen" + + if (state.currentPreviewItem) { + store.actions.screens.regenerateCss(state.currentPreviewItem) + } + + savePromise = store.actions.screens.save(screen) + return state + }) + + await savePromise + }, + save: async screen => { + const storeContents = get(store) + const pageName = storeContents.currentPageName || "main" + const currentPage = storeContents.pages[pageName] + const currentPageScreens = currentPage._screens + + let savePromise + const response = await api.post( + `/api/screens/${currentPage._id}`, + screen + ) + const json = await response.json() + screen._rev = json.rev + screen._id = json.id + const foundScreen = currentPageScreens.findIndex( + el => el._id === screen._id + ) + if (currentPageScreens !== -1) { + currentPageScreens.splice(foundScreen, 1) + } + currentPageScreens.push(screen) + + // TODO: should carry out all server updates to screen in a single call + store.update(state => { + state.pages[pageName]._screens = currentPageScreens + state.currentPreviewItem = screen + const safeProps = makePropsSafe( + state.components[screen.props._component], + screen.props + ) + state.currentComponentInfo = safeProps + screen.props = safeProps + savePromise = store.actions.pages.save() + return state + }) + await savePromise + }, + regenerateCss: screen => { + screen._css = generate_screen_css([screen.props]) + }, + regenerateCssForCurrentScreen: () => { + const { currentPreviewItem } = get(store) + if (currentPreviewItem) { + store.actions.screens.regenerateCss(currentPreviewItem) + } + }, + delete: async (screensToDelete, pageName) => { + let deletePromise + + store.update(state => { + if (pageName == null) { + pageName = state.pages.main.name + } + for (let screenToDelete of Array.isArray(screenToDelete) + ? screenToDelete + : [screenToDelete]) { + // Remove screen from current page as well + // TODO: Should be done server side + state.pages[pageName]._screens = state.pages[ + pageName + ]._screens.filter(scr => scr.name !== screenToDelete.name) + deletePromise = api.delete( + `/api/screens/${screenToDelete._id}/${screenToDelete._rev}` + ) + } + return state + }) + await deletePromise + }, + }, + preview: { + // _saveCurrentPreviewItem + saveSelected: () => { + const state = get(store) + state.currentFrontEndType === "page" + ? store.actions.pages.save() + : store.actions.screens.save(state.currentPreviewItem) + }, + }, + pages: { + select: pageName => { + store.update(state => { + const currentPage = state.pages[pageName] + + state.currentScreens = currentPage._screens + state.currentFrontEndType = "page" + state.currentView = "detail" + state.currentPageName = pageName + + // This is the root of many problems. + // Uncaught (in promise) TypeError: Cannot read property '_component' of undefined + // it appears that the currentPage sometimes has _props instead of props + // why + const safeProps = makePropsSafe( + state.components[currentPage.props._component], + currentPage.props + ) + state.currentComponentInfo = safeProps + currentPage.props = safeProps + state.currentPreviewItem = state.pages[pageName] + store.actions.screens.regenerateCssForCurrentScreen() + + for (let screen of get(allScreens)) { + screen._css = generate_screen_css([screen.props]) + } + + return state + }) + }, + save: async page => { + const storeContents = get(store) + const pageName = storeContents.currentPageName || "main" + const pageToSave = page || storeContents.pages[pageName] + + // TODO: revisit. This sends down a very weird payload + const response = await api + .post(`/api/pages/${pageToSave._id}`, { + page: { + componentLibraries: storeContents.pages.componentLibraries, + ...pageToSave, + }, + screens: pageToSave._screens, + }) + .then(response => response.json()) + + store.update(state => { + state.pages[pageName]._rev = response.rev + return state + }) + }, + }, + components: { + select: component => { + store.update(state => { + const componentDef = component._component.startsWith("##") + ? component + : state.components[component._component] + state.currentComponentInfo = makePropsSafe(componentDef, component) + state.currentView = "component" + return state + }) + }, + // addChildComponent + create: (componentToAdd, presetProps) => { + store.update(state => { + function findSlot(component_array) { + for (let i = 0; i < component_array.length; i += 1) { + if (component_array[i]._component === "##builtin/screenslot") { + return true + } + + if (component_array[i]._children) findSlot(component_array[i]) + } + + return false + } + + if ( + componentToAdd.startsWith("##") && + findSlot(state.pages[state.currentPageName].props._children) + ) { + return state + } + + const component = getComponentDefinition(state, componentToAdd) + + const instanceId = get(backendUiStore).selectedDatabase._id + const instanceName = getNewComponentName(component, state) + + const newComponent = createProps(component, { + ...presetProps, + _instanceId: instanceId, + _instanceName: instanceName, + }) + + const currentComponent = + state.components[state.currentComponentInfo._component] + + const targetParent = currentComponent.children + ? state.currentComponentInfo + : getParent( + state.currentPreviewItem.props, + state.currentComponentInfo + ) + + // Don't continue if there's no parent + if (!targetParent) { + return state + } + + targetParent._children = targetParent._children.concat( + newComponent.props + ) + + store.actions.preview.saveSelected() + + state.currentView = "component" + state.currentComponentInfo = newComponent.props + analytics.captureEvent("Added Component", { + name: newComponent.props._component, + }) + return state + }) + }, + copy: (component, cut = false) => { + store.update(state => { + state.componentToPaste = cloneDeep(component) + state.componentToPaste.isCut = cut + if (cut) { + const parent = getParent( + state.currentPreviewItem.props, + component._id + ) + parent._children = parent._children.filter( + c => c._id !== component._id + ) + store.actions.components.select(parent) + } + + return state + }) + }, + paste: (targetComponent, mode) => { + store.update(state => { + if (!state.componentToPaste) return state + + const componentToPaste = cloneDeep(state.componentToPaste) + // retain the same ids as things may be referencing this component + if (componentToPaste.isCut) { + // in case we paste a second time + state.componentToPaste.isCut = false + } else { + generateNewIdsForComponent(componentToPaste, state) + } + delete componentToPaste.isCut + + if (mode === "inside") { + targetComponent._children.push(componentToPaste) + return state + } + + const parent = getParent( + state.currentPreviewItem.props, + targetComponent + ) + + const targetIndex = parent._children.indexOf(targetComponent) + const index = mode === "above" ? targetIndex : targetIndex + 1 + parent._children.splice(index, 0, cloneDeep(componentToPaste)) + + store.actions.screens.regenerateCssForCurrentScreen() + store.actions.preview.saveSelected() + store.actions.components.select(componentToPaste) + + return state + }) + }, + updateStyle: (type, name, value) => { + store.update(state => { + if (!state.currentComponentInfo._styles) { + state.currentComponentInfo._styles = {} + } + state.currentComponentInfo._styles[type][name] = value + + store.actions.screens.regenerateCssForCurrentScreen() + + // save without messing with the store + store.actions.preview.saveSelected() + return state + }) + }, + updateProp: (name, value) => { + store.update(state => { + let current_component = state.currentComponentInfo + current_component[name] = value + + state.currentComponentInfo = current_component + store.actions.preview.saveSelected() + return state + }) + }, + findRoute: component => { + // Gets all the components to needed to construct a path. + const tempStore = get(store) + let pathComponents = [] + let parent = component + let root = false + while (!root) { + parent = getParent(tempStore.currentPreviewItem.props, parent) + if (!parent) { + root = true + } else { + pathComponents.push(parent) + } + } + + // Remove root entry since it's the screen or page layout. + // Reverse array since we need the correct order of the IDs + const reversedComponents = pathComponents.reverse().slice(1) + + // Add component + const allComponents = [...reversedComponents, component] + + // Map IDs + const IdList = allComponents.map(c => c._id) + + // Construct ID Path: + return IdList.join("/") + }, + links: { + save: async (url, title) => { + let savePromise + store.update(state => { + // Try to extract a nav component from the master screen + const nav = findChildComponentType( + state.pages.main, + "@budibase/standard-components/Navigation" + ) + if (nav) { + let newLink + + // Clone an existing link if one exists + if (nav._children && nav._children.length) { + // Clone existing link style + newLink = cloneDeep(nav._children[0]) + + // Manipulate IDs to ensure uniqueness + generateNewIdsForComponent(newLink, state, false) + + // Set our new props + newLink._instanceName = `${title} Link` + newLink.url = url + newLink.text = title + } else { + // Otherwise create vanilla new link + const component = getComponentDefinition( + state, + "@budibase/standard-components/link" + ) + const instanceId = get(backendUiStore).selectedDatabase._id + newLink = createProps(component, { + url, + text: title, + _instanceName: `${title} Link`, + _instanceId: instanceId, + }).props + } + + // Save page and regenerate all CSS because otherwise weird things happen + nav._children = [...nav._children, newLink] + state.currentPageName = "main" + store.actions.screens.regenerateCss(state.pages.main) + for (let screen of state.pages.main._screens) { + store.actions.screens.regenerateCss(screen) + } + savePromise = store.actions.pages.save() + } + return state + }) + await savePromise + }, + }, + }, + } + + return store +} diff --git a/packages/builder/src/builderStore/store/index.js b/packages/builder/src/builderStore/store/index.js deleted file mode 100644 index 0aa3d69ecd..0000000000 --- a/packages/builder/src/builderStore/store/index.js +++ /dev/null @@ -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 - }) -} diff --git a/packages/builder/src/builderStore/storeUtils.js b/packages/builder/src/builderStore/storeUtils.js index 2c486a9bdb..9c9d1ef940 100644 --- a/packages/builder/src/builderStore/storeUtils.js +++ b/packages/builder/src/builderStore/storeUtils.js @@ -1,21 +1,7 @@ -import { - makePropsSafe, - getBuiltin, -} from "components/userInterface/pagesParsing/createProps" -import api from "./api" -import { generate_screen_css } from "./generate_css" +import { getBuiltin } from "components/userInterface/pagesParsing/createProps" import { uuid } from "./uuid" import getNewComponentName from "./getNewComponentName" -export const selectComponent = (state, component) => { - const componentDef = component._component.startsWith("##") - ? component - : state.components[component._component] - state.currentComponentInfo = makePropsSafe(componentDef, component) - state.currentView = "component" - return state -} - export const getParent = (rootProps, child) => { let parent walkProps(rootProps, (p, breakWalk) => { @@ -30,41 +16,6 @@ export const getParent = (rootProps, child) => { return parent } -export const saveCurrentPreviewItem = s => - s.currentFrontEndType === "page" - ? savePage(s) - : saveScreenApi(s.currentPreviewItem, s) - -export const savePage = async s => { - const pageName = s.currentPageName || "main" - const page = s.pages[pageName] - await api.post(`/_builder/api/${s.appId}/pages/${pageName}`, { - page: { componentLibraries: s.pages.componentLibraries, ...page }, - uiFunctions: s.currentPageFunctions, - screens: page._screens, - }) -} - -export const saveScreenApi = (screen, s) => { - api - .post(`/_builder/api/${s.appId}/pages/${s.currentPageName}/screen`, screen) - .then(() => savePage(s)) -} - -export const renameCurrentScreen = (newname, state) => { - const oldname = state.currentPreviewItem.props._instanceName - state.currentPreviewItem.props._instanceName = newname - - api.patch( - `/_builder/api/${state.appId}/pages/${state.currentPageName}/screen`, - { - oldname, - newname, - } - ) - return state -} - export const walkProps = (props, action, cancelToken = null) => { cancelToken = cancelToken || { cancelled: false } action(props, () => { @@ -79,21 +30,14 @@ export const walkProps = (props, action, cancelToken = null) => { } } -export const regenerateCssForScreen = screen => { - screen._css = generate_screen_css([screen.props]) -} - -export const regenerateCssForCurrentScreen = state => { - if (state.currentPreviewItem) { - regenerateCssForScreen(state.currentPreviewItem) - } - return state -} - -export const generateNewIdsForComponent = (c, state, changeName = true) => - walkProps(c, p => { - p._id = uuid() - if (changeName) p._instanceName = getNewComponentName(p._component, state) +export const generateNewIdsForComponent = ( + component, + state, + changeName = true +) => + walkProps(component, prop => { + prop._id = uuid() + if (changeName) prop._instanceName = getNewComponentName(prop, state) }) export const getComponentDefinition = (state, name) => diff --git a/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte b/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte index 146f881b2b..0fe19f9bc0 100644 --- a/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte +++ b/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte @@ -55,7 +55,7 @@ // Record the table that created this screen so we can link it later screen.autoTableId = table._id try { - await store.createScreen(screen) + await store.actions.screens.create(screen) } catch (_) { // TODO: this is temporary // a cypress test is failing, because I added the @@ -70,7 +70,7 @@ const listPage = screens.find(screen => screen.props._instanceName.endsWith("List") ) - await store.createLink(listPage.route, table.name) + await store.actions.components.links.save(listPage.route, table.name) // Navigate to new table $goto(`./table/${table._id}`) diff --git a/packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte b/packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte index c794999ac7..1fdbe35151 100644 --- a/packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte +++ b/packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte @@ -1,5 +1,5 @@
@@ -239,7 +235,7 @@ diff --git a/packages/builder/src/components/userInterface/ComponentDropdownMenu.svelte b/packages/builder/src/components/userInterface/ComponentDropdownMenu.svelte index 62eb49145d..999014f0cd 100644 --- a/packages/builder/src/components/userInterface/ComponentDropdownMenu.svelte +++ b/packages/builder/src/components/userInterface/ComponentDropdownMenu.svelte @@ -4,7 +4,7 @@ import { getComponentDefinition } from "builderStore/storeUtils" import ConfirmDialog from "components/common/ConfirmDialog.svelte" import { last } from "lodash/fp" - import { getParent, saveCurrentPreviewItem } from "builderStore/storeUtils" + import { getParent } from "builderStore/storeUtils" import { DropdownMenu } from "@budibase/bbui" import { DropdownContainer, DropdownItem } from "components/common/Dropdowns" @@ -25,50 +25,50 @@ } const selectComponent = component => { - store.selectComponent(component) - const path = store.getPathToComponent(component) + store.actions.components.select(component) + const path = store.actions.components.findRoute(component) $goto(`./:page/:screen/${path}`) } const moveUpComponent = () => { - store.update(s => { - const parent = getParent(s.currentPreviewItem.props, component) + store.update(state => { + const parent = getParent(state.currentPreviewItem.props, component) if (parent) { const currentIndex = parent._children.indexOf(component) - if (currentIndex === 0) return s + if (currentIndex === 0) return state const newChildren = parent._children.filter(c => c !== component) newChildren.splice(currentIndex - 1, 0, component) parent._children = newChildren } - s.currentComponentInfo = component - saveCurrentPreviewItem(s) + state.currentComponentInfo = component + store.actions.preview.saveSelected() - return s + return state }) } const moveDownComponent = () => { - store.update(s => { - const parent = getParent(s.currentPreviewItem.props, component) + store.update(state => { + const parent = getParent(state.currentPreviewItem.props, component) if (parent) { const currentIndex = parent._children.indexOf(component) - if (currentIndex === parent._children.length - 1) return s + if (currentIndex === parent._children.length - 1) return state const newChildren = parent._children.filter(c => c !== component) newChildren.splice(currentIndex + 1, 0, component) parent._children = newChildren } - s.currentComponentInfo = component - saveCurrentPreviewItem(s) + state.currentComponentInfo = component + store.actions.preview.saveSelected() - return s + return state }) } - const copyComponent = () => { + const duplicateComponent = () => { storeComponentForCopy(false) pasteComponent("below") } @@ -82,19 +82,19 @@ selectComponent(parent) } - saveCurrentPreviewItem(state) + store.actions.preview.saveSelected() return state }) } const storeComponentForCopy = (cut = false) => { // lives in store - also used by drag drop - store.storeComponentForCopy(component, cut) + store.actions.components.copy(component, cut) } const pasteComponent = mode => { // lives in store - also used by drag drop - store.pasteComponent(component, mode) + store.actions.components.paste(component, mode) } @@ -117,7 +117,7 @@ + on:click={duplicateComponent} /> { + 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}
diff --git a/packages/builder/src/components/userInterface/ComponentSelectionList.svelte b/packages/builder/src/components/userInterface/ComponentSelectionList.svelte index f43faf033d..5dc9c05ea5 100644 --- a/packages/builder/src/components/userInterface/ComponentSelectionList.svelte +++ b/packages/builder/src/components/userInterface/ComponentSelectionList.svelte @@ -25,8 +25,8 @@ } const onComponentChosen = component => { - store.addChildComponent(component._component, component.presetProps) - const path = store.getPathToComponent($store.currentComponentInfo) + store.actions.components.create(component._component, component.presetProps) + const path = store.actions.components.findRoute($store.currentComponentInfo) $goto(`./:page/:screen/${path}`) close() } diff --git a/packages/builder/src/components/userInterface/ComponentsHierarchy.svelte b/packages/builder/src/components/userInterface/ComponentsHierarchy.svelte index 48fe72f8d6..a9140e2978 100644 --- a/packages/builder/src/components/userInterface/ComponentsHierarchy.svelte +++ b/packages/builder/src/components/userInterface/ComponentsHierarchy.svelte @@ -32,7 +32,7 @@ ]) const changeScreen = screen => { - store.setCurrentScreen(screen.props._instanceName) + store.actions.screens.select(screen.props._instanceName) $goto(`./:page/${screen.props._instanceName}`) } diff --git a/packages/builder/src/components/userInterface/ComponentsHierarchyChildren.svelte b/packages/builder/src/components/userInterface/ComponentsHierarchyChildren.svelte index d854c9b906..7bedf5e6a8 100644 --- a/packages/builder/src/components/userInterface/ComponentsHierarchyChildren.svelte +++ b/packages/builder/src/components/userInterface/ComponentsHierarchyChildren.svelte @@ -40,10 +40,10 @@ const selectComponent = component => { // Set current component - store.selectComponent(component) + store.actions.components.select(component) // Get ID path - const path = store.getPathToComponent(component) + const path = store.actions.components.findRoute(component) // Go to correct URL $goto(`./:page/:screen/${path}`) @@ -96,8 +96,8 @@ const drop = () => { if ($dragDropStore.targetComponent !== $dragDropStore.componentToDrop) { - store.storeComponentForCopy($dragDropStore.componentToDrop, true) - store.pasteComponent( + store.actions.components.copy($dragDropStore.componentToDrop, true) + store.actions.components.paste( $dragDropStore.targetComponent, $dragDropStore.dropPosition ) diff --git a/packages/builder/src/components/userInterface/ComponentsPaneSwitcher.svelte b/packages/builder/src/components/userInterface/ComponentsPaneSwitcher.svelte index 5904221596..4e865b4514 100644 --- a/packages/builder/src/components/userInterface/ComponentsPaneSwitcher.svelte +++ b/packages/builder/src/components/userInterface/ComponentsPaneSwitcher.svelte @@ -1,5 +1,5 @@
- {#if $store.currentFrontEndType === 'page' || $store.screens.length} + {#if $store.currentFrontEndType === 'page' || $allScreens.length}
diff --git a/packages/builder/tests/buildCodeForScreen.spec.js b/packages/builder/tests/buildCodeForScreen.spec.js deleted file mode 100644 index 87f4c02fa3..0000000000 --- a/packages/builder/tests/buildCodeForScreen.spec.js +++ /dev/null @@ -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 -} diff --git a/packages/server/scripts/exportAppTemplate.js b/packages/server/scripts/exportAppTemplate.js index 72ad797183..8268b802d4 100755 --- a/packages/server/scripts/exportAppTemplate.js +++ b/packages/server/scripts/exportAppTemplate.js @@ -23,6 +23,12 @@ yargs }, async args => { console.log("Exporting app..") + if (args.name == null || args.appId == null) { + console.error( + "Unable to export without a name and app ID being specified, check help for more info." + ) + return + } const exportPath = await exportTemplateFromApp({ templateName: args.name, appId: args.appId, diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js index a8596e38ac..2185293352 100644 --- a/packages/server/src/api/controllers/application.js +++ b/packages/server/src/api/controllers/application.js @@ -1,21 +1,29 @@ const CouchDB = require("../../db") -const { getPackageForBuilder, buildPage } = require("../../utilities/builder") +const compileStaticAssetsForPage = require("../../utilities/builder/compileStaticAssetsForPage") const env = require("../../environment") -const { copy, existsSync, readFile, writeFile } = require("fs-extra") +const { existsSync } = require("fs-extra") const { budibaseAppsDir } = require("../../utilities/budibaseDir") -const sqrl = require("squirrelly") const setBuilderToken = require("../../utilities/builder/setBuilderToken") const fs = require("fs-extra") const { join, resolve } = require("../../utilities/centralPath") -const { promisify } = require("util") -const chmodr = require("chmodr") const packageJson = require("../../../package.json") const { createLinkView } = require("../../db/linkedRows") const { downloadTemplate } = require("../../utilities/templates") -const { generateAppID, DocumentTypes, SEPARATOR } = require("../../db/utils") +const { + generateAppID, + DocumentTypes, + SEPARATOR, + getPageParams, + generatePageID, + generateScreenID, +} = require("../../db/utils") const { downloadExtractComponentLibraries, } = require("../../utilities/createAppPackage") +const { MAIN, UNAUTHENTICATED, PageTypes } = require("../../constants/pages") +const { HOME_SCREEN } = require("../../constants/screens") +const { cloneDeep } = require("lodash/fp") + const APP_PREFIX = DocumentTypes.APP + SEPARATOR async function createInstance(template) { @@ -60,13 +68,31 @@ exports.fetch = async function(ctx) { exports.fetchAppPackage = async function(ctx) { const db = new CouchDB(ctx.params.appId) const application = await db.get(ctx.params.appId) - ctx.body = await getPackageForBuilder(ctx.config, application) + + let pages = await db.allDocs( + getPageParams(null, { + include_docs: true, + }) + ) + pages = pages.rows.map(row => row.doc) + + const mainPage = pages.find(page => page.name === PageTypes.MAIN) + const unauthPage = pages.find(page => page.name === PageTypes.UNAUTHENTICATED) + ctx.body = { + application, + pages: { + main: mainPage, + unauthenticated: unauthPage, + }, + } + await setBuilderToken(ctx, ctx.params.appId, application.version) } exports.create = async function(ctx) { const instance = await createInstance(ctx.request.body.template) const appId = instance._id + const version = packageJson.version const newApplication = { _id: appId, type: "app", @@ -84,6 +110,7 @@ exports.create = async function(ctx) { await downloadExtractComponentLibraries(newAppFolder) } + await setBuilderToken(ctx, appId, version) ctx.status = 200 ctx.body = newApplication ctx.message = `Application ${ctx.request.body.name} created successfully` @@ -120,99 +147,38 @@ exports.delete = async function(ctx) { } const createEmptyAppPackage = async (ctx, app) => { - const templateFolder = resolve( - __dirname, - "..", - "..", - "utilities", - "appDirectoryTemplate" - ) - const appsFolder = budibaseAppsDir() const newAppFolder = resolve(appsFolder, app._id) + const db = new CouchDB(app._id) + if (existsSync(newAppFolder)) { ctx.throw(400, "App folder already exists for this application") } - await fs.ensureDir(join(newAppFolder, "pages", "main", "screens"), 0o777) - await fs.ensureDir( - join(newAppFolder, "pages", "unauthenticated", "screens"), - 0o777 - ) + 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() diff --git a/packages/server/src/api/controllers/deploy/aws.js b/packages/server/src/api/controllers/deploy/aws.js index 3e19812a00..e6cd514cac 100644 --- a/packages/server/src/api/controllers/deploy/aws.js +++ b/packages/server/src/api/controllers/deploy/aws.js @@ -42,6 +42,13 @@ exports.isInvalidationComplete = async function( return resp.Invalidation.Status === "Completed" } +/** + * Finalises the deployment, updating the quota for the user API key + * The verification process returns the levels to update to. + * Calls the "deployment-success" lambda. + * @param {object} quota The usage quota levels returned from the verifyDeploy + * @returns {Promise} 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 diff --git a/packages/server/src/api/controllers/page.js b/packages/server/src/api/controllers/page.js new file mode 100644 index 0000000000..4f3989ae90 --- /dev/null +++ b/packages/server/src/api/controllers/page.js @@ -0,0 +1,19 @@ +const CouchDB = require("../../db/client") +const { generatePageID } = require("../../db/utils") +const compileStaticAssetsForPage = require("../../utilities/builder/compileStaticAssetsForPage") + +exports.save = async function(ctx) { + const db = new CouchDB(ctx.user.appId) + + const appPackage = ctx.request.body + + const page = await db.get(ctx.params.pageId) + await compileStaticAssetsForPage(ctx.user.appId, page.name, ctx.request.body) + + // remove special doc props which couch will complain about + delete appPackage.page._css + delete appPackage.page._screens + appPackage.page._id = appPackage.page._id || generatePageID() + ctx.body = await db.put(appPackage.page) + ctx.status = 200 +} diff --git a/packages/server/src/api/controllers/screen.js b/packages/server/src/api/controllers/screen.js index 91c3c83dd5..88166bf0b2 100644 --- a/packages/server/src/api/controllers/screen.js +++ b/packages/server/src/api/controllers/screen.js @@ -1,17 +1,48 @@ -/** - * This controller is not currently fully implemented. Screens are - * currently managed as part of the pages API, please look in api/routes/page.js - * for routes and controllers. - */ +const CouchDB = require("../../db") +const { getScreenParams, generateScreenID } = require("../../db/utils") exports.fetch = async ctx => { - ctx.throw(501) + const db = new CouchDB(ctx.user.appId) + + const screens = await db.allDocs( + getScreenParams(null, { + include_docs: true, + }) + ) + + ctx.body = screens.rows.map(element => element.doc) +} + +exports.find = async ctx => { + const db = new CouchDB(ctx.user.appId) + + const screens = await db.allDocs( + getScreenParams(ctx.params.pageId, { + include_docs: true, + }) + ) + + ctx.body = screens.response.rows } exports.save = async ctx => { - ctx.throw(501) + const appId = ctx.user.appId + const db = new CouchDB(appId) + const screen = ctx.request.body + + if (!screen._id) { + screen._id = generateScreenID(ctx.params.pageId) + } + delete screen._css + const response = await db.put(screen) + + ctx.message = `Screen ${screen.name} saved.` + ctx.body = response } exports.destroy = async ctx => { - ctx.throw(501) + const db = new CouchDB(ctx.user.appId) + await db.remove(ctx.params.screenId, ctx.params.revId) + ctx.message = "Screen deleted successfully" + ctx.status = 200 } diff --git a/packages/server/src/api/controllers/templates.js b/packages/server/src/api/controllers/templates.js index 8e21b3c18b..94243d3c75 100644 --- a/packages/server/src/api/controllers/templates.js +++ b/packages/server/src/api/controllers/templates.js @@ -2,25 +2,34 @@ const fetch = require("node-fetch") const { downloadTemplate, exportTemplateFromApp, + getLocalTemplates, } = require("../../utilities/templates") +const env = require("../../environment") +// development flag, can be used to test against templates exported locally const DEFAULT_TEMPLATES_BUCKET = "prod-budi-templates.s3-eu-west-1.amazonaws.com" exports.fetch = async function(ctx) { const { type = "app" } = ctx.query - const response = await fetch( - `https://${DEFAULT_TEMPLATES_BUCKET}/manifest.json` - ) - const json = await response.json() - ctx.body = Object.values(json.templates[type]) + if (env.LOCAL_TEMPLATES) { + ctx.body = Object.values(getLocalTemplates()[type]) + } else { + const response = await fetch( + `https://${DEFAULT_TEMPLATES_BUCKET}/manifest.json` + ) + const json = await response.json() + ctx.body = Object.values(json.templates[type]) + } } exports.downloadTemplate = async function(ctx) { const { type, name } = ctx.params - await downloadTemplate(type, name) + if (!env.LOCAL_TEMPLATES) { + await downloadTemplate(type, name) + } ctx.body = { message: `template ${type}:${name} downloaded successfully.`, diff --git a/packages/server/src/api/controllers/user.js b/packages/server/src/api/controllers/user.js index ba6adeb060..5e4f963f5b 100644 --- a/packages/server/src/api/controllers/user.js +++ b/packages/server/src/api/controllers/user.js @@ -37,15 +37,22 @@ exports.create = async function(ctx) { accessLevelId, } - const response = await db.post(user) - - ctx.status = 200 - ctx.message = "User created successfully." - ctx.userId = response._id - ctx.body = { - _rev: response.rev, - username, - name, + try { + const response = await db.post(user) + ctx.status = 200 + ctx.message = "User created successfully." + ctx.userId = response._id + ctx.body = { + _rev: response.rev, + username, + name, + } + } catch (err) { + if (err.status === 409) { + ctx.throw(400, "User exists already") + } else { + ctx.throw(err.status, err) + } } } diff --git a/packages/server/src/api/index.js b/packages/server/src/api/index.js index 0fbd65501b..631e5a18e4 100644 --- a/packages/server/src/api/index.js +++ b/packages/server/src/api/index.js @@ -7,6 +7,7 @@ const { isDev } = require("../utilities") const { authRoutes, pageRoutes, + screenRoutes, userRoutes, deployRoutes, applicationRoutes, @@ -97,6 +98,9 @@ router.use(templatesRoutes.allowedMethods()) router.use(pageRoutes.routes()) router.use(pageRoutes.allowedMethods()) +router.use(screenRoutes.routes()) +router.use(screenRoutes.allowedMethods()) + router.use(applicationRoutes.routes()) router.use(applicationRoutes.allowedMethods()) diff --git a/packages/server/src/api/routes/index.js b/packages/server/src/api/routes/index.js index 0025c5fabf..a19742097c 100644 --- a/packages/server/src/api/routes/index.js +++ b/packages/server/src/api/routes/index.js @@ -1,5 +1,6 @@ const authRoutes = require("./auth") const pageRoutes = require("./pages") +const screenRoutes = require("./screen") const userRoutes = require("./user") const applicationRoutes = require("./application") const tableRoutes = require("./table") @@ -19,6 +20,7 @@ module.exports = { deployRoutes, authRoutes, pageRoutes, + screenRoutes, userRoutes, applicationRoutes, rowRoutes, diff --git a/packages/server/src/api/routes/pages.js b/packages/server/src/api/routes/pages.js index 43293a8911..1ec01dc780 100644 --- a/packages/server/src/api/routes/pages.js +++ b/packages/server/src/api/routes/pages.js @@ -1,117 +1,10 @@ const Router = require("@koa/router") -const StatusCodes = require("../../utilities/statusCodes") -const joiValidator = require("../../middleware/joi-validator") -const Joi = require("joi") -const { - listScreens, - saveScreen, - buildPage, - renameScreen, - deleteScreen, -} = require("../../utilities/builder") const authorized = require("../../middleware/authorized") const { BUILDER } = require("../../utilities/accessLevels") +const controller = require("../controllers/page") const router = Router() -function generateSaveValidation() { - // prettier-ignore - return joiValidator.body(Joi.object({ - _css: Joi.string().allow(""), - name: Joi.string().required(), - route: Joi.string().required(), - props: Joi.object({ - _id: Joi.string().required(), - _component: Joi.string().required(), - _children: Joi.array().required(), - _instanceName: Joi.string().required(), - _styles: Joi.object().required(), - type: Joi.string().optional(), - table: Joi.string().optional(), - }).required().unknown(true), - }).unknown(true)) -} - -function generatePatchValidation() { - return joiValidator.body( - Joi.object({ - oldname: Joi.string().required(), - newname: Joi.string().required(), - }).unknown(true) - ) -} - -router.post( - "/_builder/api/:appId/pages/:pageName", - authorized(BUILDER), - async ctx => { - await buildPage( - ctx.config, - ctx.params.appId, - ctx.params.pageName, - ctx.request.body - ) - ctx.response.status = StatusCodes.OK - } -) - -router.get( - "/_builder/api/:appId/pages/:pagename/screens", - authorized(BUILDER), - async ctx => { - ctx.body = await listScreens( - ctx.config, - ctx.params.appId, - ctx.params.pagename - ) - ctx.response.status = StatusCodes.OK - } -) - -router.post( - "/_builder/api/:appId/pages/:pagename/screen", - authorized(BUILDER), - generateSaveValidation(), - async ctx => { - ctx.body = await saveScreen( - ctx.config, - ctx.params.appId, - ctx.params.pagename, - ctx.request.body - ) - ctx.response.status = StatusCodes.OK - } -) - -router.patch( - "/_builder/api/:appname/pages/:pagename/screen", - authorized(BUILDER), - generatePatchValidation(), - async ctx => { - await renameScreen( - ctx.config, - ctx.params.appname, - ctx.params.pagename, - ctx.request.body.oldname, - ctx.request.body.newname - ) - ctx.response.status = StatusCodes.OK - } -) - -router.delete( - "/_builder/api/pages/:pagename/screens/:id", - authorized(BUILDER), - async ctx => { - await deleteScreen( - ctx.config, - ctx.user.appId, - ctx.params.pagename, - ctx.params.id - ) - - ctx.response.status = StatusCodes.OK - } -) +router.post("/api/pages/:pageId", authorized(BUILDER), controller.save) module.exports = router diff --git a/packages/server/src/api/routes/screen.js b/packages/server/src/api/routes/screen.js index 60e29bf363..407bbd1a94 100644 --- a/packages/server/src/api/routes/screen.js +++ b/packages/server/src/api/routes/screen.js @@ -2,12 +2,42 @@ const Router = require("@koa/router") const controller = require("../controllers/screen") const authorized = require("../../middleware/authorized") const { BUILDER } = require("../../utilities/accessLevels") +const joiValidator = require("../../middleware/joi-validator") +const Joi = require("joi") const router = Router() +function generateSaveValidation() { + // prettier-ignore + return joiValidator.body(Joi.object({ + _css: Joi.string().allow(""), + name: Joi.string().required(), + route: Joi.string().required(), + props: Joi.object({ + _id: Joi.string().required(), + _component: Joi.string().required(), + _children: Joi.array().required(), + _instanceName: Joi.string().required(), + _styles: Joi.object().required(), + type: Joi.string().optional(), + table: Joi.string().optional(), + }).required().unknown(true), + }).unknown(true)) +} + router .get("/api/screens", authorized(BUILDER), controller.fetch) - .post("/api/screens", authorized(BUILDER), controller.save) - .delete("/api/:screenId/:revId", authorized(BUILDER), controller.destroy) + .get("/api/screens/:pageId", authorized(BUILDER), controller.find) + .post( + "/api/screens/:pageId", + authorized(BUILDER), + generateSaveValidation(), + controller.save + ) + .delete( + "/api/screens/:screenId/:revId", + authorized(BUILDER), + controller.destroy + ) module.exports = router diff --git a/packages/server/src/constants/pages.js b/packages/server/src/constants/pages.js new file mode 100644 index 0000000000..5fe74d6123 --- /dev/null +++ b/packages/server/src/constants/pages.js @@ -0,0 +1,221 @@ +const PageTypes = { + MAIN: "main", + UNAUTHENTICATED: "unauthenticated", +} + +const MAIN = { + componentLibraries: ["@budibase/standard-components"], + title: "{{ name }}", + favicon: "./_shared/favicon.png", + stylesheets: [], + name: PageTypes.MAIN, + props: { + _id: "private-master-root", + _component: "@budibase/standard-components/container", + _children: [ + { + _id: "c74f07266980c4b6eafc33e2a6caa783d", + _component: "@budibase/standard-components/container", + _styles: { + normal: { + display: "flex", + "flex-direction": "row", + "justify-content": "flex-start", + "align-items": "flex-start", + background: "#fff", + width: "100%", + "box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)", + }, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + className: "", + onLoad: [], + type: "div", + _appId: "inst_app_80b_f158d4057d2c4bedb0042d42fda8abaf", + _instanceName: "Header", + _children: [ + { + _id: "49e0e519-9e5e-4127-885a-ee6a0a49e2c1", + _component: "@budibase/standard-components/Navigation", + _styles: { + normal: { + "max-width": "1400px", + "margin-left": "auto", + "margin-right": "auto", + padding: "20px", + color: "#757575", + "font-weight": "400", + "font-size": "16px", + flex: "1 1 auto", + }, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + logoUrl: + "https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg", + title: "", + backgroundColor: "", + color: "", + borderWidth: "", + borderColor: "", + borderStyle: "", + _appId: "inst_cf8ace4_69efc0d72e6f443db2d4c902c14d9394", + _instanceName: "Navigation", + _children: [ + { + _id: "48b35328-4c91-4343-a6a3-1a1fd77b3386", + _component: "@budibase/standard-components/link", + _styles: { + normal: { + "font-family": "Inter", + "font-weight": "500", + color: "#000000", + "text-decoration-line": "none", + "font-size": "16px", + }, + hover: { + color: "#4285f4", + }, + active: {}, + selected: {}, + }, + _code: "", + url: "/", + openInNewTab: false, + text: "Home", + color: "", + hoverColor: "", + underline: false, + fontSize: "", + fontFamily: "initial", + _appId: "inst_cf8ace4_69efc0d72e6f443db2d4c902c14d9394", + _instanceName: "Home Link", + _children: [], + }, + ], + }, + ], + }, + { + _id: "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967", + _component: "##builtin/screenslot", + _styles: { + normal: { + flex: "1 1 auto", + display: "flex", + "flex-direction": "column", + "justify-content": "flex-start", + "align-items": "stretch", + "max-width": "100%", + "margin-left": "20px", + "margin-right": "20px", + width: "1400px", + padding: "20px", + }, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + _children: [], + }, + ], + type: "div", + _styles: { + active: {}, + hover: {}, + normal: { + display: "flex", + "flex-direction": "column", + "align-items": "center", + "justify-content": "flex-start", + "margin-right": "auto", + "margin-left": "auto", + "min-height": "100%", + "background-image": + "linear-gradient(135deg, rgba(252,215,212,1) 20%, rgba(207,218,255,1) 100%);", + }, + selected: {}, + }, + _code: "", + className: "", + onLoad: [], + }, +} + +const UNAUTHENTICATED = { + componentLibraries: ["@budibase/standard-components"], + title: "{{ name }}", + favicon: "./_shared/favicon.png", + stylesheets: [], + name: PageTypes.UNAUTHENTICATED, + props: { + _id: "public-master-root", + _component: "@budibase/standard-components/container", + _children: [ + { + _id: "686c252d-dbf2-4e28-9078-414ba4719759", + _component: "@budibase/standard-components/login", + _styles: { + normal: { + padding: "64px", + background: "rgba(255, 255, 255, 0.4)", + "border-radius": "0.5rem", + "margin-top": "0px", + margin: "0px", + "line-height": "1", + "box-shadow": + "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)", + "font-size": "16px", + "font-family": "Inter", + flex: "0 1 auto", + transform: "0", + }, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + loginRedirect: "", + usernameLabel: "Username", + passwordLabel: "Password", + loginButtonLabel: "Login", + buttonClass: "", + _instanceName: "Login", + inputClass: "", + _children: [], + title: "Log in to {{ name }}", + buttonText: "Log In", + logo: + "https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg", + }, + ], + type: "div", + _styles: { + active: {}, + hover: {}, + normal: { + display: "flex", + "flex-direction": "column", + "align-items": "center", + "justify-content": "center", + "margin-right": "auto", + "margin-left": "auto", + "min-height": "100%", + "background-image": + "linear-gradient(135deg, rgba(252,215,212,1) 20%, rgba(207,218,255,1) 100%);", + }, + selected: {}, + }, + _code: "", + className: "", + onLoad: [], + }, +} + +module.exports = { MAIN, UNAUTHENTICATED, PageTypes } diff --git a/packages/server/src/constants/screens.js b/packages/server/src/constants/screens.js new file mode 100644 index 0000000000..f9a0fb68dc --- /dev/null +++ b/packages/server/src/constants/screens.js @@ -0,0 +1,103 @@ +exports.HOME_SCREEN = { + description: "", + url: "", + props: { + _id: "d834fea2-1b3e-4320-ab34-f9009f5ecc59", + _component: "@budibase/standard-components/container", + _styles: { + normal: { + flex: "1 1 auto", + display: "flex", + "flex-direction": "column", + "justify-content": "flex-start", + "align-items": "stretch", + }, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + className: "", + onLoad: [], + type: "div", + _children: [ + { + _id: "ef60083f-4a02-4df3-80f3-a0d3d16847e7", + _component: "@budibase/standard-components/heading", + _styles: { + normal: { + "text-align": "left", + }, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + className: "", + text: "Welcome to your Budibase App 👋", + type: "h2", + _appId: "inst_cf8ace4_69efc0d72e6f443db2d4c902c14d9394", + _instanceName: "Heading", + _children: [], + }, + { + _id: "cbbf41b27c2b44d1abba38bb694880c6a", + _component: "@budibase/standard-components/container", + _styles: { + normal: { + display: "flex", + "flex-direction": "column", + "justify-content": "center", + "align-items": "stretch", + flex: "1 1 auto", + "border-width": "4px", + "border-style": "Dashed", + "margin-bottom": "32px", + }, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + className: "", + onLoad: [], + type: "div", + _appId: "inst_app_2cc_ca3383f896034e9295345c05f7dfca0c", + _instanceName: "Video Container", + _children: [ + { + _id: "c07d752cb3e544b418088fa9be84ba2e4", + _component: "@budibase/standard-components/embed", + _styles: { + normal: { + width: "100%", + flex: "1 1 auto", + opacity: "0", + "transition-property": "Opacity", + "transition-duration": "1s", + "transition-timing-function:": "ease-in", + }, + hover: { + "transition-property": "Opacity", + "transition-duration": "1s", + "transition-timing-function:": "ease-out", + opacity: "1", + }, + active: {}, + selected: {}, + }, + _code: "", + embed: + '', + _appId: "inst_app_2cc_ca3383f896034e9295345c05f7dfca0c", + _instanceName: "Rick Astley Video", + _children: [], + }, + ], + }, + ], + _instanceName: "Home", + }, + route: "/", + name: "d834fea2-1b3e-4320-ab34-f9009f5ecc59", +} diff --git a/packages/server/src/db/client.js b/packages/server/src/db/client.js index a3051eea7f..1d025a1402 100644 --- a/packages/server/src/db/client.js +++ b/packages/server/src/db/client.js @@ -26,4 +26,18 @@ const Pouch = PouchDB.defaults(POUCH_DB_DEFAULTS) allDbs(Pouch) +// replicate your local levelDB pouch to a running HTTP compliant couch or pouchdb server. +// eslint-disable-next-line no-unused-vars +function replicateLocal() { + Pouch.allDbs().then(dbs => { + for (let db of dbs) { + new Pouch(db).sync( + new PouchDB(`http://127.0.0.1:5984/${db}`, { live: true }) + ) + } + }) +} + +replicateLocal() + module.exports = Pouch diff --git a/packages/server/src/db/utils.js b/packages/server/src/db/utils.js index 257f367478..a213dc9066 100644 --- a/packages/server/src/db/utils.js +++ b/packages/server/src/db/utils.js @@ -13,6 +13,8 @@ const DocumentTypes = { ACCESS_LEVEL: "ac", WEBHOOK: "wh", INSTANCE: "inst", + PAGE: "page", + SCREEN: "screen", } exports.DocumentTypes = DocumentTypes @@ -175,6 +177,36 @@ exports.generateWebhookID = () => { return `${DocumentTypes.WEBHOOK}${SEPARATOR}${newid()}` } +/** + * Generates a new page ID. + * @returns {string} The new page ID which the page doc can be stored under. + */ +exports.generatePageID = () => { + return `${DocumentTypes.PAGE}${SEPARATOR}${newid()}` +} + +/** + * Gets parameters for retrieving pages, this is a utility function for the getDocParams function. + */ +exports.getPageParams = (pageId = null, otherProps = {}) => { + return getDocParams(DocumentTypes.PAGE, pageId, otherProps) +} + +/** + * Generates a new screen ID. + * @returns {string} The new screen ID which the screen doc can be stored under. + */ +exports.generateScreenID = pageId => { + return `${DocumentTypes.SCREEN}${SEPARATOR}${pageId}${SEPARATOR}${newid()}` +} + +/** + * Gets parameters for retrieving screens for a particular page, this is a utility function for the getDocParams function. + */ +exports.getScreenParams = (pageId = null, otherProps = {}) => { + return getDocParams(DocumentTypes.SCREEN, pageId, otherProps) +} + /** * Gets parameters for retrieving a webhook, this is a utility function for the getDocParams function. */ diff --git a/packages/server/src/environment.js b/packages/server/src/environment.js index 28fa74ddcd..003390c502 100644 --- a/packages/server/src/environment.js +++ b/packages/server/src/environment.js @@ -34,6 +34,7 @@ module.exports = { USERID_API_KEY: process.env.USERID_API_KEY, ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS, DEPLOYMENT_DB_URL: process.env.DEPLOYMENT_DB_URL, + LOCAL_TEMPLATES: process.env.LOCAL_TEMPLATES, _set(key, value) { process.env[key] = value module.exports[key] = value diff --git a/packages/server/src/utilities/appDirectoryTemplate/pages/main/page.json b/packages/server/src/utilities/appDirectoryTemplate/pages/main/page.json deleted file mode 100644 index ecd63a6284..0000000000 --- a/packages/server/src/utilities/appDirectoryTemplate/pages/main/page.json +++ /dev/null @@ -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": "" -} diff --git a/packages/server/src/utilities/appDirectoryTemplate/pages/main/screens/.gitkeep b/packages/server/src/utilities/appDirectoryTemplate/pages/main/screens/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/server/src/utilities/appDirectoryTemplate/pages/main/screens/d834fea2-1b3e-4320-ab34-f9009f5ecc59.json b/packages/server/src/utilities/appDirectoryTemplate/pages/main/screens/d834fea2-1b3e-4320-ab34-f9009f5ecc59.json deleted file mode 100644 index 4104dcdb1e..0000000000 --- a/packages/server/src/utilities/appDirectoryTemplate/pages/main/screens/d834fea2-1b3e-4320-ab34-f9009f5ecc59.json +++ /dev/null @@ -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": "", - "_appId": "inst_app_2cc_ca3383f896034e9295345c05f7dfca0c", - "_instanceName": "Rick Astley Video", - "_children": [] - } - ] - } - ], - "_instanceName": "Home" - }, - "route": "/", - "name": "d834fea2-1b3e-4320-ab34-f9009f5ecc59" -} diff --git a/packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/page.json b/packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/page.json deleted file mode 100644 index d3e6e55ac9..0000000000 --- a/packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/page.json +++ /dev/null @@ -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": "" -} diff --git a/packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/screens/.gitkeep b/packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/screens/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/screens/placeholder b/packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/screens/placeholder deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/server/src/utilities/appDirectoryTemplate/plugins.js b/packages/server/src/utilities/appDirectoryTemplate/plugins.js deleted file mode 100644 index 44368bf6ec..0000000000 --- a/packages/server/src/utilities/appDirectoryTemplate/plugins.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = () => ({}) diff --git a/packages/server/src/utilities/builder/buildPage.js b/packages/server/src/utilities/builder/compileStaticAssetsForPage.js similarity index 61% rename from packages/server/src/utilities/builder/buildPage.js rename to packages/server/src/utilities/builder/compileStaticAssetsForPage.js index 7db1299fa2..598f7745fb 100644 --- a/packages/server/src/utilities/builder/buildPage.js +++ b/packages/server/src/utilities/builder/compileStaticAssetsForPage.js @@ -1,35 +1,24 @@ -const { appPackageFolder } = require("../createAppPackage") -const { - constants, - copyFile, - writeFile, - readFile, - writeJSON, -} = require("fs-extra") +const { constants, copyFile, writeFile, readFile } = require("fs-extra") const { join, resolve } = require("../centralPath") const sqrl = require("squirrelly") const { convertCssToFiles } = require("./convertCssToFiles") const publicPath = require("./publicPath") -const deleteCodeMeta = require("./deleteCodeMeta") +const { budibaseAppsDir } = require("../budibaseDir") -module.exports = async (config, appId, pageName, pkg) => { - const appPath = appPackageFolder(config, appId) +module.exports = async (appId, pageName, pkg) => { + const appPath = join(budibaseAppsDir(), appId) pkg.screens = pkg.screens || [] await convertCssToFiles(publicPath(appPath, pageName), pkg) - await buildIndexHtml(config, appId, pageName, appPath, pkg) + await buildIndexHtml(appId, pageName, appPath, pkg) - await buildFrontendAppDefinition(config, appId, pageName, pkg, appPath) + await buildFrontendAppDefinition(appId, pageName, pkg, appPath) await copyClientLib(appPath, pageName) - - await savePageJson(appPath, pageName, pkg) } -const rootPath = (config, appId) => (config.useAppRootPath ? `/${appId}` : "") - const copyClientLib = async (appPath, pageName) => { const sourcepath = require.resolve("@budibase/client") const destPath = join(publicPath(appPath, pageName), "budibase-client.js") @@ -43,11 +32,10 @@ const copyClientLib = async (appPath, pageName) => { ) } -const buildIndexHtml = async (config, appId, pageName, appPath, pkg) => { +const buildIndexHtml = async (appId, pageName, appPath, pkg) => { const appPublicPath = publicPath(appPath, pageName) - const stylesheetUrl = s => - s.startsWith("http") ? s : `/${rootPath(config, appId)}/${s}` + const stylesheetUrl = s => (s.startsWith("http") ? s : `/${appId}/${s}`) const templateObj = { title: pkg.page.title || "Budibase App", @@ -77,15 +65,13 @@ const buildIndexHtml = async (config, appId, pageName, appPath, pkg) => { await writeFile(deployableHtmlPath, deployableHtml, { flag: "w+" }) } -const buildFrontendAppDefinition = async (config, appId, pageName, pkg) => { - const appPath = appPackageFolder(config, appId) +const buildFrontendAppDefinition = async (appId, pageName, pkg) => { + const appPath = join(budibaseAppsDir(), appId) const appPublicPath = publicPath(appPath, pageName) const filename = join(appPublicPath, "clientFrontendDefinition.js") - if (pkg.page._css) { - delete pkg.page._css - } + delete pkg.page._css for (let screen of pkg.screens) { if (screen._css) { @@ -106,25 +92,3 @@ const buildFrontendAppDefinition = async (config, appId, pageName, pkg) => { ` ) } - -const savePageJson = async (appPath, pageName, pkg) => { - const pageFile = join(appPath, "pages", pageName, "page.json") - - if (pkg.page._css) { - delete pkg.page._css - } - - if (pkg.page.name) { - delete pkg.page.name - } - - if (pkg.page._screens) { - delete pkg.page._screens - } - - deleteCodeMeta(pkg.page.props) - - await writeJSON(pageFile, pkg.page, { - spaces: 2, - }) -} diff --git a/packages/server/src/utilities/builder/deleteCodeMeta.js b/packages/server/src/utilities/builder/deleteCodeMeta.js deleted file mode 100644 index 07a0bc0349..0000000000 --- a/packages/server/src/utilities/builder/deleteCodeMeta.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = props => { - if (props._codeMeta) { - delete props._codeMeta - } - - for (let child of props._children || []) { - module.exports(child) - } -} diff --git a/packages/server/src/utilities/builder/getPages.js b/packages/server/src/utilities/builder/getPages.js deleted file mode 100644 index 2485bed419..0000000000 --- a/packages/server/src/utilities/builder/getPages.js +++ /dev/null @@ -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 -} diff --git a/packages/server/src/utilities/builder/index.js b/packages/server/src/utilities/builder/index.js deleted file mode 100644 index dbc2ce6bf5..0000000000 --- a/packages/server/src/utilities/builder/index.js +++ /dev/null @@ -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) -} diff --git a/packages/server/src/utilities/builder/listScreens.js b/packages/server/src/utilities/builder/listScreens.js deleted file mode 100644 index 8964ac6cec..0000000000 --- a/packages/server/src/utilities/builder/listScreens.js +++ /dev/null @@ -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 -} diff --git a/packages/server/src/utilities/builder/setBuilderToken.js b/packages/server/src/utilities/builder/setBuilderToken.js index 2986a805a9..8cf6c44379 100644 --- a/packages/server/src/utilities/builder/setBuilderToken.js +++ b/packages/server/src/utilities/builder/setBuilderToken.js @@ -21,6 +21,7 @@ module.exports = async (ctx, appId, version) => { // set the builder token setCookie(ctx, "builder", token) + setCookie(ctx, "currentapp", appId) // need to clear all app tokens or else unable to use the app in the builder let allDbNames = await CouchDB.allDbs() allDbNames.map(dbName => { diff --git a/packages/server/src/utilities/createAppPackage.js b/packages/server/src/utilities/createAppPackage.js index d2a1840cb2..a62e8c96df 100644 --- a/packages/server/src/utilities/createAppPackage.js +++ b/packages/server/src/utilities/createAppPackage.js @@ -1,5 +1,3 @@ -const { resolve } = require("./centralPath") -const { cwd } = require("process") const stream = require("stream") const fetch = require("node-fetch") const tar = require("tar-fs") @@ -9,9 +7,6 @@ const packageJson = require("../../package.json") const streamPipeline = promisify(stream.pipeline) -exports.appPackageFolder = (config, appname) => - resolve(cwd(), config.latestPackagesFolder, appname) - exports.downloadExtractComponentLibraries = async appFolder => { const LIBRARIES = ["standard-components"] diff --git a/packages/server/src/utilities/templates.js b/packages/server/src/utilities/templates.js index 11150261ec..d5b5d5e257 100644 --- a/packages/server/src/utilities/templates.js +++ b/packages/server/src/utilities/templates.js @@ -8,12 +8,35 @@ const zlib = require("zlib") const { promisify } = require("util") const streamPipeline = promisify(stream.pipeline) const { budibaseAppsDir } = require("./budibaseDir") +const env = require("../environment") const CouchDB = require("../db") +const { DocumentTypes } = require("../db/utils") const DEFAULT_TEMPLATES_BUCKET = "prod-budi-templates.s3-eu-west-1.amazonaws.com" +exports.getLocalTemplates = function() { + const templatesDir = join(os.homedir(), ".budibase", "templates", "app") + const templateObj = { app: {} } + fs.ensureDirSync(templatesDir) + const templateNames = fs.readdirSync(templatesDir) + for (let name of templateNames) { + templateObj.app[name] = { + name, + category: "local", + description: "local template", + type: "app", + key: `app/${name}`, + } + } + return templateObj +} + exports.downloadTemplate = async function(type, name) { + const dirName = join(budibaseAppsDir(), "templates", type, name) + if (env.LOCAL_TEMPLATES) { + return dirName + } const templateUrl = `https://${DEFAULT_TEMPLATES_BUCKET}/templates/${type}/${name}.tar.gz` const response = await fetch(templateUrl) @@ -30,26 +53,27 @@ exports.downloadTemplate = async function(type, name) { tar.extract(join(budibaseAppsDir(), "templates", type)) ) - return join(budibaseAppsDir(), "templates", type, name) + return dirName } exports.exportTemplateFromApp = async function({ templateName, appId }) { // Copy frontend files - const appToExport = join(os.homedir(), ".budibase", appId, "pages") - const templatesDir = join(os.homedir(), ".budibase", "templates") - fs.ensureDirSync(templatesDir) - - const templateOutputPath = join(templatesDir, templateName) - fs.copySync(appToExport, join(templateOutputPath, "pages")) - - fs.ensureDirSync(join(templateOutputPath, "db")) - const writeStream = fs.createWriteStream( - join(templateOutputPath, "db", "dump.txt") + const templatesDir = join( + os.homedir(), + ".budibase", + "templates", + "app", + templateName, + "db" ) - + fs.ensureDirSync(templatesDir) + const writeStream = fs.createWriteStream(join(templatesDir, "dump.txt")) // perform couch dump const instanceDb = new CouchDB(appId) - - await instanceDb.dump(writeStream) - return templateOutputPath + await instanceDb.dump(writeStream, { + filter: doc => { + return !doc._id.startsWith(DocumentTypes.USER) + }, + }) + return templatesDir }