diff --git a/packages/builder/cypress/integration/createUser.spec.js b/packages/builder/cypress/integration/createUser.spec.js index cbde6179b2..a5f9934dd7 100644 --- a/packages/builder/cypress/integration/createUser.spec.js +++ b/packages/builder/cypress/integration/createUser.spec.js @@ -9,7 +9,7 @@ context('Create a User', () => { // https://on.cypress.io/interacting-with-elements it('should create a user', () => { - cy.createUser("bbuser", "test", "ADMIN") + cy.createUser("bbuser@test.com", "test", "ADMIN") // // Check to make sure user was created! cy.contains("bbuser").should('be.visible') diff --git a/packages/builder/cypress/support/commands.js b/packages/builder/cypress/support/commands.js index ecb4eac6ce..94faa124a8 100644 --- a/packages/builder/cypress/support/commands.js +++ b/packages/builder/cypress/support/commands.js @@ -44,9 +44,9 @@ Cypress.Commands.add("createApp", name => { cy.contains("Next").click() - cy.get("input[name=username]") + cy.get("input[name=email]") .click() - .type("test") + .type("test@test.com") cy.get("input[name=password]") .click() .type("test") @@ -111,7 +111,7 @@ Cypress.Commands.add("addRow", values => { }) }) -Cypress.Commands.add("createUser", (username, password, role) => { +Cypress.Commands.add("createUser", (email, password, role) => { // Create User cy.contains("Users").click() @@ -123,7 +123,7 @@ Cypress.Commands.add("createUser", (username, password, role) => { .type(password) cy.get("input") .eq(1) - .type(username) + .type(email) cy.get("select") .first() .select(role) diff --git a/packages/builder/package.json b/packages/builder/package.json index b4ffcf7b66..542c35205e 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -63,7 +63,7 @@ } }, "dependencies": { - "@budibase/bbui": "^1.50.2", + "@budibase/bbui": "^1.52.2", "@budibase/client": "^0.3.8", "@budibase/colorpicker": "^1.0.1", "@budibase/svelte-ag-grid": "^0.0.16", @@ -107,6 +107,7 @@ "rollup-plugin-alias": "^1.5.2", "rollup-plugin-copy": "^3.0.0", "rollup-plugin-css-only": "^2.1.0", + "rollup-plugin-html": "^0.2.1", "rollup-plugin-livereload": "^1.0.0", "rollup-plugin-node-builtins": "^2.1.2", "rollup-plugin-node-globals": "^1.4.0", diff --git a/packages/builder/rollup.config.js b/packages/builder/rollup.config.js index 4afb8084bd..2d5ec52f52 100644 --- a/packages/builder/rollup.config.js +++ b/packages/builder/rollup.config.js @@ -11,6 +11,7 @@ import copy from "rollup-plugin-copy" import css from "rollup-plugin-css-only" import replace from "rollup-plugin-replace" import json from "@rollup/plugin-json" +import html from "rollup-plugin-html" import path from "path" @@ -75,10 +76,6 @@ export default { { src: "src/index.html", dest: outputpath }, { src: "src/favicon.png", dest: outputpath }, { src: "assets", dest: outputpath }, - { - src: "node_modules/@budibase/client/dist/budibase-client.esm.mjs", - dest: outputpath, - }, { src: "node_modules/@budibase/bbui/dist/bbui.css", dest: outputpath, @@ -147,5 +144,6 @@ export default { // instead of npm run dev), minify production && terser(), json(), + html(), ], } diff --git a/packages/builder/src/builderStore/getNewComponentName.js b/packages/builder/src/builderStore/getNewComponentName.js index a69bec21ad..98ca05b827 100644 --- a/packages/builder/src/builderStore/getNewComponentName.js +++ b/packages/builder/src/builderStore/getNewComponentName.js @@ -2,6 +2,8 @@ import { walkProps } from "./storeUtils" import { get_capitalised_name } from "../helpers" import { get } from "svelte/store" import { allScreens } from "builderStore" +import { FrontendTypes } from "../constants" +import { currentAsset } from "." export default function(component, state) { const capitalised = get_capitalised_name( @@ -19,14 +21,16 @@ export default function(component, state) { }) } - // check page first - findMatches(state.pages[state.currentPageName].props) + // check layouts first + for (let layout of state.layouts) { + findMatches(layout.props) + } // if viewing screen, check current screen for duplicate - if (state.currentFrontEndType === "screen") { - findMatches(state.currentPreviewItem.props) + if (state.currentFrontEndType === FrontendTypes.SCREEN) { + findMatches(get(currentAsset).props) } else { - // viewing master page - need to find against all screens + // viewing a layout - need to find against all screens for (let screen of get(allScreens)) { findMatches(screen.props) } diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js index ae77889404..503d9b08a7 100644 --- a/packages/builder/src/builderStore/index.js +++ b/packages/builder/src/builderStore/index.js @@ -4,37 +4,74 @@ import { getAutomationStore } from "./store/automation/" import { getThemeStore } from "./store/theme" import { derived } from "svelte/store" import analytics from "analytics" +import { LAYOUT_NAMES } from "../constants" +import { makePropsSafe } from "components/userInterface/assetParsing/createProps" export const store = getFrontendStore() export const backendUiStore = getBackendUiStore() export const automationStore = getAutomationStore() export const themeStore = getThemeStore() +export const currentAsset = derived(store, $store => { + const layout = $store.layouts + ? $store.layouts.find(layout => layout._id === $store.currentAssetId) + : null + + if (layout) return layout + + const screen = $store.screens + ? $store.screens.find(screen => screen._id === $store.currentAssetId) + : null + + if (screen) return screen + + return null +}) + +export const selectedComponent = derived( + [store, currentAsset], + ([$store, $currentAsset]) => { + if (!$currentAsset || !$store.selectedComponentId) return null + + function traverse(node, callback) { + if (node._id === $store.selectedComponentId) return callback(node) + + if (node._children) { + node._children.forEach(child => traverse(child, callback)) + } + + if (node.props) { + traverse(node.props, callback) + } + } + + let component + traverse($currentAsset, found => { + const componentIdentifier = found._component ?? found.props._component + const componentDef = componentIdentifier.startsWith("##") + ? found + : $store.components[componentIdentifier] + + component = makePropsSafe(componentDef, found) + }) + + return component + } +) + +export const currentAssetName = derived(store, () => { + return currentAsset.name +}) + +// leave this as before for consistency 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 + return $store.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 selectedPage = derived(store, $store => { - if (!$store.pages) return null - - return $store.pages[$store.currentPageName || "main"] +export const mainLayout = derived(store, $store => { + return $store.layouts?.find( + layout => layout.props?._id === LAYOUT_NAMES.MASTER.PRIVATE + ) }) export const initialise = async () => { diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 50c4347583..7be4f68f25 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -4,34 +4,36 @@ import { createProps, getBuiltin, makePropsSafe, -} from "components/userInterface/pagesParsing/createProps" -import { allScreens, backendUiStore, selectedPage } from "builderStore" -import { generate_screen_css } from "../generate_css" +} from "components/userInterface/assetParsing/createProps" +import { + allScreens, + backendUiStore, + currentAsset, + mainLayout, + selectedComponent, +} from "builderStore" import { fetchComponentLibDefinitions } from "../loadComponentLibraries" import api from "../api" -import { DEFAULT_PAGES_OBJECT } from "../../constants" +import { FrontendTypes } from "../../constants" import getNewComponentName from "../getNewComponentName" import analytics from "analytics" import { findChildComponentType, generateNewIdsForComponent, getComponentDefinition, - getParent, + findParent, } from "../storeUtils" const INITIAL_FRONTEND_STATE = { apps: [], name: "", description: "", - pages: DEFAULT_PAGES_OBJECT, - mainUi: {}, - unauthenticatedUi: {}, + layouts: [], + screens: [], components: [], - currentPreviewItem: null, - currentComponentInfo: null, currentFrontEndType: "none", - currentPageName: "", - currentComponentProps: null, + currentAssetId: "", + selectedComponentId: "", errors: [], hasAppPackage: false, libraries: null, @@ -43,52 +45,13 @@ export const getFrontendStore = () => { const store = writable({ ...INITIAL_FRONTEND_STATE }) store.actions = { - // TODO: REFACTOR initialise: async pkg => { + const { layouts, screens, application } = pkg + store.update(state => { - state.appId = pkg.application._id + state.appId = 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) @@ -99,7 +62,8 @@ export const getFrontendStore = () => { name: pkg.application.name, description: pkg.application.description, appId: pkg.application._id, - pages: pkg.pages, + layouts, + screens, hasAppPackage: true, builtins: [getBuiltin("##builtin/screenslot")], appInstance: pkg.application.instance, @@ -107,20 +71,6 @@ export const getFrontendStore = () => { await backendUiStore.actions.database.select(pkg.application.instance) }, - selectPageOrScreen: type => { - store.update(state => { - state.currentFrontEndType = type - - const page = get(selectedPage) - - const pageOrScreen = type === "page" ? page : page._screens[0] - - state.currentComponentInfo = pageOrScreen ? pageOrScreen.props : null - state.currentPreviewItem = pageOrScreen - state.currentView = "detail" - return state - }) - }, routing: { fetch: async () => { const response = await api.get("/api/routing") @@ -133,167 +83,166 @@ export const getFrontendStore = () => { }, }, screens: { - select: screenId => { + select: async screenId => { + let promise store.update(state => { const screen = get(allScreens).find(screen => screen._id === screenId) - state.currentPreviewItem = screen - state.currentFrontEndType = "screen" + state.currentFrontEndType = FrontendTypes.SCREEN + state.currentAssetId = screenId state.currentView = "detail" - store.actions.screens.regenerateCssForCurrentScreen() - const safeProps = makePropsSafe( - state.components[screen.props._component], - screen.props - ) - screen.props = safeProps - state.currentComponentInfo = safeProps + promise = store.actions.screens.regenerateCss(screen) + state.selectedComponentId = screen.props._id return state }) + await promise }, create: async screen => { - let savePromise + screen = await store.actions.screens.save(screen) 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) + state.currentAssetId = screen._id + state.selectedComponentId = screen.props._id + state.currentFrontEndType = FrontendTypes.SCREEN return state }) - - await savePromise + return screen }, save: async screen => { - const page = get(selectedPage) - const currentPageScreens = page._screens - const creatingNewScreen = screen._id === undefined + const response = await api.post(`/api/screens`, screen) + screen = await response.json() - let savePromise - const response = await api.post(`/api/screens/${page._id}`, screen) - const json = await response.json() - screen._rev = json.rev - screen._id = json.id - const foundScreen = page._screens.findIndex(el => el._id === screen._id) - if (foundScreen !== -1) { - page._screens.splice(foundScreen, 1) - } - page._screens.push(screen) - - // TODO: should carry out all server updates to screen in a single call store.update(state => { - page._screens = currentPageScreens + const foundScreen = state.screens.findIndex( + el => el._id === screen._id + ) + if (foundScreen !== -1) { + state.screens.splice(foundScreen, 1) + } + state.screens.push(screen) if (creatingNewScreen) { - state.currentPreviewItem = screen const safeProps = makePropsSafe( state.components[screen.props._component], screen.props ) - state.currentComponentInfo = safeProps + state.selectedComponentId = safeProps._id screen.props = safeProps } - savePromise = store.actions.pages.save() - return state }) - if (savePromise) await savePromise + return screen }, - regenerateCss: screen => { - screen._css = generate_screen_css([screen.props]) + regenerateCss: async asset => { + const response = await api.post("/api/css/generate", asset) + asset._css = (await response.json())?.css }, - regenerateCssForCurrentScreen: () => { - const { currentPreviewItem } = get(store) - if (currentPreviewItem) { - store.actions.screens.regenerateCss(currentPreviewItem) + regenerateCssForCurrentScreen: async () => { + const asset = get(currentAsset) + if (asset) { + await store.actions.screens.regenerateCss(asset) } }, delete: async screens => { - let deletePromise - const screensToDelete = Array.isArray(screens) ? screens : [screens] + const screenDeletePromises = [] store.update(state => { - const currentPage = get(selectedPage) - for (let screenToDelete of screensToDelete) { - // Remove screen from current page as well - // TODO: Should be done server side - currentPage._screens = currentPage._screens.filter( - scr => scr._id !== screenToDelete._id + state.screens = state.screens.filter( + screen => screen._id !== screenToDelete._id ) - - deletePromise = api.delete( - `/api/screens/${screenToDelete._id}/${screenToDelete._rev}` + screenDeletePromises.push( + api.delete( + `/api/screens/${screenToDelete._id}/${screenToDelete._rev}` + ) ) } return state }) - await deletePromise + await Promise.all(screenDeletePromises) }, }, preview: { saveSelected: async () => { const state = get(store) - if (state.currentFrontEndType !== "page") { - await store.actions.screens.save(state.currentPreviewItem) + const selectedAsset = get(currentAsset) + + if (state.currentFrontEndType !== FrontendTypes.LAYOUT) { + await store.actions.screens.save(selectedAsset) + } else { + await store.actions.layouts.save(selectedAsset) } - await store.actions.pages.save() }, }, - pages: { - select: pageName => { + layouts: { + select: async layoutId => { store.update(state => { - const currentPage = state.pages[pageName] + const layout = store.actions.layouts.find(layoutId) - state.currentFrontEndType = "page" + state.currentFrontEndType = FrontendTypes.LAYOUT 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]) - } + state.currentAssetId = layout._id + state.selectedComponentId = layout.props._id return state }) - }, - save: async page => { - const storeContents = get(store) - const pageName = storeContents.currentPageName || "main" - const pageToSave = page || storeContents.pages[pageName] + let cssPromises = [] + cssPromises.push(store.actions.screens.regenerateCssForCurrentScreen()) - // 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, - }) + for (let screen of get(allScreens)) { + cssPromises.push(store.actions.screens.regenerateCss(screen)) + } + await Promise.all(cssPromises) + }, + save: async layout => { + const layoutToSave = cloneDeep(layout) + delete layoutToSave._css + + const response = await api.post(`/api/layouts`, layoutToSave) const json = await response.json() - if (!json.ok) throw new Error("Error updating page") + store.update(state => { + const layoutIdx = state.layouts.findIndex( + stateLayout => stateLayout._id === json._id + ) + + if (layoutIdx >= 0) { + // update existing layout + state.layouts.splice(layoutIdx, 1, json) + } else { + // save new layout + state.layouts.push(json) + } + + state.currentAssetId = json._id + state.selectedComponentId = json.props._id + return state + }) + }, + find: layoutId => { + if (!layoutId) { + return get(mainLayout) + } + const storeContents = get(store) + return storeContents.layouts.find(layout => layout._id === layoutId) + }, + delete: async layoutToDelete => { + const response = await api.delete( + `/api/layouts/${layoutToDelete._id}/${layoutToDelete._rev}` + ) + + if (response.status !== 200) { + const json = await response.json() + throw new Error(json.message) + } store.update(state => { - state.pages[pageName]._rev = json.rev + state.layouts = state.layouts.filter( + layout => layout._id !== layoutToDelete._id + ) return state }) }, @@ -301,17 +250,19 @@ export const getFrontendStore = () => { components: { select: component => { store.update(state => { - const componentDef = component._component.startsWith("##") - ? component - : state.components[component._component] - state.currentComponentInfo = makePropsSafe(componentDef, component) + state.selectedComponentId = component._id state.currentView = "component" return state }) }, create: (componentToAdd, presetProps) => { + const selectedAsset = get(currentAsset) + store.update(state => { function findSlot(component_array) { + if (!component_array) { + return false + } for (let component of component_array) { if (component._component === "##builtin/screenslot") { return true @@ -324,7 +275,7 @@ export const getFrontendStore = () => { if ( componentToAdd.startsWith("##") && - findSlot(state.pages[state.currentPageName].props._children) + findSlot(selectedAsset?.props._children) ) { return state } @@ -340,29 +291,34 @@ export const getFrontendStore = () => { _instanceName: instanceName, }) - const currentComponent = - state.components[state.currentComponentInfo._component] + const selected = get(selectedComponent) - const targetParent = currentComponent.children - ? state.currentComponentInfo - : getParent( - state.currentPreviewItem.props, - state.currentComponentInfo - ) + const currentComponentDefinition = + state.components[selected._component] - // Don't continue if there's no parent - if (!targetParent) { - return state + const allowsChildren = currentComponentDefinition.children + + // Determine where to put the new component. + let targetParent + if (allowsChildren) { + // Child of the selected component + targetParent = selected + } else { + // Sibling of selected component + targetParent = findParent(selectedAsset.props, selected) } - targetParent._children = targetParent._children.concat( - newComponent.props - ) + // Don't continue if there's no parent + if (!targetParent) return state + + // Push the new component + targetParent._children.push(newComponent.props) store.actions.preview.saveSelected() state.currentView = "component" - state.currentComponentInfo = newComponent.props + state.selectedComponentId = newComponent.props._id + analytics.captureEvent("Added Component", { name: newComponent.props._component, }) @@ -370,14 +326,12 @@ export const getFrontendStore = () => { }) }, copy: (component, cut = false) => { + const selectedAsset = get(currentAsset) store.update(state => { state.componentToPaste = cloneDeep(component) state.componentToPaste.isCut = cut if (cut) { - const parent = getParent( - state.currentPreviewItem.props, - component._id - ) + const parent = findParent(selectedAsset.props, component._id) parent._children = parent._children.filter( child => child._id !== component._id ) @@ -387,7 +341,9 @@ export const getFrontendStore = () => { return state }) }, - paste: (targetComponent, mode) => { + paste: async (targetComponent, mode) => { + const selectedAsset = get(currentAsset) + let promises = [] store.update(state => { if (!state.componentToPaste) return state @@ -406,54 +362,56 @@ export const getFrontendStore = () => { return state } - const parent = getParent( - state.currentPreviewItem.props, - targetComponent - ) + const parent = findParent(selectedAsset.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() + promises.push(store.actions.screens.regenerateCssForCurrentScreen()) + promises.push(store.actions.preview.saveSelected()) store.actions.components.select(componentToPaste) return state }) + await Promise.all(promises) }, - updateStyle: (type, name, value) => { - store.update(state => { - if (!state.currentComponentInfo._styles) { - state.currentComponentInfo._styles = {} - } - state.currentComponentInfo._styles[type][name] = value + updateStyle: async (type, name, value) => { + let promises = [] + const selected = get(selectedComponent) - store.actions.screens.regenerateCssForCurrentScreen() + store.update(state => { + if (!selected._styles) { + selected._styles = {} + } + selected._styles[type][name] = value + + promises.push(store.actions.screens.regenerateCssForCurrentScreen()) // save without messing with the store - store.actions.preview.saveSelected() + promises.push(store.actions.preview.saveSelected()) return state }) + await Promise.all(promises) }, updateProp: (name, value) => { store.update(state => { - let current_component = state.currentComponentInfo + let current_component = get(selectedComponent) current_component[name] = value - state.currentComponentInfo = current_component + state.selectedComponentId = current_component._id store.actions.preview.saveSelected() return state }) }, findRoute: component => { // Gets all the components to needed to construct a path. - const tempStore = get(store) + const selectedAsset = get(currentAsset) let pathComponents = [] let parent = component let root = false while (!root) { - parent = getParent(tempStore.currentPreviewItem.props, parent) + parent = findParent(selectedAsset.props, parent) if (!parent) { root = true } else { @@ -461,7 +419,7 @@ export const getFrontendStore = () => { } } - // Remove root entry since it's the screen or page layout. + // Remove root entry since it's the screen or layout. // Reverse array since we need the correct order of the IDs const reversedComponents = pathComponents.reverse().slice(1) @@ -476,11 +434,12 @@ export const getFrontendStore = () => { }, links: { save: async (url, title) => { - let savePromise + let promises = [] + const layout = get(mainLayout) store.update(state => { - // Try to extract a nav component from the master screen + // Try to extract a nav component from the master layout const nav = findChildComponentType( - state.pages.main, + layout, "@budibase/standard-components/navigation" ) if (nav) { @@ -513,18 +472,18 @@ export const getFrontendStore = () => { }).props } - // Save page and regenerate all CSS because otherwise weird things happen + // Save layout 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) + state.currentAssetId = layout._id + promises.push(store.actions.screens.regenerateCss(layout)) + for (let screen of get(allScreens)) { + promises.push(store.actions.screens.regenerateCss(screen)) } - savePromise = store.actions.pages.save() + promises.push(store.actions.layouts.save(layout)) } return state }) - await savePromise + await Promise.all(promises) }, }, }, diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js b/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js index 84de7e15ea..bd03fc7cdc 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js @@ -14,7 +14,6 @@ export class Component extends BaseStructure { active: {}, selected: {}, }, - _code: "", type: "", _instanceName: "", _children: [], diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/Screen.js b/packages/builder/src/builderStore/store/screenTemplates/utils/Screen.js index 76df96ae0c..00bd43ec2c 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/utils/Screen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/Screen.js @@ -4,6 +4,7 @@ export class Screen extends BaseStructure { constructor() { super(true) this._json = { + layoutId: "layout_private_master", props: { _id: "", _component: "", @@ -18,7 +19,7 @@ export class Screen extends BaseStructure { }, routing: { route: "", - roleId: "", + roleId: "BASIC", }, name: "screen-id", } diff --git a/packages/builder/src/builderStore/storeUtils.js b/packages/builder/src/builderStore/storeUtils.js index 9c9d1ef940..4ee2dd7ccc 100644 --- a/packages/builder/src/builderStore/storeUtils.js +++ b/packages/builder/src/builderStore/storeUtils.js @@ -1,15 +1,21 @@ -import { getBuiltin } from "components/userInterface/pagesParsing/createProps" +import { getBuiltin } from "components/userInterface/assetParsing/createProps" import { uuid } from "./uuid" import getNewComponentName from "./getNewComponentName" -export const getParent = (rootProps, child) => { +/** + * Find the parent component of the passed in child. + * @param {Object} rootProps - props to search for the parent in + * @param {String|Object} child - id of the child or the child itself to find the parent of + */ +export const findParent = (rootProps, child) => { let parent - walkProps(rootProps, (p, breakWalk) => { + walkProps(rootProps, (props, breakWalk) => { if ( - p._children && - (p._children.includes(child) || p._children.some(c => c._id === child)) + props._children && + (props._children.includes(child) || + props._children.some(c => c._id === child)) ) { - parent = p + parent = props breakWalk() } }) diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index 8bb4dc36dd..7e26b2155e 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -62,6 +62,8 @@ {:else if value.customType === 'password'} + {:else if value.customType === 'email'} + {:else if value.customType === 'table'} {:else if value.customType === 'row'} diff --git a/packages/builder/src/components/backend/DataTable/DataTable.svelte b/packages/builder/src/components/backend/DataTable/DataTable.svelte index 063ca34633..5ff7925c5f 100644 --- a/packages/builder/src/components/backend/DataTable/DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/DataTable.svelte @@ -4,12 +4,17 @@ import CreateColumnButton from "./buttons/CreateColumnButton.svelte" import CreateViewButton from "./buttons/CreateViewButton.svelte" import ExportButton from "./buttons/ExportButton.svelte" + import EditRolesButton from "./buttons/EditRolesButton.svelte" import * as api from "./api" import Table from "./Table.svelte" + import { TableNames } from "constants" + import CreateEditUser from "./modals/CreateEditUser.svelte" + import CreateEditRow from "./modals/CreateEditRow.svelte" let data = [] let loading = false + $: isUsersTable = $backendUiStore.selectedTable?._id === TableNames.USERS $: title = $backendUiStore.selectedTable.name $: schema = $backendUiStore.selectedTable.schema $: tableView = { @@ -29,11 +34,21 @@ } - +
{#if schema && Object.keys(schema).length > 0} - + {/if} + {#if isUsersTable} + + {/if}
diff --git a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte index c21d3449b6..bddb66e4c9 100644 --- a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte +++ b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte @@ -1,19 +1,22 @@ {#if type === 'options'} @@ -34,6 +37,11 @@ {:else if type === 'link'} +{:else if type === 'longform'} +
+ + +
{:else} + disabled={readonly} /> {/if} diff --git a/packages/builder/src/components/backend/DataTable/Table.svelte b/packages/builder/src/components/backend/DataTable/Table.svelte index 2599e4f8b0..718e56b31b 100644 --- a/packages/builder/src/components/backend/DataTable/Table.svelte +++ b/packages/builder/src/components/backend/DataTable/Table.svelte @@ -7,10 +7,15 @@ import { notifier } from "builderStore/store/notifications" import Spinner from "components/common/Spinner.svelte" import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte" - import { getRenderer, editRowRenderer } from "./cells/cellRenderers" + import { + getRenderer, + editRowRenderer, + userRowRenderer, + } from "./cells/cellRenderers" import TableLoadingOverlay from "./TableLoadingOverlay" import TableHeader from "./TableHeader" import "@budibase/svelte-ag-grid/dist/index.css" + import { TableNames } from "constants" export let schema = {} export let data = [] @@ -42,6 +47,14 @@ animateRows: true, } + $: isUsersTable = tableId === TableNames.USERS + $: { + if (isUsersTable) { + schema.email.displayFieldName = "Email" + schema.roleId.displayFieldName = "Role" + } + } + $: { let result = [] if (allowEditing) { @@ -57,12 +70,12 @@ suppressMenu: true, minWidth: 114, width: 114, - cellRenderer: editRowRenderer, + cellRenderer: isUsersTable ? userRowRenderer : editRowRenderer, }, ] } - Object.keys(schema || {}).forEach((key, idx) => { + Object.entries(schema || {}).forEach(([key, value]) => { result.push({ headerCheckboxSelection: false, headerComponent: TableHeader, @@ -70,7 +83,7 @@ field: schema[key], editable: allowEditing, }, - headerName: key, + headerName: value.displayFieldName || key, field: key, sortable: true, cellRenderer: getRenderer(schema[key], true), diff --git a/packages/builder/src/components/backend/DataTable/buttons/CreateRowButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/CreateRowButton.svelte index 3c0444881f..dfd8e6f4ac 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/CreateRowButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/CreateRowButton.svelte @@ -1,6 +1,8 @@ @@ -12,5 +14,5 @@ - + diff --git a/packages/builder/src/components/backend/DataTable/buttons/EditRolesButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/EditRolesButton.svelte new file mode 100644 index 0000000000..024905fddc --- /dev/null +++ b/packages/builder/src/components/backend/DataTable/buttons/EditRolesButton.svelte @@ -0,0 +1,23 @@ + + +
+ +
+ + + + + diff --git a/packages/builder/src/components/backend/DataTable/cells/cellRenderers.js b/packages/builder/src/components/backend/DataTable/cells/cellRenderers.js index 209f23119f..f3a7b86740 100644 --- a/packages/builder/src/components/backend/DataTable/cells/cellRenderers.js +++ b/packages/builder/src/components/backend/DataTable/cells/cellRenderers.js @@ -1,5 +1,6 @@ import AttachmentList from "./AttachmentCell.svelte" import EditRow from "../modals/EditRow.svelte" +import CreateEditUser from "../modals/CreateEditUser.svelte" import DeleteRow from "../modals/DeleteRow.svelte" import RelationshipDisplay from "./RelationshipCell.svelte" @@ -45,6 +46,23 @@ export function editRowRenderer(params) { return container } +export function userRowRenderer(params) { + const container = document.createElement("div") + container.style.height = "100%" + container.style.display = "flex" + container.style.alignItems = "center" + + new EditRow({ + target: container, + props: { + row: params.data, + modalContentComponent: CreateEditUser, + }, + }) + + return container +} + /* eslint-disable no-unused-vars */ function attachmentRenderer(options, constraints, editable) { return params => { diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditRowModal.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditRow.svelte similarity index 81% rename from packages/builder/src/components/backend/DataTable/modals/CreateEditRowModal.svelte rename to packages/builder/src/components/backend/DataTable/modals/CreateEditRow.svelte index e99c5762d8..050c7ce200 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditRowModal.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditRow.svelte @@ -1,6 +1,5 @@ + + + + + {#if creating} + + {/if} + + {#if rolesLoaded} + + {/if} + {#each customSchemaKeys as [key, meta]} + + {/each} + diff --git a/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte b/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte new file mode 100644 index 0000000000..627112d87c --- /dev/null +++ b/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte @@ -0,0 +1,136 @@ + + + + {#if errors.length} + + {/if} + + {#if selectedRole} + + + + {/if} +
+ {#if !isCreating} + + {/if} +
+
diff --git a/packages/builder/src/components/backend/DataTable/modals/EditRow.svelte b/packages/builder/src/components/backend/DataTable/modals/EditRow.svelte index a4d2a74fc2..5d858a50f4 100644 --- a/packages/builder/src/components/backend/DataTable/modals/EditRow.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/EditRow.svelte @@ -1,8 +1,9 @@ {#if hasErrors} -
+
{#each errors as error} -
{error.dataPath} {error.message}
+
{error.dataPath || ''} {error.message}
{/each}
{/if} @@ -17,6 +17,8 @@ border-radius: var(--border-radius-m); margin: 0; padding: var(--spacing-m); + background-color: rgba(241, 165, 165, 0.2); + color: var(--red); } .error { diff --git a/packages/builder/src/components/start/CreateAppModal.svelte b/packages/builder/src/components/start/CreateAppModal.svelte index 40a0e08a54..b88397b6c4 100644 --- a/packages/builder/src/components/start/CreateAppModal.svelte +++ b/packages/builder/src/components/start/CreateAppModal.svelte @@ -52,7 +52,9 @@ applicationName: string().required("Your application must have a name."), }, { - username: string().required("Your application needs a first user."), + email: string() + .email() + .required("Your application needs a first user."), password: string().required( "Please enter a password for your first user." ), @@ -151,17 +153,15 @@ const pkg = await applicationPkg.json() if (applicationPkg.ok) { backendUiStore.actions.reset() - pkg.justCreated = true await store.actions.initialise(pkg) - automationStore.actions.fetch() + await automationStore.actions.fetch() } else { throw new Error(pkg) } // Create user const user = { - name: $createAppStore.values.username, - username: $createAppStore.values.username, + email: $createAppStore.values.email, password: $createAppStore.values.password, roleId: $createAppStore.values.roleId, } diff --git a/packages/builder/src/components/start/Steps/User.svelte b/packages/builder/src/components/start/Steps/User.svelte index 0bf9ce19cb..397a06be94 100644 --- a/packages/builder/src/components/start/Steps/User.svelte +++ b/packages/builder/src/components/start/Steps/User.svelte @@ -2,18 +2,18 @@ import { Input, Select } from "@budibase/bbui" export let validationErrors - let blurred = { username: false, password: false } + let blurred = { email: false, password: false }

Create your first User

(blurred.username = true)} - label="Username" - name="username" - placeholder="Username" - type="name" - error={blurred.username && validationErrors.username} /> + on:input={() => (blurred.email = true)} + label="Email" + name="email" + placeholder="Email" + type="email" + error={blurred.email && validationErrors.email} /> (blurred.password = true)} label="Password" diff --git a/packages/builder/src/components/userInterface/AppPreview/CurrentItemPreview.svelte b/packages/builder/src/components/userInterface/AppPreview/CurrentItemPreview.svelte index 535b947f5f..ccb0153e45 100644 --- a/packages/builder/src/components/userInterface/AppPreview/CurrentItemPreview.svelte +++ b/packages/builder/src/components/userInterface/AppPreview/CurrentItemPreview.svelte @@ -1,10 +1,13 @@
- {#if $store.currentPreviewItem} - ', - _appId: "inst_app_2cc_ca3383f896034e9295345c05f7dfca0c", _instanceName: "Rick Astley Video", _children: [], }, @@ -99,5 +94,62 @@ exports.HOME_SCREEN = { route: "/", roleId: BUILTIN_ROLE_IDS.BASIC, }, - name: "d834fea2-1b3e-4320-ab34-f9009f5ecc59", -} + name: "home-screen", +}) + +exports.createLoginScreen = app => ({ + description: "", + url: "", + layoutId: BASE_LAYOUT_PROP_IDS.PUBLIC, + props: { + _instanceName: "LoginScreenContainer", + _id: "5beb4c7b-3c8b-49b2-b8b3-d447dc76dda7", + _component: "@budibase/standard-components/container", + _styles: { + normal: { + flex: "1 1 auto", + display: "flex", + "flex-direction": "column", + "justify-content": "center", + "align-items": "center", + }, + hover: {}, + active: {}, + selected: {}, + }, + type: "div", + _children: [ + { + _id: "781e497e-2e7c-11eb-adc1-0242ac120002", + _component: "@budibase/standard-components/login", + _styles: { + normal: { + padding: "64px", + background: "rgba(255, 255, 255, 0.4)", + "border-radius": "0.5rem", + "margin-top": "0px", + "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", + }, + hover: {}, + active: {}, + selected: {}, + }, + logo: + "https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg", + title: `Log in to ${app.name}`, + buttonText: "Log In", + _children: [], + _instanceName: "Login", + }, + ], + }, + routing: { + route: "/", + roleId: BUILTIN_ROLE_IDS.PUBLIC, + }, + name: "login-screen", +}) diff --git a/packages/server/src/db/utils.js b/packages/server/src/db/utils.js index 1989633463..737febbdfb 100644 --- a/packages/server/src/db/utils.js +++ b/packages/server/src/db/utils.js @@ -13,7 +13,7 @@ const DocumentTypes = { ROLE: "role", WEBHOOK: "wh", INSTANCE: "inst", - PAGE: "page", + LAYOUT: "layout", SCREEN: "screen", } @@ -101,21 +101,17 @@ exports.generateRowID = tableId => { /** * Gets parameters for retrieving users, this is a utility function for the getDocParams function. */ -exports.getUserParams = (username = "", otherProps = {}) => { - return getDocParams( - DocumentTypes.ROW, - `${ViewNames.USERS}${SEPARATOR}${DocumentTypes.USER}${SEPARATOR}${username}`, - otherProps - ) +exports.getUserParams = (email = "", otherProps = {}) => { + return exports.getRowParams(ViewNames.USERS, email, otherProps) } /** - * Generates a new user ID based on the passed in username. - * @param {string} username The username which the ID is going to be built up of. + * Generates a new user ID based on the passed in email. + * @param {string} email The email which the ID is going to be built up of. * @returns {string} The new user ID which the user doc can be stored under. */ -exports.generateUserID = username => { - return `${DocumentTypes.ROW}${SEPARATOR}${ViewNames.USERS}${SEPARATOR}${DocumentTypes.USER}${SEPARATOR}${username}` +exports.generateUserID = email => { + return `${DocumentTypes.ROW}${SEPARATOR}${ViewNames.USERS}${SEPARATOR}${DocumentTypes.USER}${SEPARATOR}${email}` } /** @@ -183,6 +179,36 @@ exports.getRoleParams = (roleId = null, otherProps = {}) => { return getDocParams(DocumentTypes.ROLE, roleId, otherProps) } +/** + * Generates a new layout ID. + * @returns {string} The new layout ID which the layout doc can be stored under. + */ +exports.generateLayoutID = id => { + return `${DocumentTypes.LAYOUT}${SEPARATOR}${id || newid()}` +} + +/** + * Gets parameters for retrieving layout, this is a utility function for the getDocParams function. + */ +exports.getLayoutParams = (layoutId = null, otherProps = {}) => { + return getDocParams(DocumentTypes.LAYOUT, layoutId, otherProps) +} + +/** + * Generates a new screen ID. + * @returns {string} The new screen ID which the screen doc can be stored under. + */ +exports.generateScreenID = () => { + return `${DocumentTypes.SCREEN}${SEPARATOR}${newid()}` +} + +/** + * Gets parameters for retrieving screens, this is a utility function for the getDocParams function. + */ +exports.getScreenParams = (screenId = null, otherProps = {}) => { + return getDocParams(DocumentTypes.SCREEN, screenId, otherProps) +} + /** * Generates a new webhook ID. * @returns {string} The new webhook ID which the webhook doc can be stored under. @@ -191,36 +217,6 @@ 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/events/AutomationEmitter.js b/packages/server/src/events/AutomationEmitter.js new file mode 100644 index 0000000000..fbfc445e2c --- /dev/null +++ b/packages/server/src/events/AutomationEmitter.js @@ -0,0 +1,51 @@ +const { rowEmission, tableEmission } = require("./utils") +const mainEmitter = require("./index") + +// max number of automations that can chain on top of each other +const MAX_AUTOMATION_CHAIN = 5 + +/** + * Special emitter which takes the count of automation runs which have occurred and blocks an + * automation from running if it has reached the maximum number of chained automations runs. + * This essentially "fakes" the normal emitter to add some functionality in-between to stop automations + * from getting stuck endlessly chaining. + */ +class AutomationEmitter { + constructor(chainCount) { + this.chainCount = chainCount + this.metadata = { + automationChainCount: chainCount, + } + } + + emitRow(eventName, appId, row, table = null) { + // don't emit even if we've reached max automation chain + if (this.chainCount >= MAX_AUTOMATION_CHAIN) { + return + } + rowEmission({ + emitter: mainEmitter, + eventName, + appId, + row, + table, + metadata: this.metadata, + }) + } + + emitTable(eventName, appId, table = null) { + // don't emit even if we've reached max automation chain + if (this.chainCount > MAX_AUTOMATION_CHAIN) { + return + } + tableEmission({ + emitter: mainEmitter, + eventName, + appId, + table, + metadata: this.metadata, + }) + } +} + +module.exports = AutomationEmitter diff --git a/packages/server/src/events/index.js b/packages/server/src/events/index.js index 237e212293..6fd97487d6 100644 --- a/packages/server/src/events/index.js +++ b/packages/server/src/events/index.js @@ -1,4 +1,5 @@ const EventEmitter = require("events").EventEmitter +const { rowEmission, tableEmission } = require("./utils") /** * keeping event emitter in one central location as it might be used for things other than @@ -12,36 +13,11 @@ const EventEmitter = require("events").EventEmitter */ class BudibaseEmitter extends EventEmitter { emitRow(eventName, appId, row, table = null) { - let event = { - row, - appId, - tableId: row.tableId, - } - if (table) { - event.table = table - } - event.id = row._id - if (row._rev) { - event.revision = row._rev - } - this.emit(eventName, event) + rowEmission({ emitter: this, eventName, appId, row, table }) } emitTable(eventName, appId, table = null) { - const tableId = table._id - let event = { - table: { - ...table, - tableId: tableId, - }, - appId, - tableId: tableId, - } - event.id = tableId - if (table._rev) { - event.revision = table._rev - } - this.emit(eventName, event) + tableEmission({ emitter: this, eventName, appId, table }) } } diff --git a/packages/server/src/events/utils.js b/packages/server/src/events/utils.js new file mode 100644 index 0000000000..2d43139d27 --- /dev/null +++ b/packages/server/src/events/utils.js @@ -0,0 +1,38 @@ +exports.rowEmission = ({ emitter, eventName, appId, row, table, metadata }) => { + let event = { + row, + appId, + tableId: row.tableId, + } + if (table) { + event.table = table + } + event.id = row._id + if (row._rev) { + event.revision = row._rev + } + if (metadata) { + event.metadata = metadata + } + emitter.emit(eventName, event) +} + +exports.tableEmission = ({ emitter, eventName, appId, table, metadata }) => { + const tableId = table._id + let event = { + table: { + ...table, + tableId: tableId, + }, + appId, + tableId: tableId, + } + event.id = tableId + if (table._rev) { + event.revision = table._rev + } + if (metadata) { + event.metadata = metadata + } + emitter.emit(eventName, event) +} diff --git a/packages/server/src/utilities/builder/compileStaticAssets.js b/packages/server/src/utilities/builder/compileStaticAssets.js new file mode 100644 index 0000000000..763fccd35a --- /dev/null +++ b/packages/server/src/utilities/builder/compileStaticAssets.js @@ -0,0 +1,83 @@ +const { + ensureDir, + constants, + copyFile, + writeFile, + readdir, + readFile, + existsSync, +} = require("fs-extra") +const { join } = require("../centralPath") +const { budibaseAppsDir } = require("../budibaseDir") + +const CSS_DIRECTORY = "css" + +/** + * Compile all the non-db static web assets that are required for the running of + * a budibase application. This includes CSS, the JSON structure of the DOM and + * the client library, a script responsible for reading the JSON structure + * and rendering the application. + * @param {string} appId id of the application we want to compile static assets for + * @param {array|object} assets a list of screens or screen layouts for which the CSS should be extracted and stored. + */ +module.exports = async (appId, assets) => { + const publicPath = join(budibaseAppsDir(), appId, "public") + await ensureDir(publicPath) + for (let asset of Array.isArray(assets) ? assets : [assets]) { + await buildCssBundle(publicPath, asset) + await copyClientLib(publicPath) + // remove props that shouldn't be present when written to DB + if (asset._css) { + delete asset._css + } + } + return assets +} + +/** + * Reads the _css property of all screens and the screen layouts, and creates a singular CSS + * bundle for the app at /public/bundle.css + * @param {String} publicPath - path to the public assets directory of the budibase application + * @param {Object} asset a single screen or screen layout which is being updated + */ +const buildCssBundle = async (publicPath, asset) => { + const cssPath = join(publicPath, CSS_DIRECTORY) + let cssString = "" + + // create a singular CSS file for this asset + const assetCss = asset._css ? asset._css.trim() : "" + if (assetCss.length !== 0) { + await ensureDir(cssPath) + await writeFile(join(cssPath, asset._id), assetCss) + } + + // bundle up all the CSS in the directory into one top level CSS file + if (existsSync(cssPath)) { + const cssFiles = await readdir(cssPath) + for (let filename of cssFiles) { + const css = await readFile(join(cssPath, filename)) + cssString += css + } + } + + await writeFile(join(publicPath, "bundle.css"), cssString) +} + +/** + * Copy the budibase client library and sourcemap from NPM to /public/. + * The client library is then served as a static asset when the budibase application + * is running in preview or prod + * @param {String} publicPath - path to write the client library to + */ +const copyClientLib = async publicPath => { + const sourcepath = require.resolve("@budibase/client") + const destPath = join(publicPath, "budibase-client.js") + + await copyFile(sourcepath, destPath, constants.COPYFILE_FICLONE) + + await copyFile( + sourcepath + ".map", + destPath + ".map", + constants.COPYFILE_FICLONE + ) +} diff --git a/packages/server/src/utilities/builder/compileStaticAssetsForPage.js b/packages/server/src/utilities/builder/compileStaticAssetsForPage.js deleted file mode 100644 index c91ba24bb3..0000000000 --- a/packages/server/src/utilities/builder/compileStaticAssetsForPage.js +++ /dev/null @@ -1,102 +0,0 @@ -const { ensureDir, constants, copyFile, writeFile } = require("fs-extra") -const { join } = require("../centralPath") -const { budibaseAppsDir } = require("../budibaseDir") - -/** - * Compile all the non-db static web assets that are required for the running of - * a budibase application. This includes CSS, the JSON structure of the DOM and - * the client library, a script responsible for reading the JSON structure - * and rendering the application. - * @param {} appId - id of the application we want to compile static assets for - * @param {*} pageName - name of the page that the assets will be served for - * @param {*} pkg - app package information/metadata - */ -module.exports = async (appId, pageName, pkg) => { - const pagePath = join(budibaseAppsDir(), appId, "public", pageName) - - pkg.screens = pkg.screens || [] - - await ensureDir(pagePath) - - await buildPageCssBundle(pagePath, pkg) - - await buildFrontendAppDefinition(pagePath, pkg) - - await copyClientLib(pagePath) -} - -/** - * Reads the _css property of a page and its screens, and creates a singular CSS - * bundle for the page at /public//bundle.css - * @param {String} publicPagePath - path to the public assets directory of the budibase application - * @param {Object} pkg - app package information - * @param {"main" | "unauthenticated"} pageName - the pagename of the page we are compiling CSS for. - */ -const buildPageCssBundle = async (publicPagePath, pkg) => { - let cssString = "" - - for (let screen of pkg.screens || []) { - if (!screen._css) continue - if (screen._css.trim().length === 0) { - delete screen._css - continue - } - cssString += screen._css - } - - if (pkg.page._css) cssString += pkg.page._css - - writeFile(join(publicPagePath, "bundle.css"), cssString) -} - -/** - * Copy the budibase client library and sourcemap from NPM to /public/. - * The client library is then served as a static asset when the budibase application - * is running in preview or prod - * @param {String} pagePath - path to write the client library to - */ -const copyClientLib = async pagePath => { - const sourcepath = require.resolve("@budibase/client") - const destPath = join(pagePath, "budibase-client.js") - - await copyFile(sourcepath, destPath, constants.COPYFILE_FICLONE) - - await copyFile( - sourcepath + ".map", - destPath + ".map", - constants.COPYFILE_FICLONE - ) -} - -/** - * Build the frontend definition for a budibase application. This includes all page and screen information, - * and is injected into the budibase client library to tell it how to start constructing - * the DOM from components defined in the frontendDefinition. - * @param {String} pagePath - path to the public folder of the page where the definition will be written - * @param {Object} pkg - app package information from which the frontendDefinition will be built. - */ -const buildFrontendAppDefinition = async (pagePath, pkg) => { - const filename = join(pagePath, "clientFrontendDefinition.js") - - // Delete CSS code from the page and screens so it's not injected - delete pkg.page._css - - for (let screen of pkg.screens) { - if (screen._css) { - delete pkg.page._css - } - } - - const clientUiDefinition = JSON.stringify({ - page: pkg.page, - screens: pkg.screens, - libraries: ["@budibase/standard-components"], - }) - - await writeFile( - filename, - ` - window['##BUDIBASE_FRONTEND_DEFINITION##'] = ${clientUiDefinition}; - ` - ) -} diff --git a/packages/builder/src/builderStore/generate_css.js b/packages/server/src/utilities/builder/generateCss.js similarity index 72% rename from packages/builder/src/builderStore/generate_css.js rename to packages/server/src/utilities/builder/generateCss.js index 2bb5a3bd2e..c3d72c741f 100644 --- a/packages/builder/src/builderStore/generate_css.js +++ b/packages/server/src/utilities/builder/generateCss.js @@ -1,21 +1,21 @@ -export const generate_screen_css = component_arr => { +exports.generateAssetCss = component_arr => { let styles = "" for (const { _styles, _id, _children, _component } of component_arr) { let [componentName] = _component.match(/[a-z]*$/) Object.keys(_styles).forEach(selector => { - const cssString = generate_css(_styles[selector]) + const cssString = exports.generateCss(_styles[selector]) if (cssString) { - styles += apply_class(_id, componentName, cssString, selector) + styles += exports.applyClass(_id, componentName, cssString, selector) } }) if (_children && _children.length) { - styles += generate_screen_css(_children) + "\n" + styles += exports.generateAssetCss(_children) + "\n" } } return styles.trim() } -export const generate_css = style => { +exports.generateCss = style => { let cssString = Object.entries(style).reduce((str, [key, value]) => { if (typeof value === "string") { if (value) { @@ -33,7 +33,7 @@ export const generate_css = style => { return (cssString || "").trim() } -export const apply_class = (id, name = "element", styles, selector) => { +exports.applyClass = (id, name = "element", styles, selector) => { if (selector === "normal") { return `.${name}-${id} {\n${styles}\n}` } else { diff --git a/packages/server/src/utilities/mustache.js b/packages/server/src/utilities/mustache.js new file mode 100644 index 0000000000..0428bdc03d --- /dev/null +++ b/packages/server/src/utilities/mustache.js @@ -0,0 +1,73 @@ +const handlebars = require("handlebars") + +handlebars.registerHelper("object", value => { + return new handlebars.SafeString(JSON.stringify(value)) +}) + +/** + * When running mustache statements to execute on the context of the automation it possible user's may input mustache + * in a few different forms, some of which are invalid but are logically valid. An example of this would be the mustache + * statement "{{steps[0].revision}}" here it is obvious the user is attempting to access an array or object using array + * like operators. These are not supported by Mustache and therefore the statement will fail. This function will clean up + * the mustache statement so it instead reads as "{{steps.0.revision}}" which is valid and will work. It may also be expanded + * to include any other mustache statement cleanup that has been deemed necessary for the system. + * + * @param {string} string The string which *may* contain mustache statements, it is OK if it does not contain any. + * @returns {string} The string that was input with cleaned up mustache statements as required. + */ +function cleanMustache(string) { + let charToReplace = { + "[": ".", + "]": "", + } + let regex = new RegExp(/{{[^}}]*}}/g) + let matches = string.match(regex) + if (matches == null) { + return string + } + for (let match of matches) { + let baseIdx = string.indexOf(match) + for (let key of Object.keys(charToReplace)) { + let idxChar = match.indexOf(key) + if (idxChar !== -1) { + string = + string.slice(baseIdx, baseIdx + idxChar) + + charToReplace[key] + + string.slice(baseIdx + idxChar + 1) + } + } + } + return string +} + +/** + * Given an input object this will recurse through all props to try and update + * any handlebars/mustache statements within. + * @param {object|array} inputs The input structure which is to be recursed, it is important to note that + * if the structure contains any cycles then this will fail. + * @param {object} context The context that handlebars should fill data from. + * @returns {object|array} The structure input, as fully updated as possible. + */ +function recurseMustache(inputs, context) { + // JSON stringify will fail if there are any cycles, stops infinite recursion + try { + JSON.stringify(inputs) + } catch (err) { + throw "Unable to process inputs to JSON, cannot recurse" + } + for (let key of Object.keys(inputs)) { + let val = inputs[key] + if (typeof val === "string") { + val = cleanMustache(inputs[key]) + const template = handlebars.compile(val) + inputs[key] = template(context) + } + // this covers objects and arrays + else if (typeof val === "object") { + inputs[key] = recurseMustache(inputs[key], context) + } + } + return inputs +} + +exports.recurseMustache = recurseMustache diff --git a/packages/server/src/utilities/security/roles.js b/packages/server/src/utilities/security/roles.js index e971217d17..6b6ec39b24 100644 --- a/packages/server/src/utilities/security/roles.js +++ b/packages/server/src/utilities/security/roles.js @@ -4,7 +4,7 @@ const { BUILTIN_PERMISSION_IDS } = require("./permissions") const BUILTIN_IDS = { ADMIN: "ADMIN", - POWER: "POWER_USER", + POWER: "POWER", BASIC: "BASIC", PUBLIC: "PUBLIC", BUILDER: "BUILDER", @@ -148,7 +148,7 @@ class AccessController { let roleIds = this.userHierarchies[userRoleId] if (!roleIds) { roleIds = await exports.getUserRoleHierarchy(this.appId, userRoleId) - this.userHierarchies[userRoleId] = userRoleId + this.userHierarchies[userRoleId] = roleIds } return roleIds.indexOf(tryingRoleId) !== -1 diff --git a/packages/standard-components/components.json b/packages/standard-components/components.json index d1376384d8..c55c04f119 100644 --- a/packages/standard-components/components.json +++ b/packages/standard-components/components.json @@ -56,7 +56,7 @@ }, "login": { "name": "Login Control", - "description": "A control that accepts username, password an also handles password resets", + "description": "A control that accepts email, password an also handles password resets", "props": { "logo": "string", "title": "string", @@ -257,6 +257,14 @@ } } }, + "cardstat": { + "name": "Stat Card", + "props": { + "title": "string", + "value": "string", + "label": "string" + } + }, "cardhorizontal": { "name": "Horizontal Card", "props": { diff --git a/packages/standard-components/package.json b/packages/standard-components/package.json index 50f429838a..440784e4bb 100644 --- a/packages/standard-components/package.json +++ b/packages/standard-components/package.json @@ -19,6 +19,7 @@ "lodash": "^4.17.15", "rollup": "^2.11.2", "rollup-plugin-livereload": "^1.0.1", + "rollup-plugin-node-builtins": "^2.1.2", "rollup-plugin-postcss": "^3.1.5", "rollup-plugin-svelte": "^6.1.1", "rollup-plugin-terser": "^7.0.2", @@ -32,13 +33,15 @@ "license": "MIT", "gitHead": "284cceb9b703c38566c6e6363c022f79a08d5691", "dependencies": { - "@budibase/bbui": "^1.51.0", + "@budibase/bbui": "^1.52.1", "@budibase/svelte-ag-grid": "^0.0.16", "@fortawesome/fontawesome-free": "^5.14.0", "apexcharts": "^3.22.1", "flatpickr": "^4.6.6", "lodash.debounce": "^4.0.8", + "markdown-it": "^12.0.2", "quill": "^1.3.7", + "turndown": "^7.0.0", "svelte-apexcharts": "^1.0.2", "svelte-flatpickr": "^3.1.0" } diff --git a/packages/standard-components/rollup.config.js b/packages/standard-components/rollup.config.js index c4c7767200..dd8fd47b60 100644 --- a/packages/standard-components/rollup.config.js +++ b/packages/standard-components/rollup.config.js @@ -2,8 +2,11 @@ import commonjs from "@rollup/plugin-commonjs" import resolve from "@rollup/plugin-node-resolve" import svelte from "rollup-plugin-svelte" import postcss from "rollup-plugin-postcss" +import json from "@rollup/plugin-json" import { terser } from "rollup-plugin-terser" +import builtins from "rollup-plugin-node-builtins" + const production = !process.env.ROLLUP_WATCH const externals = ["svelte", "svelte/internal"] @@ -18,6 +21,7 @@ export default { }, ], plugins: [ + builtins(), production && terser(), postcss(), svelte({ @@ -28,5 +32,6 @@ export default { skip: externals, }), commonjs(), + json(), ], } diff --git a/packages/standard-components/src/CardStat.svelte b/packages/standard-components/src/CardStat.svelte new file mode 100644 index 0000000000..e5e40ad862 --- /dev/null +++ b/packages/standard-components/src/CardStat.svelte @@ -0,0 +1,49 @@ + + +
+

{title}

+

{value}

+

{label}

+
+ + diff --git a/packages/standard-components/src/Form.svelte b/packages/standard-components/src/Form.svelte index 03b11307a1..f868956dd9 100644 --- a/packages/standard-components/src/Form.svelte +++ b/packages/standard-components/src/Form.svelte @@ -1,6 +1,13 @@ @@ -42,9 +42,9 @@
@@ -62,7 +62,7 @@
{#if error} -
Incorrect username or password
+
Incorrect email or password
{/if}
diff --git a/packages/standard-components/src/RichText.svelte b/packages/standard-components/src/RichText.svelte index b5b8d56eff..18aca42b1a 100644 --- a/packages/standard-components/src/RichText.svelte +++ b/packages/standard-components/src/RichText.svelte @@ -24,5 +24,5 @@
- +
diff --git a/packages/standard-components/src/grid/CreateRow/Modal.svelte b/packages/standard-components/src/grid/CreateRow/Modal.svelte index 9c7de2b0ff..1cb9aa923d 100644 --- a/packages/standard-components/src/grid/CreateRow/Modal.svelte +++ b/packages/standard-components/src/grid/CreateRow/Modal.svelte @@ -1,6 +1,6 @@