Merge branch 'develop' of github.com:Budibase/budibase into feature/table-fetching-frontend
This commit is contained in:
commit
805e417553
|
@ -37,14 +37,17 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn nx run-many -t=build --configuration=production
|
# Run build all the projects
|
||||||
|
- run: yarn build
|
||||||
|
# Check the types of the projects built via esbuild
|
||||||
|
- run: yarn check:types
|
||||||
|
|
||||||
test-libraries:
|
test-libraries:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -52,7 +55,7 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
|
@ -72,7 +75,7 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
|
@ -82,7 +85,7 @@ jobs:
|
||||||
- run: yarn test --scope=@budibase/worker --scope=@budibase/server
|
- run: yarn test --scope=@budibase/worker --scope=@budibase/server
|
||||||
- uses: codecov/codecov-action@v3
|
- uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
token: ${{ secrets.CODECOV_TOKEN || github.token }} # not required for public repos
|
||||||
name: codecov-umbrella
|
name: codecov-umbrella
|
||||||
verbose: true
|
verbose: true
|
||||||
|
|
||||||
|
@ -92,7 +95,7 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
|
@ -107,7 +110,7 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
|
@ -131,7 +134,7 @@ jobs:
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Check submodule
|
- name: Check submodule
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
name: Budibase Prerelease
|
name: Budibase Prerelease
|
||||||
concurrency: release-prerelease
|
concurrency:
|
||||||
|
group: release-prerelease
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
name: Budibase Release
|
name: Budibase Release
|
||||||
concurrency: release
|
concurrency:
|
||||||
|
group: release
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
name: Tag prerelease
|
name: Tag prerelease
|
||||||
concurrency: release-prerelease
|
concurrency:
|
||||||
|
group: tag-prerelease
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
name: Tag release
|
name: Tag release
|
||||||
concurrency: release-prerelease
|
concurrency:
|
||||||
|
group: tag-release
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.6.19-alpha.11",
|
"version": "2.6.19-alpha.52",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/backend-core",
|
"packages/backend-core",
|
||||||
|
|
10
package.json
10
package.json
|
@ -4,7 +4,6 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esbuild-plugins/node-resolve": "^0.2.2",
|
"@esbuild-plugins/node-resolve": "^0.2.2",
|
||||||
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
|
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
|
||||||
"@nx/esbuild": "16.2.1",
|
|
||||||
"@nx/js": "16.2.1",
|
"@nx/js": "16.2.1",
|
||||||
"@rollup/plugin-json": "^4.0.2",
|
"@rollup/plugin-json": "^4.0.2",
|
||||||
"@typescript-eslint/parser": "5.45.0",
|
"@typescript-eslint/parser": "5.45.0",
|
||||||
|
@ -34,6 +33,7 @@
|
||||||
"bootstrap": "./scripts/link-dependencies.sh && echo '***BOOTSTRAP ONLY REQUIRED FOR USE WITH ACCOUNT PORTAL***'",
|
"bootstrap": "./scripts/link-dependencies.sh && echo '***BOOTSTRAP ONLY REQUIRED FOR USE WITH ACCOUNT PORTAL***'",
|
||||||
"build": "yarn nx run-many -t=build",
|
"build": "yarn nx run-many -t=build",
|
||||||
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
|
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
|
||||||
|
"check:types": "lerna run check:types --skip-nx-cache",
|
||||||
"backend:bootstrap": "./scripts/scopeBackend.sh && yarn run bootstrap",
|
"backend:bootstrap": "./scripts/scopeBackend.sh && yarn run bootstrap",
|
||||||
"backend:build": "./scripts/scopeBackend.sh 'lerna run --stream build'",
|
"backend:build": "./scripts/scopeBackend.sh 'lerna run --stream build'",
|
||||||
"build:sdk": "lerna run --stream build:sdk",
|
"build:sdk": "lerna run --stream build:sdk",
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
||||||
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --scope @budibase/worker --scope @budibase/server",
|
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --scope @budibase/worker --scope @budibase/server",
|
||||||
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built",
|
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built",
|
||||||
"dev:docker": "yarn build && docker-compose -f hosting/docker-compose.dev.yaml -f hosting/docker-compose.build.yaml up --build --scale proxy-service=0 ",
|
"dev:docker": "yarn build && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
|
||||||
"test": "lerna run --stream test --stream",
|
"test": "lerna run --stream test --stream",
|
||||||
"lint:eslint": "eslint packages && eslint qa-core",
|
"lint:eslint": "eslint packages && eslint qa-core",
|
||||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
|
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
|
||||||
|
@ -110,5 +110,11 @@
|
||||||
"packages/pro/packages/pro"
|
"packages/pro/packages/pro"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"@budibase/backend-core": "0.0.0",
|
||||||
|
"@budibase/shared-core": "0.0.0",
|
||||||
|
"@budibase/string-templates": "0.0.0",
|
||||||
|
"@budibase/types": "0.0.0"
|
||||||
|
},
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "0.0.1",
|
"version": "0.0.0",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "dist/src/index.js",
|
"main": "dist/src/index.js",
|
||||||
"types": "dist/src/index.d.ts",
|
"types": "dist/src/index.d.ts",
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/nano": "10.1.2",
|
"@budibase/nano": "10.1.2",
|
||||||
"@budibase/pouchdb-replication-stream": "1.2.10",
|
"@budibase/pouchdb-replication-stream": "1.2.10",
|
||||||
"@budibase/types": "0.0.1",
|
"@budibase/types": "0.0.0",
|
||||||
"@shopify/jest-koa-mocks": "5.0.1",
|
"@shopify/jest-koa-mocks": "5.0.1",
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
"aws-cloudfront-sign": "2.2.0",
|
"aws-cloudfront-sign": "2.2.0",
|
||||||
|
@ -33,7 +33,7 @@
|
||||||
"correlation-id": "4.0.0",
|
"correlation-id": "4.0.0",
|
||||||
"dotenv": "16.0.1",
|
"dotenv": "16.0.1",
|
||||||
"emitter-listener": "1.1.2",
|
"emitter-listener": "1.1.2",
|
||||||
"ioredis": "4.28.0",
|
"ioredis": "5.3.2",
|
||||||
"joi": "17.6.0",
|
"joi": "17.6.0",
|
||||||
"jsonwebtoken": "9.0.0",
|
"jsonwebtoken": "9.0.0",
|
||||||
"koa-passport": "4.1.4",
|
"koa-passport": "4.1.4",
|
||||||
|
@ -62,7 +62,6 @@
|
||||||
"@swc/jest": "^0.2.24",
|
"@swc/jest": "^0.2.24",
|
||||||
"@trendyol/jest-testcontainers": "^2.1.1",
|
"@trendyol/jest-testcontainers": "^2.1.1",
|
||||||
"@types/chance": "1.1.3",
|
"@types/chance": "1.1.3",
|
||||||
"@types/ioredis": "4.28.0",
|
|
||||||
"@types/jest": "29.5.0",
|
"@types/jest": "29.5.0",
|
||||||
"@types/koa": "2.13.4",
|
"@types/koa": "2.13.4",
|
||||||
"@types/lodash": "4.14.180",
|
"@types/lodash": "4.14.180",
|
||||||
|
@ -74,7 +73,7 @@
|
||||||
"@types/tar-fs": "2.0.1",
|
"@types/tar-fs": "2.0.1",
|
||||||
"@types/uuid": "8.3.4",
|
"@types/uuid": "8.3.4",
|
||||||
"chance": "1.1.8",
|
"chance": "1.1.8",
|
||||||
"ioredis-mock": "5.8.0",
|
"ioredis-mock": "8.7.0",
|
||||||
"jest": "29.5.0",
|
"jest": "29.5.0",
|
||||||
"jest-environment-node": "29.5.0",
|
"jest-environment-node": "29.5.0",
|
||||||
"jest-serial-runner": "^1.2.1",
|
"jest-serial-runner": "^1.2.1",
|
||||||
|
|
|
@ -72,16 +72,12 @@ describe("writethrough", () => {
|
||||||
writethrough.put({ ...current, value: 4 }),
|
writethrough.put({ ...current, value: 4 }),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// with a lock, this will work
|
||||||
const newRev = responses.map(x => x.rev).find(x => x !== current._rev)
|
const newRev = responses.map(x => x.rev).find(x => x !== current._rev)
|
||||||
expect(newRev).toBeDefined()
|
expect(newRev).toBeDefined()
|
||||||
expect(responses.map(x => x.rev)).toEqual(
|
expect(responses.map(x => x.rev)).toEqual(
|
||||||
expect.arrayContaining([current._rev, current._rev, newRev])
|
expect.arrayContaining([current._rev, current._rev, newRev])
|
||||||
)
|
)
|
||||||
expectFunctionWasCalledTimesWith(
|
|
||||||
mocks.alerts.logWarn,
|
|
||||||
2,
|
|
||||||
"Ignoring redlock conflict in write-through cache"
|
|
||||||
)
|
|
||||||
|
|
||||||
const output = await db.get(current._id)
|
const output = await db.get(current._id)
|
||||||
expect(output.value).toBe(4)
|
expect(output.value).toBe(4)
|
||||||
|
|
|
@ -16,6 +16,7 @@ export enum Header {
|
||||||
LICENSE_KEY = "x-budibase-license-key",
|
LICENSE_KEY = "x-budibase-license-key",
|
||||||
API_VER = "x-budibase-api-version",
|
API_VER = "x-budibase-api-version",
|
||||||
APP_ID = "x-budibase-app-id",
|
APP_ID = "x-budibase-app-id",
|
||||||
|
SESSION_ID = "x-budibase-session-id",
|
||||||
TYPE = "x-budibase-type",
|
TYPE = "x-budibase-type",
|
||||||
PREVIEW_ROLE = "x-budibase-role",
|
PREVIEW_ROLE = "x-budibase-role",
|
||||||
TENANT_ID = "x-budibase-tenant-id",
|
TENANT_ID = "x-budibase-tenant-id",
|
||||||
|
|
|
@ -97,7 +97,6 @@ const environment = {
|
||||||
REDIS_URL: process.env.REDIS_URL || "localhost:6379",
|
REDIS_URL: process.env.REDIS_URL || "localhost:6379",
|
||||||
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
||||||
REDIS_CLUSTERED: process.env.REDIS_CLUSTERED,
|
REDIS_CLUSTERED: process.env.REDIS_CLUSTERED,
|
||||||
MOCK_REDIS: process.env.MOCK_REDIS,
|
|
||||||
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_REGION: process.env.AWS_REGION,
|
AWS_REGION: process.env.AWS_REGION,
|
||||||
|
@ -129,6 +128,7 @@ const environment = {
|
||||||
PLUGIN_BUCKET_NAME:
|
PLUGIN_BUCKET_NAME:
|
||||||
process.env.PLUGIN_BUCKET_NAME || DefaultBucketName.PLUGINS,
|
process.env.PLUGIN_BUCKET_NAME || DefaultBucketName.PLUGINS,
|
||||||
USE_COUCH: process.env.USE_COUCH || true,
|
USE_COUCH: process.env.USE_COUCH || true,
|
||||||
|
MOCK_REDIS: process.env.MOCK_REDIS,
|
||||||
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
|
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
|
||||||
SERVICE: process.env.SERVICE || "budibase",
|
SERVICE: process.env.SERVICE || "budibase",
|
||||||
LOG_LEVEL: process.env.LOG_LEVEL || "info",
|
LOG_LEVEL: process.env.LOG_LEVEL || "info",
|
||||||
|
|
|
@ -21,6 +21,7 @@ export * as context from "./context"
|
||||||
export * as cache from "./cache"
|
export * as cache from "./cache"
|
||||||
export * as objectStore from "./objectStore"
|
export * as objectStore from "./objectStore"
|
||||||
export * as redis from "./redis"
|
export * as redis from "./redis"
|
||||||
|
export { Client as RedisClient } from "./redis"
|
||||||
export * as locks from "./redis/redlockImpl"
|
export * as locks from "./redis/redlockImpl"
|
||||||
export * as utils from "./utils"
|
export * as utils from "./utils"
|
||||||
export * as errors from "./errors"
|
export * as errors from "./errors"
|
||||||
|
|
|
@ -96,6 +96,7 @@ if (!env.DISABLE_PINO_LOGGER) {
|
||||||
|
|
||||||
const mergingObject: any = {
|
const mergingObject: any = {
|
||||||
err: error,
|
err: error,
|
||||||
|
pid: process.pid,
|
||||||
...contextObject,
|
...contextObject,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,8 @@ let userClient: Client,
|
||||||
appClient: Client,
|
appClient: Client,
|
||||||
cacheClient: Client,
|
cacheClient: Client,
|
||||||
writethroughClient: Client,
|
writethroughClient: Client,
|
||||||
lockClient: Client
|
lockClient: Client,
|
||||||
|
socketClient: Client
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
userClient = await new Client(utils.Databases.USER_CACHE).init()
|
userClient = await new Client(utils.Databases.USER_CACHE).init()
|
||||||
|
@ -14,9 +15,10 @@ async function init() {
|
||||||
appClient = await new Client(utils.Databases.APP_METADATA).init()
|
appClient = await new Client(utils.Databases.APP_METADATA).init()
|
||||||
cacheClient = await new Client(utils.Databases.GENERIC_CACHE).init()
|
cacheClient = await new Client(utils.Databases.GENERIC_CACHE).init()
|
||||||
lockClient = await new Client(utils.Databases.LOCKS).init()
|
lockClient = await new Client(utils.Databases.LOCKS).init()
|
||||||
writethroughClient = await new Client(
|
writethroughClient = await new Client(utils.Databases.WRITE_THROUGH).init()
|
||||||
utils.Databases.WRITE_THROUGH,
|
socketClient = await new Client(
|
||||||
utils.SelectableDatabase.WRITE_THROUGH
|
utils.Databases.SOCKET_IO,
|
||||||
|
utils.SelectableDatabase.SOCKET_IO
|
||||||
).init()
|
).init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +29,7 @@ export async function shutdown() {
|
||||||
if (cacheClient) await cacheClient.finish()
|
if (cacheClient) await cacheClient.finish()
|
||||||
if (writethroughClient) await writethroughClient.finish()
|
if (writethroughClient) await writethroughClient.finish()
|
||||||
if (lockClient) await lockClient.finish()
|
if (lockClient) await lockClient.finish()
|
||||||
|
if (socketClient) await socketClient.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on("exit", async () => {
|
process.on("exit", async () => {
|
||||||
|
@ -74,3 +77,10 @@ export async function getLockClient() {
|
||||||
}
|
}
|
||||||
return lockClient
|
return lockClient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getSocketClient() {
|
||||||
|
if (!socketClient) {
|
||||||
|
await init()
|
||||||
|
}
|
||||||
|
return socketClient
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
// ioredis mock is all in memory
|
import Redis from "ioredis"
|
||||||
const Redis = env.MOCK_REDIS ? require("ioredis-mock") : require("ioredis")
|
// mock-redis doesn't have any typing
|
||||||
|
let MockRedis: any | undefined
|
||||||
|
if (env.MOCK_REDIS) {
|
||||||
|
try {
|
||||||
|
// ioredis mock is all in memory
|
||||||
|
MockRedis = require("ioredis-mock")
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Mock redis unavailable")
|
||||||
|
}
|
||||||
|
}
|
||||||
import {
|
import {
|
||||||
addDbPrefix,
|
addDbPrefix,
|
||||||
removeDbPrefix,
|
removeDbPrefix,
|
||||||
|
@ -18,7 +27,7 @@ const DEFAULT_SELECT_DB = SelectableDatabase.DEFAULT
|
||||||
// for testing just generate the client once
|
// for testing just generate the client once
|
||||||
let CLOSED = false
|
let CLOSED = false
|
||||||
let CLIENTS: { [key: number]: any } = {}
|
let CLIENTS: { [key: number]: any } = {}
|
||||||
|
0
|
||||||
let CONNECTED = false
|
let CONNECTED = false
|
||||||
|
|
||||||
// mock redis always connected
|
// mock redis always connected
|
||||||
|
@ -55,6 +64,7 @@ function connectionError(
|
||||||
* will return the ioredis client which will be ready to use.
|
* will return the ioredis client which will be ready to use.
|
||||||
*/
|
*/
|
||||||
function init(selectDb = DEFAULT_SELECT_DB) {
|
function init(selectDb = DEFAULT_SELECT_DB) {
|
||||||
|
const RedisCore = env.MOCK_REDIS && MockRedis ? MockRedis : Redis
|
||||||
let timeout: NodeJS.Timeout
|
let timeout: NodeJS.Timeout
|
||||||
CLOSED = false
|
CLOSED = false
|
||||||
let client = pickClient(selectDb)
|
let client = pickClient(selectDb)
|
||||||
|
@ -64,7 +74,7 @@ function init(selectDb = DEFAULT_SELECT_DB) {
|
||||||
}
|
}
|
||||||
// testing uses a single in memory client
|
// testing uses a single in memory client
|
||||||
if (env.MOCK_REDIS) {
|
if (env.MOCK_REDIS) {
|
||||||
CLIENTS[selectDb] = new Redis(getRedisOptions())
|
CLIENTS[selectDb] = new RedisCore(getRedisOptions())
|
||||||
}
|
}
|
||||||
// start the timer - only allowed 5 seconds to connect
|
// start the timer - only allowed 5 seconds to connect
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
|
@ -84,11 +94,11 @@ function init(selectDb = DEFAULT_SELECT_DB) {
|
||||||
const { redisProtocolUrl, opts, host, port } = getRedisOptions()
|
const { redisProtocolUrl, opts, host, port } = getRedisOptions()
|
||||||
|
|
||||||
if (CLUSTERED) {
|
if (CLUSTERED) {
|
||||||
client = new Redis.Cluster([{ host, port }], opts)
|
client = new RedisCore.Cluster([{ host, port }], opts)
|
||||||
} else if (redisProtocolUrl) {
|
} else if (redisProtocolUrl) {
|
||||||
client = new Redis(redisProtocolUrl)
|
client = new RedisCore(redisProtocolUrl)
|
||||||
} else {
|
} else {
|
||||||
client = new Redis(opts)
|
client = new RedisCore(opts)
|
||||||
}
|
}
|
||||||
// attach handlers
|
// attach handlers
|
||||||
client.on("end", (err: Error) => {
|
client.on("end", (err: Error) => {
|
||||||
|
@ -183,6 +193,9 @@ class RedisWrapper {
|
||||||
CLOSED = false
|
CLOSED = false
|
||||||
init(this._select)
|
init(this._select)
|
||||||
await waitForConnection(this._select)
|
await waitForConnection(this._select)
|
||||||
|
if (this._select && !env.isTest()) {
|
||||||
|
this.getClient().select(this._select)
|
||||||
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,6 +222,11 @@ class RedisWrapper {
|
||||||
return this.getClient().keys(addDbPrefix(db, pattern))
|
return this.getClient().keys(addDbPrefix(db, pattern))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async exists(key: string) {
|
||||||
|
const db = this._db
|
||||||
|
return await this.getClient().exists(addDbPrefix(db, key))
|
||||||
|
}
|
||||||
|
|
||||||
async get(key: string) {
|
async get(key: string) {
|
||||||
const db = this._db
|
const db = this._db
|
||||||
let response = await this.getClient().get(addDbPrefix(db, key))
|
let response = await this.getClient().get(addDbPrefix(db, key))
|
||||||
|
|
|
@ -4,10 +4,10 @@ import { LockOptions, LockType } from "@budibase/types"
|
||||||
import * as context from "../context"
|
import * as context from "../context"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
|
|
||||||
const getClient = async (
|
async function getClient(
|
||||||
type: LockType,
|
type: LockType,
|
||||||
opts?: Redlock.Options
|
opts?: Redlock.Options
|
||||||
): Promise<Redlock> => {
|
): Promise<Redlock> {
|
||||||
if (type === LockType.CUSTOM) {
|
if (type === LockType.CUSTOM) {
|
||||||
return newRedlock(opts)
|
return newRedlock(opts)
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,9 @@ const getClient = async (
|
||||||
case LockType.TRY_ONCE: {
|
case LockType.TRY_ONCE: {
|
||||||
return newRedlock(OPTIONS.TRY_ONCE)
|
return newRedlock(OPTIONS.TRY_ONCE)
|
||||||
}
|
}
|
||||||
|
case LockType.TRY_TWICE: {
|
||||||
|
return newRedlock(OPTIONS.TRY_TWICE)
|
||||||
|
}
|
||||||
case LockType.DEFAULT: {
|
case LockType.DEFAULT: {
|
||||||
return newRedlock(OPTIONS.DEFAULT)
|
return newRedlock(OPTIONS.DEFAULT)
|
||||||
}
|
}
|
||||||
|
@ -35,6 +38,9 @@ const OPTIONS = {
|
||||||
// immediately throws an error if the lock is already held
|
// immediately throws an error if the lock is already held
|
||||||
retryCount: 0,
|
retryCount: 0,
|
||||||
},
|
},
|
||||||
|
TRY_TWICE: {
|
||||||
|
retryCount: 1,
|
||||||
|
},
|
||||||
TEST: {
|
TEST: {
|
||||||
// higher retry count in unit tests
|
// higher retry count in unit tests
|
||||||
// due to high contention.
|
// due to high contention.
|
||||||
|
@ -62,7 +68,7 @@ const OPTIONS = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRedlock = async (opts: Redlock.Options = {}) => {
|
export async function newRedlock(opts: Redlock.Options = {}) {
|
||||||
let options = { ...OPTIONS.DEFAULT, ...opts }
|
let options = { ...OPTIONS.DEFAULT, ...opts }
|
||||||
const redisWrapper = await getLockClient()
|
const redisWrapper = await getLockClient()
|
||||||
const client = redisWrapper.getClient()
|
const client = redisWrapper.getClient()
|
||||||
|
@ -81,22 +87,26 @@ type RedlockExecution<T> =
|
||||||
| SuccessfulRedlockExecution<T>
|
| SuccessfulRedlockExecution<T>
|
||||||
| UnsuccessfulRedlockExecution
|
| UnsuccessfulRedlockExecution
|
||||||
|
|
||||||
export const doWithLock = async <T>(
|
function getLockName(opts: LockOptions) {
|
||||||
|
// determine lock name
|
||||||
|
// by default use the tenantId for uniqueness, unless using a system lock
|
||||||
|
const prefix = opts.systemLock ? "system" : context.getTenantId()
|
||||||
|
let name: string = `lock:${prefix}_${opts.name}`
|
||||||
|
// add additional unique name if required
|
||||||
|
if (opts.resource) {
|
||||||
|
name = name + `_${opts.resource}`
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function doWithLock<T>(
|
||||||
opts: LockOptions,
|
opts: LockOptions,
|
||||||
task: () => Promise<T>
|
task: () => Promise<T>
|
||||||
): Promise<RedlockExecution<T>> => {
|
): Promise<RedlockExecution<T>> {
|
||||||
const redlock = await getClient(opts.type, opts.customOptions)
|
const redlock = await getClient(opts.type, opts.customOptions)
|
||||||
let lock
|
let lock
|
||||||
try {
|
try {
|
||||||
// determine lock name
|
const name = getLockName(opts)
|
||||||
// by default use the tenantId for uniqueness, unless using a system lock
|
|
||||||
const prefix = opts.systemLock ? "system" : context.getTenantId()
|
|
||||||
let name: string = `lock:${prefix}_${opts.name}`
|
|
||||||
|
|
||||||
// add additional unique name if required
|
|
||||||
if (opts.resource) {
|
|
||||||
name = name + `_${opts.resource}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// create the lock
|
// create the lock
|
||||||
lock = await redlock.lock(name, opts.ttl)
|
lock = await redlock.lock(name, opts.ttl)
|
||||||
|
@ -112,7 +122,6 @@ export const doWithLock = async <T>(
|
||||||
if (opts.type === LockType.TRY_ONCE) {
|
if (opts.type === LockType.TRY_ONCE) {
|
||||||
// don't throw for try-once locks, they will always error
|
// don't throw for try-once locks, they will always error
|
||||||
// due to retry count (0) exceeded
|
// due to retry count (0) exceeded
|
||||||
console.warn(e)
|
|
||||||
return { executed: false }
|
return { executed: false }
|
||||||
} else {
|
} else {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|
|
@ -27,6 +27,7 @@ export enum Databases {
|
||||||
GENERIC_CACHE = "data_cache",
|
GENERIC_CACHE = "data_cache",
|
||||||
WRITE_THROUGH = "writeThrough",
|
WRITE_THROUGH = "writeThrough",
|
||||||
LOCKS = "locks",
|
LOCKS = "locks",
|
||||||
|
SOCKET_IO = "socket_io",
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,7 +41,7 @@ export enum Databases {
|
||||||
*/
|
*/
|
||||||
export enum SelectableDatabase {
|
export enum SelectableDatabase {
|
||||||
DEFAULT = 0,
|
DEFAULT = 0,
|
||||||
WRITE_THROUGH = 1,
|
SOCKET_IO = 1,
|
||||||
UNUSED_1 = 2,
|
UNUSED_1 = 2,
|
||||||
UNUSED_2 = 3,
|
UNUSED_2 = 3,
|
||||||
UNUSED_3 = 4,
|
UNUSED_3 = 4,
|
||||||
|
@ -94,7 +95,7 @@ export function getRedisOptions() {
|
||||||
opts.port = port
|
opts.port = port
|
||||||
opts.password = password
|
opts.password = password
|
||||||
}
|
}
|
||||||
return { opts, host, port, redisProtocolUrl }
|
return { opts, host, port: parseInt(port), redisProtocolUrl }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addDbPrefix(db: string, key: string) {
|
export function addDbPrefix(db: string, key: string) {
|
||||||
|
|
|
@ -90,6 +90,10 @@ export const useScimIntegration = () => {
|
||||||
return useFeature(Feature.SCIM)
|
return useFeature(Feature.SCIM)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useSyncAutomations = () => {
|
||||||
|
return useFeature(Feature.SYNC_AUTOMATIONS)
|
||||||
|
}
|
||||||
|
|
||||||
// QUOTAS
|
// QUOTAS
|
||||||
|
|
||||||
export const setAutomationLogsQuota = (value: number) => {
|
export const setAutomationLogsQuota = (value: number) => {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "0.0.1",
|
"version": "0.0.0",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
@ -38,8 +38,8 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "1.2.1",
|
"@adobe/spectrum-css-workflow-icons": "1.2.1",
|
||||||
"@budibase/shared-core": "0.0.1",
|
"@budibase/shared-core": "0.0.0",
|
||||||
"@budibase/string-templates": "0.0.1",
|
"@budibase/string-templates": "0.0.0",
|
||||||
"@spectrum-css/accordion": "3.0.24",
|
"@spectrum-css/accordion": "3.0.24",
|
||||||
"@spectrum-css/actionbutton": "1.0.1",
|
"@spectrum-css/actionbutton": "1.0.1",
|
||||||
"@spectrum-css/actiongroup": "1.0.1",
|
"@spectrum-css/actiongroup": "1.0.1",
|
||||||
|
|
|
@ -56,6 +56,8 @@ export default function positionDropdown(element, opts) {
|
||||||
styles.left = anchorBounds.left + anchorBounds.width - elementBounds.width
|
styles.left = anchorBounds.left + anchorBounds.width - elementBounds.width
|
||||||
} else if (align === "right-outside") {
|
} else if (align === "right-outside") {
|
||||||
styles.left = anchorBounds.right + offset
|
styles.left = anchorBounds.right + offset
|
||||||
|
} else if (align === "left-outside") {
|
||||||
|
styles.left = anchorBounds.left - elementBounds.width - offset
|
||||||
} else {
|
} else {
|
||||||
styles.left = anchorBounds.left
|
styles.left = anchorBounds.left
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,12 @@
|
||||||
export let url = ""
|
export let url = ""
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let initials = "JD"
|
export let initials = "JD"
|
||||||
|
export let color = null
|
||||||
|
|
||||||
const DefaultColor = "#3aab87"
|
const DefaultColor = "#3aab87"
|
||||||
|
|
||||||
$: color = getColor(initials)
|
$: avatarColor = color || getColor(initials)
|
||||||
|
$: style = getStyle(size, avatarColor)
|
||||||
|
|
||||||
const getColor = initials => {
|
const getColor = initials => {
|
||||||
if (!initials?.length) {
|
if (!initials?.length) {
|
||||||
|
@ -26,6 +28,12 @@
|
||||||
const hue = ((code % 26) / 26) * 360
|
const hue = ((code % 26) / 26) * 360
|
||||||
return `hsl(${hue}, 50%, 50%)`
|
return `hsl(${hue}, 50%, 50%)`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getStyle = (sizeKey, color) => {
|
||||||
|
const size = `var(${sizes.get(sizeKey)})`
|
||||||
|
const fontSize = `calc(${size} / 2)`
|
||||||
|
return `width:${size}; height:${size}; font-size:${fontSize}; background:${color};`
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if url}
|
{#if url}
|
||||||
|
@ -37,13 +45,7 @@
|
||||||
style="width: var({sizes.get(size)}); height: var({sizes.get(size)});"
|
style="width: var({sizes.get(size)}); height: var({sizes.get(size)});"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div class="spectrum-Avatar" class:is-disabled={disabled} {style}>
|
||||||
class="spectrum-Avatar"
|
|
||||||
class:is-disabled={disabled}
|
|
||||||
style="width: var({sizes.get(size)}); height: var({sizes.get(
|
|
||||||
size
|
|
||||||
)}); font-size: calc(var({sizes.get(size)}) / 2); background: {color};"
|
|
||||||
>
|
|
||||||
{initials || ""}
|
{initials || ""}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -3,11 +3,13 @@
|
||||||
import Button from "../Button/Button.svelte"
|
import Button from "../Button/Button.svelte"
|
||||||
import Body from "../Typography/Body.svelte"
|
import Body from "../Typography/Body.svelte"
|
||||||
import Heading from "../Typography/Heading.svelte"
|
import Heading from "../Typography/Heading.svelte"
|
||||||
|
import { setContext } from "svelte"
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let fillWidth
|
export let fillWidth
|
||||||
export let left = "314px"
|
export let left = "314px"
|
||||||
export let width = "calc(100% - 626px)"
|
export let width = "calc(100% - 626px)"
|
||||||
|
export let headless = false
|
||||||
|
|
||||||
let visible = false
|
let visible = false
|
||||||
|
|
||||||
|
@ -25,6 +27,11 @@
|
||||||
visible = false
|
visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setContext("drawer-actions", {
|
||||||
|
hide,
|
||||||
|
show,
|
||||||
|
})
|
||||||
|
|
||||||
const easeInOutQuad = x => {
|
const easeInOutQuad = x => {
|
||||||
return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2
|
return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2
|
||||||
}
|
}
|
||||||
|
@ -47,27 +54,34 @@
|
||||||
<section
|
<section
|
||||||
class:fillWidth
|
class:fillWidth
|
||||||
class="drawer"
|
class="drawer"
|
||||||
|
class:headless
|
||||||
transition:slide|local
|
transition:slide|local
|
||||||
style={`width: ${width}; left: ${left};`}
|
style={`width: ${width}; left: ${left};`}
|
||||||
>
|
>
|
||||||
<header>
|
{#if !headless}
|
||||||
<div class="text">
|
<header>
|
||||||
<Heading size="XS">{title}</Heading>
|
<div class="text">
|
||||||
<Body size="S">
|
<Heading size="XS">{title}</Heading>
|
||||||
<slot name="description" />
|
<Body size="S">
|
||||||
</Body>
|
<slot name="description" />
|
||||||
</div>
|
</Body>
|
||||||
<div class="buttons">
|
</div>
|
||||||
<Button secondary quiet on:click={hide}>Cancel</Button>
|
<div class="buttons">
|
||||||
<slot name="buttons" />
|
<Button secondary quiet on:click={hide}>Cancel</Button>
|
||||||
</div>
|
<slot name="buttons" />
|
||||||
</header>
|
</div>
|
||||||
|
</header>
|
||||||
|
{/if}
|
||||||
<slot name="body" />
|
<slot name="body" />
|
||||||
</section>
|
</section>
|
||||||
</Portal>
|
</Portal>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.drawer.headless :global(.drawer-contents) {
|
||||||
|
height: calc(40vh + 75px);
|
||||||
|
}
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
|
|
|
@ -165,7 +165,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if !disabled}
|
{#if !disabled}
|
||||||
<div class="delete-button" on:click={removeFile}>
|
<div class="delete-button" on:click={removeFile}>
|
||||||
<Icon name="Close" />
|
<Icon name="Delete" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -209,7 +209,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if !disabled}
|
{#if !disabled}
|
||||||
<div class="delete-button" on:click={removeFile}>
|
<div class="delete-button" on:click={removeFile}>
|
||||||
<Icon name="Close" />
|
<Icon name="Delete" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
export let emphasized = false
|
export let emphasized = false
|
||||||
export let onTop = false
|
export let onTop = false
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
|
export let beforeSwitch = null
|
||||||
|
|
||||||
let thisSelected = undefined
|
let thisSelected = undefined
|
||||||
|
|
||||||
|
@ -28,9 +29,18 @@
|
||||||
thisSelected = selected
|
thisSelected = selected
|
||||||
dispatch("select", thisSelected)
|
dispatch("select", thisSelected)
|
||||||
} else if ($tab.title !== thisSelected) {
|
} else if ($tab.title !== thisSelected) {
|
||||||
thisSelected = $tab.title
|
if (typeof beforeSwitch == "function") {
|
||||||
selected = $tab.title
|
const proceed = beforeSwitch($tab.title)
|
||||||
dispatch("select", thisSelected)
|
if (proceed) {
|
||||||
|
thisSelected = $tab.title
|
||||||
|
selected = $tab.title
|
||||||
|
dispatch("select", thisSelected)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
thisSelected = $tab.title
|
||||||
|
selected = $tab.title
|
||||||
|
dispatch("select", thisSelected)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ($tab.title !== thisSelected) {
|
if ($tab.title !== thisSelected) {
|
||||||
tab.update(state => {
|
tab.update(state => {
|
||||||
|
|
|
@ -31,4 +31,12 @@
|
||||||
.spectrum-Tooltip-tip {
|
.spectrum-Tooltip-tip {
|
||||||
border-top-color: var(--spectrum-global-color-gray-500);
|
border-top-color: var(--spectrum-global-color-gray-500);
|
||||||
}
|
}
|
||||||
|
.spectrum-Tooltip {
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
.spectrum-Tooltip-label {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "0.0.1",
|
"version": "0.0.0",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -58,11 +58,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "0.0.1",
|
"@budibase/bbui": "0.0.0",
|
||||||
"@budibase/frontend-core": "0.0.1",
|
"@budibase/frontend-core": "0.0.0",
|
||||||
"@budibase/shared-core": "0.0.1",
|
"@budibase/shared-core": "0.0.0",
|
||||||
"@budibase/string-templates": "0.0.1",
|
"@budibase/string-templates": "0.0.0",
|
||||||
"@budibase/types": "0.0.1",
|
"@budibase/types": "0.0.0",
|
||||||
|
"@codemirror/autocomplete": "^6.7.1",
|
||||||
|
"@codemirror/commands": "^6.2.4",
|
||||||
|
"@codemirror/lang-javascript": "^6.1.8",
|
||||||
|
"@codemirror/language": "^6.6.0",
|
||||||
|
"@codemirror/state": "^6.2.0",
|
||||||
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
|
"@codemirror/view": "^6.11.2",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||||
|
|
|
@ -77,7 +77,7 @@ export const getAuthBindings = () => {
|
||||||
runtime: `${safeUser}.${safeOAuth2}.${safeAccessToken}`,
|
runtime: `${safeUser}.${safeOAuth2}.${safeAccessToken}`,
|
||||||
readable: `Current User.OAuthToken`,
|
readable: `Current User.OAuthToken`,
|
||||||
key: "accessToken",
|
key: "accessToken",
|
||||||
display: { name: "OAuthToken" },
|
display: { name: "OAuthToken", type: "text" },
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -434,6 +434,9 @@ export const getUserBindings = () => {
|
||||||
providerId: "user",
|
providerId: "user",
|
||||||
category: "Current User",
|
category: "Current User",
|
||||||
icon: "User",
|
icon: "User",
|
||||||
|
display: {
|
||||||
|
name: key,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return acc
|
return acc
|
||||||
}, [])
|
}, [])
|
||||||
|
@ -550,7 +553,7 @@ const getUrlBindings = asset => {
|
||||||
readableBinding: `URL.${param}`,
|
readableBinding: `URL.${param}`,
|
||||||
category: "URL",
|
category: "URL",
|
||||||
icon: "RailTop",
|
icon: "RailTop",
|
||||||
display: { type: "string" },
|
display: { type: "string", name: param },
|
||||||
}))
|
}))
|
||||||
const queryParamsBinding = {
|
const queryParamsBinding = {
|
||||||
type: "context",
|
type: "context",
|
||||||
|
@ -558,7 +561,7 @@ const getUrlBindings = asset => {
|
||||||
readableBinding: "Query params",
|
readableBinding: "Query params",
|
||||||
category: "URL",
|
category: "URL",
|
||||||
icon: "RailTop",
|
icon: "RailTop",
|
||||||
display: { type: "object" },
|
display: { type: "object", name: "Query params" },
|
||||||
}
|
}
|
||||||
return urlParamBindings.concat([queryParamsBinding])
|
return urlParamBindings.concat([queryParamsBinding])
|
||||||
}
|
}
|
||||||
|
@ -589,7 +592,6 @@ export const getEventContextBindings = (
|
||||||
actionId
|
actionId
|
||||||
) => {
|
) => {
|
||||||
let bindings = []
|
let bindings = []
|
||||||
|
|
||||||
// Check if any context bindings are provided by the component for this
|
// Check if any context bindings are provided by the component for this
|
||||||
// setting
|
// setting
|
||||||
const component = findComponent(asset.props, componentId)
|
const component = findComponent(asset.props, componentId)
|
||||||
|
@ -605,6 +607,9 @@ export const getEventContextBindings = (
|
||||||
)}`,
|
)}`,
|
||||||
category: component._instanceName,
|
category: component._instanceName,
|
||||||
icon: def.icon,
|
icon: def.icon,
|
||||||
|
display: {
|
||||||
|
name: contextEntry.label,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -628,6 +633,9 @@ export const getEventContextBindings = (
|
||||||
runtimeBinding: `actions.${idx}.${contextValue.value}`,
|
runtimeBinding: `actions.${idx}.${contextValue.value}`,
|
||||||
category: "Actions",
|
category: "Actions",
|
||||||
icon: "JourneyAction",
|
icon: "JourneyAction",
|
||||||
|
display: {
|
||||||
|
name: contextValue.label,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { getFrontendStore } from "./store/frontend"
|
||||||
import { getAutomationStore } from "./store/automation"
|
import { getAutomationStore } from "./store/automation"
|
||||||
import { getTemporalStore } from "./store/temporal"
|
import { getTemporalStore } from "./store/temporal"
|
||||||
import { getThemeStore } from "./store/theme"
|
import { getThemeStore } from "./store/theme"
|
||||||
|
import { getUserStore } from "./store/users"
|
||||||
import { derived } from "svelte/store"
|
import { derived } from "svelte/store"
|
||||||
import { findComponent, findComponentPath } from "./componentUtils"
|
import { findComponent, findComponentPath } from "./componentUtils"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
|
@ -12,6 +13,7 @@ export const store = getFrontendStore()
|
||||||
export const automationStore = getAutomationStore()
|
export const automationStore = getAutomationStore()
|
||||||
export const themeStore = getThemeStore()
|
export const themeStore = getThemeStore()
|
||||||
export const temporalStore = getTemporalStore()
|
export const temporalStore = getTemporalStore()
|
||||||
|
export const userStore = getUserStore()
|
||||||
|
|
||||||
// Setup history for screens
|
// Setup history for screens
|
||||||
export const screenHistoryStore = createHistoryStore({
|
export const screenHistoryStore = createHistoryStore({
|
||||||
|
|
|
@ -37,8 +37,10 @@ import {
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { getComponentFieldOptions } from "helpers/formFields"
|
import { getComponentFieldOptions } from "helpers/formFields"
|
||||||
|
import { createBuilderWebsocket } from "builderStore/websocket"
|
||||||
|
|
||||||
const INITIAL_FRONTEND_STATE = {
|
const INITIAL_FRONTEND_STATE = {
|
||||||
|
initialised: false,
|
||||||
apps: [],
|
apps: [],
|
||||||
name: "",
|
name: "",
|
||||||
url: "",
|
url: "",
|
||||||
|
@ -69,7 +71,9 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
customTheme: {},
|
customTheme: {},
|
||||||
previewDevice: "desktop",
|
previewDevice: "desktop",
|
||||||
highlightedSettingKey: null,
|
highlightedSettingKey: null,
|
||||||
|
propertyFocus: null,
|
||||||
builderSidePanel: false,
|
builderSidePanel: false,
|
||||||
|
hasLock: true,
|
||||||
|
|
||||||
// URL params
|
// URL params
|
||||||
selectedScreenId: null,
|
selectedScreenId: null,
|
||||||
|
@ -86,6 +90,7 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
|
|
||||||
export const getFrontendStore = () => {
|
export const getFrontendStore = () => {
|
||||||
const store = writable({ ...INITIAL_FRONTEND_STATE })
|
const store = writable({ ...INITIAL_FRONTEND_STATE })
|
||||||
|
let websocket
|
||||||
|
|
||||||
// This is a fake implementation of a "patch" API endpoint to try and prevent
|
// This is a fake implementation of a "patch" API endpoint to try and prevent
|
||||||
// 409s. All screen doc mutations (aside from creation) use this function,
|
// 409s. All screen doc mutations (aside from creation) use this function,
|
||||||
|
@ -110,10 +115,11 @@ export const getFrontendStore = () => {
|
||||||
store.actions = {
|
store.actions = {
|
||||||
reset: () => {
|
reset: () => {
|
||||||
store.set({ ...INITIAL_FRONTEND_STATE })
|
store.set({ ...INITIAL_FRONTEND_STATE })
|
||||||
|
websocket?.disconnect()
|
||||||
},
|
},
|
||||||
initialise: async pkg => {
|
initialise: async pkg => {
|
||||||
const { layouts, screens, application, clientLibPath } = pkg
|
const { layouts, screens, application, clientLibPath, hasLock } = pkg
|
||||||
|
websocket = createBuilderWebsocket(application.appId)
|
||||||
await store.actions.components.refreshDefinitions(application.appId)
|
await store.actions.components.refreshDefinitions(application.appId)
|
||||||
|
|
||||||
// Reset store state
|
// Reset store state
|
||||||
|
@ -137,6 +143,8 @@ export const getFrontendStore = () => {
|
||||||
upgradableVersion: application.upgradableVersion,
|
upgradableVersion: application.upgradableVersion,
|
||||||
navigation: application.navigation || {},
|
navigation: application.navigation || {},
|
||||||
usedPlugins: application.usedPlugins || [],
|
usedPlugins: application.usedPlugins || [],
|
||||||
|
hasLock,
|
||||||
|
initialised: true,
|
||||||
}))
|
}))
|
||||||
screenHistoryStore.reset()
|
screenHistoryStore.reset()
|
||||||
automationHistoryStore.reset()
|
automationHistoryStore.reset()
|
||||||
|
@ -1319,6 +1327,12 @@ export const getFrontendStore = () => {
|
||||||
highlightedSettingKey: key,
|
highlightedSettingKey: key,
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
propertyFocus: key => {
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
propertyFocus: key,
|
||||||
|
}))
|
||||||
|
},
|
||||||
},
|
},
|
||||||
dnd: {
|
dnd: {
|
||||||
start: component => {
|
start: component => {
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { writable, get } from "svelte/store"
|
||||||
|
|
||||||
|
export const getUserStore = () => {
|
||||||
|
const store = writable([])
|
||||||
|
|
||||||
|
const init = users => {
|
||||||
|
store.set(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUser = user => {
|
||||||
|
const $users = get(store)
|
||||||
|
if (!$users.some(x => x.sessionId === user.sessionId)) {
|
||||||
|
store.set([...$users, user])
|
||||||
|
} else {
|
||||||
|
store.update(state => {
|
||||||
|
const index = state.findIndex(x => x.sessionId === user.sessionId)
|
||||||
|
state[index] = user
|
||||||
|
return state.slice()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeUser = sessionId => {
|
||||||
|
store.update(state => {
|
||||||
|
return state.filter(x => x.sessionId !== sessionId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
store.set([])
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...store,
|
||||||
|
actions: {
|
||||||
|
init,
|
||||||
|
updateUser,
|
||||||
|
removeUser,
|
||||||
|
reset,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { ActionStepID } from "constants/backend/automations"
|
||||||
import { TableNames } from "../constants"
|
import { TableNames } from "../constants"
|
||||||
import {
|
import {
|
||||||
AUTO_COLUMN_DISPLAY_NAMES,
|
AUTO_COLUMN_DISPLAY_NAMES,
|
||||||
|
@ -53,3 +54,9 @@ export function buildAutoColumn(tableName, name, subtype) {
|
||||||
}
|
}
|
||||||
return base
|
return base
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function checkForCollectStep(automation) {
|
||||||
|
return automation.definition.steps.some(
|
||||||
|
step => step.stepId === ActionStepID.COLLECT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { createWebsocket } from "@budibase/frontend-core"
|
||||||
|
import { userStore } from "builderStore"
|
||||||
|
import { datasources, tables } from "stores/backend"
|
||||||
|
import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core"
|
||||||
|
|
||||||
|
export const createBuilderWebsocket = appId => {
|
||||||
|
const socket = createWebsocket("/socket/builder")
|
||||||
|
|
||||||
|
// Built-in events
|
||||||
|
socket.on("connect", () => {
|
||||||
|
socket.emit(BuilderSocketEvent.SelectApp, appId, response => {
|
||||||
|
userStore.actions.init(response.users)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
socket.on("connect_error", err => {
|
||||||
|
console.log("Failed to connect to builder websocket:", err.message)
|
||||||
|
})
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
userStore.actions.reset()
|
||||||
|
})
|
||||||
|
|
||||||
|
// User events
|
||||||
|
socket.onOther(SocketEvent.UserUpdate, userStore.actions.updateUser)
|
||||||
|
socket.onOther(SocketEvent.UserDisconnect, userStore.actions.removeUser)
|
||||||
|
|
||||||
|
// Table events
|
||||||
|
socket.onOther(BuilderSocketEvent.TableChange, ({ id, table }) => {
|
||||||
|
tables.replaceTable(id, table)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Datasource events
|
||||||
|
socket.onOther(BuilderSocketEvent.DatasourceChange, ({ id, datasource }) => {
|
||||||
|
datasources.replaceDatasource(id, datasource)
|
||||||
|
})
|
||||||
|
|
||||||
|
return socket
|
||||||
|
}
|
|
@ -6,24 +6,48 @@
|
||||||
Body,
|
Body,
|
||||||
Icon,
|
Icon,
|
||||||
notifications,
|
notifications,
|
||||||
|
Tags,
|
||||||
|
Tag,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore, selectedAutomation } from "builderStore"
|
||||||
import { admin } from "stores/portal"
|
import { admin, licensing } from "stores/portal"
|
||||||
import { externalActions } from "./ExternalActions"
|
import { externalActions } from "./ExternalActions"
|
||||||
|
import { TriggerStepID } from "constants/backend/automations"
|
||||||
|
import { checkForCollectStep } from "builderStore/utils"
|
||||||
|
|
||||||
export let blockIdx
|
export let blockIdx
|
||||||
|
export let lastStep
|
||||||
|
|
||||||
const disabled = {
|
let syncAutomationsEnabled = $licensing.syncAutomationsEnabled
|
||||||
SEND_EMAIL_SMTP: {
|
let collectBlockAllowedSteps = [TriggerStepID.APP, TriggerStepID.WEBHOOK]
|
||||||
disabled: !$admin.checklist.smtp.checked,
|
|
||||||
message: "Please configure SMTP",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
let selectedAction
|
let selectedAction
|
||||||
let actionVal
|
let actionVal
|
||||||
let actions = Object.entries($automationStore.blockDefinitions.ACTION)
|
let actions = Object.entries($automationStore.blockDefinitions.ACTION)
|
||||||
|
|
||||||
|
$: collectBlockExists = checkForCollectStep($selectedAutomation)
|
||||||
|
|
||||||
|
const disabled = () => {
|
||||||
|
return {
|
||||||
|
SEND_EMAIL_SMTP: {
|
||||||
|
disabled: !$admin.checklist.smtp.checked,
|
||||||
|
message: "Please configure SMTP",
|
||||||
|
},
|
||||||
|
COLLECT: {
|
||||||
|
disabled: !lastStep || !syncAutomationsEnabled || collectBlockExists,
|
||||||
|
message: collectDisabledMessage(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectDisabledMessage = () => {
|
||||||
|
if (collectBlockExists) {
|
||||||
|
return "Only one Collect step allowed"
|
||||||
|
}
|
||||||
|
if (!lastStep) {
|
||||||
|
return "Only available as the last step"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const external = actions.reduce((acc, elm) => {
|
const external = actions.reduce((acc, elm) => {
|
||||||
const [k, v] = elm
|
const [k, v] = elm
|
||||||
if (!v.internal && !v.custom) {
|
if (!v.internal && !v.custom) {
|
||||||
|
@ -38,6 +62,15 @@
|
||||||
acc[k] = v
|
acc[k] = v
|
||||||
}
|
}
|
||||||
delete acc.LOOP
|
delete acc.LOOP
|
||||||
|
|
||||||
|
// Filter out Collect block if not App Action or Webhook
|
||||||
|
if (
|
||||||
|
!collectBlockAllowedSteps.includes(
|
||||||
|
$selectedAutomation.definition.trigger.stepId
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
delete acc.COLLECT
|
||||||
|
}
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
|
@ -48,7 +81,6 @@
|
||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
console.log(plugins)
|
|
||||||
|
|
||||||
const selectAction = action => {
|
const selectAction = action => {
|
||||||
actionVal = action
|
actionVal = action
|
||||||
|
@ -72,7 +104,7 @@
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Add automation step"
|
title="Add automation step"
|
||||||
confirmText="Save"
|
confirmText="Save"
|
||||||
size="M"
|
size="L"
|
||||||
disabled={!selectedAction}
|
disabled={!selectedAction}
|
||||||
onConfirm={addBlockToAutomation}
|
onConfirm={addBlockToAutomation}
|
||||||
>
|
>
|
||||||
|
@ -107,7 +139,7 @@
|
||||||
<Detail size="S">Actions</Detail>
|
<Detail size="S">Actions</Detail>
|
||||||
<div class="item-list">
|
<div class="item-list">
|
||||||
{#each Object.entries(internal) as [idx, action]}
|
{#each Object.entries(internal) as [idx, action]}
|
||||||
{@const isDisabled = disabled[idx] && disabled[idx].disabled}
|
{@const isDisabled = disabled()[idx] && disabled()[idx].disabled}
|
||||||
<div
|
<div
|
||||||
class="item"
|
class="item"
|
||||||
class:disabled={isDisabled}
|
class:disabled={isDisabled}
|
||||||
|
@ -117,8 +149,14 @@
|
||||||
<div class="item-body">
|
<div class="item-body">
|
||||||
<Icon name={action.icon} />
|
<Icon name={action.icon} />
|
||||||
<Body size="XS">{action.name}</Body>
|
<Body size="XS">{action.name}</Body>
|
||||||
{#if isDisabled}
|
{#if isDisabled && !syncAutomationsEnabled}
|
||||||
<Icon name="Help" tooltip={disabled[idx].message} />
|
<div class="tag-color">
|
||||||
|
<Tags>
|
||||||
|
<Tag icon="LockClosed">Business</Tag>
|
||||||
|
</Tags>
|
||||||
|
</div>
|
||||||
|
{:else if isDisabled}
|
||||||
|
<Icon name="Help" tooltip={disabled()[idx].message} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -152,6 +190,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-left: var(--spacing-m);
|
margin-left: var(--spacing-m);
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
.item-list {
|
.item-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -181,4 +220,8 @@
|
||||||
.disabled :global(.spectrum-Body) {
|
.disabled :global(.spectrum-Body) {
|
||||||
color: var(--spectrum-global-color-gray-600);
|
color: var(--spectrum-global-color-gray-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag-color :global(.spectrum-Tags-item) {
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -17,7 +17,11 @@
|
||||||
import ActionModal from "./ActionModal.svelte"
|
import ActionModal from "./ActionModal.svelte"
|
||||||
import FlowItemHeader from "./FlowItemHeader.svelte"
|
import FlowItemHeader from "./FlowItemHeader.svelte"
|
||||||
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
|
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
|
||||||
import { ActionStepID, TriggerStepID } from "constants/backend/automations"
|
import {
|
||||||
|
ActionStepID,
|
||||||
|
TriggerStepID,
|
||||||
|
Features,
|
||||||
|
} from "constants/backend/automations"
|
||||||
import { permissions } from "stores/backend"
|
import { permissions } from "stores/backend"
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
|
@ -31,6 +35,9 @@
|
||||||
let showLooping = false
|
let showLooping = false
|
||||||
let role
|
let role
|
||||||
|
|
||||||
|
$: collectBlockExists = $selectedAutomation.definition.steps.some(
|
||||||
|
step => step.stepId === ActionStepID.COLLECT
|
||||||
|
)
|
||||||
$: automationId = $selectedAutomation?._id
|
$: automationId = $selectedAutomation?._id
|
||||||
$: showBindingPicker =
|
$: showBindingPicker =
|
||||||
block.stepId === ActionStepID.CREATE_ROW ||
|
block.stepId === ActionStepID.CREATE_ROW ||
|
||||||
|
@ -184,7 +191,7 @@
|
||||||
{#if !isTrigger}
|
{#if !isTrigger}
|
||||||
<div>
|
<div>
|
||||||
<div class="block-options">
|
<div class="block-options">
|
||||||
{#if !loopBlock}
|
{#if block?.features?.[Features.LOOPING] || !block.features}
|
||||||
<ActionButton on:click={() => addLooping()} icon="Reuse">
|
<ActionButton on:click={() => addLooping()} icon="Reuse">
|
||||||
Add Looping
|
Add Looping
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
@ -224,21 +231,28 @@
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Modal bind:this={actionModal} width="30%">
|
|
||||||
<ActionModal {blockIdx} />
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal bind:this={webhookModal} width="30%">
|
|
||||||
<CreateWebhookModal />
|
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="separator" />
|
{#if !collectBlockExists || !lastStep}
|
||||||
<Icon on:click={() => actionModal.show()} hoverable name="AddCircle" size="S" />
|
|
||||||
{#if isTrigger ? totalBlocks > 1 : blockIdx !== totalBlocks - 2}
|
|
||||||
<div class="separator" />
|
<div class="separator" />
|
||||||
|
<Icon
|
||||||
|
on:click={() => actionModal.show()}
|
||||||
|
hoverable
|
||||||
|
name="AddCircle"
|
||||||
|
size="S"
|
||||||
|
/>
|
||||||
|
{#if isTrigger ? totalBlocks > 1 : blockIdx !== totalBlocks - 2}
|
||||||
|
<div class="separator" />
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<Modal bind:this={actionModal} width="30%">
|
||||||
|
<ActionModal {lastStep} {blockIdx} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal bind:this={webhookModal} width="30%">
|
||||||
|
<CreateWebhookModal />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.delete-padding {
|
.delete-padding {
|
||||||
padding-left: 30px;
|
padding-left: 30px;
|
||||||
|
|
|
@ -11,8 +11,8 @@
|
||||||
ActionButton,
|
ActionButton,
|
||||||
Drawer,
|
Drawer,
|
||||||
Modal,
|
Modal,
|
||||||
Detail,
|
|
||||||
notifications,
|
notifications,
|
||||||
|
Icon,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import { automationStore, selectedAutomation } from "builderStore"
|
import { automationStore, selectedAutomation } from "builderStore"
|
||||||
|
@ -27,9 +27,18 @@
|
||||||
import CronBuilder from "./CronBuilder.svelte"
|
import CronBuilder from "./CronBuilder.svelte"
|
||||||
import Editor from "components/integration/QueryEditor.svelte"
|
import Editor from "components/integration/QueryEditor.svelte"
|
||||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||||
|
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
||||||
|
import {
|
||||||
|
bindingsToCompletions,
|
||||||
|
jsAutocomplete,
|
||||||
|
EditorModes,
|
||||||
|
} from "components/common/CodeEditor"
|
||||||
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
||||||
import { LuceneUtils } from "@budibase/frontend-core"
|
import { LuceneUtils } from "@budibase/frontend-core"
|
||||||
import { getSchemaForTable } from "builderStore/dataBinding"
|
import {
|
||||||
|
getSchemaForTable,
|
||||||
|
getEnvironmentBindings,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
@ -43,7 +52,6 @@
|
||||||
let webhookModal
|
let webhookModal
|
||||||
let drawer
|
let drawer
|
||||||
let fillWidth = true
|
let fillWidth = true
|
||||||
let codeBindingOpen = false
|
|
||||||
let inputData
|
let inputData
|
||||||
|
|
||||||
$: filters = lookForFilters(schemaProperties) || []
|
$: filters = lookForFilters(schemaProperties) || []
|
||||||
|
@ -210,6 +218,19 @@
|
||||||
}
|
}
|
||||||
const outputs = Object.entries(schema)
|
const outputs = Object.entries(schema)
|
||||||
|
|
||||||
|
let bindingIcon = ""
|
||||||
|
let bindindingRank = 0
|
||||||
|
|
||||||
|
if (idx === 0) {
|
||||||
|
bindingIcon = automation.trigger.icon
|
||||||
|
} else if (isLoopBlock) {
|
||||||
|
bindingIcon = "Reuse"
|
||||||
|
bindindingRank = idx + 1
|
||||||
|
} else {
|
||||||
|
bindingIcon = allSteps[idx].icon
|
||||||
|
bindindingRank = idx - loopBlockCount
|
||||||
|
}
|
||||||
|
|
||||||
bindings = bindings.concat(
|
bindings = bindings.concat(
|
||||||
outputs.map(([name, value]) => {
|
outputs.map(([name, value]) => {
|
||||||
let runtimeName = isLoopBlock
|
let runtimeName = isLoopBlock
|
||||||
|
@ -218,17 +239,24 @@
|
||||||
? `steps[${idx - loopBlockCount}].${name}`
|
? `steps[${idx - loopBlockCount}].${name}`
|
||||||
: `steps.${idx - loopBlockCount}.${name}`
|
: `steps.${idx - loopBlockCount}.${name}`
|
||||||
const runtime = idx === 0 ? `trigger.${name}` : runtimeName
|
const runtime = idx === 0 ? `trigger.${name}` : runtimeName
|
||||||
|
const categoryName =
|
||||||
|
idx === 0
|
||||||
|
? "Trigger outputs"
|
||||||
|
: isLoopBlock
|
||||||
|
? "Loop Outputs"
|
||||||
|
: `Step ${idx - loopBlockCount} outputs`
|
||||||
return {
|
return {
|
||||||
label: runtime,
|
readableBinding: runtime,
|
||||||
|
runtimeBinding: runtime,
|
||||||
type: value.type,
|
type: value.type,
|
||||||
description: value.description,
|
description: value.description,
|
||||||
category:
|
icon: bindingIcon,
|
||||||
idx === 0
|
category: categoryName,
|
||||||
? "Trigger outputs"
|
display: {
|
||||||
: isLoopBlock
|
type: value.type,
|
||||||
? "Loop Outputs"
|
name: name,
|
||||||
: `Step ${idx - loopBlockCount} outputs`,
|
rank: bindindingRank,
|
||||||
path: runtime,
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -237,15 +265,12 @@
|
||||||
// Environment bindings
|
// Environment bindings
|
||||||
if ($licensing.environmentVariablesEnabled) {
|
if ($licensing.environmentVariablesEnabled) {
|
||||||
bindings = bindings.concat(
|
bindings = bindings.concat(
|
||||||
$environment.variables.map(variable => {
|
getEnvironmentBindings().map(binding => {
|
||||||
return {
|
return {
|
||||||
label: `env.${variable.name}`,
|
...binding,
|
||||||
path: `env.${variable.name}`,
|
|
||||||
icon: "Key",
|
|
||||||
category: "Environment",
|
|
||||||
display: {
|
display: {
|
||||||
type: "string",
|
...binding.display,
|
||||||
name: variable.name,
|
rank: 98,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -437,25 +462,27 @@
|
||||||
<SchemaSetup on:change={e => onChange(e, key)} value={inputData[key]} />
|
<SchemaSetup on:change={e => onChange(e, key)} value={inputData[key]} />
|
||||||
{:else if value.customType === "code"}
|
{:else if value.customType === "code"}
|
||||||
<CodeEditorModal>
|
<CodeEditorModal>
|
||||||
<ActionButton
|
<CodeEditor
|
||||||
on:click={() => (codeBindingOpen = !codeBindingOpen)}
|
value={inputData[key]}
|
||||||
quiet
|
|
||||||
icon={codeBindingOpen ? "ChevronDown" : "ChevronRight"}
|
|
||||||
>
|
|
||||||
<Detail size="S">Bindings</Detail>
|
|
||||||
</ActionButton>
|
|
||||||
{#if codeBindingOpen}
|
|
||||||
<pre>{JSON.stringify(bindings, null, 2)}</pre>
|
|
||||||
{/if}
|
|
||||||
<Editor
|
|
||||||
mode="javascript"
|
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
// need to pass without the value inside
|
// need to pass without the value inside
|
||||||
onChange({ detail: e.detail.value }, key)
|
onChange({ detail: e.detail }, key)
|
||||||
inputData[key] = e.detail.value
|
inputData[key] = e.detail
|
||||||
}}
|
}}
|
||||||
value={inputData[key]}
|
completions={[
|
||||||
|
jsAutocomplete([
|
||||||
|
...bindingsToCompletions(bindings, EditorModes.JS),
|
||||||
|
]),
|
||||||
|
]}
|
||||||
|
mode={EditorModes.JS}
|
||||||
|
height={500}
|
||||||
/>
|
/>
|
||||||
|
<div class="messaging">
|
||||||
|
<Icon name="FlashOn" />
|
||||||
|
<div class="messaging-wrap">
|
||||||
|
<div>Add available bindings by typing <strong>$</strong></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</CodeEditorModal>
|
</CodeEditorModal>
|
||||||
{:else if value.customType === "loopOption"}
|
{:else if value.customType === "loopOption"}
|
||||||
<Select
|
<Select
|
||||||
|
@ -505,6 +532,11 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.messaging {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
.fields {
|
.fields {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -16,11 +16,11 @@
|
||||||
import GridEditColumnModal from "components/backend/DataTable/modals/grid/GridEditColumnModal.svelte"
|
import GridEditColumnModal from "components/backend/DataTable/modals/grid/GridEditColumnModal.svelte"
|
||||||
|
|
||||||
const userSchemaOverrides = {
|
const userSchemaOverrides = {
|
||||||
firstName: { name: "First name", disabled: true },
|
firstName: { displayName: "First name", disabled: true },
|
||||||
lastName: { name: "Last name", disabled: true },
|
lastName: { displayName: "Last name", disabled: true },
|
||||||
email: { name: "Email", disabled: true },
|
email: { displayName: "Email", disabled: true },
|
||||||
roleId: { name: "Role", disabled: true },
|
roleId: { displayName: "Role", disabled: true },
|
||||||
status: { name: "Status", disabled: true },
|
status: { displayName: "Status", disabled: true },
|
||||||
}
|
}
|
||||||
|
|
||||||
$: id = $tables.selected?._id
|
$: id = $tables.selected?._id
|
||||||
|
@ -36,7 +36,8 @@
|
||||||
allowAddRows={!isUsersTable}
|
allowAddRows={!isUsersTable}
|
||||||
allowDeleteRows={!isUsersTable}
|
allowDeleteRows={!isUsersTable}
|
||||||
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
||||||
on:updatetable={e => tables.updateTable(e.detail)}
|
showAvatars={false}
|
||||||
|
on:updatetable={e => tables.replaceTable(id, e.detail)}
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="controls">
|
<svelte:fragment slot="controls">
|
||||||
{#if isInternal}
|
{#if isInternal}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
export let query = {}
|
export let query = {}
|
||||||
export let data = []
|
export let data = []
|
||||||
|
export let editRows = false
|
||||||
|
|
||||||
let loading = false
|
let loading = false
|
||||||
let error = false
|
let error = false
|
||||||
|
@ -12,7 +13,14 @@
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="errors">{error}</div>
|
<div class="errors">{error}</div>
|
||||||
{/if}
|
{/if}
|
||||||
<Table schema={query.schema} {data} {loading} {type} rowCount={5} />
|
<Table
|
||||||
|
schema={query.schema}
|
||||||
|
{data}
|
||||||
|
{loading}
|
||||||
|
{type}
|
||||||
|
rowCount={5}
|
||||||
|
allowEditing={editRows}
|
||||||
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.errors {
|
.errors {
|
||||||
|
|
|
@ -81,6 +81,7 @@
|
||||||
<Label>{label}</Label>
|
<Label>{label}</Label>
|
||||||
<Editor
|
<Editor
|
||||||
editorHeight="250"
|
editorHeight="250"
|
||||||
|
editorWidth="320"
|
||||||
mode="json"
|
mode="json"
|
||||||
on:change={({ detail }) => (value = detail.value)}
|
on:change={({ detail }) => (value = detail.value)}
|
||||||
value={stringVal}
|
value={stringVal}
|
||||||
|
|
|
@ -22,8 +22,8 @@
|
||||||
export let rowCount
|
export let rowCount
|
||||||
export let disableSorting = false
|
export let disableSorting = false
|
||||||
export let customPlaceholder = false
|
export let customPlaceholder = false
|
||||||
export let allowClickRows
|
|
||||||
export let allowEditing = true
|
export let allowEditing = true
|
||||||
|
export let allowClickRows
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
|
|
@ -113,17 +113,26 @@
|
||||||
})
|
})
|
||||||
download(data, `export.${exportFormat}`)
|
download(data, `export.${exportFormat}`)
|
||||||
} else if (filters || sorting) {
|
} else if (filters || sorting) {
|
||||||
const data = await API.exportRows({
|
let response
|
||||||
tableId: view,
|
try {
|
||||||
format: exportFormat,
|
response = await API.exportRows({
|
||||||
search: {
|
tableId: view,
|
||||||
query: luceneFilter,
|
format: exportFormat,
|
||||||
sort: sorting?.sortColumn,
|
search: {
|
||||||
sortOrder: sorting?.sortOrder,
|
query: luceneFilter,
|
||||||
paginate: false,
|
sort: sorting?.sortColumn,
|
||||||
},
|
sortOrder: sorting?.sortOrder,
|
||||||
})
|
paginate: false,
|
||||||
download(data, `export.${exportFormat}`)
|
},
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to export", e)
|
||||||
|
notifications.error("Export Failed")
|
||||||
|
}
|
||||||
|
if (response) {
|
||||||
|
download(response, `export.${exportFormat}`)
|
||||||
|
notifications.success("Export Successful")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await exportView()
|
await exportView()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,255 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
ModalContent,
|
|
||||||
Modal,
|
|
||||||
Body,
|
|
||||||
Layout,
|
|
||||||
Detail,
|
|
||||||
Heading,
|
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import ICONS from "../icons"
|
|
||||||
import { API } from "api"
|
|
||||||
import { IntegrationTypes, DatasourceTypes } from "constants/backend"
|
|
||||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
|
||||||
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
|
||||||
import GoogleDatasourceConfigModal from "components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte"
|
|
||||||
import { createRestDatasource } from "builderStore/datasource"
|
|
||||||
import { goto } from "@roxi/routify"
|
|
||||||
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
|
|
||||||
import DatasourceCard from "../_components/DatasourceCard.svelte"
|
|
||||||
|
|
||||||
export let modal
|
|
||||||
let integrations = {}
|
|
||||||
let integration = {}
|
|
||||||
let internalTableModal
|
|
||||||
let externalDatasourceModal
|
|
||||||
let importModal
|
|
||||||
|
|
||||||
$: showImportButton = false
|
|
||||||
$: customIntegrations = Object.entries(integrations).filter(
|
|
||||||
entry => entry[1].custom
|
|
||||||
)
|
|
||||||
$: sortedIntegrations = sortIntegrations(integrations)
|
|
||||||
|
|
||||||
checkShowImport()
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
fetchIntegrations()
|
|
||||||
})
|
|
||||||
|
|
||||||
function selectIntegration(integrationType) {
|
|
||||||
const selected = integrations[integrationType]
|
|
||||||
|
|
||||||
// build the schema
|
|
||||||
const config = {}
|
|
||||||
for (let key of Object.keys(selected.datasource)) {
|
|
||||||
config[key] = selected.datasource[key].default
|
|
||||||
}
|
|
||||||
integration = {
|
|
||||||
type: integrationType,
|
|
||||||
plus: selected.plus,
|
|
||||||
config,
|
|
||||||
schema: selected.datasource,
|
|
||||||
auth: selected.auth,
|
|
||||||
features: selected.features || [],
|
|
||||||
}
|
|
||||||
if (selected.friendlyName) {
|
|
||||||
integration.name = selected.friendlyName
|
|
||||||
}
|
|
||||||
checkShowImport()
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkShowImport() {
|
|
||||||
showImportButton = integration.type === "REST"
|
|
||||||
}
|
|
||||||
|
|
||||||
function showImportModal() {
|
|
||||||
importModal.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function chooseNextModal() {
|
|
||||||
if (integration.type === IntegrationTypes.INTERNAL) {
|
|
||||||
externalDatasourceModal.hide()
|
|
||||||
internalTableModal.show()
|
|
||||||
} else if (integration.type === IntegrationTypes.REST) {
|
|
||||||
try {
|
|
||||||
// Skip modal for rest, create straight away
|
|
||||||
const resp = await createRestDatasource(integration)
|
|
||||||
$goto(`./datasource/${resp._id}`)
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error creating datasource")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
externalDatasourceModal.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchIntegrations() {
|
|
||||||
let newIntegrations = {
|
|
||||||
[IntegrationTypes.INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const integrationList = await API.getIntegrations()
|
|
||||||
newIntegrations = {
|
|
||||||
...newIntegrations,
|
|
||||||
...integrationList,
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error fetching integrations")
|
|
||||||
}
|
|
||||||
integrations = newIntegrations
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortIntegrations(integrations) {
|
|
||||||
let integrationsArray = Object.entries(integrations)
|
|
||||||
function getTypeOrder(schema) {
|
|
||||||
if (schema.type === DatasourceTypes.API) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if (schema.type === DatasourceTypes.RELATIONAL) {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
return schema.type?.charCodeAt(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
integrationsArray.sort((a, b) => {
|
|
||||||
let typeOrderA = getTypeOrder(a[1])
|
|
||||||
let typeOrderB = getTypeOrder(b[1])
|
|
||||||
if (typeOrderA === typeOrderB) {
|
|
||||||
return a[1].friendlyName?.localeCompare(b[1].friendlyName)
|
|
||||||
}
|
|
||||||
return typeOrderA < typeOrderB ? -1 : 1
|
|
||||||
})
|
|
||||||
return integrationsArray
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Modal bind:this={internalTableModal}>
|
|
||||||
<CreateTableModal />
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal bind:this={externalDatasourceModal}>
|
|
||||||
{#if integration?.auth?.type === "google"}
|
|
||||||
<GoogleDatasourceConfigModal {integration} {modal} />
|
|
||||||
{:else}
|
|
||||||
<DatasourceConfigModal {integration} {modal} />
|
|
||||||
{/if}
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal bind:this={importModal}>
|
|
||||||
{#if integration.type === "REST"}
|
|
||||||
<ImportRestQueriesModal
|
|
||||||
navigateDatasource={true}
|
|
||||||
createDatasource={true}
|
|
||||||
onCancel={() => modal.show()}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
|
||||||
<ModalContent
|
|
||||||
disabled={!Object.keys(integration).length}
|
|
||||||
title="Add datasource"
|
|
||||||
confirmText="Continue"
|
|
||||||
showSecondaryButton={showImportButton}
|
|
||||||
secondaryButtonText="Import"
|
|
||||||
secondaryAction={() => showImportModal()}
|
|
||||||
showCancelButton={false}
|
|
||||||
size="M"
|
|
||||||
onConfirm={() => {
|
|
||||||
chooseNextModal()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<Body size="S">Get started with Budibase DB</Body>
|
|
||||||
<div
|
|
||||||
class:selected={integration.type === IntegrationTypes.INTERNAL}
|
|
||||||
on:click={() => selectIntegration(IntegrationTypes.INTERNAL)}
|
|
||||||
class="item hoverable"
|
|
||||||
>
|
|
||||||
<div class="item-body with-type">
|
|
||||||
<svelte:component this={ICONS.BUDIBASE} height="20" width="20" />
|
|
||||||
<div class="text">
|
|
||||||
<Heading size="XXS">Budibase DB</Heading>
|
|
||||||
<Detail size="S" class="type">Non-relational</Detail>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<Body size="S">Connect to an external datasource</Body>
|
|
||||||
<div class="item-list">
|
|
||||||
{#each sortedIntegrations.filter(([key, val]) => key !== IntegrationTypes.INTERNAL && !val.custom) as [integrationType, schema]}
|
|
||||||
<DatasourceCard
|
|
||||||
on:selected={evt => selectIntegration(evt.detail)}
|
|
||||||
{schema}
|
|
||||||
bind:integrationType
|
|
||||||
{integration}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
{#if customIntegrations.length > 0}
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<Body size="S">Custom datasource</Body>
|
|
||||||
<div class="item-list">
|
|
||||||
{#each customIntegrations as [integrationType, schema]}
|
|
||||||
<DatasourceCard
|
|
||||||
on:selected={evt => selectIntegration(evt.detail)}
|
|
||||||
{schema}
|
|
||||||
bind:integrationType
|
|
||||||
{integration}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
{/if}
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.item-list {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(150px, 1fr));
|
|
||||||
grid-gap: var(--spectrum-alias-grid-baseline);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
cursor: pointer;
|
|
||||||
display: grid;
|
|
||||||
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
|
|
||||||
padding: var(--spectrum-alias-item-padding-s)
|
|
||||||
var(--spectrum-alias-item-padding-m);
|
|
||||||
background: var(--spectrum-alias-background-color-secondary);
|
|
||||||
transition: background 0.13s ease-out;
|
|
||||||
border-radius: 5px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-width: 2px;
|
|
||||||
}
|
|
||||||
.item:hover,
|
|
||||||
.item.selected {
|
|
||||||
background: var(--spectrum-alias-background-color-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
}
|
|
||||||
.item-body.with-type {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.item-body.with-type :global(svg) {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text :global(.spectrum-Detail) {
|
|
||||||
color: var(--spectrum-global-color-gray-700);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -18,7 +18,6 @@
|
||||||
import { DatasourceFeature } from "@budibase/types"
|
import { DatasourceFeature } from "@budibase/types"
|
||||||
|
|
||||||
export let integration
|
export let integration
|
||||||
export let modal
|
|
||||||
|
|
||||||
// kill the reference so the input isn't saved
|
// kill the reference so the input isn't saved
|
||||||
let datasource = cloneDeep(integration)
|
let datasource = cloneDeep(integration)
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
export let integration
|
export let integration
|
||||||
export let modal
|
|
||||||
|
|
||||||
// kill the reference so the input isn't saved
|
// kill the reference so the input isn't saved
|
||||||
let datasource = cloneDeep(integration)
|
let datasource = cloneDeep(integration)
|
||||||
|
@ -21,7 +20,6 @@
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={`Connect to ${IntegrationNames[datasource.type]}`}
|
title={`Connect to ${IntegrationNames[datasource.type]}`}
|
||||||
onCancel={() => modal.show()}
|
|
||||||
cancelText="Back"
|
cancelText="Back"
|
||||||
size="L"
|
size="L"
|
||||||
>
|
>
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { parseFile } from "./utils"
|
import { parseFile } from "./utils"
|
||||||
|
|
||||||
|
let fileInput
|
||||||
let error = null
|
let error = null
|
||||||
let fileName = null
|
let fileName = null
|
||||||
let fileType = null
|
let fileType = null
|
||||||
|
@ -16,6 +17,7 @@
|
||||||
export let schema = {}
|
export let schema = {}
|
||||||
export let allValid = true
|
export let allValid = true
|
||||||
export let displayColumn = null
|
export let displayColumn = null
|
||||||
|
export let promptUpload = false
|
||||||
|
|
||||||
const typeOptions = [
|
const typeOptions = [
|
||||||
{
|
{
|
||||||
|
@ -99,10 +101,19 @@
|
||||||
schema[name].type = e.detail
|
schema[name].type = e.detail
|
||||||
schema[name].constraints = FIELDS[e.detail.toUpperCase()].constraints
|
schema[name].constraints = FIELDS[e.detail.toUpperCase()].constraints
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openFileUpload = (promptUpload, fileInput) => {
|
||||||
|
if (promptUpload && fileInput) {
|
||||||
|
fileInput.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: openFileUpload(promptUpload, fileInput)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="dropzone">
|
<div class="dropzone">
|
||||||
<input
|
<input
|
||||||
|
bind:this={fileInput}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
id="file-upload"
|
id="file-upload"
|
||||||
accept="text/csv,application/json"
|
accept="text/csv,application/json"
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
? selectedSource._id
|
? selectedSource._id
|
||||||
: BUDIBASE_INTERNAL_DB_ID
|
: BUDIBASE_INTERNAL_DB_ID
|
||||||
|
|
||||||
|
export let promptUpload = false
|
||||||
export let name
|
export let name
|
||||||
export let beforeSave = async () => {}
|
export let beforeSave = async () => {}
|
||||||
export let afterSave = async table => {
|
export let afterSave = async table => {
|
||||||
|
@ -136,7 +137,13 @@
|
||||||
<Label grey extraSmall
|
<Label grey extraSmall
|
||||||
>Create a Table from a CSV or JSON file (Optional)</Label
|
>Create a Table from a CSV or JSON file (Optional)</Label
|
||||||
>
|
>
|
||||||
<TableDataImport bind:rows bind:schema bind:allValid bind:displayColumn />
|
<TableDataImport
|
||||||
|
{promptUpload}
|
||||||
|
bind:rows
|
||||||
|
bind:schema
|
||||||
|
bind:allValid
|
||||||
|
bind:displayColumn
|
||||||
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -1,143 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
ButtonGroup,
|
|
||||||
ModalContent,
|
|
||||||
Modal,
|
|
||||||
notifications,
|
|
||||||
ProgressCircle,
|
|
||||||
Layout,
|
|
||||||
Body,
|
|
||||||
Icon,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { auth, apps } from "stores/portal"
|
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
|
||||||
import { API } from "api"
|
|
||||||
|
|
||||||
export let app
|
|
||||||
export let buttonSize = "M"
|
|
||||||
|
|
||||||
let APP_DEV_LOCK_SECONDS = 600 //common area for this?
|
|
||||||
let appLockModal
|
|
||||||
let processing = false
|
|
||||||
|
|
||||||
$: lockedBy = app?.lockedBy
|
|
||||||
$: lockedByYou = $auth.user.email === lockedBy?.email
|
|
||||||
|
|
||||||
$: lockIdentifer = `${
|
|
||||||
lockedBy && lockedBy.firstName ? lockedBy?.firstName : lockedBy?.email
|
|
||||||
}`
|
|
||||||
|
|
||||||
$: lockedByHeading =
|
|
||||||
lockedBy && lockedByYou ? "Locked by you" : `Locked by ${lockIdentifer}`
|
|
||||||
|
|
||||||
const getExpiryDuration = app => {
|
|
||||||
if (!app?.lockedBy?.lockedAt) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
let expiry =
|
|
||||||
new Date(app.lockedBy.lockedAt).getTime() + APP_DEV_LOCK_SECONDS * 1000
|
|
||||||
return expiry - new Date().getTime()
|
|
||||||
}
|
|
||||||
|
|
||||||
const releaseLock = async () => {
|
|
||||||
processing = true
|
|
||||||
if (app) {
|
|
||||||
try {
|
|
||||||
await API.releaseAppLock(app.devId)
|
|
||||||
await apps.load()
|
|
||||||
notifications.success("Lock released successfully")
|
|
||||||
} catch (err) {
|
|
||||||
notifications.error("Error releasing lock")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
notifications.error("No application is selected")
|
|
||||||
}
|
|
||||||
processing = false
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if lockedBy}
|
|
||||||
<div class="lock-status">
|
|
||||||
<Icon
|
|
||||||
name="LockClosed"
|
|
||||||
hoverable
|
|
||||||
size={buttonSize}
|
|
||||||
on:click={e => {
|
|
||||||
e.stopPropagation()
|
|
||||||
appLockModal.show()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<Modal bind:this={appLockModal}>
|
|
||||||
<ModalContent
|
|
||||||
title={lockedByHeading}
|
|
||||||
showConfirmButton={false}
|
|
||||||
showCancelButton={false}
|
|
||||||
>
|
|
||||||
<Layout noPadding>
|
|
||||||
<Body size="S">
|
|
||||||
Apps are locked to prevent work being lost from overlapping changes
|
|
||||||
between your team.
|
|
||||||
</Body>
|
|
||||||
{#if lockedByYou && getExpiryDuration(app) > 0}
|
|
||||||
<span class="lock-expiry-body">
|
|
||||||
{processStringSync(
|
|
||||||
"This lock will expire in {{ duration time 'millisecond' }} from now.",
|
|
||||||
{
|
|
||||||
time: getExpiryDuration(app),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
<div class="lock-modal-actions">
|
|
||||||
<ButtonGroup>
|
|
||||||
<Button
|
|
||||||
secondary
|
|
||||||
quiet={lockedBy && lockedByYou}
|
|
||||||
disabled={processing}
|
|
||||||
on:click={() => {
|
|
||||||
appLockModal.hide()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span class="cancel"
|
|
||||||
>{lockedBy && !lockedByYou ? "Done" : "Cancel"}</span
|
|
||||||
>
|
|
||||||
</Button>
|
|
||||||
{#if lockedByYou}
|
|
||||||
<Button
|
|
||||||
cta
|
|
||||||
disabled={processing}
|
|
||||||
on:click={() => {
|
|
||||||
releaseLock()
|
|
||||||
appLockModal.hide()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#if processing}
|
|
||||||
<ProgressCircle overBackground={true} size="S" />
|
|
||||||
{:else}
|
|
||||||
<span class="unlock">Release Lock</span>
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</ButtonGroup>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.lock-modal-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: var(--spacing-l);
|
|
||||||
gap: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
.lock-status {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
max-width: 175px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -0,0 +1,289 @@
|
||||||
|
<script>
|
||||||
|
import { Label } from "@budibase/bbui"
|
||||||
|
import { onMount, createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
import {
|
||||||
|
autocompletion,
|
||||||
|
closeBrackets,
|
||||||
|
completionKeymap,
|
||||||
|
closeBracketsKeymap,
|
||||||
|
} from "@codemirror/autocomplete"
|
||||||
|
import {
|
||||||
|
EditorView,
|
||||||
|
lineNumbers,
|
||||||
|
keymap,
|
||||||
|
highlightSpecialChars,
|
||||||
|
drawSelection,
|
||||||
|
dropCursor,
|
||||||
|
highlightActiveLine,
|
||||||
|
highlightActiveLineGutter,
|
||||||
|
highlightWhitespace,
|
||||||
|
placeholder as placeholderFn,
|
||||||
|
MatchDecorator,
|
||||||
|
ViewPlugin,
|
||||||
|
Decoration,
|
||||||
|
} from "@codemirror/view"
|
||||||
|
import {
|
||||||
|
bracketMatching,
|
||||||
|
foldKeymap,
|
||||||
|
foldGutter,
|
||||||
|
syntaxHighlighting,
|
||||||
|
} from "@codemirror/language"
|
||||||
|
import { oneDark, oneDarkHighlightStyle } from "@codemirror/theme-one-dark"
|
||||||
|
import {
|
||||||
|
defaultKeymap,
|
||||||
|
historyKeymap,
|
||||||
|
history,
|
||||||
|
indentWithTab,
|
||||||
|
} from "@codemirror/commands"
|
||||||
|
import { Compartment } from "@codemirror/state"
|
||||||
|
import { javascript } from "@codemirror/lang-javascript"
|
||||||
|
import { EditorModes, getDefaultTheme } from "./"
|
||||||
|
import { themeStore } from "builderStore"
|
||||||
|
|
||||||
|
export let label
|
||||||
|
export let completions = []
|
||||||
|
export let height = 200
|
||||||
|
export let resize = "none"
|
||||||
|
export let mode = EditorModes.Handlebars
|
||||||
|
export let value = ""
|
||||||
|
export let placeholder = null
|
||||||
|
|
||||||
|
// Export a function to expose caret position
|
||||||
|
export const getCaretPosition = () => {
|
||||||
|
const selection_range = editor.state.selection.ranges[0]
|
||||||
|
return {
|
||||||
|
start: selection_range.from,
|
||||||
|
end: selection_range.to,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const insertAtPos = opts => {
|
||||||
|
// Updating the value inside.
|
||||||
|
// Retain focus
|
||||||
|
editor.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: opts.start || editor.state.doc.length,
|
||||||
|
to: opts.end || editor.state.doc.length,
|
||||||
|
insert: opts.value,
|
||||||
|
},
|
||||||
|
selection: opts.cursor
|
||||||
|
? {
|
||||||
|
anchor: opts.start + opts.value.length,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// For handlebars only.
|
||||||
|
const bindStyle = new MatchDecorator({
|
||||||
|
regexp: /{{[."#\-\w\s\][]*}}/g,
|
||||||
|
decoration: () => {
|
||||||
|
return Decoration.mark({
|
||||||
|
tag: "span",
|
||||||
|
attributes: {
|
||||||
|
class: "binding-wrap",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let plugin = ViewPlugin.define(
|
||||||
|
view => ({
|
||||||
|
decorations: bindStyle.createDeco(view),
|
||||||
|
update(u) {
|
||||||
|
this.decorations = bindStyle.updateDeco(u, this.decorations)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
decorations: v => v.decorations,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
// Theming!
|
||||||
|
let currentTheme = $themeStore?.theme
|
||||||
|
let isDark = !currentTheme.includes("light")
|
||||||
|
let themeConfig = new Compartment()
|
||||||
|
|
||||||
|
const buildKeymap = () => {
|
||||||
|
const baseMap = [
|
||||||
|
...closeBracketsKeymap,
|
||||||
|
...defaultKeymap,
|
||||||
|
...historyKeymap,
|
||||||
|
...foldKeymap,
|
||||||
|
...completionKeymap,
|
||||||
|
indentWithTab,
|
||||||
|
]
|
||||||
|
return baseMap
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildBaseExtensions = () => {
|
||||||
|
return [
|
||||||
|
...(mode.name === "handlebars" ? [plugin] : []),
|
||||||
|
history(),
|
||||||
|
drawSelection(),
|
||||||
|
dropCursor(),
|
||||||
|
bracketMatching(),
|
||||||
|
closeBrackets(),
|
||||||
|
highlightActiveLine(),
|
||||||
|
syntaxHighlighting(oneDarkHighlightStyle, { fallback: true }),
|
||||||
|
highlightActiveLineGutter(),
|
||||||
|
highlightSpecialChars(),
|
||||||
|
autocompletion({
|
||||||
|
override: [...completions],
|
||||||
|
closeOnBlur: true,
|
||||||
|
icons: false,
|
||||||
|
optionClass: () => "autocomplete-option",
|
||||||
|
}),
|
||||||
|
EditorView.lineWrapping,
|
||||||
|
EditorView.updateListener.of(v => {
|
||||||
|
const docStr = v.state.doc?.toString()
|
||||||
|
if (docStr === value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dispatch("change", docStr)
|
||||||
|
}),
|
||||||
|
keymap.of(buildKeymap()),
|
||||||
|
themeConfig.of([
|
||||||
|
getDefaultTheme({
|
||||||
|
height: editorHeight,
|
||||||
|
resize,
|
||||||
|
dark: isDark,
|
||||||
|
}),
|
||||||
|
...(isDark ? [oneDark] : []),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildExtensions = base => {
|
||||||
|
const complete = [...base]
|
||||||
|
if (mode.name == "javascript") {
|
||||||
|
complete.push(javascript())
|
||||||
|
complete.push(highlightWhitespace())
|
||||||
|
complete.push(lineNumbers())
|
||||||
|
complete.push(foldGutter())
|
||||||
|
complete.push(
|
||||||
|
EditorView.inputHandler.of((view, from, to, insert) => {
|
||||||
|
if (insert === "$") {
|
||||||
|
let { text } = view.state.doc.lineAt(from)
|
||||||
|
|
||||||
|
const left = from ? text.substring(0, from) : ""
|
||||||
|
const right = to ? text.substring(to) : ""
|
||||||
|
const wrap = !left.includes('$("') || !right.includes('")')
|
||||||
|
const tr = view.state.update(
|
||||||
|
{
|
||||||
|
changes: [{ from, insert: wrap ? '$("")' : "$" }],
|
||||||
|
selection: {
|
||||||
|
anchor: from + (wrap ? 3 : 1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scrollIntoView: true,
|
||||||
|
userEvent: "input.type",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
view.dispatch(tr)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placeholder) {
|
||||||
|
complete.push(placeholderFn(placeholder))
|
||||||
|
}
|
||||||
|
return complete
|
||||||
|
}
|
||||||
|
|
||||||
|
let textarea
|
||||||
|
let editor
|
||||||
|
let mounted = false
|
||||||
|
let isEditorInitialised = false
|
||||||
|
|
||||||
|
const initEditor = () => {
|
||||||
|
const baseExtensions = buildBaseExtensions()
|
||||||
|
|
||||||
|
editor = new EditorView({
|
||||||
|
doc: value,
|
||||||
|
extensions: buildExtensions(baseExtensions),
|
||||||
|
parent: textarea,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$: editorHeight = typeof height === "number" ? `${height}px` : height
|
||||||
|
|
||||||
|
// Init when all elements are ready
|
||||||
|
$: if (mounted && !isEditorInitialised) {
|
||||||
|
isEditorInitialised = true
|
||||||
|
initEditor()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theme change
|
||||||
|
$: if (mounted && isEditorInitialised && $themeStore?.theme) {
|
||||||
|
if (currentTheme != $themeStore?.theme) {
|
||||||
|
currentTheme = $themeStore?.theme
|
||||||
|
isDark = !currentTheme.includes("light")
|
||||||
|
|
||||||
|
// Issue theme compartment update
|
||||||
|
editor.dispatch({
|
||||||
|
effects: themeConfig.reconfigure([
|
||||||
|
getDefaultTheme({
|
||||||
|
height: editorHeight,
|
||||||
|
resize,
|
||||||
|
dark: isDark,
|
||||||
|
}),
|
||||||
|
...(isDark ? [oneDark] : []),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
mounted = true
|
||||||
|
return () => {
|
||||||
|
if (editor) {
|
||||||
|
editor.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if label}
|
||||||
|
<div>
|
||||||
|
<Label small>{label}</Label>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class={`code-editor ${mode?.name || ""}`}>
|
||||||
|
<div tabindex="-1" bind:this={textarea} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.code-editor.handlebars :global(.cm-content) {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
}
|
||||||
|
.code-editor :global(.cm-tooltip.cm-completionInfo) {
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.code-editor :global(.cm-tooltip-autocomplete > ul > li[aria-selected]) {
|
||||||
|
border-radius: var(
|
||||||
|
--spectrum-popover-border-radius,
|
||||||
|
var(--spectrum-alias-border-radius-regular)
|
||||||
|
),
|
||||||
|
var(
|
||||||
|
--spectrum-popover-border-radius,
|
||||||
|
var(--spectrum-alias-border-radius-regular)
|
||||||
|
),
|
||||||
|
0, 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-editor :global(.autocomplete-option .cm-completionDetail) {
|
||||||
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,387 @@
|
||||||
|
import { EditorView } from "@codemirror/view"
|
||||||
|
import { getManifest } from "@budibase/string-templates"
|
||||||
|
import sanitizeHtml from "sanitize-html"
|
||||||
|
import { groupBy } from "lodash"
|
||||||
|
|
||||||
|
export const EditorModes = {
|
||||||
|
JS: {
|
||||||
|
name: "javascript",
|
||||||
|
json: false,
|
||||||
|
match: /\$$/,
|
||||||
|
},
|
||||||
|
Handlebars: {
|
||||||
|
name: "handlebars",
|
||||||
|
base: "text/html",
|
||||||
|
match: /{{[\s]*[\w\s]*/,
|
||||||
|
},
|
||||||
|
Text: {
|
||||||
|
name: "text/html",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SECTIONS = {
|
||||||
|
HB_HELPER: {
|
||||||
|
name: "Helper",
|
||||||
|
type: "helper",
|
||||||
|
icon: "Code",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDefaultTheme = opts => {
|
||||||
|
const { height, resize, dark } = opts
|
||||||
|
return EditorView.theme(
|
||||||
|
{
|
||||||
|
"&.cm-focused .cm-cursor": {
|
||||||
|
borderLeftColor: "var(--spectrum-alias-text-color)",
|
||||||
|
},
|
||||||
|
"&": {
|
||||||
|
height: height ? `${height}` : "",
|
||||||
|
lineHeight: "1.3",
|
||||||
|
border:
|
||||||
|
"var(--spectrum-alias-border-size-thin) solid var(--spectrum-alias-border-color)",
|
||||||
|
borderRadius: "var(--border-radius-s)",
|
||||||
|
backgroundColor:
|
||||||
|
"var( --spectrum-textfield-m-background-color, var(--spectrum-global-color-gray-50) )",
|
||||||
|
resize: resize ? `${resize}` : "",
|
||||||
|
overflow: "hidden",
|
||||||
|
color: "var(--spectrum-alias-text-color)",
|
||||||
|
},
|
||||||
|
"& .cm-tooltip.cm-tooltip-autocomplete > ul": {
|
||||||
|
fontFamily:
|
||||||
|
"var(--spectrum-alias-body-text-font-family, var(--spectrum-global-font-family-base))",
|
||||||
|
maxHeight: "16em",
|
||||||
|
},
|
||||||
|
"& .cm-placeholder": {
|
||||||
|
color: "var(--spectrum-alias-text-color)",
|
||||||
|
fontStyle: "italic",
|
||||||
|
},
|
||||||
|
"&.cm-focused": {
|
||||||
|
outline: "none",
|
||||||
|
borderColor: "var(--spectrum-alias-border-color-mouse-focus)",
|
||||||
|
},
|
||||||
|
// AUTO COMPLETE
|
||||||
|
"& .cm-completionDetail": {
|
||||||
|
fontStyle: "unset",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
fontSize: "10px",
|
||||||
|
backgroundColor: "var(--spectrum-global-color-gray-100)",
|
||||||
|
color: "var(--spectrum-global-color-gray-600)",
|
||||||
|
},
|
||||||
|
"& .cm-completionLabel": {
|
||||||
|
marginLeft:
|
||||||
|
"calc(var(--spectrum-alias-workflow-icon-size-m) + var(--spacing-m))",
|
||||||
|
},
|
||||||
|
"& .info-bubble": {
|
||||||
|
fontSize: "var(--font-size-s)",
|
||||||
|
display: "grid",
|
||||||
|
gridGap: "var(--spacing-s)",
|
||||||
|
gridTemplateColumns: "1fr",
|
||||||
|
color: "var(--spectrum-global-color-gray-800)",
|
||||||
|
},
|
||||||
|
"& .cm-tooltip": {
|
||||||
|
marginLeft: "var(--spacing-s)",
|
||||||
|
border: "1px solid var(--spectrum-global-color-gray-300)",
|
||||||
|
borderRadius:
|
||||||
|
"var( --spectrum-popover-border-radius, var(--spectrum-alias-border-radius-regular) )",
|
||||||
|
backgroundColor: "var(--spectrum-global-color-gray-50)",
|
||||||
|
},
|
||||||
|
// Section header
|
||||||
|
"& .info-section": {
|
||||||
|
display: "flex",
|
||||||
|
padding: "var(--spacing-s)",
|
||||||
|
gap: "var(--spacing-m)",
|
||||||
|
borderBottom: "1px solid var(--spectrum-global-color-gray-200)",
|
||||||
|
color: "var(--spectrum-global-color-gray-800)",
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
"& .info-section .spectrum-Icon": {
|
||||||
|
color: "var(--spectrum-global-color-gray-600)",
|
||||||
|
},
|
||||||
|
// Autocomplete Option
|
||||||
|
"& .cm-tooltip.cm-tooltip-autocomplete .autocomplete-option": {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
fontSize: "var(--spectrum-alias-font-size-default)",
|
||||||
|
padding: "var(--spacing-s)",
|
||||||
|
color: "var(--spectrum-global-color-gray-800)",
|
||||||
|
},
|
||||||
|
"& .cm-tooltip-autocomplete ul li[aria-selected].autocomplete-option": {
|
||||||
|
backgroundColor: "var(--spectrum-global-color-gray-200)",
|
||||||
|
},
|
||||||
|
"& .binding-wrap": {
|
||||||
|
color: "var(--spectrum-global-color-blue-700)",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ dark }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildHelperInfoNode = (completion, helper) => {
|
||||||
|
const ele = document.createElement("div")
|
||||||
|
ele.classList.add("info-bubble")
|
||||||
|
|
||||||
|
const exampleNodeHtml = helper.example
|
||||||
|
? `<div class="binding__example">${helper.example}</div>`
|
||||||
|
: ""
|
||||||
|
const descriptionMarkup = sanitizeHtml(helper.description, {
|
||||||
|
allowedTags: [],
|
||||||
|
allowedAttributes: {},
|
||||||
|
})
|
||||||
|
const descriptionNodeHtml = `<div class="binding__description">${descriptionMarkup}</div>`
|
||||||
|
|
||||||
|
ele.innerHTML = `
|
||||||
|
${exampleNodeHtml}
|
||||||
|
${descriptionNodeHtml}
|
||||||
|
`
|
||||||
|
return ele
|
||||||
|
}
|
||||||
|
|
||||||
|
const toSpectrumIcon = name => {
|
||||||
|
return `<svg
|
||||||
|
class="spectrum-Icon spectrum-Icon--sizeM"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="false"
|
||||||
|
aria-label="${name}-section-icon"
|
||||||
|
>
|
||||||
|
<use style="pointer-events: none;" xlink:href="#spectrum-icon-18-${name}" />
|
||||||
|
</svg>`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildSectionHeader = (type, sectionName, icon, rank) => {
|
||||||
|
const ele = document.createElement("div")
|
||||||
|
ele.classList.add("info-section")
|
||||||
|
ele.classList.add(type)
|
||||||
|
ele.innerHTML = `${toSpectrumIcon(icon)}<span>${sectionName}</span>`
|
||||||
|
return {
|
||||||
|
name: sectionName,
|
||||||
|
header: () => ele,
|
||||||
|
rank,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const helpersToCompletion = (helpers, mode) => {
|
||||||
|
const { type, name: sectionName, icon } = SECTIONS.HB_HELPER
|
||||||
|
const helperSection = buildSectionHeader(type, sectionName, icon, 99)
|
||||||
|
|
||||||
|
return Object.keys(helpers).reduce((acc, key) => {
|
||||||
|
let helper = helpers[key]
|
||||||
|
acc.push({
|
||||||
|
label: key,
|
||||||
|
info: completion => {
|
||||||
|
return buildHelperInfoNode(completion, helper)
|
||||||
|
},
|
||||||
|
type: "helper",
|
||||||
|
section: helperSection,
|
||||||
|
detail: "FUNCTION",
|
||||||
|
apply: (view, completion, from, to) => {
|
||||||
|
insertBinding(view, from, to, key, mode)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getHelperCompletions = mode => {
|
||||||
|
const manifest = getManifest()
|
||||||
|
return Object.keys(manifest).reduce((acc, key) => {
|
||||||
|
acc = acc || []
|
||||||
|
return [...acc, ...helpersToCompletion(manifest[key], mode)]
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
const bindingFilter = (options, query) => {
|
||||||
|
return options.filter(completion => {
|
||||||
|
const section_parsed = completion.section.name.toLowerCase()
|
||||||
|
const label_parsed = completion.label.toLowerCase()
|
||||||
|
const query_parsed = query.toLowerCase()
|
||||||
|
|
||||||
|
return (
|
||||||
|
section_parsed.includes(query_parsed) ||
|
||||||
|
label_parsed.includes(query_parsed)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hbAutocomplete = baseCompletions => {
|
||||||
|
async function coreCompletion(context) {
|
||||||
|
let bindingStart = context.matchBefore(EditorModes.Handlebars.match)
|
||||||
|
|
||||||
|
let options = baseCompletions || []
|
||||||
|
|
||||||
|
if (!bindingStart) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// Accommodate spaces
|
||||||
|
const match = bindingStart.text.match(/{{[\s]*/)
|
||||||
|
const query = bindingStart.text.replace(match[0], "")
|
||||||
|
let filtered = bindingFilter(options, query)
|
||||||
|
|
||||||
|
return {
|
||||||
|
from: bindingStart.from + match[0].length,
|
||||||
|
filter: false,
|
||||||
|
options: filtered,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return coreCompletion
|
||||||
|
}
|
||||||
|
|
||||||
|
export const jsAutocomplete = baseCompletions => {
|
||||||
|
async function coreCompletion(context) {
|
||||||
|
let jsBinding = context.matchBefore(/\$\("[\s\w]*/)
|
||||||
|
let options = baseCompletions || []
|
||||||
|
|
||||||
|
if (jsBinding) {
|
||||||
|
// Accommodate spaces
|
||||||
|
const match = jsBinding.text.match(/\$\("[\s]*/)
|
||||||
|
const query = jsBinding.text.replace(match[0], "")
|
||||||
|
let filtered = bindingFilter(options, query)
|
||||||
|
return {
|
||||||
|
from: jsBinding.from + match[0].length,
|
||||||
|
filter: false,
|
||||||
|
options: filtered,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return coreCompletion
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildBindingInfoNode = (completion, binding) => {
|
||||||
|
const ele = document.createElement("div")
|
||||||
|
ele.classList.add("info-bubble")
|
||||||
|
|
||||||
|
const exampleNodeHtml = binding.readableBinding
|
||||||
|
? `<div class="binding__example">{{ ${binding.readableBinding} }}</div>`
|
||||||
|
: ""
|
||||||
|
|
||||||
|
const descriptionNodeHtml = binding.description
|
||||||
|
? `<div class="binding__description">${binding.description}</div>`
|
||||||
|
: ""
|
||||||
|
|
||||||
|
ele.innerHTML = `
|
||||||
|
${exampleNodeHtml}
|
||||||
|
${descriptionNodeHtml}
|
||||||
|
`
|
||||||
|
return ele
|
||||||
|
}
|
||||||
|
|
||||||
|
// Readdress these methods. They shouldn't be used
|
||||||
|
export const hbInsert = (value, from, to, text) => {
|
||||||
|
let parsedInsert = ""
|
||||||
|
|
||||||
|
const left = from ? value.substring(0, from) : ""
|
||||||
|
const right = to ? value.substring(to) : ""
|
||||||
|
|
||||||
|
if (!left.includes("{{") || !right.includes("}}")) {
|
||||||
|
parsedInsert = `{{ ${text} }}`
|
||||||
|
} else {
|
||||||
|
parsedInsert = ` ${text} `
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedInsert
|
||||||
|
}
|
||||||
|
|
||||||
|
export function jsInsert(value, from, to, text, { helper } = {}) {
|
||||||
|
let parsedInsert = ""
|
||||||
|
|
||||||
|
const left = from ? value.substring(0, from) : ""
|
||||||
|
const right = to ? value.substring(to) : ""
|
||||||
|
|
||||||
|
if (helper) {
|
||||||
|
parsedInsert = `helpers.${text}()`
|
||||||
|
} else if (!left.includes('$("') || !right.includes('")')) {
|
||||||
|
parsedInsert = `$("${text}")`
|
||||||
|
} else {
|
||||||
|
parsedInsert = text
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedInsert
|
||||||
|
}
|
||||||
|
|
||||||
|
// Autocomplete apply behaviour
|
||||||
|
export const insertBinding = (view, from, to, text, mode) => {
|
||||||
|
let parsedInsert
|
||||||
|
|
||||||
|
if (mode.name == "javascript") {
|
||||||
|
parsedInsert = jsInsert(view.state.doc?.toString(), from, to, text)
|
||||||
|
} else if (mode.name == "handlebars") {
|
||||||
|
parsedInsert = hbInsert(view.state.doc?.toString(), from, to, text)
|
||||||
|
} else {
|
||||||
|
console.log("Unsupported")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let bindingClosePattern = mode.name == "javascript" ? /[\s]*"\)/ : /[\s]*}}/
|
||||||
|
let sliced = view.state.doc?.toString().slice(to)
|
||||||
|
|
||||||
|
const rightBrace = sliced.match(bindingClosePattern)
|
||||||
|
let cursorPos = from + parsedInsert.length
|
||||||
|
|
||||||
|
if (rightBrace) {
|
||||||
|
cursorPos = from + parsedInsert.length + rightBrace[0].length
|
||||||
|
}
|
||||||
|
|
||||||
|
view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
insert: parsedInsert,
|
||||||
|
},
|
||||||
|
selection: {
|
||||||
|
anchor: cursorPos,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bindingsToCompletions = (bindings, mode) => {
|
||||||
|
const bindingByCategory = groupBy(bindings, "category")
|
||||||
|
const categoryMeta = bindings?.reduce((acc, ele) => {
|
||||||
|
acc[ele.category] = acc[ele.category] || {}
|
||||||
|
|
||||||
|
if (ele.icon) {
|
||||||
|
acc[ele.category]["icon"] = acc[ele.category]["icon"] || ele.icon
|
||||||
|
}
|
||||||
|
if (typeof ele.display?.rank == "number") {
|
||||||
|
acc[ele.category]["rank"] = acc[ele.category]["rank"] || ele.display.rank
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const completions = Object.keys(bindingByCategory).reduce((comps, catKey) => {
|
||||||
|
const { icon, rank } = categoryMeta[catKey] || {}
|
||||||
|
|
||||||
|
const bindindSectionHeader = buildSectionHeader(
|
||||||
|
bindingByCategory.type,
|
||||||
|
catKey,
|
||||||
|
icon || "",
|
||||||
|
typeof rank == "number" ? rank : 1
|
||||||
|
)
|
||||||
|
|
||||||
|
return [
|
||||||
|
...comps,
|
||||||
|
...bindingByCategory[catKey].reduce((acc, binding) => {
|
||||||
|
let displayType = binding.fieldSchema?.type || binding.display?.type
|
||||||
|
acc.push({
|
||||||
|
label: binding.display?.name || "NO NAME",
|
||||||
|
info: completion => {
|
||||||
|
return buildBindingInfoNode(completion, binding)
|
||||||
|
},
|
||||||
|
type: "binding",
|
||||||
|
detail: displayType,
|
||||||
|
section: bindindSectionHeader,
|
||||||
|
apply: (view, completion, from, to) => {
|
||||||
|
insertBinding(view, from, to, binding.readableBinding, mode)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return acc
|
||||||
|
}, []),
|
||||||
|
]
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return completions
|
||||||
|
}
|
|
@ -28,7 +28,6 @@
|
||||||
.dash-card {
|
.dash-card {
|
||||||
background: var(--spectrum-alias-background-color-primary);
|
background: var(--spectrum-alias-background-color-primary);
|
||||||
border-radius: var(--border-radius-s);
|
border-radius: var(--border-radius-s);
|
||||||
overflow: hidden;
|
|
||||||
min-height: 170px;
|
min-height: 170px;
|
||||||
}
|
}
|
||||||
.dash-card-header {
|
.dash-card-header {
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
faLock,
|
faLock,
|
||||||
faFileArrowUp,
|
faFileArrowUp,
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
|
faCircleInfo,
|
||||||
} from "@fortawesome/free-solid-svg-icons"
|
} from "@fortawesome/free-solid-svg-icons"
|
||||||
import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"
|
import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"
|
||||||
|
|
||||||
|
@ -20,7 +21,8 @@
|
||||||
faDiscord,
|
faDiscord,
|
||||||
faEnvelope,
|
faEnvelope,
|
||||||
faFileArrowUp,
|
faFileArrowUp,
|
||||||
faChevronLeft
|
faChevronLeft,
|
||||||
|
faCircleInfo
|
||||||
)
|
)
|
||||||
dom.watch()
|
dom.watch()
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -83,7 +83,7 @@
|
||||||
.help {
|
.help {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: var(--spacing-xl);
|
bottom: 24px;
|
||||||
right: 24px;
|
right: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,13 @@
|
||||||
<script>
|
<script>
|
||||||
import groupBy from "lodash/fp/groupBy"
|
|
||||||
import {
|
import {
|
||||||
Search,
|
|
||||||
TextArea,
|
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
Body,
|
Body,
|
||||||
Layout,
|
|
||||||
Button,
|
Button,
|
||||||
ActionButton,
|
ActionButton,
|
||||||
|
Heading,
|
||||||
Icon,
|
Icon,
|
||||||
Popover,
|
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { createEventDispatcher, onMount } from "svelte"
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
import {
|
import {
|
||||||
|
@ -23,11 +19,21 @@
|
||||||
readableToRuntimeBinding,
|
readableToRuntimeBinding,
|
||||||
runtimeToReadableBinding,
|
runtimeToReadableBinding,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
import { handlebarsCompletions } from "constants/completions"
|
import { store } from "builderStore"
|
||||||
import { addHBSBinding, addJSBinding } from "./utils"
|
|
||||||
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
|
|
||||||
import { convertToJS } from "@budibase/string-templates"
|
import { convertToJS } from "@budibase/string-templates"
|
||||||
import { admin } from "stores/portal"
|
import { admin } from "stores/portal"
|
||||||
|
import CodeEditor from "../CodeEditor/CodeEditor.svelte"
|
||||||
|
import {
|
||||||
|
getHelperCompletions,
|
||||||
|
jsAutocomplete,
|
||||||
|
hbAutocomplete,
|
||||||
|
EditorModes,
|
||||||
|
bindingsToCompletions,
|
||||||
|
hbInsert,
|
||||||
|
jsInsert,
|
||||||
|
} from "../CodeEditor"
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import BindingPicker from "./BindingPicker.svelte"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -41,54 +47,21 @@
|
||||||
export let allowJS = false
|
export let allowJS = false
|
||||||
export let allowHelpers = true
|
export let allowHelpers = true
|
||||||
|
|
||||||
let helpers = handlebarsCompletions()
|
const drawerActions = getContext("drawer-actions")
|
||||||
|
const bindingDrawerActions = getContext("binding-drawer-actions")
|
||||||
|
|
||||||
let getCaretPosition
|
let getCaretPosition
|
||||||
let search = ""
|
let insertAtPos
|
||||||
let initialValueJS = typeof value === "string" && value?.startsWith("{{ js ")
|
let initialValueJS = typeof value === "string" && value?.startsWith("{{ js ")
|
||||||
let mode = initialValueJS ? "JavaScript" : "Handlebars"
|
let mode = initialValueJS ? "JavaScript" : "Text"
|
||||||
let jsValue = initialValueJS ? value : null
|
let jsValue = initialValueJS ? value : null
|
||||||
let hbsValue = initialValueJS ? null : value
|
let hbsValue = initialValueJS ? null : value
|
||||||
|
let sidebar = true
|
||||||
let selectedCategory = null
|
let targetMode = null
|
||||||
|
|
||||||
let popover
|
|
||||||
let popoverAnchor
|
|
||||||
let hoverTarget
|
|
||||||
|
|
||||||
$: usingJS = mode === "JavaScript"
|
$: usingJS = mode === "JavaScript"
|
||||||
$: searchRgx = new RegExp(search, "ig")
|
$: editorMode = mode == "JavaScript" ? EditorModes.JS : EditorModes.Handlebars
|
||||||
$: categories = Object.entries(groupBy("category", bindings))
|
$: bindingCompletions = bindingsToCompletions(bindings, editorMode)
|
||||||
|
|
||||||
$: bindingIcons = bindings?.reduce((acc, ele) => {
|
|
||||||
if (ele.icon) {
|
|
||||||
acc[ele.category] = acc[ele.category] || ele.icon
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
$: categoryIcons = { ...bindingIcons, Helpers: "MagicWand" }
|
|
||||||
|
|
||||||
$: filteredCategories = categories
|
|
||||||
.map(([name, categoryBindings]) => ({
|
|
||||||
name,
|
|
||||||
bindings: categoryBindings?.filter(binding => {
|
|
||||||
return binding.readableBinding.match(searchRgx)
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
.filter(category => {
|
|
||||||
return (
|
|
||||||
category.bindings?.length > 0 &&
|
|
||||||
(!selectedCategory ? true : selectedCategory === category.name)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
$: filteredHelpers = helpers?.filter(helper => {
|
|
||||||
return helper.label.match(searchRgx) || helper.description.match(searchRgx)
|
|
||||||
})
|
|
||||||
|
|
||||||
$: categoryNames = getCategoryNames(categories)
|
|
||||||
|
|
||||||
$: codeMirrorHints = bindings?.map(x => `$("${x.readableBinding}")`)
|
|
||||||
|
|
||||||
const updateValue = val => {
|
const updateValue = val => {
|
||||||
valid = isValid(readableToRuntimeBinding(bindings, val))
|
valid = isValid(readableToRuntimeBinding(bindings, val))
|
||||||
|
@ -97,43 +70,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCategoryNames = categories => {
|
|
||||||
let names = [...categories.map(cat => cat[0])]
|
|
||||||
if (allowHelpers) {
|
|
||||||
names.push("Helpers")
|
|
||||||
}
|
|
||||||
return names
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adds a JS/HBS helper to the expression
|
// Adds a JS/HBS helper to the expression
|
||||||
const addHelper = (helper, js) => {
|
const onSelectHelper = (helper, js) => {
|
||||||
let tempVal
|
|
||||||
const pos = getCaretPosition()
|
const pos = getCaretPosition()
|
||||||
|
const { start, end } = pos
|
||||||
if (js) {
|
if (js) {
|
||||||
const decoded = decodeJSBinding(jsValue)
|
let js = decodeJSBinding(jsValue)
|
||||||
tempVal = jsValue = encodeJSBinding(
|
const insertVal = jsInsert(js, start, end, helper.text, { helper: true })
|
||||||
addJSBinding(decoded, pos, helper.text, { helper: true })
|
insertAtPos({ start, end, value: insertVal })
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
tempVal = hbsValue = addHBSBinding(hbsValue, pos, helper.text)
|
const insertVal = hbInsert(hbsValue, start, end, helper.text)
|
||||||
|
insertAtPos({ start, end, value: insertVal })
|
||||||
}
|
}
|
||||||
updateValue(tempVal)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a data binding to the expression
|
// Adds a data binding to the expression
|
||||||
const addBinding = (binding, { forceJS } = {}) => {
|
const onSelectBinding = (binding, { forceJS } = {}) => {
|
||||||
|
const { start, end } = getCaretPosition()
|
||||||
if (usingJS || forceJS) {
|
if (usingJS || forceJS) {
|
||||||
let js = decodeJSBinding(jsValue)
|
let js = decodeJSBinding(jsValue)
|
||||||
js = addJSBinding(js, getCaretPosition(), binding.readableBinding)
|
const insertVal = jsInsert(js, start, end, binding.readableBinding)
|
||||||
jsValue = encodeJSBinding(js)
|
insertAtPos({ start, end, value: insertVal })
|
||||||
updateValue(jsValue)
|
|
||||||
} else {
|
} else {
|
||||||
hbsValue = addHBSBinding(
|
const insertVal = hbInsert(hbsValue, start, end, binding.readableBinding)
|
||||||
hbsValue,
|
insertAtPos({ start, end, value: insertVal })
|
||||||
getCaretPosition(),
|
|
||||||
binding.readableBinding
|
|
||||||
)
|
|
||||||
updateValue(hbsValue)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,24 +112,25 @@
|
||||||
updateValue(jsValue)
|
updateValue(jsValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const switchMode = () => {
|
||||||
|
if (targetMode == "Text") {
|
||||||
|
jsValue = null
|
||||||
|
updateValue(jsValue)
|
||||||
|
} else {
|
||||||
|
hbsValue = null
|
||||||
|
updateValue(hbsValue)
|
||||||
|
}
|
||||||
|
mode = targetMode + ""
|
||||||
|
targetMode = null
|
||||||
|
}
|
||||||
|
|
||||||
const convert = () => {
|
const convert = () => {
|
||||||
const runtime = readableToRuntimeBinding(bindings, hbsValue)
|
const runtime = readableToRuntimeBinding(bindings, hbsValue)
|
||||||
const runtimeJs = encodeJSBinding(convertToJS(runtime))
|
const runtimeJs = encodeJSBinding(convertToJS(runtime))
|
||||||
jsValue = runtimeToReadableBinding(bindings, runtimeJs)
|
jsValue = runtimeToReadableBinding(bindings, runtimeJs)
|
||||||
hbsValue = null
|
hbsValue = null
|
||||||
mode = "JavaScript"
|
mode = "JavaScript"
|
||||||
addBinding("", { forceJS: true })
|
onSelectBinding("", { forceJS: true })
|
||||||
}
|
|
||||||
|
|
||||||
const getHelperExample = (helper, js) => {
|
|
||||||
let example = helper.example || ""
|
|
||||||
if (js) {
|
|
||||||
example = convertToJS(example).split("\n")[0].split("= ")[1]
|
|
||||||
if (example === "null;") {
|
|
||||||
example = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return example || ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
@ -177,332 +138,301 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class="detailPopover">
|
<span class="binding-drawer">
|
||||||
<Popover
|
<DrawerContent>
|
||||||
align="right-outside"
|
<div class="main">
|
||||||
bind:this={popover}
|
<Tabs
|
||||||
anchor={popoverAnchor}
|
selected={mode}
|
||||||
maxWidth={300}
|
on:select={onChangeMode}
|
||||||
dismissible={false}
|
beforeSwitch={selectedMode => {
|
||||||
>
|
if (selectedMode == mode) {
|
||||||
<Layout gap="S">
|
return true
|
||||||
<div class="helper">
|
}
|
||||||
{#if hoverTarget.title}
|
|
||||||
<div class="helper__name">{hoverTarget.title}</div>
|
|
||||||
{/if}
|
|
||||||
{#if hoverTarget.description}
|
|
||||||
<div class="helper__description">
|
|
||||||
{@html hoverTarget.description}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if hoverTarget.example}
|
|
||||||
<pre class="helper__example">{hoverTarget.example}</pre>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</Popover>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<DrawerContent>
|
//Get the current mode value
|
||||||
<svelte:fragment slot="sidebar">
|
const editorValue = usingJS ? decodeJSBinding(jsValue) : hbsValue
|
||||||
<Layout noPadding gap="S">
|
|
||||||
{#if selectedCategory}
|
|
||||||
<div>
|
|
||||||
<ActionButton
|
|
||||||
secondary
|
|
||||||
icon={"ArrowLeft"}
|
|
||||||
on:click={() => {
|
|
||||||
selectedCategory = null
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if !selectedCategory}
|
if (editorValue) {
|
||||||
<div class="heading">Search</div>
|
targetMode = selectedMode
|
||||||
<Search placeholder="Search" bind:value={search} />
|
return false
|
||||||
{/if}
|
}
|
||||||
|
return true
|
||||||
{#if !selectedCategory && !search}
|
}}
|
||||||
<ul class="category-list">
|
>
|
||||||
{#each categoryNames as categoryName}
|
<Tab title="Text">
|
||||||
<li
|
<div class="main-content" class:binding-panel={sidebar}>
|
||||||
on:click={() => {
|
<div class="editor">
|
||||||
selectedCategory = categoryName
|
<div class="overlay-wrap">
|
||||||
}}
|
{#if targetMode}
|
||||||
>
|
<div class="mode-overlay">
|
||||||
<Icon name={categoryIcons[categoryName]} />
|
<div class="prompt-body">
|
||||||
<span class="category-name">{categoryName} </span>
|
<Heading size="S">
|
||||||
<span class="category-chevron"><Icon name="ChevronRight" /></span>
|
{`Switch to ${targetMode}?`}
|
||||||
</li>
|
</Heading>
|
||||||
{/each}
|
<Body>This will discard anything in your binding</Body>
|
||||||
</ul>
|
<div class="switch-actions">
|
||||||
{/if}
|
<Button
|
||||||
|
secondary
|
||||||
{#if selectedCategory || search}
|
size="S"
|
||||||
{#each filteredCategories as category}
|
on:click={() => {
|
||||||
{#if category.bindings?.length}
|
targetMode = null
|
||||||
<div class="cat-heading">
|
}}
|
||||||
<Icon name={categoryIcons[category.name]} />{category.name}
|
>
|
||||||
</div>
|
No - keep text
|
||||||
<ul>
|
</Button>
|
||||||
{#each category.bindings as binding}
|
<Button cta size="S" on:click={switchMode}>
|
||||||
<li
|
Yes - discard text
|
||||||
class="binding"
|
</Button>
|
||||||
on:mouseenter={e => {
|
</div>
|
||||||
popoverAnchor = e.target
|
</div>
|
||||||
if (!binding.description) {
|
</div>
|
||||||
return
|
{/if}
|
||||||
}
|
<CodeEditor
|
||||||
hoverTarget = {
|
value={hbsValue}
|
||||||
title: binding.display?.name || binding.fieldSchema?.name,
|
on:change={onChangeHBSValue}
|
||||||
description: binding.description,
|
bind:getCaretPosition
|
||||||
}
|
bind:insertAtPos
|
||||||
popover.show()
|
completions={[
|
||||||
e.stopPropagation()
|
hbAutocomplete([
|
||||||
}}
|
...bindingCompletions,
|
||||||
on:mouseleave={() => {
|
...getHelperCompletions(editorMode),
|
||||||
popover.hide()
|
]),
|
||||||
popoverAnchor = null
|
]}
|
||||||
hoverTarget = null
|
placeholder=""
|
||||||
}}
|
height="100%"
|
||||||
on:focus={() => {}}
|
/>
|
||||||
on:blur={() => {}}
|
</div>
|
||||||
on:click={() => addBinding(binding)}
|
<div class="binding-footer">
|
||||||
>
|
<div class="messaging">
|
||||||
<span class="binding__label">
|
{#if !valid}
|
||||||
{#if binding.display?.name}
|
<div class="syntax-error">
|
||||||
{binding.display.name}
|
Current Handlebars syntax is invalid, please check the
|
||||||
{:else if binding.fieldSchema?.name}
|
guide
|
||||||
{binding.fieldSchema?.name}
|
<a href="https://handlebarsjs.com/guide/">here</a>
|
||||||
{:else}
|
for more details.
|
||||||
{binding.readableBinding}
|
</div>
|
||||||
{/if}
|
{:else}
|
||||||
</span>
|
<Icon name="FlashOn" />
|
||||||
|
<div class="messaging-wrap">
|
||||||
{#if binding.display?.type || binding.fieldSchema?.type}
|
<div>
|
||||||
<span class="binding__typeWrap">
|
Add available bindings by typing {{ or use the
|
||||||
<span class="binding__type">
|
menu on the right
|
||||||
{binding.display?.type || binding.fieldSchema?.type}
|
</div>
|
||||||
</span>
|
</div>
|
||||||
</span>
|
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</div>
|
||||||
{/each}
|
<div class="actions">
|
||||||
</ul>
|
{#if $admin.isDev && allowJS}
|
||||||
{/if}
|
<ActionButton
|
||||||
{/each}
|
secondary
|
||||||
|
on:click={() => {
|
||||||
{#if selectedCategory === "Helpers" || search}
|
convert()
|
||||||
{#if filteredHelpers?.length}
|
targetMode = null
|
||||||
<div class="heading">Helpers</div>
|
}}
|
||||||
<ul class="helpers">
|
>
|
||||||
{#each filteredHelpers as helper}
|
Convert To JS
|
||||||
<li
|
</ActionButton>
|
||||||
class="binding"
|
{/if}
|
||||||
on:click={() => addHelper(helper, usingJS)}
|
<ActionButton
|
||||||
on:mouseenter={e => {
|
secondary
|
||||||
popoverAnchor = e.target
|
icon={sidebar ? "RailRightClose" : "RailRightOpen"}
|
||||||
if (!helper.displayText && helper.description) {
|
on:click={() => {
|
||||||
return
|
sidebar = !sidebar
|
||||||
}
|
}}
|
||||||
hoverTarget = {
|
/>
|
||||||
title: helper.displayText,
|
</div>
|
||||||
description: helper.description,
|
</div>
|
||||||
example: getHelperExample(helper, usingJS),
|
|
||||||
}
|
|
||||||
popover.show()
|
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
on:mouseleave={() => {
|
|
||||||
popover.hide()
|
|
||||||
popoverAnchor = null
|
|
||||||
hoverTarget = null
|
|
||||||
}}
|
|
||||||
on:focus={() => {}}
|
|
||||||
on:blur={() => {}}
|
|
||||||
>
|
|
||||||
<span class="binding__label">{helper.displayText}</span>
|
|
||||||
<span class="binding__typeWrap">
|
|
||||||
<span class="binding__type">function</span>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</Layout>
|
|
||||||
</svelte:fragment>
|
|
||||||
<div class="main">
|
|
||||||
<Tabs selected={mode} on:select={onChangeMode}>
|
|
||||||
<Tab title="Handlebars">
|
|
||||||
<div class="main-content">
|
|
||||||
<TextArea
|
|
||||||
bind:getCaretPosition
|
|
||||||
value={hbsValue}
|
|
||||||
on:change={onChangeHBSValue}
|
|
||||||
placeholder="Add text, or click the objects on the left to add them to the textbox."
|
|
||||||
/>
|
|
||||||
{#if !valid}
|
|
||||||
<p class="syntax-error">
|
|
||||||
Current Handlebars syntax is invalid, please check the guide
|
|
||||||
<a href="https://handlebarsjs.com/guide/">here</a>
|
|
||||||
for more details.
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
{#if $admin.isDev && allowJS}
|
|
||||||
<div class="convert">
|
|
||||||
<Button secondary on:click={convert}>Convert to JS</Button>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
</div>
|
{#if sidebar}
|
||||||
</Tab>
|
<div class="binding-picker">
|
||||||
{#if allowJS}
|
<BindingPicker
|
||||||
<Tab title="JavaScript">
|
{bindings}
|
||||||
<div class="main-content">
|
{allowHelpers}
|
||||||
<Layout noPadding gap="XS">
|
addHelper={onSelectHelper}
|
||||||
<CodeMirrorEditor
|
addBinding={onSelectBinding}
|
||||||
bind:getCaretPosition
|
mode={editorMode}
|
||||||
height={200}
|
/>
|
||||||
value={decodeJSBinding(jsValue)}
|
</div>
|
||||||
on:change={onChangeJSValue}
|
{/if}
|
||||||
hints={codeMirrorHints}
|
|
||||||
/>
|
|
||||||
<Body size="S">
|
|
||||||
JavaScript expressions are executed as functions, so ensure that
|
|
||||||
your expression returns a value.
|
|
||||||
</Body>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
{/if}
|
{#if allowJS}
|
||||||
</Tabs>
|
<Tab title="JavaScript">
|
||||||
</div>
|
<div class="main-content" class:binding-panel={sidebar}>
|
||||||
</DrawerContent>
|
<div class="editor">
|
||||||
|
<div class="overlay-wrap">
|
||||||
|
{#if targetMode}
|
||||||
|
<div class="mode-overlay">
|
||||||
|
<div class="prompt-body">
|
||||||
|
<Heading size="S">
|
||||||
|
{`Switch to ${targetMode}?`}
|
||||||
|
</Heading>
|
||||||
|
<Body>This will discard anything in your binding</Body>
|
||||||
|
<div class="switch-actions">
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
size="S"
|
||||||
|
on:click={() => {
|
||||||
|
targetMode = null
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
No - keep javascript
|
||||||
|
</Button>
|
||||||
|
<Button cta size="S" on:click={switchMode}>
|
||||||
|
Yes - discard javascript
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<CodeEditor
|
||||||
|
value={decodeJSBinding(jsValue)}
|
||||||
|
on:change={onChangeJSValue}
|
||||||
|
completions={[
|
||||||
|
jsAutocomplete([
|
||||||
|
...bindingCompletions,
|
||||||
|
...getHelperCompletions(editorMode),
|
||||||
|
]),
|
||||||
|
]}
|
||||||
|
mode={EditorModes.JS}
|
||||||
|
bind:getCaretPosition
|
||||||
|
bind:insertAtPos
|
||||||
|
height="100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="binding-footer">
|
||||||
|
<div class="messaging">
|
||||||
|
<Icon name="FlashOn" />
|
||||||
|
<div class="messaging-wrap">
|
||||||
|
<div>
|
||||||
|
Add available bindings by typing $ or use the menu on
|
||||||
|
the right
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<ActionButton
|
||||||
|
secondary
|
||||||
|
icon={sidebar ? "RailRightClose" : "RailRightOpen"}
|
||||||
|
on:click={() => {
|
||||||
|
sidebar = !sidebar
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if sidebar}
|
||||||
|
<div class="binding-picker">
|
||||||
|
<BindingPicker
|
||||||
|
{bindings}
|
||||||
|
{allowHelpers}
|
||||||
|
addHelper={onSelectHelper}
|
||||||
|
addBinding={onSelectBinding}
|
||||||
|
mode={editorMode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
{/if}
|
||||||
|
<div class="drawer-actions">
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
quiet
|
||||||
|
on:click={() => {
|
||||||
|
store.actions.settings.propertyFocus(null)
|
||||||
|
drawerActions.hide()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
cta
|
||||||
|
disabled={!valid}
|
||||||
|
on:click={() => {
|
||||||
|
bindingDrawerActions.save()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</DrawerContent>
|
||||||
|
</span>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
ul.helpers li * {
|
.binding-drawer :global(.container > .main) {
|
||||||
pointer-events: none;
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0px;
|
||||||
}
|
}
|
||||||
ul.category-list li {
|
|
||||||
|
.binding-drawer :global(.container > .main > .main) {
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binding-drawer :global(.spectrum-Tabs-content) {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binding-drawer :global(.spectrum-Tabs-content > div),
|
||||||
|
.binding-drawer :global(.spectrum-Tabs-content > div > div),
|
||||||
|
.binding-drawer :global(.spectrum-Tabs-content .main-content) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binding-drawer .main-content {
|
||||||
|
grid-template-rows: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messaging {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
align-items: center;
|
min-width: 0;
|
||||||
}
|
|
||||||
ul.category-list .category-name {
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
ul.category-list .category-chevron {
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: right;
|
|
||||||
}
|
}
|
||||||
ul.category-list .category-chevron :global(div.icon),
|
.messaging-wrap {
|
||||||
.cat-heading :global(div.icon) {
|
overflow: hidden;
|
||||||
display: inline-block;
|
|
||||||
}
|
}
|
||||||
li.binding {
|
.messaging-wrap > div {
|
||||||
display: flex;
|
text-overflow: ellipsis;
|
||||||
align-items: center;
|
white-space: nowrap;
|
||||||
}
|
overflow: hidden;
|
||||||
li.binding .binding__typeWrap {
|
|
||||||
flex: 1;
|
|
||||||
text-align: right;
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
}
|
||||||
.main :global(textarea) {
|
.main :global(textarea) {
|
||||||
min-height: 202px !important;
|
min-height: 202px !important;
|
||||||
}
|
}
|
||||||
.main {
|
|
||||||
margin: calc(-1 * var(--spacing-xl));
|
|
||||||
}
|
|
||||||
.main-content {
|
.main-content {
|
||||||
padding: var(--spacing-s) var(--spacing-xl);
|
padding: var(--spacing-s) var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading,
|
.main :global(.spectrum-Tabs div.drawer-actions) {
|
||||||
.cat-heading {
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--spectrum-global-color-gray-600);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cat-heading {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
align-items: center;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
.main :global(.spectrum-Tabs-content),
|
||||||
list-style: none;
|
.main :global(.spectrum-Tabs-content .main-content) {
|
||||||
padding: 0;
|
margin-top: 0px;
|
||||||
margin: 0;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
.main :global(.spectrum-Tabs) {
|
||||||
font-size: var(--font-size-s);
|
|
||||||
padding: var(--spacing-m);
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: var(--spectrum-global-color-gray-200);
|
|
||||||
transition: background-color 130ms ease-in-out, color 130ms ease-in-out,
|
|
||||||
border-color 130ms ease-in-out;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
li:not(:last-of-type) {
|
|
||||||
margin-bottom: var(--spacing-s);
|
|
||||||
}
|
|
||||||
li :global(*) {
|
|
||||||
transition: color 130ms ease-in-out;
|
|
||||||
}
|
|
||||||
li:hover {
|
|
||||||
color: var(--spectrum-global-color-gray-900);
|
|
||||||
background-color: var(--spectrum-global-color-gray-50);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.binding__label {
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.binding__type {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
background-color: var(--spectrum-global-color-gray-200);
|
|
||||||
border-radius: var(--border-radius-s);
|
|
||||||
padding: 2px 4px;
|
|
||||||
margin-left: 2px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.helper {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
.helper__name {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.helper__description,
|
|
||||||
.helper__description :global(*) {
|
|
||||||
color: var(--spectrum-global-color-gray-700);
|
|
||||||
}
|
|
||||||
.helper__example {
|
|
||||||
white-space: normal;
|
|
||||||
margin: 0.5rem 0 0 0;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
.helper__description :global(p) {
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.syntax-error {
|
.syntax-error {
|
||||||
padding-top: var(--spacing-m);
|
|
||||||
color: var(--red);
|
color: var(--red);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
@ -511,7 +441,66 @@
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.convert {
|
.binding-footer {
|
||||||
padding-top: var(--spacing-m);
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.main-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 380px;
|
||||||
|
}
|
||||||
|
.main-content.binding-panel {
|
||||||
|
grid-template-columns: 1fr 320px;
|
||||||
|
}
|
||||||
|
.binding-picker {
|
||||||
|
border-left: 2px solid var(--border-light);
|
||||||
|
border-left: var(--border-light);
|
||||||
|
overflow: scroll;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.editor {
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.overlay-wrap {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.mode-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(
|
||||||
|
--spectrum-textfield-m-background-color,
|
||||||
|
var(--spectrum-global-color-gray-50)
|
||||||
|
);
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
}
|
||||||
|
.prompt-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.prompt-body .switch-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.binding-drawer :global(.code-editor),
|
||||||
|
.binding-drawer :global(.code-editor > div) {
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,393 @@
|
||||||
|
<script>
|
||||||
|
import groupBy from "lodash/fp/groupBy"
|
||||||
|
import { convertToJS } from "@budibase/string-templates"
|
||||||
|
import { Input, Layout, ActionButton, Icon, Popover } from "@budibase/bbui"
|
||||||
|
import { handlebarsCompletions } from "constants/completions"
|
||||||
|
|
||||||
|
export let addHelper
|
||||||
|
export let addBinding
|
||||||
|
export let bindings
|
||||||
|
export let mode
|
||||||
|
export let allowHelpers
|
||||||
|
|
||||||
|
let search = ""
|
||||||
|
let popover
|
||||||
|
let popoverAnchor
|
||||||
|
let hoverTarget
|
||||||
|
let helpers = handlebarsCompletions()
|
||||||
|
|
||||||
|
let selectedCategory
|
||||||
|
|
||||||
|
$: searchRgx = new RegExp(search, "ig")
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
$: bindingIcons = bindings?.reduce((acc, ele) => {
|
||||||
|
if (ele.icon) {
|
||||||
|
acc[ele.category] = acc[ele.category] || ele.icon
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
$: categoryIcons = { ...bindingIcons, Helpers: "MagicWand" }
|
||||||
|
|
||||||
|
$: categories = Object.entries(groupBy("category", bindings))
|
||||||
|
$: categoryNames = getCategoryNames(categories)
|
||||||
|
|
||||||
|
$: filteredCategories = categories
|
||||||
|
.map(([name, categoryBindings]) => ({
|
||||||
|
name,
|
||||||
|
bindings: categoryBindings?.filter(binding => {
|
||||||
|
return binding.readableBinding.match(searchRgx)
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
.filter(category => {
|
||||||
|
return (
|
||||||
|
category.bindings?.length > 0 &&
|
||||||
|
(!selectedCategory ? true : selectedCategory === category.name)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
$: filteredHelpers = helpers?.filter(helper => {
|
||||||
|
return helper.label.match(searchRgx) || helper.description.match(searchRgx)
|
||||||
|
})
|
||||||
|
|
||||||
|
const getHelperExample = (helper, js) => {
|
||||||
|
let example = helper.example || ""
|
||||||
|
if (js) {
|
||||||
|
example = convertToJS(example).split("\n")[0].split("= ")[1]
|
||||||
|
if (example === "null;") {
|
||||||
|
example = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return example || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCategoryNames = categories => {
|
||||||
|
let names = [...categories.map(cat => cat[0])]
|
||||||
|
if (allowHelpers) {
|
||||||
|
names.push("Helpers")
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="detailPopover">
|
||||||
|
<Popover
|
||||||
|
align="left-outside"
|
||||||
|
bind:this={popover}
|
||||||
|
anchor={popoverAnchor}
|
||||||
|
maxWidth={300}
|
||||||
|
dismissible={false}
|
||||||
|
>
|
||||||
|
<Layout gap="S">
|
||||||
|
<div class="helper">
|
||||||
|
{#if hoverTarget.title}
|
||||||
|
<div class="helper__name">{hoverTarget.title}</div>
|
||||||
|
{/if}
|
||||||
|
{#if hoverTarget.description}
|
||||||
|
<div class="helper__description">
|
||||||
|
{@html hoverTarget.description}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if hoverTarget.example}
|
||||||
|
<pre class="helper__example">{hoverTarget.example}</pre>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</Popover>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Layout noPadding gap="S">
|
||||||
|
{#if selectedCategory}
|
||||||
|
<div class="sub-section-back">
|
||||||
|
<ActionButton
|
||||||
|
secondary
|
||||||
|
icon={"ArrowLeft"}
|
||||||
|
on:click={() => {
|
||||||
|
selectedCategory = null
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !selectedCategory}
|
||||||
|
<div class="search">
|
||||||
|
<span class="search-input">
|
||||||
|
<Input
|
||||||
|
placeholder={"Search for bindings"}
|
||||||
|
autocomplete="off"
|
||||||
|
bind:value={search}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<span
|
||||||
|
class="search-input-icon"
|
||||||
|
on:click={() => {
|
||||||
|
if (!search) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
search = null
|
||||||
|
}}
|
||||||
|
class:searching={search}
|
||||||
|
>
|
||||||
|
<Icon name={search ? "Close" : "Search"} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !selectedCategory && !search}
|
||||||
|
<ul class="category-list">
|
||||||
|
{#each categoryNames as categoryName}
|
||||||
|
<li
|
||||||
|
on:click={() => {
|
||||||
|
selectedCategory = categoryName
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name={categoryIcons[categoryName]} />
|
||||||
|
<span class="category-name">{categoryName} </span>
|
||||||
|
<span class="category-chevron"><Icon name="ChevronRight" /></span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if selectedCategory || search}
|
||||||
|
{#each filteredCategories as category}
|
||||||
|
{#if category.bindings?.length}
|
||||||
|
<div class="sub-section">
|
||||||
|
<div class="cat-heading">
|
||||||
|
<Icon name={categoryIcons[category.name]} />{category.name}
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
{#each category.bindings as binding}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<li
|
||||||
|
class="binding"
|
||||||
|
on:mouseenter={e => {
|
||||||
|
popoverAnchor = e.target
|
||||||
|
if (!binding.description) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hoverTarget = {
|
||||||
|
title: binding.display?.name || binding.fieldSchema?.name,
|
||||||
|
description: binding.description,
|
||||||
|
}
|
||||||
|
popover.show()
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
on:mouseleave={() => {
|
||||||
|
popover.hide()
|
||||||
|
popoverAnchor = null
|
||||||
|
hoverTarget = null
|
||||||
|
}}
|
||||||
|
on:focus={() => {}}
|
||||||
|
on:blur={() => {}}
|
||||||
|
on:click={() => addBinding(binding)}
|
||||||
|
>
|
||||||
|
<span class="binding__label">
|
||||||
|
{#if binding.display?.name}
|
||||||
|
{binding.display.name}
|
||||||
|
{:else if binding.fieldSchema?.name}
|
||||||
|
{binding.fieldSchema?.name}
|
||||||
|
{:else}
|
||||||
|
{binding.readableBinding}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{#if binding.display?.type || binding.fieldSchema?.type}
|
||||||
|
<span class="binding__typeWrap">
|
||||||
|
<span class="binding__type">
|
||||||
|
{binding.display?.type || binding.fieldSchema?.type}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if selectedCategory === "Helpers" || search}
|
||||||
|
{#if filteredHelpers?.length}
|
||||||
|
<div class="sub-section">
|
||||||
|
<div class="cat-heading">Helpers</div>
|
||||||
|
<ul class="helpers">
|
||||||
|
{#each filteredHelpers as helper}
|
||||||
|
<li
|
||||||
|
class="binding"
|
||||||
|
on:click={() => addHelper(helper, mode.name == "javascript")}
|
||||||
|
on:mouseenter={e => {
|
||||||
|
popoverAnchor = e.target
|
||||||
|
if (!helper.displayText && helper.description) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hoverTarget = {
|
||||||
|
title: helper.displayText,
|
||||||
|
description: helper.description,
|
||||||
|
example: getHelperExample(
|
||||||
|
helper,
|
||||||
|
mode.name == "javascript"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
popover.show()
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
on:mouseleave={() => {
|
||||||
|
popover.hide()
|
||||||
|
popoverAnchor = null
|
||||||
|
hoverTarget = null
|
||||||
|
}}
|
||||||
|
on:focus={() => {}}
|
||||||
|
on:blur={() => {}}
|
||||||
|
>
|
||||||
|
<span class="binding__label">{helper.displayText}</span>
|
||||||
|
<span class="binding__typeWrap">
|
||||||
|
<span class="binding__type">function</span>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.search :global(input) {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0px;
|
||||||
|
background: none;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
padding: var(--spacing-m) var(--spacing-l);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-top: 0px;
|
||||||
|
border-bottom: var(--border-light);
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
border-right: 2px solid transparent;
|
||||||
|
margin-right: 1px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background-color: var(--background);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-icon.searching {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.category-list {
|
||||||
|
padding: 0px var(--spacing-l);
|
||||||
|
padding-bottom: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.sub-section {
|
||||||
|
padding: var(--spacing-l);
|
||||||
|
padding-top: 0px;
|
||||||
|
}
|
||||||
|
.sub-section-back {
|
||||||
|
padding: var(--spacing-l);
|
||||||
|
padding-top: var(--spacing-xl);
|
||||||
|
padding-bottom: 0px;
|
||||||
|
}
|
||||||
|
.cat-heading {
|
||||||
|
margin-bottom: var(--spacing-l);
|
||||||
|
}
|
||||||
|
ul.helpers li * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
ul.category-list li {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
ul.category-list .category-name {
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
ul.category-list .category-chevron {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
ul.category-list .category-chevron :global(div.icon),
|
||||||
|
.cat-heading :global(div.icon) {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
li.binding {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
li.binding .binding__typeWrap {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.drawer-actions) {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-heading {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-heading {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
|
transition: background-color 130ms ease-in-out, color 130ms ease-in-out,
|
||||||
|
border-color 130ms ease-in-out;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
li:not(:last-of-type) {
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
li :global(*) {
|
||||||
|
transition: color 130ms ease-in-out;
|
||||||
|
}
|
||||||
|
li:hover {
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
background-color: var(--spectrum-global-color-gray-50);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binding__label {
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binding__type {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
padding: 2px 4px;
|
||||||
|
margin-left: 2px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -5,7 +5,7 @@
|
||||||
runtimeToReadableBinding,
|
runtimeToReadableBinding,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher, setContext } from "svelte"
|
||||||
import { isJSBinding } from "@budibase/string-templates"
|
import { isJSBinding } from "@budibase/string-templates"
|
||||||
|
|
||||||
export let panel = ClientBindingPanel
|
export let panel = ClientBindingPanel
|
||||||
|
@ -34,6 +34,10 @@
|
||||||
bindingDrawer.hide()
|
bindingDrawer.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setContext("binding-drawer-actions", {
|
||||||
|
save: handleClose,
|
||||||
|
})
|
||||||
|
|
||||||
const onChange = (value, optionPicked) => {
|
const onChange = (value, optionPicked) => {
|
||||||
// Add HBS braces if picking binding
|
// Add HBS braces if picking binding
|
||||||
if (optionPicked && !options?.includes(value)) {
|
if (optionPicked && !options?.includes(value)) {
|
||||||
|
@ -63,7 +67,6 @@
|
||||||
on:pick={e => onChange(e.detail, true)}
|
on:pick={e => onChange(e.detail, true)}
|
||||||
on:blur={() => dispatch("blur")}
|
on:blur={() => dispatch("blur")}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
options={allOptions}
|
|
||||||
{error}
|
{error}
|
||||||
/>
|
/>
|
||||||
{#if !disabled}
|
{#if !disabled}
|
||||||
|
@ -77,6 +80,7 @@
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Add the objects on the left to enrich your text.
|
Add the objects on the left to enrich your text.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
<Button cta slot="buttons" on:click={handleClose} disabled={!valid}>
|
<Button cta slot="buttons" on:click={handleClose} disabled={!valid}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -4,8 +4,11 @@
|
||||||
readableToRuntimeBinding,
|
readableToRuntimeBinding,
|
||||||
runtimeToReadableBinding,
|
runtimeToReadableBinding,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
|
|
||||||
|
import { store } from "builderStore"
|
||||||
|
|
||||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher, setContext } from "svelte"
|
||||||
import { isJSBinding } from "@budibase/string-templates"
|
import { isJSBinding } from "@budibase/string-templates"
|
||||||
|
|
||||||
export let panel = ClientBindingPanel
|
export let panel = ClientBindingPanel
|
||||||
|
@ -20,6 +23,7 @@
|
||||||
export let allowHelpers = true
|
export let allowHelpers = true
|
||||||
export let updateOnChange = true
|
export let updateOnChange = true
|
||||||
export let drawerLeft
|
export let drawerLeft
|
||||||
|
export let key
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let bindingDrawer
|
let bindingDrawer
|
||||||
|
@ -32,10 +36,15 @@
|
||||||
|
|
||||||
const saveBinding = () => {
|
const saveBinding = () => {
|
||||||
onChange(tempValue)
|
onChange(tempValue)
|
||||||
|
store.actions.settings.propertyFocus(null)
|
||||||
onBlur()
|
onBlur()
|
||||||
bindingDrawer.hide()
|
bindingDrawer.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setContext("binding-drawer-actions", {
|
||||||
|
save: saveBinding,
|
||||||
|
})
|
||||||
|
|
||||||
const onChange = value => {
|
const onChange = value => {
|
||||||
currentVal = readableToRuntimeBinding(bindings, value)
|
currentVal = readableToRuntimeBinding(bindings, value)
|
||||||
dispatch("change", currentVal)
|
dispatch("change", currentVal)
|
||||||
|
@ -58,12 +67,24 @@
|
||||||
{updateOnChange}
|
{updateOnChange}
|
||||||
/>
|
/>
|
||||||
{#if !disabled}
|
{#if !disabled}
|
||||||
<div class="icon" on:click={bindingDrawer.show}>
|
<div
|
||||||
|
class="icon"
|
||||||
|
on:click={() => {
|
||||||
|
store.actions.settings.propertyFocus(key)
|
||||||
|
bindingDrawer.show()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Icon size="S" name="FlashOn" />
|
<Icon size="S" name="FlashOn" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Drawer {fillWidth} bind:this={bindingDrawer} {title} left={drawerLeft}>
|
<Drawer
|
||||||
|
{fillWidth}
|
||||||
|
bind:this={bindingDrawer}
|
||||||
|
{title}
|
||||||
|
left={drawerLeft}
|
||||||
|
headless
|
||||||
|
>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Add the objects on the left to enrich your text.
|
Add the objects on the left to enrich your text.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
|
@ -113,109 +113,113 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="action-top-nav">
|
{#if $store.hasLock}
|
||||||
<div class="action-buttons">
|
<div class="action-top-nav">
|
||||||
<div class="version">
|
<div class="action-buttons">
|
||||||
<VersionModal />
|
<div class="version">
|
||||||
</div>
|
<VersionModal />
|
||||||
<RevertModal />
|
|
||||||
|
|
||||||
{#if isPublished}
|
|
||||||
<div class="publish-popover">
|
|
||||||
<div bind:this={publishPopoverAnchor}>
|
|
||||||
<ActionButton
|
|
||||||
quiet
|
|
||||||
icon="Globe"
|
|
||||||
size="M"
|
|
||||||
tooltip="Your published app"
|
|
||||||
on:click={publishPopover.show()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Popover
|
|
||||||
bind:this={publishPopover}
|
|
||||||
align="right"
|
|
||||||
disabled={!isPublished}
|
|
||||||
anchor={publishPopoverAnchor}
|
|
||||||
offset={10}
|
|
||||||
>
|
|
||||||
<div class="popover-content">
|
|
||||||
<Layout noPadding gap="M">
|
|
||||||
<Heading size="XS">Your published app</Heading>
|
|
||||||
<Body size="S">
|
|
||||||
<span class="publish-popover-message">
|
|
||||||
{processStringSync(
|
|
||||||
"Last published {{ duration time 'millisecond' }} ago",
|
|
||||||
{
|
|
||||||
time:
|
|
||||||
new Date().getTime() -
|
|
||||||
new Date(latestDeployments[0].updatedAt).getTime(),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</Body>
|
|
||||||
<div class="buttons">
|
|
||||||
<Button
|
|
||||||
warning={true}
|
|
||||||
icon="GlobeStrike"
|
|
||||||
disabled={!isPublished}
|
|
||||||
on:click={unpublishApp}
|
|
||||||
>
|
|
||||||
Unpublish
|
|
||||||
</Button>
|
|
||||||
<Button cta on:click={viewApp}>View app</Button>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
<RevertModal />
|
||||||
|
|
||||||
{#if !isPublished}
|
{#if isPublished}
|
||||||
<ActionButton
|
<div class="publish-popover">
|
||||||
quiet
|
<div bind:this={publishPopoverAnchor}>
|
||||||
icon="GlobeStrike"
|
<ActionButton
|
||||||
size="M"
|
quiet
|
||||||
tooltip="Your app has not been published yet"
|
icon="Globe"
|
||||||
disabled
|
size="M"
|
||||||
/>
|
tooltip="Your published app"
|
||||||
{/if}
|
on:click={publishPopover.show()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Popover
|
||||||
|
bind:this={publishPopover}
|
||||||
|
align="right"
|
||||||
|
disabled={!isPublished}
|
||||||
|
anchor={publishPopoverAnchor}
|
||||||
|
offset={10}
|
||||||
|
>
|
||||||
|
<div class="popover-content">
|
||||||
|
<Layout noPadding gap="M">
|
||||||
|
<Heading size="XS">Your published app</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
<span class="publish-popover-message">
|
||||||
|
{processStringSync(
|
||||||
|
"Last published {{ duration time 'millisecond' }} ago",
|
||||||
|
{
|
||||||
|
time:
|
||||||
|
new Date().getTime() -
|
||||||
|
new Date(latestDeployments[0].updatedAt).getTime(),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</Body>
|
||||||
|
<div class="buttons">
|
||||||
|
<Button
|
||||||
|
warning={true}
|
||||||
|
icon="GlobeStrike"
|
||||||
|
disabled={!isPublished}
|
||||||
|
on:click={unpublishApp}
|
||||||
|
>
|
||||||
|
Unpublish
|
||||||
|
</Button>
|
||||||
|
<Button cta on:click={viewApp}>View app</Button>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<TourWrap
|
{#if !isPublished}
|
||||||
tourStepKey={$store.onboarding
|
|
||||||
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
|
|
||||||
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
|
|
||||||
>
|
|
||||||
<span id="builder-app-users-button">
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
quiet
|
quiet
|
||||||
icon="UserGroup"
|
icon="GlobeStrike"
|
||||||
size="M"
|
size="M"
|
||||||
on:click={() => {
|
tooltip="Your app has not been published yet"
|
||||||
store.update(state => {
|
disabled
|
||||||
state.builderSidePanel = true
|
/>
|
||||||
return state
|
{/if}
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Users
|
|
||||||
</ActionButton>
|
|
||||||
</span>
|
|
||||||
</TourWrap>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
<TourWrap
|
||||||
bind:this={unpublishModal}
|
tourStepKey={$store.onboarding
|
||||||
title="Confirm unpublish"
|
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
|
||||||
okText="Unpublish app"
|
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
|
||||||
onOk={confirmUnpublishApp}
|
>
|
||||||
>
|
<span id="builder-app-users-button">
|
||||||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
<ActionButton
|
||||||
</ConfirmDialog>
|
quiet
|
||||||
|
icon="UserGroup"
|
||||||
|
size="M"
|
||||||
|
on:click={() => {
|
||||||
|
store.update(state => {
|
||||||
|
state.builderSidePanel = true
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Users
|
||||||
|
</ActionButton>
|
||||||
|
</span>
|
||||||
|
</TourWrap>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={unpublishModal}
|
||||||
|
title="Confirm unpublish"
|
||||||
|
okText="Unpublish app"
|
||||||
|
onOk={confirmUnpublishApp}
|
||||||
|
>
|
||||||
|
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||||
|
</ConfirmDialog>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<Button on:click={previewApp} secondary>Preview</Button>
|
<Button on:click={previewApp} secondary>Preview</Button>
|
||||||
<DeployModal onOk={completePublish} />
|
{#if $store.hasLock}
|
||||||
|
<DeployModal onOk={completePublish} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
|
||||||
|
export let disabled = false
|
||||||
|
|
||||||
let revertModal
|
let revertModal
|
||||||
let appName
|
let appName
|
||||||
|
|
||||||
|
@ -34,6 +36,7 @@
|
||||||
size="M"
|
size="M"
|
||||||
tooltip="Revert changes"
|
tooltip="Revert changes"
|
||||||
on:click={revertModal.show}
|
on:click={revertModal.show}
|
||||||
|
{disabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal bind:this={revertModal}>
|
<Modal bind:this={revertModal}>
|
||||||
|
|
|
@ -58,6 +58,7 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
transition: width 130ms ease-out;
|
transition: width 130ms ease-out;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.panel.borderLeft {
|
.panel.borderLeft {
|
||||||
border-left: var(--border-light);
|
border-left: var(--border-light);
|
||||||
|
|
|
@ -17,14 +17,14 @@ import URLSelect from "./controls/URLSelect.svelte"
|
||||||
import OptionsEditor from "./controls/OptionsEditor/OptionsEditor.svelte"
|
import OptionsEditor from "./controls/OptionsEditor/OptionsEditor.svelte"
|
||||||
import FormFieldSelect from "./controls/FormFieldSelect.svelte"
|
import FormFieldSelect from "./controls/FormFieldSelect.svelte"
|
||||||
import ValidationEditor from "./controls/ValidationEditor/ValidationEditor.svelte"
|
import ValidationEditor from "./controls/ValidationEditor/ValidationEditor.svelte"
|
||||||
import DrawerBindableCombobox from "components/common/bindings/DrawerBindableCombobox.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte"
|
import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte"
|
||||||
import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
|
import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
|
||||||
import BarButtonList from "./controls/BarButtonList.svelte"
|
import BarButtonList from "./controls/BarButtonList.svelte"
|
||||||
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
|
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
|
||||||
|
|
||||||
const componentMap = {
|
const componentMap = {
|
||||||
text: DrawerBindableCombobox,
|
text: DrawerBindableInput,
|
||||||
select: Select,
|
select: Select,
|
||||||
radio: RadioGroup,
|
radio: RadioGroup,
|
||||||
dataSource: DataSourceSelect,
|
dataSource: DataSourceSelect,
|
||||||
|
|
|
@ -126,8 +126,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAllBindings = (bindings, eventContextBindings, actions) => {
|
const getAllBindings = (bindings, eventContextBindings, actions) => {
|
||||||
let allBindings = eventContextBindings.concat(bindings)
|
let allBindings = []
|
||||||
|
|
||||||
if (!actions) {
|
if (!actions) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -145,14 +144,35 @@
|
||||||
.forEach(action => {
|
.forEach(action => {
|
||||||
// Check we have a binding for this action, and generate one if not
|
// Check we have a binding for this action, and generate one if not
|
||||||
const stateBinding = makeStateBinding(action.parameters.key)
|
const stateBinding = makeStateBinding(action.parameters.key)
|
||||||
const hasKey = allBindings.some(binding => {
|
const hasKey = bindings.some(binding => {
|
||||||
return binding.runtimeBinding === stateBinding.runtimeBinding
|
return binding.runtimeBinding === stateBinding.runtimeBinding
|
||||||
})
|
})
|
||||||
if (!hasKey) {
|
if (!hasKey) {
|
||||||
allBindings.push(stateBinding)
|
bindings.push(stateBinding)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// Get which indexes are asynchronous automations as we want to filter them out from the bindings
|
||||||
|
const asynchronousAutomationIndexes = actions
|
||||||
|
.map((action, index) => {
|
||||||
|
if (
|
||||||
|
action[EVENT_TYPE_KEY] === "Trigger Automation" &&
|
||||||
|
!action.parameters?.synchronous
|
||||||
|
) {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(index => index !== undefined)
|
||||||
|
|
||||||
|
// Based on the above, filter out the asynchronous automations from the bindings
|
||||||
|
if (asynchronousAutomationIndexes) {
|
||||||
|
allBindings = eventContextBindings
|
||||||
|
.filter((binding, index) => {
|
||||||
|
return !asynchronousAutomationIndexes.includes(index)
|
||||||
|
})
|
||||||
|
.concat(bindings)
|
||||||
|
} else {
|
||||||
|
allBindings = eventContextBindings.concat(bindings)
|
||||||
|
}
|
||||||
return allBindings
|
return allBindings
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Label, Input, Checkbox } from "@budibase/bbui"
|
import { Select, Label, Input, Checkbox, Icon } from "@budibase/bbui"
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import SaveFields from "./SaveFields.svelte"
|
import SaveFields from "./SaveFields.svelte"
|
||||||
import { TriggerStepID } from "constants/backend/automations"
|
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||||
|
|
||||||
export let parameters = {}
|
export let parameters = {}
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
|
@ -16,6 +16,14 @@
|
||||||
? AUTOMATION_STATUS.EXISTING
|
? AUTOMATION_STATUS.EXISTING
|
||||||
: AUTOMATION_STATUS.NEW
|
: AUTOMATION_STATUS.NEW
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (automationStatus === AUTOMATION_STATUS.NEW) {
|
||||||
|
parameters.synchronous = false
|
||||||
|
}
|
||||||
|
parameters.synchronous = automations.find(
|
||||||
|
automation => automation._id === parameters.automationId
|
||||||
|
)?.synchronous
|
||||||
|
}
|
||||||
$: automations = $automationStore.automations
|
$: automations = $automationStore.automations
|
||||||
.filter(a => a.definition.trigger?.stepId === TriggerStepID.APP)
|
.filter(a => a.definition.trigger?.stepId === TriggerStepID.APP)
|
||||||
.map(automation => {
|
.map(automation => {
|
||||||
|
@ -23,10 +31,15 @@
|
||||||
automation.definition.trigger.inputs.fields || {}
|
automation.definition.trigger.inputs.fields || {}
|
||||||
).map(([name, type]) => ({ name, type }))
|
).map(([name, type]) => ({ name, type }))
|
||||||
|
|
||||||
|
let hasCollectBlock = automation.definition.steps.some(
|
||||||
|
step => step.stepId === ActionStepID.COLLECT
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: automation.name,
|
name: automation.name,
|
||||||
_id: automation._id,
|
_id: automation._id,
|
||||||
schema,
|
schema,
|
||||||
|
synchronous: hasCollectBlock,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
$: hasAutomations = automations && automations.length > 0
|
$: hasAutomations = automations && automations.length > 0
|
||||||
|
@ -35,6 +48,8 @@
|
||||||
)
|
)
|
||||||
$: selectedSchema = selectedAutomation?.schema
|
$: selectedSchema = selectedAutomation?.schema
|
||||||
|
|
||||||
|
$: error = parameters.timeout > 120 ? "Timeout must be less than 120s" : null
|
||||||
|
|
||||||
const onFieldsChanged = e => {
|
const onFieldsChanged = e => {
|
||||||
parameters.fields = Object.entries(e.detail || {}).reduce(
|
parameters.fields = Object.entries(e.detail || {}).reduce(
|
||||||
(acc, [key, value]) => {
|
(acc, [key, value]) => {
|
||||||
|
@ -57,6 +72,14 @@
|
||||||
parameters.fields = {}
|
parameters.fields = {}
|
||||||
parameters.automationId = automations[0]?._id
|
parameters.automationId = automations[0]?._id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onChange = value => {
|
||||||
|
let automationId = value.detail
|
||||||
|
parameters.synchronous = automations.find(
|
||||||
|
automation => automation._id === automationId
|
||||||
|
)?.synchronous
|
||||||
|
parameters.automationId = automationId
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
|
@ -85,6 +108,7 @@
|
||||||
|
|
||||||
{#if automationStatus === AUTOMATION_STATUS.EXISTING}
|
{#if automationStatus === AUTOMATION_STATUS.EXISTING}
|
||||||
<Select
|
<Select
|
||||||
|
on:change={onChange}
|
||||||
bind:value={parameters.automationId}
|
bind:value={parameters.automationId}
|
||||||
placeholder="Choose automation"
|
placeholder="Choose automation"
|
||||||
options={automations}
|
options={automations}
|
||||||
|
@ -98,6 +122,29 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if parameters.synchronous}
|
||||||
|
<Label small />
|
||||||
|
|
||||||
|
<div class="synchronous-info">
|
||||||
|
<Icon name="Info" />
|
||||||
|
<div>
|
||||||
|
<i
|
||||||
|
>This automation will run synchronously as it contains a Collect
|
||||||
|
step</i
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Label small />
|
||||||
|
|
||||||
|
<div class="timeout-width">
|
||||||
|
<Input
|
||||||
|
label="Timeout in seconds (120 max)"
|
||||||
|
type="number"
|
||||||
|
{error}
|
||||||
|
bind:value={parameters.timeout}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<Label small />
|
<Label small />
|
||||||
<Checkbox
|
<Checkbox
|
||||||
text="Do not display default notification"
|
text="Do not display default notification"
|
||||||
|
@ -133,6 +180,9 @@
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
.timeout-width {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
.params {
|
.params {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -142,6 +192,11 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.synchronous-info {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
.fields {
|
.fields {
|
||||||
margin-top: var(--spacing-l);
|
margin-top: var(--spacing-l);
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
|
@ -57,7 +57,13 @@
|
||||||
{
|
{
|
||||||
"name": "Trigger Automation",
|
"name": "Trigger Automation",
|
||||||
"type": "application",
|
"type": "application",
|
||||||
"component": "TriggerAutomation"
|
"component": "TriggerAutomation",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "Automation Result",
|
||||||
|
"value": "result"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Update Field Value",
|
"name": "Update Field Value",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
export let componentBindings = []
|
export let componentBindings = []
|
||||||
export let nested = false
|
export let nested = false
|
||||||
export let highlighted = false
|
export let highlighted = false
|
||||||
|
export let propertyFocus = false
|
||||||
export let info = null
|
export let info = null
|
||||||
|
|
||||||
$: nullishValue = value == null || value === ""
|
$: nullishValue = value == null || value === ""
|
||||||
|
@ -72,6 +73,10 @@
|
||||||
if (highlighted) {
|
if (highlighted) {
|
||||||
store.actions.settings.highlight(null)
|
store.actions.settings.highlight(null)
|
||||||
}
|
}
|
||||||
|
// To fix focus 'affect' when property is target of a drawer other actions in the builder.
|
||||||
|
if (propertyFocus) {
|
||||||
|
store.actions.settings.propertyFocus(null)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -79,6 +84,7 @@
|
||||||
class="property-control"
|
class="property-control"
|
||||||
class:wide={!label || labelHidden}
|
class:wide={!label || labelHidden}
|
||||||
class:highlighted={highlighted && nullishValue}
|
class:highlighted={highlighted && nullishValue}
|
||||||
|
class:property-focus={propertyFocus}
|
||||||
>
|
>
|
||||||
{#if label && !labelHidden}
|
{#if label && !labelHidden}
|
||||||
<div class="label">
|
<div class="label">
|
||||||
|
@ -125,6 +131,14 @@
|
||||||
background: var(--spectrum-global-color-gray-300);
|
background: var(--spectrum-global-color-gray-300);
|
||||||
border-color: var(--spectrum-global-color-static-red-600);
|
border-color: var(--spectrum-global-color-static-red-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.property-control.property-focus :global(input) {
|
||||||
|
border-color: var(
|
||||||
|
--spectrum-textfield-m-border-color-down,
|
||||||
|
var(--spectrum-alias-border-color-mouse-focus)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { goto, beforeUrlChange } from "@roxi/routify"
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
Select,
|
Select,
|
||||||
|
@ -12,6 +12,8 @@
|
||||||
Heading,
|
Heading,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { notifications, Divider } from "@budibase/bbui"
|
import { notifications, Divider } from "@budibase/bbui"
|
||||||
import ExtraQueryConfig from "./ExtraQueryConfig.svelte"
|
import ExtraQueryConfig from "./ExtraQueryConfig.svelte"
|
||||||
|
@ -29,6 +31,12 @@
|
||||||
|
|
||||||
export let query
|
export let query
|
||||||
|
|
||||||
|
const resumeNavigation = () => {
|
||||||
|
if (typeof navigateTo == "string") {
|
||||||
|
$goto(typeof navigateTo == "string" ? `${navigateTo}` : navigateTo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const transformerDocs = "https://docs.budibase.com/docs/transformers"
|
const transformerDocs = "https://docs.budibase.com/docs/transformers"
|
||||||
|
|
||||||
let fields = query?.schema ? schemaToFields(query.schema) : []
|
let fields = query?.schema ? schemaToFields(query.schema) : []
|
||||||
|
@ -36,6 +44,31 @@
|
||||||
let data = []
|
let data = []
|
||||||
let saveId
|
let saveId
|
||||||
let currentTab = "JSON"
|
let currentTab = "JSON"
|
||||||
|
let saveModal
|
||||||
|
let override = false
|
||||||
|
let navigateTo = null
|
||||||
|
|
||||||
|
// seed the transformer
|
||||||
|
if (query && !query.transformer) {
|
||||||
|
query.transformer = "return data"
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialise a new empty schema
|
||||||
|
if (query && !query.schema) {
|
||||||
|
query.schema = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let queryStr = JSON.stringify(query)
|
||||||
|
|
||||||
|
$beforeUrlChange(event => {
|
||||||
|
const updated = JSON.stringify(query)
|
||||||
|
|
||||||
|
if (updated !== queryStr && !override) {
|
||||||
|
navigateTo = event.type == "pushstate" ? event.url : null
|
||||||
|
saveModal.show()
|
||||||
|
return false
|
||||||
|
} else return true
|
||||||
|
})
|
||||||
|
|
||||||
$: datasource = $datasources.list.find(ds => ds._id === query.datasourceId)
|
$: datasource = $datasources.list.find(ds => ds._id === query.datasourceId)
|
||||||
$: query.schema = fieldsToSchema(fields)
|
$: query.schema = fieldsToSchema(fields)
|
||||||
|
@ -60,11 +93,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// seed the transformer
|
|
||||||
if (query && !query.transformer) {
|
|
||||||
query.transformer = "return data"
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetDependentFields() {
|
function resetDependentFields() {
|
||||||
if (query.fields.extra) {
|
if (query.fields.extra) {
|
||||||
query.fields.extra = {}
|
query.fields.extra = {}
|
||||||
|
@ -101,22 +129,48 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// return the query.
|
||||||
async function saveQuery() {
|
async function saveQuery() {
|
||||||
try {
|
try {
|
||||||
const { _id } = await queries.save(query.datasourceId, query)
|
const response = await queries.save(query.datasourceId, query)
|
||||||
saveId = _id
|
saveId = response._id
|
||||||
notifications.success(`Query saved successfully`)
|
|
||||||
|
|
||||||
// Go to the correct URL if we just created a new query
|
if (response?._rev) {
|
||||||
if (!query._rev) {
|
queryStr = JSON.stringify(query)
|
||||||
$goto(`../../${_id}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error saving query")
|
notifications.error("Error saving query")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
bind:this={saveModal}
|
||||||
|
on:hide={() => {
|
||||||
|
navigateTo = null
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="You have unsaved changes"
|
||||||
|
confirmText="Save and Continue"
|
||||||
|
cancelText="Discard Changes"
|
||||||
|
size="L"
|
||||||
|
onConfirm={async () => {
|
||||||
|
await saveQuery()
|
||||||
|
override = true
|
||||||
|
resumeNavigation()
|
||||||
|
}}
|
||||||
|
onCancel={async () => {
|
||||||
|
override = true
|
||||||
|
resumeNavigation()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Body>Leaving this section will mean losing and changes to your query</Body>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<Layout gap="S" noPadding>
|
<Layout gap="S" noPadding>
|
||||||
<Heading size="M">Query {integrationInfo?.friendlyName}</Heading>
|
<Heading size="M">Query {integrationInfo?.friendlyName}</Heading>
|
||||||
|
@ -125,7 +179,13 @@
|
||||||
<div class="config">
|
<div class="config">
|
||||||
<div class="config-field">
|
<div class="config-field">
|
||||||
<Label>Query Name</Label>
|
<Label>Query Name</Label>
|
||||||
<Input bind:value={query.name} />
|
<Input
|
||||||
|
value={query.name}
|
||||||
|
on:input={e => {
|
||||||
|
let newValue = e.target.value || ""
|
||||||
|
query.name = newValue.trim()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if queryConfig}
|
{#if queryConfig}
|
||||||
<div class="config-field">
|
<div class="config-field">
|
||||||
|
@ -149,18 +209,20 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#key query.parameters}
|
{#key query.parameters}
|
||||||
<BindingBuilder
|
<div class="binding-wrap">
|
||||||
queryBindings={query.parameters}
|
<BindingBuilder
|
||||||
bindable={false}
|
queryBindings={query.parameters}
|
||||||
on:change={e => {
|
bindable={false}
|
||||||
query.parameters = e.detail.map(binding => {
|
on:change={e => {
|
||||||
return {
|
query.parameters = e.detail.map(binding => {
|
||||||
name: binding.name,
|
return {
|
||||||
default: binding.value,
|
name: binding.name,
|
||||||
}
|
default: binding.value,
|
||||||
})
|
}
|
||||||
}}
|
})
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{/key}
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -203,7 +265,18 @@
|
||||||
<div class="viewer-controls">
|
<div class="viewer-controls">
|
||||||
<Heading size="S">Results</Heading>
|
<Heading size="S">Results</Heading>
|
||||||
<ButtonGroup gap="XS">
|
<ButtonGroup gap="XS">
|
||||||
<Button cta disabled={queryInvalid} on:click={saveQuery}>
|
<Button
|
||||||
|
cta
|
||||||
|
disabled={queryInvalid}
|
||||||
|
on:click={async () => {
|
||||||
|
await saveQuery()
|
||||||
|
notifications.success(`Query saved successfully`)
|
||||||
|
// Go to the correct URL if we just created a new query
|
||||||
|
if (!query._rev) {
|
||||||
|
$goto(`../../${query._id}`)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
Save Query
|
Save Query
|
||||||
</Button>
|
</Button>
|
||||||
<Button secondary on:click={previewQuery}>Run Query</Button>
|
<Button secondary on:click={previewQuery}>Run Query</Button>
|
||||||
|
@ -274,4 +347,9 @@
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.binding-wrap :global(div.container) {
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-right: 0px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -71,6 +71,9 @@
|
||||||
tourStep.onComplete()
|
tourStep.onComplete()
|
||||||
}
|
}
|
||||||
popover.hide()
|
popover.hide()
|
||||||
|
if (tourStep.endRoute) {
|
||||||
|
$goto(tourStep.endRoute)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -76,6 +76,7 @@ const getTours = () => {
|
||||||
title: "Publish",
|
title: "Publish",
|
||||||
layout: OnboardingPublish,
|
layout: OnboardingPublish,
|
||||||
route: "/builder/app/:application/design",
|
route: "/builder/app/:application/design",
|
||||||
|
endRoute: "/builder/app/:application/data",
|
||||||
query: ".toprightnav #builder-app-publish-button",
|
query: ".toprightnav #builder-app-publish-button",
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH)
|
tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH)
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { Heading, Body, Button, Icon, notifications } from "@budibase/bbui"
|
import { Heading, Body, Button, Icon } from "@budibase/bbui"
|
||||||
import AppLockModal from "../common/AppLockModal.svelte"
|
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
import { UserAvatar } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
|
|
||||||
export let lockedAction
|
export let lockedAction
|
||||||
|
|
||||||
|
$: editing = app?.lockedBy != null
|
||||||
|
|
||||||
const handleDefaultClick = () => {
|
const handleDefaultClick = () => {
|
||||||
if (window.innerWidth < 640) {
|
if (window.innerWidth < 640) {
|
||||||
goToOverview()
|
goToOverview()
|
||||||
|
@ -17,12 +18,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToBuilder = () => {
|
const goToBuilder = () => {
|
||||||
if (app.lockedOther) {
|
|
||||||
notifications.error(
|
|
||||||
`App locked by ${app.lockedBy.email}. Please allow lock to expire or have them unlock this app.`
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
$goto(`../../app/${app.devId}`)
|
$goto(`../../app/${app.devId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +39,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="updated">
|
<div class="updated">
|
||||||
{#if app.updatedAt}
|
{#if editing}
|
||||||
|
Currently editing
|
||||||
|
<UserAvatar user={app.lockedBy} />
|
||||||
|
{:else if app.updatedAt}
|
||||||
{processStringSync("Updated {{ duration time 'millisecond' }} ago", {
|
{processStringSync("Updated {{ duration time 'millisecond' }} ago", {
|
||||||
time: new Date().getTime() - new Date(app.updatedAt).getTime(),
|
time: new Date().getTime() - new Date(app.updatedAt).getTime(),
|
||||||
})}
|
})}
|
||||||
|
@ -59,12 +57,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="app-row-actions">
|
<div class="app-row-actions">
|
||||||
<AppLockModal {app} buttonSize="M" />
|
<Button size="S" secondary on:click={lockedAction || goToOverview}>
|
||||||
<Button size="S" secondary on:click={lockedAction || goToOverview}
|
Manage
|
||||||
>Manage</Button
|
</Button>
|
||||||
>
|
<Button size="S" primary on:click={lockedAction || goToBuilder}>
|
||||||
<Button size="S" primary on:click={lockedAction || goToBuilder}>Edit</Button
|
Edit
|
||||||
>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -87,6 +85,9 @@
|
||||||
|
|
||||||
.updated {
|
.updated {
|
||||||
color: var(--spectrum-global-color-gray-700);
|
color: var(--spectrum-global-color-gray-700);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title,
|
.title,
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { writable, get as svelteGet } from "svelte/store"
|
import { writable, get as svelteGet } from "svelte/store"
|
||||||
import {
|
import { notifications, Input, ModalContent, Dropzone } from "@budibase/bbui"
|
||||||
notifications,
|
|
||||||
Input,
|
|
||||||
ModalContent,
|
|
||||||
Dropzone,
|
|
||||||
Toggle,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { store, automationStore } from "builderStore"
|
import { store, automationStore } from "builderStore"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { apps, admin, auth } from "stores/portal"
|
import { apps, admin, auth } from "stores/portal"
|
||||||
|
@ -22,7 +16,6 @@
|
||||||
|
|
||||||
let creating = false
|
let creating = false
|
||||||
let defaultAppName
|
let defaultAppName
|
||||||
let includeSampleDB = true
|
|
||||||
|
|
||||||
const values = writable({ name: "", url: null })
|
const values = writable({ name: "", url: null })
|
||||||
const validation = createValidationStore()
|
const validation = createValidationStore()
|
||||||
|
@ -117,8 +110,6 @@
|
||||||
data.append("templateName", template.name)
|
data.append("templateName", template.name)
|
||||||
data.append("templateKey", template.key)
|
data.append("templateKey", template.key)
|
||||||
data.append("templateFile", $values.file)
|
data.append("templateFile", $values.file)
|
||||||
} else {
|
|
||||||
data.append("sampleData", includeSampleDB)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create App
|
// Create App
|
||||||
|
@ -213,15 +204,6 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{#if !template && !template?.fromFile}
|
|
||||||
<span>
|
|
||||||
<Toggle
|
|
||||||
text="Include sample data"
|
|
||||||
bind:value={includeSampleDB}
|
|
||||||
disabled={creating}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -20,9 +20,14 @@ export const ActionStepID = {
|
||||||
FILTER: "FILTER",
|
FILTER: "FILTER",
|
||||||
QUERY_ROWS: "QUERY_ROWS",
|
QUERY_ROWS: "QUERY_ROWS",
|
||||||
LOOP: "LOOP",
|
LOOP: "LOOP",
|
||||||
|
COLLECT: "COLLECT",
|
||||||
// these used to be lowercase step IDs, maintain for backwards compat
|
// these used to be lowercase step IDs, maintain for backwards compat
|
||||||
discord: "discord",
|
discord: "discord",
|
||||||
slack: "slack",
|
slack: "slack",
|
||||||
zapier: "zapier",
|
zapier: "zapier",
|
||||||
integromat: "integromat",
|
integromat: "integromat",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Features = {
|
||||||
|
LOOPING: "LOOPING",
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script>
|
||||||
|
import { UserAvatar } from "@budibase/frontend-core"
|
||||||
|
|
||||||
|
export let users = []
|
||||||
|
|
||||||
|
$: uniqueUsers = unique(users)
|
||||||
|
|
||||||
|
const unique = users => {
|
||||||
|
let uniqueUsers = {}
|
||||||
|
users?.forEach(user => {
|
||||||
|
uniqueUsers[user.email] = user
|
||||||
|
})
|
||||||
|
return Object.values(uniqueUsers)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="avatars">
|
||||||
|
{#each uniqueUsers as user}
|
||||||
|
<UserAvatar {user} tooltipDirection="bottom" />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.avatars {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { store, automationStore } from "builderStore"
|
import { store, automationStore, userStore } from "builderStore"
|
||||||
import { roles, flags } from "stores/backend"
|
import { roles, flags } from "stores/backend"
|
||||||
import { auth } from "stores/portal"
|
import { auth } from "stores/portal"
|
||||||
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
||||||
|
@ -13,7 +13,6 @@
|
||||||
Modal,
|
Modal,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
|
|
||||||
import AppActions from "components/deploy/AppActions.svelte"
|
import AppActions from "components/deploy/AppActions.svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
||||||
|
@ -23,6 +22,7 @@
|
||||||
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
||||||
import TourPopover from "components/portal/onboarding/TourPopover.svelte"
|
import TourPopover from "components/portal/onboarding/TourPopover.svelte"
|
||||||
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
|
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
|
||||||
|
import UserAvatars from "./_components/UserAvatars.svelte"
|
||||||
import { TOUR_KEYS, TOURS } from "components/portal/onboarding/tours.js"
|
import { TOUR_KEYS, TOURS } from "components/portal/onboarding/tours.js"
|
||||||
|
|
||||||
export let application
|
export let application
|
||||||
|
@ -30,7 +30,9 @@
|
||||||
let promise = getPackage()
|
let promise = getPackage()
|
||||||
let hasSynced = false
|
let hasSynced = false
|
||||||
let commandPaletteModal
|
let commandPaletteModal
|
||||||
|
let loaded = false
|
||||||
|
|
||||||
|
$: loaded && initTour()
|
||||||
$: selected = capitalise(
|
$: selected = capitalise(
|
||||||
$layout.children.find(layout => $isActive(layout.path))?.title ?? "data"
|
$layout.children.find(layout => $isActive(layout.path))?.title ?? "data"
|
||||||
)
|
)
|
||||||
|
@ -43,6 +45,7 @@
|
||||||
await automationStore.actions.fetch()
|
await automationStore.actions.fetch()
|
||||||
await roles.fetch()
|
await roles.fetch()
|
||||||
await flags.fetch()
|
await flags.fetch()
|
||||||
|
loaded = true
|
||||||
return pkg
|
return pkg
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error(`Error initialising app: ${error?.message}`)
|
notifications.error(`Error initialising app: ${error?.message}`)
|
||||||
|
@ -67,13 +70,18 @@
|
||||||
|
|
||||||
// Event handler for the command palette
|
// Event handler for the command palette
|
||||||
const handleKeyDown = e => {
|
const handleKeyDown = e => {
|
||||||
if (e.key === "k" && (e.ctrlKey || e.metaKey)) {
|
if (e.key === "k" && (e.ctrlKey || e.metaKey) && $store.hasLock) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
commandPaletteModal.toggle()
|
commandPaletteModal.toggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const initTour = async () => {
|
const initTour = async () => {
|
||||||
|
// Skip tour if we don't have the lock
|
||||||
|
if (!$store.hasLock) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check if onboarding is enabled.
|
// Check if onboarding is enabled.
|
||||||
if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) {
|
if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) {
|
||||||
if (!$auth.user?.onboardedAt) {
|
if (!$auth.user?.onboardedAt) {
|
||||||
|
@ -110,7 +118,6 @@
|
||||||
// check if user has beta access
|
// check if user has beta access
|
||||||
// const betaResponse = await API.checkBetaAccess($auth?.user?.email)
|
// const betaResponse = await API.checkBetaAccess($auth?.user?.email)
|
||||||
// betaAccess = betaResponse.access
|
// betaAccess = betaResponse.access
|
||||||
initTour()
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Failed to sync with production database")
|
notifications.error("Failed to sync with production database")
|
||||||
}
|
}
|
||||||
|
@ -119,10 +126,11 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
store.update(state => {
|
// Run async on a slight delay to let other cleanup logic run without
|
||||||
state.appId = null
|
// being confused by the store wiping
|
||||||
return state
|
setTimeout(() => {
|
||||||
})
|
store.actions.reset()
|
||||||
|
}, 10)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -134,74 +142,89 @@
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<div class="top-nav">
|
<div class="top-nav">
|
||||||
<div class="topleftnav">
|
{#if $store.initialised}
|
||||||
<ActionMenu>
|
<div class="topleftnav">
|
||||||
<div slot="control">
|
<ActionMenu>
|
||||||
<Icon size="M" hoverable name="ShowMenu" />
|
<div slot="control">
|
||||||
</div>
|
<Icon size="M" hoverable name="ShowMenu" />
|
||||||
<MenuItem on:click={() => $goto("../../portal/apps")}>
|
</div>
|
||||||
Exit to portal
|
<MenuItem on:click={() => $goto("../../portal/apps")}>
|
||||||
</MenuItem>
|
Exit to portal
|
||||||
<MenuItem
|
</MenuItem>
|
||||||
on:click={() => $goto(`../../portal/overview/${application}`)}
|
<MenuItem
|
||||||
>
|
on:click={() => $goto(`../../portal/overview/${application}`)}
|
||||||
Overview
|
>
|
||||||
</MenuItem>
|
Overview
|
||||||
<MenuItem
|
</MenuItem>
|
||||||
on:click={() => $goto(`../../portal/overview/${application}/access`)}
|
<MenuItem
|
||||||
>
|
on:click={() =>
|
||||||
Access
|
$goto(`../../portal/overview/${application}/access`)}
|
||||||
</MenuItem>
|
>
|
||||||
<MenuItem
|
Access
|
||||||
on:click={() =>
|
</MenuItem>
|
||||||
$goto(`../../portal/overview/${application}/automation-history`)}
|
<MenuItem
|
||||||
>
|
on:click={() =>
|
||||||
Automation history
|
$goto(`../../portal/overview/${application}/automation-history`)}
|
||||||
</MenuItem>
|
>
|
||||||
<MenuItem
|
Automation history
|
||||||
on:click={() => $goto(`../../portal/overview/${application}/backups`)}
|
</MenuItem>
|
||||||
>
|
<MenuItem
|
||||||
Backups
|
on:click={() =>
|
||||||
</MenuItem>
|
$goto(`../../portal/overview/${application}/backups`)}
|
||||||
|
>
|
||||||
|
Backups
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
$goto(`../../portal/overview/${application}/name-and-url`)}
|
$goto(`../../portal/overview/${application}/name-and-url`)}
|
||||||
>
|
>
|
||||||
Name and URL
|
Name and URL
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
on:click={() => $goto(`../../portal/overview/${application}/version`)}
|
on:click={() =>
|
||||||
>
|
$goto(`../../portal/overview/${application}/version`)}
|
||||||
Version
|
>
|
||||||
</MenuItem>
|
Version
|
||||||
</ActionMenu>
|
</MenuItem>
|
||||||
<Heading size="XS">{$store.name}</Heading>
|
</ActionMenu>
|
||||||
</div>
|
<Heading size="XS">{$store.name}</Heading>
|
||||||
<div class="topcenternav">
|
</div>
|
||||||
<Tabs {selected} size="M">
|
<div class="topcenternav">
|
||||||
{#each $layout.children as { path, title }}
|
{#if $store.hasLock}
|
||||||
<TourWrap tourStepKey={`builder-${title}-section`}>
|
<Tabs {selected} size="M">
|
||||||
<Tab
|
{#each $layout.children as { path, title }}
|
||||||
quiet
|
<TourWrap tourStepKey={`builder-${title}-section`}>
|
||||||
selected={$isActive(path)}
|
<Tab
|
||||||
on:click={topItemNavigate(path)}
|
quiet
|
||||||
title={capitalise(title)}
|
selected={$isActive(path)}
|
||||||
id={`builder-${title}-tab`}
|
on:click={topItemNavigate(path)}
|
||||||
/>
|
title={capitalise(title)}
|
||||||
</TourWrap>
|
id={`builder-${title}-tab`}
|
||||||
{/each}
|
/>
|
||||||
</Tabs>
|
</TourWrap>
|
||||||
</div>
|
{/each}
|
||||||
<div class="toprightnav">
|
</Tabs>
|
||||||
<AppActions {application} />
|
{:else}
|
||||||
</div>
|
<div class="secondary-editor">
|
||||||
|
<Icon name="LockClosed" />
|
||||||
|
Another user is currently editing your screens and automations
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="toprightnav">
|
||||||
|
<UserAvatars users={$userStore} />
|
||||||
|
<AppActions {application} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#await promise}
|
{#await promise}
|
||||||
<!-- This should probably be some kind of loading state? -->
|
<!-- This should probably be some kind of loading state? -->
|
||||||
<div class="loading" />
|
<div class="loading" />
|
||||||
{:then _}
|
{:then _}
|
||||||
<slot />
|
<div class="body">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
{:catch error}
|
{:catch error}
|
||||||
<p>Something went wrong: {error.message}</p>
|
<p>Something went wrong: {error.message}</p>
|
||||||
{/await}
|
{/await}
|
||||||
|
@ -237,6 +260,7 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
border-bottom: var(--border-light);
|
border-bottom: var(--border-light);
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topleftnav {
|
.topleftnav {
|
||||||
|
@ -270,4 +294,18 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.secondary-editor {
|
||||||
|
align-self: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,6 +8,15 @@
|
||||||
import { onDestroy, onMount } from "svelte"
|
import { onDestroy, onMount } from "svelte"
|
||||||
import { syncURLToState } from "helpers/urlStateSync"
|
import { syncURLToState } from "helpers/urlStateSync"
|
||||||
import * as routify from "@roxi/routify"
|
import * as routify from "@roxi/routify"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import { redirect } from "@roxi/routify"
|
||||||
|
|
||||||
|
// Prevent access for other users than the lock holder
|
||||||
|
$: {
|
||||||
|
if (!$store.hasLock) {
|
||||||
|
$redirect("../data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Keep URL and state in sync for selected screen ID
|
// Keep URL and state in sync for selected screen ID
|
||||||
const stopSyncing = syncURLToState({
|
const stopSyncing = syncURLToState({
|
||||||
|
|
|
@ -2,13 +2,11 @@
|
||||||
import { Button } from "@budibase/bbui"
|
import { Button } from "@budibase/bbui"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="beta-background" />
|
|
||||||
<div class="beta">
|
<div class="beta">
|
||||||
Enjoying the Grid?
|
|
||||||
<Button
|
<Button
|
||||||
size="M"
|
size="M"
|
||||||
cta
|
cta
|
||||||
on:click={() => window.open("https://t.maze.co/156382627", "_blank")}
|
on:click={() => window.open("https://t.maze.co/165900794", "_blank")}
|
||||||
>
|
>
|
||||||
Give Feedback
|
Give Feedback
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -17,30 +15,16 @@
|
||||||
<style>
|
<style>
|
||||||
.beta {
|
.beta {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 32px;
|
bottom: 24px;
|
||||||
right: 32px;
|
right: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
.beta :global(.spectrum-Button) {
|
.beta :global(.spectrum-Button) {
|
||||||
background: var(--spectrum-global-color-magenta-400);
|
background: var(--spectrum-global-color-magenta-400);
|
||||||
border-color: var(--spectrum-global-color-magenta-400);
|
border-color: var(--spectrum-global-color-magenta-400);
|
||||||
}
|
}
|
||||||
.beta-background {
|
|
||||||
z-index: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
bottom: -230px;
|
|
||||||
right: -105px;
|
|
||||||
width: 1400px;
|
|
||||||
height: 320px;
|
|
||||||
transform: rotate(-22deg);
|
|
||||||
background: linear-gradient(
|
|
||||||
to top,
|
|
||||||
var(--cell-background) 20%,
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script>
|
||||||
|
import { Body, Label } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let title
|
||||||
|
export let description
|
||||||
|
export let disabled
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div on:click class:disabled class="option">
|
||||||
|
<div class="header">
|
||||||
|
<div class="icon">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<Body>{title}</Body>
|
||||||
|
</div>
|
||||||
|
<Label>{description}</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.option {
|
||||||
|
background-color: var(--background);
|
||||||
|
border: 1px solid var(--grey-4);
|
||||||
|
padding: 10px 16px 14px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option :global(label) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option:hover {
|
||||||
|
background-color: var(--background-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,25 +1,26 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, Layout } from "@budibase/bbui"
|
import { Button, Layout } from "@budibase/bbui"
|
||||||
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
|
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
|
||||||
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
|
|
||||||
import Panel from "components/design/Panel.svelte"
|
import Panel from "components/design/Panel.svelte"
|
||||||
|
import { isActive, goto } from "@roxi/routify"
|
||||||
let modal
|
import BetaButton from "./_components/BetaButton.svelte"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- routify:options index=1 -->
|
<!-- routify:options index=1 -->
|
||||||
<div class="data">
|
<div class="data">
|
||||||
<Panel title="Sources" borderRight>
|
{#if !$isActive("./new")}
|
||||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
<Panel title="Sources" borderRight>
|
||||||
<Button cta on:click={modal.show}>Add source</Button>
|
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||||
<CreateDatasourceModal bind:modal />
|
<Button cta on:click={() => $goto("./new")}>Add source</Button>
|
||||||
<DatasourceNavigator />
|
<DatasourceNavigator />
|
||||||
</Layout>
|
</Layout>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
<BetaButton />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -40,5 +41,6 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,22 +1,17 @@
|
||||||
<script>
|
<script>
|
||||||
import { redirect } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { admin } from "stores/portal"
|
|
||||||
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
|
|
||||||
import { datasources } from "stores/backend"
|
import { datasources } from "stores/backend"
|
||||||
|
|
||||||
let modal
|
$: hasData =
|
||||||
$: setupComplete =
|
|
||||||
$datasources.list.find(x => (x._id = "bb_internal"))?.entities?.length >
|
$datasources.list.find(x => (x._id = "bb_internal"))?.entities?.length >
|
||||||
1 || $datasources.list.length > 1
|
1 || $datasources.list.length > 1
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!setupComplete && !$admin.isDev) {
|
if (!hasData) {
|
||||||
modal.show()
|
$redirect("./new")
|
||||||
} else {
|
} else {
|
||||||
$redirect("./table")
|
$redirect("./table")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CreateDatasourceModal bind:modal />
|
|
||||||
|
|
|
@ -0,0 +1,257 @@
|
||||||
|
<script>
|
||||||
|
import { API } from "api"
|
||||||
|
import { tables, datasources } from "stores/backend"
|
||||||
|
|
||||||
|
import { Icon, Modal, notifications, Heading, Body } from "@budibase/bbui"
|
||||||
|
import { params, goto } from "@roxi/routify"
|
||||||
|
import {
|
||||||
|
IntegrationTypes,
|
||||||
|
DatasourceTypes,
|
||||||
|
DEFAULT_BB_DATASOURCE_ID,
|
||||||
|
} from "constants/backend"
|
||||||
|
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||||
|
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
||||||
|
import GoogleDatasourceConfigModal from "components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte"
|
||||||
|
import { createRestDatasource } from "builderStore/datasource"
|
||||||
|
import DatasourceOption from "./_components/DatasourceOption.svelte"
|
||||||
|
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
||||||
|
import ICONS from "components/backend/DatasourceNavigator/icons/index.js"
|
||||||
|
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
||||||
|
|
||||||
|
let internalTableModal
|
||||||
|
let externalDatasourceModal
|
||||||
|
let integrations = []
|
||||||
|
let integration = null
|
||||||
|
let disabled = false
|
||||||
|
let promptUpload = false
|
||||||
|
|
||||||
|
$: hasData = $datasources.list.length > 1 || $tables.list.length > 1
|
||||||
|
$: hasDefaultData =
|
||||||
|
$datasources.list.findIndex(
|
||||||
|
datasource => datasource._id === DEFAULT_BB_DATASOURCE_ID
|
||||||
|
) !== -1
|
||||||
|
|
||||||
|
const createSampleData = async () => {
|
||||||
|
disabled = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await API.addSampleData($params.application)
|
||||||
|
await tables.fetch()
|
||||||
|
await datasources.fetch()
|
||||||
|
$goto("./table")
|
||||||
|
} catch (e) {
|
||||||
|
disabled = false
|
||||||
|
notifications.error("Error creating datasource")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleIntegrationSelect = integrationType => {
|
||||||
|
const selected = integrations.find(([type]) => type === integrationType)[1]
|
||||||
|
|
||||||
|
// build the schema
|
||||||
|
const config = {}
|
||||||
|
|
||||||
|
for (let key of Object.keys(selected.datasource)) {
|
||||||
|
config[key] = selected.datasource[key].default
|
||||||
|
}
|
||||||
|
|
||||||
|
integration = {
|
||||||
|
type: integrationType,
|
||||||
|
plus: selected.plus,
|
||||||
|
config,
|
||||||
|
schema: selected.datasource,
|
||||||
|
auth: selected.auth,
|
||||||
|
features: selected.features || [],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected.friendlyName) {
|
||||||
|
integration.name = selected.friendlyName
|
||||||
|
}
|
||||||
|
|
||||||
|
if (integration.type === IntegrationTypes.REST) {
|
||||||
|
disabled = true
|
||||||
|
|
||||||
|
// Skip modal for rest, create straight away
|
||||||
|
createRestDatasource(integration)
|
||||||
|
.then(response => {
|
||||||
|
$goto(`./datasource/${response._id}`)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
disabled = false
|
||||||
|
notifications.error("Error creating datasource")
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
externalDatasourceModal.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInternalTable = () => {
|
||||||
|
promptUpload = false
|
||||||
|
internalTableModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDataImport = () => {
|
||||||
|
promptUpload = true
|
||||||
|
internalTableModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInternalTableSave = table => {
|
||||||
|
notifications.success(`Table created successfully.`)
|
||||||
|
$goto(`./table/${table._id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortIntegrations(integrations) {
|
||||||
|
let integrationsArray = Object.entries(integrations)
|
||||||
|
|
||||||
|
function getTypeOrder(schema) {
|
||||||
|
if (schema.type === DatasourceTypes.API) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema.type === DatasourceTypes.RELATIONAL) {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema.type?.charCodeAt(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
integrationsArray.sort((a, b) => {
|
||||||
|
let typeOrderA = getTypeOrder(a[1])
|
||||||
|
let typeOrderB = getTypeOrder(b[1])
|
||||||
|
|
||||||
|
if (typeOrderA === typeOrderB) {
|
||||||
|
return a[1].friendlyName?.localeCompare(b[1].friendlyName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeOrderA < typeOrderB ? -1 : 1
|
||||||
|
})
|
||||||
|
|
||||||
|
return integrationsArray
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchIntegrations = async () => {
|
||||||
|
const unsortedIntegrations = await API.getIntegrations()
|
||||||
|
integrations = sortIntegrations(unsortedIntegrations)
|
||||||
|
}
|
||||||
|
|
||||||
|
$: fetchIntegrations()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={internalTableModal}>
|
||||||
|
<CreateTableModal {promptUpload} afterSave={handleInternalTableSave} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal bind:this={externalDatasourceModal}>
|
||||||
|
{#if integration?.auth?.type === "google"}
|
||||||
|
<GoogleDatasourceConfigModal {integration} />
|
||||||
|
{:else}
|
||||||
|
<DatasourceConfigModal {integration} />
|
||||||
|
{/if}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
<div class="closeButton">
|
||||||
|
{#if hasData}
|
||||||
|
<Icon hoverable name="Close" on:click={$goto("./table")} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="heading">
|
||||||
|
<Heading weight="light">Add new data source</Heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="subHeading">
|
||||||
|
<Body>Get started with our Budibase DB</Body>
|
||||||
|
<div
|
||||||
|
role="tooltip"
|
||||||
|
title="Budibase DB is built with CouchDB"
|
||||||
|
class="tooltip"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon name="fa-solid fa-circle-info" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="options">
|
||||||
|
<DatasourceOption
|
||||||
|
on:click={handleInternalTable}
|
||||||
|
title="Create new table"
|
||||||
|
description="Non-relational"
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
<svelte:component this={ICONS.BUDIBASE} height="20" width="20" />
|
||||||
|
</DatasourceOption>
|
||||||
|
<DatasourceOption
|
||||||
|
on:click={createSampleData}
|
||||||
|
title="Use sample data"
|
||||||
|
description="Non-relational"
|
||||||
|
disabled={disabled || hasDefaultData}
|
||||||
|
>
|
||||||
|
<svelte:component this={ICONS.BUDIBASE} height="20" width="20" />
|
||||||
|
</DatasourceOption>
|
||||||
|
<DatasourceOption
|
||||||
|
on:click={handleDataImport}
|
||||||
|
title="Upload data"
|
||||||
|
description="Non-relational"
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
<svelte:component this={ICONS.BUDIBASE} height="20" width="20" />
|
||||||
|
</DatasourceOption>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="subHeading">
|
||||||
|
<Body>Or connect to an external datasource</Body>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="options">
|
||||||
|
{#each integrations as [key, value]}
|
||||||
|
<DatasourceOption
|
||||||
|
on:click={() => handleIntegrationSelect(key)}
|
||||||
|
title={value.friendlyName}
|
||||||
|
description={value.type}
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
<IntegrationIcon integrationType={key} schema={value} />
|
||||||
|
</DatasourceOption>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
height: 38px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: right;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subHeading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
column-gap: 24px;
|
||||||
|
row-gap: 24px;
|
||||||
|
grid-template-columns: repeat(auto-fit, 235px);
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
max-width: 1050px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -3,8 +3,10 @@
|
||||||
import QueryViewer from "components/integration/QueryViewer.svelte"
|
import QueryViewer from "components/integration/QueryViewer.svelte"
|
||||||
import RestQueryViewer from "components/integration/RestQueryViewer.svelte"
|
import RestQueryViewer from "components/integration/RestQueryViewer.svelte"
|
||||||
import { IntegrationTypes } from "constants/backend"
|
import { IntegrationTypes } from "constants/backend"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
$: query = $queries.selected
|
$: query = $queries.selected
|
||||||
|
$: editableQuery = cloneDeep(query)
|
||||||
$: datasource = $datasources.list.find(ds => ds._id === query?.datasourceId)
|
$: datasource = $datasources.list.find(ds => ds._id === query?.datasourceId)
|
||||||
$: isRestQuery = datasource?.source === IntegrationTypes.REST
|
$: isRestQuery = datasource?.source === IntegrationTypes.REST
|
||||||
</script>
|
</script>
|
||||||
|
@ -13,6 +15,6 @@
|
||||||
{#if isRestQuery}
|
{#if isRestQuery}
|
||||||
<RestQueryViewer queryId={$queries.selectedQueryId} />
|
<RestQueryViewer queryId={$queries.selectedQueryId} />
|
||||||
{:else}
|
{:else}
|
||||||
<QueryViewer {query} />
|
<QueryViewer query={editableQuery} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -101,7 +101,12 @@
|
||||||
}
|
}
|
||||||
// Ignore events when typing
|
// Ignore events when typing
|
||||||
const activeTag = document.activeElement?.tagName.toLowerCase()
|
const activeTag = document.activeElement?.tagName.toLowerCase()
|
||||||
if (["input", "textarea"].indexOf(activeTag) !== -1 && e.key !== "Escape") {
|
const inCodeEditor =
|
||||||
|
document.activeElement?.classList?.contains("cm-content")
|
||||||
|
if (
|
||||||
|
(inCodeEditor || ["input", "textarea"].indexOf(activeTag) !== -1) &&
|
||||||
|
e.key !== "Escape"
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Key events are always for the selected component
|
// Key events are always for the selected component
|
||||||
|
|
|
@ -140,6 +140,7 @@
|
||||||
nested={setting.nested}
|
nested={setting.nested}
|
||||||
onChange={val => updateSetting(setting, val)}
|
onChange={val => updateSetting(setting, val)}
|
||||||
highlighted={$store.highlightedSettingKey === setting.key}
|
highlighted={$store.highlightedSettingKey === setting.key}
|
||||||
|
propertyFocus={$store.propertyFocus === setting.key}
|
||||||
info={setting.info}
|
info={setting.info}
|
||||||
props={{
|
props={{
|
||||||
// Generic settings
|
// Generic settings
|
||||||
|
|
|
@ -1,2 +1,14 @@
|
||||||
|
<script>
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import { redirect } from "@roxi/routify"
|
||||||
|
|
||||||
|
// Prevent access for other users than the lock holder
|
||||||
|
$: {
|
||||||
|
if (!$store.hasLock) {
|
||||||
|
$redirect("../data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- routify:options index=2 -->
|
<!-- routify:options index=2 -->
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
Divider,
|
Divider,
|
||||||
ActionMenu,
|
ActionMenu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Avatar,
|
|
||||||
Page,
|
Page,
|
||||||
Icon,
|
Icon,
|
||||||
Body,
|
Body,
|
||||||
|
@ -22,6 +21,8 @@
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
import Spaceman from "assets/bb-space-man.svg"
|
import Spaceman from "assets/bb-space-man.svg"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
|
import { UserAvatar } from "@budibase/frontend-core"
|
||||||
|
import { helpers } from "@budibase/shared-core"
|
||||||
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
let userInfoModal
|
let userInfoModal
|
||||||
|
@ -96,11 +97,7 @@
|
||||||
<img class="logo" alt="logo" src={$organisation.logoUrl || Logo} />
|
<img class="logo" alt="logo" src={$organisation.logoUrl || Logo} />
|
||||||
<ActionMenu align="right">
|
<ActionMenu align="right">
|
||||||
<div slot="control" class="avatar">
|
<div slot="control" class="avatar">
|
||||||
<Avatar
|
<UserAvatar user={$auth.user} showTooltip={false} />
|
||||||
size="M"
|
|
||||||
initials={$auth.initials}
|
|
||||||
url={$auth.user.pictureUrl}
|
|
||||||
/>
|
|
||||||
<Icon size="XL" name="ChevronDown" />
|
<Icon size="XL" name="ChevronDown" />
|
||||||
</div>
|
</div>
|
||||||
<MenuItem icon="UserEdit" on:click={() => userInfoModal.show()}>
|
<MenuItem icon="UserEdit" on:click={() => userInfoModal.show()}>
|
||||||
|
@ -125,7 +122,7 @@
|
||||||
</div>
|
</div>
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
<Heading size="M">
|
<Heading size="M">
|
||||||
Hey {$auth.user.firstName || $auth.user.email}
|
Hey {helpers.getUserLabel($auth.user)}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Body>
|
<Body>
|
||||||
Welcome to the {$organisation.company} portal. Below you'll find the
|
Welcome to the {$organisation.company} portal. Below you'll find the
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { auth } from "stores/portal"
|
import { auth } from "stores/portal"
|
||||||
import { ActionMenu, Avatar, MenuItem, Icon, Modal } from "@budibase/bbui"
|
import { ActionMenu, MenuItem, Icon, Modal } from "@budibase/bbui"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import ProfileModal from "components/settings/ProfileModal.svelte"
|
import ProfileModal from "components/settings/ProfileModal.svelte"
|
||||||
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
|
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
|
||||||
import ThemeModal from "components/settings/ThemeModal.svelte"
|
import ThemeModal from "components/settings/ThemeModal.svelte"
|
||||||
import APIKeyModal from "components/settings/APIKeyModal.svelte"
|
import APIKeyModal from "components/settings/APIKeyModal.svelte"
|
||||||
|
import { UserAvatar } from "@budibase/frontend-core"
|
||||||
|
|
||||||
let themeModal
|
let themeModal
|
||||||
let profileModal
|
let profileModal
|
||||||
|
@ -23,7 +24,7 @@
|
||||||
|
|
||||||
<ActionMenu align="right">
|
<ActionMenu align="right">
|
||||||
<div slot="control" class="user-dropdown">
|
<div slot="control" class="user-dropdown">
|
||||||
<Avatar size="M" initials={$auth.initials} url={$auth.user.pictureUrl} />
|
<UserAvatar user={$auth.user} showTooltip={false} />
|
||||||
<Icon size="XL" name="ChevronDown" />
|
<Icon size="XL" name="ChevronDown" />
|
||||||
</div>
|
</div>
|
||||||
<MenuItem icon="UserEdit" on:click={() => profileModal.show()}>
|
<MenuItem icon="UserEdit" on:click={() => profileModal.show()}>
|
||||||
|
|
|
@ -1,47 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { Avatar, Tooltip } from "@budibase/bbui"
|
import { UserAvatar } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let row
|
export let row
|
||||||
|
|
||||||
let showTooltip
|
|
||||||
const getInitials = user => {
|
|
||||||
let initials = ""
|
|
||||||
initials += user.firstName ? user.firstName[0] : ""
|
|
||||||
initials += user.lastName ? user.lastName[0] : ""
|
|
||||||
|
|
||||||
return initials === "" ? user.email[0] : initials
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if row?.user?.email}
|
{#if row?.user?.email}
|
||||||
<div
|
<UserAvatar user={row.user} />
|
||||||
class="container"
|
|
||||||
on:mouseover={() => (showTooltip = true)}
|
|
||||||
on:focus={() => (showTooltip = true)}
|
|
||||||
on:mouseleave={() => (showTooltip = false)}
|
|
||||||
>
|
|
||||||
<Avatar size="M" initials={getInitials(row.user)} />
|
|
||||||
</div>
|
|
||||||
{#if showTooltip}
|
|
||||||
<div class="tooltip">
|
|
||||||
<Tooltip textWrapping text={row.user.email} direction="bottom" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.tooltip {
|
|
||||||
z-index: 1;
|
|
||||||
position: absolute;
|
|
||||||
top: 75%;
|
|
||||||
left: 120%;
|
|
||||||
transform: translateX(-100%) translateY(-50%);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 130px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -355,7 +355,6 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: var(--spacing-xl);
|
gap: var(--spacing-xl);
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-wrapper {
|
.empty-wrapper {
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
<script>
|
|
||||||
import PanelHeader from "./PanelHeader.svelte"
|
|
||||||
export let onBack = () => {}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<PanelHeader
|
|
||||||
title="Give it some data"
|
|
||||||
subtitle="Not ready to add yours? Get started with sample data!"
|
|
||||||
{onBack}
|
|
||||||
/>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
|
@ -1,120 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Button, FancyForm, FancyInput, FancyCheckbox } from "@budibase/bbui"
|
|
||||||
import GoogleButton from "components/backend/DatasourceNavigator/_components/GoogleButton.svelte"
|
|
||||||
import { capitalise } from "helpers/helpers"
|
|
||||||
import PanelHeader from "./PanelHeader.svelte"
|
|
||||||
import { helpers } from "@budibase/shared-core"
|
|
||||||
|
|
||||||
export let title = ""
|
|
||||||
export let onBack = null
|
|
||||||
export let onNext = () => {}
|
|
||||||
export let fields = {}
|
|
||||||
export let type = ""
|
|
||||||
|
|
||||||
let errors = {}
|
|
||||||
|
|
||||||
const formatName = name => {
|
|
||||||
if (name === "ca") {
|
|
||||||
return "CA"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "ssl") {
|
|
||||||
return "SSL"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "rejectUnauthorized") {
|
|
||||||
return "Reject Unauthorized"
|
|
||||||
}
|
|
||||||
|
|
||||||
return capitalise(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDefaultValues = fields => {
|
|
||||||
const newValues = {}
|
|
||||||
|
|
||||||
Object.entries(fields).forEach(([name, { default: defaultValue }]) => {
|
|
||||||
if (defaultValue) {
|
|
||||||
newValues[name] = defaultValue
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return newValues
|
|
||||||
}
|
|
||||||
|
|
||||||
const values = getDefaultValues(fields)
|
|
||||||
|
|
||||||
const validateRequired = value => {
|
|
||||||
if (value.length < 1) {
|
|
||||||
return "Required field"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getIsValid = (fields, errors, values) => {
|
|
||||||
for (const [name, { required }] of Object.entries(fields)) {
|
|
||||||
if (required && !values[name]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.values(errors).every(error => !error)
|
|
||||||
}
|
|
||||||
|
|
||||||
$: isValid = getIsValid(fields, errors, values)
|
|
||||||
$: isGoogle = helpers.isGoogleSheets(type)
|
|
||||||
|
|
||||||
const handleNext = async () => {
|
|
||||||
const parsedValues = {}
|
|
||||||
|
|
||||||
Object.entries(values).forEach(([name, value]) => {
|
|
||||||
if (fields[name].type === "number") {
|
|
||||||
parsedValues[name] = parseInt(value, 10)
|
|
||||||
} else {
|
|
||||||
parsedValues[name] = value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (isGoogle) {
|
|
||||||
parsedValues.isGoogle = isGoogle
|
|
||||||
}
|
|
||||||
return await onNext(parsedValues)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<PanelHeader
|
|
||||||
{title}
|
|
||||||
subtitle="Fill in the required fields to fetch your tables"
|
|
||||||
{onBack}
|
|
||||||
/>
|
|
||||||
<div class="form">
|
|
||||||
<FancyForm>
|
|
||||||
{#each Object.entries(fields) as [name, { type, default: defaultValue, required }]}
|
|
||||||
{#if type !== "boolean"}
|
|
||||||
<FancyInput
|
|
||||||
bind:value={values[name]}
|
|
||||||
bind:error={errors[name]}
|
|
||||||
validate={required ? validateRequired : () => {}}
|
|
||||||
label={formatName(name)}
|
|
||||||
{type}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
{#each Object.entries(fields) as [name, { type, default: defaultValue, required }]}
|
|
||||||
{#if type === "boolean"}
|
|
||||||
<FancyCheckbox bind:value={values[name]} text={formatName(name)} />
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</FancyForm>
|
|
||||||
</div>
|
|
||||||
{#if isGoogle}
|
|
||||||
<GoogleButton disabled={!isValid} preAuthStep={handleNext} samePage />
|
|
||||||
{:else}
|
|
||||||
<Button cta disabled={!isValid} on:click={handleNext}>Connect</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.form {
|
|
||||||
margin-bottom: 36px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
export let name = ""
|
export let name = ""
|
||||||
export let showData = false
|
|
||||||
|
|
||||||
const rows = [
|
const rows = [
|
||||||
{
|
{
|
||||||
|
@ -49,7 +48,7 @@
|
||||||
<h1>{name}</h1>
|
<h1>{name}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav">Home</div>
|
<div class="nav">Home</div>
|
||||||
<table class={`table ${showData ? "tableVisible" : ""}`}>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>FIRST NAME</th>
|
<th>FIRST NAME</th>
|
||||||
|
@ -71,7 +70,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class={`sidePanel ${showData ? "sidePanelVisible" : ""}`}>
|
<div class="sidePanel">
|
||||||
<h2>{rows[0].firstName}</h2>
|
<h2>{rows[0].firstName}</h2>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="exampleLastName">lastName</label>
|
<label for="exampleLastName">lastName</label>
|
||||||
|
@ -199,14 +198,6 @@
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tableVisible {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidePanel {
|
.sidePanel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
@ -216,9 +207,6 @@
|
||||||
top: 0;
|
top: 0;
|
||||||
right: -364px;
|
right: -364px;
|
||||||
padding: 42px 32px;
|
padding: 42px 32px;
|
||||||
}
|
|
||||||
|
|
||||||
.sidePanelVisible {
|
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import PanelHeader from "./PanelHeader.svelte"
|
import PanelHeader from "./PanelHeader.svelte"
|
||||||
import { APP_URL_REGEX } from "constants"
|
import { APP_URL_REGEX } from "constants"
|
||||||
|
|
||||||
|
export let disabled
|
||||||
export let name = ""
|
export let name = ""
|
||||||
export let url = ""
|
export let url = ""
|
||||||
export let onNext = () => {}
|
export let onNext = () => {}
|
||||||
|
@ -71,7 +72,9 @@
|
||||||
{:else}
|
{:else}
|
||||||
<p></p>
|
<p></p>
|
||||||
{/if}
|
{/if}
|
||||||
<Button size="L" cta disabled={!isValid} on:click={onNext}>Lets go!</Button>
|
<Button size="L" cta disabled={!isValid || disabled} on:click={onNext}
|
||||||
|
>Lets go!</Button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,102 +1,50 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import NamePanel from "./_components/NamePanel.svelte"
|
import NamePanel from "./_components/NamePanel.svelte"
|
||||||
import DataPanel from "./_components/DataPanel.svelte"
|
|
||||||
import DatasourceConfigPanel from "./_components/DatasourceConfigPanel.svelte"
|
|
||||||
import ExampleApp from "./_components/ExampleApp.svelte"
|
import ExampleApp from "./_components/ExampleApp.svelte"
|
||||||
import { FancyButton, notifications, Modal, Body } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
|
||||||
import { SplitPage } from "@budibase/frontend-core"
|
import { SplitPage } from "@budibase/frontend-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { store, automationStore } from "builderStore"
|
import { store, automationStore } from "builderStore"
|
||||||
import { saveDatasource } from "builderStore/datasource"
|
import { auth, admin } from "stores/portal"
|
||||||
import { integrations } from "stores/backend"
|
|
||||||
import { auth, admin, organisation } from "stores/portal"
|
|
||||||
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
|
||||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
|
||||||
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
|
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
|
||||||
import { Roles } from "constants/backend"
|
import { Roles } from "constants/backend"
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
|
||||||
import { helpers } from "@budibase/shared-core"
|
|
||||||
import { validateDatasourceConfig } from "builderStore/datasource"
|
|
||||||
import { DatasourceFeature } from "@budibase/types"
|
|
||||||
|
|
||||||
let name = "My first app"
|
let name = "My first app"
|
||||||
let url = "my-first-app"
|
let url = "my-first-app"
|
||||||
let stage = "name"
|
|
||||||
let appId = null
|
let appId = null
|
||||||
|
|
||||||
let plusIntegrations = {}
|
let loading = false
|
||||||
let integrationsLoading = true
|
|
||||||
let creationLoading = false
|
|
||||||
let uploadModal
|
|
||||||
let googleComplete = false
|
|
||||||
|
|
||||||
$: getIntegrations()
|
const createApp = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
const createApp = async useSampleData => {
|
|
||||||
creationLoading = true
|
|
||||||
// Create form data to create app
|
// Create form data to create app
|
||||||
// This is form based and not JSON
|
// This is form based and not JSON
|
||||||
try {
|
let data = new FormData()
|
||||||
let data = new FormData()
|
data.append("name", name.trim())
|
||||||
data.append("name", name.trim())
|
data.append("url", url.trim())
|
||||||
data.append("url", url.trim())
|
data.append("useTemplate", false)
|
||||||
data.append("useTemplate", false)
|
|
||||||
|
|
||||||
if (useSampleData) {
|
const createdApp = await API.createApp(data)
|
||||||
data.append("sampleData", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdApp = await API.createApp(data)
|
// Select Correct Application/DB in prep for creating user
|
||||||
|
const pkg = await API.fetchAppPackage(createdApp.instance._id)
|
||||||
|
await store.actions.initialise(pkg)
|
||||||
|
await automationStore.actions.fetch()
|
||||||
|
// Update checklist - in case first app
|
||||||
|
await admin.init()
|
||||||
|
|
||||||
// Select Correct Application/DB in prep for creating user
|
// Create user
|
||||||
const pkg = await API.fetchAppPackage(createdApp.instance._id)
|
await auth.setInitInfo({})
|
||||||
await store.actions.initialise(pkg)
|
|
||||||
await automationStore.actions.fetch()
|
|
||||||
// Update checklist - in case first app
|
|
||||||
await admin.init()
|
|
||||||
|
|
||||||
// Create user
|
let defaultScreenTemplate = createFromScratchScreen.create()
|
||||||
await auth.setInitInfo({})
|
defaultScreenTemplate.routing.route = "/home"
|
||||||
|
defaultScreenTemplate.routing.roldId = Roles.BASIC
|
||||||
|
await store.actions.screens.save(defaultScreenTemplate)
|
||||||
|
|
||||||
let defaultScreenTemplate = createFromScratchScreen.create()
|
appId = createdApp.instance._id
|
||||||
defaultScreenTemplate.routing.route = "/home"
|
return createdApp
|
||||||
defaultScreenTemplate.routing.roldId = Roles.BASIC
|
|
||||||
await store.actions.screens.save(defaultScreenTemplate)
|
|
||||||
|
|
||||||
appId = createdApp.instance._id
|
|
||||||
return createdApp
|
|
||||||
} catch (e) {
|
|
||||||
creationLoading = false
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getIntegrations = async () => {
|
|
||||||
try {
|
|
||||||
await integrations.init()
|
|
||||||
const newPlusIntegrations = {}
|
|
||||||
|
|
||||||
Object.entries($integrations).forEach(([integrationType, schema]) => {
|
|
||||||
// google sheets not available in self-host
|
|
||||||
if (
|
|
||||||
helpers.isGoogleSheets(integrationType) &&
|
|
||||||
!$organisation.googleDatasourceConfigured
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (schema?.plus) {
|
|
||||||
newPlusIntegrations[integrationType] = schema
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
plusIntegrations = newPlusIntegrations
|
|
||||||
} catch (e) {
|
|
||||||
notifications.error("There was a problem communicating with the server.")
|
|
||||||
} finally {
|
|
||||||
integrationsLoading = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToApp = () => {
|
const goToApp = () => {
|
||||||
|
@ -104,152 +52,23 @@
|
||||||
notifications.success(`App created successfully`)
|
notifications.success(`App created successfully`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCreateApp = async ({
|
const handleCreateApp = async () => {
|
||||||
datasourceConfig,
|
|
||||||
useSampleData,
|
|
||||||
isGoogle,
|
|
||||||
}) => {
|
|
||||||
let app
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
await createApp()
|
||||||
datasourceConfig &&
|
|
||||||
plusIntegrations[stage].features[DatasourceFeature.CONNECTION_CHECKING]
|
|
||||||
) {
|
|
||||||
const resp = await validateDatasourceConfig({
|
|
||||||
config: datasourceConfig,
|
|
||||||
type: stage,
|
|
||||||
})
|
|
||||||
if (!resp.connected) {
|
|
||||||
notifications.error(
|
|
||||||
`Unable to connect - ${resp.error ?? "Error validating datasource"}`
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app = await createApp(useSampleData)
|
goToApp()
|
||||||
|
|
||||||
let datasource
|
|
||||||
if (datasourceConfig) {
|
|
||||||
datasource = await saveDatasource({
|
|
||||||
plus: true,
|
|
||||||
auth: undefined,
|
|
||||||
name: plusIntegrations[stage].friendlyName,
|
|
||||||
schema: plusIntegrations[stage].datasource,
|
|
||||||
config: datasourceConfig,
|
|
||||||
type: stage,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
store.set()
|
|
||||||
|
|
||||||
if (isGoogle) {
|
|
||||||
googleComplete = true
|
|
||||||
return { datasource, appId: app.appId }
|
|
||||||
} else {
|
|
||||||
goToApp()
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
loading = false
|
||||||
creationLoading = false
|
|
||||||
notifications.error("There was a problem creating your app")
|
notifications.error("There was a problem creating your app")
|
||||||
|
|
||||||
// Reset the store so that we don't send up stale headers
|
|
||||||
store.actions.reset()
|
|
||||||
|
|
||||||
// If we successfully created an app, delete it again so that we
|
|
||||||
// can try again once the error has been corrected.
|
|
||||||
// This also ensures onboarding can't be skipped by entering invalid
|
|
||||||
// data credentials.
|
|
||||||
if (app?.appId) {
|
|
||||||
await API.deleteApp(app.appId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={uploadModal}>
|
|
||||||
<CreateTableModal
|
|
||||||
name="Your Data"
|
|
||||||
beforeSave={createApp}
|
|
||||||
afterSave={goToApp}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<div class="full-width">
|
<div class="full-width">
|
||||||
<SplitPage>
|
<SplitPage>
|
||||||
{#if stage === "name"}
|
<NamePanel bind:name bind:url disabled={loading} onNext={handleCreateApp} />
|
||||||
<NamePanel bind:name bind:url onNext={() => (stage = "data")} />
|
|
||||||
{:else if googleComplete}
|
|
||||||
<div class="centered">
|
|
||||||
<Body
|
|
||||||
>Please login to your Google account in the new tab which as opened to
|
|
||||||
continue.</Body
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{:else if integrationsLoading || creationLoading}
|
|
||||||
<div class="centered">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
{:else if stage === "data"}
|
|
||||||
<DataPanel onBack={() => (stage = "name")}>
|
|
||||||
<div class="dataButton">
|
|
||||||
<FancyButton
|
|
||||||
on:click={() => handleCreateApp({ useSampleData: true })}
|
|
||||||
>
|
|
||||||
<div class="dataButtonContent">
|
|
||||||
<div class="dataButtonIcon">
|
|
||||||
<img
|
|
||||||
alt="Budibase Logo"
|
|
||||||
class="budibaseLogo"
|
|
||||||
src={"https://i.imgur.com/Xhdt1YP.png"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
Budibase Sample data
|
|
||||||
</div>
|
|
||||||
</FancyButton>
|
|
||||||
</div>
|
|
||||||
<div class="dataButton">
|
|
||||||
<FancyButton on:click={uploadModal.show}>
|
|
||||||
<div class="dataButtonContent">
|
|
||||||
<div class="dataButtonIcon">
|
|
||||||
<FontAwesomeIcon name="fa-solid fa-file-arrow-up" />
|
|
||||||
</div>
|
|
||||||
Upload data (CSV or JSON)
|
|
||||||
</div>
|
|
||||||
</FancyButton>
|
|
||||||
</div>
|
|
||||||
{#each Object.entries(plusIntegrations) as [integrationType, schema]}
|
|
||||||
<div class="dataButton">
|
|
||||||
<FancyButton on:click={() => (stage = integrationType)}>
|
|
||||||
<div class="dataButtonContent">
|
|
||||||
<div class="dataButtonIcon">
|
|
||||||
<IntegrationIcon {integrationType} {schema} />
|
|
||||||
</div>
|
|
||||||
{schema.friendlyName}
|
|
||||||
</div>
|
|
||||||
</FancyButton>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</DataPanel>
|
|
||||||
{:else if stage in plusIntegrations}
|
|
||||||
<DatasourceConfigPanel
|
|
||||||
title={plusIntegrations[stage].friendlyName}
|
|
||||||
fields={plusIntegrations[stage].datasource}
|
|
||||||
type={stage}
|
|
||||||
onBack={() => (stage = "data")}
|
|
||||||
onNext={data => {
|
|
||||||
const isGoogle = data.isGoogle
|
|
||||||
delete data.isGoogle
|
|
||||||
return handleCreateApp({ datasourceConfig: data, isGoogle })
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<p>There was an problem. Please refresh the page and try again.</p>
|
|
||||||
{/if}
|
|
||||||
<div slot="right">
|
<div slot="right">
|
||||||
<ExampleApp {name} showData={stage !== "name"} />
|
<ExampleApp {name} />
|
||||||
</div>
|
</div>
|
||||||
</SplitPage>
|
</SplitPage>
|
||||||
</div>
|
</div>
|
||||||
|
@ -258,35 +77,4 @@
|
||||||
.full-width {
|
.full-width {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.centered {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dataButton {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dataButtonContent {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.budibaseLogo {
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dataButtonIcon {
|
|
||||||
width: 22px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dataButtonContent :global(svg) {
|
|
||||||
font-size: 18px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -20,11 +20,10 @@
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
Header,
|
Header,
|
||||||
} from "components/portal/page"
|
} from "components/portal/page"
|
||||||
import { apps, auth, overview } from "stores/portal"
|
import { apps, overview } from "stores/portal"
|
||||||
import { AppStatus } from "constants"
|
import { AppStatus } from "constants"
|
||||||
import analytics, { Events, EventSource } from "analytics"
|
import analytics, { Events, EventSource } from "analytics"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import AppLockModal from "components/common/AppLockModal.svelte"
|
|
||||||
import EditableIcon from "components/common/EditableIcon.svelte"
|
import EditableIcon from "components/common/EditableIcon.svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
@ -53,8 +52,6 @@
|
||||||
$: appId = $overview.selectedAppId
|
$: appId = $overview.selectedAppId
|
||||||
$: initialiseApp(appId)
|
$: initialiseApp(appId)
|
||||||
$: isPublished = app?.status === AppStatus.DEPLOYED
|
$: isPublished = app?.status === AppStatus.DEPLOYED
|
||||||
$: appLocked = !!app?.lockedBy
|
|
||||||
$: lockedByYou = $auth.user.email === app?.lockedBy?.email
|
|
||||||
|
|
||||||
const initialiseApp = async appId => {
|
const initialiseApp = async appId => {
|
||||||
loaded = false
|
loaded = false
|
||||||
|
@ -80,13 +77,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const editApp = () => {
|
const editApp = () => {
|
||||||
if (appLocked && !lockedByYou) {
|
|
||||||
const identifier = app?.lockedBy?.firstName || app?.lockedBy?.email
|
|
||||||
notifications.warning(
|
|
||||||
`App locked by ${identifier}. Please allow lock to expire or have them unlock this app.`
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
$goto(`../../../app/${app.devId}`)
|
$goto(`../../../app/${app.devId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +125,6 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div slot="buttons">
|
<div slot="buttons">
|
||||||
<AppLockModal {app} />
|
|
||||||
<span class="desktop">
|
<span class="desktop">
|
||||||
<Button
|
<Button
|
||||||
size="M"
|
size="M"
|
||||||
|
@ -148,14 +137,7 @@
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
<span class="desktop">
|
<span class="desktop">
|
||||||
<Button
|
<Button size="M" cta on:click={editApp}>Edit</Button>
|
||||||
size="M"
|
|
||||||
cta
|
|
||||||
disabled={appLocked && !lockedByYou}
|
|
||||||
on:click={editApp}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
</span>
|
</span>
|
||||||
<ActionMenu align="right">
|
<ActionMenu align="right">
|
||||||
<span slot="control" class="app-overview-actions-icon">
|
<span slot="control" class="app-overview-actions-icon">
|
||||||
|
@ -167,13 +149,7 @@
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</span>
|
</span>
|
||||||
<span class="mobile">
|
<span class="mobile">
|
||||||
<MenuItem
|
<MenuItem icon="Edit" on:click={editApp}>Edit</MenuItem>
|
||||||
icon="Edit"
|
|
||||||
disabled={appLocked && !lockedByYou}
|
|
||||||
on:click={editApp}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</MenuItem>
|
|
||||||
</span>
|
</span>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
on:click={() => exportApp({ published: false })}
|
on:click={() => exportApp({ published: false })}
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import getUserInitials from "helpers/userInitials.js"
|
import { UserAvatar } from "@budibase/frontend-core"
|
||||||
import { Avatar } from "@budibase/bbui"
|
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
$: initials = getUserInitials(value)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div title={value.email} class="cell">
|
<div class="cell">
|
||||||
<Avatar size="M" {initials} />
|
<UserAvatar user={value} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
Icon,
|
Icon,
|
||||||
Heading,
|
Heading,
|
||||||
Link,
|
Link,
|
||||||
Avatar,
|
|
||||||
Layout,
|
Layout,
|
||||||
Body,
|
Body,
|
||||||
notifications,
|
notifications,
|
||||||
|
@ -15,7 +14,7 @@
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
import { users, auth, apps, groups, overview } from "stores/portal"
|
import { users, auth, apps, groups, overview } from "stores/portal"
|
||||||
import { fetchData } from "@budibase/frontend-core"
|
import { fetchData, UserAvatar } from "@budibase/frontend-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import GroupIcon from "../../users/groups/_components/GroupIcon.svelte"
|
import GroupIcon from "../../users/groups/_components/GroupIcon.svelte"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
@ -52,18 +51,30 @@
|
||||||
return groups.actions.getGroupAppIds(group).includes(prodAppId)
|
return groups.actions.getGroupAppIds(group).includes(prodAppId)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const updateDeploymentString = () => {
|
||||||
|
return deployments?.length
|
||||||
|
? processStringSync(
|
||||||
|
"Last published {{ duration time 'millisecond' }} ago",
|
||||||
|
{
|
||||||
|
time:
|
||||||
|
new Date().getTime() -
|
||||||
|
new Date(deployments[0].updatedAt).getTime(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
// App is updating in the layout asynchronously
|
||||||
|
$: if ($store.appId?.length) {
|
||||||
|
fetchDeployments().then(resp => {
|
||||||
|
deployments = resp
|
||||||
|
})
|
||||||
|
}
|
||||||
|
$: deploymentString = updateDeploymentString(deployments)
|
||||||
|
|
||||||
async function fetchAppEditor(editorId) {
|
async function fetchAppEditor(editorId) {
|
||||||
appEditor = await users.get(editorId)
|
appEditor = await users.get(editorId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInitials = user => {
|
|
||||||
let initials = ""
|
|
||||||
initials += user.firstName ? user.firstName[0] : ""
|
|
||||||
initials += user.lastName ? user.lastName[0] : ""
|
|
||||||
|
|
||||||
return initials === "" ? user.email[0] : initials
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmUnpublishApp = async () => {
|
const confirmUnpublishApp = async () => {
|
||||||
try {
|
try {
|
||||||
await API.unpublishApp(app.prodId)
|
await API.unpublishApp(app.prodId)
|
||||||
|
@ -116,19 +127,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="status-text">
|
<div class="status-text">
|
||||||
{#if deployments?.length}
|
{#if isPublished}
|
||||||
{processStringSync(
|
{deploymentString}
|
||||||
"Last published {{ duration time 'millisecond' }} ago",
|
- <Link on:click={unpublishModal.show}>Unpublish</Link>
|
||||||
{
|
|
||||||
time:
|
|
||||||
new Date().getTime() -
|
|
||||||
new Date(deployments[0].updatedAt).getTime(),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
{#if isPublished}
|
|
||||||
- <Link on:click={unpublishModal.show}>Unpublish</Link>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !deployments?.length}
|
{#if !deployments?.length}
|
||||||
-
|
-
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -140,7 +143,7 @@
|
||||||
<div class="last-edited-content">
|
<div class="last-edited-content">
|
||||||
<div class="updated-by">
|
<div class="updated-by">
|
||||||
{#if appEditor}
|
{#if appEditor}
|
||||||
<Avatar size="M" initials={getInitials(appEditor)} />
|
<UserAvatar user={appEditor} showTooltip={false} />
|
||||||
<div class="editor-name">
|
<div class="editor-name">
|
||||||
{appEditor._id === $auth.user._id ? "You" : appEditorText}
|
{appEditor._id === $auth.user._id ? "You" : appEditorText}
|
||||||
</div>
|
</div>
|
||||||
|
@ -201,7 +204,7 @@
|
||||||
<div class="users">
|
<div class="users">
|
||||||
<div class="list">
|
<div class="list">
|
||||||
{#each appUsers.slice(0, 4) as user}
|
{#each appUsers.slice(0, 4) as user}
|
||||||
<Avatar size="M" initials={getInitials(user)} />
|
<UserAvatar {user} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await Promise.all(fetchConfig(), fetchAPIKey())
|
await Promise.all([fetchConfig(), fetchAPIKey()])
|
||||||
})
|
})
|
||||||
|
|
||||||
const copyToClipboard = async value => {
|
const copyToClipboard = async value => {
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import { goto, url } from "@roxi/routify"
|
import { goto, url } from "@roxi/routify"
|
||||||
import {
|
import {
|
||||||
ActionMenu,
|
ActionMenu,
|
||||||
Avatar,
|
|
||||||
Button,
|
Button,
|
||||||
Layout,
|
Layout,
|
||||||
Heading,
|
Heading,
|
||||||
|
@ -25,13 +24,14 @@
|
||||||
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
|
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
|
||||||
import DeleteUserModal from "./_components/DeleteUserModal.svelte"
|
import DeleteUserModal from "./_components/DeleteUserModal.svelte"
|
||||||
import GroupIcon from "../groups/_components/GroupIcon.svelte"
|
import GroupIcon from "../groups/_components/GroupIcon.svelte"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants, UserAvatar } from "@budibase/frontend-core"
|
||||||
import { Breadcrumbs, Breadcrumb } from "components/portal/page"
|
import { Breadcrumbs, Breadcrumb } from "components/portal/page"
|
||||||
import RemoveGroupTableRenderer from "./_components/RemoveGroupTableRenderer.svelte"
|
import RemoveGroupTableRenderer from "./_components/RemoveGroupTableRenderer.svelte"
|
||||||
import GroupNameTableRenderer from "../groups/_components/GroupNameTableRenderer.svelte"
|
import GroupNameTableRenderer from "../groups/_components/GroupNameTableRenderer.svelte"
|
||||||
import AppNameTableRenderer from "./_components/AppNameTableRenderer.svelte"
|
import AppNameTableRenderer from "./_components/AppNameTableRenderer.svelte"
|
||||||
import AppRoleTableRenderer from "./_components/AppRoleTableRenderer.svelte"
|
import AppRoleTableRenderer from "./_components/AppRoleTableRenderer.svelte"
|
||||||
import ScimBanner from "../_components/SCIMBanner.svelte"
|
import ScimBanner from "../_components/SCIMBanner.svelte"
|
||||||
|
import { helpers } from "@budibase/shared-core"
|
||||||
|
|
||||||
export let userId
|
export let userId
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@
|
||||||
$: readonly = !$auth.isAdmin || scimEnabled
|
$: readonly = !$auth.isAdmin || scimEnabled
|
||||||
$: privileged = user?.admin?.global || user?.builder?.global
|
$: privileged = user?.admin?.global || user?.builder?.global
|
||||||
$: nameLabel = getNameLabel(user)
|
$: nameLabel = getNameLabel(user)
|
||||||
$: initials = getInitials(nameLabel)
|
$: initials = helpers.getUserInitials(user)
|
||||||
$: filteredGroups = getFilteredGroups($groups, searchTerm)
|
$: filteredGroups = getFilteredGroups($groups, searchTerm)
|
||||||
$: availableApps = getAvailableApps($apps, privileged, user?.roles)
|
$: availableApps = getAvailableApps($apps, privileged, user?.roles)
|
||||||
$: userGroups = $groups.filter(x => {
|
$: userGroups = $groups.filter(x => {
|
||||||
|
@ -150,17 +150,6 @@
|
||||||
return label
|
return label
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInitials = nameLabel => {
|
|
||||||
if (!nameLabel) {
|
|
||||||
return "?"
|
|
||||||
}
|
|
||||||
return nameLabel
|
|
||||||
.split(" ")
|
|
||||||
.slice(0, 2)
|
|
||||||
.map(x => x[0])
|
|
||||||
.join("")
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateUserFirstName(evt) {
|
async function updateUserFirstName(evt) {
|
||||||
try {
|
try {
|
||||||
await users.save({ ...user, firstName: evt.target.value })
|
await users.save({ ...user, firstName: evt.target.value })
|
||||||
|
@ -238,7 +227,7 @@
|
||||||
|
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<Avatar size="XXL" {initials} />
|
<UserAvatar size="XXL" {user} showTooltip={false} />
|
||||||
<div class="subtitle">
|
<div class="subtitle">
|
||||||
<Heading size="M">{nameLabel}</Heading>
|
<Heading size="M">{nameLabel}</Heading>
|
||||||
{#if nameLabel !== user?.email}
|
{#if nameLabel !== user?.email}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { writable, derived } from "svelte/store"
|
import { writable, derived, get } from "svelte/store"
|
||||||
import { queries, tables } from "./"
|
import { queries, tables } from "./"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
|
||||||
|
@ -91,6 +91,39 @@ export function createDatasourcesStore() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handles external updates of datasources
|
||||||
|
const replaceDatasource = (datasourceId, datasource) => {
|
||||||
|
if (!datasourceId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle deletion
|
||||||
|
if (!datasource) {
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
list: state.list.filter(x => x._id !== datasourceId),
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new datasource
|
||||||
|
const index = get(store).list.findIndex(x => x._id === datasource._id)
|
||||||
|
if (index === -1) {
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
list: [...state.list, datasource],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing datasource
|
||||||
|
else if (datasource) {
|
||||||
|
store.update(state => {
|
||||||
|
state.list[index] = datasource
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe: derivedStore.subscribe,
|
subscribe: derivedStore.subscribe,
|
||||||
fetch,
|
fetch,
|
||||||
|
@ -100,6 +133,7 @@ export function createDatasourcesStore() {
|
||||||
save,
|
save,
|
||||||
delete: deleteDatasource,
|
delete: deleteDatasource,
|
||||||
removeSchemaError,
|
removeSchemaError,
|
||||||
|
replaceDatasource,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,18 +22,6 @@ export function createTablesStore() {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchTable = async tableId => {
|
|
||||||
const table = await API.fetchTableDefinition(tableId)
|
|
||||||
|
|
||||||
store.update(state => {
|
|
||||||
const indexToUpdate = state.list.findIndex(t => t._id === table._id)
|
|
||||||
state.list[indexToUpdate] = table
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const select = tableId => {
|
const select = tableId => {
|
||||||
store.update(state => ({
|
store.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
|
@ -74,20 +62,21 @@ export function createTablesStore() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedTable = await API.saveTable(updatedTable)
|
const savedTable = await API.saveTable(updatedTable)
|
||||||
await fetch()
|
replaceTable(savedTable._id, savedTable)
|
||||||
if (table.type === "external") {
|
await datasources.fetch()
|
||||||
await datasources.fetch()
|
select(savedTable._id)
|
||||||
}
|
|
||||||
await select(savedTable._id)
|
|
||||||
return savedTable
|
return savedTable
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteTable = async table => {
|
const deleteTable = async table => {
|
||||||
|
if (!table?._id || !table?._rev) {
|
||||||
|
return
|
||||||
|
}
|
||||||
await API.deleteTable({
|
await API.deleteTable({
|
||||||
tableId: table?._id,
|
tableId: table._id,
|
||||||
tableRev: table?._rev,
|
tableRev: table._rev,
|
||||||
})
|
})
|
||||||
await fetch()
|
replaceTable(table._id, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveField = async ({
|
const saveField = async ({
|
||||||
|
@ -135,35 +124,56 @@ export function createTablesStore() {
|
||||||
await save(draft)
|
await save(draft)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateTable = table => {
|
// Handles external updates of tables
|
||||||
const index = get(store).list.findIndex(x => x._id === table._id)
|
const replaceTable = (tableId, table) => {
|
||||||
if (index === -1) {
|
if (!tableId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function has to merge state as there discrepancies with the table
|
// Handle deletion
|
||||||
// API endpoints. The table list endpoint and get table endpoint use the
|
if (!table) {
|
||||||
// "type" property to mean different things.
|
store.update(state => ({
|
||||||
store.update(state => {
|
...state,
|
||||||
state.list[index] = {
|
list: state.list.filter(x => x._id !== tableId),
|
||||||
...table,
|
}))
|
||||||
type: state.list[index].type,
|
return
|
||||||
}
|
}
|
||||||
return state
|
|
||||||
})
|
// Add new table
|
||||||
|
const index = get(store).list.findIndex(x => x._id === table._id)
|
||||||
|
if (index === -1) {
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
list: [...state.list, table],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing table
|
||||||
|
else if (table) {
|
||||||
|
// This function has to merge state as there discrepancies with the table
|
||||||
|
// API endpoints. The table list endpoint and get table endpoint use the
|
||||||
|
// "type" property to mean different things.
|
||||||
|
store.update(state => {
|
||||||
|
state.list[index] = {
|
||||||
|
...table,
|
||||||
|
type: state.list[index].type,
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
...store,
|
||||||
subscribe: derivedStore.subscribe,
|
subscribe: derivedStore.subscribe,
|
||||||
fetch,
|
fetch,
|
||||||
fetchTable,
|
|
||||||
init: fetch,
|
init: fetch,
|
||||||
select,
|
select,
|
||||||
save,
|
save,
|
||||||
delete: deleteTable,
|
delete: deleteTable,
|
||||||
saveField,
|
saveField,
|
||||||
deleteField,
|
deleteField,
|
||||||
updateTable,
|
replaceTable,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { writable, get, derived } from "svelte/store"
|
import { writable, derived } from "svelte/store"
|
||||||
import { tables } from "./"
|
import { tables } from "./"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
|
||||||
|
@ -27,21 +27,31 @@ export function createViewsStore() {
|
||||||
|
|
||||||
const deleteView = async view => {
|
const deleteView = async view => {
|
||||||
await API.deleteView(view)
|
await API.deleteView(view)
|
||||||
await tables.fetch()
|
|
||||||
|
// Update tables
|
||||||
|
tables.update(state => {
|
||||||
|
const table = state.list.find(table => table._id === view.tableId)
|
||||||
|
if (table) {
|
||||||
|
delete table.views[view.name]
|
||||||
|
}
|
||||||
|
return { ...state }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const save = async view => {
|
const save = async view => {
|
||||||
const savedView = await API.saveView(view)
|
const savedView = await API.saveView(view)
|
||||||
const viewMeta = {
|
|
||||||
name: view.name,
|
|
||||||
...savedView,
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewTable = get(tables).list.find(table => table._id === view.tableId)
|
// Update tables
|
||||||
|
tables.update(state => {
|
||||||
if (view.originalName) delete viewTable.views[view.originalName]
|
const table = state.list.find(table => table._id === view.tableId)
|
||||||
viewTable.views[view.name] = viewMeta
|
if (table) {
|
||||||
await tables.save(viewTable)
|
if (view.originalName) {
|
||||||
|
delete table.views[view.originalName]
|
||||||
|
}
|
||||||
|
table.views[view.name] = savedView
|
||||||
|
}
|
||||||
|
return { ...state }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -116,6 +116,9 @@ export const createLicensingStore = () => {
|
||||||
const auditLogsEnabled = license.features.includes(
|
const auditLogsEnabled = license.features.includes(
|
||||||
Constants.Features.AUDIT_LOGS
|
Constants.Features.AUDIT_LOGS
|
||||||
)
|
)
|
||||||
|
const syncAutomationsEnabled = license.features.includes(
|
||||||
|
Constants.Features.SYNC_AUTOMATIONS
|
||||||
|
)
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -130,6 +133,7 @@ export const createLicensingStore = () => {
|
||||||
environmentVariablesEnabled,
|
environmentVariablesEnabled,
|
||||||
auditLogsEnabled,
|
auditLogsEnabled,
|
||||||
enforceableSSO,
|
enforceableSSO,
|
||||||
|
syncAutomationsEnabled,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "0.0.1",
|
"version": "0.0.0",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "dist/src/index.js",
|
"main": "dist/src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -29,9 +29,9 @@
|
||||||
"outputPath": "build"
|
"outputPath": "build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/backend-core": "0.0.1",
|
"@budibase/backend-core": "0.0.0",
|
||||||
"@budibase/string-templates": "0.0.1",
|
"@budibase/string-templates": "0.0.0",
|
||||||
"@budibase/types": "0.0.1",
|
"@budibase/types": "0.0.0",
|
||||||
"axios": "0.21.2",
|
"axios": "0.21.2",
|
||||||
"chalk": "4.1.0",
|
"chalk": "4.1.0",
|
||||||
"cli-progress": "3.11.2",
|
"cli-progress": "3.11.2",
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue