diff --git a/packages/builder/src/builderStore/store.js b/packages/builder/src/builderStore/store.js index 0633893f71..49f6c0308a 100644 --- a/packages/builder/src/builderStore/store.js +++ b/packages/builder/src/builderStore/store.js @@ -1,10 +1,14 @@ import {hierarchy as hierarchyFunctions, common, getTemplateApi } from "budibase-core"; -import {filter, cloneDeep, sortBy, map, last, +import {filter, cloneDeep, sortBy, map, last, keys, + cloneDeep, keyBy, find, isEmpty, groupBy, reduce} from "lodash/fp"; import {chain, getNode, validate, constructHierarchy, templateApi} from "../common/core"; import {writable} from "svelte/store"; +import { defaultPagesObject } from "../userInterface/pagesParsing/defaultPagesObject" + +const pipe = common.$; export const getStore = () => { @@ -14,6 +18,11 @@ export const getStore = () => { hierarchy: {}, actions: [], triggers: [], + pages:defaultPagesObject(), + mainUi:{}, + unauthenticatedUi:{}, + derivedComponents:[], + rootComponents:[], currentNodeIsNew: false, errors: [], activeNav: "database", @@ -41,6 +50,9 @@ export const getStore = () => { store.saveLevel = saveLevel(store); store.deleteLevel = deleteLevel(store); store.setActiveNav = setActiveNav(store); + store.saveDerivedComponent = saveDerivedComponent(store); + store.refreshComponents = refreshComponents(store); + store.addComponentLibrary = addComponentLibrary(store); return store; } @@ -312,30 +324,107 @@ const deleteLevel = store => level => { }); } -const setActiveNav = databaseStore => navName => { - databaseStore.update(db => { - db.activeNav = navName; - return db; +const setActiveNav = store => navName => { + store.update(s => { + s.activeNav = navName; + return s; }); } const createShadowHierarchy = hierarchy => constructHierarchy(JSON.parse(JSON.stringify(hierarchy))); -const savePackage = (store, db) => { +const saveDerivedComponent = store => (derivedComponent) => { + + store.update(s => { + + const derivedComponents = pipe(s.derivedComponents, [ + filter(c => c._name !== derivedComponent._name) + ]); + + s.derivedComponents = derivedComponents; + + const forSave = pipe(derivedComponents, [ + cloneDeep, + keyBy("_name") + ]); + + for(let c of forSave) { + delete c._name; + } + + s.pages.derivedComponents = forSave; + savePackage(store, s); + + return s; + }) + +}; + +const addComponentLibrary = store => async lib => { + + const response = + await fetch(`/_builder/api/${db.appname}/components?${encodeURI(lib)}`); + + const success = response.status === 200; + + const error = response.status === 404 + ? `Could not find library ${lib}` + : success + ? "" + : response.statusText; + + const components = success + ? await response.json() + : []; + + store.update(s => { + s.componentsErrors.addComponent = error; + if(success) { + s.pages.componentLibraries.push(lib); + s.rootComponents = [...s.rootComponents, components]; + } + + return s; + }) + + +} + +const refreshComponents = store => async () => { + + const components = + await fetch(`/_builder/api/${db.appname}/components`) + .then(r => jQuery.json()); + + const rootComponents = pipe(components, [ + keys, + map(k => ({...components[k], _name:k})) + ]); + + store.update(s => { + s.rootComponents = rootComponents; + return s; + }); +}; + +const savePackage = (store, s) => { const appDefinition = { - hierarchy:db.hierarchy, - triggers:db.triggers, - actions: groupBy("name")(db.actions) + hierarchy:s.hierarchy, + triggers:s.triggers, + actions: groupBy("name")(s.actions), + pages:s.pages, + mainUi: s.mainUi, + unauthenticatedUi: s.unauthenticatedUi }; const data = { appDefinition, - accessLevels:db.accessLevels + accessLevels:s.accessLevels } - fetch(`/_builder/api/${db.appname}/appPackage`, { + fetch(`/_builder/api/${s.appname}/appPackage`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/packages/builder/src/userInterface/propsDefinitionParsing/createProps.js b/packages/builder/src/userInterface/pagesParsing/createProps.js similarity index 100% rename from packages/builder/src/userInterface/propsDefinitionParsing/createProps.js rename to packages/builder/src/userInterface/pagesParsing/createProps.js diff --git a/packages/builder/src/userInterface/pagesParsing/defaultPagesObject.js b/packages/builder/src/userInterface/pagesParsing/defaultPagesObject.js new file mode 100644 index 0000000000..4fbeb98a86 --- /dev/null +++ b/packages/builder/src/userInterface/pagesParsing/defaultPagesObject.js @@ -0,0 +1,16 @@ +export const defaultPagesObject = () => ({ + main: { + index: { + _component : "./components/indexHtml" + }, + appBody: "bbapp.main.json" + }, + unauthenticated: { + index: { + _component : "./components/indexHtml" + }, + appBody: "bbapp.unauthenticated.json" + }, + componentLibraries: ["./components"], + derivedComponents:[] +}); \ No newline at end of file diff --git a/packages/builder/src/userInterface/propsDefinitionParsing/types.js b/packages/builder/src/userInterface/pagesParsing/types.js similarity index 100% rename from packages/builder/src/userInterface/propsDefinitionParsing/types.js rename to packages/builder/src/userInterface/pagesParsing/types.js diff --git a/packages/builder/src/userInterface/propsDefinitionParsing/validatePages.js b/packages/builder/src/userInterface/pagesParsing/validatePages.js similarity index 100% rename from packages/builder/src/userInterface/propsDefinitionParsing/validatePages.js rename to packages/builder/src/userInterface/pagesParsing/validatePages.js diff --git a/packages/builder/src/userInterface/propsDefinitionParsing/validateProps.js b/packages/builder/src/userInterface/pagesParsing/validateProps.js similarity index 100% rename from packages/builder/src/userInterface/propsDefinitionParsing/validateProps.js rename to packages/builder/src/userInterface/pagesParsing/validateProps.js diff --git a/packages/builder/tests/validatePages.spec.js b/packages/builder/tests/validatePages.spec.js index 3305a6bf01..29de0c73fb 100644 --- a/packages/builder/tests/validatePages.spec.js +++ b/packages/builder/tests/validatePages.spec.js @@ -1,7 +1,7 @@ import { validatePages, validatePage -} from "../src/userInterface/propsDefinitionParsing/validatePages"; +} from "../src/userInterface/pagesParsing/validatePages"; const validPages = () => ({ "main" : { @@ -85,8 +85,37 @@ describe("validate pages", () => { expect(errors).toEqual([]); }); - it("should return error when index is not set, or set incorrectly", () => { + it("should return error when component libraries not set", () => { + const pages = validPages(); + + delete pages.componentLibraries; + let errors = validatePages(pages, getComponent); + expect(errors.length).toBe(1); + + pages.componentLibraries = []; + errors = validatePages(pages, getComponent); + expect(errors.length).toBe(1); }); + it("should return error when no main or unauthenticated page", () => { + + let pages = validPages(); + delete pages.main; + let errors = validatePages(pages, getComponent); + expect(errors.length).toBe(1); + + pages = validPages(); + delete pages.unauthenticated; + errors = validatePages(pages, getComponent); + expect(errors.length).toBe(1); + + }); + + it("should return error when page is invalid", () => { + const pages = validPages(); + delete pages.main.index; + const errors = validatePages(pages, getComponent); + expect(errors.length).toBe(1); + }); }); diff --git a/packages/server/appPackages/testApp/dist/package.tar.gz b/packages/server/appPackages/testApp/dist/package.tar.gz index b3335d1d3d..43818edf6a 100644 Binary files a/packages/server/appPackages/testApp/dist/package.tar.gz and b/packages/server/appPackages/testApp/dist/package.tar.gz differ diff --git a/packages/server/appPackages/testApp/myComponents/components.json b/packages/server/appPackages/testApp/myComponents/components.json new file mode 100644 index 0000000000..3b11bb76a3 --- /dev/null +++ b/packages/server/appPackages/testApp/myComponents/components.json @@ -0,0 +1,11 @@ +{ + "textbox" : { + "path": "./textbox", + "name": "Textbox", + "description": "A text input, with a label", + "props": { + "label": "string" + }, + "tags": ["textboxt", "input", "text"] + } +} \ No newline at end of file diff --git a/packages/server/appPackages/testApp/myComponents/textbox/index.js b/packages/server/appPackages/testApp/myComponents/textbox/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/server/appPackages/testApp/pages.json b/packages/server/appPackages/testApp/pages.json new file mode 100644 index 0000000000..9d38c2c9de --- /dev/null +++ b/packages/server/appPackages/testApp/pages.json @@ -0,0 +1,19 @@ +{ + "main" : { + "index" : { + + }, + "appBody" : "./main.app.json" + }, + "unauthenticated" : { + "index" : { + "_component": "budibase-components/indexHtml", + "title": "Test App 1 - Login", + "customScripts": [ + "MyCustomComponents.js" + ] + }, + "appBody" : "./unauthenticated.app.json" + }, + "componentLibraries": ["./myComponents"] +} \ No newline at end of file diff --git a/packages/server/appPackages/testApp/ui/main/public/app.js b/packages/server/appPackages/testApp/public/main/app.js similarity index 100% rename from packages/server/appPackages/testApp/ui/main/public/app.js rename to packages/server/appPackages/testApp/public/main/app.js diff --git a/packages/server/appPackages/testApp/ui/main/public/index.html b/packages/server/appPackages/testApp/public/main/index.html similarity index 100% rename from packages/server/appPackages/testApp/ui/main/public/index.html rename to packages/server/appPackages/testApp/public/main/index.html diff --git a/packages/server/appPackages/testApp/ui/unauthenticated/public/app.js b/packages/server/appPackages/testApp/public/unauthenticated/app.js similarity index 100% rename from packages/server/appPackages/testApp/ui/unauthenticated/public/app.js rename to packages/server/appPackages/testApp/public/unauthenticated/app.js diff --git a/packages/server/appPackages/testApp/ui/unauthenticated/public/index.html b/packages/server/appPackages/testApp/public/unauthenticated/index.html similarity index 100% rename from packages/server/appPackages/testApp/ui/unauthenticated/public/index.html rename to packages/server/appPackages/testApp/public/unauthenticated/index.html diff --git a/packages/server/middleware/routers.js b/packages/server/middleware/routers.js index 564ce42dfd..42ed712caa 100644 --- a/packages/server/middleware/routers.js +++ b/packages/server/middleware/routers.js @@ -4,7 +4,7 @@ const StatusCodes = require("../utilities/statusCodes"); const fs = require("fs"); const { resolve } = require("path"); const send = require('koa-send'); -const { getPackageForBuilder, +const { getPackageForBuilder, getComponents, savePackage, getApps } = require("../utilities/builder"); const builderPath = resolve(__dirname, "../builder"); @@ -152,6 +152,27 @@ module.exports = (config, app) => { ctx.request.body); ctx.response.status = StatusCodes.OK; }) + .get("/_builder/api/:appname/components", async (ctx) => { + if(!config.dev) { + ctx.request.status = StatusCodes.FORBIDDEN; + ctx.body = "run in dev mode to access builder"; + return; + } + + try { + ctx.body = getComponents( + config, + ctx.params.appname, + ctx.query.lib); + ctx.response.status = StatusCodes.OK; + } catch(e) { + if(e.status) { + ctx.response.status = e.status; + } else { + throw e; + } + } + }) .get("/:appname", async (ctx) => { await send(ctx, "/index.html", { root: ctx.publicPath }); }) diff --git a/packages/server/tests/serveui.js b/packages/server/tests/serveui.js index c8ef0d5644..6bb6855b91 100644 --- a/packages/server/tests/serveui.js +++ b/packages/server/tests/serveui.js @@ -7,7 +7,7 @@ module.exports = (app) => { const response = await app.get("/testApp") .expect(statusCodes.OK); - const expectedIndexHtml = await readFile("appPackages/testApp/ui/unauthenticated/public/index.html", "utf8"); + const expectedIndexHtml = await readFile("appPackages/testApp/public/unauthenticated/index.html", "utf8"); expect(response.text).toBe(expectedIndexHtml); @@ -17,7 +17,7 @@ module.exports = (app) => { const response = await app.get("/testApp/app.js") .expect(statusCodes.OK); - const expectedFile = await readFile("appPackages/testApp/ui/unauthenticated/public/app.js", "utf8"); + const expectedFile = await readFile("appPackages/testApp/public/unauthenticated/app.js", "utf8"); expect(response.text).toBe(expectedFile); @@ -28,7 +28,7 @@ module.exports = (app) => { .set("cookie", app.credentials.testAppUser1.cookie) .expect(statusCodes.OK); - const expectedIndexHtml = await readFile("appPackages/testApp/ui/main/public/index.html", "utf8"); + const expectedIndexHtml = await readFile("appPackages/testApp/public/main/index.html", "utf8"); expect(response.text).toBe(expectedIndexHtml); @@ -39,7 +39,7 @@ module.exports = (app) => { .set("cookie", app.credentials.testAppUser1.cookie) .expect(statusCodes.OK); - const expectedFile = await readFile("appPackages/testApp/ui/main/public/app.js", "utf8"); + const expectedFile = await readFile("appPackages/testApp/public/main/app.js", "utf8"); expect(response.text).toBe(expectedFile); diff --git a/packages/server/utilities/builder.js b/packages/server/utilities/builder.js index ec23a9da0a..f98ca2df87 100644 --- a/packages/server/utilities/builder.js +++ b/packages/server/utilities/builder.js @@ -1,6 +1,22 @@ -const { appPackageFolder, appsFolder } = require("./createAppPackage"); -const { writeFile, readFile, readdir } = require("./fsawait"); -const { pipe : $ } = require("budibase-core").common; +const { + appPackageFolder, + appsFolder +} = require("./createAppPackage"); +const { + writeFile, + readFile, + readdir, + exists +} = require("./fsawait"); +const { resolve } = require("path"); +const { $ } = require("budibase-core").common; +const { + keys, + reduce, + map, + flatten, + some +} = require("lodash/fp"); module.exports.getPackageForBuilder = async (config, appname) => { const appPath = appPackageFolder(config, appname); @@ -11,12 +27,15 @@ module.exports.getPackageForBuilder = async (config, appname) => { accessLevels: JSON.parse(await readFile( `${appPath}/access_levels.json`, + "utf8")), + + pages: JSON.parse(await readFile( + `${appPath}/pages.json`, "utf8")) }) } - module.exports.savePackage = async (config, appname, pkg) => { const appPath = appPackageFolder(config, appname); await writeFile( @@ -29,10 +48,68 @@ module.exports.savePackage = async (config, appname, pkg) => { JSON.stringify(pkg.accessLevels), "utf8"); + await writeFile( + `${appPath}/pages.json`, + JSON.stringify(pkg.accessLevels), + "utf8"); } module.exports.getApps = async (config) => await readdir(appsFolder(config)); +module.exports.getComponents = async (config, appname, lib) => { + + const componentsInLibrary = (libname) => { + const isRelative = some(c => c === libname.substring(0,1)) + ("./~\\".split("")); + + const componentsPath = isRelative + ? resolve(appPath, libname, "components.json") + : resolve(libname, "components.json"); + + if(!await exists(componentsPath)) { + const e = new Error(`could not find components definition file at ${componentsPath}`); + e.statusCode = 404; + throw e; + } + + let components; + try { + components = JSON.parse( + readFile(componentsPath, "utf8")); + } catch(e) { + const e = new Error(`could not parse JSON - ${componentsPath} `); + throw e; + } + + return $(components, [ + keys, + reduce((obj, k) => { + obj[`${libname}/${k}`] = components[k] + return obj; + }, {}) + ]) + } + + let libs; + if(!lib) { + const appPath = appPackageFolder(config, appname); + + const pages = JSON.parse(await readFile( + `${appPath}/pages.json`, + "utf8")); + + if(!pages.componentLibraries) return []; + + libs = pages.componentLibraries; + } else { + libs = [lib]; + } + + return $(libs, [ + map(componentsInLibrary), + flatten + ]); +} diff --git a/packages/server/utilities/createAppPackage.js b/packages/server/utilities/createAppPackage.js index 68a8ee4edf..baf98a8051 100644 --- a/packages/server/utilities/createAppPackage.js +++ b/packages/server/utilities/createAppPackage.js @@ -65,9 +65,9 @@ const applictionVersionPath = (appname, versionId) => const publicPaths = (appPath) => ({ mainUiPath: resolve(join( - __dirname, appPath, "ui", "main", "public")), + __dirname, appPath, "public", "main")), unauthenticatedUiPath: resolve(join( - __dirname, appPath, "ui", "unauthenticated", "public")) + __dirname, appPath, "public", "unauthenticated")) });