Browse Source

merge

gh-pages
Martin McKeaveney 6 years ago
parent
commit
89615e58a7
  1. 0
      .github/AUTHORS.md
  2. 0
      .github/CODE_OF_CONDUCT.md
  3. 2
      .github/CONTRIBUTING.md
  4. 4
      README.md
  5. 4
      package.json
  6. 2
      packages/builder/cypress/integration/createTable.spec.js
  7. 6
      packages/builder/cypress/integration/createUser.spec.js
  8. 3
      packages/builder/cypress/setup.js
  9. 39
      packages/builder/cypress/support/commands.js
  10. 11
      packages/builder/package.json
  11. 111
      packages/builder/rollup.config.js
  12. 43
      packages/builder/src/builderStore/fetchBindableProperties.js
  13. 14
      packages/builder/src/builderStore/getNewComponentName.js
  14. 75
      packages/builder/src/builderStore/index.js
  15. 5
      packages/builder/src/builderStore/replaceBindings.js
  16. 405
      packages/builder/src/builderStore/store/frontend.js
  17. 2
      packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js
  18. 3
      packages/builder/src/builderStore/store/screenTemplates/utils/Component.js
  19. 3
      packages/builder/src/builderStore/store/screenTemplates/utils/Screen.js
  20. 18
      packages/builder/src/builderStore/storeUtils.js
  21. 2
      packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
  22. 27
      packages/builder/src/components/backend/DataTable/RowFieldControl.svelte
  23. 4
      packages/builder/src/components/backend/DataTable/api.js
  24. 10
      packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
  25. 14
      packages/builder/src/components/backend/DataTable/modals/CreateEditRowModal.svelte
  26. 3
      packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte
  27. 3
      packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte
  28. 7
      packages/builder/src/components/settings/SettingsModal.svelte
  29. 43
      packages/builder/src/components/settings/UserRow.svelte
  30. 114
      packages/builder/src/components/settings/tabs/Users.svelte
  31. 1
      packages/builder/src/components/settings/tabs/index.js
  32. 18
      packages/builder/src/components/start/CreateAppModal.svelte
  33. 16
      packages/builder/src/components/start/Steps/User.svelte
  34. 178
      packages/builder/src/components/userInterface/AppPreview/CurrentItemPreview.svelte
  35. 52
      packages/builder/src/components/userInterface/AppPreview/iframeTemplate.html
  36. 78
      packages/builder/src/components/userInterface/AppPreview/iframeTemplate.js
  37. 25
      packages/builder/src/components/userInterface/ComponentDropdownMenu.svelte
  38. 7
      packages/builder/src/components/userInterface/ComponentNavigationTree/ComponentTree.svelte
  39. 77
      packages/builder/src/components/userInterface/ComponentNavigationTree/LayoutDropdownMenu.svelte
  40. 29
      packages/builder/src/components/userInterface/ComponentNavigationTree/PathTree.svelte
  41. 27
      packages/builder/src/components/userInterface/ComponentNavigationTree/ScreenDropdownMenu.svelte
  42. 2
      packages/builder/src/components/userInterface/ComponentNavigationTree/dragDropStore.js
  43. 2
      packages/builder/src/components/userInterface/ComponentNavigationTree/index.svelte
  44. 39
      packages/builder/src/components/userInterface/ComponentPropertiesPanel.svelte
  45. 10
      packages/builder/src/components/userInterface/ComponentSelectionList.svelte
  46. 3
      packages/builder/src/components/userInterface/ComponentsPaneSwitcher.svelte
  47. 7
      packages/builder/src/components/userInterface/DesignView.svelte
  48. 9
      packages/builder/src/components/userInterface/EventsEditor/EventEditorModal.svelte
  49. 127
      packages/builder/src/components/userInterface/EventsEditor/EventsEditor.svelte
  50. 53
      packages/builder/src/components/userInterface/EventsEditor/StateBindingCascader.svelte
  51. 6
      packages/builder/src/components/userInterface/EventsEditor/actions/CreateRow.svelte
  52. 6
      packages/builder/src/components/userInterface/EventsEditor/actions/DeleteRow.svelte
  53. 6
      packages/builder/src/components/userInterface/EventsEditor/actions/SaveFields.svelte
  54. 6
      packages/builder/src/components/userInterface/EventsEditor/actions/SaveRow.svelte
  55. 6
      packages/builder/src/components/userInterface/EventsEditor/actions/UpdateRow.svelte
  56. 3
      packages/builder/src/components/userInterface/FlatButton.svelte
  57. 1
      packages/builder/src/components/userInterface/FlatButtonGroup.svelte
  58. 81
      packages/builder/src/components/userInterface/FrontendNavigatePane.svelte
  59. 41
      packages/builder/src/components/userInterface/Layout.svelte
  60. 13
      packages/builder/src/components/userInterface/LayoutSelect.svelte
  61. 26
      packages/builder/src/components/userInterface/NewLayoutModal.svelte
  62. 6
      packages/builder/src/components/userInterface/NewScreenModal.svelte
  63. 44
      packages/builder/src/components/userInterface/PageLayout.svelte
  64. 3
      packages/builder/src/components/userInterface/PagesComponents.svelte
  65. 65
      packages/builder/src/components/userInterface/PagesList.svelte
  66. 18
      packages/builder/src/components/userInterface/PropertyControl.svelte
  67. 6
      packages/builder/src/components/userInterface/ScreenSelect.svelte
  68. 41
      packages/builder/src/components/userInterface/SettingsView.svelte
  69. 23
      packages/builder/src/components/userInterface/TableViewSelect.svelte
  70. 4
      packages/builder/src/components/userInterface/assetParsing/createProps.js
  71. 0
      packages/builder/src/components/userInterface/assetParsing/getRootComponent.js
  72. 0
      packages/builder/src/components/userInterface/assetParsing/searchComponents.js
  73. 0
      packages/builder/src/components/userInterface/assetParsing/splitRootComponentName.js
  74. 0
      packages/builder/src/components/userInterface/assetParsing/types.js
  75. 60
      packages/builder/src/components/userInterface/pagesParsing/renameScreen.js
  76. 56
      packages/builder/src/components/userInterface/temporaryPanelStructure.js
  77. 10
      packages/builder/src/constants/backend/index.js
  78. 32
      packages/builder/src/constants/index.js
  79. 7
      packages/builder/src/pages/[application]/_reset.svelte
  80. 4
      packages/builder/src/pages/[application]/automate/[automation]/_layout.svelte
  81. 2
      packages/builder/src/pages/[application]/data/_layout.svelte
  82. 0
      packages/builder/src/pages/[application]/design/[assetType]/[asset]/_fallback.svelte
  83. 63
      packages/builder/src/pages/[application]/design/[assetType]/[asset]/_layout.svelte
  84. 7
      packages/builder/src/pages/[application]/design/[assetType]/_layout.svelte
  85. 17
      packages/builder/src/pages/[application]/design/[assetType]/index.svelte
  86. 69
      packages/builder/src/pages/[application]/design/[page]/[screen]/_layout.svelte
  87. 8
      packages/builder/src/pages/[application]/design/[page]/_layout.svelte
  88. 4
      packages/builder/src/pages/[application]/design/[page]/index.svelte
  89. 6
      packages/builder/src/pages/[application]/design/index.svelte
  90. 4
      packages/builder/tests/createProps.spec.js
  91. 20
      packages/builder/tests/fetchBindableProperties.spec.js
  92. 51
      packages/builder/tests/generate_css.spec.js
  93. 2
      packages/builder/tests/searchComponentsProps.spec.js
  94. 1
      packages/builder/tests/testData.js
  95. 3160
      packages/builder/yarn.lock
  96. 1
      packages/client/.gitignore
  97. 13
      packages/client/babel.config.js
  98. 47
      packages/client/package.json
  99. 21
      packages/client/rollup.config.js
  100. 96
      packages/client/src/api/api.js

0
AUTHORS.md → .github/AUTHORS.md

0
CODE_OF_CONDUCT.md → .github/CODE_OF_CONDUCT.md

2
CONTRIBUTING.md → .github/CONTRIBUTING.md

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

4
README.md

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

4
package.json

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

2
packages/builder/cypress/integration/createTable.spec.js

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

6
packages/builder/cypress/integration/createUser.spec.js

@ -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.com", "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')
}) })
}) })

3
packages/builder/cypress/setup.js

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

39
packages/builder/cypress/support/commands.js

@ -44,9 +44,9 @@ Cypress.Commands.add("createApp", name => {
cy.contains("Next").click() cy.contains("Next").click()
cy.get("input[name=username]") cy.get("input[name=email]")
.click() .click()
.type("test") .type("test@test.com")
cy.get("input[name=password]") cy.get("input[name=password]")
.click() .click()
.type("test") .type("test")
@ -111,25 +111,28 @@ Cypress.Commands.add("addRow", values => {
}) })
}) })
Cypress.Commands.add("createUser", (username, password, accessLevel) => { Cypress.Commands.add("createUser", (email, password, role) => {
// 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(".modal").within(() => {
cy.get("[name=Password]") cy.get("input")
.first() .first()
.type(password) .type(password)
cy.get("select") cy.get("input")
.first() .eq(1)
.select(accessLevel) .type(email)
cy.get("select")
// Save .first()
cy.get(".inputs") .select(role)
.contains("Create")
.click() // Save
cy.get(".buttons")
.contains("Create Row")
.click()
})
}) })
Cypress.Commands.add("addHeadlineComponent", text => { Cypress.Commands.add("addHeadlineComponent", text => {

11
packages/builder/package.json

@ -63,7 +63,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.50.2", "@budibase/bbui": "^1.52.2",
"@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,9 +105,9 @@
"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-html": "^0.2.1",
"rollup-plugin-livereload": "^1.0.0", "rollup-plugin-livereload": "^1.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",
@ -115,7 +116,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"

111
packages/builder/rollup.config.js

@ -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"
@ -11,106 +11,12 @@ import copy from "rollup-plugin-copy"
import css from "rollup-plugin-css-only" import css from "rollup-plugin-css-only"
import replace from "rollup-plugin-replace" import replace from "rollup-plugin-replace"
import json from "@rollup/plugin-json" import json from "@rollup/plugin-json"
import html from "rollup-plugin-html"
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",
@ -170,10 +76,6 @@ export default {
{ src: "src/index.html", dest: outputpath }, { src: "src/index.html", dest: outputpath },
{ src: "src/favicon.png", dest: outputpath }, { src: "src/favicon.png", dest: outputpath },
{ src: "assets", dest: outputpath }, { src: "assets", dest: outputpath },
{
src: "node_modules/@budibase/client/dist/budibase-client.esm.mjs",
dest: outputpath,
},
{ {
src: "node_modules/@budibase/bbui/dist/bbui.css", src: "node_modules/@budibase/bbui/dist/bbui.css",
dest: outputpath, dest: outputpath,
@ -224,13 +126,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"],
@ -248,5 +144,6 @@ export default {
// instead of npm run dev), minify // instead of npm run dev), minify
production && terser(), production && terser(),
json(), json(),
html(),
], ],
} }

43
packages/builder/src/builderStore/fetchBindableProperties.js

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

14
packages/builder/src/builderStore/getNewComponentName.js

@ -2,6 +2,8 @@ import { walkProps } from "./storeUtils"
import { get_capitalised_name } from "../helpers" import { get_capitalised_name } from "../helpers"
import { get } from "svelte/store" import { get } from "svelte/store"
import { allScreens } from "builderStore" import { allScreens } from "builderStore"
import { FrontendTypes } from "../constants"
import { currentAsset } from "."
export default function(component, state) { export default function(component, state) {
const capitalised = get_capitalised_name( const capitalised = get_capitalised_name(
@ -19,14 +21,16 @@ export default function(component, state) {
}) })
} }
// check page first // check layouts first
findMatches(state.pages[state.currentPageName].props) for (let layout of state.layouts) {
findMatches(layout.props)
}
// if viewing screen, check current screen for duplicate // if viewing screen, check current screen for duplicate
if (state.currentFrontEndType === "screen") { if (state.currentFrontEndType === FrontendTypes.SCREEN) {
findMatches(state.currentPreviewItem.props) findMatches(get(currentAsset).props)
} else { } else {
// viewing master page - need to find against all screens // viewing a layout - need to find against all screens
for (let screen of get(allScreens)) { for (let screen of get(allScreens)) {
findMatches(screen.props) findMatches(screen.props)
} }

75
packages/builder/src/builderStore/index.js

@ -4,37 +4,74 @@ import { getAutomationStore } from "./store/automation/"
import { getThemeStore } from "./store/theme" import { getThemeStore } from "./store/theme"
import { derived } from "svelte/store" import { derived } from "svelte/store"
import analytics from "analytics" import analytics from "analytics"
import { LAYOUT_NAMES } from "../constants"
import { makePropsSafe } from "components/userInterface/assetParsing/createProps"
export const store = getFrontendStore() export const store = getFrontendStore()
export const backendUiStore = getBackendUiStore() export const backendUiStore = getBackendUiStore()
export const automationStore = getAutomationStore() export const automationStore = getAutomationStore()
export const themeStore = getThemeStore() export const themeStore = getThemeStore()
export const allScreens = derived(store, $store => { export const currentAsset = derived(store, $store => {
let screens = [] const layout = $store.layouts
if ($store.pages == null) { ? $store.layouts.find(layout => layout._id === $store.currentAssetId)
return screens : null
}
for (let page of Object.values($store.pages)) { if (layout) return layout
screens = screens.concat(page._screens)
} const screen = $store.screens
return screens ? $store.screens.find(screen => screen._id === $store.currentAssetId)
: null
if (screen) return screen
return null
}) })
export const currentScreens = derived(store, $store => { export const selectedComponent = derived(
const currentScreens = $store.pages[$store.currentPageName]?._screens [store, currentAsset],
if (currentScreens == null) { ([$store, $currentAsset]) => {
return [] if (!$currentAsset || !$store.selectedComponentId) return null
function traverse(node, callback) {
if (node._id === $store.selectedComponentId) return callback(node)
if (node._children) {
node._children.forEach(child => traverse(child, callback))
}
if (node.props) {
traverse(node.props, callback)
}
}
let component
traverse($currentAsset, found => {
const componentIdentifier = found._component ?? found.props._component
const componentDef = componentIdentifier.startsWith("##")
? found
: $store.components[componentIdentifier]
component = makePropsSafe(componentDef, found)
})
return component
} }
return Array.isArray(currentScreens) )
? currentScreens
: Object.values(currentScreens) export const currentAssetName = derived(store, () => {
return currentAsset.name
}) })
export const selectedPage = derived(store, $store => { // leave this as before for consistency
if (!$store.pages) return null export const allScreens = derived(store, $store => {
return $store.screens
})
return $store.pages[$store.currentPageName || "main"] export const mainLayout = derived(store, $store => {
return $store.layouts?.find(
layout => layout._id === LAYOUT_NAMES.MASTER.PRIVATE
)
}) })
export const initialise = async () => { export const initialise = async () => {

5
packages/builder/src/builderStore/replaceBindings.js

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

405
packages/builder/src/builderStore/store/frontend.js

@ -4,34 +4,36 @@ import {
createProps, createProps,
getBuiltin, getBuiltin,
makePropsSafe, makePropsSafe,
} from "components/userInterface/pagesParsing/createProps" } from "components/userInterface/assetParsing/createProps"
import { allScreens, backendUiStore, selectedPage } from "builderStore" import {
import { generate_screen_css } from "../generate_css" allScreens,
backendUiStore,
currentAsset,
mainLayout,
selectedComponent,
} from "builderStore"
import { fetchComponentLibDefinitions } from "../loadComponentLibraries" import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
import api from "../api" import api from "../api"
import { DEFAULT_PAGES_OBJECT } from "../../constants" import { FrontendTypes } from "../../constants"
import getNewComponentName from "../getNewComponentName" import getNewComponentName from "../getNewComponentName"
import analytics from "analytics" import analytics from "analytics"
import { import {
findChildComponentType, findChildComponentType,
generateNewIdsForComponent, generateNewIdsForComponent,
getComponentDefinition, getComponentDefinition,
getParent, findParent,
} from "../storeUtils" } from "../storeUtils"
const INITIAL_FRONTEND_STATE = { const INITIAL_FRONTEND_STATE = {
apps: [], apps: [],
name: "", name: "",
description: "", description: "",
pages: DEFAULT_PAGES_OBJECT, layouts: [],
mainUi: {}, screens: [],
unauthenticatedUi: {},
components: [], components: [],
currentPreviewItem: null,
currentComponentInfo: null,
currentFrontEndType: "none", currentFrontEndType: "none",
currentPageName: "", currentAssetId: "",
currentComponentProps: null, selectedComponentId: "",
errors: [], errors: [],
hasAppPackage: false, hasAppPackage: false,
libraries: null, libraries: null,
@ -43,52 +45,13 @@ export const getFrontendStore = () => {
const store = writable({ ...INITIAL_FRONTEND_STATE }) const store = writable({ ...INITIAL_FRONTEND_STATE })
store.actions = { store.actions = {
// TODO: REFACTOR
initialise: async pkg => { initialise: async pkg => {
const { layouts, screens, application } = pkg
store.update(state => { store.update(state => {
state.appId = pkg.application._id state.appId = application._id
return state return state
}) })
const screens = await api.get("/api/screens").then(r => r.json())
const mainScreens = screens.filter(screen =>
screen._id.includes(pkg.pages.main._id)
),
unauthScreens = screens.filter(screen =>
screen._id.includes(pkg.pages.unauthenticated._id)
)
pkg.pages = {
main: {
...pkg.pages.main,
_screens: mainScreens,
},
unauthenticated: {
...pkg.pages.unauthenticated,
_screens: unauthScreens,
},
}
// if the app has just been created
// we need to build the CSS and save
if (pkg.justCreated) {
for (let pageName of ["main", "unauthenticated"]) {
const page = pkg.pages[pageName]
store.actions.screens.regenerateCss(page)
for (let screen of page._screens) {
store.actions.screens.regenerateCss(screen)
}
await api.post(`/api/pages/${page._id}`, {
page: {
componentLibraries: pkg.application.componentLibraries,
...page,
},
screens: page._screens,
})
}
}
pkg.justCreated = false
const components = await fetchComponentLibDefinitions(pkg.application._id) const components = await fetchComponentLibDefinitions(pkg.application._id)
@ -99,7 +62,8 @@ export const getFrontendStore = () => {
name: pkg.application.name, name: pkg.application.name,
description: pkg.application.description, description: pkg.application.description,
appId: pkg.application._id, appId: pkg.application._id,
pages: pkg.pages, layouts,
screens,
hasAppPackage: true, hasAppPackage: true,
builtins: [getBuiltin("##builtin/screenslot")], builtins: [getBuiltin("##builtin/screenslot")],
appInstance: pkg.application.instance, appInstance: pkg.application.instance,
@ -107,20 +71,6 @@ export const getFrontendStore = () => {
await backendUiStore.actions.database.select(pkg.application.instance) await backendUiStore.actions.database.select(pkg.application.instance)
}, },
selectPageOrScreen: type => {
store.update(state => {
state.currentFrontEndType = type
const page = get(selectedPage)
const pageOrScreen = type === "page" ? page : page._screens[0]
state.currentComponentInfo = pageOrScreen ? pageOrScreen.props : null
state.currentPreviewItem = pageOrScreen
state.currentView = "detail"
return state
})
},
routing: { routing: {
fetch: async () => { fetch: async () => {
const response = await api.get("/api/routing") const response = await api.get("/api/routing")
@ -133,167 +83,170 @@ export const getFrontendStore = () => {
}, },
}, },
screens: { screens: {
select: screenId => { select: async screenId => {
let promise
store.update(state => { store.update(state => {
const screen = get(allScreens).find(screen => screen._id === screenId) const screen = get(allScreens).find(screen => screen._id === screenId)
state.currentPreviewItem = screen if (!screen) return state
state.currentFrontEndType = "screen"
state.currentFrontEndType = FrontendTypes.SCREEN
state.currentAssetId = screenId
state.currentView = "detail" state.currentView = "detail"
store.actions.screens.regenerateCssForCurrentScreen() promise = store.actions.screens.regenerateCss(screen)
const safeProps = makePropsSafe( state.selectedComponentId = screen.props?._id
state.components[screen.props._component],
screen.props
)
screen.props = safeProps
state.currentComponentInfo = safeProps
return state return state
}) })
await promise
}, },
create: async screen => { create: async screen => {
let savePromise screen = await store.actions.screens.save(screen)
store.update(state => { store.update(state => {
state.currentPreviewItem = screen state.currentAssetId = screen._id
state.currentComponentInfo = screen.props state.selectedComponentId = screen.props._id
state.currentFrontEndType = "screen" state.currentFrontEndType = FrontendTypes.SCREEN
if (state.currentPreviewItem) {
store.actions.screens.regenerateCss(state.currentPreviewItem)
}
savePromise = store.actions.screens.save(screen)
return state return state
}) })
return screen
await savePromise
}, },
save: async screen => { save: async screen => {
const page = get(selectedPage)
const currentPageScreens = page._screens
const creatingNewScreen = screen._id === undefined const creatingNewScreen = screen._id === undefined
const response = await api.post(`/api/screens`, screen)
screen = await response.json()
let savePromise
const response = await api.post(`/api/screens/${page._id}`, screen)
const json = await response.json()
screen._rev = json.rev
screen._id = json.id
const foundScreen = page._screens.findIndex(el => el._id === screen._id)
if (foundScreen !== -1) {
page._screens.splice(foundScreen, 1)
}
page._screens.push(screen)
// TODO: should carry out all server updates to screen in a single call
store.update(state => { store.update(state => {
page._screens = currentPageScreens const foundScreen = state.screens.findIndex(
el => el._id === screen._id
)
if (foundScreen !== -1) {
state.screens.splice(foundScreen, 1)
}
state.screens.push(screen)
if (creatingNewScreen) { if (creatingNewScreen) {
state.currentPreviewItem = screen
const safeProps = makePropsSafe( const safeProps = makePropsSafe(
state.components[screen.props._component], state.components[screen.props._component],
screen.props screen.props
) )
state.currentComponentInfo = safeProps state.selectedComponentId = safeProps._id
screen.props = safeProps screen.props = safeProps
} }
savePromise = store.actions.pages.save()
return state return state
}) })
if (savePromise) await savePromise return screen
}, },
regenerateCss: screen => { regenerateCss: async asset => {
screen._css = generate_screen_css([screen.props]) const response = await api.post("/api/css/generate", asset)
asset._css = (await response.json())?.css
}, },
regenerateCssForCurrentScreen: () => { regenerateCssForCurrentScreen: async () => {
const { currentPreviewItem } = get(store) const asset = get(currentAsset)
if (currentPreviewItem) { if (asset) {
store.actions.screens.regenerateCss(currentPreviewItem) await store.actions.screens.regenerateCss(asset)
} }
}, },
delete: async screens => { delete: async screens => {
let deletePromise
const screensToDelete = Array.isArray(screens) ? screens : [screens] const screensToDelete = Array.isArray(screens) ? screens : [screens]
const screenDeletePromises = []
store.update(state => { store.update(state => {
const currentPage = get(selectedPage)
for (let screenToDelete of screensToDelete) { for (let screenToDelete of screensToDelete) {
// Remove screen from current page as well state.screens = state.screens.filter(
// TODO: Should be done server side screen => screen._id !== screenToDelete._id
currentPage._screens = currentPage._screens.filter(
scr => scr._id !== screenToDelete._id
) )
screenDeletePromises.push(
deletePromise = api.delete( api.delete(
`/api/screens/${screenToDelete._id}/${screenToDelete._rev}` `/api/screens/${screenToDelete._id}/${screenToDelete._rev}`
)
) )
if (screenToDelete._id === state.currentAssetId) {
state.currentAssetId = ""
}
} }
return state return state
}) })
await deletePromise await Promise.all(screenDeletePromises)
}, },
}, },
preview: { preview: {
saveSelected: async () => { saveSelected: async () => {
const state = get(store) const state = get(store)
if (state.currentFrontEndType !== "page") { const selectedAsset = get(currentAsset)
await store.actions.screens.save(state.currentPreviewItem)
if (state.currentFrontEndType !== FrontendTypes.LAYOUT) {
await store.actions.screens.save(selectedAsset)
} else {
await store.actions.layouts.save(selectedAsset)
} }
await store.actions.pages.save()
}, },
}, },
pages: { layouts: {
select: pageName => { select: async layoutId => {
store.update(state => { store.update(state => {
const currentPage = state.pages[pageName] const layout = store.actions.layouts.find(layoutId)
state.currentFrontEndType = "page" state.currentFrontEndType = FrontendTypes.LAYOUT
state.currentView = "detail" state.currentView = "detail"
state.currentPageName = pageName
state.currentAssetId = layout._id
// This is the root of many problems. state.selectedComponentId = layout.props?._id
// Uncaught (in promise) TypeError: Cannot read property '_component' of undefined
// it appears that the currentPage sometimes has _props instead of props return state
// why })
const safeProps = makePropsSafe( let cssPromises = []
state.components[currentPage.props._component], cssPromises.push(store.actions.screens.regenerateCssForCurrentScreen())
currentPage.props
for (let screen of get(allScreens)) {
cssPromises.push(store.actions.screens.regenerateCss(screen))
}
await Promise.all(cssPromises)
},
save: async layout => {
const layoutToSave = cloneDeep(layout)
delete layoutToSave._css
const response = await api.post(`/api/layouts`, layoutToSave)
const json = await response.json()
store.update(state => {
const layoutIdx = state.layouts.findIndex(
stateLayout => stateLayout._id === json._id
) )
state.currentComponentInfo = safeProps
currentPage.props = safeProps
state.currentPreviewItem = state.pages[pageName]
store.actions.screens.regenerateCssForCurrentScreen()
for (let screen of get(allScreens)) { if (layoutIdx >= 0) {
screen._css = generate_screen_css([screen.props]) // update existing layout
state.layouts.splice(layoutIdx, 1, json)
} else {
// save new layout
state.layouts.push(json)
} }
state.currentAssetId = json._id
return state return state
}) })
}, },
save: async page => { find: layoutId => {
if (!layoutId) {
return get(mainLayout)
}
const storeContents = get(store) const storeContents = get(store)
const pageName = storeContents.currentPageName || "main" return storeContents.layouts.find(layout => layout._id === layoutId)
const pageToSave = page || storeContents.pages[pageName] },
delete: async layoutToDelete => {
// TODO: revisit. This sends down a very weird payload const response = await api.delete(
const response = await api.post(`/api/pages/${pageToSave._id}`, { `/api/layouts/${layoutToDelete._id}/${layoutToDelete._rev}`
page: { )
componentLibraries: storeContents.pages.componentLibraries,
...pageToSave,
},
screens: pageToSave._screens,
})
const json = await response.json()
if (!json.ok) throw new Error("Error updating page") if (response.status !== 200) {
const json = await response.json()
throw new Error(json.message)
}
store.update(state => { store.update(state => {
state.pages[pageName]._rev = json.rev state.layouts = state.layouts.filter(
layout => layout._id !== layoutToDelete._id
)
return state return state
}) })
}, },
@ -301,17 +254,19 @@ export const getFrontendStore = () => {
components: { components: {
select: component => { select: component => {
store.update(state => { store.update(state => {
const componentDef = component._component.startsWith("##") state.selectedComponentId = component._id
? component
: state.components[component._component]
state.currentComponentInfo = makePropsSafe(componentDef, component)
state.currentView = "component" state.currentView = "component"
return state return state
}) })
}, },
create: (componentToAdd, presetProps) => { create: (componentToAdd, presetProps) => {
const selectedAsset = get(currentAsset)
store.update(state => { store.update(state => {
function findSlot(component_array) { function findSlot(component_array) {
if (!component_array) {
return false
}
for (let component of component_array) { for (let component of component_array) {
if (component._component === "##builtin/screenslot") { if (component._component === "##builtin/screenslot") {
return true return true
@ -324,7 +279,7 @@ export const getFrontendStore = () => {
if ( if (
componentToAdd.startsWith("##") && componentToAdd.startsWith("##") &&
findSlot(state.pages[state.currentPageName].props._children) findSlot(selectedAsset?.props._children)
) { ) {
return state return state
} }
@ -340,29 +295,34 @@ export const getFrontendStore = () => {
_instanceName: instanceName, _instanceName: instanceName,
}) })
const currentComponent = const selected = get(selectedComponent)
state.components[state.currentComponentInfo._component]
const targetParent = currentComponent.children const currentComponentDefinition =
? state.currentComponentInfo state.components[selected._component]
: getParent(
state.currentPreviewItem.props,
state.currentComponentInfo
)
// Don't continue if there's no parent const allowsChildren = currentComponentDefinition.children
if (!targetParent) {
return state // Determine where to put the new component.
let targetParent
if (allowsChildren) {
// Child of the selected component
targetParent = selected
} else {
// Sibling of selected component
targetParent = findParent(selectedAsset.props, selected)
} }
targetParent._children = targetParent._children.concat( // Don't continue if there's no parent
newComponent.props if (!targetParent) return state
)
// Push the new component
targetParent._children.push(newComponent.props)
store.actions.preview.saveSelected() store.actions.preview.saveSelected()
state.currentView = "component" state.currentView = "component"
state.currentComponentInfo = newComponent.props state.selectedComponentId = newComponent.props._id
analytics.captureEvent("Added Component", { analytics.captureEvent("Added Component", {
name: newComponent.props._component, name: newComponent.props._component,
}) })
@ -370,14 +330,12 @@ export const getFrontendStore = () => {
}) })
}, },
copy: (component, cut = false) => { copy: (component, cut = false) => {
const selectedAsset = get(currentAsset)
store.update(state => { store.update(state => {
state.componentToPaste = cloneDeep(component) state.componentToPaste = cloneDeep(component)
state.componentToPaste.isCut = cut state.componentToPaste.isCut = cut
if (cut) { if (cut) {
const parent = getParent( const parent = findParent(selectedAsset.props, component._id)
state.currentPreviewItem.props,
component._id
)
parent._children = parent._children.filter( parent._children = parent._children.filter(
child => child._id !== component._id child => child._id !== component._id
) )
@ -387,7 +345,9 @@ export const getFrontendStore = () => {
return state return state
}) })
}, },
paste: (targetComponent, mode) => { paste: async (targetComponent, mode) => {
const selectedAsset = get(currentAsset)
let promises = []
store.update(state => { store.update(state => {
if (!state.componentToPaste) return state if (!state.componentToPaste) return state
@ -406,54 +366,56 @@ export const getFrontendStore = () => {
return state return state
} }
const parent = getParent( const parent = findParent(selectedAsset.props, targetComponent)
state.currentPreviewItem.props,
targetComponent
)
const targetIndex = parent._children.indexOf(targetComponent) const targetIndex = parent._children.indexOf(targetComponent)
const index = mode === "above" ? targetIndex : targetIndex + 1 const index = mode === "above" ? targetIndex : targetIndex + 1
parent._children.splice(index, 0, cloneDeep(componentToPaste)) parent._children.splice(index, 0, cloneDeep(componentToPaste))
store.actions.screens.regenerateCssForCurrentScreen() promises.push(store.actions.screens.regenerateCssForCurrentScreen())
store.actions.preview.saveSelected() promises.push(store.actions.preview.saveSelected())
store.actions.components.select(componentToPaste) store.actions.components.select(componentToPaste)
return state return state
}) })
await Promise.all(promises)
}, },
updateStyle: (type, name, value) => { updateStyle: async (type, name, value) => {
let promises = []
const selected = get(selectedComponent)
store.update(state => { store.update(state => {
if (!state.currentComponentInfo._styles) { if (!selected._styles) {
state.currentComponentInfo._styles = {} selected._styles = {}
} }
state.currentComponentInfo._styles[type][name] = value selected._styles[type][name] = value
store.actions.screens.regenerateCssForCurrentScreen() promises.push(store.actions.screens.regenerateCssForCurrentScreen())
// save without messing with the store // save without messing with the store
store.actions.preview.saveSelected() promises.push(store.actions.preview.saveSelected())
return state return state
}) })
await Promise.all(promises)
}, },
updateProp: (name, value) => { updateProp: (name, value) => {
store.update(state => { store.update(state => {
let current_component = state.currentComponentInfo let current_component = get(selectedComponent)
current_component[name] = value current_component[name] = value
state.currentComponentInfo = current_component state.selectedComponentId = current_component._id
store.actions.preview.saveSelected() store.actions.preview.saveSelected()
return state return state
}) })
}, },
findRoute: component => { findRoute: component => {
// Gets all the components to needed to construct a path. // Gets all the components to needed to construct a path.
const tempStore = get(store) const selectedAsset = get(currentAsset)
let pathComponents = [] let pathComponents = []
let parent = component let parent = component
let root = false let root = false
while (!root) { while (!root) {
parent = getParent(tempStore.currentPreviewItem.props, parent) parent = findParent(selectedAsset.props, parent)
if (!parent) { if (!parent) {
root = true root = true
} else { } else {
@ -461,7 +423,7 @@ export const getFrontendStore = () => {
} }
} }
// Remove root entry since it's the screen or page layout. // Remove root entry since it's the screen or layout.
// Reverse array since we need the correct order of the IDs // Reverse array since we need the correct order of the IDs
const reversedComponents = pathComponents.reverse().slice(1) const reversedComponents = pathComponents.reverse().slice(1)
@ -476,12 +438,13 @@ export const getFrontendStore = () => {
}, },
links: { links: {
save: async (url, title) => { save: async (url, title) => {
let savePromise let promises = []
const layout = get(mainLayout)
store.update(state => { store.update(state => {
// Try to extract a nav component from the master screen // Try to extract a nav component from the master layout
const nav = findChildComponentType( const nav = findChildComponentType(
state.pages.main, layout,
"@budibase/standard-components/Navigation" "@budibase/standard-components/navigation"
) )
if (nav) { if (nav) {
let newLink let newLink
@ -513,18 +476,18 @@ export const getFrontendStore = () => {
}).props }).props
} }
// Save page and regenerate all CSS because otherwise weird things happen // Save layout and regenerate all CSS because otherwise weird things happen
nav._children = [...nav._children, newLink] nav._children = [...nav._children, newLink]
state.currentPageName = "main" state.currentAssetId = layout._id
store.actions.screens.regenerateCss(state.pages.main) promises.push(store.actions.screens.regenerateCss(layout))
for (let screen of state.pages.main._screens) { for (let screen of get(allScreens)) {
store.actions.screens.regenerateCss(screen) promises.push(store.actions.screens.regenerateCss(screen))
} }
savePromise = store.actions.pages.save() promises.push(store.actions.layouts.save(layout))
} }
return state return state
}) })
await savePromise await Promise.all(promises)
}, },
}, },
}, },

2
packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js

@ -18,7 +18,7 @@ export default function(tables) {
}) })
} }
export const newRowUrl = table => sanitizeUrl(`/${table.name}/new`) export const newRowUrl = table => sanitizeUrl(`/${table.name}/new/row`)
export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE" export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE"
function generateTitleContainer(table) { function generateTitleContainer(table) {

3
packages/builder/src/builderStore/store/screenTemplates/utils/Component.js

@ -14,9 +14,6 @@ export class Component extends BaseStructure {
active: {}, active: {},
selected: {}, selected: {},
}, },
_code: "",
className: "",
onLoad: [],
type: "", type: "",
_instanceName: "", _instanceName: "",
_children: [], _children: [],

3
packages/builder/src/builderStore/store/screenTemplates/utils/Screen.js

@ -4,6 +4,7 @@ export class Screen extends BaseStructure {
constructor() { constructor() {
super(true) super(true)
this._json = { this._json = {
layoutId: "layout_private_master",
props: { props: {
_id: "", _id: "",
_component: "", _component: "",
@ -18,7 +19,7 @@ export class Screen extends BaseStructure {
}, },
routing: { routing: {
route: "", route: "",
accessLevelId: "", roleId: "BASIC",
}, },
name: "screen-id", name: "screen-id",
} }

18
packages/builder/src/builderStore/storeUtils.js

@ -1,15 +1,21 @@
import { getBuiltin } from "components/userInterface/pagesParsing/createProps" import { getBuiltin } from "components/userInterface/assetParsing/createProps"
import { uuid } from "./uuid" import { uuid } from "./uuid"
import getNewComponentName from "./getNewComponentName" import getNewComponentName from "./getNewComponentName"
export const getParent = (rootProps, child) => { /**
* Find the parent component of the passed in child.
* @param {Object} rootProps - props to search for the parent in
* @param {String|Object} child - id of the child or the child itself to find the parent of
*/
export const findParent = (rootProps, child) => {
let parent let parent
walkProps(rootProps, (p, breakWalk) => { walkProps(rootProps, (props, breakWalk) => {
if ( if (
p._children && props._children &&
(p._children.includes(child) || p._children.some(c => c._id === child)) (props._children.includes(child) ||
props._children.some(c => c._id === child))
) { ) {
parent = p parent = props
breakWalk() breakWalk()
} }
}) })

2
packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte

@ -62,6 +62,8 @@
</Select> </Select>
{:else if value.customType === 'password'} {:else if value.customType === 'password'}
<Input type="password" extraThin bind:value={block.inputs[key]} /> <Input type="password" extraThin bind:value={block.inputs[key]} />
{:else if value.customType === 'email'}
<Input type="email" extraThin bind:value={block.inputs[key]} />
{:else if value.customType === 'table'} {:else if value.customType === 'table'}
<TableSelector bind:value={block.inputs[key]} /> <TableSelector bind:value={block.inputs[key]} />
{:else if value.customType === 'row'} {:else if value.customType === 'row'}

27
packages/builder/src/components/backend/DataTable/RowFieldControl.svelte

@ -1,14 +1,26 @@
<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 +41,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}

4
packages/builder/src/components/backend/DataTable/api.js

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

10
packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte

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

14
packages/builder/src/components/backend/DataTable/modals/CreateEditRowModal.svelte

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

3
packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte

@ -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"
@ -43,7 +44,7 @@
{#each $backendUiStore.tables as table, idx} {#each $backendUiStore.tables as table, idx}
<NavItem <NavItem
border={idx > 0} border={idx > 0}
icon={table.integration?.type ? 'ri-database-2-line' : 'ri-table-line'} icon={table.integration?.type ? 'ri-database-2-line' : `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)}>

3
packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte

@ -54,14 +54,13 @@
const screens = screenTemplates($store, [table]) const screens = screenTemplates($store, [table])
.filter(template => defaultScreens.includes(template.id)) .filter(template => defaultScreens.includes(template.id))
.map(template => template.create()) .map(template => template.create())
store.actions.pages.select("main")
for (let screen of screens) { for (let screen of screens) {
// Record the table that created this screen so we can link it later // Record the table that created this screen so we can link it later
screen.autoTableId = table._id screen.autoTableId = table._id
await store.actions.screens.create(screen) await store.actions.screens.create(screen)
} }
// Create autolink to newly created list page // Create autolink to newly created list screen
const listScreen = screens.find(screen => const listScreen = screens.find(screen =>
screen.props._instanceName.endsWith("List") screen.props._instanceName.endsWith("List")
) )

7
packages/builder/src/components/settings/SettingsModal.svelte

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

43
packages/builder/src/components/settings/UserRow.svelte

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

114
packages/builder/src/components/settings/tabs/Users.svelte

@ -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
packages/builder/src/components/settings/tabs/index.js

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

18
packages/builder/src/components/start/CreateAppModal.svelte

@ -52,13 +52,13 @@
applicationName: string().required("Your application must have a name."), applicationName: string().required("Your application must have a name."),
}, },
{ {
username: string().required("Your application needs a first user."), email: string()
.email()
.required("Your application needs a first user."),
password: string().required( password: string().required(
"Please enter a password for your first user." "Please enter a password for your first user."
), ),
accessLevelId: string().required( roleId: string().required("You need to select a role for your user."),
"You need to select an access level for your user."
),
}, },
] ]
@ -79,9 +79,7 @@
if (hasKey) { if (hasKey) {
validationSchemas.shift() validationSchemas.shift()
validationSchemas = validationSchemas
steps.shift() steps.shift()
steps = steps
} }
// Handles form navigation // Handles form navigation
@ -155,19 +153,17 @@
const pkg = await applicationPkg.json() const pkg = await applicationPkg.json()
if (applicationPkg.ok) { if (applicationPkg.ok) {
backendUiStore.actions.reset() backendUiStore.actions.reset()
pkg.justCreated = true
await store.actions.initialise(pkg) await store.actions.initialise(pkg)
automationStore.actions.fetch() await automationStore.actions.fetch()
} else { } else {
throw new Error(pkg) throw new Error(pkg)
} }
// Create user // Create user
const user = { const user = {
name: $createAppStore.values.username, email: $createAppStore.values.email,
username: $createAppStore.values.username,
password: $createAppStore.values.password, password: $createAppStore.values.password,
accessLevelId: $createAppStore.values.accessLevelId, roleId: $createAppStore.values.roleId,
} }
const userResp = await api.post(`/api/users`, user) const userResp = await api.post(`/api/users`, user)
const json = await userResp.json() const json = await userResp.json()

16
packages/builder/src/components/start/Steps/User.svelte

@ -2,18 +2,18 @@
import { Input, Select } from "@budibase/bbui" import { Input, Select } from "@budibase/bbui"
export let validationErrors export let validationErrors
let blurred = { username: false, password: false } let blurred = { email: false, password: false }
</script> </script>
<h2>Create your first User</h2> <h2>Create your first User</h2>
<div class="container"> <div class="container">
<Input <Input
on:input={() => (blurred.username = true)} on:input={() => (blurred.email = true)}
label="Username" label="Email"
name="username" name="email"
placeholder="Username" placeholder="Email"
type="name" type="email"
error={blurred.username && validationErrors.username} /> error={blurred.email && validationErrors.email} />
<Input <Input
on:input={() => (blurred.password = true)} on:input={() => (blurred.password = true)}
label="Password" label="Password"
@ -21,7 +21,7 @@
placeholder="Password" placeholder="Password"
type="password" type="password"
error={blurred.password && validationErrors.password} /> error={blurred.password && validationErrors.password} />
<Select label="Access Level" secondary name="accessLevelId"> <Select label="Role" secondary name="roleId">
<option value="ADMIN">Admin</option> <option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option> <option value="POWER_USER">Power User</option>
</Select> </Select>

178
packages/builder/src/components/userInterface/AppPreview/CurrentItemPreview.svelte

@ -1,146 +1,79 @@
<script> <script>
import { store, backendUiStore } from "builderStore" import { onMount } from "svelte"
import { map, join } from "lodash/fp" import { store, currentAsset } 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 { FrontendTypes } from "../../../constants"
import { Component } from "../../../builderStore/store/screenTemplates/utils/Component"
let iframe let iframe
let styles = "" let layout
let screen
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
$: { $: {
styles = "" if ($store.currentFrontEndType === FrontendTypes.LAYOUT) {
// Apply the CSS from the currently selected page and its screens layout = $currentAsset
const currentPage = $store.pages[$store.currentPageName] screen = screenPlaceholder
styles += currentPage._css } else {
for (let screen of currentPage._screens) { screen = $currentAsset
styles += screen._css layout = $store.layouts.find(layout => layout._id === screen?.layoutId)
} }
styles = styles
} }
$: selectedComponentId = $store.selectedComponentId ?? ""
$: stylesheetLinks = pipe($store.pages.stylesheets, [ $: previewData = {
map(s => `<link rel="stylesheet" href="${s}"/>`), layout,
join("\n"), screen,
]) selectedComponentId,
$: screensExist =
$store.currentPreviewItem._screens &&
$store.currentPreviewItem._screens.length > 0
$: frontendDefinition = {
appId: $store.appId,
libraries: $store.libraries,
page: $store.pages[$store.currentPageName],
screens: [
$store.currentFrontEndType === "page"
? screenPlaceholder
: $store.currentPreviewItem,
],
} }
$: selectedComponentType = getComponentTypeName($store.currentComponentInfo) // Saving pages and screens to the DB causes them to have _revs.
// These revisions change every time a save happens and causes
$: selectedComponentId = $store.currentComponentInfo // these reactive statements to fire, even though the actual
? $store.currentComponentInfo._id // definition hasn't changed.
: "" // By deleting all _rev properties we can avoid this and increase
// performance.
const refreshContent = () => { $: json = JSON.stringify(previewData)
iframe.contentWindow.postMessage( $: strippedJson = json.replaceAll(/"_rev":\s*"[^"]+"/g, `"_rev":""`)
JSON.stringify({
styles, // Update the iframe with the builder info to render the correct preview
stylesheetLinks, const refreshContent = message => {
selectedComponentType, if (iframe) {
selectedComponentId, iframe.contentWindow.postMessage(message)
frontendDefinition, }
appId: $store.appId,
instanceId: $backendUiStore.selectedDatabase._id,
})
)
} }
$: if (iframe) // Refresh the preview when required
iframe.contentWindow.addEventListener("bb-ready", refreshContent, { $: refreshContent(strippedJson)
once: true,
}) // Initialise the app when mounted
onMount(() => {
$: if (iframe && frontendDefinition) { iframe.contentWindow.addEventListener(
refreshContent() "bb-ready",
} () => {
refreshContent(strippedJson)
},
{
once: true,
}
)
})
</script> </script>
<div class="component-container"> <div class="component-container">
{#if hasComponent && $store.currentPreviewItem} <iframe
<iframe style="height: 100%; width: 100%"
style="height: 100%; width: 100%" title="componentPreview"
title="componentPreview" bind:this={iframe}
bind:this={iframe} srcdoc={iframeTemplate} />
srcdoc={iframeTemplate} />
{/if}
</div> </div>
<style> <style>
@ -152,7 +85,6 @@
margin: auto; margin: auto;
height: 100%; height: 100%;
} }
.component-container iframe { .component-container iframe {
border: 0; border: 0;
left: 0; left: 0;

52
packages/builder/src/components/userInterface/AppPreview/iframeTemplate.html

@ -0,0 +1,52 @@
<html>
<head>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<style>
body, html {
height: 100% !important;
font-family: Inter, sans-serif !important;
margin: 0 !important;
}
*, *:before, *:after {
box-sizing: border-box;
}
</style>
<script src='/assets/budibase-client.js'></script>
<script>
function receiveMessage(event) {
if (!event.data) {
return
}
// Extract data from message
const { selectedComponentId, layout, screen } = JSON.parse(event.data)
// Set some flags so the app knows we're in the builder
window["##BUDIBASE_IN_BUILDER##"] = true
window["##BUDIBASE_PREVIEW_LAYOUT##"] = layout
window["##BUDIBASE_PREVIEW_SCREEN##"] = screen
window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId
window["##BUDIBASE_PREVIEW_ID##"] = Math.random()
// Initialise app
if (window.loadBudibase) {
loadBudibase()
}
}
// Ignore clicks
["click", "mousedown"].forEach(type => {
document.addEventListener(type, function(e) {
e.preventDefault()
e.stopPropagation()
return false
}, true)
})
window.addEventListener("message", receiveMessage)
window.dispatchEvent(new Event("bb-ready"))
</script>
</head>
<body>
</body>
</html>

78
packages/builder/src/components/userInterface/AppPreview/iframeTemplate.js

@ -1,77 +1 @@
export default `<html> export { default } from "./iframeTemplate.html"
<head>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
<style>
body, html {
height: 100%!important;
font-family: Inter !important;
margin: 0px!important;
}
*, *:before, *:after {
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>
<script src='/assets/budibase-client.js'></script>
<script>
function receiveMessage(event) {
if (!event.data) return
const data = JSON.parse(event.data)
try {
if (styles) document.head.removeChild(styles)
} catch(_) { }
try {
if (selectedComponentStyle) document.head.removeChild(selectedComponentStyle)
} 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) {
loadBudibase({ window, localStorage })
}
}
let styles
let selectedComponentStyle
document.addEventListener("click", function(e) {
e.preventDefault()
e.stopPropagation()
return false;
}, true)
window.addEventListener('message', receiveMessage)
window.dispatchEvent(new Event('bb-ready'))
</script>
</head>
<body>
</body>
</html>`

25
packages/builder/src/components/userInterface/ComponentDropdownMenu.svelte

@ -1,10 +1,11 @@
<script> <script>
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { store } from "builderStore" import { get } from "svelte/store"
import { store, currentAsset } from "builderStore"
import { getComponentDefinition } from "builderStore/storeUtils" import { getComponentDefinition } from "builderStore/storeUtils"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { last } from "lodash/fp" import { last } from "lodash/fp"
import { getParent } from "builderStore/storeUtils" import { findParent } from "builderStore/storeUtils"
import { DropdownMenu } from "@budibase/bbui" import { DropdownMenu } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns" import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
@ -15,7 +16,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("/")) : "")
@ -27,12 +29,13 @@
const selectComponent = component => { const selectComponent = component => {
store.actions.components.select(component) store.actions.components.select(component)
const path = store.actions.components.findRoute(component) const path = store.actions.components.findRoute(component)
$goto(`./:page/:screen/${path}`) $goto(`./${$store.currentFrontEndType}/${path}`)
} }
const moveUpComponent = () => { const moveUpComponent = () => {
store.update(state => { store.update(state => {
const parent = getParent(state.currentPreviewItem.props, component) const asset = get(currentAsset)
const parent = findParent(asset.props, component)
if (parent) { if (parent) {
const currentIndex = parent._children.indexOf(component) const currentIndex = parent._children.indexOf(component)
@ -42,7 +45,7 @@
newChildren.splice(currentIndex - 1, 0, component) newChildren.splice(currentIndex - 1, 0, component)
parent._children = newChildren parent._children = newChildren
} }
state.currentComponentInfo = component state.selectedComponentId = component._id
store.actions.preview.saveSelected() store.actions.preview.saveSelected()
return state return state
@ -51,7 +54,8 @@
const moveDownComponent = () => { const moveDownComponent = () => {
store.update(state => { store.update(state => {
const parent = getParent(state.currentPreviewItem.props, component) const asset = get(currentAsset)
const parent = findParent(asset.props, component)
if (parent) { if (parent) {
const currentIndex = parent._children.indexOf(component) const currentIndex = parent._children.indexOf(component)
@ -61,7 +65,7 @@
newChildren.splice(currentIndex + 1, 0, component) newChildren.splice(currentIndex + 1, 0, component)
parent._children = newChildren parent._children = newChildren
} }
state.currentComponentInfo = component state.selectedComponentId = component._id
store.actions.preview.saveSelected() store.actions.preview.saveSelected()
return state return state
@ -75,10 +79,11 @@
const deleteComponent = () => { const deleteComponent = () => {
store.update(state => { store.update(state => {
const parent = getParent(state.currentPreviewItem.props, component) const asset = get(currentAsset)
const parent = findParent(asset.props, component)
if (parent) { if (parent) {
parent._children = parent._children.filter(c => c !== component) parent._children = parent._children.filter(child => child !== component)
selectComponent(parent) selectComponent(parent)
} }

7
packages/builder/src/components/userInterface/ComponentNavigationTree/ComponentTree.svelte

@ -1,6 +1,6 @@
<script> <script>
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { store } from "builderStore" import { store, currentAsset } from "builderStore"
import { getComponentDefinition } from "builderStore/storeUtils" import { getComponentDefinition } from "builderStore/storeUtils"
import { DropEffect, DropPosition } from "./dragDropStore" import { DropEffect, DropPosition } from "./dragDropStore"
import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte" import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte"
@ -10,7 +10,6 @@
export let currentComponent export let currentComponent
export let onSelect = () => {} export let onSelect = () => {}
export let level = 0 export let level = 0
export let dragDropStore export let dragDropStore
const isScreenslot = name => name === "##builtin/screenslot" const isScreenslot = name => name === "##builtin/screenslot"
@ -23,7 +22,7 @@
const path = store.actions.components.findRoute(component) const path = store.actions.components.findRoute(component)
// Go to correct URL // Go to correct URL
$goto(`./:page/:screen/${path}`) $goto(`./${$store.currentAssetId}/${path}`)
} }
const dragstart = component => e => { const dragstart = component => e => {
@ -73,7 +72,7 @@
text={isScreenslot(component._component) ? 'Screenslot' : component._instanceName} text={isScreenslot(component._component) ? 'Screenslot' : component._instanceName}
withArrow withArrow
indentLevel={level + 3} indentLevel={level + 3}
selected={currentComponent === component}> selected={$store.selectedComponentId === component._id}>
<ComponentDropdownMenu {component} /> <ComponentDropdownMenu {component} />
</NavItem> </NavItem>

77
packages/builder/src/components/userInterface/ComponentNavigationTree/LayoutDropdownMenu.svelte

@ -0,0 +1,77 @@
<script>
import { goto } from "@sveltech/routify"
import { store } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { DropdownMenu, Modal, ModalContent, Input } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
import { cloneDeep } from "lodash/fp"
export let layout
let confirmDeleteDialog
let editLayoutNameModal
let dropdown
let anchor
let name = layout.name
const deleteLayout = async () => {
try {
await store.actions.layouts.delete(layout)
notifier.success(`Layout ${layout.name} deleted successfully.`)
} catch (err) {
notifier.danger(`Error deleting layout: ${err.message}`)
}
}
const saveLayout = async () => {
try {
const layoutToSave = cloneDeep(layout)
layoutToSave.name = name
await store.actions.layouts.save(layoutToSave)
notifier.success(`Layout saved successfully.`)
} catch (err) {
notifier.danger(`Error saving layout: ${err.message}`)
}
}
</script>
<div bind:this={anchor} on:click|stopPropagation>
<div class="icon" on:click={() => dropdown.show()}>
<i class="ri-more-line" />
</div>
<DropdownMenu bind:this={dropdown} {anchor} align="left">
<DropdownContainer>
<DropdownItem
icon="ri-pencil-line"
title="Edit"
on:click={() => editLayoutNameModal.show()} />
<DropdownItem
icon="ri-delete-bin-line"
title="Delete"
on:click={() => confirmDeleteDialog.show()} />
</DropdownContainer>
</DropdownMenu>
</div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Deletion"
body={'Are you sure you wish to delete this layout?'}
okText="Delete Layout"
onOk={deleteLayout} />
<Modal bind:this={editLayoutNameModal}>
<ModalContent
title="Edit Layout Name"
confirmText="Save"
onConfirm={saveLayout}
disabled={!name}>
<Input thin type="text" label="Name" bind:value={name} />
</ModalContent>
</Modal>
<style>
.icon i {
font-size: 16px;
}
</style>

29
packages/builder/src/components/userInterface/ComponentNavigationTree/PathTree.svelte

@ -1,25 +1,32 @@
<script> <script>
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { store } from "builderStore" import { store, selectedComponent, currentAsset } from "builderStore"
import instantiateStore from "./dragDropStore" import instantiateStore from "./dragDropStore"
import ComponentsTree from "./ComponentTree.svelte" import ComponentTree from "./ComponentTree.svelte"
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte" import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
const ROUTE_NAME_MAP = {
"/": {
BASIC: "Home",
PUBLIC: "Login",
},
}
const dragDropStore = instantiateStore() const dragDropStore = instantiateStore()
export let route export let route
export let path export let path
export let indent export let indent
$: selectedScreen = $store.currentPreviewItem $: selectedScreen = $currentAsset
const changeScreen = screenId => { const changeScreen = screenId => {
// select the route // select the route
store.actions.screens.select(screenId) store.actions.screens.select(screenId)
$goto(`./:page/${screenId}`) $goto(`./${screenId}`)
} }
</script> </script>
@ -30,21 +37,21 @@
withArrow={route.subpaths} /> withArrow={route.subpaths} />
{#each Object.entries(route.subpaths) as [url, subpath]} {#each Object.entries(route.subpaths) as [url, subpath]}
{#each Object.values(subpath.screens) as screenId} {#each Object.entries(subpath.screens) as [role, screenId]}
<NavItem <NavItem
icon="ri-artboard-2-line" icon="ri-artboard-2-line"
indentLevel={indent || 1} indentLevel={indent || 1}
selected={$store.currentPreviewItem._id === screenId} selected={$store.currentAssetId === screenId}
opened={$store.currentPreviewItem._id === screenId} opened={$store.currentAssetId === screenId}
text={url === '/' ? 'Home' : url} text={ROUTE_NAME_MAP[url]?.[role] || url}
withArrow={route.subpaths} withArrow={route.subpaths}
on:click={() => changeScreen(screenId)}> on:click={() => changeScreen(screenId)}>
<ScreenDropdownMenu screen={screenId} /> <ScreenDropdownMenu {screenId} />
</NavItem> </NavItem>
{#if selectedScreen?._id === screenId} {#if selectedScreen?._id === screenId}
<ComponentsTree <ComponentTree
components={selectedScreen.props._children} components={selectedScreen.props._children}
currentComponent={$store.currentComponentInfo} currentComponent={$selectedComponent}
{dragDropStore} /> {dragDropStore} />
{/if} {/if}
{/each} {/each}

27
packages/builder/src/components/userInterface/ComponentNavigationTree/ScreenDropdownMenu.svelte

@ -3,28 +3,27 @@
import { store, allScreens } from "builderStore" import { store, allScreens } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { DropdownMenu } from "@budibase/bbui" import { DropdownMenu, Modal, ModalContent } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns" import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
export let screen export let screenId
let confirmDeleteDialog let confirmDeleteDialog
let dropdown let dropdown
let anchor let anchor
$: screen = $allScreens.find(screen => screen._id === screenId)
const deleteScreen = () => { const deleteScreen = () => {
const screenToDelete = $allScreens.find(scr => scr._id === screen) try {
store.actions.screens.delete(screenToDelete) store.actions.screens.delete(screen)
store.actions.routing.fetch() store.actions.routing.fetch()
// update the page if required confirmDeleteDialog.hide()
store.update(state => { $goto("../")
if (state.currentPreviewItem._id === screen) { notifier.success("Deleted screen successfully.")
store.actions.pages.select($store.currentPageName) } catch (err) {
notifier.success(`Screen ${screenToDelete.name} deleted successfully.`) notifier.danger("Error deleting screen")
$goto(`./:page/page-layout`) }
}
return state
})
} }
</script> </script>

2
packages/builder/src/components/userInterface/ComponentNavigationTree/dragDropStore.js

@ -48,7 +48,7 @@ export default function() {
if (mousePosition > 0.4 && mousePosition < 0.8) { if (mousePosition > 0.4 && mousePosition < 0.8) {
state.dropPosition = DropPosition.INSIDE state.dropPosition = DropPosition.INSIDE
} }
return return state
} }
// bottom half // bottom half

2
packages/builder/src/components/userInterface/ComponentNavigationTree/index.svelte

@ -5,7 +5,7 @@
</script> </script>
<div class="root"> <div class="root">
{#each Object.keys($store.routes) as path} {#each Object.keys($store.routes || {}) as path}
<PathTree {path} route={$store.routes[path]} /> <PathTree {path} route={$store.routes[path]} />
{/each} {/each}
</div> </div>

39
packages/builder/src/components/userInterface/ComponentPropertiesPanel.svelte

@ -1,5 +1,7 @@
<script> <script>
import { store } from "builderStore" import { get } from "svelte/store"
import { store, selectedComponent, currentAsset } from "builderStore"
import { FrontendTypes } from "constants"
import panelStructure from "./temporaryPanelStructure.js" import panelStructure from "./temporaryPanelStructure.js"
import CategoryTab from "./CategoryTab.svelte" import CategoryTab from "./CategoryTab.svelte"
import DesignView from "./DesignView.svelte" import DesignView from "./DesignView.svelte"
@ -14,12 +16,12 @@
$: componentInstance = $: componentInstance =
$store.currentView !== "component" $store.currentView !== "component"
? { ...$store.currentPreviewItem, ...$store.currentComponentInfo } ? { ...$currentAsset, ...$selectedComponent }
: $store.currentComponentInfo : $selectedComponent
$: componentDefinition = $store.components[componentInstance._component] $: componentDefinition = $store.components[componentInstance._component]
$: componentPropDefinition = $: componentPropDefinition =
flattenedPanel.find( flattenedPanel.find(
//use for getting controls for each component property // use for getting controls for each component property
c => c._component === componentInstance._component c => c._component === componentInstance._component
) || {} ) || {}
@ -31,7 +33,7 @@
$: isComponentOrScreen = $: isComponentOrScreen =
$store.currentView === "component" || $store.currentView === "component" ||
$store.currentFrontEndType === "screen" $store.currentFrontEndType === FrontendTypes.SCREEN
$: isNotScreenslot = componentInstance._component !== "##builtin/screenslot" $: isNotScreenslot = componentInstance._component !== "##builtin/screenslot"
$: displayName = $: displayName =
@ -58,16 +60,20 @@
return components return components
} }
function setPageOrScreenProp(name, value) { function setAssetProps(name, value) {
const selectedAsset = get(currentAsset)
store.update(state => { store.update(state => {
if (name === "_instanceName" && state.currentFrontEndType === "screen") { if (
state.currentPreviewItem.props[name] = value name === "_instanceName" &&
state.currentFrontEndType === FrontendTypes.SCREEN
) {
selectedAsset.props._instanceName = value
} else { } else {
state.currentPreviewItem[name] = value selectedAsset[name] = value
} }
store.actions.preview.saveSelected()
return state return state
}) })
store.actions.preview.saveSelected()
} }
function getProps(obj, keys) { function getProps(obj, keys) {
@ -94,21 +100,12 @@
{panelDefinition} {panelDefinition}
displayNameField={displayName} displayNameField={displayName}
onChange={store.actions.components.updateProp} onChange={store.actions.components.updateProp}
onScreenPropChange={setPageOrScreenProp} onScreenPropChange={setAssetProps}
screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} /> assetInstance={$store.currentView !== 'component' && $currentAsset} />
{/if} {/if}
</div> </div>
<style> <style>
.title > div:nth-child(1) {
grid-column-start: name;
color: var(--ink);
}
.title > div:nth-child(2) {
grid-column-start: actions;
}
.component-props-container { .component-props-container {
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;

10
packages/builder/src/components/userInterface/ComponentSelectionList.svelte

@ -1,6 +1,6 @@
<script> <script>
import { goto } from "@sveltech/routify" import { goto, url } from "@sveltech/routify"
import { store } from "builderStore" import { store, currentAssetName, selectedComponent } from "builderStore"
import components from "./temporaryPanelStructure.js" import components from "./temporaryPanelStructure.js"
import { DropdownMenu } from "@budibase/bbui" import { DropdownMenu } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns" import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
@ -26,8 +26,8 @@
const onComponentChosen = component => { const onComponentChosen = component => {
store.actions.components.create(component._component, component.presetProps) store.actions.components.create(component._component, component.presetProps)
const path = store.actions.components.findRoute($store.currentComponentInfo) const path = store.actions.components.findRoute($selectedComponent)
$goto(`./:page/:screen/${path}`) $goto(`./${$store.currentAssetId}/${path}`)
close() close()
} }
</script> </script>
@ -52,7 +52,7 @@
align="left"> align="left">
<DropdownContainer> <DropdownContainer>
{#each categories[selectedIndex].children as item} {#each categories[selectedIndex].children as item}
{#if !item.showOnPages || item.showOnPages.includes($store.currentPageName)} {#if !item.showOnAsset || item.showOnAsset.includes($currentAssetName)}
<DropdownItem <DropdownItem
icon={item.icon} icon={item.icon}
title={item.name} title={item.name}

3
packages/builder/src/components/userInterface/ComponentsPaneSwitcher.svelte

@ -1,5 +1,6 @@
<script> <script>
import { store, allScreens } from "builderStore" import { store, allScreens } from "builderStore"
import { FrontendTypes } from "constants"
import ComponentPropertiesPanel from "./ComponentPropertiesPanel.svelte" import ComponentPropertiesPanel from "./ComponentPropertiesPanel.svelte"
import ComponentSelectionList from "./ComponentSelectionList.svelte" import ComponentSelectionList from "./ComponentSelectionList.svelte"
@ -18,7 +19,7 @@
</script> </script>
<div class="root"> <div class="root">
{#if $store.currentFrontEndType === 'page' || $allScreens.length} {#if $store.currentFrontEndType === FrontendTypes.LAYOUT || $allScreens.length}
<div class="switcher"> <div class="switcher">
<button <button
class:selected={selected === COMPONENT_SELECTION_TAB} class:selected={selected === COMPONENT_SELECTION_TAB}

7
packages/builder/src/components/userInterface/DesignView.svelte

@ -12,8 +12,6 @@
let propGroup = null let propGroup = null
let currentGroup let currentGroup
const getProperties = name => panelDefinition[name]
function onChange(category) { function onChange(category) {
selectedCategory = category selectedCategory = category
} }
@ -38,7 +36,7 @@
{#each propertyGroupNames as groupName} {#each propertyGroupNames as groupName}
<PropertyGroup <PropertyGroup
name={groupName} name={groupName}
properties={getProperties(groupName)} properties={panelDefinition[groupName]}
styleCategory={selectedCategory} styleCategory={selectedCategory}
{onStyleChanged} {onStyleChanged}
{componentDefinition} {componentDefinition}
@ -64,9 +62,6 @@
gap: var(--spacing-l); gap: var(--spacing-l);
} }
.design-view-state-categories {
}
.positioned-wrapper { .positioned-wrapper {
position: relative; position: relative;
display: flex; display: flex;

9
packages/builder/src/components/userInterface/EventsEditor/EventEditorModal.svelte

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

127
packages/builder/src/components/userInterface/EventsEditor/EventsEditor.svelte

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

53
packages/builder/src/components/userInterface/EventsEditor/StateBindingCascader.svelte

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

6
packages/builder/src/components/userInterface/EventsEditor/actions/CreateRow.svelte

@ -1,15 +1,15 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties" import fetchBindableProperties from "builderStore/fetchBindableProperties"
import SaveFields from "./SaveFields.svelte" import SaveFields from "./SaveFields.svelte"
export let parameters export let parameters
$: bindableProperties = fetchBindableProperties({ $: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id, componentInstanceId: $store.selectedComponentId,
components: $store.components, components: $store.components,
screen: $store.currentPreviewItem, screen: $currentAsset,
tables: $backendUiStore.tables, tables: $backendUiStore.tables,
}) })

6
packages/builder/src/components/userInterface/EventsEditor/actions/DeleteRow.svelte

@ -1,6 +1,6 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties" import fetchBindableProperties from "builderStore/fetchBindableProperties"
export let parameters export let parameters
@ -8,9 +8,9 @@
let idFields let idFields
$: bindableProperties = fetchBindableProperties({ $: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id, componentInstanceId: $store.selectedComponentId,
components: $store.components, components: $store.components,
screen: $store.currentPreviewItem, screen: $currentAsset,
tables: $backendUiStore.tables, tables: $backendUiStore.tables,
}) })

6
packages/builder/src/components/userInterface/EventsEditor/actions/SaveFields.svelte

@ -1,7 +1,7 @@
<script> <script>
// accepts an array of field names, and outputs an object of { FieldName: value } // accepts an array of field names, and outputs an object of { FieldName: value }
import { DataList, Label, TextButton, Spacer, Select } from "@budibase/bbui" import { DataList, Label, TextButton, Spacer, Select } from "@budibase/bbui"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties" import fetchBindableProperties from "builderStore/fetchBindableProperties"
import { CloseCircleIcon, AddIcon } from "components/common/Icons" import { CloseCircleIcon, AddIcon } from "components/common/Icons"
import { import {
@ -32,9 +32,9 @@
})) }))
$: bindableProperties = fetchBindableProperties({ $: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id, componentInstanceId: $store.selectedComponentId,
components: $store.components, components: $store.components,
screen: $store.currentPreviewItem, screen: $currentAsset,
tables: $backendUiStore.tables, tables: $backendUiStore.tables,
}) })

6
packages/builder/src/components/userInterface/EventsEditor/actions/SaveRow.svelte

@ -1,6 +1,6 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties" import fetchBindableProperties from "builderStore/fetchBindableProperties"
import SaveFields from "./SaveFields.svelte" import SaveFields from "./SaveFields.svelte"
import { import {
@ -16,9 +16,9 @@
let schemaFields let schemaFields
$: bindableProperties = fetchBindableProperties({ $: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id, componentInstanceId: $store.selectedComponentId,
components: $store.components, components: $store.components,
screen: $store.currentPreviewItem, screen: $currentAsset,
tables: $backendUiStore.tables, tables: $backendUiStore.tables,
}) })

6
packages/builder/src/components/userInterface/EventsEditor/actions/UpdateRow.svelte

@ -1,6 +1,6 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties" import fetchBindableProperties from "builderStore/fetchBindableProperties"
import SaveFields from "./SaveFields.svelte" import SaveFields from "./SaveFields.svelte"
import { import {
@ -11,9 +11,9 @@
export let parameters export let parameters
$: bindableProperties = fetchBindableProperties({ $: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id, componentInstanceId: $store.selectedComponentId,
components: $store.components, components: $store.components,
screen: $store.currentPreviewItem, screen: $currentAsset,
tables: $backendUiStore.tables, tables: $backendUiStore.tables,
}) })

3
packages/builder/src/components/userInterface/FlatButton.svelte

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

1
packages/builder/src/components/userInterface/FlatButtonGroup.svelte

@ -1,6 +1,7 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import FlatButton from "./FlatButton.svelte" import FlatButton from "./FlatButton.svelte"
export let buttonProps = [] export let buttonProps = []
export let isMultiSelect = false export let isMultiSelect = false
export let value = [] export let value = []

81
packages/builder/src/components/userInterface/FrontendNavigatePane.svelte

@ -1,16 +1,33 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import { store, currentScreens } from "builderStore" import { goto, params, url } from "@sveltech/routify"
import api from "builderStore/api" import { store, currentAsset, selectedComponent } from "builderStore"
import { FrontendTypes } from "constants"
import ComponentNavigationTree from "components/userInterface/ComponentNavigationTree/index.svelte" import ComponentNavigationTree from "components/userInterface/ComponentNavigationTree/index.svelte"
import PageLayout from "components/userInterface/PageLayout.svelte" import Layout from "components/userInterface/Layout.svelte"
import PagesList from "components/userInterface/PagesList.svelte"
import NewScreenModal from "components/userInterface/NewScreenModal.svelte" import NewScreenModal from "components/userInterface/NewScreenModal.svelte"
import { Modal } from "@budibase/bbui" import NewLayoutModal from "components/userInterface/NewLayoutModal.svelte"
import { Modal, Switcher } from "@budibase/bbui"
let modal const tabs = [
{
title: "Screens",
key: "screen",
},
{
title: "Layouts",
key: "layout",
},
]
let modal
let routes = {} let routes = {}
let tab = $params.assetType
function navigate({ detail }) {
if (!detail) return
$goto(`../${detail.heading.key}`)
}
onMount(() => { onMount(() => {
store.actions.routing.fetch() store.actions.routing.fetch()
@ -18,32 +35,48 @@
</script> </script>
<div class="title"> <div class="title">
<h1>Screens</h1> <Switcher headings={tabs} bind:value={tab} on:change={navigate}>
<i on:click={modal.show} data-cy="new-screen" class="ri-add-circle-fill" /> {#if tab === FrontendTypes.SCREEN}
</div> <i
<PagesList /> on:click={modal.show}
<div class="nav-items-container"> data-cy="new-screen"
<PageLayout layout={$store.pages[$store.currentPageName]} /> class="ri-add-circle-fill" />
<ComponentNavigationTree /> {#if $currentAsset}
<div class="nav-items-container">
<ComponentNavigationTree />
</div>
{/if}
<Modal bind:this={modal}>
<NewScreenModal />
</Modal>
{:else if tab === FrontendTypes.LAYOUT}
<i
on:click={modal.show}
data-cy="new-layout"
class="ri-add-circle-fill" />
{#each $store.layouts as layout (layout._id)}
<Layout {layout} />
{/each}
<Modal bind:this={modal}>
<NewLayoutModal />
</Modal>
{/if}
</Switcher>
</div> </div>
<Modal bind:this={modal}>
<NewScreenModal />
</Modal>
<style> <style>
.title { .title {
display: flex; display: flex;
flex-direction: row; flex-direction: column;
justify-content: space-between; justify-content: flex-start;
align-items: center; align-items: stretch;
} position: relative;
.title h1 {
font-size: var(--font-size-m);
font-weight: 500;
margin: 0;
} }
.title i { .title i {
font-size: 20px; font-size: 20px;
position: absolute;
top: 0;
right: 0;
} }
.title i:hover { .title i:hover {
cursor: pointer; cursor: pointer;

41
packages/builder/src/components/userInterface/Layout.svelte

@ -0,0 +1,41 @@
<script>
import { goto } from "@sveltech/routify"
import { FrontendTypes } from "constants"
import ComponentTree from "./ComponentNavigationTree/ComponentTree.svelte"
import LayoutDropdownMenu from "./ComponentNavigationTree/LayoutDropdownMenu.svelte"
import initDragDropStore from "./ComponentNavigationTree/dragDropStore"
import NavItem from "components/common/NavItem.svelte"
import { last } from "lodash/fp"
import { store, currentAsset, selectedComponent } from "builderStore"
import { writable } from "svelte/store"
export let layout
let confirmDeleteDialog
let componentToDelete = ""
const dragDropStore = initDragDropStore()
const selectLayout = () => {
store.actions.layouts.select(layout._id)
$goto(`./${layout._id}`)
}
</script>
<NavItem
border={false}
icon="ri-layout-3-line"
text={layout.name}
withArrow
selected={$store.currentAssetId === layout._id}
opened={$store.currentAssetId === layout._id}
on:click={selectLayout}>
<LayoutDropdownMenu {layout} />
</NavItem>
{#if $store.currentAssetId === layout._id && layout.props?._children}
<ComponentTree
components={layout.props._children}
currentComponent={$selectedComponent}
{dragDropStore} />
{/if}

13
packages/builder/src/components/userInterface/LayoutSelect.svelte

@ -0,0 +1,13 @@
<script>
import { store, currentAsset } from "builderStore"
import { Select } from "@budibase/bbui"
export let value
</script>
<Select bind:value extraThin secondary on:change>
<option value="">Choose an option</option>
{#each $store.layouts as layout}
<option value={layout._id}>{layout.name}</option>
{/each}
</Select>

26
packages/builder/src/components/userInterface/NewLayoutModal.svelte

@ -0,0 +1,26 @@
<script>
import { goto } from "@sveltech/routify"
import api from "builderStore/api"
import { notifier } from "builderStore/store/notifications"
import { store, backendUiStore, allScreens } from "builderStore"
import { Input, ModalContent } from "@budibase/bbui"
import analytics from "analytics"
const CONTAINER = "@budibase/standard-components/container"
let name = ""
async function save() {
try {
await store.actions.layouts.save({ name })
$goto(`./${$store.currentAssetId}`)
notifier.success(`Layout ${name} created successfully`)
} catch (err) {
notifier.danger(`Error creating layout ${name}.`)
}
}
</script>
<ModalContent title="Create Layout" confirmText="Create" onConfirm={save}>
<Input thin label="Name" bind:value={name} />
</ModalContent>

6
packages/builder/src/components/userInterface/NewScreenModal.svelte

@ -70,9 +70,9 @@
draftScreen.props._instanceName = name draftScreen.props._instanceName = name
draftScreen.props._component = baseComponent draftScreen.props._component = baseComponent
// TODO: need to fix this up correctly // TODO: need to fix this up correctly
draftScreen.routing = { route, accessLevelId: "ADMIN" } draftScreen.routing = { route, roleId: "ADMIN" }
await store.actions.screens.create(draftScreen) const createdScreen = await store.actions.screens.create(draftScreen)
if (createLink) { if (createLink) {
await store.actions.components.links.save(route, name) await store.actions.components.links.save(route, name)
} }
@ -85,7 +85,7 @@
}) })
} }
$goto(`./:page/${name}`) $goto(`./screen/${createdScreen._id}`)
} }
const routeNameExists = route => { const routeNameExists = route => {

44
packages/builder/src/components/userInterface/PageLayout.svelte

@ -1,44 +0,0 @@
<script>
import { goto } from "@sveltech/routify"
import ComponentTree from "./ComponentNavigationTree/ComponentTree.svelte"
import NavItem from "components/common/NavItem.svelte"
import { last } from "lodash/fp"
import { store } from "builderStore"
import { writable } from "svelte/store"
export let layout
let confirmDeleteDialog
let componentToDelete = ""
const dragDropStore = writable({})
const lastPartOfName = c =>
c && last(c.name ? c.name.split("/") : c._component.split("/"))
$: _layout = {
component: layout,
title: lastPartOfName(layout),
}
const setCurrentScreenToLayout = () => {
store.actions.selectPageOrScreen("page")
$goto("./:page/page-layout")
}
</script>
<NavItem
border={false}
icon="ri-layout-3-line"
text="Master Screen"
withArrow
selected={$store.currentComponentInfo?._id === _layout.component.props._id}
opened={$store.currentPreviewItem?.name === _layout.title}
on:click={setCurrentScreenToLayout} />
{#if $store.currentPreviewItem?.name === _layout.title && _layout.component.props._children}
<ComponentTree
components={_layout.component.props._children}
currentComponent={$store.currentComponentInfo}
{dragDropStore} />
{/if}

3
packages/builder/src/components/userInterface/PagesComponents.svelte

@ -1,3 +0,0 @@
<script>
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
</script>

65
packages/builder/src/components/userInterface/PagesList.svelte

@ -1,65 +0,0 @@
<script>
import { params, goto } from "@sveltech/routify"
import { store } from "builderStore"
const getPage = (state, name) => {
const props = state.pages[name]
return { name, props }
}
const pages = [
{
title: "Private",
id: "main",
},
{
title: "Public",
id: "unauthenticated",
},
]
if (!$store.currentPageName)
store.actions.pages.select($params.page ? $params.page : "main")
const changePage = id => {
store.actions.pages.select(id)
$goto(`./${id}/page-layout`)
}
</script>
<div class="root">
{#each pages as { title, id }}
<button class:active={id === $params.page} on:click={() => changePage(id)}>
{title}
</button>
{/each}
</div>
<style>
.root {
display: flex;
flex-direction: row;
}
button {
cursor: pointer;
padding: 0 var(--spacing-m);
height: 32px;
text-align: center;
background: var(--background);
color: var(--grey-7);
border-radius: 5px;
font-size: var(--font-size-xs);
font-weight: 500;
transition: all 0.3s;
text-rendering: optimizeLegibility;
border: none !important;
outline: none;
font-family: var(--font-sans);
}
.active {
background: var(--grey-2);
color: var(--ink);
}
</style>

18
packages/builder/src/components/userInterface/PropertyControl.svelte

@ -1,7 +1,7 @@
<script> <script>
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
import Input from "./PropertyPanelControls/Input.svelte" import Input from "./PropertyPanelControls/Input.svelte"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties" import fetchBindableProperties from "builderStore/fetchBindableProperties"
import { import {
readableToRuntimeBinding, readableToRuntimeBinding,
@ -22,22 +22,20 @@
export let onChange = () => {} export let onChange = () => {}
let temporaryBindableValue = value let temporaryBindableValue = value
let bindableProperties = []
let anchor
let dropdown
function handleClose() { function handleClose() {
handleChange(key, temporaryBindableValue) handleChange(key, temporaryBindableValue)
} }
let bindableProperties = []
let anchor
let dropdown
function getBindableProperties() { function getBindableProperties() {
// Get all bindableProperties // Get all bindableProperties
bindableProperties = fetchBindableProperties({ bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id, componentInstanceId: $store.selectedComponentId,
components: $store.components, components: $store.components,
screen: $store.currentPreviewItem, screen: $currentAsset,
tables: $backendUiStore.tables, tables: $backendUiStore.tables,
}) })
} }
@ -77,7 +75,7 @@
: temp : temp
} }
//Incase the component has a different value key name // Incase the component has a different value key name
const handlevalueKey = value => const handlevalueKey = value =>
props.valueKey ? { [props.valueKey]: safeValue() } : { value: safeValue() } props.valueKey ? { [props.valueKey]: safeValue() } : { value: safeValue() }
</script> </script>
@ -94,7 +92,7 @@
{...props} {...props}
name={key} /> name={key} />
</div> </div>
{#if bindable && control === Input && !key.startsWith('_')} {#if bindable && !key.startsWith('_') && control === Input}
<div <div
class="icon" class="icon"
data-cy={`${key}-binding-button`} data-cy={`${key}-binding-button`}

6
packages/builder/src/components/userInterface/ScreenSelect.svelte

@ -1,7 +1,7 @@
<script> <script>
import { DataList } from "@budibase/bbui" import { DataList } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { store, allScreens, backendUiStore } from "builderStore" import { store, allScreens, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties" import fetchBindableProperties from "builderStore/fetchBindableProperties"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -27,9 +27,9 @@
] ]
const bindableProperties = fetchBindableProperties({ const bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id, componentInstanceId: $store.selectedComponentId,
components: $store.components, components: $store.components,
screen: $store.currentPreviewItem, screen: $currentAsset,
tables: $backendUiStore.tables, tables: $backendUiStore.tables,
}) })

41
packages/builder/src/components/userInterface/SettingsView.svelte

@ -1,10 +1,11 @@
<script> <script>
import { isEmpty } from "lodash/fp" import { isEmpty } from "lodash/fp"
import { FrontendTypes } from "constants"
import PropertyControl from "./PropertyControl.svelte" import PropertyControl from "./PropertyControl.svelte"
import LayoutSelect from "./LayoutSelect.svelte"
import Input from "./PropertyPanelControls/Input.svelte" import Input from "./PropertyPanelControls/Input.svelte"
import { goto } from "@sveltech/routify"
import { excludeProps } from "./propertyCategories.js" import { excludeProps } from "./propertyCategories.js"
import { store, allScreens } from "builderStore" import { store, allScreens, currentAsset } from "builderStore"
import { walkProps } from "builderStore/storeUtils" import { walkProps } from "builderStore/storeUtils"
export let panelDefinition = [] export let panelDefinition = []
@ -13,13 +14,13 @@
export let onChange = () => {} export let onChange = () => {}
export let onScreenPropChange = () => {} export let onScreenPropChange = () => {}
export let displayNameField = false export let displayNameField = false
export let screenOrPageInstance export let assetInstance
let pageScreenProps = ["title", "favicon", "description", "route"] let assetProps = ["title", "description", "route", "layoutId"]
let duplicateName = false let duplicateName = false
const propExistsOnComponentDef = prop => const propExistsOnComponentDef = prop =>
pageScreenProps.includes(prop) || prop in componentDefinition.props assetProps.includes(prop) || prop in componentDefinition.props
function handleChange(key, data) { function handleChange(key, data) {
data.target ? onChange(key, data.target.value) : onChange(key, data) data.target ? onChange(key, data.target.value) : onChange(key, data)
@ -28,12 +29,10 @@
const screenDefinition = [ const screenDefinition = [
{ key: "description", label: "Description", control: Input }, { key: "description", label: "Description", control: Input },
{ key: "route", label: "Route", control: Input }, { key: "route", label: "Route", control: Input },
{ key: "layoutId", label: "Layout", control: LayoutSelect },
] ]
const pageDefinition = [ const layoutDefinition = [{ key: "title", label: "Title", control: Input }]
{ key: "title", label: "Title", control: Input },
{ key: "favicon", label: "Favicon", control: Input },
]
const canRenderControl = (key, dependsOn) => { const canRenderControl = (key, dependsOn) => {
let test = !isEmpty(componentInstance[dependsOn]) let test = !isEmpty(componentInstance[dependsOn])
@ -44,8 +43,8 @@
) )
} }
$: isPage = screenOrPageInstance && screenOrPageInstance.favicon $: isLayout = assetInstance && assetInstance.favicon
$: screenOrPageDefinition = isPage ? pageDefinition : screenDefinition $: assetDefinition = isLayout ? layoutDefinition : screenDefinition
const isDuplicateName = name => { const isDuplicateName = name => {
let duplicate = false let duplicate = false
@ -58,15 +57,15 @@
} }
}) })
} }
// check page first // check against layouts
lookForDuplicate($store.pages[$store.currentPageName].props) for (let layout of $store.layouts) {
if (duplicate) return true lookForDuplicate(layout.props)
}
// if viewing screen, check current screen for duplicate // if viewing screen, check current screen for duplicate
if ($store.currentFrontEndType === "screen") { if ($store.currentFrontEndType === FrontendTypes.SCREEN) {
lookForDuplicate($store.currentPreviewItem.props) lookForDuplicate($currentAsset.props)
} else { } else {
// viewing master page - need to dedupe against all screens // need to dedupe against all screens
for (let screen of $allScreens) { for (let screen of $allScreens) {
lookForDuplicate(screen.props) lookForDuplicate(screen.props)
} }
@ -86,14 +85,14 @@
</script> </script>
<div class="settings-view-container"> <div class="settings-view-container">
{#if screenOrPageInstance} {#if assetInstance}
{#each screenOrPageDefinition as def} {#each assetDefinition as def}
<PropertyControl <PropertyControl
bindable={false} bindable={false}
control={def.control} control={def.control}
label={def.label} label={def.label}
key={def.key} key={def.key}
value={screenOrPageInstance[def.key]} value={assetInstance[def.key]}
onChange={onScreenPropChange} onChange={onScreenPropChange}
props={{ ...excludeProps(def, ['control', 'label']) }} /> props={{ ...excludeProps(def, ['control', 'label']) }} />
{/each} {/each}

23
packages/builder/src/components/userInterface/TableViewSelect.svelte

@ -1,7 +1,7 @@
<script> <script>
import { Button, Icon, DropdownMenu, Spacer, Heading } from "@budibase/bbui" import { Button, Icon, DropdownMenu, Spacer, Heading } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "../../builderStore/fetchBindableProperties" import fetchBindableProperties from "../../builderStore/fetchBindableProperties"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -32,21 +32,24 @@
}, []) }, [])
$: bindableProperties = fetchBindableProperties({ $: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id, componentInstanceId: $store.selectedComponentId,
components: $store.components, components: $store.components,
screen: $store.currentPreviewItem, screen: $currentAsset,
tables: $backendUiStore.tables, tables: $backendUiStore.tables,
}) })
$: 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

4
packages/builder/src/components/userInterface/pagesParsing/createProps.js → packages/builder/src/components/userInterface/assetParsing/createProps.js

@ -25,7 +25,6 @@ export const createProps = (componentDefinition, derivedFromProps) => {
_id: uuid(), _id: uuid(),
_component: componentDefinition._component, _component: componentDefinition._component,
_styles: { normal: {}, hover: {}, active: {}, selected: {} }, _styles: { normal: {}, hover: {}, active: {}, selected: {} },
_code: "",
} }
const errors = [] const errors = []
@ -96,6 +95,3 @@ const parsePropDef = propDef => {
return cloneDeep(propDef.default) return cloneDeep(propDef.default)
} }
export const arrayElementComponentName = (parentComponentName, arrayPropName) =>
`${parentComponentName}:${arrayPropName}`

0
packages/builder/src/components/userInterface/pagesParsing/getRootComponent.js → packages/builder/src/components/userInterface/assetParsing/getRootComponent.js

0
packages/builder/src/components/userInterface/pagesParsing/searchComponents.js → packages/builder/src/components/userInterface/assetParsing/searchComponents.js

0
packages/builder/src/components/userInterface/pagesParsing/splitRootComponentName.js → packages/builder/src/components/userInterface/assetParsing/splitRootComponentName.js

0
packages/builder/src/components/userInterface/pagesParsing/types.js → packages/builder/src/components/userInterface/assetParsing/types.js

60
packages/builder/src/components/userInterface/pagesParsing/renameScreen.js

@ -1,60 +0,0 @@
import { isPlainObject, isArray, cloneDeep } from "lodash/fp"
import { getExactComponent } from "./searchComponents"
export const rename = (pages, screens, oldname, newname) => {
pages = cloneDeep(pages)
screens = cloneDeep(screens)
const changedScreens = []
const existingWithNewName = getExactComponent(screens, newname, true)
if (existingWithNewName)
return {
components: screens,
pages,
error: "Component by that name already exists",
}
const traverseProps = props => {
let hasEdited = false
if (props._component && props._component === oldname) {
props._component = newname
hasEdited = true
}
for (let propName in props) {
const prop = props[propName]
if (isPlainObject(prop) && prop._component) {
hasEdited = traverseProps(prop) || hasEdited
}
if (isArray(prop)) {
for (let element of prop) {
hasEdited = traverseProps(element) || hasEdited
}
}
}
return hasEdited
}
for (let screen of screens) {
let hasEdited = false
if (screen.props.instanceName === oldname) {
screen.props.instanceName = newname
hasEdited = true
}
hasEdited = traverseProps(screen.props) || hasEdited
if (hasEdited && screen.props.instanceName !== newname)
changedScreens.push(screen.props.instanceName)
}
for (let pageName in pages) {
const page = pages[pageName]
if (page.appBody === oldname) {
page.appBody = newname
}
}
return { screens, pages, changedScreens }
}

56
packages/builder/src/components/userInterface/temporaryPanelStructure.js

@ -310,15 +310,15 @@ export default {
}, },
{ {
label: "Link Color", label: "Link Color",
key: "color", key: "linkColor",
control: Input, control: Colorpicker,
placeholder: "Link Color", defaultValue: "#000",
}, },
{ {
label: "Hover Color", label: "Hover Color",
key: "linkHoverColor", key: "linkHoverColor",
control: Input, control: Colorpicker,
placeholder: "Hover Color", defaultValue: "#222",
}, },
{ {
label: "Image Height", label: "Image Height",
@ -385,15 +385,15 @@ export default {
}, },
{ {
label: "Link Color", label: "Link Color",
key: "color", key: "linkColor",
control: Input, control: Colorpicker,
placeholder: "Link Color", defaultValue: "#000",
}, },
{ {
label: "Hover Color", label: "Hover Color",
key: "linkHoverColor", key: "linkHoverColor",
control: Input, control: Colorpicker,
placeholder: "Hover Color", defaultValue: "#222",
}, },
{ {
label: "Card Width", label: "Card Width",
@ -427,6 +427,36 @@ export default {
], ],
}, },
}, },
{
_component: "@budibase/standard-components/cardstat",
name: "Stat",
description: "A card component for displaying numbers.",
icon: "ri-dual-sim-2-line",
children: [],
properties: {
design: { ...all },
settings: [
{
label: "Title",
key: "title",
control: Input,
placeholder: "Total Revenue",
},
{
label: "Value",
key: "value",
control: Input,
placeholder: "$1,981,983",
},
{
label: "Label",
key: "label",
control: Input,
placeholder: "Stripe",
},
],
},
},
], ],
}, },
{ {
@ -1167,7 +1197,7 @@ export default {
_component: "##builtin/screenslot", _component: "##builtin/screenslot",
name: "Screen Slot", name: "Screen Slot",
description: description:
"This component is a placeholder for the rendering of a screen within a page.", "This component is a placeholder for the rendering of a screen within a layout.",
icon: "ri-crop-2-line", icon: "ri-crop-2-line",
properties: { design: { ...all } }, properties: { design: { ...all } },
commonProps: {}, commonProps: {},
@ -1175,7 +1205,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",
@ -1192,7 +1222,7 @@ export default {
"A component that automatically generates a login screen for your app.", "A component that automatically generates a login screen for your app.",
icon: "ri-login-box-line", icon: "ri-login-box-line",
children: [], children: [],
showOnPages: ["unauthenticated"], showOnAsset: ["login-screen"],
properties: { properties: {
design: { ...all }, design: { ...all },
settings: [ settings: [

10
packages/builder/src/constants/backend/index.js

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

32
packages/builder/src/constants/index.js

@ -1,16 +1,20 @@
export const DEFAULT_PAGES_OBJECT = { export const TableNames = {
main: { USERS: "ta_users",
props: { }
_component: "@budibase/standard-components/container",
}, export const FrontendTypes = {
_screens: {}, PAGE: "page",
}, SCREEN: "screen",
unauthenticated: { LAYOUT: "layout",
props: { NONE: "none",
_component: "@budibase/standard-components/container", }
},
_screens: {}, // fields on the user table that cannot be edited
export const UNEDITABLE_USER_FIELDS = ["email", "password", "roleId"]
export const LAYOUT_NAMES = {
MASTER: {
PRIVATE: "layout_private_master",
PUBLIC: "layout_private_master",
}, },
componentLibraries: [],
stylesheets: [],
} }

7
packages/builder/src/pages/[application]/_reset.svelte

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

4
packages/builder/src/pages/[application]/automate/[automation]/_layout.svelte

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

2
packages/builder/src/pages/[application]/data/_layout.svelte

@ -2,7 +2,7 @@
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte" import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
</script> </script>
<!-- routify:options index=1 --> <!-- routify:options index=0 -->
<div class="root"> <div class="root">
<div class="nav"> <div class="nav">
<TableNavigator /> <TableNavigator />

0
packages/builder/src/pages/[application]/design/[page]/[screen]/_fallback.svelte → packages/builder/src/pages/[application]/design/[assetType]/[asset]/_fallback.svelte

63
packages/builder/src/pages/[application]/design/[assetType]/[asset]/_layout.svelte

@ -0,0 +1,63 @@
<script>
import { params, leftover, goto } from "@sveltech/routify"
import { FrontendTypes } from "constants"
import { store, allScreens } from "builderStore"
// Get any leftover params not caught by Routifys params store.
const componentIds = $leftover.split("/").filter(id => id !== "")
const currentAssetId = decodeURI($params.asset)
let assetList
let actions
// Determine screens or layouts based on the URL
if ($params.assetType === FrontendTypes.SCREEN) {
assetList = $allScreens
actions = store.actions.screens
} else {
assetList = $store.layouts
actions = store.actions.layouts
}
// select the screen or layout in the UI
actions.select(currentAssetId)
// There are leftover stuff, like IDs, so navigate the components and find the ID and select it.
if ($leftover) {
// Get the correct screen children.
const assetChildren = assetList.find(
asset =>
asset._id === $params.asset ||
asset._id === decodeURIComponent($params.asset)
).props._children
findComponent(componentIds, assetChildren)
}
// }
// Find Component with ID and continue
function findComponent(ids, children) {
// Setup stuff
let componentToSelect
let currentChildren = children
// Loop through each ID
ids.forEach(id => {
// Find ID
const component = currentChildren.find(child => child._id === id)
// If it does not exist, ignore (use last valid route)
if (!component) return
componentToSelect = component
// Update childrens array to selected components children
currentChildren = componentToSelect._children
})
// Select Component!
if (componentToSelect) store.actions.components.select(componentToSelect)
}
</script>
<slot />

7
packages/builder/src/pages/[application]/design/_layout.svelte → packages/builder/src/pages/[application]/design/[assetType]/_layout.svelte

@ -1,6 +1,7 @@
<script> <script>
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import { onMount } from "svelte" import { onMount } from "svelte"
import { FrontendTypes } from "constants"
import CurrentItemPreview from "components/userInterface/AppPreview" import CurrentItemPreview from "components/userInterface/AppPreview"
import ComponentPropertiesPanel from "components/userInterface/ComponentPropertiesPanel.svelte" import ComponentPropertiesPanel from "components/userInterface/ComponentPropertiesPanel.svelte"
import ComponentSelectionList from "components/userInterface/ComponentSelectionList.svelte" import ComponentSelectionList from "components/userInterface/ComponentSelectionList.svelte"
@ -26,8 +27,6 @@
const settings = () => { const settings = () => {
settingsView.show() settingsView.show()
} }
const lastPartOfName = c => (c ? last(c.split("/")) : "")
</script> </script>
<!-- routify:options index=1 --> <!-- routify:options index=1 -->
@ -37,7 +36,7 @@
</div> </div>
<div class="preview-pane"> <div class="preview-pane">
{#if $store.currentPageName && $store.currentPageName.length > 0} {#if $store.currentAssetId && $store.currentAssetId.length > 0}
<ComponentSelectionList /> <ComponentSelectionList />
<div class="preview-content"> <div class="preview-content">
<CurrentItemPreview /> <CurrentItemPreview />
@ -45,7 +44,7 @@
{/if} {/if}
</div> </div>
{#if $store.currentFrontEndType === 'screen' || $store.currentFrontEndType === 'page'} {#if $store.currentFrontEndType === FrontendTypes.SCREEN || $store.currentFrontEndType === FrontendTypes.LAYOUT}
<div class="components-pane"> <div class="components-pane">
<ComponentPropertiesPanel /> <ComponentPropertiesPanel />
</div> </div>

17
packages/builder/src/pages/[application]/design/[assetType]/index.svelte

@ -0,0 +1,17 @@
<script>
import { store, allScreens } from "builderStore"
import { FrontendTypes } from "constants"
import { goto, params } from "@sveltech/routify"
// Go to first layout
if ($params.assetType === FrontendTypes.LAYOUT) {
$goto(`../${$store.layouts[0]?._id}`)
}
// Go to first screen
if ($params.assetType === FrontendTypes.SCREEN) {
$goto(`../${$allScreens[0]?._id}`)
}
</script>
<!-- routify:options index=false -->

69
packages/builder/src/pages/[application]/design/[page]/[screen]/_layout.svelte

@ -1,69 +0,0 @@
<script>
import { onMount } from "svelte"
import { params, leftover, goto } from "@sveltech/routify"
import { store, allScreens } from "builderStore"
// Get any leftover params not caught by Routifys params store.
const componentIds = $leftover.split("/").filter(id => id !== "")
// It's a screen, set it to that screen
if ($params.screen !== "page-layout") {
const currentScreenName = decodeURI($params.screen)
const validScreen =
$allScreens.findIndex(screen => screen._id === currentScreenName) !== -1
if (!validScreen) {
// Go to main layout if URL set to invalid screen
store.actions.pages.select("main")
$goto("../../main")
} else {
// Otherwise proceed to set screen
store.actions.screens.select(currentScreenName)
// There are leftover stuff, like IDs, so navigate the components and find the ID and select it.
if ($leftover) {
// Get the correct screen children.
const screenChildren = $store.pages[$params.page]._screens.find(
screen =>
screen._id === $params.screen ||
screen._id === decodeURIComponent($params.screen)
).props._children
findComponent(componentIds, screenChildren)
}
}
} else {
// It's a page, so set the screentype to page.
store.actions.selectPageOrScreen("page")
// There are leftover stuff, like IDs, so navigate the components and find the ID and select it.
if ($leftover) {
findComponent(componentIds, $store.pages[$params.page].props._children)
}
}
// Find Component with ID and continue
function findComponent(ids, children) {
// Setup stuff
let componentToSelect
let currentChildren = children
// Loop through each ID
ids.forEach(id => {
// Find ID
const component = currentChildren.find(child => child._id === id)
// If it does not exist, ignore (use last valid route)
if (!component) return
componentToSelect = component
// Update childrens array to selected components children
currentChildren = componentToSelect._children
})
// Select Component!
if (componentToSelect) store.actions.components.select(componentToSelect)
}
</script>
<slot />

8
packages/builder/src/pages/[application]/design/[page]/_layout.svelte

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

4
packages/builder/src/pages/[application]/design/[page]/index.svelte

@ -1,4 +0,0 @@
<script>
import { goto } from "@sveltech/routify"
$goto("../page-layout")
</script>

6
packages/builder/src/pages/[application]/design/index.svelte

@ -1,6 +1,8 @@
<script> <script>
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
$goto("../main") import { FrontendTypes } from "constants"
$goto(`../${FrontendTypes.SCREEN}`)
</script> </script>
<!-- routify:options index=false --> <!-- routify:options index=1 -->

4
packages/builder/tests/createProps.spec.js

@ -1,4 +1,4 @@
import { createProps } from "../src/components/userInterface/pagesParsing/createProps" import { createProps } from "../src/components/userInterface/assetParsing/createProps"
import { keys, some } from "lodash/fp" import { keys, some } from "lodash/fp"
import { stripStandardProps } from "./testData" import { stripStandardProps } from "./testData"
@ -158,8 +158,6 @@ describe("createDefaultProps", () => {
const comp = getcomponent() const comp = getcomponent()
comp.props.fieldName = { type: "string", default: 1 } comp.props.fieldName = { type: "string", default: 1 }
const { props } = createProps(comp) const { props } = createProps(comp)
expect(props._code).toBeDefined()
expect(props._styles).toBeDefined() expect(props._styles).toBeDefined()
expect(props._code).toBeDefined()
}) })
}) })

20
packages/builder/tests/fetchBindableProperties.spec.js

@ -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", () => {

51
packages/builder/tests/generate_css.spec.js

@ -1,51 +0,0 @@
import {
generate_css,
generate_screen_css,
} from "../src/builderStore/generate_css.js"
describe("generate_css", () => {
test("Check how array styles are output", () => {
expect(generate_css({ margin: ["0", "10", "0", "15"] })).toBe("margin: 0px 10px 0px 15px;")
})
test("Check handling of an array with empty string values", () => {
expect(generate_css({ padding: ["", "", "", ""] })).toBe("")
})
test("Check handling of an empty array", () => {
expect(generate_css({ margin: [] })).toBe("")
})
test("Check handling of valid font property", () => {
expect(generate_css({ "font-size": "10px" })).toBe("font-size: 10px;")
})
})
describe("generate_screen_css", () => {
const normalComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: { "font-size": "16px" }, hover: {}, active: {}, selected: {} } }
test("Test generation of normal css styles", () => {
expect(generate_screen_css([normalComponent])).toBe(".header-123-456 {\nfont-size: 16px;\n}")
})
const hoverComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {"font-size": "16px"}, active: {}, selected: {} } }
test("Test generation of hover css styles", () => {
expect(generate_screen_css([hoverComponent])).toBe(".header-123-456:hover {\nfont-size: 16px;\n}")
})
const selectedComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {}, active: {}, selected: { "font-size": "16px" } } }
test("Test generation of selection css styles", () => {
expect(generate_screen_css([selectedComponent])).toBe(".header-123-456::selection {\nfont-size: 16px;\n}")
})
const emptyComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {}, active: {}, selected: {} } }
test.only("Testing handling of empty component styles", () => {
expect(generate_screen_css([emptyComponent])).toBe("")
})
})

2
packages/builder/tests/searchComponentsProps.spec.js

@ -2,7 +2,7 @@ import {
searchAllComponents, searchAllComponents,
getExactComponent, getExactComponent,
getAncestorProps, getAncestorProps,
} from "../src/components/userInterface/pagesParsing/searchComponents" } from "../src/components/userInterface/assetParsing/searchComponents"
import { componentsAndScreens } from "./testData" import { componentsAndScreens } from "./testData"

1
packages/builder/tests/testData.js

@ -106,7 +106,6 @@ export const componentsAndScreens = () => ({
}) })
export const stripStandardProps = props => { export const stripStandardProps = props => {
delete props._code
delete props._id delete props._id
delete props._styles delete props._styles
} }

3160
packages/builder/yarn.lock

File diff suppressed because it is too large

1
packages/client/.gitignore

@ -1,6 +1,5 @@
.DS_Store .DS_Store
node_modules node_modules
package-lock.json package-lock.json
yarn.lock
release/ release/
dist/ dist/

13
packages/client/babel.config.js

@ -1,13 +0,0 @@
module.exports = {
presets: ["@babel/preset-env"],
sourceMaps: "inline",
retainLines: true,
plugins: [
[
"@babel/plugin-transform-runtime",
{
regenerator: true,
},
],
],
}

47
packages/client/package.json

@ -3,59 +3,30 @@
"version": "0.3.8", "version": "0.3.8",
"license": "MPL-2.0", "license": "MPL-2.0",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
"module": "dist/budibase-client.esm.mjs", "module": "dist/budibase-client.js",
"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"

21
packages/client/rollup.config.js

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

96
packages/client/src/api/api.js

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

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save