Merge remote-tracking branch 'origin/master' into fix/export-data-ui
This commit is contained in:
commit
7303aaebda
37 changed files with 1022 additions and 212 deletions
25
.github/workflows/tag-release.yml
vendored
25
.github/workflows/tag-release.yml
vendored
|
@ -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 }}"
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.12.12",
|
"version": "2.13.1",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
49
packages/backend-core/src/utils/Duration.ts
Normal file
49
packages/backend-core/src/utils/Duration.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
19
packages/backend-core/src/utils/tests/Duration.spec.ts
Normal file
19
packages/backend-core/src/utils/tests/Duration.spec.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -2776,6 +2776,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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2833,6 +2862,35 @@
|
||||||
"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 +2943,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 +3029,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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -3110,6 +3226,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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -3272,6 +3417,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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -3352,6 +3526,35 @@
|
||||||
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -3431,6 +3634,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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -3512,6 +3744,35 @@
|
||||||
"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 +3859,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 +3890,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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -3789,6 +4095,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 +4192,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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -3909,6 +4273,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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -5590,23 +5983,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 +6297,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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
@ -226,16 +227,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 +250,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>
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
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
|
||||||
|
@ -72,32 +73,25 @@
|
||||||
{field}
|
{field}
|
||||||
{disabled}
|
{disabled}
|
||||||
{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}
|
||||||
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>
|
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
|
@ -14,6 +14,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
|
||||||
|
@ -48,6 +49,7 @@
|
||||||
{beepOnScan}
|
{beepOnScan}
|
||||||
{beepFrequency}
|
{beepFrequency}
|
||||||
{customFrequency}
|
{customFrequency}
|
||||||
|
{preferredCamera}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -13,6 +13,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
|
||||||
|
@ -31,6 +32,7 @@
|
||||||
{disabled}
|
{disabled}
|
||||||
{validation}
|
{validation}
|
||||||
{defaultValue}
|
{defaultValue}
|
||||||
|
{span}
|
||||||
type="datetime"
|
type="datetime"
|
||||||
bind:fieldState
|
bind:fieldState
|
||||||
bind:fieldApi
|
bind:fieldApi
|
||||||
|
|
|
@ -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
|
||||||
|
@ -12,6 +11,7 @@
|
||||||
export let type
|
export let type
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let validation
|
export let validation
|
||||||
|
export let span = 6
|
||||||
|
|
||||||
// Get contexts
|
// Get contexts
|
||||||
const formContext = getContext("form")
|
const formContext = getContext("form")
|
||||||
|
@ -62,40 +62,58 @@
|
||||||
})
|
})
|
||||||
</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">
|
for={fieldState?.fieldId}
|
||||||
{#if !formContext}
|
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelClass}`}
|
||||||
<Placeholder text="Form components need to be wrapped in a form" />
|
>
|
||||||
{:else if !fieldState}
|
{label || " "}
|
||||||
<Placeholder />
|
</label>
|
||||||
{:else if schemaType && schemaType !== type && !["options", "longform"].includes(type)}
|
{/key}
|
||||||
<Placeholder
|
<div class="spectrum-Form-itemField">
|
||||||
text="This Field setting is the wrong data type for this component"
|
{#if !formContext}
|
||||||
/>
|
<Placeholder text="Form components need to be wrapped in a form" />
|
||||||
{:else}
|
{:else if !fieldState}
|
||||||
<slot />
|
<Placeholder />
|
||||||
{#if fieldState.error}
|
{:else if schemaType && schemaType !== type && !["options", "longform"].includes(type)}
|
||||||
<div class="error">{fieldState.error}</div>
|
<Placeholder
|
||||||
{/if}
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,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
|
||||||
|
@ -56,6 +57,7 @@
|
||||||
{label}
|
{label}
|
||||||
{disabled}
|
{disabled}
|
||||||
{validation}
|
{validation}
|
||||||
|
{span}
|
||||||
defaultValue={expandedDefaultValue}
|
defaultValue={expandedDefaultValue}
|
||||||
type="array"
|
type="array"
|
||||||
bind:fieldState
|
bind:fieldState
|
||||||
|
|
|
@ -18,6 +18,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
|
||||||
|
@ -47,6 +48,7 @@
|
||||||
{disabled}
|
{disabled}
|
||||||
{validation}
|
{validation}
|
||||||
{defaultValue}
|
{defaultValue}
|
||||||
|
{span}
|
||||||
type="options"
|
type="options"
|
||||||
bind:fieldState
|
bind:fieldState
|
||||||
bind:fieldApi
|
bind:fieldApi
|
||||||
|
|
|
@ -18,6 +18,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 +138,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,6 +189,7 @@
|
||||||
{validation}
|
{validation}
|
||||||
defaultValue={expandedDefaultValue}
|
defaultValue={expandedDefaultValue}
|
||||||
{type}
|
{type}
|
||||||
|
{span}
|
||||||
bind:fieldState
|
bind:fieldState
|
||||||
bind:fieldApi
|
bind:fieldApi
|
||||||
bind:fieldSchema
|
bind:fieldSchema
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
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
|
||||||
|
@ -29,6 +30,7 @@
|
||||||
{disabled}
|
{disabled}
|
||||||
{validation}
|
{validation}
|
||||||
{defaultValue}
|
{defaultValue}
|
||||||
|
{span}
|
||||||
type={type === "number" ? "number" : "string"}
|
type={type === "number" ? "number" : "string"}
|
||||||
bind:fieldState
|
bind:fieldState
|
||||||
bind:fieldApi
|
bind:fieldApi
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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", () => {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
169
packages/server/src/automations/tests/updateRow.spec.ts
Normal file
169
packages/server/src/automations/tests/updateRow.spec.ts
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
Loading…
Reference in a new issue