diff --git a/LICENSE b/LICENSE index a017209adf..cbb55109f4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,9 @@ -Copyright 2019-2021, Budibase Ltd. +Copyright 2019-2023, Budibase Ltd. Each Budibase package has its own license, please check the license file in each package. You can consider Budibase to be GPLv3 licensed overall. The apps that you build with Budibase do not package any GPLv3 licensed code, thus do not fall under those restrictions. + +Budibase ships with Structured Query Server, by The Neighbourhoodie Software GmbH. This license for this can be found at ./SQS_LICENSE diff --git a/SQS_LICENSE b/SQS_LICENSE new file mode 100644 index 0000000000..0315ee9527 --- /dev/null +++ b/SQS_LICENSE @@ -0,0 +1,31 @@ +FORM OF CUSTOMER LICENCE + +Budibase hereby grants the Customer a worldwide, royalty free, non-exclusive, +perpetual (for the lifetime of the intellectual property rights contained in the Product) +right and title to utilise the binary code of the The Neighbourhoodie Software GmbH +Structured Query Server software product (Product) for its own internal business +purposes (the Purpose) only (the Licence). The Product has the function of bringing a +CouchDB database (NoSQL database) into an SQL database form (SQLite) and thereby +making it usable for complex queries - which originally could only be displayed in an +SQL database. By indexing in SQLite and a server that is tailored to it, the Product +enables the use of CouchDB with SQL queries. +The Licence shall not permit sub-licensing, resale or transfer of the Product to third +parties, other than sub-licensing to the Customer’s direct contractors for the purposes +of utilizing the Product as contemplated above. +The Licence shall not permit the adaptation, modification, decompilation, reverse +engineering or similar activities with respect to the Product. +This licence is granted to the Customer only, although Customer and its Affiliates’ +employees, servants and agents shall be entitled to utilize the Product within the scope +of the Licence for the Customer’s Purpose only. +Reproduction is not permitted to users, except for reproductions that are necessary for +the use of the product under the licence described above. These conditions apply to the +product regardless of the form in which we make the product available and on which +devices it is installed and/or with which devices it is ultimately used. Depending on the +product variant or intended use, certain technical requirements in the IT infrastructure +must be satisfied as a prerequisite for use. +The law of the Northern Ireland applies exclusively to this licence, and the courts of +Northern Ireland shall have exclusive jurisdiction, save that we reserve a right to sue +you in the jurisdiction in which you are based. The application of the UN Sales +Convention (CISG) is excluded. +The invalidity of any part of this licence does not affect the validity of the remaining +regulations. diff --git a/charts/budibase/README.md b/charts/budibase/README.md index 164388b730..d8191026ce 100644 --- a/charts/budibase/README.md +++ b/charts/budibase/README.md @@ -87,6 +87,7 @@ couchdb: storageClass: "nfs-client" adminPassword: admin +services: objectStore: storageClass: "nfs-client" redis: diff --git a/charts/budibase/README.md.gotmpl b/charts/budibase/README.md.gotmpl index 92e91f8e09..e37c323837 100644 --- a/charts/budibase/README.md.gotmpl +++ b/charts/budibase/README.md.gotmpl @@ -86,6 +86,7 @@ couchdb: storageClass: "nfs-client" adminPassword: admin +services: objectStore: storageClass: "nfs-client" redis: diff --git a/charts/budibase/templates/proxy-service-deployment.yaml b/charts/budibase/templates/proxy-service-deployment.yaml index 2e23f12793..706e9b4b73 100644 --- a/charts/budibase/templates/proxy-service-deployment.yaml +++ b/charts/budibase/templates/proxy-service-deployment.yaml @@ -16,6 +16,7 @@ spec: selector: matchLabels: app.kubernetes.io/name: budibase-proxy + minReadySeconds: 10 strategy: type: RollingUpdate template: diff --git a/hosting/couchdb/runner.sh b/hosting/couchdb/runner.sh index b576c886c2..e56b8e0e7f 100644 --- a/hosting/couchdb/runner.sh +++ b/hosting/couchdb/runner.sh @@ -30,10 +30,18 @@ elif [[ "${TARGETBUILD}" = "single" ]]; then # mount, so we use that for all persistent data. sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini +elif [[ "${TARGETBUILD}" = "docker-compose" ]]; then + # We remove the database_dir and view_index_dir settings from the local.ini + # in docker-compose because it will default to /opt/couchdb/data which is what + # our docker-compose was using prior to us switching to using our own CouchDB + # image. + sed -i "s#^database_dir.*\$##g" /opt/couchdb/etc/local.ini + sed -i "s#^view_index_dir.*\$##g" /opt/couchdb/etc/local.ini + sed -i "s#^dir=.*\$#dir=/opt/couchdb/data#g" /opt/clouseau/clouseau.ini elif [[ -n $KUBERNETES_SERVICE_HOST ]]; then # In Kubernetes the directory /opt/couchdb/data has a persistent volume # mount for storing database data. - sed -i "s#DATA_DIR#/opt/couchdb/data#g" /opt/clouseau/clouseau.ini + sed -i "s#^dir=.*\$#dir=/opt/couchdb/data#g" /opt/clouseau/clouseau.ini # We remove the database_dir and view_index_dir settings from the local.ini # in Kubernetes because it will default to /opt/couchdb/data which is what diff --git a/hosting/docker-compose.yaml b/hosting/docker-compose.yaml index 8f66d211f7..7803916069 100644 --- a/hosting/docker-compose.yaml +++ b/hosting/docker-compose.yaml @@ -57,7 +57,6 @@ services: depends_on: - redis-service - minio-service - - couch-init minio-service: restart: unless-stopped @@ -70,7 +69,7 @@ services: MINIO_BROWSER: "off" command: server /data --console-address ":9001" healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + test: "timeout 5s bash -c ':> /dev/tcp/127.0.0.1/9000' || exit 1" interval: 30s timeout: 20s retries: 3 @@ -98,26 +97,15 @@ services: couchdb-service: restart: unless-stopped - image: ibmcom/couchdb3 + image: budibase/couchdb + pull_policy: always environment: - COUCHDB_PASSWORD=${COUCH_DB_PASSWORD} - COUCHDB_USER=${COUCH_DB_USER} + - TARGETBUILD=docker-compose volumes: - couchdb3_data:/opt/couchdb/data - couch-init: - image: curlimages/curl - environment: - PUT_CALL: "curl -u ${COUCH_DB_USER}:${COUCH_DB_PASSWORD} -X PUT couchdb-service:5984" - depends_on: - - couchdb-service - command: - [ - "sh", - "-c", - "sleep 10 && $${PUT_CALL}/_users && $${PUT_CALL}/_replicator; fg;", - ] - redis-service: restart: unless-stopped image: redis diff --git a/hosting/nginx.dev.conf b/hosting/nginx.dev.conf index 915125cbce..f0a58a9a98 100644 --- a/hosting/nginx.dev.conf +++ b/hosting/nginx.dev.conf @@ -42,7 +42,7 @@ http { server { listen 10000 default_server; server_name _; - client_max_body_size 1000m; + client_max_body_size 50000m; ignore_invalid_headers off; proxy_buffering off; diff --git a/hosting/proxy/nginx.prod.conf b/hosting/proxy/nginx.prod.conf index 6da2e4a1c3..88f9645f80 100644 --- a/hosting/proxy/nginx.prod.conf +++ b/hosting/proxy/nginx.prod.conf @@ -249,4 +249,30 @@ http { gzip_comp_level 6; gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; } + + # From https://docs.datadoghq.com/integrations/nginx/?tab=kubernetes + server { + listen 81; + server_name localhost; + + access_log off; + allow 127.0.0.1; + deny all; + + location /nginx_status { + # Choose your status module + + # freely available with open source NGINX + stub_status; + + # for open source NGINX < version 1.7.5 + # stub_status on; + + # available only with NGINX Plus + # status; + + # ensures the version information can be retrieved + server_tokens on; + } + } } diff --git a/lerna.json b/lerna.json index ccbd2d9d60..d6a5f41281 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.13.31", + "version": "2.13.35", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/src/auth/auth.ts b/packages/backend-core/src/auth/auth.ts index e31bc81eed..1951c7986c 100644 --- a/packages/backend-core/src/auth/auth.ts +++ b/packages/backend-core/src/auth/auth.ts @@ -19,6 +19,7 @@ import { GoogleInnerConfig, OIDCInnerConfig, PlatformLogoutOpts, + SessionCookie, SSOProviderType, } from "@budibase/types" import * as events from "../events" @@ -44,7 +45,6 @@ export const buildAuthMiddleware = authenticated export const buildTenancyMiddleware = tenancy export const buildCsrfMiddleware = csrf export const passport = _passport -export const jwt = require("jsonwebtoken") // Strategies _passport.use(new LocalStrategy(local.options, local.authenticate)) @@ -191,10 +191,10 @@ export async function platformLogout(opts: PlatformLogoutOpts) { if (!ctx) throw new Error("Koa context must be supplied to logout.") - const currentSession = getCookie(ctx, Cookie.Auth) + const currentSession = getCookie(ctx, Cookie.Auth) let sessions = await getSessionsForUser(userId) - if (keepActiveSession) { + if (currentSession && keepActiveSession) { sessions = sessions.filter( session => session.sessionId !== currentSession.sessionId ) diff --git a/packages/backend-core/src/middleware/authenticated.ts b/packages/backend-core/src/middleware/authenticated.ts index 8bd6591d05..16f658b90a 100644 --- a/packages/backend-core/src/middleware/authenticated.ts +++ b/packages/backend-core/src/middleware/authenticated.ts @@ -13,7 +13,7 @@ import { getGlobalDB, doInTenant } from "../context" import { decrypt } from "../security/encryption" import * as identity from "../context/identity" import env from "../environment" -import { Ctx, EndpointMatcher } from "@budibase/types" +import { Ctx, EndpointMatcher, SessionCookie } from "@budibase/types" import { InvalidAPIKeyError, ErrorCode } from "../errors" const ONE_MINUTE = env.SESSION_UPDATE_PERIOD @@ -98,7 +98,9 @@ export default function ( // check the actual user is authenticated first, try header or cookie let headerToken = ctx.request.headers[Header.TOKEN] - const authCookie = getCookie(ctx, Cookie.Auth) || openJwt(headerToken) + const authCookie = + getCookie(ctx, Cookie.Auth) || + openJwt(headerToken) let apiKey = ctx.request.headers[Header.API_KEY] if (!apiKey && ctx.request.headers[Header.AUTHORIZATION]) { diff --git a/packages/backend-core/src/middleware/passport/datasource/google.ts b/packages/backend-core/src/middleware/passport/datasource/google.ts index ae6b3b4913..ab4ffee9d2 100644 --- a/packages/backend-core/src/middleware/passport/datasource/google.ts +++ b/packages/backend-core/src/middleware/passport/datasource/google.ts @@ -3,7 +3,7 @@ import { Cookie } from "../../../constants" import * as configs from "../../../configs" import * as cache from "../../../cache" import * as utils from "../../../utils" -import { UserCtx, SSOProfile } from "@budibase/types" +import { UserCtx, SSOProfile, DatasourceAuthCookie } from "@budibase/types" import { ssoSaveUserNoOp } from "../sso/sso" const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy @@ -58,7 +58,14 @@ export async function postAuth( const platformUrl = await configs.getPlatformUrl({ tenantAware: false }) let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback` - const authStateCookie = utils.getCookie(ctx, Cookie.DatasourceAuth) + const authStateCookie = utils.getCookie<{ appId: string }>( + ctx, + Cookie.DatasourceAuth + ) + + if (!authStateCookie) { + throw new Error("Unable to fetch datasource auth cookie") + } return passport.authenticate( new GoogleStrategy( diff --git a/packages/backend-core/src/objectStore/objectStore.ts b/packages/backend-core/src/objectStore/objectStore.ts index 1971c09e9d..9b44eace49 100644 --- a/packages/backend-core/src/objectStore/objectStore.ts +++ b/packages/backend-core/src/objectStore/objectStore.ts @@ -305,20 +305,33 @@ export async function retrieveDirectory(bucketName: string, path: string) { let writePath = join(budibaseTempDir(), v4()) fs.mkdirSync(writePath) const objects = await listAllObjects(bucketName, path) - let fullObjects = await Promise.all( - objects.map(obj => retrieve(bucketName, obj.Key!)) + let streams = await Promise.all( + objects.map(obj => getReadStream(bucketName, obj.Key!)) ) let count = 0 + const writePromises: Promise[] = [] for (let obj of objects) { const filename = obj.Key! - const data = fullObjects[count++] + const stream = streams[count++] const possiblePath = filename.split("/") - if (possiblePath.length > 1) { - const dirs = possiblePath.slice(0, possiblePath.length - 1) - fs.mkdirSync(join(writePath, ...dirs), { recursive: true }) + const dirs = possiblePath.slice(0, possiblePath.length - 1) + const possibleDir = join(writePath, ...dirs) + if (possiblePath.length > 1 && !fs.existsSync(possibleDir)) { + fs.mkdirSync(possibleDir, { recursive: true }) } - fs.writeFileSync(join(writePath, ...possiblePath), data) + const writeStream = fs.createWriteStream(join(writePath, ...possiblePath), { + mode: 0o644, + }) + stream.pipe(writeStream) + writePromises.push( + new Promise((resolve, reject) => { + stream.on("finish", resolve) + stream.on("error", reject) + writeStream.on("error", reject) + }) + ) } + await Promise.all(writePromises) return writePath } diff --git a/packages/backend-core/src/security/encryption.ts b/packages/backend-core/src/security/encryption.ts index 7a8cfaf04a..45ed566a92 100644 --- a/packages/backend-core/src/security/encryption.ts +++ b/packages/backend-core/src/security/encryption.ts @@ -73,6 +73,9 @@ export async function encryptFile( const outputFileName = `${filename}.enc` const filePath = join(dir, filename) + if (fs.lstatSync(filePath).isDirectory()) { + throw new Error("Unable to encrypt directory") + } const inputFile = fs.createReadStream(filePath) const outputFile = fs.createWriteStream(join(dir, outputFileName)) @@ -110,6 +113,9 @@ export async function decryptFile( outputPath: string, secret: string ) { + if (fs.lstatSync(inputPath).isDirectory()) { + throw new Error("Unable to encrypt directory") + } const { salt, iv } = await getSaltAndIV(inputPath) const inputFile = fs.createReadStream(inputPath, { start: SALT_LENGTH + IV_LENGTH, diff --git a/packages/backend-core/src/utils/utils.ts b/packages/backend-core/src/utils/utils.ts index b10d9ebdc0..ee1ef6da0c 100644 --- a/packages/backend-core/src/utils/utils.ts +++ b/packages/backend-core/src/utils/utils.ts @@ -11,8 +11,7 @@ import { TenantResolutionStrategy, } from "@budibase/types" import type { SetOption } from "cookies" - -const jwt = require("jsonwebtoken") +import jwt, { Secret } from "jsonwebtoken" const APP_PREFIX = DocumentType.APP + SEPARATOR const PROD_APP_PREFIX = "/app/" @@ -60,10 +59,7 @@ export function isServingApp(ctx: Ctx) { return true } // prod app - if (ctx.path.startsWith(PROD_APP_PREFIX)) { - return true - } - return false + return ctx.path.startsWith(PROD_APP_PREFIX) } export function isServingBuilder(ctx: Ctx): boolean { @@ -138,16 +134,16 @@ function parseAppIdFromUrl(url?: string) { * opens the contents of the specified encrypted JWT. * @return the contents of the token. */ -export function openJwt(token: string) { +export function openJwt(token?: string): T | undefined { if (!token) { - return token + return undefined } try { - return jwt.verify(token, env.JWT_SECRET) + return jwt.verify(token, env.JWT_SECRET as Secret) as T } catch (e) { if (env.JWT_SECRET_FALLBACK) { // fallback to enable rotation - return jwt.verify(token, env.JWT_SECRET_FALLBACK) + return jwt.verify(token, env.JWT_SECRET_FALLBACK) as T } else { throw e } @@ -159,13 +155,9 @@ export function isValidInternalAPIKey(apiKey: string) { return true } // fallback to enable rotation - if ( - env.INTERNAL_API_KEY_FALLBACK && - env.INTERNAL_API_KEY_FALLBACK === apiKey - ) { - return true - } - return false + return !!( + env.INTERNAL_API_KEY_FALLBACK && env.INTERNAL_API_KEY_FALLBACK === apiKey + ) } /** @@ -173,14 +165,14 @@ export function isValidInternalAPIKey(apiKey: string) { * @param ctx The request which is to be manipulated. * @param name The name of the cookie to get. */ -export function getCookie(ctx: Ctx, name: string) { +export function getCookie(ctx: Ctx, name: string) { const cookie = ctx.cookies.get(name) if (!cookie) { - return cookie + return undefined } - return openJwt(cookie) + return openJwt(cookie) } /** @@ -197,7 +189,7 @@ export function setCookie( opts = { sign: true } ) { if (value && opts && opts.sign) { - value = jwt.sign(value, env.JWT_SECRET) + value = jwt.sign(value, env.JWT_SECRET as Secret) } const config: SetOption = { diff --git a/packages/bbui/src/DetailSummary/DetailSummary.svelte b/packages/bbui/src/DetailSummary/DetailSummary.svelte index daa9f3f5ca..e5d6fda86b 100644 --- a/packages/bbui/src/DetailSummary/DetailSummary.svelte +++ b/packages/bbui/src/DetailSummary/DetailSummary.svelte @@ -1,20 +1,17 @@ diff --git a/packages/bbui/src/Form/Core/Dropzone.svelte b/packages/bbui/src/Form/Core/Dropzone.svelte index dc89476db2..fa0be630ba 100644 --- a/packages/bbui/src/Form/Core/Dropzone.svelte +++ b/packages/bbui/src/Form/Core/Dropzone.svelte @@ -53,7 +53,7 @@ $: { if (selectedImage?.url) { selectedUrl = selectedImage?.url - } else if (selectedImage) { + } else if (selectedImage && isImage) { try { let reader = new FileReader() reader.readAsDataURL(selectedImage) diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js index ece17cb46f..dd54dcf13e 100644 --- a/packages/builder/src/builderStore/index.js +++ b/packages/builder/src/builderStore/index.js @@ -8,6 +8,7 @@ import { derived, get } from "svelte/store" import { findComponent, findComponentPath } from "./componentUtils" import { RoleUtils } from "@budibase/frontend-core" import { createHistoryStore } from "builderStore/store/history" +import { cloneDeep } from "lodash/fp" export const store = getFrontendStore() export const automationStore = getAutomationStore() @@ -69,7 +70,14 @@ export const selectedComponent = derived( if (!$selectedScreen || !$store.selectedComponentId) { return null } - return findComponent($selectedScreen?.props, $store.selectedComponentId) + const selected = findComponent( + $selectedScreen?.props, + $store.selectedComponentId + ) + + const clone = selected ? cloneDeep(selected) : selected + store.actions.components.migrateSettings(clone) + return clone } ) diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index a4729b4a8a..aaa0eb0184 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -85,6 +85,7 @@ const INITIAL_FRONTEND_STATE = { selectedScreenId: null, selectedComponentId: null, selectedLayoutId: null, + hoverComponentId: null, // Client state selectedComponentInstance: null, @@ -112,7 +113,7 @@ export const getFrontendStore = () => { } let clone = cloneDeep(screen) const result = patchFn(clone) - + // An explicit false result means skip this change if (result === false) { return } @@ -601,6 +602,36 @@ export const getFrontendStore = () => { // Finally try an external table return validTables.find(table => table.sourceType === DB_TYPE_EXTERNAL) }, + migrateSettings: enrichedComponent => { + const componentPrefix = "@budibase/standard-components" + let migrated = false + + if (enrichedComponent?._component == `${componentPrefix}/formblock`) { + // Use default config if the 'buttons' prop has never been initialised + if (!("buttons" in enrichedComponent)) { + enrichedComponent["buttons"] = + Utils.buildDynamicButtonConfig(enrichedComponent) + migrated = true + } else if (enrichedComponent["buttons"] == null) { + // Ignore legacy config if 'buttons' has been reset by 'resetOn' + const { _id, actionType, dataSource } = enrichedComponent + enrichedComponent["buttons"] = Utils.buildDynamicButtonConfig({ + _id, + actionType, + dataSource, + }) + migrated = true + } + + // Ensure existing Formblocks position their buttons at the top. + if (!("buttonPosition" in enrichedComponent)) { + enrichedComponent["buttonPosition"] = "top" + migrated = true + } + } + + return migrated + }, enrichEmptySettings: (component, opts) => { if (!component?._component) { return @@ -672,7 +703,6 @@ export const getFrontendStore = () => { component[setting.key] = setting.defaultValue } } - // Validate non-empty settings else { if (setting.type === "dataProvider") { @@ -722,6 +752,9 @@ export const getFrontendStore = () => { useDefaultValues: true, }) + // Migrate nested component settings + store.actions.components.migrateSettings(instance) + // Add any extra properties the component needs let extras = {} if (definition.hasChildren) { @@ -845,7 +878,16 @@ export const getFrontendStore = () => { if (!component) { return false } - return patchFn(component, screen) + + // Mutates the fetched component with updates + const patchResult = patchFn(component, screen) + + // Mutates the component with any required settings updates + const migrated = store.actions.components.migrateSettings(component) + + // Returning an explicit false signifies that we should skip this + // update. If we migrated something, ensure we never skip. + return migrated ? null : patchResult } await store.actions.screens.patch(patchScreen, screenId) }, @@ -1247,9 +1289,13 @@ export const getFrontendStore = () => { const settings = getComponentSettings(component._component) const updatedSetting = settings.find(setting => setting.key === name) - const resetFields = settings.filter( - setting => name === setting.resetOn - ) + // Can be a single string or array of strings + const resetFields = settings.filter(setting => { + return ( + name === setting.resetOn || + (Array.isArray(setting.resetOn) && setting.resetOn.includes(name)) + ) + }) resetFields?.forEach(setting => { component[setting.key] = null }) @@ -1271,6 +1317,7 @@ export const getFrontendStore = () => { }) } component[name] = value + return true } }, requestEjectBlock: componentId => { @@ -1278,7 +1325,6 @@ export const getFrontendStore = () => { }, handleEjectBlock: async (componentId, ejectedDefinition) => { let nextSelectedComponentId - await store.actions.screens.patch(screen => { const block = findComponent(screen.props, componentId) const parent = findComponentParent(screen.props, componentId) diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte index 84fa62fe69..ffe769d024 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte @@ -57,16 +57,11 @@ }} class="buttons" > - +
Run test
- +
{ diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte index 3c9e1a13b1..5ddba31bb8 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte @@ -97,6 +97,7 @@ class:typing={typing && !automationNameError} class:typing-error={automationNameError} class="blockSection" + on:click={() => dispatch("toggle")} >
@@ -138,7 +139,20 @@ on:input={e => { automationName = e.target.value.trim() }} - on:click={startTyping} + on:click={e => { + e.stopPropagation() + startTyping() + }} + on:keydown={async e => { + if (e.key === "Enter") { + typing = false + if (automationNameError) { + automationName = stepNames[block.id] || block?.name + } else { + await saveName() + } + } + }} on:blur={async () => { typing = false if (automationNameError) { @@ -168,7 +182,11 @@
dispatch("toggle")} + e.stopPropagation() + on:click={e => { + e.stopPropagation() + dispatch("toggle") + }} hoverable name={open ? "ChevronUp" : "ChevronDown"} /> @@ -195,7 +213,10 @@ {/if} {#if !showTestStatus} dispatch("toggle")} + on:click={e => { + e.stopPropagation() + dispatch("toggle") + }} hoverable name={open ? "ChevronUp" : "ChevronDown"} /> diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte index 5c97d77ae8..76def72bf6 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte @@ -1,11 +1,9 @@ - - -
- -
- -
- -
-