Merge branch 'master' of github.com:Budibase/budibase into stat-card
This commit is contained in:
commit
3164b45f95
|
@ -186,7 +186,7 @@ Or if you are in the builder you can run `yarn cy:test`.
|
||||||
|
|
||||||
### Other Useful Information
|
### Other Useful Information
|
||||||
|
|
||||||
* The contributors are listed in [AUTHORS.md](https://github.com/budibase/server/blob/master/AUTHORS.md) (add yourself).
|
* The contributors are listed in [AUTHORS.md](https://github.com/Budibase/budibase/blob/master/.github/AUTHORS.md) (add yourself).
|
||||||
|
|
||||||
* This project uses a modified version of the MPLv2 license, see [LICENSE](https://github.com/budibase/server/blob/master/LICENSE).
|
* This project uses a modified version of the MPLv2 license, see [LICENSE](https://github.com/budibase/server/blob/master/LICENSE).
|
||||||
|
|
|
@ -116,7 +116,7 @@ You can also follow a quick tutorial on [how to build a CRM with Budibase](https
|
||||||
|
|
||||||
## ❗ Code of Conduct
|
## ❗ Code of Conduct
|
||||||
|
|
||||||
Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/master/CODE_OF_CONDUCT.md). Please read it.
|
Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/master/.github/CODE_OF_CONDUCT.md). Please read it.
|
||||||
|
|
||||||
## 🙌 Contributing to Budibase
|
## 🙌 Contributing to Budibase
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ Budibase is a monorepo managed by lerna. Lerna manages the building and publishi
|
||||||
|
|
||||||
- [packages/server](https://github.com/Budibase/budibase/tree/master/packages/server) - The budibase server. This Koa app is responsible for serving the JS for the builder and budibase apps, as well as providing the API for interaction with the database and file system.
|
- [packages/server](https://github.com/Budibase/budibase/tree/master/packages/server) - The budibase server. This Koa app is responsible for serving the JS for the builder and budibase apps, as well as providing the API for interaction with the database and file system.
|
||||||
|
|
||||||
For more information, see [CONTRIBUTING.md](./CONTRIBUTING.md)
|
For more information, see [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/master/.github/CONTRIBUTING.md)
|
||||||
|
|
||||||
## 📝 License
|
## 📝 License
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
"prettier-plugin-svelte": "^1.4.0",
|
"prettier-plugin-svelte": "^1.4.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rollup-plugin-replace": "^2.2.0",
|
"rollup-plugin-replace": "^2.2.0",
|
||||||
"svelte": "^3.28.0"
|
"svelte": "^3.30.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"bootstrap": "lerna bootstrap",
|
"bootstrap": "lerna bootstrap",
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
"nuke": "rimraf ~/.budibase && npm run restore",
|
"nuke": "rimraf ~/.budibase && npm run restore",
|
||||||
"clean": "lerna clean",
|
"clean": "lerna clean",
|
||||||
"kill-port": "kill-port 4001",
|
"kill-port": "kill-port 4001",
|
||||||
"dev": "npm run kill-port && node ./scripts/symlinkDev.js && lerna run --parallel dev:builder --concurrency 1",
|
"dev": "yarn run kill-port && node ./scripts/symlinkDev.js && lerna run --parallel dev:builder --concurrency 1",
|
||||||
"test": "lerna run test",
|
"test": "lerna run test",
|
||||||
"lint": "eslint packages",
|
"lint": "eslint packages",
|
||||||
"lint:fix": "eslint --fix packages",
|
"lint:fix": "eslint --fix packages",
|
||||||
|
|
|
@ -60,7 +60,7 @@ context("Create a Table", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("deletes a table", () => {
|
it("deletes a table", () => {
|
||||||
cy.contains(".nav-item", "dog").get(".actions").invoke("show").click()
|
cy.get(".actions").first().invoke("show").click()
|
||||||
cy.get("[data-cy=delete-table]").click()
|
cy.get("[data-cy=delete-table]").click()
|
||||||
cy.contains("Delete Table").click()
|
cy.contains("Delete Table").click()
|
||||||
cy.contains("dog").should("not.exist")
|
cy.contains("dog").should("not.exist")
|
||||||
|
|
|
@ -9,9 +9,9 @@ context('Create a User', () => {
|
||||||
|
|
||||||
// https://on.cypress.io/interacting-with-elements
|
// https://on.cypress.io/interacting-with-elements
|
||||||
it('should create a user', () => {
|
it('should create a user', () => {
|
||||||
cy.createUser('bbuser', 'test', 'POWER_USER')
|
cy.createUser("bbuser", "test", "ADMIN")
|
||||||
|
|
||||||
// Check to make sure user was created!
|
// // Check to make sure user was created!
|
||||||
cy.get("input[disabled]").should('have.value', 'bbuser')
|
cy.contains("bbuser").should('be.visible')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,6 +16,9 @@ process.env.BUDIBASE_API_KEY = "6BE826CB-6B30-4AEC-8777-2E90464633DE"
|
||||||
process.env.NODE_ENV = "cypress"
|
process.env.NODE_ENV = "cypress"
|
||||||
process.env.ENABLE_ANALYTICS = "false"
|
process.env.ENABLE_ANALYTICS = "false"
|
||||||
|
|
||||||
|
// Stop info logs polluting test outputs
|
||||||
|
process.env.LOG_LEVEL = "error"
|
||||||
|
|
||||||
async function run(dir) {
|
async function run(dir) {
|
||||||
process.env.BUDIBASE_DIR = resolve(dir)
|
process.env.BUDIBASE_DIR = resolve(dir)
|
||||||
require("dotenv").config({ path: resolve(dir, ".env") })
|
require("dotenv").config({ path: resolve(dir, ".env") })
|
||||||
|
|
|
@ -113,23 +113,26 @@ Cypress.Commands.add("addRow", values => {
|
||||||
|
|
||||||
Cypress.Commands.add("createUser", (username, password, accessLevel) => {
|
Cypress.Commands.add("createUser", (username, password, accessLevel) => {
|
||||||
// Create User
|
// Create User
|
||||||
cy.get(".toprightnav > .settings").click()
|
|
||||||
cy.contains("Users").click()
|
cy.contains("Users").click()
|
||||||
|
|
||||||
cy.get("[name=Name]")
|
cy.contains("Create New Row").click()
|
||||||
.first()
|
|
||||||
.type(username)
|
|
||||||
cy.get("[name=Password]")
|
|
||||||
.first()
|
|
||||||
.type(password)
|
|
||||||
cy.get("select")
|
|
||||||
.first()
|
|
||||||
.select(accessLevel)
|
|
||||||
|
|
||||||
// Save
|
cy.get(".modal").within(() => {
|
||||||
cy.get(".inputs")
|
cy.get("input")
|
||||||
.contains("Create")
|
.first()
|
||||||
.click()
|
.type(password)
|
||||||
|
cy.get("input")
|
||||||
|
.eq(1)
|
||||||
|
.type(username)
|
||||||
|
cy.get("select")
|
||||||
|
.first()
|
||||||
|
.select(accessLevel)
|
||||||
|
|
||||||
|
// Save
|
||||||
|
cy.get(".buttons")
|
||||||
|
.contains("Create Row")
|
||||||
|
.click()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("addHeadlineComponent", text => {
|
Cypress.Commands.add("addHeadlineComponent", text => {
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.50.2",
|
"@budibase/bbui": "^1.51.0",
|
||||||
"@budibase/client": "^0.3.8",
|
"@budibase/client": "^0.3.8",
|
||||||
"@budibase/colorpicker": "^1.0.1",
|
"@budibase/colorpicker": "^1.0.1",
|
||||||
"@budibase/svelte-ag-grid": "^0.0.16",
|
"@budibase/svelte-ag-grid": "^0.0.16",
|
||||||
|
@ -81,8 +81,8 @@
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"svelte-loading-spinners": "^0.1.1",
|
"svelte-loading-spinners": "^0.1.1",
|
||||||
"svelte-portal": "^0.1.0",
|
"svelte-portal": "^0.1.0",
|
||||||
"yup": "^0.29.2",
|
"uuid": "^8.3.1",
|
||||||
"uuid": "^8.3.1"
|
"yup": "^0.29.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.5.5",
|
"@babel/core": "^7.5.5",
|
||||||
|
@ -90,6 +90,7 @@
|
||||||
"@babel/preset-env": "^7.5.5",
|
"@babel/preset-env": "^7.5.5",
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"@rollup/plugin-alias": "^3.0.1",
|
"@rollup/plugin-alias": "^3.0.1",
|
||||||
|
"@rollup/plugin-commonjs": "^16.0.0",
|
||||||
"@rollup/plugin-json": "^4.0.3",
|
"@rollup/plugin-json": "^4.0.3",
|
||||||
"@sveltech/routify": "1.7.11",
|
"@sveltech/routify": "1.7.11",
|
||||||
"@testing-library/jest-dom": "^5.11.0",
|
"@testing-library/jest-dom": "^5.11.0",
|
||||||
|
@ -104,7 +105,6 @@
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rollup": "^2.11.2",
|
"rollup": "^2.11.2",
|
||||||
"rollup-plugin-alias": "^1.5.2",
|
"rollup-plugin-alias": "^1.5.2",
|
||||||
"rollup-plugin-commonjs": "^10.0.0",
|
|
||||||
"rollup-plugin-copy": "^3.0.0",
|
"rollup-plugin-copy": "^3.0.0",
|
||||||
"rollup-plugin-css-only": "^2.1.0",
|
"rollup-plugin-css-only": "^2.1.0",
|
||||||
"rollup-plugin-livereload": "^1.0.0",
|
"rollup-plugin-livereload": "^1.0.0",
|
||||||
|
@ -115,7 +115,7 @@
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"rollup-plugin-url": "^2.2.2",
|
"rollup-plugin-url": "^2.2.2",
|
||||||
"start-server-and-test": "^1.11.0",
|
"start-server-and-test": "^1.11.0",
|
||||||
"svelte": "^3.29.0",
|
"svelte": "^3.30.0",
|
||||||
"svelte-jester": "^1.0.6"
|
"svelte-jester": "^1.0.6"
|
||||||
},
|
},
|
||||||
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import alias from "@rollup/plugin-alias"
|
import alias from "@rollup/plugin-alias"
|
||||||
import svelte from "rollup-plugin-svelte"
|
import svelte from "rollup-plugin-svelte"
|
||||||
import resolve from "rollup-plugin-node-resolve"
|
import resolve from "rollup-plugin-node-resolve"
|
||||||
import commonjs from "rollup-plugin-commonjs"
|
import commonjs from "@rollup/plugin-commonjs"
|
||||||
import url from "rollup-plugin-url"
|
import url from "rollup-plugin-url"
|
||||||
import livereload from "rollup-plugin-livereload"
|
import livereload from "rollup-plugin-livereload"
|
||||||
import { terser } from "rollup-plugin-terser"
|
import { terser } from "rollup-plugin-terser"
|
||||||
|
@ -15,102 +15,7 @@ import json from "@rollup/plugin-json"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
const production = !process.env.ROLLUP_WATCH
|
const production = !process.env.ROLLUP_WATCH
|
||||||
|
|
||||||
const lodash_fp_exports = [
|
|
||||||
"flow",
|
|
||||||
"pipe",
|
|
||||||
"union",
|
|
||||||
"reduce",
|
|
||||||
"isUndefined",
|
|
||||||
"cloneDeep",
|
|
||||||
"split",
|
|
||||||
"some",
|
|
||||||
"map",
|
|
||||||
"filter",
|
|
||||||
"isEmpty",
|
|
||||||
"countBy",
|
|
||||||
"includes",
|
|
||||||
"last",
|
|
||||||
"find",
|
|
||||||
"constant",
|
|
||||||
"take",
|
|
||||||
"first",
|
|
||||||
"intersection",
|
|
||||||
"mapValues",
|
|
||||||
"isNull",
|
|
||||||
"has",
|
|
||||||
"isInteger",
|
|
||||||
"isNumber",
|
|
||||||
"isString",
|
|
||||||
"isBoolean",
|
|
||||||
"isDate",
|
|
||||||
"isArray",
|
|
||||||
"isObject",
|
|
||||||
"clone",
|
|
||||||
"values",
|
|
||||||
"keyBy",
|
|
||||||
"isNaN",
|
|
||||||
"keys",
|
|
||||||
"orderBy",
|
|
||||||
"concat",
|
|
||||||
"reverse",
|
|
||||||
"difference",
|
|
||||||
"merge",
|
|
||||||
"flatten",
|
|
||||||
"each",
|
|
||||||
"pull",
|
|
||||||
"join",
|
|
||||||
"defaultCase",
|
|
||||||
"uniqBy",
|
|
||||||
"every",
|
|
||||||
"uniqWith",
|
|
||||||
"isFunction",
|
|
||||||
"groupBy",
|
|
||||||
"differenceBy",
|
|
||||||
"intersectionBy",
|
|
||||||
"isEqual",
|
|
||||||
"max",
|
|
||||||
"sortBy",
|
|
||||||
"assign",
|
|
||||||
"uniq",
|
|
||||||
"trimChars",
|
|
||||||
"trimCharsStart",
|
|
||||||
"isObjectLike",
|
|
||||||
"flattenDeep",
|
|
||||||
"indexOf",
|
|
||||||
"isPlainObject",
|
|
||||||
"toNumber",
|
|
||||||
"takeRight",
|
|
||||||
"toPairs",
|
|
||||||
"remove",
|
|
||||||
"findIndex",
|
|
||||||
"compose",
|
|
||||||
"get",
|
|
||||||
"tap",
|
|
||||||
]
|
|
||||||
|
|
||||||
const lodash_exports = [
|
|
||||||
"flow",
|
|
||||||
"join",
|
|
||||||
"replace",
|
|
||||||
"trim",
|
|
||||||
"dropRight",
|
|
||||||
"takeRight",
|
|
||||||
"head",
|
|
||||||
"reduce",
|
|
||||||
"tail",
|
|
||||||
"startsWith",
|
|
||||||
"findIndex",
|
|
||||||
"merge",
|
|
||||||
"assign",
|
|
||||||
"each",
|
|
||||||
"find",
|
|
||||||
"orderBy",
|
|
||||||
"union",
|
|
||||||
]
|
|
||||||
|
|
||||||
const outputpath = "../server/builder"
|
const outputpath = "../server/builder"
|
||||||
|
|
||||||
const coreExternal = [
|
const coreExternal = [
|
||||||
"lodash",
|
"lodash",
|
||||||
"lodash/fp",
|
"lodash/fp",
|
||||||
|
@ -224,13 +129,7 @@ export default {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
commonjs({
|
commonjs(),
|
||||||
namedExports: {
|
|
||||||
"lodash/fp": lodash_fp_exports,
|
|
||||||
lodash: lodash_exports,
|
|
||||||
shortid: ["generate"],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
url({
|
url({
|
||||||
limit: 0,
|
limit: 0,
|
||||||
include: ["**/*.woff2", "**/*.png"],
|
include: ["**/*.woff2", "**/*.png"],
|
||||||
|
|
|
@ -24,7 +24,7 @@ import { cloneDeep, difference } from "lodash/fp"
|
||||||
* @returns {Array.<BindableProperty>}
|
* @returns {Array.<BindableProperty>}
|
||||||
*/
|
*/
|
||||||
export default function({ componentInstanceId, screen, components, tables }) {
|
export default function({ componentInstanceId, screen, components, tables }) {
|
||||||
const walkResult = walk({
|
const result = walk({
|
||||||
// cloning so we are free to mutate props (e.g. by adding _contexts)
|
// cloning so we are free to mutate props (e.g. by adding _contexts)
|
||||||
instance: cloneDeep(screen.props),
|
instance: cloneDeep(screen.props),
|
||||||
targetId: componentInstanceId,
|
targetId: componentInstanceId,
|
||||||
|
@ -33,13 +33,10 @@ export default function({ componentInstanceId, screen, components, tables }) {
|
||||||
})
|
})
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...walkResult.bindableInstances
|
...result.bindableInstances
|
||||||
.filter(isInstanceInSharedContext(walkResult))
|
.filter(isInstanceInSharedContext(result))
|
||||||
.map(componentInstanceToBindable(walkResult)),
|
.map(componentInstanceToBindable),
|
||||||
|
...(result.target?._contexts.map(contextToBindables(tables)).flat() ?? []),
|
||||||
...(walkResult.target?._contexts
|
|
||||||
.map(contextToBindables(tables, walkResult))
|
|
||||||
.flat() ?? []),
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,26 +50,18 @@ const isInstanceInSharedContext = walkResult => i =>
|
||||||
|
|
||||||
// turns a component instance prop into binding expressions
|
// turns a component instance prop into binding expressions
|
||||||
// used by the UI
|
// used by the UI
|
||||||
const componentInstanceToBindable = walkResult => i => {
|
const componentInstanceToBindable = i => {
|
||||||
const lastContext =
|
|
||||||
i.instance._contexts.length &&
|
|
||||||
i.instance._contexts[i.instance._contexts.length - 1]
|
|
||||||
const contextParentPath = lastContext
|
|
||||||
? getParentPath(walkResult, lastContext)
|
|
||||||
: ""
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "instance",
|
type: "instance",
|
||||||
instance: i.instance,
|
instance: i.instance,
|
||||||
// how the binding expression persists, and is used in the app at runtime
|
// how the binding expression persists, and is used in the app at runtime
|
||||||
runtimeBinding: `${contextParentPath}${i.instance._id}.${i.prop}`,
|
runtimeBinding: `${i.instance._id}`,
|
||||||
// how the binding exressions looks to the user of the builder
|
// how the binding exressions looks to the user of the builder
|
||||||
readableBinding: `${i.instance._instanceName}`,
|
readableBinding: `${i.instance._instanceName}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const contextToBindables = (tables, walkResult) => context => {
|
const contextToBindables = tables => context => {
|
||||||
const contextParentPath = getParentPath(walkResult, context)
|
|
||||||
const tableId = context.table?.tableId ?? context.table
|
const tableId = context.table?.tableId ?? context.table
|
||||||
const table = tables.find(table => table._id === tableId)
|
const table = tables.find(table => table._id === tableId)
|
||||||
let schema =
|
let schema =
|
||||||
|
@ -98,7 +87,7 @@ const contextToBindables = (tables, walkResult) => context => {
|
||||||
fieldSchema,
|
fieldSchema,
|
||||||
instance: context.instance,
|
instance: context.instance,
|
||||||
// how the binding expression persists, and is used in the app at runtime
|
// how the binding expression persists, and is used in the app at runtime
|
||||||
runtimeBinding: `${contextParentPath}data.${runtimeBoundKey}`,
|
runtimeBinding: `${context.instance._id}.${runtimeBoundKey}`,
|
||||||
// how the binding expressions looks to the user of the builder
|
// how the binding expressions looks to the user of the builder
|
||||||
readableBinding: `${context.instance._instanceName}.${table.name}.${key}`,
|
readableBinding: `${context.instance._instanceName}.${table.name}.${key}`,
|
||||||
// table / view info
|
// table / view info
|
||||||
|
@ -118,20 +107,6 @@ const contextToBindables = (tables, walkResult) => context => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getParentPath = (walkResult, context) => {
|
|
||||||
// describes the number of "parent" in the path
|
|
||||||
// clone array first so original array is not mtated
|
|
||||||
const contextParentNumber = [...walkResult.target._contexts]
|
|
||||||
.reverse()
|
|
||||||
.indexOf(context)
|
|
||||||
|
|
||||||
return (
|
|
||||||
new Array(contextParentNumber).fill("parent").join(".") +
|
|
||||||
// trailing . if has parents
|
|
||||||
(contextParentNumber ? "." : "")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const walk = ({ instance, targetId, components, tables, result }) => {
|
const walk = ({ instance, targetId, components, tables, result }) => {
|
||||||
if (!result) {
|
if (!result) {
|
||||||
result = {
|
result = {
|
||||||
|
|
|
@ -12,10 +12,7 @@ export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
|
||||||
return boundValue === `{{ ${readableBinding} }}`
|
return boundValue === `{{ ${readableBinding} }}`
|
||||||
})
|
})
|
||||||
if (binding) {
|
if (binding) {
|
||||||
result = textWithBindings.replace(
|
result = result.replace(boundValue, `{{ ${binding.runtimeBinding} }}`)
|
||||||
boundValue,
|
|
||||||
`{{ ${binding.runtimeBinding} }}`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -481,7 +481,7 @@ export const getFrontendStore = () => {
|
||||||
// Try to extract a nav component from the master screen
|
// Try to extract a nav component from the master screen
|
||||||
const nav = findChildComponentType(
|
const nav = findChildComponentType(
|
||||||
state.pages.main,
|
state.pages.main,
|
||||||
"@budibase/standard-components/Navigation"
|
"@budibase/standard-components/navigation"
|
||||||
)
|
)
|
||||||
if (nav) {
|
if (nav) {
|
||||||
let newLink
|
let newLink
|
||||||
|
|
|
@ -15,8 +15,6 @@ export class Component extends BaseStructure {
|
||||||
selected: {},
|
selected: {},
|
||||||
},
|
},
|
||||||
_code: "",
|
_code: "",
|
||||||
className: "",
|
|
||||||
onLoad: [],
|
|
||||||
type: "",
|
type: "",
|
||||||
_instanceName: "",
|
_instanceName: "",
|
||||||
_children: [],
|
_children: [],
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
<script>
|
<script>
|
||||||
import { Input, Select, Label, DatePicker, Toggle } from "@budibase/bbui"
|
import { Input, Select, Label, DatePicker, Toggle, RichText } from "@budibase/bbui"
|
||||||
|
import { backendUiStore } from "builderStore"
|
||||||
|
import { TableNames } from "constants"
|
||||||
import Dropzone from "components/common/Dropzone.svelte"
|
import Dropzone from "components/common/Dropzone.svelte"
|
||||||
import { capitalise } from "../../../helpers"
|
import { capitalise } from "../../../helpers"
|
||||||
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
||||||
|
|
||||||
export let meta
|
export let meta
|
||||||
|
export let creating
|
||||||
export let value = meta.type === "boolean" ? false : ""
|
export let value = meta.type === "boolean" ? false : ""
|
||||||
|
|
||||||
$: type = meta.type
|
$: type = meta.type
|
||||||
$: label = capitalise(meta.name)
|
$: label = capitalise(meta.name)
|
||||||
|
$: editingUser =
|
||||||
|
!creating && $backendUiStore.selectedTable?._id === TableNames.USERS
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if type === 'options'}
|
{#if type === 'options'}
|
||||||
|
@ -29,6 +34,17 @@
|
||||||
<Toggle text={label} bind:checked={value} data-cy="{meta.name}-input" />
|
<Toggle text={label} bind:checked={value} data-cy="{meta.name}-input" />
|
||||||
{:else if type === 'link'}
|
{:else if type === 'link'}
|
||||||
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
||||||
|
{:else if type === 'longform'}
|
||||||
|
<div>
|
||||||
|
<Label extraSmall grey>{label}</Label>
|
||||||
|
<RichText bind:value />
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Input thin {label} data-cy="{meta.name}-input" {type} bind:value />
|
<Input
|
||||||
|
thin
|
||||||
|
{label}
|
||||||
|
data-cy="{meta.name}-input"
|
||||||
|
{type}
|
||||||
|
bind:value
|
||||||
|
disabled={editingUser} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -7,8 +7,8 @@ export async function createUser(user) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveRow(row, tableId) {
|
export async function saveRow(row, tableId) {
|
||||||
const SAVE_ROWS_URL = `/api/${tableId}/rows`
|
const SAVE_ROW_URL = `/api/${tableId}/rows`
|
||||||
const response = await api.post(SAVE_ROWS_URL, row)
|
const response = await api.post(SAVE_ROW_URL, row)
|
||||||
|
|
||||||
return await response.json()
|
return await response.json()
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { Input, Button, TextButton, Select, Toggle } from "@budibase/bbui"
|
import { Input, Button, TextButton, Select, Toggle } from "@budibase/bbui"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
|
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||||
import { FIELDS } from "constants/backend"
|
import { FIELDS } from "constants/backend"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import ValuesList from "components/common/ValuesList.svelte"
|
import ValuesList from "components/common/ValuesList.svelte"
|
||||||
|
@ -30,6 +31,9 @@
|
||||||
table => table._id !== $backendUiStore.draftTable._id
|
table => table._id !== $backendUiStore.draftTable._id
|
||||||
)
|
)
|
||||||
$: required = !!field?.constraints?.presence || primaryDisplay
|
$: required = !!field?.constraints?.presence || primaryDisplay
|
||||||
|
$: uneditable =
|
||||||
|
$backendUiStore.selectedTable?._id === TableNames.USERS &&
|
||||||
|
UNEDITABLE_USER_FIELDS.includes(field.name)
|
||||||
|
|
||||||
async function saveColumn() {
|
async function saveColumn() {
|
||||||
backendUiStore.update(state => {
|
backendUiStore.update(state => {
|
||||||
|
@ -87,7 +91,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="actions" class:hidden={deletion}>
|
<div class="actions" class:hidden={deletion}>
|
||||||
<Input label="Name" thin bind:value={field.name} />
|
<Input label="Name" thin bind:value={field.name} disabled={uneditable} />
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
disabled={originalName}
|
disabled={originalName}
|
||||||
|
@ -101,7 +105,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
{#if field.type !== 'link'}
|
{#if field.type !== 'link' && !uneditable}
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={required}
|
checked={required}
|
||||||
on:change={onChangeRequired}
|
on:change={onChangeRequired}
|
||||||
|
@ -157,7 +161,7 @@
|
||||||
bind:value={field.fieldName} />
|
bind:value={field.fieldName} />
|
||||||
{/if}
|
{/if}
|
||||||
<footer class="create-column-options">
|
<footer class="create-column-options">
|
||||||
{#if originalName}
|
{#if !uneditable && originalName}
|
||||||
<TextButton text on:click={confirmDelete}>Delete Column</TextButton>
|
<TextButton text on:click={confirmDelete}>Delete Column</TextButton>
|
||||||
{/if}
|
{/if}
|
||||||
<Button secondary on:click={onClosed}>Cancel</Button>
|
<Button secondary on:click={onClosed}>Cancel</Button>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
|
import { TableNames } from "constants"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import RowFieldControl from "../RowFieldControl.svelte"
|
import RowFieldControl from "../RowFieldControl.svelte"
|
||||||
import * as api from "../api"
|
import * as api from "../api"
|
||||||
|
@ -21,9 +22,10 @@
|
||||||
{ ...row, tableId: table._id },
|
{ ...row, tableId: table._id },
|
||||||
table._id
|
table._id
|
||||||
)
|
)
|
||||||
|
|
||||||
if (rowResponse.errors) {
|
if (rowResponse.errors) {
|
||||||
errors = Object.keys(rowResponse.errors)
|
errors = Object.entries(rowResponse.errors)
|
||||||
.map(k => ({ dataPath: k, message: rowResponse.errors[k] }))
|
.map(([key, error]) => ({ dataPath: key, message: error }))
|
||||||
.flat()
|
.flat()
|
||||||
// Prevent modal closing if there were errors
|
// Prevent modal closing if there were errors
|
||||||
return false
|
return false
|
||||||
|
@ -38,9 +40,15 @@
|
||||||
confirmText={creating ? 'Create Row' : 'Save Row'}
|
confirmText={creating ? 'Create Row' : 'Save Row'}
|
||||||
onConfirm={saveRow}>
|
onConfirm={saveRow}>
|
||||||
<ErrorsBox {errors} />
|
<ErrorsBox {errors} />
|
||||||
|
{#if creating && table._id === TableNames.USERS}
|
||||||
|
<RowFieldControl
|
||||||
|
{creating}
|
||||||
|
meta={{ name: 'password', type: 'password' }}
|
||||||
|
bind:value={row.password} />
|
||||||
|
{/if}
|
||||||
{#each tableSchema as [key, meta]}
|
{#each tableSchema as [key, meta]}
|
||||||
<div>
|
<div>
|
||||||
<RowFieldControl {meta} bind:value={row[key]} />
|
<RowFieldControl {meta} bind:value={row[key]} {creating} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@sveltech/routify"
|
import { goto } from "@sveltech/routify"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
|
import { TableNames } from "constants"
|
||||||
import ListItem from "./ListItem.svelte"
|
import ListItem from "./ListItem.svelte"
|
||||||
import CreateTableModal from "./modals/CreateTableModal.svelte"
|
import CreateTableModal from "./modals/CreateTableModal.svelte"
|
||||||
import EditTablePopover from "./popovers/EditTablePopover.svelte"
|
import EditTablePopover from "./popovers/EditTablePopover.svelte"
|
||||||
|
@ -42,7 +43,7 @@
|
||||||
{#each $backendUiStore.tables as table, idx}
|
{#each $backendUiStore.tables as table, idx}
|
||||||
<NavItem
|
<NavItem
|
||||||
border={idx > 0}
|
border={idx > 0}
|
||||||
icon="ri-table-line"
|
icon={`ri-${table._id === TableNames.USERS ? 'user' : 'table'}-line`}
|
||||||
text={table.name}
|
text={table.name}
|
||||||
selected={selectedView === `all_${table._id}`}
|
selected={selectedView === `all_${table._id}`}
|
||||||
on:click={() => selectTable(table)}>
|
on:click={() => selectTable(table)}>
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
|
|
||||||
async function deleteTable() {
|
async function deleteTable() {
|
||||||
await backendUiStore.actions.tables.delete(table)
|
await backendUiStore.actions.tables.delete(table)
|
||||||
store.store.actions.screens.delete(templateScreens)
|
store.actions.screens.delete(templateScreens)
|
||||||
await backendUiStore.actions.tables.fetch()
|
await backendUiStore.actions.tables.fetch()
|
||||||
notifier.success("Table deleted")
|
notifier.success("Table deleted")
|
||||||
hideEditor()
|
hideEditor()
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { General, Users, DangerZone, APIKeys } from "./tabs"
|
import { General, DangerZone, APIKeys } from "./tabs"
|
||||||
import { Switcher, ModalContent } from "@budibase/bbui"
|
import { Switcher, ModalContent } from "@budibase/bbui"
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
|
@ -8,11 +8,6 @@
|
||||||
key: "GENERAL",
|
key: "GENERAL",
|
||||||
component: General,
|
component: General,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "Users",
|
|
||||||
key: "USERS",
|
|
||||||
component: Users,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "API Keys",
|
title: "API Keys",
|
||||||
key: "API_KEYS",
|
key: "API_KEYS",
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
<script>
|
|
||||||
import { createEventDispatcher } from "svelte"
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
import { Input, Select, Button } from "@budibase/bbui"
|
|
||||||
export let user
|
|
||||||
|
|
||||||
let editMode = false
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="inputs">
|
|
||||||
<Input
|
|
||||||
disabled
|
|
||||||
thin
|
|
||||||
bind:value={user.username}
|
|
||||||
name="Name"
|
|
||||||
placeholder="Username" />
|
|
||||||
<Select disabled={!editMode} bind:value={user.accessLevelId} thin secondary>
|
|
||||||
<option value="">Choose an option</option>
|
|
||||||
<option value="ADMIN">Admin</option>
|
|
||||||
<option value="POWER_USER">Power User</option>
|
|
||||||
</Select>
|
|
||||||
{#if editMode}
|
|
||||||
<Button
|
|
||||||
blue
|
|
||||||
on:click={() => {
|
|
||||||
dispatch('save', user)
|
|
||||||
editMode = false
|
|
||||||
}}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
{:else}
|
|
||||||
<Button secondary on:click={() => (editMode = true)}>Edit</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.inputs {
|
|
||||||
display: grid;
|
|
||||||
justify-items: stretch;
|
|
||||||
grid-gap: var(--spacing-m);
|
|
||||||
grid-template-columns: 1fr 1fr 140px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,114 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Input, Select, Button, Label } from "@budibase/bbui"
|
|
||||||
import UserRow from "../UserRow.svelte"
|
|
||||||
|
|
||||||
import { store, backendUiStore } from "builderStore"
|
|
||||||
import api from "builderStore/api"
|
|
||||||
// import * as api from "../api"
|
|
||||||
|
|
||||||
let username = ""
|
|
||||||
let password = ""
|
|
||||||
let accessLevelId = "ADMIN"
|
|
||||||
|
|
||||||
$: valid = username && password && accessLevelId
|
|
||||||
$: appId = $store.appId
|
|
||||||
|
|
||||||
// Create user!
|
|
||||||
async function createUser() {
|
|
||||||
if (valid) {
|
|
||||||
const user = { name: username, username, password, accessLevelId }
|
|
||||||
const response = await api.post(`/api/users`, user)
|
|
||||||
const json = await response.json()
|
|
||||||
backendUiStore.actions.users.create(json)
|
|
||||||
fetchUsersPromise = fetchUsers()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update user!
|
|
||||||
async function updateUser(event) {
|
|
||||||
let data = event.detail
|
|
||||||
delete data.password
|
|
||||||
const response = await api.put(`/api/users`, data)
|
|
||||||
const users = await response.json()
|
|
||||||
backendUiStore.update(state => {
|
|
||||||
state.users = users
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
fetchUsersPromise = fetchUsers()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get users
|
|
||||||
async function fetchUsers() {
|
|
||||||
const response = await api.get(`/api/users`)
|
|
||||||
const users = await response.json()
|
|
||||||
backendUiStore.update(state => {
|
|
||||||
state.users = users
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
return users
|
|
||||||
}
|
|
||||||
|
|
||||||
let fetchUsersPromise = fetchUsers()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<div>
|
|
||||||
<Label extraSmall grey>Create New User</Label>
|
|
||||||
<div class="inputs">
|
|
||||||
<Input thin bind:value={username} name="Name" placeholder="Username" />
|
|
||||||
<Input
|
|
||||||
thin
|
|
||||||
type="password"
|
|
||||||
bind:value={password}
|
|
||||||
name="Password"
|
|
||||||
placeholder="Password" />
|
|
||||||
<Select secondary bind:value={accessLevelId} thin>
|
|
||||||
<option value="">Choose an option</option>
|
|
||||||
<option value="ADMIN">Admin</option>
|
|
||||||
<option value="POWER_USER">Power User</option>
|
|
||||||
</Select>
|
|
||||||
<Button on:click={createUser} primary>Create</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label extraSmall grey>Current Users</Label>
|
|
||||||
{#await fetchUsersPromise}
|
|
||||||
Loading...
|
|
||||||
{:then users}
|
|
||||||
<ul>
|
|
||||||
{#each users as user}
|
|
||||||
<li>
|
|
||||||
<UserRow {user} on:save={updateUser} />
|
|
||||||
</li>
|
|
||||||
{:else}
|
|
||||||
<li>No Users found</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{:catch err}
|
|
||||||
Something went wrong when trying to fetch users. Please refresh (CMD + R /
|
|
||||||
CTRL + R) the page and try again.
|
|
||||||
{/await}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
display: grid;
|
|
||||||
grid-gap: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.inputs {
|
|
||||||
display: grid;
|
|
||||||
justify-items: stretch;
|
|
||||||
grid-gap: var(--spacing-m);
|
|
||||||
grid-template-columns: 1fr 1fr 1fr 140px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
display: grid;
|
|
||||||
grid-gap: var(--spacing-m);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,6 +1,5 @@
|
||||||
export { default as General } from "./General.svelte"
|
export { default as General } from "./General.svelte"
|
||||||
export { default as Integrations } from "./Integrations.svelte"
|
export { default as Integrations } from "./Integrations.svelte"
|
||||||
export { default as Permissions } from "./Permissions.svelte"
|
export { default as Permissions } from "./Permissions.svelte"
|
||||||
export { default as Users } from "./Users.svelte"
|
|
||||||
export { default as APIKeys } from "./APIKeys.svelte"
|
export { default as APIKeys } from "./APIKeys.svelte"
|
||||||
export { default as DangerZone } from "./DangerZone.svelte"
|
export { default as DangerZone } from "./DangerZone.svelte"
|
||||||
|
|
|
@ -1,140 +1,63 @@
|
||||||
<script>
|
<script>
|
||||||
import { store, backendUiStore } from "builderStore"
|
import { onMount } from "svelte"
|
||||||
import { map, join } from "lodash/fp"
|
import { store } from "builderStore"
|
||||||
import iframeTemplate from "./iframeTemplate"
|
import iframeTemplate from "./iframeTemplate"
|
||||||
import { pipe } from "../../../helpers"
|
import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
|
||||||
import { Screen } from "../../../builderStore/store/screenTemplates/utils/Screen"
|
|
||||||
import { Component } from "../../../builderStore/store/screenTemplates/utils/Component"
|
|
||||||
|
|
||||||
let iframe
|
let iframe
|
||||||
let styles = ""
|
|
||||||
|
|
||||||
function transform_component(comp) {
|
// Create screen slot placeholder for use when a page is selected rather
|
||||||
const props = comp.props || comp
|
// than a screen
|
||||||
if (props && props._children && props._children.length) {
|
|
||||||
props._children = props._children.map(transform_component)
|
|
||||||
}
|
|
||||||
|
|
||||||
return props
|
|
||||||
}
|
|
||||||
|
|
||||||
const getComponentTypeName = component => {
|
|
||||||
let [componentName] = component._component.match(/[a-z]*$/)
|
|
||||||
return componentName || "element"
|
|
||||||
}
|
|
||||||
|
|
||||||
const headingStyle = {
|
|
||||||
width: "500px",
|
|
||||||
padding: "8px",
|
|
||||||
}
|
|
||||||
const textStyle = {
|
|
||||||
...headingStyle,
|
|
||||||
"max-width": "",
|
|
||||||
"text-align": "left",
|
|
||||||
}
|
|
||||||
|
|
||||||
const heading = new Component("@budibase/standard-components/heading")
|
|
||||||
.normalStyle(headingStyle)
|
|
||||||
.type("h1")
|
|
||||||
.text("Screen Slot")
|
|
||||||
.instanceName("Heading")
|
|
||||||
const textScreenDisplay = new Component("@budibase/standard-components/text")
|
|
||||||
.normalStyle(textStyle)
|
|
||||||
.instanceName("Text")
|
|
||||||
.type("none")
|
|
||||||
.text(
|
|
||||||
"The screens that you create will be displayed inside this box. This box is just a placeholder, to show you the position of screens."
|
|
||||||
)
|
|
||||||
const container = new Component("@budibase/standard-components/container")
|
|
||||||
.normalStyle({
|
|
||||||
display: "flex",
|
|
||||||
"flex-direction": "column",
|
|
||||||
"align-items": "center",
|
|
||||||
flex: "1 1 auto",
|
|
||||||
})
|
|
||||||
.type("div")
|
|
||||||
.instanceName("Container")
|
|
||||||
.addChild(heading)
|
|
||||||
.addChild(textScreenDisplay)
|
|
||||||
const screenPlaceholder = new Screen()
|
const screenPlaceholder = new Screen()
|
||||||
.name("Screen Placeholder")
|
.name("Screen Placeholder")
|
||||||
.route("*")
|
.route("*")
|
||||||
.component("@budibase/standard-components/container")
|
.component("@budibase/standard-components/screenslotplaceholder")
|
||||||
.mainType("div")
|
|
||||||
.instanceName("Content Placeholder")
|
.instanceName("Content Placeholder")
|
||||||
.normalStyle({
|
|
||||||
flex: "1 1 auto",
|
|
||||||
})
|
|
||||||
.addChild(container)
|
|
||||||
.json()
|
.json()
|
||||||
// TODO: this ID is attached to how the screen slot is rendered, confusing, would be better a type etc
|
|
||||||
screenPlaceholder.props._id = "screenslot-placeholder"
|
|
||||||
|
|
||||||
$: hasComponent = !!$store.currentPreviewItem
|
// Extract data to pass to the iframe
|
||||||
|
$: page = $store.pages[$store.currentPageName]
|
||||||
|
$: screen =
|
||||||
|
$store.currentFrontEndType === "page"
|
||||||
|
? screenPlaceholder
|
||||||
|
: $store.currentPreviewItem
|
||||||
|
$: selectedComponentId = $store.currentComponentInfo?._id ?? ""
|
||||||
|
|
||||||
$: {
|
// Saving pages and screens to the DB causes them to have _revs.
|
||||||
styles = ""
|
// These revisions change every time a save happens and causes
|
||||||
// Apply the CSS from the currently selected page and its screens
|
// these reactive statements to fire, even though the actual
|
||||||
const currentPage = $store.pages[$store.currentPageName]
|
// definition hasn't changed.
|
||||||
styles += currentPage._css
|
// By deleting all _rev properties we can avoid this and increase
|
||||||
for (let screen of currentPage._screens) {
|
// performance.
|
||||||
styles += screen._css
|
$: json = JSON.stringify({ page, screen, selectedComponentId })
|
||||||
|
$: strippedJson = json.replaceAll(/"_rev":\s*"[^"]+"/g, `"_rev":""`)
|
||||||
|
|
||||||
|
// Update the iframe with the builder info to render the correct preview
|
||||||
|
const refreshContent = message => {
|
||||||
|
if (iframe) {
|
||||||
|
iframe.contentWindow.postMessage(message)
|
||||||
}
|
}
|
||||||
styles = styles
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$: stylesheetLinks = pipe($store.pages.stylesheets, [
|
// Refresh the preview when required
|
||||||
map(s => `<link rel="stylesheet" href="${s}"/>`),
|
$: refreshContent(strippedJson)
|
||||||
join("\n"),
|
|
||||||
])
|
|
||||||
|
|
||||||
$: screensExist =
|
// Initialise the app when mounted
|
||||||
$store.currentPreviewItem._screens &&
|
onMount(() => {
|
||||||
$store.currentPreviewItem._screens.length > 0
|
iframe.contentWindow.addEventListener(
|
||||||
|
"bb-ready",
|
||||||
$: frontendDefinition = {
|
() => {
|
||||||
appId: $store.appId,
|
refreshContent(strippedJson)
|
||||||
libraries: $store.libraries,
|
},
|
||||||
page: $store.pages[$store.currentPageName],
|
{
|
||||||
screens: [
|
once: true,
|
||||||
$store.currentFrontEndType === "page"
|
}
|
||||||
? screenPlaceholder
|
|
||||||
: $store.currentPreviewItem,
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
$: selectedComponentType = getComponentTypeName($store.currentComponentInfo)
|
|
||||||
|
|
||||||
$: selectedComponentId = $store.currentComponentInfo
|
|
||||||
? $store.currentComponentInfo._id
|
|
||||||
: ""
|
|
||||||
|
|
||||||
const refreshContent = () => {
|
|
||||||
iframe.contentWindow.postMessage(
|
|
||||||
JSON.stringify({
|
|
||||||
styles,
|
|
||||||
stylesheetLinks,
|
|
||||||
selectedComponentType,
|
|
||||||
selectedComponentId,
|
|
||||||
frontendDefinition,
|
|
||||||
appId: $store.appId,
|
|
||||||
instanceId: $backendUiStore.selectedDatabase._id,
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
$: if (iframe)
|
|
||||||
iframe.contentWindow.addEventListener("bb-ready", refreshContent, {
|
|
||||||
once: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
$: if (iframe && frontendDefinition) {
|
|
||||||
refreshContent()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="component-container">
|
<div class="component-container">
|
||||||
{#if hasComponent && $store.currentPreviewItem}
|
{#if $store.currentPreviewItem}
|
||||||
<iframe
|
<iframe
|
||||||
style="height: 100%; width: 100%"
|
style="height: 100%; width: 100%"
|
||||||
title="componentPreview"
|
title="componentPreview"
|
||||||
|
@ -152,7 +75,6 @@
|
||||||
margin: auto;
|
margin: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-container iframe {
|
.component-container iframe {
|
||||||
border: 0;
|
border: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
|
@ -4,72 +4,50 @@ export default `<html>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
|
||||||
<style>
|
<style>
|
||||||
body, html {
|
body, html {
|
||||||
height: 100%!important;
|
height: 100% !important;
|
||||||
font-family: Inter !important;
|
font-family: Inter !important;
|
||||||
margin: 0px!important;
|
margin: 0px !important;
|
||||||
}
|
}
|
||||||
*, *:before, *:after {
|
*, *:before, *:after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.container-screenslot-placeholder {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
border-style: dashed !important;
|
|
||||||
border-width: 1px;
|
|
||||||
color: #000000;
|
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
.container-screenslot-placeholder span {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
<script src='/assets/budibase-client.js'></script>
|
<script src='/assets/budibase-client.js'></script>
|
||||||
<script>
|
<script>
|
||||||
function receiveMessage(event) {
|
function receiveMessage(event) {
|
||||||
|
if (!event.data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!event.data) return
|
// Extract data from message
|
||||||
|
const { selectedComponentId, page, screen } = JSON.parse(event.data)
|
||||||
|
|
||||||
const data = JSON.parse(event.data)
|
// Set some flags so the app knows we're in the builder
|
||||||
|
window["##BUDIBASE_IN_BUILDER##"] = true
|
||||||
try {
|
window["##BUDIBASE_PREVIEW_PAGE##"] = page
|
||||||
if (styles) document.head.removeChild(styles)
|
window["##BUDIBASE_PREVIEW_SCREEN##"] = screen
|
||||||
} catch(_) { }
|
window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId
|
||||||
|
window["##BUDIBASE_PREVIEW_ID##"] = Math.random()
|
||||||
try {
|
|
||||||
if (selectedComponentStyle) document.head.removeChild(selectedComponentStyle)
|
// Initialise app
|
||||||
} catch(_) { }
|
|
||||||
|
|
||||||
selectedComponentStyle = document.createElement('style');
|
|
||||||
document.head.appendChild(selectedComponentStyle)
|
|
||||||
var selectedCss = '.' + data.selectedComponentType + '-' + data.selectedComponentId + '{ border: 2px solid #0055ff; }'
|
|
||||||
selectedComponentStyle.appendChild(document.createTextNode(selectedCss))
|
|
||||||
|
|
||||||
styles = document.createElement('style')
|
|
||||||
document.head.appendChild(styles)
|
|
||||||
styles.appendChild(document.createTextNode(data.styles))
|
|
||||||
|
|
||||||
window["##BUDIBASE_FRONTEND_DEFINITION##"] = data.frontendDefinition;
|
|
||||||
if (window.loadBudibase) {
|
if (window.loadBudibase) {
|
||||||
loadBudibase({ window, localStorage })
|
loadBudibase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let styles
|
|
||||||
let selectedComponentStyle
|
let selectedComponentStyle
|
||||||
|
|
||||||
document.addEventListener("click", function(e) {
|
// Ignore clicks
|
||||||
e.preventDefault()
|
["click", "mousedown"].forEach(type => {
|
||||||
e.stopPropagation()
|
document.addEventListener(type, function(e) {
|
||||||
return false;
|
e.preventDefault()
|
||||||
}, true)
|
e.stopPropagation()
|
||||||
|
return false
|
||||||
|
}, true)
|
||||||
|
})
|
||||||
|
|
||||||
window.addEventListener('message', receiveMessage)
|
window.addEventListener("message", receiveMessage)
|
||||||
window.dispatchEvent(new Event('bb-ready'))
|
window.dispatchEvent(new Event("bb-ready"))
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -15,7 +15,8 @@
|
||||||
let anchor
|
let anchor
|
||||||
|
|
||||||
$: noChildrenAllowed =
|
$: noChildrenAllowed =
|
||||||
!component || !getComponentDefinition($store, component._component).children
|
!component ||
|
||||||
|
!getComponentDefinition($store, component._component)?.children
|
||||||
$: noPaste = !$store.componentToPaste
|
$: noPaste = !$store.componentToPaste
|
||||||
|
|
||||||
const lastPartOfName = c => (c ? last(c._component.split("/")) : "")
|
const lastPartOfName = c => (c ? last(c._component.split("/")) : "")
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { TextButton, Body, DropdownMenu, ModalContent } from "@budibase/bbui"
|
import { TextButton, Body, DropdownMenu, ModalContent } from "@budibase/bbui"
|
||||||
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
|
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
|
||||||
import { EVENT_TYPE_MEMBER_NAME } from "../../../../../client/src/state/eventHandlers"
|
|
||||||
import actionTypes from "./actions"
|
import actionTypes from "./actions"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
const eventTypeKey = "##eventHandlerType"
|
||||||
|
|
||||||
export let event
|
export let event
|
||||||
|
|
||||||
|
@ -18,8 +18,7 @@
|
||||||
$: actions = event || []
|
$: actions = event || []
|
||||||
$: selectedActionComponent =
|
$: selectedActionComponent =
|
||||||
selectedAction &&
|
selectedAction &&
|
||||||
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_MEMBER_NAME])
|
actionTypes.find(t => t.name === selectedAction[eventTypeKey]).component
|
||||||
.component
|
|
||||||
|
|
||||||
const updateEventHandler = (updatedHandler, index) => {
|
const updateEventHandler = (updatedHandler, index) => {
|
||||||
actions[index] = updatedHandler
|
actions[index] = updatedHandler
|
||||||
|
@ -33,7 +32,7 @@
|
||||||
const addAction = actionType => () => {
|
const addAction = actionType => () => {
|
||||||
const newAction = {
|
const newAction = {
|
||||||
parameters: {},
|
parameters: {},
|
||||||
[EVENT_TYPE_MEMBER_NAME]: actionType.name,
|
[eventTypeKey]: actionType.name,
|
||||||
}
|
}
|
||||||
actions.push(newAction)
|
actions.push(newAction)
|
||||||
selectedAction = newAction
|
selectedAction = newAction
|
||||||
|
@ -79,7 +78,7 @@
|
||||||
{#each actions as action, index}
|
{#each actions as action, index}
|
||||||
<div class="action-container">
|
<div class="action-container">
|
||||||
<div class="action-header" on:click={selectAction(action)}>
|
<div class="action-header" on:click={selectAction(action)}>
|
||||||
<Body small lh>{index + 1}. {action[EVENT_TYPE_MEMBER_NAME]}</Body>
|
<Body small lh>{index + 1}. {action[eventTypeKey]}</Body>
|
||||||
<div class="row-expander" class:rotate={action !== selectedAction}>
|
<div class="row-expander" class:rotate={action !== selectedAction}>
|
||||||
<ArrowDownIcon />
|
<ArrowDownIcon />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,127 +0,0 @@
|
||||||
<script>
|
|
||||||
import { keys, map, includes, filter } from "lodash/fp"
|
|
||||||
import EventEditorModal from "./EventEditorModal.svelte"
|
|
||||||
import { Modal } from "@budibase/bbui"
|
|
||||||
|
|
||||||
export const EVENT_TYPE = "event"
|
|
||||||
export let component
|
|
||||||
|
|
||||||
let events = []
|
|
||||||
let selectedEvent = null
|
|
||||||
let modal
|
|
||||||
|
|
||||||
$: {
|
|
||||||
events = Object.keys(component)
|
|
||||||
// TODO: use real events
|
|
||||||
.filter(propName => ["onChange", "onClick", "onLoad"].includes(propName))
|
|
||||||
.map(propName => ({
|
|
||||||
name: propName,
|
|
||||||
handlers: component[propName] || [],
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const openModal = event => {
|
|
||||||
selectedEvent = event
|
|
||||||
modal.show()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button class="newevent" on:click={() => openModal()}>
|
|
||||||
<i class="icon ri-add-circle-fill" />
|
|
||||||
Create New Event
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="root">
|
|
||||||
<form on:submit|preventDefault class="form-root">
|
|
||||||
{#each events as event, index}
|
|
||||||
{#if event.handlers.length > 0}
|
|
||||||
<div
|
|
||||||
class:selected={selectedEvent && selectedEvent.index === index}
|
|
||||||
class="handler-container budibase__nav-item"
|
|
||||||
on:click={() => openModal({ ...event, index })}>
|
|
||||||
<span class="event-name">{event.name}</span>
|
|
||||||
<span class="edit-text">EDIT</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Modal bind:this={modal} width="600px">
|
|
||||||
<EventEditorModal eventOptions={events} event={selectedEvent} />
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.root {
|
|
||||||
font-size: 10pt;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.newevent {
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid var(--grey-4);
|
|
||||||
border-radius: 3px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 16px;
|
|
||||||
margin: 0px 0px 12px 0px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background: var(--background);
|
|
||||||
color: var(--ink);
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 2ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.newevent:hover {
|
|
||||||
background: var(--grey-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
color: var(--ink);
|
|
||||||
font-size: 16px;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-root {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.handler-container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
border: 2px solid var(--grey-1);
|
|
||||||
height: 80px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-name {
|
|
||||||
margin-top: 5px;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 16px;
|
|
||||||
color: rgba(22, 48, 87, 0.6);
|
|
||||||
align-self: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-text {
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
font-weight: bold;
|
|
||||||
align-self: end;
|
|
||||||
justify-self: end;
|
|
||||||
font-size: 10px;
|
|
||||||
color: rgba(35, 65, 105, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected {
|
|
||||||
color: var(--blue);
|
|
||||||
background: var(--grey-1) !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,53 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Input, DataList, Select } from "@budibase/bbui"
|
|
||||||
import { automationStore, allScreens } from "builderStore"
|
|
||||||
|
|
||||||
export let parameter
|
|
||||||
|
|
||||||
let isOpen = false
|
|
||||||
|
|
||||||
const capitalize = s => {
|
|
||||||
if (typeof s !== "string") return ""
|
|
||||||
return s.charAt(0).toUpperCase() + s.slice(1)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="handler-option">
|
|
||||||
{#if parameter.name === 'automation'}<span>{parameter.name}</span>{/if}
|
|
||||||
{#if parameter.name === 'automation'}
|
|
||||||
<Select on:change bind:value={parameter.value}>
|
|
||||||
<option value="" />
|
|
||||||
{#each $automationStore.automations.filter(wf => wf.live) as automation}
|
|
||||||
<option value={automation._id}>{automation.name}</option>
|
|
||||||
{/each}
|
|
||||||
</Select>
|
|
||||||
{:else if parameter.name === 'url'}
|
|
||||||
<DataList on:change bind:value={parameter.value}>
|
|
||||||
<option value="" />
|
|
||||||
{#each $allScreens as screen}
|
|
||||||
<option value={screen.routing.route}>
|
|
||||||
{screen.props._instanceName}
|
|
||||||
</option>
|
|
||||||
{/each}
|
|
||||||
</DataList>
|
|
||||||
{:else}
|
|
||||||
<Input
|
|
||||||
name={parameter.name}
|
|
||||||
label={capitalize(parameter.name)}
|
|
||||||
on:change
|
|
||||||
value={parameter.value} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.handler-option {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 18px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,9 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { buildStyle } from "../../helpers.js"
|
|
||||||
export let value = ""
|
export let value = ""
|
||||||
export let text = ""
|
export let text = ""
|
||||||
export let icon = ""
|
export let icon = ""
|
||||||
export let onClick = value => {}
|
export let onClick = () => {}
|
||||||
export let selected = false
|
export let selected = false
|
||||||
|
|
||||||
$: useIcon = !!icon
|
$: useIcon = !!icon
|
||||||
|
|
|
@ -40,13 +40,16 @@
|
||||||
|
|
||||||
$: links = bindableProperties
|
$: links = bindableProperties
|
||||||
.filter(x => x.fieldSchema?.type === "link")
|
.filter(x => x.fieldSchema?.type === "link")
|
||||||
.map(property => ({
|
.map(property => {
|
||||||
label: property.readableBinding,
|
return {
|
||||||
fieldName: property.fieldSchema.name,
|
providerId: property.instance._id,
|
||||||
name: `all_${property.fieldSchema.tableId}`,
|
label: property.readableBinding,
|
||||||
tableId: property.fieldSchema.tableId,
|
fieldName: property.fieldSchema.name,
|
||||||
type: "link",
|
name: `all_${property.fieldSchema.tableId}`,
|
||||||
}))
|
tableId: property.fieldSchema.tableId,
|
||||||
|
type: "link",
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1217,7 +1217,7 @@ export default {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Nav Bar",
|
name: "Nav Bar",
|
||||||
_component: "@budibase/standard-components/Navigation",
|
_component: "@budibase/standard-components/navigation",
|
||||||
description:
|
description:
|
||||||
"A component for handling the navigation within your app.",
|
"A component for handling the navigation within your app.",
|
||||||
icon: "ri-navigation-line",
|
icon: "ri-navigation-line",
|
||||||
|
|
|
@ -9,6 +9,16 @@ export const FIELDS = {
|
||||||
presence: false,
|
presence: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
LONGFORM: {
|
||||||
|
name: "Long Form Text",
|
||||||
|
icon: "ri-file-text-line",
|
||||||
|
type: "longform",
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
length: {},
|
||||||
|
presence: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
OPTIONS: {
|
OPTIONS: {
|
||||||
name: "Options",
|
name: "Options",
|
||||||
icon: "ri-list-check-2",
|
icon: "ri-list-check-2",
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
export const TableNames = {
|
||||||
|
USERS: "ta_users",
|
||||||
|
}
|
||||||
|
|
||||||
|
// fields on the user table that cannot be edited
|
||||||
|
export const UNEDITABLE_USER_FIELDS = ["username", "password", "accessLevelId"]
|
||||||
|
|
||||||
export const DEFAULT_PAGES_OBJECT = {
|
export const DEFAULT_PAGES_OBJECT = {
|
||||||
main: {
|
main: {
|
||||||
props: {
|
props: {
|
||||||
|
|
|
@ -68,16 +68,11 @@
|
||||||
<div class="toprightnav">
|
<div class="toprightnav">
|
||||||
<ThemeEditor />
|
<ThemeEditor />
|
||||||
<FeedbackNavLink />
|
<FeedbackNavLink />
|
||||||
<div class="topnavitemright">
|
|
||||||
<a target="_blank" href="https://docs.budibase.com">
|
|
||||||
<i class="ri-question-line" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="topnavitemright">
|
<div class="topnavitemright">
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://github.com/Budibase/budibase/discussions">
|
href="https://github.com/Budibase/budibase/discussions">
|
||||||
<i class="ri-discuss-line" />
|
<i class="ri-question-line" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<SettingsLink />
|
<SettingsLink />
|
||||||
|
|
|
@ -11,7 +11,7 @@ describe("fetch bindable properties", () => {
|
||||||
)
|
)
|
||||||
expect(componentBinding).toBeDefined()
|
expect(componentBinding).toBeDefined()
|
||||||
expect(componentBinding.type).toBe("instance")
|
expect(componentBinding.type).toBe("instance")
|
||||||
expect(componentBinding.runtimeBinding).toBe("search-input-id.value")
|
expect(componentBinding.runtimeBinding).toBe("search-input-id")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should not return bindable components when not in their context", () => {
|
it("should not return bindable components when not in their context", () => {
|
||||||
|
@ -37,20 +37,22 @@ describe("fetch bindable properties", () => {
|
||||||
expect(contextBindings.length).toBe(4)
|
expect(contextBindings.length).toBe(4)
|
||||||
|
|
||||||
const namebinding = contextBindings.find(
|
const namebinding = contextBindings.find(
|
||||||
b => b.runtimeBinding === "data.name"
|
b => b.runtimeBinding === "list-id.name"
|
||||||
)
|
)
|
||||||
expect(namebinding).toBeDefined()
|
expect(namebinding).toBeDefined()
|
||||||
expect(namebinding.readableBinding).toBe("list-name.Test Table.name")
|
expect(namebinding.readableBinding).toBe("list-name.Test Table.name")
|
||||||
|
|
||||||
const descriptionbinding = contextBindings.find(
|
const descriptionbinding = contextBindings.find(
|
||||||
b => b.runtimeBinding === "data.description"
|
b => b.runtimeBinding === "list-id.description"
|
||||||
)
|
)
|
||||||
expect(descriptionbinding).toBeDefined()
|
expect(descriptionbinding).toBeDefined()
|
||||||
expect(descriptionbinding.readableBinding).toBe(
|
expect(descriptionbinding.readableBinding).toBe(
|
||||||
"list-name.Test Table.description"
|
"list-name.Test Table.description"
|
||||||
)
|
)
|
||||||
|
|
||||||
const idbinding = contextBindings.find(b => b.runtimeBinding === "data._id")
|
const idbinding = contextBindings.find(
|
||||||
|
b => b.runtimeBinding === "list-id._id"
|
||||||
|
)
|
||||||
expect(idbinding).toBeDefined()
|
expect(idbinding).toBeDefined()
|
||||||
expect(idbinding.readableBinding).toBe("list-name.Test Table._id")
|
expect(idbinding.readableBinding).toBe("list-name.Test Table._id")
|
||||||
})
|
})
|
||||||
|
@ -65,13 +67,13 @@ describe("fetch bindable properties", () => {
|
||||||
expect(contextBindings.length).toBe(8)
|
expect(contextBindings.length).toBe(8)
|
||||||
|
|
||||||
const namebinding_parent = contextBindings.find(
|
const namebinding_parent = contextBindings.find(
|
||||||
b => b.runtimeBinding === "parent.data.name"
|
b => b.runtimeBinding === "list-id.name"
|
||||||
)
|
)
|
||||||
expect(namebinding_parent).toBeDefined()
|
expect(namebinding_parent).toBeDefined()
|
||||||
expect(namebinding_parent.readableBinding).toBe("list-name.Test Table.name")
|
expect(namebinding_parent.readableBinding).toBe("list-name.Test Table.name")
|
||||||
|
|
||||||
const descriptionbinding_parent = contextBindings.find(
|
const descriptionbinding_parent = contextBindings.find(
|
||||||
b => b.runtimeBinding === "parent.data.description"
|
b => b.runtimeBinding === "list-id.description"
|
||||||
)
|
)
|
||||||
expect(descriptionbinding_parent).toBeDefined()
|
expect(descriptionbinding_parent).toBeDefined()
|
||||||
expect(descriptionbinding_parent.readableBinding).toBe(
|
expect(descriptionbinding_parent.readableBinding).toBe(
|
||||||
|
@ -79,7 +81,7 @@ describe("fetch bindable properties", () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const namebinding_own = contextBindings.find(
|
const namebinding_own = contextBindings.find(
|
||||||
b => b.runtimeBinding === "data.name"
|
b => b.runtimeBinding === "child-list-id.name"
|
||||||
)
|
)
|
||||||
expect(namebinding_own).toBeDefined()
|
expect(namebinding_own).toBeDefined()
|
||||||
expect(namebinding_own.readableBinding).toBe(
|
expect(namebinding_own.readableBinding).toBe(
|
||||||
|
@ -87,7 +89,7 @@ describe("fetch bindable properties", () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const descriptionbinding_own = contextBindings.find(
|
const descriptionbinding_own = contextBindings.find(
|
||||||
b => b.runtimeBinding === "data.description"
|
b => b.runtimeBinding === "child-list-id.description"
|
||||||
)
|
)
|
||||||
expect(descriptionbinding_own).toBeDefined()
|
expect(descriptionbinding_own).toBeDefined()
|
||||||
expect(descriptionbinding_own.readableBinding).toBe(
|
expect(descriptionbinding_own.readableBinding).toBe(
|
||||||
|
@ -104,7 +106,7 @@ describe("fetch bindable properties", () => {
|
||||||
r => r.instance._id === "list-item-input-id" && r.type === "instance"
|
r => r.instance._id === "list-item-input-id" && r.type === "instance"
|
||||||
)
|
)
|
||||||
expect(componentBinding).toBeDefined()
|
expect(componentBinding).toBeDefined()
|
||||||
expect(componentBinding.runtimeBinding).toBe("list-item-input-id.value")
|
expect(componentBinding.runtimeBinding).toBe("list-item-input-id")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should not return components from child context", () => {
|
it("should not return components from child context", () => {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,5 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
package-lock.json
|
package-lock.json
|
||||||
yarn.lock
|
|
||||||
release/
|
release/
|
||||||
dist/
|
dist/
|
|
@ -1,13 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
presets: ["@babel/preset-env"],
|
|
||||||
sourceMaps: "inline",
|
|
||||||
retainLines: true,
|
|
||||||
plugins: [
|
|
||||||
[
|
|
||||||
"@babel/plugin-transform-runtime",
|
|
||||||
{
|
|
||||||
regenerator: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
}
|
|
|
@ -6,56 +6,27 @@
|
||||||
"module": "dist/budibase-client.esm.mjs",
|
"module": "dist/budibase-client.esm.mjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rollup -c",
|
"build": "rollup -c",
|
||||||
"test": "jest",
|
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"jest": {
|
|
||||||
"globals": {
|
|
||||||
"GLOBALS": {
|
|
||||||
"client": "web"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"testURL": "http://test.com",
|
|
||||||
"moduleNameMapper": {
|
|
||||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/internals/mocks/fileMock.js",
|
|
||||||
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
|
|
||||||
},
|
|
||||||
"moduleFileExtensions": [
|
|
||||||
"js",
|
|
||||||
"svelte"
|
|
||||||
],
|
|
||||||
"moduleDirectories": [
|
|
||||||
"node_modules"
|
|
||||||
],
|
|
||||||
"transform": {
|
|
||||||
"^.+js$": "babel-jest",
|
|
||||||
"^.+.svelte$": "svelte-jester"
|
|
||||||
},
|
|
||||||
"transformIgnorePatterns": [
|
|
||||||
"/node_modules/(?!svelte).+\\.js$"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deep-equal": "^2.0.1",
|
"deep-equal": "^2.0.1",
|
||||||
"mustache": "^4.0.1",
|
"mustache": "^4.0.1",
|
||||||
"regexparam": "^1.3.0"
|
"regexparam": "^1.3.0",
|
||||||
|
"svelte-spa-router": "^3.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.5.5",
|
"@budibase/standard-components": "^0.3.8",
|
||||||
"@babel/plugin-transform-runtime": "^7.5.5",
|
"@rollup/plugin-commonjs": "^16.0.0",
|
||||||
"@babel/preset-env": "^7.5.5",
|
"@rollup/plugin-node-resolve": "^10.0.0",
|
||||||
"@babel/runtime": "^7.5.5",
|
|
||||||
"babel-jest": "^24.8.0",
|
|
||||||
"fs-extra": "^8.1.0",
|
"fs-extra": "^8.1.0",
|
||||||
"jest": "^24.8.0",
|
|
||||||
"jsdom": "^16.0.1",
|
"jsdom": "^16.0.1",
|
||||||
"rollup": "^1.12.0",
|
"rollup": "^2.33.2",
|
||||||
"rollup-plugin-commonjs": "^10.0.0",
|
|
||||||
"rollup-plugin-node-builtins": "^2.1.2",
|
"rollup-plugin-node-builtins": "^2.1.2",
|
||||||
"rollup-plugin-node-globals": "^1.4.0",
|
"rollup-plugin-node-globals": "^1.4.0",
|
||||||
|
"rollup-plugin-svelte": "^6.1.1",
|
||||||
"rollup-plugin-node-resolve": "^5.2.0",
|
"rollup-plugin-node-resolve": "^5.2.0",
|
||||||
"rollup-plugin-terser": "^4.0.4",
|
"rollup-plugin-terser": "^4.0.4",
|
||||||
"svelte": "^3.29.7",
|
"svelte": "^3.30.0",
|
||||||
"svelte-jester": "^1.0.6"
|
"svelte-jester": "^1.0.6"
|
||||||
},
|
},
|
||||||
"gitHead": "e4e053cb6ff9a0ddc7115b44ccaa24b8ec41fb9a"
|
"gitHead": "e4e053cb6ff9a0ddc7115b44ccaa24b8ec41fb9a"
|
||||||
|
|
|
@ -1,31 +1,30 @@
|
||||||
import resolve from "rollup-plugin-node-resolve"
|
import commonjs from "@rollup/plugin-commonjs"
|
||||||
import commonjs from "rollup-plugin-commonjs"
|
import resolve from "@rollup/plugin-node-resolve"
|
||||||
import builtins from "rollup-plugin-node-builtins"
|
import builtins from "rollup-plugin-node-builtins"
|
||||||
import nodeglobals from "rollup-plugin-node-globals"
|
import svelte from "rollup-plugin-svelte"
|
||||||
|
|
||||||
|
const production = !process.env.ROLLUP_WATCH
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: "src/index.js",
|
input: "src/index.js",
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
format: "iife",
|
|
||||||
name: "app",
|
|
||||||
file: `./dist/budibase-client.js`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
file: "dist/budibase-client.esm.mjs",
|
|
||||||
format: "esm",
|
format: "esm",
|
||||||
sourcemap: "inline",
|
file: `./dist/budibase-client.js`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
|
svelte({
|
||||||
|
dev: !production,
|
||||||
|
}),
|
||||||
resolve({
|
resolve({
|
||||||
preferBuiltins: true,
|
preferBuiltins: true,
|
||||||
browser: true,
|
browser: true,
|
||||||
|
dedupe: ["svelte", "svelte/internal"],
|
||||||
}),
|
}),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
builtins(),
|
builtins(),
|
||||||
nodeglobals(),
|
|
||||||
],
|
],
|
||||||
watch: {
|
watch: {
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { getAppId } from "../utils/getAppId"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API cache for cached request responses.
|
||||||
|
*/
|
||||||
|
let cache = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a fully formatted URL based on the SDK configuration.
|
||||||
|
*/
|
||||||
|
const makeFullURL = path => {
|
||||||
|
return `/${path}`.replace("//", "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for API errors.
|
||||||
|
*/
|
||||||
|
const handleError = error => {
|
||||||
|
return { error }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs an API call to the server.
|
||||||
|
* App ID header is always correctly set.
|
||||||
|
*/
|
||||||
|
const makeApiCall = async ({ method, url, body, json = true }) => {
|
||||||
|
try {
|
||||||
|
const requestBody = json ? JSON.stringify(body) : body
|
||||||
|
let headers = {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-budibase-app-id": getAppId(),
|
||||||
|
}
|
||||||
|
if (!window["##BUDIBASE_IN_BUILDER##"]) {
|
||||||
|
headers["x-budibase-type"] = "client"
|
||||||
|
}
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: requestBody,
|
||||||
|
credentials: "same-origin",
|
||||||
|
})
|
||||||
|
switch (response.status) {
|
||||||
|
case 200:
|
||||||
|
return response.json()
|
||||||
|
case 404:
|
||||||
|
return handleError(`${url}: Not Found`)
|
||||||
|
case 400:
|
||||||
|
return handleError(`${url}: Bad Request`)
|
||||||
|
case 403:
|
||||||
|
return handleError(`${url}: Forbidden`)
|
||||||
|
default:
|
||||||
|
if (response.status >= 200 && response.status < 400) {
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
return handleError(`${url} - ${response.statusText}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs an API call to the server and caches the response.
|
||||||
|
* Future invocation for this URL will return the cached result instead of
|
||||||
|
* hitting the server again.
|
||||||
|
*/
|
||||||
|
const makeCachedApiCall = async params => {
|
||||||
|
const identifier = params.url
|
||||||
|
if (!identifier) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (!cache[identifier]) {
|
||||||
|
cache[identifier] = makeApiCall(params)
|
||||||
|
cache[identifier] = await cache[identifier]
|
||||||
|
}
|
||||||
|
return await cache[identifier]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an API call function for a particular HTTP method.
|
||||||
|
*/
|
||||||
|
const requestApiCall = method => async params => {
|
||||||
|
const { url, cache = false } = params
|
||||||
|
const fullURL = makeFullURL(url)
|
||||||
|
const enrichedParams = { ...params, method, url: fullURL }
|
||||||
|
return await (cache ? makeCachedApiCall : makeApiCall)(enrichedParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
post: requestApiCall("POST"),
|
||||||
|
get: requestApiCall("GET"),
|
||||||
|
patch: requestApiCall("PATCH"),
|
||||||
|
del: requestApiCall("DELETE"),
|
||||||
|
error: handleError,
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import API from "./api"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches screen definition for an app.
|
||||||
|
*/
|
||||||
|
export const fetchAppDefinition = async appId => {
|
||||||
|
return await API.get({
|
||||||
|
url: `/api/applications/${appId}/definition`,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import API from "./api"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads an attachment to the server.
|
||||||
|
*/
|
||||||
|
export const uploadAttachment = async data => {
|
||||||
|
return await API.post({
|
||||||
|
url: "/api/attachments/upload",
|
||||||
|
body: data,
|
||||||
|
json: false,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import API from "./api"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a log in request.
|
||||||
|
*/
|
||||||
|
export const logIn = async ({ username, password }) => {
|
||||||
|
if (!username) {
|
||||||
|
return API.error("Please enter your username")
|
||||||
|
}
|
||||||
|
if (!password) {
|
||||||
|
return API.error("Please enter your password")
|
||||||
|
}
|
||||||
|
return await API.post({
|
||||||
|
url: "/api/authenticate",
|
||||||
|
body: { username, password },
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,28 +0,0 @@
|
||||||
import appStore from "../state/store"
|
|
||||||
|
|
||||||
export const USER_STATE_PATH = "_bbuser"
|
|
||||||
|
|
||||||
export const authenticate = api => async ({ username, password }) => {
|
|
||||||
if (!username) {
|
|
||||||
api.error("Authenticate: username not set")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!password) {
|
|
||||||
api.error("Authenticate: password not set")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await api.post({
|
|
||||||
url: "/api/authenticate",
|
|
||||||
body: { username, password },
|
|
||||||
})
|
|
||||||
|
|
||||||
// set user even if error - so it is defined at least
|
|
||||||
appStore.update(s => {
|
|
||||||
s[USER_STATE_PATH] = user
|
|
||||||
return s
|
|
||||||
})
|
|
||||||
|
|
||||||
localStorage.setItem("budibase:user", JSON.stringify(user))
|
|
||||||
}
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { fetchTableData } from "./tables"
|
||||||
|
import { fetchViewData } from "./views"
|
||||||
|
import { fetchRelationshipData } from "./relationships"
|
||||||
|
import { enrichRows } from "./rows"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all rows for a particular Budibase data source.
|
||||||
|
*/
|
||||||
|
export const fetchDatasource = async (datasource, dataContext) => {
|
||||||
|
if (!datasource || !datasource.type) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all rows in data source
|
||||||
|
const { type, tableId, fieldName } = datasource
|
||||||
|
let rows = []
|
||||||
|
if (type === "table") {
|
||||||
|
rows = await fetchTableData(tableId)
|
||||||
|
} else if (type === "view") {
|
||||||
|
rows = await fetchViewData(datasource)
|
||||||
|
} else if (type === "link") {
|
||||||
|
const row = dataContext[datasource.providerId]
|
||||||
|
rows = await fetchRelationshipData({
|
||||||
|
rowId: row?._id,
|
||||||
|
tableId: row?.tableId,
|
||||||
|
fieldName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrich rows
|
||||||
|
return await enrichRows(rows, tableId)
|
||||||
|
}
|
|
@ -1,120 +1,9 @@
|
||||||
import { authenticate } from "./authenticate"
|
export * from "./rows"
|
||||||
import { getAppId } from "../render/getAppId"
|
export * from "./auth"
|
||||||
|
export * from "./datasources"
|
||||||
export async function baseApiCall(method, url, body) {
|
export * from "./tables"
|
||||||
return await fetch(url, {
|
export * from "./attachments"
|
||||||
method: method,
|
export * from "./views"
|
||||||
headers: {
|
export * from "./relationships"
|
||||||
"Content-Type": "application/json",
|
export * from "./routes"
|
||||||
"x-budibase-app-id": getAppId(window.document.cookie),
|
export * from "./app"
|
||||||
"x-budibase-type": "client",
|
|
||||||
},
|
|
||||||
body: body && JSON.stringify(body),
|
|
||||||
credentials: "same-origin",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiCall = method => async ({ url, body }) => {
|
|
||||||
const response = await baseApiCall(method, url, body)
|
|
||||||
|
|
||||||
switch (response.status) {
|
|
||||||
case 200:
|
|
||||||
return response.json()
|
|
||||||
case 404:
|
|
||||||
return error(`${url} Not found`)
|
|
||||||
case 400:
|
|
||||||
return error(`${url} Bad Request`)
|
|
||||||
case 403:
|
|
||||||
return error(`${url} Forbidden`)
|
|
||||||
default:
|
|
||||||
if (response.status >= 200 && response.status < 400) {
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
return error(`${url} - ${response.statusText}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const post = apiCall("POST")
|
|
||||||
const get = apiCall("GET")
|
|
||||||
const patch = apiCall("PATCH")
|
|
||||||
const del = apiCall("DELETE")
|
|
||||||
|
|
||||||
const ERROR_MEMBER = "##error"
|
|
||||||
const error = message => {
|
|
||||||
// appStore.update(s => s["##error_message"], message)
|
|
||||||
return { [ERROR_MEMBER]: message }
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSuccess = obj => !obj || !obj[ERROR_MEMBER]
|
|
||||||
|
|
||||||
const apiOpts = {
|
|
||||||
isSuccess,
|
|
||||||
error,
|
|
||||||
post,
|
|
||||||
get,
|
|
||||||
patch,
|
|
||||||
delete: del,
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveRow = async (params, state) =>
|
|
||||||
await post({
|
|
||||||
url: `/api/${params.tableId}/rows`,
|
|
||||||
body: makeRowRequestBody(params, state),
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateRow = async (params, state) => {
|
|
||||||
const row = makeRowRequestBody(params, state)
|
|
||||||
row._id = params._id
|
|
||||||
await patch({
|
|
||||||
url: `/api/${params.tableId}/rows/${params._id}`,
|
|
||||||
body: row,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteRow = async params =>
|
|
||||||
await del({
|
|
||||||
url: `/api/${params.tableId}/rows/${params.rowId}/${params.revId}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
const makeRowRequestBody = (parameters, state) => {
|
|
||||||
// start with the row thats currently in context
|
|
||||||
const body = { ...(state.data || {}) }
|
|
||||||
|
|
||||||
// dont send the table
|
|
||||||
if (body._table) delete body._table
|
|
||||||
|
|
||||||
// then override with supplied parameters
|
|
||||||
if (parameters.fields) {
|
|
||||||
for (let fieldName of Object.keys(parameters.fields)) {
|
|
||||||
const field = parameters.fields[fieldName]
|
|
||||||
|
|
||||||
// ensure fields sent are of the correct type
|
|
||||||
if (field.type === "boolean") {
|
|
||||||
if (field.value === "true") body[fieldName] = true
|
|
||||||
if (field.value === "false") body[fieldName] = false
|
|
||||||
} else if (field.type === "number") {
|
|
||||||
const val = parseFloat(field.value)
|
|
||||||
if (!isNaN(val)) {
|
|
||||||
body[fieldName] = val
|
|
||||||
}
|
|
||||||
} else if (field.type === "datetime") {
|
|
||||||
const date = new Date(field.value)
|
|
||||||
if (!isNaN(date.getTime())) {
|
|
||||||
body[fieldName] = date.toISOString()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
body[fieldName] = field.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
authenticate: authenticate(apiOpts),
|
|
||||||
saveRow,
|
|
||||||
updateRow,
|
|
||||||
deleteRow,
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import API from "./api"
|
||||||
|
import { enrichRows } from "./rows"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches related rows for a certain field of a certain row.
|
||||||
|
*/
|
||||||
|
export const fetchRelationshipData = async ({ tableId, rowId, fieldName }) => {
|
||||||
|
if (!tableId || !rowId || !fieldName) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const response = await API.get({ url: `/api/${tableId}/${rowId}/enrich` })
|
||||||
|
const rows = response[fieldName] || []
|
||||||
|
return await enrichRows(rows, tableId)
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import API from "./api"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches available routes for the client app.
|
||||||
|
*/
|
||||||
|
export const fetchRoutes = async () => {
|
||||||
|
return await API.get({
|
||||||
|
url: `/api/routing/client`,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
import API from "./api"
|
||||||
|
import { fetchTableDefinition } from "./tables"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches data about a certain row in a table.
|
||||||
|
*/
|
||||||
|
export const fetchRow = async ({ tableId, rowId }) => {
|
||||||
|
const row = await API.get({
|
||||||
|
url: `/api/${tableId}/rows/${rowId}`,
|
||||||
|
})
|
||||||
|
return (await enrichRows([row], tableId))[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a row in a table.
|
||||||
|
*/
|
||||||
|
export const saveRow = async row => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/${row.tableId}/rows`,
|
||||||
|
body: row,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a row in a table.
|
||||||
|
*/
|
||||||
|
export const updateRow = async row => {
|
||||||
|
return await API.patch({
|
||||||
|
url: `/api/${row.tableId}/rows/${row._id}`,
|
||||||
|
body: row,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a row from a table.
|
||||||
|
*/
|
||||||
|
export const deleteRow = async ({ tableId, rowId, revId }) => {
|
||||||
|
return await API.del({
|
||||||
|
url: `/api/${tableId}/rows/${rowId}/${revId}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes many rows from a table.
|
||||||
|
*/
|
||||||
|
export const deleteRows = async ({ tableId, rows }) => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/${tableId}/rows`,
|
||||||
|
body: {
|
||||||
|
rows,
|
||||||
|
type: "delete",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enriches rows which contain certain field types so that they can
|
||||||
|
* be properly displayed.
|
||||||
|
*/
|
||||||
|
export const enrichRows = async (rows, tableId) => {
|
||||||
|
if (rows && rows.length && tableId) {
|
||||||
|
// Fetch table schema so we can check column types
|
||||||
|
const tableDefinition = await fetchTableDefinition(tableId)
|
||||||
|
const schema = tableDefinition && tableDefinition.schema
|
||||||
|
if (schema) {
|
||||||
|
const keys = Object.keys(schema)
|
||||||
|
rows.forEach(row => {
|
||||||
|
for (let key of keys) {
|
||||||
|
const type = schema[key].type
|
||||||
|
if (type === "link") {
|
||||||
|
// Enrich row with the count of any relationship fields
|
||||||
|
row[`${key}_count`] = Array.isArray(row[key]) ? row[key].length : 0
|
||||||
|
} else if (type === "attachment") {
|
||||||
|
// Enrich row with the first image URL for any attachment fields
|
||||||
|
let url = null
|
||||||
|
if (Array.isArray(row[key]) && row[key][0] != null) {
|
||||||
|
url = row[key][0].url
|
||||||
|
}
|
||||||
|
row[`${key}_first`] = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import API from "./api"
|
||||||
|
import { enrichRows } from "./rows"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a table definition.
|
||||||
|
* Since definitions cannot change at runtime, the result is cached.
|
||||||
|
*/
|
||||||
|
export const fetchTableDefinition = async tableId => {
|
||||||
|
return await API.get({ url: `/api/tables/${tableId}`, cache: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all rows from a table.
|
||||||
|
*/
|
||||||
|
export const fetchTableData = async tableId => {
|
||||||
|
const rows = await API.get({ url: `/api/${tableId}/rows` })
|
||||||
|
return await enrichRows(rows, tableId)
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import API from "./api"
|
||||||
|
import { enrichRows } from "./rows"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all rows in a view.
|
||||||
|
*/
|
||||||
|
export const fetchViewData = async ({
|
||||||
|
name,
|
||||||
|
field,
|
||||||
|
groupBy,
|
||||||
|
calculation,
|
||||||
|
tableId,
|
||||||
|
}) => {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
|
||||||
|
if (calculation) {
|
||||||
|
params.set("field", field)
|
||||||
|
params.set("calculation", calculation)
|
||||||
|
}
|
||||||
|
if (groupBy) {
|
||||||
|
params.set("group", groupBy)
|
||||||
|
}
|
||||||
|
|
||||||
|
const QUERY_VIEW_URL = field
|
||||||
|
? `/api/views/${name}?${params}`
|
||||||
|
: `/api/views/${name}`
|
||||||
|
|
||||||
|
const rows = await API.get({ url: QUERY_VIEW_URL })
|
||||||
|
return await enrichRows(rows, tableId)
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
<script>
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import { setContext, onMount } from "svelte"
|
||||||
|
import Component from "./Component.svelte"
|
||||||
|
import SDK from "../sdk"
|
||||||
|
import { createDataStore, routeStore, screenStore } from "../store"
|
||||||
|
|
||||||
|
// Provide contexts
|
||||||
|
setContext("sdk", SDK)
|
||||||
|
setContext("component", writable({}))
|
||||||
|
setContext("data", createDataStore())
|
||||||
|
|
||||||
|
let loaded = false
|
||||||
|
|
||||||
|
// Load app config
|
||||||
|
onMount(async () => {
|
||||||
|
await routeStore.actions.fetchRoutes()
|
||||||
|
await screenStore.actions.fetchScreens()
|
||||||
|
loaded = true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if loaded}
|
||||||
|
<Component definition={$screenStore.page.props} />
|
||||||
|
{/if}
|
|
@ -0,0 +1,52 @@
|
||||||
|
<script>
|
||||||
|
import { getContext, setContext } from "svelte"
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import * as ComponentLibrary from "@budibase/standard-components"
|
||||||
|
import Router from "./Router.svelte"
|
||||||
|
import { enrichProps } from "../utils/componentProps"
|
||||||
|
import { bindingStore, builderStore } from "../store"
|
||||||
|
|
||||||
|
export let definition = {}
|
||||||
|
|
||||||
|
// Get local data binding context
|
||||||
|
const dataContext = getContext("data")
|
||||||
|
|
||||||
|
// Create component context
|
||||||
|
const componentStore = writable({})
|
||||||
|
setContext("component", componentStore)
|
||||||
|
|
||||||
|
// Extract component definition info
|
||||||
|
$: constructor = getComponentConstructor(definition._component)
|
||||||
|
$: children = definition._children
|
||||||
|
$: id = definition._id
|
||||||
|
$: enrichedProps = enrichProps(definition, $dataContext, $bindingStore)
|
||||||
|
$: selected = id === $builderStore.selectedComponentId
|
||||||
|
|
||||||
|
// Update component context
|
||||||
|
$: componentStore.set({ id, styles: { ...definition._styles, selected } })
|
||||||
|
|
||||||
|
// Gets the component constructor for the specified component
|
||||||
|
const getComponentConstructor = component => {
|
||||||
|
const split = component?.split("/")
|
||||||
|
const name = split?.[split.length - 1]
|
||||||
|
return name === "screenslot" ? Router : ComponentLibrary[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a unique key to let svelte know when to remount components.
|
||||||
|
// If a component is selected we want to remount it every time any props
|
||||||
|
// change.
|
||||||
|
const getChildKey = childId => {
|
||||||
|
const selected = childId === $builderStore.selectedComponentId
|
||||||
|
return selected ? `${childId}-${$builderStore.previewId}` : childId
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if constructor}
|
||||||
|
<svelte:component this={constructor} {...enrichedProps}>
|
||||||
|
{#if children && children.length}
|
||||||
|
{#each children as child (getChildKey(child._id))}
|
||||||
|
<svelte:self definition={child} />
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</svelte:component>
|
||||||
|
{/if}
|
|
@ -0,0 +1,15 @@
|
||||||
|
<script>
|
||||||
|
import { getContext, setContext } from "svelte"
|
||||||
|
import { createDataStore } from "../store"
|
||||||
|
|
||||||
|
export let row
|
||||||
|
|
||||||
|
// Clone and create new data context for this component tree
|
||||||
|
const dataContext = getContext("data")
|
||||||
|
const component = getContext("component")
|
||||||
|
const newData = createDataStore($dataContext)
|
||||||
|
setContext("data", newData)
|
||||||
|
$: newData.actions.addContext(row, $component.id)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
|
@ -0,0 +1,38 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import Router from "svelte-spa-router"
|
||||||
|
import { routeStore } from "../store"
|
||||||
|
import Screen from "./Screen.svelte"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const component = getContext("component")
|
||||||
|
|
||||||
|
$: routerConfig = getRouterConfig($routeStore.routes)
|
||||||
|
|
||||||
|
const getRouterConfig = routes => {
|
||||||
|
let config = {}
|
||||||
|
routes.forEach(route => {
|
||||||
|
config[route.path] = Screen
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add catch-all route so that we serve the Screen component always
|
||||||
|
config["*"] = Screen
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRouteLoading = ({ detail }) => {
|
||||||
|
routeStore.actions.setActiveRoute(detail.route)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if routerConfig}
|
||||||
|
<div use:styleable={$component.styles}>
|
||||||
|
<Router on:routeLoading={onRouteLoading} routes={routerConfig} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script>
|
||||||
|
import { fade } from "svelte/transition"
|
||||||
|
import { screenStore, routeStore } from "../store"
|
||||||
|
import Component from "./Component.svelte"
|
||||||
|
|
||||||
|
// Keep route params up to date
|
||||||
|
export let params = {}
|
||||||
|
$: routeStore.actions.setRouteParams(params || {})
|
||||||
|
|
||||||
|
// Get the screen definition for the current route
|
||||||
|
$: screenDefinition = $screenStore.activeScreen?.props
|
||||||
|
|
||||||
|
// Redirect to home page if no matching route
|
||||||
|
$: screenDefinition == null && routeStore.actions.navigate("/")
|
||||||
|
|
||||||
|
// Make a screen array so we can use keying to properly re-render each screen
|
||||||
|
$: screens = screenDefinition ? [screenDefinition] : []
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each screens as screen (screen._id)}
|
||||||
|
<div in:fade>
|
||||||
|
<Component definition={screen} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
align-self: stretch;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,91 +0,0 @@
|
||||||
import { attachChildren } from "./render/attachChildren"
|
|
||||||
import { createTreeNode } from "./render/prepareRenderComponent"
|
|
||||||
import { screenRouter } from "./render/screenRouter"
|
|
||||||
import { createStateManager } from "./state/stateManager"
|
|
||||||
import { getAppId } from "./render/getAppId"
|
|
||||||
|
|
||||||
export const createApp = ({
|
|
||||||
componentLibraries,
|
|
||||||
frontendDefinition,
|
|
||||||
window,
|
|
||||||
}) => {
|
|
||||||
let routeTo
|
|
||||||
let currentUrl
|
|
||||||
let screenStateManager
|
|
||||||
|
|
||||||
const onScreenSlotRendered = screenSlotNode => {
|
|
||||||
const onScreenSelected = (screen, url) => {
|
|
||||||
const stateManager = createStateManager({
|
|
||||||
componentLibraries,
|
|
||||||
onScreenSlotRendered: () => {},
|
|
||||||
routeTo,
|
|
||||||
})
|
|
||||||
const getAttachChildrenParams = attachChildrenParams(stateManager)
|
|
||||||
screenSlotNode.props._children = [screen.props]
|
|
||||||
const initialiseChildParams = getAttachChildrenParams(screenSlotNode)
|
|
||||||
attachChildren(initialiseChildParams)(screenSlotNode.rootElement, {
|
|
||||||
hydrate: true,
|
|
||||||
force: true,
|
|
||||||
})
|
|
||||||
if (screenStateManager) screenStateManager.destroy()
|
|
||||||
screenStateManager = stateManager
|
|
||||||
currentUrl = url
|
|
||||||
}
|
|
||||||
|
|
||||||
routeTo = screenRouter({
|
|
||||||
screens: frontendDefinition.screens,
|
|
||||||
onScreenSelected,
|
|
||||||
window,
|
|
||||||
})
|
|
||||||
const fallbackPath = window.location.pathname.replace(
|
|
||||||
getAppId(window.document.cookie),
|
|
||||||
""
|
|
||||||
)
|
|
||||||
routeTo(currentUrl || fallbackPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
const attachChildrenParams = stateManager => {
|
|
||||||
const getInitialiseParams = treeNode => ({
|
|
||||||
componentLibraries,
|
|
||||||
treeNode,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
setupState: stateManager.setup,
|
|
||||||
})
|
|
||||||
|
|
||||||
return getInitialiseParams
|
|
||||||
}
|
|
||||||
|
|
||||||
let rootTreeNode
|
|
||||||
const pageStateManager = createStateManager({
|
|
||||||
componentLibraries,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
// seems weird, but the routeTo variable may not be available at this point
|
|
||||||
routeTo: url => routeTo(url),
|
|
||||||
})
|
|
||||||
|
|
||||||
const initialisePage = (page, target, urlPath) => {
|
|
||||||
currentUrl = urlPath
|
|
||||||
|
|
||||||
rootTreeNode = createTreeNode()
|
|
||||||
rootTreeNode.props = {
|
|
||||||
_children: [page.props],
|
|
||||||
}
|
|
||||||
const getInitialiseParams = attachChildrenParams(pageStateManager)
|
|
||||||
const initChildParams = getInitialiseParams(rootTreeNode)
|
|
||||||
|
|
||||||
attachChildren(initChildParams)(target, {
|
|
||||||
hydrate: true,
|
|
||||||
force: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
return rootTreeNode
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
initialisePage,
|
|
||||||
screenStore: () => screenStateManager.store,
|
|
||||||
pageStore: () => pageStateManager.store,
|
|
||||||
routeTo: () => routeTo,
|
|
||||||
rootNode: () => rootTreeNode,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,59 +1,25 @@
|
||||||
import { createApp } from "./createApp"
|
import ClientApp from "./components/ClientApp.svelte"
|
||||||
import { builtins, builtinLibName } from "./render/builtinComponents"
|
import { builderStore } from "./store"
|
||||||
import { getAppId } from "./render/getAppId"
|
|
||||||
|
|
||||||
/**
|
let app
|
||||||
* create a web application from static budibase definition files.
|
|
||||||
* @param {object} opts - configuration options for budibase client libary
|
|
||||||
*/
|
|
||||||
export const loadBudibase = async opts => {
|
|
||||||
const _window = (opts && opts.window) || window
|
|
||||||
// const _localStorage = (opts && opts.localStorage) || localStorage
|
|
||||||
const appId = getAppId(window.document.cookie)
|
|
||||||
const frontendDefinition = _window["##BUDIBASE_FRONTEND_DEFINITION##"]
|
|
||||||
|
|
||||||
const user = {}
|
const loadBudibase = () => {
|
||||||
|
// Update builder store with any builder flags
|
||||||
const componentLibraryModules = (opts && opts.componentLibraries) || {}
|
builderStore.set({
|
||||||
|
inBuilder: !!window["##BUDIBASE_IN_BUILDER##"],
|
||||||
const libraries = frontendDefinition.libraries || []
|
page: window["##BUDIBASE_PREVIEW_PAGE##"],
|
||||||
|
screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
|
||||||
for (let library of libraries) {
|
selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"],
|
||||||
// fetch the JavaScript for the component libraries from the server
|
previewId: window["##BUDIBASE_PREVIEW_ID##"],
|
||||||
componentLibraryModules[library] = await import(
|
|
||||||
`/componentlibrary?library=${encodeURI(library)}&appId=${appId}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentLibraryModules[builtinLibName] = builtins(_window)
|
|
||||||
|
|
||||||
const {
|
|
||||||
initialisePage,
|
|
||||||
screenStore,
|
|
||||||
pageStore,
|
|
||||||
routeTo,
|
|
||||||
rootNode,
|
|
||||||
} = createApp({
|
|
||||||
componentLibraries: componentLibraryModules,
|
|
||||||
frontendDefinition,
|
|
||||||
user,
|
|
||||||
window: _window,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = _window.location
|
// Create app if one hasn't been created yet
|
||||||
? _window.location.pathname.replace(`${appId}/`, "").replace(appId, "")
|
if (!app) {
|
||||||
: ""
|
app = new ClientApp({
|
||||||
|
target: window.document.body,
|
||||||
initialisePage(frontendDefinition.page, _window.document.body, route)
|
})
|
||||||
|
|
||||||
return {
|
|
||||||
screenStore,
|
|
||||||
pageStore,
|
|
||||||
routeTo,
|
|
||||||
rootNode,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window) {
|
// Attach to window so the HTML template can call this when it loads
|
||||||
window.loadBudibase = loadBudibase
|
window.loadBudibase = loadBudibase
|
||||||
}
|
|
||||||
|
|
|
@ -1,138 +0,0 @@
|
||||||
import { prepareRenderComponent } from "./prepareRenderComponent"
|
|
||||||
import { isScreenSlot } from "./builtinComponents"
|
|
||||||
import deepEqual from "deep-equal"
|
|
||||||
import appStore from "../state/store"
|
|
||||||
|
|
||||||
export const attachChildren = initialiseOpts => (htmlElement, options) => {
|
|
||||||
const {
|
|
||||||
componentLibraries,
|
|
||||||
treeNode,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
setupState,
|
|
||||||
} = initialiseOpts
|
|
||||||
|
|
||||||
const anchor = options && options.anchor ? options.anchor : null
|
|
||||||
const force = options ? options.force : false
|
|
||||||
const hydrate = options ? options.hydrate : true
|
|
||||||
const context = options && options.context
|
|
||||||
|
|
||||||
if (!force && treeNode.children.length > 0) return treeNode.children
|
|
||||||
|
|
||||||
for (let childNode of treeNode.children) {
|
|
||||||
childNode.destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!htmlElement) return
|
|
||||||
|
|
||||||
if (hydrate) {
|
|
||||||
while (htmlElement.firstChild) {
|
|
||||||
htmlElement.removeChild(htmlElement.firstChild)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const contextStoreKeys = []
|
|
||||||
|
|
||||||
// create new context if supplied
|
|
||||||
if (context) {
|
|
||||||
let childIndex = 0
|
|
||||||
// if context is an array, map to new structure
|
|
||||||
const contextArray = Array.isArray(context) ? context : [context]
|
|
||||||
for (let ctx of contextArray) {
|
|
||||||
const key = appStore.create(
|
|
||||||
ctx,
|
|
||||||
treeNode.props._id,
|
|
||||||
childIndex,
|
|
||||||
treeNode.contextStoreKey
|
|
||||||
)
|
|
||||||
contextStoreKeys.push(key)
|
|
||||||
childIndex++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const childNodes = []
|
|
||||||
|
|
||||||
const createChildNodes = contextStoreKey => {
|
|
||||||
for (let childProps of treeNode.props._children) {
|
|
||||||
const { componentName, libName } = splitName(childProps._component)
|
|
||||||
|
|
||||||
if (!componentName || !libName) return
|
|
||||||
|
|
||||||
const ComponentConstructor = componentLibraries[libName][componentName]
|
|
||||||
|
|
||||||
const childNode = prepareRenderComponent({
|
|
||||||
props: childProps,
|
|
||||||
parentNode: treeNode,
|
|
||||||
ComponentConstructor,
|
|
||||||
htmlElement,
|
|
||||||
anchor,
|
|
||||||
// in same context as parent, unless a new one was supplied
|
|
||||||
contextStoreKey,
|
|
||||||
})
|
|
||||||
|
|
||||||
childNodes.push(childNode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context) {
|
|
||||||
// if new context(s) is supplied, then create nodes
|
|
||||||
// with keys to new context stores
|
|
||||||
for (let contextStoreKey of contextStoreKeys) {
|
|
||||||
createChildNodes(contextStoreKey)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// otherwise, use same context store as parent
|
|
||||||
// which maybe undefined (therfor using the root state)
|
|
||||||
createChildNodes(treeNode.contextStoreKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if everything is equal, then don't re-render
|
|
||||||
if (areTreeNodesEqual(treeNode.children, childNodes)) return treeNode.children
|
|
||||||
|
|
||||||
for (let node of childNodes) {
|
|
||||||
const initialProps = setupState(node)
|
|
||||||
node.render(initialProps)
|
|
||||||
}
|
|
||||||
|
|
||||||
const screenSlot = childNodes.find(n => isScreenSlot(n.props._component))
|
|
||||||
|
|
||||||
if (onScreenSlotRendered && screenSlot) {
|
|
||||||
// assuming there is only ever one screen slot
|
|
||||||
onScreenSlotRendered(screenSlot)
|
|
||||||
}
|
|
||||||
|
|
||||||
treeNode.children = childNodes
|
|
||||||
|
|
||||||
return childNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
const splitName = fullname => {
|
|
||||||
const nameParts = fullname.split("/")
|
|
||||||
|
|
||||||
const componentName = nameParts[nameParts.length - 1]
|
|
||||||
|
|
||||||
const libName = fullname.substring(
|
|
||||||
0,
|
|
||||||
fullname.length - componentName.length - 1
|
|
||||||
)
|
|
||||||
|
|
||||||
return { libName, componentName }
|
|
||||||
}
|
|
||||||
|
|
||||||
const areTreeNodesEqual = (children1, children2) => {
|
|
||||||
if (children1.length !== children2.length) return false
|
|
||||||
if (children1 === children2) return true
|
|
||||||
|
|
||||||
let isEqual = false
|
|
||||||
for (let i = 0; i < children1.length; i++) {
|
|
||||||
// same context and same children, then nothing has changed
|
|
||||||
isEqual =
|
|
||||||
deepEqual(children1[i].context, children2[i].context) &&
|
|
||||||
areTreeNodesEqual(children1[i].children, children2[i].children)
|
|
||||||
if (!isEqual) return false
|
|
||||||
if (isScreenSlot(children1[i].parentNode.props._component)) {
|
|
||||||
isEqual = deepEqual(children1[i].props, children2[i].props)
|
|
||||||
}
|
|
||||||
if (!isEqual) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { screenSlotComponent } from "./screenSlotComponent"
|
|
||||||
|
|
||||||
export const builtinLibName = "##builtin"
|
|
||||||
|
|
||||||
export const isScreenSlot = componentName =>
|
|
||||||
componentName === "##builtin/screenslot"
|
|
||||||
|
|
||||||
export const builtins = window => ({
|
|
||||||
screenslot: screenSlotComponent(window),
|
|
||||||
})
|
|
|
@ -1,88 +0,0 @@
|
||||||
import renderTemplateString from "../state/renderTemplateString"
|
|
||||||
import appStore from "../state/store"
|
|
||||||
import hasBinding from "../state/hasBinding"
|
|
||||||
|
|
||||||
export const prepareRenderComponent = ({
|
|
||||||
ComponentConstructor,
|
|
||||||
htmlElement,
|
|
||||||
anchor,
|
|
||||||
props,
|
|
||||||
parentNode,
|
|
||||||
contextStoreKey,
|
|
||||||
}) => {
|
|
||||||
const thisNode = createTreeNode()
|
|
||||||
thisNode.parentNode = parentNode
|
|
||||||
thisNode.props = props
|
|
||||||
thisNode.contextStoreKey = contextStoreKey
|
|
||||||
|
|
||||||
// the treeNode is first created (above), and then this
|
|
||||||
// render method is add. The treeNode is returned, and
|
|
||||||
// render is called later (in attachChildren)
|
|
||||||
thisNode.render = initialProps => {
|
|
||||||
thisNode.component = new ComponentConstructor({
|
|
||||||
target: htmlElement,
|
|
||||||
props: initialProps,
|
|
||||||
hydrate: false,
|
|
||||||
anchor,
|
|
||||||
})
|
|
||||||
|
|
||||||
// finds the root element of the component, which was created by the contructor above
|
|
||||||
// we use this later to attach a className to. This is how styles
|
|
||||||
// are applied by the builder
|
|
||||||
thisNode.rootElement = htmlElement.children[htmlElement.children.length - 1]
|
|
||||||
|
|
||||||
let [componentName] = props._component.match(/[a-z]*$/)
|
|
||||||
if (props._id && thisNode.rootElement) {
|
|
||||||
thisNode.rootElement.classList.add(`${componentName}-${props._id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// make this node listen to the store
|
|
||||||
if (thisNode.stateBound) {
|
|
||||||
const unsubscribe = appStore.subscribe(state => {
|
|
||||||
const storeBoundProps = Object.keys(initialProps._bb.props).filter(p =>
|
|
||||||
hasBinding(initialProps._bb.props[p])
|
|
||||||
)
|
|
||||||
if (storeBoundProps.length > 0) {
|
|
||||||
const toSet = {}
|
|
||||||
for (let prop of storeBoundProps) {
|
|
||||||
const propValue = initialProps._bb.props[prop]
|
|
||||||
toSet[prop] = renderTemplateString(propValue, state)
|
|
||||||
}
|
|
||||||
thisNode.component.$set(toSet)
|
|
||||||
}
|
|
||||||
}, thisNode.contextStoreKey)
|
|
||||||
thisNode.unsubscribe = unsubscribe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return thisNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createTreeNode = () => ({
|
|
||||||
context: {},
|
|
||||||
props: {},
|
|
||||||
rootElement: null,
|
|
||||||
parentNode: null,
|
|
||||||
children: [],
|
|
||||||
bindings: [],
|
|
||||||
component: null,
|
|
||||||
unsubscribe: () => {},
|
|
||||||
render: () => {},
|
|
||||||
get destroy() {
|
|
||||||
const node = this
|
|
||||||
return () => {
|
|
||||||
if (node.children) {
|
|
||||||
// destroy children first - from leaf nodes up
|
|
||||||
for (let child of node.children) {
|
|
||||||
child.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (node.unsubscribe) node.unsubscribe()
|
|
||||||
if (node.component && node.component.$destroy) node.component.$destroy()
|
|
||||||
for (let onDestroyItem of node.onDestroy) {
|
|
||||||
onDestroyItem()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDestroy: [],
|
|
||||||
})
|
|
|
@ -1,124 +0,0 @@
|
||||||
import regexparam from "regexparam"
|
|
||||||
import appStore from "../state/store"
|
|
||||||
import { getAppId } from "./getAppId"
|
|
||||||
|
|
||||||
export const screenRouter = ({ screens, onScreenSelected, window }) => {
|
|
||||||
function sanitize(url) {
|
|
||||||
if (!url) return url
|
|
||||||
return url
|
|
||||||
.split("/")
|
|
||||||
.map(part => {
|
|
||||||
// if parameter, then use as is
|
|
||||||
if (part.startsWith(":")) return part
|
|
||||||
return encodeURIComponent(part)
|
|
||||||
})
|
|
||||||
.join("/")
|
|
||||||
.toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRunningLocally = () => {
|
|
||||||
const hostname = (window.location && window.location.hostname) || ""
|
|
||||||
return (
|
|
||||||
hostname === "localhost" ||
|
|
||||||
hostname === "127.0.0.1" ||
|
|
||||||
hostname.startsWith("192.168")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeRootedPath = url => {
|
|
||||||
if (isRunningLocally()) {
|
|
||||||
const appId = getAppId(window.document.cookie)
|
|
||||||
if (url) {
|
|
||||||
url = sanitize(url)
|
|
||||||
if (!url.startsWith("/")) {
|
|
||||||
url = `/${url}`
|
|
||||||
}
|
|
||||||
if (url.startsWith(`/${appId}`)) {
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
return `/${appId}${url}`
|
|
||||||
}
|
|
||||||
return `/${appId}`
|
|
||||||
}
|
|
||||||
return sanitize(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
const routes = screens.map(screen =>
|
|
||||||
makeRootedPath(screen.routing ? screen.routing.route : null)
|
|
||||||
)
|
|
||||||
let fallback = routes.findIndex(([p]) => p === makeRootedPath("*"))
|
|
||||||
if (fallback < 0) fallback = 0
|
|
||||||
|
|
||||||
let current
|
|
||||||
|
|
||||||
function route(url) {
|
|
||||||
const _url = makeRootedPath(url.state || url)
|
|
||||||
current = routes.findIndex(
|
|
||||||
p =>
|
|
||||||
p !== makeRootedPath("*") &&
|
|
||||||
new RegExp("^" + p.toLowerCase() + "$").test(_url.toLowerCase())
|
|
||||||
)
|
|
||||||
|
|
||||||
const params = {}
|
|
||||||
|
|
||||||
if (current === -1) {
|
|
||||||
routes.forEach((p, i) => {
|
|
||||||
// ignore home - which matched everything
|
|
||||||
if (p === makeRootedPath("*")) return
|
|
||||||
const pm = regexparam(p)
|
|
||||||
const matches = pm.pattern.exec(_url)
|
|
||||||
|
|
||||||
if (!matches) return
|
|
||||||
|
|
||||||
let j = 0
|
|
||||||
while (j < pm.keys.length) {
|
|
||||||
params[pm.keys[j]] = matches[++j] || null
|
|
||||||
}
|
|
||||||
|
|
||||||
current = i
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
appStore.update(state => {
|
|
||||||
state["##routeParams"] = params
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
|
|
||||||
const screenIndex = current !== -1 ? current : fallback
|
|
||||||
|
|
||||||
try {
|
|
||||||
!url.state && history.pushState(_url, null, _url)
|
|
||||||
} catch (_) {
|
|
||||||
// ignoring an exception here as the builder runs an iframe, which does not like this
|
|
||||||
}
|
|
||||||
|
|
||||||
onScreenSelected(screens[screenIndex], _url)
|
|
||||||
}
|
|
||||||
|
|
||||||
function click(e) {
|
|
||||||
const x = e.target.closest("a")
|
|
||||||
const y = x && x.getAttribute("href")
|
|
||||||
|
|
||||||
if (
|
|
||||||
e.ctrlKey ||
|
|
||||||
e.metaKey ||
|
|
||||||
e.altKey ||
|
|
||||||
e.shiftKey ||
|
|
||||||
e.button ||
|
|
||||||
e.defaultPrevented
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
const target = (x && x.target) || "_self"
|
|
||||||
if (!y || target !== "_self" || x.host !== location.host) return
|
|
||||||
|
|
||||||
e.preventDefault()
|
|
||||||
route(y)
|
|
||||||
}
|
|
||||||
|
|
||||||
addEventListener("popstate", route)
|
|
||||||
addEventListener("pushstate", route)
|
|
||||||
addEventListener("click", click)
|
|
||||||
|
|
||||||
return route
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
export const screenSlotComponent = window => {
|
|
||||||
return function(opts) {
|
|
||||||
const node = window.document.createElement("DIV")
|
|
||||||
const $set = props => {
|
|
||||||
props._bb.attachChildren(node)
|
|
||||||
}
|
|
||||||
const $destroy = () => {
|
|
||||||
if (opts.target && node) opts.target.removeChild(node)
|
|
||||||
}
|
|
||||||
this.$set = $set
|
|
||||||
this.$destroy = $destroy
|
|
||||||
opts.target.appendChild(node)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import * as API from "./api"
|
||||||
|
import { authStore, routeStore, screenStore, bindingStore } from "./store"
|
||||||
|
import { styleable } from "./utils/styleable"
|
||||||
|
import { getAppId } from "./utils/getAppId"
|
||||||
|
import { link as linkable } from "svelte-spa-router"
|
||||||
|
import DataProvider from "./components/DataProvider.svelte"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
API,
|
||||||
|
authStore,
|
||||||
|
routeStore,
|
||||||
|
screenStore,
|
||||||
|
styleable,
|
||||||
|
linkable,
|
||||||
|
getAppId,
|
||||||
|
DataProvider,
|
||||||
|
setBindableValue: bindingStore.actions.setBindableValue,
|
||||||
|
}
|
|
@ -1,43 +0,0 @@
|
||||||
import setBindableComponentProp from "./setBindableComponentProp"
|
|
||||||
import { attachChildren } from "../render/attachChildren"
|
|
||||||
import store from "../state/store"
|
|
||||||
import { baseApiCall } from "../api/index"
|
|
||||||
|
|
||||||
export const bbFactory = ({
|
|
||||||
componentLibraries,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
runEventActions,
|
|
||||||
}) => {
|
|
||||||
const api = {
|
|
||||||
post: (url, body) => baseApiCall("POST", url, body),
|
|
||||||
get: (url, body) => baseApiCall("GET", url, body),
|
|
||||||
patch: (url, body) => baseApiCall("PATCH", url, body),
|
|
||||||
delete: (url, body) => baseApiCall("DELETE", url, body),
|
|
||||||
}
|
|
||||||
|
|
||||||
return (treeNode, setupState) => {
|
|
||||||
const attachParams = {
|
|
||||||
componentLibraries,
|
|
||||||
treeNode,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
setupState,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
attachChildren: attachChildren(attachParams),
|
|
||||||
props: treeNode.props,
|
|
||||||
call: async eventName =>
|
|
||||||
eventName &&
|
|
||||||
(await runEventActions(
|
|
||||||
treeNode.props[eventName],
|
|
||||||
store.getState(treeNode.contextStoreKey)
|
|
||||||
)),
|
|
||||||
setBinding: setBindableComponentProp(treeNode),
|
|
||||||
api,
|
|
||||||
parent,
|
|
||||||
store: store.getStore(treeNode.contextStoreKey),
|
|
||||||
// these parameters are populated by screenRouter
|
|
||||||
routeParams: () => store.getState()["##routeParams"],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
import api from "../api"
|
|
||||||
import renderTemplateString from "./renderTemplateString"
|
|
||||||
|
|
||||||
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
|
||||||
|
|
||||||
export const eventHandlers = routeTo => {
|
|
||||||
const handlers = {
|
|
||||||
"Navigate To": param => routeTo(param && param.url),
|
|
||||||
"Update Row": api.updateRow,
|
|
||||||
"Save Row": api.saveRow,
|
|
||||||
"Delete Row": api.deleteRow,
|
|
||||||
"Trigger Workflow": api.triggerWorkflow,
|
|
||||||
}
|
|
||||||
|
|
||||||
// when an event is called, this is what gets run
|
|
||||||
const runEventActions = async (actions, state) => {
|
|
||||||
if (!actions) return
|
|
||||||
// calls event handlers sequentially
|
|
||||||
for (let action of actions) {
|
|
||||||
const handler = handlers[action[EVENT_TYPE_MEMBER_NAME]]
|
|
||||||
const parameters = createParameters(action.parameters, state)
|
|
||||||
if (handler) {
|
|
||||||
await handler(parameters, state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return runEventActions
|
|
||||||
}
|
|
||||||
|
|
||||||
// this will take a parameters obj, iterate all keys, and do a mustache render
|
|
||||||
// for every string. It will work recursively if it encounnters an {}
|
|
||||||
const createParameters = (parameterTemplateObj, state) => {
|
|
||||||
const parameters = {}
|
|
||||||
for (let key in parameterTemplateObj) {
|
|
||||||
if (typeof parameterTemplateObj[key] === "string") {
|
|
||||||
parameters[key] = renderTemplateString(parameterTemplateObj[key], state)
|
|
||||||
} else if (typeof parameterTemplateObj[key] === "object") {
|
|
||||||
parameters[key] = createParameters(parameterTemplateObj[key], state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parameters
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
export const setContext = treeNode => (key, value) =>
|
|
||||||
(treeNode.context[key] = value)
|
|
||||||
|
|
||||||
export const getContext = treeNode => key => {
|
|
||||||
if (treeNode.context && treeNode.context[key] !== undefined)
|
|
||||||
return treeNode.context[key]
|
|
||||||
|
|
||||||
if (!treeNode.context.$parent) return
|
|
||||||
|
|
||||||
return getContext(treeNode.parentNode)(key)
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export default value => typeof value === "string" && value.includes("{{")
|
|
|
@ -1,17 +0,0 @@
|
||||||
import mustache from "mustache"
|
|
||||||
|
|
||||||
// this is a much more liberal version of mustache's escape function
|
|
||||||
// ...just ignoring < and > to prevent tags from user input
|
|
||||||
// original version here https://github.com/janl/mustache.js/blob/4b7908f5c9fec469a11cfaed2f2bed23c84e1c5c/mustache.js#L78
|
|
||||||
|
|
||||||
const entityMap = {
|
|
||||||
"<": "<",
|
|
||||||
">": ">",
|
|
||||||
}
|
|
||||||
|
|
||||||
mustache.escape = text =>
|
|
||||||
String(text).replace(/[&<>"'`=/]/g, function fromEntityMap(s) {
|
|
||||||
return entityMap[s] || s
|
|
||||||
})
|
|
||||||
|
|
||||||
export default mustache.render
|
|
|
@ -1,13 +0,0 @@
|
||||||
import appStore from "./store"
|
|
||||||
|
|
||||||
export default treeNode => (propName, value) => {
|
|
||||||
if (!propName || propName.length === 0) return
|
|
||||||
if (!treeNode) return
|
|
||||||
const componentId = treeNode.props._id
|
|
||||||
|
|
||||||
appStore.update(state => {
|
|
||||||
state[componentId] = state[componentId] || {}
|
|
||||||
state[componentId][propName] = value
|
|
||||||
return state
|
|
||||||
}, treeNode.contextStoreKey)
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
import { eventHandlers } from "./eventHandlers"
|
|
||||||
import { bbFactory } from "./bbComponentApi"
|
|
||||||
import renderTemplateString from "./renderTemplateString"
|
|
||||||
import appStore from "./store"
|
|
||||||
import hasBinding from "./hasBinding"
|
|
||||||
|
|
||||||
const doNothing = () => {}
|
|
||||||
doNothing.isPlaceholder = true
|
|
||||||
|
|
||||||
const isMetaProp = propName =>
|
|
||||||
propName === "_component" ||
|
|
||||||
propName === "_children" ||
|
|
||||||
propName === "_id" ||
|
|
||||||
propName === "_style" ||
|
|
||||||
propName === "_code" ||
|
|
||||||
propName === "_codeMeta" ||
|
|
||||||
propName === "_styles"
|
|
||||||
|
|
||||||
export const createStateManager = ({
|
|
||||||
componentLibraries,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
routeTo,
|
|
||||||
}) => {
|
|
||||||
let runEventActions = eventHandlers(routeTo)
|
|
||||||
|
|
||||||
const bb = bbFactory({
|
|
||||||
componentLibraries,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
runEventActions,
|
|
||||||
})
|
|
||||||
|
|
||||||
const setup = _setup(bb)
|
|
||||||
|
|
||||||
return {
|
|
||||||
setup,
|
|
||||||
destroy: () => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const _setup = bb => node => {
|
|
||||||
const props = node.props
|
|
||||||
const initialProps = { ...props }
|
|
||||||
|
|
||||||
for (let propName in props) {
|
|
||||||
if (isMetaProp(propName)) continue
|
|
||||||
|
|
||||||
const propValue = props[propName]
|
|
||||||
|
|
||||||
const isBound = hasBinding(propValue)
|
|
||||||
|
|
||||||
if (isBound) {
|
|
||||||
const state = appStore.getState(node.contextStoreKey)
|
|
||||||
initialProps[propName] = renderTemplateString(propValue, state)
|
|
||||||
|
|
||||||
if (!node.stateBound) {
|
|
||||||
node.stateBound = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setup = _setup(bb)
|
|
||||||
initialProps._bb = bb(node, setup)
|
|
||||||
|
|
||||||
return initialProps
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
import { writable } from "svelte/store"
|
|
||||||
|
|
||||||
// we assume that the reference to this state object
|
|
||||||
// will remain for the life of the application
|
|
||||||
const rootState = {}
|
|
||||||
const rootStore = writable(rootState)
|
|
||||||
const contextStores = {}
|
|
||||||
|
|
||||||
// contextProviderId is the component id that provides the data for the context
|
|
||||||
const contextStoreKey = (dataProviderId, childIndex) =>
|
|
||||||
`${dataProviderId}${childIndex >= 0 ? ":" + childIndex : ""}`
|
|
||||||
|
|
||||||
// creates a store for a datacontext (e.g. each item in a list component)
|
|
||||||
// overrides store if already exists
|
|
||||||
const create = (data, dataProviderId, childIndex, parentContextStoreId) => {
|
|
||||||
const key = contextStoreKey(dataProviderId, childIndex)
|
|
||||||
const state = { data }
|
|
||||||
|
|
||||||
// add reference to parent state object,
|
|
||||||
// so we can use bindings like state.parent.parent
|
|
||||||
// (if no parent, then parent is rootState )
|
|
||||||
state.parent = parentContextStoreId
|
|
||||||
? contextStores[parentContextStoreId].state
|
|
||||||
: rootState
|
|
||||||
|
|
||||||
contextStores[key] = {
|
|
||||||
store: writable(state),
|
|
||||||
subscriberCount: 0,
|
|
||||||
state,
|
|
||||||
parentContextStoreId,
|
|
||||||
}
|
|
||||||
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
const subscribe = (subscription, storeKey) => {
|
|
||||||
if (!storeKey) {
|
|
||||||
return rootStore.subscribe(subscription)
|
|
||||||
}
|
|
||||||
const contextStore = contextStores[storeKey]
|
|
||||||
|
|
||||||
// we are subscribing to multiple stores,
|
|
||||||
// we dont want to run our listener for every subscription, the first time
|
|
||||||
// as this could repeatedly run $set on the same component
|
|
||||||
// ... which already has its initial properties set properly
|
|
||||||
const ignoreFirstSubscription = () => {
|
|
||||||
let hasRunOnce = false
|
|
||||||
return () => {
|
|
||||||
if (hasRunOnce) subscription(contextStore.state)
|
|
||||||
hasRunOnce = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsubscribes = [rootStore.subscribe(ignoreFirstSubscription())]
|
|
||||||
|
|
||||||
// we subscribe to all stores in the hierarchy
|
|
||||||
const ancestorSubscribe = ctxStore => {
|
|
||||||
// unsubscribe func returned by svelte store
|
|
||||||
const svelteUnsub = ctxStore.store.subscribe(ignoreFirstSubscription())
|
|
||||||
|
|
||||||
// we wrap the svelte unsubscribe, so we can
|
|
||||||
// cleanup stores when they are no longer subscribed to
|
|
||||||
const unsub = () => {
|
|
||||||
ctxStore.subscriberCount = contextStore.subscriberCount - 1
|
|
||||||
// when no subscribers left, we delete the store
|
|
||||||
if (ctxStore.subscriberCount === 0) {
|
|
||||||
delete ctxStore[storeKey]
|
|
||||||
}
|
|
||||||
svelteUnsub()
|
|
||||||
}
|
|
||||||
unsubscribes.push(unsub)
|
|
||||||
if (ctxStore.parentContextStoreId) {
|
|
||||||
ancestorSubscribe(contextStores[ctxStore.parentContextStoreId])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ancestorSubscribe(contextStore)
|
|
||||||
|
|
||||||
// our final unsubscribe function calls unsubscribe on all stores
|
|
||||||
return () => unsubscribes.forEach(u => u())
|
|
||||||
}
|
|
||||||
|
|
||||||
const findStore = (dataProviderId, childIndex) =>
|
|
||||||
dataProviderId
|
|
||||||
? contextStores[contextStoreKey(dataProviderId, childIndex)].store
|
|
||||||
: rootStore
|
|
||||||
|
|
||||||
const update = (updatefunc, dataProviderId, childIndex) =>
|
|
||||||
findStore(dataProviderId, childIndex).update(updatefunc)
|
|
||||||
|
|
||||||
const set = (value, dataProviderId, childIndex) =>
|
|
||||||
findStore(dataProviderId, childIndex).set(value)
|
|
||||||
|
|
||||||
const getState = contextStoreKey =>
|
|
||||||
contextStoreKey ? contextStores[contextStoreKey].state : rootState
|
|
||||||
|
|
||||||
const getStore = contextStoreKey =>
|
|
||||||
contextStoreKey ? contextStores[contextStoreKey].store : rootStore
|
|
||||||
|
|
||||||
export default {
|
|
||||||
subscribe,
|
|
||||||
update,
|
|
||||||
set,
|
|
||||||
getState,
|
|
||||||
create,
|
|
||||||
contextStoreKey,
|
|
||||||
getStore,
|
|
||||||
}
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import * as API from "../api"
|
||||||
|
import { getAppId } from "../utils/getAppId"
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
|
const createAuthStore = () => {
|
||||||
|
const store = writable("")
|
||||||
|
|
||||||
|
const logIn = async ({ username, password }) => {
|
||||||
|
const user = await API.logIn({ username, password })
|
||||||
|
if (!user.error) {
|
||||||
|
store.set(user.token)
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const logOut = () => {
|
||||||
|
store.set("")
|
||||||
|
const appId = getAppId()
|
||||||
|
if (appId) {
|
||||||
|
for (let environment of ["local", "cloud"]) {
|
||||||
|
window.document.cookie = `budibase:${appId}:${environment}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions: { logIn, logOut },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authStore = createAuthStore()
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
|
const createBindingStore = () => {
|
||||||
|
const store = writable({})
|
||||||
|
|
||||||
|
const setBindableValue = (value, componentId) => {
|
||||||
|
store.update(state => {
|
||||||
|
if (componentId) {
|
||||||
|
state[componentId] = value
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions: { setBindableValue },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bindingStore = createBindingStore()
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
|
const createBuilderStore = () => {
|
||||||
|
const initialState = {
|
||||||
|
inBuilder: false,
|
||||||
|
page: null,
|
||||||
|
screen: null,
|
||||||
|
selectedComponentId: null,
|
||||||
|
previewId: null,
|
||||||
|
}
|
||||||
|
return writable(initialState)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const builderStore = createBuilderStore()
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
|
export const createDataStore = existingContext => {
|
||||||
|
const store = writable({ ...existingContext })
|
||||||
|
|
||||||
|
// Adds a context layer to the data context tree
|
||||||
|
const addContext = (row, componentId) => {
|
||||||
|
store.update(state => {
|
||||||
|
if (componentId) {
|
||||||
|
state[componentId] = row
|
||||||
|
state[`${componentId}_draft`] = cloneDeep(row)
|
||||||
|
state.closestComponentId = componentId
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
update: store.update,
|
||||||
|
actions: { addContext },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dataStore = createDataStore()
|
|
@ -0,0 +1,8 @@
|
||||||
|
export { authStore } from "./auth"
|
||||||
|
export { routeStore } from "./routes"
|
||||||
|
export { screenStore } from "./screens"
|
||||||
|
export { builderStore } from "./builder"
|
||||||
|
export { bindingStore } from "./binding"
|
||||||
|
|
||||||
|
// Data stores are layered and duplicated, so it is not a singleton
|
||||||
|
export { createDataStore, dataStore } from "./data"
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import { push } from "svelte-spa-router"
|
||||||
|
import * as API from "../api"
|
||||||
|
|
||||||
|
const createRouteStore = () => {
|
||||||
|
const initialState = {
|
||||||
|
routes: [],
|
||||||
|
routeParams: {},
|
||||||
|
activeRoute: null,
|
||||||
|
}
|
||||||
|
const store = writable(initialState)
|
||||||
|
|
||||||
|
const fetchRoutes = async () => {
|
||||||
|
const routeConfig = await API.fetchRoutes()
|
||||||
|
let routes = []
|
||||||
|
Object.values(routeConfig.routes).forEach(route => {
|
||||||
|
Object.entries(route.subpaths).forEach(([path, config]) => {
|
||||||
|
routes.push({
|
||||||
|
path,
|
||||||
|
screenId: config.screenId,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
store.update(state => {
|
||||||
|
state.routes = routes
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const setRouteParams = routeParams => {
|
||||||
|
store.update(state => {
|
||||||
|
state.routeParams = routeParams
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const setActiveRoute = route => {
|
||||||
|
store.update(state => {
|
||||||
|
state.activeRoute = route
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const navigate = push
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions: { fetchRoutes, navigate, setRouteParams, setActiveRoute },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const routeStore = createRouteStore()
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { writable, derived } from "svelte/store"
|
||||||
|
import { routeStore } from "./routes"
|
||||||
|
import { builderStore } from "./builder"
|
||||||
|
import * as API from "../api"
|
||||||
|
import { getAppId } from "../utils/getAppId"
|
||||||
|
|
||||||
|
const createScreenStore = () => {
|
||||||
|
const config = writable({
|
||||||
|
screens: [],
|
||||||
|
page: {},
|
||||||
|
})
|
||||||
|
const store = derived(
|
||||||
|
[config, routeStore, builderStore],
|
||||||
|
([$config, $routeStore, $builderStore]) => {
|
||||||
|
let page
|
||||||
|
let activeScreen
|
||||||
|
if ($builderStore.inBuilder) {
|
||||||
|
// Use builder defined definitions if inside the builder preview
|
||||||
|
page = $builderStore.page
|
||||||
|
activeScreen = $builderStore.screen
|
||||||
|
} else {
|
||||||
|
// Otherwise find the correct screen by matching the current route
|
||||||
|
page = $config.page
|
||||||
|
const { screens } = $config
|
||||||
|
if (screens.length === 1) {
|
||||||
|
activeScreen = screens[0]
|
||||||
|
} else {
|
||||||
|
activeScreen = screens.find(
|
||||||
|
screen => screen.routing.route === $routeStore.activeRoute
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { page, activeScreen }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const fetchScreens = async () => {
|
||||||
|
const appDefinition = await API.fetchAppDefinition(getAppId())
|
||||||
|
config.set({
|
||||||
|
screens: appDefinition.screens,
|
||||||
|
page: appDefinition.page,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions: { fetchScreens },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const screenStore = createScreenStore()
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { enrichDataBinding } from "./enrichDataBinding"
|
||||||
|
import { routeStore } from "../store"
|
||||||
|
import { saveRow, deleteRow } from "../api"
|
||||||
|
|
||||||
|
const saveRowHandler = async (action, context) => {
|
||||||
|
let draft = context[`${action.parameters.contextPath}_draft`]
|
||||||
|
if (action.parameters.fields) {
|
||||||
|
Object.entries(action.parameters.fields).forEach(([key, entry]) => {
|
||||||
|
draft[key] = enrichDataBinding(entry.value, context)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await saveRow(draft)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteRowHandler = async (action, context) => {
|
||||||
|
const { tableId, revId, rowId } = action.parameters
|
||||||
|
await deleteRow({
|
||||||
|
tableId: enrichDataBinding(tableId, context),
|
||||||
|
rowId: enrichDataBinding(rowId, context),
|
||||||
|
revId: enrichDataBinding(revId, context),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigationHandler = action => {
|
||||||
|
routeStore.actions.navigate(action.parameters.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlerMap = {
|
||||||
|
["Save Row"]: saveRowHandler,
|
||||||
|
["Delete Row"]: deleteRowHandler,
|
||||||
|
["Navigate To"]: navigationHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an array of actions and returns a function which will execute the
|
||||||
|
* actions in the current context.
|
||||||
|
*/
|
||||||
|
export const enrichButtonActions = (actions, context) => {
|
||||||
|
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
|
||||||
|
return async () => {
|
||||||
|
for (let i = 0; i < handlers.length; i++) {
|
||||||
|
await handlers[i](actions[i], context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { enrichDataBindings } from "./enrichDataBinding"
|
||||||
|
import { enrichButtonActions } from "./buttonActions"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enriches component props.
|
||||||
|
* Data bindings are enriched, and button actions are enriched.
|
||||||
|
*/
|
||||||
|
export const enrichProps = (props, dataContexts, dataBindings) => {
|
||||||
|
// Exclude all private props that start with an underscore
|
||||||
|
let validProps = {}
|
||||||
|
Object.entries(props)
|
||||||
|
.filter(([name]) => !name.startsWith("_"))
|
||||||
|
.forEach(([key, value]) => {
|
||||||
|
validProps[key] = value
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create context of all bindings and data contexts
|
||||||
|
// Duplicate the closest context as "data" which the builder requires
|
||||||
|
const context = {
|
||||||
|
...dataContexts,
|
||||||
|
...dataBindings,
|
||||||
|
data: dataContexts[dataContexts.closestComponentId],
|
||||||
|
data_draft: dataContexts[`${dataContexts.closestComponentId}_draft`],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrich all data bindings in top level props
|
||||||
|
let enrichedProps = enrichDataBindings(validProps, context)
|
||||||
|
|
||||||
|
// Enrich button actions if they exist
|
||||||
|
if (props._component.endsWith("/button") && enrichedProps.onClick) {
|
||||||
|
enrichedProps.onClick = enrichButtonActions(enrichedProps.onClick, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
return enrichedProps
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
import mustache from "mustache"
|
||||||
|
|
||||||
|
// this is a much more liberal version of mustache's escape function
|
||||||
|
// ...just ignoring < and > to prevent tags from user input
|
||||||
|
// original version here https://github.com/janl/mustache.js/blob/4b7908f5c9fec469a11cfaed2f2bed23c84e1c5c/mustache.js#L78
|
||||||
|
const entityMap = {
|
||||||
|
"<": "<",
|
||||||
|
">": ">",
|
||||||
|
}
|
||||||
|
mustache.escape = text => {
|
||||||
|
if (text == null || typeof text !== "string") {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
return text.replace(/[<>]/g, function fromEntityMap(s) {
|
||||||
|
return entityMap[s] || s
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regex to test inputs with to see if they are likely candidates for mustache
|
||||||
|
const looksLikeMustache = /{{.*}}/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enriches a given input with a row from the database.
|
||||||
|
*/
|
||||||
|
export const enrichDataBinding = (input, context) => {
|
||||||
|
// Only accept string inputs
|
||||||
|
if (!input || typeof input !== "string") {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
// Do a fast regex check if this looks like a mustache string
|
||||||
|
if (!looksLikeMustache.test(input)) {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
return mustache.render(input, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enriches each prop in a props object
|
||||||
|
*/
|
||||||
|
export const enrichDataBindings = (props, context) => {
|
||||||
|
let enrichedProps = {}
|
||||||
|
Object.entries(props).forEach(([key, value]) => {
|
||||||
|
enrichedProps[key] = enrichDataBinding(value, context)
|
||||||
|
})
|
||||||
|
return enrichedProps
|
||||||
|
}
|
|
@ -9,6 +9,9 @@ function confirmAppId(possibleAppId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryGetFromCookie({ cookies }) {
|
function tryGetFromCookie({ cookies }) {
|
||||||
|
if (!cookies) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
const cookie = cookies
|
const cookie = cookies
|
||||||
.split(COOKIE_SEPARATOR)
|
.split(COOKIE_SEPARATOR)
|
||||||
.find(cookie => cookie.trim().startsWith("budibase:currentapp"))
|
.find(cookie => cookie.trim().startsWith("budibase:currentapp"))
|
||||||
|
@ -30,7 +33,7 @@ function tryGetFromSubdomain() {
|
||||||
return confirmAppId(appId)
|
return confirmAppId(appId)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAppId = cookies => {
|
export const getAppId = (cookies = window.document.cookie) => {
|
||||||
const functions = [tryGetFromSubdomain, tryGetFromPath, tryGetFromCookie]
|
const functions = [tryGetFromSubdomain, tryGetFromPath, tryGetFromCookie]
|
||||||
// try getting the app Id in order
|
// try getting the app Id in order
|
||||||
let appId
|
let appId
|
||||||
|
@ -42,5 +45,3 @@ export const getAppId = cookies => {
|
||||||
}
|
}
|
||||||
return appId
|
return appId
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAppIdFromPath = getAppId
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
/**
|
||||||
|
* Helper to build a CSS string from a style object
|
||||||
|
*/
|
||||||
|
const buildStyleString = (styles, selected) => {
|
||||||
|
let str = ""
|
||||||
|
if (selected) {
|
||||||
|
styles.border = "2px solid #0055ff !important"
|
||||||
|
}
|
||||||
|
Object.entries(styles).forEach(([style, value]) => {
|
||||||
|
if (style && value != null) {
|
||||||
|
str += `${style}: ${value}; `
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Svelte action to apply correct component styles.
|
||||||
|
*/
|
||||||
|
export const styleable = (node, styles = {}) => {
|
||||||
|
let applyNormalStyles
|
||||||
|
let applyHoverStyles
|
||||||
|
|
||||||
|
// Creates event listeners and applies initial styles
|
||||||
|
const setupStyles = newStyles => {
|
||||||
|
const selected = newStyles.selected
|
||||||
|
const normalStyles = newStyles.normal || {}
|
||||||
|
const hoverStyles = {
|
||||||
|
...normalStyles,
|
||||||
|
...newStyles.hover,
|
||||||
|
}
|
||||||
|
|
||||||
|
applyNormalStyles = () => {
|
||||||
|
node.style = buildStyleString(normalStyles, selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
applyHoverStyles = () => {
|
||||||
|
node.style = buildStyleString(hoverStyles, selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add listeners to toggle hover styles
|
||||||
|
node.addEventListener("mouseover", applyHoverStyles)
|
||||||
|
node.addEventListener("mouseout", applyNormalStyles)
|
||||||
|
|
||||||
|
// Apply initial normal styles
|
||||||
|
applyNormalStyles()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes the current event listeners
|
||||||
|
const removeListeners = () => {
|
||||||
|
node.removeEventListener("mouseover", applyHoverStyles)
|
||||||
|
node.removeEventListener("mouseout", applyNormalStyles)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply initial styles
|
||||||
|
setupStyles(styles)
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Clean up old listeners and apply new ones on update
|
||||||
|
update: newStyles => {
|
||||||
|
removeListeners()
|
||||||
|
setupStyles(newStyles)
|
||||||
|
},
|
||||||
|
// Clean up listeners when component is destroyed
|
||||||
|
destroy: () => {
|
||||||
|
removeListeners()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,209 +0,0 @@
|
||||||
import { load, makePage, makeScreen } from "./testAppDef"
|
|
||||||
|
|
||||||
describe("binding", () => {
|
|
||||||
|
|
||||||
|
|
||||||
it("should bind to data in context", async () => {
|
|
||||||
const { dom } = await load(
|
|
||||||
makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "##builtin/screenslot",
|
|
||||||
text: "header one",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
makeScreen("/", {
|
|
||||||
_component: "testlib/list",
|
|
||||||
data: dataArray,
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "{{data.name}}",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
|
|
||||||
const screenRoot = rootDiv.children[0]
|
|
||||||
|
|
||||||
expect(screenRoot.children[0].children.length).toBe(2)
|
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe(dataArray[0].name)
|
|
||||||
expect(screenRoot.children[0].children[1].innerText).toBe(dataArray[1].name)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should bind to input in root", async () => {
|
|
||||||
const { dom } = await load(
|
|
||||||
makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "##builtin/screenslot",
|
|
||||||
text: "header one",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
makeScreen("/", {
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "{{inputid.value}}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: "inputid",
|
|
||||||
_component: "testlib/input",
|
|
||||||
value: "hello"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
|
|
||||||
const screenRoot = rootDiv.children[0]
|
|
||||||
|
|
||||||
expect(screenRoot.children[0].children.length).toBe(2)
|
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe("hello")
|
|
||||||
|
|
||||||
// change value of input
|
|
||||||
const input = dom.window.document.getElementsByClassName("input-inputid")[0]
|
|
||||||
|
|
||||||
changeInputValue(dom, input, "new value")
|
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe("new value")
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should bind to input in context", async () => {
|
|
||||||
const { dom } = await load(
|
|
||||||
makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "##builtin/screenslot",
|
|
||||||
text: "header one",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
makeScreen("/", {
|
|
||||||
_component: "testlib/list",
|
|
||||||
data: dataArray,
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "{{inputid.value}}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: "inputid",
|
|
||||||
_component: "testlib/input",
|
|
||||||
value: "hello"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
|
|
||||||
const screenRoot = rootDiv.children[0]
|
|
||||||
expect(screenRoot.children[0].children.length).toBe(4)
|
|
||||||
|
|
||||||
const firstHeader = screenRoot.children[0].children[0]
|
|
||||||
const firstInput = screenRoot.children[0].children[1]
|
|
||||||
const secondHeader = screenRoot.children[0].children[2]
|
|
||||||
const secondInput = screenRoot.children[0].children[3]
|
|
||||||
|
|
||||||
expect(firstHeader.innerText).toBe("hello")
|
|
||||||
expect(secondHeader.innerText).toBe("hello")
|
|
||||||
|
|
||||||
changeInputValue(dom, firstInput, "first input value")
|
|
||||||
expect(firstHeader.innerText).toBe("first input value")
|
|
||||||
|
|
||||||
changeInputValue(dom, secondInput, "second input value")
|
|
||||||
expect(secondHeader.innerText).toBe("second input value")
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should bind contextual component, to input in root context", async () => {
|
|
||||||
const { dom } = await load(
|
|
||||||
makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "##builtin/screenslot",
|
|
||||||
text: "header one",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
makeScreen("/", {
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_id: "inputid",
|
|
||||||
_component: "testlib/input",
|
|
||||||
value: "hello"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_component: "testlib/list",
|
|
||||||
data: dataArray,
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "{{parent.inputid.value}}",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
|
|
||||||
const screenRoot = rootDiv.children[0]
|
|
||||||
expect(screenRoot.children[0].children.length).toBe(2)
|
|
||||||
|
|
||||||
const input = screenRoot.children[0].children[0]
|
|
||||||
|
|
||||||
const firstHeader = screenRoot.children[0].children[1].children[0]
|
|
||||||
const secondHeader = screenRoot.children[0].children[1].children[0]
|
|
||||||
|
|
||||||
expect(firstHeader.innerText).toBe("hello")
|
|
||||||
expect(secondHeader.innerText).toBe("hello")
|
|
||||||
|
|
||||||
changeInputValue(dom, input, "new input value")
|
|
||||||
expect(firstHeader.innerText).toBe("new input value")
|
|
||||||
expect(secondHeader.innerText).toBe("new input value")
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
const changeInputValue = (dom, input, newValue) => {
|
|
||||||
var event = new dom.window.Event("change")
|
|
||||||
input.value = newValue
|
|
||||||
input.dispatchEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataArray = [
|
|
||||||
{
|
|
||||||
name: "katherine",
|
|
||||||
age: 30,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "steve",
|
|
||||||
age: 41,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
|
@ -1,172 +0,0 @@
|
||||||
import { load, makePage, makeScreen } from "./testAppDef"
|
|
||||||
|
|
||||||
describe("initialiseApp", () => {
|
|
||||||
it("should populate simple div with initial props", async () => {
|
|
||||||
const { dom } = await load(
|
|
||||||
makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
className: "my-test-class",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(dom.window.document.body.children.length).toBe(1)
|
|
||||||
const child = dom.window.document.body.children[0]
|
|
||||||
expect(child.className.includes("my-test-class")).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should populate child component with props", async () => {
|
|
||||||
const { dom } = await load(
|
|
||||||
makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "header one",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "header two",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
|
|
||||||
expect(rootDiv.children.length).toBe(2)
|
|
||||||
expect(rootDiv.children[0].tagName).toBe("H1")
|
|
||||||
expect(rootDiv.children[0].innerText).toBe("header one")
|
|
||||||
expect(rootDiv.children[1].tagName).toBe("H1")
|
|
||||||
expect(rootDiv.children[1].innerText).toBe("header two")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should append children when told to do so", async () => {
|
|
||||||
const { dom } = await load(
|
|
||||||
makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "header one",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "header two",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
append: true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
|
|
||||||
expect(rootDiv.children.length).toBe(3)
|
|
||||||
expect(rootDiv.children[0].tagName).toBe("DIV")
|
|
||||||
expect(rootDiv.children[0].className).toBe("default-child")
|
|
||||||
expect(rootDiv.children[1].tagName).toBe("H1")
|
|
||||||
expect(rootDiv.children[1].innerText).toBe("header one")
|
|
||||||
expect(rootDiv.children[2].tagName).toBe("H1")
|
|
||||||
expect(rootDiv.children[2].innerText).toBe("header two")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should populate page with correct screen", async () => {
|
|
||||||
const { dom } = await load(
|
|
||||||
makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "##builtin/screenslot",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
makeScreen("/", {
|
|
||||||
_component: "testlib/div",
|
|
||||||
className: "screen-class",
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
expect(rootDiv.children[0].children.length).toBe(1)
|
|
||||||
expect(
|
|
||||||
rootDiv.children[0].children[0].className.includes("screen-class")
|
|
||||||
).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should populate screen with children", async () => {
|
|
||||||
const { dom } = await load(
|
|
||||||
makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "##builtin/screenslot",
|
|
||||||
text: "header one",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
makeScreen("/", {
|
|
||||||
_component: "testlib/div",
|
|
||||||
className: "screen-class",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "header one",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "header two",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
|
|
||||||
const screenRoot = rootDiv.children[0]
|
|
||||||
|
|
||||||
expect(screenRoot.children.length).toBe(1)
|
|
||||||
expect(screenRoot.children[0].children.length).toBe(2)
|
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe("header one")
|
|
||||||
expect(screenRoot.children[0].children[1].innerText).toBe("header two")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should repeat elements that pass an array of contexts", async () => {
|
|
||||||
const { dom } = await load(
|
|
||||||
makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "##builtin/screenslot",
|
|
||||||
text: "header one",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
makeScreen("/", {
|
|
||||||
_component: "testlib/list",
|
|
||||||
data: [1,2,3,4],
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "header",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
|
|
||||||
const screenRoot = rootDiv.children[0]
|
|
||||||
|
|
||||||
expect(screenRoot.children[0].children.length).toBe(4)
|
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe("header")
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,174 +0,0 @@
|
||||||
import { load, makePage, makeScreen, walkComponentTree } from "./testAppDef"
|
|
||||||
import { isScreenSlot } from "../src/render/builtinComponents"
|
|
||||||
jest.mock("../src/render/getAppId", () => ({
|
|
||||||
getAppId: () => "TEST_APP_ID"
|
|
||||||
}))
|
|
||||||
|
|
||||||
describe("screenRouting", () => {
|
|
||||||
it("should load correct screen, for initial URL", async () => {
|
|
||||||
const { page, screens } = pageWith3Screens()
|
|
||||||
const { dom } = await load(page, screens, "/screen2")
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
|
|
||||||
const screenRoot = rootDiv.children[0]
|
|
||||||
|
|
||||||
expect(screenRoot.children.length).toBe(1)
|
|
||||||
expect(screenRoot.children[0].children.length).toBe(1)
|
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe("screen 2")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should load correct screen, for initial URL, when appRootPath is something", async () => {
|
|
||||||
const { page, screens } = pageWith3Screens()
|
|
||||||
const { dom } = await load(page, screens, "/TEST_APP_ID/screen2", "127.0.0.1")
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
|
|
||||||
const screenRoot = rootDiv.children[0]
|
|
||||||
|
|
||||||
expect(screenRoot.children.length).toBe(1)
|
|
||||||
expect(screenRoot.children[0].children.length).toBe(1)
|
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe("screen 2")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should be able to route to the correct screen", async () => {
|
|
||||||
const { page, screens } = pageWith3Screens()
|
|
||||||
const { dom, app } = await load(page, screens, "/screen2")
|
|
||||||
|
|
||||||
app.routeTo()("/screen3")
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
|
|
||||||
const screenRoot = rootDiv.children[0]
|
|
||||||
|
|
||||||
expect(screenRoot.children.length).toBe(1)
|
|
||||||
expect(screenRoot.children[0].children.length).toBe(1)
|
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe("screen 3")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should be able to route to the correct screen, when appRootPath is something", async () => {
|
|
||||||
const { page, screens } = pageWith3Screens()
|
|
||||||
const { dom, app } = await load(
|
|
||||||
page,
|
|
||||||
screens,
|
|
||||||
"/TEST_APP_ID/screen2",
|
|
||||||
"127.0.0.1"
|
|
||||||
)
|
|
||||||
|
|
||||||
app.routeTo()("/screen3")
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
|
|
||||||
const screenRoot = rootDiv.children[0]
|
|
||||||
|
|
||||||
expect(screenRoot.children.length).toBe(1)
|
|
||||||
expect(screenRoot.children[0].children.length).toBe(1)
|
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe("screen 3")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should destroy and unsubscribe all components on a screen whe screen is changed", async () => {
|
|
||||||
const { page, screens } = pageWith3Screens()
|
|
||||||
const { app } = await load(page, screens, "/screen2")
|
|
||||||
|
|
||||||
const nodes = createTrackerNodes(app)
|
|
||||||
|
|
||||||
app.routeTo()("/screen3")
|
|
||||||
|
|
||||||
expect(nodes.length > 0).toBe(true)
|
|
||||||
expect(
|
|
||||||
nodes.some(n => n.isDestroyed === false && isUnderScreenSlot(n.node))
|
|
||||||
).toBe(false)
|
|
||||||
expect(
|
|
||||||
nodes.some(n => n.isUnsubscribed === false && isUnderScreenSlot(n.node))
|
|
||||||
).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should not destroy and unsubscribe page and screenslot components when screen is changed", async () => {
|
|
||||||
const { page, screens } = pageWith3Screens()
|
|
||||||
const { app } = await load(page, screens, "/screen2")
|
|
||||||
|
|
||||||
const nodes = createTrackerNodes(app)
|
|
||||||
|
|
||||||
app.routeTo()("/screen3")
|
|
||||||
|
|
||||||
expect(nodes.length > 0).toBe(true)
|
|
||||||
expect(
|
|
||||||
nodes.some(n => n.isDestroyed === true && !isUnderScreenSlot(n.node))
|
|
||||||
).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const createTrackerNodes = app => {
|
|
||||||
const nodes = []
|
|
||||||
walkComponentTree(app.rootNode(), n => {
|
|
||||||
if (!n.component) return
|
|
||||||
const tracker = { node: n, isDestroyed: false, isUnsubscribed: false }
|
|
||||||
const _destroy = n.component.$destroy
|
|
||||||
n.component.$destroy = () => {
|
|
||||||
_destroy()
|
|
||||||
tracker.isDestroyed = true
|
|
||||||
}
|
|
||||||
const _unsubscribe = n.unsubscribe
|
|
||||||
if (!_unsubscribe) {
|
|
||||||
tracker.isUnsubscribed = undefined
|
|
||||||
} else {
|
|
||||||
n.unsubscribe = () => {
|
|
||||||
_unsubscribe()
|
|
||||||
tracker.isUnsubscribed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nodes.push(tracker)
|
|
||||||
})
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUnderScreenSlot = node =>
|
|
||||||
node.parentNode &&
|
|
||||||
(isScreenSlot(node.parentNode.props._component) ||
|
|
||||||
isUnderScreenSlot(node.parentNode))
|
|
||||||
|
|
||||||
const pageWith3Screens = () => ({
|
|
||||||
page: makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "##builtin/screenslot",
|
|
||||||
text: "header one",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
screens: [
|
|
||||||
makeScreen("/", {
|
|
||||||
_component: "testlib/div",
|
|
||||||
className: "screen-class",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "screen 1",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
makeScreen("/screen2", {
|
|
||||||
_component: "testlib/div",
|
|
||||||
className: "screen-class",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "screen 2",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
makeScreen("/screen3", {
|
|
||||||
_component: "testlib/div",
|
|
||||||
className: "screen-class",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "screen 3",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
|
@ -1,244 +0,0 @@
|
||||||
import jsdom, { JSDOM } from "jsdom"
|
|
||||||
import { loadBudibase } from "../src/index"
|
|
||||||
|
|
||||||
export const APP_ID = "TEST_APP_ID"
|
|
||||||
|
|
||||||
export const load = async (page, screens, url, host = "test.com") => {
|
|
||||||
screens = screens || []
|
|
||||||
url = url || "/"
|
|
||||||
|
|
||||||
const fullUrl = `http://${host}${url}`
|
|
||||||
const cookieJar = new jsdom.CookieJar()
|
|
||||||
const cookie = `${btoa("{}")}.${btoa(`{"appId":"${APP_ID}"}`)}.signature`
|
|
||||||
cookieJar.setCookie(
|
|
||||||
`budibase:${APP_ID}:local=${cookie};domain=${host};path=/`,
|
|
||||||
fullUrl,
|
|
||||||
{
|
|
||||||
looseMode: false,
|
|
||||||
},
|
|
||||||
() => {}
|
|
||||||
)
|
|
||||||
|
|
||||||
const dom = new JSDOM("<!DOCTYPE html><html><body></body><html>", {
|
|
||||||
url: fullUrl,
|
|
||||||
cookieJar,
|
|
||||||
})
|
|
||||||
|
|
||||||
autoAssignIds(page.props)
|
|
||||||
for (let s of screens) {
|
|
||||||
autoAssignIds(s.props)
|
|
||||||
}
|
|
||||||
setAppDef(dom.window, page, screens)
|
|
||||||
addWindowGlobals(dom.window, page, screens, {
|
|
||||||
hierarchy: {},
|
|
||||||
actions: [],
|
|
||||||
triggers: [],
|
|
||||||
})
|
|
||||||
setComponentCodeMeta(page, screens)
|
|
||||||
const app = await loadBudibase({
|
|
||||||
componentLibraries: allLibs(dom.window),
|
|
||||||
window: dom.window,
|
|
||||||
localStorage: createLocalStorage(),
|
|
||||||
})
|
|
||||||
return { dom, app }
|
|
||||||
}
|
|
||||||
|
|
||||||
const addWindowGlobals = (window, page, screens) => {
|
|
||||||
window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
|
|
||||||
page,
|
|
||||||
screens,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makePage = props => ({ props })
|
|
||||||
export const makeScreen = (route, props) => ({
|
|
||||||
props,
|
|
||||||
routing: { route, accessLevelId: "" },
|
|
||||||
})
|
|
||||||
|
|
||||||
export const timeout = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
||||||
|
|
||||||
export const walkComponentTree = (node, action) => {
|
|
||||||
action(node)
|
|
||||||
|
|
||||||
// works for nodes or props
|
|
||||||
const children = node.children || node._children
|
|
||||||
|
|
||||||
if (children) {
|
|
||||||
for (let child of children) {
|
|
||||||
walkComponentTree(child, action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this happens for real by the builder...
|
|
||||||
// ..this only assigns _ids when missing
|
|
||||||
const autoAssignIds = (props, count = 0) => {
|
|
||||||
if (!props._id) {
|
|
||||||
props._id = `auto_id_${count}`
|
|
||||||
}
|
|
||||||
if (props._children) {
|
|
||||||
for (let child of props._children) {
|
|
||||||
count += 1
|
|
||||||
autoAssignIds(child, count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// any component with an id that include "based_on_store" is
|
|
||||||
// assumed to have code that depends on store value
|
|
||||||
const setComponentCodeMeta = (page, screens) => {
|
|
||||||
const setComponentCodeMeta_single = props => {
|
|
||||||
walkComponentTree(props, c => {
|
|
||||||
if (c._id.indexOf("based_on_store") >= 0) {
|
|
||||||
c._codeMeta = { dependsOnStore: true }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
setComponentCodeMeta_single(page.props)
|
|
||||||
for (let s of screens || []) {
|
|
||||||
setComponentCodeMeta_single(s.props)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setAppDef = (window, page, screens) => {
|
|
||||||
window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
|
|
||||||
componentLibraries: [],
|
|
||||||
page,
|
|
||||||
screens,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const allLibs = window => ({
|
|
||||||
testlib: maketestlib(window),
|
|
||||||
})
|
|
||||||
|
|
||||||
const createLocalStorage = () => {
|
|
||||||
const data = {}
|
|
||||||
return {
|
|
||||||
getItem: key => data[key],
|
|
||||||
setItem: (key, value) => (data[key] = value),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const maketestlib = window => ({
|
|
||||||
div: function(opts) {
|
|
||||||
const node = window.document.createElement("DIV")
|
|
||||||
const defaultChild = window.document.createElement("DIV")
|
|
||||||
defaultChild.className = "default-child"
|
|
||||||
node.appendChild(defaultChild)
|
|
||||||
|
|
||||||
let currentProps = { ...opts.props }
|
|
||||||
let childNodes = []
|
|
||||||
|
|
||||||
const set = props => {
|
|
||||||
currentProps = Object.assign(currentProps, props)
|
|
||||||
node.className = currentProps.className || ""
|
|
||||||
if (currentProps._children && currentProps._children.length > 0) {
|
|
||||||
if (currentProps.append) {
|
|
||||||
for (let c of childNodes) {
|
|
||||||
node.removeChild(c)
|
|
||||||
}
|
|
||||||
const components = currentProps._bb.attachChildren(node, {
|
|
||||||
hydrate: false,
|
|
||||||
})
|
|
||||||
childNodes = components.map(c => c.component._element)
|
|
||||||
} else {
|
|
||||||
currentProps._bb.attachChildren(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$destroy = () => opts.target.removeChild(node)
|
|
||||||
|
|
||||||
this.$set = set
|
|
||||||
this._element = node
|
|
||||||
set(opts.props)
|
|
||||||
opts.target.appendChild(node)
|
|
||||||
},
|
|
||||||
|
|
||||||
h1: function(opts) {
|
|
||||||
const node = window.document.createElement("H1")
|
|
||||||
|
|
||||||
let currentProps = { ...opts.props }
|
|
||||||
|
|
||||||
const set = props => {
|
|
||||||
currentProps = Object.assign(currentProps, props)
|
|
||||||
if (currentProps.text) {
|
|
||||||
node.innerText = currentProps.text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$destroy = () => opts.target.removeChild(node)
|
|
||||||
|
|
||||||
this.$set = set
|
|
||||||
this._element = node
|
|
||||||
set(opts.props)
|
|
||||||
opts.target.appendChild(node)
|
|
||||||
},
|
|
||||||
|
|
||||||
button: function(opts) {
|
|
||||||
const node = window.document.createElement("BUTTON")
|
|
||||||
|
|
||||||
let currentProps = { ...opts.props }
|
|
||||||
|
|
||||||
const set = props => {
|
|
||||||
currentProps = Object.assign(currentProps, props)
|
|
||||||
if (currentProps.onClick) {
|
|
||||||
node.addEventListener("click", () => {
|
|
||||||
currentProps._bb.call("onClick")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$destroy = () => opts.target.removeChild(node)
|
|
||||||
|
|
||||||
this.$set = set
|
|
||||||
this._element = node
|
|
||||||
set(opts.props)
|
|
||||||
opts.target.appendChild(node)
|
|
||||||
},
|
|
||||||
|
|
||||||
list: function(opts) {
|
|
||||||
const node = window.document.createElement("DIV")
|
|
||||||
|
|
||||||
let currentProps = { ...opts.props }
|
|
||||||
|
|
||||||
const set = props => {
|
|
||||||
currentProps = Object.assign(currentProps, props)
|
|
||||||
if (currentProps._children && currentProps._children.length > 0) {
|
|
||||||
currentProps._bb.attachChildren(node, {
|
|
||||||
context: currentProps.data || {},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$destroy = () => opts.target.removeChild(node)
|
|
||||||
|
|
||||||
this.$set = set
|
|
||||||
this._element = node
|
|
||||||
set(opts.props)
|
|
||||||
opts.target.appendChild(node)
|
|
||||||
},
|
|
||||||
|
|
||||||
input: function(opts) {
|
|
||||||
const node = window.document.createElement("INPUT")
|
|
||||||
let currentProps = { ...opts.props }
|
|
||||||
|
|
||||||
const set = props => {
|
|
||||||
currentProps = Object.assign(currentProps, props)
|
|
||||||
opts.props._bb.setBinding("value", props.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
node.addEventListener("change", e => {
|
|
||||||
opts.props._bb.setBinding("value", e.target.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$destroy = () => opts.target.removeChild(node)
|
|
||||||
|
|
||||||
this.$set = set
|
|
||||||
this._element = node
|
|
||||||
set(opts.props)
|
|
||||||
opts.target.appendChild(node)
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,16 +0,0 @@
|
||||||
<script>
|
|
||||||
export let _bb
|
|
||||||
export let className = ""
|
|
||||||
|
|
||||||
let containerElement
|
|
||||||
let hasLoaded
|
|
||||||
|
|
||||||
$: {
|
|
||||||
if (containerElement) {
|
|
||||||
_bb.attachChildren(containerElement)
|
|
||||||
hasLoaded = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div bind:this={containerElement} class={className} />
|
|
File diff suppressed because it is too large
Load Diff
|
@ -81,12 +81,13 @@
|
||||||
"lodash": "^4.17.13",
|
"lodash": "^4.17.13",
|
||||||
"mustache": "^4.0.1",
|
"mustache": "^4.0.1",
|
||||||
"node-fetch": "^2.6.0",
|
"node-fetch": "^2.6.0",
|
||||||
|
"open": "^7.3.0",
|
||||||
"pino-pretty": "^4.0.0",
|
"pino-pretty": "^4.0.0",
|
||||||
"pouchdb": "^7.2.1",
|
"pouchdb": "^7.2.1",
|
||||||
"pouchdb-all-dbs": "^1.0.2",
|
"pouchdb-all-dbs": "^1.0.2",
|
||||||
"pouchdb-replication-stream": "^1.2.9",
|
"pouchdb-replication-stream": "^1.2.9",
|
||||||
"sanitize-s3-objectkey": "^0.0.1",
|
"sanitize-s3-objectkey": "^0.0.1",
|
||||||
"svelte": "^3.29.4",
|
"svelte": "^3.30.0",
|
||||||
"tar-fs": "^2.1.0",
|
"tar-fs": "^2.1.0",
|
||||||
"to-json-schema": "^0.2.5",
|
"to-json-schema": "^0.2.5",
|
||||||
"uuid": "^3.3.2",
|
"uuid": "^3.3.2",
|
||||||
|
|
|
@ -26,6 +26,7 @@ const {
|
||||||
const { MAIN, UNAUTHENTICATED, PageTypes } = require("../../constants/pages")
|
const { MAIN, UNAUTHENTICATED, PageTypes } = require("../../constants/pages")
|
||||||
const { HOME_SCREEN } = require("../../constants/screens")
|
const { HOME_SCREEN } = require("../../constants/screens")
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
|
const { USERS_TABLE_SCHEMA } = require("../../constants")
|
||||||
|
|
||||||
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
||||||
|
|
||||||
|
@ -67,6 +68,9 @@ async function createInstance(template) {
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
throw "Error loading database dump from template."
|
throw "Error loading database dump from template."
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// create the users table
|
||||||
|
await db.put(USERS_TABLE_SCHEMA)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { _id: appId }
|
return { _id: appId }
|
||||||
|
|
|
@ -6,7 +6,9 @@ const {
|
||||||
generateRowID,
|
generateRowID,
|
||||||
DocumentTypes,
|
DocumentTypes,
|
||||||
SEPARATOR,
|
SEPARATOR,
|
||||||
|
ViewNames,
|
||||||
} = require("../../db/utils")
|
} = require("../../db/utils")
|
||||||
|
const usersController = require("./user")
|
||||||
const { cloneDeep } = require("lodash")
|
const { cloneDeep } = require("lodash")
|
||||||
|
|
||||||
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
|
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
|
||||||
|
@ -118,6 +120,16 @@ exports.save = async function(ctx) {
|
||||||
table,
|
table,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Creation of a new user goes to the user controller
|
||||||
|
if (!existingRow && row.tableId === ViewNames.USERS) {
|
||||||
|
try {
|
||||||
|
await usersController.create(ctx)
|
||||||
|
} catch (err) {
|
||||||
|
ctx.body = { errors: [err.message] }
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (existingRow) {
|
if (existingRow) {
|
||||||
const response = await db.put(row)
|
const response = await db.put(row)
|
||||||
row._rev = response.rev
|
row._rev = response.rev
|
||||||
|
@ -315,8 +327,8 @@ exports.fetchEnrichedRow = async function(ctx) {
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
}
|
}
|
||||||
|
|
||||||
function coerceRowValues(rec, table) {
|
function coerceRowValues(record, table) {
|
||||||
const row = cloneDeep(rec)
|
const row = cloneDeep(record)
|
||||||
for (let [key, value] of Object.entries(row)) {
|
for (let [key, value] of Object.entries(row)) {
|
||||||
const field = table.schema[key]
|
const field = table.schema[key]
|
||||||
if (!field) continue
|
if (!field) continue
|
||||||
|
@ -347,6 +359,11 @@ const TYPE_TRANSFORM_MAP = {
|
||||||
[null]: "",
|
[null]: "",
|
||||||
[undefined]: undefined,
|
[undefined]: undefined,
|
||||||
},
|
},
|
||||||
|
longform: {
|
||||||
|
"": "",
|
||||||
|
[null]: "",
|
||||||
|
[undefined]: undefined,
|
||||||
|
},
|
||||||
number: {
|
number: {
|
||||||
"": null,
|
"": null,
|
||||||
[null]: null,
|
[null]: null,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const bcrypt = require("../../utilities/bcrypt")
|
const bcrypt = require("../../utilities/bcrypt")
|
||||||
const { generateUserID, getUserParams } = require("../../db/utils")
|
const { generateUserID, getUserParams, ViewNames } = require("../../db/utils")
|
||||||
const {
|
const {
|
||||||
BUILTIN_LEVEL_ID_ARRAY,
|
BUILTIN_LEVEL_ID_ARRAY,
|
||||||
} = require("../../utilities/security/accessLevels")
|
} = require("../../utilities/security/accessLevels")
|
||||||
|
@ -11,7 +11,7 @@ const {
|
||||||
exports.fetch = async function(ctx) {
|
exports.fetch = async function(ctx) {
|
||||||
const database = new CouchDB(ctx.user.appId)
|
const database = new CouchDB(ctx.user.appId)
|
||||||
const data = await database.allDocs(
|
const data = await database.allDocs(
|
||||||
getUserParams(null, {
|
getUserParams("", {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -44,6 +44,7 @@ exports.create = async function(ctx) {
|
||||||
type: "user",
|
type: "user",
|
||||||
accessLevelId,
|
accessLevelId,
|
||||||
permissions: permissions || [BUILTIN_PERMISSION_NAMES.POWER],
|
permissions: permissions || [BUILTIN_PERMISSION_NAMES.POWER],
|
||||||
|
tableId: ViewNames.USERS,
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -28,7 +28,7 @@ module.exports.definition = {
|
||||||
accessLevelId: {
|
accessLevelId: {
|
||||||
type: "string",
|
type: "string",
|
||||||
title: "Access Level",
|
title: "Access Level",
|
||||||
enum: accessLevels.BUILTIN_LEVEL_IDS,
|
enum: accessLevels.BUILTIN_LEVEL_ID_ARRAY,
|
||||||
pretty: accessLevels.BUILTIN_LEVEL_NAME_ARRAY,
|
pretty: accessLevels.BUILTIN_LEVEL_NAME_ARRAY,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue