Merge branch 'develop' into oracle-datasource

This commit is contained in:
Rory Powell 2021-11-22 10:47:48 +00:00
commit f94a0eadbe
221 changed files with 13737 additions and 8947 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 }}

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

@ -1,5 +1,5 @@
{ {
"version": "0.9.175", "version": "0.9.185-alpha.10",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -59,6 +59,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.175", "version": "0.9.185-alpha.10",
"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

@ -6,6 +6,7 @@ exports.UserStatus = {
exports.Cookies = { exports.Cookies = {
CurrentApp: "budibase:currentapp", CurrentApp: "budibase:currentapp",
Auth: "budibase:auth", Auth: "budibase:auth",
Init: "budibase:init",
OIDC_CONFIG: "budibase:oidc:config", OIDC_CONFIG: "budibase:oidc:config",
} }

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.175", "version": "0.9.185-alpha.10",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",

View File

@ -36,5 +36,7 @@
{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

@ -40,8 +40,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,7 +69,7 @@
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}
@ -99,7 +106,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

@ -47,5 +47,6 @@
--spectrum-semantic-positive-border-color: #2d9d78; --spectrum-semantic-positive-border-color: #2d9d78;
--spectrum-semantic-positive-icon-color: #2d9d78; --spectrum-semantic-positive-icon-color: #2d9d78;
--spectrum-semantic-negative-icon-color: #e34850; --spectrum-semantic-negative-icon-color: #e34850;
min-width: 100px;
} }
</style> </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

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

View File

@ -11,7 +11,8 @@ it("should rename an unpublished application", () => {
renameApp(appRename) renameApp(appRename)
cy.searchForApplication(appRename) cy.searchForApplication(appRename)
cy.get(".appGrid").find(".wrapper").should("have.length", 1) cy.get(".appGrid").find(".wrapper").should("have.length", 1)
}) cy.deleteApp(appRename)
})
xit("Should rename a published application", () => { xit("Should rename a published application", () => {
// It is not possible to rename a published application // It is not possible to rename a published application

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

@ -43,24 +43,26 @@ Cypress.Commands.add("createApp", name => {
}) })
}) })
Cypress.Commands.add("deleteApp", () => { Cypress.Commands.add("deleteApp", appName => {
cy.visit(`localhost:${Cypress.env("PORT")}/builder`) cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
cy.wait(1000) cy.wait(1000)
cy.request(`localhost:${Cypress.env("PORT")}/api/applications?status=all`) cy.request(`localhost:${Cypress.env("PORT")}/api/applications?status=all`)
.its("body") .its("body")
.then(val => { .then(val => {
console.log(val)
if (val.length > 0) { if (val.length > 0) {
cy.get(".title > :nth-child(3) > .spectrum-Icon").click() cy.get(".title > :nth-child(3) > .spectrum-Icon").click()
cy.contains("Delete").click() cy.contains("Delete").click()
cy.get(".spectrum-Button--warning").click() cy.get(".spectrum-Modal").within(() => {
cy.get("input").type(appName)
cy.get(".spectrum-Button--warning").click()
})
} }
}) })
}) })
Cypress.Commands.add("createTestApp", () => { Cypress.Commands.add("createTestApp", () => {
const appName = "Cypress Tests" const appName = "Cypress Tests"
cy.deleteApp() cy.deleteApp(appName)
cy.createApp(appName, "This app is used for Cypress testing.") cy.createApp(appName, "This app is used for Cypress testing.")
}) })
@ -186,8 +188,16 @@ Cypress.Commands.add("navigateToFrontend", () => {
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-Modal").within(() => {
cy.get("input").first().clear().type(screenName)
cy.get("input").eq(1).clear().type(route)
cy.get(".spectrum-Button--cta").click()
})
cy.get(".spectrum-Modal").within(() => {
cy.get(`[data-cy="left-nav"]`).click()
cy.get(".spectrum-Button--cta").click() cy.get(".spectrum-Button--cta").click()
}) })
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "0.9.175", "version": "0.9.185-alpha.10",
"license": "AGPL-3.0", "license": "AGPL-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.175", "@budibase/bbui": "^0.9.185-alpha.10",
"@budibase/client": "^0.9.175", "@budibase/client": "^0.9.185-alpha.10",
"@budibase/colorpicker": "1.1.2", "@budibase/colorpicker": "1.1.2",
"@budibase/string-templates": "^0.9.175", "@budibase/string-templates": "^0.9.185-alpha.10",
"@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

@ -12,7 +12,7 @@ export default class PosthogClient {
posthog.init(this.token, { posthog.init(this.token, {
autocapture: false, autocapture: false,
capture_pageview: false, capture_pageview: true,
api_host: this.url, api_host: this.url,
}) })
posthog.set_config({ persistence: "cookie" }) posthog.set_config({ persistence: "cookie" })

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

@ -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"
@ -44,6 +45,7 @@ const INITIAL_FRONTEND_STATE = {
state: false, state: false,
customThemes: false, customThemes: false,
devicePreview: false, devicePreview: false,
messagePassing: false,
}, },
currentFrontEndType: "none", currentFrontEndType: "none",
selectedScreenId: "", selectedScreenId: "",
@ -368,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 = {}

View File

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

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

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

@ -8,10 +8,13 @@
let name let name
let selectedTrigger let selectedTrigger
let nameTouched = false
let triggerVal let triggerVal
export let webhookModal export let webhookModal
$: instanceId = $database._id $: instanceId = $database._id
$: nameError =
nameTouched && !name ? "Please specify a name for the automation." : null
async function createAutomation() { async function createAutomation() {
await automationStore.actions.create({ await automationStore.actions.create({
@ -51,13 +54,18 @@
confirmText="Save" confirmText="Save"
size="M" size="M"
onConfirm={createAutomation} onConfirm={createAutomation}
disabled={!selectedTrigger} disabled={!selectedTrigger || !name}
> >
<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.
</Body> </Body>
<Input bind:value={name} label="Name" /> <Input
bind:value={name}
on:change={() => (nameTouched = true)}
bind:error={nameError}
label="Name"
/>
<Layout noPadding> <Layout noPadding>
<Body size="S">Triggers</Body> <Body size="S">Triggers</Body>

View File

@ -234,7 +234,8 @@
<Editor <Editor
mode="javascript" mode="javascript"
on:change={e => { on:change={e => {
onChange(e, key) // need to pass without the value inside
onChange({ detail: e.detail.value }, key)
inputData[key] = e.detail.value inputData[key] = e.detail.value
}} }}
value={inputData[key]} value={inputData[key]}

View File

@ -6,6 +6,7 @@
import CreateViewButton from "./buttons/CreateViewButton.svelte" import CreateViewButton from "./buttons/CreateViewButton.svelte"
import ExistingRelationshipButton from "./buttons/ExistingRelationshipButton.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"
@ -124,6 +125,10 @@
<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

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

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

@ -18,6 +18,11 @@
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 { notifications } from "@budibase/bbui"
@ -59,9 +64,6 @@
let deletion let deletion
$: checkConstraints(field) $: checkConstraints(field)
$: tableOptions = $tables.list.filter(
opt => opt._id !== $tables.draft._id && opt.type === table.type
)
$: required = !!field?.constraints?.presence || primaryDisplay $: required = !!field?.constraints?.presence || primaryDisplay
$: uneditable = $: uneditable =
$tables.selected?._id === TableNames.USERS && $tables.selected?._id === TableNames.USERS &&
@ -88,6 +90,16 @@
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" $: 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) {
@ -174,7 +186,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
} }
@ -200,7 +212,14 @@
} }
function getAllowedTypes() { function getAllowedTypes() {
if (!external) { 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 [ return [
...Object.values(fieldDefinitions), ...Object.values(fieldDefinitions),
{ name: "Auto Column", type: AUTO_TYPE }, { name: "Auto Column", type: AUTO_TYPE },
@ -255,7 +274,7 @@
/> />
<Select <Select
disabled={originalName} disabled={!typeEnabled}
label="Type" label="Type"
bind:value={field.type} bind:value={field.type}
on:change={handleTypeChange} on:change={handleTypeChange}

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

@ -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,11 +11,13 @@
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)
$: dataImport = { $: dataImport = {
@ -25,6 +26,9 @@
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 +50,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 +68,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 +144,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 +156,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

@ -8,6 +8,7 @@
export let onOk = undefined export let onOk = undefined
export let onCancel = undefined export let onCancel = undefined
export let warning = true export let warning = true
export let disabled
let modal let modal
@ -26,6 +27,7 @@
confirmText={okText} confirmText={okText}
{cancelText} {cancelText}
{warning} {warning}
{disabled}
> >
<Body size="S"> <Body size="S">
{body} {body}

View File

@ -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,15 +25,30 @@
$: 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">
@ -40,12 +56,17 @@
{label} {label}
{disabled} {disabled}
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}

View File

@ -1,9 +1,16 @@
<script> <script>
import { Icon, Modal, notifications, ModalContent } from "@budibase/bbui" import {
Icon,
Input,
Modal,
notifications,
ModalContent,
} from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
import api from "builderStore/api" import api from "builderStore/api"
let revertModal let revertModal
let appName
$: appId = $store.appId $: appId = $store.appId
@ -33,10 +40,17 @@
<Icon name="Revert" hoverable on:click={revertModal.show} /> <Icon name="Revert" hoverable on:click={revertModal.show} />
<Modal bind:this={revertModal}> <Modal bind:this={revertModal}>
<ModalContent title="Revert Changes" confirmText="Revert" onConfirm={revert}> <ModalContent
title="Revert Changes"
confirmText="Revert"
onConfirm={revert}
disabled={appName !== $store.name}
>
<span <span
>The changes you have made will be deleted and the application reverted >The changes you have made will be deleted and the application reverted
back to its production state.</span back to its production state.</span
> >
<span>Please enter your app name to continue.</span>
<Input bind:value={appName} />
</ModalContent> </ModalContent>
</Modal> </Modal>

View File

@ -32,6 +32,16 @@
.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
// Budibase events are and initalisation events
const MessageTypes = {
IFRAME_LOADED: "iframe-loaded",
READY: "ready",
ERROR: "error",
BUDIBASE: "type",
KEYDOWN: "keydown"
}
// Construct iframe template // Construct iframe template
$: template = iframeTemplate.replace( $: template = iframeTemplate.replace(
@ -59,6 +69,7 @@
theme: $store.theme, theme: $store.theme,
customTheme: $store.customTheme, customTheme: $store.customTheme,
previewDevice: $store.previewDevice, previewDevice: $store.previewDevice,
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.
@ -80,11 +91,14 @@
// Refresh the preview when required // Refresh the preview when required
$: refreshContent(strippedJson) $: refreshContent(strippedJson)
onMount(() => { function receiveMessage(message) {
// Initialise the app when mounted const handlers = {
iframe.contentWindow.addEventListener( [MessageTypes.READY]: () => {
"ready", // Initialise the app when mounted
() => { if ($store.clientFeatures.messagePassing) {
if (!loading) return
}
// Display preview immediately if the intelligent loading feature // Display preview immediately if the intelligent loading feature
// is not supported // is not supported
if (!$store.clientFeatures.intelligentLoading) { if (!$store.clientFeatures.intelligentLoading) {
@ -92,34 +106,48 @@
} }
refreshContent(strippedJson) refreshContent(strippedJson)
}, },
{ once: true } [MessageTypes.ERROR]: event => {
) // Catch any app errors
// Catch any app errors
iframe.contentWindow.addEventListener(
"error",
event => {
loading = false loading = false
error = event.detail || "An unknown error occurred" error = event.error || "An unknown error occurred"
}, },
{ once: true } [MessageTypes.KEYDOWN]: handleKeydownEvent
) }
// Add listener for events sent by client library in preview const messageHandler = handlers[message.data.type] || handleBudibaseEvent
iframe.contentWindow.addEventListener("bb-event", handleBudibaseEvent) messageHandler(message)
iframe.contentWindow.addEventListener("keydown", handleKeydownEvent) }
onMount(() => {
window.addEventListener("message", receiveMessage)
if (!$store.clientFeatures.messagePassing) {
// Legacy - remove in later versions of BB
iframe.contentWindow.addEventListener("ready", () => {
receiveMessage({ data: { type: MessageTypes.READY }})
}, { once: true })
iframe.contentWindow.addEventListener("error", event => {
receiveMessage({ data: { type: MessageTypes.ERROR, error: event.detail }})
}, { once: true })
// Add listener for events sent by client library in preview
iframe.contentWindow.addEventListener("bb-event", handleBudibaseEvent)
iframe.contentWindow.addEventListener("keydown", handleKeydownEvent)
}
}) })
// Remove all iframe event listeners on component destroy // Remove all iframe event listeners on component destroy
onDestroy(() => { onDestroy(() => {
if (iframe.contentWindow) { if (iframe.contentWindow) {
iframe.contentWindow.removeEventListener("bb-event", handleBudibaseEvent) window.removeEventListener("message", receiveMessage)
iframe.contentWindow.removeEventListener("keydown", handleKeydownEvent) if (!$store.clientFeatures.messagePassing) {
// Legacy - remove in later versions of BB
iframe.contentWindow.removeEventListener("bb-event", handleBudibaseEvent)
iframe.contentWindow.removeEventListener("keydown", handleKeydownEvent)
}
} }
}) })
const handleBudibaseEvent = event => { const handleBudibaseEvent = event => {
const { type, data } = event.detail const { type, data } = event.data || event.detail
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") {
@ -151,13 +179,14 @@
store.actions.components.paste(destination, data.mode) store.actions.components.paste(destination, data.mode)
} }
} else { } else {
console.warning(`Client sent unknown event type: ${type}`) console.warn(`Client sent unknown event type: ${type}`)
} }
} }
const handleKeydownEvent = event => { const handleKeydownEvent = event => {
const { key } = event.data || event
if ( if (
(event.key === "Delete" || event.key === "Backspace") && (key === "Delete" || key === "Backspace") &&
selectedComponentId && selectedComponentId &&
["input", "textarea"].indexOf( ["input", "textarea"].indexOf(
iframe.contentWindow.document.activeElement?.tagName.toLowerCase() iframe.contentWindow.document.activeElement?.tagName.toLowerCase()

View File

@ -1,4 +1,13 @@
[ [
{
"name": "Blocks",
"icon": "Article",
"children": [
"tableblock",
"cardsblock",
"repeaterblock"
]
},
"section", "section",
"container", "container",
"dataprovider", "dataprovider",

View File

@ -54,7 +54,7 @@ export default `
if (!parsed) { if (!parsed) {
return return
} }
// Extract data from message // Extract data from message
const { const {
selectedComponentId, selectedComponentId,
@ -84,17 +84,21 @@ export default `
if (window.loadBudibase) { if (window.loadBudibase) {
window.loadBudibase() window.loadBudibase()
document.documentElement.classList.add("loaded") document.documentElement.classList.add("loaded")
window.dispatchEvent(new Event("iframe-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"
} }
} catch (error) { } catch (error) {
window.dispatchEvent(new CustomEvent("error", { detail: error })) window.parent.postMessage({ type: "error", error })
} }
} }
window.addEventListener("message", receiveMessage) window.addEventListener("message", receiveMessage)
window.dispatchEvent(new Event("ready")) window.addEventListener("keydown", evt => {
window.parent.postMessage({ type: "keydown", key: event.key })
})
window.parent.postMessage({ type: "ready" })
</script> </script>
</head> </head>
<body/> <body/>

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",
@ -85,9 +86,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">
@ -102,7 +100,7 @@
</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={showModal()} />
</div> </div>
</div> </div>

View File

@ -0,0 +1,179 @@
<script>
import { ModalContent, Body, Detail } from "@budibase/bbui"
import { store, selectedAccessRole, allScreens } from "builderStore"
import { cloneDeep } from "lodash/fp"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import { onDestroy } from "svelte"
export let selectedScreens
export let screenName
export let url
export let chooseModal
let roleId = $selectedAccessRole || "BASIC"
let routeError
let selectedNav
let createdScreens = []
$: {
selectedScreens?.forEach(screen => {
createdScreens = [...createdScreens, screen.create()]
})
}
$: blankSelected = selectedScreens.length === 1
const save = async screens => {
for (let screen of screens) {
await saveScreens(screen)
}
let navLayout = cloneDeep(
$store.layouts.find(layout => layout._id === "layout_private_master")
)
navLayout.props.navigation = selectedNav
await store.actions.routing.fetch()
await store.actions.layouts.save(navLayout)
selectedScreens = []
screenName = ""
url = ""
}
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
)
}
onDestroy(() => {
selectedScreens = []
screenName = ""
url = ""
})
</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,115 @@
<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 { ModalContent, Body, Detail, Layout, Icon } from "@budibase/bbui"
import { Input, Select, ModalContent, Toggle } from "@budibase/bbui"
import getTemplates from "builderStore/store/screenTemplates" import getTemplates from "builderStore/store/screenTemplates"
import analytics, { Events } from "analytics"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
const CONTAINER = "@budibase/standard-components/container" export let selectedScreens = []
export let chooseModal
let name = "" const blankScreen = "createFromScratch"
let routeError
let baseComponent = CONTAINER $: blankSelected = selectedScreens?.length === 1
let templateIndex $: autoSelected = selectedScreens?.length > 0 && !blankSelected
let draftScreen
let createLink = true
let roleId = $selectedAccessRole || "BASIC"
$: templates = getTemplates($store, $tables.list) $: templates = getTemplates($store, $tables.list)
$: route = !route && $allScreens.length === 0 ? "*" : route const toggleScreenSelection = table => {
$: { if (selectedScreens.find(s => s.name.includes(table.name))) {
if (templates && templateIndex === undefined) { selectedScreens = selectedScreens.filter(
templateIndex = 0 screen => !screen.name.includes(table.name)
templateChanged(0) )
}
}
const templateChanged = newTemplateIndex => {
if (newTemplateIndex === undefined) return
draftScreen = templates[newTemplateIndex].create()
if (draftScreen.props._instanceName) {
name = draftScreen.props._instanceName
}
if (draftScreen.props._component) {
baseComponent = draftScreen.props._component
}
if (draftScreen.routing) {
route = draftScreen.routing.route
}
}
const save = async () => {
if (!route) {
routeError = "URL is required"
} else { } else {
if (routeExists(route, roleId)) { templates = templates.filter(template =>
routeError = "This URL is already taken for this access role" template.name.includes(table.name)
} else { )
routeError = "" selectedScreens = [...templates, ...selectedScreens]
}
} }
if (routeError) return false
draftScreen.props._instanceName = name
draftScreen.props._component = baseComponent
draftScreen.routing = { route, roleId }
await store.actions.screens.create(draftScreen)
if (createLink) {
await store.actions.components.links.save(route, name)
}
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) => {
return $allScreens.some(
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}> <ModalContent
<Select title="Add screens"
label="Choose a Template" confirmText="Add Screens"
bind:value={templateIndex} cancelText="Cancel"
on:change={ev => templateChanged(ev.detail)} onConfirm={() => (autoSelected ? chooseModal(2) : chooseModal(1))}
options={templates} disabled={!selectedScreens.length}
placeholder={null} size="L"
getOptionLabel={x => x.name} >
getOptionValue={(x, idx) => idx} <Body size="XS"
/> >Please select the screens you would like to add to your application.
<Input label="Name" bind:value={name} /> Autogenerated screens come with CRUD functionality.</Body
<Input >
label="Url"
error={routeError} <Layout noPadding gap="S">
bind:value={route} <Detail size="S">Blank screen</Detail>
on:change={routeChanged} <div
/> class="item"
<Select class:selected={selectedScreens.find(x => x.id.includes(blankScreen))}
label="Access" on:click={() =>
bind:value={roleId} toggleScreenSelection(templates.find(t => t.id === blankScreen))}
options={$roles} class:disabled={autoSelected}
getOptionLabel={x => x.name} >
getOptionValue={x => x._id} <div data-cy="blank-screen" class="content">
/> <Body size="S">Blank</Body>
<Toggle text="Create link in navigation bar" bind:value={createLink} /> </div>
<div style="color: var(--spectrum-global-color-green-600); float: right">
{#if selectedScreens.find(x => x.id === blankScreen)}
<Icon size="S" name="CheckmarkCircleOutline" />
{/if}
</div>
</div>
<Detail size="S">Autogenerated Screens</Detail>
{#each $tables.list.filter(table => table.type !== "external") as table}
<div
class:disabled={blankSelected}
class:selected={selectedScreens.find(x => x.name.includes(table.name))}
on:click={() => toggleScreenSelection(table)}
class="item"
>
<div class="content">
{table.name}
</div>
<div
style="color: var(--spectrum-global-color-green-600); float: right"
>
{#if selectedScreens.find(x => x.name.includes(table.name))}
<Icon size="S" name="CheckmarkCircleOutline" />
{/if}
</div>
</div>
{/each}
</Layout>
</ModalContent> </ModalContent>
<style>
.disabled {
opacity: 0.3;
pointer-events: none;
}
.content {
letter-spacing: 0px;
color: #2c2c2c;
}
.item {
cursor: pointer;
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
padding: var(--spectrum-alias-item-padding-s);
background: var(--background);
transition: 0.3s all;
border: solid var(--spectrum-alias-border-color);
border-radius: 2px;
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,51 @@
<script>
import { ModalContent, Input } from "@budibase/bbui"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import { selectedAccessRole, allScreens } from "builderStore"
export let screenName
export let url
export let chooseModal
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
)
}
</script>
<ModalContent
size="M"
title={"Enter details"}
confirmText={"Continue"}
onCancel={() => chooseModal(0)}
onConfirm={() => chooseModal(2)}
cancelText={"Back"}
disabled={!screenName || !url || routeError}
>
<Input label="Name" bind:value={screenName} />
<Input
label="URL"
error={routeError}
bind:value={url}
on:change={routeChanged}
/>
</ModalContent>

View File

@ -0,0 +1,48 @@
<script>
import NavigationSelectionModal from "components/design/NavigationPanel/NavigationSelectionModal.svelte"
import ScreenDetailsModal from "components/design/NavigationPanel/ScreenDetailsModal.svelte"
import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte"
import { Modal } from "@budibase/bbui"
let newScreenModal
let navigationSelectionModal
let screenDetailsModal
let screenName = ""
let url = ""
let selectedScreens = []
export const showModal = () => {
newScreenModal.show()
}
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 bind:selectedScreens {chooseModal} />
</Modal>
<Modal bind:this={screenDetailsModal}>
<ScreenDetailsModal bind:screenName bind:url {chooseModal} />
</Modal>
<Modal bind:this={navigationSelectionModal}>
<NavigationSelectionModal
bind:url
bind:screenName
bind:selectedScreens
{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

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

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

View File

@ -19,15 +19,24 @@
import IntegrationQueryEditor from "components/integration/index.svelte" import IntegrationQueryEditor from "components/integration/index.svelte"
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte" import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte" import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
import { datasources, integrations, queries } from "stores/backend" import {
datasources,
integrations,
queries,
roles,
permissions,
} from "stores/backend"
import { capitalise } from "../../helpers" import { capitalise } from "../../helpers"
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte" import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
import { Roles } from "constants/backend"
import { onMount } from "svelte"
export let query export let query
export let fields = [] export let fields = []
let parameters let parameters
let data = [] let data = []
let roleId
const transformerDocs = const transformerDocs =
"https://docs.budibase.com/building-apps/data/transformers" "https://docs.budibase.com/building-apps/data/transformers"
const typeOptions = [ const typeOptions = [
@ -70,7 +79,22 @@
} }
function resetDependentFields() { function resetDependentFields() {
if (query.fields.extra) query.fields.extra = {} if (query.fields.extra) {
query.fields.extra = {}
}
}
async function updateRole(role, id = null) {
roleId = role
if (query?._id || id) {
for (let level of ["read", "write"]) {
await permissions.save({
level,
role,
resource: query?._id || id,
})
}
}
} }
function populateExtraQuery(extraQueryFields) { function populateExtraQuery(extraQueryFields) {
@ -122,6 +146,7 @@
async function saveQuery() { async function saveQuery() {
try { try {
const { _id } = await queries.save(query.datasourceId, query) const { _id } = await queries.save(query.datasourceId, query)
await updateRole(roleId, _id)
notifications.success(`Query saved successfully.`) notifications.success(`Query saved successfully.`)
$goto(`../${_id}`) $goto(`../${_id}`)
} catch (err) { } catch (err) {
@ -129,6 +154,18 @@
notifications.error(`Error creating query. ${err.message}`) notifications.error(`Error creating query. ${err.message}`)
} }
} }
onMount(async () => {
if (!query || !query._id) {
roleId = Roles.BASIC
return
}
try {
roleId = (await permissions.forResource(query._id))["read"]
} catch (err) {
roleId = Roles.BASIC
}
})
</script> </script>
<Layout gap="S" noPadding> <Layout gap="S" noPadding>
@ -151,6 +188,16 @@
queryConfig[verb]?.displayName || capitalise(verb)} queryConfig[verb]?.displayName || capitalise(verb)}
/> />
</div> </div>
<div class="config-field">
<Label>Access level</Label>
<Select
value={roleId}
on:change={e => updateRole(e.detail)}
options={$roles}
getOptionLabel={x => x.name}
getOptionValue={x => x._id}
/>
</div>
{#if integrationInfo?.extra && query.queryVerb} {#if integrationInfo?.extra && query.queryVerb}
<ExtraQueryConfig <ExtraQueryConfig
{query} {query}

View File

@ -9,7 +9,7 @@
Checkbox, Checkbox,
} from "@budibase/bbui" } from "@budibase/bbui"
import { store, automationStore, hostingStore } from "builderStore" import { store, automationStore, hostingStore } from "builderStore"
import { admin } from "stores/portal" import { admin, auth } from "stores/portal"
import { string, mixed, object } from "yup" import { string, mixed, object } from "yup"
import api, { get, post } from "builderStore/api" import api, { get, post } from "builderStore/api"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
@ -139,6 +139,7 @@
} }
const userResp = await api.post(`/api/users/metadata/self`, user) const userResp = await api.post(`/api/users/metadata/self`, user)
await userResp.json() await userResp.json()
await auth.setInitInfo({})
$goto(`/builder/app/${appJson.instance._id}`) $goto(`/builder/app/${appJson.instance._id}`)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@ -146,6 +147,21 @@
submitting = false submitting = false
} }
} }
function getModalTitle() {
let title = "Create App"
if (template.fromFile) {
title = "Import App"
} else if (template.key) {
title = "Create app from template"
}
return title
}
async function onCancel() {
template = null
await auth.setInitInfo({})
}
</script> </script>
{#if showTemplateSelection} {#if showTemplateSelection}
@ -172,10 +188,10 @@
</ModalContent> </ModalContent>
{:else} {:else}
<ModalContent <ModalContent
title={template?.fromFile ? "Import app" : "Create app"} title={getModalTitle()}
confirmText={template?.fromFile ? "Import app" : "Create app"} confirmText={template?.fromFile ? "Import app" : "Create app"}
onConfirm={createNewApp} onConfirm={createNewApp}
onCancel={inline ? () => (template = null) : null} onCancel={inline ? onCancel : null}
cancelText={inline ? "Back" : undefined} cancelText={inline ? "Back" : undefined}
showCloseIcon={!inline} showCloseIcon={!inline}
disabled={!valid} disabled={!valid}

View File

@ -37,33 +37,33 @@
<p class="detail">{template?.category?.toUpperCase()}</p> <p class="detail">{template?.category?.toUpperCase()}</p>
</div> </div>
{/each} {/each}
<div class="template start-from-scratch" on:click={() => onSelect(null)}>
<div
class="background-icon"
style={`background: rgb(50, 50, 50); color: white;`}
>
<Icon name="Add" />
</div>
<Heading size="XS">Start from scratch</Heading>
<p class="detail">BLANK</p>
</div>
<div
class="template import"
on:click={() => onSelect(null, { useImport: true })}
>
<div
class="background-icon"
style={`background: rgb(50, 50, 50); color: white;`}
>
<Icon name="Add" />
</div>
<Heading size="XS">Import an app</Heading>
<p class="detail">BLANK</p>
</div>
</div> </div>
{:catch err} {:catch err}
<h1 style="color:red">{err}</h1> <h1 style="color:red">{err}</h1>
{/await} {/await}
<div class="template start-from-scratch" on:click={() => onSelect(null)}>
<div
class="background-icon"
style={`background: rgb(50, 50, 50); color: white;`}
>
<Icon name="Add" />
</div>
<Heading size="XS">Start from scratch</Heading>
<p class="detail">BLANK</p>
</div>
<div
class="template import"
on:click={() => onSelect(null, { useImport: true })}
>
<div
class="background-icon"
style={`background: rgb(50, 50, 50); color: white;`}
>
<Icon name="Add" />
</div>
<Heading size="XS">Import an app</Heading>
<p class="detail">BLANK</p>
</div>
</Layout> </Layout>
<style> <style>

View File

@ -138,3 +138,19 @@ export const RelationshipTypes = {
ONE_TO_MANY: "one-to-many", ONE_TO_MANY: "one-to-many",
MANY_TO_ONE: "many-to-one", MANY_TO_ONE: "many-to-one",
} }
export const ALLOWABLE_STRING_OPTIONS = [FIELDS.STRING, FIELDS.OPTIONS]
export const ALLOWABLE_STRING_TYPES = ALLOWABLE_STRING_OPTIONS.map(
opt => opt.type
)
export const ALLOWABLE_NUMBER_OPTIONS = [FIELDS.NUMBER, FIELDS.BOOLEAN]
export const ALLOWABLE_NUMBER_TYPES = ALLOWABLE_NUMBER_OPTIONS.map(
opt => opt.type
)
export const SWITCHABLE_TYPES = ALLOWABLE_NUMBER_TYPES.concat(
ALLOWABLE_STRING_TYPES
)

View File

@ -40,6 +40,8 @@ export const UNEDITABLE_USER_FIELDS = [
"lastName", "lastName",
] ]
export const UNSORTABLE_TYPES = ["formula", "attachment", "array", "link"]
export const LAYOUT_NAMES = { export const LAYOUT_NAMES = {
MASTER: { MASTER: {
PRIVATE: "layout_private_master", PRIVATE: "layout_private_master",

View File

@ -1,5 +1,5 @@
<script> <script>
import { isActive, redirect } from "@roxi/routify" import { isActive, redirect, params } from "@roxi/routify"
import { admin, auth } from "stores/portal" import { admin, auth } from "stores/portal"
import { onMount } from "svelte" import { onMount } from "svelte"
@ -28,9 +28,13 @@
} }
if (user && user.tenantId) { if (user && user.tenantId) {
// no tenant in the url - send to account portal to fix this
if (!urlTenantId) { if (!urlTenantId) {
window.location.href = $admin.accountPortalUrl // redirect to correct tenantId subdomain
if (!window.location.host.includes("localhost")) {
let redirectUrl = window.location.href
redirectUrl = redirectUrl.replace("://", `://${user.tenantId}.`)
window.location.href = redirectUrl
}
return return
} }
@ -47,6 +51,10 @@
} }
onMount(async () => { onMount(async () => {
if ($params["?template"]) {
await auth.setInitInfo({ init_template: $params["?template"] })
}
await auth.checkAuth() await auth.checkAuth()
await admin.init() await admin.init()

View File

@ -79,6 +79,10 @@
try { try {
// Create datasource // Create datasource
await datasources.save(datasource) await datasources.save(datasource)
if (datasource?.plus) {
await tables.fetch()
}
await datasources.fetch()
notifications.success(`Datasource ${name} updated successfully.`) notifications.success(`Datasource ${name} updated successfully.`)
} catch (err) { } catch (err) {
notifications.error(`Error saving datasource: ${err}`) notifications.error(`Error saving datasource: ${err}`)

View File

@ -5,6 +5,8 @@
selectedComponent, selectedComponent,
allScreens, allScreens,
} from "builderStore" } from "builderStore"
import { Detail, Layout, Button, Icon } from "@budibase/bbui"
import CurrentItemPreview from "components/design/AppPreview" import CurrentItemPreview from "components/design/AppPreview"
import PropertiesPanel from "components/design/PropertiesPanel/PropertiesPanel.svelte" import PropertiesPanel from "components/design/PropertiesPanel/PropertiesPanel.svelte"
import ComponentSelectionList from "components/design/AppPreview/ComponentSelectionList.svelte" import ComponentSelectionList from "components/design/AppPreview/ComponentSelectionList.svelte"
@ -16,6 +18,8 @@
import AppThemeSelect from "components/design/AppPreview/AppThemeSelect.svelte" import AppThemeSelect from "components/design/AppPreview/AppThemeSelect.svelte"
import ThemeEditor from "components/design/AppPreview/ThemeEditor.svelte" import ThemeEditor from "components/design/AppPreview/ThemeEditor.svelte"
import DevicePreviewSelect from "components/design/AppPreview/DevicePreviewSelect.svelte" import DevicePreviewSelect from "components/design/AppPreview/DevicePreviewSelect.svelte"
import Logo from "assets/bb-space-man.svg"
import ScreenWizard from "components/design/NavigationPanel/ScreenWizard.svelte"
// Cache previous values so we don't update the URL more than necessary // Cache previous values so we don't update the URL more than necessary
let previousType let previousType
@ -23,6 +27,9 @@
let previousComponentId let previousComponentId
let hydrationComplete = false let hydrationComplete = false
// Manage the layout modal flow from here
let showModal
// Hydrate state from URL params // Hydrate state from URL params
$: hydrateStateFromURL($params, $leftover) $: hydrateStateFromURL($params, $leftover)
@ -145,7 +152,7 @@
<!-- routify:options index=1 --> <!-- routify:options index=1 -->
<div class="root"> <div class="root">
<div class="ui-nav"> <div class="ui-nav">
<FrontendNavigatePane /> <FrontendNavigatePane {showModal} />
</div> </div>
<div class="preview-pane"> <div class="preview-pane">
@ -166,6 +173,25 @@
<CurrentItemPreview /> <CurrentItemPreview />
{/key} {/key}
</div> </div>
{:else}
<div class="centered">
<div class="main">
<Layout gap="S" justifyItems="center">
<img class="img-size" alt="logo" src={Logo} />
<div class="new-screen-text">
<Detail size="M">Let's add some life to this screen</Detail>
</div>
<Button on:click={() => showModal()} size="M" cta>
<div class="new-screen-button">
<div class="background-icon" style="color: white;">
<Icon name="Add" />
</div>
Add Screen
</div></Button
>
</Layout>
</div>
</div>
{/if} {/if}
</div> </div>
@ -178,6 +204,8 @@
<slot /> <slot />
<ScreenWizard bind:showModal />
<style> <style>
.root { .root {
display: grid; display: grid;
@ -186,7 +214,30 @@
flex: 1 1 auto; flex: 1 1 auto;
height: 0; height: 0;
} }
.new-screen-text {
width: 160px;
text-align: center;
color: #2c2c2c;
font-weight: 600;
}
.new-screen-button {
margin-left: 5px;
height: 20px;
width: 100px;
display: flex;
align-items: center;
}
.background-icon {
margin-top: 4px;
margin-right: 4px;
}
.img-size {
width: 160px;
height: 160px;
}
.ui-nav { .ui-nav {
grid-column: 1; grid-column: 1;
background-color: var(--background); background-color: var(--background);
@ -237,4 +288,21 @@
border-left: var(--border-light); border-left: var(--border-light);
overflow-x: hidden; overflow-x: hidden;
} }
.centered {
top: 0;
bottom: 0;
left: 10%;
right: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
width: 300px;
}
</style> </style>

View File

@ -6,6 +6,7 @@
ActionButton, ActionButton,
ActionGroup, ActionGroup,
ButtonGroup, ButtonGroup,
Input,
Select, Select,
Modal, Modal,
Page, Page,
@ -36,6 +37,7 @@
let loaded = false let loaded = false
let searchTerm = "" let searchTerm = ""
let cloud = $admin.cloud let cloud = $admin.cloud
let appName = ""
$: enrichedApps = enrichApps($apps, $auth.user, sortBy) $: enrichedApps = enrichApps($apps, $auth.user, sortBy)
$: filteredApps = enrichedApps.filter(app => $: filteredApps = enrichedApps.filter(app =>
@ -163,6 +165,7 @@
notifications.error(`Error deleting app: ${err}`) notifications.error(`Error deleting app: ${err}`)
} }
selectedApp = null selectedApp = null
appName = null
} }
const updateApp = async app => { const updateApp = async app => {
@ -184,9 +187,27 @@
} }
} }
function createAppFromTemplateUrl(templateKey) {
// validate the template key just to make sure
const templateParts = templateKey.split("/")
if (templateParts.length === 2 && templateParts[0] === "app") {
template = {
key: templateKey,
}
initiateAppCreation()
} else {
notifications.error("Your Template URL is invalid. Please try another.")
}
}
onMount(async () => { onMount(async () => {
await apps.load() await apps.load()
loaded = true loaded = true
// if the portal is loaded from an external URL with a template param
const initInfo = await auth.getInitInfo()
if (initInfo.init_template) {
createAppFromTemplateUrl(initInfo.init_template)
}
}) })
</script> </script>
@ -278,8 +299,13 @@
title="Confirm deletion" title="Confirm deletion"
okText="Delete app" okText="Delete app"
onOk={confirmDeleteApp} onOk={confirmDeleteApp}
onCancel={() => (appName = null)}
disabled={appName !== selectedApp?.name}
> >
Are you sure you want to delete the app <b>{selectedApp?.name}</b>? Are you sure you want to delete the app <b>{selectedApp?.name}</b>?
<p>Please enter the app name below to confirm.</p>
<Input bind:value={appName} data-cy="delete-app-confirmation" />
</ConfirmDialog> </ConfirmDialog>
<ConfirmDialog <ConfirmDialog
bind:this={unpublishModal} bind:this={unpublishModal}

View File

@ -21,26 +21,25 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import { onMount } from "svelte" import { onMount } from "svelte"
import api from "builderStore/api" import api from "builderStore/api"
import { organisation, auth, admin } from "stores/portal" import { organisation, admin } from "stores/portal"
import { uuid } from "builderStore/uuid" import { uuid } from "builderStore/uuid"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
$: tenantId = $auth.tenantId
$: multiTenancyEnabled = $admin.multiTenancy
const ConfigTypes = { const ConfigTypes = {
Google: "google", Google: "google",
OIDC: "oidc", OIDC: "oidc",
} }
function callbackUrl(tenantId, end) { // Some older google configs contain a manually specified value - retain the functionality to edit the field
let url = `/api/global/auth` // When there is no value or we are in the cloud - prohibit editing the field, must use platform url to change
if (multiTenancyEnabled && tenantId) { $: googleCallbackUrl = undefined
url += `/${tenantId}` $: googleCallbackReadonly = $admin.cloud || !googleCallbackUrl
}
url += end // Indicate to user that callback is based on platform url
return url // If there is an existing value, indicate that it may be removed to return to default behaviour
} $: googleCallbackTooltip = googleCallbackReadonly
? "Vist the organisation page to update the platform URL"
: "Leave blank to use the default callback URL"
$: GoogleConfigFields = { $: GoogleConfigFields = {
Google: [ Google: [
@ -49,8 +48,9 @@
{ {
name: "callbackURL", name: "callbackURL",
label: "Callback URL", label: "Callback URL",
readonly: true, readonly: googleCallbackReadonly,
placeholder: callbackUrl(tenantId, "/google/callback"), tooltip: googleCallbackTooltip,
placeholder: $organisation.googleCallbackUrl,
}, },
], ],
} }
@ -62,9 +62,10 @@
{ name: "clientSecret", label: "Client Secret" }, { name: "clientSecret", label: "Client Secret" },
{ {
name: "callbackURL", name: "callbackURL",
label: "Callback URL",
readonly: true, readonly: true,
placeholder: callbackUrl(tenantId, "/oidc/callback"), tooltip: "Vist the organisation page to update the platform URL",
label: "Callback URL",
placeholder: $organisation.oidcCallbackUrl,
}, },
], ],
} }
@ -241,6 +242,8 @@
providers.google = googleDoc providers.google = googleDoc
} }
googleCallbackUrl = providers?.google?.config?.callbackURL
//Get the list of user uploaded logos and push it to the dropdown options. //Get the list of user uploaded logos and push it to the dropdown options.
//This needs to be done before the config call so they're available when the dropdown renders //This needs to be done before the config call so they're available when the dropdown renders
const res = await api.get(`/api/global/configs/logos_oidc`) const res = await api.get(`/api/global/configs/logos_oidc`)
@ -308,7 +311,7 @@
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
{#each GoogleConfigFields.Google as field} {#each GoogleConfigFields.Google as field}
<div class="form-row"> <div class="form-row">
<Label size="L">{field.label}</Label> <Label size="L" tooltip={field.tooltip}>{field.label}</Label>
<Input <Input
bind:value={providers.google.config[field.name]} bind:value={providers.google.config[field.name]}
readonly={field.readonly} readonly={field.readonly}
@ -346,7 +349,7 @@
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
{#each OIDCConfigFields.Oidc as field} {#each OIDCConfigFields.Oidc as field}
<div class="form-row"> <div class="form-row">
<Label size="L">{field.label}</Label> <Label size="L" tooltip={field.tooltip}>{field.label}</Label>
<Input <Input
bind:value={providers.oidc.config.configs[0][field.name]} bind:value={providers.oidc.config.configs[0][field.name]}
readonly={field.readonly} readonly={field.readonly}

View File

@ -116,7 +116,11 @@
</Layout> </Layout>
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<Label size="L">Platform URL</Label> <Label
size="L"
tooltip={"Update the Platform URL to match your Budibase web URL. This keeps email templates and authentication configs up to date."}
>Platform URL</Label
>
<Input thin bind:value={$values.platformUrl} /> <Input thin bind:value={$values.platformUrl} />
</div> </div>
</div> </div>
@ -135,6 +139,7 @@
.field { .field {
display: grid; display: grid;
grid-template-columns: 100px 1fr; grid-template-columns: 100px 1fr;
grid-gap: var(--spacing-l);
align-items: center; align-items: center;
} }
.file { .file {

View File

@ -95,6 +95,7 @@ export function createDatasourcesStore() {
return { list: sources, selected: null } return { list: sources, selected: null }
}) })
await queries.fetch()
return response return response
}, },
removeSchemaError: () => { removeSchemaError: () => {

View File

@ -10,13 +10,11 @@ export function createPermissionStore() {
const response = await api.post( const response = await api.post(
`/api/permission/${role}/${resource}/${level}` `/api/permission/${role}/${resource}/${level}`
) )
const json = await response.json() return await response.json()
return json
}, },
forResource: async resourceId => { forResource: async resourceId => {
const response = await api.get(`/api/permission/${resourceId}`) const response = await api.get(`/api/permission/${resourceId}`)
const json = await response.json() return await response.json()
return json
}, },
} }
} }

View File

@ -2,6 +2,7 @@ import { writable, get } from "svelte/store"
import { views, queries, datasources } from "./" import { views, queries, datasources } from "./"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import api from "builderStore/api" import api from "builderStore/api"
import { SWITCHABLE_TYPES } from "../../constants/backend"
export function createTablesStore() { export function createTablesStore() {
const store = writable({}) const store = writable({})
@ -47,7 +48,11 @@ export function createTablesStore() {
const field = updatedTable.schema[key] const field = updatedTable.schema[key]
const oldField = oldTable?.schema[key] const oldField = oldTable?.schema[key]
// if the type has changed then revert back to the old field // if the type has changed then revert back to the old field
if (oldField != null && oldField?.type !== field.type) { if (
oldField != null &&
oldField?.type !== field.type &&
SWITCHABLE_TYPES.indexOf(oldField?.type) === -1
) {
updatedTable.schema[key] = oldField updatedTable.schema[key] = oldField
} }
// field has been renamed // field has been renamed

View File

@ -57,11 +57,11 @@ export function createAuthStore() {
analytics.showChat({ analytics.showChat({
email: user.email, email: user.email,
created_at: (user.createdAt || Date.now()) / 1000, created_at: (user.createdAt || Date.now()) / 1000,
name: user.name, name: user.account?.name,
user_id: user._id, user_id: user._id,
tenant: user.tenantId, tenant: user.tenantId,
"Company size": user.size, "Company size": user.account?.size,
"Job role": user.profession, "Job role": user.account?.profession,
}) })
}) })
} }
@ -80,9 +80,30 @@ export function createAuthStore() {
} }
} }
async function setInitInfo(info) {
await api.post(`/api/global/auth/init`, info)
auth.update(store => {
store.initInfo = info
return store
})
return info
}
async function getInitInfo() {
const response = await api.get(`/api/global/auth/init`)
const json = response.json()
auth.update(store => {
store.initInfo = json
return store
})
return json
}
return { return {
subscribe: store.subscribe, subscribe: store.subscribe,
setOrganisation: setOrganisation, setOrganisation,
getInitInfo,
setInitInfo,
checkQueryString: async () => { checkQueryString: async () => {
const urlParams = new URLSearchParams(window.location.search) const urlParams = new URLSearchParams(window.location.search)
if (urlParams.has("tenantId")) { if (urlParams.has("tenantId")) {
@ -122,6 +143,7 @@ export function createAuthStore() {
throw "Unable to create logout" throw "Unable to create logout"
} }
await response.json() await response.json()
await setInitInfo({})
setUser(null) setUser(null)
}, },
updateSelf: async fields => { updateSelf: async fields => {

View File

@ -3,12 +3,14 @@ import api from "builderStore/api"
import { auth } from "stores/portal" import { auth } from "stores/portal"
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
platformUrl: "http://localhost:10000", platformUrl: "",
logoUrl: undefined, logoUrl: undefined,
docsUrl: undefined, docsUrl: undefined,
company: "Budibase", company: "Budibase",
oidc: undefined, oidc: undefined,
google: undefined, google: undefined,
oidcCallbackUrl: "",
googleCallbackUrl: "",
} }
export function createOrganisationStore() { export function createOrganisationStore() {
@ -28,6 +30,13 @@ export function createOrganisationStore() {
} }
async function save(config) { async function save(config) {
// delete non-persisted fields
const storeConfig = get(store)
delete storeConfig.oidc
delete storeConfig.google
delete storeConfig.oidcCallbackUrl
delete storeConfig.googleCallbackUrl
const res = await api.post("/api/global/configs", { const res = await api.post("/api/global/configs", {
type: "settings", type: "settings",
config: { ...get(store), ...config }, config: { ...get(store), ...config },

View File

@ -1,4 +1,4 @@
import svelte from "@sveltejs/vite-plugin-svelte" import { svelte } from "@sveltejs/vite-plugin-svelte"
import replace from "@rollup/plugin-replace" import replace from "@rollup/plugin-replace"
import path from "path" import path from "path"
@ -6,6 +6,11 @@ import path from "path"
export default ({ mode }) => { export default ({ mode }) => {
const isProduction = mode === "production" const isProduction = mode === "production"
return { return {
server: {
fs: {
strict: false,
},
},
base: "/builder/", base: "/builder/",
build: { build: {
minify: isProduction, minify: isProduction,

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "0.9.175", "version": "0.9.185-alpha.10",
"description": "Budibase CLI, for developers, self hosting and migrations.", "description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js", "main": "src/index.js",
"bin": { "bin": {

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,7 @@ The object key is the name of the component, as exported by `index.js`.
- **bindable** - whether the components provides a bindable value or not - **bindable** - whether the components provides a bindable value or not
- **settings** - array of settings displayed in the builder - **settings** - array of settings displayed in the builder
###Settings Definitions ### Settings Definitions
The `type` field in each setting is used by the builder to know which component to use to display The `type` field in each setting is used by the builder to know which component to use to display
the setting, so it's important that this field is correct. The valid options are: the setting, so it's important that this field is correct. The valid options are:

View File

@ -5,7 +5,8 @@
"deviceAwareness": true, "deviceAwareness": true,
"state": true, "state": true,
"customThemes": true, "customThemes": true,
"devicePreview": true "devicePreview": true,
"messagePassing": true
}, },
"layout": { "layout": {
"name": "Layout", "name": "Layout",
@ -2457,12 +2458,12 @@
"settings": [ "settings": [
{ {
"type": "dataProvider", "type": "dataProvider",
"label": "Provider", "label": "Data provider",
"key": "dataProvider" "key": "dataProvider"
}, },
{ {
"type": "number", "type": "number",
"label": "Row Count", "label": "Row count",
"key": "rowCount", "key": "rowCount",
"defaultValue": 8 "defaultValue": 8
}, },
@ -2496,9 +2497,36 @@
}, },
{ {
"type": "boolean", "type": "boolean",
"label": "Auto Columns", "label": "Show auto columns",
"key": "showAutoColumns", "key": "showAutoColumns",
"defaultValue": false "defaultValue": false
},
{
"type": "boolean",
"label": "Link table rows",
"key": "linkRows"
},
{
"type": "boolean",
"label": "Open link screens in modal",
"key": "linkPeek"
},
{
"type": "url",
"label": "Link screen",
"key": "linkURL"
},
{
"section": true,
"name": "Advanced",
"settings": [
{
"type": "field",
"label": "ID column for linking (appended to URL)",
"key": "linkColumn",
"placeholder": "Default"
}
]
} }
], ],
"context": { "context": {
@ -2568,10 +2596,539 @@
"key": "linkURL", "key": "linkURL",
"label": "Link URL" "label": "Link URL"
}, },
{
"type": "boolean",
"key": "linkPeek",
"label": "Open link in modal"
},
{ {
"type": "boolean", "type": "boolean",
"key": "horizontal", "key": "horizontal",
"label": "Horizontal" "label": "Horizontal"
},
{
"type": "boolean",
"label": "Show button",
"key": "showButton"
},
{
"type": "text",
"key": "buttonText",
"label": "Button text"
},
{
"type": "event",
"label": "Button action",
"key": "buttonOnClick"
}
]
},
"tableblock": {
"block": true,
"name": "Table block",
"icon": "Table",
"styles": ["size"],
"info": "Only the first 3 search columns will be used.",
"settings": [
{
"type": "text",
"label": "Title",
"key": "title"
},
{
"type": "dataSource",
"label": "Data",
"key": "dataSource"
},
{
"type": "multifield",
"label": "Search Columns",
"key": "searchColumns",
"placeholder": "Choose search columns"
},
{
"type": "filter",
"label": "Filtering",
"key": "filter"
},
{
"type": "field",
"label": "Sort Column",
"key": "sortColumn"
},
{
"type": "select",
"label": "Sort Order",
"key": "sortOrder",
"options": ["Ascending", "Descending"],
"defaultValue": "Descending"
},
{
"type": "select",
"label": "Size",
"key": "size",
"defaultValue": "spectrum--medium",
"options": [
{
"label": "Medium",
"value": "spectrum--medium"
},
{
"label": "Large",
"value": "spectrum--large"
}
]
},
{
"type": "boolean",
"label": "Paginate",
"key": "paginate",
"defaultValue": true
},
{
"section": true,
"name": "Table",
"settings": [
{
"type": "number",
"label": "Row Count",
"key": "rowCount",
"defaultValue": 8
},
{
"type": "multifield",
"label": "Table Columns",
"key": "tableColumns",
"dependsOn": "dataSource",
"placeholder": "All columns"
},
{
"type": "boolean",
"label": "Quiet table variant",
"key": "quiet"
},
{
"type": "boolean",
"label": "Show auto columns",
"key": "showAutoColumns"
},
{
"type": "boolean",
"label": "Link table rows",
"key": "linkRows"
},
{
"type": "boolean",
"label": "Open link in modal",
"key": "linkPeek"
},
{
"type": "url",
"label": "Link screen",
"key": "linkURL"
}
]
},
{
"section": true,
"name": "Title button",
"settings": [
{
"type": "boolean",
"key": "showTitleButton",
"label": "Show link button",
"defaultValue": false
},
{
"type": "boolean",
"label": "Open link in modal",
"key": "titleButtonPeek"
},
{
"type": "text",
"key": "titleButtonText",
"label": "Button text"
},
{
"type": "url",
"label": "Button link",
"key": "titleButtonURL"
}
]
},
{
"section": true,
"name": "Advanced",
"settings": [
{
"type": "field",
"label": "ID column for linking (appended to URL)",
"key": "linkColumn",
"placeholder": "Default"
}
]
}
]
},
"cardsblock": {
"block": true,
"name": "Cards block",
"icon": "Table",
"styles": ["size"],
"info": "Only the first 3 search columns will be used.",
"settings": [
{
"type": "text",
"label": "Title",
"key": "title"
},
{
"type": "dataSource",
"label": "Data",
"key": "dataSource"
},
{
"type": "multifield",
"label": "Search Columns",
"key": "searchColumns",
"placeholder": "Choose search columns"
},
{
"type": "filter",
"label": "Filtering",
"key": "filter"
},
{
"type": "field",
"label": "Sort Column",
"key": "sortColumn"
},
{
"type": "select",
"label": "Sort Order",
"key": "sortOrder",
"options": ["Ascending", "Descending"],
"defaultValue": "Descending"
},
{
"type": "number",
"label": "Limit",
"key": "limit",
"defaultValue": 8
},
{
"type": "boolean",
"label": "Paginate",
"key": "paginate"
},
{
"section": true,
"name": "Cards",
"settings": [
{
"type": "text",
"key": "cardTitle",
"label": "Title",
"nested": true
},
{
"type": "text",
"key": "cardSubtitle",
"label": "Subtitle",
"nested": true
},
{
"type": "text",
"key": "cardDescription",
"label": "Description",
"nested": true
},
{
"type": "text",
"key": "cardImageURL",
"label": "Image URL",
"nested": true
},
{
"type": "boolean",
"key": "linkCardTitle",
"label": "Link card title"
},
{
"type": "boolean",
"key": "cardPeek",
"label": "Open link in modal"
},
{
"type": "url",
"label": "Link screen",
"key": "cardURL",
"nested": true
},
{
"type": "boolean",
"key": "cardHorizontal",
"label": "Horizontal"
},
{
"type": "boolean",
"label": "Show button",
"key": "showCardButton"
},
{
"type": "text",
"key": "cardButtonText",
"label": "Button text",
"nested": true
},
{
"type": "event",
"label": "Button action",
"key": "cardButtonOnClick",
"nested": true
}
]
},
{
"section": true,
"name": "Title button",
"settings": [
{
"type": "boolean",
"key": "showTitleButton",
"label": "Show link button"
},
{
"type": "boolean",
"label": "Open link in modal",
"key": "titleButtonPeek"
},
{
"type": "text",
"key": "titleButtonText",
"label": "Button text"
},
{
"type": "url",
"label": "Button link",
"key": "titleButtonURL"
}
]
},
{
"section": true,
"name": "Advanced",
"settings": [
{
"type": "field",
"label": "ID column for linking (appended to URL)",
"key": "linkColumn",
"placeholder": "Default"
}
]
}
],
"context": {
"type": "schema",
"suffix": "repeater"
}
},
"repeaterblock": {
"name": "Repeater block",
"icon": "ViewList",
"illegalChildren": ["section"],
"hasChildren": true,
"showSettingsBar": true,
"settings": [
{
"type": "dataSource",
"label": "Data",
"key": "dataSource"
},
{
"type": "filter",
"label": "Filtering",
"key": "filter"
},
{
"type": "field",
"label": "Sort Column",
"key": "sortColumn"
},
{
"type": "select",
"label": "Sort Order",
"key": "sortOrder",
"options": ["Ascending", "Descending"],
"defaultValue": "Descending"
},
{
"type": "number",
"label": "Limit",
"key": "limit",
"defaultValue": 10
},
{
"type": "boolean",
"label": "Paginate",
"key": "paginate"
},
{
"section": true,
"name": "Layout settings",
"settings": [
{
"type": "text",
"label": "Empty Text",
"key": "noRowsMessage",
"defaultValue": "No rows found"
},
{
"type": "select",
"label": "Direction",
"key": "direction",
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "Column",
"value": "column",
"barIcon": "ViewRow",
"barTitle": "Column layout"
},
{
"label": "Row",
"value": "row",
"barIcon": "ViewColumn",
"barTitle": "Row layout"
}
],
"defaultValue": "column"
},
{
"type": "select",
"label": "Horiz. Align",
"key": "hAlign",
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "Left",
"value": "left",
"barIcon": "AlignLeft",
"barTitle": "Align left"
},
{
"label": "Center",
"value": "center",
"barIcon": "AlignCenter",
"barTitle": "Align center"
},
{
"label": "Right",
"value": "right",
"barIcon": "AlignRight",
"barTitle": "Align right"
},
{
"label": "Stretch",
"value": "stretch",
"barIcon": "MoveLeftRight",
"barTitle": "Align stretched horizontally"
}
],
"defaultValue": "stretch"
},
{
"type": "select",
"label": "Vert. Align",
"key": "vAlign",
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "Top",
"value": "top",
"barIcon": "AlignTop",
"barTitle": "Align top"
},
{
"label": "Middle",
"value": "middle",
"barIcon": "AlignMiddle",
"barTitle": "Align middle"
},
{
"label": "Bottom",
"value": "bottom",
"barIcon": "AlignBottom",
"barTitle": "Align bottom"
},
{
"label": "Stretch",
"value": "stretch",
"barIcon": "MoveUpDown",
"barTitle": "Align stretched vertically"
}
],
"defaultValue": "top"
},
{
"type": "select",
"label": "Gap",
"key": "gap",
"showInBar": true,
"barStyle": "picker",
"options": [
{
"label": "None",
"value": "N"
},
{
"label": "Small",
"value": "S"
},
{
"label": "Medium",
"value": "M"
},
{
"label": "Large",
"value": "L"
}
],
"defaultValue": "M"
}
]
}
],
"context": [
{
"type": "static",
"suffix": "provider",
"values": [
{
"label": "Rows",
"key": "rows"
},
{
"label": "Rows Length",
"key": "rowsLength"
},
{
"label": "Schema",
"key": "schema"
},
{
"label": "Page Number",
"key": "pageNumber"
}
]
},
{
"type": "schema",
"suffix": "repeater"
} }
] ]
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "0.9.175", "version": "0.9.185-alpha.10",
"license": "MPL-2.0", "license": "MPL-2.0",
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^0.9.175", "@budibase/bbui": "^0.9.185-alpha.10",
"@budibase/standard-components": "^0.9.139", "@budibase/standard-components": "^0.9.139",
"@budibase/string-templates": "^0.9.175", "@budibase/string-templates": "^0.9.185-alpha.10",
"regexparam": "^1.3.0", "regexparam": "^1.3.0",
"shortid": "^2.2.15", "shortid": "^2.2.15",
"svelte-spa-router": "^3.0.5" "svelte-spa-router": "^3.0.5"

View File

@ -1,8 +1,9 @@
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { fetchTableData } from "./tables" import { fetchTableData, fetchTableDefinition } from "./tables"
import { fetchViewData } from "./views" import { fetchViewData } from "./views"
import { fetchRelationshipData } from "./relationships" import { fetchRelationshipData } from "./relationships"
import { executeQuery } from "./queries" import { FieldTypes } from "../constants"
import { executeQuery, fetchQueryDefinition } from "./queries"
/** /**
* Fetches all rows for a particular Budibase data source. * Fetches all rows for a particular Budibase data source.
@ -28,7 +29,7 @@ export const fetchDatasource = async dataSource => {
} }
} }
rows = await executeQuery({ queryId: dataSource._id, parameters }) rows = await executeQuery({ queryId: dataSource._id, parameters })
} else if (type === "link") { } else if (type === FieldTypes.LINK) {
rows = await fetchRelationshipData({ rows = await fetchRelationshipData({
rowId: dataSource.rowId, rowId: dataSource.rowId,
tableId: dataSource.rowTableId, tableId: dataSource.rowTableId,
@ -39,3 +40,55 @@ export const fetchDatasource = async dataSource => {
// Enrich the result is always an array // Enrich the result is always an array
return Array.isArray(rows) ? rows : [] return Array.isArray(rows) ? rows : []
} }
/**
* Fetches the schema of any kind of datasource.
*/
export const fetchDatasourceSchema = async dataSource => {
if (!dataSource) {
return null
}
const { type } = dataSource
// Nested providers should already have exposed their own schema
if (type === "provider") {
return dataSource.value?.schema
}
// Field sources have their schema statically defined
if (type === "field") {
if (dataSource.fieldType === "attachment") {
return {
url: {
type: "string",
},
name: {
type: "string",
},
}
} else if (dataSource.fieldType === "array") {
return {
value: {
type: "string",
},
}
}
}
// Tables, views and links can be fetched by table ID
if (
(type === "table" || type === "view" || type === "link") &&
dataSource.tableId
) {
const table = await fetchTableDefinition(dataSource.tableId)
return table?.schema
}
// Queries can be fetched by query ID
if (type === "query" && dataSource._id) {
const definition = await fetchQueryDefinition(dataSource._id)
return definition?.schema
}
return null
}

View File

@ -5,7 +5,7 @@ import API from "./api"
* Executes a query against an external data connector. * Executes a query against an external data connector.
*/ */
export const executeQuery = async ({ queryId, parameters }) => { export const executeQuery = async ({ queryId, parameters }) => {
const query = await API.get({ url: `/api/queries/${queryId}` }) const query = await fetchQueryDefinition(queryId)
if (query?.datasourceId == null) { if (query?.datasourceId == null) {
notificationStore.actions.error("That query couldn't be found") notificationStore.actions.error("That query couldn't be found")
return return
@ -24,3 +24,10 @@ export const executeQuery = async ({ queryId, parameters }) => {
} }
return res return res
} }
/**
* Fetches the definition of an external query.
*/
export const fetchQueryDefinition = async queryId => {
return await API.get({ url: `/api/queries/${queryId}`, cache: true })
}

View File

@ -1,6 +1,7 @@
import { notificationStore, dataSourceStore } from "stores" import { notificationStore, dataSourceStore } from "stores"
import API from "./api" import API from "./api"
import { fetchTableDefinition } from "./tables" import { fetchTableDefinition } from "./tables"
import { FieldTypes } from "../constants"
/** /**
* Fetches data about a certain row in a table. * Fetches data about a certain row in a table.
@ -107,6 +108,8 @@ export const deleteRows = async ({ tableId, rows }) => {
/** /**
* Enriches rows which contain certain field types so that they can * Enriches rows which contain certain field types so that they can
* be properly displayed. * be properly displayed.
* The ability to create these bindings has been removed, but they will still
* exist in client apps to support backwards compatibility.
*/ */
export const enrichRows = async (rows, tableId) => { export const enrichRows = async (rows, tableId) => {
if (!Array.isArray(rows)) { if (!Array.isArray(rows)) {
@ -129,7 +132,7 @@ export const enrichRows = async (rows, tableId) => {
const keys = Object.keys(schema) const keys = Object.keys(schema)
for (let key of keys) { for (let key of keys) {
const type = schema[key].type const type = schema[key].type
if (type === "link" && Array.isArray(row[key])) { if (type === FieldTypes.LINK && Array.isArray(row[key])) {
// Enrich row a string join of relationship fields // Enrich row a string join of relationship fields
row[`${key}_text`] = row[`${key}_text`] =
row[key] row[key]

View File

@ -0,0 +1,12 @@
<script>
import { getContext, setContext } from "svelte"
const component = getContext("component")
// We need to set a block context to know we're inside a block, but also
// to be able to reference the actual component ID of the block from
// any depth
setContext("block", { id: $component.id })
</script>
<slot />

View File

@ -0,0 +1,35 @@
<script>
import { getContext } from "svelte"
import { generate } from "shortid"
import Component from "components/Component.svelte"
export let type
export let props
export let styles
export let context
// ID is only exposed as a prop so that it can be bound to from parent
// block components
export let id
const block = getContext("block")
const rand = generate()
// Create a fake component instance so that we can use the core Component
// to render this part of the block, taking advantage of binding enrichment
$: id = `${block.id}-${context ?? rand}`
$: instance = {
_component: `@budibase/standard-components/${type}`,
_id: id,
_styles: {
normal: {
...styles,
},
},
...props,
}
</script>
<Component {instance} isBlock>
<slot />
</Component>

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