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

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

4
package.json

@ -14,7 +14,7 @@
"prettier-plugin-svelte": "^1.4.0",
"rimraf": "^3.0.2",
"rollup-plugin-replace": "^2.2.0",
"svelte": "^3.28.0"
"svelte": "^3.30.0"
},
"scripts": {
"bootstrap": "lerna bootstrap",
@ -26,7 +26,7 @@
"nuke": "rimraf ~/.budibase && npm run restore",
"clean": "lerna clean",
"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",
"lint": "eslint 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", () => {
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.contains("Delete Table").click()
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
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!
cy.get("input[disabled]").should('have.value', 'bbuser')
// // Check to make sure user was created!
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.ENABLE_ANALYTICS = "false"
// Stop info logs polluting test outputs
process.env.LOG_LEVEL = "error"
async function run(dir) {
process.env.BUDIBASE_DIR = resolve(dir)
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.get("input[name=username]")
cy.get("input[name=email]")
.click()
.type("test")
.type("test@test.com")
cy.get("input[name=password]")
.click()
.type("test")
@ -111,25 +111,28 @@ Cypress.Commands.add("addRow", values => {
})
})
Cypress.Commands.add("createUser", (username, password, accessLevel) => {
Cypress.Commands.add("createUser", (email, password, role) => {
// Create User
cy.get(".toprightnav > .settings").click()
cy.contains("Users").click()
cy.get("[name=Name]")
.first()
.type(username)
cy.get("[name=Password]")
.first()
.type(password)
cy.get("select")
.first()
.select(accessLevel)
// Save
cy.get(".inputs")
.contains("Create")
.click()
cy.contains("Create New Row").click()
cy.get(".modal").within(() => {
cy.get("input")
.first()
.type(password)
cy.get("input")
.eq(1)
.type(email)
cy.get("select")
.first()
.select(role)
// Save
cy.get(".buttons")
.contains("Create Row")
.click()
})
})
Cypress.Commands.add("addHeadlineComponent", text => {

11
packages/builder/package.json

@ -63,7 +63,7 @@
}
},
"dependencies": {
"@budibase/bbui": "^1.50.2",
"@budibase/bbui": "^1.52.2",
"@budibase/client": "^0.3.8",
"@budibase/colorpicker": "^1.0.1",
"@budibase/svelte-ag-grid": "^0.0.16",
@ -81,8 +81,8 @@
"shortid": "^2.2.15",
"svelte-loading-spinners": "^0.1.1",
"svelte-portal": "^0.1.0",
"yup": "^0.29.2",
"uuid": "^8.3.1"
"uuid": "^8.3.1",
"yup": "^0.29.2"
},
"devDependencies": {
"@babel/core": "^7.5.5",
@ -90,6 +90,7 @@
"@babel/preset-env": "^7.5.5",
"@babel/runtime": "^7.5.5",
"@rollup/plugin-alias": "^3.0.1",
"@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-json": "^4.0.3",
"@sveltech/routify": "1.7.11",
"@testing-library/jest-dom": "^5.11.0",
@ -104,9 +105,9 @@
"rimraf": "^3.0.2",
"rollup": "^2.11.2",
"rollup-plugin-alias": "^1.5.2",
"rollup-plugin-commonjs": "^10.0.0",
"rollup-plugin-copy": "^3.0.0",
"rollup-plugin-css-only": "^2.1.0",
"rollup-plugin-html": "^0.2.1",
"rollup-plugin-livereload": "^1.0.0",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
@ -115,7 +116,7 @@
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-url": "^2.2.2",
"start-server-and-test": "^1.11.0",
"svelte": "^3.29.0",
"svelte": "^3.30.0",
"svelte-jester": "^1.0.6"
},
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"

111
packages/builder/rollup.config.js

@ -1,7 +1,7 @@
import alias from "@rollup/plugin-alias"
import svelte from "rollup-plugin-svelte"
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 livereload from "rollup-plugin-livereload"
import { terser } from "rollup-plugin-terser"
@ -11,106 +11,12 @@ import copy from "rollup-plugin-copy"
import css from "rollup-plugin-css-only"
import replace from "rollup-plugin-replace"
import json from "@rollup/plugin-json"
import html from "rollup-plugin-html"
import path from "path"
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 coreExternal = [
"lodash",
"lodash/fp",
@ -170,10 +76,6 @@ export default {
{ src: "src/index.html", dest: outputpath },
{ src: "src/favicon.png", dest: outputpath },
{ src: "assets", dest: outputpath },
{
src: "node_modules/@budibase/client/dist/budibase-client.esm.mjs",
dest: outputpath,
},
{
src: "node_modules/@budibase/bbui/dist/bbui.css",
dest: outputpath,
@ -224,13 +126,7 @@ export default {
)
},
}),
commonjs({
namedExports: {
"lodash/fp": lodash_fp_exports,
lodash: lodash_exports,
shortid: ["generate"],
},
}),
commonjs(),
url({
limit: 0,
include: ["**/*.woff2", "**/*.png"],
@ -248,5 +144,6 @@ export default {
// instead of npm run dev), minify
production && terser(),
json(),
html(),
],
}

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

@ -24,7 +24,7 @@ import { cloneDeep, difference } from "lodash/fp"
* @returns {Array.<BindableProperty>}
*/
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)
instance: cloneDeep(screen.props),
targetId: componentInstanceId,
@ -33,13 +33,10 @@ export default function({ componentInstanceId, screen, components, tables }) {
})
return [
...walkResult.bindableInstances
.filter(isInstanceInSharedContext(walkResult))
.map(componentInstanceToBindable(walkResult)),
...(walkResult.target?._contexts
.map(contextToBindables(tables, walkResult))
.flat() ?? []),
...result.bindableInstances
.filter(isInstanceInSharedContext(result))
.map(componentInstanceToBindable),
...(result.target?._contexts.map(contextToBindables(tables)).flat() ?? []),
]
}
@ -53,26 +50,18 @@ const isInstanceInSharedContext = walkResult => i =>
// turns a component instance prop into binding expressions
// used by the UI
const componentInstanceToBindable = walkResult => i => {
const lastContext =
i.instance._contexts.length &&
i.instance._contexts[i.instance._contexts.length - 1]
const contextParentPath = lastContext
? getParentPath(walkResult, lastContext)
: ""
const componentInstanceToBindable = i => {
return {
type: "instance",
instance: i.instance,
// 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
readableBinding: `${i.instance._instanceName}`,
}
}
const contextToBindables = (tables, walkResult) => context => {
const contextParentPath = getParentPath(walkResult, context)
const contextToBindables = tables => context => {
const tableId = context.table?.tableId ?? context.table
const table = tables.find(table => table._id === tableId)
let schema =
@ -98,7 +87,7 @@ const contextToBindables = (tables, walkResult) => context => {
fieldSchema,
instance: context.instance,
// 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
readableBinding: `${context.instance._instanceName}.${table.name}.${key}`,
// 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 }) => {
if (!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 } from "svelte/store"
import { allScreens } from "builderStore"
import { FrontendTypes } from "../constants"
import { currentAsset } from "."
export default function(component, state) {
const capitalised = get_capitalised_name(
@ -19,14 +21,16 @@ export default function(component, state) {
})
}
// check page first
findMatches(state.pages[state.currentPageName].props)
// check layouts first
for (let layout of state.layouts) {
findMatches(layout.props)
}
// if viewing screen, check current screen for duplicate
if (state.currentFrontEndType === "screen") {
findMatches(state.currentPreviewItem.props)
if (state.currentFrontEndType === FrontendTypes.SCREEN) {
findMatches(get(currentAsset).props)
} else {
// viewing master page - need to find against all screens
// viewing a layout - need to find against all screens
for (let screen of get(allScreens)) {
findMatches(screen.props)
}

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

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

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

@ -12,10 +12,7 @@ export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
return boundValue === `{{ ${readableBinding} }}`
})
if (binding) {
result = textWithBindings.replace(
boundValue,
`{{ ${binding.runtimeBinding} }}`
)
result = result.replace(boundValue, `{{ ${binding.runtimeBinding} }}`)
}
})
return result

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

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

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"
function generateTitleContainer(table) {

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

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

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

@ -4,6 +4,7 @@ export class Screen extends BaseStructure {
constructor() {
super(true)
this._json = {
layoutId: "layout_private_master",
props: {
_id: "",
_component: "",
@ -18,7 +19,7 @@ export class Screen extends BaseStructure {
},
routing: {
route: "",
accessLevelId: "",
roleId: "BASIC",
},
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 getNewComponentName from "./getNewComponentName"
export const getParent = (rootProps, child) => {
/**
* Find the parent component of the passed in child.
* @param {Object} rootProps - props to search for the parent in
* @param {String|Object} child - id of the child or the child itself to find the parent of
*/
export const findParent = (rootProps, child) => {
let parent
walkProps(rootProps, (p, breakWalk) => {
walkProps(rootProps, (props, breakWalk) => {
if (
p._children &&
(p._children.includes(child) || p._children.some(c => c._id === child))
props._children &&
(props._children.includes(child) ||
props._children.some(c => c._id === child))
) {
parent = p
parent = props
breakWalk()
}
})

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

@ -62,6 +62,8 @@
</Select>
{:else if value.customType === 'password'}
<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'}
<TableSelector bind:value={block.inputs[key]} />
{:else if value.customType === 'row'}

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

@ -1,14 +1,26 @@
<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 { capitalise } from "../../../helpers"
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
export let meta
export let creating
export let value = meta.type === "boolean" ? false : ""
$: type = meta.type
$: label = capitalise(meta.name)
$: editingUser =
!creating && $backendUiStore.selectedTable?._id === TableNames.USERS
</script>
{#if type === 'options'}
@ -29,6 +41,17 @@
<Toggle text={label} bind:checked={value} data-cy="{meta.name}-input" />
{:else if type === 'link'}
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
{:else if type === 'longform'}
<div>
<Label extraSmall grey>{label}</Label>
<RichText bind:value />
</div>
{: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}

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

@ -7,8 +7,8 @@ export async function createUser(user) {
}
export async function saveRow(row, tableId) {
const SAVE_ROWS_URL = `/api/${tableId}/rows`
const response = await api.post(SAVE_ROWS_URL, row)
const SAVE_ROW_URL = `/api/${tableId}/rows`
const response = await api.post(SAVE_ROW_URL, row)
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 { cloneDeep } from "lodash/fp"
import { backendUiStore } from "builderStore"
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
import { FIELDS } from "constants/backend"
import { notifier } from "builderStore/store/notifications"
import ValuesList from "components/common/ValuesList.svelte"
@ -30,6 +31,9 @@
table => table._id !== $backendUiStore.draftTable._id
)
$: required = !!field?.constraints?.presence || primaryDisplay
$: uneditable =
$backendUiStore.selectedTable?._id === TableNames.USERS &&
UNEDITABLE_USER_FIELDS.includes(field.name)
async function saveColumn() {
backendUiStore.update(state => {
@ -87,7 +91,7 @@
</script>
<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
disabled={originalName}
@ -101,7 +105,7 @@
{/each}
</Select>
{#if field.type !== 'link'}
{#if field.type !== 'link' && !uneditable}
<Toggle
checked={required}
on:change={onChangeRequired}
@ -157,7 +161,7 @@
bind:value={field.fieldName} />
{/if}
<footer class="create-column-options">
{#if originalName}
{#if !uneditable && originalName}
<TextButton text on:click={confirmDelete}>Delete Column</TextButton>
{/if}
<Button secondary on:click={onClosed}>Cancel</Button>

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

@ -1,5 +1,6 @@
<script>
import { backendUiStore } from "builderStore"
import { TableNames } from "constants"
import { notifier } from "builderStore/store/notifications"
import RowFieldControl from "../RowFieldControl.svelte"
import * as api from "../api"
@ -21,9 +22,10 @@
{ ...row, tableId: table._id },
table._id
)
if (rowResponse.errors) {
errors = Object.keys(rowResponse.errors)
.map(k => ({ dataPath: k, message: rowResponse.errors[k] }))
errors = Object.entries(rowResponse.errors)
.map(([key, error]) => ({ dataPath: key, message: error }))
.flat()
// Prevent modal closing if there were errors
return false
@ -38,9 +40,15 @@
confirmText={creating ? 'Create Row' : 'Save Row'}
onConfirm={saveRow}>
<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]}
<div>
<RowFieldControl {meta} bind:value={row[key]} />
<RowFieldControl {meta} bind:value={row[key]} {creating} />
</div>
{/each}
</ModalContent>

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

@ -1,6 +1,7 @@
<script>
import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import { TableNames } from "constants"
import ListItem from "./ListItem.svelte"
import CreateTableModal from "./modals/CreateTableModal.svelte"
import EditTablePopover from "./popovers/EditTablePopover.svelte"
@ -43,7 +44,7 @@
{#each $backendUiStore.tables as table, idx}
<NavItem
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}
selected={selectedView === `all_${table._id}`}
on:click={() => selectTable(table)}>

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

@ -54,14 +54,13 @@
const screens = screenTemplates($store, [table])
.filter(template => defaultScreens.includes(template.id))
.map(template => template.create())
store.actions.pages.select("main")
for (let screen of screens) {
// Record the table that created this screen so we can link it later
screen.autoTableId = table._id
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 =>
screen.props._instanceName.endsWith("List")
)

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

@ -1,5 +1,5 @@
<script>
import { General, Users, DangerZone, APIKeys } from "./tabs"
import { General, DangerZone, APIKeys } from "./tabs"
import { Switcher, ModalContent } from "@budibase/bbui"
const tabs = [
@ -8,11 +8,6 @@
key: "GENERAL",
component: General,
},
{
title: "Users",
key: "USERS",
component: Users,
},
{
title: "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 Integrations } from "./Integrations.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 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."),
},
{
username: string().required("Your application needs a first user."),
email: string()
.email()
.required("Your application needs a first user."),
password: string().required(
"Please enter a password for your first user."
),
accessLevelId: string().required(
"You need to select an access level for your user."
),
roleId: string().required("You need to select a role for your user."),
},
]
@ -79,9 +79,7 @@
if (hasKey) {
validationSchemas.shift()
validationSchemas = validationSchemas
steps.shift()
steps = steps
}
// Handles form navigation
@ -155,19 +153,17 @@
const pkg = await applicationPkg.json()
if (applicationPkg.ok) {
backendUiStore.actions.reset()
pkg.justCreated = true
await store.actions.initialise(pkg)
automationStore.actions.fetch()
await automationStore.actions.fetch()
} else {
throw new Error(pkg)
}
// Create user
const user = {
name: $createAppStore.values.username,
username: $createAppStore.values.username,
email: $createAppStore.values.email,
password: $createAppStore.values.password,
accessLevelId: $createAppStore.values.accessLevelId,
roleId: $createAppStore.values.roleId,
}
const userResp = await api.post(`/api/users`, user)
const json = await userResp.json()

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

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

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

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

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

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

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

@ -1,6 +1,6 @@
<script>
import { goto } from "@sveltech/routify"
import { store } from "builderStore"
import { store, currentAsset } from "builderStore"
import { getComponentDefinition } from "builderStore/storeUtils"
import { DropEffect, DropPosition } from "./dragDropStore"
import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte"
@ -10,7 +10,6 @@
export let currentComponent
export let onSelect = () => {}
export let level = 0
export let dragDropStore
const isScreenslot = name => name === "##builtin/screenslot"
@ -23,7 +22,7 @@
const path = store.actions.components.findRoute(component)
// Go to correct URL
$goto(`./:page/:screen/${path}`)
$goto(`./${$store.currentAssetId}/${path}`)
}
const dragstart = component => e => {
@ -73,7 +72,7 @@
text={isScreenslot(component._component) ? 'Screenslot' : component._instanceName}
withArrow
indentLevel={level + 3}
selected={currentComponent === component}>
selected={$store.selectedComponentId === component._id}>
<ComponentDropdownMenu {component} />
</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>
import { writable } from "svelte/store"
import { goto } from "@sveltech/routify"
import { store } from "builderStore"
import { store, selectedComponent, currentAsset } from "builderStore"
import instantiateStore from "./dragDropStore"
import ComponentsTree from "./ComponentTree.svelte"
import ComponentTree from "./ComponentTree.svelte"
import NavItem from "components/common/NavItem.svelte"
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
const ROUTE_NAME_MAP = {
"/": {
BASIC: "Home",
PUBLIC: "Login",
},
}
const dragDropStore = instantiateStore()
export let route
export let path
export let indent
$: selectedScreen = $store.currentPreviewItem
$: selectedScreen = $currentAsset
const changeScreen = screenId => {
// select the route
store.actions.screens.select(screenId)
$goto(`./:page/${screenId}`)
$goto(`./${screenId}`)
}
</script>
@ -30,21 +37,21 @@
withArrow={route.subpaths} />
{#each Object.entries(route.subpaths) as [url, subpath]}
{#each Object.values(subpath.screens) as screenId}
{#each Object.entries(subpath.screens) as [role, screenId]}
<NavItem
icon="ri-artboard-2-line"
indentLevel={indent || 1}
selected={$store.currentPreviewItem._id === screenId}
opened={$store.currentPreviewItem._id === screenId}
text={url === '/' ? 'Home' : url}
selected={$store.currentAssetId === screenId}
opened={$store.currentAssetId === screenId}
text={ROUTE_NAME_MAP[url]?.[role] || url}
withArrow={route.subpaths}
on:click={() => changeScreen(screenId)}>
<ScreenDropdownMenu screen={screenId} />
<ScreenDropdownMenu {screenId} />
</NavItem>
{#if selectedScreen?._id === screenId}
<ComponentsTree
<ComponentTree
components={selectedScreen.props._children}
currentComponent={$store.currentComponentInfo}
currentComponent={$selectedComponent}
{dragDropStore} />
{/if}
{/each}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,11 +1,11 @@
<script>
import { TextButton, Body, DropdownMenu, ModalContent } from "@budibase/bbui"
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
import { EVENT_TYPE_MEMBER_NAME } from "../../../../../client/src/state/eventHandlers"
import actionTypes from "./actions"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
const eventTypeKey = "##eventHandlerType"
export let event
@ -18,8 +18,7 @@
$: actions = event || []
$: selectedActionComponent =
selectedAction &&
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_MEMBER_NAME])
.component
actionTypes.find(t => t.name === selectedAction[eventTypeKey]).component
const updateEventHandler = (updatedHandler, index) => {
actions[index] = updatedHandler
@ -33,7 +32,7 @@
const addAction = actionType => () => {
const newAction = {
parameters: {},
[EVENT_TYPE_MEMBER_NAME]: actionType.name,
[eventTypeKey]: actionType.name,
}
actions.push(newAction)
selectedAction = newAction
@ -79,7 +78,7 @@
{#each actions as action, index}
<div class="action-container">
<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}>
<ArrowDownIcon />
</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>
import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore } from "builderStore"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import SaveFields from "./SaveFields.svelte"
export let parameters
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id,
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $store.currentPreviewItem,
screen: $currentAsset,
tables: $backendUiStore.tables,
})

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

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

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

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

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

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

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

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

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

@ -1,9 +1,8 @@
<script>
import { buildStyle } from "../../helpers.js"
export let value = ""
export let text = ""
export let icon = ""
export let onClick = value => {}
export let onClick = () => {}
export let selected = false
$: useIcon = !!icon

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

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

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

@ -1,16 +1,33 @@
<script>
import { onMount } from "svelte"
import { store, currentScreens } from "builderStore"
import api from "builderStore/api"
import { goto, params, url } from "@sveltech/routify"
import { store, currentAsset, selectedComponent } from "builderStore"
import { FrontendTypes } from "constants"
import ComponentNavigationTree from "components/userInterface/ComponentNavigationTree/index.svelte"
import PageLayout from "components/userInterface/PageLayout.svelte"
import PagesList from "components/userInterface/PagesList.svelte"
import Layout from "components/userInterface/Layout.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 tab = $params.assetType
function navigate({ detail }) {
if (!detail) return
$goto(`../${detail.heading.key}`)
}
onMount(() => {
store.actions.routing.fetch()
@ -18,32 +35,48 @@
</script>
<div class="title">
<h1>Screens</h1>
<i on:click={modal.show} data-cy="new-screen" class="ri-add-circle-fill" />
</div>
<PagesList />
<div class="nav-items-container">
<PageLayout layout={$store.pages[$store.currentPageName]} />
<ComponentNavigationTree />
<Switcher headings={tabs} bind:value={tab} on:change={navigate}>
{#if tab === FrontendTypes.SCREEN}
<i
on:click={modal.show}
data-cy="new-screen"
class="ri-add-circle-fill" />
{#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>
<Modal bind:this={modal}>
<NewScreenModal />
</Modal>
<style>
.title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.title h1 {
font-size: var(--font-size-m);
font-weight: 500;
margin: 0;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
position: relative;
}
.title i {
font-size: 20px;
position: absolute;
top: 0;
right: 0;
}
.title i:hover {
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._component = baseComponent
// 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) {
await store.actions.components.links.save(route, name)
}
@ -85,7 +85,7 @@
})
}
$goto(`./:page/${name}`)
$goto(`./screen/${createdScreen._id}`)
}
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>
import { Icon } from "@budibase/bbui"
import Input from "./PropertyPanelControls/Input.svelte"
import { store, backendUiStore } from "builderStore"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import {
readableToRuntimeBinding,
@ -22,22 +22,20 @@
export let onChange = () => {}
let temporaryBindableValue = value
let bindableProperties = []
let anchor
let dropdown
function handleClose() {
handleChange(key, temporaryBindableValue)
}
let bindableProperties = []
let anchor
let dropdown
function getBindableProperties() {
// Get all bindableProperties
bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id,
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $store.currentPreviewItem,
screen: $currentAsset,
tables: $backendUiStore.tables,
})
}
@ -77,7 +75,7 @@
: temp
}
//Incase the component has a different value key name
// Incase the component has a different value key name
const handlevalueKey = value =>
props.valueKey ? { [props.valueKey]: safeValue() } : { value: safeValue() }
</script>
@ -94,7 +92,7 @@
{...props}
name={key} />
</div>
{#if bindable && control === Input && !key.startsWith('_')}
{#if bindable && !key.startsWith('_') && control === Input}
<div
class="icon"
data-cy={`${key}-binding-button`}

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

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

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

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

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

@ -1,7 +1,7 @@
<script>
import { Button, Icon, DropdownMenu, Spacer, Heading } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { store, backendUiStore } from "builderStore"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "../../builderStore/fetchBindableProperties"
const dispatch = createEventDispatcher()
@ -32,21 +32,24 @@
}, [])
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id,
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $store.currentPreviewItem,
screen: $currentAsset,
tables: $backendUiStore.tables,
})
$: links = bindableProperties
.filter(x => x.fieldSchema?.type === "link")
.map(property => ({
label: property.readableBinding,
fieldName: property.fieldSchema.name,
name: `all_${property.fieldSchema.tableId}`,
tableId: property.fieldSchema.tableId,
type: "link",
}))
.map(property => {
return {
providerId: property.instance._id,
label: property.readableBinding,
fieldName: property.fieldSchema.name,
name: `all_${property.fieldSchema.tableId}`,
tableId: property.fieldSchema.tableId,
type: "link",
}
})
</script>
<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(),
_component: componentDefinition._component,
_styles: { normal: {}, hover: {}, active: {}, selected: {} },
_code: "",
}
const errors = []
@ -96,6 +95,3 @@ const parsePropDef = propDef => {
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",
key: "color",
control: Input,
placeholder: "Link Color",
key: "linkColor",
control: Colorpicker,
defaultValue: "#000",
},
{
label: "Hover Color",
key: "linkHoverColor",
control: Input,
placeholder: "Hover Color",
control: Colorpicker,
defaultValue: "#222",
},
{
label: "Image Height",
@ -385,15 +385,15 @@ export default {
},
{
label: "Link Color",
key: "color",
control: Input,
placeholder: "Link Color",
key: "linkColor",
control: Colorpicker,
defaultValue: "#000",
},
{
label: "Hover Color",
key: "linkHoverColor",
control: Input,
placeholder: "Hover Color",
control: Colorpicker,
defaultValue: "#222",
},
{
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",
name: "Screen Slot",
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",
properties: { design: { ...all } },
commonProps: {},
@ -1175,7 +1205,7 @@ export default {
},
{
name: "Nav Bar",
_component: "@budibase/standard-components/Navigation",
_component: "@budibase/standard-components/navigation",
description:
"A component for handling the navigation within your app.",
icon: "ri-navigation-line",
@ -1192,7 +1222,7 @@ export default {
"A component that automatically generates a login screen for your app.",
icon: "ri-login-box-line",
children: [],
showOnPages: ["unauthenticated"],
showOnAsset: ["login-screen"],
properties: {
design: { ...all },
settings: [

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

@ -9,6 +9,16 @@ export const FIELDS = {
presence: false,
},
},
LONGFORM: {
name: "Long Form Text",
icon: "ri-file-text-line",
type: "longform",
constraints: {
type: "string",
length: {},
presence: false,
},
},
OPTIONS: {
name: "Options",
icon: "ri-list-check-2",

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

@ -1,16 +1,20 @@
export const DEFAULT_PAGES_OBJECT = {
main: {
props: {
_component: "@budibase/standard-components/container",
},
_screens: {},
},
unauthenticated: {
props: {
_component: "@budibase/standard-components/container",
},
_screens: {},
export const TableNames = {
USERS: "ta_users",
}
export const FrontendTypes = {
PAGE: "page",
SCREEN: "screen",
LAYOUT: "layout",
NONE: "none",
}
// 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">
<ThemeEditor />
<FeedbackNavLink />
<div class="topnavitemright">
<a target="_blank" href="https://docs.budibase.com">
<i class="ri-question-line" />
</a>
</div>
<div class="topnavitemright">
<a
target="_blank"
href="https://github.com/Budibase/budibase/discussions">
<i class="ri-discuss-line" />
<i class="ri-question-line" />
</a>
</div>
<SettingsLink />

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

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

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

@ -2,7 +2,7 @@
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
</script>
<!-- routify:options index=1 -->
<!-- routify:options index=0 -->
<div class="root">
<div class="nav">
<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>
import { store, backendUiStore } from "builderStore"
import { onMount } from "svelte"
import { FrontendTypes } from "constants"
import CurrentItemPreview from "components/userInterface/AppPreview"
import ComponentPropertiesPanel from "components/userInterface/ComponentPropertiesPanel.svelte"
import ComponentSelectionList from "components/userInterface/ComponentSelectionList.svelte"
@ -26,8 +27,6 @@
const settings = () => {
settingsView.show()
}
const lastPartOfName = c => (c ? last(c.split("/")) : "")
</script>
<!-- routify:options index=1 -->
@ -37,7 +36,7 @@
</div>
<div class="preview-pane">
{#if $store.currentPageName && $store.currentPageName.length > 0}
{#if $store.currentAssetId && $store.currentAssetId.length > 0}
<ComponentSelectionList />
<div class="preview-content">
<CurrentItemPreview />
@ -45,7 +44,7 @@
{/if}
</div>
{#if $store.currentFrontEndType === 'screen' || $store.currentFrontEndType === 'page'}
{#if $store.currentFrontEndType === FrontendTypes.SCREEN || $store.currentFrontEndType === FrontendTypes.LAYOUT}
<div class="components-pane">
<ComponentPropertiesPanel />
</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>
import { goto } from "@sveltech/routify"
$goto("../main")
import { FrontendTypes } from "constants"
$goto(`../${FrontendTypes.SCREEN}`)
</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 { stripStandardProps } from "./testData"
@ -158,8 +158,6 @@ describe("createDefaultProps", () => {
const comp = getcomponent()
comp.props.fieldName = { type: "string", default: 1 }
const { props } = createProps(comp)
expect(props._code).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.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", () => {
@ -37,20 +37,22 @@ describe("fetch bindable properties", () => {
expect(contextBindings.length).toBe(4)
const namebinding = contextBindings.find(
b => b.runtimeBinding === "data.name"
b => b.runtimeBinding === "list-id.name"
)
expect(namebinding).toBeDefined()
expect(namebinding.readableBinding).toBe("list-name.Test Table.name")
const descriptionbinding = contextBindings.find(
b => b.runtimeBinding === "data.description"
b => b.runtimeBinding === "list-id.description"
)
expect(descriptionbinding).toBeDefined()
expect(descriptionbinding.readableBinding).toBe(
"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.readableBinding).toBe("list-name.Test Table._id")
})
@ -65,13 +67,13 @@ describe("fetch bindable properties", () => {
expect(contextBindings.length).toBe(8)
const namebinding_parent = contextBindings.find(
b => b.runtimeBinding === "parent.data.name"
b => b.runtimeBinding === "list-id.name"
)
expect(namebinding_parent).toBeDefined()
expect(namebinding_parent.readableBinding).toBe("list-name.Test Table.name")
const descriptionbinding_parent = contextBindings.find(
b => b.runtimeBinding === "parent.data.description"
b => b.runtimeBinding === "list-id.description"
)
expect(descriptionbinding_parent).toBeDefined()
expect(descriptionbinding_parent.readableBinding).toBe(
@ -79,7 +81,7 @@ describe("fetch bindable properties", () => {
)
const namebinding_own = contextBindings.find(
b => b.runtimeBinding === "data.name"
b => b.runtimeBinding === "child-list-id.name"
)
expect(namebinding_own).toBeDefined()
expect(namebinding_own.readableBinding).toBe(
@ -87,7 +89,7 @@ describe("fetch bindable properties", () => {
)
const descriptionbinding_own = contextBindings.find(
b => b.runtimeBinding === "data.description"
b => b.runtimeBinding === "child-list-id.description"
)
expect(descriptionbinding_own).toBeDefined()
expect(descriptionbinding_own.readableBinding).toBe(
@ -104,7 +106,7 @@ describe("fetch bindable properties", () => {
r => r.instance._id === "list-item-input-id" && r.type === "instance"
)
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", () => {

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,
getExactComponent,
getAncestorProps,
} from "../src/components/userInterface/pagesParsing/searchComponents"
} from "../src/components/userInterface/assetParsing/searchComponents"
import { componentsAndScreens } from "./testData"

1
packages/builder/tests/testData.js

@ -106,7 +106,6 @@ export const componentsAndScreens = () => ({
})
export const stripStandardProps = props => {
delete props._code
delete props._id
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
node_modules
package-lock.json
yarn.lock
release/
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",
"license": "MPL-2.0",
"main": "dist/budibase-client.js",
"module": "dist/budibase-client.esm.mjs",
"module": "dist/budibase-client.js",
"scripts": {
"build": "rollup -c",
"test": "jest",
"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": {
"deep-equal": "^2.0.1",
"mustache": "^4.0.1",
"regexparam": "^1.3.0"
"regexparam": "^1.3.0",
"svelte-spa-router": "^3.0.5"
},
"devDependencies": {
"@babel/core": "^7.5.5",
"@babel/plugin-transform-runtime": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"@babel/runtime": "^7.5.5",
"babel-jest": "^24.8.0",
"@budibase/standard-components": "^0.3.8",
"@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-node-resolve": "^10.0.0",
"fs-extra": "^8.1.0",
"jest": "^24.8.0",
"jsdom": "^16.0.1",
"rollup": "^1.12.0",
"rollup-plugin-commonjs": "^10.0.0",
"rollup": "^2.33.2",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-svelte": "^6.1.1",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^4.0.4",
"svelte": "^3.29.7",
"svelte": "^3.30.0",
"svelte-jester": "^1.0.6"
},
"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 nodeglobals from "rollup-plugin-node-globals"
import svelte from "rollup-plugin-svelte"
const production = !process.env.ROLLUP_WATCH
export default {
input: "src/index.js",
output: [
{
sourcemap: true,
format: "iife",
name: "app",
file: `./dist/budibase-client.js`,
},
{
file: "dist/budibase-client.esm.mjs",
format: "esm",
sourcemap: "inline",
file: `./dist/budibase-client.js`,
},
],
plugins: [
svelte({
dev: !production,
}),
resolve({
preferBuiltins: true,
browser: true,
dedupe: ["svelte", "svelte/internal"],
}),
commonjs(),
builtins(),
nodeglobals(),
],
watch: {
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