Merge branch 'master' into BUDI-9011
This commit is contained in:
commit
4f0988b6d7
|
@ -202,6 +202,9 @@ jobs:
|
||||||
|
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build client library - necessary for component tests
|
||||||
|
run: yarn build:client
|
||||||
|
|
||||||
- name: Set up PostgreSQL 16
|
- name: Set up PostgreSQL 16
|
||||||
if: matrix.datasource == 'postgres'
|
if: matrix.datasource == 'postgres'
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -41,6 +41,11 @@ server {
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~ ^/api/(system|admin|global)/ {
|
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;
|
proxy_pass http://127.0.0.1:4002;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"version": "3.4.4",
|
"version": "3.4.7",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"concurrency": 20,
|
"concurrency": 20,
|
||||||
"command": {
|
"command": {
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
"eslint-plugin-jest": "28.9.0",
|
"eslint-plugin-jest": "28.9.0",
|
||||||
"eslint-plugin-local-rules": "3.0.2",
|
"eslint-plugin-local-rules": "3.0.2",
|
||||||
"eslint-plugin-svelte": "2.46.1",
|
"eslint-plugin-svelte": "2.46.1",
|
||||||
|
"svelte-preprocess": "^6.0.3",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"kill-port": "^1.6.1",
|
"kill-port": "^1.6.1",
|
||||||
"lerna": "7.4.2",
|
"lerna": "7.4.2",
|
||||||
|
@ -67,6 +68,7 @@
|
||||||
"lint:fix:eslint": "eslint --fix --max-warnings=0 packages",
|
"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: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",
|
"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:specs": "lerna run --stream specs",
|
||||||
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
||||||
"build:docker:airgap:single": "SINGLE_IMAGE=1 node hosting/scripts/airgapped/airgappedDockerBuild",
|
"build:docker:airgap:single": "SINGLE_IMAGE=1 node hosting/scripts/airgapped/airgappedDockerBuild",
|
||||||
|
|
|
@ -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 }))
|
|
@ -0,0 +1,4 @@
|
||||||
|
export const getSignedUrl = jest.fn((_, cmd) => {
|
||||||
|
const { inputs } = cmd
|
||||||
|
return `http://s3.example.com/${inputs?.Bucket}/${inputs?.Key}`
|
||||||
|
})
|
|
@ -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
|
|
|
@ -30,6 +30,9 @@
|
||||||
"test:watch": "jest --watchAll"
|
"test:watch": "jest --watchAll"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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/nano": "10.1.5",
|
||||||
"@budibase/pouchdb-replication-stream": "1.2.11",
|
"@budibase/pouchdb-replication-stream": "1.2.11",
|
||||||
"@budibase/shared-core": "*",
|
"@budibase/shared-core": "*",
|
||||||
|
@ -71,11 +74,13 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/types": "^29.6.3",
|
"@jest/types": "^29.6.3",
|
||||||
"@shopify/jest-koa-mocks": "5.1.1",
|
"@shopify/jest-koa-mocks": "5.1.1",
|
||||||
|
"@smithy/types": "4.0.0",
|
||||||
"@swc/core": "1.3.71",
|
"@swc/core": "1.3.71",
|
||||||
"@swc/jest": "0.2.27",
|
"@swc/jest": "0.2.27",
|
||||||
"@types/chance": "1.1.3",
|
"@types/chance": "1.1.3",
|
||||||
"@types/cookies": "0.7.8",
|
"@types/cookies": "0.7.8",
|
||||||
"@types/jest": "29.5.5",
|
"@types/jest": "29.5.5",
|
||||||
|
"@types/koa": "2.13.4",
|
||||||
"@types/lodash": "4.14.200",
|
"@types/lodash": "4.14.200",
|
||||||
"@types/node-fetch": "2.6.4",
|
"@types/node-fetch": "2.6.4",
|
||||||
"@types/pouchdb": "6.4.2",
|
"@types/pouchdb": "6.4.2",
|
||||||
|
@ -83,7 +88,6 @@
|
||||||
"@types/semver": "7.3.7",
|
"@types/semver": "7.3.7",
|
||||||
"@types/tar-fs": "2.0.1",
|
"@types/tar-fs": "2.0.1",
|
||||||
"@types/uuid": "8.3.4",
|
"@types/uuid": "8.3.4",
|
||||||
"@types/koa": "2.13.4",
|
|
||||||
"chance": "1.1.8",
|
"chance": "1.1.8",
|
||||||
"ioredis-mock": "8.9.0",
|
"ioredis-mock": "8.9.0",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
|
|
|
@ -8,6 +8,10 @@ import {
|
||||||
import { getProdAppID } from "./conversions"
|
import { getProdAppID } from "./conversions"
|
||||||
import { DatabaseQueryOpts, VirtualDocumentType } from "@budibase/types"
|
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
|
* 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.
|
* 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}`
|
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.
|
* Check if a given ID is that of a table.
|
||||||
*/
|
*/
|
||||||
|
@ -72,7 +81,7 @@ export const isTableId = (id: string): boolean => {
|
||||||
return (
|
return (
|
||||||
!!id &&
|
!!id &&
|
||||||
(id.startsWith(`${DocumentType.TABLE}${SEPARATOR}`) ||
|
(id.startsWith(`${DocumentType.TABLE}${SEPARATOR}`) ||
|
||||||
id.startsWith(`${DocumentType.DATASOURCE_PLUS}${SEPARATOR}`))
|
isExternalTableId(id))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -154,7 +154,7 @@ const environment = {
|
||||||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
||||||
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
||||||
AWS_SESSION_TOKEN: process.env.AWS_SESSION_TOKEN,
|
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_URL: process.env.MINIO_URL,
|
||||||
MINIO_ENABLED: process.env.MINIO_ENABLED || 1,
|
MINIO_ENABLED: process.env.MINIO_ENABLED || 1,
|
||||||
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
||||||
|
|
|
@ -13,7 +13,7 @@ export function clientLibraryPath(appId: string) {
|
||||||
* due to issues with the domain we were unable to continue doing this - keeping
|
* 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.
|
* 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)
|
let file = clientLibraryPath(appId)
|
||||||
if (env.CLOUDFRONT_CDN) {
|
if (env.CLOUDFRONT_CDN) {
|
||||||
// append app version to bust the cache
|
// append app version to bust the cache
|
||||||
|
@ -24,7 +24,7 @@ export function clientLibraryCDNUrl(appId: string, version: string) {
|
||||||
// file is public
|
// file is public
|
||||||
return cloudfront.getUrl(file)
|
return cloudfront.getUrl(file)
|
||||||
} else {
|
} 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)}`
|
return `/api/assets/client?${qs.encode(qsParams)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAppFileUrl(s3Key: string) {
|
export async function getAppFileUrl(s3Key: string) {
|
||||||
if (env.CLOUDFRONT_CDN) {
|
if (env.CLOUDFRONT_CDN) {
|
||||||
return cloudfront.getPresignedUrl(s3Key)
|
return cloudfront.getPresignedUrl(s3Key)
|
||||||
} else {
|
} else {
|
||||||
return objectStore.getPresignedUrl(env.APPS_BUCKET_NAME, s3Key)
|
return await objectStore.getPresignedUrl(env.APPS_BUCKET_NAME, s3Key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,11 @@ import * as cloudfront from "../cloudfront"
|
||||||
|
|
||||||
// URLs
|
// 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)
|
let file = getGlobalFileS3Key(type, name)
|
||||||
if (env.CLOUDFRONT_CDN) {
|
if (env.CLOUDFRONT_CDN) {
|
||||||
if (etag) {
|
if (etag) {
|
||||||
|
@ -13,7 +17,7 @@ export const getGlobalFileUrl = (type: string, name: string, etag?: string) => {
|
||||||
}
|
}
|
||||||
return cloudfront.getPresignedUrl(file)
|
return cloudfront.getPresignedUrl(file)
|
||||||
} else {
|
} else {
|
||||||
return objectStore.getPresignedUrl(env.GLOBAL_BUCKET_NAME, file)
|
return await objectStore.getPresignedUrl(env.GLOBAL_BUCKET_NAME, file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,23 +6,25 @@ import { Plugin } from "@budibase/types"
|
||||||
|
|
||||||
// URLS
|
// URLS
|
||||||
|
|
||||||
export function enrichPluginURLs(plugins?: Plugin[]): Plugin[] {
|
export async function enrichPluginURLs(plugins?: Plugin[]): Promise<Plugin[]> {
|
||||||
if (!plugins || !plugins.length) {
|
if (!plugins || !plugins.length) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return plugins.map(plugin => {
|
return await Promise.all(
|
||||||
const jsUrl = getPluginJSUrl(plugin)
|
plugins.map(async plugin => {
|
||||||
const iconUrl = getPluginIconUrl(plugin)
|
const jsUrl = await getPluginJSUrl(plugin)
|
||||||
|
const iconUrl = await getPluginIconUrl(plugin)
|
||||||
return { ...plugin, jsUrl, iconUrl }
|
return { ...plugin, jsUrl, iconUrl }
|
||||||
})
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPluginJSUrl(plugin: Plugin) {
|
async function getPluginJSUrl(plugin: Plugin) {
|
||||||
const s3Key = getPluginJSKey(plugin)
|
const s3Key = getPluginJSKey(plugin)
|
||||||
return getPluginUrl(s3Key)
|
return getPluginUrl(s3Key)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPluginIconUrl(plugin: Plugin): string | undefined {
|
async function getPluginIconUrl(plugin: Plugin) {
|
||||||
const s3Key = getPluginIconKey(plugin)
|
const s3Key = getPluginIconKey(plugin)
|
||||||
if (!s3Key) {
|
if (!s3Key) {
|
||||||
return
|
return
|
||||||
|
@ -30,11 +32,11 @@ function getPluginIconUrl(plugin: Plugin): string | undefined {
|
||||||
return getPluginUrl(s3Key)
|
return getPluginUrl(s3Key)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPluginUrl(s3Key: string) {
|
async function getPluginUrl(s3Key: string) {
|
||||||
if (env.CLOUDFRONT_CDN) {
|
if (env.CLOUDFRONT_CDN) {
|
||||||
return cloudfront.getPresignedUrl(s3Key)
|
return cloudfront.getPresignedUrl(s3Key)
|
||||||
} else {
|
} else {
|
||||||
return objectStore.getPresignedUrl(env.PLUGIN_BUCKET_NAME, s3Key)
|
return await objectStore.getPresignedUrl(env.PLUGIN_BUCKET_NAME, s3Key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -93,25 +93,25 @@ describe("app", () => {
|
||||||
testEnv.multiTenant()
|
testEnv.multiTenant()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("gets url with embedded minio", () => {
|
it("gets url with embedded minio", async () => {
|
||||||
testEnv.withMinio()
|
testEnv.withMinio()
|
||||||
const url = getAppFileUrl()
|
const url = await getAppFileUrl()
|
||||||
expect(url).toBe(
|
expect(url).toBe(
|
||||||
"/files/signed/prod-budi-app-assets/app_123/attachments/image.jpeg"
|
"/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()
|
testEnv.withS3()
|
||||||
const url = getAppFileUrl()
|
const url = await getAppFileUrl()
|
||||||
expect(url).toBe(
|
expect(url).toBe(
|
||||||
"http://s3.example.com/prod-budi-app-assets/app_123/attachments/image.jpeg"
|
"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()
|
testEnv.withCloudfront()
|
||||||
const url = getAppFileUrl()
|
const url = await getAppFileUrl()
|
||||||
// omit rest of signed params
|
// omit rest of signed params
|
||||||
expect(
|
expect(
|
||||||
url.includes("http://cf.example.com/app_123/attachments/image.jpeg?")
|
url.includes("http://cf.example.com/app_123/attachments/image.jpeg?")
|
||||||
|
@ -126,8 +126,8 @@ describe("app", () => {
|
||||||
|
|
||||||
it("gets url with embedded minio", async () => {
|
it("gets url with embedded minio", async () => {
|
||||||
testEnv.withMinio()
|
testEnv.withMinio()
|
||||||
await testEnv.withTenant(() => {
|
await testEnv.withTenant(async () => {
|
||||||
const url = getAppFileUrl()
|
const url = await getAppFileUrl()
|
||||||
expect(url).toBe(
|
expect(url).toBe(
|
||||||
"/files/signed/prod-budi-app-assets/app_123/attachments/image.jpeg"
|
"/files/signed/prod-budi-app-assets/app_123/attachments/image.jpeg"
|
||||||
)
|
)
|
||||||
|
@ -136,8 +136,8 @@ describe("app", () => {
|
||||||
|
|
||||||
it("gets url with custom S3", async () => {
|
it("gets url with custom S3", async () => {
|
||||||
testEnv.withS3()
|
testEnv.withS3()
|
||||||
await testEnv.withTenant(() => {
|
await testEnv.withTenant(async () => {
|
||||||
const url = getAppFileUrl()
|
const url = await getAppFileUrl()
|
||||||
expect(url).toBe(
|
expect(url).toBe(
|
||||||
"http://s3.example.com/prod-budi-app-assets/app_123/attachments/image.jpeg"
|
"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 () => {
|
it("gets url with cloudfront + s3", async () => {
|
||||||
testEnv.withCloudfront()
|
testEnv.withCloudfront()
|
||||||
await testEnv.withTenant(() => {
|
await testEnv.withTenant(async () => {
|
||||||
const url = getAppFileUrl()
|
const url = await getAppFileUrl()
|
||||||
// omit rest of signed params
|
// omit rest of signed params
|
||||||
expect(
|
expect(
|
||||||
url.includes(
|
url.includes(
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { testEnv } from "../../../../tests/extra"
|
||||||
|
|
||||||
describe("global", () => {
|
describe("global", () => {
|
||||||
describe("getGlobalFileUrl", () => {
|
describe("getGlobalFileUrl", () => {
|
||||||
function getGlobalFileUrl() {
|
async function getGlobalFileUrl() {
|
||||||
return global.getGlobalFileUrl("settings", "logoUrl", "etag")
|
return global.getGlobalFileUrl("settings", "logoUrl", "etag")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,21 +12,21 @@ describe("global", () => {
|
||||||
testEnv.singleTenant()
|
testEnv.singleTenant()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("gets url with embedded minio", () => {
|
it("gets url with embedded minio", async () => {
|
||||||
testEnv.withMinio()
|
testEnv.withMinio()
|
||||||
const url = getGlobalFileUrl()
|
const url = await getGlobalFileUrl()
|
||||||
expect(url).toBe("/files/signed/global/settings/logoUrl")
|
expect(url).toBe("/files/signed/global/settings/logoUrl")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("gets url with custom S3", () => {
|
it("gets url with custom S3", async () => {
|
||||||
testEnv.withS3()
|
testEnv.withS3()
|
||||||
const url = getGlobalFileUrl()
|
const url = await getGlobalFileUrl()
|
||||||
expect(url).toBe("http://s3.example.com/global/settings/logoUrl")
|
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()
|
testEnv.withCloudfront()
|
||||||
const url = getGlobalFileUrl()
|
const url = await getGlobalFileUrl()
|
||||||
// omit rest of signed params
|
// omit rest of signed params
|
||||||
expect(
|
expect(
|
||||||
url.includes("http://cf.example.com/settings/logoUrl?etag=etag&")
|
url.includes("http://cf.example.com/settings/logoUrl?etag=etag&")
|
||||||
|
@ -41,16 +41,16 @@ describe("global", () => {
|
||||||
|
|
||||||
it("gets url with embedded minio", async () => {
|
it("gets url with embedded minio", async () => {
|
||||||
testEnv.withMinio()
|
testEnv.withMinio()
|
||||||
await testEnv.withTenant(tenantId => {
|
await testEnv.withTenant(async tenantId => {
|
||||||
const url = getGlobalFileUrl()
|
const url = await getGlobalFileUrl()
|
||||||
expect(url).toBe(`/files/signed/global/${tenantId}/settings/logoUrl`)
|
expect(url).toBe(`/files/signed/global/${tenantId}/settings/logoUrl`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("gets url with custom S3", async () => {
|
it("gets url with custom S3", async () => {
|
||||||
testEnv.withS3()
|
testEnv.withS3()
|
||||||
await testEnv.withTenant(tenantId => {
|
await testEnv.withTenant(async tenantId => {
|
||||||
const url = getGlobalFileUrl()
|
const url = await getGlobalFileUrl()
|
||||||
expect(url).toBe(
|
expect(url).toBe(
|
||||||
`http://s3.example.com/global/${tenantId}/settings/logoUrl`
|
`http://s3.example.com/global/${tenantId}/settings/logoUrl`
|
||||||
)
|
)
|
||||||
|
@ -59,8 +59,8 @@ describe("global", () => {
|
||||||
|
|
||||||
it("gets url with cloudfront + s3", async () => {
|
it("gets url with cloudfront + s3", async () => {
|
||||||
testEnv.withCloudfront()
|
testEnv.withCloudfront()
|
||||||
await testEnv.withTenant(tenantId => {
|
await testEnv.withTenant(async tenantId => {
|
||||||
const url = getGlobalFileUrl()
|
const url = await getGlobalFileUrl()
|
||||||
// omit rest of signed params
|
// omit rest of signed params
|
||||||
expect(
|
expect(
|
||||||
url.includes(
|
url.includes(
|
||||||
|
|
|
@ -6,8 +6,8 @@ describe("plugins", () => {
|
||||||
describe("enrichPluginURLs", () => {
|
describe("enrichPluginURLs", () => {
|
||||||
const plugin = structures.plugins.plugin()
|
const plugin = structures.plugins.plugin()
|
||||||
|
|
||||||
function getEnrichedPluginUrls() {
|
async function getEnrichedPluginUrls() {
|
||||||
const enriched = plugins.enrichPluginURLs([plugin])[0]
|
const enriched = (await plugins.enrichPluginURLs([plugin]))[0]
|
||||||
return {
|
return {
|
||||||
jsUrl: enriched.jsUrl!,
|
jsUrl: enriched.jsUrl!,
|
||||||
iconUrl: enriched.iconUrl!,
|
iconUrl: enriched.iconUrl!,
|
||||||
|
@ -19,9 +19,9 @@ describe("plugins", () => {
|
||||||
testEnv.singleTenant()
|
testEnv.singleTenant()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("gets url with embedded minio", () => {
|
it("gets url with embedded minio", async () => {
|
||||||
testEnv.withMinio()
|
testEnv.withMinio()
|
||||||
const urls = getEnrichedPluginUrls()
|
const urls = await getEnrichedPluginUrls()
|
||||||
expect(urls.jsUrl).toBe(
|
expect(urls.jsUrl).toBe(
|
||||||
`/files/signed/plugins/${plugin.name}/plugin.min.js`
|
`/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()
|
testEnv.withS3()
|
||||||
const urls = getEnrichedPluginUrls()
|
const urls = await getEnrichedPluginUrls()
|
||||||
expect(urls.jsUrl).toBe(
|
expect(urls.jsUrl).toBe(
|
||||||
`http://s3.example.com/plugins/${plugin.name}/plugin.min.js`
|
`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()
|
testEnv.withCloudfront()
|
||||||
const urls = getEnrichedPluginUrls()
|
const urls = await getEnrichedPluginUrls()
|
||||||
// omit rest of signed params
|
// omit rest of signed params
|
||||||
expect(
|
expect(
|
||||||
urls.jsUrl.includes(
|
urls.jsUrl.includes(
|
||||||
|
@ -65,8 +65,8 @@ describe("plugins", () => {
|
||||||
|
|
||||||
it("gets url with embedded minio", async () => {
|
it("gets url with embedded minio", async () => {
|
||||||
testEnv.withMinio()
|
testEnv.withMinio()
|
||||||
await testEnv.withTenant(tenantId => {
|
await testEnv.withTenant(async tenantId => {
|
||||||
const urls = getEnrichedPluginUrls()
|
const urls = await getEnrichedPluginUrls()
|
||||||
expect(urls.jsUrl).toBe(
|
expect(urls.jsUrl).toBe(
|
||||||
`/files/signed/plugins/${tenantId}/${plugin.name}/plugin.min.js`
|
`/files/signed/plugins/${tenantId}/${plugin.name}/plugin.min.js`
|
||||||
)
|
)
|
||||||
|
@ -78,8 +78,8 @@ describe("plugins", () => {
|
||||||
|
|
||||||
it("gets url with custom S3", async () => {
|
it("gets url with custom S3", async () => {
|
||||||
testEnv.withS3()
|
testEnv.withS3()
|
||||||
await testEnv.withTenant(tenantId => {
|
await testEnv.withTenant(async tenantId => {
|
||||||
const urls = getEnrichedPluginUrls()
|
const urls = await getEnrichedPluginUrls()
|
||||||
expect(urls.jsUrl).toBe(
|
expect(urls.jsUrl).toBe(
|
||||||
`http://s3.example.com/plugins/${tenantId}/${plugin.name}/plugin.min.js`
|
`http://s3.example.com/plugins/${tenantId}/${plugin.name}/plugin.min.js`
|
||||||
)
|
)
|
||||||
|
@ -91,8 +91,8 @@ describe("plugins", () => {
|
||||||
|
|
||||||
it("gets url with cloudfront + s3", async () => {
|
it("gets url with cloudfront + s3", async () => {
|
||||||
testEnv.withCloudfront()
|
testEnv.withCloudfront()
|
||||||
await testEnv.withTenant(tenantId => {
|
await testEnv.withTenant(async tenantId => {
|
||||||
const urls = getEnrichedPluginUrls()
|
const urls = await getEnrichedPluginUrls()
|
||||||
// omit rest of signed params
|
// omit rest of signed params
|
||||||
expect(
|
expect(
|
||||||
urls.jsUrl.includes(
|
urls.jsUrl.includes(
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
const sanitize = require("sanitize-s3-objectkey")
|
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 stream, { Readable } from "stream"
|
||||||
import fetch from "node-fetch"
|
import fetch from "node-fetch"
|
||||||
import tar from "tar-fs"
|
import tar from "tar-fs"
|
||||||
|
@ -13,8 +22,8 @@ import { bucketTTLConfig, budibaseTempDir } from "./utils"
|
||||||
import { v4 } from "uuid"
|
import { v4 } from "uuid"
|
||||||
import { APP_PREFIX, APP_DEV_PREFIX } from "../db"
|
import { APP_PREFIX, APP_DEV_PREFIX } from "../db"
|
||||||
import fsp from "fs/promises"
|
import fsp from "fs/promises"
|
||||||
import { HeadObjectOutput } from "aws-sdk/clients/s3"
|
|
||||||
import { ReadableStream } from "stream/web"
|
import { ReadableStream } from "stream/web"
|
||||||
|
import { NodeJsClient } from "@smithy/types"
|
||||||
|
|
||||||
const streamPipeline = promisify(stream.pipeline)
|
const streamPipeline = promisify(stream.pipeline)
|
||||||
// use this as a temporary store of buckets that are being created
|
// use this as a temporary store of buckets that are being created
|
||||||
|
@ -84,26 +93,24 @@ export function sanitizeBucket(input: string) {
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export function ObjectStore(
|
export function ObjectStore(
|
||||||
bucket: string,
|
|
||||||
opts: { presigning: boolean } = { presigning: false }
|
opts: { presigning: boolean } = { presigning: false }
|
||||||
) {
|
) {
|
||||||
const config: AWS.S3.ClientConfiguration = {
|
const config: S3ClientConfig = {
|
||||||
s3ForcePathStyle: true,
|
forcePathStyle: true,
|
||||||
signatureVersion: "v4",
|
credentials: {
|
||||||
apiVersion: "2006-03-01",
|
accessKeyId: env.MINIO_ACCESS_KEY!,
|
||||||
accessKeyId: env.MINIO_ACCESS_KEY,
|
secretAccessKey: env.MINIO_SECRET_KEY!,
|
||||||
secretAccessKey: env.MINIO_SECRET_KEY,
|
},
|
||||||
region: env.AWS_REGION,
|
region: env.AWS_REGION,
|
||||||
}
|
}
|
||||||
if (bucket) {
|
|
||||||
config.params = {
|
|
||||||
Bucket: sanitizeBucket(bucket),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// for AWS Credentials using temporary session token
|
// for AWS Credentials using temporary session token
|
||||||
if (!env.MINIO_ENABLED && env.AWS_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
|
// 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.
|
// 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,
|
// 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
|
// 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 {
|
} else {
|
||||||
config.endpoint = env.MINIO_URL
|
config.endpoint = env.MINIO_URL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AWS.S3(config)
|
return new S3(config) as NodeJsClient<S3>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -132,26 +139,25 @@ export async function createBucketIfNotExists(
|
||||||
): Promise<{ created: boolean; exists: boolean }> {
|
): Promise<{ created: boolean; exists: boolean }> {
|
||||||
bucketName = sanitizeBucket(bucketName)
|
bucketName = sanitizeBucket(bucketName)
|
||||||
try {
|
try {
|
||||||
await client
|
await client.headBucket({
|
||||||
.headBucket({
|
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
})
|
})
|
||||||
.promise()
|
|
||||||
return { created: false, exists: true }
|
return { created: false, exists: true }
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const promises: any = STATE.bucketCreationPromises
|
const statusCode = err.statusCode || err.$response?.statusCode
|
||||||
const doesntExist = err.statusCode === 404,
|
const promises: Record<string, Promise<any> | undefined> =
|
||||||
noAccess = err.statusCode === 403
|
STATE.bucketCreationPromises
|
||||||
|
const doesntExist = statusCode === 404,
|
||||||
|
noAccess = statusCode === 403
|
||||||
if (promises[bucketName]) {
|
if (promises[bucketName]) {
|
||||||
await promises[bucketName]
|
await promises[bucketName]
|
||||||
return { created: false, exists: true }
|
return { created: false, exists: true }
|
||||||
} else if (doesntExist || noAccess) {
|
} else if (doesntExist || noAccess) {
|
||||||
if (doesntExist) {
|
if (doesntExist) {
|
||||||
promises[bucketName] = client
|
promises[bucketName] = client.createBucket({
|
||||||
.createBucket({
|
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
})
|
})
|
||||||
.promise()
|
|
||||||
await promises[bucketName]
|
await promises[bucketName]
|
||||||
delete promises[bucketName]
|
delete promises[bucketName]
|
||||||
return { created: true, exists: false }
|
return { created: true, exists: false }
|
||||||
|
@ -180,25 +186,26 @@ export async function upload({
|
||||||
|
|
||||||
const fileBytes = path ? (await fsp.open(path)).createReadStream() : body
|
const fileBytes = path ? (await fsp.open(path)).createReadStream() : body
|
||||||
|
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore()
|
||||||
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
|
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
|
||||||
|
|
||||||
if (ttl && bucketCreated.created) {
|
if (ttl && bucketCreated.created) {
|
||||||
let ttlConfig = bucketTTLConfig(bucketName, ttl)
|
let ttlConfig = bucketTTLConfig(bucketName, ttl)
|
||||||
await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
|
await objectStore.putBucketLifecycleConfiguration(ttlConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
let contentType = type
|
let contentType = type
|
||||||
if (!contentType) {
|
const finalContentType = contentType
|
||||||
contentType = extension
|
? contentType
|
||||||
|
: extension
|
||||||
? CONTENT_TYPE_MAP[extension.toLowerCase()]
|
? CONTENT_TYPE_MAP[extension.toLowerCase()]
|
||||||
: CONTENT_TYPE_MAP.txt
|
: CONTENT_TYPE_MAP.txt
|
||||||
}
|
const config: PutObjectCommandInput = {
|
||||||
const config: any = {
|
|
||||||
// windows file paths need to be converted to forward slashes for s3
|
// windows file paths need to be converted to forward slashes for s3
|
||||||
|
Bucket: sanitizeBucket(bucketName),
|
||||||
Key: sanitizeKey(filename),
|
Key: sanitizeKey(filename),
|
||||||
Body: fileBytes,
|
Body: fileBytes as stream.Readable | Buffer,
|
||||||
ContentType: contentType,
|
ContentType: finalContentType,
|
||||||
}
|
}
|
||||||
if (metadata && typeof metadata === "object") {
|
if (metadata && typeof metadata === "object") {
|
||||||
// remove any nullish keys from the metadata object, as these may be considered invalid
|
// 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]
|
delete metadata[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
config.Metadata = metadata
|
config.Metadata = metadata as Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
throw new Error("Stream to upload is invalid/undefined")
|
||||||
}
|
}
|
||||||
const extension = filename.split(".").pop()
|
const extension = filename.split(".").pop()
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore()
|
||||||
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
|
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
|
||||||
|
|
||||||
if (ttl && bucketCreated.created) {
|
if (ttl && bucketCreated.created) {
|
||||||
let ttlConfig = bucketTTLConfig(bucketName, ttl)
|
let ttlConfig = bucketTTLConfig(bucketName, ttl)
|
||||||
await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
|
await objectStore.putBucketLifecycleConfiguration(ttlConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set content type for certain known extensions
|
// Set content type for certain known extensions
|
||||||
|
@ -267,13 +279,15 @@ export async function streamUpload({
|
||||||
...extra,
|
...extra,
|
||||||
}
|
}
|
||||||
|
|
||||||
const details = await objectStore.upload(params).promise()
|
const upload = new Upload({
|
||||||
const headDetails = await objectStore
|
client: objectStore,
|
||||||
.headObject({
|
params,
|
||||||
|
})
|
||||||
|
const details = await upload.done()
|
||||||
|
const headDetails = await objectStore.headObject({
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Key: objKey,
|
Key: objKey,
|
||||||
})
|
})
|
||||||
.promise()
|
|
||||||
return {
|
return {
|
||||||
...details,
|
...details,
|
||||||
ContentLength: headDetails.ContentLength,
|
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
|
* 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.
|
* will be converted, otherwise it will be returned as a buffer stream.
|
||||||
*/
|
*/
|
||||||
export async function retrieve(bucketName: string, filepath: string) {
|
export async function retrieve(
|
||||||
const objectStore = ObjectStore(bucketName)
|
bucketName: string,
|
||||||
|
filepath: string
|
||||||
|
): Promise<string | stream.Readable> {
|
||||||
|
const objectStore = ObjectStore()
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: sanitizeBucket(bucketName),
|
Bucket: sanitizeBucket(bucketName),
|
||||||
Key: sanitizeKey(filepath),
|
Key: sanitizeKey(filepath),
|
||||||
}
|
}
|
||||||
const response: any = await objectStore.getObject(params).promise()
|
const response = await objectStore.getObject(params)
|
||||||
// currently these are all strings
|
if (!response.Body) {
|
||||||
|
throw new Error("Unable to retrieve object")
|
||||||
|
}
|
||||||
if (STRING_CONTENT_TYPES.includes(response.ContentType)) {
|
if (STRING_CONTENT_TYPES.includes(response.ContentType)) {
|
||||||
return response.Body.toString("utf8")
|
return response.Body.transformToString()
|
||||||
} else {
|
} 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) {
|
export async function listAllObjects(
|
||||||
const objectStore = ObjectStore(bucketName)
|
bucketName: string,
|
||||||
|
path: string
|
||||||
|
): Promise<S3Object[]> {
|
||||||
|
const objectStore = ObjectStore()
|
||||||
const list = (params: ListParams = {}) => {
|
const list = (params: ListParams = {}) => {
|
||||||
return objectStore
|
return objectStore.listObjectsV2({
|
||||||
.listObjectsV2({
|
|
||||||
...params,
|
...params,
|
||||||
Bucket: sanitizeBucket(bucketName),
|
Bucket: sanitizeBucket(bucketName),
|
||||||
Prefix: sanitizeKey(path),
|
Prefix: sanitizeKey(path),
|
||||||
})
|
})
|
||||||
.promise()
|
|
||||||
}
|
}
|
||||||
let isTruncated = false,
|
let isTruncated = false,
|
||||||
token,
|
token,
|
||||||
objects: AWS.S3.Types.Object[] = []
|
objects: Object[] = []
|
||||||
do {
|
do {
|
||||||
let params: ListParams = {}
|
let params: ListParams = {}
|
||||||
if (token) {
|
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
|
* Generate a presigned url with a default TTL of 1 hour
|
||||||
*/
|
*/
|
||||||
export function getPresignedUrl(
|
export async function getPresignedUrl(
|
||||||
bucketName: string,
|
bucketName: string,
|
||||||
key: string,
|
key: string,
|
||||||
durationSeconds = 3600
|
durationSeconds = 3600
|
||||||
) {
|
) {
|
||||||
const objectStore = ObjectStore(bucketName, { presigning: true })
|
const objectStore = ObjectStore({ presigning: true })
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: sanitizeBucket(bucketName),
|
Bucket: sanitizeBucket(bucketName),
|
||||||
Key: sanitizeKey(key),
|
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) {
|
if (!env.MINIO_ENABLED) {
|
||||||
// return the full URL to the client
|
// return the full URL to the client
|
||||||
|
@ -366,7 +392,11 @@ export async function retrieveToTmp(bucketName: string, filepath: string) {
|
||||||
filepath = sanitizeKey(filepath)
|
filepath = sanitizeKey(filepath)
|
||||||
const data = await retrieve(bucketName, filepath)
|
const data = await retrieve(bucketName, filepath)
|
||||||
const outputPath = join(budibaseTempDir(), v4())
|
const outputPath = join(budibaseTempDir(), v4())
|
||||||
|
if (data instanceof stream.Readable) {
|
||||||
|
data.pipe(fs.createWriteStream(outputPath))
|
||||||
|
} else {
|
||||||
fs.writeFileSync(outputPath, data)
|
fs.writeFileSync(outputPath, data)
|
||||||
|
}
|
||||||
return outputPath
|
return outputPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -394,7 +424,7 @@ export async function retrieveDirectory(bucketName: string, path: string) {
|
||||||
stream.pipe(writeStream)
|
stream.pipe(writeStream)
|
||||||
writePromises.push(
|
writePromises.push(
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
stream.on("finish", resolve)
|
writeStream.on("finish", resolve)
|
||||||
stream.on("error", reject)
|
stream.on("error", reject)
|
||||||
writeStream.on("error", reject)
|
writeStream.on("error", reject)
|
||||||
})
|
})
|
||||||
|
@ -408,17 +438,17 @@ export async function retrieveDirectory(bucketName: string, path: string) {
|
||||||
* Delete a single file.
|
* Delete a single file.
|
||||||
*/
|
*/
|
||||||
export async function deleteFile(bucketName: string, filepath: string) {
|
export async function deleteFile(bucketName: string, filepath: string) {
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore()
|
||||||
await createBucketIfNotExists(objectStore, bucketName)
|
await createBucketIfNotExists(objectStore, bucketName)
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
Key: sanitizeKey(filepath),
|
Key: sanitizeKey(filepath),
|
||||||
}
|
}
|
||||||
return objectStore.deleteObject(params).promise()
|
return objectStore.deleteObject(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteFiles(bucketName: string, filepaths: string[]) {
|
export async function deleteFiles(bucketName: string, filepaths: string[]) {
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore()
|
||||||
await createBucketIfNotExists(objectStore, bucketName)
|
await createBucketIfNotExists(objectStore, bucketName)
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
|
@ -426,7 +456,7 @@ export async function deleteFiles(bucketName: string, filepaths: string[]) {
|
||||||
Objects: filepaths.map((path: any) => ({ Key: sanitizeKey(path) })),
|
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<any> {
|
): Promise<any> {
|
||||||
bucketName = sanitizeBucket(bucketName)
|
bucketName = sanitizeBucket(bucketName)
|
||||||
folder = sanitizeKey(folder)
|
folder = sanitizeKey(folder)
|
||||||
const client = ObjectStore(bucketName)
|
const client = ObjectStore()
|
||||||
const listParams = {
|
const listParams = {
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
Prefix: folder,
|
Prefix: folder,
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingObjectsResponse = await client.listObjects(listParams).promise()
|
const existingObjectsResponse = await client.listObjects(listParams)
|
||||||
if (existingObjectsResponse.Contents?.length === 0) {
|
if (existingObjectsResponse.Contents?.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -459,7 +489,7 @@ export async function deleteFolder(
|
||||||
deleteParams.Delete.Objects.push({ Key: content.Key })
|
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
|
// can only empty 1000 items at once
|
||||||
if (deleteResponse.Deleted?.length === 1000) {
|
if (deleteResponse.Deleted?.length === 1000) {
|
||||||
return deleteFolder(bucketName, folder)
|
return deleteFolder(bucketName, folder)
|
||||||
|
@ -534,29 +564,33 @@ export async function getReadStream(
|
||||||
): Promise<Readable> {
|
): Promise<Readable> {
|
||||||
bucketName = sanitizeBucket(bucketName)
|
bucketName = sanitizeBucket(bucketName)
|
||||||
path = sanitizeKey(path)
|
path = sanitizeKey(path)
|
||||||
const client = ObjectStore(bucketName)
|
const client = ObjectStore()
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
Key: path,
|
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(
|
export async function getObjectMetadata(
|
||||||
bucket: string,
|
bucket: string,
|
||||||
path: string
|
path: string
|
||||||
): Promise<HeadObjectOutput> {
|
): Promise<HeadObjectCommandOutput> {
|
||||||
bucket = sanitizeBucket(bucket)
|
bucket = sanitizeBucket(bucket)
|
||||||
path = sanitizeKey(path)
|
path = sanitizeKey(path)
|
||||||
|
|
||||||
const client = ObjectStore(bucket)
|
const client = ObjectStore()
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Key: path,
|
Key: path,
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await client.headObject(params).promise()
|
return await client.headObject(params)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
throw new Error("Unable to retrieve metadata from object")
|
throw new Error("Unable to retrieve metadata from object")
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,10 @@ import path, { join } from "path"
|
||||||
import { tmpdir } from "os"
|
import { tmpdir } from "os"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import env from "../environment"
|
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 * as objectStore from "./objectStore"
|
||||||
import {
|
import {
|
||||||
AutomationAttachment,
|
AutomationAttachment,
|
||||||
|
@ -43,8 +46,8 @@ export function budibaseTempDir() {
|
||||||
export const bucketTTLConfig = (
|
export const bucketTTLConfig = (
|
||||||
bucketName: string,
|
bucketName: string,
|
||||||
days: number
|
days: number
|
||||||
): PutBucketLifecycleConfigurationRequest => {
|
): PutBucketLifecycleConfigurationCommandInput => {
|
||||||
const lifecycleRule = {
|
const lifecycleRule: LifecycleRule = {
|
||||||
ID: `${bucketName}-ExpireAfter${days}days`,
|
ID: `${bucketName}-ExpireAfter${days}days`,
|
||||||
Prefix: "",
|
Prefix: "",
|
||||||
Status: "Enabled",
|
Status: "Enabled",
|
||||||
|
|
|
@ -199,6 +199,12 @@ class InMemoryQueue implements Partial<Queue> {
|
||||||
return this as unknown as Queue
|
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() {
|
async count() {
|
||||||
return this._messages.length
|
return this._messages.length
|
||||||
}
|
}
|
||||||
|
|
|
@ -264,7 +264,9 @@ export class UserDB {
|
||||||
const creatorsChange =
|
const creatorsChange =
|
||||||
(await isCreator(dbUser)) !== (await isCreator(user)) ? 1 : 0
|
(await isCreator(dbUser)) !== (await isCreator(user)) ? 1 : 0
|
||||||
return UserDB.quotas.addUsers(change, creatorsChange, async () => {
|
return UserDB.quotas.addUsers(change, creatorsChange, async () => {
|
||||||
|
if (!opts.isAccountHolder) {
|
||||||
await validateUniqueUser(email, tenantId)
|
await validateUniqueUser(email, tenantId)
|
||||||
|
}
|
||||||
|
|
||||||
let builtUser = await UserDB.buildUser(user, opts, tenantId, dbUser)
|
let builtUser = await UserDB.buildUser(user, opts, tenantId, dbUser)
|
||||||
// don't allow a user to update its own roles/perms
|
// don't allow a user to update its own roles/perms
|
||||||
|
@ -569,6 +571,7 @@ export class UserDB {
|
||||||
hashPassword: opts?.hashPassword,
|
hashPassword: opts?.hashPassword,
|
||||||
requirePassword: opts?.requirePassword,
|
requirePassword: opts?.requirePassword,
|
||||||
skipPasswordValidation: opts?.skipPasswordValidation,
|
skipPasswordValidation: opts?.skipPasswordValidation,
|
||||||
|
isAccountHolder: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -93,7 +93,10 @@ const handleMouseDown = (e: MouseEvent) => {
|
||||||
|
|
||||||
// Handle iframe clicks by detecting a loss of focus on the main window
|
// Handle iframe clicks by detecting a loss of focus on the main window
|
||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
if (document.activeElement?.tagName === "IFRAME") {
|
if (
|
||||||
|
document.activeElement &&
|
||||||
|
["IFRAME", "BODY"].includes(document.activeElement.tagName)
|
||||||
|
) {
|
||||||
handleClick(
|
handleClick(
|
||||||
new MouseEvent("click", { relatedTarget: document.activeElement })
|
new MouseEvent("click", { relatedTarget: document.activeElement })
|
||||||
)
|
)
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
"@budibase/shared-core": "*",
|
"@budibase/shared-core": "*",
|
||||||
"@budibase/string-templates": "*",
|
"@budibase/string-templates": "*",
|
||||||
"@budibase/types": "*",
|
"@budibase/types": "*",
|
||||||
"@codemirror/autocomplete": "^6.7.1",
|
"@codemirror/autocomplete": "6.9.0",
|
||||||
"@codemirror/commands": "^6.2.4",
|
"@codemirror/commands": "^6.2.4",
|
||||||
"@codemirror/lang-javascript": "^6.1.8",
|
"@codemirror/lang-javascript": "^6.1.8",
|
||||||
"@codemirror/language": "^6.6.0",
|
"@codemirror/language": "^6.6.0",
|
||||||
|
|
|
@ -151,6 +151,8 @@
|
||||||
const screenCount = affectedScreens.length
|
const screenCount = affectedScreens.length
|
||||||
let message = `Removing ${source?.name} `
|
let message = `Removing ${source?.name} `
|
||||||
let initialLength = message.length
|
let initialLength = message.length
|
||||||
|
const hasChanged = () => message.length !== initialLength
|
||||||
|
|
||||||
if (sourceType === SourceType.TABLE) {
|
if (sourceType === SourceType.TABLE) {
|
||||||
const views = "views" in source ? Object.values(source?.views ?? []) : []
|
const views = "views" in source ? Object.values(source?.views ?? []) : []
|
||||||
message += `will delete its data${
|
message += `will delete its data${
|
||||||
|
@ -169,10 +171,10 @@
|
||||||
initialLength !== message.length
|
initialLength !== message.length
|
||||||
? ", and break connected screens:"
|
? ", and break connected screens:"
|
||||||
: "will break connected screens:"
|
: "will break connected screens:"
|
||||||
} else {
|
} else if (hasChanged()) {
|
||||||
message += "."
|
message += "."
|
||||||
}
|
}
|
||||||
return message.length !== initialLength ? message : ""
|
return hasChanged() ? message : ""
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -45,10 +45,11 @@
|
||||||
import { EditorModes } from "./"
|
import { EditorModes } from "./"
|
||||||
import { themeStore } from "@/stores/portal"
|
import { themeStore } from "@/stores/portal"
|
||||||
import type { EditorMode } from "@budibase/types"
|
import type { EditorMode } from "@budibase/types"
|
||||||
|
import type { BindingCompletion } from "@/types"
|
||||||
|
|
||||||
export let label: string | undefined = undefined
|
export let label: string | undefined = undefined
|
||||||
// TODO: work out what best type fits this
|
// TODO: work out what best type fits this
|
||||||
export let completions: any[] = []
|
export let completions: BindingCompletion[] = []
|
||||||
export let mode: EditorMode = EditorModes.Handlebars
|
export let mode: EditorMode = EditorModes.Handlebars
|
||||||
export let value: string | null = ""
|
export let value: string | null = ""
|
||||||
export let placeholder: string | null = null
|
export let placeholder: string | null = null
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
import { getManifest } from "@budibase/string-templates"
|
import { getManifest } from "@budibase/string-templates"
|
||||||
import sanitizeHtml from "sanitize-html"
|
import sanitizeHtml from "sanitize-html"
|
||||||
import { groupBy } from "lodash"
|
import { groupBy } from "lodash"
|
||||||
import {
|
import { EditorModesMap, Helper, Snippet } from "@budibase/types"
|
||||||
BindingCompletion,
|
|
||||||
EditorModesMap,
|
|
||||||
Helper,
|
|
||||||
Snippet,
|
|
||||||
} from "@budibase/types"
|
|
||||||
import { CompletionContext } from "@codemirror/autocomplete"
|
import { CompletionContext } from "@codemirror/autocomplete"
|
||||||
|
import { EditorView } from "@codemirror/view"
|
||||||
|
import { BindingCompletion, BindingCompletionOption } from "@/types"
|
||||||
|
|
||||||
export const EditorModes: EditorModesMap = {
|
export const EditorModes: EditorModesMap = {
|
||||||
JS: {
|
JS: {
|
||||||
|
@ -25,15 +22,7 @@ export const EditorModes: EditorModesMap = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SECTIONS = {
|
const buildHelperInfoNode = (helper: Helper) => {
|
||||||
HB_HELPER: {
|
|
||||||
name: "Helper",
|
|
||||||
type: "helper",
|
|
||||||
icon: "Code",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const buildHelperInfoNode = (completion: any, helper: Helper) => {
|
|
||||||
const ele = document.createElement("div")
|
const ele = document.createElement("div")
|
||||||
ele.classList.add("info-bubble")
|
ele.classList.add("info-bubble")
|
||||||
|
|
||||||
|
@ -65,7 +54,7 @@ const toSpectrumIcon = (name: string) => {
|
||||||
</svg>`
|
</svg>`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildSectionHeader = (
|
const buildSectionHeader = (
|
||||||
type: string,
|
type: string,
|
||||||
sectionName: string,
|
sectionName: string,
|
||||||
icon: string,
|
icon: string,
|
||||||
|
@ -84,30 +73,27 @@ export const buildSectionHeader = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const helpersToCompletion = (
|
const helpersToCompletion = (
|
||||||
helpers: Record<string, Helper>,
|
helpers: Record<string, Helper>,
|
||||||
mode: { name: "javascript" | "handlebars" }
|
mode: { name: "javascript" | "handlebars" }
|
||||||
) => {
|
): BindingCompletionOption[] => {
|
||||||
const { type, name: sectionName, icon } = SECTIONS.HB_HELPER
|
const helperSection = buildSectionHeader("helper", "Helpers", "Code", 99)
|
||||||
const helperSection = buildSectionHeader(type, sectionName, icon, 99)
|
|
||||||
|
|
||||||
return Object.keys(helpers).flatMap(helperName => {
|
return Object.keys(helpers).flatMap(helperName => {
|
||||||
let helper = helpers[helperName]
|
const helper = helpers[helperName]
|
||||||
return {
|
return {
|
||||||
label: helperName,
|
label: helperName,
|
||||||
info: (completion: BindingCompletion) => {
|
info: () => buildHelperInfoNode(helper),
|
||||||
return buildHelperInfoNode(completion, helper)
|
|
||||||
},
|
|
||||||
type: "helper",
|
type: "helper",
|
||||||
section: helperSection,
|
section: helperSection,
|
||||||
detail: "Function",
|
detail: "Function",
|
||||||
apply: (
|
apply: (
|
||||||
view: any,
|
view: EditorView,
|
||||||
completion: BindingCompletion,
|
_completion: BindingCompletionOption,
|
||||||
from: number,
|
from: number,
|
||||||
to: 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: {
|
export const getHelperCompletions = (mode: {
|
||||||
name: "javascript" | "handlebars"
|
name: "javascript" | "handlebars"
|
||||||
}) => {
|
}): BindingCompletionOption[] => {
|
||||||
// TODO: manifest needs to be properly typed
|
// TODO: manifest needs to be properly typed
|
||||||
const manifest: any = getManifest()
|
const manifest: any = getManifest()
|
||||||
return Object.keys(manifest).flatMap(key => {
|
return Object.keys(manifest).flatMap(key => {
|
||||||
|
@ -123,49 +109,33 @@ export const getHelperCompletions = (mode: {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const snippetAutoComplete = (snippets: Snippet[]) => {
|
export const snippetAutoComplete = (snippets: Snippet[]): BindingCompletion => {
|
||||||
return function myCompletions(context: CompletionContext) {
|
return setAutocomplete(
|
||||||
if (!snippets?.length) {
|
snippets.map(snippet => ({
|
||||||
return null
|
section: buildSectionHeader("snippets", "Snippets", "Code", 100),
|
||||||
}
|
|
||||||
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}`,
|
label: `snippets.${snippet.name}`,
|
||||||
type: "text",
|
displayLabel: snippet.name,
|
||||||
simple: true,
|
}))
|
||||||
apply: (
|
)
|
||||||
view: any,
|
|
||||||
completion: BindingCompletion,
|
|
||||||
from: number,
|
|
||||||
to: number
|
|
||||||
) => {
|
|
||||||
insertSnippet(view, from, to, completion.label)
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const bindingFilter = (options: BindingCompletion[], query: string) => {
|
const bindingFilter = (options: BindingCompletionOption[], query: string) => {
|
||||||
return options.filter(completion => {
|
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 label_parsed = completion.label.toLowerCase()
|
||||||
const query_parsed = query.toLowerCase()
|
const query_parsed = query.toLowerCase()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
section_parsed.includes(query_parsed) ||
|
section_parsed?.includes(query_parsed) ||
|
||||||
label_parsed.includes(query_parsed)
|
label_parsed.includes(query_parsed)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hbAutocomplete = (baseCompletions: BindingCompletion[]) => {
|
export const hbAutocomplete = (
|
||||||
async function coreCompletion(context: CompletionContext) {
|
baseCompletions: BindingCompletionOption[]
|
||||||
|
): BindingCompletion => {
|
||||||
|
function coreCompletion(context: CompletionContext) {
|
||||||
let bindingStart = context.matchBefore(EditorModes.Handlebars.match)
|
let bindingStart = context.matchBefore(EditorModes.Handlebars.match)
|
||||||
|
|
||||||
let options = baseCompletions || []
|
let options = baseCompletions || []
|
||||||
|
@ -191,9 +161,15 @@ export const hbAutocomplete = (baseCompletions: BindingCompletion[]) => {
|
||||||
return coreCompletion
|
return coreCompletion
|
||||||
}
|
}
|
||||||
|
|
||||||
export const jsAutocomplete = (baseCompletions: BindingCompletion[]) => {
|
function wrappedAutocompleteMatch(context: CompletionContext) {
|
||||||
async function coreCompletion(context: CompletionContext) {
|
return context.matchBefore(/\$\("[\s\w]*/)
|
||||||
let jsBinding = context.matchBefore(/\$\("[\s\w]*/)
|
}
|
||||||
|
|
||||||
|
export const jsAutocomplete = (
|
||||||
|
baseCompletions: BindingCompletionOption[]
|
||||||
|
): BindingCompletion => {
|
||||||
|
function coreCompletion(context: CompletionContext) {
|
||||||
|
let jsBinding = wrappedAutocompleteMatch(context)
|
||||||
let options = baseCompletions || []
|
let options = baseCompletions || []
|
||||||
|
|
||||||
if (jsBinding) {
|
if (jsBinding) {
|
||||||
|
@ -217,10 +193,42 @@ export const jsAutocomplete = (baseCompletions: BindingCompletion[]) => {
|
||||||
return coreCompletion
|
return coreCompletion
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildBindingInfoNode = (
|
export const jsHelperAutocomplete = (
|
||||||
completion: BindingCompletion,
|
baseCompletions: BindingCompletionOption[]
|
||||||
binding: any
|
): 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) {
|
if (!binding.valueHTML || binding.value == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -278,18 +286,28 @@ export function jsInsert(
|
||||||
return parsedInsert
|
return parsedInsert
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const enum AutocompleteType {
|
||||||
|
BINDING,
|
||||||
|
HELPER,
|
||||||
|
TEXT,
|
||||||
|
}
|
||||||
|
|
||||||
// Autocomplete apply behaviour
|
// Autocomplete apply behaviour
|
||||||
export const insertBinding = (
|
const insertBinding = (
|
||||||
view: any,
|
view: EditorView,
|
||||||
from: number,
|
from: number,
|
||||||
to: number,
|
to: number,
|
||||||
text: string,
|
text: string,
|
||||||
mode: { name: "javascript" | "handlebars" }
|
mode: { name: "javascript" | "handlebars" },
|
||||||
|
type: AutocompleteType
|
||||||
) => {
|
) => {
|
||||||
let parsedInsert
|
let parsedInsert
|
||||||
|
|
||||||
if (mode.name == "javascript") {
|
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") {
|
} else if (mode.name == "handlebars") {
|
||||||
parsedInsert = hbInsert(view.state.doc?.toString(), from, to, text)
|
parsedInsert = hbInsert(view.state.doc?.toString(), from, to, text)
|
||||||
} else {
|
} 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
|
// TODO: typing in this function isn't great
|
||||||
export const bindingsToCompletions = (
|
export const bindingsToCompletions = (
|
||||||
bindings: any,
|
bindings: any,
|
||||||
mode: { name: "javascript" | "handlebars" }
|
mode: { name: "javascript" | "handlebars" }
|
||||||
) => {
|
): BindingCompletionOption[] => {
|
||||||
const bindingByCategory = groupBy(bindings, "category")
|
const bindingByCategory = groupBy(bindings, "category")
|
||||||
const categoryMeta = bindings?.reduce((acc: any, ele: any) => {
|
const categoryMeta = bindings?.reduce((acc: any, ele: any) => {
|
||||||
acc[ele.category] = acc[ele.category] || {}
|
acc[ele.category] = acc[ele.category] || {}
|
||||||
|
@ -356,8 +355,9 @@ export const bindingsToCompletions = (
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
const completions = Object.keys(bindingByCategory).reduce(
|
const completions = Object.keys(bindingByCategory).reduce<
|
||||||
(comps: any, catKey: string) => {
|
BindingCompletionOption[]
|
||||||
|
>((comps, catKey) => {
|
||||||
const { icon, rank } = categoryMeta[catKey] || {}
|
const { icon, rank } = categoryMeta[catKey] || {}
|
||||||
|
|
||||||
const bindingSectionHeader = buildSectionHeader(
|
const bindingSectionHeader = buildSectionHeader(
|
||||||
|
@ -368,34 +368,41 @@ export const bindingsToCompletions = (
|
||||||
typeof rank == "number" ? rank : 1
|
typeof rank == "number" ? rank : 1
|
||||||
)
|
)
|
||||||
|
|
||||||
return [
|
comps.push(
|
||||||
...comps,
|
...bindingByCategory[catKey].reduce<BindingCompletionOption[]>(
|
||||||
...bindingByCategory[catKey].reduce((acc, binding) => {
|
(acc, binding) => {
|
||||||
let displayType = binding.fieldSchema?.type || binding.display?.type
|
let displayType = binding.fieldSchema?.type || binding.display?.type
|
||||||
acc.push({
|
acc.push({
|
||||||
label:
|
label:
|
||||||
binding.display?.name || binding.readableBinding || "NO NAME",
|
binding.display?.name || binding.readableBinding || "NO NAME",
|
||||||
info: (completion: BindingCompletion) => {
|
info: () => buildBindingInfoNode(binding),
|
||||||
return buildBindingInfoNode(completion, binding)
|
|
||||||
},
|
|
||||||
type: "binding",
|
type: "binding",
|
||||||
detail: displayType,
|
detail: displayType,
|
||||||
section: bindingSectionHeader,
|
section: bindingSectionHeader,
|
||||||
apply: (
|
apply: (
|
||||||
view: any,
|
view: EditorView,
|
||||||
completion: BindingCompletion,
|
_completion: BindingCompletionOption,
|
||||||
from: number,
|
from: number,
|
||||||
to: number
|
to: number
|
||||||
) => {
|
) => {
|
||||||
insertBinding(view, from, to, binding.readableBinding, mode)
|
insertBinding(
|
||||||
|
view,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
binding.readableBinding,
|
||||||
|
mode,
|
||||||
|
AutocompleteType.BINDING
|
||||||
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return acc
|
return acc
|
||||||
}, []),
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return comps
|
||||||
|
}, [])
|
||||||
|
|
||||||
return completions
|
return completions
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
snippetAutoComplete,
|
snippetAutoComplete,
|
||||||
EditorModes,
|
EditorModes,
|
||||||
bindingsToCompletions,
|
bindingsToCompletions,
|
||||||
|
jsHelperAutocomplete,
|
||||||
} from "../CodeEditor"
|
} from "../CodeEditor"
|
||||||
import BindingSidePanel from "./BindingSidePanel.svelte"
|
import BindingSidePanel from "./BindingSidePanel.svelte"
|
||||||
import EvaluationSidePanel from "./EvaluationSidePanel.svelte"
|
import EvaluationSidePanel from "./EvaluationSidePanel.svelte"
|
||||||
|
@ -34,7 +35,6 @@
|
||||||
import { BindingMode, SidePanel } from "@budibase/types"
|
import { BindingMode, SidePanel } from "@budibase/types"
|
||||||
import type {
|
import type {
|
||||||
EnrichedBinding,
|
EnrichedBinding,
|
||||||
BindingCompletion,
|
|
||||||
Snippet,
|
Snippet,
|
||||||
Helper,
|
Helper,
|
||||||
CaretPositionFn,
|
CaretPositionFn,
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
JSONValue,
|
JSONValue,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import type { Log } from "@budibase/string-templates"
|
import type { Log } from "@budibase/string-templates"
|
||||||
import type { CompletionContext } from "@codemirror/autocomplete"
|
import type { BindingCompletion, BindingCompletionOption } from "@/types"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -91,7 +91,10 @@
|
||||||
$: bindingCompletions = bindingsToCompletions(enrichedBindings, editorMode)
|
$: bindingCompletions = bindingsToCompletions(enrichedBindings, editorMode)
|
||||||
$: bindingHelpers = new BindingHelpers(getCaretPosition, insertAtPos)
|
$: bindingHelpers = new BindingHelpers(getCaretPosition, insertAtPos)
|
||||||
$: hbsCompletions = getHBSCompletions(bindingCompletions)
|
$: hbsCompletions = getHBSCompletions(bindingCompletions)
|
||||||
$: jsCompletions = getJSCompletions(bindingCompletions, snippets, useSnippets)
|
$: jsCompletions = getJSCompletions(bindingCompletions, snippets, {
|
||||||
|
useHelpers: allowHelpers,
|
||||||
|
useSnippets,
|
||||||
|
})
|
||||||
$: {
|
$: {
|
||||||
// Ensure a valid side panel option is always selected
|
// Ensure a valid side panel option is always selected
|
||||||
if (sidePanel && !sidePanelOptions.includes(sidePanel)) {
|
if (sidePanel && !sidePanelOptions.includes(sidePanel)) {
|
||||||
|
@ -99,7 +102,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getHBSCompletions = (bindingCompletions: BindingCompletion[]) => {
|
const getHBSCompletions = (bindingCompletions: BindingCompletionOption[]) => {
|
||||||
return [
|
return [
|
||||||
hbAutocomplete([
|
hbAutocomplete([
|
||||||
...bindingCompletions,
|
...bindingCompletions,
|
||||||
|
@ -109,17 +112,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const getJSCompletions = (
|
const getJSCompletions = (
|
||||||
bindingCompletions: BindingCompletion[],
|
bindingCompletions: BindingCompletionOption[],
|
||||||
snippets: Snippet[] | null,
|
snippets: Snippet[] | null,
|
||||||
useSnippets?: boolean
|
config: {
|
||||||
|
useHelpers: boolean
|
||||||
|
useSnippets: boolean
|
||||||
|
}
|
||||||
) => {
|
) => {
|
||||||
const completions: ((_: CompletionContext) => any)[] = [
|
const completions: BindingCompletion[] = []
|
||||||
jsAutocomplete([
|
if (bindingCompletions.length) {
|
||||||
...bindingCompletions,
|
completions.push(jsAutocomplete([...bindingCompletions]))
|
||||||
...getHelperCompletions(EditorModes.JS),
|
}
|
||||||
]),
|
if (config.useHelpers) {
|
||||||
]
|
completions.push(
|
||||||
if (useSnippets && snippets) {
|
jsHelperAutocomplete([...getHelperCompletions(EditorModes.JS)])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (config.useSnippets && snippets) {
|
||||||
completions.push(snippetAutoComplete(snippets))
|
completions.push(snippetAutoComplete(snippets))
|
||||||
}
|
}
|
||||||
return completions
|
return completions
|
||||||
|
@ -381,7 +390,7 @@
|
||||||
autofocus={autofocusEditor}
|
autofocus={autofocusEditor}
|
||||||
placeholder={placeholder ||
|
placeholder={placeholder ||
|
||||||
"Add bindings by typing $ or use the menu on the right"}
|
"Add bindings by typing $ or use the menu on the right"}
|
||||||
jsBindingWrapping
|
jsBindingWrapping={bindingCompletions.length > 0}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -118,6 +118,7 @@
|
||||||
allowHBS={false}
|
allowHBS={false}
|
||||||
allowJS
|
allowJS
|
||||||
allowSnippets={false}
|
allowSnippets={false}
|
||||||
|
allowHelpers={false}
|
||||||
showTabBar={false}
|
showTabBar={false}
|
||||||
placeholder="return function(input) ❴ ... ❵"
|
placeholder="return function(input) ❴ ... ❵"
|
||||||
value={code}
|
value={code}
|
||||||
|
|
|
@ -13,6 +13,11 @@
|
||||||
import { fly } from "svelte/transition"
|
import { fly } from "svelte/transition"
|
||||||
import { findComponentPath } from "@/helpers/components"
|
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 searchString
|
||||||
let searchRef
|
let searchRef
|
||||||
let selectedIndex
|
let selectedIndex
|
||||||
|
@ -217,7 +222,8 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const onDragStart = component => {
|
const onDragStart = (e, component) => {
|
||||||
|
e.dataTransfer.setDragImage(ghost, 0, 0)
|
||||||
previewStore.startDrag(component)
|
previewStore.startDrag(component)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,13 +256,12 @@
|
||||||
{#each category.children as component}
|
{#each category.children as component}
|
||||||
<div
|
<div
|
||||||
draggable="true"
|
draggable="true"
|
||||||
on:dragstart={() => onDragStart(component.component)}
|
on:dragstart={e => onDragStart(e, component.component)}
|
||||||
on:dragend={onDragEnd}
|
on:dragend={onDragEnd}
|
||||||
class="component"
|
class="component"
|
||||||
class:selected={selectedIndex === orderMap[component.component]}
|
class:selected={selectedIndex === orderMap[component.component]}
|
||||||
on:click={() => addComponent(component.component)}
|
on:click={() => addComponent(component.component)}
|
||||||
on:mouseover={() => (selectedIndex = null)}
|
on:mouseenter={() => (selectedIndex = null)}
|
||||||
on:focus
|
|
||||||
>
|
>
|
||||||
<Icon name={component.icon} />
|
<Icon name={component.icon} />
|
||||||
<Body size="XS">{component.name}</Body>
|
<Body size="XS">{component.name}</Body>
|
||||||
|
@ -308,7 +313,6 @@
|
||||||
}
|
}
|
||||||
.component:hover {
|
.component:hover {
|
||||||
background: var(--spectrum-global-color-gray-300);
|
background: var(--spectrum-global-color-gray-300);
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
.component :global(.spectrum-Body) {
|
.component :global(.spectrum-Body) {
|
||||||
line-height: 1.2 !important;
|
line-height: 1.2 !important;
|
||||||
|
|
|
@ -189,8 +189,8 @@
|
||||||
} else if (type === "reload-plugin") {
|
} else if (type === "reload-plugin") {
|
||||||
await componentStore.refreshDefinitions()
|
await componentStore.refreshDefinitions()
|
||||||
} else if (type === "drop-new-component") {
|
} else if (type === "drop-new-component") {
|
||||||
const { component, parent, index } = data
|
const { component, parent, index, props } = data
|
||||||
await componentStore.create(component, null, parent, index)
|
await componentStore.create(component, props, parent, index)
|
||||||
} else if (type === "add-parent-component") {
|
} else if (type === "add-parent-component") {
|
||||||
const { componentId, parentType } = data
|
const { componentId, parentType } = data
|
||||||
await componentStore.addParent(componentId, parentType)
|
await componentStore.addParent(componentId, parentType)
|
||||||
|
|
|
@ -39,7 +39,7 @@ interface AppMetaState {
|
||||||
appInstance: { _id: string } | null
|
appInstance: { _id: string } | null
|
||||||
initialised: boolean
|
initialised: boolean
|
||||||
hasAppPackage: boolean
|
hasAppPackage: boolean
|
||||||
usedPlugins: Plugin[] | null
|
usedPlugins: Plugin[]
|
||||||
automations: AutomationSettings
|
automations: AutomationSettings
|
||||||
routes: { [key: string]: any }
|
routes: { [key: string]: any }
|
||||||
version?: string
|
version?: string
|
||||||
|
@ -76,7 +76,7 @@ export const INITIAL_APP_META_STATE: AppMetaState = {
|
||||||
appInstance: null,
|
appInstance: null,
|
||||||
initialised: false,
|
initialised: false,
|
||||||
hasAppPackage: false,
|
hasAppPackage: false,
|
||||||
usedPlugins: null,
|
usedPlugins: [],
|
||||||
automations: {},
|
automations: {},
|
||||||
routes: {},
|
routes: {},
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ export class AppMetaStore extends BudiStore<AppMetaState> {
|
||||||
appInstance: app.instance,
|
appInstance: app.instance,
|
||||||
revertableVersion: app.revertableVersion,
|
revertableVersion: app.revertableVersion,
|
||||||
upgradableVersion: app.upgradableVersion,
|
upgradableVersion: app.upgradableVersion,
|
||||||
usedPlugins: app.usedPlugins || null,
|
usedPlugins: app.usedPlugins || [],
|
||||||
icon: app.icon,
|
icon: app.icon,
|
||||||
features: {
|
features: {
|
||||||
...INITIAL_APP_META_STATE.features,
|
...INITIAL_APP_META_STATE.features,
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
findComponentParent,
|
findComponentParent,
|
||||||
findAllMatchingComponents,
|
findAllMatchingComponents,
|
||||||
makeComponentUnique,
|
makeComponentUnique,
|
||||||
|
findComponentType,
|
||||||
} from "@/helpers/components"
|
} from "@/helpers/components"
|
||||||
import { getComponentFieldOptions } from "@/helpers/formFields"
|
import { getComponentFieldOptions } from "@/helpers/formFields"
|
||||||
import { selectedScreen } from "./screens"
|
import { selectedScreen } from "./screens"
|
||||||
|
@ -139,10 +140,6 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the component definition object
|
* Retrieve the component definition object
|
||||||
* @param {string} componentType
|
|
||||||
* @example
|
|
||||||
* '@budibase/standard-components/container'
|
|
||||||
* @returns {object}
|
|
||||||
*/
|
*/
|
||||||
getDefinition(componentType: string) {
|
getDefinition(componentType: string) {
|
||||||
if (!componentType) {
|
if (!componentType) {
|
||||||
|
@ -151,10 +148,6 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
return get(this.store).components[componentType]
|
return get(this.store).components[componentType]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @returns {object}
|
|
||||||
*/
|
|
||||||
getDefaultDatasource() {
|
getDefaultDatasource() {
|
||||||
// Ignore users table
|
// Ignore users table
|
||||||
const validTables = get(tables).list.filter(x => x._id !== "ta_users")
|
const validTables = get(tables).list.filter(x => x._id !== "ta_users")
|
||||||
|
@ -188,8 +181,6 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
/**
|
/**
|
||||||
* Takes an enriched component instance and applies any required migration
|
* Takes an enriched component instance and applies any required migration
|
||||||
* logic
|
* logic
|
||||||
* @param {object} enrichedComponent
|
|
||||||
* @returns {object} migrated Component
|
|
||||||
*/
|
*/
|
||||||
migrateSettings(enrichedComponent: Component) {
|
migrateSettings(enrichedComponent: Component) {
|
||||||
const componentPrefix = "@budibase/standard-components"
|
const componentPrefix = "@budibase/standard-components"
|
||||||
|
@ -230,22 +221,15 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
for (let setting of filterableTypes || []) {
|
for (let setting of filterableTypes || []) {
|
||||||
const isLegacy = Array.isArray(enrichedComponent[setting.key])
|
const isLegacy = Array.isArray(enrichedComponent[setting.key])
|
||||||
if (isLegacy) {
|
if (isLegacy) {
|
||||||
const processedSetting = utils.processSearchFilters(
|
enrichedComponent[setting.key] = utils.processSearchFilters(
|
||||||
enrichedComponent[setting.key]
|
enrichedComponent[setting.key]
|
||||||
)
|
)
|
||||||
enrichedComponent[setting.key] = processedSetting
|
|
||||||
migrated = true
|
migrated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return migrated
|
return migrated
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {object} component
|
|
||||||
* @param {object} opts
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
enrichEmptySettings(
|
enrichEmptySettings(
|
||||||
component: Component,
|
component: Component,
|
||||||
opts: { screen?: Screen; parent?: Component; useDefaultValues?: boolean }
|
opts: { screen?: Screen; parent?: Component; useDefaultValues?: boolean }
|
||||||
|
@ -280,14 +264,25 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
type: "table",
|
type: "table",
|
||||||
}
|
}
|
||||||
} else if (setting.type === "dataProvider") {
|
} 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 path = findComponentPath(screen.props, treeId)
|
||||||
const providers = path.filter((component: Component) =>
|
const providers = path.filter((component: Component) =>
|
||||||
component._component?.endsWith("/dataprovider")
|
component._component?.endsWith("/dataprovider")
|
||||||
)
|
)
|
||||||
if (providers.length) {
|
providerId = providers[providers.length - 1]?._id
|
||||||
const id = providers[providers.length - 1]?._id
|
|
||||||
component[setting.key] = `{{ literal ${safe(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/")) {
|
} else if (setting.type.startsWith("field/")) {
|
||||||
// Autofill form field names
|
// Autofill form field names
|
||||||
|
@ -427,17 +422,10 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {string} componentName
|
|
||||||
* @param {object} presetProps
|
|
||||||
* @param {object} parent
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
createInstance(
|
createInstance(
|
||||||
componentType: string,
|
componentType: string,
|
||||||
presetProps: any,
|
presetProps?: Record<string, any>,
|
||||||
parent: any
|
parent?: Component
|
||||||
): Component | null {
|
): Component | null {
|
||||||
const screen = get(selectedScreen)
|
const screen = get(selectedScreen)
|
||||||
if (!screen) {
|
if (!screen) {
|
||||||
|
@ -463,7 +451,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
_id: Helpers.uuid(),
|
_id: Helpers.uuid(),
|
||||||
_component: definition.component,
|
_component: definition.component,
|
||||||
_styles: {
|
_styles: {
|
||||||
normal: {},
|
normal: { ...presetProps?._styles?.normal },
|
||||||
hover: {},
|
hover: {},
|
||||||
active: {},
|
active: {},
|
||||||
},
|
},
|
||||||
|
@ -512,19 +500,11 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {string} componentName
|
|
||||||
* @param {object} presetProps
|
|
||||||
* @param {object} parent
|
|
||||||
* @param {number} index
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async create(
|
async create(
|
||||||
componentType: string,
|
componentType: string,
|
||||||
presetProps: any,
|
presetProps?: Record<string, any>,
|
||||||
parent: Component,
|
parent?: Component,
|
||||||
index: number
|
index?: number
|
||||||
) {
|
) {
|
||||||
const state = get(this.store)
|
const state = get(this.store)
|
||||||
const componentInstance = this.createInstance(
|
const componentInstance = this.createInstance(
|
||||||
|
@ -611,13 +591,6 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
return componentInstance
|
return componentInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {function} patchFn
|
|
||||||
* @param {string} componentId
|
|
||||||
* @param {string} screenId
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async patch(
|
async patch(
|
||||||
patchFn: (component: Component, screen: Screen) => any,
|
patchFn: (component: Component, screen: Screen) => any,
|
||||||
componentId?: string,
|
componentId?: string,
|
||||||
|
@ -652,11 +625,6 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
await screenStore.patch(patchScreen, screenId)
|
await screenStore.patch(patchScreen, screenId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {object} component
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async delete(component: Component) {
|
async delete(component: Component) {
|
||||||
if (!component) {
|
if (!component) {
|
||||||
return
|
return
|
||||||
|
@ -737,13 +705,6 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {object} targetComponent
|
|
||||||
* @param {string} mode
|
|
||||||
* @param {object} targetScreen
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async paste(
|
async paste(
|
||||||
targetComponent: Component,
|
targetComponent: Component,
|
||||||
mode: string,
|
mode: string,
|
||||||
|
@ -1101,6 +1062,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
|
|
||||||
async updateStyles(styles: Record<string, string>, id: string) {
|
async updateStyles(styles: Record<string, string>, id: string) {
|
||||||
const patchFn = (component: Component) => {
|
const patchFn = (component: Component) => {
|
||||||
|
delete component._placeholder
|
||||||
component._styles.normal = {
|
component._styles.normal = {
|
||||||
...component._styles.normal,
|
...component._styles.normal,
|
||||||
...styles,
|
...styles,
|
||||||
|
@ -1231,7 +1193,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new parent instance
|
// Create new parent instance
|
||||||
const newParentDefinition = this.createInstance(parentType, null, parent)
|
const newParentDefinition = this.createInstance(parentType)
|
||||||
if (!newParentDefinition) {
|
if (!newParentDefinition) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1267,10 +1229,6 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the components settings have been cached
|
* Check if the components settings have been cached
|
||||||
* @param {string} componentType
|
|
||||||
* @example
|
|
||||||
* '@budibase/standard-components/container'
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
*/
|
||||||
isCached(componentType: string) {
|
isCached(componentType: string) {
|
||||||
const settings = get(this.store).settingsCache
|
const settings = get(this.store).settingsCache
|
||||||
|
@ -1279,11 +1237,6 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache component settings
|
* Cache component settings
|
||||||
* @param {string} componentType
|
|
||||||
* @param {object} definition
|
|
||||||
* @example
|
|
||||||
* '@budibase/standard-components/container'
|
|
||||||
* @returns {array} the settings
|
|
||||||
*/
|
*/
|
||||||
cacheSettings(componentType: string, definition: ComponentDefinition | null) {
|
cacheSettings(componentType: string, definition: ComponentDefinition | null) {
|
||||||
let settings: ComponentSetting[] = []
|
let settings: ComponentSetting[] = []
|
||||||
|
@ -1313,12 +1266,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
/**
|
/**
|
||||||
* Retrieve an array of the component settings.
|
* Retrieve an array of the component settings.
|
||||||
* These settings are cached because they cannot change at run time.
|
* These settings are cached because they cannot change at run time.
|
||||||
*
|
|
||||||
* Searches a component's definition for a setting matching a certain predicate.
|
* Searches a component's definition for a setting matching a certain predicate.
|
||||||
* @param {string} componentType
|
|
||||||
* @example
|
|
||||||
* '@budibase/standard-components/container'
|
|
||||||
* @returns {Array<object>}
|
|
||||||
*/
|
*/
|
||||||
getComponentSettings(componentType: string) {
|
getComponentSettings(componentType: string) {
|
||||||
if (!componentType) {
|
if (!componentType) {
|
||||||
|
|
|
@ -4,15 +4,13 @@ import { appStore } from "@/stores/builder"
|
||||||
import { BudiStore } from "../BudiStore"
|
import { BudiStore } from "../BudiStore"
|
||||||
import { AppNavigation, AppNavigationLink, UIObject } from "@budibase/types"
|
import { AppNavigation, AppNavigationLink, UIObject } from "@budibase/types"
|
||||||
|
|
||||||
interface BuilderNavigationStore extends AppNavigation {}
|
|
||||||
|
|
||||||
export const INITIAL_NAVIGATION_STATE = {
|
export const INITIAL_NAVIGATION_STATE = {
|
||||||
navigation: "Top",
|
navigation: "Top",
|
||||||
links: [],
|
links: [],
|
||||||
textAlign: "Left",
|
textAlign: "Left",
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NavigationStore extends BudiStore<BuilderNavigationStore> {
|
export class NavigationStore extends BudiStore<AppNavigation> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(INITIAL_NAVIGATION_STATE)
|
super(INITIAL_NAVIGATION_STATE)
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ export class PreviewStore extends BudiStore<PreviewState> {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
startDrag(component: any) {
|
async startDrag(component: string) {
|
||||||
this.sendEvent("dragging-new-component", {
|
this.sendEvent("dragging-new-component", {
|
||||||
dragging: true,
|
dragging: true,
|
||||||
component,
|
component,
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { CompletionContext, Completion } from "@codemirror/autocomplete"
|
||||||
|
|
||||||
|
export type BindingCompletion = (context: CompletionContext) => {
|
||||||
|
from: number
|
||||||
|
options: Completion[]
|
||||||
|
} | null
|
||||||
|
|
||||||
|
export type BindingCompletionOption = Completion
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./bindings"
|
|
@ -3,6 +3,7 @@ import fs from "fs"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import { TEMP_DIR, MINIO_DIR } from "./utils"
|
import { TEMP_DIR, MINIO_DIR } from "./utils"
|
||||||
import { progressBar } from "../utils"
|
import { progressBar } from "../utils"
|
||||||
|
import * as stream from "node:stream"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ObjectStoreBuckets,
|
ObjectStoreBuckets,
|
||||||
|
@ -20,15 +21,21 @@ export async function exportObjects() {
|
||||||
let fullList: any[] = []
|
let fullList: any[] = []
|
||||||
let errorCount = 0
|
let errorCount = 0
|
||||||
for (let bucket of bucketList) {
|
for (let bucket of bucketList) {
|
||||||
const client = ObjectStore(bucket)
|
const client = ObjectStore()
|
||||||
try {
|
try {
|
||||||
await client.headBucket().promise()
|
await client.headBucket({
|
||||||
|
Bucket: bucket,
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorCount++
|
errorCount++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const list = (await client.listObjectsV2().promise()) as { Contents: any[] }
|
const list = await client.listObjectsV2({
|
||||||
fullList = fullList.concat(list.Contents.map(el => ({ ...el, bucket })))
|
Bucket: bucket,
|
||||||
|
})
|
||||||
|
fullList = fullList.concat(
|
||||||
|
list.Contents?.map(el => ({ ...el, bucket })) || []
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (errorCount === bucketList.length) {
|
if (errorCount === bucketList.length) {
|
||||||
throw new Error("Unable to access MinIO/S3 - check environment config.")
|
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)
|
const dirs = possiblePath.slice(0, possiblePath.length - 1)
|
||||||
fs.mkdirSync(join(path, object.bucket, ...dirs), { recursive: true })
|
fs.mkdirSync(join(path, object.bucket, ...dirs), { recursive: true })
|
||||||
}
|
}
|
||||||
|
if (data instanceof stream.Readable) {
|
||||||
|
data.pipe(
|
||||||
|
fs.createWriteStream(join(path, object.bucket, ...possiblePath))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
fs.writeFileSync(join(path, object.bucket, ...possiblePath), data)
|
fs.writeFileSync(join(path, object.bucket, ...possiblePath), data)
|
||||||
|
}
|
||||||
bar.update(++count)
|
bar.update(++count)
|
||||||
}
|
}
|
||||||
bar.stop()
|
bar.stop()
|
||||||
|
@ -60,7 +73,7 @@ export async function importObjects() {
|
||||||
const bar = progressBar(total)
|
const bar = progressBar(total)
|
||||||
let count = 0
|
let count = 0
|
||||||
for (let bucket of buckets) {
|
for (let bucket of buckets) {
|
||||||
const client = ObjectStore(bucket)
|
const client = ObjectStore()
|
||||||
await createBucketIfNotExists(client, bucket)
|
await createBucketIfNotExists(client, bucket)
|
||||||
const files = await uploadDirectory(bucket, join(path, bucket), "/")
|
const files = await uploadDirectory(bucket, join(path, bucket), "/")
|
||||||
count += files.length
|
count += files.length
|
||||||
|
|
|
@ -1455,7 +1455,8 @@
|
||||||
"type": "icon",
|
"type": "icon",
|
||||||
"label": "Icon",
|
"label": "Icon",
|
||||||
"key": "icon",
|
"key": "icon",
|
||||||
"required": true
|
"required": true,
|
||||||
|
"defaultValue": "ri-star-fill"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "select",
|
"type": "select",
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./dist/budibase-client.js",
|
"import": "./dist/budibase-client.js",
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import { createAPIClient } from "@budibase/frontend-core"
|
import { createAPIClient } from "@budibase/frontend-core"
|
||||||
import { authStore } from "../stores/auth"
|
import { authStore } from "../stores/auth"
|
||||||
import {
|
import { notificationStore, devToolsEnabled, devToolsStore } from "../stores"
|
||||||
notificationStore,
|
|
||||||
devToolsEnabled,
|
|
||||||
devToolsStore,
|
|
||||||
} from "../stores/index"
|
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
export const API = createAPIClient({
|
export const API = createAPIClient({
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, onDestroy, onMount, setContext } from "svelte"
|
import { getContext, onDestroy, onMount, setContext } from "svelte"
|
||||||
import { builderStore } from "stores/builder.js"
|
import { builderStore } from "@/stores/builder.js"
|
||||||
import { blockStore } from "stores/blocks"
|
import { blockStore } from "@/stores/blocks"
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { styleable } = getContext("sdk")
|
const { styleable } = getContext("sdk")
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { getContext, onDestroy } from "svelte"
|
import { getContext, onDestroy } from "svelte"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import { builderStore } from "../stores/builder.js"
|
import { builderStore } from "../stores/builder.js"
|
||||||
import Component from "components/Component.svelte"
|
import Component from "@/components/Component.svelte"
|
||||||
|
|
||||||
export let type
|
export let type
|
||||||
export let props
|
export let props
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import { Constants, CookieUtils } from "@budibase/frontend-core"
|
import { Constants, CookieUtils } from "@budibase/frontend-core"
|
||||||
import { getThemeClassNames } from "@budibase/shared-core"
|
import { getThemeClassNames } from "@budibase/shared-core"
|
||||||
import Component from "./Component.svelte"
|
import Component from "./Component.svelte"
|
||||||
import SDK from "sdk"
|
import SDK from "@/sdk"
|
||||||
import {
|
import {
|
||||||
featuresStore,
|
featuresStore,
|
||||||
createContextStore,
|
createContextStore,
|
||||||
|
@ -22,28 +22,29 @@
|
||||||
environmentStore,
|
environmentStore,
|
||||||
sidePanelStore,
|
sidePanelStore,
|
||||||
modalStore,
|
modalStore,
|
||||||
} from "stores"
|
} from "@/stores"
|
||||||
import NotificationDisplay from "components/overlay/NotificationDisplay.svelte"
|
import NotificationDisplay from "./overlay/NotificationDisplay.svelte"
|
||||||
import ConfirmationDisplay from "components/overlay/ConfirmationDisplay.svelte"
|
import ConfirmationDisplay from "./overlay/ConfirmationDisplay.svelte"
|
||||||
import PeekScreenDisplay from "components/overlay/PeekScreenDisplay.svelte"
|
import PeekScreenDisplay from "./overlay/PeekScreenDisplay.svelte"
|
||||||
import UserBindingsProvider from "components/context/UserBindingsProvider.svelte"
|
import UserBindingsProvider from "./context/UserBindingsProvider.svelte"
|
||||||
import DeviceBindingsProvider from "components/context/DeviceBindingsProvider.svelte"
|
import DeviceBindingsProvider from "./context/DeviceBindingsProvider.svelte"
|
||||||
import StateBindingsProvider from "components/context/StateBindingsProvider.svelte"
|
import StateBindingsProvider from "./context/StateBindingsProvider.svelte"
|
||||||
import RowSelectionProvider from "components/context/RowSelectionProvider.svelte"
|
import RowSelectionProvider from "./context/RowSelectionProvider.svelte"
|
||||||
import QueryParamsProvider from "components/context/QueryParamsProvider.svelte"
|
import QueryParamsProvider from "./context/QueryParamsProvider.svelte"
|
||||||
import SettingsBar from "components/preview/SettingsBar.svelte"
|
import SettingsBar from "./preview/SettingsBar.svelte"
|
||||||
import SelectionIndicator from "components/preview/SelectionIndicator.svelte"
|
import SelectionIndicator from "./preview/SelectionIndicator.svelte"
|
||||||
import HoverIndicator from "components/preview/HoverIndicator.svelte"
|
import HoverIndicator from "./preview/HoverIndicator.svelte"
|
||||||
import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
|
import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
|
||||||
import DNDHandler from "components/preview/DNDHandler.svelte"
|
import DNDHandler from "./preview/DNDHandler.svelte"
|
||||||
import GridDNDHandler from "components/preview/GridDNDHandler.svelte"
|
import GridDNDHandler from "./preview/GridDNDHandler.svelte"
|
||||||
import KeyboardManager from "components/preview/KeyboardManager.svelte"
|
import KeyboardManager from "./preview/KeyboardManager.svelte"
|
||||||
import DevToolsHeader from "components/devtools/DevToolsHeader.svelte"
|
import DevToolsHeader from "./devtools/DevToolsHeader.svelte"
|
||||||
import DevTools from "components/devtools/DevTools.svelte"
|
import DevTools from "./devtools/DevTools.svelte"
|
||||||
import FreeFooter from "components/FreeFooter.svelte"
|
import FreeFooter from "./FreeFooter.svelte"
|
||||||
import MaintenanceScreen from "components/MaintenanceScreen.svelte"
|
import MaintenanceScreen from "./MaintenanceScreen.svelte"
|
||||||
import SnippetsProvider from "./context/SnippetsProvider.svelte"
|
import SnippetsProvider from "./context/SnippetsProvider.svelte"
|
||||||
import EmbedProvider from "./context/EmbedProvider.svelte"
|
import EmbedProvider from "./context/EmbedProvider.svelte"
|
||||||
|
import DNDSelectionIndicators from "./preview/DNDSelectionIndicators.svelte"
|
||||||
|
|
||||||
// Provide contexts
|
// Provide contexts
|
||||||
setContext("sdk", SDK)
|
setContext("sdk", SDK)
|
||||||
|
@ -266,6 +267,7 @@
|
||||||
{#if $builderStore.inBuilder}
|
{#if $builderStore.inBuilder}
|
||||||
<DNDHandler />
|
<DNDHandler />
|
||||||
<GridDNDHandler />
|
<GridDNDHandler />
|
||||||
|
<DNDSelectionIndicators />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</SnippetsProvider>
|
</SnippetsProvider>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, setContext, onMount } from "svelte"
|
import { getContext, setContext, onMount } from "svelte"
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import { enrichProps, propsAreSame } from "utils/componentProps"
|
import { enrichProps, propsAreSame } from "@/utils/componentProps"
|
||||||
import { getSettingsDefinition } from "@budibase/frontend-core"
|
import { getSettingsDefinition } from "@budibase/frontend-core"
|
||||||
import {
|
import {
|
||||||
builderStore,
|
builderStore,
|
||||||
|
@ -20,12 +20,15 @@
|
||||||
appStore,
|
appStore,
|
||||||
dndComponentPath,
|
dndComponentPath,
|
||||||
dndIsDragging,
|
dndIsDragging,
|
||||||
} from "stores"
|
} from "@/stores"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { getActiveConditions, reduceConditionActions } from "utils/conditions"
|
import {
|
||||||
import EmptyPlaceholder from "components/app/EmptyPlaceholder.svelte"
|
getActiveConditions,
|
||||||
import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte"
|
reduceConditionActions,
|
||||||
import ComponentErrorState from "components/error-states/ComponentErrorState.svelte"
|
} from "@/utils/conditions"
|
||||||
|
import EmptyPlaceholder from "@/components/app/EmptyPlaceholder.svelte"
|
||||||
|
import ScreenPlaceholder from "@/components/app/ScreenPlaceholder.svelte"
|
||||||
|
import ComponentErrorState from "@/components/error-states/ComponentErrorState.svelte"
|
||||||
import {
|
import {
|
||||||
decodeJSBinding,
|
decodeJSBinding,
|
||||||
findHBSBlocks,
|
findHBSBlocks,
|
||||||
|
@ -35,7 +38,7 @@
|
||||||
getActionContextKey,
|
getActionContextKey,
|
||||||
getActionDependentContextKeys,
|
getActionDependentContextKeys,
|
||||||
} from "../utils/buttonActions.js"
|
} from "../utils/buttonActions.js"
|
||||||
import { gridLayout } from "utils/grid"
|
import { gridLayout } from "@/utils/grid"
|
||||||
|
|
||||||
export let instance = {}
|
export let instance = {}
|
||||||
export let parent = null
|
export let parent = null
|
||||||
|
@ -120,7 +123,7 @@
|
||||||
$: children = instance._children || []
|
$: children = instance._children || []
|
||||||
$: id = instance._id
|
$: id = instance._id
|
||||||
$: name = isRoot ? "Screen" : instance._instanceName
|
$: name = isRoot ? "Screen" : instance._instanceName
|
||||||
$: icon = definition?.icon
|
$: icon = instance._icon || definition?.icon
|
||||||
|
|
||||||
// Determine if the component is selected or is part of the critical path
|
// Determine if the component is selected or is part of the critical path
|
||||||
// leading to the selected component
|
// leading to the selected component
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { themeStore } from "stores"
|
import { themeStore } from "@/stores"
|
||||||
import { setContext } from "svelte"
|
import { setContext } from "svelte"
|
||||||
import { Context } from "@budibase/bbui"
|
import { Context } from "@budibase/bbui"
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { setContext, getContext, onMount } from "svelte"
|
import { setContext, getContext, onMount } from "svelte"
|
||||||
import Router, { querystring } from "svelte-spa-router"
|
import Router, { querystring } from "svelte-spa-router"
|
||||||
import { routeStore, stateStore } from "stores"
|
import { routeStore, stateStore } from "@/stores"
|
||||||
import Screen from "./Screen.svelte"
|
import Screen from "./Screen.svelte"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { screenStore, routeStore, builderStore } from "stores"
|
import { screenStore, routeStore, builderStore } from "@/stores"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import Component from "./Component.svelte"
|
import Component from "./Component.svelte"
|
||||||
import Provider from "./context/Provider.svelte"
|
import Provider from "./context/Provider.svelte"
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
// because it functions similarly to one
|
// because it functions similarly to one
|
||||||
import { getContext, onMount } from "svelte"
|
import { getContext, onMount } from "svelte"
|
||||||
import { get, derived, readable } from "svelte/store"
|
import { get, derived, readable } from "svelte/store"
|
||||||
import { featuresStore } from "stores"
|
import { featuresStore } from "@/stores"
|
||||||
import { Grid } from "@budibase/frontend-core"
|
import { Grid } from "@budibase/frontend-core"
|
||||||
// import { processStringSync } from "@budibase/string-templates"
|
// import { processStringSync } from "@budibase/string-templates"
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import Block from "components/Block.svelte"
|
import Block from "@/components/Block.svelte"
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "@/components/BlockComponent.svelte"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { enrichSearchColumns, enrichFilter } from "utils/blocks"
|
import { enrichSearchColumns, enrichFilter } from "@/utils/blocks"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import Block from "components/Block.svelte"
|
import Block from "@/components/Block.svelte"
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "@/components/BlockComponent.svelte"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
|
|
||||||
// Datasource
|
// Datasource
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "@/components/BlockComponent.svelte"
|
||||||
import { FieldType } from "@budibase/types"
|
import { FieldType } from "@budibase/types"
|
||||||
|
|
||||||
export let field
|
export let field
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "@/components/BlockComponent.svelte"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { getContext, setContext } from "svelte"
|
import { getContext, setContext } from "svelte"
|
||||||
import { builderStore } from "stores"
|
import { builderStore } from "@/stores"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
import FormBlockWrapper from "./form/FormBlockWrapper.svelte"
|
import FormBlockWrapper from "./form/FormBlockWrapper.svelte"
|
||||||
import { get, writable } from "svelte/store"
|
import { get, writable } from "svelte/store"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "@/components/BlockComponent.svelte"
|
||||||
import Block from "components/Block.svelte"
|
import Block from "@/components/Block.svelte"
|
||||||
import Placeholder from "components/app/Placeholder.svelte"
|
import Placeholder from "@/components/app/Placeholder.svelte"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import Block from "components/Block.svelte"
|
import Block from "@/components/Block.svelte"
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "@/components/BlockComponent.svelte"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "@/components/BlockComponent.svelte"
|
||||||
import Block from "components/Block.svelte"
|
import Block from "@/components/Block.svelte"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "@/components/BlockComponent.svelte"
|
||||||
import Placeholder from "components/app/Placeholder.svelte"
|
import Placeholder from "@/components/app/Placeholder.svelte"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import FormBlockComponent from "../FormBlockComponent.svelte"
|
import FormBlockComponent from "../FormBlockComponent.svelte"
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, onMount } from "svelte"
|
import { getContext, onMount } from "svelte"
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
import { GridRowHeight, GridColumns } from "constants"
|
import { GridRowHeight, GridColumns } from "@/constants"
|
||||||
import { memo } from "@budibase/frontend-core"
|
import { memo } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let onClick
|
export let onClick
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import Block from "components/Block.svelte"
|
import Block from "@/components/Block.svelte"
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "@/components/BlockComponent.svelte"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { enrichSearchColumns, enrichFilter } from "utils/blocks"
|
import { enrichSearchColumns, enrichFilter } from "@/utils/blocks"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import { Table } from "@budibase/bbui"
|
import { Table } from "@budibase/bbui"
|
||||||
import SlotRenderer from "./SlotRenderer.svelte"
|
import SlotRenderer from "./SlotRenderer.svelte"
|
||||||
import { canBeSortColumn } from "@budibase/frontend-core"
|
import { canBeSortColumn } from "@budibase/frontend-core"
|
||||||
import Provider from "components/context/Provider.svelte"
|
import Provider from "@/components/context/Provider.svelte"
|
||||||
|
|
||||||
export let dataProvider
|
export let dataProvider
|
||||||
export let columns
|
export let columns
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import Field from "./Field.svelte"
|
import Field from "./Field.svelte"
|
||||||
import { CoreDropzone, ProgressCircle, Helpers } from "@budibase/bbui"
|
import { CoreDropzone, ProgressCircle, Helpers } from "@budibase/bbui"
|
||||||
import { getContext, onMount, onDestroy } from "svelte"
|
import { getContext, onMount, onDestroy } from "svelte"
|
||||||
import { builderStore } from "stores/builder.js"
|
import { builderStore } from "@/stores/builder.js"
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
|
||||||
export let datasourceId
|
export let datasourceId
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import Provider from "./Provider.svelte"
|
import Provider from "./Provider.svelte"
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
import { themeStore } from "stores"
|
import { themeStore } from "@/stores"
|
||||||
|
|
||||||
let width = window.innerWidth
|
let width = window.innerWidth
|
||||||
let height = window.innerHeight
|
let height = window.innerHeight
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, setContext, onDestroy } from "svelte"
|
import { getContext, setContext, onDestroy } from "svelte"
|
||||||
import { dataSourceStore, createContextStore } from "stores"
|
import { dataSourceStore, createContextStore } from "@/stores"
|
||||||
import { ActionTypes } from "constants"
|
import { ActionTypes } from "@/constants"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
|
|
||||||
const { ContextScopes } = getContext("sdk")
|
const { ContextScopes } = getContext("sdk")
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import Provider from "./Provider.svelte"
|
import Provider from "./Provider.svelte"
|
||||||
import { routeStore } from "stores"
|
import { routeStore } from "@/stores"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Provider key="query" data={$routeStore.queryParams}>
|
<Provider key="query" data={$routeStore.queryParams}>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import Provider from "./Provider.svelte"
|
import Provider from "./Provider.svelte"
|
||||||
import { rowSelectionStore } from "stores"
|
import { rowSelectionStore } from "@/stores"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Provider key="rowSelection" data={$rowSelectionStore}>
|
<Provider key="rowSelection" data={$rowSelectionStore}>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import Provider from "./Provider.svelte"
|
import Provider from "./Provider.svelte"
|
||||||
import { snippets } from "stores"
|
import { snippets } from "@/stores"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Provider key="snippets" data={$snippets}>
|
<Provider key="snippets" data={$snippets}>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import Provider from "./Provider.svelte"
|
import Provider from "./Provider.svelte"
|
||||||
import { stateStore } from "stores"
|
import { stateStore } from "@/stores"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Provider key="state" data={$stateStore}>
|
<Provider key="state" data={$stateStore}>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import Provider from "./Provider.svelte"
|
import Provider from "./Provider.svelte"
|
||||||
import { authStore, currentRole } from "stores"
|
import { authStore, currentRole } from "@/stores"
|
||||||
import { ActionTypes } from "constants"
|
import { ActionTypes } from "@/constants"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
|
||||||
// Register this as a refreshable datasource so that user changes cause
|
// Register this as a refreshable datasource so that user changes cause
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import { Layout, Heading, Tabs, Tab, Icon } from "@budibase/bbui"
|
import { Layout, Heading, Tabs, Tab, Icon } from "@budibase/bbui"
|
||||||
import DevToolsStatsTab from "./DevToolsStatsTab.svelte"
|
import DevToolsStatsTab from "./DevToolsStatsTab.svelte"
|
||||||
import DevToolsComponentTab from "./DevToolsComponentTab.svelte"
|
import DevToolsComponentTab from "./DevToolsComponentTab.svelte"
|
||||||
import { devToolsStore } from "stores"
|
import { devToolsStore } from "@/stores"
|
||||||
|
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { Layout, Select, Body } from "@budibase/bbui"
|
import { Layout, Select, Body } from "@budibase/bbui"
|
||||||
import { componentStore } from "stores/index.js"
|
import { componentStore } from "@/stores"
|
||||||
import DevToolsStat from "./DevToolsStat.svelte"
|
import DevToolsStat from "./DevToolsStat.svelte"
|
||||||
|
|
||||||
const ReadableBindingMap = {
|
const ReadableBindingMap = {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { Layout, Toggle } from "@budibase/bbui"
|
import { Layout, Toggle } from "@budibase/bbui"
|
||||||
import { getSettingsDefinition } from "@budibase/frontend-core"
|
import { getSettingsDefinition } from "@budibase/frontend-core"
|
||||||
import DevToolsStat from "./DevToolsStat.svelte"
|
import DevToolsStat from "./DevToolsStat.svelte"
|
||||||
import { componentStore } from "stores/index.js"
|
import { componentStore } from "@/stores"
|
||||||
|
|
||||||
let showEnrichedSettings = true
|
let showEnrichedSettings = true
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { Body, Layout, Heading, Button, Tabs, Tab } from "@budibase/bbui"
|
import { Body, Layout, Heading, Button, Tabs, Tab } from "@budibase/bbui"
|
||||||
import { builderStore, devToolsStore, componentStore } from "stores"
|
import { builderStore, devToolsStore, componentStore } from "@/stores"
|
||||||
import DevToolsStat from "./DevToolsStat.svelte"
|
import DevToolsStat from "./DevToolsStat.svelte"
|
||||||
import DevToolsComponentSettingsTab from "./DevToolsComponentSettingsTab.svelte"
|
import DevToolsComponentSettingsTab from "./DevToolsComponentSettingsTab.svelte"
|
||||||
import DevToolsComponentContextTab from "./DevToolsComponentContextTab.svelte"
|
import DevToolsComponentContextTab from "./DevToolsComponentContextTab.svelte"
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { Heading, Select, ActionButton } from "@budibase/bbui"
|
import { Heading, Select, ActionButton } from "@budibase/bbui"
|
||||||
import { devToolsStore, appStore } from "../../stores"
|
import { devToolsStore, appStore } from "@/stores"
|
||||||
import { getContext, onMount } from "svelte"
|
import { getContext, onMount } from "svelte"
|
||||||
import { API } from "api"
|
import { API } from "@/api"
|
||||||
|
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
const SELF_ROLE = "self"
|
const SELF_ROLE = "self"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { notificationStore } from "stores"
|
import { notificationStore } from "@/stores"
|
||||||
|
|
||||||
export let label
|
export let label
|
||||||
export let value
|
export let value
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { Layout } from "@budibase/bbui"
|
import { Layout } from "@budibase/bbui"
|
||||||
import { authStore, appStore, screenStore, componentStore } from "stores"
|
import { authStore, appStore, screenStore, componentStore } from "@/stores"
|
||||||
import DevToolsStat from "./DevToolsStat.svelte"
|
import DevToolsStat from "./DevToolsStat.svelte"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { confirmationStore } from "stores"
|
import { confirmationStore } from "@/stores"
|
||||||
import { Modal, ModalContent } from "@budibase/bbui"
|
import { Modal, ModalContent } from "@budibase/bbui"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { notificationStore } from "stores"
|
import { notificationStore } from "@/stores"
|
||||||
import { Notification } from "@budibase/bbui"
|
import { Notification } from "@budibase/bbui"
|
||||||
import { fly } from "svelte/transition"
|
import { fly } from "svelte/transition"
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
notificationStore,
|
notificationStore,
|
||||||
routeStore,
|
routeStore,
|
||||||
stateStore,
|
stateStore,
|
||||||
} from "stores"
|
} from "@/stores"
|
||||||
import { Modal, ModalContent, ActionButton } from "@budibase/bbui"
|
import { Modal, ModalContent, ActionButton } from "@budibase/bbui"
|
||||||
import { onDestroy } from "svelte"
|
import { onDestroy } from "svelte"
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import IndicatorSet from "./IndicatorSet.svelte"
|
import { builderStore, screenStore, dndStore, isGridScreen } from "@/stores"
|
||||||
import {
|
|
||||||
builderStore,
|
|
||||||
screenStore,
|
|
||||||
dndStore,
|
|
||||||
dndParent,
|
|
||||||
dndIsDragging,
|
|
||||||
} from "stores"
|
|
||||||
import DNDPlaceholderOverlay from "./DNDPlaceholderOverlay.svelte"
|
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
import { findComponentById } from "utils/components.js"
|
import { findComponentById } from "@/utils/components.js"
|
||||||
import { DNDPlaceholderID } from "constants"
|
import { isGridEvent } from "@/utils/grid"
|
||||||
import { isGridEvent } from "utils/grid"
|
import { DNDPlaceholderID } from "@/constants"
|
||||||
|
import { Component } from "@budibase/types"
|
||||||
|
|
||||||
|
type ChildCoords = {
|
||||||
|
placeholder: boolean
|
||||||
|
centerX: number
|
||||||
|
centerY: number
|
||||||
|
left: number
|
||||||
|
right: number
|
||||||
|
top: number
|
||||||
|
bottom: number
|
||||||
|
}
|
||||||
|
|
||||||
const ThrottleRate = 130
|
const ThrottleRate = 130
|
||||||
|
|
||||||
|
@ -22,17 +25,19 @@
|
||||||
$: source = $dndStore.source
|
$: source = $dndStore.source
|
||||||
$: target = $dndStore.target
|
$: target = $dndStore.target
|
||||||
$: drop = $dndStore.drop
|
$: drop = $dndStore.drop
|
||||||
|
$: gridScreen = $isGridScreen
|
||||||
|
|
||||||
// Local flag for whether we are awaiting an async drop event
|
// Local flag for whether we are awaiting an async drop event
|
||||||
let dropping = false
|
let dropping = false
|
||||||
|
|
||||||
// Util to get the inner DOM node by a component ID
|
// Util to get the inner DOM element by a component ID
|
||||||
const getDOMNode = id => {
|
const getDOMElement = (id: string): HTMLElement | undefined => {
|
||||||
return document.getElementsByClassName(`${id}-dom`)[0]
|
const el = document.getElementsByClassName(`${id}-dom`)[0]
|
||||||
|
return el instanceof HTMLElement ? el : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// Util to calculate the variance of a set of data
|
// Util to calculate the variance of a set of data
|
||||||
const variance = arr => {
|
const variance = (arr: number[]) => {
|
||||||
const mean = arr.reduce((a, b) => a + b, 0) / arr.length
|
const mean = arr.reduce((a, b) => a + b, 0) / arr.length
|
||||||
let squareSum = 0
|
let squareSum = 0
|
||||||
arr.forEach(value => {
|
arr.forEach(value => {
|
||||||
|
@ -61,36 +66,43 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback when initially starting a drag on a draggable component
|
// Callback when initially starting a drag on a draggable component
|
||||||
const onDragStart = e => {
|
const onDragStart = (e: DragEvent) => {
|
||||||
if (isGridEvent(e)) {
|
if (isGridEvent(e)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!(e.target instanceof HTMLElement)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const component = e.target.closest(".component")
|
const component = e.target.closest(".component")
|
||||||
if (!component?.classList.contains("draggable")) {
|
if (
|
||||||
|
!(component instanceof HTMLElement) ||
|
||||||
|
!component.classList.contains("draggable")
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide drag ghost image
|
// Hide drag ghost image
|
||||||
e.dataTransfer.setDragImage(new Image(), 0, 0)
|
e.dataTransfer?.setDragImage(new Image(), 0, 0)
|
||||||
|
|
||||||
// Add event handler to clear all drag state when dragging ends
|
// Add event handler to clear all drag state when dragging ends
|
||||||
component.addEventListener("dragend", stopDragging)
|
component.addEventListener("dragend", stopDragging)
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
const id = component.dataset.id
|
const id = component.dataset.id!
|
||||||
const parentId = component.dataset.parent
|
const parentId = component.dataset.parent!
|
||||||
const parent = findComponentById(
|
const parent: Component = findComponentById(
|
||||||
get(screenStore).activeScreen?.props,
|
get(screenStore).activeScreen?.props,
|
||||||
parentId
|
parentId
|
||||||
)
|
)
|
||||||
const index = parent._children.findIndex(
|
const index = parent._children!.findIndex(child => child._id === id)
|
||||||
x => x._id === component.dataset.id
|
|
||||||
)
|
|
||||||
dndStore.actions.startDraggingExistingComponent({
|
dndStore.actions.startDraggingExistingComponent({
|
||||||
id,
|
id,
|
||||||
bounds: component.children[0].getBoundingClientRect(),
|
bounds: component.children[0].getBoundingClientRect(),
|
||||||
parent: parentId,
|
parent: parentId,
|
||||||
index,
|
index,
|
||||||
|
name: component.dataset.name,
|
||||||
|
icon: component.dataset.icon,
|
||||||
|
type: parent._children![index]!._component,
|
||||||
})
|
})
|
||||||
builderStore.actions.selectComponent(id)
|
builderStore.actions.selectComponent(id)
|
||||||
|
|
||||||
|
@ -109,18 +121,18 @@
|
||||||
|
|
||||||
// Core logic for handling drop events and determining where to render the
|
// Core logic for handling drop events and determining where to render the
|
||||||
// drop target placeholder
|
// drop target placeholder
|
||||||
const processEvent = Utils.throttle((mouseX, mouseY) => {
|
const processEvent = Utils.throttle((mouseX: number, mouseY: number) => {
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let { id, parent, node, acceptsChildren, empty } = target
|
let { id, parent, element, acceptsChildren, empty } = target
|
||||||
|
|
||||||
// If we're over something that does not accept children then we go up a
|
// If we're over something that does not accept children then we go up a
|
||||||
// level and consider the mouse position relative to the parent
|
// level and consider the mouse position relative to the parent
|
||||||
if (!acceptsChildren) {
|
if (!acceptsChildren) {
|
||||||
id = parent
|
id = parent
|
||||||
empty = false
|
empty = false
|
||||||
node = getDOMNode(parent)
|
element = getDOMElement(parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're now hovering over something which does accept children.
|
// We're now hovering over something which does accept children.
|
||||||
|
@ -133,31 +145,33 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// As the first DOM node in a component may not necessarily contain the
|
// As the first DOM element in a component may not necessarily contain the
|
||||||
// child components, we can find to try the parent of the first child
|
// child components, we can find to try the parent of the first child
|
||||||
// component and use that as the real parent DOM node
|
// component and use that as the real parent DOM node
|
||||||
const childNode = node.getElementsByClassName("component")[0]
|
const childElement = element?.getElementsByClassName("component")[0]
|
||||||
if (childNode?.parentNode) {
|
if (childElement?.parentNode instanceof HTMLElement) {
|
||||||
node = childNode.parentNode
|
element = childElement.parentNode
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append an ephemeral div to allow us to determine layout if only one
|
// Append an ephemeral div to allow us to determine layout if only one
|
||||||
// child exists
|
// child exists
|
||||||
let ephemeralDiv
|
let ephemeralDiv
|
||||||
if (node.children.length === 1) {
|
if (element?.children.length === 1) {
|
||||||
ephemeralDiv = document.createElement("div")
|
ephemeralDiv = document.createElement("div")
|
||||||
ephemeralDiv.dataset.id = DNDPlaceholderID
|
ephemeralDiv.dataset.id = DNDPlaceholderID
|
||||||
node.appendChild(ephemeralDiv)
|
element.appendChild(ephemeralDiv)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're now hovering over something which accepts children and is not
|
// We're now hovering over something which accepts children and is not
|
||||||
// empty, so we need to work out where to inside the placeholder
|
// empty, so we need to work out where to inside the placeholder
|
||||||
// Calculate the coordinates of various locations on each child.
|
// Calculate the coordinates of various locations on each child.
|
||||||
const childCoords = [...(node.children || [])].map(node => {
|
const childCoords: ChildCoords[] = [...(element?.children || [])]
|
||||||
const child = node.children?.[0] || node
|
.filter(el => el instanceof HTMLElement)
|
||||||
|
.map(el => {
|
||||||
|
const child = el.children?.[0] || el
|
||||||
const bounds = child.getBoundingClientRect()
|
const bounds = child.getBoundingClientRect()
|
||||||
return {
|
return {
|
||||||
placeholder: node.dataset.id === DNDPlaceholderID,
|
placeholder: el.dataset.id === DNDPlaceholderID,
|
||||||
centerX: bounds.left + bounds.width / 2,
|
centerX: bounds.left + bounds.width / 2,
|
||||||
centerY: bounds.top + bounds.height / 2,
|
centerY: bounds.top + bounds.height / 2,
|
||||||
left: bounds.left,
|
left: bounds.left,
|
||||||
|
@ -170,14 +184,15 @@
|
||||||
// Now that we've calculated the position of the children, we no longer need
|
// Now that we've calculated the position of the children, we no longer need
|
||||||
// the ephemeral div
|
// the ephemeral div
|
||||||
if (ephemeralDiv) {
|
if (ephemeralDiv) {
|
||||||
node.removeChild(ephemeralDiv)
|
element?.removeChild(ephemeralDiv)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the variance between each set of positions on the children
|
// Calculate the variance between each set of positions on the children
|
||||||
const variances = Object.keys(childCoords[0])
|
const variances = Object.keys(childCoords[0] || {})
|
||||||
.filter(x => x !== "placeholder")
|
.filter(key => key !== "placeholder")
|
||||||
.map(key => {
|
.map(key => {
|
||||||
const coords = childCoords.map(x => x[key])
|
const numericalKey = key as keyof Omit<ChildCoords, "placeholder">
|
||||||
|
const coords = childCoords.map(x => x[numericalKey])
|
||||||
return {
|
return {
|
||||||
variance: variance(coords),
|
variance: variance(coords),
|
||||||
side: key,
|
side: key,
|
||||||
|
@ -189,13 +204,13 @@
|
||||||
variances.sort((a, b) => {
|
variances.sort((a, b) => {
|
||||||
return a.variance < b.variance ? -1 : 1
|
return a.variance < b.variance ? -1 : 1
|
||||||
})
|
})
|
||||||
const column = ["centerX", "left", "right"].includes(variances[0].side)
|
const column = ["centerX", "left", "right"].includes(variances[0]?.side)
|
||||||
|
|
||||||
// Calculate breakpoints between child components so we can determine the
|
// Calculate breakpoints between child components so we can determine the
|
||||||
// index to drop the component in.
|
// index to drop the component in.
|
||||||
// We want to ignore the placeholder from this calculation as it should not
|
// We want to ignore the placeholder from this calculation as it should not
|
||||||
// be considered a real child of the parent.
|
// be considered a real child of the parent.
|
||||||
let breakpoints = childCoords
|
const breakpoints = childCoords
|
||||||
.filter(x => !x.placeholder)
|
.filter(x => !x.placeholder)
|
||||||
.map(x => {
|
.map(x => {
|
||||||
return column ? x.centerY : x.centerX
|
return column ? x.centerY : x.centerX
|
||||||
|
@ -213,38 +228,39 @@
|
||||||
})
|
})
|
||||||
}, ThrottleRate)
|
}, ThrottleRate)
|
||||||
|
|
||||||
const handleEvent = e => {
|
const handleEvent = (e: DragEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
processEvent(e.clientX, e.clientY)
|
processEvent(e.clientX, e.clientY)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback when on top of a component.
|
// Callback when on top of a component
|
||||||
const onDragOver = e => {
|
const onDragOver = (e: DragEvent) => {
|
||||||
if (!source || !target) {
|
if (!source || !target || gridScreen) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleEvent(e)
|
handleEvent(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback when entering a potential drop target
|
// Callback when entering a potential drop target
|
||||||
const onDragEnter = e => {
|
const onDragEnter = async (e: DragEvent) => {
|
||||||
if (!source) {
|
if (!source || gridScreen || !(e.target instanceof HTMLElement)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the next valid component to consider dropping over, ignoring nested
|
// Find the next valid component to consider dropping over, ignoring nested
|
||||||
// block components
|
// block components
|
||||||
const component = e.target?.closest?.(
|
let comp = e.target.closest?.(`.component:not(.block):not(.${source.id})`)
|
||||||
`.component:not(.block):not(.${source.id})`
|
if (!(comp instanceof HTMLElement)) {
|
||||||
)
|
return
|
||||||
if (component && component.classList.contains("droppable")) {
|
}
|
||||||
|
if (comp?.classList.contains("droppable")) {
|
||||||
dndStore.actions.updateTarget({
|
dndStore.actions.updateTarget({
|
||||||
id: component.dataset.id,
|
id: comp.dataset.id!,
|
||||||
parent: component.dataset.parent,
|
parent: comp.dataset.parent!,
|
||||||
node: getDOMNode(component.dataset.id),
|
element: getDOMElement(comp.dataset.id!),
|
||||||
empty: component.classList.contains("empty"),
|
empty: comp.classList.contains("empty"),
|
||||||
acceptsChildren: component.classList.contains("parent"),
|
acceptsChildren: comp.classList.contains("parent"),
|
||||||
})
|
})
|
||||||
handleEvent(e)
|
handleEvent(e)
|
||||||
}
|
}
|
||||||
|
@ -257,12 +273,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we're adding a new component rather than moving one
|
// Check if we're adding a new component rather than moving one
|
||||||
if (source.newComponentType) {
|
if (source.isNew) {
|
||||||
dropping = true
|
dropping = true
|
||||||
builderStore.actions.dropNewComponent(
|
builderStore.actions.dropNewComponent(
|
||||||
source.newComponentType,
|
source.type,
|
||||||
drop.parent,
|
drop.parent,
|
||||||
drop.index
|
drop.index,
|
||||||
|
$dndStore.meta?.props
|
||||||
)
|
)
|
||||||
dropping = false
|
dropping = false
|
||||||
stopDragging()
|
stopDragging()
|
||||||
|
@ -271,7 +288,7 @@
|
||||||
|
|
||||||
// Convert parent + index into target + mode
|
// Convert parent + index into target + mode
|
||||||
let legacyDropTarget, legacyDropMode
|
let legacyDropTarget, legacyDropMode
|
||||||
const parent = findComponentById(
|
const parent: Component | null = findComponentById(
|
||||||
get(screenStore).activeScreen?.props,
|
get(screenStore).activeScreen?.props,
|
||||||
drop.parent
|
drop.parent
|
||||||
)
|
)
|
||||||
|
@ -333,14 +350,3 @@
|
||||||
document.removeEventListener("drop", onDrop, false)
|
document.removeEventListener("drop", onDrop, false)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<IndicatorSet
|
|
||||||
componentId={$dndParent}
|
|
||||||
color="var(--spectrum-global-color-static-green-500)"
|
|
||||||
zIndex={920}
|
|
||||||
prefix="Inside"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if $dndIsDragging}
|
|
||||||
<DNDPlaceholderOverlay />
|
|
||||||
{/if}
|
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { DNDPlaceholderID } from "constants"
|
|
||||||
import { Utils } from "@budibase/frontend-core"
|
|
||||||
|
|
||||||
let left, top, height, width
|
|
||||||
|
|
||||||
const updatePosition = () => {
|
|
||||||
let node = document.getElementsByClassName(DNDPlaceholderID)[0]
|
|
||||||
const insideGrid = node?.dataset.insideGrid === "true"
|
|
||||||
if (!insideGrid) {
|
|
||||||
node = document.getElementsByClassName(`${DNDPlaceholderID}-dom`)[0]
|
|
||||||
}
|
|
||||||
if (!node) {
|
|
||||||
height = 0
|
|
||||||
width = 0
|
|
||||||
} else {
|
|
||||||
const bounds = node.getBoundingClientRect()
|
|
||||||
left = bounds.left
|
|
||||||
top = bounds.top
|
|
||||||
height = bounds.height
|
|
||||||
width = bounds.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const debouncedUpdate = Utils.domDebounce(updatePosition)
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
const interval = setInterval(debouncedUpdate, 100)
|
|
||||||
return () => {
|
|
||||||
clearInterval(interval)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if left != null && top != null && width && height}
|
|
||||||
<div
|
|
||||||
class="overlay"
|
|
||||||
style="left: {left}px; top: {top}px; width: {width}px; height: {height}px;"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.overlay {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 800;
|
|
||||||
background: hsl(160, 64%, 90%);
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: all 130ms ease-out;
|
|
||||||
border: 2px solid var(--spectrum-global-color-static-green-500);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
isGridScreen,
|
||||||
|
dndParent,
|
||||||
|
dndSource,
|
||||||
|
dndIsDragging,
|
||||||
|
dndStore,
|
||||||
|
} from "@/stores"
|
||||||
|
import { DNDPlaceholderID } from "@/constants"
|
||||||
|
import IndicatorSet from "./IndicatorSet.svelte"
|
||||||
|
|
||||||
|
// On grid screens, don't draw the indicator until we've dragged over the
|
||||||
|
// screen. When this happens, the dndSource props will be set as we will have
|
||||||
|
// attached grid metadata styles.
|
||||||
|
$: waitingForGrid = $isGridScreen && !$dndStore.meta?.props
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $dndIsDragging}
|
||||||
|
{#if !$isGridScreen && $dndParent}
|
||||||
|
<IndicatorSet
|
||||||
|
componentId={$dndParent}
|
||||||
|
color="var(--spectrum-global-color-static-green-400)"
|
||||||
|
zIndex={920}
|
||||||
|
prefix="Inside"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !waitingForGrid}
|
||||||
|
<IndicatorSet
|
||||||
|
componentId={DNDPlaceholderID}
|
||||||
|
color="var(--spectrum-global-color-static-green-500)"
|
||||||
|
zIndex={930}
|
||||||
|
allowResizeAnchors={false}
|
||||||
|
background="hsl(160, 64%, 90%)"
|
||||||
|
animate={!$isGridScreen}
|
||||||
|
text={$dndSource?.name}
|
||||||
|
icon={$dndSource?.icon}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
|
@ -1,15 +1,50 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { onMount, onDestroy, getContext } from "svelte"
|
import { onMount, onDestroy, getContext } from "svelte"
|
||||||
import { builderStore, componentStore } from "stores"
|
import {
|
||||||
|
builderStore,
|
||||||
|
componentStore,
|
||||||
|
dndIsDragging,
|
||||||
|
dndStore,
|
||||||
|
dndSource,
|
||||||
|
isGridScreen,
|
||||||
|
} from "@/stores"
|
||||||
import { Utils, memo } from "@budibase/frontend-core"
|
import { Utils, memo } from "@budibase/frontend-core"
|
||||||
import { GridRowHeight } from "constants"
|
import { DNDPlaceholderID, GridRowHeight } from "@/constants"
|
||||||
import {
|
import {
|
||||||
isGridEvent,
|
isGridEvent,
|
||||||
GridParams,
|
GridParams,
|
||||||
getGridVar,
|
getGridVar,
|
||||||
Devices,
|
Devices,
|
||||||
GridDragModes,
|
GridDragMode,
|
||||||
} from "utils/grid"
|
} from "@/utils/grid"
|
||||||
|
|
||||||
|
type GridDragSide =
|
||||||
|
| "top"
|
||||||
|
| "right"
|
||||||
|
| "bottom"
|
||||||
|
| "left"
|
||||||
|
| "top-left"
|
||||||
|
| "top-right"
|
||||||
|
| "bottom-left"
|
||||||
|
| "bottom-right"
|
||||||
|
|
||||||
|
interface GridDragInfo {
|
||||||
|
mode: GridDragMode
|
||||||
|
side?: GridDragSide
|
||||||
|
domTarget?: HTMLElement
|
||||||
|
domComponent: HTMLElement
|
||||||
|
domGrid: HTMLElement
|
||||||
|
id: string
|
||||||
|
gridId: string
|
||||||
|
grid: {
|
||||||
|
startX: number
|
||||||
|
startY: number
|
||||||
|
rowStart: number
|
||||||
|
rowEnd: number
|
||||||
|
colStart: number
|
||||||
|
colEnd: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
|
|
||||||
|
@ -18,11 +53,12 @@
|
||||||
ghost.src =
|
ghost.src =
|
||||||
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
|
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
|
||||||
|
|
||||||
let dragInfo
|
let scrollElement: HTMLElement
|
||||||
let styles = memo()
|
let dragInfo: GridDragInfo | undefined
|
||||||
|
let styles = memo<Record<string, number> | undefined>()
|
||||||
|
|
||||||
// Grid CSS variables
|
// Grid CSS variables
|
||||||
$: device = $context.device.mobile ? Devices.Mobile : Devices.Desktop
|
$: device = $context.device?.mobile ? Devices.Mobile : Devices.Desktop
|
||||||
$: vars = {
|
$: vars = {
|
||||||
colStart: getGridVar(device, GridParams.ColStart),
|
colStart: getGridVar(device, GridParams.ColStart),
|
||||||
colEnd: getGridVar(device, GridParams.ColEnd),
|
colEnd: getGridVar(device, GridParams.ColEnd),
|
||||||
|
@ -35,27 +71,54 @@
|
||||||
|
|
||||||
// Set ephemeral styles
|
// Set ephemeral styles
|
||||||
$: instance = componentStore.actions.getComponentInstance(id)
|
$: instance = componentStore.actions.getComponentInstance(id)
|
||||||
$: $instance?.setEphemeralStyles($styles)
|
$: applyStyles($instance, $styles)
|
||||||
|
|
||||||
|
// Reset when not dragging new components
|
||||||
|
$: !$dndIsDragging && stopDragging()
|
||||||
|
|
||||||
|
const scrollOffset = () => scrollElement?.scrollTop || 0
|
||||||
|
|
||||||
|
const applyStyles = async (
|
||||||
|
instance: any,
|
||||||
|
styles: Record<string, number> | undefined
|
||||||
|
) => {
|
||||||
|
instance?.setEphemeralStyles(styles)
|
||||||
|
|
||||||
|
// If dragging a new component on to a grid screen, tick to allow the
|
||||||
|
// real component to render in the new position before updating the DND
|
||||||
|
// store, preventing the green DND overlay from being out of position
|
||||||
|
if ($dndSource?.isNew && styles) {
|
||||||
|
dndStore.actions.updateNewComponentProps({
|
||||||
|
_styles: {
|
||||||
|
normal: styles,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sugar for a combination of both min and max
|
// Sugar for a combination of both min and max
|
||||||
const minMax = (value, min, max) => Math.min(max, Math.max(min, value))
|
const minMax = (value: number, min: number, max: number) =>
|
||||||
|
Math.min(max, Math.max(min, value))
|
||||||
|
|
||||||
const processEvent = Utils.domDebounce((mouseX, mouseY) => {
|
const processEvent = Utils.domDebounce((mouseX: number, mouseY: number) => {
|
||||||
if (!dragInfo?.grid) {
|
if (!dragInfo?.grid) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const { mode, side, grid, domGrid } = dragInfo
|
const { mode, grid, domGrid } = dragInfo
|
||||||
const { startX, startY, rowStart, rowEnd, colStart, colEnd } = grid
|
const { startX, startY, rowStart, rowEnd, colStart, colEnd } = grid
|
||||||
if (!domGrid) {
|
if (!domGrid) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const cols = parseInt(domGrid.dataset.cols)
|
const cols = parseInt(domGrid.dataset.cols || "")
|
||||||
const colSize = parseInt(domGrid.dataset.colSize)
|
const colSize = parseInt(domGrid.dataset.colSize || "")
|
||||||
|
if (isNaN(cols) || isNaN(colSize)) {
|
||||||
|
throw "DOM grid missing required dataset attributes"
|
||||||
|
}
|
||||||
const diffX = mouseX - startX
|
const diffX = mouseX - startX
|
||||||
let deltaX = Math.round(diffX / colSize)
|
let deltaX = Math.round(diffX / colSize)
|
||||||
const diffY = mouseY - startY
|
const diffY = mouseY - startY + scrollOffset()
|
||||||
let deltaY = Math.round(diffY / GridRowHeight)
|
let deltaY = Math.round(diffY / GridRowHeight)
|
||||||
if (mode === GridDragModes.Move) {
|
if (mode === GridDragMode.Move) {
|
||||||
deltaX = minMax(deltaX, 1 - colStart, cols + 1 - colEnd)
|
deltaX = minMax(deltaX, 1 - colStart, cols + 1 - colEnd)
|
||||||
deltaY = Math.max(deltaY, 1 - rowStart)
|
deltaY = Math.max(deltaY, 1 - rowStart)
|
||||||
const newStyles = {
|
const newStyles = {
|
||||||
|
@ -65,8 +128,9 @@
|
||||||
[vars.rowEnd]: rowEnd + deltaY,
|
[vars.rowEnd]: rowEnd + deltaY,
|
||||||
}
|
}
|
||||||
styles.set(newStyles)
|
styles.set(newStyles)
|
||||||
} else if (mode === GridDragModes.Resize) {
|
} else if (mode === GridDragMode.Resize) {
|
||||||
let newStyles = {}
|
const { side } = dragInfo
|
||||||
|
let newStyles: Record<string, number> = {}
|
||||||
if (side === "right") {
|
if (side === "right") {
|
||||||
newStyles[vars.colEnd] = Math.max(colEnd + deltaX, colStart + 1)
|
newStyles[vars.colEnd] = Math.max(colEnd + deltaX, colStart + 1)
|
||||||
} else if (side === "left") {
|
} else if (side === "left") {
|
||||||
|
@ -92,14 +156,50 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleEvent = e => {
|
const handleEvent = (e: DragEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
processEvent(e.clientX, e.clientY)
|
processEvent(e.clientX, e.clientY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Callback when dragging a new component over the preview iframe in a valid
|
||||||
|
// position for the first time
|
||||||
|
const startDraggingPlaceholder = () => {
|
||||||
|
const domComponent = document.getElementsByClassName(DNDPlaceholderID)[0]
|
||||||
|
const domGrid = domComponent?.closest(".grid")
|
||||||
|
if (
|
||||||
|
!(domComponent instanceof HTMLElement) ||
|
||||||
|
!(domGrid instanceof HTMLElement)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const styles = getComputedStyle(domComponent)
|
||||||
|
const bounds = domComponent.getBoundingClientRect()
|
||||||
|
|
||||||
|
// Show as active
|
||||||
|
domComponent.classList.add("dragging")
|
||||||
|
domGrid.classList.add("highlight")
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
dragInfo = {
|
||||||
|
domComponent,
|
||||||
|
domGrid,
|
||||||
|
id: DNDPlaceholderID,
|
||||||
|
gridId: domGrid.parentElement!.dataset.id!,
|
||||||
|
mode: GridDragMode.Move,
|
||||||
|
grid: {
|
||||||
|
startX: bounds.left + bounds.width / 2,
|
||||||
|
startY: bounds.top + bounds.height / 2 + scrollOffset(),
|
||||||
|
rowStart: parseInt(styles.gridRowStart),
|
||||||
|
rowEnd: parseInt(styles.gridRowEnd),
|
||||||
|
colStart: parseInt(styles.gridColumnStart),
|
||||||
|
colEnd: parseInt(styles.gridColumnEnd),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Callback when initially starting a drag on a draggable component
|
// Callback when initially starting a drag on a draggable component
|
||||||
const onDragStart = e => {
|
const onDragStart = (e: DragEvent) => {
|
||||||
if (!isGridEvent(e)) {
|
if (!isGridEvent(e)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -108,27 +208,30 @@
|
||||||
e.dataTransfer.setDragImage(ghost, 0, 0)
|
e.dataTransfer.setDragImage(ghost, 0, 0)
|
||||||
|
|
||||||
// Extract state
|
// Extract state
|
||||||
let mode, id, side
|
let mode: GridDragMode, id: string, side
|
||||||
if (e.target.dataset.indicator === "true") {
|
if (e.target.dataset.indicator === "true") {
|
||||||
mode = e.target.dataset.dragMode
|
mode = e.target.dataset.dragMode as GridDragMode
|
||||||
id = e.target.dataset.id
|
id = e.target.dataset.id!
|
||||||
side = e.target.dataset.side
|
side = e.target.dataset.side as GridDragSide
|
||||||
} else {
|
} else {
|
||||||
// Handle move
|
// Handle move
|
||||||
mode = GridDragModes.Move
|
mode = GridDragMode.Move
|
||||||
const component = e.target.closest(".component")
|
const component = e.target.closest(".component") as HTMLElement
|
||||||
id = component.dataset.id
|
id = component.dataset.id!
|
||||||
}
|
}
|
||||||
|
|
||||||
// If holding ctrl/cmd then leave behind a duplicate of this component
|
// If holding ctrl/cmd then leave behind a duplicate of this component
|
||||||
if (mode === GridDragModes.Move && (e.ctrlKey || e.metaKey)) {
|
if (mode === GridDragMode.Move && (e.ctrlKey || e.metaKey)) {
|
||||||
builderStore.actions.duplicateComponent(id, "above", false)
|
builderStore.actions.duplicateComponent(id, "above", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find grid parent and read from DOM
|
// Find grid parent and read from DOM
|
||||||
const domComponent = document.getElementsByClassName(id)[0]
|
const domComponent = document.getElementsByClassName(id)[0]
|
||||||
const domGrid = domComponent?.closest(".grid")
|
const domGrid = domComponent?.closest(".grid")
|
||||||
if (!domGrid) {
|
if (
|
||||||
|
!(domComponent instanceof HTMLElement) ||
|
||||||
|
!(domGrid instanceof HTMLElement)
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const styles = getComputedStyle(domComponent)
|
const styles = getComputedStyle(domComponent)
|
||||||
|
@ -144,25 +247,29 @@
|
||||||
domComponent,
|
domComponent,
|
||||||
domGrid,
|
domGrid,
|
||||||
id,
|
id,
|
||||||
gridId: domGrid.parentNode.dataset.id,
|
gridId: domGrid.parentElement!.dataset.id!,
|
||||||
mode,
|
mode,
|
||||||
side,
|
side,
|
||||||
grid: {
|
grid: {
|
||||||
startX: e.clientX,
|
startX: e.clientX,
|
||||||
startY: e.clientY,
|
startY: e.clientY + scrollOffset(),
|
||||||
rowStart: parseInt(styles["grid-row-start"]),
|
rowStart: parseInt(styles.gridRowStart),
|
||||||
rowEnd: parseInt(styles["grid-row-end"]),
|
rowEnd: parseInt(styles.gridRowEnd),
|
||||||
colStart: parseInt(styles["grid-column-start"]),
|
colStart: parseInt(styles.gridColumnStart),
|
||||||
colEnd: parseInt(styles["grid-column-end"]),
|
colEnd: parseInt(styles.gridColumnEnd),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add event handler to clear all drag state when dragging ends
|
// Add event handler to clear all drag state when dragging ends
|
||||||
dragInfo.domTarget.addEventListener("dragend", stopDragging)
|
dragInfo.domTarget!.addEventListener("dragend", stopDragging, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onDragOver = e => {
|
const onDragOver = (e: DragEvent) => {
|
||||||
if (!dragInfo) {
|
if (!dragInfo) {
|
||||||
|
// Check if we're dragging a new component
|
||||||
|
if ($dndIsDragging && $dndSource?.isNew && $isGridScreen) {
|
||||||
|
startDraggingPlaceholder()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleEvent(e)
|
handleEvent(e)
|
||||||
|
@ -178,7 +285,7 @@
|
||||||
// Reset DOM
|
// Reset DOM
|
||||||
domComponent.classList.remove("dragging")
|
domComponent.classList.remove("dragging")
|
||||||
domGrid.classList.remove("highlight")
|
domGrid.classList.remove("highlight")
|
||||||
domTarget.removeEventListener("dragend", stopDragging)
|
domTarget?.removeEventListener("dragend", stopDragging)
|
||||||
|
|
||||||
// Save changes
|
// Save changes
|
||||||
if ($styles) {
|
if ($styles) {
|
||||||
|
@ -186,17 +293,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
dragInfo = null
|
dragInfo = undefined
|
||||||
styles.set(null)
|
styles.set(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
scrollElement = document.getElementsByClassName(
|
||||||
|
"screen-wrapper"
|
||||||
|
)[0] as HTMLElement
|
||||||
document.addEventListener("dragstart", onDragStart, false)
|
document.addEventListener("dragstart", onDragStart, false)
|
||||||
document.addEventListener("dragover", onDragOver, false)
|
document.addEventListener("dragover", onDragOver, false)
|
||||||
|
document.addEventListener("scroll", processEvent)
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
document.removeEventListener("dragstart", onDragStart, false)
|
document.removeEventListener("dragstart", onDragStart, false)
|
||||||
document.removeEventListener("dragover", onDragOver, false)
|
document.removeEventListener("dragover", onDragOver, false)
|
||||||
|
document.removeEventListener("scroll", processEvent)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import { builderStore } from "stores"
|
import { builderStore } from "@/stores"
|
||||||
|
|
||||||
export let style
|
export let style
|
||||||
export let value
|
export let value
|
||||||
|
|
|
@ -1,26 +1,30 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
import IndicatorSet from "./IndicatorSet.svelte"
|
import IndicatorSet from "./IndicatorSet.svelte"
|
||||||
import { dndIsDragging, hoverStore, builderStore } from "stores"
|
import { dndIsDragging, hoverStore, builderStore } from "@/stores"
|
||||||
|
|
||||||
$: componentId = $hoverStore.hoveredComponentId
|
$: componentId = $hoverStore.hoveredComponentId
|
||||||
$: selectedComponentId = $builderStore.selectedComponentId
|
$: selectedComponentId = $builderStore.selectedComponentId
|
||||||
$: selected = componentId === selectedComponentId
|
$: selected = componentId === selectedComponentId
|
||||||
|
|
||||||
const onMouseOver = e => {
|
const onMouseOver = (e: MouseEvent) => {
|
||||||
|
const target = e.target as HTMLElement
|
||||||
|
|
||||||
// Ignore if dragging
|
// Ignore if dragging
|
||||||
if (e.buttons > 0) {
|
if (e.buttons > 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let newId
|
let newId
|
||||||
if (e.target.classList.contains("anchor")) {
|
if (target.classList.contains("anchor")) {
|
||||||
// Handle resize anchors
|
// Handle resize anchors
|
||||||
newId = e.target.dataset.id
|
newId = target.dataset.id
|
||||||
} else {
|
} else {
|
||||||
// Handle normal components
|
// Handle normal components
|
||||||
const element = e.target.closest(".interactive.component:not(.root)")
|
const element = target.closest(".interactive.component:not(.root)")
|
||||||
newId = element?.dataset?.id
|
if (element instanceof HTMLElement) {
|
||||||
|
newId = element.dataset?.id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newId !== componentId) {
|
if (newId !== componentId) {
|
||||||
|
@ -43,9 +47,11 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if !$dndIsDragging && componentId}
|
||||||
<IndicatorSet
|
<IndicatorSet
|
||||||
componentId={$dndIsDragging ? null : componentId}
|
{componentId}
|
||||||
color="var(--spectrum-global-color-static-blue-200)"
|
color="var(--spectrum-global-color-static-blue-200)"
|
||||||
zIndex={selected ? 890 : 910}
|
zIndex={selected ? 890 : 910}
|
||||||
allowResizeAnchors
|
allowResizeAnchors
|
||||||
/>
|
/>
|
||||||
|
{/if}}
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import { GridDragModes } from "utils/grid"
|
import { GridDragMode } from "@/utils/grid"
|
||||||
|
|
||||||
export let top
|
export let top: number
|
||||||
export let left
|
export let left: number
|
||||||
export let width
|
export let width: number
|
||||||
export let height
|
export let height: number
|
||||||
export let text
|
export let text: string | undefined
|
||||||
export let icon
|
export let icon: string | undefined
|
||||||
export let color
|
export let color: string
|
||||||
export let zIndex
|
export let zIndex: number
|
||||||
export let componentId
|
export let componentId: string
|
||||||
export let line = false
|
export let line = false
|
||||||
export let alignRight = false
|
export let alignRight = false
|
||||||
export let showResizeAnchors = false
|
export let showResizeAnchors = false
|
||||||
|
export let background: string | undefined
|
||||||
|
export let animate = false
|
||||||
|
|
||||||
const AnchorSides = [
|
const AnchorSides = [
|
||||||
"right",
|
"right",
|
||||||
|
@ -33,10 +35,12 @@
|
||||||
class="indicator"
|
class="indicator"
|
||||||
class:flipped
|
class:flipped
|
||||||
class:line
|
class:line
|
||||||
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex};"
|
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex}; --bg: {background ||
|
||||||
|
'none'};"
|
||||||
class:withText={!!text}
|
class:withText={!!text}
|
||||||
class:vCompact={height < 40}
|
class:vCompact={height < 40}
|
||||||
class:hCompact={width < 40}
|
class:hCompact={width < 40}
|
||||||
|
class:animate
|
||||||
>
|
>
|
||||||
{#if text || icon}
|
{#if text || icon}
|
||||||
<div
|
<div
|
||||||
|
@ -46,7 +50,7 @@
|
||||||
class:right={alignRight}
|
class:right={alignRight}
|
||||||
draggable="true"
|
draggable="true"
|
||||||
data-indicator="true"
|
data-indicator="true"
|
||||||
data-drag-mode={GridDragModes.Move}
|
data-drag-mode={GridDragMode.Move}
|
||||||
data-id={componentId}
|
data-id={componentId}
|
||||||
>
|
>
|
||||||
{#if icon}
|
{#if icon}
|
||||||
|
@ -65,7 +69,7 @@
|
||||||
class="anchor {side}"
|
class="anchor {side}"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
data-indicator="true"
|
data-indicator="true"
|
||||||
data-drag-mode={GridDragModes.Resize}
|
data-drag-mode={GridDragMode.Resize}
|
||||||
data-side={side}
|
data-side={side}
|
||||||
data-id={componentId}
|
data-id={componentId}
|
||||||
>
|
>
|
||||||
|
@ -84,6 +88,7 @@
|
||||||
border: 2px solid var(--color);
|
border: 2px solid var(--color);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
.indicator.withText {
|
.indicator.withText {
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
|
@ -94,6 +99,9 @@
|
||||||
.indicator.line {
|
.indicator.line {
|
||||||
border-radius: 4px !important;
|
border-radius: 4px !important;
|
||||||
}
|
}
|
||||||
|
.indicator.animate {
|
||||||
|
transition: all 130ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
/* Label styles */
|
/* Label styles */
|
||||||
.label {
|
.label {
|
||||||
|
|
|
@ -1,42 +1,68 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
import Indicator from "./Indicator.svelte"
|
import Indicator from "./Indicator.svelte"
|
||||||
import { builderStore } from "stores"
|
import { builderStore } from "@/stores"
|
||||||
import { memo, Utils } from "@budibase/frontend-core"
|
import { memo, Utils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let componentId = null
|
export let componentId: string
|
||||||
export let color = null
|
export let color: string
|
||||||
export let zIndex = 900
|
export let zIndex: number = 900
|
||||||
export let prefix = null
|
export let prefix: string | undefined = undefined
|
||||||
export let allowResizeAnchors = false
|
export let allowResizeAnchors: boolean = false
|
||||||
|
export let background: string | undefined = undefined
|
||||||
|
export let animate: boolean = false
|
||||||
|
export let text: string | undefined = undefined
|
||||||
|
export let icon: string | undefined = undefined
|
||||||
|
|
||||||
|
interface IndicatorState {
|
||||||
|
visible: boolean
|
||||||
|
insideModal: boolean
|
||||||
|
insideSidePanel: boolean
|
||||||
|
top: number
|
||||||
|
left: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IndicatorSetState {
|
||||||
|
// Cached props
|
||||||
|
componentId: string
|
||||||
|
color: string
|
||||||
|
zIndex: number
|
||||||
|
prefix?: string
|
||||||
|
allowResizeAnchors: boolean
|
||||||
|
|
||||||
|
// Computed state
|
||||||
|
indicators: IndicatorState[]
|
||||||
|
text?: string
|
||||||
|
icon?: string
|
||||||
|
insideGrid: boolean
|
||||||
|
error: boolean
|
||||||
|
}
|
||||||
|
|
||||||
// Offset = 6 (clip-root padding) - 1 (half the border thickness)
|
|
||||||
const config = memo($$props)
|
const config = memo($$props)
|
||||||
const errorColor = "var(--spectrum-global-color-static-red-600)"
|
const errorColor = "var(--spectrum-global-color-static-red-600)"
|
||||||
const mutationObserver = new MutationObserver(() => debouncedUpdate())
|
const mutationObserver = new MutationObserver(() => debouncedUpdate())
|
||||||
const defaultState = () => ({
|
const defaultState = (): IndicatorSetState => ({
|
||||||
// Cached props
|
|
||||||
componentId,
|
componentId,
|
||||||
color,
|
color,
|
||||||
zIndex,
|
zIndex,
|
||||||
prefix,
|
prefix,
|
||||||
allowResizeAnchors,
|
allowResizeAnchors,
|
||||||
|
|
||||||
// Computed state
|
|
||||||
indicators: [],
|
indicators: [],
|
||||||
text: null,
|
text,
|
||||||
icon: null,
|
icon,
|
||||||
insideGrid: false,
|
insideGrid: false,
|
||||||
error: false,
|
error: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
let interval
|
let interval: ReturnType<typeof setInterval>
|
||||||
let state = defaultState()
|
let state = defaultState()
|
||||||
let observingMutations = false
|
let observingMutations = false
|
||||||
let updating = false
|
let updating = false
|
||||||
let intersectionObservers = []
|
let intersectionObservers: IntersectionObserver[] = []
|
||||||
let callbackCount = 0
|
let callbackCount = 0
|
||||||
let nextState
|
let nextState: ReturnType<typeof defaultState>
|
||||||
|
|
||||||
$: componentId, reset()
|
$: componentId, reset()
|
||||||
$: visibleIndicators = state.indicators.filter(x => x.visible)
|
$: visibleIndicators = state.indicators.filter(x => x.visible)
|
||||||
|
@ -58,15 +84,22 @@
|
||||||
updating = false
|
updating = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const observeMutations = element => {
|
const getElements = (className: string): HTMLElement[] => {
|
||||||
mutationObserver.observe(element, {
|
return [...document.getElementsByClassName(className)]
|
||||||
|
.filter(el => el instanceof HTMLElement)
|
||||||
|
.slice(0, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const observeMutations = (node: Node) => {
|
||||||
|
mutationObserver.observe(node, {
|
||||||
attributes: true,
|
attributes: true,
|
||||||
attributeFilter: ["style"],
|
attributeFilter: ["style"],
|
||||||
})
|
})
|
||||||
observingMutations = true
|
observingMutations = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const createIntersectionCallback = idx => entries => {
|
const createIntersectionCallback =
|
||||||
|
(idx: number) => (entries: IntersectionObserverEntry[]) => {
|
||||||
if (callbackCount >= intersectionObservers.length) {
|
if (callbackCount >= intersectionObservers.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -86,11 +119,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check
|
// Sanity check
|
||||||
if (!componentId) {
|
let elements = getElements(componentId)
|
||||||
state = defaultState()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let elements = document.getElementsByClassName(componentId)
|
|
||||||
if (!elements.length) {
|
if (!elements.length) {
|
||||||
state = defaultState()
|
state = defaultState()
|
||||||
return
|
return
|
||||||
|
@ -108,18 +137,20 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we're inside a grid
|
// Check if we're inside a grid
|
||||||
if (allowResizeAnchors) {
|
|
||||||
nextState.insideGrid = elements[0]?.dataset.insideGrid === "true"
|
nextState.insideGrid = elements[0]?.dataset.insideGrid === "true"
|
||||||
}
|
|
||||||
|
|
||||||
// Get text to display
|
// Get text and icon to display
|
||||||
|
if (!text) {
|
||||||
nextState.text = elements[0].dataset.name
|
nextState.text = elements[0].dataset.name
|
||||||
if (nextState.prefix) {
|
if (nextState.prefix) {
|
||||||
nextState.text = `${nextState.prefix} ${nextState.text}`
|
nextState.text = `${nextState.prefix} ${nextState.text}`
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (!icon) {
|
||||||
if (elements[0].dataset.icon) {
|
if (elements[0].dataset.icon) {
|
||||||
nextState.icon = elements[0].dataset.icon
|
nextState.icon = elements[0].dataset.icon
|
||||||
}
|
}
|
||||||
|
}
|
||||||
nextState.error = elements[0].classList.contains("error")
|
nextState.error = elements[0].classList.contains("error")
|
||||||
|
|
||||||
// Batch reads to minimize reflow
|
// Batch reads to minimize reflow
|
||||||
|
@ -129,11 +160,8 @@
|
||||||
// Extract valid children
|
// Extract valid children
|
||||||
// Sanity limit of active indicators
|
// Sanity limit of active indicators
|
||||||
if (!nextState.insideGrid) {
|
if (!nextState.insideGrid) {
|
||||||
elements = document.getElementsByClassName(`${componentId}-dom`)
|
elements = getElements(`${componentId}-dom`)
|
||||||
}
|
}
|
||||||
elements = Array.from(elements)
|
|
||||||
.filter(x => x != null)
|
|
||||||
.slice(0, 100)
|
|
||||||
const multi = elements.length > 1
|
const multi = elements.length > 1
|
||||||
|
|
||||||
// If there aren't any nodes then reset
|
// If there aren't any nodes then reset
|
||||||
|
@ -143,15 +171,20 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const device = document.getElementById("app-root")
|
const device = document.getElementById("app-root")
|
||||||
|
if (!device) {
|
||||||
|
throw "app-root node not found"
|
||||||
|
}
|
||||||
const deviceBounds = device.getBoundingClientRect()
|
const deviceBounds = device.getBoundingClientRect()
|
||||||
nextState.indicators = elements.map((element, idx) => {
|
nextState.indicators = elements.map((element, idx) => {
|
||||||
const elBounds = element.getBoundingClientRect()
|
const elBounds = element.getBoundingClientRect()
|
||||||
let indicator = {
|
let indicator: IndicatorState = {
|
||||||
top: Math.round(elBounds.top + scrollY - deviceBounds.top + offset),
|
top: Math.round(elBounds.top + scrollY - deviceBounds.top + offset),
|
||||||
left: Math.round(elBounds.left + scrollX - deviceBounds.left + offset),
|
left: Math.round(elBounds.left + scrollX - deviceBounds.left + offset),
|
||||||
width: Math.round(elBounds.width + 2),
|
width: Math.round(elBounds.width + 2),
|
||||||
height: Math.round(elBounds.height + 2),
|
height: Math.round(elBounds.height + 2),
|
||||||
visible: true,
|
visible: true,
|
||||||
|
insideModal: false,
|
||||||
|
insideSidePanel: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// If observing more than one node then we need to use an intersection
|
// If observing more than one node then we need to use an intersection
|
||||||
|
@ -199,11 +232,13 @@
|
||||||
left={indicator.left}
|
left={indicator.left}
|
||||||
width={indicator.width}
|
width={indicator.width}
|
||||||
height={indicator.height}
|
height={indicator.height}
|
||||||
text={idx === 0 ? state.text : null}
|
text={idx === 0 ? state.text : undefined}
|
||||||
icon={idx === 0 ? state.icon : null}
|
icon={idx === 0 ? state.icon : undefined}
|
||||||
showResizeAnchors={state.allowResizeAnchors && state.insideGrid}
|
showResizeAnchors={state.allowResizeAnchors && state.insideGrid}
|
||||||
color={state.error ? errorColor : state.color}
|
color={state.error ? errorColor : state.color}
|
||||||
componentId={state.componentId}
|
componentId={state.componentId}
|
||||||
zIndex={state.zIndex}
|
zIndex={state.zIndex}
|
||||||
|
{background}
|
||||||
|
{animate}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { builderStore } from "stores"
|
import { builderStore } from "@/stores"
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (get(builderStore).inBuilder) {
|
if (get(builderStore).inBuilder) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { builderStore } from "stores"
|
import { dndIsDragging, builderStore } from "@/stores"
|
||||||
import IndicatorSet from "./IndicatorSet.svelte"
|
import IndicatorSet from "./IndicatorSet.svelte"
|
||||||
|
|
||||||
$: color = $builderStore.editMode
|
$: color = $builderStore.editMode
|
||||||
|
@ -7,9 +7,11 @@
|
||||||
: "var(--spectrum-global-color-static-blue-600)"
|
: "var(--spectrum-global-color-static-blue-600)"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if !$dndIsDragging && $builderStore.selectedComponentId}
|
||||||
<IndicatorSet
|
<IndicatorSet
|
||||||
componentId={$builderStore.selectedComponentId}
|
componentId={$builderStore.selectedComponentId}
|
||||||
{color}
|
{color}
|
||||||
zIndex={900}
|
zIndex={900}
|
||||||
allowResizeAnchors
|
allowResizeAnchors
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
import GridStylesButton from "./GridStylesButton.svelte"
|
import GridStylesButton from "./GridStylesButton.svelte"
|
||||||
import SettingsColorPicker from "./SettingsColorPicker.svelte"
|
import SettingsColorPicker from "./SettingsColorPicker.svelte"
|
||||||
import SettingsPicker from "./SettingsPicker.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 { 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 context = getContext("context")
|
||||||
const verticalOffset = 36
|
const verticalOffset = 36
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import { builderStore } from "stores"
|
import { builderStore } from "@/stores"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let prop
|
export let prop
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { ColorPicker } from "@budibase/bbui"
|
import { ColorPicker } from "@budibase/bbui"
|
||||||
import { builderStore } from "stores"
|
import { builderStore } from "@/stores"
|
||||||
|
|
||||||
export let prop
|
export let prop
|
||||||
export let component
|
export let component
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select } from "@budibase/bbui"
|
import { Select } from "@budibase/bbui"
|
||||||
import { builderStore } from "stores"
|
import { builderStore } from "@/stores"
|
||||||
|
|
||||||
export let prop
|
export let prop
|
||||||
export let options
|
export let options
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
interface Window {
|
|
||||||
"##BUDIBASE_APP_ID##": string
|
|
||||||
"##BUDIBASE_IN_BUILDER##": string
|
|
||||||
MIGRATING_APP: boolean
|
|
||||||
}
|
|
|
@ -1,138 +0,0 @@
|
||||||
import ClientApp from "./components/ClientApp.svelte"
|
|
||||||
import UpdatingApp from "./components/UpdatingApp.svelte"
|
|
||||||
import {
|
|
||||||
builderStore,
|
|
||||||
appStore,
|
|
||||||
blockStore,
|
|
||||||
componentStore,
|
|
||||||
environmentStore,
|
|
||||||
dndStore,
|
|
||||||
eventStore,
|
|
||||||
hoverStore,
|
|
||||||
stateStore,
|
|
||||||
} from "./stores"
|
|
||||||
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
|
|
||||||
import { get } from "svelte/store"
|
|
||||||
import { initWebsocket } from "./websocket.js"
|
|
||||||
|
|
||||||
// Provide svelte and svelte/internal as globals for custom components
|
|
||||||
import * as svelte from "svelte"
|
|
||||||
import * as internal from "svelte/internal"
|
|
||||||
|
|
||||||
window.svelte_internal = internal
|
|
||||||
window.svelte = svelte
|
|
||||||
|
|
||||||
// Initialise spectrum icons
|
|
||||||
loadSpectrumIcons()
|
|
||||||
|
|
||||||
let app
|
|
||||||
|
|
||||||
const loadBudibase = async () => {
|
|
||||||
// Update builder store with any builder flags
|
|
||||||
builderStore.set({
|
|
||||||
...get(builderStore),
|
|
||||||
inBuilder: !!window["##BUDIBASE_IN_BUILDER##"],
|
|
||||||
layout: window["##BUDIBASE_PREVIEW_LAYOUT##"],
|
|
||||||
screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
|
|
||||||
selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"],
|
|
||||||
previewId: window["##BUDIBASE_PREVIEW_ID##"],
|
|
||||||
theme: window["##BUDIBASE_PREVIEW_THEME##"],
|
|
||||||
customTheme: window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"],
|
|
||||||
previewDevice: window["##BUDIBASE_PREVIEW_DEVICE##"],
|
|
||||||
navigation: window["##BUDIBASE_PREVIEW_NAVIGATION##"],
|
|
||||||
hiddenComponentIds: window["##BUDIBASE_HIDDEN_COMPONENT_IDS##"],
|
|
||||||
usedPlugins: window["##BUDIBASE_USED_PLUGINS##"],
|
|
||||||
location: window["##BUDIBASE_LOCATION##"],
|
|
||||||
snippets: window["##BUDIBASE_SNIPPETS##"],
|
|
||||||
componentErrors: window["##BUDIBASE_COMPONENT_ERRORS##"],
|
|
||||||
})
|
|
||||||
|
|
||||||
// Set app ID - this window flag is set by both the preview and the real
|
|
||||||
// server rendered app HTML
|
|
||||||
appStore.actions.setAppId(window["##BUDIBASE_APP_ID##"])
|
|
||||||
|
|
||||||
// Set the flag used to determine if the app is being loaded via an iframe
|
|
||||||
appStore.actions.setAppEmbedded(
|
|
||||||
window["##BUDIBASE_APP_EMBEDDED##"] === "true"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (window.MIGRATING_APP) {
|
|
||||||
new UpdatingApp({
|
|
||||||
target: window.document.body,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch environment info
|
|
||||||
if (!get(environmentStore)?.loaded) {
|
|
||||||
await environmentStore.actions.fetchEnvironment()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register handler for runtime events from the builder
|
|
||||||
window.handleBuilderRuntimeEvent = (type, data) => {
|
|
||||||
if (!window["##BUDIBASE_IN_BUILDER##"]) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (type === "event-completed") {
|
|
||||||
eventStore.actions.resolveEvent(data)
|
|
||||||
} else if (type === "eject-block") {
|
|
||||||
const block = blockStore.actions.getBlock(data)
|
|
||||||
block?.eject()
|
|
||||||
} else if (type === "dragging-new-component") {
|
|
||||||
const { dragging, component } = data
|
|
||||||
if (dragging) {
|
|
||||||
const definition =
|
|
||||||
componentStore.actions.getComponentDefinition(component)
|
|
||||||
dndStore.actions.startDraggingNewComponent({ component, definition })
|
|
||||||
} else {
|
|
||||||
dndStore.actions.reset()
|
|
||||||
}
|
|
||||||
} else if (type === "request-context") {
|
|
||||||
const { selectedComponentInstance, screenslotInstance } =
|
|
||||||
get(componentStore)
|
|
||||||
const instance = selectedComponentInstance || screenslotInstance
|
|
||||||
const context = instance?.getDataContext()
|
|
||||||
let stringifiedContext = null
|
|
||||||
try {
|
|
||||||
stringifiedContext = JSON.stringify(context)
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore - invalid context
|
|
||||||
}
|
|
||||||
eventStore.actions.dispatchEvent("provide-context", {
|
|
||||||
context: stringifiedContext,
|
|
||||||
})
|
|
||||||
} else if (type === "hover-component") {
|
|
||||||
hoverStore.actions.hoverComponent(data, false)
|
|
||||||
} else if (type === "builder-meta") {
|
|
||||||
builderStore.actions.setMetadata(data)
|
|
||||||
} else if (type === "builder-state") {
|
|
||||||
const [[key, value]] = Object.entries(data)
|
|
||||||
stateStore.actions.setValue(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register any custom components
|
|
||||||
if (window["##BUDIBASE_CUSTOM_COMPONENTS##"]) {
|
|
||||||
window["##BUDIBASE_CUSTOM_COMPONENTS##"].forEach(component => {
|
|
||||||
componentStore.actions.registerCustomComponent(component)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make a callback available for custom component bundles to register
|
|
||||||
// themselves at runtime
|
|
||||||
window.registerCustomComponent =
|
|
||||||
componentStore.actions.registerCustomComponent
|
|
||||||
|
|
||||||
// Initialise websocket
|
|
||||||
initWebsocket()
|
|
||||||
|
|
||||||
// Create app if one hasn't been created yet
|
|
||||||
if (!app) {
|
|
||||||
app = new ClientApp({
|
|
||||||
target: window.document.body,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach to window so the HTML template can call this when it loads
|
|
||||||
window.loadBudibase = loadBudibase
|
|
|
@ -1,6 +1,83 @@
|
||||||
|
import ClientApp from "./components/ClientApp.svelte"
|
||||||
|
import UpdatingApp from "./components/UpdatingApp.svelte"
|
||||||
|
import {
|
||||||
|
builderStore,
|
||||||
|
appStore,
|
||||||
|
blockStore,
|
||||||
|
componentStore,
|
||||||
|
environmentStore,
|
||||||
|
dndStore,
|
||||||
|
eventStore,
|
||||||
|
hoverStore,
|
||||||
|
stateStore,
|
||||||
|
} from "@/stores"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { initWebsocket } from "@/websocket"
|
||||||
import { APIClient } from "@budibase/frontend-core"
|
import { APIClient } from "@budibase/frontend-core"
|
||||||
import type { ActionTypes } from "./constants"
|
import type { ActionTypes } from "@/constants"
|
||||||
import { Readable } from "svelte/store"
|
import { Readable } from "svelte/store"
|
||||||
|
import {
|
||||||
|
Screen,
|
||||||
|
Layout,
|
||||||
|
Theme,
|
||||||
|
AppCustomTheme,
|
||||||
|
PreviewDevice,
|
||||||
|
AppNavigation,
|
||||||
|
Plugin,
|
||||||
|
Snippet,
|
||||||
|
UIComponentError,
|
||||||
|
CustomComponent,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
|
// Provide svelte and svelte/internal as globals for custom components
|
||||||
|
import * as svelte from "svelte"
|
||||||
|
// @ts-ignore
|
||||||
|
import * as internal from "svelte/internal"
|
||||||
|
window.svelte_internal = internal
|
||||||
|
window.svelte = svelte
|
||||||
|
|
||||||
|
// Initialise spectrum icons
|
||||||
|
// eslint-disable-next-line local-rules/no-budibase-imports
|
||||||
|
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
|
||||||
|
loadSpectrumIcons()
|
||||||
|
|
||||||
|
// Extend global window scope
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
// Data from builder
|
||||||
|
"##BUDIBASE_APP_ID##"?: string
|
||||||
|
"##BUDIBASE_IN_BUILDER##"?: true
|
||||||
|
"##BUDIBASE_PREVIEW_LAYOUT##"?: Layout
|
||||||
|
"##BUDIBASE_PREVIEW_SCREEN##"?: Screen
|
||||||
|
"##BUDIBASE_SELECTED_COMPONENT_ID##"?: string
|
||||||
|
"##BUDIBASE_PREVIEW_ID##"?: number
|
||||||
|
"##BUDIBASE_PREVIEW_THEME##"?: Theme
|
||||||
|
"##BUDIBASE_PREVIEW_CUSTOM_THEME##"?: AppCustomTheme
|
||||||
|
"##BUDIBASE_PREVIEW_DEVICE##"?: PreviewDevice
|
||||||
|
"##BUDIBASE_APP_EMBEDDED##"?: string // This is a bool wrapped in a string
|
||||||
|
"##BUDIBASE_PREVIEW_NAVIGATION##"?: AppNavigation
|
||||||
|
"##BUDIBASE_HIDDEN_COMPONENT_IDS##"?: string[]
|
||||||
|
"##BUDIBASE_USED_PLUGINS##"?: Plugin[]
|
||||||
|
"##BUDIBASE_LOCATION##"?: {
|
||||||
|
protocol: string
|
||||||
|
hostname: string
|
||||||
|
port: string
|
||||||
|
}
|
||||||
|
"##BUDIBASE_SNIPPETS##"?: Snippet[]
|
||||||
|
"##BUDIBASE_COMPONENT_ERRORS##"?: Record<string, UIComponentError>[]
|
||||||
|
"##BUDIBASE_CUSTOM_COMPONENTS##"?: CustomComponent[]
|
||||||
|
|
||||||
|
// Other flags
|
||||||
|
MIGRATING_APP: boolean
|
||||||
|
|
||||||
|
// Client additions
|
||||||
|
handleBuilderRuntimeEvent: (type: string, data: any) => void
|
||||||
|
registerCustomComponent: typeof componentStore.actions.registerCustomComponent
|
||||||
|
loadBudibase: typeof loadBudibase
|
||||||
|
svelte: typeof svelte
|
||||||
|
svelte_internal: typeof internal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface SDK {
|
export interface SDK {
|
||||||
API: APIClient
|
API: APIClient
|
||||||
|
@ -28,4 +105,114 @@ export type Component = Readable<{
|
||||||
errorState: boolean
|
errorState: boolean
|
||||||
}>
|
}>
|
||||||
|
|
||||||
export type Context = Readable<{}>
|
export type Context = Readable<Record<string, any>>
|
||||||
|
|
||||||
|
let app: ClientApp
|
||||||
|
|
||||||
|
const loadBudibase = async () => {
|
||||||
|
// Update builder store with any builder flags
|
||||||
|
builderStore.set({
|
||||||
|
...get(builderStore),
|
||||||
|
inBuilder: !!window["##BUDIBASE_IN_BUILDER##"],
|
||||||
|
layout: window["##BUDIBASE_PREVIEW_LAYOUT##"],
|
||||||
|
screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
|
||||||
|
selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"],
|
||||||
|
previewId: window["##BUDIBASE_PREVIEW_ID##"],
|
||||||
|
theme: window["##BUDIBASE_PREVIEW_THEME##"],
|
||||||
|
customTheme: window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"],
|
||||||
|
previewDevice: window["##BUDIBASE_PREVIEW_DEVICE##"],
|
||||||
|
navigation: window["##BUDIBASE_PREVIEW_NAVIGATION##"],
|
||||||
|
hiddenComponentIds: window["##BUDIBASE_HIDDEN_COMPONENT_IDS##"],
|
||||||
|
usedPlugins: window["##BUDIBASE_USED_PLUGINS##"],
|
||||||
|
location: window["##BUDIBASE_LOCATION##"],
|
||||||
|
snippets: window["##BUDIBASE_SNIPPETS##"],
|
||||||
|
componentErrors: window["##BUDIBASE_COMPONENT_ERRORS##"],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set app ID - this window flag is set by both the preview and the real
|
||||||
|
// server rendered app HTML
|
||||||
|
appStore.actions.setAppId(window["##BUDIBASE_APP_ID##"])
|
||||||
|
|
||||||
|
// Set the flag used to determine if the app is being loaded via an iframe
|
||||||
|
appStore.actions.setAppEmbedded(
|
||||||
|
window["##BUDIBASE_APP_EMBEDDED##"] === "true"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (window.MIGRATING_APP) {
|
||||||
|
new UpdatingApp({
|
||||||
|
target: window.document.body,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch environment info
|
||||||
|
if (!get(environmentStore)?.loaded) {
|
||||||
|
await environmentStore.actions.fetchEnvironment()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register handler for runtime events from the builder
|
||||||
|
window.handleBuilderRuntimeEvent = (type, data) => {
|
||||||
|
if (!window["##BUDIBASE_IN_BUILDER##"]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (type === "event-completed") {
|
||||||
|
eventStore.actions.resolveEvent(data)
|
||||||
|
} else if (type === "eject-block") {
|
||||||
|
const block = blockStore.actions.getBlock(data)
|
||||||
|
block?.eject()
|
||||||
|
} else if (type === "dragging-new-component") {
|
||||||
|
const { dragging, component } = data
|
||||||
|
if (dragging) {
|
||||||
|
dndStore.actions.startDraggingNewComponent(component)
|
||||||
|
} else {
|
||||||
|
dndStore.actions.reset()
|
||||||
|
}
|
||||||
|
} else if (type === "request-context") {
|
||||||
|
const { selectedComponentInstance, screenslotInstance } =
|
||||||
|
get(componentStore)
|
||||||
|
const instance = selectedComponentInstance || screenslotInstance
|
||||||
|
const context = instance?.getDataContext()
|
||||||
|
let stringifiedContext = null
|
||||||
|
try {
|
||||||
|
stringifiedContext = JSON.stringify(context)
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore - invalid context
|
||||||
|
}
|
||||||
|
eventStore.actions.dispatchEvent("provide-context", {
|
||||||
|
context: stringifiedContext,
|
||||||
|
})
|
||||||
|
} else if (type === "hover-component") {
|
||||||
|
hoverStore.actions.hoverComponent(data, false)
|
||||||
|
} else if (type === "builder-meta") {
|
||||||
|
builderStore.actions.setMetadata(data)
|
||||||
|
} else if (type === "builder-state") {
|
||||||
|
const [[key, value]] = Object.entries(data)
|
||||||
|
stateStore.actions.setValue(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register any custom components
|
||||||
|
if (window["##BUDIBASE_CUSTOM_COMPONENTS##"]) {
|
||||||
|
window["##BUDIBASE_CUSTOM_COMPONENTS##"].forEach(component => {
|
||||||
|
componentStore.actions.registerCustomComponent(component)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a callback available for custom component bundles to register
|
||||||
|
// themselves at runtime
|
||||||
|
window.registerCustomComponent =
|
||||||
|
componentStore.actions.registerCustomComponent
|
||||||
|
|
||||||
|
// Initialise websocket
|
||||||
|
initWebsocket()
|
||||||
|
|
||||||
|
// Create app if one hasn't been created yet
|
||||||
|
if (!app) {
|
||||||
|
app = new ClientApp({
|
||||||
|
target: window.document.body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach to window so the HTML template can call this when it loads
|
||||||
|
window.loadBudibase = loadBudibase
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { API } from "api"
|
import { API } from "@/api"
|
||||||
import {
|
import {
|
||||||
authStore,
|
authStore,
|
||||||
notificationStore,
|
notificationStore,
|
||||||
|
@ -18,13 +18,13 @@ import {
|
||||||
appStore,
|
appStore,
|
||||||
stateStore,
|
stateStore,
|
||||||
createContextStore,
|
createContextStore,
|
||||||
} from "stores"
|
} from "@/stores"
|
||||||
import { styleable } from "utils/styleable"
|
import { styleable } from "@/utils/styleable"
|
||||||
import { linkable } from "utils/linkable"
|
import { linkable } from "@/utils/linkable"
|
||||||
import { getAction } from "utils/getAction"
|
import { getAction } from "@/utils/getAction"
|
||||||
import Provider from "components/context/Provider.svelte"
|
import Provider from "@/components/context/Provider.svelte"
|
||||||
import Block from "components/Block.svelte"
|
import Block from "@/components/Block.svelte"
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "@/components/BlockComponent.svelte"
|
||||||
import { ActionTypes } from "./constants"
|
import { ActionTypes } from "./constants"
|
||||||
import {
|
import {
|
||||||
fetchDatasourceSchema,
|
fetchDatasourceSchema,
|
||||||
|
@ -41,7 +41,7 @@ import {
|
||||||
memo,
|
memo,
|
||||||
derivedMemo,
|
derivedMemo,
|
||||||
} from "@budibase/frontend-core"
|
} from "@budibase/frontend-core"
|
||||||
import { createValidatorFromConstraints } from "components/app/forms/validation"
|
import { createValidatorFromConstraints } from "@/components/app/forms/validation"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
API,
|
API,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { API } from "api"
|
import { API } from "@/api"
|
||||||
import { get, writable, derived } from "svelte/store"
|
import { get, writable, derived } from "svelte/store"
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { API } from "api"
|
import { API } from "@/api"
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
const createAuthStore = () => {
|
const createAuthStore = () => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import { API } from "api"
|
import { API } from "@/api"
|
||||||
import { devToolsStore } from "./devTools.js"
|
import { devToolsStore } from "./devTools.js"
|
||||||
import { eventStore } from "./events.js"
|
import { eventStore } from "./events.js"
|
||||||
|
|
||||||
|
@ -77,11 +77,12 @@ const createBuilderStore = () => {
|
||||||
mode,
|
mode,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
dropNewComponent: (component, parent, index) => {
|
dropNewComponent: (component, parent, index, props) => {
|
||||||
eventStore.actions.dispatchEvent("drop-new-component", {
|
eventStore.actions.dispatchEvent("drop-new-component", {
|
||||||
component,
|
component,
|
||||||
parent,
|
parent,
|
||||||
index,
|
index,
|
||||||
|
props,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
setEditMode: enabled => {
|
setEditMode: enabled => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import { API } from "api"
|
import { API } from "@/api"
|
||||||
import { FieldTypes } from "../constants"
|
import { FieldTypes } from "../constants"
|
||||||
import { routeStore } from "./routes"
|
import { routeStore } from "./routes"
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { derived } from "svelte/store"
|
import { derived } from "svelte/store"
|
||||||
import { findComponentPathById } from "utils/components.js"
|
import { findComponentPathById } from "@/utils/components.js"
|
||||||
import { dndParent } from "../dnd.js"
|
import { dndParent } from "../dnd.ts"
|
||||||
import { screenStore } from "../screens.js"
|
import { screenStore } from "../screens.js"
|
||||||
|
|
||||||
export const dndComponentPath = derived(
|
export const dndComponentPath = derived(
|
||||||
|
|
|
@ -1,88 +0,0 @@
|
||||||
import { writable } from "svelte/store"
|
|
||||||
import { derivedMemo } from "@budibase/frontend-core"
|
|
||||||
|
|
||||||
const createDndStore = () => {
|
|
||||||
const initialState = {
|
|
||||||
// Info about the dragged component
|
|
||||||
source: null,
|
|
||||||
|
|
||||||
// Info about the target component being hovered over
|
|
||||||
target: null,
|
|
||||||
|
|
||||||
// Info about where the component would be dropped
|
|
||||||
drop: null,
|
|
||||||
}
|
|
||||||
const store = writable(initialState)
|
|
||||||
|
|
||||||
const startDraggingExistingComponent = ({ id, parent, bounds, index }) => {
|
|
||||||
store.set({
|
|
||||||
...initialState,
|
|
||||||
source: { id, parent, bounds, index },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const startDraggingNewComponent = ({ component, definition }) => {
|
|
||||||
if (!component) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get size of new component so we can show a properly sized placeholder
|
|
||||||
const width = definition?.size?.width || 128
|
|
||||||
const height = definition?.size?.height || 64
|
|
||||||
|
|
||||||
store.set({
|
|
||||||
...initialState,
|
|
||||||
source: {
|
|
||||||
id: null,
|
|
||||||
parent: null,
|
|
||||||
bounds: { height, width },
|
|
||||||
index: null,
|
|
||||||
newComponentType: component,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateTarget = ({ id, parent, node, empty, acceptsChildren }) => {
|
|
||||||
store.update(state => {
|
|
||||||
state.target = { id, parent, node, empty, acceptsChildren }
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateDrop = ({ parent, index }) => {
|
|
||||||
store.update(state => {
|
|
||||||
state.drop = { parent, index }
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const reset = () => {
|
|
||||||
store.set(initialState)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe: store.subscribe,
|
|
||||||
actions: {
|
|
||||||
startDraggingExistingComponent,
|
|
||||||
startDraggingNewComponent,
|
|
||||||
updateTarget,
|
|
||||||
updateDrop,
|
|
||||||
reset,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const dndStore = createDndStore()
|
|
||||||
|
|
||||||
// The DND store is updated extremely frequently, so we can greatly improve
|
|
||||||
// performance by deriving any state that needs to be externally observed.
|
|
||||||
// By doing this and using primitives, we can avoid invalidating other stores
|
|
||||||
// or components which depend on DND state unless values actually change.
|
|
||||||
export const dndParent = derivedMemo(dndStore, x => x.drop?.parent)
|
|
||||||
export const dndIndex = derivedMemo(dndStore, x => x.drop?.index)
|
|
||||||
export const dndBounds = derivedMemo(dndStore, x => x.source?.bounds)
|
|
||||||
export const dndIsDragging = derivedMemo(dndStore, x => !!x.source)
|
|
||||||
export const dndIsNewComponent = derivedMemo(
|
|
||||||
dndStore,
|
|
||||||
x => x.source?.newComponentType != null
|
|
||||||
)
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue