Merge remote-tracking branch 'origin/master' into fix/export-data-ui

This commit is contained in:
Dean 2023-11-07 09:09:51 +00:00
commit 7303aaebda
37 changed files with 1022 additions and 212 deletions

View File

@ -1,4 +1,4 @@
name: Tag release
name: Release
concurrency:
group: tag-release
cancel-in-progress: false
@ -19,6 +19,8 @@ on:
jobs:
tag-release:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.tag-release.outputs.version }}
steps:
- name: Fail if branch is not master
@ -33,6 +35,7 @@ jobs:
- run: cd scripts && yarn
- name: Tag release
id: tag-release
run: |
cd scripts
# setup the username and email.
@ -41,3 +44,23 @@ jobs:
BUMP_TYPE_INPUT=${{ github.event.inputs.versioning }}
BUMP_TYPE=${BUMP_TYPE_INPUT:-"patch"}
./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

@ -2,16 +2,18 @@ server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
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;
error_log /dev/stderr warn;
access_log /dev/stdout main;
client_max_body_size 1000m;
ignore_invalid_headers off;
proxy_buffering 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/ {
default_type "text/plain";
root /var/www/html;
@ -47,6 +49,24 @@ server {
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/ {
# calls to the API are rate limited with bursting
limit_req zone=ratelimit burst=20 nodelay;
@ -70,18 +90,49 @@ server {
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 / {
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;
proxy_set_header Host $http_host;
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;
}
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_body_timeout 60;
keepalive_timeout 60;

View File

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

View File

@ -30,6 +30,7 @@ export * as timers from "./timers"
export { default as env } from "./environment"
export * as blacklist from "./blacklist"
export * as docUpdates from "./docUpdates"
export * from "./utils/Duration"
export { SearchParams } from "./db"
// Add context to tenancy for backwards compatibility
// 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
* 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._opts = opts
this._messages = []

View File

@ -2,11 +2,17 @@ import env from "../environment"
import { getRedisOptions } from "../redis/utils"
import { JobQueue } from "./constants"
import InMemoryQueue from "./inMemoryQueue"
import BullQueue from "bull"
import BullQueue, { QueueOptions } from "bull"
import { addListeners, StalledFn } from "./listeners"
import { Duration } from "../utils"
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 cleanupInterval: NodeJS.Timeout
@ -20,8 +26,15 @@ export function createQueue<T>(
jobQueue: JobQueue,
opts: { removeStalledCb?: StalledFn } = {}
): BullQueue.Queue<T> {
const { opts: redisOpts, redisProtocolUrl } = getRedisOptions()
const queueConfig: any = redisProtocolUrl || { redis: redisOpts }
const redisOpts = getRedisOptions()
const queueConfig: QueueOptions = {
redis: redisOpts,
settings: {
maxStalledCount: 0,
lockDuration: QUEUE_LOCK_MS,
lockRenewTime: QUEUE_LOCK_RENEW_INTERNAL_MS,
},
}
let queue: any
if (!env.isTest()) {
queue = new BullQueue(jobQueue, queueConfig)

View File

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

View File

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

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

View File

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

View File

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

View File

@ -2776,6 +2776,35 @@
"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",
"label": "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",
"key": "disabled",
"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",
"label": "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",
"label": "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",
"label": "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",
"label": "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",
"label": "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",
"label": "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"
}
},
{
"type": "select",
"label": "Preferred camera",
"key": "preferredCamera",
"defaultValue": "environment",
"options": [
{
"label": "Front",
"value": "user"
},
{
"label": "Back",
"value": "environment"
}
]
},
{
"type": "event",
"label": "On change",
@ -3613,6 +3890,35 @@
"type": "validation/string",
"label": "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",
"label": "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",
"key": "disabled",
"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",
"key": "disabled",
"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",
"type": "select",
@ -5921,6 +6297,35 @@
"label": "Disabled",
"key": "disabled",
"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
$: inBuilder = $builderStore.inBuilder
$: instance = {
...props,
_component: getComponent(type),
_id: id,
_instanceName: getInstanceName(name, type),
_containsSlot: containsSlot,
_styles: {
...styles,
normal: styles?.normal || {},
},
_containsSlot: containsSlot,
...props,
}
// Register this block component if we're inside the builder so it can be

View File

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

View File

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

View File

@ -2,6 +2,7 @@
import BlockComponent from "components/BlockComponent.svelte"
import Placeholder from "components/app/Placeholder.svelte"
import { makePropSafe as safe } from "@budibase/string-templates"
import { getContext } from "svelte"
export let dataSource
export let actionUrl
@ -9,7 +10,6 @@
export let size
export let disabled
export let fields
export let labelPosition
export let title
export let description
export let saveButtonLabel
@ -33,6 +33,7 @@
barcodeqr: "codescanner",
bb_reference: "bbreferencefield",
}
const context = getContext("context")
let formId
@ -226,16 +227,20 @@
<BlockComponent type="text" props={{ text: description }} order={1} />
{/if}
{#key fields}
<BlockComponent type="fieldgroup" props={{ labelPosition }} order={1}>
{#each fields as field, idx}
{#if getComponentForField(field) && field.active}
<BlockComponent
type={getComponentForField(field)}
props={getPropsForField(field)}
order={idx}
/>
{/if}
{/each}
<BlockComponent type="container">
<div class="form-block fields" class:mobile={$context.device.mobile}>
{#each fields as field, idx}
{#if getComponentForField(field) && field.active}
<BlockComponent
type={getComponentForField(field)}
props={getPropsForField(field)}
order={idx}
interactive
name={field?.field}
/>
{/if}
{/each}
</div>
</BlockComponent>
{/key}
</BlockComponent>
@ -245,3 +250,14 @@
text="Choose your table and add some fields to your form to get started"
/>
{/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

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

View File

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

View File

@ -14,6 +14,7 @@
export let beepOnScan
export let beepFrequency
export let customFrequency
export let preferredCamera
let fieldState
let fieldApi
@ -48,6 +49,7 @@
{beepOnScan}
{beepFrequency}
{customFrequency}
{preferredCamera}
/>
{/if}
</Field>

View File

@ -13,6 +13,7 @@
export let validation
export let defaultValue
export let onChange
export let span
let fieldState
let fieldApi
@ -31,6 +32,7 @@
{disabled}
{validation}
{defaultValue}
{span}
type="datetime"
bind:fieldState
bind:fieldApi

View File

@ -1,6 +1,5 @@
<script>
import Placeholder from "../Placeholder.svelte"
import FieldGroupFallback from "./FieldGroupFallback.svelte"
import { getContext, onDestroy } from "svelte"
export let label
@ -12,6 +11,7 @@
export let type
export let disabled = false
export let validation
export let span = 6
// Get contexts
const formContext = getContext("form")
@ -62,40 +62,58 @@
})
</script>
<FieldGroupFallback>
<div class="spectrum-Form-item" use:styleable={$component.styles}>
{#key $component.editing}
<label
bind:this={labelNode}
contenteditable={$component.editing}
on:blur={$component.editing ? updateLabel : null}
class:hidden={!label}
for={fieldState?.fieldId}
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelClass}`}
>
{label || " "}
</label>
{/key}
<div class="spectrum-Form-itemField">
{#if !formContext}
<Placeholder text="Form components need to be wrapped in a form" />
{:else if !fieldState}
<Placeholder />
{:else if schemaType && schemaType !== type && !["options", "longform"].includes(type)}
<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}
<div
class="spectrum-Form-item"
class:span-2={span === 2}
class:span-3={span === 3}
class:span-6={span === 6 || !span}
use:styleable={$component.styles}
class:above={labelPos === "above"}
>
{#key $component.editing}
<label
bind:this={labelNode}
contenteditable={$component.editing}
on:blur={$component.editing ? updateLabel : null}
class:hidden={!label}
for={fieldState?.fieldId}
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelClass}`}
>
{label || " "}
</label>
{/key}
<div class="spectrum-Form-itemField">
{#if !formContext}
<Placeholder text="Form components need to be wrapped in a form" />
{:else if !fieldState}
<Placeholder />
{:else if schemaType && schemaType !== type && !["options", "longform"].includes(type)}
<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}
</div>
{/if}
</div>
</FieldGroupFallback>
</div>
<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 {
white-space: nowrap;
}

View File

@ -17,6 +17,7 @@
export let onChange
export let optionsType = "select"
export let direction = "vertical"
export let span
let fieldState
let fieldApi
@ -56,6 +57,7 @@
{label}
{disabled}
{validation}
{span}
defaultValue={expandedDefaultValue}
type="array"
bind:fieldState

View File

@ -18,6 +18,7 @@
export let direction = "vertical"
export let onChange
export let sort = true
export let span
let fieldState
let fieldApi
@ -47,6 +48,7 @@
{disabled}
{validation}
{defaultValue}
{span}
type="options"
bind:fieldState
bind:fieldApi

View File

@ -18,6 +18,7 @@
export let filter
export let datasourceType = "table"
export let primaryDisplay
export let span
let fieldState
let fieldApi
@ -137,7 +138,9 @@
typeof value === "object" ? value._id : value
)
// Make sure field state is valid
fieldApi.setValue(values)
if (values?.length > 0) {
fieldApi.setValue(values)
}
return values
}
@ -186,6 +189,7 @@
{validation}
defaultValue={expandedDefaultValue}
{type}
{span}
bind:fieldState
bind:fieldApi
bind:fieldSchema

View File

@ -11,6 +11,7 @@
export let defaultValue = ""
export let align
export let onChange
export let span
let fieldState
let fieldApi
@ -29,6 +30,7 @@
{disabled}
{validation}
{defaultValue}
{span}
type={type === "number" ? "number" : "string"}
bind:fieldState
bind:fieldApi

View File

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

View File

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

View File

@ -563,6 +563,56 @@ describe.each([
await assertRowUsage(rowUsage)
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", () => {

View File

@ -36,7 +36,7 @@ describe("Run through some parts of the automations system", () => {
it("should be able to init in builder", async () => {
const automation: Automation = {
...basicAutomation(),
appId: config.appId,
appId: config.appId!,
}
const fields: any = { a: 1, appId: config.appId }
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 env from "../../../environment"
let config: any
let config: TestConfig
export function getConfig() {
export function getConfig(): TestConfig {
if (!config) {
config = new TestConfig(false)
config = new TestConfig(true)
}
return config
}

View File

@ -22,7 +22,15 @@ export class TableAPI extends TestAPI {
.send(data)
.set(this.config.defaultHeaders())
.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
}

View File

@ -31,10 +31,6 @@ import destroyable from "server-destroy"
import { initPro } from "./initPro"
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) {
console.warn(
"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())}`)
await initPro()
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 => {