diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml index eaf71ae61a..78c07a037c 100644 --- a/.github/workflows/tag-release.yml +++ b/.github/workflows/tag-release.yml @@ -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 }}" + } diff --git a/hosting/letsencrypt/nginx-ssl.conf b/hosting/letsencrypt/nginx-ssl.conf index 50c5e0198a..b3f51e5cc5 100644 --- a/hosting/letsencrypt/nginx-ssl.conf +++ b/hosting/letsencrypt/nginx-ssl.conf @@ -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; diff --git a/lerna.json b/lerna.json index 79f80e0d7d..2126adac29 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.12.12", + "version": "2.13.1", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index ffffd8240a..c7cf9f56cc 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -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 diff --git a/packages/backend-core/src/queue/inMemoryQueue.ts b/packages/backend-core/src/queue/inMemoryQueue.ts index af2ec6dbaa..a8add7ecb6 100644 --- a/packages/backend-core/src/queue/inMemoryQueue.ts +++ b/packages/backend-core/src/queue/inMemoryQueue.ts @@ -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 = [] diff --git a/packages/backend-core/src/queue/queue.ts b/packages/backend-core/src/queue/queue.ts index 0658147709..0657437a3b 100644 --- a/packages/backend-core/src/queue/queue.ts +++ b/packages/backend-core/src/queue/queue.ts @@ -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( jobQueue: JobQueue, opts: { removeStalledCb?: StalledFn } = {} ): BullQueue.Queue { - 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) diff --git a/packages/backend-core/src/redis/redis.ts b/packages/backend-core/src/redis/redis.ts index d1e2d8989e..6f1b573718 100644 --- a/packages/backend-core/src/redis/redis.ts +++ b/packages/backend-core/src/redis/redis.ts @@ -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) } diff --git a/packages/backend-core/src/redis/utils.ts b/packages/backend-core/src/redis/utils.ts index 34b7275a2b..5187fe13f8 100644 --- a/packages/backend-core/src/redis/utils.ts +++ b/packages/backend-core/src/redis/utils.ts @@ -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) { diff --git a/packages/backend-core/src/utils/Duration.ts b/packages/backend-core/src/utils/Duration.ts new file mode 100644 index 0000000000..f376c2f7c7 --- /dev/null +++ b/packages/backend-core/src/utils/Duration.ts @@ -0,0 +1,49 @@ +export enum DurationType { + MILLISECONDS = "milliseconds", + SECONDS = "seconds", + MINUTES = "minutes", + HOURS = "hours", + DAYS = "days", +} + +const conversion: Record = { + 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) + } +} diff --git a/packages/backend-core/src/utils/index.ts b/packages/backend-core/src/utils/index.ts index 318a7f13ba..ac17227459 100644 --- a/packages/backend-core/src/utils/index.ts +++ b/packages/backend-core/src/utils/index.ts @@ -1,3 +1,4 @@ export * from "./hashing" export * from "./utils" export * from "./stringUtils" +export * from "./Duration" diff --git a/packages/backend-core/src/utils/tests/Duration.spec.ts b/packages/backend-core/src/utils/tests/Duration.spec.ts new file mode 100644 index 0000000000..46b996f788 --- /dev/null +++ b/packages/backend-core/src/utils/tests/Duration.spec.ts @@ -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) + }) +}) diff --git a/packages/bbui/src/Form/Core/Dropzone.svelte b/packages/bbui/src/Form/Core/Dropzone.svelte index 0b6a9bb94f..bfa0285250 100644 --- a/packages/bbui/src/Form/Core/Dropzone.svelte +++ b/packages/bbui/src/Form/Core/Dropzone.svelte @@ -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; } diff --git a/packages/builder/src/components/design/settings/controls/EditComponentPopover.svelte b/packages/builder/src/components/design/settings/controls/EditComponentPopover.svelte index 1533c0d1d5..26c1ced502 100644 --- a/packages/builder/src/components/design/settings/controls/EditComponentPopover.svelte +++ b/packages/builder/src/components/design/settings/controls/EditComponentPopover.svelte @@ -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 @@ { + 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) } diff --git a/packages/client/manifest.json b/packages/client/manifest.json index eef1e50b7c..eeb8366af8 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -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" + } + ] } ] } diff --git a/packages/client/src/components/BlockComponent.svelte b/packages/client/src/components/BlockComponent.svelte index c9516b0d71..12555a7fb5 100644 --- a/packages/client/src/components/BlockComponent.svelte +++ b/packages/client/src/components/BlockComponent.svelte @@ -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 diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 03aab21d38..63ce8dc152 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -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, diff --git a/packages/client/src/components/app/blocks/form/FormBlock.svelte b/packages/client/src/components/app/blocks/form/FormBlock.svelte index f905227af9..e4d3b55eff 100644 --- a/packages/client/src/components/app/blocks/form/FormBlock.svelte +++ b/packages/client/src/components/app/blocks/form/FormBlock.svelte @@ -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, diff --git a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte index f7e9a0d2ed..e78cbd89c7 100644 --- a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte +++ b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte @@ -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 @@ {/if} {#key fields} - - {#each fields as field, idx} - {#if getComponentForField(field) && field.active} - - {/if} - {/each} + +
+ {#each fields as field, idx} + {#if getComponentForField(field) && field.active} + + {/if} + {/each} +
{/key}
@@ -245,3 +250,14 @@ text="Choose your table and add some fields to your form to get started" /> {/if} + + diff --git a/packages/client/src/components/app/forms/AttachmentField.svelte b/packages/client/src/components/app/forms/AttachmentField.svelte index e24115ebc0..f28d4801d4 100644 --- a/packages/client/src/components/app/forms/AttachmentField.svelte +++ b/packages/client/src/components/app/forms/AttachmentField.svelte @@ -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={[]} > -
- {#if fieldState} - - {/if} -
+ {#if fieldState} + + {/if} - - diff --git a/packages/client/src/components/app/forms/CodeScanner.svelte b/packages/client/src/components/app/forms/CodeScanner.svelte index 04d6919157..f2e32004cf 100644 --- a/packages/client/src/components/app/forms/CodeScanner.svelte +++ b/packages/client/src/components/app/forms/CodeScanner.svelte @@ -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 }, diff --git a/packages/client/src/components/app/forms/CodeScannerField.svelte b/packages/client/src/components/app/forms/CodeScannerField.svelte index c408f78d7c..1a57d8fbe2 100644 --- a/packages/client/src/components/app/forms/CodeScannerField.svelte +++ b/packages/client/src/components/app/forms/CodeScannerField.svelte @@ -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} diff --git a/packages/client/src/components/app/forms/DateTimeField.svelte b/packages/client/src/components/app/forms/DateTimeField.svelte index 6bcd20d250..661c0c2fad 100644 --- a/packages/client/src/components/app/forms/DateTimeField.svelte +++ b/packages/client/src/components/app/forms/DateTimeField.svelte @@ -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 diff --git a/packages/client/src/components/app/forms/Field.svelte b/packages/client/src/components/app/forms/Field.svelte index 5d4da5afef..f6fbe37681 100644 --- a/packages/client/src/components/app/forms/Field.svelte +++ b/packages/client/src/components/app/forms/Field.svelte @@ -1,6 +1,5 @@ - -
- {#key $component.editing} - - {/key} -
- {#if !formContext} - - {:else if !fieldState} - - {:else if schemaType && schemaType !== type && !["options", "longform"].includes(type)} - - {:else} - - {#if fieldState.error} -
{fieldState.error}
- {/if} +
+ {#key $component.editing} + + {/key} +
+ {#if !formContext} + + {:else if !fieldState} + + {:else if schemaType && schemaType !== type && !["options", "longform"].includes(type)} + + {:else} + + {#if fieldState.error} +
{fieldState.error}
{/if} -
+ {/if}
- +