diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index e81925876b5..e294e50e8de 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -202,6 +202,9 @@ jobs: - run: yarn --frozen-lockfile + - name: Build client library - necessary for component tests + run: yarn build:client + - name: Set up PostgreSQL 16 if: matrix.datasource == 'postgres' run: | diff --git a/hosting/letsencrypt/nginx-ssl.conf b/hosting/letsencrypt/nginx-ssl.conf index b3f51e5cc5b..710ce2d22dd 100644 --- a/hosting/letsencrypt/nginx-ssl.conf +++ b/hosting/letsencrypt/nginx-ssl.conf @@ -41,6 +41,11 @@ server { } location ~ ^/api/(system|admin|global)/ { + # Enable buffering for potentially large OIDC configs + proxy_buffering on; + proxy_buffer_size 16k; + proxy_buffers 4 32k; + proxy_pass http://127.0.0.1:4002; } diff --git a/lerna.json b/lerna.json index 730d145cedf..18c131fe564 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.4.4", + "version": "3.4.7", "npmClient": "yarn", "concurrency": 20, "command": { diff --git a/package.json b/package.json index 771d3cefd5e..1475abadf90 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "eslint-plugin-jest": "28.9.0", "eslint-plugin-local-rules": "3.0.2", "eslint-plugin-svelte": "2.46.1", + "svelte-preprocess": "^6.0.3", "husky": "^8.0.3", "kill-port": "^1.6.1", "lerna": "7.4.2", @@ -67,6 +68,7 @@ "lint:fix:eslint": "eslint --fix --max-warnings=0 packages", "lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"", "lint:fix": "yarn run lint:fix:eslint && yarn run lint:fix:prettier", + "build:client": "lerna run --stream build --scope @budibase/client", "build:specs": "lerna run --stream specs", "build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild", "build:docker:airgap:single": "SINGLE_IMAGE=1 node hosting/scripts/airgapped/airgappedDockerBuild", diff --git a/packages/backend-core/__mocks__/@aws-sdk/client-s3.ts b/packages/backend-core/__mocks__/@aws-sdk/client-s3.ts new file mode 100644 index 00000000000..8f002f41a82 --- /dev/null +++ b/packages/backend-core/__mocks__/@aws-sdk/client-s3.ts @@ -0,0 +1,28 @@ +export class S3 { + headBucket() { + return jest.fn().mockReturnThis() + } + deleteObject() { + return jest.fn().mockReturnThis() + } + deleteObjects() { + return jest.fn().mockReturnThis() + } + createBucket() { + return jest.fn().mockReturnThis() + } + getObject() { + return jest.fn().mockReturnThis() + } + listObject() { + return jest.fn().mockReturnThis() + } + promise() { + return jest.fn().mockReturnThis() + } + catch() { + return jest.fn() + } +} + +export const GetObjectCommand = jest.fn(inputs => ({ inputs })) diff --git a/packages/backend-core/__mocks__/@aws-sdk/s3-request-presigner.ts b/packages/backend-core/__mocks__/@aws-sdk/s3-request-presigner.ts new file mode 100644 index 00000000000..3ed2c10595a --- /dev/null +++ b/packages/backend-core/__mocks__/@aws-sdk/s3-request-presigner.ts @@ -0,0 +1,4 @@ +export const getSignedUrl = jest.fn((_, cmd) => { + const { inputs } = cmd + return `http://s3.example.com/${inputs?.Bucket}/${inputs?.Key}` +}) diff --git a/packages/backend-core/__mocks__/aws-sdk.ts b/packages/backend-core/__mocks__/aws-sdk.ts deleted file mode 100644 index e3be511d082..00000000000 --- a/packages/backend-core/__mocks__/aws-sdk.ts +++ /dev/null @@ -1,19 +0,0 @@ -const mockS3 = { - headBucket: jest.fn().mockReturnThis(), - deleteObject: jest.fn().mockReturnThis(), - deleteObjects: jest.fn().mockReturnThis(), - createBucket: jest.fn().mockReturnThis(), - getObject: jest.fn().mockReturnThis(), - listObject: jest.fn().mockReturnThis(), - getSignedUrl: jest.fn((operation: string, params: any) => { - return `http://s3.example.com/${params.Bucket}/${params.Key}` - }), - promise: jest.fn().mockReturnThis(), - catch: jest.fn(), -} - -const AWS = { - S3: jest.fn(() => mockS3), -} - -export default AWS diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 1ab05cd14ba..059114dee82 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -30,6 +30,9 @@ "test:watch": "jest --watchAll" }, "dependencies": { + "@aws-sdk/client-s3": "3.709.0", + "@aws-sdk/lib-storage": "3.709.0", + "@aws-sdk/s3-request-presigner": "3.709.0", "@budibase/nano": "10.1.5", "@budibase/pouchdb-replication-stream": "1.2.11", "@budibase/shared-core": "*", @@ -71,11 +74,13 @@ "devDependencies": { "@jest/types": "^29.6.3", "@shopify/jest-koa-mocks": "5.1.1", + "@smithy/types": "4.0.0", "@swc/core": "1.3.71", "@swc/jest": "0.2.27", "@types/chance": "1.1.3", "@types/cookies": "0.7.8", "@types/jest": "29.5.5", + "@types/koa": "2.13.4", "@types/lodash": "4.14.200", "@types/node-fetch": "2.6.4", "@types/pouchdb": "6.4.2", @@ -83,7 +88,6 @@ "@types/semver": "7.3.7", "@types/tar-fs": "2.0.1", "@types/uuid": "8.3.4", - "@types/koa": "2.13.4", "chance": "1.1.8", "ioredis-mock": "8.9.0", "jest": "29.7.0", diff --git a/packages/backend-core/src/docIds/params.ts b/packages/backend-core/src/docIds/params.ts index 5f1c053bde7..61708bb71b7 100644 --- a/packages/backend-core/src/docIds/params.ts +++ b/packages/backend-core/src/docIds/params.ts @@ -8,6 +8,10 @@ import { import { getProdAppID } from "./conversions" import { DatabaseQueryOpts, VirtualDocumentType } from "@budibase/types" +const EXTERNAL_TABLE_ID_REGEX = new RegExp( + `^${DocumentType.DATASOURCE_PLUS}_(.+)__(.+)$` +) + /** * If creating DB allDocs/query params with only a single top level ID this can be used, this * is usually the case as most of our docs are top level e.g. tables, automations, users and so on. @@ -64,6 +68,11 @@ export function getQueryIndex(viewName: ViewName) { return `database/${viewName}` } +export const isExternalTableId = (id: string): boolean => { + const matches = id.match(EXTERNAL_TABLE_ID_REGEX) + return !!id && matches !== null +} + /** * Check if a given ID is that of a table. */ @@ -72,7 +81,7 @@ export const isTableId = (id: string): boolean => { return ( !!id && (id.startsWith(`${DocumentType.TABLE}${SEPARATOR}`) || - id.startsWith(`${DocumentType.DATASOURCE_PLUS}${SEPARATOR}`)) + isExternalTableId(id)) ) } diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index 954fdd41353..9a467580628 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -154,7 +154,7 @@ const environment = { MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, AWS_SESSION_TOKEN: process.env.AWS_SESSION_TOKEN, - AWS_REGION: process.env.AWS_REGION, + AWS_REGION: process.env.AWS_REGION || "eu-west-1", MINIO_URL: process.env.MINIO_URL, MINIO_ENABLED: process.env.MINIO_ENABLED || 1, INTERNAL_API_KEY: process.env.INTERNAL_API_KEY, diff --git a/packages/backend-core/src/objectStore/buckets/app.ts b/packages/backend-core/src/objectStore/buckets/app.ts index 43bc965c650..dbf49ca9940 100644 --- a/packages/backend-core/src/objectStore/buckets/app.ts +++ b/packages/backend-core/src/objectStore/buckets/app.ts @@ -13,7 +13,7 @@ export function clientLibraryPath(appId: string) { * due to issues with the domain we were unable to continue doing this - keeping * incase we are able to switch back to CDN path again in future. */ -export function clientLibraryCDNUrl(appId: string, version: string) { +export async function clientLibraryCDNUrl(appId: string, version: string) { let file = clientLibraryPath(appId) if (env.CLOUDFRONT_CDN) { // append app version to bust the cache @@ -24,7 +24,7 @@ export function clientLibraryCDNUrl(appId: string, version: string) { // file is public return cloudfront.getUrl(file) } else { - return objectStore.getPresignedUrl(env.APPS_BUCKET_NAME, file) + return await objectStore.getPresignedUrl(env.APPS_BUCKET_NAME, file) } } @@ -44,10 +44,10 @@ export function clientLibraryUrl(appId: string, version: string) { return `/api/assets/client?${qs.encode(qsParams)}` } -export function getAppFileUrl(s3Key: string) { +export async function getAppFileUrl(s3Key: string) { if (env.CLOUDFRONT_CDN) { return cloudfront.getPresignedUrl(s3Key) } else { - return objectStore.getPresignedUrl(env.APPS_BUCKET_NAME, s3Key) + return await objectStore.getPresignedUrl(env.APPS_BUCKET_NAME, s3Key) } } diff --git a/packages/backend-core/src/objectStore/buckets/global.ts b/packages/backend-core/src/objectStore/buckets/global.ts index 69e201bb988..29c3347b051 100644 --- a/packages/backend-core/src/objectStore/buckets/global.ts +++ b/packages/backend-core/src/objectStore/buckets/global.ts @@ -5,7 +5,11 @@ import * as cloudfront from "../cloudfront" // URLs -export const getGlobalFileUrl = (type: string, name: string, etag?: string) => { +export const getGlobalFileUrl = async ( + type: string, + name: string, + etag?: string +) => { let file = getGlobalFileS3Key(type, name) if (env.CLOUDFRONT_CDN) { if (etag) { @@ -13,7 +17,7 @@ export const getGlobalFileUrl = (type: string, name: string, etag?: string) => { } return cloudfront.getPresignedUrl(file) } else { - return objectStore.getPresignedUrl(env.GLOBAL_BUCKET_NAME, file) + return await objectStore.getPresignedUrl(env.GLOBAL_BUCKET_NAME, file) } } diff --git a/packages/backend-core/src/objectStore/buckets/plugins.ts b/packages/backend-core/src/objectStore/buckets/plugins.ts index 02be9345ab5..131f180f48d 100644 --- a/packages/backend-core/src/objectStore/buckets/plugins.ts +++ b/packages/backend-core/src/objectStore/buckets/plugins.ts @@ -6,23 +6,25 @@ import { Plugin } from "@budibase/types" // URLS -export function enrichPluginURLs(plugins?: Plugin[]): Plugin[] { +export async function enrichPluginURLs(plugins?: Plugin[]): Promise { if (!plugins || !plugins.length) { return [] } - return plugins.map(plugin => { - const jsUrl = getPluginJSUrl(plugin) - const iconUrl = getPluginIconUrl(plugin) - return { ...plugin, jsUrl, iconUrl } - }) + return await Promise.all( + plugins.map(async plugin => { + const jsUrl = await getPluginJSUrl(plugin) + const iconUrl = await getPluginIconUrl(plugin) + return { ...plugin, jsUrl, iconUrl } + }) + ) } -function getPluginJSUrl(plugin: Plugin) { +async function getPluginJSUrl(plugin: Plugin) { const s3Key = getPluginJSKey(plugin) return getPluginUrl(s3Key) } -function getPluginIconUrl(plugin: Plugin): string | undefined { +async function getPluginIconUrl(plugin: Plugin) { const s3Key = getPluginIconKey(plugin) if (!s3Key) { return @@ -30,11 +32,11 @@ function getPluginIconUrl(plugin: Plugin): string | undefined { return getPluginUrl(s3Key) } -function getPluginUrl(s3Key: string) { +async function getPluginUrl(s3Key: string) { if (env.CLOUDFRONT_CDN) { return cloudfront.getPresignedUrl(s3Key) } else { - return objectStore.getPresignedUrl(env.PLUGIN_BUCKET_NAME, s3Key) + return await objectStore.getPresignedUrl(env.PLUGIN_BUCKET_NAME, s3Key) } } diff --git a/packages/backend-core/src/objectStore/buckets/tests/app.spec.ts b/packages/backend-core/src/objectStore/buckets/tests/app.spec.ts index 4a132ce54dd..e36b36d39d3 100644 --- a/packages/backend-core/src/objectStore/buckets/tests/app.spec.ts +++ b/packages/backend-core/src/objectStore/buckets/tests/app.spec.ts @@ -93,25 +93,25 @@ describe("app", () => { testEnv.multiTenant() }) - it("gets url with embedded minio", () => { + it("gets url with embedded minio", async () => { testEnv.withMinio() - const url = getAppFileUrl() + const url = await getAppFileUrl() expect(url).toBe( "/files/signed/prod-budi-app-assets/app_123/attachments/image.jpeg" ) }) - it("gets url with custom S3", () => { + it("gets url with custom S3", async () => { testEnv.withS3() - const url = getAppFileUrl() + const url = await getAppFileUrl() expect(url).toBe( "http://s3.example.com/prod-budi-app-assets/app_123/attachments/image.jpeg" ) }) - it("gets url with cloudfront + s3", () => { + it("gets url with cloudfront + s3", async () => { testEnv.withCloudfront() - const url = getAppFileUrl() + const url = await getAppFileUrl() // omit rest of signed params expect( url.includes("http://cf.example.com/app_123/attachments/image.jpeg?") @@ -126,8 +126,8 @@ describe("app", () => { it("gets url with embedded minio", async () => { testEnv.withMinio() - await testEnv.withTenant(() => { - const url = getAppFileUrl() + await testEnv.withTenant(async () => { + const url = await getAppFileUrl() expect(url).toBe( "/files/signed/prod-budi-app-assets/app_123/attachments/image.jpeg" ) @@ -136,8 +136,8 @@ describe("app", () => { it("gets url with custom S3", async () => { testEnv.withS3() - await testEnv.withTenant(() => { - const url = getAppFileUrl() + await testEnv.withTenant(async () => { + const url = await getAppFileUrl() expect(url).toBe( "http://s3.example.com/prod-budi-app-assets/app_123/attachments/image.jpeg" ) @@ -146,8 +146,8 @@ describe("app", () => { it("gets url with cloudfront + s3", async () => { testEnv.withCloudfront() - await testEnv.withTenant(() => { - const url = getAppFileUrl() + await testEnv.withTenant(async () => { + const url = await getAppFileUrl() // omit rest of signed params expect( url.includes( diff --git a/packages/backend-core/src/objectStore/buckets/tests/global.spec.ts b/packages/backend-core/src/objectStore/buckets/tests/global.spec.ts index 148a4c80bf5..c98d98e0167 100644 --- a/packages/backend-core/src/objectStore/buckets/tests/global.spec.ts +++ b/packages/backend-core/src/objectStore/buckets/tests/global.spec.ts @@ -3,7 +3,7 @@ import { testEnv } from "../../../../tests/extra" describe("global", () => { describe("getGlobalFileUrl", () => { - function getGlobalFileUrl() { + async function getGlobalFileUrl() { return global.getGlobalFileUrl("settings", "logoUrl", "etag") } @@ -12,21 +12,21 @@ describe("global", () => { testEnv.singleTenant() }) - it("gets url with embedded minio", () => { + it("gets url with embedded minio", async () => { testEnv.withMinio() - const url = getGlobalFileUrl() + const url = await getGlobalFileUrl() expect(url).toBe("/files/signed/global/settings/logoUrl") }) - it("gets url with custom S3", () => { + it("gets url with custom S3", async () => { testEnv.withS3() - const url = getGlobalFileUrl() + const url = await getGlobalFileUrl() expect(url).toBe("http://s3.example.com/global/settings/logoUrl") }) - it("gets url with cloudfront + s3", () => { + it("gets url with cloudfront + s3", async () => { testEnv.withCloudfront() - const url = getGlobalFileUrl() + const url = await getGlobalFileUrl() // omit rest of signed params expect( url.includes("http://cf.example.com/settings/logoUrl?etag=etag&") @@ -41,16 +41,16 @@ describe("global", () => { it("gets url with embedded minio", async () => { testEnv.withMinio() - await testEnv.withTenant(tenantId => { - const url = getGlobalFileUrl() + await testEnv.withTenant(async tenantId => { + const url = await getGlobalFileUrl() expect(url).toBe(`/files/signed/global/${tenantId}/settings/logoUrl`) }) }) it("gets url with custom S3", async () => { testEnv.withS3() - await testEnv.withTenant(tenantId => { - const url = getGlobalFileUrl() + await testEnv.withTenant(async tenantId => { + const url = await getGlobalFileUrl() expect(url).toBe( `http://s3.example.com/global/${tenantId}/settings/logoUrl` ) @@ -59,8 +59,8 @@ describe("global", () => { it("gets url with cloudfront + s3", async () => { testEnv.withCloudfront() - await testEnv.withTenant(tenantId => { - const url = getGlobalFileUrl() + await testEnv.withTenant(async tenantId => { + const url = await getGlobalFileUrl() // omit rest of signed params expect( url.includes( diff --git a/packages/backend-core/src/objectStore/buckets/tests/plugins.spec.ts b/packages/backend-core/src/objectStore/buckets/tests/plugins.spec.ts index fc2822314e0..0906ea91c22 100644 --- a/packages/backend-core/src/objectStore/buckets/tests/plugins.spec.ts +++ b/packages/backend-core/src/objectStore/buckets/tests/plugins.spec.ts @@ -6,8 +6,8 @@ describe("plugins", () => { describe("enrichPluginURLs", () => { const plugin = structures.plugins.plugin() - function getEnrichedPluginUrls() { - const enriched = plugins.enrichPluginURLs([plugin])[0] + async function getEnrichedPluginUrls() { + const enriched = (await plugins.enrichPluginURLs([plugin]))[0] return { jsUrl: enriched.jsUrl!, iconUrl: enriched.iconUrl!, @@ -19,9 +19,9 @@ describe("plugins", () => { testEnv.singleTenant() }) - it("gets url with embedded minio", () => { + it("gets url with embedded minio", async () => { testEnv.withMinio() - const urls = getEnrichedPluginUrls() + const urls = await getEnrichedPluginUrls() expect(urls.jsUrl).toBe( `/files/signed/plugins/${plugin.name}/plugin.min.js` ) @@ -30,9 +30,9 @@ describe("plugins", () => { ) }) - it("gets url with custom S3", () => { + it("gets url with custom S3", async () => { testEnv.withS3() - const urls = getEnrichedPluginUrls() + const urls = await getEnrichedPluginUrls() expect(urls.jsUrl).toBe( `http://s3.example.com/plugins/${plugin.name}/plugin.min.js` ) @@ -41,9 +41,9 @@ describe("plugins", () => { ) }) - it("gets url with cloudfront + s3", () => { + it("gets url with cloudfront + s3", async () => { testEnv.withCloudfront() - const urls = getEnrichedPluginUrls() + const urls = await getEnrichedPluginUrls() // omit rest of signed params expect( urls.jsUrl.includes( @@ -65,8 +65,8 @@ describe("plugins", () => { it("gets url with embedded minio", async () => { testEnv.withMinio() - await testEnv.withTenant(tenantId => { - const urls = getEnrichedPluginUrls() + await testEnv.withTenant(async tenantId => { + const urls = await getEnrichedPluginUrls() expect(urls.jsUrl).toBe( `/files/signed/plugins/${tenantId}/${plugin.name}/plugin.min.js` ) @@ -78,8 +78,8 @@ describe("plugins", () => { it("gets url with custom S3", async () => { testEnv.withS3() - await testEnv.withTenant(tenantId => { - const urls = getEnrichedPluginUrls() + await testEnv.withTenant(async tenantId => { + const urls = await getEnrichedPluginUrls() expect(urls.jsUrl).toBe( `http://s3.example.com/plugins/${tenantId}/${plugin.name}/plugin.min.js` ) @@ -91,8 +91,8 @@ describe("plugins", () => { it("gets url with cloudfront + s3", async () => { testEnv.withCloudfront() - await testEnv.withTenant(tenantId => { - const urls = getEnrichedPluginUrls() + await testEnv.withTenant(async tenantId => { + const urls = await getEnrichedPluginUrls() // omit rest of signed params expect( urls.jsUrl.includes( diff --git a/packages/backend-core/src/objectStore/objectStore.ts b/packages/backend-core/src/objectStore/objectStore.ts index 79875b5e997..064cc630d80 100644 --- a/packages/backend-core/src/objectStore/objectStore.ts +++ b/packages/backend-core/src/objectStore/objectStore.ts @@ -1,6 +1,15 @@ const sanitize = require("sanitize-s3-objectkey") -import AWS from "aws-sdk" +import { + HeadObjectCommandOutput, + PutObjectCommandInput, + S3, + S3ClientConfig, + GetObjectCommand, + _Object as S3Object, +} from "@aws-sdk/client-s3" +import { Upload } from "@aws-sdk/lib-storage" +import { getSignedUrl } from "@aws-sdk/s3-request-presigner" import stream, { Readable } from "stream" import fetch from "node-fetch" import tar from "tar-fs" @@ -13,8 +22,8 @@ import { bucketTTLConfig, budibaseTempDir } from "./utils" import { v4 } from "uuid" import { APP_PREFIX, APP_DEV_PREFIX } from "../db" import fsp from "fs/promises" -import { HeadObjectOutput } from "aws-sdk/clients/s3" import { ReadableStream } from "stream/web" +import { NodeJsClient } from "@smithy/types" const streamPipeline = promisify(stream.pipeline) // use this as a temporary store of buckets that are being created @@ -84,26 +93,24 @@ export function sanitizeBucket(input: string) { * @constructor */ export function ObjectStore( - bucket: string, opts: { presigning: boolean } = { presigning: false } ) { - const config: AWS.S3.ClientConfiguration = { - s3ForcePathStyle: true, - signatureVersion: "v4", - apiVersion: "2006-03-01", - accessKeyId: env.MINIO_ACCESS_KEY, - secretAccessKey: env.MINIO_SECRET_KEY, + const config: S3ClientConfig = { + forcePathStyle: true, + credentials: { + accessKeyId: env.MINIO_ACCESS_KEY!, + secretAccessKey: env.MINIO_SECRET_KEY!, + }, region: env.AWS_REGION, } - if (bucket) { - config.params = { - Bucket: sanitizeBucket(bucket), - } - } // for AWS Credentials using temporary session token if (!env.MINIO_ENABLED && env.AWS_SESSION_TOKEN) { - config.sessionToken = env.AWS_SESSION_TOKEN + config.credentials = { + accessKeyId: env.MINIO_ACCESS_KEY!, + secretAccessKey: env.MINIO_SECRET_KEY!, + sessionToken: env.AWS_SESSION_TOKEN, + } } // custom S3 is in use i.e. minio @@ -113,13 +120,13 @@ export function ObjectStore( // Normally a signed url will need to be generated with a specified host in mind. // To support dynamic hosts, e.g. some unknown self-hosted installation url, // use a predefined host. The host 'minio-service' is also forwarded to minio requests via nginx - config.endpoint = "minio-service" + config.endpoint = "http://minio-service" } else { config.endpoint = env.MINIO_URL } } - return new AWS.S3(config) + return new S3(config) as NodeJsClient } /** @@ -132,26 +139,25 @@ export async function createBucketIfNotExists( ): Promise<{ created: boolean; exists: boolean }> { bucketName = sanitizeBucket(bucketName) try { - await client - .headBucket({ - Bucket: bucketName, - }) - .promise() + await client.headBucket({ + Bucket: bucketName, + }) return { created: false, exists: true } } catch (err: any) { - const promises: any = STATE.bucketCreationPromises - const doesntExist = err.statusCode === 404, - noAccess = err.statusCode === 403 + const statusCode = err.statusCode || err.$response?.statusCode + const promises: Record | undefined> = + STATE.bucketCreationPromises + const doesntExist = statusCode === 404, + noAccess = statusCode === 403 if (promises[bucketName]) { await promises[bucketName] return { created: false, exists: true } } else if (doesntExist || noAccess) { if (doesntExist) { - promises[bucketName] = client - .createBucket({ - Bucket: bucketName, - }) - .promise() + promises[bucketName] = client.createBucket({ + Bucket: bucketName, + }) + await promises[bucketName] delete promises[bucketName] return { created: true, exists: false } @@ -180,25 +186,26 @@ export async function upload({ const fileBytes = path ? (await fsp.open(path)).createReadStream() : body - const objectStore = ObjectStore(bucketName) + const objectStore = ObjectStore() const bucketCreated = await createBucketIfNotExists(objectStore, bucketName) if (ttl && bucketCreated.created) { let ttlConfig = bucketTTLConfig(bucketName, ttl) - await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise() + await objectStore.putBucketLifecycleConfiguration(ttlConfig) } let contentType = type - if (!contentType) { - contentType = extension - ? CONTENT_TYPE_MAP[extension.toLowerCase()] - : CONTENT_TYPE_MAP.txt - } - const config: any = { + const finalContentType = contentType + ? contentType + : extension + ? CONTENT_TYPE_MAP[extension.toLowerCase()] + : CONTENT_TYPE_MAP.txt + const config: PutObjectCommandInput = { // windows file paths need to be converted to forward slashes for s3 + Bucket: sanitizeBucket(bucketName), Key: sanitizeKey(filename), - Body: fileBytes, - ContentType: contentType, + Body: fileBytes as stream.Readable | Buffer, + ContentType: finalContentType, } if (metadata && typeof metadata === "object") { // remove any nullish keys from the metadata object, as these may be considered invalid @@ -207,10 +214,15 @@ export async function upload({ delete metadata[key] } } - config.Metadata = metadata + config.Metadata = metadata as Record } - return objectStore.upload(config).promise() + const upload = new Upload({ + client: objectStore, + params: config, + }) + + return upload.done() } /** @@ -229,12 +241,12 @@ export async function streamUpload({ throw new Error("Stream to upload is invalid/undefined") } const extension = filename.split(".").pop() - const objectStore = ObjectStore(bucketName) + const objectStore = ObjectStore() const bucketCreated = await createBucketIfNotExists(objectStore, bucketName) if (ttl && bucketCreated.created) { let ttlConfig = bucketTTLConfig(bucketName, ttl) - await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise() + await objectStore.putBucketLifecycleConfiguration(ttlConfig) } // Set content type for certain known extensions @@ -267,13 +279,15 @@ export async function streamUpload({ ...extra, } - const details = await objectStore.upload(params).promise() - const headDetails = await objectStore - .headObject({ - Bucket: bucket, - Key: objKey, - }) - .promise() + const upload = new Upload({ + client: objectStore, + params, + }) + const details = await upload.done() + const headDetails = await objectStore.headObject({ + Bucket: bucket, + Key: objKey, + }) return { ...details, ContentLength: headDetails.ContentLength, @@ -284,35 +298,46 @@ export async function streamUpload({ * retrieves the contents of a file from the object store, if it is a known content type it * will be converted, otherwise it will be returned as a buffer stream. */ -export async function retrieve(bucketName: string, filepath: string) { - const objectStore = ObjectStore(bucketName) +export async function retrieve( + bucketName: string, + filepath: string +): Promise { + const objectStore = ObjectStore() const params = { Bucket: sanitizeBucket(bucketName), Key: sanitizeKey(filepath), } - const response: any = await objectStore.getObject(params).promise() - // currently these are all strings + const response = await objectStore.getObject(params) + if (!response.Body) { + throw new Error("Unable to retrieve object") + } if (STRING_CONTENT_TYPES.includes(response.ContentType)) { - return response.Body.toString("utf8") + return response.Body.transformToString() } else { - return response.Body + // this typecast is required - for some reason the AWS SDK V3 defines its own "ReadableStream" + // found in the @aws-sdk/types package which is meant to be the Node type, but due to the SDK + // supporting both the browser and Nodejs it is a polyfill which causes a type clash with Node. + const readableStream = + response.Body.transformToWebStream() as ReadableStream + return stream.Readable.fromWeb(readableStream) } } -export async function listAllObjects(bucketName: string, path: string) { - const objectStore = ObjectStore(bucketName) +export async function listAllObjects( + bucketName: string, + path: string +): Promise { + const objectStore = ObjectStore() const list = (params: ListParams = {}) => { - return objectStore - .listObjectsV2({ - ...params, - Bucket: sanitizeBucket(bucketName), - Prefix: sanitizeKey(path), - }) - .promise() + return objectStore.listObjectsV2({ + ...params, + Bucket: sanitizeBucket(bucketName), + Prefix: sanitizeKey(path), + }) } let isTruncated = false, token, - objects: AWS.S3.Types.Object[] = [] + objects: Object[] = [] do { let params: ListParams = {} if (token) { @@ -331,18 +356,19 @@ export async function listAllObjects(bucketName: string, path: string) { /** * Generate a presigned url with a default TTL of 1 hour */ -export function getPresignedUrl( +export async function getPresignedUrl( bucketName: string, key: string, durationSeconds = 3600 ) { - const objectStore = ObjectStore(bucketName, { presigning: true }) + const objectStore = ObjectStore({ presigning: true }) const params = { Bucket: sanitizeBucket(bucketName), Key: sanitizeKey(key), - Expires: durationSeconds, } - const url = objectStore.getSignedUrl("getObject", params) + const url = await getSignedUrl(objectStore, new GetObjectCommand(params), { + expiresIn: durationSeconds, + }) if (!env.MINIO_ENABLED) { // return the full URL to the client @@ -366,7 +392,11 @@ export async function retrieveToTmp(bucketName: string, filepath: string) { filepath = sanitizeKey(filepath) const data = await retrieve(bucketName, filepath) const outputPath = join(budibaseTempDir(), v4()) - fs.writeFileSync(outputPath, data) + if (data instanceof stream.Readable) { + data.pipe(fs.createWriteStream(outputPath)) + } else { + fs.writeFileSync(outputPath, data) + } return outputPath } @@ -394,7 +424,7 @@ export async function retrieveDirectory(bucketName: string, path: string) { stream.pipe(writeStream) writePromises.push( new Promise((resolve, reject) => { - stream.on("finish", resolve) + writeStream.on("finish", resolve) stream.on("error", reject) writeStream.on("error", reject) }) @@ -408,17 +438,17 @@ export async function retrieveDirectory(bucketName: string, path: string) { * Delete a single file. */ export async function deleteFile(bucketName: string, filepath: string) { - const objectStore = ObjectStore(bucketName) + const objectStore = ObjectStore() await createBucketIfNotExists(objectStore, bucketName) const params = { Bucket: bucketName, Key: sanitizeKey(filepath), } - return objectStore.deleteObject(params).promise() + return objectStore.deleteObject(params) } export async function deleteFiles(bucketName: string, filepaths: string[]) { - const objectStore = ObjectStore(bucketName) + const objectStore = ObjectStore() await createBucketIfNotExists(objectStore, bucketName) const params = { Bucket: bucketName, @@ -426,7 +456,7 @@ export async function deleteFiles(bucketName: string, filepaths: string[]) { Objects: filepaths.map((path: any) => ({ Key: sanitizeKey(path) })), }, } - return objectStore.deleteObjects(params).promise() + return objectStore.deleteObjects(params) } /** @@ -438,13 +468,13 @@ export async function deleteFolder( ): Promise { bucketName = sanitizeBucket(bucketName) folder = sanitizeKey(folder) - const client = ObjectStore(bucketName) + const client = ObjectStore() const listParams = { Bucket: bucketName, Prefix: folder, } - const existingObjectsResponse = await client.listObjects(listParams).promise() + const existingObjectsResponse = await client.listObjects(listParams) if (existingObjectsResponse.Contents?.length === 0) { return } @@ -459,7 +489,7 @@ export async function deleteFolder( deleteParams.Delete.Objects.push({ Key: content.Key }) }) - const deleteResponse = await client.deleteObjects(deleteParams).promise() + const deleteResponse = await client.deleteObjects(deleteParams) // can only empty 1000 items at once if (deleteResponse.Deleted?.length === 1000) { return deleteFolder(bucketName, folder) @@ -534,29 +564,33 @@ export async function getReadStream( ): Promise { bucketName = sanitizeBucket(bucketName) path = sanitizeKey(path) - const client = ObjectStore(bucketName) + const client = ObjectStore() const params = { Bucket: bucketName, Key: path, } - return client.getObject(params).createReadStream() + const response = await client.getObject(params) + if (!response.Body || !(response.Body instanceof stream.Readable)) { + throw new Error("Unable to retrieve stream - invalid response") + } + return response.Body } export async function getObjectMetadata( bucket: string, path: string -): Promise { +): Promise { bucket = sanitizeBucket(bucket) path = sanitizeKey(path) - const client = ObjectStore(bucket) + const client = ObjectStore() const params = { Bucket: bucket, Key: path, } try { - return await client.headObject(params).promise() + return await client.headObject(params) } catch (err: any) { throw new Error("Unable to retrieve metadata from object") } diff --git a/packages/backend-core/src/objectStore/utils.ts b/packages/backend-core/src/objectStore/utils.ts index 30c2fefbf11..2a9dd26e027 100644 --- a/packages/backend-core/src/objectStore/utils.ts +++ b/packages/backend-core/src/objectStore/utils.ts @@ -2,7 +2,10 @@ import path, { join } from "path" import { tmpdir } from "os" import fs from "fs" import env from "../environment" -import { PutBucketLifecycleConfigurationRequest } from "aws-sdk/clients/s3" +import { + LifecycleRule, + PutBucketLifecycleConfigurationCommandInput, +} from "@aws-sdk/client-s3" import * as objectStore from "./objectStore" import { AutomationAttachment, @@ -43,8 +46,8 @@ export function budibaseTempDir() { export const bucketTTLConfig = ( bucketName: string, days: number -): PutBucketLifecycleConfigurationRequest => { - const lifecycleRule = { +): PutBucketLifecycleConfigurationCommandInput => { + const lifecycleRule: LifecycleRule = { ID: `${bucketName}-ExpireAfter${days}days`, Prefix: "", Status: "Enabled", diff --git a/packages/backend-core/src/queue/inMemoryQueue.ts b/packages/backend-core/src/queue/inMemoryQueue.ts index 6833c9a3067..867388ae073 100644 --- a/packages/backend-core/src/queue/inMemoryQueue.ts +++ b/packages/backend-core/src/queue/inMemoryQueue.ts @@ -199,6 +199,12 @@ class InMemoryQueue implements Partial { return this as unknown as Queue } + off(event: string, callback: (...args: any[]) => void): Queue { + // @ts-expect-error - this callback can be one of many types + this._emitter.off(event, callback) + return this as unknown as Queue + } + async count() { return this._messages.length } diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index cbc00193035..2b153389252 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -264,7 +264,9 @@ export class UserDB { const creatorsChange = (await isCreator(dbUser)) !== (await isCreator(user)) ? 1 : 0 return UserDB.quotas.addUsers(change, creatorsChange, async () => { - await validateUniqueUser(email, tenantId) + if (!opts.isAccountHolder) { + await validateUniqueUser(email, tenantId) + } let builtUser = await UserDB.buildUser(user, opts, tenantId, dbUser) // don't allow a user to update its own roles/perms @@ -569,6 +571,7 @@ export class UserDB { hashPassword: opts?.hashPassword, requirePassword: opts?.requirePassword, skipPasswordValidation: opts?.skipPasswordValidation, + isAccountHolder: true, }) } diff --git a/packages/bbui/src/Actions/click_outside.ts b/packages/bbui/src/Actions/click_outside.ts index 0c2eb036bc7..551fbde6001 100644 --- a/packages/bbui/src/Actions/click_outside.ts +++ b/packages/bbui/src/Actions/click_outside.ts @@ -93,7 +93,10 @@ const handleMouseDown = (e: MouseEvent) => { // Handle iframe clicks by detecting a loss of focus on the main window const handleBlur = () => { - if (document.activeElement?.tagName === "IFRAME") { + if ( + document.activeElement && + ["IFRAME", "BODY"].includes(document.activeElement.tagName) + ) { handleClick( new MouseEvent("click", { relatedTarget: document.activeElement }) ) diff --git a/packages/builder/package.json b/packages/builder/package.json index 0fa46872425..a70d19209e7 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -53,7 +53,7 @@ "@budibase/shared-core": "*", "@budibase/string-templates": "*", "@budibase/types": "*", - "@codemirror/autocomplete": "^6.7.1", + "@codemirror/autocomplete": "6.9.0", "@codemirror/commands": "^6.2.4", "@codemirror/lang-javascript": "^6.1.8", "@codemirror/language": "^6.6.0", diff --git a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte index 82271bd0667..512f98de187 100644 --- a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte +++ b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte @@ -151,6 +151,8 @@ const screenCount = affectedScreens.length let message = `Removing ${source?.name} ` let initialLength = message.length + const hasChanged = () => message.length !== initialLength + if (sourceType === SourceType.TABLE) { const views = "views" in source ? Object.values(source?.views ?? []) : [] message += `will delete its data${ @@ -169,10 +171,10 @@ initialLength !== message.length ? ", and break connected screens:" : "will break connected screens:" - } else { + } else if (hasChanged()) { message += "." } - return message.length !== initialLength ? message : "" + return hasChanged() ? message : "" } diff --git a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte index bc88f0f981a..2acde475390 100644 --- a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte +++ b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte @@ -45,10 +45,11 @@ import { EditorModes } from "./" import { themeStore } from "@/stores/portal" import type { EditorMode } from "@budibase/types" + import type { BindingCompletion } from "@/types" export let label: string | undefined = undefined // TODO: work out what best type fits this - export let completions: any[] = [] + export let completions: BindingCompletion[] = [] export let mode: EditorMode = EditorModes.Handlebars export let value: string | null = "" export let placeholder: string | null = null diff --git a/packages/builder/src/components/common/CodeEditor/index.ts b/packages/builder/src/components/common/CodeEditor/index.ts index 0c974e0bf43..55294846658 100644 --- a/packages/builder/src/components/common/CodeEditor/index.ts +++ b/packages/builder/src/components/common/CodeEditor/index.ts @@ -1,13 +1,10 @@ import { getManifest } from "@budibase/string-templates" import sanitizeHtml from "sanitize-html" import { groupBy } from "lodash" -import { - BindingCompletion, - EditorModesMap, - Helper, - Snippet, -} from "@budibase/types" +import { EditorModesMap, Helper, Snippet } from "@budibase/types" import { CompletionContext } from "@codemirror/autocomplete" +import { EditorView } from "@codemirror/view" +import { BindingCompletion, BindingCompletionOption } from "@/types" export const EditorModes: EditorModesMap = { JS: { @@ -25,15 +22,7 @@ export const EditorModes: EditorModesMap = { }, } -export const SECTIONS = { - HB_HELPER: { - name: "Helper", - type: "helper", - icon: "Code", - }, -} - -export const buildHelperInfoNode = (completion: any, helper: Helper) => { +const buildHelperInfoNode = (helper: Helper) => { const ele = document.createElement("div") ele.classList.add("info-bubble") @@ -65,7 +54,7 @@ const toSpectrumIcon = (name: string) => { ` } -export const buildSectionHeader = ( +const buildSectionHeader = ( type: string, sectionName: string, icon: string, @@ -84,30 +73,27 @@ export const buildSectionHeader = ( } } -export const helpersToCompletion = ( +const helpersToCompletion = ( helpers: Record, mode: { name: "javascript" | "handlebars" } -) => { - const { type, name: sectionName, icon } = SECTIONS.HB_HELPER - const helperSection = buildSectionHeader(type, sectionName, icon, 99) +): BindingCompletionOption[] => { + const helperSection = buildSectionHeader("helper", "Helpers", "Code", 99) return Object.keys(helpers).flatMap(helperName => { - let helper = helpers[helperName] + const helper = helpers[helperName] return { label: helperName, - info: (completion: BindingCompletion) => { - return buildHelperInfoNode(completion, helper) - }, + info: () => buildHelperInfoNode(helper), type: "helper", section: helperSection, detail: "Function", apply: ( - view: any, - completion: BindingCompletion, + view: EditorView, + _completion: BindingCompletionOption, from: number, to: number ) => { - insertBinding(view, from, to, helperName, mode) + insertBinding(view, from, to, helperName, mode, AutocompleteType.HELPER) }, } }) @@ -115,7 +101,7 @@ export const helpersToCompletion = ( export const getHelperCompletions = (mode: { name: "javascript" | "handlebars" -}) => { +}): BindingCompletionOption[] => { // TODO: manifest needs to be properly typed const manifest: any = getManifest() return Object.keys(manifest).flatMap(key => { @@ -123,49 +109,33 @@ export const getHelperCompletions = (mode: { }) } -export const snippetAutoComplete = (snippets: Snippet[]) => { - return function myCompletions(context: CompletionContext) { - if (!snippets?.length) { - return null - } - const word = context.matchBefore(/\w*/) - if (!word || (word.from == word.to && !context.explicit)) { - return null - } - return { - from: word.from, - options: snippets.map(snippet => ({ - label: `snippets.${snippet.name}`, - type: "text", - simple: true, - apply: ( - view: any, - completion: BindingCompletion, - from: number, - to: number - ) => { - insertSnippet(view, from, to, completion.label) - }, - })), - } - } +export const snippetAutoComplete = (snippets: Snippet[]): BindingCompletion => { + return setAutocomplete( + snippets.map(snippet => ({ + section: buildSectionHeader("snippets", "Snippets", "Code", 100), + label: `snippets.${snippet.name}`, + displayLabel: snippet.name, + })) + ) } -const bindingFilter = (options: BindingCompletion[], query: string) => { +const bindingFilter = (options: BindingCompletionOption[], query: string) => { return options.filter(completion => { - const section_parsed = completion.section.name.toLowerCase() + const section_parsed = completion.section?.toString().toLowerCase() const label_parsed = completion.label.toLowerCase() const query_parsed = query.toLowerCase() return ( - section_parsed.includes(query_parsed) || + section_parsed?.includes(query_parsed) || label_parsed.includes(query_parsed) ) }) } -export const hbAutocomplete = (baseCompletions: BindingCompletion[]) => { - async function coreCompletion(context: CompletionContext) { +export const hbAutocomplete = ( + baseCompletions: BindingCompletionOption[] +): BindingCompletion => { + function coreCompletion(context: CompletionContext) { let bindingStart = context.matchBefore(EditorModes.Handlebars.match) let options = baseCompletions || [] @@ -191,9 +161,15 @@ export const hbAutocomplete = (baseCompletions: BindingCompletion[]) => { return coreCompletion } -export const jsAutocomplete = (baseCompletions: BindingCompletion[]) => { - async function coreCompletion(context: CompletionContext) { - let jsBinding = context.matchBefore(/\$\("[\s\w]*/) +function wrappedAutocompleteMatch(context: CompletionContext) { + return context.matchBefore(/\$\("[\s\w]*/) +} + +export const jsAutocomplete = ( + baseCompletions: BindingCompletionOption[] +): BindingCompletion => { + function coreCompletion(context: CompletionContext) { + let jsBinding = wrappedAutocompleteMatch(context) let options = baseCompletions || [] if (jsBinding) { @@ -217,10 +193,42 @@ export const jsAutocomplete = (baseCompletions: BindingCompletion[]) => { return coreCompletion } -export const buildBindingInfoNode = ( - completion: BindingCompletion, - binding: any -) => { +export const jsHelperAutocomplete = ( + baseCompletions: BindingCompletionOption[] +): BindingCompletion => { + return setAutocomplete( + baseCompletions.map(helper => ({ + ...helper, + displayLabel: helper.label, + label: `helpers.${helper.label}()`, + })) + ) +} + +function setAutocomplete( + options: BindingCompletionOption[] +): BindingCompletion { + return function (context: CompletionContext) { + if (wrappedAutocompleteMatch(context)) { + return null + } + + const word = context.matchBefore(/\b\w*(\.\w*)?/) + if (!word || (word.from == word.to && !context.explicit)) { + return null + } + + return { + from: word.from, + options, + } + } +} + +const buildBindingInfoNode = (binding: { + valueHTML: string + value: string | null +}) => { if (!binding.valueHTML || binding.value == null) { return null } @@ -278,18 +286,28 @@ export function jsInsert( return parsedInsert } +const enum AutocompleteType { + BINDING, + HELPER, + TEXT, +} + // Autocomplete apply behaviour -export const insertBinding = ( - view: any, +const insertBinding = ( + view: EditorView, from: number, to: number, text: string, - mode: { name: "javascript" | "handlebars" } + mode: { name: "javascript" | "handlebars" }, + type: AutocompleteType ) => { let parsedInsert if (mode.name == "javascript") { - parsedInsert = jsInsert(view.state.doc?.toString(), from, to, text) + parsedInsert = jsInsert(view.state.doc?.toString(), from, to, text, { + helper: type === AutocompleteType.HELPER, + disableWrapping: type === AutocompleteType.TEXT, + }) } else if (mode.name == "handlebars") { parsedInsert = hbInsert(view.state.doc?.toString(), from, to, text) } else { @@ -319,30 +337,11 @@ export const insertBinding = ( }) } -export const insertSnippet = ( - view: any, - from: number, - to: number, - text: string -) => { - let cursorPos = from + text.length - view.dispatch({ - changes: { - from, - to, - insert: text, - }, - selection: { - anchor: cursorPos, - }, - }) -} - // TODO: typing in this function isn't great export const bindingsToCompletions = ( bindings: any, mode: { name: "javascript" | "handlebars" } -) => { +): BindingCompletionOption[] => { const bindingByCategory = groupBy(bindings, "category") const categoryMeta = bindings?.reduce((acc: any, ele: any) => { acc[ele.category] = acc[ele.category] || {} @@ -356,46 +355,54 @@ export const bindingsToCompletions = ( return acc }, {}) - const completions = Object.keys(bindingByCategory).reduce( - (comps: any, catKey: string) => { - const { icon, rank } = categoryMeta[catKey] || {} - - const bindingSectionHeader = buildSectionHeader( - // @ts-ignore something wrong with this - logically this should be dictionary - bindingByCategory.type, - catKey, - icon || "", - typeof rank == "number" ? rank : 1 - ) + const completions = Object.keys(bindingByCategory).reduce< + BindingCompletionOption[] + >((comps, catKey) => { + const { icon, rank } = categoryMeta[catKey] || {} + + const bindingSectionHeader = buildSectionHeader( + // @ts-ignore something wrong with this - logically this should be dictionary + bindingByCategory.type, + catKey, + icon || "", + typeof rank == "number" ? rank : 1 + ) - return [ - ...comps, - ...bindingByCategory[catKey].reduce((acc, binding) => { + comps.push( + ...bindingByCategory[catKey].reduce( + (acc, binding) => { let displayType = binding.fieldSchema?.type || binding.display?.type acc.push({ label: binding.display?.name || binding.readableBinding || "NO NAME", - info: (completion: BindingCompletion) => { - return buildBindingInfoNode(completion, binding) - }, + info: () => buildBindingInfoNode(binding), type: "binding", detail: displayType, section: bindingSectionHeader, apply: ( - view: any, - completion: BindingCompletion, + view: EditorView, + _completion: BindingCompletionOption, from: number, to: number ) => { - insertBinding(view, from, to, binding.readableBinding, mode) + insertBinding( + view, + from, + to, + binding.readableBinding, + mode, + AutocompleteType.BINDING + ) }, }) return acc - }, []), - ] - }, - [] - ) + }, + [] + ) + ) + + return comps + }, []) return completions } diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte index ffb477012cb..4bd37bf72c3 100644 --- a/packages/builder/src/components/common/bindings/BindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte @@ -23,6 +23,7 @@ snippetAutoComplete, EditorModes, bindingsToCompletions, + jsHelperAutocomplete, } from "../CodeEditor" import BindingSidePanel from "./BindingSidePanel.svelte" import EvaluationSidePanel from "./EvaluationSidePanel.svelte" @@ -34,7 +35,6 @@ import { BindingMode, SidePanel } from "@budibase/types" import type { EnrichedBinding, - BindingCompletion, Snippet, Helper, CaretPositionFn, @@ -42,7 +42,7 @@ JSONValue, } from "@budibase/types" import type { Log } from "@budibase/string-templates" - import type { CompletionContext } from "@codemirror/autocomplete" + import type { BindingCompletion, BindingCompletionOption } from "@/types" const dispatch = createEventDispatcher() @@ -91,7 +91,10 @@ $: bindingCompletions = bindingsToCompletions(enrichedBindings, editorMode) $: bindingHelpers = new BindingHelpers(getCaretPosition, insertAtPos) $: hbsCompletions = getHBSCompletions(bindingCompletions) - $: jsCompletions = getJSCompletions(bindingCompletions, snippets, useSnippets) + $: jsCompletions = getJSCompletions(bindingCompletions, snippets, { + useHelpers: allowHelpers, + useSnippets, + }) $: { // Ensure a valid side panel option is always selected if (sidePanel && !sidePanelOptions.includes(sidePanel)) { @@ -99,7 +102,7 @@ } } - const getHBSCompletions = (bindingCompletions: BindingCompletion[]) => { + const getHBSCompletions = (bindingCompletions: BindingCompletionOption[]) => { return [ hbAutocomplete([ ...bindingCompletions, @@ -109,17 +112,23 @@ } const getJSCompletions = ( - bindingCompletions: BindingCompletion[], + bindingCompletions: BindingCompletionOption[], snippets: Snippet[] | null, - useSnippets?: boolean + config: { + useHelpers: boolean + useSnippets: boolean + } ) => { - const completions: ((_: CompletionContext) => any)[] = [ - jsAutocomplete([ - ...bindingCompletions, - ...getHelperCompletions(EditorModes.JS), - ]), - ] - if (useSnippets && snippets) { + const completions: BindingCompletion[] = [] + if (bindingCompletions.length) { + completions.push(jsAutocomplete([...bindingCompletions])) + } + if (config.useHelpers) { + completions.push( + jsHelperAutocomplete([...getHelperCompletions(EditorModes.JS)]) + ) + } + if (config.useSnippets && snippets) { completions.push(snippetAutoComplete(snippets)) } return completions @@ -381,7 +390,7 @@ autofocus={autofocusEditor} placeholder={placeholder || "Add bindings by typing $ or use the menu on the right"} - jsBindingWrapping + jsBindingWrapping={bindingCompletions.length > 0} /> {/key} {/if} diff --git a/packages/builder/src/components/common/bindings/SnippetDrawer.svelte b/packages/builder/src/components/common/bindings/SnippetDrawer.svelte index f10c44f81fb..4862217b135 100644 --- a/packages/builder/src/components/common/bindings/SnippetDrawer.svelte +++ b/packages/builder/src/components/common/bindings/SnippetDrawer.svelte @@ -118,6 +118,7 @@ allowHBS={false} allowJS allowSnippets={false} + allowHelpers={false} showTabBar={false} placeholder="return function(input) ❴ ... ❵" value={code} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte index aa0b26f6b72..d13d72b89fb 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte @@ -13,6 +13,11 @@ import { fly } from "svelte/transition" import { findComponentPath } from "@/helpers/components" + // Smallest possible 1x1 transparent GIF + const ghost = new Image(1, 1) + ghost.src = + "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" + let searchString let searchRef let selectedIndex @@ -217,7 +222,8 @@ } }) - const onDragStart = component => { + const onDragStart = (e, component) => { + e.dataTransfer.setDragImage(ghost, 0, 0) previewStore.startDrag(component) } @@ -250,13 +256,12 @@ {#each category.children as component}
onDragStart(component.component)} + on:dragstart={e => onDragStart(e, component.component)} on:dragend={onDragEnd} class="component" class:selected={selectedIndex === orderMap[component.component]} on:click={() => addComponent(component.component)} - on:mouseover={() => (selectedIndex = null)} - on:focus + on:mouseenter={() => (selectedIndex = null)} > {component.name} @@ -308,7 +313,6 @@ } .component:hover { background: var(--spectrum-global-color-gray-300); - cursor: pointer; } .component :global(.spectrum-Body) { line-height: 1.2 !important; diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte index ba600c8eef1..59548ada012 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte @@ -189,8 +189,8 @@ } else if (type === "reload-plugin") { await componentStore.refreshDefinitions() } else if (type === "drop-new-component") { - const { component, parent, index } = data - await componentStore.create(component, null, parent, index) + const { component, parent, index, props } = data + await componentStore.create(component, props, parent, index) } else if (type === "add-parent-component") { const { componentId, parentType } = data await componentStore.addParent(componentId, parentType) diff --git a/packages/builder/src/stores/builder/app.ts b/packages/builder/src/stores/builder/app.ts index 3c8ed4afc1c..abebb5d9f1b 100644 --- a/packages/builder/src/stores/builder/app.ts +++ b/packages/builder/src/stores/builder/app.ts @@ -39,7 +39,7 @@ interface AppMetaState { appInstance: { _id: string } | null initialised: boolean hasAppPackage: boolean - usedPlugins: Plugin[] | null + usedPlugins: Plugin[] automations: AutomationSettings routes: { [key: string]: any } version?: string @@ -76,7 +76,7 @@ export const INITIAL_APP_META_STATE: AppMetaState = { appInstance: null, initialised: false, hasAppPackage: false, - usedPlugins: null, + usedPlugins: [], automations: {}, routes: {}, } @@ -109,7 +109,7 @@ export class AppMetaStore extends BudiStore { appInstance: app.instance, revertableVersion: app.revertableVersion, upgradableVersion: app.upgradableVersion, - usedPlugins: app.usedPlugins || null, + usedPlugins: app.usedPlugins || [], icon: app.icon, features: { ...INITIAL_APP_META_STATE.features, diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index b1198ca7b47..38fa9a6a418 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -11,6 +11,7 @@ import { findComponentParent, findAllMatchingComponents, makeComponentUnique, + findComponentType, } from "@/helpers/components" import { getComponentFieldOptions } from "@/helpers/formFields" import { selectedScreen } from "./screens" @@ -139,10 +140,6 @@ export class ComponentStore extends BudiStore { /** * Retrieve the component definition object - * @param {string} componentType - * @example - * '@budibase/standard-components/container' - * @returns {object} */ getDefinition(componentType: string) { if (!componentType) { @@ -151,10 +148,6 @@ export class ComponentStore extends BudiStore { return get(this.store).components[componentType] } - /** - * - * @returns {object} - */ getDefaultDatasource() { // Ignore users table const validTables = get(tables).list.filter(x => x._id !== "ta_users") @@ -188,8 +181,6 @@ export class ComponentStore extends BudiStore { /** * Takes an enriched component instance and applies any required migration * logic - * @param {object} enrichedComponent - * @returns {object} migrated Component */ migrateSettings(enrichedComponent: Component) { const componentPrefix = "@budibase/standard-components" @@ -230,22 +221,15 @@ export class ComponentStore extends BudiStore { for (let setting of filterableTypes || []) { const isLegacy = Array.isArray(enrichedComponent[setting.key]) if (isLegacy) { - const processedSetting = utils.processSearchFilters( + enrichedComponent[setting.key] = utils.processSearchFilters( enrichedComponent[setting.key] ) - enrichedComponent[setting.key] = processedSetting migrated = true } } return migrated } - /** - * - * @param {object} component - * @param {object} opts - * @returns - */ enrichEmptySettings( component: Component, opts: { screen?: Screen; parent?: Component; useDefaultValues?: boolean } @@ -280,14 +264,25 @@ export class ComponentStore extends BudiStore { type: "table", } } else if (setting.type === "dataProvider") { - // Pick closest data provider where required + let providerId + + // Pick closest parent data provider if one exists const path = findComponentPath(screen.props, treeId) const providers = path.filter((component: Component) => component._component?.endsWith("/dataprovider") ) - if (providers.length) { - const id = providers[providers.length - 1]?._id - component[setting.key] = `{{ literal ${safe(id)} }}` + providerId = providers[providers.length - 1]?._id + + // If none in our direct path, select the first one the screen + if (!providerId) { + providerId = findComponentType( + screen.props, + "@budibase/standard-components/dataprovider" + )?._id + } + + if (providerId) { + component[setting.key] = `{{ literal ${safe(providerId)} }}` } } else if (setting.type.startsWith("field/")) { // Autofill form field names @@ -427,17 +422,10 @@ export class ComponentStore extends BudiStore { } } - /** - * - * @param {string} componentName - * @param {object} presetProps - * @param {object} parent - * @returns - */ createInstance( componentType: string, - presetProps: any, - parent: any + presetProps?: Record, + parent?: Component ): Component | null { const screen = get(selectedScreen) if (!screen) { @@ -463,7 +451,7 @@ export class ComponentStore extends BudiStore { _id: Helpers.uuid(), _component: definition.component, _styles: { - normal: {}, + normal: { ...presetProps?._styles?.normal }, hover: {}, active: {}, }, @@ -512,19 +500,11 @@ export class ComponentStore extends BudiStore { } } - /** - * - * @param {string} componentName - * @param {object} presetProps - * @param {object} parent - * @param {number} index - * @returns - */ async create( componentType: string, - presetProps: any, - parent: Component, - index: number + presetProps?: Record, + parent?: Component, + index?: number ) { const state = get(this.store) const componentInstance = this.createInstance( @@ -611,13 +591,6 @@ export class ComponentStore extends BudiStore { return componentInstance } - /** - * - * @param {function} patchFn - * @param {string} componentId - * @param {string} screenId - * @returns - */ async patch( patchFn: (component: Component, screen: Screen) => any, componentId?: string, @@ -652,11 +625,6 @@ export class ComponentStore extends BudiStore { await screenStore.patch(patchScreen, screenId) } - /** - * - * @param {object} component - * @returns - */ async delete(component: Component) { if (!component) { return @@ -737,13 +705,6 @@ export class ComponentStore extends BudiStore { }) } - /** - * - * @param {object} targetComponent - * @param {string} mode - * @param {object} targetScreen - * @returns - */ async paste( targetComponent: Component, mode: string, @@ -1101,6 +1062,7 @@ export class ComponentStore extends BudiStore { async updateStyles(styles: Record, id: string) { const patchFn = (component: Component) => { + delete component._placeholder component._styles.normal = { ...component._styles.normal, ...styles, @@ -1231,7 +1193,7 @@ export class ComponentStore extends BudiStore { } // Create new parent instance - const newParentDefinition = this.createInstance(parentType, null, parent) + const newParentDefinition = this.createInstance(parentType) if (!newParentDefinition) { return } @@ -1267,10 +1229,6 @@ export class ComponentStore extends BudiStore { /** * Check if the components settings have been cached - * @param {string} componentType - * @example - * '@budibase/standard-components/container' - * @returns {boolean} */ isCached(componentType: string) { const settings = get(this.store).settingsCache @@ -1279,11 +1237,6 @@ export class ComponentStore extends BudiStore { /** * Cache component settings - * @param {string} componentType - * @param {object} definition - * @example - * '@budibase/standard-components/container' - * @returns {array} the settings */ cacheSettings(componentType: string, definition: ComponentDefinition | null) { let settings: ComponentSetting[] = [] @@ -1313,12 +1266,7 @@ export class ComponentStore extends BudiStore { /** * Retrieve an array of the component settings. * These settings are cached because they cannot change at run time. - * * Searches a component's definition for a setting matching a certain predicate. - * @param {string} componentType - * @example - * '@budibase/standard-components/container' - * @returns {Array} */ getComponentSettings(componentType: string) { if (!componentType) { diff --git a/packages/builder/src/stores/builder/navigation.ts b/packages/builder/src/stores/builder/navigation.ts index 1574efee2a1..1ef019a11c5 100644 --- a/packages/builder/src/stores/builder/navigation.ts +++ b/packages/builder/src/stores/builder/navigation.ts @@ -4,15 +4,13 @@ import { appStore } from "@/stores/builder" import { BudiStore } from "../BudiStore" import { AppNavigation, AppNavigationLink, UIObject } from "@budibase/types" -interface BuilderNavigationStore extends AppNavigation {} - export const INITIAL_NAVIGATION_STATE = { navigation: "Top", links: [], textAlign: "Left", } -export class NavigationStore extends BudiStore { +export class NavigationStore extends BudiStore { constructor() { super(INITIAL_NAVIGATION_STATE) } diff --git a/packages/builder/src/stores/builder/preview.ts b/packages/builder/src/stores/builder/preview.ts index 87b2b9355eb..a382eeefb0f 100644 --- a/packages/builder/src/stores/builder/preview.ts +++ b/packages/builder/src/stores/builder/preview.ts @@ -54,7 +54,7 @@ export class PreviewStore extends BudiStore { })) } - startDrag(component: any) { + async startDrag(component: string) { this.sendEvent("dragging-new-component", { dragging: true, component, diff --git a/packages/builder/src/types/bindings.ts b/packages/builder/src/types/bindings.ts new file mode 100644 index 00000000000..00571f1d8ba --- /dev/null +++ b/packages/builder/src/types/bindings.ts @@ -0,0 +1,8 @@ +import { CompletionContext, Completion } from "@codemirror/autocomplete" + +export type BindingCompletion = (context: CompletionContext) => { + from: number + options: Completion[] +} | null + +export type BindingCompletionOption = Completion diff --git a/packages/builder/src/types/index.ts b/packages/builder/src/types/index.ts new file mode 100644 index 00000000000..f280d975b46 --- /dev/null +++ b/packages/builder/src/types/index.ts @@ -0,0 +1 @@ +export * from "./bindings" diff --git a/packages/cli/src/backups/objectStore.ts b/packages/cli/src/backups/objectStore.ts index 2a24199603b..34e231b87bd 100644 --- a/packages/cli/src/backups/objectStore.ts +++ b/packages/cli/src/backups/objectStore.ts @@ -3,6 +3,7 @@ import fs from "fs" import { join } from "path" import { TEMP_DIR, MINIO_DIR } from "./utils" import { progressBar } from "../utils" +import * as stream from "node:stream" const { ObjectStoreBuckets, @@ -20,15 +21,21 @@ export async function exportObjects() { let fullList: any[] = [] let errorCount = 0 for (let bucket of bucketList) { - const client = ObjectStore(bucket) + const client = ObjectStore() try { - await client.headBucket().promise() + await client.headBucket({ + Bucket: bucket, + }) } catch (err) { errorCount++ continue } - const list = (await client.listObjectsV2().promise()) as { Contents: any[] } - fullList = fullList.concat(list.Contents.map(el => ({ ...el, bucket }))) + const list = await client.listObjectsV2({ + Bucket: bucket, + }) + fullList = fullList.concat( + list.Contents?.map(el => ({ ...el, bucket })) || [] + ) } if (errorCount === bucketList.length) { throw new Error("Unable to access MinIO/S3 - check environment config.") @@ -43,7 +50,13 @@ export async function exportObjects() { const dirs = possiblePath.slice(0, possiblePath.length - 1) fs.mkdirSync(join(path, object.bucket, ...dirs), { recursive: true }) } - fs.writeFileSync(join(path, object.bucket, ...possiblePath), data) + if (data instanceof stream.Readable) { + data.pipe( + fs.createWriteStream(join(path, object.bucket, ...possiblePath)) + ) + } else { + fs.writeFileSync(join(path, object.bucket, ...possiblePath), data) + } bar.update(++count) } bar.stop() @@ -60,7 +73,7 @@ export async function importObjects() { const bar = progressBar(total) let count = 0 for (let bucket of buckets) { - const client = ObjectStore(bucket) + const client = ObjectStore() await createBucketIfNotExists(client, bucket) const files = await uploadDirectory(bucket, join(path, bucket), "/") count += files.length diff --git a/packages/client/manifest.json b/packages/client/manifest.json index c236dd1ad91..930d5addff2 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -1455,7 +1455,8 @@ "type": "icon", "label": "Icon", "key": "icon", - "required": true + "required": true, + "defaultValue": "ri-star-fill" }, { "type": "select", diff --git a/packages/client/package.json b/packages/client/package.json index 2ae049f6d0f..72be403698e 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -5,7 +5,7 @@ "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", "type": "module", - "svelte": "src/index.js", + "svelte": "src/index.ts", "exports": { ".": { "import": "./dist/budibase-client.js", diff --git a/packages/client/src/api/api.ts b/packages/client/src/api/api.ts index b944f7bd7cc..564f401164d 100644 --- a/packages/client/src/api/api.ts +++ b/packages/client/src/api/api.ts @@ -1,10 +1,6 @@ import { createAPIClient } from "@budibase/frontend-core" import { authStore } from "../stores/auth" -import { - notificationStore, - devToolsEnabled, - devToolsStore, -} from "../stores/index" +import { notificationStore, devToolsEnabled, devToolsStore } from "../stores" import { get } from "svelte/store" export const API = createAPIClient({ diff --git a/packages/client/src/components/Block.svelte b/packages/client/src/components/Block.svelte index 48c11f152a2..5f39f7eef19 100644 --- a/packages/client/src/components/Block.svelte +++ b/packages/client/src/components/Block.svelte @@ -1,7 +1,7 @@ diff --git a/packages/client/src/components/context/RowSelectionProvider.svelte b/packages/client/src/components/context/RowSelectionProvider.svelte index 2c87a5fa002..da731e6f05b 100644 --- a/packages/client/src/components/context/RowSelectionProvider.svelte +++ b/packages/client/src/components/context/RowSelectionProvider.svelte @@ -1,6 +1,6 @@ diff --git a/packages/client/src/components/context/SnippetsProvider.svelte b/packages/client/src/components/context/SnippetsProvider.svelte index 53fa1e8b7f9..104147ecf7c 100644 --- a/packages/client/src/components/context/SnippetsProvider.svelte +++ b/packages/client/src/components/context/SnippetsProvider.svelte @@ -1,6 +1,6 @@ diff --git a/packages/client/src/components/context/StateBindingsProvider.svelte b/packages/client/src/components/context/StateBindingsProvider.svelte index a1166594a87..4ef99228c1c 100644 --- a/packages/client/src/components/context/StateBindingsProvider.svelte +++ b/packages/client/src/components/context/StateBindingsProvider.svelte @@ -1,6 +1,6 @@ diff --git a/packages/client/src/components/context/UserBindingsProvider.svelte b/packages/client/src/components/context/UserBindingsProvider.svelte index 98769cf76a5..02293e2f50a 100644 --- a/packages/client/src/components/context/UserBindingsProvider.svelte +++ b/packages/client/src/components/context/UserBindingsProvider.svelte @@ -1,7 +1,7 @@ diff --git a/packages/client/src/components/devtools/DevToolsComponentContextTab.svelte b/packages/client/src/components/devtools/DevToolsComponentContextTab.svelte index 3b4c4268512..02032aa0a5f 100644 --- a/packages/client/src/components/devtools/DevToolsComponentContextTab.svelte +++ b/packages/client/src/components/devtools/DevToolsComponentContextTab.svelte @@ -1,6 +1,6 @@ diff --git a/packages/client/src/components/overlay/ConfirmationDisplay.svelte b/packages/client/src/components/overlay/ConfirmationDisplay.svelte index b96af502df7..823c1c1ab28 100644 --- a/packages/client/src/components/overlay/ConfirmationDisplay.svelte +++ b/packages/client/src/components/overlay/ConfirmationDisplay.svelte @@ -1,5 +1,5 @@ diff --git a/packages/client/src/components/overlay/NotificationDisplay.svelte b/packages/client/src/components/overlay/NotificationDisplay.svelte index 46b3a2a6a1d..28f4b33433a 100644 --- a/packages/client/src/components/overlay/NotificationDisplay.svelte +++ b/packages/client/src/components/overlay/NotificationDisplay.svelte @@ -1,5 +1,5 @@ diff --git a/packages/client/src/components/overlay/PeekScreenDisplay.svelte b/packages/client/src/components/overlay/PeekScreenDisplay.svelte index 6e0fa81b439..17a92797d5e 100644 --- a/packages/client/src/components/overlay/PeekScreenDisplay.svelte +++ b/packages/client/src/components/overlay/PeekScreenDisplay.svelte @@ -5,7 +5,7 @@ notificationStore, routeStore, stateStore, - } from "stores" + } from "@/stores" import { Modal, ModalContent, ActionButton } from "@budibase/bbui" import { onDestroy } from "svelte" diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index bdd538748b1..15177a90c4e 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -1,19 +1,22 @@ - - - - -{#if $dndIsDragging} - -{/if} diff --git a/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte b/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte deleted file mode 100644 index 61cecc885b8..00000000000 --- a/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte +++ /dev/null @@ -1,51 +0,0 @@ - - -{#if left != null && top != null && width && height} -
-{/if} - - diff --git a/packages/client/src/components/preview/DNDSelectionIndicators.svelte b/packages/client/src/components/preview/DNDSelectionIndicators.svelte new file mode 100644 index 00000000000..cd7eb8302da --- /dev/null +++ b/packages/client/src/components/preview/DNDSelectionIndicators.svelte @@ -0,0 +1,40 @@ + + +{#if $dndIsDragging} + {#if !$isGridScreen && $dndParent} + + {/if} + + {#if !waitingForGrid} + + {/if} +{/if} diff --git a/packages/client/src/components/preview/GridDNDHandler.svelte b/packages/client/src/components/preview/GridDNDHandler.svelte index f173dfd9605..e5297e92088 100644 --- a/packages/client/src/components/preview/GridDNDHandler.svelte +++ b/packages/client/src/components/preview/GridDNDHandler.svelte @@ -1,15 +1,50 @@ - diff --git a/packages/client/src/components/preview/GridStylesButton.svelte b/packages/client/src/components/preview/GridStylesButton.svelte index 430a0659ec2..059971b0dab 100644 --- a/packages/client/src/components/preview/GridStylesButton.svelte +++ b/packages/client/src/components/preview/GridStylesButton.svelte @@ -1,6 +1,6 @@ - +{#if !$dndIsDragging && componentId} + +{/if}} diff --git a/packages/client/src/components/preview/Indicator.svelte b/packages/client/src/components/preview/Indicator.svelte index dce7945b296..800bc0423ba 100644 --- a/packages/client/src/components/preview/Indicator.svelte +++ b/packages/client/src/components/preview/Indicator.svelte @@ -1,19 +1,21 @@ - - +{#if !$dndIsDragging && $builderStore.selectedComponentId} + +{/if} diff --git a/packages/client/src/components/preview/SettingsBar.svelte b/packages/client/src/components/preview/SettingsBar.svelte index 9dae1dcb222..32d2e8e62e1 100644 --- a/packages/client/src/components/preview/SettingsBar.svelte +++ b/packages/client/src/components/preview/SettingsBar.svelte @@ -4,9 +4,9 @@ import GridStylesButton from "./GridStylesButton.svelte" import SettingsColorPicker from "./SettingsColorPicker.svelte" import SettingsPicker from "./SettingsPicker.svelte" - import { builderStore, componentStore, dndIsDragging } from "stores" + import { builderStore, componentStore, dndIsDragging } from "@/stores" import { Utils, shouldDisplaySetting } from "@budibase/frontend-core" - import { getGridVar, GridParams, Devices } from "utils/grid" + import { getGridVar, GridParams, Devices } from "@/utils/grid" const context = getContext("context") const verticalOffset = 36 diff --git a/packages/client/src/components/preview/SettingsButton.svelte b/packages/client/src/components/preview/SettingsButton.svelte index 16a4fe23d0f..01e5964917f 100644 --- a/packages/client/src/components/preview/SettingsButton.svelte +++ b/packages/client/src/components/preview/SettingsButton.svelte @@ -1,6 +1,6 @@ @@ -28,27 +14,27 @@ /> - - + + - - + + - + - - - - + + + + - {title} - {#if favicon !== ""} - + {props.title} + {#if props.favicon !== ""} + {:else} {/if} @@ -105,11 +91,15 @@ - {#if showSkeletonLoader} - + {#if props.showSkeletonLoader} + {/if}
- {#if clientLibPath} + {#if props.clientLibPath}

There was an error loading your app

The Budibase client library could not be loaded. Try republishing your @@ -120,24 +110,24 @@

{/if}

- - {#if appMigrating} - {/if} - - {#if usedPlugins?.length} - {#each usedPlugins as plugin} + {#if props.usedPlugins?.length} + {#each props.usedPlugins as plugin} {/each} {/if} -