Browse Source

Merge pull request #3597 from Budibase/develop

Develop -> Master
pull/3614/head
Martin McKeaveney 5 years ago
committed by GitHub
parent
commit
b1e2c2ae98
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/workflows/budibase_ci.yml
  2. 2
      .github/workflows/release-develop.yml
  3. 2
      .github/workflows/release-selfhost.yml
  4. 4
      .github/workflows/release.yml
  5. 1
      .gitignore
  6. 46
      README.md
  7. 1
      hosting/docker-compose.yaml
  8. 2
      hosting/kubernetes/budibase/templates/worker-service-deployment.yaml
  9. 51
      hosting/scripts/airgapped/airgappedDockerBuild.js
  10. 2
      lerna.json
  11. 3
      package.json
  12. 1
      packages/auth/cache.js
  13. 2
      packages/auth/package.json
  14. 85
      packages/auth/src/cache/appMetadata.js
  15. 88
      packages/auth/src/db/utils.js
  16. 1
      packages/auth/src/environment.js
  17. 4
      packages/auth/src/middleware/authenticated.js
  18. 1
      packages/auth/src/objectStore/utils.js
  19. 10
      packages/auth/src/redis/authRedis.js
  20. 1
      packages/auth/src/redis/utils.js
  21. 1050
      packages/auth/yarn.lock
  22. 3
      packages/bbui/package.json
  23. 4
      packages/bbui/src/Form/Combobox.svelte
  24. 11
      packages/bbui/src/Form/Core/Checkbox.svelte
  25. 17
      packages/bbui/src/Form/Core/Combobox.svelte
  26. 10
      packages/bbui/src/Form/Core/TextField.svelte
  27. 2
      packages/bbui/src/Form/Input.svelte
  28. 56
      packages/bbui/src/InlineAlert/InlineAlert.svelte
  29. 57
      packages/bbui/src/Label/Label.svelte
  30. 20
      packages/bbui/src/Tooltip/Tooltip.svelte
  31. 1
      packages/bbui/src/index.js
  32. 5
      packages/bbui/yarn.lock
  33. 290
      packages/builder/assets/bb-space-man.svg
  34. BIN
      packages/builder/assets/oracle.png
  35. 75
      packages/builder/cypress/integration/addMultiOptionDatatype.spec.js
  36. 6
      packages/builder/cypress/integration/createAutomation.spec.js
  37. 2
      packages/builder/cypress/integration/createTable.spec.js
  38. 1
      packages/builder/cypress/setup.js
  39. 13
      packages/builder/cypress/support/commands.js
  40. 12
      packages/builder/package.json
  41. 120
      packages/builder/src/builderStore/dataBinding.js
  42. 2
      packages/builder/src/builderStore/store/automation/index.js
  43. 21
      packages/builder/src/builderStore/store/frontend.js
  44. 2
      packages/builder/src/builderStore/store/screenTemplates/createFromScratchScreen.js
  45. 1
      packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js
  46. 1
      packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js
  47. 1
      packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js
  48. 19
      packages/builder/src/builderStore/storeUtils.js
  49. 1
      packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte
  50. 13
      packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte
  51. 1
      packages/builder/src/components/automation/AutomationPanel/AutomationList.svelte
  52. 13
      packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte
  53. 44
      packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
  54. 16
      packages/builder/src/components/backend/DataTable/DataTable.svelte
  55. 4
      packages/builder/src/components/backend/DataTable/RowFieldControl.svelte
  56. 17
      packages/builder/src/components/backend/DataTable/Table.svelte
  57. 54
      packages/builder/src/components/backend/DataTable/buttons/ExistingRelationshipButton.svelte
  58. 2
      packages/builder/src/components/backend/DataTable/buttons/ExportButton.svelte
  59. 15
      packages/builder/src/components/backend/DataTable/buttons/ImportButton.svelte
  60. 165
      packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
  61. 17
      packages/builder/src/components/backend/DataTable/modals/FilterModal.svelte
  62. 43
      packages/builder/src/components/backend/DataTable/modals/ImportModal.svelte
  63. 2
      packages/builder/src/components/backend/DataTable/modals/ManageAccessModal.svelte
  64. 16
      packages/builder/src/components/backend/DatasourceNavigator/icons/Oracle.svelte
  65. 2
      packages/builder/src/components/backend/DatasourceNavigator/icons/index.js
  66. 10
      packages/builder/src/components/backend/DatasourceNavigator/popovers/EditDatasourcePopover.svelte
  67. 60
      packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte
  68. 0
      packages/builder/src/components/backend/Datasources/TableSelect.svelte
  69. 45
      packages/builder/src/components/backend/TableNavigator/TableDataImport.svelte
  70. 65
      packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte
  71. 8
      packages/builder/src/components/common/CodeMirrorEditor.svelte
  72. 75
      packages/builder/src/components/common/bindings/BindingPanel.svelte
  73. 30
      packages/builder/src/components/common/bindings/ClientBindingPanel.svelte
  74. 36
      packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte
  75. 7
      packages/builder/src/components/common/bindings/DrawerBindableInput.svelte
  76. 12
      packages/builder/src/components/common/bindings/ModalBindableInput.svelte
  77. 222
      packages/builder/src/components/common/bindings/ServerBindingPanel.svelte
  78. 55
      packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte
  79. 34
      packages/builder/src/components/design/AppPreview/componentStructure.json
  80. 5
      packages/builder/src/components/design/AppPreview/iframeTemplate.js
  81. 14
      packages/builder/src/components/design/NavigationPanel/ComponentDropdownMenu.svelte
  82. 17
      packages/builder/src/components/design/NavigationPanel/FrontendNavigatePane.svelte
  83. 93
      packages/builder/src/components/design/NavigationPanel/NavigationSelectionModal.svelte
  84. 253
      packages/builder/src/components/design/NavigationPanel/NewScreenModal.svelte
  85. 70
      packages/builder/src/components/design/NavigationPanel/ScreenDetailsModal.svelte
  86. 133
      packages/builder/src/components/design/NavigationPanel/ScreenWizard.svelte
  87. 66
      packages/builder/src/components/design/PropertiesPanel/ComponentSettingsSection.svelte
  88. 10
      packages/builder/src/components/design/PropertiesPanel/PropertiesPanel.svelte
  89. 26
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/ConditionalUIDrawer.svelte
  90. 5
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataProviderSelect.svelte
  91. 181
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataSourceSelect.svelte
  92. 4
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte
  93. 15
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/Input.svelte
  94. 6
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/NavigationEditor/NavigationEditor.svelte
  95. 84
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte
  96. 8
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/URLSelect.svelte
  97. 4
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/componentSettings.js
  98. 1
      packages/builder/src/components/design/PropertiesPanel/ScreenSettingsSection.svelte
  99. 1
      packages/builder/src/components/design/PropertiesPanel/StyleSection.svelte
  100. 8
      packages/builder/src/components/design/PropertiesPanel/componentStyles.js

2
.github/workflows/budibase_ci.yml

@ -18,7 +18,7 @@ jobs:
strategy:
matrix:
node-version: [12.x]
node-version: [14.x]
steps:
- uses: actions/checkout@v2

2
.github/workflows/release-develop.yml

@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12.x
node-version: 14.x
- run: yarn
- run: yarn bootstrap
- run: yarn lint

2
.github/workflows/release-selfhost.yml

@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12.x
node-version: 14.x
- run: yarn
- run: yarn bootstrap

4
.github/workflows/release.yml

@ -19,7 +19,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12.x
node-version: 14.x
- run: yarn
- run: yarn bootstrap
- run: yarn lint
@ -55,4 +55,4 @@ jobs:
env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
BUDIBASE_RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }}
BUDIBASE_RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }}

1
.gitignore

@ -3,6 +3,7 @@ builder/*
.temp/
packages/server/runtime_apps/
.idea/
bb-airgapped.tar.gz
# Logs
logs

46
README.md

@ -54,17 +54,51 @@
<br /><br />
## ✨ Features
- **Build and ship real software.** Unlike other platforms, with Budibase you build and ship single page applications. Budibase applications have performance baked in and can be designed responsively, providing your users with a great experience.
### Build and ship real software
Unlike other platforms, with Budibase you build and ship single page applications. Budibase applications have performance baked in and can be designed responsively, providing your users with a great experience.
<br /><br />
### Open source and extensible
Budibase is open-source - licensed as GPL v3. This should fill you with confidence that Budibase will always be around. You can also code against Budibase or fork it and make changes as you please, providing a developer-friendly experience.
<br /><br />
- **Open source and extensible.** Budibase is open-source - licensed as GPL v3. This should fill you with confidence that Budibase will always be around. You can also code against Budibase or fork it and make changes as you please, providing a developer-friendly experience.
### Load data or start from scratch
Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new data sources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
- **Load data or start from scratch.** Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new data sources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
<p align="center">
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/data_n1tlhf.png">
</p>
<br /><br />
- **Design and build apps with powerful pre-made components.** Budibase comes out of the box with beautifully designed, powerful components which you can use like building blocks to build your UI. We also expose a lot of your favourite CSS styling options so you can go that extra creative mile. [Request new component](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
### Design and build apps with powerful pre-made components
Budibase comes out of the box with beautifully designed, powerful components which you can use like building blocks to build your UI. We also expose a lot of your favourite CSS styling options so you can go that extra creative mile. [Request new component](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
<p align="center">
<img alt="Budibase design" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970243/Out%20of%20beta%20launch/design-like-a-pro_qhlfeu.gif">
</p>
<br /><br />
### Automate processes, integrate with other tools, and connect to webhooks
Save time by automating manual processes and workflows. From connecting to webhooks, to automating emails, simply tell Budibase what to do and let it work for you. You can easily [create new automations for Budibase here](https://github.com/Budibase/automations) or [Request new automation](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
<p align="center">
<img alt="Budibase automations" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970486/Out%20of%20beta%20launch/automation_riro7u.png">
</p>
<br /><br />
### Integrate with your favorite tools
Budibase integrates with a number of popular tools allowing you to build apps that perfectly fit your stack.
<p align="center">
<img alt="Budibase integrations" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/integrations_kc7dqt.png">
</p>
<br /><br />
- **Automate processes, integrate with other tools, and connect to webhooks.** Save time by automating manual processes and workflows. From connecting to webhooks, to automating emails, simply tell Budibase what to do and let it work for you. You can easily [create new automations for Budibase here](https://github.com/Budibase/automations) or [Request new automation](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
### Admin paradise
Budibase is made to scale. With Budibase, you can self-host on your own infrastructure and globally manage users, onboarding, SMTP, apps, groups, theming and more. You can also provide users/groups with an app portal and disseminate user-management to the group manager.
- **Admin paradise.** Budibase is made to scale. With Budibase, you can self-host on your own infrastructure and globally manage users, onboarding, SMTP, apps, groups, theming and more. You can also provide users/groups with an app portal and disseminate user-management to the group manager.
- Checkout the promo video: https://youtu.be/xoljVpty_Kw
<br /><br /><br />

1
hosting/docker-compose.yaml

@ -45,6 +45,7 @@ services:
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
MINIO_URL: http://minio-service:9000
APPS_URL: http://app-service:4002
COUCH_DB_USERNAME: ${COUCH_DB_USER}
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984

2
hosting/kubernetes/budibase/templates/worker-service-deployment.yaml

@ -113,6 +113,8 @@ spec:
value: {{ .Values.globals.smtp.port | quote }}
- name: SMTP_FROM_ADDRESS
value: {{ .Values.globals.smtp.from | quote }}
- name: APPS_URL
value: http://app-service:{{ .Values.services.apps.port }}
image: budibase/worker
imagePullPolicy: Always
name: bbworker

51
hosting/scripts/airgapped/airgappedDockerBuild.js

@ -0,0 +1,51 @@
const fs = require("fs")
const { execSync } = require("child_process")
const path = require("path")
const IMAGES = {
worker: "budibase/worker",
apps: "budibase/apps",
proxy: "envoyproxy/envoy:v1.16-latest",
minio: "minio/minio",
couch: "ibmcom/couchdb3",
curl: "curlimages/curl",
redis: "redis",
watchtower: "containrrr/watchtower"
}
const FILES = {
COMPOSE: "docker-compose.yaml",
ENVOY: "envoy.yaml",
PROPERTIES: "hosting.properties"
}
const OUTPUT_DIR = path.join(__dirname, "../", "bb-airgapped")
function copyFile(file) {
fs.copyFileSync(
path.join(__dirname, "../", "../", file),
path.join(OUTPUT_DIR, file)
)
}
// create output dir
console.log(`Creating ${OUTPUT_DIR} for build..`)
fs.rmdirSync(OUTPUT_DIR, { recursive: true })
fs.mkdirSync(OUTPUT_DIR)
// package images into tar files
for (let image in IMAGES) {
console.log(`Creating tar for ${image}..`)
execSync(`docker save ${IMAGES[image]} -o ${OUTPUT_DIR}/${image}.tar`)
}
// copy config files
copyFile(FILES.COMPOSE)
copyFile(FILES.ENVOY)
copyFile(FILES.PROPERTIES)
// compress
execSync(`tar -czf bb-airgapped.tar.gz hosting/scripts/bb-airgapped`)
// clean up
fs.rmdirSync(OUTPUT_DIR, { recursive: true })

2
lerna.json

@ -1,5 +1,5 @@
{
"version": "0.9.189",
"version": "0.9.190-alpha.12",
"npmClient": "yarn",
"packages": [
"packages/*"

3
package.json

@ -46,6 +46,8 @@
"build:docker": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
"build:docker:selfhost": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
"build:docker:develop": "node scripts/pinVersions && lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
"build:docs": "lerna run build:docs",
"release:helm": "./scripts/release_helm_chart.sh",
"env:multi:enable": "lerna run env:multi:enable",
"env:multi:disable": "lerna run env:multi:disable",
@ -58,6 +60,7 @@
"mode:self": "yarn env:selfhost:enable && yarn env:multi:disable && yarn env:account:disable",
"mode:cloud": "yarn env:selfhost:disable && yarn env:multi:enable && yarn env:account:disable",
"mode:account": "yarn mode:cloud && yarn env:account:enable",
"security:audit": "node scripts/audit.js",
"postinstall": "husky install"
}
}

1
packages/auth/cache.js

@ -1,3 +1,4 @@
module.exports = {
user: require("./src/cache/user"),
app: require("./src/cache/appMetadata"),
}

2
packages/auth/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/auth",
"version": "0.9.189",
"version": "0.9.190-alpha.12",
"description": "Authentication middlewares for budibase builder and apps",
"main": "src/index.js",
"author": "Budibase",

85
packages/auth/src/cache/appMetadata.js

@ -0,0 +1,85 @@
const redis = require("../redis/authRedis")
const { getCouch } = require("../db")
const { DocumentTypes } = require("../db/constants")
const AppState = {
INVALID: "invalid",
}
const EXPIRY_SECONDS = 3600
/**
* The default populate app metadata function
*/
const populateFromDB = async (appId, CouchDB = null) => {
if (!CouchDB) {
CouchDB = getCouch()
}
const db = new CouchDB(appId, { skip_setup: true })
return db.get(DocumentTypes.APP_METADATA)
}
const isInvalid = metadata => {
return !metadata || metadata.state === AppState.INVALID
}
/**
* Get the requested app metadata by id.
* Use redis cache to first read the app metadata.
* If not present fallback to loading the app metadata directly and re-caching.
* @param {string} appId the id of the app to get metadata from.
* @param {object} CouchDB the database being passed
* @returns {object} the app metadata.
*/
exports.getAppMetadata = async (appId, CouchDB = null) => {
const client = await redis.getAppClient()
// try cache
let metadata = await client.get(appId)
if (!metadata) {
let expiry = EXPIRY_SECONDS
try {
metadata = await populateFromDB(appId, CouchDB)
} catch (err) {
// app DB left around, but no metadata, it is invalid
if (err && err.status === 404) {
metadata = { state: AppState.INVALID }
// don't expire the reference to an invalid app, it'll only be
// updated if a metadata doc actually gets stored (app is remade/reverted)
expiry = undefined
} else {
throw err
}
}
// needed for cypress/some scenarios where the caching happens
// so quickly the requests can get slightly out of sync
// might store its invalid just before it stores its valid
if (isInvalid(metadata)) {
const temp = await client.get(appId)
if (temp) {
metadata = temp
}
}
await client.store(appId, metadata, expiry)
}
// we've stored in the cache an object to tell us that it is currently invalid
if (isInvalid(metadata)) {
throw { status: 404, message: "No app metadata found" }
}
return metadata
}
/**
* Invalidate/reset the cached metadata when a change occurs in the db.
* @param appId {string} the cache key to bust/update.
* @param newMetadata {object|undefined} optional - can simply provide the new metadata to update with.
* @return {Promise<void>} will respond with success when cache is updated.
*/
exports.invalidateAppMetadata = async (appId, newMetadata = null) => {
if (!appId) {
throw "Cannot invalidate if no app ID provided."
}
const client = await redis.getAppClient()
await client.delete(appId)
if (newMetadata) {
await client.store(appId, newMetadata, EXPIRY_SECONDS)
}
}

88
packages/auth/src/db/utils.js

@ -1,11 +1,14 @@
const { newid } = require("../hashing")
const Replication = require("./Replication")
const { DEFAULT_TENANT_ID } = require("../constants")
const { DEFAULT_TENANT_ID, Configs } = require("../constants")
const env = require("../environment")
const { StaticDatabases, SEPARATOR, DocumentTypes } = require("./constants")
const { getTenantId, getTenantIDFromAppID } = require("../tenancy")
const fetch = require("node-fetch")
const { getCouch } = require("./index")
const { getAppMetadata } = require("../cache/appMetadata")
const NO_APP_ERROR = "No app provided"
const UNICODE_MAX = "\ufff0"
@ -45,14 +48,23 @@ function getDocParams(docType, docId = null, otherProps = {}) {
}
exports.isDevAppID = appId => {
if (!appId) {
throw NO_APP_ERROR
}
return appId.startsWith(exports.APP_DEV_PREFIX)
}
exports.isProdAppID = appId => {
if (!appId) {
throw NO_APP_ERROR
}
return appId.startsWith(exports.APP_PREFIX) && !exports.isDevAppID(appId)
}
function isDevApp(app) {
if (!app) {
throw NO_APP_ERROR
}
return exports.isDevAppID(app.appId)
}
@ -152,6 +164,17 @@ exports.getDeployedAppID = appId => {
return appId
}
/**
* Convert a deployed app ID to a development app ID.
*/
exports.getDevelopmentAppID = appId => {
if (!appId.startsWith(exports.APP_DEV_PREFIX)) {
const id = appId.split(exports.APP_PREFIX)[1]
return `${exports.APP_DEV_PREFIX}${id}`
}
return appId
}
exports.getCouchUrl = () => {
if (!env.COUCH_DB_URL) return
@ -221,16 +244,16 @@ exports.getAllApps = async (CouchDB, { dev, all, idsOnly } = {}) => {
if (idsOnly) {
return appDbNames
}
const appPromises = appDbNames.map(db =>
const appPromises = appDbNames.map(app =>
// skip setup otherwise databases could be re-created
new CouchDB(db, { skip_setup: true }).get(DocumentTypes.APP_METADATA)
getAppMetadata(app, CouchDB)
)
if (appPromises.length === 0) {
return []
} else {
const response = await Promise.allSettled(appPromises)
const apps = response
.filter(result => result.status === "fulfilled")
.filter(result => result.status === "fulfilled" && result.value != null)
.map(({ value }) => value)
if (!all) {
return apps.filter(app => {
@ -248,6 +271,24 @@ exports.getAllApps = async (CouchDB, { dev, all, idsOnly } = {}) => {
}
}
/**
* Utility function for getAllApps but filters to production apps only.
*/
exports.getDeployedAppIDs = async CouchDB => {
return (await exports.getAllApps(CouchDB, { idsOnly: true })).filter(
id => !exports.isDevAppID(id)
)
}
/**
* Utility function for the inverse of above.
*/
exports.getDevAppIDs = async CouchDB => {
return (await exports.getAllApps(CouchDB, { idsOnly: true })).filter(id =>
exports.isDevAppID(id)
)
}
exports.dbExists = async (CouchDB, dbName) => {
let exists = false
try {
@ -322,13 +363,50 @@ const getScopedFullConfig = async function (db, { type, user, workspace }) {
}
// Find the config with the most granular scope based on context
const scopedConfig = response.rows.sort(
let scopedConfig = response.rows.sort(
(a, b) => determineScore(a) - determineScore(b)
)[0]
// custom logic for settings doc
// always provide the platform URL
if (type === Configs.SETTINGS) {
if (scopedConfig && scopedConfig.doc) {
scopedConfig.doc.config.platformUrl = await getPlatformUrl(
scopedConfig.doc.config
)
} else {
scopedConfig = {
doc: {
config: {
platformUrl: await getPlatformUrl(),
},
},
}
}
}
return scopedConfig && scopedConfig.doc
}
const getPlatformUrl = async settings => {
let platformUrl = env.PLATFORM_URL
if (!env.SELF_HOSTED && env.MULTI_TENANCY) {
// cloud and multi tenant - add the tenant to the default platform url
const tenantId = getTenantId()
if (!platformUrl.includes("localhost:")) {
platformUrl = platformUrl.replace("://", `://${tenantId}.`)
}
} else {
// self hosted - check for platform url override
if (settings && settings.platformUrl) {
platformUrl = settings.platformUrl
}
}
return platformUrl ? platformUrl : "http://localhost:10000"
}
async function getScopedConfig(db, params) {
const configDoc = await getScopedFullConfig(db, params)
return configDoc && configDoc.config ? configDoc.config : configDoc

1
packages/auth/src/environment.js

@ -25,6 +25,7 @@ module.exports = {
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED),
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
PLATFORM_URL: process.env.PLATFORM_URL,
isTest,
_set(key, value) {
process.env[key] = value

4
packages/auth/src/middleware/authenticated.js

@ -92,6 +92,10 @@ module.exports = (
finalise(ctx, { authenticated, user, internal, version, publicEndpoint })
return next()
} catch (err) {
// invalid token, clear the cookie
if (err && err.name === "JsonWebTokenError") {
clearCookie(ctx, Cookies.Auth)
}
// allow configuring for public access
if ((opts && opts.publicAllowed) || publicEndpoint) {
finalise(ctx, { authenticated: false, version, publicEndpoint })

1
packages/auth/src/objectStore/utils.js

@ -6,6 +6,7 @@ exports.ObjectStoreBuckets = {
APPS: "prod-budi-app-assets",
TEMPLATES: "templates",
GLOBAL: "global",
GLOBAL_CLOUD: "prod-budi-tenant-uploads",
}
exports.budibaseTempDir = function () {

10
packages/auth/src/redis/authRedis.js

@ -1,16 +1,18 @@
const Client = require("./index")
const utils = require("./utils")
let userClient, sessionClient
let userClient, sessionClient, appClient
async function init() {
userClient = await new Client(utils.Databases.USER_CACHE).init()
sessionClient = await new Client(utils.Databases.SESSIONS).init()
appClient = await new Client(utils.Databases.APP_METADATA).init()
}
process.on("exit", async () => {
if (userClient) await userClient.finish()
if (sessionClient) await sessionClient.finish()
if (appClient) await appClient.finish()
})
module.exports = {
@ -26,4 +28,10 @@ module.exports = {
}
return sessionClient
},
getAppClient: async () => {
if (!appClient) {
await init()
}
return appClient
},
}

1
packages/auth/src/redis/utils.js

@ -15,6 +15,7 @@ exports.Databases = {
SESSIONS: "session",
USER_CACHE: "users",
FLAGS: "flags",
APP_METADATA: "appMetadata",
}
exports.SEPARATOR = SEPARATOR

1050
packages/auth/yarn.lock

File diff suppressed because it is too large

3
packages/bbui/package.json

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "0.9.189",
"version": "0.9.190-alpha.12",
"license": "MPL-2.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",
@ -51,6 +51,7 @@
"@spectrum-css/fieldlabel": "^3.0.1",
"@spectrum-css/icon": "^3.0.1",
"@spectrum-css/illustratedmessage": "^3.0.2",
"@spectrum-css/inlinealert": "^2.0.1",
"@spectrum-css/inputgroup": "^3.0.2",
"@spectrum-css/label": "^2.0.10",
"@spectrum-css/link": "^3.1.1",

4
packages/bbui/src/Form/Combobox.svelte

@ -6,6 +6,7 @@
export let value = null
export let label = undefined
export let disabled = false
export let readonly = false
export let labelPosition = "above"
export let error = null
export let placeholder = "Choose an option or type"
@ -33,8 +34,11 @@
{value}
{options}
{placeholder}
{readonly}
{getOptionLabel}
{getOptionValue}
on:change={onChange}
on:pick
on:type
/>
</Field>

11
packages/bbui/src/Form/Core/Checkbox.svelte

@ -21,6 +21,7 @@
<label
class="spectrum-Checkbox spectrum-Checkbox--emphasized {sizeClass}"
class:is-invalid={!!error}
class:checked={value}
>
<input
checked={value}
@ -50,6 +51,16 @@
</label>
<style>
.spectrum-Checkbox--sizeL .spectrum-Checkbox-checkmark {
transform: scale(1.1);
left: 55%;
top: 55%;
}
.spectrum-Checkbox--sizeXL .spectrum-Checkbox-checkmark {
transform: scale(1.2);
left: 60%;
top: 60%;
}
.spectrum-Checkbox-input {
opacity: 0;
}

17
packages/bbui/src/Form/Core/Combobox.svelte

@ -9,6 +9,7 @@
export let id = null
export let placeholder = "Choose an option or type"
export let disabled = false
export let readonly = false
export let error = null
export let options = []
export let getOptionLabel = option => option
@ -40,8 +41,15 @@
open = false
}
const onChange = e => {
selectOption(e.target.value)
const onType = e => {
const value = e.target.value
dispatch("type", value)
selectOption(value)
}
const onPick = value => {
dispatch("pick", value)
selectOption(value)
}
</script>
@ -62,10 +70,11 @@
type="text"
on:focus={() => (focus = true)}
on:blur={() => (focus = false)}
on:change={onChange}
on:change={onType}
value={value || ""}
placeholder={placeholder || ""}
{disabled}
{readonly}
class="spectrum-Textfield-input spectrum-InputGroup-input"
/>
</div>
@ -99,7 +108,7 @@
role="option"
aria-selected="true"
tabindex="0"
on:click={() => selectOption(getOptionValue(option))}
on:click={() => onPick(getOptionValue(option))}
>
<span class="spectrum-Menu-itemLabel"
>{getOptionLabel(option)}</span

10
packages/bbui/src/Form/Core/TextField.svelte

@ -11,19 +11,20 @@
export let readonly = false
export let updateOnChange = true
export let quiet = false
export let dataCy
const dispatch = createEventDispatcher()
let focus = false
const updateValue = value => {
const updateValue = newValue => {
if (readonly) {
return
}
if (type === "number") {
const float = parseFloat(value)
value = isNaN(float) ? null : float
const float = parseFloat(newValue)
newValue = isNaN(float) ? null : float
}
dispatch("change", value)
dispatch("change", newValue)
}
const onFocus = () => {
@ -78,6 +79,7 @@
{disabled}
{readonly}
{id}
data-cy={dataCy}
value={value || ""}
placeholder={placeholder || ""}
on:click

2
packages/bbui/src/Form/Input.svelte

@ -13,6 +13,7 @@
export let error = null
export let updateOnChange = true
export let quiet = false
export let dataCy
const dispatch = createEventDispatcher()
const onChange = e => {
@ -23,6 +24,7 @@
<Field {label} {labelPosition} {error}>
<TextField
{dataCy}
{updateOnChange}
{error}
{disabled}

56
packages/bbui/src/InlineAlert/InlineAlert.svelte

@ -0,0 +1,56 @@
<script>
import "@spectrum-css/inlinealert/dist/index-vars.css"
import Button from "../Button/Button.svelte"
export let type = "info"
export let header = ""
export let message = ""
export let onConfirm = undefined
$: icon = selectIcon(type)
// if newlines used, convert them to different elements
$: split = message.split("\n")
function selectIcon(alertType) {
switch (alertType) {
case "error":
case "negative":
return "Alert"
case "success":
return "CheckmarkCircle"
case "help":
return "Help"
default:
return "Info"
}
}
</script>
<div class="spectrum-InLineAlert spectrum-InLineAlert--{type}">
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-InLineAlert-icon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-{icon}" />
</svg>
<div class="spectrum-InLineAlert-header">{header}</div>
{#each split as splitMsg}
<div class="spectrum-InLineAlert-content">{splitMsg}</div>
{/each}
{#if onConfirm}
<div class="spectrum-InLineAlert-footer">
<Button secondary on:click={onConfirm}>OK</Button>
</div>
{/if}
</div>
<style>
.spectrum-InLineAlert {
--spectrum-semantic-negative-border-color: #e34850;
--spectrum-semantic-positive-border-color: #2d9d78;
--spectrum-semantic-positive-icon-color: #2d9d78;
--spectrum-semantic-negative-icon-color: #e34850;
min-width: 100px;
}
</style>

57
packages/bbui/src/Label/Label.svelte

@ -1,16 +1,67 @@
<script>
import "@spectrum-css/fieldlabel/dist/index-vars.css"
import Tooltip from "../Tooltip/Tooltip.svelte"
import Icon from "../Icon/Icon.svelte"
export let size = "M"
export let tooltip = ""
export let showTooltip = false
</script>
<label for="" class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}>
<slot />
</label>
{#if tooltip}
<div class="container">
<label
for=""
class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}
>
<slot />
</label>
<div class="icon-container">
<div
class="icon"
on:mouseover={() => (showTooltip = true)}
on:mouseleave={() => (showTooltip = false)}
>
<Icon name="InfoOutline" size="S" disabled={true} />
</div>
{#if showTooltip}
<div class="tooltip">
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
</div>
{/if}
</div>
</div>
{:else}
<label for="" class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}>
<slot />
</label>
{/if}
<style>
label {
padding: 0;
white-space: nowrap;
}
.container {
display: flex;
}
.icon-container {
position: relative;
display: flex;
justify-content: center;
margin-top: 1px;
margin-left: 5px;
margin-right: 5px;
}
.tooltip {
position: absolute;
display: flex;
justify-content: center;
top: 15px;
z-index: 1;
width: 160px;
}
.icon {
transform: scale(0.75);
}
</style>

20
packages/bbui/src/Tooltip/Tooltip.svelte

@ -3,12 +3,22 @@
export let direction = "top"
export let text = ""
export let textWrapping = false
</script>
<span class="u-tooltip-showOnHover tooltip">
<slot />
<div class={`spectrum-Tooltip spectrum-Tooltip--${direction}`}>
<!-- Showing / Hiding a text wrapped tooltip should be handled outside the component -->
{#if textWrapping}
<span class="spectrum-Tooltip spectrum-Tooltip--{direction} is-open">
<span class="spectrum-Tooltip-label">{text}</span>
<span class="spectrum-Tooltip-tip" />
</div>
</span>
</span>
{:else}
<!-- The default show on hover tooltip does not support text wrapping -->
<span class="u-tooltip-showOnHover tooltip">
<slot />
<div class={`spectrum-Tooltip spectrum-Tooltip--${direction}`}>
<span class="spectrum-Tooltip-label">{text}</span>
<span class="spectrum-Tooltip-tip" />
</div>
</span>
{/if}

1
packages/bbui/src/index.js

@ -58,6 +58,7 @@ export { default as Pagination } from "./Pagination/Pagination.svelte"
export { default as Badge } from "./Badge/Badge.svelte"
export { default as StatusLight } from "./StatusLight/StatusLight.svelte"
export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte"
export { default as InlineAlert } from "./InlineAlert/InlineAlert.svelte"
// Typography
export { default as Body } from "./Typography/Body.svelte"

5
packages/bbui/yarn.lock

@ -136,6 +136,11 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/illustratedmessage/-/illustratedmessage-3.0.2.tgz#6a480be98b027e050b086e7899e40d87adb0a8c0"
integrity sha512-dqnE8X27bGcO0HN8+dYx8O4o0dNNIAqeivOzDHhe2El+V4dTzMrNIerF6G0NLm3GjVf6XliwmitsZK+K6FmbtA==
"@spectrum-css/inlinealert@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@spectrum-css/inlinealert/-/inlinealert-2.0.1.tgz#7521f88f6c845806403cc7d925773c7414e204a2"
integrity sha512-Xy5RCOwgurqUXuGQCsEDUduDd5408bmEpmFg+feynG7VFUgLFZWBeylSENB/OqjlFtO76PHXNVdHkhDscPIHTA==
"@spectrum-css/inputgroup@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@spectrum-css/inputgroup/-/inputgroup-3.0.2.tgz#f1b13603832cbd22394f3d898af13203961f8691"

290
packages/builder/assets/bb-space-man.svg

@ -0,0 +1,290 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 300 300" style="enable-background:new 0 0 300 300;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#Path_1317_00000103227440483474435190000016933188118935327121_);}
.st1{fill:url(#Path_1318_00000101104841355089154840000006956526578009727927_);}
.st2{fill:url(#Path_1319_00000075161440626185692110000014996044328070588571_);}
.st3{fill:#C1CAE8;}
.st4{fill:#7A8BB6;}
.st5{fill:#030404;}
</style>
<g id="Group_143" transform="translate(-862 -389)">
<g id="Component_4_1" transform="translate(862 389)">
<linearGradient id="Path_1317_00000031906008897315583580000011933716758006396041_" gradientUnits="userSpaceOnUse" x1="-47.7766" y1="297.132" x2="-45.9378" y2="297.132" gradientTransform="matrix(158.712 0 0 -159.1408 7586.6768 47435.5273)">
<stop offset="0" style="stop-color:#7DC8DA"/>
<stop offset="0.992" style="stop-color:#E67FB0"/>
</linearGradient>
<path id="Path_1317" style="fill:url(#Path_1317_00000031906008897315583580000011933716758006396041_);" d="M295.5,141
C290.9,60.5,222-1,141.5,3.6C67.5,7.9,8.4,66.9,4.2,141l0,0v0.1C4,143.8,4,146.5,4,149.3c0,2.8,0.1,6,0.1,6L4.2,296h291.6V146.5
L295.5,141z"/>
<linearGradient id="Path_1318_00000022543908329791846970000010277972945691778971_" gradientUnits="userSpaceOnUse" x1="553.9203" y1="-1379.7488" x2="555.7591" y2="-1379.7488" gradientTransform="matrix(0.6482 0 0 -0.1118 -97.2294 -70.4447)">
<stop offset="6.000000e-03" style="stop-color:#E6A480"/>
<stop offset="0.998" style="stop-color:#F5B463"/>
</linearGradient>
<path id="Path_1318" style="fill:url(#Path_1318_00000022543908329791846970000010277972945691778971_);" d="M263,83.9
c-1.7-0.3-1.2-0.2-0.7-0.1c0.2,0,0.4,0.1,0.6,0.1L263,83.9z"/>
<linearGradient id="Path_1319_00000018948029474416481050000007780147372085156030_" gradientUnits="userSpaceOnUse" x1="530.262" y1="-1661.0725" x2="532.1008" y2="-1661.0725" gradientTransform="matrix(0.6484 0 0 -0.1118 -115.4 -32.9427)">
<stop offset="6.000000e-03" style="stop-color:#E6A480"/>
<stop offset="0.998" style="stop-color:#F5B463"/>
</linearGradient>
<path id="Path_1319" style="fill:url(#Path_1319_00000018948029474416481050000007780147372085156030_);" d="M229.6,152.9
c-1.7-0.3-1.2-0.2-0.7-0.1c0.2,0,0.4,0.1,0.6,0.1L229.6,152.9z"/>
<path id="Path_1321" class="st3" d="M239,272.3c-0.1-2.1-0.1-4.8-0.5-7.4c0.1-0.2,0.1-0.4,0.2-0.7c-0.1,0-0.2-0.1-0.3-0.1
c-0.1-1-0.2-1.9-0.4-2.9v-95.7c-0.2-4-0.4-8-0.6-12.1c0.3-0.2,0.5-0.6,0.5-0.9l0.6-13.1c-5.8,0.3-11.6-0.1-17.3,0
c-3.8,0.1-7.6,0-11.5-0.2c-3.5-0.4-7-0.3-10.5,0.3c-1,0.2-2-0.5-2.2-1.5c0-0.2,0-0.5,0-0.7c0-0.4,0-0.8,0-1.2
c0.4-8.2-0.6-16.5-2.7-24.4c-1.9-6.6-5.2-12.8-9.6-18c-1.2-0.9-2.5-1.8-3.8-2.6c-8.3-5.4-17.9-8.3-27.8-8.3
c-0.5,0-0.9-0.1-1.3-0.4c-3.9,0.4-7.8,1.3-11.5,2.6c-4.1,1.4-7.9,3.3-11.5,5.6c-4.5,3.6-8.2,8.2-10.7,13.4
c-2.5,5.6-2.9,11.9-4.4,17.8c-0.1,0.3-0.2,0.6-0.5,0.8c-1,8.6-1.2,17.2-0.7,25.8c0.1,0.1,0.1,0.1,0.2,0.2c0.1,0.1,0.2,0.3,0.3,0.4
c0,0,0,0.1,0.1,0.1c0,0.1,0.1,0.1,0.1,0.2c0.1,0.2,0.2,0.3,0.2,0.5l0.1,0.5c0,0.3-0.1,0.6-0.2,0.9l-0.3,0.4
c-0.2,0.2-0.4,0.3-0.6,0.4c-0.2,0.1-0.5,0.2-0.7,0.2c-0.2,0-0.4,0-0.5-0.1c-1.8,2.6-0.3,6.7-2.3,9.6c-0.8,1.2-1.7,2.2-2.5,3.3
c0,0.4-0.1,0.8-0.3,1.1c-0.9,1.3-1.8,2.6-2.7,4c-0.4,0.7-0.9,1.4-1.3,2.1c-7.5,12-13,25.7-17.8,38.8c0,0,0,0.1,0,0.1
c0.1,0.1,0.2,0.1,0.3,0.2c-0.1,0.2-0.3,0.4-0.4,0.6c-0.2,0.3-0.3,0.6-0.4,1c-1.4-0.3-2.9-0.6-4.3-0.9c-6.5-2.1-13-4-19.6-5.7
c-0.2,0-0.3-0.1-0.5-0.1c-3.1-1-6.2-1.9-9.3-2.7l-6.7-2c0-0.1-0.1-0.1-0.1-0.2c-0.3-0.6-1.1-0.8-1.7-0.4c-0.6,0.3-0.8,1.1-0.4,1.7
l0.1,0.1c0,0.4,0.3,0.8,0.7,1c1,1.4,1.9,2.9,2.8,4.4c3.9,6.5,6.8,13.5,8.6,20.8c0.2,0.6,0.9,1,1.5,0.8c0.3-0.1,0.6-0.3,0.7-0.6
c2,4.6,3.9,9.1,5.9,13.7c0.2,0.4,0.6,0.6,1,0.6c0.2,0.4,0.4,0.9,0.7,1.2c1,1,2.3,1.6,3.7,1.9l8,2.4c0.3,0.5,0.7,0.8,1.2,1
c0.4,0.1,0.8,0.3,1.1,0.4l-0.6,1.2c-0.6,1-0.2,2.4,0.8,3c0,0,0.1,0,0.1,0.1c0.2,0.2,0.5,0.4,0.7,0.6c0.9,0.4,1.9,0.7,2.9,0.9
l9.5,3.8c0.6,0.3,1.2,0.2,1.7-0.1c1.8,0.6,3.6,0.8,4.5-1c0.2-0.4,0.3-0.8,0.3-1.2c2.9,1,5.8,2,8.7,3c0.1,5.3,0.2,10.6,0.2,15.8
c0,1.3,0.4,2.6,1.2,3.7c0,1.1-0.1,2.3-0.1,3.4c0,0.3-0.6,9.5-0.7,12.1H129c0.2,0,0.5,0,0.7,0.1h37.5c0.1,0,0.1-0.1,0.2-0.1
c0.9,0,1.8-0.1,2.8-0.1c0.2-0.1,0.4-0.1,0.7-0.1h49.3c0.2,0.1,0.5,0.1,0.7,0.1h16.1c0.2,0.1,0.4,0.1,0.6,0.1
c0.3,0,0.5-0.1,0.7-0.2c0.9-0.3,1.4-1.3,1.1-2.2c0-0.1,0-0.1-0.1-0.2v-4.4c0-0.1,0-0.2,0-0.3c0.1-0.2,0.2-0.4,0.2-0.6
C240.7,282.7,239.2,277.1,239,272.3z"/>
<path id="Path_1322" class="st4" d="M212,159.8c-0.8-3.4-3.7-5.3-4.6-8.4c-0.6-2,0.2-4.2,0.1-6.2c-0.1-1-0.4-1.9-0.9-2.7
c-0.4-0.6-0.9-1.3-0.6-2.1c0.4-1.2-1.5-1.7-1.9-0.5c-0.1,0.2-0.1,0.3-0.1,0.5c-0.5-0.3-1.1-0.2-1.4,0.3c0,0,0,0,0,0
c0,0-0.1,0-0.1,0c-0.1-0.7-0.7-1.3-1.4-1.5c-0.4-0.1-0.9,0.1-1.1,0.4l-0.1,0.2c-0.1-0.1-0.3-0.1-0.5,0c-0.5,0.1-0.9,0.4-1.3,0.8
c-0.2,0.4-0.3,0.9-0.3,1.4c0,0.2,0,0.3,0,0.5c0,0.9,0.5,1.7,1.4,2c0.2,0.1,0.5,0.1,0.7,0.1c0,0.1,0.1,0.2,0.1,0.3c0,0,0,0.1,0,0.1
c-0.1,0.1-0.1,0.2-0.2,0.4c-0.1,0.3-0.1,0.7-0.1,1c-0.1-0.1-0.3-0.1-0.4-0.2c0-0.2-0.1-0.4-0.2-0.6c-0.3-0.4-0.8-0.6-1.3-0.4
l-0.1,0c0-0.1,0-0.1,0-0.2c-0.2-2-0.2-3.9,0.1-5.9c0.2-1.3-1.4-1.7-2.3-1.1c0.1-1.5,0.3-3,0.4-4.4c0.2-1.9,0.3-3.8,0.4-5.7
c0-0.8,0-1.6-0.1-2.4c0-1,0-2-0.1-3c-0.1-1.3-0.4-2.5-0.6-3.8c-0.1-0.5-0.8-0.3-0.9,0.1c-0.2,1.3-0.4,2.6-0.5,4
c-0.1,1.2-0.2,2.4-0.3,3.7c-0.1,2.5-0.4,5.1-0.8,7.6c-0.8,5-2,10-3.5,14.8c-0.1,0.2-0.1,0.4,0,0.6c0.4-0.3,0.9-0.6,1.3-0.9
c0.6-0.5,1.6-0.4,2.1,0.2c0,0,0,0,0,0.1h0.3c0,0,0.1,0,0.1,0c0.2,0,0.4,0.1,0.6,0.1l0.1,0l0.1,0l0.2,0.1l0.1,0
c-0.1-0.1-0.1,0,0,0.1c0,0,0,0,0,0.1c0,0.1,0,0.1,0,0l0,0.1l0,0.1c0-0.1,0-0.1,0,0c0,1.1,0,2.1-0.2,3.2c-0.2,1.2-0.4,2.4-0.6,3.5
c-0.1,0.3,0,0.6,0,0.8c-2,0.9-4,1.8-6,2.6c-12.4,4.8-25.6,7.5-39,8c-2.7,0.4-5.4,0.7-8.1,0.9c-7.5,0.4-15.8,0.1-22.8-3.1h-0.2
c-0.3,0-0.6,0.3-0.6,0.6c0,0.2,0.1,0.4,0.3,0.5c2.7,1.2,5.5,2,8.4,2.4c2.9,0.5,5.8,1,8.7,1.3c5.9,0.6,11.7,0.7,17.6,0.3
c11.8-0.7,23.4-3.1,34.6-7.1c3.2-1.2,6.3-2.4,9.4-3.9c1.5-0.7,3-1.4,4.4-2.2c0.5-0.3,1-0.5,1.5-0.8c1.5,0.4,2.9,0.9,4.2,1.7
c0.4,0.5,0.9,0.9,1.5,1.2c0.5,0.2,1,0.4,1.4,0.7c0.1,0,0.1,0.1,0.2,0.1l0,0c0.1,0.1,0.2,0.2,0.3,0.2l0,0c0,0,0.1,0.1,0.1,0.1
c0,0,0,0.1,0.1,0.2c0.1,0.4,0.4,0.7,0.8,0.7c0,0.2,0.2,0.4,0.4,0.6c0.2,0,0.5,0,0.7,0.1c0.8,0.3,1.5,0.7,2.1,1.3
C213.2,162.2,212.6,161,212,159.8z M199.8,153.9c-0.1,0.1-0.3,0.1-0.4,0.2c-0.6,0.3-1.1,0.6-1.7,0.9c0.1-0.3,0.1-0.6,0.2-0.9
c0.5,0.4,1.3,0.3,1.7-0.2c0.1-0.1,0.2-0.2,0.2-0.4c0.1-0.3,0.1-0.6,0.1-0.9c0-0.2-0.1-0.4-0.1-0.6c0-0.1,0-0.1,0-0.2
c0-0.5-0.1-0.9-0.1-1.4c0-0.1,0-0.2,0-0.4c0,0,0-0.1,0-0.1c0-0.2,0.1-0.5,0.1-0.7c0.1-0.2,0.1-0.5,0.2-0.7c0,0,0.1-0.1,0.1-0.2
c0.1,0.6,0.2,1.1,0.3,1.7c0,0.1-0.1,0.3-0.1,0.4c-0.1,0.3-0.1,0.6,0.1,0.8C200.4,152.1,200.1,153,199.8,153.9L199.8,153.9z"/>
<path id="Path_1323" class="st4" d="M220.2,168.3c-13.1,0.1-25.2,7.6-32.8,18c-3.9,5.6-6.9,11.7-9,18.2c-1.1,3.2-2,6.4-2.7,9.7
c-0.7,3.8-1.2,7.7-1.4,11.5c-0.3,4.3-0.4,8.7-0.3,13c0,2.2,0.1,4.4,0.2,6.6c0.1,1.1,0.1,2.2,0.2,3.3c0,0.5,0.1,1.1,0.2,1.6
c0.1,0.6,0.3,1.1,0.6,1.7c0.1,0.3,0.4,0.4,0.7,0.4c1,0.4,2.2,0.3,3.1-0.2l11.6-4.4c0.4,0.9,1.1,1.9,2.1,1.9c1.3-0.1,1.7-1.7,1.8-3
c0-0.8,0.1-1.5,0.1-2.3c-0.1-4.7-0.6-9.3-1.4-13.9c-0.3-1.9-0.6-3.9-1.1-5.8c0.1-1,0.1-2.1,0.1-3.1c0-1.9,0.3-3.7,0.2-5.6
c0.1-3.6-0.5-7.3-1.6-10.7c-0.1-0.2-0.3-0.3-0.5-0.3c-0.1,0-0.2,0.1-0.3,0.3c-0.7,3.5-0.7,7.1-0.2,10.6c0.1,1.1,0.3,2.1,0.4,3.2
c-0.2-0.6-0.5-1.2-0.7-1.8c-0.3-0.6-0.5-1.3-0.8-1.9c-0.3-0.8-0.9-1.5-1.2-2.3c0-0.1-0.2-0.2-0.3-0.1c-0.1,0-0.2,0.2-0.2,0.3
c0.1,0.8,0.2,1.6,0.2,2.5c0.1,0.7,0.2,1.4,0.3,2.2c0.2,1.5,0.3,3,0.5,4.5c0.3,2.9,0.9,5.8,1.3,8.7s0.6,5.9,0.7,8.8
c0,0.4,0,0.9,0.1,1.3c0,0.1,0,0.1,0,0.2c-0.2,1-0.6,1.9-1.1,2.8c-0.3,0.4-0.5,0.8-0.9,1.2c-3.4,1.5-6.9,2.8-10.4,4.2
c-0.3,0.1-0.5,0.2-0.8,0.4c0-0.2,0-0.5,0-0.7c0-0.9-0.1-1.9-0.1-2.8c-0.1-1.9-0.2-3.9-0.2-5.8c-0.1-3.8-0.1-7.5,0-11.3
c0.1-3.8,0.4-7.6,1-11.3c0.4-3.3,1-6.5,1.8-9.8c3.2-12.3,10.5-23.8,21.4-30.7c5.7-3.7,12.4-5.7,19.2-5.9c0.8-0.1,1.5-0.8,1.4-1.6
C221.6,169,221,168.4,220.2,168.3z"/>
<path id="Path_1324" class="st4" d="M190,262.5c-0.7-0.3-1.5-0.4-2.3-0.1l-2.2,0.5c-1.4,0.3-2.7,0.6-4.1,0.8
c-1.4,0.2-2.8,0.5-4.1,0.7c-0.7,0.1-1.4,0.3-2,0.5c-0.4,0.1-0.8,0.2-1.1,0.4c-0.3,0.2-0.5,0.4-0.8,0.7c-0.3,0.3-0.2,0.7,0,1
c0.1,0.1,0.2,0.1,0.2,0.1c0.3,0.1-0.7,0.7-0.4,0.7s-0.3,0.6,0.1,0.6c0.8,0,3.6-1,4.4-1.1c1.4-0.2,2.7-0.5,4.1-0.8
c1.4-0.3,2.7-0.6,4.1-0.8c0.7-0.1,1.4-0.3,2.1-0.4c0.8,0,1.6-0.4,2.2-0.9c0.5-0.5,0.4-1.3-0.1-1.8
C190.1,262.6,190.1,262.5,190,262.5z"/>
<path id="Path_1325" class="st4" d="M111,162.9c0-0.1-0.1-0.1-0.2-0.1c-1.5,0.5-2,2.6-2.6,3.9c-0.7,1.7-1.5,3.4-2.1,5.1
c-1.2,3.4-2.3,6.9-3.3,10.4c-1.9,7-3.2,14.2-3.9,21.5c-0.4,4.1-0.5,8.2-0.5,12.3c0,1,0.9,1.8,1.9,1.7c1,0,1.7-0.8,1.7-1.7
c-0.2-7.1,0.3-14.2,1.3-21.2c1-7.1,2.5-14.1,4.7-20.9c0.6-1.9,1.2-3.8,1.8-5.7c0.3-1,0.7-2,1.1-2.9
C111.3,164.6,111.3,163.7,111,162.9z"/>
<path id="Path_1326" class="st4" d="M97.9,257.2c-0.7-0.6-1.7-1-2.6-1.2c-0.9-0.2-1.7-0.5-2.5-0.7c-1.8-0.5-3.6-1.1-5.3-1.6
c-1.7-0.6-3.3-1.1-5-1.7c-1.3-0.4-3.5-1.3-4.6-0.2c-0.1,0.1-0.2,0.3-0.1,0.5c0.5,0.5,1,0.8,1.7,0.9c0.7,0.2,1.5,0.5,2.2,0.8
c1.6,0.7,3.3,1.3,5,2c1.7,0.6,3.3,1.2,5,1.8c0.8,0.3,1.7,0.5,2.5,0.8c0.9,0.4,1.9,0.6,2.9,0.5C97.9,258.6,98.5,257.8,97.9,257.2z"
/>
<path id="Path_1327" class="st4" d="M219.5,272.7c-1.6,0.4-3.3,0.7-4.9,0.9c-1.6,0.3-3.2,0.7-4.8,1.1c-3.2,0.8-6.4,1.7-9.6,2.7
c-3.2,0.9-6.5,1.8-9.8,2.6c-3.3,1.1-6.6,1.9-10,2.4c-0.5,0.1-1.1,0.1-1.6,0.2c-1-0.1-2,0-3-0.1c-1.7-0.1-3.4-0.1-5,0.1
c-0.4,0-0.7-0.1-1.1-0.1c0.4-0.2,0.7-0.4,1-0.6c0.2-0.2,0.5-0.3,0.7-0.5l0,0c0.5-0.1,0.9-0.4,1.3-0.7c0.3-0.3,0.5-0.7,0.5-1
c0.1-0.1,0.1-0.3,0.1-0.5v0l0,0c0.2-0.4,0.1-0.9-0.3-1.2c-0.1,0-0.2-0.1-0.3-0.1c0,0-0.1-0.1-0.1-0.1l-0.3-0.1
c-0.3-0.3-0.7-0.4-1.2-0.4c-0.4,0-0.8,0.1-1.2,0.3c-0.7,0.3-1.5,0.6-2.2,0.8c-0.4,0.1-0.8,0.2-1.2,0.4c0,0-0.1-0.1-0.1-0.1
c0,0-0.1,0-0.1-0.1c0,0,0,0-0.1-0.1c-0.3-0.3-0.7-0.4-1.2-0.3c-0.2,0.1-0.4,0.2-0.5,0.3c-0.1,0.1-0.3,0.3-0.3,0.5
c-0.1,0.2-0.1,0.4-0.1,0.7c0,0.1,0,0.1,0,0.2c0,0.3,0.1,0.6,0.3,0.9c0.2,0.3,0.4,0.6,0.8,0.8c0,0,0,0,0,0.1c0,0.1,0,0.2,0.1,0.3
l-0.5-0.2c-0.4-1.1-1.2-1.9-2.1-2.5c-0.2-0.1-0.5-0.3-0.8-0.4c-2.5,0.3-5,0.3-7.5-0.2c-1.4-0.3-2.7-0.7-4.1-0.9
c-0.8-0.1-1.5-0.4-2.1-0.7c-0.6-0.4-1-0.9-1.6-1.3c-0.1-0.1-0.3-0.2-0.4-0.3c0,0-0.1,0-0.1,0c-0.1,0-0.3,0-0.4,0
c0,0-0.1,0-0.1-0.1c-0.3-0.2-0.6-0.4-1-0.4c-0.2,0-0.3,0-0.5,0c0.1,0,0.3,0.1-0.1,0c-0.4-0.1-0.7-0.2-1-0.4
c-0.8-0.5-1.5-1.1-2.1-1.7c-0.8-0.8-1.8-1.4-2.8-1.9c-0.6-0.3-1.3-0.6-1.9-0.7c-0.7-0.1-1.4-0.2-2.1-0.4c-2.8-0.7-5.5-1.6-8.1-2.7
c-2.8-1-5.7-2.1-8.5-3.2c-1.5-0.6-3.1-1.2-4.6-1.7c-0.8-0.3-1.6-0.6-2.4-0.8c-0.4-0.1-0.9-0.2-1.3-0.2c-0.1,0-0.3,0-0.4,0
c-0.7-0.5-1.6-0.3-2.1,0.4c-0.1,0.1-0.2,0.3-0.2,0.4c-0.4,1.9-0.5,3.8-0.3,5.8c0.1,2,0.2,3.9,0.3,5.9c0.1,1.9,0.5,3.8,0.6,5.7
c0,1,0.1,2,0.3,3c0,0.2,0.1,0.3,0.1,0.5c-0.1,0.2-0.3,0.4-0.4,0.6c-1,0.4-1.7,1.4-1.7,2.6c0.1,0.6,0,1.3-0.3,1.9
c0.3,0.9,1.2,1.4,2.1,1.2l41.1-8.1c0.3-0.1,0.5-0.2,0.7-0.4c1.1,0.6,2.4,1,3.6,1.3c1.4,0.2,2.8,0.3,4.2,0.3c1.3-0.1,2.6,0,3.8,0.3
c0.4,0.2,0.8,0.4,1.1,0.7c0.3,0.7,0.8,1.4,1.4,1.8c1.7,1.1,3.6,1.7,5.6,1.8c2,0.1,4.1,0.1,6.1-0.1c1.1-0.1,2.1-0.4,3.2-0.5
c0.4,0,0.7-0.1,1.1-0.1c2.2-0.1,4.3-0.3,6.4-0.6c8.4-1,16.7-2.7,24.9-4.9c2.3-0.6,4.6-1.2,6.9-1.8c1-0.2,1.9-0.5,2.8-1
c1-0.7,1.6-1.8,1.6-3c0-0.9-0.8-1.7-1.7-1.7C219.8,272.6,219.7,272.7,219.5,272.7z"/>
<path id="Path_1328" class="st4" d="M183,121.5c-0.8-0.7-1.7-1.2-2.7-1.4c-2-0.5-4.1,0.5-5,2.4c-0.6,1.3-0.6,2.8,0,4.1
c0.7,1.2,1.8,2.2,3.2,2.5c1.4,0.5,2.9,0.4,4.3-0.2c1.4-0.7,2.2-2.1,2.1-3.7C184.8,123.8,184.1,122.4,183,121.5z"/>
<path id="Path_1329" class="st4" d="M210.9,160.3c-0.5-0.8-1.2-1.5-2-2.1c-0.7-0.4-1.4-0.8-2.2-1c-1.5-0.2-2.9-0.8-4.1-1.8
c-0.7-0.7-1.7,0.4-1.1,1.1c0.5,0.5,1.1,0.9,1.7,1.1c0,0.1,0,0.1,0,0.2c0.3,1.2,1.6,1.6,2.6,2l4.1,1.5c0.4,0.1,0.8-0.1,0.9-0.6
C211,160.6,211,160.5,210.9,160.3z"/>
<g id="Group_12">
<path id="Path_1357" class="st5" d="M186.6,124.7c0-3.7-3-6.8-6.8-6.8s-6.8,3-6.8,6.8c0,3.7,3,6.8,6.8,6.8
C183.6,131.4,186.6,128.4,186.6,124.7z M175.8,124.7c0-2.2,1.8-4,4-4c2.2,0,4,1.8,4,4s-1.8,4-4,4c0,0,0,0,0,0
C177.6,128.7,175.8,126.9,175.8,124.7z"/>
<path id="Path_1358" class="st5" d="M203.2,229c0.1,0.5,0.7,0.8,1.2,0.7c0,0,0.1,0,0.1,0c0.7,0,1.5,0,2.2-0.1
c0.9-0.1,1.8-0.1,2.7-0.2c1.9-0.2,3.8-0.3,5.7-0.5c1.8-0.2,3.7-0.4,5.5-0.6c0.9-0.1,1.7-0.2,2.6-0.3c0.9-0.1,1.8-0.1,2.3-0.9
c0.1-0.1,0.1-0.3,0.1-0.5c0.2-0.9,0.2-1.8,0.1-2.7c0-0.9-0.1-1.9-0.1-2.8c-0.1-2-0.4-4-0.6-6c-0.4-3.7-1.1-7.4-2.1-11
c0.1-0.2,0.1-0.5,0-0.7c-0.3-0.7-1.3-0.5-2-0.4c-0.9,0.1-1.9,0.1-2.8,0.1c-2.1,0-4.1,0-6.2,0c-2.1,0-4.2,0.1-6.4,0.3
c-1,0.1-2.1,0.1-3.2,0.2c-0.9,0.1-2.1-0.1-2.6,0.8c-0.1,0.2-0.1,0.5,0,0.8c0.1,0.1,0.1,0.1,0.2,0.2c0,0.9,0,1.8,0.1,2.7
c0.1,1,0.3,2,0.5,3c0.3,2,0.7,4.1,1,6.1c0.3,2,0.6,4,0.8,6c0.1,1,0.2,2,0.4,3C202.8,226.9,203,227.9,203.2,229z M209.3,227.3
c-0.9,0.1-1.8,0.1-2.7,0.2c-0.4,0-0.9,0-1.3,0.1c-0.1-0.6-0.2-1.3-0.2-1.9c-0.1-1-0.2-2-0.4-3c-0.3-2-0.6-4-0.9-6.1
c-0.2-1.2-0.4-2.4-0.7-3.6c0.3-0.1,0.7-0.2,1-0.2c1.1-0.2,2.2-0.4,3.3-0.6c0.5-0.1,1-0.2,1.5-0.2c0,0.2,0,0.5,0.1,0.7
c0,0.7,0.1,1.3,0.1,2c0.1,1.5,0.2,2.9,0.3,4.4c0.2,2.7,0.5,5.4,0.8,8.1C209.9,227.2,209.6,227.3,209.3,227.3L209.3,227.3z
M214.8,226.8c-1,0.1-1.9,0.2-2.9,0.3c-0.3-2.7-0.6-5.4-0.8-8.1c-0.1-1.5-0.3-2.9-0.5-4.4c-0.1-0.7-0.1-1.5-0.2-2.2
c0-0.2,0-0.4,0-0.6c1.3-0.1,2.5-0.2,3.8-0.2c0.8,0,1.6,0,2.4,0c0,0.3,0.1,0.5,0.1,0.8c0,0.7,0.1,1.3,0.1,2
c0.1,1.5,0.2,2.9,0.3,4.4c0.2,2.6,0.5,5.1,0.7,7.7C216.8,226.6,215.8,226.7,214.8,226.8L214.8,226.8z M224,223.9
c0.1,0.6,0.2,1.2,0.3,1.9c-0.4,0-0.8,0.1-1.1,0.1c-0.9,0.1-1.8,0.2-2.7,0.3c-0.3,0-0.7,0.1-1,0.1c-0.3-2.6-0.5-5.1-0.8-7.7
c-0.1-1.5-0.3-2.9-0.5-4.4c-0.1-0.7-0.1-1.5-0.2-2.2c0-0.2,0-0.3,0-0.5c0.6,0,1.3-0.1,1.9-0.2c0.9-0.2,1.8-0.4,2.6-0.8
c0.1,1.6,0.3,3.1,0.5,4.7c0.2,1.9,0.3,3.9,0.6,5.8C223.8,222,223.9,223,224,223.9L224,223.9z M205.8,205.2
c2.3-0.1,4.6-0.2,6.9-0.3c2.1,0,4.2,0.1,6.3-0.1c1.1,0,2.2-0.2,3.3-0.5c0,1.5,0,2.9,0.1,4.4c-0.4,0.1-0.7,0.2-1,0.3
c-0.9,0.2-1.8,0.4-2.7,0.4c-1.9,0.1-3.9-0.1-5.8,0c-2.1,0.1-4.1,0.3-6.2,0.7c-1,0.2-2.1,0.4-3.1,0.6c-0.3,0.1-0.6,0.1-0.8,0.1
c0-0.1,0-0.1,0-0.2c-0.2-1-0.4-2-0.6-2.9c-0.2-0.8-0.5-1.6-0.8-2.3c0.4,0,0.8-0.1,1.2-0.1C203.6,205.4,204.7,205.3,205.8,205.2z"
/>
<path id="Path_1359" class="st5" d="M113.6,202.6c4.9,1.4,9.9,2.2,14.9,2.3c0.6,0,1-0.5,1-1l0.3-6.9c0.3-2.2,0.1-4.5-0.4-6.6
c-0.1-0.3-0.5-0.4-0.6-0.1c0,0,0,0,0,0c-0.1-0.1-0.2-0.2-0.4-0.3c-1.2-0.3-2.4-0.4-3.6-0.4c-1.3-0.1-2.6-0.2-3.8-0.4
c-2.5-0.3-5-0.7-7.5-1.2c-0.5-0.1-1.1,0.2-1.3,0.7c0,0.1,0,0.2,0,0.3c0.2,4.2,0.5,8.3,0.7,12.5
C112.8,202.1,113.1,202.5,113.6,202.6z M124.5,191.6c1.3,0.2,2.6,0.2,3.8,0c-0.4,1.7-0.6,3.5-0.6,5.2c-0.1,2-0.2,3.9-0.3,5.9
c-4.3-0.2-8.5-0.9-12.7-2c-0.2-3.5-0.4-7-0.6-10.5c2.1,0.4,4.2,0.7,6.4,0.9C121.9,191.4,123.2,191.5,124.5,191.6L124.5,191.6z"/>
<path id="Path_1360" class="st5" d="M297.3,140.9L297.3,140.9c-4.6-81.4-74.4-143.6-155.8-139C67.2,6.1,7.6,65,2.6,139.3H2.5v1.7
c-0.2,2.9-0.2,5.7-0.2,8.3c0,2.8,0.1,6,0.1,6l0.1,142.4h295V146.5L297.3,140.9z M107,267.3c0-1.7-0.1-3.5-0.1-5.2
c4.3,1.5,8.5,2.9,12.8,4.4c2.7,0.9,5.4,1.9,8.1,2.8c1.3,0.4,2.6,0.9,3.9,1.3c0.6,0.2,1.3,0.4,1.9,0.5c0.2,0.4,0.6,0.6,1,0.6
l2.5-0.3c0,0.2,0.1,0.4,0.3,0.5c0.7,0.6,1.4,1.1,2.2,1.5c0.8,0.5,1.5,0.9,2.3,1.4c1.4,0.8,2.7,1.7,4.1,2.5
c2.2,1.6,4.7,2.7,7.4,3.3c1.6,0.2,3.2,0.1,4.8-0.1c1.2-0.1,2.4-0.2,3.6-0.4c0,0,0,0.1,0,0.1c1.7,3.8,6.1,5,10,5.1
c2.5,0.1,5-0.1,7.4-0.5c-0.1,3.1-0.3,6.3-0.4,9.4h-71.2C107.3,285.3,107.2,276.3,107,267.3z M78,253.5c0.1-0.4,0.2-0.8,0.2-1.2
l17.8,6.1c-0.1,0.2-0.2,0.5-0.3,0.8s-0.2,1.2-0.5,1.3c-0.3,0.1-1.3-0.4-1.6-0.4c-0.6-0.2-1.2-0.4-1.8-0.5c-2.3-0.7-4.6-1.3-6.9-2
l-3.4-1c-1.2-0.3-2.3-0.7-3.5-1.1C77.4,255,77.9,254.2,78,253.5z M79,250.3L63.3,245c-5.8-11.7-11.6-23.3-17.4-35
c-0.8-1.6-1.6-3.3-2.4-4.9c-0.3-0.6-0.7-1.5-1.1-2.2c0.3,0.1,0.6,0.2,0.8,0.2l3.6,1c2.3,0.7,4.6,1.3,6.9,1.9l13.9,3.8l27.4,7.5
c2,0.5,4,1,5.9,1.6c0.2,0.2,0.5,0.3,0.8,0.2l0.8,0.2c2.4,0.7,4.8,1.2,7.2,1.6c0.2,1.5,0.7,2.9,1.6,4.2c0.8,1.6,1.7,3.2,2.5,4.9
l5.1,9.9c3.4,6.6,6.8,13.1,10.3,19.7l5.2,10c-0.1,0-0.1,0-0.2,0.1c-1.6-0.7-3.2-1.3-4.9-1.8c-2.4-0.8-4.7-1.6-7.1-2.5l-14.3-4.9
C98.2,257,88.6,253.7,79,250.3L79,250.3z M110.5,160l-0.1-1.7c0-1.1-0.1-2.3-0.1-3.4c0-0.7,0.2-1.4,0.9-1.5c0.2,0,0.4,0,0.6,0
c0.1,0.2,0.3,0.3,0.5,0.4c3.8,3.9,9.4,5.7,14.6,6.6c5.8,0.8,11.7,1,17.6,0.7c12.2-0.3,24.4-2.4,36-6.4c3.1-1.1,6.2-2.4,9.2-3.8
c1.5-0.7,3-1.5,4.4-2.3c1.4-0.7,2.7-1.6,3.8-2.6l0.1,0c-0.2,0,0.3,0.1,0.1,0c0.1,0,0.2,0.1,0.2,0.1c0,0,0,0,0,0l0.1,0.1
c0,0.1,0.1,0.1,0.1,0.2c0.1,0.1,0.1,0.3,0.1,0.4c0.1,0.3,0.1,0.7,0.1,1c0,1.9,0,3.7,0.1,5.6c-1,0.4-2,0.9-2.9,1.5
c-1.3,0.7-2.7,1.4-4.1,2.1c-2.7,1.3-5.5,2.5-8.4,3.5c-5.9,2.1-11.9,3.7-18,4.8c-6.2,1.1-12.5,1.8-18.8,2.1
c-6.1,0.4-12.3,0.3-18.4-0.1c-6.3-0.3-12.3-2.4-17.3-6.3c-0.1-0.1-0.2-0.2-0.4-0.2C110.5,160.5,110.5,160.2,110.5,160z
M237.3,167.7c0,10.5,0.1,21,0.1,31.5c0,10.8,0.1,21.6,0.1,32.4c0,10.5,0.1,21.1,0.1,31.6c0,9.6,0,19.3,0.1,28.9
c0,0.7,0.1,1.4,0.1,2.2h-20.2c0,0,0-0.1,0-0.1c-0.4-6.5-0.4-13-0.7-19.4c0.8-0.3,1.6-0.5,2.4-0.8c5.2-1.9,10.4-4.7,13.1-9.6
c1.4-2.7,2.1-5.6,2.1-8.6c0-4.4-0.3-8.8-0.5-13.1c-0.4-9-0.7-18.1-1.1-27.1c-0.2-4.4-0.3-8.8-0.6-13.2c0-0.4,0-0.8-0.1-1.2
c0.1-0.2,0.2-0.4,0.2-0.6v-43.6c-0.1-0.6-0.5-1.1-1-1.3h-9.3c-0.6,0.2-1,0.7-1,1.3v11.7c-0.4-0.4-0.8-0.7-1.2-1.1
c0,0-0.1-0.1-0.1-0.1c-0.9-0.7-1.8-1.4-2.7-2.1c0-0.6,0-1.2,0-1.8c0-1.1,0-2.3,0-3.4c0-2.2,0-4.4,0-6.6c0-2.2,0-4.4,0-6.6
c0-1.1,0-2.3,0-3.4c0-0.9-0.1-1.9-0.3-2.8h0.5h10.4c3.3,0,6.6,0.1,9.8-0.2C237.1,149.6,237.3,158.7,237.3,167.7L237.3,167.7z
M110.6,220c-0.4-0.2-0.9-0.3-1.3-0.5c0.1-0.2,0.2-0.4,0.2-0.6c0.1-3.7,0.3-7.4,0.6-11c0.3-3.6,0.7-7.3,1-10.9
c0.5-7.4,0.7-14.8,1.1-22.1c0.1-2,0.2-4.1,0.3-6.1c0.1-1.1,0.1-2.1,0.1-3.2c0-0.1,0-0.3,0-0.4c3.8,2.2,8,3.5,12.3,4
c6.2,0.8,12.4,1,18.7,0.7c13-0.3,25.9-2.4,38.3-6.4c3.4-1.1,6.6-2.4,9.9-3.9c1.6-0.7,3.1-1.5,4.6-2.3c0.7-0.4,1.4-0.8,2.1-1.2
c0.6,0.6,1.3,1.1,2.1,1.5l0.3,0.1c-0.8,0.2-1.6,0.6-2.3,0.9c-1.6,0.6-3.1,1.3-4.6,2.1c-2.9,1.5-5.6,3.2-8.2,5.2
c-5,3.9-9.4,8.5-12.9,13.8c-8.3,12.4-11.6,27.3-12.7,41.9c-0.7,9.1-0.6,18.2-0.9,27.3l-22.9-5.1c-2.6-0.6-5.1-1.1-7.7-1.7
c-1.2-0.3-2.5-0.5-3.7-0.8c-1-0.3-2.1-0.5-3.2-0.6l-5.5-10.5c-0.9-1.8-1.9-3.7-2.9-5.5c-0.5-0.9-1-1.8-1.5-2.7
c-0.3-0.6-0.7-1.1-1.2-1.6C110.9,220.4,110.9,220.2,110.6,220C110.6,220,110.6,220,110.6,220z M108.5,195.5
c-0.3,3.7-0.7,7.4-1,11.1c-0.4,4-0.6,8-0.7,12.1c-1.4-0.5-2.8-0.9-4.2-1.3c0.3-14.4,1.3-29,5.4-42.9c0.5-1.7,1.1-3.4,1.7-5
c-0.1,1.4-0.2,2.7-0.3,4.1c-0.2,3.7-0.3,7.3-0.4,11C109,188.2,108.8,191.8,108.5,195.5z M184.9,176.4l0.5-0.4c1-0.8,2-1.6,3-2.4
c2-1.6,4.1-3.1,6.3-4.5c1.1-0.7,2-1.6,3.1-2.2c1.1-0.7,2.2-1.3,3.4-1.9c1.2-0.6,2.4-1.1,3.6-1.6c1.1-0.4,2.2-0.9,3.3-1.4
c0.3,0.1,0.5,0.3,0.8,0.4l0,0c-0.5,0.2-1,0.5-1.4,0.7c-1.2,0.6-2.5,1.2-3.7,1.8c-2.3,1.2-4.6,2.6-6.8,4.1c-2.2,1.5-4.3,3-6.4,4.7
c-1.1,0.9-2.1,1.8-3.1,2.7c-0.4,0.4-0.7,0.8-1.1,1.2C186.1,177.1,185.6,176.7,184.9,176.4z M183.3,178.8c0.7,0,1.3,0.6,1.3,1.3
c0,0.7-0.6,1.3-1.3,1.3c-0.7,0-1.3-0.6-1.3-1.3C182,179.4,182.6,178.8,183.3,178.8z M203.9,161.8c-1.1,0.6-2.3,1.2-3.4,1.8
c-1.1,0.6-2.2,1.2-3.4,1.9c-1.1,0.7-2.4,1.2-3.5,1.9c-2.2,1.4-4.3,2.9-6.3,4.6c-1,0.8-2,1.6-2.9,2.5c-0.5,0.5-1,1-1.5,1.6
c-2.2,0.3-3.8,2.3-3.6,4.5c0.3,2.2,2.3,3.8,4.5,3.6c2-0.2,3.6-2,3.6-4c0-0.4-0.1-0.8-0.2-1.2c0.6-0.4,1.1-0.8,1.7-1.2
c1-0.8,1.9-1.7,2.9-2.5c2-1.6,4.2-3.2,6.4-4.6s4.4-2.8,6.7-4c1.1-0.6,2.3-1.2,3.4-1.7c0.6-0.3,1.2-0.6,1.9-0.9
c0.3-0.1,0.5-0.2,0.8-0.4c1.3,0.8,2.6,1.7,3.9,2.5c0,0.2,0.1,0.3,0.2,0.5c-2.3,0.2-4.5,0.7-6.6,1.5c-3.4,1.2-6.6,2.9-9.5,5
c-5.5,3.8-10.2,8.6-13.9,14.1c-3.7,5.5-6.6,11.5-8.5,17.9c-2.1,7.4-3.3,14.9-3.6,22.6c-0.2,4.3-0.4,8.6-0.2,12.8
c0.1,2,0.1,4,0.2,5.9c0,1.9,0.1,3.8,0.3,5.7c-3.3-0.7-6.6-1.5-9.9-2.2l-1.9-0.4c0-0.1,0.1-0.2,0.1-0.3c0.1-15.4-0.2-30.9,3.5-46
c3.2-13.1,9.6-25.2,20-34c2.7-2.2,5.5-4.2,8.6-5.9c1.6-0.9,3.2-1.7,4.9-2.4c1.5-0.7,3.1-1.1,4.5-1.9l0.3,0.2
c0.9,0.5,1.7,1,2.6,1.5C205.2,161.1,204.5,161.5,203.9,161.8L203.9,161.8z M123.8,243.2c1.2,0.3,2.4,0.6,3.7,0.9
c2.2,0.5,4.4,1,6.7,1.5l13.7,3l27,6c2.2,0.5,4.5,1,6.7,1.4l-0.9,0.1c-3.1,0.3-6.2,0.6-9.3,0.9c-0.6-1.4-1.8-2.5-3.1-3.1
c-0.7-0.3-1.5-0.4-2.2-0.5c-1-0.2-2-0.3-3-0.4c-0.9,0-1.8-0.1-2.6-0.3c-0.9-0.2-1.7-0.3-2.6-0.2c-1.7,0.4-1.7,2.3-1.3,3.7
c0.2,0.7,0.6,1.4,1.2,1.9c0.5,0.4,1,0.8,1.5,1.2c-8.8,0.9-17.6,1.8-26.4,2.6c-3.3-6.3-6.6-12.6-9.8-18.8
C123.2,243.1,123.5,243.2,123.8,243.2z M189.2,257.3c0,0.2-0.1,0.3-0.1,0.5c-0.1,0.6-0.1,1.3-0.1,1.9c0,0.6,0,1.2,0,1.8
c0,0.4,0,0.7,0,1.1c0,0,0,0-0.1,0c-0.3,0-0.6,0-1,0c-0.7,0.1-1.4,0.2-2.1,0.2l-4,0.5l-8.2,1c-0.1,0-0.1,0-0.2,0
c-0.1-0.9-0.2-1.7-0.3-2.6c-0.2-0.9-0.4-1.9-0.8-2.8c2.8-0.3,5.7-0.6,8.5-0.9l4.7-0.5c0.8-0.1,1.5-0.1,2.3-0.2
c0.4,0,0.8-0.1,1.1-0.2c0,0,0,0,0,0L189.2,257.3z M165.2,279.8C165.3,279.9,165.3,279.8,165.2,279.8z M168.5,281.7
c-0.2-0.1-0.4-0.1-0.6-0.2c-0.1,0-0.4-0.2-0.5-0.2c-0.3-0.1-0.6-0.3-0.9-0.5c-0.1-0.1-0.3-0.2-0.4-0.3c0,0-0.3-0.2-0.3-0.3
c-0.1-0.1-0.3-0.3-0.4-0.4c0,0-0.1-0.1-0.1-0.1c1.1-0.2,2.2-0.3,3.3-0.5c1.4-0.2,2.8-0.5,4.2-1c0,0.7-0.1,1.4-0.5,2
c-0.5,0.9-1.5,1.5-2.5,1.6C169.3,281.9,168.9,281.8,168.5,281.7z M160.3,261.4c0,0.5,0,1.1-0.1,1.6c-0.1,1-0.3,2.1-0.6,3.2
c0,0-0.1,0-0.1,0c-7.7,1-15.3,2-23,3c-0.9-1.7-1.8-3.4-2.6-5.1C142.7,263.2,151.5,262.3,160.3,261.4z M155.2,268.9
c1.4,0.4,2.5,1.5,4.1,1.3c1.6-0.2,2.3-1.5,2.6-2.9c0.9-2.4,1-5,0.4-7.5c-0.6-1.3-1.5-2.3-2.7-3c-0.6-0.4-1-0.9-1.2-1.6
c0-0.2-0.1-0.7-0.1-0.7c0,0,0.9,0.1,1.1,0.2c1.8,0.2,3.6,0.7,5.4,1c1.5,0,2.9,0.6,3.8,1.7c0.9,1.4,1.6,3,2.1,4.6
c0.3,0.9,0.5,1.9,0.7,2.8c0.1,0.5,0.3,1,0.6,1.5c-0.1,0.4-0.1,0.8-0.1,1.2c0,0.6,0.1,1.3,0.2,1.9c0.2,1.3,0.3,2.6,0.4,3.9
c0.1,1,0.1,2.1,0.2,3.1c-1.4,0-2.8,0.1-4.2,0.3c-1.7,0.2-3.3,0.4-5,0.6s-3.3,0.4-5,0.5c-1.6,0.2-3.1,0.3-4.7,0.3
c-2.5-0.5-4.8-1.6-6.8-3.2c-1.4-0.9-2.7-1.8-4.1-2.7c-0.7-0.5-1.4-1-2.2-1.5L155.2,268.9z M174.8,275c-0.1-1.6-0.3-3.2-0.5-4.8
c-0.1-0.8-0.2-1.5-0.4-2.2c-0.1-0.5-0.3-0.9-0.5-1.3c0.1,0,0.1,0,0.2,0l8.3-1.1l4.2-0.6c0.6-0.1,1.3-0.2,1.9-0.3
c0.3,0,0.6-0.1,0.9-0.2c0.4-0.2,0.8-0.4,1.2-0.6h0c0.2-0.1,0.4-0.2,0.5-0.3c0.3-0.6,0.4-1.4,0.4-2.1c0-0.6,0-1.2,0-1.8
c0-0.6,0-1.2-0.1-1.8c0-0.3-0.1-0.7-0.2-1c-0.1-0.2-0.2-0.4-0.3-0.6c0-0.3-0.2-0.6-0.5-0.6c-0.5-0.1-0.9-0.3-1.3-0.3
c-0.3,0-0.7,0-1,0c-0.2,0-0.3,0-0.5,0c-2.4-0.7-4.9-1.3-7.3-1.8l2.9-0.9c2.3-0.7,4.7-1.2,7-1.9c1.9-0.9,4.1-1,6.2-0.5
c1,0.4,2,0.8,3,1.3c0.5,0.2,1,0.4,1.4,0.6c0.6,0.2,1.2,0.3,1.9,0.4c0.2,0.1,0.4-0.1,0.5-0.3c0-0.1,0-0.3-0.1-0.4
c-0.5-0.4-0.8-0.9-1.3-1.3c-0.5-0.4-1-0.7-1.5-1c-0.9-0.7-1.9-1.2-3-1.7c-0.5-0.2-1-0.4-1.6-0.5c0.1-1.2,0.1-2.4-0.1-3.6
c-0.1-1.6-0.2-3.2-0.3-4.7c-0.2-3.1-0.4-6.2-0.6-9.3c-0.5-6.3-1.1-12.7-1.8-19c-0.2-1.7-0.4-3.4-0.7-5.1
c-0.3-1.8-0.8-3.5-1.2-5.2c0-0.1-0.2-0.2-0.3-0.1c-0.1,0-0.2,0.1-0.2,0.2c-0.1,3.1-0.1,6.2,0.1,9.3c0.2,2.7,0.4,5.4,0.6,8.1
c-0.1-0.3-0.2-0.6-0.3-0.9c-0.4-1.3-0.9-2.6-1.3-4c-0.2-0.7-0.4-1.4-0.7-2.1c-0.3-0.7-0.6-1.4-0.9-2.1c-0.1-0.2-0.3-0.3-0.5-0.2
c-0.1,0.1-0.2,0.2-0.3,0.3c0,0.7-0.2,1.5-0.2,2.2c0,0.7,0.1,1.5,0.3,2.2c0.3,1.4,0.7,2.7,1.1,4c0.9,2.7,1.6,5.5,2.2,8.3
c0.5,2.8,1,5.7,1.3,8.6c0.2,1.4,0.3,2.8,0.4,4.2c0.1,1,0.2,2,0.3,2.9c0,0.6,0.1,1.2,0.1,1.9c0,0.9,0.1,1.8,0.1,2.7
c0,0.5,0,1,0.1,1.4c-0.9,0.1-1.8,0.3-2.7,0.5c-2.5,0.7-4.8,1.7-7.3,2.5l-3.5,1.1c-1.2,0.3-2.4,0.7-3.5,1.2l-0.4-0.1
c0.1-0.1,0.2-0.3,0.2-0.5c0.3-3.3-0.1-6.7-0.1-10.1c0.1-3.6,0-7.3,0.2-10.9c0.2-7.4,1.1-14.8,2.8-22c2.8-12.6,9.8-23.8,19.7-32
c2.8-2.2,6-4.1,9.3-5.6c1.9-0.9,4-1.5,6.1-2c1.7-0.3,3.5-0.5,5.2-0.7c0.1,0,0.1,0.1,0.2,0.1c5.2,4.7,8.6,11,9.8,17.8
c0.6,3.5,1.1,6.9,1.2,10.5c0.2,3.7,0.3,7.5,0.5,11.3c0.3,8.8,0.6,17.5,1,26.3c0.2,4.4,0.3,8.9,0.5,13.3l0,0.3
c-2.1-0.3-4.3,0-6.1,1c-2.2,1-4.2,2.5-5.8,4.4c-3.3,3.7-5.8,8-7.3,12.7c-0.8,2.3-1.3,4.6-1.6,7c-1.2,0.3-2.4,0.6-3.5,1
c-5.2,1.5-10.3,3.1-15.5,4.5c-5.1,1.5-10.4,2.6-15.7,3.1c-0.8,0.1-1.6,0.1-2.3,0.1C175.3,280.3,175,277.4,174.8,275L174.8,275z
M231.8,254.7c0.1,1.4,0,2.9-0.3,4.3c-1.1,5.2-4.8,8.6-9.4,10.8c-2.8,1.3-5.7,2.3-8.7,3c0.6-3.9,1.9-7.6,3.8-11
c1.9-4.1,5.1-7.6,9-9.9c1.1-0.5,2.3-0.9,3.5-1.1c0.5-0.1,1.3-0.1,2-0.2C231.7,251.9,231.8,253.3,231.8,254.7L231.8,254.7z
M230.5,186.1c-1-5.2-3.2-10.1-6.4-14.4c-0.3-0.3-0.6-0.7-0.9-1v-13.1h7.3L230.5,186.1z M187.5,283.2c5.6-1.4,11.1-3.1,16.6-4.8
c2.3-0.7,4.7-1.4,7-2c1.3-0.3,2.6-0.7,3.9-1c0.3,6.3,0.4,12.6,0.7,18.8c0,0,0,0.1,0,0.1h-34.9c0.1-3.3,0.3-6.5,0.4-9.8
C183.3,284.2,185.4,283.7,187.5,283.2L187.5,283.2z M214.6,153.5c0,2.2,0,4.4,0,6.6c0,1.1,0,2.3,0,3.4c0,0,0,0.1,0,0.1
c-3.3-2.1-6.7-4.1-10.2-6c-1-0.5-1.9-1-2.9-1.5c-0.6-0.3-1.3-0.6-2-0.7c0.2-0.2,0.4-0.4,0.6-0.6c0.5-0.1,0.9-0.6,0.9-1.1
c0-1.2-0.1-2.4-0.1-3.7c0-1.2,0-2.4-0.2-3.6c-0.1-1.1-0.9-2.1-2-2.4c-0.2,0-0.3-0.1-0.5-0.1c-0.1-1.1-0.1-2.3-0.2-3.4
c1.3,0.1,2.5,0.2,3.8,0.2l5.1,0c2.6,0,5.2,0,7.8,0c-0.2,0.9-0.3,1.8-0.3,2.8c0,1.1,0,2.3,0,3.4
C214.6,149.1,214.6,151.3,214.6,153.5L214.6,153.5z M113.5,142.3c0-0.3,0-0.7,0-1c3.2,3.5,10.3,4.5,14,4.9
c11.3,1.4,22.7,0.7,33.8-1.9c3.2-0.8,13.7-3.7,14.1-8c0.2-3-2.6-4.8-4.3-7.1c-2-2.8-1.5-6-0.6-8.8c0.8-2.6,1.8-6-1.4-7.9
c-1.2-0.6-2.5-1-3.9-0.9c-1.6-0.1-3.2-0.1-4.8-0.1c-3.4-0.1-6.8-0.3-10.2-0.6c-6.7-0.6-13.4-1.5-20.1-2.7
c-0.8-0.1-8.1-1.4-11.6-1.8c1.6-3.9,4-7.4,6.9-10.5c3.4-3.5,7.5-6.2,12-8c9.4-3.8,19.7-4.3,29.4-1.3
c10.8,3.1,19.7,10.8,24.3,21.1c4.4,10.5,4,22.1,4.5,33.2c0.1,1.3,0.1,2.6,0.2,3.9c0,0.1,0,0.2,0.1,0.2c-0.7,0.4-1.4,0.8-2.1,1.2
c-1.2,0.7-2.5,1.4-3.8,2c-2.6,1.3-5.3,2.5-8,3.6c-5.4,2.1-11.1,3.7-16.8,4.8c-5.9,1.1-11.8,1.8-17.7,2.1
c-5.7,0.4-11.5,0.3-17.2-0.1c-5.9-0.3-11.6-2.5-16.2-6.3c-0.1-0.1-0.3-0.2-0.5-0.3C113.7,148.7,113.7,145.5,113.5,142.3
L113.5,142.3z M109,162.4c0,0.1,0.1,0.2,0.2,0.2c0.2,0.2,0.5,0.4,0.8,0.7c-6,12.9-8,27.5-8.9,41.5c-0.3,4-0.4,8-0.5,12
c-1.2-0.3-2.3-0.6-3.5-1c-4.2-1.2-8.5-2.3-12.7-3.5c4.7-14.1,11-27.6,18.8-40.3c1.1-1.8,2.2-3.5,3.3-5.2
C107.4,165.4,108.3,163.9,109,162.4L109,162.4z M294.1,294.3h-54c0.3-4.2,0.1-8.4,0.1-12.6c0-4.9,0-9.8,0-14.6
c0-10.5,0-21-0.1-31.5c0-10.8-0.1-21.6-0.1-32.4c0-10.5-0.1-21.1-0.1-31.6c0-9.6,0.2-19.3-0.3-28.9c0-1.1-0.1-2.2-0.2-3.2
c0-0.4-0.3-0.7-0.6-0.8c-0.2-0.1-0.3-0.2-0.5-0.2c-3.5-0.3-7-0.2-10.5-0.2h-10.4c-3.5,0-6.9,0-10.4,0l-5.1,0
c-1.3,0-2.6,0-3.9,0.2c-0.3-9.5-0.3-19.2-3.3-28.3c-1.9-5.9-5.1-11.3-9.5-15.8c-4.1-4-9-7.2-14.3-9.3c-10-3.9-21-4.3-31.2-1
c-10.1,3.1-18.5,10.2-23.1,19.6c-3.2,7.1-5.1,14.7-5.7,22.5c-0.5,7.3-0.4,14.6,0.2,21.9c0.1,1.3,0.2,2.5,0.3,3.7
c-0.5-0.2-1.1-0.2-1.6,0.1c-1,0.6-1.6,1.6-1.6,2.8c0,0.6,0,1.1,0.1,1.7c0,0.6,0,1.2,0.1,1.8l0.1,1.9c0,0.4,0,0.8,0,1.2
c-1.3,1.2-2.4,2.6-3.3,4c-1.1,1.6-2.1,3.2-3.1,4.8c-2,3.2-3.9,6.5-5.8,9.8c-3.6,6.6-6.8,13.3-9.6,20.3c-1.6,3.9-3.1,7.8-4.5,11.7
l-26.4-7.2c-2.5-0.7-5.1-1.4-7.6-2.1c-1.3-0.4-2.6-0.7-4-1c-1.3-0.4-2.6-0.7-3.9-0.8c-0.4,0-0.7,0.4-0.7,0.8
c0,0.3,0.1,0.5,0.4,0.6c0,0,0,0,0,0c0.2,1.2,0.6,2.3,1.2,3.3c0.7,1.5,1.4,3,2.2,4.4l4.4,8.8c2.9,5.9,5.8,11.8,8.8,17.7l5,10.1
c0.1,0.3,0.4,0.5,0.7,0.5l13.5,4.6c-0.2,1.9-1.3,4.4,1,5.5c1.3,0.5,2.6,0.9,3.9,1.2l4.4,1.2c2.8,0.8,5.7,1.7,8.5,2.5
c1.1,0.3,2.2,0.6,3-0.4c0.3-0.5,0.5-1,0.7-1.5c0.1-0.4,0.2-0.8,0.3-1.1l7.2,2.5c0.3,9.1,0.4,18.3,0.6,27.4c0,1.8,0.1,3.7,0.1,5.5
H5.9L5.8,155.3c0,0-0.1-3.2-0.1-5.9c0-2.5,0.1-5.1,0.2-7.8l0-0.4C10.4,61.5,78.6,0.8,158.1,5.3c73.2,4.2,131.6,62.6,135.7,135.7
l0.2,5.5L294.1,294.3z"/>
</g>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 27 KiB

BIN
packages/builder/assets/oracle.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

75
packages/builder/cypress/integration/addMultiOptionDatatype.spec.js

@ -1,41 +1,44 @@
context("Add Multi-Option Datatype", () => {
before(() => {
cy.login()
cy.createTestApp()
})
before(() => {
cy.login()
cy.createTestApp()
})
it("should create a new table, with data", () => {
cy.createTable("Multi Data")
cy.addColumn("Multi Data", "Test Data", "Multi-select", "1\n2\n3\n4\n5")
cy.addRowMultiValue(["1", "2", "3", "4", "5"])
})
it("should create a new table, with data", () => {
cy.createTable("Multi Data")
cy.addColumn("Multi Data", "Test Data", "Multi-select", "1\n2\n3\n4\n5")
cy.addRowMultiValue(["1", "2", "3", "4", "5"])
})
it ("should add form with multi select picker, containing 5 options", () => {
cy.navigateToFrontend()
cy.wait(500)
// Add data provider
cy.get(`[data-cy="category-Data Provider"]`).click()
cy.get('[data-cy="dataSource-prop-control"]').click()
cy.get(".dropdown").contains("Multi Data").click()
cy.wait(500)
// Add Form with schema to match table
cy.addComponent("Form", "Form")
cy.get('[data-cy="dataSource-prop-control"').click()
cy.get(".dropdown").contains("Multi Data").click()
cy.wait(500)
// Add multi-select picker to form
cy.addComponent("Form", "Multi-select Picker").then((componentId) => {
cy.get('[data-cy="field-prop-control"]').type("Test Data").type('{enter}')
cy.wait(1000)
cy.getComponent(componentId).contains("Choose some options").click()
// Check picker has 5 items
cy.getComponent(componentId).find('li').should('have.length', 5)
// Select all items
for (let i = 1; i < 6; i++) {
cy.getComponent(componentId).find('li').contains(i).click()
}
// Check items have been selected
cy.getComponent(componentId).find('.spectrum-Picker-label').contains("(5)")
})
it("should add form with multi select picker, containing 5 options", () => {
cy.navigateToFrontend()
cy.wait(500)
// Add data provider
cy.get(`[data-cy="category-Data"]`).click()
cy.get(`[data-cy="component-Data Provider"]`).click()
cy.get('[data-cy="dataSource-prop-control"]').click()
cy.get(".dropdown").contains("Multi Data").click()
cy.wait(500)
// Add Form with schema to match table
cy.addComponent("Form", "Form")
cy.get('[data-cy="dataSource-prop-control"').click()
cy.get(".dropdown").contains("Multi Data").click()
cy.wait(500)
// Add multi-select picker to form
cy.addComponent("Form", "Multi-select Picker").then(componentId => {
cy.get('[data-cy="field-prop-control"]').type("Test Data").type("{enter}")
cy.wait(1000)
cy.getComponent(componentId).contains("Choose some options").click()
// Check picker has 5 items
cy.getComponent(componentId).find("li").should("have.length", 5)
// Select all items
for (let i = 1; i < 6; i++) {
cy.getComponent(componentId).find("li").contains(i).click()
}
// Check items have been selected
cy.getComponent(componentId)
.find(".spectrum-Picker-label")
.contains("(5)")
})
})
})

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

@ -12,7 +12,7 @@ context("Create a automation", () => {
cy.get("[data-cy='new-screen'] > .spectrum-Icon").click()
cy.get(".modal-inner-wrapper").within(() => {
cy.get("input").type("Add Row")
cy.contains("Row Created").click()
cy.contains("Row Created").click({ force: true })
cy.wait(500)
cy.get(".spectrum-Button--cta").click()
})
@ -22,9 +22,9 @@ context("Create a automation", () => {
cy.get(".spectrum-Picker-label").click()
cy.wait(500)
cy.contains("dog").click()
cy.wait(2000)
// Create action
cy.contains("Add Action").click()
cy.get(".block > .spectrum-Icon").click()
cy.get(".modal-inner-wrapper").within(() => {
cy.wait(1000)
cy.contains("Create Row").trigger('mouseover').click().click()

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

@ -53,6 +53,7 @@ context("Create a Table", () => {
cy.get(".spectrum-Table-editIcon > use").click()
cy.contains("Delete").click()
cy.wait(50)
cy.get(`[data-cy="delete-column-confirm"]`).type("nameupdated")
cy.contains("Delete Column").click()
cy.contains("nameupdated").should("not.exist")
})
@ -66,6 +67,7 @@ context("Create a Table", () => {
cy.get(".actions .spectrum-Icon").click({ force: true })
})
cy.get(".spectrum-Menu > :nth-child(2)").click()
cy.get(`[data-cy="delete-table-confirm"]`).type("dog")
cy.contains("Delete Table").click()
cy.contains("dog").should("not.exist")
})

1
packages/builder/cypress/setup.js

@ -17,6 +17,7 @@ process.env.JWT_SECRET = cypressConfig.env.JWT_SECRET
process.env.COUCH_URL = `leveldb://${tmpdir}/.data/`
process.env.SELF_HOSTED = 1
process.env.WORKER_URL = "http://localhost:10002/"
process.env.APPS_URL = `http://localhost:${MAIN_PORT}/`
process.env.MINIO_URL = `http://localhost:${MAIN_PORT}/`
process.env.MINIO_ACCESS_KEY = "budibase"
process.env.MINIO_SECRET_KEY = "budibase"

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

@ -182,16 +182,23 @@ Cypress.Commands.add("navigateToFrontend", () => {
cy.wait(1000)
cy.contains("Design").click()
cy.get(".spectrum-Search").type("/")
cy.get(".nav-item").contains("Home").click()
cy.createScreen("home", "home")
cy.addComponent("Elements", "Headline")
cy.get(".nav-item").contains("home").click()
})
Cypress.Commands.add("createScreen", (screenName, route) => {
cy.get("[aria-label=AddCircle]").click()
cy.get(".spectrum-Modal").within(() => {
cy.get("input").first().type(screenName)
cy.get("input").eq(1).type(route)
cy.get(".item").first().click()
cy.get(".spectrum-Button--cta").click()
})
cy.get(".spectrum-Modal").within(() => {
cy.get("input").first().clear().type(screenName)
cy.get("input").eq(1).clear().type(route)
cy.get(".spectrum-Button--cta").click()
cy.wait(2000)
})
})
Cypress.Commands.add("expandBudibaseConnection", () => {

12
packages/builder/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "0.9.189",
"version": "0.9.190-alpha.12",
"license": "GPL-3.0",
"private": true,
"scripts": {
@ -9,7 +9,7 @@
"test": "jest",
"test:watch": "jest --watchAll",
"dev:builder": "routify -c dev:vite",
"dev:vite": "vite",
"dev:vite": "vite --host 0.0.0.0",
"rollup": "rollup -c -w",
"cy:setup": "node ./cypress/setup.js",
"cy:run": "cypress run",
@ -65,10 +65,10 @@
}
},
"dependencies": {
"@budibase/bbui": "^0.9.189",
"@budibase/client": "^0.9.189",
"@budibase/bbui": "^0.9.190-alpha.12",
"@budibase/client": "^0.9.190-alpha.12",
"@budibase/colorpicker": "1.1.2",
"@budibase/string-templates": "^0.9.189",
"@budibase/string-templates": "^0.9.190-alpha.12",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",
@ -91,7 +91,7 @@
"@babel/runtime": "^7.13.10",
"@rollup/plugin-replace": "^2.4.2",
"@roxi/routify": "2.18.0",
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.5",
"@sveltejs/vite-plugin-svelte": "1.0.0-next.19",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/svelte": "^3.0.0",
"babel-jest": "^26.6.3",

120
packages/builder/src/builderStore/dataBinding.js

@ -4,6 +4,7 @@ import {
findAllMatchingComponents,
findComponent,
findComponentPath,
getComponentSettings,
} from "./storeUtils"
import { store } from "builderStore"
import { queries as queriesStores, tables as tablesStore } from "stores/backend"
@ -30,14 +31,33 @@ export const getBindableProperties = (asset, componentId) => {
const deviceBindings = getDeviceBindings()
const stateBindings = getStateBindings()
return [
...stateBindings,
...deviceBindings,
...urlBindings,
...contextBindings,
...urlBindings,
...stateBindings,
...userBindings,
...deviceBindings,
]
}
/**
* Gets the bindable properties exposed by a certain component.
*/
export const getComponentBindableProperties = (asset, componentId) => {
if (!asset || !componentId) {
return []
}
// Ensure that the component exists and exposes context
const component = findComponent(asset.props, componentId)
const def = store.actions.components.getDefinition(component?._component)
if (!def?.context) {
return []
}
// Get the bindings for the component
return getProviderContextBindings(asset, component)
}
/**
* Gets all data provider components above a component.
*/
@ -82,13 +102,10 @@ export const getActionProviderComponents = (asset, componentId, actionType) => {
* Gets a datasource object for a certain data provider component
*/
export const getDatasourceForProvider = (asset, component) => {
const def = store.actions.components.getDefinition(component?._component)
if (!def) {
return null
}
const settings = getComponentSettings(component?._component)
// If this component has a dataProvider setting, go up the stack and use it
const dataProviderSetting = def.settings.find(setting => {
const dataProviderSetting = settings.find(setting => {
return setting.type === "dataProvider"
})
if (dataProviderSetting) {
@ -100,7 +117,7 @@ export const getDatasourceForProvider = (asset, component) => {
// Extract datasource from component instance
const validSettingTypes = ["dataSource", "table", "schema"]
const datasourceSetting = def.settings.find(setting => {
const datasourceSetting = settings.find(setting => {
return validSettingTypes.includes(setting.type)
})
if (!datasourceSetting) {
@ -127,9 +144,26 @@ export const getDatasourceForProvider = (asset, component) => {
const getContextBindings = (asset, componentId) => {
// Extract any components which provide data contexts
const dataProviders = getDataProviderComponents(asset, componentId)
let bindings = []
// Generate bindings for all matching components
return getProviderContextBindings(asset, dataProviders)
}
/**
* Gets the context bindings exposed by a set of data provider components.
*/
const getProviderContextBindings = (asset, dataProviders) => {
if (!asset || !dataProviders) {
return []
}
// Ensure providers is an array
if (!Array.isArray(dataProviders)) {
dataProviders = [dataProviders]
}
// Create bindings for each data provider
let bindings = []
dataProviders.forEach(component => {
const def = store.actions.components.getDefinition(component._component)
const contexts = Array.isArray(def.context) ? def.context : [def.context]
@ -142,6 +176,7 @@ const getContextBindings = (asset, componentId) => {
let schema
let readablePrefix
let runtimeSuffix = context.suffix
if (context.type === "form") {
// Forms do not need table schemas
@ -171,22 +206,19 @@ const getContextBindings = (asset, componentId) => {
const keys = Object.keys(schema).sort()
// Generate safe unique runtime prefix
let providerId = component._id
if (runtimeSuffix) {
providerId += `-${runtimeSuffix}`
}
const safeComponentId = makePropSafe(providerId)
// Create bindable properties for each schema field
const safeComponentId = makePropSafe(component._id)
keys.forEach(key => {
const fieldSchema = schema[key]
// Make safe runtime binding and replace certain bindings with a
// new property to help display components
let runtimeBoundKey = key
if (fieldSchema.type === "link") {
runtimeBoundKey = `${key}_text`
} else if (fieldSchema.type === "attachment") {
runtimeBoundKey = `${key}_first`
}
const runtimeBinding = `${safeComponentId}.${makePropSafe(
runtimeBoundKey
)}`
// Make safe runtime binding
const runtimeBinding = `${safeComponentId}.${makePropSafe(key)}`
// Optionally use a prefix with readable bindings
let readableBinding = component._instanceName
@ -203,7 +235,7 @@ const getContextBindings = (asset, componentId) => {
// Field schema and provider are required to construct relationship
// datasource options, based on bindable properties
fieldSchema,
providerId: component._id,
providerId,
})
})
})
@ -225,17 +257,9 @@ const getUserBindings = () => {
const safeUser = makePropSafe("user")
keys.forEach(key => {
const fieldSchema = schema[key]
// Replace certain bindings with a new property to help display components
let runtimeBoundKey = key
if (fieldSchema.type === "link") {
runtimeBoundKey = `${key}_text`
} else if (fieldSchema.type === "attachment") {
runtimeBoundKey = `${key}_first`
}
bindings.push({
type: "context",
runtimeBinding: `${safeUser}.${makePropSafe(runtimeBoundKey)}`,
runtimeBinding: `${safeUser}.${makePropSafe(key)}`,
readableBinding: `Current User.${key}`,
// Field schema and provider are required to construct relationship
// datasource options, based on bindable properties
@ -309,8 +333,11 @@ const getUrlBindings = asset => {
*/
export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
let schema, table
if (datasource) {
const { type } = datasource
// Determine the source table from the datasource type
if (type === "provider") {
const component = findComponent(asset.props, datasource.providerId)
const source = getDatasourceForProvider(asset, component)
@ -318,11 +345,32 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
} else if (type === "query") {
const queries = get(queriesStores).list
table = queries.find(query => query._id === datasource._id)
} else if (type === "field") {
table = { name: datasource.fieldName }
const { fieldType } = datasource
if (fieldType === "attachment") {
schema = {
url: {
type: "string",
},
name: {
type: "string",
},
}
} else if (fieldType === "array") {
schema = {
value: {
type: "string",
},
}
}
} else {
const tables = get(tablesStore).list
table = tables.find(table => table._id === datasource.tableId)
}
if (table) {
// Determine the schema from the table if not already determined
if (table && !schema) {
if (type === "view") {
schema = cloneDeep(table.views?.[datasource.name]?.schema)
} else if (type === "query" && isForm) {
@ -374,8 +422,8 @@ const buildFormSchema = component => {
if (!component) {
return schema
}
const def = store.actions.components.getDefinition(component._component)
const fieldSetting = def?.settings?.find(
const settings = getComponentSettings(component._component)
const fieldSetting = settings.find(
setting => setting.key === "field" && setting.type.startsWith("field/")
)
if (fieldSetting && component.field) {
@ -501,7 +549,7 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
* {{ literal [componentId] }}
*/
function extractLiteralHandlebarsID(value) {
return value?.match(/{{\s*literal[\s[]+([a-fA-F0-9]+)[\s\]]*}}/)?.[1]
return value?.match(/{{\s*literal\s*\[+([^\]]+)].*}}/)?.[1]
}
/**

2
packages/builder/src/builderStore/store/automation/index.js

@ -93,7 +93,9 @@ const automationActions = store => ({
},
select: automation => {
store.update(state => {
let testResults = state.selectedAutomation?.testResults
state.selectedAutomation = new Automation(cloneDeep(automation))
state.selectedAutomation.testResults = testResults
state.selectedBlock = null
return state
})

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

@ -25,6 +25,7 @@ import {
findClosestMatchingComponent,
findAllMatchingComponents,
findComponent,
getComponentSettings,
} from "../storeUtils"
import { uuid } from "../uuid"
import { removeBindings } from "../dataBinding"
@ -369,14 +370,13 @@ export const getFrontendStore = () => {
}
// Generate default props
const settings = getComponentSettings(componentName)
let props = { ...presetProps }
if (definition.settings) {
definition.settings.forEach(setting => {
if (setting.defaultValue !== undefined) {
props[setting.key] = setting.defaultValue
}
})
}
settings.forEach(setting => {
if (setting.defaultValue !== undefined) {
props[setting.key] = setting.defaultValue
}
})
// Add any extra properties the component needs
let extras = {}
@ -524,7 +524,7 @@ export const getFrontendStore = () => {
}
}
},
paste: async (targetComponent, mode) => {
paste: async (targetComponent, mode, preserveBindings = false) => {
let promises = []
store.update(state => {
// Stop if we have nothing to paste
@ -536,7 +536,7 @@ export const getFrontendStore = () => {
const cut = state.componentToPaste.isCut
// immediately need to remove bindings, currently these aren't valid when pasted
if (!cut) {
if (!cut && !preserveBindings) {
state.componentToPaste = removeBindings(state.componentToPaste)
}
@ -620,6 +620,9 @@ export const getFrontendStore = () => {
if (!name || !component) {
return
}
if (component[name] === value) {
return
}
component[name] = value
store.update(state => {
state.selectedComponentId = component._id

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

@ -2,7 +2,9 @@ import { Screen } from "./utils/Screen"
export default {
name: `Create from scratch`,
id: `createFromScratch`,
create: () => createScreen(),
table: `Create from scratch`,
}
const createScreen = () => {

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

@ -15,6 +15,7 @@ export default function (tables) {
name: `${table.name} - New`,
create: () => createScreen(table),
id: NEW_ROW_TEMPLATE,
table: table.name,
}
})
}

1
packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js

@ -17,6 +17,7 @@ export default function (tables) {
name: `${table.name} - Detail`,
create: () => createScreen(table),
id: ROW_DETAIL_TEMPLATE,
table: table.name,
}
})
}

1
packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js

@ -10,6 +10,7 @@ export default function (tables) {
name: `${table.name} - List`,
create: () => createScreen(table),
id: ROW_LIST_TEMPLATE,
table: table.name,
}
})
}

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

@ -1,3 +1,5 @@
import { store } from "./index"
/**
* Recursively searches for a specific component ID
*/
@ -123,3 +125,20 @@ const searchComponentTree = (rootComponent, matchComponent) => {
}
return null
}
/**
* Searches a component's definition for a setting matching a certin predicate.
*/
export const getComponentSettings = componentType => {
const def = store.actions.components.getDefinition(componentType)
if (!def) {
return []
}
let settings = def.settings?.filter(setting => !setting.section) ?? []
def.settings
?.filter(setting => setting.section)
.forEach(section => {
settings = settings.concat(section.settings || [])
})
return settings
}

1
packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte

@ -84,7 +84,6 @@
class="block"
animate:flip={{ duration: 500 }}
in:fly|local={{ x: 500, duration: 1500 }}
out:fly|local={{ x: 500, duration: 800 }}
>
<FlowItem {testDataModal} {testAutomation} {onSelect} {block} />
</div>

13
packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte

@ -25,10 +25,10 @@
let resultsModal
let setupToggled
let blockComplete
$: testResult = $automationStore.selectedAutomation.testResults?.steps.filter(
step => step.stepId === block.stepId
)
$: isTrigger = block.type === "TRIGGER"
$: selected = $automationStore.selectedBlock?.id === block.id
@ -150,15 +150,6 @@
>Finish and test automation</Button
>
{/if}
<Button
disabled={!hasCompletedInputs}
on:click={() => {
setupToggled = false
actionModal.show()
}}
primary={!isTrigger}
cta={isTrigger}>Add Action</Button
>
{/if}
</Layout>
</div>
@ -202,7 +193,7 @@
display: inline-block;
}
.block {
width: 360px;
width: 480px;
font-size: 16px;
background-color: var(--background);
border: 1px solid var(--spectrum-global-color-gray-300);

1
packages/builder/src/components/automation/AutomationPanel/AutomationList.svelte

@ -9,6 +9,7 @@
onMount(() => {
automationStore.actions.fetch()
})
function selectAutomation(automation) {
automationStore.actions.select(automation)
$goto(`./${automation._id}`)

13
packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte

@ -3,7 +3,14 @@
import { database } from "stores/backend"
import { automationStore } from "builderStore"
import { notifications } from "@budibase/bbui"
import { Input, ModalContent, Layout, Body, Icon } from "@budibase/bbui"
import {
Input,
InlineAlert,
ModalContent,
Layout,
Body,
Icon,
} from "@budibase/bbui"
import analytics, { Events } from "analytics"
let name
@ -56,6 +63,10 @@
onConfirm={createAutomation}
disabled={!selectedTrigger || !name}
>
<InlineAlert
header="You must publish your app to activate your automations."
message="To test your automation before publishing, you can use the 'Run Test' functionality on the next screen."
/>
<Body size="XS"
>Please name your automation, then select a trigger. Every automation must
start with a trigger.

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

@ -51,31 +51,31 @@
: { schema: {} }
$: schemaFields = table ? Object.values(table.schema) : []
const onChange = debounce(
async function (e, key) {
if (isTestModal) {
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents
if (stepId === "WEBHOOK") {
automationStore.actions.addTestDataToAutomation({
body: {
[key]: e.detail,
...$automationStore.selectedAutomation.automation.testData.body,
},
})
}
const onChange = debounce(async function (e, key) {
if (isTestModal) {
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents
if (stepId === "WEBHOOK") {
automationStore.actions.addTestDataToAutomation({
[key]: e.detail,
body: {
[key]: e.detail,
...$automationStore.selectedAutomation.automation.testData.body,
},
})
testData[key] = e.detail
} else {
block.inputs[key] = e.detail
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
}
},
isTestModal ? 0 : 800
)
automationStore.actions.addTestDataToAutomation({
[key]: e.detail,
})
testData[key] = e.detail
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
} else {
block.inputs[key] = e.detail
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
}
}, 800)
function getAvailableBindings(block, automation) {
if (!block || !automation) {

16
packages/builder/src/components/backend/DataTable/DataTable.svelte

@ -4,7 +4,9 @@
import CreateRowButton from "./buttons/CreateRowButton.svelte"
import CreateColumnButton from "./buttons/CreateColumnButton.svelte"
import CreateViewButton from "./buttons/CreateViewButton.svelte"
import ExistingRelationshipButton from "./buttons/ExistingRelationshipButton.svelte"
import ExportButton from "./buttons/ExportButton.svelte"
import ImportButton from "./buttons/ImportButton.svelte"
import EditRolesButton from "./buttons/EditRolesButton.svelte"
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
@ -98,9 +100,7 @@
on:updatecolumns={onUpdateColumns}
on:updaterows={onUpdateRows}
>
{#if isInternal}
<CreateColumnButton on:updatecolumns={onUpdateColumns} />
{/if}
<CreateColumnButton on:updatecolumns={onUpdateColumns} />
{#if schema && Object.keys(schema).length > 0}
{#if !isUsersTable}
<CreateRowButton
@ -116,9 +116,19 @@
{#if isUsersTable}
<EditRolesButton />
{/if}
{#if !isInternal}
<ExistingRelationshipButton
table={$tables.selected}
on:updatecolumns={onUpdateColumns}
/>
{/if}
<HideAutocolumnButton bind:hideAutocolumns />
<!-- always have the export last -->
<ExportButton view={$tables.selected?._id} />
<ImportButton
tableId={$tables.selected?._id}
on:updaterows={onUpdateRows}
/>
{#key id}
<TableFilterButton {schema} on:change={onFilter} />
{/key}

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

@ -16,8 +16,8 @@
export let value = defaultValue || (meta.type === "boolean" ? false : "")
export let readonly
$: type = meta.type
$: label = capitalise(meta.name)
$: type = meta?.type
$: label = meta.name ? capitalise(meta.name) : ""
</script>
{#if type === "options"}

17
packages/builder/src/components/backend/DataTable/Table.svelte

@ -8,7 +8,11 @@
import CreateEditRow from "./modals/CreateEditRow.svelte"
import CreateEditUser from "./modals/CreateEditUser.svelte"
import CreateEditColumn from "./modals/CreateEditColumn.svelte"
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
import {
TableNames,
UNEDITABLE_USER_FIELDS,
UNSORTABLE_TYPES,
} from "constants"
import RoleCell from "./cells/RoleCell.svelte"
export let schema = {}
@ -33,6 +37,15 @@
$: isUsersTable = tableId === TableNames.USERS
$: data && resetSelectedRows()
$: editRowComponent = isUsersTable ? CreateEditUser : CreateEditRow
$: {
UNSORTABLE_TYPES.forEach(type => {
Object.values(schema).forEach(col => {
if (col.type === type) {
col.sortable = false
}
})
})
}
$: {
if (isUsersTable) {
customRenderers = [
@ -129,7 +142,7 @@
bind:selectedRows
allowSelectRows={allowEditing && !isUsersTable}
allowEditRows={allowEditing}
allowEditColumns={allowEditing && isInternal}
allowEditColumns={allowEditing}
showAutoColumns={!hideAutocolumns}
on:editcolumn={e => editColumn(e.detail)}
on:editrow={e => editRow(e.detail)}

54
packages/builder/src/components/backend/DataTable/buttons/ExistingRelationshipButton.svelte

@ -0,0 +1,54 @@
<script>
import { ActionButton, Modal, notifications } from "@budibase/bbui"
import CreateEditRelationship from "../../Datasources/CreateEditRelationship.svelte"
import { datasources, tables } from "../../../../stores/backend"
import { createEventDispatcher } from "svelte"
export let table
const dispatch = createEventDispatcher()
$: plusTables = datasource?.plus
? Object.values(datasource?.entities || {})
: []
$: datasource = $datasources.list.find(
source => source._id === table?.sourceId
)
let modal
async function saveRelationship() {
try {
// Create datasource
await datasources.save(datasource)
notifications.success(`Relationship information saved.`)
const tableList = await tables.fetch()
await tables.select(tableList.find(tbl => tbl._id === table._id))
dispatch("updatecolumns")
} catch (err) {
notifications.error(`Error saving relationship info: ${err}`)
}
}
</script>
{#if table.sourceId}
<div>
<ActionButton
icon="DataCorrelated"
primary
size="S"
quiet
on:click={modal.show}
>
Define existing relationship
</ActionButton>
</div>
<Modal bind:this={modal}>
<CreateEditRelationship
{datasource}
save={saveRelationship}
close={modal.hide}
{plusTables}
selectedFromTable={table}
/>
</Modal>
{/if}

2
packages/builder/src/components/backend/DataTable/buttons/ExportButton.svelte

@ -7,7 +7,7 @@
let modal
</script>
<ActionButton icon="Download" size="S" quiet on:click={modal.show}>
<ActionButton icon="DataDownload" size="S" quiet on:click={modal.show}>
Export
</ActionButton>
<Modal bind:this={modal}>

15
packages/builder/src/components/backend/DataTable/buttons/ImportButton.svelte

@ -0,0 +1,15 @@
<script>
import { ActionButton, Modal } from "@budibase/bbui"
import ImportModal from "../modals/ImportModal.svelte"
export let tableId
let modal
</script>
<ActionButton icon="DataUpload" size="S" quiet on:click={modal.show}>
Import
</ActionButton>
<Modal bind:this={modal}>
<ImportModal {tableId} on:updaterows />
</Modal>

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

@ -9,6 +9,7 @@
DatePicker,
ModalContent,
Context,
notifications,
} from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { cloneDeep } from "lodash/fp"
@ -18,9 +19,13 @@
FIELDS,
AUTO_COLUMN_SUB_TYPES,
RelationshipTypes,
ALLOWABLE_STRING_OPTIONS,
ALLOWABLE_NUMBER_OPTIONS,
ALLOWABLE_STRING_TYPES,
ALLOWABLE_NUMBER_TYPES,
SWITCHABLE_TYPES,
} from "constants/backend"
import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils"
import { notifications } from "@budibase/bbui"
import ValuesList from "components/common/ValuesList.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { truncate } from "lodash"
@ -31,6 +36,10 @@
const AUTO_TYPE = "auto"
const FORMULA_TYPE = FIELDS.FORMULA.type
const LINK_TYPE = FIELDS.LINK.type
const STRING_TYPE = FIELDS.STRING.type
const NUMBER_TYPE = FIELDS.NUMBER.type
const DATE_TYPE = FIELDS.DATETIME.type
const dispatch = createEventDispatcher()
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
const { hide } = getContext(Context.Modal)
@ -54,10 +63,9 @@
let indexes = [...($tables.selected.indexes || [])]
let confirmDeleteDialog
let deletion
let deleteColName
$: tableOptions = $tables.list.filter(
table => table._id !== $tables.draft._id && table.type !== "external"
)
$: checkConstraints(field)
$: required = !!field?.constraints?.presence || primaryDisplay
$: uneditable =
$tables.selected?._id === TableNames.USERS &&
@ -65,13 +73,8 @@
$: invalid =
!field.name ||
(field.type === LINK_TYPE && !field.tableId) ||
Object.keys($tables.draft?.schema ?? {}).some(
key => key !== originalName && key === field.name
) ||
columnNameInvalid
$: columnNameInvalid = PROHIBITED_COLUMN_NAMES.some(
name => field.name === name
)
Object.keys(errors).length !== 0
$: errors = checkErrors(field)
// used to select what different options can be displayed for column type
$: canBeSearched =
@ -83,18 +86,36 @@
$: canBeRequired =
field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_TYPE
$: relationshipOptions = getRelationshipOptions(field)
$: external = table.type === "external"
// in the case of internal tables the sourceId will just be undefined
$: tableOptions = $tables.list.filter(
opt =>
opt._id !== $tables.draft._id &&
opt.type === table.type &&
table.sourceId === opt.sourceId
)
$: typeEnabled =
!originalName ||
(originalName && SWITCHABLE_TYPES.indexOf(field.type) !== -1)
async function saveColumn() {
if (field.type === AUTO_TYPE) {
field = buildAutoColumn($tables.draft.name, field.name, field.subtype)
}
await tables.saveField({
originalName,
field,
primaryDisplay,
indexes,
})
dispatch("updatecolumns")
if (field.type !== LINK_TYPE) {
delete field.fieldName
}
try {
await tables.saveField({
originalName,
field,
primaryDisplay,
indexes,
})
dispatch("updatecolumns")
} catch (err) {
notifications.error(err)
}
}
function deleteColumn() {
@ -162,6 +183,7 @@
function hideDeleteDialog() {
confirmDeleteDialog.hide()
deleteColName = ""
deletion = false
}
@ -169,7 +191,7 @@
if (!field || !field.tableId) {
return null
}
const linkTable = tableOptions.find(table => table._id === field.tableId)
const linkTable = tableOptions?.find(table => table._id === field.tableId)
if (!linkTable) {
return null
}
@ -193,6 +215,80 @@
},
]
}
function getAllowedTypes() {
if (originalName && ALLOWABLE_STRING_TYPES.indexOf(field.type) !== -1) {
return ALLOWABLE_STRING_OPTIONS
} else if (
originalName &&
ALLOWABLE_NUMBER_TYPES.indexOf(field.type) !== -1
) {
return ALLOWABLE_NUMBER_OPTIONS
} else if (!external) {
return [
...Object.values(fieldDefinitions),
{ name: "Auto Column", type: AUTO_TYPE },
]
} else {
return [
FIELDS.STRING,
FIELDS.LONGFORM,
FIELDS.OPTIONS,
FIELDS.DATETIME,
FIELDS.NUMBER,
FIELDS.BOOLEAN,
FIELDS.ARRAY,
FIELDS.FORMULA,
FIELDS.LINK,
]
}
}
function checkConstraints(fieldToCheck) {
// most types need this, just make sure its always present
if (fieldToCheck && !fieldToCheck.constraints) {
fieldToCheck.constraints = {}
}
// some string types may have been built by server, may not always have constraints
if (fieldToCheck.type === STRING_TYPE && !fieldToCheck.constraints.length) {
fieldToCheck.constraints.length = {}
}
// some number types made server-side will be missing constraints
if (
fieldToCheck.type === NUMBER_TYPE &&
!fieldToCheck.constraints.numericality
) {
fieldToCheck.constraints.numericality = {}
}
if (fieldToCheck.type === DATE_TYPE && !fieldToCheck.constraints.datetime) {
fieldToCheck.constraints.datetime = {}
}
}
function checkErrors(fieldInfo) {
function inUse(tbl, column, ogName = null) {
return Object.keys(tbl?.schema || {}).some(
key => key !== ogName && key === column
)
}
const newError = {}
if (PROHIBITED_COLUMN_NAMES.some(name => fieldInfo.name === name)) {
newError.name = `${PROHIBITED_COLUMN_NAMES.join(
", "
)} are not allowed as column names`
} else if (inUse($tables.draft, fieldInfo.name, originalName)) {
newError.name = `Column name already in use.`
}
if (fieldInfo.fieldName && fieldInfo.tableId) {
const relatedTable = $tables.list.find(
tbl => tbl._id === fieldInfo.tableId
)
if (inUse(relatedTable, fieldInfo.fieldName)) {
newError.relatedName = `Column name already in use in table ${relatedTable.name}`
}
}
return newError
}
</script>
<ModalContent
@ -205,20 +301,15 @@
label="Name"
bind:value={field.name}
disabled={uneditable || (linkEditDisabled && field.type === LINK_TYPE)}
error={columnNameInvalid
? `${PROHIBITED_COLUMN_NAMES.join(", ")} are not allowed as column names`
: ""}
error={errors?.name}
/>
<Select
disabled={originalName}
disabled={!typeEnabled}
label="Type"
bind:value={field.type}
on:change={handleTypeChange}
options={[
...Object.values(fieldDefinitions),
{ name: "Auto Column", type: AUTO_TYPE },
]}
options={getAllowedTypes()}
getOptionLabel={field => field.name}
getOptionValue={field => field.type}
/>
@ -245,7 +336,7 @@
</div>
{/if}
{#if canBeSearched}
{#if canBeSearched && !external}
<div>
<Label grey small>Search Indexes</Label>
<Toggle
@ -319,6 +410,7 @@
disabled={linkEditDisabled}
label={`Column name in other table`}
bind:value={field.fieldName}
error={errors.relatedName}
/>
{:else if field.type === FORMULA_TYPE}
<ModalBindableInput
@ -327,7 +419,7 @@
value={field.formula}
on:change={e => (field.formula = e.detail)}
bindings={getBindings({ table })}
serverSide="true"
allowJS
/>
{:else if field.type === AUTO_TYPE}
<Select
@ -348,9 +440,20 @@
</ModalContent>
<ConfirmDialog
bind:this={confirmDeleteDialog}
body={`Are you sure you wish to delete this column? Your data will be deleted and this action cannot be undone.`}
okText="Delete Column"
onOk={deleteColumn}
onCancel={hideDeleteDialog}
title="Confirm Deletion"
/>
disabled={deleteColName !== field.name}
>
<p>
Are you sure you wish to delete the column <b>{field.name}?</b>
Your data will be deleted and this action cannot be undone - enter the column
name to confirm.
</p>
<Input
dataCy="delete-column-confirm"
bind:value={deleteColName}
placeholder={field.name}
/>
</ConfirmDialog>

17
packages/builder/src/components/backend/DataTable/modals/FilterModal.svelte

@ -69,6 +69,7 @@
({ _id }) => _id === $views.selected?.tableId
)
$: fields = viewTable && Object.keys(viewTable.schema)
$: schema = viewTable && viewTable.schema ? viewTable.schema : {}
function saveView() {
views.save(view)
@ -90,29 +91,29 @@
function isMultipleChoice(field) {
return (
viewTable.schema[field]?.constraints?.inclusion?.length ||
viewTable.schema[field]?.type === "boolean"
schema[field]?.constraints?.inclusion?.length ||
schema[field]?.type === "boolean"
)
}
function fieldOptions(field) {
return viewTable.schema[field]?.type === "options"
? viewTable.schema[field]?.constraints.inclusion
return schema[field]?.type === "options"
? schema[field]?.constraints.inclusion
: [true, false]
}
function isDate(field) {
return viewTable.schema[field]?.type === "datetime"
return schema[field]?.type === "datetime"
}
function isNumber(field) {
return viewTable.schema[field]?.type === "number"
return schema[field]?.type === "number"
}
const fieldChanged = filter => ev => {
// Reset if type changed
const oldType = viewTable.schema[filter.key]?.type
const newType = viewTable.schema[ev.detail]?.type
const oldType = schema[filter.key]?.type
const newType = schema[ev.detail]?.type
if (filter.key && ev.detail && oldType !== newType) {
filter.value = ""
}

43
packages/builder/src/components/backend/DataTable/modals/ImportModal.svelte

@ -0,0 +1,43 @@
<script>
import { ModalContent, Label, notifications, Body } from "@budibase/bbui"
import TableDataImport from "../../TableNavigator/TableDataImport.svelte"
import api from "builderStore/api"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let tableId
let dataImport
$: valid = dataImport?.csvString != null && dataImport?.valid
async function importData() {
const response = await api.post(`/api/tables/${tableId}/import`, {
dataImport,
})
if (response.status !== 200) {
const error = await response.text()
notifications.error(`Unable to import data - ${error}`)
} else {
notifications.success("Rows successfully imported.")
}
dispatch("updaterows")
}
</script>
<ModalContent
title="Import Data"
confirmText="Import"
onConfirm={importData}
disabled={!valid}
>
<Body
>Import rows to an existing table from a CSV. Only columns from the CSV
which exist in the table will be imported.</Body
>
<Label grey extraSmall>CSV to import</Label>
<TableDataImport bind:dataImport bind:existingTableId={tableId} />
</ModalContent>
<style>
</style>

2
packages/builder/src/components/backend/DataTable/modals/ManageAccessModal.svelte

@ -23,8 +23,6 @@
// Show updated permissions in UI: REMOVE
permissions = await permissionsStore.forResource(resourceId)
notifications.success("Updated permissions.")
// TODO: update permissions
// permissions[]
}
</script>

16
packages/builder/src/components/backend/DatasourceNavigator/icons/Oracle.svelte

@ -0,0 +1,16 @@
<script>
export let width = "18"
export let height = "18"
import OracleLogo from "assets/oracle.png"
</script>
<div class>
<img {height} {width} src={OracleLogo} alt="oracle logo" />
</div>
<style>
img {
padding-top: 1px;
}
</style>

2
packages/builder/src/components/backend/DatasourceNavigator/icons/index.js

@ -10,6 +10,7 @@ import MySQL from "./MySQL.svelte"
import ArangoDB from "./ArangoDB.svelte"
import Rest from "./Rest.svelte"
import Budibase from "./Budibase.svelte"
import Oracle from "./Oracle.svelte"
export default {
BUDIBASE: Budibase,
@ -24,4 +25,5 @@ export default {
MYSQL: MySQL,
ARANGODB: ArangoDB,
REST: Rest,
ORACLE: Oracle,
}

10
packages/builder/src/components/backend/DatasourceNavigator/popovers/EditDatasourcePopover.svelte

@ -1,6 +1,6 @@
<script>
import { goto } from "@roxi/routify"
import { datasources } from "stores/backend"
import { datasources, tables } from "stores/backend"
import { notifications } from "@budibase/bbui"
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -13,10 +13,16 @@
async function deleteDatasource() {
const wasSelectedSource = $datasources.selected
const wasSelectedTable = $tables.selected
await datasources.delete(datasource)
notifications.success("Datasource deleted")
// navigate to first index page if the source you are deleting is selected
if (wasSelectedSource === datasource._id) {
const entities = Object.values(datasource.entities)
if (
wasSelectedSource === datasource._id ||
(entities &&
entities.find(entity => entity._id === wasSelectedTable?._id))
) {
$goto("./datasource")
}
}

60
packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/CreateEditRelationship/CreateEditRelationship.svelte → packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte

@ -18,10 +18,19 @@
export let fromRelationship = {}
export let toRelationship = {}
export let close
export let selectedFromTable
let originalFromName = fromRelationship.name,
originalToName = toRelationship.name
if (fromRelationship && !fromRelationship.relationshipType) {
fromRelationship.relationshipType = RelationshipTypes.MANY_TO_ONE
}
if (toRelationship && selectedFromTable) {
toRelationship.tableId = selectedFromTable._id
}
function inSchema(table, prop, ogName) {
if (!table || !prop || prop === ogName) {
return false
@ -62,6 +71,9 @@
if ($touched.toCol && !toRelate.name) {
errors.toCol = colNotSet
}
if ($touched.primary && !fromPrimary) {
errors.primary = "Please pick the primary key"
}
// currently don't support relationships back onto the table itself, needs to relate out
const tableError = "From/to/through tables must be different"
if (fromTable && (fromTable === toTable || fromTable === throughTable)) {
@ -86,6 +98,16 @@
return errors
}
let fromPrimary
$: {
if (!fromPrimary && fromTable) {
fromPrimary = fromTable.primary[0]
}
}
$: isManyToMany =
fromRelationship?.relationshipType === RelationshipTypes.MANY_TO_MANY
$: isManyToOne =
fromRelationship?.relationshipType === RelationshipTypes.MANY_TO_ONE
$: tableOptions = plusTables.map(table => ({
label: table.name,
value: table._id,
@ -114,6 +136,7 @@
},
]
$: updateRelationshipType(fromRelationship?.relationshipType)
$: tableChanged(fromTable, toTable)
function updateRelationshipType(fromType) {
if (fromType === RelationshipTypes.MANY_TO_MANY) {
@ -169,13 +192,13 @@
// foreignKey is what is linking out of the current table.
relateFrom = {
...relateFrom,
foreignKey: fromTable.primary[0],
foreignKey: fromPrimary,
}
relateTo = {
...relateTo,
relationshipType: RelationshipTypes.ONE_TO_MANY,
foreignKey: relateFrom.fieldName,
fieldName: fromTable.primary[0],
fieldName: fromPrimary,
}
}
@ -205,7 +228,6 @@
originalToName = toRelationship.name
originalFromName = fromRelationship.name
await save()
await tables.fetch()
}
async function deleteRelationship() {
@ -215,10 +237,26 @@
await tables.fetch()
close()
}
function tableChanged(fromTbl, toTbl) {
fromRelationship.name = toTbl?.name || ""
errors.fromCol = ""
toRelationship.name = fromTbl?.name || ""
errors.toCol = ""
if (toTbl || fromTbl) {
checkForErrors(
fromTable,
toTable,
through,
fromRelationship,
toRelationship
)
}
}
</script>
<ModalContent
title="Create Relationship"
title="Define Relationship"
confirmText="Save"
onConfirm={saveRelationship}
disabled={!valid}
@ -234,10 +272,20 @@
<Select
label="Select from table"
options={tableOptions}
disabled={!!selectedFromTable}
on:change={() => ($touched.from = true)}
bind:error={errors.from}
bind:value={toRelationship.tableId}
/>
{#if isManyToOne && fromTable}
<Select
label={`Primary Key (${fromTable?.name})`}
options={Object.keys(fromTable?.schema)}
on:change={() => ($touched.primary = true)}
bind:error={errors.primary}
bind:value={fromPrimary}
/>
{/if}
<Select
label={"Select to table"}
options={tableOptions}
@ -245,7 +293,7 @@
bind:error={errors.to}
bind:value={fromRelationship.tableId}
/>
{#if fromRelationship?.relationshipType === RelationshipTypes.MANY_TO_MANY}
{#if isManyToMany}
<Select
label={"Through"}
options={tableOptions}
@ -269,7 +317,7 @@
bind:value={fromRelationship.throughFrom}
/>
{/if}
{:else if fromRelationship?.relationshipType && toTable}
{:else if isManyToOne && toTable}
<Select
label={`Foreign Key (${toTable?.name})`}
options={Object.keys(toTable?.schema).filter(

0
packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/CreateEditRelationship/TableSelect.svelte → packages/builder/src/components/backend/Datasources/TableSelect.svelte

45
packages/builder/src/components/backend/TableNavigator/TableDataImport.svelte

@ -1,6 +1,5 @@
<script>
import { Select } from "@budibase/bbui"
import { notifications } from "@budibase/bbui"
import { Select, InlineAlert, notifications } from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import api from "builderStore/api"
@ -12,19 +11,27 @@
valid: true,
schema: {},
}
export let existingTableId
let csvString
let primaryDisplay
let csvString = undefined
let primaryDisplay = undefined
let schema = {}
let fields = []
let hasValidated = false
$: valid = !schema || fields.every(column => schema[column].success)
$: valid =
!schema ||
(fields.every(column => schema[column].success) &&
(!hasValidated || Object.keys(schema).length > 0))
$: dataImport = {
valid,
schema: buildTableSchema(schema),
csvString,
primaryDisplay,
}
$: noFieldsError = existingTableId
? "No columns in CSV match existing table schema"
: "Could not find any columns to import"
function buildTableSchema(schema) {
const tableSchema = {}
@ -46,6 +53,7 @@
const response = await api.post("/api/tables/csv/validate", {
csvString,
schema: schema || {},
tableId: existingTableId,
})
const parseResult = await response.json()
@ -63,6 +71,7 @@
notifications.error("CSV Invalid, please try another CSV file")
return []
}
hasValidated = true
}
async function handleFile(evt) {
@ -138,6 +147,7 @@
placeholder={null}
getOptionLabel={option => option.label}
getOptionValue={option => option.value}
disabled={!!existingTableId}
/>
<span class="field-status" class:error={!schema[columnName].success}>
{schema[columnName].success ? "Success" : "Failure"}
@ -149,15 +159,22 @@
</div>
{/each}
</div>
{/if}
{#if fields.length}
<div class="display-column">
<Select
label="Display Column"
bind:value={primaryDisplay}
options={fields}
sort
{#if !existingTableId}
<div class="display-column">
<Select
label="Display Column"
bind:value={primaryDisplay}
options={fields}
sort
/>
</div>
{/if}
{:else if hasValidated}
<div>
<InlineAlert
header="Invalid CSV"
bind:message={noFieldsError}
type="error"
/>
</div>
{/if}

65
packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte

@ -1,7 +1,7 @@
<script>
import { goto } from "@roxi/routify"
import { allScreens, store } from "builderStore"
import { tables } from "stores/backend"
import { tables, datasources } from "stores/backend"
import {
ActionMenu,
Icon,
@ -21,8 +21,10 @@
let originalName = table.name
let templateScreens
let willBeDeleted
let deleteTableName
$: external = table?.type === "external"
$: allowDeletion = !external || table?.created
function showDeleteModal() {
templateScreens = $allScreens.filter(
@ -36,15 +38,26 @@
async function deleteTable() {
const wasSelectedTable = $tables.selected
await tables.delete(table)
store.actions.screens.delete(templateScreens)
await tables.fetch()
notifications.success("Table deleted")
if (wasSelectedTable._id === table._id) {
$goto("./table")
try {
await tables.delete(table)
await store.actions.screens.delete(templateScreens)
await tables.fetch()
if (table.type === "external") {
await datasources.fetch()
}
notifications.success("Table deleted")
if (wasSelectedTable && wasSelectedTable._id === table._id) {
$goto("./table")
}
} catch (err) {
notifications.error(err)
}
}
function hideDeleteDialog() {
deleteTableName = ""
}
async function save() {
await tables.save(table)
notifications.success("Table renamed successfully")
@ -59,15 +72,17 @@
}
</script>
<ActionMenu>
<div slot="control" class="icon">
<Icon s hoverable name="MoreSmallList" />
</div>
<MenuItem icon="Edit" on:click={editorModal.show}>Edit</MenuItem>
{#if !external}
{#if allowDeletion}
<ActionMenu>
<div slot="control" class="icon">
<Icon s hoverable name="MoreSmallList" />
</div>
{#if !external}
<MenuItem icon="Edit" on:click={editorModal.show}>Edit</MenuItem>
{/if}
<MenuItem icon="Delete" on:click={showDeleteModal}>Delete</MenuItem>
{/if}
</ActionMenu>
</ActionMenu>
{/if}
<Modal bind:this={editorModal}>
<ModalContent
@ -89,11 +104,15 @@
bind:this={confirmDeleteDialog}
okText="Delete Table"
onOk={deleteTable}
onCancel={hideDeleteDialog}
title="Confirm Deletion"
disabled={deleteTableName !== table.name}
>
Are you sure you wish to delete the table
<i>{table.name}?</i>
The following will also be deleted:
<p>
Are you sure you wish to delete the table
<b>{table.name}?</b>
The following will also be deleted:
</p>
<b>
<div class="delete-items">
{#each willBeDeleted as item}
@ -101,7 +120,15 @@
{/each}
</div>
</b>
This action cannot be undone.
<p>
This action cannot be undone - to continue please enter the table name below
to confirm.
</p>
<Input
bind:value={deleteTableName}
placeholder={table.name}
dataCy="delete-table-confirm"
/>
</ConfirmDialog>
<style>

8
packages/builder/src/components/common/CodeMirrorEditor.svelte

@ -1,6 +1,4 @@
<script context="module">
import { Label } from "@budibase/bbui"
export const EditorModes = {
JS: {
name: "javascript",
@ -21,6 +19,7 @@
</script>
<script>
import { Label } from "@budibase/bbui"
import CodeMirror from "components/integration/codemirror"
import { themeStore } from "builderStore"
import { createEventDispatcher, onMount } from "svelte"
@ -156,4 +155,9 @@
div :global(.CodeMirror-focused) {
border-color: var(--spectrum-alias-border-color-mouse-focus);
}
/* Ensure hints are always on top */
:global(.CodeMirror-hints) {
z-index: 999999;
}
</style>

75
packages/builder/src/components/common/bindings/BindingPanel.svelte

@ -22,7 +22,7 @@
const dispatch = createEventDispatcher()
export let bindableProperties
export let bindings
export let value = ""
export let valid
export let allowJS = false
@ -36,17 +36,23 @@
let hbsValue = initialValueJS ? null : value
$: usingJS = mode === "JavaScript"
$: ({ context } = groupBy("type", bindableProperties))
$: searchRgx = new RegExp(search, "ig")
$: filteredBindings = context?.filter(context => {
return context.readableBinding.match(searchRgx)
})
$: categories = Object.entries(groupBy("category", bindings))
$: filteredCategories = categories
.map(([name, categoryBindings]) => ({
name,
bindings: categoryBindings?.filter(binding => {
return binding.readableBinding.match(searchRgx)
}),
}))
.filter(category => category.bindings?.length > 0)
$: filteredHelpers = helpers?.filter(helper => {
return helper.label.match(searchRgx) || helper.description.match(searchRgx)
})
$: codeMirrorHints = bindings?.map(x => `$("${x.readableBinding}")`)
const updateValue = value => {
valid = isValid(readableToRuntimeBinding(bindableProperties, value))
valid = isValid(readableToRuntimeBinding(bindings, value))
if (valid) {
dispatch("change", value)
}
@ -91,7 +97,7 @@
}
onMount(() => {
valid = isValid(readableToRuntimeBinding(bindableProperties, value))
valid = isValid(readableToRuntimeBinding(bindings, value))
})
</script>
@ -102,18 +108,29 @@
<div class="heading">Search</div>
<Search placeholder="Search" bind:value={search} />
</section>
{#if filteredBindings?.length}
<section>
<div class="heading">Bindable Values</div>
<ul>
{#each filteredBindings as binding}
<li on:click={() => addBinding(binding)}>
{binding.readableBinding}
</li>
{/each}
</ul>
</section>
{/if}
{#each filteredCategories as category}
{#if category.bindings?.length}
<section>
<div class="heading">{category.name}</div>
<ul>
{#each category.bindings as binding}
<li on:click={() => addBinding(binding)}>
<span class="binding__label">{binding.readableBinding}</span>
{#if binding.type}
<span class="binding__type">{binding.type}</span>
{/if}
{#if binding.description}
<br />
<div class="binding__description">
{binding.description || ""}
</div>
{/if}
</li>
{/each}
</ul>
</section>
{/if}
{/each}
{#if filteredHelpers?.length && !usingJS}
<section>
<div class="heading">Helpers</div>
@ -162,7 +179,7 @@
height={200}
value={decodeJSBinding(jsValue)}
on:change={onChangeJSValue}
hints={context?.map(x => `$("${x.readableBinding}")`)}
hints={codeMirrorHints}
/>
<Body size="S">
JavaScript expressions are executed as functions, so ensure that
@ -234,6 +251,24 @@
color: var(--spectrum-global-color-gray-900) !important;
}
.binding__label {
font-weight: 600;
text-transform: capitalize;
}
.binding__description {
color: var(--spectrum-global-color-gray-700);
margin: 0.5rem 0 0 0;
white-space: normal;
}
.binding__type {
font-family: monospace;
background-color: var(--spectrum-global-color-gray-200);
border-radius: var(--border-radius-s);
padding: 2px 4px;
margin-left: 2px;
font-weight: 600;
}
.helper {
display: flex;
flex-direction: column;

30
packages/builder/src/components/common/bindings/ClientBindingPanel.svelte

@ -0,0 +1,30 @@
<script>
import BindingPanel from "./BindingPanel.svelte"
export let bindings = []
export let valid
export let value = ""
export let allowJS = false
$: enrichedBindings = enrichBindings(bindings)
// Ensure bindings have the correct categories
const enrichBindings = bindings => {
if (!bindings?.length) {
return bindings
}
return bindings?.map(binding => ({
...binding,
category: "Bindable Values",
type: null,
}))
}
</script>
<BindingPanel
bind:valid
bindings={enrichedBindings}
{value}
{allowJS}
on:change
/>

36
packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte

@ -4,11 +4,11 @@
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import { createEventDispatcher } from "svelte"
import { isJSBinding } from "@budibase/string-templates"
export let panel = BindingPanel
export let panel = ClientBindingPanel
export let value = ""
export let bindings = []
export let title = "Bindings"
@ -17,6 +17,7 @@
export let disabled = false
export let options
export let allowJS = true
export let appendBindingsAsOptions = true
const dispatch = createEventDispatcher()
let bindingDrawer
@ -24,28 +25,49 @@
$: readableValue = runtimeToReadableBinding(bindings, value)
$: tempValue = readableValue
$: isJS = isJSBinding(value)
$: allOptions = buildOptions(options, bindings, appendBindingsAsOptions)
const handleClose = () => {
onChange(tempValue)
bindingDrawer.hide()
}
const onChange = value => {
const onChange = (value, optionPicked) => {
// Add HBS braces if picking binding
if (optionPicked && !options?.includes(value)) {
value = `{{ ${value} }}`
}
dispatch("change", readableToRuntimeBinding(bindings, value))
}
const buildOptions = (options, bindings, appendBindingsAsOptions) => {
if (!appendBindingsAsOptions) {
return options
}
return []
.concat(options || [])
.concat(bindings?.map(binding => binding.readableBinding) || [])
}
</script>
<div class="control">
<Combobox
{label}
{disabled}
readonly={isJS}
value={isJS ? "(JavaScript function)" : readableValue}
on:change={event => onChange(event.detail)}
on:type={e => onChange(e.detail, false)}
on:pick={e => onChange(e.detail, true)}
{placeholder}
{options}
options={allOptions}
/>
{#if !disabled}
<div class="icon" on:click={bindingDrawer.show}>
<div
class="icon"
on:click={bindingDrawer.show}
data-cy="text-binding-button"
>
<Icon size="S" name="FlashOn" />
</div>
{/if}
@ -61,7 +83,7 @@
value={readableValue}
close={handleClose}
on:change={event => (tempValue = event.detail)}
bindableProperties={bindings}
{bindings}
{allowJS}
/>
</Drawer>

7
packages/builder/src/components/common/bindings/DrawerBindableInput.svelte

@ -4,11 +4,11 @@
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import { createEventDispatcher } from "svelte"
import { isJSBinding } from "@budibase/string-templates"
export let panel = BindingPanel
export let panel = ClientBindingPanel
export let value = ""
export let bindings = []
export let title = "Bindings"
@ -40,6 +40,7 @@
<Input
{label}
{disabled}
readonly={isJS}
value={isJS ? "(JavaScript function)" : readableValue}
on:change={event => onChange(event.detail)}
{placeholder}
@ -63,7 +64,7 @@
bind:valid
value={readableValue}
on:change={event => (tempValue = event.detail)}
bindableProperties={bindings}
{bindings}
{allowJS}
/>
</Drawer>

12
packages/builder/src/components/common/bindings/ModalBindableInput.svelte

@ -6,20 +6,23 @@
} from "builderStore/dataBinding"
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
import { createEventDispatcher } from "svelte"
import { isJSBinding } from "@budibase/string-templates"
export let panel = ServerBindingPanel
export let value = ""
export let bindings = []
export let thin = true
export let title = "Bindings"
export let placeholder
export let label
export let allowJS = false
const dispatch = createEventDispatcher()
let bindingModal
let valid = true
$: readableValue = runtimeToReadableBinding(bindings, value)
$: tempValue = readableValue
$: isJS = isJSBinding(value)
const saveBinding = () => {
onChange(tempValue)
@ -34,8 +37,8 @@
<div class="control">
<Input
{label}
{thin}
value={readableValue}
readonly={isJS}
value={isJS ? "(JavaScript function)" : readableValue}
on:change={event => onChange(event.detail)}
{placeholder}
/>
@ -55,7 +58,8 @@
value={readableValue}
bind:valid
on:change={e => (tempValue = e.detail)}
bindableProperties={bindings}
{bindings}
{allowJS}
/>
</div>
</ModalContent>

222
packages/builder/src/components/common/bindings/ServerBindingPanel.svelte

@ -1,209 +1,27 @@
<script>
import groupBy from "lodash/fp/groupBy"
import { Search, TextArea, DrawerContent } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { isValid } from "@budibase/string-templates"
import { handlebarsCompletions } from "constants/completions"
import { readableToRuntimeBinding } from "builderStore/dataBinding"
import { addHBSBinding } from "./utils"
import BindingPanel from "./BindingPanel.svelte"
const dispatch = createEventDispatcher()
export let bindableProperties = []
export let valid = true
export let bindings = []
export let valid
export let value = ""
export let allowJS = false
let helpers = handlebarsCompletions()
let getCaretPosition
let search = ""
$: categories = Object.entries(groupBy("category", bindableProperties))
$: valid = isValid(readableToRuntimeBinding(bindableProperties, value))
$: dispatch("change", value)
$: searchRgx = new RegExp(search, "ig")
$: filteredCategories = categories.map(([categoryName, bindings]) => {
const filteredBindings = bindings.filter(binding => {
return binding.label.match(searchRgx)
})
return [categoryName, filteredBindings]
})
$: filteredHelpers = helpers?.filter(helper => {
return helper.label.match(searchRgx) || helper.description.match(searchRgx)
})
</script>
<DrawerContent>
<svelte:fragment slot="sidebar">
<div class="container">
<section>
<div class="heading">Search</div>
<Search placeholder="Search" bind:value={search} />
</section>
{#each filteredCategories as [categoryName, bindings]}
{#if bindings.length}
<section>
<div class="heading">{categoryName}</div>
<ul>
{#each bindings as binding}
<li
on:click={() => {
value = addHBSBinding(value, getCaretPosition(), binding)
}}
>
<span class="binding__label">{binding.label}</span>
<span class="binding__type">{binding.type}</span>
{#if binding.description}
<br />
<div class="binding__description">
{binding.description || ""}
</div>
{/if}
</li>
{/each}
</ul>
</section>
{/if}
{/each}
{#if filteredHelpers?.length}
<section>
<div class="heading">Helpers</div>
<ul>
{#each filteredHelpers as helper}
<li
on:click={() => {
value = addHBSBinding(value, getCaretPosition(), helper.text)
}}
>
<div class="helper">
<div class="helper__name">{helper.displayText}</div>
<div class="helper__description">
{@html helper.description}
</div>
<pre class="helper__example">{helper.example || ''}</pre>
</div>
</li>
{/each}
</ul>
</section>
{/if}
</div>
</svelte:fragment>
<div class="main">
<TextArea
bind:getCaretPosition
bind:value
placeholder="Add text, or click the objects on the left to add them to the textbox."
/>
{#if !valid}
<p class="syntax-error">
Current Handlebars syntax is invalid, please check the guide
<a href="https://handlebarsjs.com/guide/">here</a>
for more details.
</p>
{/if}
</div>
</DrawerContent>
<style>
.main :global(textarea) {
min-height: 150px !important;
}
.container {
margin: calc(-1 * var(--spacing-xl));
}
.heading {
font-size: var(--font-size-s);
font-weight: 600;
text-transform: uppercase;
color: var(--spectrum-global-color-gray-600);
padding: var(--spacing-xl) 0 var(--spacing-m) 0;
}
section {
padding: 0 var(--spacing-xl) var(--spacing-xl) var(--spacing-xl);
}
section:not(:first-child) {
border-top: var(--border-light);
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
li {
font-size: var(--font-size-s);
padding: var(--spacing-m);
border-radius: 4px;
border: var(--border-light);
transition: background-color 130ms ease-in-out, color 130ms ease-in-out,
border-color 130ms ease-in-out;
}
li:not(:last-of-type) {
margin-bottom: var(--spacing-s);
}
li :global(*) {
transition: color 130ms ease-in-out;
}
li:hover {
color: var(--spectrum-global-color-gray-900);
background-color: var(--spectrum-global-color-gray-50);
border-color: var(--spectrum-global-color-gray-500);
cursor: pointer;
}
li:hover :global(*) {
color: var(--spectrum-global-color-gray-900) !important;
}
.helper {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: var(--spacing-xs);
}
.helper__name {
font-weight: bold;
}
.helper__description,
.helper__description :global(*) {
color: var(--spectrum-global-color-gray-700);
}
.helper__example {
white-space: normal;
margin: 0.5rem 0 0 0;
font-weight: 700;
}
.helper__description :global(p) {
margin: 0;
}
$: enrichedBindings = enrichBindings(bindings)
.syntax-error {
padding-top: var(--spacing-m);
color: var(--red);
font-size: 12px;
}
.syntax-error a {
color: var(--red);
text-decoration: underline;
// Ensure bindings have the correct properties
const enrichBindings = bindings => {
return bindings?.map(binding => ({
...binding,
readableBinding: binding.label || binding.readableBinding,
runtimeBinding: binding.path || binding.runtimeBinding,
}))
}
</script>
.binding__label {
font-weight: 600;
text-transform: capitalize;
}
.binding__description {
color: var(--spectrum-global-color-gray-700);
margin: 0.5rem 0 0 0;
white-space: normal;
}
.binding__type {
font-family: monospace;
background-color: var(--spectrum-global-color-gray-200);
border-radius: var(--border-radius-s);
padding: 2px 4px;
margin-left: 2px;
font-weight: 600;
}
</style>
<BindingPanel
bind:valid
bindings={enrichedBindings}
{value}
{allowJS}
on:change
/>

55
packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte

@ -32,15 +32,13 @@
.component("@budibase/standard-components/screenslot")
.instanceName("Content Placeholder")
.json()
// Messages that can be sent from the iframe preview to the builder
// Budibase events are and initalisation events
const MessageTypes = {
IFRAME_LOADED: "iframe-loaded",
READY: "ready",
ERROR: "error",
BUDIBASE: "type",
KEYDOWN: "keydown"
}
// Construct iframe template
@ -69,7 +67,7 @@
theme: $store.theme,
customTheme: $store.customTheme,
previewDevice: $store.previewDevice,
messagePassing: $store.clientFeatures.messagePassing
messagePassing: $store.clientFeatures.messagePassing,
}
// Saving pages and screens to the DB causes them to have _revs.
@ -111,7 +109,6 @@
loading = false
error = event.error || "An unknown error occurred"
},
[MessageTypes.KEYDOWN]: handleKeydownEvent
}
const messageHandler = handlers[message.data.type] || handleBudibaseEvent
@ -122,16 +119,25 @@
window.addEventListener("message", receiveMessage)
if (!$store.clientFeatures.messagePassing) {
// Legacy - remove in later versions of BB
iframe.contentWindow.addEventListener("ready", () => {
receiveMessage({ data: { type: MessageTypes.READY }})
}, { once: true })
iframe.contentWindow.addEventListener("error", event => {
receiveMessage({ data: { type: MessageTypes.ERROR, error: event.detail }})
}, { once: true })
iframe.contentWindow.addEventListener(
"ready",
() => {
receiveMessage({ data: { type: MessageTypes.READY } })
},
{ once: true }
)
iframe.contentWindow.addEventListener(
"error",
event => {
receiveMessage({
data: { type: MessageTypes.ERROR, error: event.detail },
})
},
{ once: true }
)
// Add listener for events sent by client library in preview
iframe.contentWindow.addEventListener("bb-event", handleBudibaseEvent)
iframe.contentWindow.addEventListener("keydown", handleKeydownEvent)
}
}
})
// Remove all iframe event listeners on component destroy
@ -140,14 +146,20 @@
window.removeEventListener("message", receiveMessage)
if (!$store.clientFeatures.messagePassing) {
// Legacy - remove in later versions of BB
iframe.contentWindow.removeEventListener("bb-event", handleBudibaseEvent)
iframe.contentWindow.removeEventListener("keydown", handleKeydownEvent)
iframe.contentWindow.removeEventListener(
"bb-event",
handleBudibaseEvent
)
}
}
})
const handleBudibaseEvent = event => {
const { type, data } = event.data || event.detail
if (!type) {
return
}
if (type === "select-component" && data.id) {
store.actions.components.select({ _id: data.id })
} else if (type === "update-prop") {
@ -183,19 +195,6 @@
}
}
const handleKeydownEvent = event => {
const { key } = event.data || event
if (
(key === "Delete" || key === "Backspace") &&
selectedComponentId &&
["input", "textarea"].indexOf(
iframe.contentWindow.document.activeElement?.tagName.toLowerCase()
) === -1
) {
confirmDeleteComponent(selectedComponentId)
}
}
const confirmDeleteComponent = componentId => {
idToDelete = componentId
confirmDeleteDialog.show()

34
packages/builder/src/components/design/AppPreview/componentStructure.json

@ -1,10 +1,31 @@
[
"section",
"container",
"dataprovider",
"table",
"repeater",
"button",
{
"name": "Blocks",
"icon": "Article",
"children": [
"tableblock",
"cardsblock",
"repeaterblock"
]
},
{
"name": "Layout",
"icon": "ClassicGridView",
"children": [
"container",
"section"
]
},
{
"name": "Data",
"icon": "Data",
"children": [
"dataprovider",
"repeater",
"table",
"dynamicfilter"
]
},
{
"name": "Form",
"icon": "Form",
@ -51,6 +72,7 @@
"children": [
"heading",
"text",
"button",
"divider",
"image",
"backgroundimage",

5
packages/builder/src/components/design/AppPreview/iframeTemplate.js

@ -84,7 +84,6 @@ export default `
if (window.loadBudibase) {
window.loadBudibase()
document.documentElement.classList.add("loaded")
window.parent.postMessage({ type: "iframe-loaded" })
} else {
throw "The client library couldn't be loaded"
}
@ -94,10 +93,6 @@ export default `
}
window.addEventListener("message", receiveMessage)
window.addEventListener("keydown", evt => {
window.parent.postMessage({ type: "keydown", key: event.key })
})
window.parent.postMessage({ type: "ready" })
</script>
</head>

14
packages/builder/src/components/design/NavigationPanel/ComponentDropdownMenu.svelte

@ -13,6 +13,12 @@
$: noChildrenAllowed = !component || !definition?.hasChildren
$: noPaste = !$store.componentToPaste
// "editable" has been repurposed for inline text editing.
// It remains here for legacy compatibility.
// Future components should define "static": true for indicate they should
// not show a context menu.
$: showMenu = definition?.editable !== false && definition?.static !== true
const moveUpComponent = () => {
const asset = get(currentAsset)
const parent = findComponentParent(asset.props, component._id)
@ -47,7 +53,7 @@
const duplicateComponent = () => {
storeComponentForCopy(false)
pasteComponent("below")
pasteComponent("below", true)
}
const deleteComponent = async () => {
@ -63,13 +69,13 @@
store.actions.components.copy(component, cut)
}
const pasteComponent = mode => {
const pasteComponent = (mode, preserveBindings = false) => {
// lives in store - also used by drag drop
store.actions.components.paste(component, mode)
store.actions.components.paste(component, mode, preserveBindings)
}
</script>
{#if definition?.editable !== false}
{#if showMenu}
<ActionMenu>
<div slot="control" class="icon">
<Icon size="S" hoverable name="MoreSmallList" />

17
packages/builder/src/components/design/NavigationPanel/FrontendNavigatePane.svelte

@ -10,10 +10,11 @@
import { roles } from "stores/backend"
import ComponentNavigationTree from "components/design/NavigationPanel/ComponentNavigationTree/index.svelte"
import Layout from "components/design/NavigationPanel/Layout.svelte"
import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte"
import NewLayoutModal from "components/design/NavigationPanel/NewLayoutModal.svelte"
import { Icon, Modal, Select, Search, Tabs, Tab } from "@budibase/bbui"
export let showModal
const tabs = [
{
title: "Screens",
@ -24,8 +25,7 @@
key: "layout",
},
]
let modal
let newLayoutModal
$: selected = tabs.find(t => t.key === $params.assetType)?.title || "Screens"
const navigate = ({ detail }) => {
@ -85,9 +85,6 @@
<div class="nav-items-container">
<ComponentNavigationTree />
</div>
<Modal bind:this={modal}>
<NewScreenModal />
</Modal>
</div>
</Tab>
<Tab title="Layouts">
@ -95,14 +92,18 @@
{#each $store.layouts as layout, idx (layout._id)}
<Layout {layout} border={idx > 0} />
{/each}
<Modal bind:this={modal}>
<Modal bind:this={newLayoutModal}>
<NewLayoutModal />
</Modal>
</div>
</Tab>
</Tabs>
<div class="add-button">
<Icon hoverable name="AddCircle" on:click={modal.show} />
<Icon
hoverable
name="AddCircle"
on:click={selected === "Layouts" ? newLayoutModal.show() : showModal()}
/>
</div>
</div>

93
packages/builder/src/components/design/NavigationPanel/NavigationSelectionModal.svelte

@ -0,0 +1,93 @@
<script>
import { ModalContent, Body, Detail } from "@budibase/bbui"
export let selectedScreens
export let chooseModal
export let save
let selectedNav
let createdScreens = []
$: blankSelected = selectedScreens.length === 1
</script>
<ModalContent
title="Select navigation"
cancelText="Back"
onCancel={() => (blankSelected ? chooseModal(1) : chooseModal(0))}
size="M"
onConfirm={() => {
save(createdScreens)
}}
disabled={!selectedNav}
>
<Body size="S"
>Please select your preferred layout for the new application:</Body
>
<div class="wrapper">
<div
data-cy="left-nav"
on:click={() => (selectedNav = "Left")}
class:unselected={selectedNav && selectedNav !== "Left"}
>
<div class="box">
<div class="side-nav" />
</div>
<div><Detail>Side Nav</Detail></div>
</div>
<div
on:click={() => (selectedNav = "Top")}
class:unselected={selectedNav && selectedNav !== "Top"}
>
<div class="box">
<div class="top-nav" />
</div>
<div><Detail>Top Nav</Detail></div>
</div>
<div
on:click={() => (selectedNav = "None")}
class:unselected={selectedNav && selectedNav !== "None"}
>
<div class="box" />
<div><Detail>No Nav</Detail></div>
</div>
</div>
</ModalContent>
<style>
.side-nav {
float: left;
background: #d3d3d3 0% 0% no-repeat padding-box;
border-radius: 2px 0px 0px 2px;
height: 100%;
width: 10%;
}
.top-nav {
background: #d3d3d3 0% 0% no-repeat padding-box;
vertical-align: top;
width: 100%;
height: 15%;
}
.box {
display: inline-block;
background: #eaeaea 0% 0% no-repeat padding-box;
border: 1px solid #d3d3d3;
border-radius: 2px;
opacity: 1;
width: 120px;
height: 70px;
margin-right: 20px;
}
.wrapper {
display: flex;
padding-top: 4%;
list-style-type: none;
margin: 0;
padding: 0;
margin-right: 5%;
}
.unselected {
opacity: 0.3;
}
</style>

253
packages/builder/src/components/design/NavigationPanel/NewScreenModal.svelte

@ -1,117 +1,178 @@
<script>
import { store, allScreens, selectedAccessRole } from "builderStore"
import { store } from "builderStore"
import { tables } from "stores/backend"
import { roles } from "stores/backend"
import { Input, Select, ModalContent, Toggle } from "@budibase/bbui"
import {
ModalContent,
Body,
Detail,
Layout,
Icon,
ProgressCircle,
} from "@budibase/bbui"
import getTemplates from "builderStore/store/screenTemplates"
import analytics, { Events } from "analytics"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
const CONTAINER = "@budibase/standard-components/container"
let name = ""
let routeError
let baseComponent = CONTAINER
let templateIndex
let draftScreen
let createLink = true
let roleId = $selectedAccessRole || "BASIC"
$: templates = getTemplates($store, $tables.list)
$: route = !route && $allScreens.length === 0 ? "*" : route
$: {
if (templates && templateIndex === undefined) {
templateIndex = 0
templateChanged(0)
}
import { onDestroy } from "svelte"
import { createEventDispatcher } from "svelte"
export let chooseModal
export let save
export let showProgressCircle = false
let selectedScreens = []
const blankScreen = "createFromScratch"
const dispatch = createEventDispatcher()
function setScreens() {
dispatch("save", {
screens: selectedScreens,
})
}
const templateChanged = newTemplateIndex => {
if (newTemplateIndex === undefined) return
draftScreen = templates[newTemplateIndex].create()
if (draftScreen.props._instanceName) {
name = draftScreen.props._instanceName
}
$: blankSelected = selectedScreens?.length === 1
$: autoSelected = selectedScreens?.length > 0 && !blankSelected
if (draftScreen.props._component) {
baseComponent = draftScreen.props._component
}
let templates = getTemplates($store, $tables.list)
if (draftScreen.routing) {
route = draftScreen.routing.route
const confirm = async () => {
if (autoSelected) {
setScreens()
await save()
} else {
setScreens()
chooseModal(1)
}
}
const save = async () => {
if (!route) {
routeError = "URL is required"
const toggleScreenSelection = table => {
if (selectedScreens.find(s => s.table === table.name)) {
selectedScreens = selectedScreens.filter(
screen => screen.table !== table.name
)
} else {
if (routeExists(route, roleId)) {
routeError = "This URL is already taken for this access role"
} else {
routeError = ""
}
let partialTemplates = getTemplates($store, $tables.list).filter(
template => template.table === table.name
)
selectedScreens = [...partialTemplates, ...selectedScreens]
}
}
if (routeError) return false
onDestroy(() => {
selectedScreens = []
})
</script>
draftScreen.props._instanceName = name
draftScreen.props._component = baseComponent
draftScreen.routing = { route, roleId }
<div>
<ModalContent
title="Add screens"
confirmText="Add Screens"
cancelText="Cancel"
onConfirm={() => confirm()}
disabled={!selectedScreens.length}
size="L"
>
<Body size="XS"
>Please select the screens you would like to add to your application.
Autogenerated screens come with CRUD functionality.</Body
>
await store.actions.screens.create(draftScreen)
if (createLink) {
await store.actions.components.links.save(route, name)
}
await store.actions.routing.fetch()
<Layout noPadding gap="S">
<Detail size="S">Blank screen</Detail>
<div
class="item"
class:selected={selectedScreens.find(x => x.id.includes(blankScreen))}
on:click={() =>
toggleScreenSelection(templates.find(t => t.id === blankScreen))}
class:disabled={autoSelected}
>
<div data-cy="blank-screen" class="content">
<div class="text">Blank</div>
</div>
<div
style="color: var(--spectrum-global-color-green-600); float: right"
>
{#if selectedScreens.find(x => x.id === blankScreen)}
<div class="checkmark-spacing">
<Icon size="S" name="CheckmarkCircleOutline" />
</div>
{/if}
</div>
</div>
{#if $tables.list.filter(table => table._id !== "ta_users").length > 0}
<Detail size="S">Autogenerated Screens</Detail>
if (templateIndex !== undefined) {
const template = templates[templateIndex]
analytics.captureEvent(Events.SCREEN.CREATED, {
template: template.id || template.name,
})
}
{#each $tables.list.filter(table => table._id !== "ta_users") as table}
<div
class:disabled={blankSelected}
class:selected={selectedScreens.find(x => x.table === table.name)}
on:click={() => toggleScreenSelection(table)}
class="item"
>
<div class="content">
<div class="text">{table.name}</div>
</div>
<div
style="color: var(--spectrum-global-color-green-600); float: right"
>
{#if selectedScreens.find(x => x.table === table.name)}
<div class="checkmark-spacing">
<Icon size="S" name="CheckmarkCircleOutline" />
</div>
{/if}
</div>
</div>
{/each}
{/if}
</Layout>
<div slot="footer">
{#if showProgressCircle}
<div class="footer-progress"><ProgressCircle size="S" /></div>
{/if}
</div>
</ModalContent>
</div>
<style>
.disabled {
opacity: 0.3;
pointer-events: none;
}
.checkmark-spacing {
margin-right: var(--spacing-m);
}
const routeExists = (route, roleId) => {
return $allScreens.some(
screen =>
screen.routing.route.toLowerCase() === route.toLowerCase() &&
screen.routing.roleId === roleId
)
.content {
letter-spacing: 0px;
}
const routeChanged = event => {
if (!event.detail.startsWith("/")) {
route = "/" + event.detail
}
route = sanitizeUrl(route)
.footer-progress {
margin-top: var(--spacing-s);
}
</script>
<ModalContent title="New Screen" confirmText="Create Screen" onConfirm={save}>
<Select
label="Choose a Template"
bind:value={templateIndex}
on:change={ev => templateChanged(ev.detail)}
options={templates}
placeholder={null}
getOptionLabel={x => x.name}
getOptionValue={(x, idx) => idx}
/>
<Input label="Name" bind:value={name} />
<Input
label="Url"
error={routeError}
bind:value={route}
on:change={routeChanged}
/>
<Select
label="Access"
bind:value={roleId}
options={$roles}
getOptionLabel={x => x.name}
getOptionValue={x => x._id}
/>
<Toggle text="Create link in navigation bar" bind:value={createLink} />
</ModalContent>
.text {
font-weight: 600;
margin-left: var(--spacing-m);
font-size: 14px;
text-transform: capitalize;
}
.item {
cursor: pointer;
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
padding: var(--spectrum-alias-item-padding-s);
background: var(--spectrum-alias-background-color-primary);
transition: 0.3s all;
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px;
box-sizing: border-box;
border-width: 1px;
display: flex;
justify-content: space-between;
align-items: center;
height: 60px;
}
.item:hover,
.selected {
background: var(--spectrum-alias-background-color-tertiary);
}
</style>

70
packages/builder/src/components/design/NavigationPanel/ScreenDetailsModal.svelte

@ -0,0 +1,70 @@
<script>
import { ModalContent, Input, ProgressCircle } from "@budibase/bbui"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import { selectedAccessRole, allScreens } from "builderStore"
import { onDestroy } from "svelte"
export let screenName
export let url
export let chooseModal
export let save
export let showProgressCircle = false
let routeError
let roleId = $selectedAccessRole || "BASIC"
const routeChanged = event => {
if (!event.detail.startsWith("/")) {
url = "/" + event.detail
}
url = sanitizeUrl(url)
if (routeExists(url, roleId)) {
routeError = "This URL is already taken for this access role"
} else {
routeError = ""
}
}
const routeExists = (url, roleId) => {
return $allScreens.some(
screen =>
screen.routing.route.toLowerCase() === url.toLowerCase() &&
screen.routing.roleId === roleId
)
}
onDestroy(() => {
screenName = ""
url = ""
})
</script>
<ModalContent
size="M"
title={"Enter details"}
confirmText={"Continue"}
onCancel={() => chooseModal(0)}
onConfirm={() => save()}
cancelText={"Back"}
disabled={!screenName || !url || routeError}
>
<Input label="Name" bind:value={screenName} />
<Input
label="URL"
error={routeError}
bind:value={url}
on:change={routeChanged}
/>
<div slot="footer">
{#if showProgressCircle}
<div class="footer-progress"><ProgressCircle size="S" /></div>
{/if}
</div>
</ModalContent>
<style>
.footer-progress {
margin-top: var(--spacing-s);
}
</style>

133
packages/builder/src/components/design/NavigationPanel/ScreenWizard.svelte

@ -0,0 +1,133 @@
<script>
import ScreenDetailsModal from "components/design/NavigationPanel/ScreenDetailsModal.svelte"
import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import { Modal } from "@budibase/bbui"
import { store, selectedAccessRole, allScreens } from "builderStore"
import analytics, { Events } from "analytics"
let newScreenModal
let navigationSelectionModal
let screenDetailsModal
let screenName = ""
let url = ""
let selectedScreens = []
let roleId = $selectedAccessRole || "BASIC"
let showProgressCircle = false
let routeError
let createdScreens = []
const createScreens = async () => {
for (let screen of selectedScreens) {
let test = screen.create()
createdScreens.push(test)
analytics.captureEvent(Events.SCREEN.CREATED, {
template: screen.id || screen.name,
})
}
}
const save = async () => {
showProgressCircle = true
await createScreens()
for (let screen of createdScreens) {
await saveScreens(screen)
}
await store.actions.routing.fetch()
selectedScreens = []
createdScreens = []
screenName = ""
url = ""
showProgressCircle = false
}
const saveScreens = async draftScreen => {
let existingScreenCount = $store.screens.filter(
s => s.props._instanceName == draftScreen.props._instanceName
).length
if (existingScreenCount > 0) {
let oldUrlArr = draftScreen.routing.route.split("/")
oldUrlArr[1] = `${oldUrlArr[1]}-${existingScreenCount + 1}`
draftScreen.routing.route = oldUrlArr.join("/")
}
let route = url ? sanitizeUrl(`${url}`) : draftScreen.routing.route
if (draftScreen) {
if (!route) {
routeError = "URL is required"
} else {
if (routeExists(route, roleId)) {
routeError = "This URL is already taken for this access role"
} else {
routeError = ""
}
}
if (routeError) return false
if (screenName) {
draftScreen.props._instanceName = screenName
}
draftScreen.routing.route = route
await store.actions.screens.create(draftScreen)
if (draftScreen.props._instanceName.endsWith("List")) {
await store.actions.components.links.save(
draftScreen.routing.route,
draftScreen.routing.route.split("/")[1]
)
}
}
}
const routeExists = (route, roleId) => {
return $allScreens.some(
screen =>
screen.routing.route.toLowerCase() === route.toLowerCase() &&
screen.routing.roleId === roleId
)
}
export const showModal = () => {
newScreenModal.show()
}
const setScreens = evt => {
selectedScreens = evt.detail.screens
}
const chooseModal = index => {
/*
0 = newScreenModal
1 = screenDetailsModal
2 = navigationSelectionModal
*/
if (index === 0) {
newScreenModal.show()
} else if (index === 1) {
screenDetailsModal.show()
} else if (index === 2) {
navigationSelectionModal.show()
}
}
</script>
<Modal bind:this={newScreenModal}>
<NewScreenModal
on:save={setScreens}
{showProgressCircle}
{save}
{chooseModal}
/>
</Modal>
<Modal bind:this={screenDetailsModal}>
<ScreenDetailsModal
bind:screenName
bind:url
{showProgressCircle}
{save}
{chooseModal}
/>
</Modal>

66
packages/builder/src/components/design/PropertiesPanel/ComponentSettingsSection.svelte

@ -12,6 +12,7 @@
export let componentInstance
export let assetInstance
export let bindings
export let componentBindings
const layoutDefinition = []
const screenDefinition = [
@ -21,10 +22,24 @@
{ key: "layoutId", label: "Layout", control: LayoutSelect },
]
$: settings = componentDefinition?.settings ?? []
$: sections = getSections(componentDefinition)
$: isLayout = assetInstance && assetInstance.favicon
$: assetDefinition = isLayout ? layoutDefinition : screenDefinition
const getSections = definition => {
const settings = definition?.settings ?? []
const generalSettings = settings.filter(setting => !setting.section)
const customSections = settings.filter(setting => setting.section)
return [
{
name: "General",
info: componentDefinition?.info,
settings: generalSettings,
},
...(customSections || []),
]
}
const updateProp = store.actions.components.updateProp
const canRenderControl = setting => {
@ -59,19 +74,18 @@
}
</script>
<DetailSummary name="General" collapsible={false}>
{#if !componentInstance._component.endsWith("/layout")}
<PropertyControl
bindable={false}
control={Input}
label="Name"
key="_instanceName"
value={componentInstance._instanceName}
onChange={val => updateProp("_instanceName", val)}
/>
{/if}
{#if settings && settings.length > 0}
{#each settings as setting (setting.key)}
{#each sections as section, idx (section.name)}
<DetailSummary name={section.name} collapsible={false}>
{#if idx === 0 && !componentInstance._component.endsWith("/layout")}
<PropertyControl
control={Input}
label="Name"
key="_instanceName"
value={componentInstance._instanceName}
onChange={val => updateProp("_instanceName", val)}
/>
{/if}
{#each section.settings as setting (setting.key)}
{#if canRenderControl(setting)}
<PropertyControl
type={setting.type}
@ -80,7 +94,7 @@
key={setting.key}
value={componentInstance[setting.key] ??
componentInstance[setting.key]?.defaultValue}
{componentInstance}
nested={setting.nested}
onChange={val => updateProp(setting.key, val)}
props={{
options: setting.options || [],
@ -89,20 +103,22 @@
max: setting.max || null,
}}
{bindings}
{componentBindings}
{componentInstance}
{componentDefinition}
/>
{/if}
{/each}
{/if}
{#if componentDefinition?.component?.endsWith("/fieldgroup")}
<ResetFieldsButton {componentInstance} />
{/if}
{#if componentDefinition?.info}
<div class="text">
{@html componentDefinition?.info}
</div>
{/if}
</DetailSummary>
{#if idx === 0 && componentDefinition?.component?.endsWith("/fieldgroup")}
<ResetFieldsButton {componentInstance} />
{/if}
{#if section?.info}
<div class="text">
{@html section.info}
</div>
{/if}
</DetailSummary>
{/each}
<style>
.text {

10
packages/builder/src/components/design/PropertiesPanel/PropertiesPanel.svelte

@ -6,13 +6,20 @@
import DesignSection from "./DesignSection.svelte"
import CustomStylesSection from "./CustomStylesSection.svelte"
import ConditionalUISection from "./ConditionalUISection.svelte"
import { getBindableProperties } from "builderStore/dataBinding"
import {
getBindableProperties,
getComponentBindableProperties,
} from "builderStore/dataBinding"
$: componentInstance = $selectedComponent
$: componentDefinition = store.actions.components.getDefinition(
$selectedComponent?._component
)
$: bindings = getBindableProperties($currentAsset, $store.selectedComponentId)
$: componentBindings = getComponentBindableProperties(
$currentAsset,
$store.selectedComponentId
)
</script>
<Tabs selected="Settings" noPadding>
@ -28,6 +35,7 @@
{componentInstance}
{componentDefinition}
{bindings}
{componentBindings}
/>
<DesignSection {componentInstance} {componentDefinition} {bindings} />
<CustomStylesSection

26
packages/builder/src/components/design/PropertiesPanel/PropertyControls/ConditionalUIDrawer.svelte

@ -13,9 +13,10 @@
import { generate } from "shortid"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { OperatorOptions, getValidOperatorsForType } from "constants/lucene"
import { selectedComponent, store } from "builderStore"
import { selectedComponent } from "builderStore"
import { getComponentForSettingType } from "./componentSettings"
import PropertyControl from "./PropertyControl.svelte"
import { getComponentSettings } from "builderStore/storeUtils"
export let conditions = []
export let bindings = []
@ -55,15 +56,11 @@
]
let dragDisabled = true
$: definition = store.actions.components.getDefinition(
$selectedComponent?._component
)
$: settings = (definition?.settings ?? []).map(setting => {
return {
label: setting.label,
value: setting.key,
}
})
$: settings = getComponentSettings($selectedComponent?._component)
$: settingOptions = settings.map(setting => ({
label: setting.label,
value: setting.key,
}))
$: conditions.forEach(link => {
if (!link.id) {
link.id = generate()
@ -71,9 +68,7 @@
})
const getSettingDefinition = key => {
return definition?.settings?.find(setting => {
return setting.key === key
})
return settings.find(setting => setting.key === key)
}
const getComponentForSetting = key => {
@ -175,7 +170,10 @@
bind:value={condition.action}
/>
{#if condition.action === "update"}
<Select options={settings} bind:value={condition.setting} />
<Select
options={settingOptions}
bind:value={condition.setting}
/>
<div>TO</div>
{#if getSettingDefinition(condition.setting)}
<PropertyControl

5
packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataProviderSelect.svelte

@ -11,10 +11,7 @@
const getValue = component => `{{ literal ${makePropSafe(component._id)} }}`
$: path = findComponentPath($currentAsset.props, $store.selectedComponentId)
$: providers = path.filter(
component =>
component._component === "@budibase/standard-components/dataprovider"
)
$: providers = path.filter(c => c._component?.endsWith("/dataprovider"))
// Set initial value to closest data provider
onMount(() => {

181
packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataSourceSelect.svelte

@ -20,16 +20,18 @@
import { notifications } from "@budibase/bbui"
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
import IntegrationQueryEditor from "components/integration/index.svelte"
const dispatch = createEventDispatcher()
let anchorRight, dropdownRight
let drawer
import { makePropSafe as safe } from "@budibase/string-templates"
export let value = {}
export let otherSources
export let showAllQueries
export let bindings = []
const dispatch = createEventDispatcher()
const arrayTypes = ["attachment", "array"]
let anchorRight, dropdownRight
let drawer
$: text = value?.label ?? "Choose an option"
$: tables = $tablesStore.list.map(m => ({
label: m.name,
@ -54,8 +56,6 @@
name: query.name,
tableId: query._id,
...query,
schema: query.schema,
parameters: query.parameters,
type: "query",
}))
$: dataProviders = getDataProviderComponents(
@ -65,29 +65,40 @@
label: provider._instanceName,
name: provider._instanceName,
providerId: provider._id,
value: `{{ literal [${provider._id}] }}`,
value: `{{ literal ${safe(provider._id)} }}`,
type: "provider",
schema: provider.schema,
}))
$: queryBindableProperties = bindings.map(property => ({
...property,
category: property.type === "instance" ? "Component" : "Table",
label: property.readableBinding,
path: property.readableBinding,
}))
$: links = bindings
.filter(x => x.fieldSchema?.type === "link")
.map(property => {
.map(binding => {
const { providerId, readableBinding, fieldSchema } = binding || {}
const { name, tableId } = fieldSchema || {}
const safeProviderId = safe(providerId)
return {
providerId: property.providerId,
label: property.readableBinding,
fieldName: property.fieldSchema.name,
tableId: property.fieldSchema.tableId,
providerId,
label: readableBinding,
fieldName: name,
tableId,
type: "link",
// These properties will be enriched by the client library and provide
// details of the parent row of the relationship field, from context
rowId: `{{ ${property.providerId}._id }}`,
rowTableId: `{{ ${property.providerId}.tableId }}`,
rowId: `{{ ${safeProviderId}.${safe("_id")} }}`,
rowTableId: `{{ ${safeProviderId}.${safe("tableId")} }}`,
}
})
$: fields = bindings
.filter(x => arrayTypes.includes(x.fieldSchema?.type))
.map(binding => {
const { providerId, readableBinding, runtimeBinding } = binding
const { name, type, tableId } = binding.fieldSchema
return {
providerId,
label: readableBinding,
fieldName: name,
fieldType: type,
tableId,
type: "field",
value: `{{ literal ${runtimeBinding} }}`,
}
})
@ -102,6 +113,14 @@
).source
return $integrations[source].query[query.queryVerb]
}
const getQueryParams = query => {
return $queriesStore.list.find(q => q._id === query?._id)?.parameters || []
}
const getQueryDatasource = query => {
return $datasources.list.find(ds => ds._id === query?.datasourceId)
}
</script>
<div class="container" bind:this={anchorRight}>
@ -127,11 +146,10 @@
</Button>
<DrawerContent slot="body">
<Layout noPadding>
{#if value.parameters.length > 0}
{#if getQueryParams(value._id).length > 0}
<ParameterBuilder
bind:customParams={value.queryParams}
parameters={queries.find(query => query._id === value._id)
.parameters}
parameters={getQueryParams(value)}
{bindings}
/>
{/if}
@ -139,9 +157,7 @@
height={200}
query={value}
schema={fetchQueryDefinition(value)}
datasource={$datasources.list.find(
ds => ds._id === value.datasourceId
)}
datasource={getQueryDatasource(value)}
editable={false}
/>
</Layout>
@ -159,52 +175,71 @@
<li on:click={() => handleSelected(table)}>{table.label}</li>
{/each}
</ul>
<Divider size="S" />
<div class="title">
<Heading size="XS">Views</Heading>
</div>
<ul>
{#each views as view}
<li on:click={() => handleSelected(view)}>{view.label}</li>
{/each}
</ul>
<Divider size="S" />
<div class="title">
<Heading size="XS">Relationships</Heading>
</div>
<ul>
{#each links as link}
<li on:click={() => handleSelected(link)}>{link.label}</li>
{/each}
</ul>
<Divider size="S" />
<div class="title">
<Heading size="XS">Queries</Heading>
</div>
<ul>
{#each queries as query}
<li
class:selected={value === query}
on:click={() => handleSelected(query)}
>
{query.label}
</li>
{/each}
</ul>
<Divider size="S" />
<div class="title">
<Heading size="XS">Data Providers</Heading>
</div>
<ul>
{#each dataProviders as provider}
<li
class:selected={value === provider}
on:click={() => handleSelected(provider)}
>
{provider.label}
</li>
{/each}
</ul>
{#if views?.length}
<Divider size="S" />
<div class="title">
<Heading size="XS">Views</Heading>
</div>
<ul>
{#each views as view}
<li on:click={() => handleSelected(view)}>{view.label}</li>
{/each}
</ul>
{/if}
{#if queries?.length}
<Divider size="S" />
<div class="title">
<Heading size="XS">Queries</Heading>
</div>
<ul>
{#each queries as query}
<li
class:selected={value === query}
on:click={() => handleSelected(query)}
>
{query.label}
</li>
{/each}
</ul>
{/if}
{#if links?.length}
<Divider size="S" />
<div class="title">
<Heading size="XS">Relationships</Heading>
</div>
<ul>
{#each links as link}
<li on:click={() => handleSelected(link)}>{link.label}</li>
{/each}
</ul>
{/if}
{#if fields?.length}
<Divider size="S" />
<div class="title">
<Heading size="XS">Fields</Heading>
</div>
<ul>
{#each fields as field}
<li on:click={() => handleSelected(field)}>{field.label}</li>
{/each}
</ul>
{/if}
{#if dataProviders?.length}
<Divider size="S" />
<div class="title">
<Heading size="XS">Data Providers</Heading>
</div>
<ul>
{#each dataProviders as provider}
<li
class:selected={value === provider}
on:click={() => handleSelected(provider)}
>
{provider.label}
</li>
{/each}
</ul>
{/if}
{#if otherSources?.length}
<Divider size="S" />
<div class="title">

4
packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte

@ -11,14 +11,14 @@
Select,
} from "@budibase/bbui"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import { generate } from "shortid"
import { getValidOperatorsForType, OperatorOptions } from "constants/lucene"
export let schemaFields
export let filters = []
export let bindings = []
export let panel = BindingPanel
export let panel = ClientBindingPanel
export let allowBindings = true
const BannedTypes = ["link", "attachment", "formula"]

15
packages/builder/src/components/design/PropertiesPanel/PropertyControls/Input.svelte

@ -1,15 +0,0 @@
<script>
import { Input } from "@budibase/bbui"
import { isJSBinding } from "@budibase/string-templates"
export let value
$: isJS = isJSBinding(value)
</script>
<Input
{...$$props}
value={isJS ? "(JavaScript function)" : value}
readonly={isJS}
on:change
/>

6
packages/builder/src/components/design/PropertiesPanel/PropertyControls/NavigationEditor/NavigationEditor.svelte

@ -2,13 +2,15 @@
import { Button, ActionButton, Drawer } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import NavigationDrawer from "./NavigationDrawer.svelte"
import { cloneDeep } from "lodash/fp"
export let value = []
let drawer
let links = cloneDeep(value)
const dispatch = createEventDispatcher()
const save = () => {
dispatch("change", value)
dispatch("change", links)
drawer.hide()
}
</script>
@ -19,5 +21,5 @@
Configure the links in your navigation bar.
</svelte:fragment>
<Button cta slot="buttons" on:click={save}>Save</Button>
<NavigationDrawer slot="body" bind:links={value} />
<NavigationDrawer slot="body" bind:links />
</Drawer>

84
packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte

@ -1,14 +1,11 @@
<script>
import { Button, Icon, Drawer, Label } from "@budibase/bbui"
import { Label } from "@budibase/bbui"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
import { capitalise } from "helpers"
export let label = ""
export let bindable = true
export let componentInstance = {}
export let control = null
export let key = ""
@ -17,18 +14,19 @@
export let props = {}
export let onChange = () => {}
export let bindings = []
export let componentBindings = []
export let nested = false
let bindingDrawer
let anchor
let valid
$: safeValue = getSafeValue(value, props.defaultValue, bindings)
$: allBindings = getAllBindings(bindings, componentBindings, nested)
$: safeValue = getSafeValue(value, props.defaultValue, allBindings)
$: tempValue = safeValue
$: replaceBindings = val => readableToRuntimeBinding(bindings, val)
$: replaceBindings = val => readableToRuntimeBinding(allBindings, val)
const handleClose = () => {
handleChange(tempValue)
bindingDrawer.hide()
const getAllBindings = (bindings, componentBindings, nested) => {
if (!nested) {
return bindings
}
return [...(componentBindings || []), ...(bindings || [])]
}
// Handle a value change of any type
@ -64,7 +62,7 @@
}
</script>
<div class="property-control" bind:this={anchor} data-cy={`setting-${key}`}>
<div class="property-control" data-cy={`setting-${key}`}>
{#if type !== "boolean" && label}
<div class="label">
<Label>{label}</Label>
@ -78,37 +76,12 @@
updateOnChange={false}
on:change={handleChange}
onChange={handleChange}
{bindings}
bindings={allBindings}
name={key}
text={label}
{type}
{...props}
/>
{#if bindable && !key.startsWith("_") && type === "text"}
<div
class="icon"
data-cy={`${key}-binding-button`}
on:click={bindingDrawer.show}
>
<Icon size="S" name="FlashOn" />
</div>
<Drawer bind:this={bindingDrawer} title={capitalise(key)}>
<svelte:fragment slot="description">
Add the objects on the left to enrich your text.
</svelte:fragment>
<Button cta slot="buttons" disabled={!valid} on:click={handleClose}>
Save
</Button>
<BindingPanel
slot="body"
bind:valid
value={safeValue}
on:change={e => (tempValue = e.detail)}
bindableProperties={bindings}
allowJS
/>
</Drawer>
{/if}
</div>
</div>
@ -120,41 +93,10 @@
justify-content: flex-start;
align-items: stretch;
}
.label {
text-transform: capitalize;
padding-bottom: var(--spectrum-global-dimension-size-65);
}
.control {
position: relative;
}
.icon {
right: 1px;
top: 1px;
bottom: 1px;
position: absolute;
justify-content: center;
align-items: center;
display: flex;
flex-direction: row;
box-sizing: border-box;
border-left: 1px solid var(--spectrum-alias-border-color);
border-top-right-radius: var(--spectrum-alias-border-radius-regular);
border-bottom-right-radius: var(--spectrum-alias-border-radius-regular);
width: 31px;
color: var(--spectrum-alias-text-color);
background-color: var(--spectrum-global-color-gray-75);
transition: background-color
var(--spectrum-global-animation-duration-100, 130ms),
box-shadow var(--spectrum-global-animation-duration-100, 130ms),
border-color var(--spectrum-global-animation-duration-100, 130ms);
}
.icon:hover {
cursor: pointer;
color: var(--spectrum-alias-text-color-hover);
background-color: var(--spectrum-global-color-gray-50);
border-color: var(--spectrum-alias-border-color-hover);
}
</style>

8
packages/builder/src/components/design/PropertiesPanel/PropertyControls/URLSelect.svelte

@ -10,4 +10,10 @@
.filter(x => x != null)
</script>
<DrawerBindableCombobox {value} {bindings} on:change options={urlOptions} />
<DrawerBindableCombobox
{value}
{bindings}
on:change
options={urlOptions}
appendBindingsAsOptions={false}
/>

4
packages/builder/src/components/design/PropertiesPanel/PropertyControls/componentSettings.js

@ -15,10 +15,10 @@ import URLSelect from "./URLSelect.svelte"
import OptionsEditor from "./OptionsEditor/OptionsEditor.svelte"
import FormFieldSelect from "./FormFieldSelect.svelte"
import ValidationEditor from "./ValidationEditor/ValidationEditor.svelte"
import Input from "./Input.svelte"
import DrawerBindableCombobox from "components/common/bindings/DrawerBindableCombobox.svelte"
const componentMap = {
text: Input,
text: DrawerBindableCombobox,
select: Select,
dataSource: DataSourceSelect,
dataProvider: DataProviderSelect,

1
packages/builder/src/components/design/PropertiesPanel/ScreenSettingsSection.svelte

@ -54,7 +54,6 @@
<DetailSummary name="Screen" collapsible={false}>
{#each screenSettings as def (`${componentInstance._id}-${def.key}`)}
<PropertyControl
bindable={false}
control={def.control}
label={def.label}
key={def.key}

1
packages/builder/src/components/design/PropertiesPanel/StyleSection.svelte

@ -30,7 +30,6 @@
{#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)}
<div style="grid-column: {prop.column || 'auto'}">
<PropertyControl
bindable={false}
label={`${prop.label}${hasPropChanged(style, prop) ? " *" : ""}`}
control={prop.control}
key={prop.key}

8
packages/builder/src/components/design/PropertiesPanel/componentStyles.js

@ -9,7 +9,6 @@ export const margin = {
label: "Top",
key: "margin-top",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -30,7 +29,6 @@ export const margin = {
label: "Right",
key: "margin-right",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -51,7 +49,6 @@ export const margin = {
label: "Bottom",
key: "margin-bottom",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -72,7 +69,6 @@ export const margin = {
label: "Left",
key: "margin-left",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -100,7 +96,6 @@ export const padding = {
label: "Top",
key: "padding-top",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -121,7 +116,6 @@ export const padding = {
label: "Right",
key: "padding-right",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -142,7 +136,6 @@ export const padding = {
label: "Bottom",
key: "padding-bottom",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -163,7 +156,6 @@ export const padding = {
label: "Left",
key: "padding-left",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },

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

Loading…
Cancel
Save