Merge branch 'master' into features/enterprise-basic-plan

This commit is contained in:
jvcalderon 2023-11-07 13:40:42 +01:00
commit e2037b3335
91 changed files with 2055 additions and 952 deletions

View File

@ -1,4 +1,4 @@
name: Tag release name: Release
concurrency: concurrency:
group: tag-release group: tag-release
cancel-in-progress: false cancel-in-progress: false
@ -19,6 +19,8 @@ on:
jobs: jobs:
tag-release: tag-release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
version: ${{ steps.tag-release.outputs.version }}
steps: steps:
- name: Fail if branch is not master - name: Fail if branch is not master
@ -33,6 +35,7 @@ jobs:
- run: cd scripts && yarn - run: cd scripts && yarn
- name: Tag release - name: Tag release
id: tag-release
run: | run: |
cd scripts cd scripts
# setup the username and email. # setup the username and email.
@ -41,3 +44,23 @@ jobs:
BUMP_TYPE_INPUT=${{ github.event.inputs.versioning }} BUMP_TYPE_INPUT=${{ github.event.inputs.versioning }}
BUMP_TYPE=${BUMP_TYPE_INPUT:-"patch"} BUMP_TYPE=${BUMP_TYPE_INPUT:-"patch"}
./versionCommit.sh $BUMP_TYPE ./versionCommit.sh $BUMP_TYPE
new_version=$(./getCurrentVersion.sh)
echo "version=$new_version" >> $GITHUB_OUTPUT
trigger-release:
needs: [tag-release]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: peter-evans/repository-dispatch@v2
with:
repository: budibase/budibase-deploys
event-type: release-prod
token: ${{ secrets.GH_ACCESS_TOKEN }}
client-payload: |-
{
"TAG": "${{ needs.tag-release.outputs.version }}"
}

View File

@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a href="https://www.budibase.com"> <a href="https://www.budibase.com">
<img alt="Budibase" src="https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg" width="60" /> <img alt="Budibase" src="https://res.cloudinary.com/daog6scxm/image/upload/v1696515725/Branding/Assets/Symbol/RGB/Full%20Colour/Budibase_Symbol_RGB_FullColour_cbqvha_1_z5cwq2.svg" width="60" />
</a> </a>
</p> </p>
<h1 align="center"> <h1 align="center">

View File

@ -2,16 +2,18 @@ server {
listen 443 ssl default_server; listen 443 ssl default_server;
listen [::]:443 ssl default_server; listen [::]:443 ssl default_server;
server_name _; server_name _;
ssl_certificate /etc/letsencrypt/live/CUSTOM_DOMAIN/fullchain.pem; error_log /dev/stderr warn;
ssl_certificate_key /etc/letsencrypt/live/CUSTOM_DOMAIN/privkey.pem; access_log /dev/stdout main;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
client_max_body_size 1000m; client_max_body_size 1000m;
ignore_invalid_headers off; ignore_invalid_headers off;
proxy_buffering off; proxy_buffering off;
# port_in_redirect off; # port_in_redirect off;
ssl_certificate /etc/letsencrypt/live/CUSTOM_DOMAIN/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/CUSTOM_DOMAIN/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location ^~ /.well-known/acme-challenge/ { location ^~ /.well-known/acme-challenge/ {
default_type "text/plain"; default_type "text/plain";
root /var/www/html; root /var/www/html;
@ -47,6 +49,24 @@ server {
rewrite ^/worker/(.*)$ /$1 break; rewrite ^/worker/(.*)$ /$1 break;
} }
location /api/backups/ {
# calls to export apps are limited
limit_req zone=ratelimit burst=20 nodelay;
# 1800s timeout for app export requests
proxy_read_timeout 1800s;
proxy_connect_timeout 1800s;
proxy_send_timeout 1800s;
proxy_http_version 1.1;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:4001;
}
location /api/ { location /api/ {
# calls to the API are rate limited with bursting # calls to the API are rate limited with bursting
limit_req zone=ratelimit burst=20 nodelay; limit_req zone=ratelimit burst=20 nodelay;
@ -70,18 +90,49 @@ server {
rewrite ^/db/(.*)$ /$1 break; rewrite ^/db/(.*)$ /$1 break;
} }
location /socket/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_pass http://127.0.0.1:4001;
}
location / { location / {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_connect_timeout 300; proxy_connect_timeout 300;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Connection ""; proxy_set_header Connection "";
chunked_transfer_encoding off; chunked_transfer_encoding off;
proxy_pass http://127.0.0.1:9000; proxy_pass http://127.0.0.1:9000;
} }
location /files/signed/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# IMPORTANT: Signed urls will inspect the host header of the request.
# Normally a signed url will need to be generated with a specified client host in mind.
# To support dynamic hosts, e.g. some unknown self-hosted installation url,
# use a predefined host header. The host 'minio-service' is also used at the time of url signing.
proxy_set_header Host minio-service;
proxy_connect_timeout 300;
proxy_http_version 1.1;
proxy_set_header Connection "";
chunked_transfer_encoding off;
proxy_pass http://127.0.0.1:9000;
rewrite ^/files/signed/(.*)$ /$1 break;
}
client_header_timeout 60; client_header_timeout 60;
client_body_timeout 60; client_body_timeout 60;
keepalive_timeout 60; keepalive_timeout 60;

View File

@ -57,8 +57,8 @@
--spectrum-global-color-gray-600: rgb(144,144,144); --spectrum-global-color-gray-600: rgb(144,144,144);
--spectrum-global-color-gray-900: rgb(255,255,255); --spectrum-global-color-gray-900: rgb(255,255,255);
--spectrum-global-color-gray-800: rgb(227,227,227); --spectrum-global-color-gray-800: rgb(227,227,227);
--spectrum-global-color-static-blue-600: rgb(20,115,230); --bb-indigo: #6E56FF;
--spectrum-global-color-static-blue-hover: rgb( 18, 103, 207); --bb-indigo-light: #9F8FFF;
} }
html, body { html, body {
@ -90,15 +90,8 @@
.info { .info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: left; align-items: flex-start;
} }
@media only screen and (max-width: 600px) {
.info {
align-items: center;
}
}
.status { .status {
color: var(--spectrum-global-color-gray-600) color: var(--spectrum-global-color-gray-600)
} }
@ -113,13 +106,14 @@
.buttons { .buttons {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start;
margin-top: 15px; margin-top: 15px;
} }
.homeButton { .homeButton {
background-color: var(--spectrum-global-color-static-blue-600); background-color: var(--bb-indigo);
} }
.homeButton:hover { .homeButton:hover {
background-color: var(--spectrum-global-color-static-blue-hover); background-color: var(--bb-indigo-light);
} }
.statusButton { .statusButton {
background-color: transparent; background-color: transparent;
@ -127,20 +121,30 @@
border: none; border: none;
} }
.hero { .hero {
height: 160px; height: 60px;
width: 160px; margin: 10px 40px 10px 0;
margin-right: 80px; }
.hero img {
height: 100%;
} }
.content { .content {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: flex-end; align-items: center;
justify-content: center; justify-content: center;
padding: 0 40px;
}
h1 {
margin-bottom: 10px;
}
h3 {
margin-top: 0;
} }
@media only screen and (max-width: 600px) { @media only screen and (max-width: 600px) {
.content { .content {
flex-direction: column; flex-direction: column;
align-items: flex-start;
} }
} }
</style> </style>
@ -152,16 +156,15 @@
<div class="main"> <div class="main">
<div class="content"> <div class="content">
<div class="hero"> <div class="hero">
<img src="https://raw.githubusercontent.com/Budibase/budibase/master/packages/builder/assets/bb-space-man.svg" alt="Budibase Logo"> <img src="https://res.cloudinary.com/daog6scxm/image/upload/v1696515725/Branding/Assets/Symbol/RGB/Full%20Colour/Budibase_Symbol_RGB_FullColour_cbqvha_1_z5cwq2.svg" alt="Budibase Logo">
</div> </div>
<div class="info"> <div class="info">
<div> <div>
<h4 id="status" class="status"></h4> <h4 id="status" class="status">&nbsp;</h4>
<h1 class="title"> <h1 class="title">
Houston we have a problem! Houston we have a problem!
</h1> </h1>
<h3 id="message" class="message"> <h3 id="message" class="message">&nbsp;</h3>
</h3>
</div> </div>
<div class="buttons"> <div class="buttons">
<button class="homeButton" onclick=goHome()>Return home</button> <button class="homeButton" onclick=goHome()>Return home</button>

View File

@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a href="https://www.budibase.com"> <a href="https://www.budibase.com">
<img alt="Budibase" src="https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg" width="60" /> <img alt="Budibase" src="https://res.cloudinary.com/daog6scxm/image/upload/v1696515725/Branding/Assets/Symbol/RGB/Full%20Colour/Budibase_Symbol_RGB_FullColour_cbqvha_1_z5cwq2.svg" width="60" />
</a> </a>
</p> </p>
<h1 align="center"> <h1 align="center">

View File

@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a href="https://www.budibase.com"> <a href="https://www.budibase.com">
<img alt="Budibase" src="https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg" width="60" /> <img alt="Budibase" src="https://res.cloudinary.com/daog6scxm/image/upload/v1696515725/Branding/Assets/Symbol/RGB/Full%20Colour/Budibase_Symbol_RGB_FullColour_cbqvha_1_z5cwq2.svg" width="60" />
</a> </a>
</p> </p>
<h1 align="center"> <h1 align="center">

View File

@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a href="https://www.budibase.com"> <a href="https://www.budibase.com">
<img alt="Budibase" src="https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg" width="60" /> <img alt="Budibase" src="https://res.cloudinary.com/daog6scxm/image/upload/v1696515725/Branding/Assets/Symbol/RGB/Full%20Colour/Budibase_Symbol_RGB_FullColour_cbqvha_1_z5cwq2.svg" width="60" />
</a> </a>
</p> </p>
<h1 align="center"> <h1 align="center">

View File

@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a href="https://www.budibase.com"> <a href="https://www.budibase.com">
<img alt="Budibase" src="https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg" width="60" /> <img alt="Budibase" src="https://res.cloudinary.com/daog6scxm/image/upload/v1696515725/Branding/Assets/Symbol/RGB/Full%20Colour/Budibase_Symbol_RGB_FullColour_cbqvha_1_z5cwq2.svg" width="60" />
</a> </a>
</p> </p>
<h1 align="center"> <h1 align="center">

View File

@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a href="https://www.budibase.com"> <a href="https://www.budibase.com">
<img alt="Budibase" src="https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg" width="60" /> <img alt="Budibase" src="https://res.cloudinary.com/daog6scxm/image/upload/v1696515725/Branding/Assets/Symbol/RGB/Full%20Colour/Budibase_Symbol_RGB_FullColour_cbqvha_1_z5cwq2.svg" width="60" />
</a> </a>
</p> </p>
<h1 align="center"> <h1 align="center">

View File

@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a href="https://www.budibase.com"> <a href="https://www.budibase.com">
<img alt="Budibase" src="https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg" width="60" /> <img alt="Budibase" src="https://res.cloudinary.com/daog6scxm/image/upload/v1696515725/Branding/Assets/Symbol/RGB/Full%20Colour/Budibase_Symbol_RGB_FullColour_cbqvha_1_z5cwq2.svg" width="60" />
</a> </a>
</p> </p>
<h1 align="center"> <h1 align="center">

View File

@ -1,5 +1,5 @@
{ {
"version": "2.12.11", "version": "2.13.2",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -30,6 +30,7 @@ export * as timers from "./timers"
export { default as env } from "./environment" export { default as env } from "./environment"
export * as blacklist from "./blacklist" export * as blacklist from "./blacklist"
export * as docUpdates from "./docUpdates" export * as docUpdates from "./docUpdates"
export * from "./utils/Duration"
export { SearchParams } from "./db" export { SearchParams } from "./db"
// Add context to tenancy for backwards compatibility // Add context to tenancy for backwards compatibility
// only do this for external usages to prevent internal // only do this for external usages to prevent internal

View File

@ -36,7 +36,7 @@ class InMemoryQueue {
* @param opts This is not used by the in memory queue as there is no real use * @param opts This is not used by the in memory queue as there is no real use
* case when in memory, but is the same API as Bull * case when in memory, but is the same API as Bull
*/ */
constructor(name: string, opts = null) { constructor(name: string, opts?: any) {
this._name = name this._name = name
this._opts = opts this._opts = opts
this._messages = [] this._messages = []

View File

@ -2,11 +2,17 @@ import env from "../environment"
import { getRedisOptions } from "../redis/utils" import { getRedisOptions } from "../redis/utils"
import { JobQueue } from "./constants" import { JobQueue } from "./constants"
import InMemoryQueue from "./inMemoryQueue" import InMemoryQueue from "./inMemoryQueue"
import BullQueue from "bull" import BullQueue, { QueueOptions } from "bull"
import { addListeners, StalledFn } from "./listeners" import { addListeners, StalledFn } from "./listeners"
import { Duration } from "../utils"
import * as timers from "../timers" import * as timers from "../timers"
const CLEANUP_PERIOD_MS = 60 * 1000 // the queue lock is held for 5 minutes
const QUEUE_LOCK_MS = Duration.fromMinutes(5).toMs()
// queue lock is refreshed every 30 seconds
const QUEUE_LOCK_RENEW_INTERNAL_MS = Duration.fromSeconds(30).toMs()
// cleanup the queue every 60 seconds
const CLEANUP_PERIOD_MS = Duration.fromSeconds(60).toMs()
let QUEUES: BullQueue.Queue[] | InMemoryQueue[] = [] let QUEUES: BullQueue.Queue[] | InMemoryQueue[] = []
let cleanupInterval: NodeJS.Timeout let cleanupInterval: NodeJS.Timeout
@ -20,8 +26,15 @@ export function createQueue<T>(
jobQueue: JobQueue, jobQueue: JobQueue,
opts: { removeStalledCb?: StalledFn } = {} opts: { removeStalledCb?: StalledFn } = {}
): BullQueue.Queue<T> { ): BullQueue.Queue<T> {
const { opts: redisOpts, redisProtocolUrl } = getRedisOptions() const redisOpts = getRedisOptions()
const queueConfig: any = redisProtocolUrl || { redis: redisOpts } const queueConfig: QueueOptions = {
redis: redisOpts,
settings: {
maxStalledCount: 0,
lockDuration: QUEUE_LOCK_MS,
lockRenewTime: QUEUE_LOCK_RENEW_INTERNAL_MS,
},
}
let queue: any let queue: any
if (!env.isTest()) { if (!env.isTest()) {
queue = new BullQueue(jobQueue, queueConfig) queue = new BullQueue(jobQueue, queueConfig)

View File

@ -16,6 +16,7 @@ import {
getRedisOptions, getRedisOptions,
SEPARATOR, SEPARATOR,
SelectableDatabase, SelectableDatabase,
getRedisConnectionDetails,
} from "./utils" } from "./utils"
import * as timers from "../timers" import * as timers from "../timers"
@ -91,12 +92,11 @@ function init(selectDb = DEFAULT_SELECT_DB) {
if (client) { if (client) {
client.disconnect() client.disconnect()
} }
const { redisProtocolUrl, opts, host, port } = getRedisOptions() const { host, port } = getRedisConnectionDetails()
const opts = getRedisOptions()
if (CLUSTERED) { if (CLUSTERED) {
client = new RedisCore.Cluster([{ host, port }], opts) client = new RedisCore.Cluster([{ host, port }], opts)
} else if (redisProtocolUrl) {
client = new RedisCore(redisProtocolUrl)
} else { } else {
client = new RedisCore(opts) client = new RedisCore(opts)
} }

View File

@ -1,4 +1,5 @@
import env from "../environment" import env from "../environment"
import * as Redis from "ioredis"
const SLOT_REFRESH_MS = 2000 const SLOT_REFRESH_MS = 2000
const CONNECT_TIMEOUT_MS = 10000 const CONNECT_TIMEOUT_MS = 10000
@ -42,7 +43,7 @@ export enum Databases {
export enum SelectableDatabase { export enum SelectableDatabase {
DEFAULT = 0, DEFAULT = 0,
SOCKET_IO = 1, SOCKET_IO = 1,
UNUSED_1 = 2, RATE_LIMITING = 2,
UNUSED_2 = 3, UNUSED_2 = 3,
UNUSED_3 = 4, UNUSED_3 = 4,
UNUSED_4 = 5, UNUSED_4 = 5,
@ -58,7 +59,7 @@ export enum SelectableDatabase {
UNUSED_14 = 15, UNUSED_14 = 15,
} }
export function getRedisOptions() { export function getRedisConnectionDetails() {
let password = env.REDIS_PASSWORD let password = env.REDIS_PASSWORD
let url: string[] | string = env.REDIS_URL.split("//") let url: string[] | string = env.REDIS_URL.split("//")
// get rid of the protocol // get rid of the protocol
@ -74,28 +75,34 @@ export function getRedisOptions() {
} }
const [host, port] = url.split(":") const [host, port] = url.split(":")
let redisProtocolUrl return {
host,
// fully qualified redis URL password,
if (/rediss?:\/\//.test(env.REDIS_URL)) { port: parseInt(port),
redisProtocolUrl = env.REDIS_URL
} }
}
const opts: any = { export function getRedisOptions() {
const { host, password, port } = getRedisConnectionDetails()
let redisOpts: Redis.RedisOptions = {
connectTimeout: CONNECT_TIMEOUT_MS, connectTimeout: CONNECT_TIMEOUT_MS,
port: port,
host,
password,
} }
let opts: Redis.ClusterOptions | Redis.RedisOptions = redisOpts
if (env.REDIS_CLUSTERED) { if (env.REDIS_CLUSTERED) {
opts.redisOptions = {} opts = {
opts.redisOptions.tls = {} connectTimeout: CONNECT_TIMEOUT_MS,
opts.redisOptions.password = password redisOptions: {
opts.slotsRefreshTimeout = SLOT_REFRESH_MS ...redisOpts,
opts.dnsLookup = (address: string, callback: any) => callback(null, address) tls: {},
} else { },
opts.host = host slotsRefreshTimeout: SLOT_REFRESH_MS,
opts.port = port dnsLookup: (address: string, callback: any) => callback(null, address),
opts.password = password } as Redis.ClusterOptions
} }
return { opts, host, port: parseInt(port), redisProtocolUrl } return opts
} }
export function addDbPrefix(db: string, key: string) { export function addDbPrefix(db: string, key: string) {

View File

@ -0,0 +1,49 @@
export enum DurationType {
MILLISECONDS = "milliseconds",
SECONDS = "seconds",
MINUTES = "minutes",
HOURS = "hours",
DAYS = "days",
}
const conversion: Record<DurationType, number> = {
milliseconds: 1,
seconds: 1000,
minutes: 60 * 1000,
hours: 60 * 60 * 1000,
days: 24 * 60 * 60 * 1000,
}
export class Duration {
static convert(from: DurationType, to: DurationType, duration: number) {
const milliseconds = duration * conversion[from]
return milliseconds / conversion[to]
}
static from(from: DurationType, duration: number) {
return {
to: (to: DurationType) => {
return Duration.convert(from, to, duration)
},
toMs: () => {
return Duration.convert(from, DurationType.MILLISECONDS, duration)
},
}
}
static fromSeconds(duration: number) {
return Duration.from(DurationType.SECONDS, duration)
}
static fromMinutes(duration: number) {
return Duration.from(DurationType.MINUTES, duration)
}
static fromHours(duration: number) {
return Duration.from(DurationType.HOURS, duration)
}
static fromDays(duration: number) {
return Duration.from(DurationType.DAYS, duration)
}
}

View File

@ -1,3 +1,4 @@
export * from "./hashing" export * from "./hashing"
export * from "./utils" export * from "./utils"
export * from "./stringUtils" export * from "./stringUtils"
export * from "./Duration"

View File

@ -0,0 +1,19 @@
import { Duration, DurationType } from "../Duration"
describe("duration", () => {
it("should convert minutes to milliseconds", () => {
expect(Duration.fromMinutes(5).toMs()).toBe(300000)
})
it("should convert seconds to milliseconds", () => {
expect(Duration.fromSeconds(30).toMs()).toBe(30000)
})
it("should convert days to milliseconds", () => {
expect(Duration.fromDays(1).toMs()).toBe(86400000)
})
it("should convert minutes to days", () => {
expect(Duration.fromMinutes(1440).to(DurationType.DAYS)).toBe(1)
})
})

View File

@ -8,6 +8,7 @@
export let id = null export let id = null
export let text = null export let text = null
export let disabled = false export let disabled = false
export let readonly = false
export let size export let size
export let indeterminate = false export let indeterminate = false
@ -24,6 +25,7 @@
class:is-invalid={!!error} class:is-invalid={!!error}
class:checked={value} class:checked={value}
class:is-indeterminate={indeterminate} class:is-indeterminate={indeterminate}
class:readonly
> >
<input <input
checked={value} checked={value}
@ -68,4 +70,7 @@
.spectrum-Checkbox-input { .spectrum-Checkbox-input {
opacity: 0; opacity: 0;
} }
.readonly {
pointer-events: none;
}
</style> </style>

View File

@ -8,6 +8,7 @@
export let options = [] export let options = []
export let error = null export let error = null
export let disabled = false export let disabled = false
export let readonly = false
export let getOptionLabel = option => option export let getOptionLabel = option => option
export let getOptionValue = option => option export let getOptionValue = option => option
@ -34,6 +35,7 @@
title={getOptionLabel(option)} title={getOptionLabel(option)}
class="spectrum-Checkbox spectrum-FieldGroup-item" class="spectrum-Checkbox spectrum-FieldGroup-item"
class:is-invalid={!!error} class:is-invalid={!!error}
class:readonly
> >
<label <label
class="spectrum-Checkbox spectrum-Checkbox--sizeM spectrum-FieldGroup-item" class="spectrum-Checkbox spectrum-Checkbox--sizeM spectrum-FieldGroup-item"
@ -66,4 +68,7 @@
.spectrum-Checkbox-input { .spectrum-Checkbox-input {
opacity: 0; opacity: 0;
} }
.readonly {
pointer-events: none;
}
</style> </style>

View File

@ -9,6 +9,7 @@
export let id = null export let id = null
export let disabled = false export let disabled = false
export let readonly = false
export let error = null export let error = null
export let enableTime = true export let enableTime = true
export let value = null export let value = null
@ -186,7 +187,7 @@
> >
<div <div
id={flatpickrId} id={flatpickrId}
class:is-disabled={disabled} class:is-disabled={disabled || readonly}
class:is-invalid={!!error} class:is-invalid={!!error}
class="flatpickr spectrum-InputGroup spectrum-Datepicker" class="flatpickr spectrum-InputGroup spectrum-Datepicker"
class:is-focused={open} class:is-focused={open}
@ -211,6 +212,7 @@
{/if} {/if}
<input <input
{disabled} {disabled}
{readonly}
data-input data-input
type="text" type="text"
class="spectrum-Textfield-input spectrum-InputGroup-input" class="spectrum-Textfield-input spectrum-InputGroup-input"

View File

@ -386,7 +386,7 @@
} }
.compact .placeholder, .compact .placeholder,
.compact img { .compact img {
margin: 10px 16px; margin: 8px 16px;
} }
.compact img { .compact img {
height: 90px; height: 90px;
@ -456,6 +456,12 @@
color: var(--red); color: var(--red);
} }
.spectrum-Dropzone {
height: 220px;
}
.compact .spectrum-Dropzone {
height: 40px;
}
.spectrum-Dropzone.disabled { .spectrum-Dropzone.disabled {
pointer-events: none; pointer-events: none;
background-color: var(--spectrum-global-color-gray-200); background-color: var(--spectrum-global-color-gray-200);
@ -463,10 +469,6 @@
.disabled .spectrum-Heading--sizeL { .disabled .spectrum-Heading--sizeL {
color: var(--spectrum-alias-text-color-disabled); color: var(--spectrum-alias-text-color-disabled);
} }
.compact .spectrum-Dropzone {
padding-top: 8px;
padding-bottom: 8px;
}
.compact .spectrum-IllustratedMessage-description { .compact .spectrum-IllustratedMessage-description {
margin: 0; margin: 0;
} }
@ -477,7 +479,6 @@
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
} }
.tag { .tag {
margin-top: 8px; margin-top: 8px;
} }

View File

@ -8,6 +8,7 @@
export let options = [] export let options = []
export let error = null export let error = null
export let disabled = false export let disabled = false
export let readonly = false
export let getOptionLabel = option => option export let getOptionLabel = option => option
export let getOptionValue = option => option export let getOptionValue = option => option
export let getOptionTitle = option => option export let getOptionTitle = option => option
@ -40,6 +41,7 @@
title={getOptionTitle(option)} title={getOptionTitle(option)}
class="spectrum-Radio spectrum-FieldGroup-item spectrum-Radio--emphasized" class="spectrum-Radio spectrum-FieldGroup-item spectrum-Radio--emphasized"
class:is-invalid={!!error} class:is-invalid={!!error}
class:readonly
> >
<input <input
on:change={onChange} on:change={onChange}
@ -62,4 +64,7 @@
.spectrum-Radio-input { .spectrum-Radio-input {
opacity: 0; opacity: 0;
} }
.readonly {
pointer-events: none;
}
</style> </style>

View File

@ -4,6 +4,7 @@
export let value = "" export let value = ""
export let placeholder = null export let placeholder = null
export let disabled = false export let disabled = false
export let readonly = false
export let error = null export let error = null
export let height = null export let height = null
export let id = null export let id = null
@ -20,6 +21,7 @@
{fullScreenOffset} {fullScreenOffset}
{disabled} {disabled}
{easyMDEOptions} {easyMDEOptions}
{readonly}
on:change on:change
/> />
</div> </div>

View File

@ -5,6 +5,7 @@
export let value = "" export let value = ""
export let placeholder = null export let placeholder = null
export let disabled = false export let disabled = false
export let readonly = false
export let error = null export let error = null
export let id = null export let id = null
export let height = null export let height = null
@ -61,6 +62,7 @@
class="spectrum-Textfield-input" class="spectrum-Textfield-input"
style={align ? `text-align: ${align}` : ""} style={align ? `text-align: ${align}` : ""}
{disabled} {disabled}
{readonly}
{id} {id}
on:focus={() => (focus = true)} on:focus={() => (focus = true)}
on:blur={onChange} on:blur={onChange}

View File

@ -7,6 +7,7 @@
export let label = null export let label = null
export let labelPosition = "above" export let labelPosition = "above"
export let disabled = false export let disabled = false
export let readonly = false
export let error = null export let error = null
export let enableTime = true export let enableTime = true
export let timeOnly = false export let timeOnly = false
@ -33,6 +34,7 @@
<DatePicker <DatePicker
{error} {error}
{disabled} {disabled}
{readonly}
{value} {value}
{placeholder} {placeholder}
{enableTime} {enableTime}

View File

@ -8,6 +8,7 @@
export let id = null export let id = null
export let fullScreenOffset = 0 export let fullScreenOffset = 0
export let disabled = false export let disabled = false
export let readonly = false
export let easyMDEOptions export let easyMDEOptions
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -19,6 +20,9 @@
// control // control
$: checkValue(value) $: checkValue(value)
$: mde?.codemirror.on("change", debouncedUpdate) $: mde?.codemirror.on("change", debouncedUpdate)
$: if (readonly || disabled) {
mde?.togglePreview()
}
const checkValue = val => { const checkValue = val => {
if (mde && val !== latestValue) { if (mde && val !== latestValue) {
@ -54,6 +58,7 @@
easyMDEOptions={{ easyMDEOptions={{
initialValue: value, initialValue: value,
placeholder, placeholder,
toolbar: disabled || readonly ? false : undefined,
...easyMDEOptions, ...easyMDEOptions,
}} }}
/> />

View File

@ -2,6 +2,15 @@
--background: #ffffff; --background: #ffffff;
--ink: #000000; --ink: #000000;
/* Brand colours */
--bb-coral: #FF4E4E;
--bb-coral-light: #F97777;
--bb-indigo: #6E56FF;
--bb-indigo-light: #9F8FFF;
--bb-lime: #ECFFB5;
--bb-forest-green: #053835;
--bb-beige: #F6EFEA;
--grey-1: #fafafa; --grey-1: #fafafa;
--grey-2: #f5f5f5; --grey-2: #f5f5f5;
--grey-3: #eeeeee; --grey-3: #eeeeee;

View File

@ -6,3 +6,4 @@ release/
dist/ dist/
routify routify
.routify/ .routify/
svelte.config.js

View File

@ -1,80 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <svg width="265" height="265" viewBox="0 0 265 265" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Generator: Adobe Illustrator 24.1.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <g clip-path="url(#clip0_1_1799)">
<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" <path d="M158.2 8.6V116.6C158.2 121.3 162 125.2 166.8 125.2H213.8C218 125.2 222 123.2 224.6 119.8L262.9 68.9C265.7 65.2 265.7 60.1 262.9 56.4L224.6 5.4C222 2 218 0 213.8 0H166.8C162 0 158.2 3.8 158.2 8.6Z" fill="#FF4E4E"/>
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve"> <path d="M158.2 148.4V256.4C158.2 261.1 162 265 166.8 265H213.8C218 265 222 263 224.6 259.6L262.9 208.7C265.7 205 265.7 199.9 262.9 196.2L224.6 145.3C222.1 141.9 218.1 139.9 213.8 139.9H166.8C162 139.8 158.2 143.7 158.2 148.4Z" fill="#6E56FF"/>
<style type="text/css"> <path d="M0 8.6V116.6C0 121.3 3.8 125.2 8.6 125.2H109.6C113.8 125.2 117.8 123.2 120.4 119.8L155.9 72.5C160.3 66.6 160.3 58.5 155.9 52.6L120.3 5.4C117.8 2 113.8 0 109.5 0H8.6C3.8 0 0 3.8 0 8.6Z" fill="#F97777"/>
.st0{fill:#000000;} <path d="M0 148.4V256.4C0 261.1 3.8 265 8.6 265H109.6C113.8 265 117.8 263 120.4 259.6L155.9 212.3C160.3 206.4 160.3 198.3 155.9 192.4L120.4 145.1C117.9 141.7 113.9 139.7 109.6 139.7H8.6C3.8 139.8 0 143.7 0 148.4Z" fill="#9F8FFF"/>
.st1{fill:#FFFFFF;}
.st2{fill:#4285F4;}
</style>
<rect x="-152.17" y="-24.17" class="st0" width="96.17" height="96.17"/>
<path class="st1" d="M-83.19,48h-41.79c-1.76,0-3.19-1.43-3.19-3.19V3.02c0-1.76,1.43-3.19,3.19-3.19h41.79
c1.76,0,3.19,1.43,3.19,3.19v41.79C-80,46.57-81.43,48-83.19,48z"/>
<g>
<g>
<path class="st0" d="M-99.62,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35h-4.89V12.57H-99.62z
M-93.46,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C-93.55,28.92-93.46,28.52-93.46,28.11z"/>
</g>
<g>
<path class="st0" d="M-114.76,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58
c0.86,0.39,1.59,0.91,2.19,1.57c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89
c-0.35,0.9-0.84,1.68-1.47,2.35c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35
h-4.89V12.57H-114.76z M-108.6,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C-108.68,28.92-108.6,28.52-108.6,28.11z"/>
</g>
</g>
<path class="st2" d="M44.81,159H3.02c-1.76,0-3.19-1.43-3.19-3.19v-41.79c0-1.76,1.43-3.19,3.19-3.19h41.79
c1.76,0,3.19,1.43,3.19,3.19v41.79C48,157.57,46.57,159,44.81,159z"/>
<g>
<g>
<path class="st1" d="M28.38,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146h-4.89v-22.43H28.38z
M34.54,139.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C34.45,139.92,34.54,139.52,34.54,139.11z"/>
</g>
<g>
<path class="st1" d="M13.24,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146H8.35v-22.43H13.24z M19.4,139.11
c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69c-0.38-0.17-0.79-0.26-1.24-0.26
c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01c-0.17,0.39-0.26,0.8-0.26,1.23
c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68c0.39,0.17,0.8,0.26,1.23,0.26
c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1C19.32,139.92,19.4,139.52,19.4,139.11z"/>
</g>
</g>
<g>
<path class="st0" d="M44,48H4c-2.21,0-4-1.79-4-4V4c0-2.21,1.79-4,4-4h40c2.21,0,4,1.79,4,4v40C48,46.21,46.21,48,44,48z"/>
<g>
<path class="st1" d="M28.48,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
c-0.64,0.7-1.4,1.25-2.28,1.66C34.8,35.8,33.86,36,32.84,36c-1.84,0-3.3-0.69-4.37-2.07v1.62h-5V12H28.48z M34.78,28.31
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C34.69,29.16,34.78,28.75,34.78,28.31z"
/>
</g>
<g>
<path class="st1" d="M13,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
c-0.64,0.7-1.4,1.25-2.28,1.66C19.32,35.8,18.38,36,17.37,36c-1.84,0-3.3-0.69-4.37-2.07v1.62H8V12H13z M19.3,28.31
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C19.21,29.16,19.3,28.75,19.3,28.31z"/>
</g>
</g> </g>
<defs>
<clipPath id="clip0_1_1799">
<rect width="265" height="265" fill="white"/>
</clipPath>
</defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.2.3, 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 46.39 46.39" style="enable-background:new 0 0 46.39 46.39;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g>
<path class="st0" d="M45.28,22.9c-0.49,28.44-42.79,28.43-43.27,0C2.5-5.54,44.8-5.54,45.28,22.9z"/>
<path d="M23.65,45.45c-29.64-0.48-29.63-44.63,0-45.1C53.28,0.82,53.28,44.98,23.65,45.45z M23.65,2.18
c-27.09,0.09-27.09,41.36,0,41.44C50.74,43.53,50.74,2.26,23.65,2.18z"/>
<path d="M41.94,21.07C38.86,8.69,3.47,8.77,5.01,24.45C5.74,49.51,46.24,46.16,41.94,21.07z"/>
<path class="st0" d="M14.69,22.35c0.06,4.27-6.65,4.27-6.58,0C8.05,18.08,14.76,18.08,14.69,22.35z"/>
<path class="st0" d="M11.07,28.39c0.02,1.7-2.65,1.7-2.62,0C8.42,26.68,11.09,26.68,11.07,28.39z"/>
<path class="st0" d="M30.56,16.2c0.28-1.27,6.76,0.79,8.45,5.64c1.69,4.84-2.11,12.22-3.52,11.43
C36.02,25.33,37.44,22.84,30.56,16.2z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.2.3, 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 34.41 34.41" style="enable-background:new 0 0 34.41 34.41;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;stroke:url(#SVGID_1_);stroke-miterlimit:10;}
.st1{fill:url(#SVGID_2_);}
.st2{fill:#FFFFFF;}
</style>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="0.9311" y1="17.2025" x2="33.4739" y2="17.2025">
<stop offset="0" style="stop-color:#9E99FF"/>
<stop offset="1" style="stop-color:#5C45FF"/>
</linearGradient>
<path class="st0" d="M17.2,33.2c-21.03-0.34-21.03-31.67,0-32C38.23,1.54,38.23,32.87,17.2,33.2z"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="3.9435" y1="20.058" x2="30.407" y2="20.058">
<stop offset="0" style="stop-color:#9E99FF"/>
<stop offset="1" style="stop-color:#5C45FF"/>
</linearGradient>
<path class="st1" d="M30.18,15.91C27.99,7.12,2.89,7.18,3.98,18.3C4.5,36.09,33.23,33.71,30.18,15.91z"/>
<path class="st2" d="M6.42,21.1c-0.02-1.21,1.88-1.21,1.86,0C8.29,22.3,6.4,22.3,6.42,21.1z"/>
<path class="st2" d="M6.18,16.81c-0.04-3.03,4.72-3.03,4.67,0C10.89,19.84,6.13,19.84,6.18,16.81z"/>
<path class="st2" d="M25.61,24.56c0.38-5.63,1.38-7.4-3.5-12.11c0.2-0.9,4.8,0.56,6,4C29.3,19.88,26.61,25.12,25.61,24.56z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 787 B

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -3,6 +3,7 @@ import { API } from "api"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { generate } from "shortid" import { generate } from "shortid"
import { selectedAutomation } from "builderStore" import { selectedAutomation } from "builderStore"
import { notifications } from "@budibase/bbui"
const initialAutomationState = { const initialAutomationState = {
automations: [], automations: [],
@ -21,6 +22,37 @@ export const getAutomationStore = () => {
return store return store
} }
const updateReferencesInObject = (obj, modifiedIndex, action) => {
const regex = /{{\s*steps\.(\d+)\./g
for (const key in obj) {
if (typeof obj[key] === "string") {
let matches
while ((matches = regex.exec(obj[key])) !== null) {
const referencedStep = parseInt(matches[1])
if (action === "add" && referencedStep >= modifiedIndex) {
obj[key] = obj[key].replace(
`{{ steps.${referencedStep}.`,
`{{ steps.${referencedStep + 1}.`
)
} else if (action === "delete" && referencedStep > modifiedIndex) {
obj[key] = obj[key].replace(
`{{ steps.${referencedStep}.`,
`{{ steps.${referencedStep - 1}.`
)
}
}
} else if (typeof obj[key] === "object" && obj[key] !== null) {
updateReferencesInObject(obj[key], modifiedIndex, action)
}
}
}
const updateStepReferences = (steps, modifiedIndex, action) => {
steps.forEach(step => {
updateReferencesInObject(step.inputs, modifiedIndex, action)
})
}
const automationActions = store => ({ const automationActions = store => ({
definitions: async () => { definitions: async () => {
const response = await API.getAutomationDefinitions() const response = await API.getAutomationDefinitions()
@ -218,10 +250,40 @@ const automationActions = store => ({
if (!automation) { if (!automation) {
return return
} }
try {
updateStepReferences(newAutomation.definition.steps, blockIdx, "add")
} catch (e) {
notifications.error("Error adding automation block")
}
newAutomation.definition.steps.splice(blockIdx, 0, block) newAutomation.definition.steps.splice(blockIdx, 0, block)
await store.actions.save(newAutomation) await store.actions.save(newAutomation)
}, },
deleteAutomationBlock: async block => { saveAutomationName: async (blockId, name) => {
const automation = get(selectedAutomation)
let newAutomation = cloneDeep(automation)
if (!automation) {
return
}
newAutomation.definition.stepNames = {
...newAutomation.definition.stepNames,
[blockId]: name.trim(),
}
await store.actions.save(newAutomation)
},
deleteAutomationName: async blockId => {
const automation = get(selectedAutomation)
let newAutomation = cloneDeep(automation)
if (!automation) {
return
}
delete newAutomation.definition.stepNames[blockId]
await store.actions.save(newAutomation)
},
deleteAutomationBlock: async (block, blockIdx) => {
const automation = get(selectedAutomation) const automation = get(selectedAutomation)
let newAutomation = cloneDeep(automation) let newAutomation = cloneDeep(automation)
@ -233,7 +295,14 @@ const automationActions = store => ({
newAutomation.definition.steps = newAutomation.definition.steps.filter( newAutomation.definition.steps = newAutomation.definition.steps.filter(
step => step.id !== block.id step => step.id !== block.id
) )
delete newAutomation.definition.stepNames?.[block.id]
} }
try {
updateStepReferences(newAutomation.definition.steps, blockIdx, "delete")
} catch (e) {
notifications.error("Error deleting automation block")
}
await store.actions.save(newAutomation) await store.actions.save(newAutomation)
}, },
replace: async (automationId, automation) => { replace: async (automationId, automation) => {

View File

@ -5,13 +5,7 @@
import TestDataModal from "./TestDataModal.svelte" import TestDataModal from "./TestDataModal.svelte"
import { flip } from "svelte/animate" import { flip } from "svelte/animate"
import { fly } from "svelte/transition" import { fly } from "svelte/transition"
import { import { Icon, notifications, Modal } from "@budibase/bbui"
Heading,
Icon,
ActionButton,
notifications,
Modal,
} from "@budibase/bbui"
import { ActionStepID } from "constants/backend/automations" import { ActionStepID } from "constants/backend/automations"
import UndoRedoControl from "components/common/UndoRedoControl.svelte" import UndoRedoControl from "components/common/UndoRedoControl.svelte"
import { automationHistoryStore } from "builderStore" import { automationHistoryStore } from "builderStore"
@ -20,9 +14,8 @@
let testDataModal let testDataModal
let confirmDeleteDialog let confirmDeleteDialog
let scrolling = false
$: blocks = getBlocks(automation) $: blocks = getBlocks(automation).filter(x => x.stepId !== ActionStepID.LOOP)
const getBlocks = automation => { const getBlocks = automation => {
let blocks = [] let blocks = []
if (automation.definition.trigger) { if (automation.definition.trigger) {
@ -32,58 +25,72 @@
return blocks return blocks
} }
async function deleteAutomation() { const deleteAutomation = async () => {
try { try {
await automationStore.actions.delete($selectedAutomation) await automationStore.actions.delete($selectedAutomation)
} catch (error) { } catch (error) {
notifications.error("Error deleting automation") notifications.error("Error deleting automation")
} }
} }
const handleScroll = e => {
if (e.target.scrollTop >= 30) {
scrolling = true
} else if (e.target.scrollTop) {
// Set scrolling back to false if scrolled back to less than 100px
scrolling = false
}
}
</script> </script>
<div class="canvas"> <div class="header" class:scrolling>
<div class="header"> <div class="header-left">
<Heading size="S">{automation.name}</Heading> <UndoRedoControl store={automationHistoryStore} />
<div class="controls"> </div>
<UndoRedoControl store={automationHistoryStore} /> <div class="controls">
<div class="buttons">
<Icon hoverable size="M" name="Play" />
<div
on:click={() => {
testDataModal.show()
}}
>
Run test
</div>
</div>
<div class="buttons">
<Icon <Icon
on:click={confirmDeleteDialog.show} disabled={!$automationStore.testResults}
hoverable hoverable
size="M" size="M"
name="DeleteOutline" name="Multiple"
/> />
<div class="buttons"> <div
<ActionButton class:disabled={!$automationStore.testResults}
on:click={() => { on:click={() => {
testDataModal.show() $automationStore.showTestPanel = true
}} }}
icon="MultipleCheck" >
size="M">Run test</ActionButton Test details
>
<ActionButton
disabled={!$automationStore.testResults}
on:click={() => {
$automationStore.showTestPanel = true
}}
size="M">Test Details</ActionButton
>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="content"> <div class="canvas" on:scroll={handleScroll}>
{#each blocks as block, idx (block.id)} <div class="content">
<div {#each blocks as block, idx (block.id)}
class="block" <div
animate:flip={{ duration: 500 }} class="block"
in:fly={{ x: 500, duration: 500 }} animate:flip={{ duration: 500 }}
out:fly|local={{ x: 500, duration: 500 }} in:fly={{ x: 500, duration: 500 }}
> out:fly|local={{ x: 500, duration: 500 }}
{#if block.stepId !== ActionStepID.LOOP} >
<FlowItem {testDataModal} {block} {idx} /> {#if block.stepId !== ActionStepID.LOOP}
{/if} <FlowItem {testDataModal} {block} {idx} />
</div> {/if}
{/each} </div>
{/each}
</div>
</div> </div>
<ConfirmDialog <ConfirmDialog
bind:this={confirmDeleteDialog} bind:this={confirmDeleteDialog}
@ -103,6 +110,12 @@
<style> <style>
.canvas { .canvas {
padding: var(--spacing-l) var(--spacing-xl); padding: var(--spacing-l) var(--spacing-xl);
overflow-y: auto;
max-height: 100%;
}
.header-left :global(div) {
border-right: none;
} }
/* Fix for firefox not respecting bottom padding in scrolling containers */ /* Fix for firefox not respecting bottom padding in scrolling containers */
.canvas > *:last-child { .canvas > *:last-child {
@ -117,23 +130,45 @@
} }
.content { .content {
display: inline-block; flex-grow: 1;
text-align: left; padding: 23px 23px 80px;
box-sizing: border-box;
}
.header.scrolling {
background: var(--background);
border-bottom: var(--border-light);
border-left: var(--border-light);
z-index: 1;
} }
.header { .header {
z-index: 1;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding-left: var(--spacing-l);
transition: background 130ms ease-out;
flex: 0 0 48px;
padding-right: var(--spacing-xl);
}
.controls {
display: flex;
gap: var(--spacing-xl);
} }
.controls,
.buttons { .buttons {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
gap: var(--spacing-xl);
}
.buttons {
gap: var(--spacing-s); gap: var(--spacing-s);
} }
.buttons:hover {
cursor: pointer;
}
.disabled {
pointer-events: none;
color: var(--spectrum-global-color-gray-500) !important;
}
</style> </style>

View File

@ -7,20 +7,16 @@
Detail, Detail,
Modal, Modal,
Button, Button,
ActionButton,
notifications, notifications,
Label, Label,
AbsTooltip,
} from "@budibase/bbui" } from "@budibase/bbui"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte" import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
import ActionModal from "./ActionModal.svelte" import ActionModal from "./ActionModal.svelte"
import FlowItemHeader from "./FlowItemHeader.svelte" import FlowItemHeader from "./FlowItemHeader.svelte"
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte" import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
import { import { ActionStepID, TriggerStepID } from "constants/backend/automations"
ActionStepID,
TriggerStepID,
Features,
} from "constants/backend/automations"
import { permissions } from "stores/backend" import { permissions } from "stores/backend"
export let block export let block
@ -86,7 +82,7 @@
if (loopBlock) { if (loopBlock) {
await automationStore.actions.deleteAutomationBlock(loopBlock) await automationStore.actions.deleteAutomationBlock(loopBlock)
} }
await automationStore.actions.deleteAutomationBlock(block) await automationStore.actions.deleteAutomationBlock(block, blockIdx)
} catch (error) { } catch (error) {
notifications.error("Error saving automation") notifications.error("Error saving automation")
} }
@ -129,6 +125,10 @@
</div> </div>
<div class="blockTitle"> <div class="blockTitle">
<AbsTooltip type="negative" text="Remove looping">
<Icon on:click={removeLooping} hoverable name="DeleteOutline" />
</AbsTooltip>
<div style="margin-left: 10px;" on:click={() => {}}> <div style="margin-left: 10px;" on:click={() => {}}>
<Icon hoverable name={showLooping ? "ChevronDown" : "ChevronUp"} /> <Icon hoverable name={showLooping ? "ChevronDown" : "ChevronUp"} />
</div> </div>
@ -139,9 +139,6 @@
<Divider noMargin /> <Divider noMargin />
{#if !showLooping} {#if !showLooping}
<div class="blockSection"> <div class="blockSection">
<div class="block-options">
<ActionButton on:click={() => removeLooping()} icon="DeleteOutline" />
</div>
<Layout noPadding gap="S"> <Layout noPadding gap="S">
<AutomationBlockSetup <AutomationBlockSetup
schemaProperties={Object.entries( schemaProperties={Object.entries(
@ -162,31 +159,19 @@
{block} {block}
{testDataModal} {testDataModal}
{idx} {idx}
{addLooping}
{deleteStep}
on:toggle={() => (open = !open)} on:toggle={() => (open = !open)}
/> />
{#if open} {#if open}
<Divider noMargin /> <Divider noMargin />
<div class="blockSection"> <div class="blockSection">
<Layout noPadding gap="S"> <Layout noPadding gap="S">
{#if !isTrigger}
<div>
<div class="block-options">
{#if !loopBlock && (block?.features?.[Features.LOOPING] || !block.features)}
<ActionButton on:click={() => addLooping()} icon="Reuse">
Add Looping
</ActionButton>
{/if}
<ActionButton
on:click={() => deleteStep()}
icon="DeleteOutline"
/>
</div>
</div>
{/if}
{#if isAppAction} {#if isAppAction}
<Label>Role</Label> <div>
<RoleSelect bind:value={role} /> <Label>Role</Label>
<RoleSelect bind:value={role} />
</div>
{/if} {/if}
<AutomationBlockSetup <AutomationBlockSetup
schemaProperties={Object.entries(block.schema.inputs.properties)} schemaProperties={Object.entries(block.schema.inputs.properties)}
@ -270,5 +255,6 @@
.blockTitle { .blockTitle {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-s);
} }
</style> </style>

View File

@ -1,8 +1,9 @@
<script> <script>
import { automationStore } from "builderStore" import { automationStore, selectedAutomation } from "builderStore"
import { Icon, Body, Detail, StatusLight } from "@budibase/bbui" import { Icon, Body, StatusLight, AbsTooltip } from "@budibase/bbui"
import { externalActions } from "./ExternalActions" import { externalActions } from "./ExternalActions"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { Features } from "constants/backend/automations"
export let block export let block
export let open export let open
@ -10,9 +11,20 @@
export let testResult export let testResult
export let isTrigger export let isTrigger
export let idx export let idx
export let addLooping
export let deleteStep
let validRegex = /^[A-Za-z0-9_\s]+$/
let typing = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
$: stepNames = $selectedAutomation.definition.stepNames
$: automationName = stepNames?.[block.id] || block?.name || ""
$: automationNameError = getAutomationNameError(automationName)
$: status = updateStatus(testResult, isTrigger)
$: isHeaderTrigger = isTrigger || block.type === "TRIGGER"
$: { $: {
if (!testResult) { if (!testResult) {
testResult = $automationStore.testResults?.steps?.filter(step => testResult = $automationStore.testResults?.steps?.filter(step =>
@ -20,8 +32,9 @@
)?.[0] )?.[0]
} }
} }
$: isTrigger = isTrigger || block.type === "TRIGGER" $: loopBlock = $selectedAutomation.definition.steps.find(
$: status = updateStatus(testResult, isTrigger) x => x.blockToLoop === block?.id
)
async function onSelect(block) { async function onSelect(block) {
await automationStore.update(state => { await automationStore.update(state => {
@ -43,10 +56,49 @@
return { negative: true, message: "Error" } return { negative: true, message: "Error" }
} }
} }
const getAutomationNameError = name => {
if (stepNames) {
for (const [key, value] of Object.entries(stepNames)) {
if (name === value && key !== block.id) {
return "This name already exists, please enter a unique name"
}
}
}
if (name !== block.name && name?.length > 0) {
let invalidRoleName = !validRegex.test(name)
if (invalidRoleName) {
return "Please enter a role name consisting of only alphanumeric symbols and underscores"
}
return null
}
}
const startTyping = async () => {
typing = true
}
const saveName = async () => {
if (automationNameError || block.name === automationName) {
return
}
if (automationName.length === 0) {
await automationStore.actions.deleteAutomationName(block.id)
} else {
await automationStore.actions.saveAutomationName(block.id, automationName)
}
}
</script> </script>
<div class="blockSection"> <div
<div on:click={() => dispatch("toggle")} class="splitHeader"> class:typing={typing && !automationNameError}
class:typing-error={automationNameError}
class="blockSection"
>
<div class="splitHeader">
<div class="center-items"> <div class="center-items">
{#if externalActions[block.stepId]} {#if externalActions[block.stepId]}
<img <img
@ -67,40 +119,104 @@
</svg> </svg>
{/if} {/if}
<div class="iconAlign"> <div class="iconAlign">
{#if isTrigger} {#if isHeaderTrigger}
<Body size="XS"><b>Trigger</b></Body> <Body size="XS"><b>Trigger</b></Body>
<Body size="XS">When this happens:</Body>
{:else} {:else}
<Body size="XS"><b>Step {idx}</b></Body> <div style="margin-left: 2px;">
<Body size="XS">Do this:</Body> <Body size="XS"><b>Step {idx}</b></Body>
</div>
{/if} {/if}
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail> <input
placeholder="Enter some text"
name="name"
autocomplete="off"
value={automationName}
on:input={e => {
automationName = e.target.value.trim()
}}
on:click={startTyping}
on:blur={async () => {
typing = false
if (automationNameError) {
automationName = stepNames[block.id] || block?.name
} else {
await saveName()
}
}}
/>
</div> </div>
</div> </div>
<div class="blockTitle"> <div class="blockTitle">
{#if showTestStatus && testResult} {#if showTestStatus && testResult}
<div style="float: right;"> <div class="status-container">
<StatusLight <div style="float:right;">
positive={status?.positive} <StatusLight
yellow={status?.yellow} positive={status?.positive}
negative={status?.negative} yellow={status?.yellow}
><Body size="XS">{status?.message}</Body></StatusLight negative={status?.negative}
> >
<Body size="XS">{status?.message}</Body>
</StatusLight>
</div>
<Icon
on:click={() => dispatch("toggle")}
hoverable
name={open ? "ChevronUp" : "ChevronDown"}
/>
</div> </div>
{/if} {/if}
<div <div
style="margin-left: 10px; margin-bottom: var(--spacing-xs);" class="context-actions"
class:hide-context-actions={typing}
on:click={() => { on:click={() => {
onSelect(block) onSelect(block)
}} }}
> >
<Icon hoverable name={open ? "ChevronUp" : "ChevronDown"} /> {#if !showTestStatus}
{#if !isHeaderTrigger && !loopBlock && (block?.features?.[Features.LOOPING] || !block.features)}
<AbsTooltip type="info" text="Add looping">
<Icon on:click={addLooping} hoverable name="RotateCW" />
</AbsTooltip>
{/if}
<AbsTooltip type="negative" text="Delete step">
<Icon on:click={deleteStep} hoverable name="DeleteOutline" />
</AbsTooltip>
{/if}
{#if !showTestStatus}
<Icon
on:click={() => dispatch("toggle")}
hoverable
name={open ? "ChevronUp" : "ChevronDown"}
/>
{/if}
</div> </div>
{#if automationNameError}
<div class="error-container">
<AbsTooltip type="negative" text={automationNameError}>
<div class="error-icon">
<Icon size="S" name="Alert" />
</div>
</AbsTooltip>
</div>
{/if}
</div> </div>
</div> </div>
</div> </div>
<style> <style>
.status-container {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-m);
/* You can also add padding or margin to adjust the spacing between the text and the chevron if needed. */
}
.context-actions {
display: flex;
gap: var(--spacing-l);
margin-bottom: var(--spacing-xs);
}
.center-items { .center-items {
display: flex; display: flex;
align-items: center; align-items: center;
@ -117,10 +233,55 @@
.blockSection { .blockSection {
padding: var(--spacing-xl); padding: var(--spacing-xl);
border: 1px solid transparent;
} }
.blockTitle { .blockTitle {
display: flex; display: flex;
align-items: center; }
.hide-context-actions {
display: none;
}
input {
font-family: var(--font-sans);
color: var(--ink);
background-color: transparent;
border: 1px solid transparent;
font-size: var(--spectrum-alias-font-size-default);
width: 230px;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
input:focus {
outline: none;
}
/* Hide arrows for number fields */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.typing {
border: 1px solid var(--spectrum-global-color-static-blue-500);
border-radius: 4px 4px 4px 4px;
}
.typing-error {
border: 1px solid var(--spectrum-global-color-static-red-500);
border-radius: 4px 4px 4px 4px;
}
.error-icon :global(.spectrum-Icon) {
fill: var(--spectrum-global-color-red-400);
}
.error-container {
padding-top: var(--spacing-xl);
} }
</style> </style>

View File

@ -60,6 +60,7 @@
<ModalContent <ModalContent
title="Add test data" title="Add test data"
confirmText="Test" confirmText="Test"
size="M"
showConfirmButton={true} showConfirmButton={true}
disabled={isError} disabled={isError}
onConfirm={testAutomation} onConfirm={testAutomation}

View File

@ -58,7 +58,6 @@
let fillWidth = true let fillWidth = true
let inputData let inputData
let codeBindingOpen = false let codeBindingOpen = false
$: filters = lookForFilters(schemaProperties) || [] $: filters = lookForFilters(schemaProperties) || []
$: tempFilters = filters $: tempFilters = filters
$: stepId = block.stepId $: stepId = block.stepId
@ -155,7 +154,7 @@
} }
let blockIdx = allSteps.findIndex(step => step.id === block.id) let blockIdx = allSteps.findIndex(step => step.id === block.id)
// Extract all outputs from all previous steps as available bindins // Extract all outputs from all previous steps as available bindingsx§x
let bindings = [] let bindings = []
let loopBlockCount = 0 let loopBlockCount = 0
for (let idx = 0; idx < blockIdx; idx++) { for (let idx = 0; idx < blockIdx; idx++) {
@ -183,20 +182,19 @@
} }
} }
const outputs = Object.entries(schema) const outputs = Object.entries(schema)
let bindingIcon = "" let bindingIcon = ""
let bindindingRank = 0 let bindingRank = 0
if (idx === 0) { if (idx === 0) {
bindingIcon = automation.trigger.icon bindingIcon = automation.trigger.icon
} else if (isLoopBlock) { } else if (isLoopBlock) {
bindingIcon = "Reuse" bindingIcon = "Reuse"
bindindingRank = idx + 1 bindingRank = idx + 1
} else { } else {
bindingIcon = allSteps[idx].icon bindingIcon = allSteps[idx].icon
bindindingRank = idx - loopBlockCount bindingRank = idx - loopBlockCount
} }
let bindingName =
automation.stepNames?.[allSteps[idx - loopBlockCount].id]
bindings = bindings.concat( bindings = bindings.concat(
outputs.map(([name, value]) => { outputs.map(([name, value]) => {
let runtimeName = isLoopBlock let runtimeName = isLoopBlock
@ -205,14 +203,20 @@
? `steps[${idx - loopBlockCount}].${name}` ? `steps[${idx - loopBlockCount}].${name}`
: `steps.${idx - loopBlockCount}.${name}` : `steps.${idx - loopBlockCount}.${name}`
const runtime = idx === 0 ? `trigger.${name}` : runtimeName const runtime = idx === 0 ? `trigger.${name}` : runtimeName
const categoryName =
idx === 0 let categoryName
? "Trigger outputs" if (idx === 0) {
: isLoopBlock categoryName = "Trigger outputs"
? "Loop Outputs" } else if (isLoopBlock) {
: `Step ${idx - loopBlockCount} outputs` categoryName = "Loop Outputs"
} else if (bindingName) {
categoryName = `${bindingName} outputs`
} else {
categoryName = `Step ${idx - loopBlockCount} outputs`
}
return { return {
readableBinding: runtime, readableBinding: bindingName ? `${bindingName}.${name}` : runtime,
runtimeBinding: runtime, runtimeBinding: runtime,
type: value.type, type: value.type,
description: value.description, description: value.description,
@ -221,7 +225,7 @@
display: { display: {
type: value.type, type: value.type,
name: name, name: name,
rank: bindindingRank, rank: bindingRank,
}, },
} }
}) })
@ -277,6 +281,16 @@
return !dependsOn || !!inputData[dependsOn] return !dependsOn || !!inputData[dependsOn]
} }
function shouldRenderField(value) {
return (
value.customType !== "row" &&
value.customType !== "code" &&
value.customType !== "queryParams" &&
value.customType !== "cron" &&
value.customType !== "triggerSchema"
)
}
onMount(async () => { onMount(async () => {
try { try {
await environment.loadVariables() await environment.loadVariables()
@ -289,245 +303,248 @@
<div class="fields"> <div class="fields">
{#each schemaProperties as [key, value]} {#each schemaProperties as [key, value]}
{#if canShowField(key, value)} {#if canShowField(key, value)}
<div class="block-field"> <div class:block-field={shouldRenderField(value)}>
{#if key !== "fields" && value.type !== "boolean"} {#if key !== "fields" && value.type !== "boolean" && shouldRenderField(value)}
<Label <Label
tooltip={value.title === "Binding / Value" tooltip={value.title === "Binding / Value"
? "If using the String input type, please use a comma or newline separated string" ? "If using the String input type, please use a comma or newline separated string"
: null}>{value.title || (key === "row" ? "Table" : key)}</Label : null}>{value.title || (key === "row" ? "Table" : key)}</Label
> >
{/if} {/if}
{#if value.type === "string" && value.enum && canShowField(key, value)} <div class:field-width={shouldRenderField(value)}>
<Select {#if value.type === "string" && value.enum && canShowField(key, value)}
on:change={e => onChange(e, key)} <Select
value={inputData[key]}
placeholder={false}
options={value.enum}
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
/>
{:else if value.type === "json"}
<Editor
editorHeight="250"
editorWidth="448"
mode="json"
value={inputData[key]?.value}
on:change={e => {
onChange(e, key)
}}
/>
{:else if value.type === "boolean"}
<div style="margin-top: 10px">
<Checkbox
text={value.title}
value={inputData[key]}
on:change={e => onChange(e, key)} on:change={e => onChange(e, key)}
/>
</div>
{:else if value.type === "date"}
<DrawerBindableSlot
fillWidth
title={value.title}
panel={AutomationBindingPanel}
type={"date"}
value={inputData[key]}
on:change={e => onChange(e, key)}
{bindings}
allowJS={true}
updateOnChange={false}
drawerLeft="260px"
>
<DatePicker
value={inputData[key]} value={inputData[key]}
on:change={e => onChange(e, key)} placeholder={false}
options={value.enum}
getOptionLabel={(x, idx) =>
value.pretty ? value.pretty[idx] : x}
/> />
</DrawerBindableSlot> {:else if value.type === "json"}
{:else if value.customType === "column"} <Editor
<Select editorHeight="250"
on:change={e => onChange(e, key)} editorWidth="448"
value={inputData[key]} mode="json"
options={Object.keys(table?.schema || {})} value={inputData[key]?.value}
/> on:change={e => {
{:else if value.customType === "filters"} onChange(e, key)
<ActionButton on:click={drawer.show}>Define filters</ActionButton> }}
<Drawer bind:this={drawer} {fillWidth} title="Filtering">
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
Save
</Button>
<FilterDrawer
slot="body"
{filters}
{bindings}
{schemaFields}
datasource={{ type: "table", tableId }}
panel={AutomationBindingPanel}
fillWidth
on:change={e => (tempFilters = e.detail)}
/> />
</Drawer> {:else if value.type === "boolean"}
{:else if value.customType === "password"} <div style="margin-top: 10px">
<Input <Checkbox
type="password" text={value.title}
on:change={e => onChange(e, key)} value={inputData[key]}
value={inputData[key]} on:change={e => onChange(e, key)}
/> />
{:else if value.customType === "email"} </div>
{#if isTestModal} {:else if value.type === "date"}
<ModalBindableInput <DrawerBindableSlot
title={value.title}
value={inputData[key]}
panel={AutomationBindingPanel}
type="email"
on:change={e => onChange(e, key)}
{bindings}
fillWidth
updateOnChange={false}
/>
{:else}
<DrawerBindableInput
fillWidth fillWidth
title={value.title} title={value.title}
panel={AutomationBindingPanel} panel={AutomationBindingPanel}
type="email" type={"date"}
value={inputData[key]} value={inputData[key]}
on:change={e => onChange(e, key)} on:change={e => onChange(e, key)}
{bindings} {bindings}
allowJS={false} allowJS={true}
updateOnChange={false} updateOnChange={false}
drawerLeft="260px" drawerLeft="260px"
/> >
{/if} <DatePicker
{:else if value.customType === "query"} value={inputData[key]}
<QuerySelector on:change={e => onChange(e, key)}
on:change={e => onChange(e, key)} />
value={inputData[key]} </DrawerBindableSlot>
/> {:else if value.customType === "column"}
{:else if value.customType === "cron"} <Select
<CronBuilder
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "queryParams"}
<QueryParamSelector
on:change={e => onChange(e, key)}
value={inputData[key]}
{bindings}
/>
{:else if value.customType === "table"}
<TableSelector
{isTrigger}
value={inputData[key]}
on:change={e => onChange(e, key)}
/>
{:else if value.customType === "row"}
<RowSelector
value={inputData[key]}
meta={inputData["meta"] || {}}
on:change={e => {
if (e.detail?.key) {
onChange(e, e.detail.key)
} else {
onChange(e, key)
}
}}
{bindings}
{isTestModal}
{isUpdateRow}
/>
{:else if value.customType === "webhookUrl"}
<WebhookDisplay
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "fields"}
<FieldSelector
{block}
value={inputData[key]}
on:change={e => onChange(e, key)}
{bindings}
{isTestModal}
/>
{:else if value.customType === "triggerSchema"}
<SchemaSetup
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "code"}
<CodeEditorModal>
{#if codeMode == EditorModes.JS}
<ActionButton
on:click={() => (codeBindingOpen = !codeBindingOpen)}
quiet
icon={codeBindingOpen ? "ChevronDown" : "ChevronRight"}
>
<Detail size="S">Bindings</Detail>
</ActionButton>
{#if codeBindingOpen}
<pre>{JSON.stringify(bindings, null, 2)}</pre>
{/if}
{/if}
<CodeEditor
value={inputData[key]}
on:change={e => {
// need to pass without the value inside
onChange({ detail: e.detail }, key)
inputData[key] = e.detail
}}
completions={stepCompletions}
mode={codeMode}
autocompleteEnabled={codeMode != EditorModes.JS}
height={500}
/>
<div class="messaging">
{#if codeMode == EditorModes.Handlebars}
<Icon name="FlashOn" />
<div class="messaging-wrap">
<div>
Add available bindings by typing <strong>
&#125;&#125;
</strong>
</div>
</div>
{/if}
</div>
</CodeEditorModal>
{:else if value.customType === "loopOption"}
<Select
on:change={e => onChange(e, key)}
autoWidth
value={inputData[key]}
options={["Array", "String"]}
defaultValue={"Array"}
/>
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
{#if isTestModal}
<ModalBindableInput
title={value.title}
value={inputData[key]}
panel={AutomationBindingPanel}
type={value.customType}
on:change={e => onChange(e, key)} on:change={e => onChange(e, key)}
{bindings} value={inputData[key]}
updateOnChange={false} options={Object.keys(table?.schema || {})}
/> />
{:else} {:else if value.customType === "filters"}
<div class="test"> <ActionButton on:click={drawer.show}>Define filters</ActionButton>
<Drawer bind:this={drawer} {fillWidth} title="Filtering">
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
Save
</Button>
<FilterDrawer
slot="body"
{filters}
{bindings}
{schemaFields}
datasource={{ type: "table", tableId }}
panel={AutomationBindingPanel}
fillWidth
on:change={e => (tempFilters = e.detail)}
/>
</Drawer>
{:else if value.customType === "password"}
<Input
type="password"
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "email"}
{#if isTestModal}
<ModalBindableInput
title={value.title}
value={inputData[key]}
panel={AutomationBindingPanel}
type="email"
on:change={e => onChange(e, key)}
{bindings}
fillWidth
updateOnChange={false}
/>
{:else}
<DrawerBindableInput <DrawerBindableInput
fillWidth={true} fillWidth
title={value.title} title={value.title}
panel={AutomationBindingPanel} panel={AutomationBindingPanel}
type={value.customType} type="email"
value={inputData[key]} value={inputData[key]}
on:change={e => onChange(e, key)} on:change={e => onChange(e, key)}
{bindings} {bindings}
allowJS={false}
updateOnChange={false} updateOnChange={false}
placeholder={value.customType === "queryLimit"
? queryLimit
: ""}
drawerLeft="260px" drawerLeft="260px"
/> />
</div> {/if}
{:else if value.customType === "query"}
<QuerySelector
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "cron"}
<CronBuilder
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "queryParams"}
<QueryParamSelector
on:change={e => onChange(e, key)}
value={inputData[key]}
{bindings}
/>
{:else if value.customType === "table"}
<TableSelector
{isTrigger}
value={inputData[key]}
on:change={e => onChange(e, key)}
/>
{:else if value.customType === "row"}
<RowSelector
value={inputData[key]}
meta={inputData["meta"] || {}}
on:change={e => {
if (e.detail?.key) {
onChange(e, e.detail.key)
} else {
onChange(e, key)
}
}}
{bindings}
{isTestModal}
{isUpdateRow}
/>
{:else if value.customType === "webhookUrl"}
<WebhookDisplay
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "fields"}
<FieldSelector
{block}
value={inputData[key]}
on:change={e => onChange(e, key)}
{bindings}
{isTestModal}
/>
{:else if value.customType === "triggerSchema"}
<SchemaSetup
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "code"}
<CodeEditorModal>
{#if codeMode == EditorModes.JS}
<ActionButton
on:click={() => (codeBindingOpen = !codeBindingOpen)}
quiet
icon={codeBindingOpen ? "ChevronDown" : "ChevronRight"}
>
<Detail size="S">Bindings</Detail>
</ActionButton>
{#if codeBindingOpen}
<pre>{JSON.stringify(bindings, null, 2)}</pre>
{/if}
{/if}
<CodeEditor
value={inputData[key]}
on:change={e => {
// need to pass without the value inside
onChange({ detail: e.detail }, key)
inputData[key] = e.detail
}}
completions={stepCompletions}
mode={codeMode}
autocompleteEnabled={codeMode != EditorModes.JS}
height={500}
/>
<div class="messaging">
{#if codeMode == EditorModes.Handlebars}
<Icon name="FlashOn" />
<div class="messaging-wrap">
<div>
Add available bindings by typing <strong>
&#125;&#125;
</strong>
</div>
</div>
{/if}
</div>
</CodeEditorModal>
{:else if value.customType === "loopOption"}
<Select
on:change={e => onChange(e, key)}
autoWidth
value={inputData[key]}
options={["Array", "String"]}
defaultValue={"Array"}
/>
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
{#if isTestModal}
<ModalBindableInput
title={value.title}
value={inputData[key]}
panel={AutomationBindingPanel}
type={value.customType}
on:change={e => onChange(e, key)}
{bindings}
updateOnChange={false}
/>
{:else}
<div class="test">
<DrawerBindableInput
fillWidth={true}
title={value.title}
panel={AutomationBindingPanel}
type={value.customType}
value={inputData[key]}
on:change={e => onChange(e, key)}
{bindings}
updateOnChange={false}
placeholder={value.customType === "queryLimit"
? queryLimit
: ""}
drawerLeft="260px"
/>
</div>
{/if}
{/if} {/if}
{/if} </div>
</div> </div>
{/if} {/if}
{/each} {/each}
@ -541,6 +558,10 @@
{/if} {/if}
<style> <style>
.field-width {
width: 320px;
}
.messaging { .messaging {
display: flex; display: flex;
align-items: center; align-items: center;
@ -555,8 +576,13 @@
} }
.block-field { .block-field {
display: grid; display: flex; /* Use Flexbox */
grid-gap: 5px; justify-content: space-between;
align-items: center;
flex-direction: row; /* Arrange label and field side by side */
align-items: center; /* Align vertically in the center */
gap: 10px; /* Add some space between label and field */
flex: 1;
} }
.test :global(.drawer) { .test :global(.drawer) {

View File

@ -23,7 +23,9 @@
</div> </div>
</ModalContent> </ModalContent>
</Modal> </Modal>
<Button primary on:click={show}>Edit Code</Button> <div class="center">
<Button primary on:click={show}>Edit Code</Button>
</div>
<style> <style>
.container :global(section > header) { .container :global(section > header) {
@ -33,4 +35,9 @@
.container :global(textarea) { .container :global(textarea) {
min-height: 60px; min-height: 60px;
} }
.center {
display: flex;
justify-content: center;
}
</style> </style>

View File

@ -1,7 +1,7 @@
<script> <script>
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { queries } from "stores/backend" import { queries } from "stores/backend"
import { Select } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte" import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
@ -27,41 +27,55 @@
$: if (value?.queryId == null) value = { queryId: "" } $: if (value?.queryId == null) value = { queryId: "" }
</script> </script>
<div class="block-field"> <div class="schema-fields">
<Select <Label>Query</Label>
label="Query" <div class="field-width">
on:change={onChangeQuery} <Select
value={value.queryId} on:change={onChangeQuery}
options={$queries.list} value={value.queryId}
getOptionValue={query => query._id} options={$queries.list}
getOptionLabel={query => query.name} getOptionValue={query => query._id}
/> getOptionLabel={query => query.name}
/>
</div>
</div> </div>
{#if parameters.length} {#if parameters.length}
<div class="schema-fields"> <div class="schema-fields">
{#each parameters as field} {#each parameters as field}
<DrawerBindableInput <Label>{field.name}</Label>
panel={AutomationBindingPanel} <div class="field-width">
extraThin <DrawerBindableInput
value={value[field.name]} panel={AutomationBindingPanel}
on:change={e => onChange(e, field)} extraThin
label={field.name} value={value[field.name]}
type="string" on:change={e => onChange(e, field)}
{bindings} type="string"
fillWidth={true} {bindings}
updateOnChange={false} fillWidth={true}
/> updateOnChange={false}
/>
</div>
{/each} {/each}
</div> </div>
{/if} {/if}
<style> <style>
.schema-fields { .field-width {
display: grid; width: 320px;
grid-gap: var(--spacing-xl);
margin-top: var(--spacing-xl);
} }
.schema-fields {
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: row;
align-items: center;
gap: 10px;
flex: 1;
margin-bottom: 10px;
}
.schema-fields :global(label) { .schema-fields :global(label) {
text-transform: capitalize; text-transform: capitalize;
} }

View File

@ -1,10 +1,11 @@
<script> <script>
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { Select, Checkbox } from "@budibase/bbui" import { Select, Checkbox, Label } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import RowSelectorTypes from "./RowSelectorTypes.svelte" import RowSelectorTypes from "./RowSelectorTypes.svelte"
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte" import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte" import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import { TableNames } from "constants"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -99,41 +100,25 @@
$: if (value?.tableId == null) value = { tableId: "" } $: if (value?.tableId == null) value = { tableId: "" }
</script> </script>
<Select <div class="schema-fields">
on:change={onChangeTable} <Label>Table</Label>
value={value.tableId} <div class="field-width">
options={$tables.list} <Select
getOptionLabel={table => table.name} on:change={onChangeTable}
getOptionValue={table => table._id} value={value.tableId}
/> options={$tables.list.filter(table => table._id !== TableNames.USERS)}
getOptionLabel={table => table.name}
getOptionValue={table => table._id}
/>
</div>
</div>
{#if schemaFields.length} {#if schemaFields.length}
<div class="schema-fields"> {#each schemaFields as [field, schema]}
{#each schemaFields as [field, schema]} <div class="schema-fields">
{#if !schema.autocolumn && schema.type !== "attachment"} <Label>{field}</Label>
{#if isTestModal} <div class="field-width">
<RowSelectorTypes {#if !schema.autocolumn && schema.type !== "attachment"}
{isTestModal} {#if isTestModal}
{field}
{schema}
bindings={parsedBindings}
{value}
{onChange}
/>
{:else}
<DrawerBindableSlot
fillWidth
title={value.title}
label={field}
panel={AutomationBindingPanel}
type={schema.type}
{schema}
value={value[field]}
on:change={e => onChange(e, field)}
{bindings}
allowJS={true}
updateOnChange={false}
drawerLeft="260px"
>
<RowSelectorTypes <RowSelectorTypes
{isTestModal} {isTestModal}
{field} {field}
@ -142,28 +127,61 @@
{value} {value}
{onChange} {onChange}
/> />
</DrawerBindableSlot> {:else}
<DrawerBindableSlot
fillWidth
title={value.title}
panel={AutomationBindingPanel}
type={schema.type}
{schema}
value={value[field]}
on:change={e => onChange(e, field)}
{bindings}
allowJS={true}
updateOnChange={false}
drawerLeft="260px"
>
<RowSelectorTypes
{isTestModal}
{field}
{schema}
bindings={parsedBindings}
{value}
{onChange}
/>
</DrawerBindableSlot>
{/if}
{/if} {/if}
{/if}
{#if isUpdateRow && schema.type === "link"} {#if isUpdateRow && schema.type === "link"}
<div class="checkbox-field"> <div class="checkbox-field">
<Checkbox <Checkbox
value={meta.fields?.[field]?.clearRelationships} value={meta.fields?.[field]?.clearRelationships}
text={"Clear relationships if empty?"} text={"Clear relationships if empty?"}
size={"S"} size={"S"}
on:change={e => onChangeSetting(e, field)} on:change={e => onChangeSetting(e, field)}
/> />
</div> </div>
{/if} {/if}
{/each} </div>
</div> </div>
{/each}
{/if} {/if}
<style> <style>
.field-width {
width: 320px;
}
.schema-fields { .schema-fields {
display: grid; display: flex;
grid-gap: var(--spacing-s); justify-content: space-between;
margin-top: var(--spacing-s); align-items: center;
flex-direction: row;
align-items: center;
gap: 10px;
flex: 1;
margin-bottom: 10px;
} }
.schema-fields :global(label) { .schema-fields :global(label) {
text-transform: capitalize; text-transform: capitalize;

View File

@ -1,11 +1,5 @@
<script> <script>
import { import { Select, DatePicker, Multiselect, TextArea } from "@budibase/bbui"
Select,
DatePicker,
Multiselect,
TextArea,
Label,
} from "@budibase/bbui"
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte" import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte" import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte"
@ -33,20 +27,14 @@
{#if schemaHasOptions(schema) && schema.type !== "array"} {#if schemaHasOptions(schema) && schema.type !== "array"}
<Select <Select
on:change={e => onChange(e, field)} on:change={e => onChange(e, field)}
label={field}
value={value[field]} value={value[field]}
options={schema.constraints.inclusion} options={schema.constraints.inclusion}
/> />
{:else if schema.type === "datetime"} {:else if schema.type === "datetime"}
<DatePicker <DatePicker value={value[field]} on:change={e => onChange(e, field)} />
label={field}
value={value[field]}
on:change={e => onChange(e, field)}
/>
{:else if schema.type === "boolean"} {:else if schema.type === "boolean"}
<Select <Select
on:change={e => onChange(e, field)} on:change={e => onChange(e, field)}
label={field}
value={value[field]} value={value[field]}
options={[ options={[
{ label: "True", value: "true" }, { label: "True", value: "true" },
@ -56,19 +44,13 @@
{:else if schema.type === "array"} {:else if schema.type === "array"}
<Multiselect <Multiselect
bind:value={value[field]} bind:value={value[field]}
label={field}
options={schema.constraints.inclusion} options={schema.constraints.inclusion}
on:change={e => onChange(e, field)} on:change={e => onChange(e, field)}
/> />
{:else if schema.type === "longform"} {:else if schema.type === "longform"}
<TextArea <TextArea bind:value={value[field]} on:change={e => onChange(e, field)} />
label={field}
bind:value={value[field]}
on:change={e => onChange(e, field)}
/>
{:else if schema.type === "json"} {:else if schema.type === "json"}
<span> <span>
<Label>{field}</Label>
<Editor <Editor
editorHeight="150" editorHeight="150"
mode="json" mode="json"
@ -92,7 +74,6 @@
panel={AutomationBindingPanel} panel={AutomationBindingPanel}
value={value[field]} value={value[field]}
on:change={e => onChange(e, field)} on:change={e => onChange(e, field)}
label={field}
type="string" type="string"
bindings={parsedBindings} bindings={parsedBindings}
fillWidth={true} fillWidth={true}

View File

@ -22,7 +22,7 @@
<Select <Select
on:change={onChange} on:change={onChange}
bind:value bind:value
options={filteredTables} options={filteredTables.filter(table => table._id !== TableNames.USERS)}
getOptionLabel={table => table.name} getOptionLabel={table => table.name}
getOptionValue={table => table._id} getOptionValue={table => table._id}
/> />

View File

@ -44,6 +44,8 @@
const NUMBER_TYPE = FIELDS.NUMBER.type const NUMBER_TYPE = FIELDS.NUMBER.type
const JSON_TYPE = FIELDS.JSON.type const JSON_TYPE = FIELDS.JSON.type
const DATE_TYPE = FIELDS.DATETIME.type const DATE_TYPE = FIELDS.DATETIME.type
const USER_TYPE = FIELDS.USER.subtype
const USERS_TYPE = FIELDS.USERS.subtype
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"] const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
@ -287,6 +289,14 @@
if (saveColumn.type !== LINK_TYPE) { if (saveColumn.type !== LINK_TYPE) {
delete saveColumn.fieldName delete saveColumn.fieldName
} }
if (isUsersColumn(saveColumn)) {
if (saveColumn.subtype === USER_TYPE) {
saveColumn.relationshipType = RelationshipType.ONE_TO_MANY
} else if (saveColumn.subtype === USERS_TYPE) {
saveColumn.relationshipType = RelationshipType.MANY_TO_MANY
}
}
try { try {
await tables.saveField({ await tables.saveField({
originalName, originalName,

View File

@ -4,123 +4,33 @@
</script> </script>
<svg <svg
version="1.1" viewBox="0 0 265 265"
id="Layer_1" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 48 48"
style="enable-background:new 0 0 48 48;"
xml:space="preserve"
{height} {height}
{width} {width}
> >
<style type="text/css"> <g clip-path="url(#clip0_1_1799)">
.st0 {
fill: #393c44;
}
.st1 {
fill: #ffffff;
}
.st2 {
fill: #4285f4;
}
</style>
<rect x="-152.17" y="-24.17" class="st0" width="96.17" height="96.17" />
<path
class="st1"
d="M-83.19,48h-41.79c-1.76,0-3.19-1.43-3.19-3.19V3.02c0-1.76,1.43-3.19,3.19-3.19h41.79
c1.76,0,3.19,1.43,3.19,3.19v41.79C-80,46.57-81.43,48-83.19,48z"
/>
<g>
<g>
<path
class="st0"
d="M-99.62,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35h-4.89V12.57H-99.62z
M-93.46,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C-93.55,28.92-93.46,28.52-93.46,28.11z"
/>
</g>
<g>
<path
class="st0"
d="M-114.76,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58
c0.86,0.39,1.59,0.91,2.19,1.57c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89
c-0.35,0.9-0.84,1.68-1.47,2.35c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35
h-4.89V12.57H-114.76z M-108.6,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C-108.68,28.92-108.6,28.52-108.6,28.11z"
/>
</g>
</g>
<path
class="st2"
d="M44.81,159H3.02c-1.76,0-3.19-1.43-3.19-3.19v-41.79c0-1.76,1.43-3.19,3.19-3.19h41.79
c1.76,0,3.19,1.43,3.19,3.19v41.79C48,157.57,46.57,159,44.81,159z"
/>
<g>
<g>
<path
class="st1"
d="M28.38,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146h-4.89v-22.43H28.38z
M34.54,139.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C34.45,139.92,34.54,139.52,34.54,139.11z"
/>
</g>
<g>
<path
class="st1"
d="M13.24,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146H8.35v-22.43H13.24z M19.4,139.11
c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69c-0.38-0.17-0.79-0.26-1.24-0.26
c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01c-0.17,0.39-0.26,0.8-0.26,1.23
c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68c0.39,0.17,0.8,0.26,1.23,0.26
c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1C19.32,139.92,19.4,139.52,19.4,139.11z"
/>
</g>
</g>
<g>
<path <path
class="st0" d="M158.2 8.6V116.6C158.2 121.3 162 125.2 166.8 125.2H213.8C218 125.2 222 123.2 224.6 119.8L262.9 68.9C265.7 65.2 265.7 60.1 262.9 56.4L224.6 5.4C222 2 218 0 213.8 0H166.8C162 0 158.2 3.8 158.2 8.6Z"
d="M44,48H4c-2.21,0-4-1.79-4-4V4c0-2.21,1.79-4,4-4h40c2.21,0,4,1.79,4,4v40C48,46.21,46.21,48,44,48z" fill="#FF4E4E"
/>
<path
d="M158.2 148.4V256.4C158.2 261.1 162 265 166.8 265H213.8C218 265 222 263 224.6 259.6L262.9 208.7C265.7 205 265.7 199.9 262.9 196.2L224.6 145.3C222.1 141.9 218.1 139.9 213.8 139.9H166.8C162 139.8 158.2 143.7 158.2 148.4Z"
fill="#6E56FF"
/>
<path
d="M0 8.6V116.6C0 121.3 3.8 125.2 8.6 125.2H109.6C113.8 125.2 117.8 123.2 120.4 119.8L155.9 72.5C160.3 66.6 160.3 58.5 155.9 52.6L120.3 5.4C117.8 2 113.8 0 109.5 0H8.6C3.8 0 0 3.8 0 8.6Z"
fill="#F97777"
/>
<path
d="M0 148.4V256.4C0 261.1 3.8 265 8.6 265H109.6C113.8 265 117.8 263 120.4 259.6L155.9 212.3C160.3 206.4 160.3 198.3 155.9 192.4L120.4 145.1C117.9 141.7 113.9 139.7 109.6 139.7H8.6C3.8 139.8 0 143.7 0 148.4Z"
fill="#9F8FFF"
/> />
<g>
<path
class="st1"
d="M28.48,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
c-0.64,0.7-1.4,1.25-2.28,1.66C34.8,35.8,33.86,36,32.84,36c-1.84,0-3.3-0.69-4.37-2.07v1.62h-5V12H28.48z M34.78,28.31
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C34.69,29.16,34.78,28.75,34.78,28.31z"
/>
</g>
<g>
<path
class="st1"
d="M13,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
c-0.64,0.7-1.4,1.25-2.28,1.66C19.32,35.8,18.38,36,17.37,36c-1.84,0-3.3-0.69-4.37-2.07v1.62H8V12H13z M19.3,28.31
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C19.21,29.16,19.3,28.75,19.3,28.31z"
/>
</g>
</g> </g>
<defs>
<clipPath id="clip0_1_1799">
<rect width="265" height="265" fill="white" />
</clipPath>
</defs>
</svg> </svg>

View File

@ -90,13 +90,17 @@
.openMenu { .openMenu {
cursor: pointer; cursor: pointer;
background-color: #6a1dc8; background-color: var(--bb-indigo);
border-radius: 100px; border-radius: 100px;
color: white; color: white;
border: none; border: none;
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
padding: 10px 18px; padding: 10px 18px;
transition: background-color 130ms ease-out;
}
.openMenu:hover {
background-color: var(--bb-indigo-light);
} }
.helpMenu { .helpMenu {

View File

@ -206,12 +206,12 @@
.text-area-slot-icon { .text-area-slot-icon {
border-bottom: 1px solid var(--spectrum-alias-border-color); border-bottom: 1px solid var(--spectrum-alias-border-color);
border-bottom-right-radius: 0px !important; border-bottom-right-radius: 0px !important;
top: 26px !important; top: 1px !important;
} }
.json-slot-icon { .json-slot-icon {
border-bottom: 1px solid var(--spectrum-alias-border-color); border-bottom: 1px solid var(--spectrum-alias-border-color);
border-bottom-right-radius: 0px !important; border-bottom-right-radius: 0px !important;
top: 23px !important; top: 1px !important;
right: 0px !important; right: 0px !important;
} }

View File

@ -20,7 +20,7 @@
let open = false let open = false
// Auto hide the component when another item is selected // Auto hide the component when another item is selected
$: if (open && $draggable.selected != componentInstance._id) { $: if (open && $draggable.selected !== componentInstance._id) {
popover.hide() popover.hide()
} }
@ -100,13 +100,13 @@
}} }}
on:close={() => { on:close={() => {
open = false open = false
if ($draggable.selected == componentInstance._id) { if ($draggable.selected === componentInstance._id) {
$draggable.actions.select() $draggable.actions.select()
} }
}} }}
{anchor} {anchor}
align="left-outside" align="left-outside"
showPopover={drawers.length == 0} showPopover={drawers.length === 0}
clickOutsideOverride={drawers.length > 0} clickOutsideOverride={drawers.length > 0}
maxHeight={600} maxHeight={600}
handlePostionUpdate={customPositionHandler} handlePostionUpdate={customPositionHandler}
@ -115,6 +115,7 @@
<Layout noPadding noGap> <Layout noPadding noGap>
<slot name="header" /> <slot name="header" />
<ComponentSettingsSection <ComponentSettingsSection
includeHidden
{componentInstance} {componentInstance}
componentDefinition={parsedComponentDef} componentDefinition={parsedComponentDef}
isScreen={false} isScreen={false}

View File

@ -112,9 +112,9 @@
} }
await usersFetch.update({ await usersFetch.update({
query: { query: {
appId: query || !filterByAppAccess ? null : prodAppId, string: { email: query },
email: query,
}, },
appId: query || !filterByAppAccess ? null : prodAppId,
limit: 50, limit: 50,
paginate: query || !filterByAppAccess ? null : false, paginate: query || !filterByAppAccess ? null : false,
}) })

View File

@ -91,7 +91,6 @@
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
gap: var(--spacing-l);
overflow: auto; overflow: auto;
} }
.centered { .centered {

View File

@ -3,7 +3,6 @@
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte" import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
import Panel from "components/design/Panel.svelte" import Panel from "components/design/Panel.svelte"
import { isActive, redirect, goto, params } from "@roxi/routify" import { isActive, redirect, goto, params } from "@roxi/routify"
import BetaButton from "./_components/BetaButton.svelte"
import { datasources } from "stores/backend" import { datasources } from "stores/backend"
$: { $: {
@ -30,7 +29,6 @@
<div class="content"> <div class="content">
<slot /> <slot />
</div> </div>
<BetaButton />
</div> </div>
<style> <style>

View File

@ -16,16 +16,18 @@
export let isScreen = false export let isScreen = false
export let onUpdateSetting export let onUpdateSetting
export let showSectionTitle = true export let showSectionTitle = true
export let includeHidden = false
export let tag export let tag
$: sections = getSections( $: sections = getSections(
componentInstance, componentInstance,
componentDefinition, componentDefinition,
isScreen, isScreen,
tag tag,
includeHidden
) )
const getSections = (instance, definition, isScreen, tag) => { const getSections = (instance, definition, isScreen, tag, includeHidden) => {
const settings = definition?.settings ?? [] const settings = definition?.settings ?? []
const generalSettings = settings.filter( const generalSettings = settings.filter(
setting => !setting.section && setting.tag === tag setting => !setting.section && setting.tag === tag
@ -52,7 +54,12 @@
return return
} }
section.settings.forEach(setting => { section.settings.forEach(setting => {
setting.visible = canRenderControl(instance, setting, isScreen) setting.visible = canRenderControl(
instance,
setting,
isScreen,
includeHidden
)
}) })
section.visible = section.visible =
section.name === "General" || section.name === "General" ||
@ -122,16 +129,20 @@
}) })
} }
const canRenderControl = (instance, setting, isScreen) => { const canRenderControl = (instance, setting, isScreen, includeHidden) => {
// Prevent rendering on click setting for screens // Prevent rendering on click setting for screens
if (setting?.type === "event" && isScreen) { if (setting?.type === "event" && isScreen) {
return false return false
} }
// Check we have a component to render for this setting
const control = getComponentForSetting(setting) const control = getComponentForSetting(setting)
if (!control) { if (!control) {
return false return false
} }
// Check if setting is hidden
if (setting.hidden && !includeHidden) {
return false
}
return shouldDisplay(instance, setting) return shouldDisplay(instance, setting)
} }
</script> </script>

View File

@ -9,12 +9,18 @@
let searchString let searchString
let searching = false let searching = false
$: filteredApps = $apps.filter(app => { $: filteredApps = $apps
return ( .filter(app => {
!searchString || return (
app.name.toLowerCase().includes(searchString.toLowerCase()) !searchString ||
) app.name.toLowerCase().includes(searchString.toLowerCase())
}) )
})
.sort((a, b) => {
const lowerA = a.name.toLowerCase()
const lowerB = b.name.toLowerCase()
return lowerA > lowerB ? 1 : -1
})
const startSearching = async () => { const startSearching = async () => {
searching = true searching = true

View File

@ -8,11 +8,7 @@
</script> </script>
<div class="header"> <div class="header">
<img <img alt="Budibase Logo" class="budibaseLogo" src="/builder/bblogo.png" />
alt="Budibase Logo"
class="budibaseLogo"
src={"https://i.imgur.com/Xhdt1YP.png"}
/>
<div class="headingAndBack"> <div class="headingAndBack">
{#if onBack} {#if onBack}
<button on:click={onBack}> <button on:click={onBack}>

View File

@ -2589,6 +2589,17 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{ {
"type": "text", "type": "text",
"label": "Initial form step", "label": "Initial form step",
@ -2738,6 +2749,17 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{ {
"type": "validation/string", "type": "validation/string",
"label": "Validation", "label": "Validation",
@ -2776,6 +2798,35 @@
"barTitle": "Justify text" "barTitle": "Justify text"
} }
] ]
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -2829,10 +2880,50 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{ {
"type": "validation/number", "type": "validation/number",
"label": "Validation", "label": "Validation",
"key": "validation" "key": "validation"
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -2885,6 +2976,35 @@
"label": "Disabled", "label": "Disabled",
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -2942,6 +3062,35 @@
"type": "validation/string", "type": "validation/string",
"label": "Validation", "label": "Validation",
"key": "validation" "key": "validation"
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -3049,6 +3198,17 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{ {
"type": "select", "type": "select",
"label": "Options source", "label": "Options source",
@ -3110,6 +3270,35 @@
"type": "validation/string", "type": "validation/string",
"label": "Validation", "label": "Validation",
"key": "validation" "key": "validation"
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -3174,6 +3363,17 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{ {
"type": "select", "type": "select",
"label": "Type", "label": "Type",
@ -3272,6 +3472,35 @@
"type": "validation/array", "type": "validation/array",
"label": "Validation", "label": "Validation",
"key": "validation" "key": "validation"
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -3348,10 +3577,50 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{ {
"type": "validation/boolean", "type": "validation/boolean",
"label": "Validation", "label": "Validation",
"key": "validation" "key": "validation"
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -3427,10 +3696,50 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{ {
"type": "validation/string", "type": "validation/string",
"label": "Validation", "label": "Validation",
"key": "validation" "key": "validation"
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -3508,10 +3817,50 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{ {
"type": "validation/datetime", "type": "validation/datetime",
"label": "Validation", "label": "Validation",
"key": "validation" "key": "validation"
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -3598,6 +3947,22 @@
"value": "custom" "value": "custom"
} }
}, },
{
"type": "select",
"label": "Preferred camera",
"key": "preferredCamera",
"defaultValue": "environment",
"options": [
{
"label": "Front",
"value": "user"
},
{
"label": "Back",
"value": "environment"
}
]
},
{ {
"type": "event", "type": "event",
"label": "On change", "label": "On change",
@ -3613,6 +3978,35 @@
"type": "validation/string", "type": "validation/string",
"label": "Validation", "label": "Validation",
"key": "validation" "key": "validation"
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -3781,7 +4175,7 @@
}, },
{ {
"type": "boolean", "type": "boolean",
"label": "Disabled", "label": "Read only",
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
@ -3789,6 +4183,35 @@
"type": "validation/attachment", "type": "validation/attachment",
"label": "Validation", "label": "Validation",
"key": "validation" "key": "validation"
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -3857,6 +4280,46 @@
"label": "Disabled", "label": "Disabled",
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
},
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -3909,6 +4372,46 @@
"label": "Disabled", "label": "Disabled",
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
},
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -5530,12 +6033,7 @@
"type": "boolean", "type": "boolean",
"label": "Disabled", "label": "Disabled",
"key": "disabled", "key": "disabled",
"defaultValue": false, "defaultValue": false
"dependsOn": {
"setting": "actionType",
"value": "View",
"invert": true
}
} }
] ]
}, },
@ -5590,23 +6088,6 @@
} }
] ]
}, },
{
"tag": "style",
"type": "select",
"label": "Align labels",
"key": "labelPosition",
"defaultValue": "left",
"options": [
{
"label": "Left",
"value": "left"
},
{
"label": "Above",
"value": "above"
}
]
},
{ {
"tag": "style", "tag": "style",
"type": "select", "type": "select",
@ -5921,6 +6402,35 @@
"label": "Disabled", "label": "Disabled",
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
} }

View File

@ -26,15 +26,15 @@
$: parentId = $component?.id $: parentId = $component?.id
$: inBuilder = $builderStore.inBuilder $: inBuilder = $builderStore.inBuilder
$: instance = { $: instance = {
...props,
_component: getComponent(type), _component: getComponent(type),
_id: id, _id: id,
_instanceName: getInstanceName(name, type), _instanceName: getInstanceName(name, type),
_containsSlot: containsSlot,
_styles: { _styles: {
...styles, ...styles,
normal: styles?.normal || {}, normal: styles?.normal || {},
}, },
_containsSlot: containsSlot,
...props,
} }
// Register this block component if we're inside the builder so it can be // Register this block component if we're inside the builder so it can be

View File

@ -140,6 +140,7 @@
interactive && interactive &&
!isLayout && !isLayout &&
!isRoot && !isRoot &&
!isBlock &&
definition?.draggable !== false definition?.draggable !== false
$: droppable = interactive $: droppable = interactive
$: builderHidden = $: builderHidden =
@ -194,6 +195,7 @@
interactive, interactive,
draggable, draggable,
editable, editable,
isBlock,
}, },
empty: emptyState, empty: emptyState,
selected, selected,

View File

@ -10,7 +10,6 @@
export let size export let size
export let disabled export let disabled
export let fields export let fields
export let labelPosition
export let title export let title
export let description export let description
export let showDeleteButton export let showDeleteButton
@ -97,7 +96,6 @@
size, size,
disabled, disabled,
fields: fieldsOrDefault, fields: fieldsOrDefault,
labelPosition,
title, title,
description, description,
saveButtonLabel: saveLabel, saveButtonLabel: saveLabel,

View File

@ -2,6 +2,7 @@
import BlockComponent from "components/BlockComponent.svelte" import BlockComponent from "components/BlockComponent.svelte"
import Placeholder from "components/app/Placeholder.svelte" import Placeholder from "components/app/Placeholder.svelte"
import { makePropSafe as safe } from "@budibase/string-templates" import { makePropSafe as safe } from "@budibase/string-templates"
import { getContext } from "svelte"
export let dataSource export let dataSource
export let actionUrl export let actionUrl
@ -9,7 +10,6 @@
export let size export let size
export let disabled export let disabled
export let fields export let fields
export let labelPosition
export let title export let title
export let description export let description
export let saveButtonLabel export let saveButtonLabel
@ -33,6 +33,7 @@
barcodeqr: "codescanner", barcodeqr: "codescanner",
bb_reference: "bbreferencefield", bb_reference: "bbreferencefield",
} }
const context = getContext("context")
let formId let formId
@ -136,7 +137,8 @@
actionType: actionType === "Create" ? "Create" : "Update", actionType: actionType === "Create" ? "Create" : "Update",
dataSource, dataSource,
size, size,
disabled: disabled || actionType === "View", disabled,
readonly: !disabled && actionType === "View",
}} }}
styles={{ styles={{
normal: { normal: {
@ -226,16 +228,20 @@
<BlockComponent type="text" props={{ text: description }} order={1} /> <BlockComponent type="text" props={{ text: description }} order={1} />
{/if} {/if}
{#key fields} {#key fields}
<BlockComponent type="fieldgroup" props={{ labelPosition }} order={1}> <BlockComponent type="container">
{#each fields as field, idx} <div class="form-block fields" class:mobile={$context.device.mobile}>
{#if getComponentForField(field) && field.active} {#each fields as field, idx}
<BlockComponent {#if getComponentForField(field) && field.active}
type={getComponentForField(field)} <BlockComponent
props={getPropsForField(field)} type={getComponentForField(field)}
order={idx} props={getPropsForField(field)}
/> order={idx}
{/if} interactive
{/each} name={field?.field}
/>
{/if}
{/each}
</div>
</BlockComponent> </BlockComponent>
{/key} {/key}
</BlockComponent> </BlockComponent>
@ -245,3 +251,14 @@
text="Choose your table and add some fields to your form to get started" text="Choose your table and add some fields to your form to get started"
/> />
{/if} {/if}
<style>
.fields {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 8px 16px;
}
.fields.mobile :global(.spectrum-Form-item) {
grid-column: span 6 !important;
}
</style>

View File

@ -4,9 +4,6 @@
const { linkable, styleable } = getContext("sdk") const { linkable, styleable } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
// BB emblem: https://i.imgur.com/Xhdt1YP.png
// Space logo: https://i.imgur.com/Dn7Xt1G.png
export let logoUrl export let logoUrl
export let hideLogo export let hideLogo
</script> </script>

View File

@ -6,11 +6,13 @@
export let field export let field
export let label export let label
export let disabled = false export let disabled = false
export let readonly = false
export let compact = false export let compact = false
export let validation export let validation
export let extensions export let extensions
export let onChange export let onChange
export let maximum = undefined export let maximum = undefined
export let span
let fieldState let fieldState
let fieldApi let fieldApi
@ -71,33 +73,27 @@
{label} {label}
{field} {field}
{disabled} {disabled}
{readonly}
{validation} {validation}
{span}
type="attachment" type="attachment"
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi
defaultValue={[]} defaultValue={[]}
> >
<div class="minHeightWrapper"> {#if fieldState}
{#if fieldState} <CoreDropzone
<CoreDropzone value={fieldState.value}
value={fieldState.value} disabled={fieldState.disabled || fieldState.readonly}
disabled={fieldState.disabled} error={fieldState.error}
error={fieldState.error} on:change={handleChange}
on:change={handleChange} {processFiles}
{processFiles} {deleteAttachments}
{deleteAttachments} {handleFileTooLarge}
{handleFileTooLarge} {handleTooManyFiles}
{handleTooManyFiles} {maximum}
{maximum} {extensions}
{extensions} {compact}
{compact} />
/> {/if}
{/if}
</div>
</Field> </Field>
<style>
.minHeightWrapper {
min-height: 80px;
}
</style>

View File

@ -6,6 +6,7 @@
export let label export let label
export let text export let text
export let disabled = false export let disabled = false
export let readonly = false
export let size export let size
export let validation export let validation
export let defaultValue export let defaultValue
@ -39,6 +40,7 @@
{label} {label}
{field} {field}
{disabled} {disabled}
{readonly}
{validation} {validation}
defaultValue={isTruthy(defaultValue)} defaultValue={isTruthy(defaultValue)}
type="boolean" type="boolean"
@ -49,6 +51,7 @@
<CoreCheckbox <CoreCheckbox
value={fieldState.value} value={fieldState.value}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
error={fieldState.error} error={fieldState.error}
id={fieldState.fieldId} id={fieldState.fieldId}
{size} {size}

View File

@ -11,6 +11,7 @@
export let beepOnScan = false export let beepOnScan = false
export let beepFrequency = 2637 export let beepFrequency = 2637
export let customFrequency = 1046 export let customFrequency = 1046
export let preferredCamera = "environment"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -20,7 +21,7 @@
let cameraEnabled let cameraEnabled
let cameraStarted = false let cameraStarted = false
let html5QrCode let html5QrCode
let cameraSetting = { facingMode: "environment" } let cameraSetting = { facingMode: preferredCamera }
let cameraConfig = { let cameraConfig = {
fps: 25, fps: 25,
qrbox: { width: 250, height: 250 }, qrbox: { width: 250, height: 250 },

View File

@ -6,6 +6,7 @@
export let label export let label
export let type = "barcodeqr" export let type = "barcodeqr"
export let disabled = false export let disabled = false
export let readonly = false
export let validation export let validation
export let defaultValue = "" export let defaultValue = ""
export let onChange export let onChange
@ -14,6 +15,7 @@
export let beepOnScan export let beepOnScan
export let beepFrequency export let beepFrequency
export let customFrequency export let customFrequency
export let preferredCamera
let fieldState let fieldState
let fieldApi let fieldApi
@ -32,6 +34,7 @@
{label} {label}
{field} {field}
{disabled} {disabled}
{readonly}
{validation} {validation}
{defaultValue} {defaultValue}
{type} {type}
@ -42,12 +45,13 @@
<CodeScanner <CodeScanner
value={fieldState.value} value={fieldState.value}
on:change={handleUpdate} on:change={handleUpdate}
disabled={fieldState.disabled} disabled={fieldState.disabled || fieldState.readonly}
{allowManualEntry} {allowManualEntry}
scanButtonText={scanText} scanButtonText={scanText}
{beepOnScan} {beepOnScan}
{beepFrequency} {beepFrequency}
{customFrequency} {customFrequency}
{preferredCamera}
/> />
{/if} {/if}
</Field> </Field>

View File

@ -6,6 +6,7 @@
export let label export let label
export let placeholder export let placeholder
export let disabled = false export let disabled = false
export let readonly = false
export let enableTime = true export let enableTime = true
export let timeOnly = false export let timeOnly = false
export let time24hr = false export let time24hr = false
@ -13,6 +14,7 @@
export let validation export let validation
export let defaultValue export let defaultValue
export let onChange export let onChange
export let span
let fieldState let fieldState
let fieldApi let fieldApi
@ -29,8 +31,10 @@
{label} {label}
{field} {field}
{disabled} {disabled}
{readonly}
{validation} {validation}
{defaultValue} {defaultValue}
{span}
type="datetime" type="datetime"
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi
@ -40,6 +44,7 @@
value={fieldState.value} value={fieldState.value}
on:change={handleChange} on:change={handleChange}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
error={fieldState.error} error={fieldState.error}
id={fieldState.fieldId} id={fieldState.fieldId}
appendTo={document.getElementById("flatpickr-root")} appendTo={document.getElementById("flatpickr-root")}

View File

@ -1,6 +1,5 @@
<script> <script>
import Placeholder from "../Placeholder.svelte" import Placeholder from "../Placeholder.svelte"
import FieldGroupFallback from "./FieldGroupFallback.svelte"
import { getContext, onDestroy } from "svelte" import { getContext, onDestroy } from "svelte"
export let label export let label
@ -11,7 +10,9 @@
export let defaultValue export let defaultValue
export let type export let type
export let disabled = false export let disabled = false
export let readonly = false
export let validation export let validation
export let span = 6
// Get contexts // Get contexts
const formContext = getContext("form") const formContext = getContext("form")
@ -29,6 +30,7 @@
type, type,
defaultValue, defaultValue,
disabled, disabled,
readonly,
validation, validation,
formStep formStep
) )
@ -62,40 +64,59 @@
}) })
</script> </script>
<FieldGroupFallback> <div
<div class="spectrum-Form-item" use:styleable={$component.styles}> class="spectrum-Form-item"
{#key $component.editing} class:span-2={span === 2}
<label class:span-3={span === 3}
bind:this={labelNode} class:span-6={span === 6 || !span}
contenteditable={$component.editing} use:styleable={$component.styles}
on:blur={$component.editing ? updateLabel : null} class:above={labelPos === "above"}
class:hidden={!label} >
for={fieldState?.fieldId} {#key $component.editing}
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelClass}`} <label
> bind:this={labelNode}
{label || " "} contenteditable={$component.editing}
</label> on:blur={$component.editing ? updateLabel : null}
{/key} class:hidden={!label}
<div class="spectrum-Form-itemField"> class:readonly
{#if !formContext} for={fieldState?.fieldId}
<Placeholder text="Form components need to be wrapped in a form" /> class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelClass}`}
{:else if !fieldState} >
<Placeholder /> {label || " "}
{:else if schemaType && schemaType !== type && !["options", "longform"].includes(type)} </label>
<Placeholder {/key}
text="This Field setting is the wrong data type for this component" <div class="spectrum-Form-itemField">
/> {#if !formContext}
{:else} <Placeholder text="Form components need to be wrapped in a form" />
<slot /> {:else if !fieldState}
{#if fieldState.error} <Placeholder />
<div class="error">{fieldState.error}</div> {:else if schemaType && schemaType !== type && !["options", "longform"].includes(type)}
{/if} <Placeholder
text="This Field setting is the wrong data type for this component"
/>
{:else}
<slot />
{#if fieldState.error}
<div class="error">{fieldState.error}</div>
{/if} {/if}
</div> {/if}
</div> </div>
</FieldGroupFallback> </div>
<style> <style>
:global(.form-block .spectrum-Form-item.span-2) {
grid-column: span 2;
}
:global(.form-block .spectrum-Form-item.span-3) {
grid-column: span 3;
}
:global(.form-block .spectrum-Form-item.span-6) {
grid-column: span 6;
}
.spectrum-Form-item.above {
display: flex;
flex-direction: column;
}
label { label {
white-space: nowrap; white-space: nowrap;
} }
@ -118,4 +139,7 @@
.spectrum-FieldLabel--left { .spectrum-FieldLabel--left {
padding-right: var(--spectrum-global-dimension-size-200); padding-right: var(--spectrum-global-dimension-size-200);
} }
.readonly {
pointer-events: none;
}
</style> </style>

View File

@ -8,6 +8,7 @@
export let theme export let theme
export let size export let size
export let disabled = false export let disabled = false
export let readonly = false
export let actionType = "Create" export let actionType = "Create"
export let initialFormStep = 1 export let initialFormStep = 1
@ -39,7 +40,7 @@
$: schemaKey = generateSchemaKey(schema) $: schemaKey = generateSchemaKey(schema)
$: initialValues = getInitialValues(actionType, dataSource, $context) $: initialValues = getInitialValues(actionType, dataSource, $context)
$: resetKey = Helpers.hashString( $: resetKey = Helpers.hashString(
schemaKey + JSON.stringify(initialValues) + disabled schemaKey + JSON.stringify(initialValues) + disabled + readonly
) )
// Returns the closes data context which isn't a built in context // Returns the closes data context which isn't a built in context
@ -97,6 +98,7 @@
{theme} {theme}
{size} {size}
{disabled} {disabled}
{readonly}
{actionType} {actionType}
{schema} {schema}
{table} {table}

View File

@ -6,6 +6,7 @@
export let dataSource export let dataSource
export let disabled = false export let disabled = false
export let readonly = false
export let initialValues export let initialValues
export let size export let size
export let schema export let schema
@ -148,6 +149,7 @@
type, type,
defaultValue = null, defaultValue = null,
fieldDisabled = false, fieldDisabled = false,
fieldReadOnly = false,
validationRules, validationRules,
step = 1 step = 1
) => { ) => {
@ -205,6 +207,7 @@
error: initialError, error: initialError,
disabled: disabled:
disabled || fieldDisabled || (isAutoColumn && !editAutoColumns), disabled || fieldDisabled || (isAutoColumn && !editAutoColumns),
readonly: readonly || fieldReadOnly,
defaultValue, defaultValue,
validator, validator,
lastUpdate: Date.now(), lastUpdate: Date.now(),

View File

@ -7,6 +7,7 @@
export let label export let label
export let placeholder export let placeholder
export let disabled = false export let disabled = false
export let readonly = false
export let defaultValue = "" export let defaultValue = ""
export let onChange export let onChange
@ -48,6 +49,7 @@
{label} {label}
{field} {field}
{disabled} {disabled}
{readonly}
{validation} {validation}
{defaultValue} {defaultValue}
type="json" type="json"
@ -60,6 +62,7 @@
value={serialiseValue(fieldState.value)} value={serialiseValue(fieldState.value)}
on:change={handleChange} on:change={handleChange}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
error={fieldState.error} error={fieldState.error}
id={fieldState.fieldId} id={fieldState.fieldId}
{placeholder} {placeholder}

View File

@ -8,6 +8,7 @@
export let label export let label
export let placeholder export let placeholder
export let disabled = false export let disabled = false
export let readonly = false
export let validation export let validation
export let defaultValue = "" export let defaultValue = ""
export let format = "auto" export let format = "auto"
@ -58,6 +59,7 @@
{label} {label}
{field} {field}
{disabled} {disabled}
{readonly}
{validation} {validation}
{defaultValue} {defaultValue}
type="longform" type="longform"
@ -71,6 +73,7 @@
value={fieldState.value} value={fieldState.value}
on:change={handleChange} on:change={handleChange}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
error={fieldState.error} error={fieldState.error}
id={fieldState.fieldId} id={fieldState.fieldId}
{placeholder} {placeholder}
@ -88,6 +91,7 @@
value={fieldState.value} value={fieldState.value}
on:change={handleChange} on:change={handleChange}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
error={fieldState.error} error={fieldState.error}
id={fieldState.fieldId} id={fieldState.fieldId}
{placeholder} {placeholder}

View File

@ -6,6 +6,7 @@
export let label export let label
export let placeholder export let placeholder
export let disabled = false export let disabled = false
export let readonly = false
export let validation export let validation
export let defaultValue export let defaultValue
export let optionsSource = "schema" export let optionsSource = "schema"
@ -17,6 +18,7 @@
export let onChange export let onChange
export let optionsType = "select" export let optionsType = "select"
export let direction = "vertical" export let direction = "vertical"
export let span
let fieldState let fieldState
let fieldApi let fieldApi
@ -55,7 +57,9 @@
{field} {field}
{label} {label}
{disabled} {disabled}
{readonly}
{validation} {validation}
{span}
defaultValue={expandedDefaultValue} defaultValue={expandedDefaultValue}
type="array" type="array"
bind:fieldState bind:fieldState
@ -71,6 +75,7 @@
getOptionValue={flatOptions ? x => x : x => x.value} getOptionValue={flatOptions ? x => x : x => x.value}
id={fieldState.fieldId} id={fieldState.fieldId}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
on:change={handleChange} on:change={handleChange}
{placeholder} {placeholder}
{options} {options}
@ -81,6 +86,7 @@
value={fieldState.value || []} value={fieldState.value || []}
id={fieldState.fieldId} id={fieldState.fieldId}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
error={fieldState.error} error={fieldState.error}
{options} {options}
{direction} {direction}

View File

@ -6,6 +6,7 @@
export let label export let label
export let placeholder export let placeholder
export let disabled = false export let disabled = false
export let readonly = false
export let optionsType = "select" export let optionsType = "select"
export let validation export let validation
export let defaultValue export let defaultValue
@ -18,6 +19,7 @@
export let direction = "vertical" export let direction = "vertical"
export let onChange export let onChange
export let sort = true export let sort = true
export let span
let fieldState let fieldState
let fieldApi let fieldApi
@ -45,8 +47,10 @@
{field} {field}
{label} {label}
{disabled} {disabled}
{readonly}
{validation} {validation}
{defaultValue} {defaultValue}
{span}
type="options" type="options"
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi
@ -58,6 +62,7 @@
value={fieldState.value} value={fieldState.value}
id={fieldState.fieldId} id={fieldState.fieldId}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
error={fieldState.error} error={fieldState.error}
{options} {options}
{placeholder} {placeholder}
@ -72,6 +77,7 @@
value={fieldState.value} value={fieldState.value}
id={fieldState.fieldId} id={fieldState.fieldId}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
error={fieldState.error} error={fieldState.error}
{options} {options}
{direction} {direction}

View File

@ -11,6 +11,7 @@
export let label export let label
export let placeholder export let placeholder
export let disabled = false export let disabled = false
export let readonly = false
export let validation export let validation
export let autocomplete = true export let autocomplete = true
export let defaultValue export let defaultValue
@ -18,6 +19,7 @@
export let filter export let filter
export let datasourceType = "table" export let datasourceType = "table"
export let primaryDisplay export let primaryDisplay
export let span
let fieldState let fieldState
let fieldApi let fieldApi
@ -137,7 +139,9 @@
typeof value === "object" ? value._id : value typeof value === "object" ? value._id : value
) )
// Make sure field state is valid // Make sure field state is valid
fieldApi.setValue(values) if (values?.length > 0) {
fieldApi.setValue(values)
}
return values return values
} }
@ -183,9 +187,11 @@
{label} {label}
{field} {field}
{disabled} {disabled}
{readonly}
{validation} {validation}
defaultValue={expandedDefaultValue} defaultValue={expandedDefaultValue}
{type} {type}
{span}
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi
bind:fieldSchema bind:fieldSchema
@ -200,6 +206,7 @@
on:loadMore={loadMore} on:loadMore={loadMore}
id={fieldState.fieldId} id={fieldState.fieldId}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
error={fieldState.error} error={fieldState.error}
getOptionLabel={getDisplayName} getOptionLabel={getDisplayName}
getOptionValue={option => option._id} getOptionValue={option => option._id}

View File

@ -7,10 +7,12 @@
export let placeholder export let placeholder
export let type = "text" export let type = "text"
export let disabled = false export let disabled = false
export let readonly = false
export let validation export let validation
export let defaultValue = "" export let defaultValue = ""
export let align export let align
export let onChange export let onChange
export let span
let fieldState let fieldState
let fieldApi let fieldApi
@ -27,8 +29,10 @@
{label} {label}
{field} {field}
{disabled} {disabled}
{readonly}
{validation} {validation}
{defaultValue} {defaultValue}
{span}
type={type === "number" ? "number" : "string"} type={type === "number" ? "number" : "string"}
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi
@ -39,6 +43,7 @@
value={fieldState.value} value={fieldState.value}
on:change={handleChange} on:change={handleChange}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
error={fieldState.error} error={fieldState.error}
id={fieldState.fieldId} id={fieldState.fieldId}
{placeholder} {placeholder}

View File

@ -40,6 +40,7 @@ export const styleable = (node, styles = {}) => {
const componentId = newStyles.id const componentId = newStyles.id
const customStyles = newStyles.custom || "" const customStyles = newStyles.custom || ""
const { isBlock } = newStyles
const normalStyles = { ...baseStyles, ...newStyles.normal } const normalStyles = { ...baseStyles, ...newStyles.normal }
const hoverStyles = { const hoverStyles = {
...normalStyles, ...normalStyles,
@ -76,6 +77,9 @@ export const styleable = (node, styles = {}) => {
// Handler to start editing a component (if applicable) when double // Handler to start editing a component (if applicable) when double
// clicking in the builder preview // clicking in the builder preview
editComponent = event => { editComponent = event => {
if (isBlock) {
return
}
if (newStyles.interactive && newStyles.editable) { if (newStyles.interactive && newStyles.editable) {
builderStore.actions.setEditMode(true) builderStore.actions.setEditMode(true)
} }

View File

@ -33,7 +33,7 @@ export default class UserFetch extends DataFetch {
let finalQuery let finalQuery
// convert old format to new one - we now allow use of the lucene format // convert old format to new one - we now allow use of the lucene format
const { appId, paginated, ...rest } = query const { appId, paginated, ...rest } = query
if (!LuceneUtils.hasFilters(query) && rest.email) { if (!LuceneUtils.hasFilters(query) && rest.email != null) {
finalQuery = { string: { email: rest.email } } finalQuery = { string: { email: rest.email } }
} else { } else {
finalQuery = rest finalQuery = rest

View File

@ -147,7 +147,7 @@ export const serveApp = async function (ctx: Ctx) {
const { head, html, css } = App.render({ const { head, html, css } = App.render({
metaImage: metaImage:
branding?.metaImageUrl || branding?.metaImageUrl ||
"https://res.cloudinary.com/daog6scxm/image/upload/v1666109324/meta-images/budibase-meta-image_uukc1m.png", "https://res.cloudinary.com/daog6scxm/image/upload/v1698759482/meta-images/plain-branded-meta-image-coral_ocxmgu.png",
metaDescription: branding?.metaDescription || "", metaDescription: branding?.metaDescription || "",
metaTitle: metaTitle:
branding?.metaTitle || `${appInfo.name} - built with Budibase`, branding?.metaTitle || `${appInfo.name} - built with Budibase`,
@ -185,7 +185,7 @@ export const serveApp = async function (ctx: Ctx) {
metaTitle: branding?.metaTitle, metaTitle: branding?.metaTitle,
metaImage: metaImage:
branding?.metaImageUrl || branding?.metaImageUrl ||
"https://res.cloudinary.com/daog6scxm/image/upload/v1666109324/meta-images/budibase-meta-image_uukc1m.png", "https://res.cloudinary.com/daog6scxm/image/upload/v1698759482/meta-images/plain-branded-meta-image-coral_ocxmgu.png",
metaDescription: branding?.metaDescription || "", metaDescription: branding?.metaDescription || "",
favicon: favicon:
branding.faviconUrl !== "" branding.faviconUrl !== ""

View File

@ -15,6 +15,16 @@ import env from "../../../environment"
const Router = require("@koa/router") const Router = require("@koa/router")
const { RateLimit, Stores } = require("koa2-ratelimit") const { RateLimit, Stores } = require("koa2-ratelimit")
import { middleware, redis } from "@budibase/backend-core" import { middleware, redis } from "@budibase/backend-core"
import { SelectableDatabase } from "@budibase/backend-core/src/redis/utils"
interface KoaRateLimitOptions {
socket: {
host: string
port: number
}
password?: string
database?: number
}
const PREFIX = "/api/public/v1" const PREFIX = "/api/public/v1"
// allow a lot more requests when in test // allow a lot more requests when in test
@ -29,32 +39,21 @@ function getApiLimitPerSecond(): number {
let rateLimitStore: any = null let rateLimitStore: any = null
if (!env.isTest()) { if (!env.isTest()) {
const REDIS_OPTS = redis.utils.getRedisOptions() const { password, host, port } = redis.utils.getRedisConnectionDetails()
let options let options: KoaRateLimitOptions = {
if (REDIS_OPTS.redisProtocolUrl) { socket: {
// fully qualified redis URL host: host,
options = { port: port,
url: REDIS_OPTS.redisProtocolUrl, },
} }
} else {
options = {
socket: {
host: REDIS_OPTS.host,
port: REDIS_OPTS.port,
},
}
if (REDIS_OPTS.opts?.password || REDIS_OPTS.opts.redisOptions?.password) { if (password) {
// @ts-ignore options.password = password
options.password = }
REDIS_OPTS.opts.password || REDIS_OPTS.opts.redisOptions.password
}
if (!env.REDIS_CLUSTERED) { if (!env.REDIS_CLUSTERED) {
// @ts-ignore // Can't set direct redis db in clustered env
// Can't set direct redis db in clustered env options.database = SelectableDatabase.RATE_LIMITING
options.database = 1
}
} }
rateLimitStore = new Stores.Redis(options) rateLimitStore = new Stores.Redis(options)
RateLimit.defaultOptions({ RateLimit.defaultOptions({

View File

@ -563,6 +563,56 @@ describe.each([
await assertRowUsage(rowUsage) await assertRowUsage(rowUsage)
await assertQueryUsage(queryUsage) await assertQueryUsage(queryUsage)
}) })
it("should not overwrite links if those links are not set", async () => {
let linkField: FieldSchema = {
type: FieldType.LINK,
name: "",
fieldName: "",
constraints: {
type: "array",
presence: false,
},
relationshipType: RelationshipType.ONE_TO_MANY,
tableId: InternalTable.USER_METADATA,
}
let table = await config.api.table.create({
name: "TestTable",
type: "table",
sourceType: TableSourceType.INTERNAL,
sourceId: INTERNAL_TABLE_SOURCE_ID,
schema: {
user1: { ...linkField, name: "user1", fieldName: "user1" },
user2: { ...linkField, name: "user2", fieldName: "user2" },
},
})
let user1 = await config.createUser()
let user2 = await config.createUser()
let row = await config.api.row.save(table._id!, {
user1: [{ _id: user1._id }],
user2: [{ _id: user2._id }],
})
let getResp = await config.api.row.get(table._id!, row._id!)
expect(getResp.body.user1[0]._id).toEqual(user1._id)
expect(getResp.body.user2[0]._id).toEqual(user2._id)
let patchResp = await config.api.row.patch(table._id!, {
_id: row._id!,
_rev: row._rev!,
tableId: table._id!,
user1: [{ _id: user2._id }],
})
expect(patchResp.user1[0]._id).toEqual(user2._id)
expect(patchResp.user2[0]._id).toEqual(user2._id)
getResp = await config.api.row.get(table._id!, row._id!)
expect(getResp.body.user1[0]._id).toEqual(user2._id)
expect(getResp.body.user2[0]._id).toEqual(user2._id)
})
}) })
describe("destroy", () => { describe("destroy", () => {

View File

@ -36,7 +36,7 @@ describe("Run through some parts of the automations system", () => {
it("should be able to init in builder", async () => { it("should be able to init in builder", async () => {
const automation: Automation = { const automation: Automation = {
...basicAutomation(), ...basicAutomation(),
appId: config.appId, appId: config.appId!,
} }
const fields: any = { a: 1, appId: config.appId } const fields: any = { a: 1, appId: config.appId }
await triggers.externalTrigger(automation, fields) await triggers.externalTrigger(automation, fields)

View File

@ -1,44 +0,0 @@
const setup = require("./utilities")
describe("test the update row action", () => {
let table, row, inputs
let config = setup.getConfig()
beforeAll(async () => {
await config.init()
table = await config.createTable()
row = await config.createRow()
inputs = {
rowId: row._id,
row: {
...row,
name: "Updated name",
// put a falsy option in to be removed
description: "",
}
}
})
afterAll(setup.afterAll)
it("should be able to run the action", async () => {
const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, inputs)
expect(res.success).toEqual(true)
const updatedRow = await config.getRow(table._id, res.id)
expect(updatedRow.name).toEqual("Updated name")
expect(updatedRow.description).not.toEqual("")
})
it("should check invalid inputs return an error", async () => {
const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, {})
expect(res.success).toEqual(false)
})
it("should return an error when table doesn't exist", async () => {
const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, {
row: { _id: "invalid" },
rowId: "invalid",
})
expect(res.success).toEqual(false)
})
})

View File

@ -0,0 +1,169 @@
import {
FieldSchema,
FieldType,
INTERNAL_TABLE_SOURCE_ID,
InternalTable,
RelationshipType,
Row,
Table,
TableSourceType,
} from "@budibase/types"
import * as setup from "./utilities"
import * as uuid from "uuid"
describe("test the update row action", () => {
let table: Table, row: Row, inputs: any
let config = setup.getConfig()
beforeAll(async () => {
await config.init()
table = await config.createTable()
row = await config.createRow()
inputs = {
rowId: row._id,
row: {
...row,
name: "Updated name",
// put a falsy option in to be removed
description: "",
},
}
})
afterAll(setup.afterAll)
it("should be able to run the action", async () => {
const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, inputs)
expect(res.success).toEqual(true)
const updatedRow = await config.getRow(table._id!, res.id)
expect(updatedRow.name).toEqual("Updated name")
expect(updatedRow.description).not.toEqual("")
})
it("should check invalid inputs return an error", async () => {
const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, {})
expect(res.success).toEqual(false)
})
it("should return an error when table doesn't exist", async () => {
const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, {
row: { _id: "invalid" },
rowId: "invalid",
})
expect(res.success).toEqual(false)
})
it("should not overwrite links if those links are not set", async () => {
let linkField: FieldSchema = {
type: FieldType.LINK,
name: "",
fieldName: "",
constraints: {
type: "array",
presence: false,
},
relationshipType: RelationshipType.ONE_TO_MANY,
tableId: InternalTable.USER_METADATA,
}
let table = await config.api.table.create({
name: uuid.v4(),
type: "table",
sourceType: TableSourceType.INTERNAL,
sourceId: INTERNAL_TABLE_SOURCE_ID,
schema: {
user1: { ...linkField, name: "user1", fieldName: uuid.v4() },
user2: { ...linkField, name: "user2", fieldName: uuid.v4() },
},
})
let user1 = await config.createUser()
let user2 = await config.createUser()
let row = await config.api.row.save(table._id!, {
user1: [{ _id: user1._id }],
user2: [{ _id: user2._id }],
})
let getResp = await config.api.row.get(table._id!, row._id!)
expect(getResp.body.user1[0]._id).toEqual(user1._id)
expect(getResp.body.user2[0]._id).toEqual(user2._id)
let stepResp = await setup.runStep(setup.actions.UPDATE_ROW.stepId, {
rowId: row._id,
row: {
_id: row._id,
_rev: row._rev,
tableId: row.tableId,
user1: [user2._id],
user2: "",
},
})
expect(stepResp.success).toEqual(true)
getResp = await config.api.row.get(table._id!, row._id!)
expect(getResp.body.user1[0]._id).toEqual(user2._id)
expect(getResp.body.user2[0]._id).toEqual(user2._id)
})
it("should overwrite links if those links are not set and we ask it do", async () => {
let linkField: FieldSchema = {
type: FieldType.LINK,
name: "",
fieldName: "",
constraints: {
type: "array",
presence: false,
},
relationshipType: RelationshipType.ONE_TO_MANY,
tableId: InternalTable.USER_METADATA,
}
let table = await config.api.table.create({
name: uuid.v4(),
type: "table",
sourceType: TableSourceType.INTERNAL,
sourceId: INTERNAL_TABLE_SOURCE_ID,
schema: {
user1: { ...linkField, name: "user1", fieldName: uuid.v4() },
user2: { ...linkField, name: "user2", fieldName: uuid.v4() },
},
})
let user1 = await config.createUser()
let user2 = await config.createUser()
let row = await config.api.row.save(table._id!, {
user1: [{ _id: user1._id }],
user2: [{ _id: user2._id }],
})
let getResp = await config.api.row.get(table._id!, row._id!)
expect(getResp.body.user1[0]._id).toEqual(user1._id)
expect(getResp.body.user2[0]._id).toEqual(user2._id)
let stepResp = await setup.runStep(setup.actions.UPDATE_ROW.stepId, {
rowId: row._id,
row: {
_id: row._id,
_rev: row._rev,
tableId: row.tableId,
user1: [user2._id],
user2: "",
},
meta: {
fields: {
user2: {
clearRelationships: true,
},
},
},
})
expect(stepResp.success).toEqual(true)
getResp = await config.api.row.get(table._id!, row._id!)
expect(getResp.body.user1[0]._id).toEqual(user2._id)
expect(getResp.body.user2).toBeUndefined()
})
})

View File

@ -4,11 +4,11 @@ import { BUILTIN_ACTION_DEFINITIONS, getAction } from "../../actions"
import emitter from "../../../events/index" import emitter from "../../../events/index"
import env from "../../../environment" import env from "../../../environment"
let config: any let config: TestConfig
export function getConfig() { export function getConfig(): TestConfig {
if (!config) { if (!config) {
config = new TestConfig(false) config = new TestConfig(true)
} }
return config return config
} }

View File

@ -22,7 +22,15 @@ export class TableAPI extends TestAPI {
.send(data) .send(data)
.set(this.config.defaultHeaders()) .set(this.config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(expectStatus)
if (res.status !== expectStatus) {
throw new Error(
`Expected status ${expectStatus} but got ${
res.status
} with body ${JSON.stringify(res.body)}`
)
}
return res.body return res.body
} }

View File

@ -102,6 +102,7 @@ export interface BBReferenceFieldMetadata
extends Omit<BaseFieldSchema, "subtype"> { extends Omit<BaseFieldSchema, "subtype"> {
type: FieldType.BB_REFERENCE type: FieldType.BB_REFERENCE
subtype: FieldSubtype.USER | FieldSubtype.USERS subtype: FieldSubtype.USER | FieldSubtype.USERS
relationshipType?: RelationshipType
} }
export interface FieldConstraints { export interface FieldConstraints {

View File

@ -1,8 +1,7 @@
export enum FeatureFlag { export enum FeatureFlag {
LICENSING = "LICENSING", LICENSING = "LICENSING",
// Feature IDs in Posthog PER_CREATOR_PER_USER_PRICE = "PER_CREATOR_PER_USER_PRICE",
PER_CREATOR_PER_USER_PRICE = "18873", PER_CREATOR_PER_USER_PRICE_ALERT = "PER_CREATOR_PER_USER_PRICE_ALERT",
PER_CREATOR_PER_USER_PRICE_ALERT = "18530",
} }
export interface TenantFeatureFlags { export interface TenantFeatureFlags {

View File

@ -19,7 +19,7 @@
} }
a { a {
color: #3869D4 !important; color: #6E56FF !important;
} }
a img { a img {
@ -109,11 +109,11 @@
/* Buttons ------------------------------ */ /* Buttons ------------------------------ */
.button { .button {
background-color: #3869D4; background-color: #6E56FF;
border-top: 10px solid #3869D4; border-top: 10px solid #6E56FF;
border-right: 18px solid #3869D4; border-right: 18px solid #6E56FF;
border-bottom: 10px solid #3869D4; border-bottom: 10px solid #6E56FF;
border-left: 18px solid #3869D4; border-left: 18px solid #6E56FF;
display: inline-block; display: inline-block;
color: #FFF !important; color: #FFF !important;
text-decoration: none !important; text-decoration: none !important;

View File

@ -16,15 +16,11 @@
cellspacing="0" cellspacing="0"
> >
<img <img
width="32"
height="32" height="32"
style="margin-right:16px; vertical-align: middle;" style="margin-right:16px; vertical-align: middle;"
alt="Budibase Logo" alt="Budibase Logo"
src="https://i.imgur.com/Xhdt1YP.png" src="https://res.cloudinary.com/daog6scxm/image/upload/v1696521007/Branding/Assets/Logo/RGB/Full%20Colour/Budibase_Logo_RGB_FullColour_Negative_e9yziz_1_u6oxzg.png"
/> />
<strong style="vertical-align: middle; font-size: 1.1em">
Budibase
</strong>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@ -31,10 +31,6 @@ import destroyable from "server-destroy"
import { initPro } from "./initPro" import { initPro } from "./initPro"
import { handleScimBody } from "./middleware/handleScimBody" import { handleScimBody } from "./middleware/handleScimBody"
// configure events to use the pro audit log write
// can't integrate directly into backend-core due to cyclic issues
events.processors.init(proSdk.auditLogs.write)
if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE) { if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE) {
console.warn( console.warn(
"Warning: ENABLE_SSO_MAINTENANCE_MODE is set. It is recommended this flag is disabled if maintenance is not in progress" "Warning: ENABLE_SSO_MAINTENANCE_MODE is set. It is recommended this flag is disabled if maintenance is not in progress"
@ -93,6 +89,9 @@ export default server.listen(parseInt(env.PORT || "4002"), async () => {
console.log(`Worker running on ${JSON.stringify(server.address())}`) console.log(`Worker running on ${JSON.stringify(server.address())}`)
await initPro() await initPro()
await redis.init() await redis.init()
// configure events to use the pro audit log write
// can't integrate directly into backend-core due to cyclic issues
await events.processors.init(proSdk.auditLogs.write)
}) })
process.on("uncaughtException", err => { process.on("uncaughtException", err => {