Merge pull request #3597 from Budibase/develop

Develop -> Master
This commit is contained in:
Martin McKeaveney 2021-11-30 14:29:57 +01:00 committed by GitHub
commit 3991c4b8d0
280 changed files with 20304 additions and 9700 deletions

View File

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

View File

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

View File

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

View File

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

1
.gitignore vendored
View File

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

View File

@ -54,17 +54,51 @@
<br /><br /> <br /><br />
## ✨ Features ## ✨ 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. ### 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 />
- **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).
- **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 data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/data_n1tlhf.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). ### Design and build apps with powerful pre-made components
- **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. 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 />
### 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 /> <br /><br /><br />

View File

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

View File

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

View File

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

View File

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

View File

@ -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": "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: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: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", "release:helm": "./scripts/release_helm_chart.sh",
"env:multi:enable": "lerna run env:multi:enable", "env:multi:enable": "lerna run env:multi:enable",
"env:multi:disable": "lerna run env:multi:disable", "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: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:cloud": "yarn env:selfhost:disable && yarn env:multi:enable && yarn env:account:disable",
"mode:account": "yarn mode:cloud && yarn env:account:enable", "mode:account": "yarn mode:cloud && yarn env:account:enable",
"security:audit": "node scripts/audit.js",
"postinstall": "husky install" "postinstall": "husky install"
} }
} }

View File

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

View File

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

85
packages/auth/src/cache/appMetadata.js vendored Normal file
View File

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

View File

@ -1,11 +1,14 @@
const { newid } = require("../hashing") const { newid } = require("../hashing")
const Replication = require("./Replication") const Replication = require("./Replication")
const { DEFAULT_TENANT_ID } = require("../constants") const { DEFAULT_TENANT_ID, Configs } = require("../constants")
const env = require("../environment") const env = require("../environment")
const { StaticDatabases, SEPARATOR, DocumentTypes } = require("./constants") const { StaticDatabases, SEPARATOR, DocumentTypes } = require("./constants")
const { getTenantId, getTenantIDFromAppID } = require("../tenancy") const { getTenantId, getTenantIDFromAppID } = require("../tenancy")
const fetch = require("node-fetch") const fetch = require("node-fetch")
const { getCouch } = require("./index") const { getCouch } = require("./index")
const { getAppMetadata } = require("../cache/appMetadata")
const NO_APP_ERROR = "No app provided"
const UNICODE_MAX = "\ufff0" const UNICODE_MAX = "\ufff0"
@ -45,14 +48,23 @@ function getDocParams(docType, docId = null, otherProps = {}) {
} }
exports.isDevAppID = appId => { exports.isDevAppID = appId => {
if (!appId) {
throw NO_APP_ERROR
}
return appId.startsWith(exports.APP_DEV_PREFIX) return appId.startsWith(exports.APP_DEV_PREFIX)
} }
exports.isProdAppID = appId => { exports.isProdAppID = appId => {
if (!appId) {
throw NO_APP_ERROR
}
return appId.startsWith(exports.APP_PREFIX) && !exports.isDevAppID(appId) return appId.startsWith(exports.APP_PREFIX) && !exports.isDevAppID(appId)
} }
function isDevApp(app) { function isDevApp(app) {
if (!app) {
throw NO_APP_ERROR
}
return exports.isDevAppID(app.appId) return exports.isDevAppID(app.appId)
} }
@ -152,6 +164,17 @@ exports.getDeployedAppID = appId => {
return 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 = () => { exports.getCouchUrl = () => {
if (!env.COUCH_DB_URL) return if (!env.COUCH_DB_URL) return
@ -221,16 +244,16 @@ exports.getAllApps = async (CouchDB, { dev, all, idsOnly } = {}) => {
if (idsOnly) { if (idsOnly) {
return appDbNames return appDbNames
} }
const appPromises = appDbNames.map(db => const appPromises = appDbNames.map(app =>
// skip setup otherwise databases could be re-created // 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) { if (appPromises.length === 0) {
return [] return []
} else { } else {
const response = await Promise.allSettled(appPromises) const response = await Promise.allSettled(appPromises)
const apps = response const apps = response
.filter(result => result.status === "fulfilled") .filter(result => result.status === "fulfilled" && result.value != null)
.map(({ value }) => value) .map(({ value }) => value)
if (!all) { if (!all) {
return apps.filter(app => { 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) => { exports.dbExists = async (CouchDB, dbName) => {
let exists = false let exists = false
try { try {
@ -322,13 +363,50 @@ const getScopedFullConfig = async function (db, { type, user, workspace }) {
} }
// Find the config with the most granular scope based on context // 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) (a, b) => determineScore(a) - determineScore(b)
)[0] )[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 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) { async function getScopedConfig(db, params) {
const configDoc = await getScopedFullConfig(db, params) const configDoc = await getScopedFullConfig(db, params)
return configDoc && configDoc.config ? configDoc.config : configDoc return configDoc && configDoc.config ? configDoc.config : configDoc

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -21,6 +21,7 @@
<label <label
class="spectrum-Checkbox spectrum-Checkbox--emphasized {sizeClass}" class="spectrum-Checkbox spectrum-Checkbox--emphasized {sizeClass}"
class:is-invalid={!!error} class:is-invalid={!!error}
class:checked={value}
> >
<input <input
checked={value} checked={value}
@ -50,6 +51,16 @@
</label> </label>
<style> <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 { .spectrum-Checkbox-input {
opacity: 0; opacity: 0;
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,67 @@
<script> <script>
import "@spectrum-css/fieldlabel/dist/index-vars.css" 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 size = "M"
export let tooltip = ""
export let showTooltip = false
</script> </script>
<label for="" class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}> {#if tooltip}
<slot /> <div class="container">
</label> <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> <style>
label { label {
padding: 0; padding: 0;
white-space: nowrap; 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> </style>

View File

@ -3,12 +3,22 @@
export let direction = "top" export let direction = "top"
export let text = "" export let text = ""
export let textWrapping = false
</script> </script>
<span class="u-tooltip-showOnHover tooltip"> <!-- Showing / Hiding a text wrapped tooltip should be handled outside the component -->
<slot /> {#if textWrapping}
<div class={`spectrum-Tooltip spectrum-Tooltip--${direction}`}> <span class="spectrum-Tooltip spectrum-Tooltip--{direction} is-open">
<span class="spectrum-Tooltip-label">{text}</span> <span class="spectrum-Tooltip-label">{text}</span>
<span class="spectrum-Tooltip-tip" /> <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}

View File

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

View File

@ -136,6 +136,11 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/illustratedmessage/-/illustratedmessage-3.0.2.tgz#6a480be98b027e050b086e7899e40d87adb0a8c0" resolved "https://registry.yarnpkg.com/@spectrum-css/illustratedmessage/-/illustratedmessage-3.0.2.tgz#6a480be98b027e050b086e7899e40d87adb0a8c0"
integrity sha512-dqnE8X27bGcO0HN8+dYx8O4o0dNNIAqeivOzDHhe2El+V4dTzMrNIerF6G0NLm3GjVf6XliwmitsZK+K6FmbtA== 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": "@spectrum-css/inputgroup@^3.0.2":
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/@spectrum-css/inputgroup/-/inputgroup-3.0.2.tgz#f1b13603832cbd22394f3d898af13203961f8691" resolved "https://registry.yarnpkg.com/@spectrum-css/inputgroup/-/inputgroup-3.0.2.tgz#f1b13603832cbd22394f3d898af13203961f8691"

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@ process.env.JWT_SECRET = cypressConfig.env.JWT_SECRET
process.env.COUCH_URL = `leveldb://${tmpdir}/.data/` process.env.COUCH_URL = `leveldb://${tmpdir}/.data/`
process.env.SELF_HOSTED = 1 process.env.SELF_HOSTED = 1
process.env.WORKER_URL = "http://localhost:10002/" 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_URL = `http://localhost:${MAIN_PORT}/`
process.env.MINIO_ACCESS_KEY = "budibase" process.env.MINIO_ACCESS_KEY = "budibase"
process.env.MINIO_SECRET_KEY = "budibase" process.env.MINIO_SECRET_KEY = "budibase"

View File

@ -182,16 +182,23 @@ Cypress.Commands.add("navigateToFrontend", () => {
cy.wait(1000) cy.wait(1000)
cy.contains("Design").click() cy.contains("Design").click()
cy.get(".spectrum-Search").type("/") 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) => { Cypress.Commands.add("createScreen", (screenName, route) => {
cy.get("[aria-label=AddCircle]").click() cy.get("[aria-label=AddCircle]").click()
cy.get(".spectrum-Modal").within(() => { cy.get(".spectrum-Modal").within(() => {
cy.get("input").first().type(screenName) cy.get(".item").first().click()
cy.get("input").eq(1).type(route)
cy.get(".spectrum-Button--cta").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", () => { Cypress.Commands.add("expandBudibaseConnection", () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
import { store } from "./index"
/** /**
* Recursively searches for a specific component ID * Recursively searches for a specific component ID
*/ */
@ -123,3 +125,20 @@ const searchComponentTree = (rootComponent, matchComponent) => {
} }
return null 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
}

View File

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

View File

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

View File

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

View File

@ -3,7 +3,14 @@
import { database } from "stores/backend" import { database } from "stores/backend"
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import { notifications } from "@budibase/bbui" 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" import analytics, { Events } from "analytics"
let name let name
@ -56,6 +63,10 @@
onConfirm={createAutomation} onConfirm={createAutomation}
disabled={!selectedTrigger || !name} 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" <Body size="XS"
>Please name your automation, then select a trigger. Every automation must >Please name your automation, then select a trigger. Every automation must
start with a trigger. start with a trigger.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@
DatePicker, DatePicker,
ModalContent, ModalContent,
Context, Context,
notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
@ -18,9 +19,13 @@
FIELDS, FIELDS,
AUTO_COLUMN_SUB_TYPES, AUTO_COLUMN_SUB_TYPES,
RelationshipTypes, RelationshipTypes,
ALLOWABLE_STRING_OPTIONS,
ALLOWABLE_NUMBER_OPTIONS,
ALLOWABLE_STRING_TYPES,
ALLOWABLE_NUMBER_TYPES,
SWITCHABLE_TYPES,
} from "constants/backend" } from "constants/backend"
import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils" import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils"
import { notifications } from "@budibase/bbui"
import ValuesList from "components/common/ValuesList.svelte" import ValuesList from "components/common/ValuesList.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { truncate } from "lodash" import { truncate } from "lodash"
@ -31,6 +36,10 @@
const AUTO_TYPE = "auto" const AUTO_TYPE = "auto"
const FORMULA_TYPE = FIELDS.FORMULA.type const FORMULA_TYPE = FIELDS.FORMULA.type
const LINK_TYPE = FIELDS.LINK.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 dispatch = createEventDispatcher()
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"] const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
const { hide } = getContext(Context.Modal) const { hide } = getContext(Context.Modal)
@ -54,10 +63,9 @@
let indexes = [...($tables.selected.indexes || [])] let indexes = [...($tables.selected.indexes || [])]
let confirmDeleteDialog let confirmDeleteDialog
let deletion let deletion
let deleteColName
$: tableOptions = $tables.list.filter( $: checkConstraints(field)
table => table._id !== $tables.draft._id && table.type !== "external"
)
$: required = !!field?.constraints?.presence || primaryDisplay $: required = !!field?.constraints?.presence || primaryDisplay
$: uneditable = $: uneditable =
$tables.selected?._id === TableNames.USERS && $tables.selected?._id === TableNames.USERS &&
@ -65,13 +73,8 @@
$: invalid = $: invalid =
!field.name || !field.name ||
(field.type === LINK_TYPE && !field.tableId) || (field.type === LINK_TYPE && !field.tableId) ||
Object.keys($tables.draft?.schema ?? {}).some( Object.keys(errors).length !== 0
key => key !== originalName && key === field.name $: errors = checkErrors(field)
) ||
columnNameInvalid
$: columnNameInvalid = PROHIBITED_COLUMN_NAMES.some(
name => field.name === name
)
// used to select what different options can be displayed for column type // used to select what different options can be displayed for column type
$: canBeSearched = $: canBeSearched =
@ -83,18 +86,36 @@
$: canBeRequired = $: canBeRequired =
field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_TYPE field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_TYPE
$: relationshipOptions = getRelationshipOptions(field) $: 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() { async function saveColumn() {
if (field.type === AUTO_TYPE) { if (field.type === AUTO_TYPE) {
field = buildAutoColumn($tables.draft.name, field.name, field.subtype) field = buildAutoColumn($tables.draft.name, field.name, field.subtype)
} }
await tables.saveField({ if (field.type !== LINK_TYPE) {
originalName, delete field.fieldName
field, }
primaryDisplay, try {
indexes, await tables.saveField({
}) originalName,
dispatch("updatecolumns") field,
primaryDisplay,
indexes,
})
dispatch("updatecolumns")
} catch (err) {
notifications.error(err)
}
} }
function deleteColumn() { function deleteColumn() {
@ -162,6 +183,7 @@
function hideDeleteDialog() { function hideDeleteDialog() {
confirmDeleteDialog.hide() confirmDeleteDialog.hide()
deleteColName = ""
deletion = false deletion = false
} }
@ -169,7 +191,7 @@
if (!field || !field.tableId) { if (!field || !field.tableId) {
return null return null
} }
const linkTable = tableOptions.find(table => table._id === field.tableId) const linkTable = tableOptions?.find(table => table._id === field.tableId)
if (!linkTable) { if (!linkTable) {
return null 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> </script>
<ModalContent <ModalContent
@ -205,20 +301,15 @@
label="Name" label="Name"
bind:value={field.name} bind:value={field.name}
disabled={uneditable || (linkEditDisabled && field.type === LINK_TYPE)} disabled={uneditable || (linkEditDisabled && field.type === LINK_TYPE)}
error={columnNameInvalid error={errors?.name}
? `${PROHIBITED_COLUMN_NAMES.join(", ")} are not allowed as column names`
: ""}
/> />
<Select <Select
disabled={originalName} disabled={!typeEnabled}
label="Type" label="Type"
bind:value={field.type} bind:value={field.type}
on:change={handleTypeChange} on:change={handleTypeChange}
options={[ options={getAllowedTypes()}
...Object.values(fieldDefinitions),
{ name: "Auto Column", type: AUTO_TYPE },
]}
getOptionLabel={field => field.name} getOptionLabel={field => field.name}
getOptionValue={field => field.type} getOptionValue={field => field.type}
/> />
@ -245,7 +336,7 @@
</div> </div>
{/if} {/if}
{#if canBeSearched} {#if canBeSearched && !external}
<div> <div>
<Label grey small>Search Indexes</Label> <Label grey small>Search Indexes</Label>
<Toggle <Toggle
@ -319,6 +410,7 @@
disabled={linkEditDisabled} disabled={linkEditDisabled}
label={`Column name in other table`} label={`Column name in other table`}
bind:value={field.fieldName} bind:value={field.fieldName}
error={errors.relatedName}
/> />
{:else if field.type === FORMULA_TYPE} {:else if field.type === FORMULA_TYPE}
<ModalBindableInput <ModalBindableInput
@ -327,7 +419,7 @@
value={field.formula} value={field.formula}
on:change={e => (field.formula = e.detail)} on:change={e => (field.formula = e.detail)}
bindings={getBindings({ table })} bindings={getBindings({ table })}
serverSide="true" allowJS
/> />
{:else if field.type === AUTO_TYPE} {:else if field.type === AUTO_TYPE}
<Select <Select
@ -348,9 +440,20 @@
</ModalContent> </ModalContent>
<ConfirmDialog <ConfirmDialog
bind:this={confirmDeleteDialog} 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" okText="Delete Column"
onOk={deleteColumn} onOk={deleteColumn}
onCancel={hideDeleteDialog} onCancel={hideDeleteDialog}
title="Confirm Deletion" 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,209 +1,27 @@
<script> <script>
import groupBy from "lodash/fp/groupBy" import BindingPanel from "./BindingPanel.svelte"
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"
const dispatch = createEventDispatcher() export let bindings = []
export let valid
export let bindableProperties = []
export let valid = true
export let value = "" export let value = ""
export let allowJS = false
let helpers = handlebarsCompletions() $: enrichedBindings = enrichBindings(bindings)
let getCaretPosition
let search = ""
$: categories = Object.entries(groupBy("category", bindableProperties)) // Ensure bindings have the correct properties
$: valid = isValid(readableToRuntimeBinding(bindableProperties, value)) const enrichBindings = bindings => {
$: dispatch("change", value) return bindings?.map(binding => ({
$: searchRgx = new RegExp(search, "ig") ...binding,
$: filteredCategories = categories.map(([categoryName, bindings]) => { readableBinding: binding.label || binding.readableBinding,
const filteredBindings = bindings.filter(binding => { runtimeBinding: binding.path || binding.runtimeBinding,
return binding.label.match(searchRgx) }))
}) }
return [categoryName, filteredBindings]
})
$: filteredHelpers = helpers?.filter(helper => {
return helper.label.match(searchRgx) || helper.description.match(searchRgx)
})
</script> </script>
<DrawerContent> <BindingPanel
<svelte:fragment slot="sidebar"> bind:valid
<div class="container"> bindings={enrichedBindings}
<section> {value}
<div class="heading">Search</div> {allowJS}
<Search placeholder="Search" bind:value={search} /> on:change
</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;
}
.syntax-error {
padding-top: var(--spacing-m);
color: var(--red);
font-size: 12px;
}
.syntax-error a {
color: var(--red);
text-decoration: underline;
}
.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>

View File

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

View File

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

View File

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

View File

@ -13,6 +13,12 @@
$: noChildrenAllowed = !component || !definition?.hasChildren $: noChildrenAllowed = !component || !definition?.hasChildren
$: noPaste = !$store.componentToPaste $: 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 moveUpComponent = () => {
const asset = get(currentAsset) const asset = get(currentAsset)
const parent = findComponentParent(asset.props, component._id) const parent = findComponentParent(asset.props, component._id)
@ -47,7 +53,7 @@
const duplicateComponent = () => { const duplicateComponent = () => {
storeComponentForCopy(false) storeComponentForCopy(false)
pasteComponent("below") pasteComponent("below", true)
} }
const deleteComponent = async () => { const deleteComponent = async () => {
@ -63,13 +69,13 @@
store.actions.components.copy(component, cut) store.actions.components.copy(component, cut)
} }
const pasteComponent = mode => { const pasteComponent = (mode, preserveBindings = false) => {
// lives in store - also used by drag drop // lives in store - also used by drag drop
store.actions.components.paste(component, mode) store.actions.components.paste(component, mode, preserveBindings)
} }
</script> </script>
{#if definition?.editable !== false} {#if showMenu}
<ActionMenu> <ActionMenu>
<div slot="control" class="icon"> <div slot="control" class="icon">
<Icon size="S" hoverable name="MoreSmallList" /> <Icon size="S" hoverable name="MoreSmallList" />

View File

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

View File

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

View File

@ -1,117 +1,178 @@
<script> <script>
import { store, allScreens, selectedAccessRole } from "builderStore" import { store } from "builderStore"
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { roles } from "stores/backend" import {
import { Input, Select, ModalContent, Toggle } from "@budibase/bbui" ModalContent,
Body,
Detail,
Layout,
Icon,
ProgressCircle,
} from "@budibase/bbui"
import getTemplates from "builderStore/store/screenTemplates" import getTemplates from "builderStore/store/screenTemplates"
import analytics, { Events } from "analytics" import { onDestroy } from "svelte"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
const CONTAINER = "@budibase/standard-components/container" import { createEventDispatcher } from "svelte"
let name = "" export let chooseModal
let routeError export let save
let baseComponent = CONTAINER export let showProgressCircle = false
let templateIndex
let draftScreen
let createLink = true
let roleId = $selectedAccessRole || "BASIC"
$: templates = getTemplates($store, $tables.list) let selectedScreens = []
$: route = !route && $allScreens.length === 0 ? "*" : route
$: { const blankScreen = "createFromScratch"
if (templates && templateIndex === undefined) { const dispatch = createEventDispatcher()
templateIndex = 0
templateChanged(0) function setScreens() {
} dispatch("save", {
screens: selectedScreens,
})
} }
const templateChanged = newTemplateIndex => { $: blankSelected = selectedScreens?.length === 1
if (newTemplateIndex === undefined) return $: autoSelected = selectedScreens?.length > 0 && !blankSelected
draftScreen = templates[newTemplateIndex].create()
if (draftScreen.props._instanceName) {
name = draftScreen.props._instanceName
}
if (draftScreen.props._component) { let templates = getTemplates($store, $tables.list)
baseComponent = draftScreen.props._component
}
if (draftScreen.routing) { const confirm = async () => {
route = draftScreen.routing.route if (autoSelected) {
} setScreens()
} await save()
const save = async () => {
if (!route) {
routeError = "URL is required"
} else { } else {
if (routeExists(route, roleId)) { setScreens()
routeError = "This URL is already taken for this access role" chooseModal(1)
} else {
routeError = ""
}
} }
}
if (routeError) return false const toggleScreenSelection = table => {
if (selectedScreens.find(s => s.table === table.name)) {
draftScreen.props._instanceName = name selectedScreens = selectedScreens.filter(
draftScreen.props._component = baseComponent screen => screen.table !== table.name
draftScreen.routing = { route, roleId } )
} else {
await store.actions.screens.create(draftScreen) let partialTemplates = getTemplates($store, $tables.list).filter(
if (createLink) { template => template.table === table.name
await store.actions.components.links.save(route, name) )
} selectedScreens = [...partialTemplates, ...selectedScreens]
await store.actions.routing.fetch()
if (templateIndex !== undefined) {
const template = templates[templateIndex]
analytics.captureEvent(Events.SCREEN.CREATED, {
template: template.id || template.name,
})
} }
} }
const routeExists = (route, roleId) => { onDestroy(() => {
return $allScreens.some( selectedScreens = []
screen => })
screen.routing.route.toLowerCase() === route.toLowerCase() &&
screen.routing.roleId === roleId
)
}
const routeChanged = event => {
if (!event.detail.startsWith("/")) {
route = "/" + event.detail
}
route = sanitizeUrl(route)
}
</script> </script>
<ModalContent title="New Screen" confirmText="Create Screen" onConfirm={save}> <div>
<Select <ModalContent
label="Choose a Template" title="Add screens"
bind:value={templateIndex} confirmText="Add Screens"
on:change={ev => templateChanged(ev.detail)} cancelText="Cancel"
options={templates} onConfirm={() => confirm()}
placeholder={null} disabled={!selectedScreens.length}
getOptionLabel={x => x.name} size="L"
getOptionValue={(x, idx) => idx} >
/> <Body size="XS"
<Input label="Name" bind:value={name} /> >Please select the screens you would like to add to your application.
<Input Autogenerated screens come with CRUD functionality.</Body
label="Url" >
error={routeError}
bind:value={route} <Layout noPadding gap="S">
on:change={routeChanged} <Detail size="S">Blank screen</Detail>
/> <div
<Select class="item"
label="Access" class:selected={selectedScreens.find(x => x.id.includes(blankScreen))}
bind:value={roleId} on:click={() =>
options={$roles} toggleScreenSelection(templates.find(t => t.id === blankScreen))}
getOptionLabel={x => x.name} class:disabled={autoSelected}
getOptionValue={x => x._id} >
/> <div data-cy="blank-screen" class="content">
<Toggle text="Create link in navigation bar" bind:value={createLink} /> <div class="text">Blank</div>
</ModalContent> </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>
{#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);
}
.content {
letter-spacing: 0px;
}
.footer-progress {
margin-top: var(--spacing-s);
}
.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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,11 @@
<script> <script>
import { Button, Icon, Drawer, Label } from "@budibase/bbui" import { Label } from "@budibase/bbui"
import { import {
readableToRuntimeBinding, readableToRuntimeBinding,
runtimeToReadableBinding, runtimeToReadableBinding,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
import { capitalise } from "helpers"
export let label = "" export let label = ""
export let bindable = true
export let componentInstance = {} export let componentInstance = {}
export let control = null export let control = null
export let key = "" export let key = ""
@ -17,18 +14,19 @@
export let props = {} export let props = {}
export let onChange = () => {} export let onChange = () => {}
export let bindings = [] export let bindings = []
export let componentBindings = []
export let nested = false
let bindingDrawer $: allBindings = getAllBindings(bindings, componentBindings, nested)
let anchor $: safeValue = getSafeValue(value, props.defaultValue, allBindings)
let valid
$: safeValue = getSafeValue(value, props.defaultValue, bindings)
$: tempValue = safeValue $: tempValue = safeValue
$: replaceBindings = val => readableToRuntimeBinding(bindings, val) $: replaceBindings = val => readableToRuntimeBinding(allBindings, val)
const handleClose = () => { const getAllBindings = (bindings, componentBindings, nested) => {
handleChange(tempValue) if (!nested) {
bindingDrawer.hide() return bindings
}
return [...(componentBindings || []), ...(bindings || [])]
} }
// Handle a value change of any type // Handle a value change of any type
@ -64,7 +62,7 @@
} }
</script> </script>
<div class="property-control" bind:this={anchor} data-cy={`setting-${key}`}> <div class="property-control" data-cy={`setting-${key}`}>
{#if type !== "boolean" && label} {#if type !== "boolean" && label}
<div class="label"> <div class="label">
<Label>{label}</Label> <Label>{label}</Label>
@ -78,37 +76,12 @@
updateOnChange={false} updateOnChange={false}
on:change={handleChange} on:change={handleChange}
onChange={handleChange} onChange={handleChange}
{bindings} bindings={allBindings}
name={key} name={key}
text={label} text={label}
{type} {type}
{...props} {...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>
</div> </div>
@ -120,41 +93,10 @@
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
} }
.label { .label {
text-transform: capitalize;
padding-bottom: var(--spectrum-global-dimension-size-65); padding-bottom: var(--spectrum-global-dimension-size-65);
} }
.control { .control {
position: relative; 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> </style>

View File

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

View File

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

View File

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

View File

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

View File

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

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