1
0
Fork 0
mirror of synced 2024-08-15 10:01:34 +12:00

Merge branch 'master' into master

This commit is contained in:
kellis5137 2023-11-06 13:46:08 -05:00 committed by GitHub
commit c4cbebca79
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
95 changed files with 2036 additions and 1224 deletions

View file

@ -11,10 +11,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: peter-evans/repository-dispatch@v2 - uses: peter-evans/repository-dispatch@v2
env:
PAYLOAD_VERSION: ${{ github.sha }}
REF_NAME: ${{ github.ref_name}}
with: with:
repository: budibase/budibase-deploys repository: budibase/budibase-deploys
event-type: budicloud-qa-deploy event-type: budicloud-qa-deploy
token: ${{ secrets.GH_ACCESS_TOKEN }} token: ${{ secrets.GH_ACCESS_TOKEN }}
client-payload: |-
{
"VERSION": "${{ github.sha }}",
"REF_NAME": "${{ github.ref_name}}"
}

View file

@ -165,17 +165,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Get the current budibase release version
id: version
run: |
release_version=$(cat lerna.json | jq -r '.version')
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- uses: passeidireto/trigger-external-workflow-action@main - uses: peter-evans/repository-dispatch@v2
env:
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
REF_NAME: ${{ github.ref_name}}
with: with:
repository: budibase/budibase-deploys repository: budibase/budibase-deploys
event: budicloud-qa-deploy event-type: budicloud-qa-deploy
github_pat: ${{ secrets.GH_ACCESS_TOKEN }} token: ${{ secrets.GH_ACCESS_TOKEN }}
client-payload: |-
{
"VERSION": "${{ github.ref_name }}",
"REF_NAME": "${{ github.ref_name}}"
}

View file

@ -66,7 +66,7 @@ jobs:
context: . context: .
push: true push: true
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
build-args: BUDIBASE_VERSION=$BUDIBASE_VERSION build-args: BUDIBASE_VERSION=${{ env.BUDIBASE_VERSION }}
tags: budibase/budibase,budibase/budibase:${{ env.RELEASE_VERSION }} tags: budibase/budibase,budibase/budibase:${{ env.RELEASE_VERSION }}
file: ./hosting/single/Dockerfile.v2 file: ./hosting/single/Dockerfile.v2
env: env:
@ -79,7 +79,7 @@ jobs:
platforms: linux/amd64 platforms: linux/amd64
build-args: | build-args: |
TARGETBUILD=aas TARGETBUILD=aas
BUDIBASE_VERSION=$BUDIBASE_VERSION BUDIBASE_VERSION=${{ env.BUDIBASE_VERSION }}
tags: budibase/budibase-aas,budibase/budibase-aas:${{ env.RELEASE_VERSION }} tags: budibase/budibase-aas,budibase/budibase-aas:${{ env.RELEASE_VERSION }}
file: ./hosting/single/Dockerfile.v2 file: ./hosting/single/Dockerfile.v2
env: env:

View file

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

View file

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

View file

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

View file

@ -77,7 +77,7 @@ mkdir -p ${DATA_DIR}/minio
chown -R couchdb:couchdb ${DATA_DIR}/couch chown -R couchdb:couchdb ${DATA_DIR}/couch
redis-server --requirepass $REDIS_PASSWORD > /dev/stdout 2>&1 & redis-server --requirepass $REDIS_PASSWORD > /dev/stdout 2>&1 &
/bbcouch-runner.sh & /bbcouch-runner.sh &
minio server --console-address ":9001" ${DATA_DIR}/minio > /dev/stdout 2>&1 & /minio/minio server --console-address ":9001" ${DATA_DIR}/minio > /dev/stdout 2>&1 &
/etc/init.d/nginx restart /etc/init.d/nginx restart
if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then
# Add monthly cron job to renew certbot certificate # Add monthly cron job to renew certbot certificate

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -27,7 +27,7 @@
"scripts": { "scripts": {
"preinstall": "node scripts/syncProPackage.js", "preinstall": "node scripts/syncProPackage.js",
"setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev", "setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev",
"build": "lerna run build --stream", "build": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream",
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput", "build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
"check:types": "lerna run check:types", "check:types": "lerna run check:types",
"build:sdk": "lerna run --stream build:sdk", "build:sdk": "lerna run --stream build:sdk",

View file

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

View file

@ -18,8 +18,12 @@ export const ObjectStoreBuckets = {
} }
const bbTmp = join(tmpdir(), ".budibase") const bbTmp = join(tmpdir(), ".budibase")
if (!fs.existsSync(bbTmp)) { try {
fs.mkdirSync(bbTmp) fs.mkdirSync(bbTmp)
} catch (e: any) {
if (e.code !== "EEXIST") {
throw e
}
} }
export function budibaseTempDir() { export function budibaseTempDir() {

View file

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

View file

@ -2,18 +2,11 @@ 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, { QueueOptions } from "bull" import BullQueue 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"
import * as Redis from "ioredis"
// the queue lock is held for 5 minutes const CLEANUP_PERIOD_MS = 60 * 1000
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
@ -28,14 +21,7 @@ export function createQueue<T>(
opts: { removeStalledCb?: StalledFn } = {} opts: { removeStalledCb?: StalledFn } = {}
): BullQueue.Queue<T> { ): BullQueue.Queue<T> {
const { opts: redisOpts, redisProtocolUrl } = getRedisOptions() const { opts: redisOpts, redisProtocolUrl } = getRedisOptions()
const queueConfig: QueueOptions = { const queueConfig: any = redisProtocolUrl || { redis: redisOpts }
redis: redisProtocolUrl! || (redisOpts as Redis.RedisOptions),
settings: {
maxStalledCount: 0,
lockDuration: QUEUE_LOCK_MS,
lockRenewTime: QUEUE_LOCK_RENEW_INTERNAL_MS,
},
}
let queue: any let queue: any
if (!env.isTest()) { if (!env.isTest()) {
queue = new BullQueue(jobQueue, queueConfig) queue = new BullQueue(jobQueue, queueConfig)

View file

@ -1,49 +0,0 @@
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,4 +1,3 @@
export * from "./hashing" export * from "./hashing"
export * from "./utils" export * from "./utils"
export * from "./stringUtils" export * from "./stringUtils"
export * from "./Duration"

View file

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

View file

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

View file

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

View file

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

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

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

Before

Width:  |  Height:  |  Size: 1.2 KiB

View file

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

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 787 B

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -30,15 +30,15 @@
part2: PrettyRelationshipDefinitions.MANY, part2: PrettyRelationshipDefinitions.MANY,
}, },
[RelationshipType.MANY_TO_ONE]: { [RelationshipType.MANY_TO_ONE]: {
part1: PrettyRelationshipDefinitions.ONE, part1: PrettyRelationshipDefinitions.MANY,
part2: PrettyRelationshipDefinitions.MANY, part2: PrettyRelationshipDefinitions.ONE,
}, },
} }
let relationshipOpts1 = Object.values(PrettyRelationshipDefinitions) let relationshipOpts1 = Object.values(PrettyRelationshipDefinitions)
let relationshipOpts2 = Object.values(PrettyRelationshipDefinitions) let relationshipOpts2 = Object.values(PrettyRelationshipDefinitions)
let relationshipPart1 = PrettyRelationshipDefinitions.MANY let relationshipPart1 = PrettyRelationshipDefinitions.ONE
let relationshipPart2 = PrettyRelationshipDefinitions.ONE let relationshipPart2 = PrettyRelationshipDefinitions.MANY
let originalFromColumnName = toRelationship.name, let originalFromColumnName = toRelationship.name,
originalToColumnName = fromRelationship.name originalToColumnName = fromRelationship.name

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import { Checkbox, Select, RadioGroup, Stepper, Input } from "@budibase/bbui" import { Checkbox, Select, RadioGroup, Stepper, Input } from "@budibase/bbui"
import DataSourceSelect from "./controls/DataSourceSelect.svelte" import DataSourceSelect from "./controls/DataSourceSelect/DataSourceSelect.svelte"
import S3DataSourceSelect from "./controls/S3DataSourceSelect.svelte" import S3DataSourceSelect from "./controls/S3DataSourceSelect.svelte"
import DataProviderSelect from "./controls/DataProviderSelect.svelte" import DataProviderSelect from "./controls/DataProviderSelect.svelte"
import ButtonActionEditor from "./controls/ButtonActionEditor/ButtonActionEditor.svelte" import ButtonActionEditor from "./controls/ButtonActionEditor/ButtonActionEditor.svelte"

View file

@ -0,0 +1,55 @@
<script>
import { Divider, Heading } from "@budibase/bbui"
export let dividerState
export let heading
export let dataSet
export let value
export let onSelect
</script>
{#if dividerState}
<Divider />
{/if}
{#if heading}
<div class="title">
<Heading size="XS">{heading}</Heading>
</div>
{/if}
<ul class="spectrum-Menu" role="listbox">
{#each dataSet as data}
<li
class="spectrum-Menu-item"
class:is-selected={value?.label === data.label &&
value?.type === data.type}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => onSelect(data)}
>
<span class="spectrum-Menu-itemLabel">
{data.label}
</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/each}
</ul>
<style>
.title {
padding: 0 var(--spacing-m) var(--spacing-s) var(--spacing-m);
}
ul {
list-style: none;
padding-left: 0px;
margin: 0px;
width: 100%;
}
</style>

View file

@ -7,10 +7,8 @@
import { import {
Button, Button,
Popover, Popover,
Divider,
Select, Select,
Layout, Layout,
Heading,
Drawer, Drawer,
DrawerContent, DrawerContent,
Icon, Icon,
@ -32,6 +30,7 @@
import IntegrationQueryEditor from "components/integration/index.svelte" import IntegrationQueryEditor from "components/integration/index.svelte"
import { makePropSafe as safe } from "@budibase/string-templates" import { makePropSafe as safe } from "@budibase/string-templates"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte" import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import DataSourceCategory from "components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte"
import { API } from "api" import { API } from "api"
export let value = {} export let value = {}
@ -279,102 +278,81 @@
</div> </div>
<Popover bind:this={dropdownRight} anchor={anchorRight}> <Popover bind:this={dropdownRight} anchor={anchorRight}>
<div class="dropdown"> <div class="dropdown">
<div class="title"> <DataSourceCategory
<Heading size="XS">Tables</Heading> heading="Tables"
</div> dataSet={tables}
<ul> {value}
{#each tables as table} onSelect={handleSelected}
<li on:click={() => handleSelected(table)}>{table.label}</li> />
{/each}
</ul>
{#if views?.length} {#if views?.length}
<Divider /> <DataSourceCategory
<div class="title"> dividerState={true}
<Heading size="XS">Views</Heading> heading="Views"
</div> dataSet={views}
<ul> {value}
{#each views as view} onSelect={handleSelected}
<li on:click={() => handleSelected(view)}>{view.label}</li> />
{/each}
</ul>
{/if} {/if}
{#if queries?.length} {#if queries?.length}
<Divider /> <DataSourceCategory
<div class="title"> dividerState={true}
<Heading size="XS">Queries</Heading> heading="Queries"
</div> dataSet={queries}
<ul> {value}
{#each queries as query} onSelect={handleSelected}
<li />
class:selected={value === query}
on:click={() => handleSelected(query)}
>
{query.label}
</li>
{/each}
</ul>
{/if} {/if}
{#if links?.length} {#if links?.length}
<Divider /> <DataSourceCategory
<div class="title"> dividerState={true}
<Heading size="XS">Relationships</Heading> heading="Links"
</div> dataSet={links}
<ul> {value}
{#each links as link} onSelect={handleSelected}
<li on:click={() => handleSelected(link)}>{link.label}</li> />
{/each}
</ul>
{/if} {/if}
{#if fields?.length} {#if fields?.length}
<Divider /> <DataSourceCategory
<div class="title"> dividerState={true}
<Heading size="XS">Fields</Heading> heading="Fields"
</div> dataSet={fields}
<ul> {value}
{#each fields as field} onSelect={handleSelected}
<li on:click={() => handleSelected(field)}>{field.label}</li> />
{/each}
</ul>
{/if} {/if}
{#if jsonArrays?.length} {#if jsonArrays?.length}
<Divider /> <DataSourceCategory
<div class="title"> dividerState={true}
<Heading size="XS">JSON Arrays</Heading> heading="JSON Arrays"
</div> dataSet={jsonArrays}
<ul> {value}
{#each jsonArrays as field} onSelect={handleSelected}
<li on:click={() => handleSelected(field)}>{field.label}</li> />
{/each}
</ul>
{/if} {/if}
{#if showDataProviders && dataProviders?.length} {#if showDataProviders && dataProviders?.length}
<Divider /> <DataSourceCategory
<div class="title"> dividerState={true}
<Heading size="XS">Data Providers</Heading> heading="Data Providers"
</div> dataSet={dataProviders}
<ul> {value}
{#each dataProviders as provider} onSelect={handleSelected}
<li />
class:selected={value === provider} {/if}
on:click={() => handleSelected(provider)} <DataSourceCategory
> dividerState={true}
{provider.label} heading="Other"
</li> dataSet={[custom]}
{/each} {value}
</ul> onSelect={handleSelected}
/>
{#if otherSources?.length}
<DataSourceCategory
dividerState={false}
dataSet={otherSources}
{value}
onSelect={handleSelected}
/>
{/if} {/if}
<Divider />
<div class="title">
<Heading size="XS">Other</Heading>
</div>
<ul>
<li on:click={() => handleSelected(custom)}>{custom.label}</li>
{#if otherSources?.length}
{#each otherSources as source}
<li on:click={() => handleSelected(source)}>{source.label}</li>
{/each}
{/if}
</ul>
</div> </div>
</Popover> </Popover>
@ -398,31 +376,6 @@
.dropdown { .dropdown {
padding: var(--spacing-m) 0; padding: var(--spacing-m) 0;
z-index: 99999999; z-index: 99999999;
overflow-y: scroll;
}
.title {
padding: 0 var(--spacing-m) var(--spacing-s) var(--spacing-m);
}
ul {
list-style: none;
padding-left: 0px;
margin: 0px;
}
li {
cursor: pointer;
margin: 0px;
padding: var(--spacing-s) var(--spacing-m);
font-size: var(--font-size-m);
}
.selected {
color: var(--spectrum-global-color-blue-600);
}
li:hover {
background-color: var(--spectrum-global-color-gray-200);
} }
.icon { .icon {

View file

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

View file

@ -1,5 +1,5 @@
<script> <script>
import DataSourceSelect from "./DataSourceSelect.svelte" import DataSourceSelect from "./DataSourceSelect/DataSourceSelect.svelte"
const otherSources = [{ name: "Custom", label: "Custom" }] const otherSources = [{ name: "Custom", label: "Custom" }]
</script> </script>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@
import BlockComponent from "components/BlockComponent.svelte" import BlockComponent from "components/BlockComponent.svelte"
import Placeholder from "components/app/Placeholder.svelte" import Placeholder from "components/app/Placeholder.svelte"
import { makePropSafe as safe } from "@budibase/string-templates" import { makePropSafe as safe } from "@budibase/string-templates"
import { getContext } from "svelte"
export let dataSource export let dataSource
export let actionUrl export let actionUrl
@ -9,7 +10,6 @@
export let size export let size
export let disabled export let disabled
export let fields export let fields
export let labelPosition
export let title export let title
export let description export let description
export let saveButtonLabel export let saveButtonLabel
@ -33,6 +33,7 @@
barcodeqr: "codescanner", barcodeqr: "codescanner",
bb_reference: "bbreferencefield", bb_reference: "bbreferencefield",
} }
const context = getContext("context")
let formId let formId
@ -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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
<script> <script>
import Placeholder from "../Placeholder.svelte" import Placeholder from "../Placeholder.svelte"
import FieldGroupFallback from "./FieldGroupFallback.svelte"
import { getContext, onDestroy } from "svelte" import { getContext, onDestroy } from "svelte"
export let label export let label
@ -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;
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,27 +1,10 @@
<script context="module">
// We can create a module level cache for all relationship cells to avoid
// having to fetch the table definition one time for each cell
let primaryDisplayCache = {}
const getPrimaryDisplayForTableId = async (API, tableId) => {
if (primaryDisplayCache[tableId]) {
return primaryDisplayCache[tableId]
}
const definition = await API.fetchTableDefinition(tableId)
const primaryDisplay =
definition?.primaryDisplay || definition?.schema?.[0]?.name
primaryDisplayCache[tableId] = primaryDisplay
return primaryDisplay
}
</script>
<script> <script>
import { getColor } from "../lib/utils" import { getColor } from "../lib/utils"
import { onMount, getContext } from "svelte" import { onMount, getContext } from "svelte"
import { Icon, Input, ProgressCircle, clickOutside } from "@budibase/bbui" import { Icon, Input, ProgressCircle, clickOutside } from "@budibase/bbui"
import { debounce } from "../../../utils/utils" import { debounce } from "../../../utils/utils"
const { API, dispatch } = getContext("grid") const { API, dispatch, cache } = getContext("grid")
export let value export let value
export let api export let api
@ -147,7 +130,9 @@
// Find the primary display for the related table // Find the primary display for the related table
if (!primaryDisplay) { if (!primaryDisplay) {
searching = true searching = true
primaryDisplay = await getPrimaryDisplayForTableId(API, schema.tableId) primaryDisplay = await cache.actions.getPrimaryDisplayForTableId(
schema.tableId
)
} }
// Show initial list of results // Show initial list of results
@ -195,7 +180,7 @@
const toggleRow = async row => { const toggleRow = async row => {
if (value?.some(x => x._id === row._id)) { if (value?.some(x => x._id === row._id)) {
// If the row is already included, remove it and update the candidate // If the row is already included, remove it and update the candidate
// row to be the the same position if possible // row to be the same position if possible
if (oneRowOnly) { if (oneRowOnly) {
await onChange([]) await onChange([])
} else { } else {
@ -260,31 +245,29 @@
class:wrap={editable || contentLines > 1} class:wrap={editable || contentLines > 1}
on:wheel={e => (focused ? e.stopPropagation() : null)} on:wheel={e => (focused ? e.stopPropagation() : null)}
> >
{#if Array.isArray(value) && value.length} {#each value || [] as relationship}
{#each value as relationship} {#if relationship[primaryDisplay] || relationship.primaryDisplay}
{#if relationship[primaryDisplay] || relationship.primaryDisplay} <div class="badge">
<div class="badge"> <span
<span on:click={editable
on:click={editable ? () => showRelationship(relationship._id)
? () => showRelationship(relationship._id) : null}
: null} >
> {readable(
{readable( relationship[primaryDisplay] || relationship.primaryDisplay
relationship[primaryDisplay] || relationship.primaryDisplay )}
)} </span>
</span> {#if editable}
{#if editable} <Icon
<Icon name="Close"
name="Close" size="XS"
size="XS" hoverable
hoverable on:click={() => toggleRow(relationship)}
on:click={() => toggleRow(relationship)} />
/> {/if}
{/if} </div>
</div> {/if}
{/if} {/each}
{/each}
{/if}
{#if editable} {#if editable}
<div class="add" on:click={open}> <div class="add" on:click={open}>
<Icon name="Add" size="S" /> <Icon name="Add" size="S" />
@ -320,7 +303,7 @@
<div class="searching"> <div class="searching">
<ProgressCircle size="S" /> <ProgressCircle size="S" />
</div> </div>
{:else if Array.isArray(searchResults) && searchResults.length} {:else if searchResults?.length}
<div class="results"> <div class="results">
{#each searchResults as row, idx} {#each searchResults as row, idx}
<div <div

View file

@ -0,0 +1,47 @@
export const createActions = context => {
const { API } = context
// Cache for the primary display columns of different tables.
// If we ever need to cache table definitions for other purposes then we can
// expand this to be a more generic cache.
let primaryDisplayCache = {}
const resetPrimaryDisplayCache = () => {
primaryDisplayCache = {}
}
const getPrimaryDisplayForTableId = async tableId => {
// If we've never encountered this tableId before then store a promise that
// resolves to the primary display so that subsequent invocations before the
// promise completes can reuse this promise
if (!primaryDisplayCache[tableId]) {
primaryDisplayCache[tableId] = new Promise(resolve => {
API.fetchTableDefinition(tableId).then(def => {
const display = def?.primaryDisplay || def?.schema?.[0]?.name
primaryDisplayCache[tableId] = display
resolve(display)
})
})
}
// We await the result so that we account for both promises and primitives
return await primaryDisplayCache[tableId]
}
return {
cache: {
actions: {
getPrimaryDisplayForTableId,
resetPrimaryDisplayCache,
},
},
}
}
export const initialise = context => {
const { datasource, cache } = context
// Wipe the caches whenever the datasource changes to ensure we aren't
// storing any stale information
datasource.subscribe(cache.actions.resetPrimaryDisplayCache)
}

View file

@ -160,11 +160,6 @@ export const createActions = context => {
return getAPI()?.actions.canUseColumn(name) return getAPI()?.actions.canUseColumn(name)
} }
// Gets the default number of rows for a single page
const getFeatures = () => {
return getAPI()?.actions.getFeatures()
}
return { return {
datasource: { datasource: {
...datasource, ...datasource,
@ -177,7 +172,6 @@ export const createActions = context => {
getRow, getRow,
isDatasourceValid, isDatasourceValid,
canUseColumn, canUseColumn,
getFeatures,
}, },
}, },
} }

View file

@ -35,11 +35,6 @@ export const createActions = context => {
return $columns.some(col => col.name === name) || $sticky?.name === name return $columns.some(col => col.name === name) || $sticky?.name === name
} }
const getFeatures = () => {
// We don't support any features
return {}
}
return { return {
nonPlus: { nonPlus: {
actions: { actions: {
@ -50,7 +45,6 @@ export const createActions = context => {
getRow, getRow,
isDatasourceValid, isDatasourceValid,
canUseColumn, canUseColumn,
getFeatures,
}, },
}, },
} }

View file

@ -1,5 +1,4 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import TableFetch from "../../../../fetch/TableFetch"
const SuppressErrors = true const SuppressErrors = true
@ -46,10 +45,6 @@ export const createActions = context => {
return $columns.some(col => col.name === name) || $sticky?.name === name return $columns.some(col => col.name === name) || $sticky?.name === name
} }
const getFeatures = () => {
return new TableFetch({ API }).determineFeatureFlags()
}
return { return {
table: { table: {
actions: { actions: {
@ -60,7 +55,6 @@ export const createActions = context => {
getRow, getRow,
isDatasourceValid, isDatasourceValid,
canUseColumn, canUseColumn,
getFeatures,
}, },
}, },
} }

View file

@ -1,5 +1,4 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import ViewV2Fetch from "../../../../fetch/ViewV2Fetch"
const SuppressErrors = true const SuppressErrors = true
@ -46,10 +45,6 @@ export const createActions = context => {
) )
} }
const getFeatures = () => {
return new ViewV2Fetch({ API }).determineFeatureFlags()
}
return { return {
viewV2: { viewV2: {
actions: { actions: {
@ -60,7 +55,6 @@ export const createActions = context => {
getRow, getRow,
isDatasourceValid, isDatasourceValid,
canUseColumn, canUseColumn,
getFeatures,
}, },
}, },
} }

View file

@ -19,6 +19,7 @@ import * as Datasource from "./datasource"
import * as Table from "./datasources/table" import * as Table from "./datasources/table"
import * as ViewV2 from "./datasources/viewV2" import * as ViewV2 from "./datasources/viewV2"
import * as NonPlus from "./datasources/nonPlus" import * as NonPlus from "./datasources/nonPlus"
import * as Cache from "./cache"
const DependencyOrderedStores = [ const DependencyOrderedStores = [
Sort, Sort,
@ -42,6 +43,7 @@ const DependencyOrderedStores = [
Clipboard, Clipboard,
Config, Config,
Notifications, Notifications,
Cache,
] ]
export const attachStores = context => { export const attachStores = context => {

View file

@ -114,10 +114,6 @@ export const createActions = context => {
const $allFilters = get(allFilters) const $allFilters = get(allFilters)
const $sort = get(sort) const $sort = get(sort)
// Determine how many rows to fetch per page
const features = datasource.actions.getFeatures()
const limit = features?.supportsPagination ? RowPageSize : null
// Create new fetch model // Create new fetch model
const newFetch = fetchData({ const newFetch = fetchData({
API, API,
@ -126,8 +122,12 @@ export const createActions = context => {
filter: $allFilters, filter: $allFilters,
sortColumn: $sort.column, sortColumn: $sort.column,
sortOrder: $sort.order, sortOrder: $sort.order,
limit, limit: RowPageSize,
paginate: true, paginate: true,
// Disable client side limiting, so that for queries and custom data
// sources we don't impose fake row limits. We want all the data.
clientSideLimiting: false,
}, },
}) })

View file

@ -43,6 +43,11 @@ export default class DataFetch {
// Pagination config // Pagination config
paginate: true, paginate: true,
// Client side feature customisation
clientSideSearching: true,
clientSideSorting: true,
clientSideLimiting: true,
} }
// State of the fetch // State of the fetch
@ -208,24 +213,32 @@ export default class DataFetch {
* Fetches some filtered, sorted and paginated data * Fetches some filtered, sorted and paginated data
*/ */
async getPage() { async getPage() {
const { sortColumn, sortOrder, sortType, limit } = this.options const {
sortColumn,
sortOrder,
sortType,
limit,
clientSideSearching,
clientSideSorting,
clientSideLimiting,
} = this.options
const { query } = get(this.store) const { query } = get(this.store)
// Get the actual data // Get the actual data
let { rows, info, hasNextPage, cursor, error } = await this.getData() let { rows, info, hasNextPage, cursor, error } = await this.getData()
// If we don't support searching, do a client search // If we don't support searching, do a client search
if (!this.features.supportsSearch) { if (!this.features.supportsSearch && clientSideSearching) {
rows = runLuceneQuery(rows, query) rows = runLuceneQuery(rows, query)
} }
// If we don't support sorting, do a client-side sort // If we don't support sorting, do a client-side sort
if (!this.features.supportsSort) { if (!this.features.supportsSort && clientSideSorting) {
rows = luceneSort(rows, sortColumn, sortOrder, sortType) rows = luceneSort(rows, sortColumn, sortOrder, sortType)
} }
// If we don't support pagination, do a client-side limit // If we don't support pagination, do a client-side limit
if (!this.features.supportsPagination) { if (!this.features.supportsPagination && clientSideLimiting) {
rows = luceneLimit(rows, limit) rows = luceneLimit(rows, limit)
} }

View file

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

View file

@ -49,7 +49,12 @@ describe.each([
let table: Table let table: Table
let tableId: string let tableId: string
afterAll(setup.afterAll) afterAll(async () => {
if (dsProvider) {
await dsProvider.stopContainer()
}
setup.afterAll()
})
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
@ -521,20 +526,17 @@ describe.each([
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage() const queryUsage = await getQueryUsage()
const res = await config.api.row.patch(table._id!, { const row = await config.api.row.patch(table._id!, {
_id: existing._id!, _id: existing._id!,
_rev: existing._rev!, _rev: existing._rev!,
tableId: table._id!, tableId: table._id!,
name: "Updated Name", name: "Updated Name",
}) })
expect((res as any).res.statusMessage).toEqual( expect(row.name).toEqual("Updated Name")
`${table.name} updated successfully.` expect(row.description).toEqual(existing.description)
)
expect(res.body.name).toEqual("Updated Name")
expect(res.body.description).toEqual(existing.description)
const savedRow = await loadRow(res.body._id, table._id!) const savedRow = await loadRow(row._id!, table._id!)
expect(savedRow.body.description).toEqual(existing.description) expect(savedRow.body.description).toEqual(existing.description)
expect(savedRow.body.name).toEqual("Updated Name") expect(savedRow.body.name).toEqual("Updated Name")
@ -561,6 +563,56 @@ describe.each([
await assertRowUsage(rowUsage) await assertRowUsage(rowUsage)
await assertQueryUsage(queryUsage) await assertQueryUsage(queryUsage)
}) })
it("should not overwrite links if those links are not set", async () => {
let linkField: FieldSchema = {
type: FieldType.LINK,
name: "",
fieldName: "",
constraints: {
type: "array",
presence: false,
},
relationshipType: RelationshipType.ONE_TO_MANY,
tableId: InternalTable.USER_METADATA,
}
let table = await config.api.table.create({
name: "TestTable",
type: "table",
sourceType: TableSourceType.INTERNAL,
sourceId: INTERNAL_TABLE_SOURCE_ID,
schema: {
user1: { ...linkField, name: "user1", fieldName: "user1" },
user2: { ...linkField, name: "user2", fieldName: "user2" },
},
})
let user1 = await config.createUser()
let user2 = await config.createUser()
let row = await config.api.row.save(table._id!, {
user1: [{ _id: user1._id }],
user2: [{ _id: user2._id }],
})
let getResp = await config.api.row.get(table._id!, row._id!)
expect(getResp.body.user1[0]._id).toEqual(user1._id)
expect(getResp.body.user2[0]._id).toEqual(user2._id)
let patchResp = await config.api.row.patch(table._id!, {
_id: row._id!,
_rev: row._rev!,
tableId: table._id!,
user1: [{ _id: user2._id }],
})
expect(patchResp.user1[0]._id).toEqual(user2._id)
expect(patchResp.user2[0]._id).toEqual(user2._id)
getResp = await config.api.row.get(table._id!, row._id!)
expect(getResp.body.user1[0]._id).toEqual(user2._id)
expect(getResp.body.user2[0]._id).toEqual(user2._id)
})
}) })
describe("destroy", () => { describe("destroy", () => {

View file

@ -492,6 +492,67 @@ describe("/tables", () => {
} }
}) })
it("should succeed when the row is created from the other side of the relationship", async () => {
// We found a bug just after releasing this feature where if the row was created from the
// users table, not the table linking to it, the migration would succeed but lose the data.
// This happened because the order of the documents in the link was reversed.
const table = await config.api.table.create({
name: "table",
type: "table",
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
schema: {
"user relationship": {
type: FieldType.LINK,
fieldName: "test",
name: "user relationship",
constraints: {
type: "array",
presence: false,
},
relationshipType: RelationshipType.MANY_TO_ONE,
tableId: InternalTable.USER_METADATA,
},
},
})
let testRow = await config.api.row.save(table._id!, {})
await Promise.all(
users.map(u =>
config.api.row.patch(InternalTable.USER_METADATA, {
tableId: InternalTable.USER_METADATA,
_rev: u._rev!,
_id: u._id!,
test: [testRow],
})
)
)
await config.api.table.migrate(table._id!, {
oldColumn: table.schema["user relationship"],
newColumn: {
name: "user column",
type: FieldType.BB_REFERENCE,
subtype: FieldSubtype.USERS,
},
})
const migratedTable = await config.api.table.get(table._id!)
expect(migratedTable.schema["user column"]).toBeDefined()
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
const resp = await config.api.row.get(table._id!, testRow._id!)
const migratedRow = resp.body as Row
expect(migratedRow["user column"]).toBeDefined()
expect(migratedRow["user relationship"]).not.toBeDefined()
expect(migratedRow["user column"]).toHaveLength(3)
expect(migratedRow["user column"].map((u: Row) => u._id)).toEqual(
expect.arrayContaining(users.map(u => u._id))
)
})
it("should successfully migrate a many-to-many user relationship to a users column", async () => { it("should successfully migrate a many-to-many user relationship to a users column", async () => {
const table = await config.api.table.create({ const table = await config.api.table.create({
name: "table", name: "table",

View file

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

View file

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

View file

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

View file

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

View file

@ -7,6 +7,7 @@ import {
isBBReferenceField, isBBReferenceField,
isRelationshipField, isRelationshipField,
LinkDocument, LinkDocument,
LinkInfo,
RelationshipFieldMetadata, RelationshipFieldMetadata,
RelationshipType, RelationshipType,
Row, Row,
@ -125,7 +126,23 @@ abstract class UserColumnMigrator implements ColumnMigrator {
protected newColumn: BBReferenceFieldMetadata protected newColumn: BBReferenceFieldMetadata
) {} ) {}
abstract updateRow(row: Row, link: LinkDocument): void abstract updateRow(row: Row, linkInfo: LinkInfo): void
pickUserTableLinkSide(link: LinkDocument): LinkInfo {
if (link.doc1.tableId === InternalTable.USER_METADATA) {
return link.doc1
} else {
return link.doc2
}
}
pickOtherTableLinkSide(link: LinkDocument): LinkInfo {
if (link.doc1.tableId === InternalTable.USER_METADATA) {
return link.doc2
} else {
return link.doc1
}
}
async doMigration(): Promise<MigrationResult> { async doMigration(): Promise<MigrationResult> {
let oldTable = cloneDeep(this.table) let oldTable = cloneDeep(this.table)
@ -137,15 +154,17 @@ abstract class UserColumnMigrator implements ColumnMigrator {
let links = await sdk.links.fetchWithDocument(this.table._id!) let links = await sdk.links.fetchWithDocument(this.table._id!)
for (let link of links) { for (let link of links) {
const userSide = this.pickUserTableLinkSide(link)
const otherSide = this.pickOtherTableLinkSide(link)
if ( if (
link.doc1.tableId !== this.table._id || otherSide.tableId !== this.table._id ||
link.doc1.fieldName !== this.oldColumn.name || otherSide.fieldName !== this.oldColumn.name ||
link.doc2.tableId !== InternalTable.USER_METADATA userSide.tableId !== InternalTable.USER_METADATA
) { ) {
continue continue
} }
let row = rowsById[link.doc1.rowId] let row = rowsById[otherSide.rowId]
if (!row) { if (!row) {
// This can happen if the row has been deleted but the link hasn't, // This can happen if the row has been deleted but the link hasn't,
// which was a state that was found during the initial testing of this // which was a state that was found during the initial testing of this
@ -153,7 +172,7 @@ abstract class UserColumnMigrator implements ColumnMigrator {
continue continue
} }
this.updateRow(row, link) this.updateRow(row, userSide)
} }
let db = context.getAppDB() let db = context.getAppDB()
@ -175,20 +194,20 @@ abstract class UserColumnMigrator implements ColumnMigrator {
} }
class SingleUserColumnMigrator extends UserColumnMigrator { class SingleUserColumnMigrator extends UserColumnMigrator {
updateRow(row: Row, link: LinkDocument): void { updateRow(row: Row, linkInfo: LinkInfo): void {
row[this.newColumn.name] = dbCore.getGlobalIDFromUserMetadataID( row[this.newColumn.name] = dbCore.getGlobalIDFromUserMetadataID(
link.doc2.rowId linkInfo.rowId
) )
} }
} }
class MultiUserColumnMigrator extends UserColumnMigrator { class MultiUserColumnMigrator extends UserColumnMigrator {
updateRow(row: Row, link: LinkDocument): void { updateRow(row: Row, linkInfo: LinkInfo): void {
if (!row[this.newColumn.name]) { if (!row[this.newColumn.name]) {
row[this.newColumn.name] = [] row[this.newColumn.name] = []
} }
row[this.newColumn.name].push( row[this.newColumn.name].push(
dbCore.getGlobalIDFromUserMetadataID(link.doc2.rowId) dbCore.getGlobalIDFromUserMetadataID(linkInfo.rowId)
) )
} }
} }

View file

@ -55,7 +55,13 @@ export class RowAPI extends TestAPI {
.send(row) .send(row)
.set(this.config.defaultHeaders()) .set(this.config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(expectStatus) if (resp.status !== expectStatus) {
throw new Error(
`Expected status ${expectStatus} but got ${
resp.status
}, body: ${JSON.stringify(resp.body)}`
)
}
return resp.body as Row return resp.body as Row
} }
@ -77,13 +83,20 @@ export class RowAPI extends TestAPI {
sourceId: string, sourceId: string,
row: PatchRowRequest, row: PatchRowRequest,
{ expectStatus } = { expectStatus: 200 } { expectStatus } = { expectStatus: 200 }
) => { ): Promise<Row> => {
return this.request let resp = await this.request
.patch(`/api/${sourceId}/rows`) .patch(`/api/${sourceId}/rows`)
.send(row) .send(row)
.set(this.config.defaultHeaders()) .set(this.config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(expectStatus) if (resp.status !== expectStatus) {
throw new Error(
`Expected status ${expectStatus} but got ${
resp.status
}, body: ${JSON.stringify(resp.body)}`
)
}
return resp.body as Row
} }
delete = async ( delete = async (

View file

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

View file

@ -1,17 +1,15 @@
import { Document } from "../document" import { Document } from "../document"
export interface LinkInfo {
rowId: string
fieldName: string
tableId: string
}
export interface LinkDocument extends Document { export interface LinkDocument extends Document {
type: string type: string
doc1: { doc1: LinkInfo
rowId: string doc2: LinkInfo
fieldName: string
tableId: string
}
doc2: {
rowId: string
fieldName: string
tableId: string
}
} }
export interface LinkDocumentValue { export interface LinkDocumentValue {

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
## Description ## Description
_Describe the problem or feature in addition to a link to the relevant github issues._ _Describe the problem or feature in addition to a link to the relevant github issues._
Addresses: ### Addresses:
- `<Enter the Link to the issue(s) this PR addresses>` - `<Enter the Link to the issue(s) this PR addresses>`
- ...more if required - ...more if required
@ -10,9 +10,3 @@ Addresses:
## Screenshots ## Screenshots
_If a UI facing feature, a short video of the happy path, and some screenshots of the new functionality._ _If a UI facing feature, a short video of the happy path, and some screenshots of the new functionality._
## Documentation
- [ ] I have reviewed the budibase documentatation to verify if this feature requires any changes. If changes or new docs are required I have written them.

View file

@ -2,9 +2,9 @@
if [[ $TARGETARCH == arm* ]] ; if [[ $TARGETARCH == arm* ]] ;
then then
echo "INSTALLING ARM64 MINIO" echo "INSTALLING ARM64 MINIO"
wget wget https://dl.min.io/server/minio/release/linux-arm64/archive/minio.deb -O minio.deb wget https://dl.min.io/server/minio/release/linux-arm64/minio
else else
echo "INSTALLING AMD64 MINIO" echo "INSTALLING AMD64 MINIO"
wget wget https://dl.min.io/server/minio/release/linux-amd64/archive/minio.deb -O minio.deb wget https://dl.min.io/server/minio/release/linux-amd64/minio
fi fi
dpkg -i minio.deb chmod +x minio