Merge branch 'develop' of github.com:Budibase/budibase into fix/no-iterations-loop
This commit is contained in:
commit
09a48a1d21
|
@ -56,7 +56,6 @@ jobs:
|
||||||
run: yarn install:pro $BRANCH $BASE_BRANCH
|
run: yarn install:pro $BRANCH $BASE_BRANCH
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn bootstrap
|
- run: yarn bootstrap
|
||||||
- run: yarn build:client
|
|
||||||
- run: yarn test
|
- run: yarn test
|
||||||
- uses: codecov/codecov-action@v3
|
- uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
|
@ -78,28 +77,28 @@ jobs:
|
||||||
- run: yarn bootstrap
|
- run: yarn bootstrap
|
||||||
- run: yarn test:pro
|
- run: yarn test:pro
|
||||||
|
|
||||||
integration-test:
|
# integration-test:
|
||||||
runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
services:
|
# services:
|
||||||
couchdb:
|
# couchdb:
|
||||||
image: ibmcom/couchdb3
|
# image: ibmcom/couchdb3
|
||||||
env:
|
# env:
|
||||||
COUCHDB_PASSWORD: budibase
|
# COUCHDB_PASSWORD: budibase
|
||||||
COUCHDB_USER: budibase
|
# COUCHDB_USER: budibase
|
||||||
ports:
|
# ports:
|
||||||
- 4567:5984
|
# - 4567:5984
|
||||||
steps:
|
# steps:
|
||||||
- uses: actions/checkout@v2
|
# - uses: actions/checkout@v2
|
||||||
- name: Use Node.js 14.x
|
# - name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
# uses: actions/setup-node@v1
|
||||||
with:
|
# with:
|
||||||
node-version: 14.x
|
# node-version: 14.x
|
||||||
- name: Install Pro
|
# - name: Install Pro
|
||||||
run: yarn install:pro $BRANCH $BASE_BRANCH
|
# run: yarn install:pro $BRANCH $BASE_BRANCH
|
||||||
- run: yarn
|
# - run: yarn
|
||||||
- run: yarn bootstrap
|
# - run: yarn bootstrap
|
||||||
- run: yarn build
|
# - run: yarn build
|
||||||
- run: |
|
# - run: |
|
||||||
cd qa-core
|
# cd qa-core
|
||||||
yarn
|
# yarn
|
||||||
yarn api:test:ci
|
# yarn api:test:ci
|
||||||
|
|
|
@ -17,6 +17,7 @@ jobs:
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
if [ -z "${{ github.event.inputs.version }}" ]; then
|
if [ -z "${{ github.event.inputs.version }}" ]; then
|
||||||
|
git pull
|
||||||
release_version=$(cat lerna.json | jq -r '.version')
|
release_version=$(cat lerna.json | jq -r '.version')
|
||||||
else
|
else
|
||||||
release_version=${{ github.event.inputs.version }}
|
release_version=${{ github.event.inputs.version }}
|
||||||
|
|
|
@ -134,6 +134,7 @@ jobs:
|
||||||
- name: Get the latest budibase release version
|
- name: Get the latest budibase release version
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
|
git pull
|
||||||
release_version=$(cat lerna.json | jq -r '.version')
|
release_version=$(cat lerna.json | jq -r '.version')
|
||||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
|
|
@ -212,11 +212,24 @@ spec:
|
||||||
image: budibase/apps:{{ .Values.globals.appVersion }}
|
image: budibase/apps:{{ .Values.globals.appVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: {{ .Values.services.apps.port }}
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 5
|
||||||
|
successThreshold: 1
|
||||||
|
failureThreshold: 3
|
||||||
|
timeoutSeconds: 3
|
||||||
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health
|
||||||
port: {{ .Values.services.apps.port }}
|
port: {{ .Values.services.apps.port }}
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 5
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
|
successThreshold: 1
|
||||||
|
failureThreshold: 3
|
||||||
|
timeoutSeconds: 3
|
||||||
|
|
||||||
name: bbapps
|
name: bbapps
|
||||||
ports:
|
ports:
|
||||||
- containerPort: {{ .Values.services.apps.port }}
|
- containerPort: {{ .Values.services.apps.port }}
|
||||||
|
|
|
@ -202,11 +202,23 @@ spec:
|
||||||
image: budibase/worker:{{ .Values.globals.appVersion }}
|
image: budibase/worker:{{ .Values.globals.appVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: {{ .Values.services.worker.port }}
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 5
|
||||||
|
successThreshold: 1
|
||||||
|
failureThreshold: 3
|
||||||
|
timeoutSeconds: 3
|
||||||
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health
|
||||||
port: {{ .Values.services.worker.port }}
|
port: {{ .Values.services.worker.port }}
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 5
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
|
successThreshold: 1
|
||||||
|
failureThreshold: 3
|
||||||
|
timeoutSeconds: 3
|
||||||
name: bbworker
|
name: bbworker
|
||||||
ports:
|
ports:
|
||||||
- containerPort: {{ .Values.services.worker.port }}
|
- containerPort: {{ .Values.services.worker.port }}
|
||||||
|
|
|
@ -343,6 +343,7 @@ couchdb:
|
||||||
|
|
||||||
## Configure liveness and readiness probe values
|
## Configure liveness and readiness probe values
|
||||||
## Ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes
|
## Ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes
|
||||||
|
# FOR COUCHDB
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
enabled: true
|
enabled: true
|
||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.4.27-alpha.8",
|
"version": "2.4.43",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -25,7 +25,6 @@
|
||||||
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
||||||
"bootstrap": "lerna bootstrap && lerna link && ./scripts/link-dependencies.sh",
|
"bootstrap": "lerna bootstrap && lerna link && ./scripts/link-dependencies.sh",
|
||||||
"build": "lerna run build",
|
"build": "lerna run build",
|
||||||
"build:client": "lerna run build --ignore @budibase/backend-core --ignore @budibase/worker --ignore @budibase/server --ignore @budibase/builder --ignore @budibase/cli --ignore @budibase/sdk",
|
|
||||||
"build:dev": "lerna run prebuild && tsc --build --watch --preserveWatchOutput",
|
"build:dev": "lerna run prebuild && tsc --build --watch --preserveWatchOutput",
|
||||||
"build:backend": "lerna run build --ignore @budibase/client --ignore @budibase/bbui --ignore @budibase/builder --ignore @budibase/cli",
|
"build:backend": "lerna run build --ignore @budibase/client --ignore @budibase/bbui --ignore @budibase/builder --ignore @budibase/cli",
|
||||||
"build:sdk": "lerna run build:sdk",
|
"build:sdk": "lerna run build:sdk",
|
||||||
|
@ -45,7 +44,7 @@
|
||||||
"dev": "yarn run kill-all && lerna link && lerna run --parallel dev:builder --concurrency 1",
|
"dev": "yarn run kill-all && lerna link && lerna run --parallel dev:builder --concurrency 1",
|
||||||
"dev:noserver": "yarn run kill-builder && lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
"dev:noserver": "yarn run kill-builder && lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
||||||
"dev:server": "yarn run kill-server && lerna run --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server",
|
"dev:server": "yarn run kill-server && lerna run --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server",
|
||||||
"test": "lerna run test",
|
"test": "lerna run test --stream",
|
||||||
"test:pro": "bash scripts/pro/test.sh",
|
"test:pro": "bash scripts/pro/test.sh",
|
||||||
"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}\"",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "2.4.27-alpha.8",
|
"version": "2.4.43",
|
||||||
"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",
|
||||||
|
@ -24,7 +24,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": "2.4.27-alpha.8",
|
"@budibase/types": "^2.4.43",
|
||||||
"@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",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
if [[ -n $CI ]]
|
if [[ -n $CI ]]
|
||||||
then
|
then
|
||||||
|
@ -7,6 +8,6 @@ then
|
||||||
jest --coverage --runInBand --forceExit
|
jest --coverage --runInBand --forceExit
|
||||||
else
|
else
|
||||||
# --maxWorkers performs better in development
|
# --maxWorkers performs better in development
|
||||||
echo "jest --coverage"
|
echo "jest --coverage --forceExit"
|
||||||
jest --coverage
|
jest --coverage --forceExit
|
||||||
fi
|
fi
|
|
@ -199,7 +199,6 @@ export async function platformLogout(opts: PlatformLogoutOpts) {
|
||||||
} else {
|
} else {
|
||||||
// clear cookies
|
// clear cookies
|
||||||
clearCookie(ctx, Cookie.Auth)
|
clearCookie(ctx, Cookie.Auth)
|
||||||
clearCookie(ctx, Cookie.CurrentApp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionIds = sessions.map(({ sessionId }) => sessionId)
|
const sessionIds = sessions.map(({ sessionId }) => sessionId)
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import dns from "dns"
|
||||||
|
import net from "net"
|
||||||
|
import env from "../environment"
|
||||||
|
import { promisify } from "util"
|
||||||
|
|
||||||
|
let blackListArray: string[] | undefined
|
||||||
|
const performLookup = promisify(dns.lookup)
|
||||||
|
|
||||||
|
async function lookup(address: string): Promise<string[]> {
|
||||||
|
if (!net.isIP(address)) {
|
||||||
|
// need this for URL parsing simply
|
||||||
|
if (!address.startsWith("http")) {
|
||||||
|
address = `https://${address}`
|
||||||
|
}
|
||||||
|
address = new URL(address).hostname
|
||||||
|
}
|
||||||
|
const addresses = await performLookup(address, {
|
||||||
|
all: true,
|
||||||
|
})
|
||||||
|
return addresses.map(addr => addr.address)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function refreshBlacklist() {
|
||||||
|
const blacklist = env.BLACKLIST_IPS
|
||||||
|
const list = blacklist?.split(",") || []
|
||||||
|
let final: string[] = []
|
||||||
|
for (let addr of list) {
|
||||||
|
const trimmed = addr.trim()
|
||||||
|
if (!net.isIP(trimmed)) {
|
||||||
|
const addresses = await lookup(trimmed)
|
||||||
|
final = final.concat(addresses)
|
||||||
|
} else {
|
||||||
|
final.push(trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blackListArray = final
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isBlacklisted(address: string): Promise<boolean> {
|
||||||
|
if (!blackListArray) {
|
||||||
|
await refreshBlacklist()
|
||||||
|
}
|
||||||
|
if (blackListArray?.length === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// no need for DNS
|
||||||
|
let ips: string[]
|
||||||
|
if (!net.isIP(address)) {
|
||||||
|
ips = await lookup(address)
|
||||||
|
} else {
|
||||||
|
ips = [address]
|
||||||
|
}
|
||||||
|
return !!blackListArray?.find(addr => ips.includes(addr))
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./blacklist"
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { refreshBlacklist, isBlacklisted } from ".."
|
||||||
|
import env from "../../environment"
|
||||||
|
|
||||||
|
describe("blacklist", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
env._set(
|
||||||
|
"BLACKLIST_IPS",
|
||||||
|
"www.google.com,192.168.1.1, 1.1.1.1,2.2.2.2/something"
|
||||||
|
)
|
||||||
|
await refreshBlacklist()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should blacklist 192.168.1.1", async () => {
|
||||||
|
expect(await isBlacklisted("192.168.1.1")).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should allow 192.168.1.2", async () => {
|
||||||
|
expect(await isBlacklisted("192.168.1.2")).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should blacklist www.google.com", async () => {
|
||||||
|
expect(await isBlacklisted("www.google.com")).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle a complex domain", async () => {
|
||||||
|
expect(
|
||||||
|
await isBlacklisted("https://www.google.com/derp/?something=1")
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should allow www.microsoft.com", async () => {
|
||||||
|
expect(await isBlacklisted("www.microsoft.com")).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should blacklist an IP that needed trimming", async () => {
|
||||||
|
expect(await isBlacklisted("1.1.1.1")).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should blacklist 1.1.1.1/something", async () => {
|
||||||
|
expect(await isBlacklisted("1.1.1.1/something")).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should blacklist 2.2.2.2", async () => {
|
||||||
|
expect(await isBlacklisted("2.2.2.2")).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
|
@ -32,8 +32,7 @@ export async function getConfig<T extends Config>(
|
||||||
const db = context.getGlobalDB()
|
const db = context.getGlobalDB()
|
||||||
try {
|
try {
|
||||||
// await to catch error
|
// await to catch error
|
||||||
const config = (await db.get(generateConfigID(type))) as T
|
return (await db.get(generateConfigID(type))) as T
|
||||||
return config
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.status === 404) {
|
if (e.status === 404) {
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
import { DBTestConfiguration, generator, testEnv } from "../../../tests"
|
import {
|
||||||
|
DBTestConfiguration,
|
||||||
|
generator,
|
||||||
|
testEnv,
|
||||||
|
structures,
|
||||||
|
} from "../../../tests"
|
||||||
import { ConfigType } from "@budibase/types"
|
import { ConfigType } from "@budibase/types"
|
||||||
import env from "../../environment"
|
import env from "../../environment"
|
||||||
import * as configs from "../configs"
|
import * as configs from "../configs"
|
||||||
|
@ -113,4 +118,71 @@ describe("configs", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("getGoogleDatasourceConfig", () => {
|
||||||
|
function setEnvVars() {
|
||||||
|
env.GOOGLE_CLIENT_SECRET = "test"
|
||||||
|
env.GOOGLE_CLIENT_ID = "test"
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsetEnvVars() {
|
||||||
|
env.GOOGLE_CLIENT_SECRET = undefined
|
||||||
|
env.GOOGLE_CLIENT_ID = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("cloud", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
testEnv.cloudHosted()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns from env vars", async () => {
|
||||||
|
await config.doInTenant(async () => {
|
||||||
|
setEnvVars()
|
||||||
|
const config = await configs.getGoogleDatasourceConfig()
|
||||||
|
unsetEnvVars()
|
||||||
|
|
||||||
|
expect(config).toEqual({
|
||||||
|
activated: true,
|
||||||
|
clientID: "test",
|
||||||
|
clientSecret: "test",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns undefined when no env vars are configured", async () => {
|
||||||
|
await config.doInTenant(async () => {
|
||||||
|
const config = await configs.getGoogleDatasourceConfig()
|
||||||
|
expect(config).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("self host", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
testEnv.selfHosted()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns from config", async () => {
|
||||||
|
await config.doInTenant(async () => {
|
||||||
|
const googleDoc = structures.sso.googleConfigDoc()
|
||||||
|
await configs.save(googleDoc)
|
||||||
|
const config = await configs.getGoogleDatasourceConfig()
|
||||||
|
expect(config).toEqual(googleDoc.config)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("falls back to env vars when config is disabled", async () => {
|
||||||
|
await config.doInTenant(async () => {
|
||||||
|
setEnvVars()
|
||||||
|
const config = await configs.getGoogleDatasourceConfig()
|
||||||
|
unsetEnvVars()
|
||||||
|
expect(config).toEqual({
|
||||||
|
activated: true,
|
||||||
|
clientID: "test",
|
||||||
|
clientSecret: "test",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,7 +4,6 @@ export enum UserStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Cookie {
|
export enum Cookie {
|
||||||
CurrentApp = "budibase:currentapp",
|
|
||||||
Auth = "budibase:auth",
|
Auth = "budibase:auth",
|
||||||
Init = "budibase:init",
|
Init = "budibase:init",
|
||||||
ACCOUNT_RETURN_URL = "budibase:account:returnurl",
|
ACCOUNT_RETURN_URL = "budibase:account:returnurl",
|
||||||
|
|
|
@ -104,6 +104,7 @@ const environment = {
|
||||||
SMTP_PORT: parseInt(process.env.SMTP_PORT || ""),
|
SMTP_PORT: parseInt(process.env.SMTP_PORT || ""),
|
||||||
SMTP_FROM_ADDRESS: process.env.SMTP_FROM_ADDRESS,
|
SMTP_FROM_ADDRESS: process.env.SMTP_FROM_ADDRESS,
|
||||||
DISABLE_JWT_WARNING: process.env.DISABLE_JWT_WARNING,
|
DISABLE_JWT_WARNING: process.env.DISABLE_JWT_WARNING,
|
||||||
|
BLACKLIST_IPS: process.env.BLACKLIST_IPS,
|
||||||
/**
|
/**
|
||||||
* Enable to allow an admin user to login using a password.
|
* Enable to allow an admin user to login using a password.
|
||||||
* This can be useful to prevent lockout when configuring SSO.
|
* This can be useful to prevent lockout when configuring SSO.
|
||||||
|
|
|
@ -26,6 +26,7 @@ export * as utils from "./utils"
|
||||||
export * as errors from "./errors"
|
export * as errors from "./errors"
|
||||||
export * as timers from "./timers"
|
export * as timers from "./timers"
|
||||||
export { default as env } from "./environment"
|
export { default as env } from "./environment"
|
||||||
|
export * as blacklist from "./blacklist"
|
||||||
export { SearchParams } from "./db"
|
export { SearchParams } from "./db"
|
||||||
// Add context to tenancy for backwards compatibility
|
// Add context to tenancy for backwards compatibility
|
||||||
// only do this for external usages to prevent internal
|
// only do this for external usages to prevent internal
|
||||||
|
|
|
@ -78,17 +78,23 @@ export async function postAuth(
|
||||||
),
|
),
|
||||||
{ successRedirect: "/", failureRedirect: "/error" },
|
{ successRedirect: "/", failureRedirect: "/error" },
|
||||||
async (err: any, tokens: string[]) => {
|
async (err: any, tokens: string[]) => {
|
||||||
|
const baseUrl = `/builder/app/${authStateCookie.appId}/data`
|
||||||
// update the DB for the datasource with all the user info
|
// update the DB for the datasource with all the user info
|
||||||
await doWithDB(authStateCookie.appId, async (db: Database) => {
|
await doWithDB(authStateCookie.appId, async (db: Database) => {
|
||||||
const datasource = await db.get(authStateCookie.datasourceId)
|
let datasource
|
||||||
|
try {
|
||||||
|
datasource = await db.get(authStateCookie.datasourceId)
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.status === 404) {
|
||||||
|
ctx.redirect(baseUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!datasource.config) {
|
if (!datasource.config) {
|
||||||
datasource.config = {}
|
datasource.config = {}
|
||||||
}
|
}
|
||||||
datasource.config.auth = { type: "google", ...tokens }
|
datasource.config.auth = { type: "google", ...tokens }
|
||||||
await db.put(datasource)
|
await db.put(datasource)
|
||||||
ctx.redirect(
|
ctx.redirect(`${baseUrl}/datasource/${authStateCookie.datasourceId}`)
|
||||||
`/builder/app/${authStateCookie.appId}/data/datasource/${authStateCookie.datasourceId}`
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)(ctx, next)
|
)(ctx, next)
|
||||||
|
|
|
@ -8,4 +8,5 @@ export * as plugins from "./plugins"
|
||||||
export * as sso from "./sso"
|
export * as sso from "./sso"
|
||||||
export * as tenant from "./tenants"
|
export * as tenant from "./tenants"
|
||||||
export * as users from "./users"
|
export * as users from "./users"
|
||||||
|
export * as userGroups from "./userGroups"
|
||||||
export { generator } from "./generator"
|
export { generator } from "./generator"
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import {
|
import {
|
||||||
|
ConfigType,
|
||||||
|
GoogleConfig,
|
||||||
GoogleInnerConfig,
|
GoogleInnerConfig,
|
||||||
JwtClaims,
|
JwtClaims,
|
||||||
OAuth2,
|
OAuth2,
|
||||||
|
@ -10,10 +12,10 @@ import {
|
||||||
User,
|
User,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { generator } from "./generator"
|
import { generator } from "./generator"
|
||||||
import { uuid, email } from "./common"
|
import { email, uuid } from "./common"
|
||||||
import * as shared from "./shared"
|
import * as shared from "./shared"
|
||||||
import _ from "lodash"
|
|
||||||
import { user } from "./shared"
|
import { user } from "./shared"
|
||||||
|
import _ from "lodash"
|
||||||
|
|
||||||
export function OAuth(): OAuth2 {
|
export function OAuth(): OAuth2 {
|
||||||
return {
|
return {
|
||||||
|
@ -107,3 +109,11 @@ export function googleConfig(): GoogleInnerConfig {
|
||||||
clientSecret: generator.string(),
|
clientSecret: generator.string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function googleConfigDoc(): GoogleConfig {
|
||||||
|
return {
|
||||||
|
_id: "config_google",
|
||||||
|
type: ConfigType.GOOGLE,
|
||||||
|
config: googleConfig(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { UserGroup } from "@budibase/types"
|
||||||
|
import { generator } from "./generator"
|
||||||
|
|
||||||
|
export function userGroup(): UserGroup {
|
||||||
|
return {
|
||||||
|
name: generator.word(),
|
||||||
|
icon: generator.word(),
|
||||||
|
color: generator.word(),
|
||||||
|
}
|
||||||
|
}
|
|
@ -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": "2.4.27-alpha.8",
|
"version": "2.4.43",
|
||||||
"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": "2.4.27-alpha.8",
|
"@budibase/shared-core": "^2.4.43",
|
||||||
"@budibase/string-templates": "2.4.27-alpha.8",
|
"@budibase/string-templates": "^2.4.43",
|
||||||
"@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",
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
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% - 576px)"
|
export let width = "calc(100% - 626px)"
|
||||||
|
|
||||||
let visible = false
|
let visible = false
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
<script>
|
||||||
|
import ActionButton from "../../ActionButton/ActionButton.svelte"
|
||||||
|
import { uuid } from "../../helpers"
|
||||||
|
import Icon from "../../Icon/Icon.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let value = null
|
||||||
|
export let title = "Upload file"
|
||||||
|
export let disabled = false
|
||||||
|
export let allowClear = null
|
||||||
|
export let extensions = null
|
||||||
|
export let handleFileTooLarge = null
|
||||||
|
export let fileSizeLimit = BYTES_IN_MB * 20
|
||||||
|
export let id = null
|
||||||
|
export let previewUrl = null
|
||||||
|
|
||||||
|
const fieldId = id || uuid()
|
||||||
|
const BYTES_IN_KB = 1000
|
||||||
|
const BYTES_IN_MB = 1000000
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let fileInput
|
||||||
|
|
||||||
|
$: inputAccept = Array.isArray(extensions) ? extensions.join(",") : "*"
|
||||||
|
|
||||||
|
async function processFile(targetFile) {
|
||||||
|
if (handleFileTooLarge && targetFile?.size >= fileSizeLimit) {
|
||||||
|
handleFileTooLarge(targetFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dispatch("change", targetFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFile(evt) {
|
||||||
|
processFile(evt.target.files[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFile() {
|
||||||
|
dispatch("change", null)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id={fieldId}
|
||||||
|
{disabled}
|
||||||
|
type="file"
|
||||||
|
accept={inputAccept}
|
||||||
|
bind:this={fileInput}
|
||||||
|
on:change={handleFile}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
{#if value}
|
||||||
|
<div class="file-view">
|
||||||
|
{#if previewUrl}
|
||||||
|
<img class="preview" alt="" src={previewUrl} />
|
||||||
|
{/if}
|
||||||
|
<div class="filename">{value.name}</div>
|
||||||
|
{#if value.size}
|
||||||
|
<div class="filesize">
|
||||||
|
{#if value.size <= BYTES_IN_MB}
|
||||||
|
{`${value.size / BYTES_IN_KB} KB`}
|
||||||
|
{:else}
|
||||||
|
{`${value.size / BYTES_IN_MB} MB`}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if !disabled || (allowClear === true && disabled)}
|
||||||
|
<div class="delete-button" on:click={clearFile}>
|
||||||
|
<Icon name="Close" size="XS" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<ActionButton {disabled} on:click={fileInput.click()}>{title}</ActionButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.file-view {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--spectrum-alias-border-color);
|
||||||
|
border-radius: var(--spectrum-global-dimension-size-50);
|
||||||
|
padding: 0px var(--spectrum-alias-item-padding-m);
|
||||||
|
}
|
||||||
|
input[type="file"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.delete-button {
|
||||||
|
transition: all 0.3s;
|
||||||
|
margin-left: 10px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.delete-button:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
.filesize {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.filename {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.preview {
|
||||||
|
height: 1.5em;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -42,9 +42,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFieldText = (value, options, placeholder) => {
|
const getFieldText = (value, options, placeholder) => {
|
||||||
// Always use placeholder if no value
|
|
||||||
if (value == null || value === "") {
|
if (value == null || value === "") {
|
||||||
return placeholder !== false ? "Choose an option" : ""
|
// Explicit false means use no placeholder and allow an empty fields
|
||||||
|
if (placeholder === false) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Otherwise we use the placeholder if possible
|
||||||
|
return placeholder || "Choose an option"
|
||||||
}
|
}
|
||||||
|
|
||||||
return getFieldAttribute(getOptionLabel, value, options)
|
return getFieldAttribute(getOptionLabel, value, options)
|
||||||
|
|
|
@ -13,3 +13,4 @@ export { default as CoreDropzone } from "./Dropzone.svelte"
|
||||||
export { default as CoreStepper } from "./Stepper.svelte"
|
export { default as CoreStepper } from "./Stepper.svelte"
|
||||||
export { default as CoreRichTextField } from "./RichTextField.svelte"
|
export { default as CoreRichTextField } from "./RichTextField.svelte"
|
||||||
export { default as CoreSlider } from "./Slider.svelte"
|
export { default as CoreSlider } from "./Slider.svelte"
|
||||||
|
export { default as CoreFile } from "./File.svelte"
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
<script>
|
||||||
|
import Field from "./Field.svelte"
|
||||||
|
import { CoreFile } from "./Core"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let label = null
|
||||||
|
export let labelPosition = "above"
|
||||||
|
export let disabled = false
|
||||||
|
export let allowClear = null
|
||||||
|
export let handleFileTooLarge = () => {}
|
||||||
|
export let previewUrl = null
|
||||||
|
export let extensions = null
|
||||||
|
export let error = null
|
||||||
|
export let title = null
|
||||||
|
export let value = null
|
||||||
|
export let tooltip = null
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
const onChange = e => {
|
||||||
|
value = e.detail
|
||||||
|
dispatch("change", e.detail)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Field {label} {labelPosition} {error} {tooltip}>
|
||||||
|
<CoreFile
|
||||||
|
{error}
|
||||||
|
{disabled}
|
||||||
|
{allowClear}
|
||||||
|
{title}
|
||||||
|
{value}
|
||||||
|
{previewUrl}
|
||||||
|
{handleFileTooLarge}
|
||||||
|
{extensions}
|
||||||
|
on:change={onChange}
|
||||||
|
/>
|
||||||
|
</Field>
|
|
@ -77,6 +77,7 @@ export { default as IconSideNav } from "./IconSideNav/IconSideNav.svelte"
|
||||||
export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte"
|
export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte"
|
||||||
export { default as Slider } from "./Form/Slider.svelte"
|
export { default as Slider } from "./Form/Slider.svelte"
|
||||||
export { default as Accordion } from "./Accordion/Accordion.svelte"
|
export { default as Accordion } from "./Accordion/Accordion.svelte"
|
||||||
|
export { default as File } from "./Form/File.svelte"
|
||||||
|
|
||||||
// Renderers
|
// Renderers
|
||||||
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
|
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html class="spectrum spectrum--medium spectrum--darkest" lang="en" dir="ltr">
|
<html class="spectrum spectrum--medium spectrum--darkest" lang="en" dir="ltr">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset='utf8'>
|
<meta charset='utf8'>
|
||||||
<meta name='viewport' content='width=device-width'>
|
<meta name='viewport' content='width=device-width'>
|
||||||
<title>Budibase</title>
|
<title>Budibase</title>
|
||||||
<link rel='icon' href='/src/favicon.png'>
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||||
<link
|
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"
|
||||||
href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"
|
rel="stylesheet" />
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body id="app">
|
<body id="app">
|
||||||
<script type="module" src='/src/main.js'></script>
|
<script type="module" src='/src/main.js'></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "2.4.27-alpha.8",
|
"version": "2.4.43",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -58,11 +58,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.4.27-alpha.8",
|
"@budibase/bbui": "^2.4.43",
|
||||||
"@budibase/client": "2.4.27-alpha.8",
|
"@budibase/client": "^2.4.43",
|
||||||
"@budibase/frontend-core": "2.4.27-alpha.8",
|
"@budibase/frontend-core": "^2.4.43",
|
||||||
"@budibase/shared-core": "2.4.27-alpha.8",
|
"@budibase/shared-core": "^2.4.43",
|
||||||
"@budibase/string-templates": "2.4.27-alpha.8",
|
"@budibase/string-templates": "^2.4.43",
|
||||||
"@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",
|
||||||
|
|
|
@ -163,7 +163,12 @@ export const getComponentSettings = componentType => {
|
||||||
def.settings
|
def.settings
|
||||||
?.filter(setting => setting.section)
|
?.filter(setting => setting.section)
|
||||||
.forEach(section => {
|
.forEach(section => {
|
||||||
settings = settings.concat(section.settings || [])
|
settings = settings.concat(
|
||||||
|
(section.settings || []).map(setting => ({
|
||||||
|
...setting,
|
||||||
|
section: section.name,
|
||||||
|
}))
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
componentSettingCache[componentType] = settings
|
componentSettingCache[componentType] = settings
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
findComponent,
|
findComponent,
|
||||||
getComponentSettings,
|
getComponentSettings,
|
||||||
makeComponentUnique,
|
makeComponentUnique,
|
||||||
|
findComponentPath,
|
||||||
} from "../componentUtils"
|
} from "../componentUtils"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
@ -30,7 +31,12 @@ import {
|
||||||
DB_TYPE_INTERNAL,
|
DB_TYPE_INTERNAL,
|
||||||
DB_TYPE_EXTERNAL,
|
DB_TYPE_EXTERNAL,
|
||||||
} from "constants/backend"
|
} from "constants/backend"
|
||||||
import { getSchemaForDatasource } from "builderStore/dataBinding"
|
import {
|
||||||
|
buildFormSchema,
|
||||||
|
getSchemaForDatasource,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
|
import { getComponentFieldOptions } from "helpers/formFields"
|
||||||
|
|
||||||
const INITIAL_FRONTEND_STATE = {
|
const INITIAL_FRONTEND_STATE = {
|
||||||
apps: [],
|
apps: [],
|
||||||
|
@ -63,17 +69,19 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
customTheme: {},
|
customTheme: {},
|
||||||
previewDevice: "desktop",
|
previewDevice: "desktop",
|
||||||
highlightedSettingKey: null,
|
highlightedSettingKey: null,
|
||||||
|
builderSidePanel: false,
|
||||||
|
|
||||||
// URL params
|
// URL params
|
||||||
selectedScreenId: null,
|
selectedScreenId: null,
|
||||||
selectedComponentId: null,
|
selectedComponentId: null,
|
||||||
selectedLayoutId: null,
|
selectedLayoutId: null,
|
||||||
|
|
||||||
// onboarding
|
// Client state
|
||||||
|
selectedComponentInstance: null,
|
||||||
|
|
||||||
|
// Onboarding
|
||||||
onboarding: false,
|
onboarding: false,
|
||||||
tourNodes: null,
|
tourNodes: null,
|
||||||
|
|
||||||
builderSidePanel: false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFrontendStore = () => {
|
export const getFrontendStore = () => {
|
||||||
|
@ -262,22 +270,27 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
save: async screen => {
|
save: async screen => {
|
||||||
/*
|
// Validate screen structure
|
||||||
Temporarily disabled to accomodate migration issues.
|
// Temporarily disabled to accommodate migration issues
|
||||||
store.actions.screens.validate(screen)
|
// store.actions.screens.validate(screen)
|
||||||
*/
|
|
||||||
const state = get(store)
|
// Check screen definition for any component settings which need updated
|
||||||
|
store.actions.screens.enrichEmptySettings(screen)
|
||||||
|
|
||||||
|
// Save screen
|
||||||
const creatingNewScreen = screen._id === undefined
|
const creatingNewScreen = screen._id === undefined
|
||||||
const savedScreen = await API.saveScreen(screen)
|
const savedScreen = await API.saveScreen(screen)
|
||||||
const routesResponse = await API.fetchAppRoutes()
|
const routesResponse = await API.fetchAppRoutes()
|
||||||
let usedPlugins = state.usedPlugins
|
|
||||||
|
|
||||||
// If plugins changed we need to fetch the latest app metadata
|
// If plugins changed we need to fetch the latest app metadata
|
||||||
|
const state = get(store)
|
||||||
|
let usedPlugins = state.usedPlugins
|
||||||
if (savedScreen.pluginAdded) {
|
if (savedScreen.pluginAdded) {
|
||||||
const { application } = await API.fetchAppPackage(state.appId)
|
const { application } = await API.fetchAppPackage(state.appId)
|
||||||
usedPlugins = application.usedPlugins || []
|
usedPlugins = application.usedPlugins || []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update state
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
// Update screen object
|
// Update screen object
|
||||||
const idx = state.screens.findIndex(x => x._id === savedScreen._id)
|
const idx = state.screens.findIndex(x => x._id === savedScreen._id)
|
||||||
|
@ -298,7 +311,6 @@ export const getFrontendStore = () => {
|
||||||
|
|
||||||
// Update used plugins
|
// Update used plugins
|
||||||
state.usedPlugins = usedPlugins
|
state.usedPlugins = usedPlugins
|
||||||
|
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
return savedScreen
|
return savedScreen
|
||||||
|
@ -406,6 +418,17 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
await store.actions.screens.patch(patch, screen._id)
|
await store.actions.screens.patch(patch, screen._id)
|
||||||
},
|
},
|
||||||
|
enrichEmptySettings: screen => {
|
||||||
|
// Flatten the recursive component tree
|
||||||
|
const components = findAllMatchingComponents(screen.props, x => x)
|
||||||
|
|
||||||
|
// Iterate over all components and run checks
|
||||||
|
components.forEach(component => {
|
||||||
|
store.actions.components.enrichEmptySettings(component, {
|
||||||
|
screen,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
setDevice: device => {
|
setDevice: device => {
|
||||||
|
@ -493,65 +516,155 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
return get(store).components[componentName]
|
return get(store).components[componentName]
|
||||||
},
|
},
|
||||||
createInstance: (componentName, presetProps) => {
|
getDefaultDatasource: () => {
|
||||||
|
// Ignore users table
|
||||||
|
const validTables = get(tables).list.filter(x => x._id !== "ta_users")
|
||||||
|
|
||||||
|
// Try to use their own internal table first
|
||||||
|
let table = validTables.find(table => {
|
||||||
|
return (
|
||||||
|
table.sourceId !== BUDIBASE_INTERNAL_DB_ID &&
|
||||||
|
table.type === DB_TYPE_INTERNAL
|
||||||
|
)
|
||||||
|
})
|
||||||
|
if (table) {
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then try sample data
|
||||||
|
table = validTables.find(table => {
|
||||||
|
return (
|
||||||
|
table.sourceId === BUDIBASE_INTERNAL_DB_ID &&
|
||||||
|
table.type === DB_TYPE_INTERNAL
|
||||||
|
)
|
||||||
|
})
|
||||||
|
if (table) {
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally try an external table
|
||||||
|
return validTables.find(table => table.type === DB_TYPE_EXTERNAL)
|
||||||
|
},
|
||||||
|
enrichEmptySettings: (component, opts) => {
|
||||||
|
if (!component?._component) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const defaultDS = store.actions.components.getDefaultDatasource()
|
||||||
|
const settings = getComponentSettings(component._component)
|
||||||
|
const { parent, screen, useDefaultValues } = opts || {}
|
||||||
|
const treeId = parent?._id || component._id
|
||||||
|
if (!screen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
settings.forEach(setting => {
|
||||||
|
const value = component[setting.key]
|
||||||
|
|
||||||
|
// Fill empty settings
|
||||||
|
if (value == null || value === "") {
|
||||||
|
if (setting.type === "multifield" && setting.selectAllFields) {
|
||||||
|
// Select all schema fields where required
|
||||||
|
component[setting.key] = Object.keys(defaultDS?.schema || {})
|
||||||
|
} else if (
|
||||||
|
(setting.type === "dataSource" || setting.type === "table") &&
|
||||||
|
defaultDS
|
||||||
|
) {
|
||||||
|
// Select default datasource where required
|
||||||
|
component[setting.key] = {
|
||||||
|
label: defaultDS.name,
|
||||||
|
tableId: defaultDS._id,
|
||||||
|
type: "table",
|
||||||
|
}
|
||||||
|
} else if (setting.type === "dataProvider") {
|
||||||
|
// Pick closest data provider where required
|
||||||
|
const path = findComponentPath(screen.props, treeId)
|
||||||
|
const providers = path.filter(component =>
|
||||||
|
component._component?.endsWith("/dataprovider")
|
||||||
|
)
|
||||||
|
if (providers.length) {
|
||||||
|
const id = providers[providers.length - 1]?._id
|
||||||
|
component[setting.key] = `{{ literal ${safe(id)} }}`
|
||||||
|
}
|
||||||
|
} else if (setting.type.startsWith("field/")) {
|
||||||
|
// Autofill form field names
|
||||||
|
// Get all available field names in this form schema
|
||||||
|
let fieldOptions = getComponentFieldOptions(
|
||||||
|
screen.props,
|
||||||
|
treeId,
|
||||||
|
setting.type,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get all currently used fields
|
||||||
|
const form = findClosestMatchingComponent(
|
||||||
|
screen.props,
|
||||||
|
treeId,
|
||||||
|
x => x._component === "@budibase/standard-components/form"
|
||||||
|
)
|
||||||
|
const usedFields = Object.keys(buildFormSchema(form) || {})
|
||||||
|
|
||||||
|
// Filter out already used fields
|
||||||
|
fieldOptions = fieldOptions.filter(x => !usedFields.includes(x))
|
||||||
|
|
||||||
|
// Set field name and also assume we have a label setting
|
||||||
|
if (fieldOptions[0]) {
|
||||||
|
component[setting.key] = fieldOptions[0]
|
||||||
|
component.label = fieldOptions[0]
|
||||||
|
}
|
||||||
|
} else if (useDefaultValues && setting.defaultValue !== undefined) {
|
||||||
|
// Use default value where required
|
||||||
|
component[setting.key] = setting.defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate non-empty settings
|
||||||
|
else {
|
||||||
|
if (setting.type === "dataProvider") {
|
||||||
|
// Validate data provider exists, or else clear it
|
||||||
|
const treeId = parent?._id || component._id
|
||||||
|
const path = findComponentPath(screen?.props, treeId)
|
||||||
|
const providers = path.filter(component =>
|
||||||
|
component._component?.endsWith("/dataprovider")
|
||||||
|
)
|
||||||
|
// Validate non-empty values
|
||||||
|
const valid = providers?.some(dp => value.includes?.(dp._id))
|
||||||
|
if (!valid) {
|
||||||
|
if (providers.length) {
|
||||||
|
const id = providers[providers.length - 1]?._id
|
||||||
|
component[setting.key] = `{{ literal ${safe(id)} }}`
|
||||||
|
} else {
|
||||||
|
delete component[setting.key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
createInstance: (componentName, presetProps, parent) => {
|
||||||
const definition = store.actions.components.getDefinition(componentName)
|
const definition = store.actions.components.getDefinition(componentName)
|
||||||
if (!definition) {
|
if (!definition) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flattened settings
|
// Generate basic component structure
|
||||||
const settings = getComponentSettings(componentName)
|
let instance = {
|
||||||
|
_id: Helpers.uuid(),
|
||||||
let dataSourceField = settings.find(
|
_component: definition.component,
|
||||||
setting => setting.type == "dataSource" || setting.type == "table"
|
_styles: {
|
||||||
)
|
normal: {},
|
||||||
|
hover: {},
|
||||||
let defaultDatasource
|
active: {},
|
||||||
if (dataSourceField) {
|
},
|
||||||
const _tables = get(tables)
|
_instanceName: `New ${definition.friendlyName || definition.name}`,
|
||||||
const filteredTables = _tables.list.filter(
|
...presetProps,
|
||||||
table => table._id != "ta_users"
|
|
||||||
)
|
|
||||||
|
|
||||||
const internalTable = filteredTables.find(
|
|
||||||
table =>
|
|
||||||
table.sourceId === BUDIBASE_INTERNAL_DB_ID &&
|
|
||||||
table.type == DB_TYPE_INTERNAL
|
|
||||||
)
|
|
||||||
|
|
||||||
const defaultSourceTable = filteredTables.find(
|
|
||||||
table =>
|
|
||||||
table.sourceId !== BUDIBASE_INTERNAL_DB_ID &&
|
|
||||||
table.type == DB_TYPE_INTERNAL
|
|
||||||
)
|
|
||||||
|
|
||||||
const defaultExternalTable = filteredTables.find(
|
|
||||||
table => table.type == DB_TYPE_EXTERNAL
|
|
||||||
)
|
|
||||||
|
|
||||||
defaultDatasource =
|
|
||||||
defaultSourceTable || internalTable || defaultExternalTable
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate default props
|
// Enrich empty settings
|
||||||
let props = { ...presetProps }
|
store.actions.components.enrichEmptySettings(instance, {
|
||||||
settings.forEach(setting => {
|
parent,
|
||||||
if (setting.type === "multifield" && setting.selectAllFields) {
|
screen: get(selectedScreen),
|
||||||
props[setting.key] = Object.keys(defaultDatasource.schema || {})
|
useDefaultValues: true,
|
||||||
} else if (setting.defaultValue !== undefined) {
|
|
||||||
props[setting.key] = setting.defaultValue
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set a default datasource
|
|
||||||
if (dataSourceField && defaultDatasource) {
|
|
||||||
props[dataSourceField.key] = {
|
|
||||||
label: defaultDatasource.name,
|
|
||||||
tableId: defaultDatasource._id,
|
|
||||||
type: "table",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add any extra properties the component needs
|
// Add any extra properties the component needs
|
||||||
let extras = {}
|
let extras = {}
|
||||||
if (definition.hasChildren) {
|
if (definition.hasChildren) {
|
||||||
|
@ -569,17 +682,8 @@ export const getFrontendStore = () => {
|
||||||
extras.step = formSteps.length + 1
|
extras.step = formSteps.length + 1
|
||||||
extras._instanceName = `Step ${formSteps.length + 1}`
|
extras._instanceName = `Step ${formSteps.length + 1}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_id: Helpers.uuid(),
|
...cloneDeep(instance),
|
||||||
_component: definition.component,
|
|
||||||
_styles: {
|
|
||||||
normal: {},
|
|
||||||
hover: {},
|
|
||||||
active: {},
|
|
||||||
},
|
|
||||||
_instanceName: `New ${definition.friendlyName || definition.name}`,
|
|
||||||
...cloneDeep(props),
|
|
||||||
...extras,
|
...extras,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -587,7 +691,8 @@ export const getFrontendStore = () => {
|
||||||
const state = get(store)
|
const state = get(store)
|
||||||
const componentInstance = store.actions.components.createInstance(
|
const componentInstance = store.actions.components.createInstance(
|
||||||
componentName,
|
componentName,
|
||||||
presetProps
|
presetProps,
|
||||||
|
parent
|
||||||
)
|
)
|
||||||
if (!componentInstance) {
|
if (!componentInstance) {
|
||||||
return
|
return
|
||||||
|
@ -1123,6 +1228,52 @@ export const getFrontendStore = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
addParent: async (componentId, parentType) => {
|
||||||
|
if (!componentId || !parentType) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new parent instance
|
||||||
|
const newParentDefinition = store.actions.components.createInstance(
|
||||||
|
parentType,
|
||||||
|
null,
|
||||||
|
parent
|
||||||
|
)
|
||||||
|
if (!newParentDefinition) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace component with a version wrapped in a new parent
|
||||||
|
await store.actions.screens.patch(screen => {
|
||||||
|
// Get this component definition and parent definition
|
||||||
|
let definition = findComponent(screen.props, componentId)
|
||||||
|
let oldParentDefinition = findComponentParent(
|
||||||
|
screen.props,
|
||||||
|
componentId
|
||||||
|
)
|
||||||
|
if (!definition || !oldParentDefinition) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace component with parent
|
||||||
|
const index = oldParentDefinition._children.findIndex(
|
||||||
|
component => component._id === componentId
|
||||||
|
)
|
||||||
|
if (index === -1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
oldParentDefinition._children[index] = {
|
||||||
|
...newParentDefinition,
|
||||||
|
_children: [definition],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Select the new parent
|
||||||
|
store.update(state => {
|
||||||
|
state.selectedComponentId = newParentDefinition._id
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
links: {
|
links: {
|
||||||
save: async (url, title) => {
|
save: async (url, title) => {
|
||||||
|
|
|
@ -32,8 +32,8 @@
|
||||||
import { getSchemaForTable } from "builderStore/dataBinding"
|
import { getSchemaForTable } 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 { cloneDeep } from "lodash/fp"
|
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
export let testData
|
export let testData
|
||||||
|
@ -214,8 +214,6 @@
|
||||||
function saveFilters(key) {
|
function saveFilters(key) {
|
||||||
const filters = LuceneUtils.buildLuceneQuery(tempFilters)
|
const filters = LuceneUtils.buildLuceneQuery(tempFilters)
|
||||||
const defKey = `${key}-def`
|
const defKey = `${key}-def`
|
||||||
inputData[key] = filters
|
|
||||||
inputData[defKey] = tempFilters
|
|
||||||
onChange({ detail: filters }, key)
|
onChange({ detail: filters }, key)
|
||||||
// need to store the builder definition in the automation
|
// need to store the builder definition in the automation
|
||||||
onChange({ detail: tempFilters }, defKey)
|
onChange({ detail: tempFilters }, defKey)
|
||||||
|
|
|
@ -95,8 +95,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChange = (e, field, type) => {
|
const onChange = (e, field, type) => {
|
||||||
value[field] = coerce(e.detail, type)
|
let newValue = {
|
||||||
dispatch("change", value)
|
...value,
|
||||||
|
[field]: coerce(e.detail, type),
|
||||||
|
}
|
||||||
|
dispatch("change", newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChangeSetting = (e, field) => {
|
const onChangeSetting = (e, field) => {
|
||||||
|
|
|
@ -136,6 +136,7 @@
|
||||||
const onUpdateColumns = () => {
|
const onUpdateColumns = () => {
|
||||||
selectedRows = []
|
selectedRows = []
|
||||||
fetch.refresh()
|
fetch.refresh()
|
||||||
|
tables.fetchTable(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch data whenever rows are modified. Unfortunately we have to lose
|
// Fetch data whenever rows are modified. Unfortunately we have to lose
|
||||||
|
|
|
@ -5,18 +5,28 @@
|
||||||
|
|
||||||
export let preAuthStep
|
export let preAuthStep
|
||||||
export let datasource
|
export let datasource
|
||||||
|
export let disabled
|
||||||
|
|
||||||
$: tenantId = $auth.tenantId
|
$: tenantId = $auth.tenantId
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
class:disabled
|
||||||
|
{disabled}
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
let ds = datasource
|
let ds = datasource
|
||||||
|
let appId = $store.appId
|
||||||
if (!ds) {
|
if (!ds) {
|
||||||
ds = await preAuthStep()
|
const resp = await preAuthStep()
|
||||||
|
if (resp.datasource && resp.appId) {
|
||||||
|
ds = resp.datasource
|
||||||
|
appId = resp.appId
|
||||||
|
} else {
|
||||||
|
ds = resp
|
||||||
|
}
|
||||||
}
|
}
|
||||||
window.open(
|
window.open(
|
||||||
`/api/global/auth/${tenantId}/datasource/google?datasourceId=${ds._id}&appId=${$store.appId}`,
|
`/api/global/auth/${tenantId}/datasource/google?datasourceId=${ds._id}&appId=${appId}`,
|
||||||
"_blank"
|
"_blank"
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
@ -26,6 +36,10 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
width: 195px;
|
width: 195px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
// 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)
|
||||||
$: isGoogleConfigured = !!$organisation.google
|
$: isGoogleConfigured = !!$organisation.googleDatasourceConfigured
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await organisation.init()
|
await organisation.init()
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let icon
|
export let icon
|
||||||
export let expandable = false
|
|
||||||
export let showAddButton = false
|
export let showAddButton = false
|
||||||
export let showBackButton = false
|
export let showBackButton = false
|
||||||
export let showCloseButton = false
|
export let showCloseButton = false
|
||||||
|
@ -12,8 +11,8 @@
|
||||||
export let onClickCloseButton
|
export let onClickCloseButton
|
||||||
export let borderLeft = false
|
export let borderLeft = false
|
||||||
export let borderRight = false
|
export let borderRight = false
|
||||||
|
export let wide = false
|
||||||
|
|
||||||
let wide = false
|
|
||||||
$: customHeaderContent = $$slots["panel-header-content"]
|
$: customHeaderContent = $$slots["panel-header-content"]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -28,13 +27,6 @@
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<Heading size="XXS">{title || ""}</Heading>
|
<Heading size="XXS">{title || ""}</Heading>
|
||||||
</div>
|
</div>
|
||||||
{#if expandable}
|
|
||||||
<Icon
|
|
||||||
name={wide ? "Minimize" : "Maximize"}
|
|
||||||
hoverable
|
|
||||||
on:click={() => (wide = !wide)}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{#if showAddButton}
|
{#if showAddButton}
|
||||||
<div class="add-button" on:click={onClickAddButton}>
|
<div class="add-button" on:click={onClickAddButton}>
|
||||||
<Icon name="Add" />
|
<Icon name="Add" />
|
||||||
|
@ -74,8 +66,8 @@
|
||||||
border-right: var(--border-light);
|
border-right: var(--border-light);
|
||||||
}
|
}
|
||||||
.panel.wide {
|
.panel.wide {
|
||||||
width: 420px;
|
width: 310px;
|
||||||
flex: 0 0 420px;
|
flex: 0 0 310px;
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
flex: 0 0 48px;
|
flex: 0 0 48px;
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
: enrichedSchemaFields?.map(field => field.name)
|
: enrichedSchemaFields?.map(field => field.name)
|
||||||
$: sanitisedValue = getValidColumns(value, options)
|
$: sanitisedValue = getValidColumns(value, options)
|
||||||
$: updateBoundValue(sanitisedValue)
|
$: updateBoundValue(sanitisedValue)
|
||||||
$: enrichedSchemaFields = getFields(Object.values(schema) || [], {
|
$: enrichedSchemaFields = getFields(Object.values(schema || {}), {
|
||||||
allowLinks: true,
|
allowLinks: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -3,23 +3,13 @@
|
||||||
import { makePropSafe } from "@budibase/string-templates"
|
import { makePropSafe } from "@budibase/string-templates"
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import { findComponentPath } from "builderStore/componentUtils"
|
import { findComponentPath } from "builderStore/componentUtils"
|
||||||
import { createEventDispatcher, onMount } from "svelte"
|
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
const getValue = component => `{{ literal ${makePropSafe(component._id)} }}`
|
const getValue = component => `{{ literal ${makePropSafe(component._id)} }}`
|
||||||
|
|
||||||
$: path = findComponentPath($currentAsset?.props, $store.selectedComponentId)
|
$: path = findComponentPath($currentAsset?.props, $store.selectedComponentId)
|
||||||
$: providers = path.filter(c => c._component?.endsWith("/dataprovider"))
|
$: providers = path.filter(c => c._component?.endsWith("/dataprovider"))
|
||||||
|
|
||||||
// Set initial value to closest data provider
|
|
||||||
onMount(() => {
|
|
||||||
const valid = value && providers.find(x => getValue(x) === value) != null
|
|
||||||
if (!valid && providers.length) {
|
|
||||||
dispatch("change", getValue(providers[providers.length - 1]))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
|
|
|
@ -1,43 +1,17 @@
|
||||||
<script>
|
<script>
|
||||||
import { Combobox } from "@budibase/bbui"
|
import { Combobox } from "@budibase/bbui"
|
||||||
import {
|
|
||||||
getDatasourceForProvider,
|
|
||||||
getSchemaForDatasource,
|
|
||||||
} from "builderStore/dataBinding"
|
|
||||||
import { currentAsset } from "builderStore"
|
import { currentAsset } from "builderStore"
|
||||||
import { findClosestMatchingComponent } from "builderStore/componentUtils"
|
import { getComponentFieldOptions } from "helpers/formFields"
|
||||||
|
|
||||||
export let componentInstance
|
export let componentInstance
|
||||||
export let value
|
export let value
|
||||||
export let type
|
export let type
|
||||||
|
|
||||||
$: form = findClosestMatchingComponent(
|
$: options = getComponentFieldOptions(
|
||||||
$currentAsset?.props,
|
$currentAsset?.props,
|
||||||
componentInstance._id,
|
componentInstance?._id,
|
||||||
component => component._component === "@budibase/standard-components/form"
|
type
|
||||||
)
|
)
|
||||||
$: datasource = getDatasourceForProvider($currentAsset, form)
|
|
||||||
$: schema = getSchemaForDatasource($currentAsset, datasource, {
|
|
||||||
formSchema: true,
|
|
||||||
}).schema
|
|
||||||
$: options = getOptions(schema, type)
|
|
||||||
|
|
||||||
const getOptions = (schema, type) => {
|
|
||||||
let entries = Object.entries(schema ?? {})
|
|
||||||
let types = []
|
|
||||||
if (type === "field/options" || type === "field/longform") {
|
|
||||||
// allow options and longform to be used on string fields as well
|
|
||||||
types = [type, "field/string"]
|
|
||||||
} else {
|
|
||||||
types = [type]
|
|
||||||
}
|
|
||||||
|
|
||||||
types = types.map(type => type.slice(type.indexOf("/") + 1))
|
|
||||||
|
|
||||||
entries = entries.filter(entry => types.includes(entry[1].type))
|
|
||||||
|
|
||||||
return entries.map(entry => entry[0])
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Combobox on:change {value} {options} />
|
<Combobox on:change {value} {options} />
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import { onDestroy } from "svelte"
|
import { onDestroy } from "svelte"
|
||||||
|
|
||||||
export let label = ""
|
export let label = ""
|
||||||
|
export let labelHidden = false
|
||||||
export let componentInstance = {}
|
export let componentInstance = {}
|
||||||
export let control = null
|
export let control = null
|
||||||
export let key = ""
|
export let key = ""
|
||||||
|
@ -74,11 +75,13 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="property-control" class:highlighted={highlighted && nullishValue}>
|
<div
|
||||||
{#if type !== "boolean" && label}
|
class="property-control"
|
||||||
<div class="label">
|
class:wide={!label || labelHidden}
|
||||||
<Label>{label}</Label>
|
class:highlighted={highlighted && nullishValue}
|
||||||
</div>
|
>
|
||||||
|
{#if label && !labelHidden}
|
||||||
|
<Label size="M">{label}</Label>
|
||||||
{/if}
|
{/if}
|
||||||
<div id={`${key}-prop-control`} class="control">
|
<div id={`${key}-prop-control`} class="control">
|
||||||
<svelte:component
|
<svelte:component
|
||||||
|
@ -90,7 +93,6 @@
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
bindings={allBindings}
|
bindings={allBindings}
|
||||||
name={key}
|
name={key}
|
||||||
text={label}
|
|
||||||
{nested}
|
{nested}
|
||||||
{key}
|
{key}
|
||||||
{type}
|
{type}
|
||||||
|
@ -105,28 +107,34 @@
|
||||||
<style>
|
<style>
|
||||||
.property-control {
|
.property-control {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
grid-template-columns: 90px 1fr;
|
||||||
justify-content: flex-start;
|
align-items: center;
|
||||||
align-items: stretch;
|
|
||||||
transition: background 130ms ease-out, border-color 130ms ease-out;
|
transition: background 130ms ease-out, border-color 130ms ease-out;
|
||||||
border-left: 4px solid transparent;
|
border-left: 4px solid transparent;
|
||||||
margin: -6px calc(-1 * var(--spacing-xl));
|
margin: 0 calc(-1 * var(--spacing-xl));
|
||||||
padding: 6px var(--spacing-xl) 6px calc(var(--spacing-xl) - 4px);
|
padding: 0 var(--spacing-xl) 0 calc(var(--spacing-xl) - 4px);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.property-control :global(.spectrum-FieldLabel) {
|
||||||
|
white-space: normal;
|
||||||
}
|
}
|
||||||
.property-control.highlighted {
|
.property-control.highlighted {
|
||||||
background: var(--spectrum-global-color-gray-300);
|
background: var(--spectrum-global-color-gray-300);
|
||||||
border-color: var(--spectrum-global-color-blue-400);
|
border-color: var(--spectrum-global-color-static-red-600);
|
||||||
}
|
|
||||||
.label {
|
|
||||||
padding-bottom: var(--spectrum-global-dimension-size-65);
|
|
||||||
}
|
}
|
||||||
.control {
|
.control {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
.property-control.wide .control {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
.text {
|
.text {
|
||||||
margin-top: var(--spectrum-global-dimension-size-65);
|
|
||||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
font-size: var(--spectrum-global-dimension-font-size-75);
|
||||||
color: var(--grey-6);
|
color: var(--grey-6);
|
||||||
|
grid-column: 2 / 2;
|
||||||
|
}
|
||||||
|
.property-control.wide .text {
|
||||||
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { findClosestMatchingComponent } from "builderStore/componentUtils"
|
||||||
|
import {
|
||||||
|
getDatasourceForProvider,
|
||||||
|
getSchemaForDatasource,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
|
|
||||||
|
export const getComponentFieldOptions = (asset, id, type, loose = true) => {
|
||||||
|
const form = findClosestMatchingComponent(
|
||||||
|
asset,
|
||||||
|
id,
|
||||||
|
component => component._component === "@budibase/standard-components/form"
|
||||||
|
)
|
||||||
|
const datasource = getDatasourceForProvider(asset, form)
|
||||||
|
const schema = getSchemaForDatasource(asset, datasource, {
|
||||||
|
formSchema: true,
|
||||||
|
}).schema
|
||||||
|
|
||||||
|
// Get valid types for this field
|
||||||
|
let types = [type]
|
||||||
|
if (loose) {
|
||||||
|
if (type === "field/options" || type === "field/longform") {
|
||||||
|
// Allow options and longform to be used on string fields as well
|
||||||
|
types = [type, "field/string"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
types = types.map(type => type.slice(type.indexOf("/") + 1))
|
||||||
|
|
||||||
|
// Find fields of valid types
|
||||||
|
return Object.entries(schema || {})
|
||||||
|
.filter(entry => types.includes(entry[1].type))
|
||||||
|
.map(entry => entry[0])
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
<script>
|
||||||
|
import { organisation, auth } from "stores/portal"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
let loaded = false
|
||||||
|
|
||||||
|
$: platformTitleText = $organisation.platformTitle
|
||||||
|
$: platformTitle =
|
||||||
|
!$auth.user && platformTitleText ? platformTitleText : "Budibase"
|
||||||
|
|
||||||
|
$: faviconUrl = $organisation.faviconUrl || "https://i.imgur.com/Xhdt1YP.png"
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await organisation.init()
|
||||||
|
loaded = true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
In order to update the org elements, an update will have to be made to clear them.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{platformTitle}</title>
|
||||||
|
|
||||||
|
{#if loaded && !$auth.user && faviconUrl}
|
||||||
|
<link rel="icon" href={faviconUrl} />
|
||||||
|
{:else}
|
||||||
|
<!-- A default must be set or the browser defaults to favicon.ico behaviour -->
|
||||||
|
<link rel="icon" href={"https://i.imgur.com/Xhdt1YP.png"} />
|
||||||
|
{/if}
|
||||||
|
</svelte:head>
|
|
@ -4,29 +4,33 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { CookieUtils, Constants } from "@budibase/frontend-core"
|
import { CookieUtils, Constants } from "@budibase/frontend-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
import Branding from "./Branding.svelte"
|
||||||
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
|
||||||
$: multiTenancyEnabled = $admin.multiTenancy
|
$: multiTenancyEnabled = $admin.multiTenancy
|
||||||
$: hasAdminUser = $admin?.checklist?.adminUser?.checked
|
$: hasAdminUser = $admin?.checklist?.adminUser?.checked
|
||||||
|
$: baseUrl = $admin?.baseUrl
|
||||||
$: tenantSet = $auth.tenantSet
|
$: tenantSet = $auth.tenantSet
|
||||||
$: cloud = $admin.cloud
|
$: cloud = $admin?.cloud
|
||||||
$: user = $auth.user
|
$: user = $auth.user
|
||||||
|
|
||||||
$: useAccountPortal = cloud && !$admin.disableAccountPortal
|
$: useAccountPortal = cloud && !$admin.disableAccountPortal
|
||||||
|
|
||||||
const validateTenantId = async () => {
|
const validateTenantId = async () => {
|
||||||
const host = window.location.host
|
const host = window.location.host
|
||||||
if (host.includes("localhost:")) {
|
if (host.includes("localhost:") || !baseUrl) {
|
||||||
// ignore local dev
|
// ignore local dev
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// e.g. ['tenant', 'budibase', 'app'] vs ['budibase', 'app']
|
const mainHost = new URL(baseUrl).host
|
||||||
let urlTenantId
|
let urlTenantId
|
||||||
const hostParts = host.split(".")
|
// remove the main host part
|
||||||
if (hostParts.length > 2) {
|
const hostParts = host.split(mainHost).filter(part => part !== "")
|
||||||
urlTenantId = hostParts[0]
|
// if there is a part left, it has to be the tenant ID subdomain
|
||||||
|
if (hostParts.length === 1) {
|
||||||
|
urlTenantId = hostParts[0].replace(/\./g, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user && user.tenantId) {
|
if (user && user.tenantId) {
|
||||||
|
@ -40,13 +44,15 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.tenantId !== urlTenantId) {
|
if (urlTenantId && user.tenantId !== urlTenantId) {
|
||||||
// user should not be here - play it safe and log them out
|
// user should not be here - play it safe and log them out
|
||||||
try {
|
try {
|
||||||
await auth.logout()
|
await auth.logout()
|
||||||
await auth.setOrganisation(null)
|
await auth.setOrganisation(null)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Swallow error and do nothing
|
console.error(
|
||||||
|
`Tenant mis-match - "${urlTenantId}" and "${user.tenantId}" - logout`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -73,7 +79,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate tenant if in a multi-tenant env
|
// Validate tenant if in a multi-tenant env
|
||||||
if (useAccountPortal && multiTenancyEnabled) {
|
if (multiTenancyEnabled) {
|
||||||
await validateTenantId()
|
await validateTenantId()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -146,6 +152,9 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!--Portal branding overrides -->
|
||||||
|
<Branding />
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<slot />
|
<slot />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -220,6 +220,9 @@
|
||||||
} else if (type === "drop-new-component") {
|
} else if (type === "drop-new-component") {
|
||||||
const { component, parent, index } = data
|
const { component, parent, index } = data
|
||||||
await store.actions.components.create(component, null, parent, index)
|
await store.actions.components.create(component, null, parent, index)
|
||||||
|
} else if (type === "add-parent-component") {
|
||||||
|
const { componentId, parentType } = data
|
||||||
|
await store.actions.components.addParent(componentId, parentType)
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Client sent unknown event type: ${type}`)
|
console.warn(`Client sent unknown event type: ${type}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
|
|
||||||
{#if $selectedComponent}
|
{#if $selectedComponent}
|
||||||
{#key $selectedComponent._id}
|
{#key $selectedComponent._id}
|
||||||
<Panel {title} icon={componentDefinition?.icon} borderLeft>
|
<Panel {title} icon={componentDefinition?.icon} borderLeft wide>
|
||||||
<span slot="panel-header-content">
|
<span slot="panel-header-content">
|
||||||
<div class="settings-tabs">
|
<div class="settings-tabs">
|
||||||
{#each tabs as tab}
|
{#each tabs as tab}
|
||||||
|
|
|
@ -117,49 +117,52 @@
|
||||||
{#each sections as section, idx (section.name)}
|
{#each sections as section, idx (section.name)}
|
||||||
{#if section.visible}
|
{#if section.visible}
|
||||||
<DetailSummary name={section.name} collapsible={false}>
|
<DetailSummary name={section.name} collapsible={false}>
|
||||||
{#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen}
|
<div class="settings">
|
||||||
<PropertyControl
|
{#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen}
|
||||||
control={Input}
|
|
||||||
label="Name"
|
|
||||||
key="_instanceName"
|
|
||||||
value={componentInstance._instanceName}
|
|
||||||
onChange={val => updateSetting({ key: "_instanceName" }, val)}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{#each section.settings as setting (setting.key)}
|
|
||||||
{#if setting.visible}
|
|
||||||
<PropertyControl
|
<PropertyControl
|
||||||
type={setting.type}
|
control={Input}
|
||||||
control={getComponentForSetting(setting)}
|
label="Name"
|
||||||
label={setting.label}
|
key="_instanceName"
|
||||||
key={setting.key}
|
value={componentInstance._instanceName}
|
||||||
value={componentInstance[setting.key]}
|
onChange={val => updateSetting({ key: "_instanceName" }, val)}
|
||||||
defaultValue={setting.defaultValue}
|
|
||||||
nested={setting.nested}
|
|
||||||
onChange={val => updateSetting(setting, val)}
|
|
||||||
highlighted={$store.highlightedSettingKey === setting.key}
|
|
||||||
info={setting.info}
|
|
||||||
props={{
|
|
||||||
// Generic settings
|
|
||||||
placeholder: setting.placeholder || null,
|
|
||||||
|
|
||||||
// Select settings
|
|
||||||
options: setting.options || [],
|
|
||||||
|
|
||||||
// Number fields
|
|
||||||
min: setting.min ?? null,
|
|
||||||
max: setting.max ?? null,
|
|
||||||
}}
|
|
||||||
{bindings}
|
|
||||||
{componentBindings}
|
|
||||||
{componentInstance}
|
|
||||||
{componentDefinition}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{#each section.settings as setting (setting.key)}
|
||||||
{#if idx === 0 && componentDefinition?.component?.endsWith("/fieldgroup")}
|
{#if setting.visible}
|
||||||
<ResetFieldsButton {componentInstance} />
|
<PropertyControl
|
||||||
{/if}
|
type={setting.type}
|
||||||
|
control={getComponentForSetting(setting)}
|
||||||
|
label={setting.label}
|
||||||
|
labelHidden={setting.labelHidden}
|
||||||
|
key={setting.key}
|
||||||
|
value={componentInstance[setting.key]}
|
||||||
|
defaultValue={setting.defaultValue}
|
||||||
|
nested={setting.nested}
|
||||||
|
onChange={val => updateSetting(setting, val)}
|
||||||
|
highlighted={$store.highlightedSettingKey === setting.key}
|
||||||
|
info={setting.info}
|
||||||
|
props={{
|
||||||
|
// Generic settings
|
||||||
|
placeholder: setting.placeholder || null,
|
||||||
|
|
||||||
|
// Select settings
|
||||||
|
options: setting.options || [],
|
||||||
|
|
||||||
|
// Number fields
|
||||||
|
min: setting.min ?? null,
|
||||||
|
max: setting.max ?? null,
|
||||||
|
}}
|
||||||
|
{bindings}
|
||||||
|
{componentBindings}
|
||||||
|
{componentInstance}
|
||||||
|
{componentDefinition}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{#if idx === 0 && componentDefinition?.component?.endsWith("/fieldgroup")}
|
||||||
|
<ResetFieldsButton {componentInstance} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</DetailSummary>
|
</DetailSummary>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -168,3 +171,13 @@
|
||||||
<EjectBlockButton />
|
<EjectBlockButton />
|
||||||
</DetailSummary>
|
</DetailSummary>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
type: "text",
|
type: "text",
|
||||||
})
|
})
|
||||||
$: settingOptions = settings.map(setting => ({
|
$: settingOptions = settings.map(setting => ({
|
||||||
label: setting.label,
|
label: makeLabel(setting),
|
||||||
value: setting.key,
|
value: setting.key,
|
||||||
}))
|
}))
|
||||||
$: conditions.forEach(link => {
|
$: conditions.forEach(link => {
|
||||||
|
@ -71,6 +71,15 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const makeLabel = setting => {
|
||||||
|
const { section, label } = setting
|
||||||
|
if (section) {
|
||||||
|
return label ? `${section} - ${label}` : section
|
||||||
|
} else {
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getSettingDefinition = key => {
|
const getSettingDefinition = key => {
|
||||||
return settings.find(setting => setting.key === key)
|
return settings.find(setting => setting.key === key)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,6 @@
|
||||||
<StyleSection
|
<StyleSection
|
||||||
{style}
|
{style}
|
||||||
name={style.label}
|
name={style.label}
|
||||||
columns={style.columns}
|
|
||||||
properties={style.settings}
|
properties={style.settings}
|
||||||
{componentInstance}
|
{componentInstance}
|
||||||
{bindings}
|
{bindings}
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
|
|
||||||
export let name
|
export let name
|
||||||
export let columns
|
|
||||||
export let properties
|
export let properties
|
||||||
export let componentInstance
|
export let componentInstance
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
|
@ -34,27 +33,27 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DetailSummary collapsible={false} name={`${name}${changed ? " *" : ""}`}>
|
<DetailSummary collapsible={false} name={`${name}${changed ? " *" : ""}`}>
|
||||||
<div class="group-content" style="grid-template-columns: {columns || '1fr'}">
|
<div class="styles">
|
||||||
{#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)}
|
{#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)}
|
||||||
<div style="grid-column: {prop.column || 'auto'}">
|
<PropertyControl
|
||||||
<PropertyControl
|
label={`${prop.label}${hasPropChanged(style, prop) ? " *" : ""}`}
|
||||||
label={`${prop.label}${hasPropChanged(style, prop) ? " *" : ""}`}
|
control={prop.control}
|
||||||
control={prop.control}
|
key={prop.key}
|
||||||
key={prop.key}
|
value={style[prop.key]}
|
||||||
value={style[prop.key]}
|
onChange={val => updateStyle(prop.key, val)}
|
||||||
onChange={val => updateStyle(prop.key, val)}
|
props={getControlProps(prop)}
|
||||||
props={getControlProps(prop)}
|
{bindings}
|
||||||
{bindings}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</DetailSummary>
|
</DetailSummary>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.group-content {
|
.styles {
|
||||||
display: grid;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: var(--spacing-l);
|
gap: 8px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,7 +3,6 @@ import ColorPicker from "components/design/settings/controls/ColorPicker.svelte"
|
||||||
|
|
||||||
export const margin = {
|
export const margin = {
|
||||||
label: "Margin",
|
label: "Margin",
|
||||||
columns: "1fr 1fr",
|
|
||||||
settings: [
|
settings: [
|
||||||
{
|
{
|
||||||
label: "Top",
|
label: "Top",
|
||||||
|
@ -90,7 +89,6 @@ export const margin = {
|
||||||
|
|
||||||
export const padding = {
|
export const padding = {
|
||||||
label: "Padding",
|
label: "Padding",
|
||||||
columns: "1fr 1fr",
|
|
||||||
settings: [
|
settings: [
|
||||||
{
|
{
|
||||||
label: "Top",
|
label: "Top",
|
||||||
|
@ -177,7 +175,6 @@ export const padding = {
|
||||||
|
|
||||||
export const size = {
|
export const size = {
|
||||||
label: "Size",
|
label: "Size",
|
||||||
columns: "1fr 1fr",
|
|
||||||
settings: [
|
settings: [
|
||||||
{
|
{
|
||||||
label: "Width",
|
label: "Width",
|
||||||
|
@ -196,7 +193,6 @@ export const size = {
|
||||||
|
|
||||||
export const background = {
|
export const background = {
|
||||||
label: "Background",
|
label: "Background",
|
||||||
columns: "auto 1fr",
|
|
||||||
settings: [
|
settings: [
|
||||||
{
|
{
|
||||||
label: "Color",
|
label: "Color",
|
||||||
|
@ -285,7 +281,6 @@ export const background = {
|
||||||
|
|
||||||
export const border = {
|
export const border = {
|
||||||
label: "Border",
|
label: "Border",
|
||||||
columns: "1fr 1fr",
|
|
||||||
settings: [
|
settings: [
|
||||||
{
|
{
|
||||||
label: "Color",
|
label: "Color",
|
||||||
|
|
|
@ -1,22 +1,13 @@
|
||||||
<script>
|
<script>
|
||||||
import Panel from "components/design/Panel.svelte"
|
import Panel from "components/design/Panel.svelte"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import {
|
import { Layout, Search, Icon, Body, notifications } from "@budibase/bbui"
|
||||||
Layout,
|
|
||||||
ActionGroup,
|
|
||||||
ActionButton,
|
|
||||||
Search,
|
|
||||||
Icon,
|
|
||||||
Body,
|
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import structure from "./componentStructure.json"
|
import structure from "./componentStructure.json"
|
||||||
import { store, selectedComponent, selectedScreen } from "builderStore"
|
import { store, selectedComponent, selectedScreen } from "builderStore"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { fly } from "svelte/transition"
|
import { fly } from "svelte/transition"
|
||||||
import { findComponentPath } from "builderStore/componentUtils"
|
import { findComponentPath } from "builderStore/componentUtils"
|
||||||
|
|
||||||
let section = "components"
|
|
||||||
let searchString
|
let searchString
|
||||||
let searchRef
|
let searchRef
|
||||||
let selectedIndex
|
let selectedIndex
|
||||||
|
@ -37,7 +28,6 @@
|
||||||
allowedComponents,
|
allowedComponents,
|
||||||
searchString
|
searchString
|
||||||
)
|
)
|
||||||
$: blocks = enrichedStructure.find(x => x.name === "Blocks").children
|
|
||||||
$: orderMap = createComponentOrderMap(componentList)
|
$: orderMap = createComponentOrderMap(componentList)
|
||||||
|
|
||||||
const getAllowedComponents = (allComponents, screen, component) => {
|
const getAllowedComponents = (allComponents, screen, component) => {
|
||||||
|
@ -127,6 +117,11 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Swap blocks and plugins
|
||||||
|
let tmp = enrichedStructure[1]
|
||||||
|
enrichedStructure[1] = enrichedStructure[0]
|
||||||
|
enrichedStructure[0] = tmp
|
||||||
|
|
||||||
return enrichedStructure
|
return enrichedStructure
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,11 +132,6 @@
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove blocks if there is no search string
|
|
||||||
if (!search) {
|
|
||||||
structure = structure.filter(category => category.name !== "Blocks")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return only items which match the search string
|
// Return only items which match the search string
|
||||||
let filteredStructure = []
|
let filteredStructure = []
|
||||||
structure.forEach(category => {
|
structure.forEach(category => {
|
||||||
|
@ -225,6 +215,7 @@
|
||||||
showCloseButton
|
showCloseButton
|
||||||
onClickCloseButton={() => $goto("../")}
|
onClickCloseButton={() => $goto("../")}
|
||||||
borderLeft
|
borderLeft
|
||||||
|
wide
|
||||||
>
|
>
|
||||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||||
<Search
|
<Search
|
||||||
|
@ -233,64 +224,31 @@
|
||||||
on:change={e => (searchString = e.detail)}
|
on:change={e => (searchString = e.detail)}
|
||||||
bind:inputRef={searchRef}
|
bind:inputRef={searchRef}
|
||||||
/>
|
/>
|
||||||
{#if !searchString}
|
{#if filteredStructure.length}
|
||||||
<ActionGroup compact justified>
|
{#each filteredStructure as category}
|
||||||
<ActionButton
|
<Layout noPadding gap="XS">
|
||||||
fullWidth
|
<div class="category-label">{category.name}</div>
|
||||||
selected={section === "components"}
|
{#each category.children as component}
|
||||||
on:click={() => (section = "components")}>Components</ActionButton
|
<div
|
||||||
>
|
draggable="true"
|
||||||
<ActionButton
|
on:dragstart={() => onDragStart(component.component)}
|
||||||
fullWidth
|
on:dragend={onDragEnd}
|
||||||
selected={section === "blocks"}
|
class="component"
|
||||||
on:click={() => (section = "blocks")}>Blocks</ActionButton
|
class:selected={selectedIndex === orderMap[component.component]}
|
||||||
>
|
on:click={() => addComponent(component.component)}
|
||||||
</ActionGroup>
|
on:mouseover={() => (selectedIndex = null)}
|
||||||
{/if}
|
on:focus
|
||||||
{#if searchString || section === "components"}
|
>
|
||||||
{#if filteredStructure.length}
|
<Icon name={component.icon} />
|
||||||
{#each filteredStructure as category}
|
<Body size="XS">{component.name}</Body>
|
||||||
<Layout noPadding gap="XS">
|
</div>
|
||||||
<div class="category-label">{category.name}</div>
|
{/each}
|
||||||
{#each category.children as component}
|
</Layout>
|
||||||
<div
|
{/each}
|
||||||
draggable="true"
|
|
||||||
on:dragstart={() => onDragStart(component.component)}
|
|
||||||
on:dragend={onDragEnd}
|
|
||||||
class="component"
|
|
||||||
class:selected={selectedIndex ===
|
|
||||||
orderMap[component.component]}
|
|
||||||
on:click={() => addComponent(component.component)}
|
|
||||||
on:mouseover={() => (selectedIndex = null)}
|
|
||||||
on:focus
|
|
||||||
>
|
|
||||||
<Icon name={component.icon} />
|
|
||||||
<Body size="XS">{component.name}</Body>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</Layout>
|
|
||||||
{/each}
|
|
||||||
{:else}
|
|
||||||
<Body size="S">
|
|
||||||
There aren't any components matching the current filter
|
|
||||||
</Body>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
{:else}
|
||||||
<Body size="S">Blocks are collections of pre-built components</Body>
|
<Body size="S">
|
||||||
<Layout noPadding gap="XS">
|
There aren't any components matching the current filter
|
||||||
{#each blocks as block}
|
</Body>
|
||||||
<div
|
|
||||||
draggable="true"
|
|
||||||
class="component"
|
|
||||||
on:click={() => addComponent(block.component)}
|
|
||||||
on:dragstart={() => onDragStart(block.component)}
|
|
||||||
on:dragend={onDragEnd}
|
|
||||||
>
|
|
||||||
<Icon name={block.icon} />
|
|
||||||
<Body size="XS">{block.name}</Body>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</Layout>
|
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
async function login() {
|
async function login() {
|
||||||
form.validate()
|
form.validate()
|
||||||
if (Object.keys(errors).length > 0) {
|
if (Object.keys(errors).length > 0) {
|
||||||
console.log("errors")
|
console.log("errors", errors)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
@ -64,99 +64,106 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKeydown} />
|
<svelte:window on:keydown={handleKeydown} />
|
||||||
|
{#if loaded}
|
||||||
<TestimonialPage>
|
<TestimonialPage enabled={$organisation.testimonialsEnabled}>
|
||||||
<Layout gap="L" noPadding>
|
<Layout gap="L" noPadding>
|
||||||
<Layout justifyItems="center" noPadding>
|
<Layout justifyItems="center" noPadding>
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||||
{/if}
|
{/if}
|
||||||
<Heading size="M">Log in to Budibase</Heading>
|
<Heading size="M">
|
||||||
</Layout>
|
{$organisation.loginHeading || "Log in to Budibase"}
|
||||||
<Layout gap="S" noPadding>
|
</Heading>
|
||||||
{#if loaded && ($organisation.google || $organisation.oidc)}
|
</Layout>
|
||||||
<FancyForm>
|
<Layout gap="S" noPadding>
|
||||||
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
|
{#if loaded && ($organisation.google || $organisation.oidc)}
|
||||||
<GoogleButton />
|
<FancyForm>
|
||||||
</FancyForm>
|
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
|
||||||
{/if}
|
<GoogleButton />
|
||||||
|
</FancyForm>
|
||||||
|
{/if}
|
||||||
|
{#if !$organisation.isSSOEnforced}
|
||||||
|
<Divider />
|
||||||
|
<FancyForm bind:this={form}>
|
||||||
|
<FancyInput
|
||||||
|
label="Your work email"
|
||||||
|
value={formData.username}
|
||||||
|
on:change={e => {
|
||||||
|
formData = {
|
||||||
|
...formData,
|
||||||
|
username: e.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
let fieldError = {
|
||||||
|
username: !formData.username
|
||||||
|
? "Please enter a valid email"
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
errors = handleError({ ...errors, ...fieldError })
|
||||||
|
}}
|
||||||
|
error={errors.username}
|
||||||
|
/>
|
||||||
|
<FancyInput
|
||||||
|
label="Password"
|
||||||
|
value={formData.password}
|
||||||
|
type="password"
|
||||||
|
on:change={e => {
|
||||||
|
formData = {
|
||||||
|
...formData,
|
||||||
|
password: e.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
let fieldError = {
|
||||||
|
password: !formData.password
|
||||||
|
? "Please enter your password"
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
errors = handleError({ ...errors, ...fieldError })
|
||||||
|
}}
|
||||||
|
error={errors.password}
|
||||||
|
/>
|
||||||
|
</FancyForm>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
{#if !$organisation.isSSOEnforced}
|
{#if !$organisation.isSSOEnforced}
|
||||||
<Divider />
|
<Layout gap="XS" noPadding justifyItems="center">
|
||||||
<FancyForm bind:this={form}>
|
<Button
|
||||||
<FancyInput
|
size="L"
|
||||||
label="Your work email"
|
cta
|
||||||
value={formData.username}
|
disabled={Object.keys(errors).length > 0}
|
||||||
on:change={e => {
|
on:click={login}
|
||||||
formData = {
|
>
|
||||||
...formData,
|
{$organisation.loginButton || `Log in to ${company}`}
|
||||||
username: e.detail,
|
</Button>
|
||||||
}
|
</Layout>
|
||||||
}}
|
<Layout gap="XS" noPadding justifyItems="center">
|
||||||
validate={() => {
|
<div class="user-actions">
|
||||||
let fieldError = {
|
<ActionButton size="L" quiet on:click={() => $goto("./forgot")}>
|
||||||
username: !formData.username
|
Forgot password?
|
||||||
? "Please enter a valid email"
|
</ActionButton>
|
||||||
: undefined,
|
</div>
|
||||||
}
|
</Layout>
|
||||||
errors = handleError({ ...errors, ...fieldError })
|
{/if}
|
||||||
}}
|
|
||||||
error={errors.username}
|
{#if cloud}
|
||||||
/>
|
<Body size="xs" textAlign="center">
|
||||||
<FancyInput
|
By using Budibase Cloud
|
||||||
label="Password"
|
<br />
|
||||||
value={formData.password}
|
you are agreeing to our
|
||||||
type="password"
|
<Link
|
||||||
on:change={e => {
|
href="https://budibase.com/eula"
|
||||||
formData = {
|
target="_blank"
|
||||||
...formData,
|
secondary={true}
|
||||||
password: e.detail,
|
>
|
||||||
}
|
License Agreement
|
||||||
}}
|
</Link>
|
||||||
validate={() => {
|
</Body>
|
||||||
let fieldError = {
|
|
||||||
password: !formData.password
|
|
||||||
? "Please enter your password"
|
|
||||||
: undefined,
|
|
||||||
}
|
|
||||||
errors = handleError({ ...errors, ...fieldError })
|
|
||||||
}}
|
|
||||||
error={errors.password}
|
|
||||||
/>
|
|
||||||
</FancyForm>
|
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
{#if !$organisation.isSSOEnforced}
|
</TestimonialPage>
|
||||||
<Layout gap="XS" noPadding justifyItems="center">
|
{/if}
|
||||||
<Button
|
|
||||||
size="L"
|
|
||||||
cta
|
|
||||||
disabled={Object.keys(errors).length > 0}
|
|
||||||
on:click={login}
|
|
||||||
>
|
|
||||||
Log in to {company}
|
|
||||||
</Button>
|
|
||||||
</Layout>
|
|
||||||
<Layout gap="XS" noPadding justifyItems="center">
|
|
||||||
<div class="user-actions">
|
|
||||||
<ActionButton size="L" quiet on:click={() => $goto("./forgot")}>
|
|
||||||
Forgot password?
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if cloud}
|
|
||||||
<Body size="xs" textAlign="center">
|
|
||||||
By using Budibase Cloud
|
|
||||||
<br />
|
|
||||||
you are agreeing to our
|
|
||||||
<Link href="https://budibase.com/eula" target="_blank" secondary={true}>
|
|
||||||
License Agreement
|
|
||||||
</Link>
|
|
||||||
</Body>
|
|
||||||
{/if}
|
|
||||||
</Layout>
|
|
||||||
</TestimonialPage>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.user-actions {
|
.user-actions {
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, FancyForm, FancyInput, FancyCheckbox } from "@budibase/bbui"
|
import { Button, FancyForm, FancyInput, FancyCheckbox } from "@budibase/bbui"
|
||||||
|
import GoogleButton from "components/backend/DatasourceNavigator/_components/GoogleButton.svelte"
|
||||||
import { capitalise } from "helpers/helpers"
|
import { capitalise } from "helpers/helpers"
|
||||||
import PanelHeader from "./PanelHeader.svelte"
|
import PanelHeader from "./PanelHeader.svelte"
|
||||||
|
import { helpers } from "@budibase/shared-core"
|
||||||
|
|
||||||
export let title = ""
|
export let title = ""
|
||||||
export let onBack = null
|
export let onBack = null
|
||||||
export let onNext = () => {}
|
export let onNext = () => {}
|
||||||
export let fields = {}
|
export let fields = {}
|
||||||
|
export let type = ""
|
||||||
|
|
||||||
let errors = {}
|
let errors = {}
|
||||||
|
|
||||||
|
@ -57,8 +60,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
$: isValid = getIsValid(fields, errors, values)
|
$: isValid = getIsValid(fields, errors, values)
|
||||||
|
$: isGoogle = helpers.isGoogleSheets(type)
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = async () => {
|
||||||
const parsedValues = {}
|
const parsedValues = {}
|
||||||
|
|
||||||
Object.entries(values).forEach(([name, value]) => {
|
Object.entries(values).forEach(([name, value]) => {
|
||||||
|
@ -69,7 +73,10 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return onNext(parsedValues)
|
if (isGoogle) {
|
||||||
|
parsedValues.isGoogle = isGoogle
|
||||||
|
}
|
||||||
|
return await onNext(parsedValues)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -99,7 +106,11 @@
|
||||||
{/each}
|
{/each}
|
||||||
</FancyForm>
|
</FancyForm>
|
||||||
</div>
|
</div>
|
||||||
<Button cta disabled={!isValid} on:click={handleNext}>Connect</Button>
|
{#if isGoogle}
|
||||||
|
<GoogleButton disabled={!isValid} preAuthStep={handleNext} />
|
||||||
|
{:else}
|
||||||
|
<Button cta disabled={!isValid} on:click={handleNext}>Connect</Button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -4,19 +4,20 @@
|
||||||
import DataPanel from "./_components/DataPanel.svelte"
|
import DataPanel from "./_components/DataPanel.svelte"
|
||||||
import DatasourceConfigPanel from "./_components/DatasourceConfigPanel.svelte"
|
import DatasourceConfigPanel from "./_components/DatasourceConfigPanel.svelte"
|
||||||
import ExampleApp from "./_components/ExampleApp.svelte"
|
import ExampleApp from "./_components/ExampleApp.svelte"
|
||||||
import { FancyButton, notifications, Modal } from "@budibase/bbui"
|
import { FancyButton, notifications, Modal, Body } from "@budibase/bbui"
|
||||||
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
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 { saveDatasource } from "builderStore/datasource"
|
||||||
import { integrations } from "stores/backend"
|
import { integrations } from "stores/backend"
|
||||||
import { auth, admin } from "stores/portal"
|
import { auth, admin, organisation } from "stores/portal"
|
||||||
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
||||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.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 Spinner from "components/common/Spinner.svelte"
|
||||||
|
import { helpers } from "@budibase/shared-core"
|
||||||
|
|
||||||
let name = "My first app"
|
let name = "My first app"
|
||||||
let url = "my-first-app"
|
let url = "my-first-app"
|
||||||
|
@ -25,10 +26,11 @@
|
||||||
|
|
||||||
let plusIntegrations = {}
|
let plusIntegrations = {}
|
||||||
let integrationsLoading = true
|
let integrationsLoading = true
|
||||||
$: getIntegrations()
|
|
||||||
let creationLoading = false
|
let creationLoading = false
|
||||||
|
|
||||||
let uploadModal
|
let uploadModal
|
||||||
|
let googleComplete = false
|
||||||
|
|
||||||
|
$: getIntegrations()
|
||||||
|
|
||||||
const createApp = async useSampleData => {
|
const createApp = async useSampleData => {
|
||||||
creationLoading = true
|
creationLoading = true
|
||||||
|
@ -62,6 +64,7 @@
|
||||||
await store.actions.screens.save(defaultScreenTemplate)
|
await store.actions.screens.save(defaultScreenTemplate)
|
||||||
|
|
||||||
appId = createdApp.instance._id
|
appId = createdApp.instance._id
|
||||||
|
return createdApp
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
creationLoading = false
|
creationLoading = false
|
||||||
throw e
|
throw e
|
||||||
|
@ -74,6 +77,13 @@
|
||||||
const newPlusIntegrations = {}
|
const newPlusIntegrations = {}
|
||||||
|
|
||||||
Object.entries($integrations).forEach(([integrationType, schema]) => {
|
Object.entries($integrations).forEach(([integrationType, schema]) => {
|
||||||
|
// google sheets not available in self-host
|
||||||
|
if (
|
||||||
|
helpers.isGoogleSheets(integrationType) &&
|
||||||
|
!$organisation.googleDatasourceConfigured
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (schema?.plus) {
|
if (schema?.plus) {
|
||||||
newPlusIntegrations[integrationType] = schema
|
newPlusIntegrations[integrationType] = schema
|
||||||
}
|
}
|
||||||
|
@ -92,12 +102,17 @@
|
||||||
notifications.success(`App created successfully`)
|
notifications.success(`App created successfully`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCreateApp = async ({ datasourceConfig, useSampleData }) => {
|
const handleCreateApp = async ({
|
||||||
|
datasourceConfig,
|
||||||
|
useSampleData,
|
||||||
|
isGoogle,
|
||||||
|
}) => {
|
||||||
try {
|
try {
|
||||||
await createApp(useSampleData)
|
const app = await createApp(useSampleData)
|
||||||
|
|
||||||
|
let datasource
|
||||||
if (datasourceConfig) {
|
if (datasourceConfig) {
|
||||||
await saveDatasource({
|
datasource = await saveDatasource({
|
||||||
plus: true,
|
plus: true,
|
||||||
auth: undefined,
|
auth: undefined,
|
||||||
name: plusIntegrations[stage].friendlyName,
|
name: plusIntegrations[stage].friendlyName,
|
||||||
|
@ -107,7 +122,14 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
goToApp()
|
store.set()
|
||||||
|
|
||||||
|
if (isGoogle) {
|
||||||
|
googleComplete = true
|
||||||
|
return { datasource, appId: app.appId }
|
||||||
|
} else {
|
||||||
|
goToApp()
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
creationLoading = false
|
creationLoading = false
|
||||||
|
@ -127,8 +149,15 @@
|
||||||
<SplitPage>
|
<SplitPage>
|
||||||
{#if stage === "name"}
|
{#if stage === "name"}
|
||||||
<NamePanel bind:name bind:url onNext={() => (stage = "data")} />
|
<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}
|
{:else if integrationsLoading || creationLoading}
|
||||||
<div class="spinner">
|
<div class="centered">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
{:else if stage === "data"}
|
{:else if stage === "data"}
|
||||||
|
@ -174,8 +203,13 @@
|
||||||
<DatasourceConfigPanel
|
<DatasourceConfigPanel
|
||||||
title={plusIntegrations[stage].friendlyName}
|
title={plusIntegrations[stage].friendlyName}
|
||||||
fields={plusIntegrations[stage].datasource}
|
fields={plusIntegrations[stage].datasource}
|
||||||
|
type={stage}
|
||||||
onBack={() => (stage = "data")}
|
onBack={() => (stage = "data")}
|
||||||
onNext={data => handleCreateApp({ datasourceConfig: data })}
|
onNext={data => {
|
||||||
|
const isGoogle = data.isGoogle
|
||||||
|
delete data.isGoogle
|
||||||
|
return handleCreateApp({ datasourceConfig: data, isGoogle })
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<p>There was an problem. Please refresh the page and try again.</p>
|
<p>There was an problem. Please refresh the page and try again.</p>
|
||||||
|
@ -186,7 +220,7 @@
|
||||||
</SplitPage>
|
</SplitPage>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.spinner {
|
.centered {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -0,0 +1,446 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Divider,
|
||||||
|
File,
|
||||||
|
notifications,
|
||||||
|
Tags,
|
||||||
|
Tag,
|
||||||
|
Button,
|
||||||
|
Toggle,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
TextArea,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { auth, organisation, licensing, admin } from "stores/portal"
|
||||||
|
import { API } from "api"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
|
const imageExtensions = [
|
||||||
|
".png",
|
||||||
|
".tiff",
|
||||||
|
".gif",
|
||||||
|
".raw",
|
||||||
|
".jpg",
|
||||||
|
".jpeg",
|
||||||
|
".svg",
|
||||||
|
".bmp",
|
||||||
|
".jfif",
|
||||||
|
]
|
||||||
|
|
||||||
|
const faviconExtensions = [".png", ".ico", ".gif"]
|
||||||
|
|
||||||
|
let mounted = false
|
||||||
|
let saving = false
|
||||||
|
|
||||||
|
let logoFile = null
|
||||||
|
let logoPreview = null
|
||||||
|
let faviconFile = null
|
||||||
|
let faviconPreview = null
|
||||||
|
|
||||||
|
let config = {}
|
||||||
|
let updated = false
|
||||||
|
|
||||||
|
$: onConfigUpdate(config, mounted)
|
||||||
|
$: init = Object.keys(config).length > 0
|
||||||
|
|
||||||
|
$: isCloud = $admin.cloud
|
||||||
|
$: brandingEnabled = $licensing.brandingEnabled
|
||||||
|
|
||||||
|
const onConfigUpdate = () => {
|
||||||
|
if (!mounted || updated || !init) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
$: logo = config.logoUrl
|
||||||
|
? { url: config.logoUrl, type: "image", name: "Logo" }
|
||||||
|
: null
|
||||||
|
|
||||||
|
$: favicon = config.faviconUrl
|
||||||
|
? { url: config.faviconUrl, type: "image", name: "Favicon" }
|
||||||
|
: null
|
||||||
|
|
||||||
|
const previewUrl = async localFile => {
|
||||||
|
if (!localFile) {
|
||||||
|
return Promise.resolve(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
let reader = new FileReader()
|
||||||
|
try {
|
||||||
|
reader.onload = e => {
|
||||||
|
resolve({
|
||||||
|
result: e.target.result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(localFile)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
resolve(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$: previewUrl(logoFile).then(response => {
|
||||||
|
if (response) {
|
||||||
|
logoPreview = response.result
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$: previewUrl(faviconFile).then(response => {
|
||||||
|
if (response) {
|
||||||
|
faviconPreview = response.result
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function uploadLogo(file) {
|
||||||
|
let response = {}
|
||||||
|
try {
|
||||||
|
let data = new FormData()
|
||||||
|
data.append("file", file)
|
||||||
|
response = await API.uploadLogo(data)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error uploading logo")
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFavicon(file) {
|
||||||
|
let response = {}
|
||||||
|
try {
|
||||||
|
let data = new FormData()
|
||||||
|
data.append("file", file)
|
||||||
|
response = await API.uploadFavicon(data)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error uploading favicon")
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveConfig() {
|
||||||
|
saving = true
|
||||||
|
|
||||||
|
if (logoFile) {
|
||||||
|
const logoResp = await uploadLogo(logoFile)
|
||||||
|
if (logoResp.url) {
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
logoUrl: logoResp.url,
|
||||||
|
}
|
||||||
|
logoFile = null
|
||||||
|
logoPreview = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faviconFile) {
|
||||||
|
const faviconResp = await uploadFavicon(faviconFile)
|
||||||
|
if (faviconResp.url) {
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
faviconUrl: faviconResp.url,
|
||||||
|
}
|
||||||
|
faviconFile = null
|
||||||
|
faviconPreview = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim
|
||||||
|
const userStrings = [
|
||||||
|
"metaTitle",
|
||||||
|
"platformTitle",
|
||||||
|
"loginButton",
|
||||||
|
"loginHeading",
|
||||||
|
"metaDescription",
|
||||||
|
"metaImageUrl",
|
||||||
|
]
|
||||||
|
|
||||||
|
const trimmed = userStrings.reduce((acc, fieldName) => {
|
||||||
|
acc[fieldName] = config[fieldName] ? config[fieldName].trim() : undefined
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
...trimmed,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update settings
|
||||||
|
await organisation.save(config)
|
||||||
|
await organisation.init()
|
||||||
|
notifications.success("Branding settings updated")
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Branding updated failed", e)
|
||||||
|
notifications.error("Branding updated failed")
|
||||||
|
}
|
||||||
|
updated = false
|
||||||
|
saving = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await organisation.init()
|
||||||
|
|
||||||
|
config = {
|
||||||
|
faviconUrl: $organisation.faviconUrl,
|
||||||
|
logoUrl: $organisation.logoUrl,
|
||||||
|
platformTitle: $organisation.platformTitle,
|
||||||
|
emailBrandingEnabled: $organisation.emailBrandingEnabled,
|
||||||
|
loginHeading: $organisation.loginHeading,
|
||||||
|
loginButton: $organisation.loginButton,
|
||||||
|
testimonialsEnabled: $organisation.testimonialsEnabled,
|
||||||
|
metaDescription: $organisation.metaDescription,
|
||||||
|
metaImageUrl: $organisation.metaImageUrl,
|
||||||
|
metaTitle: $organisation.metaTitle,
|
||||||
|
}
|
||||||
|
mounted = true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $auth.isAdmin && mounted}
|
||||||
|
<Layout noPadding>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<div class="title">
|
||||||
|
<Heading size="M">Branding</Heading>
|
||||||
|
{#if !isCloud && !brandingEnabled}
|
||||||
|
<Tags>
|
||||||
|
<Tag icon="LockClosed">Business</Tag>
|
||||||
|
</Tags>
|
||||||
|
{/if}
|
||||||
|
{#if isCloud && !brandingEnabled}
|
||||||
|
<Tags>
|
||||||
|
<Tag icon="LockClosed">Pro</Tag>
|
||||||
|
</Tags>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Body>Remove all Budibase branding and use your own.</Body>
|
||||||
|
</Layout>
|
||||||
|
<Divider />
|
||||||
|
<div class="branding fields">
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Logo</Label>
|
||||||
|
<File
|
||||||
|
title="Upload image"
|
||||||
|
handleFileTooLarge={() => {
|
||||||
|
notifications.warn("File too large. 20mb limit")
|
||||||
|
}}
|
||||||
|
extensions={imageExtensions}
|
||||||
|
previewUrl={logoPreview || logo?.url}
|
||||||
|
on:change={e => {
|
||||||
|
let clone = { ...config }
|
||||||
|
if (e.detail) {
|
||||||
|
logoFile = e.detail
|
||||||
|
logoPreview = null
|
||||||
|
} else {
|
||||||
|
logoFile = null
|
||||||
|
clone.logoUrl = ""
|
||||||
|
}
|
||||||
|
config = clone
|
||||||
|
}}
|
||||||
|
value={logoFile || logo}
|
||||||
|
disabled={!brandingEnabled || saving}
|
||||||
|
allowClear={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Favicon</Label>
|
||||||
|
<File
|
||||||
|
title="Upload image"
|
||||||
|
handleFileTooLarge={() => {
|
||||||
|
notifications.warn("File too large. 20mb limit")
|
||||||
|
}}
|
||||||
|
extensions={faviconExtensions}
|
||||||
|
previewUrl={faviconPreview || favicon?.url}
|
||||||
|
on:change={e => {
|
||||||
|
let clone = { ...config }
|
||||||
|
if (e.detail) {
|
||||||
|
faviconFile = e.detail
|
||||||
|
faviconPreview = null
|
||||||
|
} else {
|
||||||
|
clone.faviconUrl = ""
|
||||||
|
}
|
||||||
|
config = clone
|
||||||
|
}}
|
||||||
|
value={faviconFile || favicon}
|
||||||
|
disabled={!brandingEnabled || saving}
|
||||||
|
allowClear={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if !isCloud}
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Title</Label>
|
||||||
|
<Input
|
||||||
|
on:change={e => {
|
||||||
|
let clone = { ...config }
|
||||||
|
clone.platformTitle = e.detail ? e.detail : ""
|
||||||
|
config = clone
|
||||||
|
}}
|
||||||
|
value={config.platformTitle || ""}
|
||||||
|
disabled={!brandingEnabled || saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<Toggle
|
||||||
|
text={"Remove Budibase brand from emails"}
|
||||||
|
on:change={e => {
|
||||||
|
let clone = { ...config }
|
||||||
|
clone.emailBrandingEnabled = !e.detail
|
||||||
|
config = clone
|
||||||
|
}}
|
||||||
|
value={!config.emailBrandingEnabled}
|
||||||
|
disabled={!brandingEnabled || saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !isCloud}
|
||||||
|
<Divider />
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading size="S">Login page</Heading>
|
||||||
|
<Body />
|
||||||
|
</Layout>
|
||||||
|
<div class="login">
|
||||||
|
<div class="fields">
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Header</Label>
|
||||||
|
<Input
|
||||||
|
on:change={e => {
|
||||||
|
let clone = { ...config }
|
||||||
|
clone.loginHeading = e.detail ? e.detail : ""
|
||||||
|
config = clone
|
||||||
|
}}
|
||||||
|
value={config.loginHeading || ""}
|
||||||
|
disabled={!brandingEnabled || saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Button</Label>
|
||||||
|
<Input
|
||||||
|
on:change={e => {
|
||||||
|
let clone = { ...config }
|
||||||
|
clone.loginButton = e.detail ? e.detail : ""
|
||||||
|
config = clone
|
||||||
|
}}
|
||||||
|
value={config.loginButton || ""}
|
||||||
|
disabled={!brandingEnabled || saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Toggle
|
||||||
|
text={"Remove customer testimonials"}
|
||||||
|
on:change={e => {
|
||||||
|
let clone = { ...config }
|
||||||
|
clone.testimonialsEnabled = !e.detail
|
||||||
|
config = clone
|
||||||
|
}}
|
||||||
|
value={!config.testimonialsEnabled}
|
||||||
|
disabled={!brandingEnabled || saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<Divider />
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading size="S">Application previews</Heading>
|
||||||
|
<Body>Customise the meta tags on your app preview</Body>
|
||||||
|
</Layout>
|
||||||
|
<div class="app-previews">
|
||||||
|
<div class="fields">
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Image URL</Label>
|
||||||
|
<Input
|
||||||
|
on:change={e => {
|
||||||
|
let clone = { ...config }
|
||||||
|
clone.metaImageUrl = e.detail ? e.detail : ""
|
||||||
|
config = clone
|
||||||
|
}}
|
||||||
|
value={config.metaImageUrl}
|
||||||
|
disabled={!brandingEnabled || saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Title</Label>
|
||||||
|
<Input
|
||||||
|
on:change={e => {
|
||||||
|
let clone = { ...config }
|
||||||
|
clone.metaTitle = e.detail ? e.detail : ""
|
||||||
|
config = clone
|
||||||
|
}}
|
||||||
|
value={config.metaTitle}
|
||||||
|
disabled={!brandingEnabled || saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Description</Label>
|
||||||
|
<TextArea
|
||||||
|
on:change={e => {
|
||||||
|
let clone = { ...config }
|
||||||
|
clone.metaDescription = e.detail ? e.detail : ""
|
||||||
|
config = clone
|
||||||
|
}}
|
||||||
|
value={config.metaDescription}
|
||||||
|
disabled={!brandingEnabled || saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
{#if !brandingEnabled}
|
||||||
|
<Button
|
||||||
|
on:click={() => {
|
||||||
|
if (isCloud && $auth?.user?.accountPortalAccess) {
|
||||||
|
window.open($admin.accountPortalUrl + "/portal/upgrade", "_blank")
|
||||||
|
} else if ($auth.isAdmin) {
|
||||||
|
$goto("/builder/portal/account/upgrade")
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
secondary
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
Upgrade
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<Button on:click={saveConfig} cta disabled={saving || !updated || !init}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.branding,
|
||||||
|
.login {
|
||||||
|
width: 70%;
|
||||||
|
max-width: 70%;
|
||||||
|
}
|
||||||
|
.fields {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.field {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 80px auto;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -7,12 +7,10 @@
|
||||||
Divider,
|
Divider,
|
||||||
Label,
|
Label,
|
||||||
Input,
|
Input,
|
||||||
Dropzone,
|
|
||||||
notifications,
|
notifications,
|
||||||
Toggle,
|
Toggle,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { auth, organisation, admin } from "stores/portal"
|
import { auth, organisation, admin } from "stores/portal"
|
||||||
import { API } from "api"
|
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
import { redirect } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
|
|
||||||
|
@ -28,32 +26,14 @@
|
||||||
company: $organisation.company,
|
company: $organisation.company,
|
||||||
platformUrl: $organisation.platformUrl,
|
platformUrl: $organisation.platformUrl,
|
||||||
analyticsEnabled: $organisation.analyticsEnabled,
|
analyticsEnabled: $organisation.analyticsEnabled,
|
||||||
logo: $organisation.logoUrl
|
|
||||||
? { url: $organisation.logoUrl, type: "image", name: "Logo" }
|
|
||||||
: null,
|
|
||||||
})
|
})
|
||||||
let loading = false
|
|
||||||
|
|
||||||
async function uploadLogo(file) {
|
let loading = false
|
||||||
try {
|
|
||||||
let data = new FormData()
|
|
||||||
data.append("file", file)
|
|
||||||
await API.uploadLogo(data)
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error uploading logo")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveConfig() {
|
async function saveConfig() {
|
||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Upload logo if required
|
|
||||||
if ($values.logo && !$values.logo.url) {
|
|
||||||
await uploadLogo($values.logo)
|
|
||||||
await organisation.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
isSSOEnforced: $values.isSSOEnforced,
|
isSSOEnforced: $values.isSSOEnforced,
|
||||||
company: $values.company ?? "",
|
company: $values.company ?? "",
|
||||||
|
@ -61,11 +41,6 @@
|
||||||
analyticsEnabled: $values.analyticsEnabled,
|
analyticsEnabled: $values.analyticsEnabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove logo if required
|
|
||||||
if (!$values.logo) {
|
|
||||||
config.logoUrl = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update settings
|
// Update settings
|
||||||
await organisation.save(config)
|
await organisation.save(config)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -87,21 +62,7 @@
|
||||||
<Label size="L">Org. name</Label>
|
<Label size="L">Org. name</Label>
|
||||||
<Input thin bind:value={$values.company} />
|
<Input thin bind:value={$values.company} />
|
||||||
</div>
|
</div>
|
||||||
<div class="field logo">
|
|
||||||
<Label size="L">Logo</Label>
|
|
||||||
<div class="file">
|
|
||||||
<Dropzone
|
|
||||||
value={[$values.logo]}
|
|
||||||
on:change={e => {
|
|
||||||
if (!e.detail || e.detail.length === 0) {
|
|
||||||
$values.logo = null
|
|
||||||
} else {
|
|
||||||
$values.logo = e.detail[0]
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if !$admin.cloud}
|
{#if !$admin.cloud}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<Label
|
<Label
|
||||||
|
@ -137,10 +98,4 @@
|
||||||
grid-gap: var(--spacing-l);
|
grid-gap: var(--spacing-l);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.file {
|
|
||||||
max-width: 30ch;
|
|
||||||
}
|
|
||||||
.logo {
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -22,6 +22,18 @@ 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,
|
||||||
|
@ -126,6 +138,7 @@ export function createTablesStore() {
|
||||||
return {
|
return {
|
||||||
subscribe: derivedStore.subscribe,
|
subscribe: derivedStore.subscribe,
|
||||||
fetch,
|
fetch,
|
||||||
|
fetchTable,
|
||||||
init: fetch,
|
init: fetch,
|
||||||
select,
|
select,
|
||||||
save,
|
save,
|
||||||
|
|
|
@ -53,6 +53,7 @@ export function createAdminStore() {
|
||||||
store.disableAccountPortal = environment.disableAccountPortal
|
store.disableAccountPortal = environment.disableAccountPortal
|
||||||
store.accountPortalUrl = environment.accountPortalUrl
|
store.accountPortalUrl = environment.accountPortalUrl
|
||||||
store.isDev = environment.isDev
|
store.isDev = environment.isDev
|
||||||
|
store.baseUrl = environment.baseUrl
|
||||||
return store
|
return store
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,9 +13,11 @@ export const createLicensingStore = () => {
|
||||||
license: undefined,
|
license: undefined,
|
||||||
isFreePlan: true,
|
isFreePlan: true,
|
||||||
isEnterprisePlan: true,
|
isEnterprisePlan: true,
|
||||||
|
isBusinessPlan: true,
|
||||||
// features
|
// features
|
||||||
groupsEnabled: false,
|
groupsEnabled: false,
|
||||||
backupsEnabled: false,
|
backupsEnabled: false,
|
||||||
|
brandingEnabled: false,
|
||||||
// the currently used quotas from the db
|
// the currently used quotas from the db
|
||||||
quotaUsage: undefined,
|
quotaUsage: undefined,
|
||||||
// derived quota metrics for percentages used
|
// derived quota metrics for percentages used
|
||||||
|
@ -57,6 +59,7 @@ export const createLicensingStore = () => {
|
||||||
const planType = license?.plan.type
|
const planType = license?.plan.type
|
||||||
const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE
|
const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE
|
||||||
const isFreePlan = planType === Constants.PlanType.FREE
|
const isFreePlan = planType === Constants.PlanType.FREE
|
||||||
|
const isBusinessPlan = planType === Constants.PlanType.BUSINESS
|
||||||
const groupsEnabled = license.features.includes(
|
const groupsEnabled = license.features.includes(
|
||||||
Constants.Features.USER_GROUPS
|
Constants.Features.USER_GROUPS
|
||||||
)
|
)
|
||||||
|
@ -69,7 +72,9 @@ export const createLicensingStore = () => {
|
||||||
const enforceableSSO = license.features.includes(
|
const enforceableSSO = license.features.includes(
|
||||||
Constants.Features.ENFORCEABLE_SSO
|
Constants.Features.ENFORCEABLE_SSO
|
||||||
)
|
)
|
||||||
|
const brandingEnabled = license.features.includes(
|
||||||
|
Constants.Features.BRANDING
|
||||||
|
)
|
||||||
const auditLogsEnabled = license.features.includes(
|
const auditLogsEnabled = license.features.includes(
|
||||||
Constants.Features.AUDIT_LOGS
|
Constants.Features.AUDIT_LOGS
|
||||||
)
|
)
|
||||||
|
@ -79,8 +84,10 @@ export const createLicensingStore = () => {
|
||||||
license,
|
license,
|
||||||
isEnterprisePlan,
|
isEnterprisePlan,
|
||||||
isFreePlan,
|
isFreePlan,
|
||||||
|
isBusinessPlan,
|
||||||
groupsEnabled,
|
groupsEnabled,
|
||||||
backupsEnabled,
|
backupsEnabled,
|
||||||
|
brandingEnabled,
|
||||||
environmentVariablesEnabled,
|
environmentVariablesEnabled,
|
||||||
auditLogsEnabled,
|
auditLogsEnabled,
|
||||||
enforceableSSO,
|
enforceableSSO,
|
||||||
|
|
|
@ -50,6 +50,10 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
||||||
title: "Organisation",
|
title: "Organisation",
|
||||||
href: "/builder/portal/settings/organisation",
|
href: "/builder/portal/settings/organisation",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Branding",
|
||||||
|
href: "/builder/portal/settings/branding",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Environment",
|
title: "Environment",
|
||||||
href: "/builder/portal/settings/environment",
|
href: "/builder/portal/settings/environment",
|
||||||
|
|
|
@ -6,10 +6,20 @@ import _ from "lodash"
|
||||||
const DEFAULT_CONFIG = {
|
const DEFAULT_CONFIG = {
|
||||||
platformUrl: "",
|
platformUrl: "",
|
||||||
logoUrl: undefined,
|
logoUrl: undefined,
|
||||||
|
faviconUrl: undefined,
|
||||||
|
emailBrandingEnabled: true,
|
||||||
|
testimonialsEnabled: true,
|
||||||
|
platformTitle: "Budibase",
|
||||||
|
loginHeading: undefined,
|
||||||
|
loginButton: undefined,
|
||||||
|
metaDescription: undefined,
|
||||||
|
metaImageUrl: undefined,
|
||||||
|
metaTitle: undefined,
|
||||||
docsUrl: undefined,
|
docsUrl: undefined,
|
||||||
company: "Budibase",
|
company: "Budibase",
|
||||||
oidc: undefined,
|
oidc: undefined,
|
||||||
google: undefined,
|
google: undefined,
|
||||||
|
googleDatasourceConfigured: undefined,
|
||||||
oidcCallbackUrl: "",
|
oidcCallbackUrl: "",
|
||||||
googleCallbackUrl: "",
|
googleCallbackUrl: "",
|
||||||
isSSOEnforced: false,
|
isSSOEnforced: false,
|
||||||
|
@ -30,6 +40,7 @@ export function createOrganisationStore() {
|
||||||
const storeConfig = _.cloneDeep(get(store))
|
const storeConfig = _.cloneDeep(get(store))
|
||||||
delete storeConfig.oidc
|
delete storeConfig.oidc
|
||||||
delete storeConfig.google
|
delete storeConfig.google
|
||||||
|
delete storeConfig.googleDatasourceConfigured
|
||||||
delete storeConfig.oidcCallbackUrl
|
delete storeConfig.oidcCallbackUrl
|
||||||
delete storeConfig.googleCallbackUrl
|
delete storeConfig.googleCallbackUrl
|
||||||
await API.saveConfig({
|
await API.saveConfig({
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "2.4.27-alpha.8",
|
"version": "2.4.43",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -29,9 +29,9 @@
|
||||||
"outputPath": "build"
|
"outputPath": "build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/backend-core": "2.4.27-alpha.8",
|
"@budibase/backend-core": "^2.4.43",
|
||||||
"@budibase/string-templates": "2.4.27-alpha.8",
|
"@budibase/string-templates": "^2.4.43",
|
||||||
"@budibase/types": "2.4.27-alpha.8",
|
"@budibase/types": "^2.4.43",
|
||||||
"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",
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "2.4.27-alpha.8",
|
"version": "2.4.43",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
|
@ -19,11 +19,11 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.4.27-alpha.8",
|
"@budibase/bbui": "^2.4.43",
|
||||||
"@budibase/frontend-core": "2.4.27-alpha.8",
|
"@budibase/frontend-core": "^2.4.43",
|
||||||
"@budibase/shared-core": "2.4.27-alpha.8",
|
"@budibase/shared-core": "^2.4.43",
|
||||||
"@budibase/string-templates": "2.4.27-alpha.8",
|
"@budibase/string-templates": "^2.4.43",
|
||||||
"@budibase/types": "2.4.27-alpha.8",
|
"@budibase/types": "^2.4.43",
|
||||||
"@spectrum-css/button": "^3.0.3",
|
"@spectrum-css/button": "^3.0.3",
|
||||||
"@spectrum-css/card": "^3.0.3",
|
"@spectrum-css/card": "^3.0.3",
|
||||||
"@spectrum-css/divider": "^1.0.3",
|
"@spectrum-css/divider": "^1.0.3",
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
|
|
||||||
// Provide contexts
|
// Provide contexts
|
||||||
setContext("sdk", SDK)
|
setContext("sdk", SDK)
|
||||||
setContext("component", writable({}))
|
setContext("component", writable({ id: null, ancestors: [] }))
|
||||||
setContext("context", createContextStore())
|
setContext("context", createContextStore())
|
||||||
|
|
||||||
let dataLoaded = false
|
let dataLoaded = false
|
||||||
|
|
|
@ -26,19 +26,20 @@
|
||||||
} from "stores"
|
} from "stores"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { getActiveConditions, reduceConditionActions } from "utils/conditions"
|
import { getActiveConditions, reduceConditionActions } from "utils/conditions"
|
||||||
import Placeholder from "components/app/Placeholder.svelte"
|
import EmptyPlaceholder from "components/app/EmptyPlaceholder.svelte"
|
||||||
import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte"
|
import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte"
|
||||||
import ComponentPlaceholder from "components/app/ComponentPlaceholder.svelte"
|
import ComponentErrorState from "components/error-states/ComponentErrorState.svelte"
|
||||||
|
import { BudibasePrefix } from "../stores/components.js"
|
||||||
|
|
||||||
export let instance = {}
|
export let instance = {}
|
||||||
export let isLayout = false
|
export let isLayout = false
|
||||||
export let isScreen = false
|
export let isScreen = false
|
||||||
export let isBlock = false
|
export let isBlock = false
|
||||||
export let parent = null
|
|
||||||
|
|
||||||
// Get parent contexts
|
// Get parent contexts
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
const insideScreenslot = !!getContext("screenslot")
|
const insideScreenslot = !!getContext("screenslot")
|
||||||
|
const component = getContext("component")
|
||||||
|
|
||||||
// Create component context
|
// Create component context
|
||||||
const store = writable({})
|
const store = writable({})
|
||||||
|
@ -120,6 +121,12 @@
|
||||||
$: showEmptyState = definition?.showEmptyState !== false
|
$: showEmptyState = definition?.showEmptyState !== false
|
||||||
$: hasMissingRequiredSettings = missingRequiredSettings?.length > 0
|
$: hasMissingRequiredSettings = missingRequiredSettings?.length > 0
|
||||||
$: editable = !!definition?.editable && !hasMissingRequiredSettings
|
$: editable = !!definition?.editable && !hasMissingRequiredSettings
|
||||||
|
$: requiredAncestors = definition?.requiredAncestors || []
|
||||||
|
$: missingRequiredAncestors = requiredAncestors.filter(
|
||||||
|
ancestor => !$component.ancestors.includes(`${BudibasePrefix}${ancestor}`)
|
||||||
|
)
|
||||||
|
$: hasMissingRequiredAncestors = missingRequiredAncestors?.length > 0
|
||||||
|
$: errorState = hasMissingRequiredSettings || hasMissingRequiredAncestors
|
||||||
|
|
||||||
// Interactive components can be selected, dragged and highlighted inside
|
// Interactive components can be selected, dragged and highlighted inside
|
||||||
// the builder preview
|
// the builder preview
|
||||||
|
@ -183,6 +190,7 @@
|
||||||
custom: customCSS,
|
custom: customCSS,
|
||||||
id,
|
id,
|
||||||
empty: emptyState,
|
empty: emptyState,
|
||||||
|
selected,
|
||||||
interactive,
|
interactive,
|
||||||
draggable,
|
draggable,
|
||||||
editable,
|
editable,
|
||||||
|
@ -193,7 +201,9 @@
|
||||||
name,
|
name,
|
||||||
editing,
|
editing,
|
||||||
type: instance._component,
|
type: instance._component,
|
||||||
missingRequiredSettings,
|
errorState,
|
||||||
|
parent: id,
|
||||||
|
ancestors: [...$component?.ancestors, instance._component],
|
||||||
})
|
})
|
||||||
|
|
||||||
const initialise = (instance, force = false) => {
|
const initialise = (instance, force = false) => {
|
||||||
|
@ -482,6 +492,7 @@
|
||||||
getDataContext: () => get(context),
|
getDataContext: () => get(context),
|
||||||
reload: () => initialise(instance, true),
|
reload: () => initialise(instance, true),
|
||||||
setEphemeralStyles: styles => (ephemeralStyles = styles),
|
setEphemeralStyles: styles => (ephemeralStyles = styles),
|
||||||
|
state: store,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -509,24 +520,28 @@
|
||||||
class:pad
|
class:pad
|
||||||
class:parent={hasChildren}
|
class:parent={hasChildren}
|
||||||
class:block={isBlock}
|
class:block={isBlock}
|
||||||
|
class:error={errorState}
|
||||||
data-id={id}
|
data-id={id}
|
||||||
data-name={name}
|
data-name={name}
|
||||||
data-icon={icon}
|
data-icon={icon}
|
||||||
data-parent={parent}
|
data-parent={$component.id}
|
||||||
>
|
>
|
||||||
{#if hasMissingRequiredSettings}
|
{#if errorState}
|
||||||
<ComponentPlaceholder />
|
<ComponentErrorState
|
||||||
|
{missingRequiredSettings}
|
||||||
|
{missingRequiredAncestors}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<svelte:component this={constructor} bind:this={ref} {...initialSettings}>
|
<svelte:component this={constructor} bind:this={ref} {...initialSettings}>
|
||||||
{#if children.length}
|
{#if children.length}
|
||||||
{#each children as child (child._id)}
|
{#each children as child (child._id)}
|
||||||
<svelte:self instance={child} parent={id} />
|
<svelte:self instance={child} />
|
||||||
{/each}
|
{/each}
|
||||||
{:else if emptyState}
|
{:else if emptyState}
|
||||||
{#if isScreen}
|
{#if isScreen}
|
||||||
<ScreenPlaceholder />
|
<ScreenPlaceholder />
|
||||||
{:else}
|
{:else}
|
||||||
<Placeholder />
|
<EmptyPlaceholder />
|
||||||
{/if}
|
{/if}
|
||||||
{:else if isBlock}
|
{:else if isBlock}
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { builderStore } from "stores"
|
|
||||||
|
|
||||||
const component = getContext("component")
|
|
||||||
const { styleable } = getContext("sdk")
|
|
||||||
|
|
||||||
$: requiredSetting = $component.missingRequiredSettings?.[0]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if $builderStore.inBuilder && requiredSetting}
|
|
||||||
<div class="component-placeholder" use:styleable={$component.styles}>
|
|
||||||
<span>
|
|
||||||
Add the <mark>{requiredSetting.label}</mark> setting to start using your component
|
|
||||||
-
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="spectrum-Link"
|
|
||||||
on:click={() => {
|
|
||||||
builderStore.actions.highlightSetting(requiredSetting.key)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Show me
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.component-placeholder {
|
|
||||||
color: var(--spectrum-global-color-gray-600);
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
padding: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
.component-placeholder mark {
|
|
||||||
background-color: var(--spectrum-global-color-gray-400);
|
|
||||||
padding: 0 4px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
.component-placeholder .spectrum-Link {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import { Icon } from "@budibase/bbui"
|
||||||
|
|
||||||
|
const component = getContext("component")
|
||||||
|
const { builderStore, componentStore } = getContext("sdk")
|
||||||
|
|
||||||
|
$: definition = componentStore.actions.getComponentDefinition($component.type)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $builderStore.inBuilder}
|
||||||
|
<div class="component-placeholder">
|
||||||
|
<Icon name="Help" color="var(--spectrum-global-color-blue-600)" />
|
||||||
|
<span
|
||||||
|
class="spectrum-Link"
|
||||||
|
on:click={() => {
|
||||||
|
builderStore.actions.requestAddComponent()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add components inside your {definition?.name || $component.type}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.component-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Common styles for all error states to use */
|
||||||
|
.component-placeholder :global(mark) {
|
||||||
|
background-color: var(--spectrum-global-color-gray-400);
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.component-placeholder :global(.spectrum-Link) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
let width = window.innerWidth
|
let width = window.innerWidth
|
||||||
let height = window.innerHeight
|
let height = window.innerHeight
|
||||||
const tabletBreakpoint = 768
|
const tabletBreakpoint = 720
|
||||||
const desktopBreakpoint = 1280
|
const desktopBreakpoint = 1280
|
||||||
const resizeObserver = new ResizeObserver(entries => {
|
const resizeObserver = new ResizeObserver(entries => {
|
||||||
if (entries?.[0]) {
|
if (entries?.[0]) {
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import { Icon } from "@budibase/bbui"
|
||||||
|
import MissingRequiredSetting from "./MissingRequiredSetting.svelte"
|
||||||
|
import MissingRequiredAncestor from "./MissingRequiredAncestor.svelte"
|
||||||
|
|
||||||
|
export let missingRequiredSettings
|
||||||
|
export let missingRequiredAncestors
|
||||||
|
|
||||||
|
const component = getContext("component")
|
||||||
|
const { styleable, builderStore } = getContext("sdk")
|
||||||
|
|
||||||
|
$: styles = { ...$component.styles, normal: {}, custom: null, empty: true }
|
||||||
|
$: requiredSetting = missingRequiredSettings?.[0]
|
||||||
|
$: requiredAncestor = missingRequiredAncestors?.[0]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $builderStore.inBuilder}
|
||||||
|
{#if $component.errorState}
|
||||||
|
<div class="component-placeholder" use:styleable={styles}>
|
||||||
|
<Icon name="Alert" color="var(--spectrum-global-color-static-red-600)" />
|
||||||
|
{#if requiredAncestor}
|
||||||
|
<MissingRequiredAncestor {requiredAncestor} />
|
||||||
|
{:else if requiredSetting}
|
||||||
|
<MissingRequiredSetting {requiredSetting} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.component-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
padding: var(--spacing-xs);
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Common styles for all error states to use */
|
||||||
|
.component-placeholder :global(mark) {
|
||||||
|
background-color: var(--spectrum-global-color-gray-400);
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.component-placeholder :global(.spectrum-Link) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,41 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import { BudibasePrefix } from "stores/components"
|
||||||
|
|
||||||
|
export let requiredAncestor
|
||||||
|
|
||||||
|
const component = getContext("component")
|
||||||
|
const { builderStore, componentStore } = getContext("sdk")
|
||||||
|
|
||||||
|
$: definition = componentStore.actions.getComponentDefinition($component.type)
|
||||||
|
$: fullAncestorType = `${BudibasePrefix}${requiredAncestor}`
|
||||||
|
$: ancestorDefinition =
|
||||||
|
componentStore.actions.getComponentDefinition(fullAncestorType)
|
||||||
|
$: pluralName = getPluralName(definition?.name, $component.type)
|
||||||
|
$: ancestorName = getAncestorName(ancestorDefinition?.name, requiredAncestor)
|
||||||
|
|
||||||
|
const getPluralName = (name, type) => {
|
||||||
|
if (!name) {
|
||||||
|
name = type.replace(BudibasePrefix, "")
|
||||||
|
}
|
||||||
|
return name.endsWith("s") ? `${name}'` : `${name}s`
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAncestorName = name => {
|
||||||
|
return name || requiredAncestor
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{pluralName} need to be inside a
|
||||||
|
<mark>{ancestorName}</mark>
|
||||||
|
</span>
|
||||||
|
<span>-</span>
|
||||||
|
<span
|
||||||
|
class="spectrum-Link"
|
||||||
|
on:click={() => {
|
||||||
|
builderStore.actions.addParentComponent($component.id, fullAncestorType)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add {ancestorName}
|
||||||
|
</span>
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
export let requiredSetting
|
||||||
|
|
||||||
|
const { builderStore } = getContext("sdk")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
Add the <mark>{requiredSetting.label}</mark> setting to start using your component
|
||||||
|
</span>
|
||||||
|
<span>-</span>
|
||||||
|
<span
|
||||||
|
class="spectrum-Link"
|
||||||
|
on:click={() => {
|
||||||
|
builderStore.actions.highlightSetting(requiredSetting.key)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Show me
|
||||||
|
</span>
|
|
@ -12,7 +12,8 @@
|
||||||
$: id = dragInfo?.id || id
|
$: id = dragInfo?.id || id
|
||||||
|
|
||||||
// Set ephemeral grid styles on the dragged component
|
// Set ephemeral grid styles on the dragged component
|
||||||
$: componentStore.actions.getComponentInstance(id)?.setEphemeralStyles({
|
$: instance = componentStore.actions.getComponentInstance(id)
|
||||||
|
$: $instance?.setEphemeralStyles({
|
||||||
...gridStyles,
|
...gridStyles,
|
||||||
...(gridStyles ? { "z-index": 999 } : null),
|
...(gridStyles ? { "z-index": 999 } : null),
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
let text
|
let text
|
||||||
let icon
|
let icon
|
||||||
let insideGrid = false
|
let insideGrid = false
|
||||||
|
let errorState = false
|
||||||
|
|
||||||
$: visibleIndicators = indicators.filter(x => x.visible)
|
$: visibleIndicators = indicators.filter(x => x.visible)
|
||||||
$: offset = $builderStore.inBuilder ? 0 : 2
|
$: offset = $builderStore.inBuilder ? 0 : 2
|
||||||
|
@ -85,6 +86,7 @@
|
||||||
icon = parents[0].dataset.icon
|
icon = parents[0].dataset.icon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
errorState = parents?.[0]?.classList.contains("error")
|
||||||
|
|
||||||
// Batch reads to minimize reflow
|
// Batch reads to minimize reflow
|
||||||
const scrollX = window.scrollX
|
const scrollX = window.scrollX
|
||||||
|
@ -152,10 +154,10 @@
|
||||||
text={idx === 0 ? text : null}
|
text={idx === 0 ? text : null}
|
||||||
icon={idx === 0 ? icon : null}
|
icon={idx === 0 ? icon : null}
|
||||||
showResizeAnchors={allowResizeAnchors && insideGrid}
|
showResizeAnchors={allowResizeAnchors && insideGrid}
|
||||||
|
color={errorState ? "var(--spectrum-global-color-static-red-600)" : color}
|
||||||
{componentId}
|
{componentId}
|
||||||
{transition}
|
{transition}
|
||||||
{zIndex}
|
{zIndex}
|
||||||
{color}
|
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
{/key}
|
{/key}
|
||||||
|
|
|
@ -15,17 +15,22 @@
|
||||||
let self
|
let self
|
||||||
let measured = false
|
let measured = false
|
||||||
|
|
||||||
|
$: id = $builderStore.selectedComponentId
|
||||||
|
$: instance = componentStore.actions.getComponentInstance(id)
|
||||||
|
$: state = $instance?.state
|
||||||
$: definition = $componentStore.selectedComponentDefinition
|
$: definition = $componentStore.selectedComponentDefinition
|
||||||
$: showBar =
|
$: showBar =
|
||||||
definition?.showSettingsBar !== false && !$dndIsDragging && definition
|
definition?.showSettingsBar !== false &&
|
||||||
|
!$dndIsDragging &&
|
||||||
|
definition &&
|
||||||
|
!$state?.errorState
|
||||||
$: {
|
$: {
|
||||||
if (!showBar) {
|
if (!showBar) {
|
||||||
measured = false
|
measured = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$: settings = getBarSettings(definition)
|
$: settings = getBarSettings(definition)
|
||||||
$: isScreen =
|
$: isScreen = id === $builderStore.screen?.props?._id
|
||||||
$builderStore.selectedComponentId === $builderStore.screen?.props?._id
|
|
||||||
|
|
||||||
const getBarSettings = definition => {
|
const getBarSettings = definition => {
|
||||||
let allSettings = []
|
let allSettings = []
|
||||||
|
|
|
@ -109,6 +109,12 @@ const createBuilderStore = () => {
|
||||||
// Notify the builder so we can reload component definitions
|
// Notify the builder so we can reload component definitions
|
||||||
eventStore.actions.dispatchEvent("reload-plugin")
|
eventStore.actions.dispatchEvent("reload-plugin")
|
||||||
},
|
},
|
||||||
|
addParentComponent: (componentId, parentType) => {
|
||||||
|
eventStore.actions.dispatchEvent("add-parent-component", {
|
||||||
|
componentId,
|
||||||
|
parentType,
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...store,
|
...store,
|
||||||
|
|
|
@ -8,7 +8,7 @@ import Router from "../components/Router.svelte"
|
||||||
import * as AppComponents from "../components/app/index.js"
|
import * as AppComponents from "../components/app/index.js"
|
||||||
import { ScreenslotType } from "../constants.js"
|
import { ScreenslotType } from "../constants.js"
|
||||||
|
|
||||||
const budibasePrefix = "@budibase/standard-components/"
|
export const BudibasePrefix = "@budibase/standard-components/"
|
||||||
|
|
||||||
const createComponentStore = () => {
|
const createComponentStore = () => {
|
||||||
const store = writable({
|
const store = writable({
|
||||||
|
@ -107,12 +107,12 @@ const createComponentStore = () => {
|
||||||
|
|
||||||
// Screenslot is an edge case
|
// Screenslot is an edge case
|
||||||
if (type === ScreenslotType) {
|
if (type === ScreenslotType) {
|
||||||
type = `${budibasePrefix}${type}`
|
type = `${BudibasePrefix}${type}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle built-in components
|
// Handle built-in components
|
||||||
if (type.startsWith(budibasePrefix)) {
|
if (type.startsWith(BudibasePrefix)) {
|
||||||
type = type.replace(budibasePrefix, "")
|
type = type.replace(BudibasePrefix, "")
|
||||||
return type ? Manifest[type] : null
|
return type ? Manifest[type] : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,7 +130,7 @@ const createComponentStore = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle budibase components
|
// Handle budibase components
|
||||||
if (type.startsWith(budibasePrefix)) {
|
if (type.startsWith(BudibasePrefix)) {
|
||||||
const split = type.split("/")
|
const split = type.split("/")
|
||||||
const name = split[split.length - 1]
|
const name = split[split.length - 1]
|
||||||
return AppComponents[name]
|
return AppComponents[name]
|
||||||
|
@ -145,7 +145,7 @@ const createComponentStore = () => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return get(store).mountedComponents[id]
|
return derived(store, $store => $store.mountedComponents[id])
|
||||||
}
|
}
|
||||||
|
|
||||||
const registerCustomComponent = ({ Component, schema, version }) => {
|
const registerCustomComponent = ({ Component, schema, version }) => {
|
||||||
|
|
|
@ -16,6 +16,7 @@ export { rowSelectionStore } from "./rowSelection.js"
|
||||||
export { blockStore } from "./blocks.js"
|
export { blockStore } from "./blocks.js"
|
||||||
export { environmentStore } from "./environment"
|
export { environmentStore } from "./environment"
|
||||||
export { eventStore } from "./events.js"
|
export { eventStore } from "./events.js"
|
||||||
|
export { orgStore } from "./org.js"
|
||||||
export {
|
export {
|
||||||
dndStore,
|
dndStore,
|
||||||
dndIndex,
|
dndIndex,
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { routeStore } from "./routes"
|
import { routeStore } from "./routes"
|
||||||
import { appStore } from "./app"
|
import { appStore } from "./app"
|
||||||
|
import { orgStore } from "./org"
|
||||||
|
|
||||||
export async function initialise() {
|
export async function initialise() {
|
||||||
await routeStore.actions.fetchRoutes()
|
await routeStore.actions.fetchRoutes()
|
||||||
await appStore.actions.fetchAppDefinition()
|
await appStore.actions.fetchAppDefinition()
|
||||||
|
await orgStore.actions.init()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { API } from "api"
|
||||||
|
import { writable, get } from "svelte/store"
|
||||||
|
import { appStore } from "./app"
|
||||||
|
|
||||||
|
const createOrgStore = () => {
|
||||||
|
const store = writable(null)
|
||||||
|
|
||||||
|
const { subscribe, set } = store
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
const tenantId = get(appStore).application?.tenantId
|
||||||
|
if (!tenantId) return
|
||||||
|
try {
|
||||||
|
const settingsConfigDoc = await API.getTenantConfig(tenantId)
|
||||||
|
set({ logoUrl: settingsConfigDoc.config.logoUrl })
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Could not init org ", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
actions: {
|
||||||
|
init,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const orgStore = createOrgStore()
|
|
@ -2,6 +2,7 @@ import { derived } from "svelte/store"
|
||||||
import { routeStore } from "./routes"
|
import { routeStore } from "./routes"
|
||||||
import { builderStore } from "./builder"
|
import { builderStore } from "./builder"
|
||||||
import { appStore } from "./app"
|
import { appStore } from "./app"
|
||||||
|
import { orgStore } from "./org"
|
||||||
import { dndIndex, dndParent, dndIsNewComponent, dndBounds } from "./dnd.js"
|
import { dndIndex, dndParent, dndIsNewComponent, dndBounds } from "./dnd.js"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
import { findComponentById, findComponentParent } from "../utils/components.js"
|
import { findComponentById, findComponentParent } from "../utils/components.js"
|
||||||
|
@ -14,6 +15,7 @@ const createScreenStore = () => {
|
||||||
appStore,
|
appStore,
|
||||||
routeStore,
|
routeStore,
|
||||||
builderStore,
|
builderStore,
|
||||||
|
orgStore,
|
||||||
dndParent,
|
dndParent,
|
||||||
dndIndex,
|
dndIndex,
|
||||||
dndIsNewComponent,
|
dndIsNewComponent,
|
||||||
|
@ -23,6 +25,7 @@ const createScreenStore = () => {
|
||||||
$appStore,
|
$appStore,
|
||||||
$routeStore,
|
$routeStore,
|
||||||
$builderStore,
|
$builderStore,
|
||||||
|
$orgStore,
|
||||||
$dndParent,
|
$dndParent,
|
||||||
$dndIndex,
|
$dndIndex,
|
||||||
$dndIsNewComponent,
|
$dndIsNewComponent,
|
||||||
|
@ -146,6 +149,11 @@ const createScreenStore = () => {
|
||||||
if (!navigationSettings.title && !navigationSettings.hideTitle) {
|
if (!navigationSettings.title && !navigationSettings.hideTitle) {
|
||||||
navigationSettings.title = $appStore.application?.name
|
navigationSettings.title = $appStore.application?.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default to the org logo
|
||||||
|
if (!navigationSettings.logoUrl) {
|
||||||
|
navigationSettings.logoUrl = $orgStore?.logoUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
activeLayout = {
|
activeLayout = {
|
||||||
_id: "layout",
|
_id: "layout",
|
||||||
|
|
|
@ -29,9 +29,13 @@ export const styleable = (node, styles = {}) => {
|
||||||
|
|
||||||
let baseStyles = {}
|
let baseStyles = {}
|
||||||
if (newStyles.empty) {
|
if (newStyles.empty) {
|
||||||
baseStyles.border = "2px dashed var(--spectrum-global-color-gray-400)"
|
|
||||||
baseStyles.padding = "var(--spacing-l)"
|
baseStyles.padding = "var(--spacing-l)"
|
||||||
baseStyles.overflow = "hidden"
|
baseStyles.overflow = "hidden"
|
||||||
|
if (newStyles.selected) {
|
||||||
|
baseStyles.border = "2px solid transparent"
|
||||||
|
} else {
|
||||||
|
baseStyles.border = "2px dashed var(--spectrum-global-color-gray-400)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const componentId = newStyles.id
|
const componentId = newStyles.id
|
||||||
|
|
|
@ -23,11 +23,6 @@
|
||||||
chalk "^2.0.0"
|
chalk "^2.0.0"
|
||||||
js-tokens "^4.0.0"
|
js-tokens "^4.0.0"
|
||||||
|
|
||||||
"@budibase/types@2.4.8-alpha.4":
|
|
||||||
version "2.4.8-alpha.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.4.8-alpha.4.tgz#4e6dec50eef381994432ef4d08587a9a7156dd84"
|
|
||||||
integrity sha512-aiHHOvsDLHQ2OFmLgaSUttQwSuaPBqF1lbyyCkEJIbbl/qo9EPNZGl+AkB7wo12U5HdqWhr9OpFL12EqkcD4GA==
|
|
||||||
|
|
||||||
"@jridgewell/gen-mapping@^0.3.0":
|
"@jridgewell/gen-mapping@^0.3.0":
|
||||||
version "0.3.2"
|
version "0.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
|
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/frontend-core",
|
"name": "@budibase/frontend-core",
|
||||||
"version": "2.4.27-alpha.8",
|
"version": "2.4.43",
|
||||||
"description": "Budibase frontend core libraries used in builder and client",
|
"description": "Budibase frontend core libraries used in builder and client",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.4.27-alpha.8",
|
"@budibase/bbui": "^2.4.43",
|
||||||
"@budibase/shared-core": "2.4.27-alpha.8",
|
"@budibase/shared-core": "^2.4.43",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"svelte": "^3.46.2"
|
"svelte": "^3.46.2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,18 @@ export const buildConfigEndpoints = API => ({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the company favicon for the environment.
|
||||||
|
* @param data the favicon form data
|
||||||
|
*/
|
||||||
|
uploadFavicon: async data => {
|
||||||
|
return await API.post({
|
||||||
|
url: "/api/global/configs/upload/settings/faviconUrl",
|
||||||
|
body: data,
|
||||||
|
json: false,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads a logo for an OIDC provider.
|
* Uploads a logo for an OIDC provider.
|
||||||
* @param name the name of the OIDC provider
|
* @param name the name of the OIDC provider
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
import Covanta from "../../assets/covanta.png"
|
import Covanta from "../../assets/covanta.png"
|
||||||
import Schnellecke from "../../assets/schnellecke.png"
|
import Schnellecke from "../../assets/schnellecke.png"
|
||||||
|
|
||||||
|
export let enabled = true
|
||||||
|
|
||||||
const testimonials = [
|
const testimonials = [
|
||||||
{
|
{
|
||||||
text: "Budibase was the only solution that checked all the boxes for Covanta. Covanta expects to realize $3.2MM in savings due to the elimination of redundant data entry.",
|
text: "Budibase was the only solution that checked all the boxes for Covanta. Covanta expects to realize $3.2MM in savings due to the elimination of redundant data entry.",
|
||||||
|
@ -33,23 +35,25 @@
|
||||||
|
|
||||||
<SplitPage>
|
<SplitPage>
|
||||||
<slot />
|
<slot />
|
||||||
<div class="wrapper" slot="right">
|
<div class:wrapper={enabled} slot="right">
|
||||||
<div class="testimonial">
|
{#if enabled}
|
||||||
<Layout noPadding gap="S">
|
<div class="testimonial">
|
||||||
<img
|
<Layout noPadding gap="S">
|
||||||
width={testimonial.imageSize}
|
<img
|
||||||
alt="a-happy-budibase-user"
|
width={testimonial.imageSize}
|
||||||
src={testimonial.image}
|
alt="a-happy-budibase-user"
|
||||||
/>
|
src={testimonial.image}
|
||||||
<div class="text">
|
/>
|
||||||
"{testimonial.text}"
|
<div class="text">
|
||||||
</div>
|
"{testimonial.text}"
|
||||||
<div class="author">
|
</div>
|
||||||
<div class="name">{testimonial.name}</div>
|
<div class="author">
|
||||||
<div class="company">{testimonial.role}</div>
|
<div class="name">{testimonial.name}</div>
|
||||||
</div>
|
<div class="company">{testimonial.role}</div>
|
||||||
</Layout>
|
</div>
|
||||||
</div>
|
</Layout>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</SplitPage>
|
</SplitPage>
|
||||||
|
|
||||||
|
|
|
@ -68,6 +68,7 @@ export const Features = {
|
||||||
ENVIRONMENT_VARIABLES: "environmentVariables",
|
ENVIRONMENT_VARIABLES: "environmentVariables",
|
||||||
AUDIT_LOGS: "auditLogs",
|
AUDIT_LOGS: "auditLogs",
|
||||||
ENFORCEABLE_SSO: "enforceableSSO",
|
ENFORCEABLE_SSO: "enforceableSSO",
|
||||||
|
BRANDING: "branding",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Role IDs
|
// Role IDs
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/sdk",
|
"name": "@budibase/sdk",
|
||||||
"version": "2.4.27-alpha.8",
|
"version": "2.4.43",
|
||||||
"description": "Budibase Public API SDK",
|
"description": "Budibase Public API SDK",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
|
|
|
@ -44,6 +44,7 @@ const config: Config.InitialOptions = {
|
||||||
// The use of coverage with couchdb view functions breaks tests
|
// The use of coverage with couchdb view functions breaks tests
|
||||||
"!src/db/views/staticViews.*",
|
"!src/db/views/staticViews.*",
|
||||||
"!src/**/*.spec.{js,ts}",
|
"!src/**/*.spec.{js,ts}",
|
||||||
|
"!src/tests/**/*.{js,ts}",
|
||||||
],
|
],
|
||||||
coverageReporters: ["lcov", "json", "clover"],
|
coverageReporters: ["lcov", "json", "clover"],
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "2.4.27-alpha.8",
|
"version": "2.4.43",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
||||||
"debug": "yarn build && node --expose-gc --inspect=9222 dist/index.js",
|
"debug": "yarn build && node --expose-gc --inspect=9222 dist/index.js",
|
||||||
"postbuild": "copyfiles -u 1 src/**/*.svelte dist/ && copyfiles -u 1 src/**/*.hbs dist/ && copyfiles -u 1 src/**/*.json dist/",
|
"postbuild": "copyfiles -u 1 src/**/*.svelte dist/ && copyfiles -u 1 src/**/*.hbs dist/ && copyfiles -u 1 src/**/*.json dist/",
|
||||||
"test": "NODE_OPTIONS=\"--max-old-space-size=4096\" bash scripts/test.sh",
|
"test": "bash scripts/test.sh",
|
||||||
"test:memory": "jest --maxWorkers=2 --logHeapUsage --forceExit",
|
"test:memory": "jest --maxWorkers=2 --logHeapUsage --forceExit",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"predocker": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client",
|
"predocker": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client",
|
||||||
|
@ -44,12 +44,12 @@
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "10.0.3",
|
"@apidevtools/swagger-parser": "10.0.3",
|
||||||
"@budibase/backend-core": "2.4.27-alpha.8",
|
"@budibase/backend-core": "^2.4.43",
|
||||||
"@budibase/client": "2.4.27-alpha.8",
|
"@budibase/client": "^2.4.43",
|
||||||
"@budibase/pro": "2.4.27-alpha.8",
|
"@budibase/pro": "2.4.43",
|
||||||
"@budibase/shared-core": "2.4.27-alpha.8",
|
"@budibase/shared-core": "^2.4.43",
|
||||||
"@budibase/string-templates": "2.4.27-alpha.8",
|
"@budibase/string-templates": "^2.4.43",
|
||||||
"@budibase/types": "2.4.27-alpha.8",
|
"@budibase/types": "^2.4.43",
|
||||||
"@bull-board/api": "3.7.0",
|
"@bull-board/api": "3.7.0",
|
||||||
"@bull-board/koa": "3.9.4",
|
"@bull-board/koa": "3.9.4",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
if [[ -n $CI ]]
|
if [[ -n $CI ]]
|
||||||
then
|
then
|
||||||
# --runInBand performs better in ci where resources are limited
|
# --runInBand performs better in ci where resources are limited
|
||||||
|
export NODE_OPTIONS="--max-old-space-size=4096"
|
||||||
echo "jest --coverage --runInBand --forceExit"
|
echo "jest --coverage --runInBand --forceExit"
|
||||||
jest --coverage --runInBand --forceExit
|
jest --coverage --runInBand --forceExit
|
||||||
else
|
else
|
||||||
# --maxWorkers performs better in development
|
# --maxWorkers performs better in development
|
||||||
echo "jest --coverage --maxWorkers=2"
|
echo "jest --coverage --maxWorkers=2 --forceExit"
|
||||||
jest --coverage --maxWorkers=2
|
jest --coverage --maxWorkers=2 --forceExit
|
||||||
fi
|
fi
|
|
@ -2,9 +2,9 @@ import { DocumentType } from "../../db/utils"
|
||||||
import { Plugin } from "@budibase/types"
|
import { Plugin } from "@budibase/types"
|
||||||
import { db as dbCore, context, tenancy } from "@budibase/backend-core"
|
import { db as dbCore, context, tenancy } from "@budibase/backend-core"
|
||||||
import { getComponentLibraryManifest } from "../../utilities/fileSystem"
|
import { getComponentLibraryManifest } from "../../utilities/fileSystem"
|
||||||
import { BBContext } from "@budibase/types"
|
import { UserCtx } from "@budibase/types"
|
||||||
|
|
||||||
export async function fetchAppComponentDefinitions(ctx: BBContext) {
|
export async function fetchAppComponentDefinitions(ctx: UserCtx) {
|
||||||
try {
|
try {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const app = await db.get(DocumentType.APP_METADATA)
|
const app = await db.get(DocumentType.APP_METADATA)
|
||||||
|
|
|
@ -133,10 +133,15 @@ export async function search(ctx: any) {
|
||||||
|
|
||||||
export async function validate(ctx: Ctx) {
|
export async function validate(ctx: Ctx) {
|
||||||
const tableId = getTableId(ctx)
|
const tableId = getTableId(ctx)
|
||||||
ctx.body = await utils.validate({
|
// external tables are hard to validate currently
|
||||||
row: ctx.request.body,
|
if (isExternalTable(tableId)) {
|
||||||
tableId,
|
ctx.body = { valid: true }
|
||||||
})
|
} else {
|
||||||
|
ctx.body = await utils.validate({
|
||||||
|
row: ctx.request.body,
|
||||||
|
tableId,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchEnrichedRow(ctx: any) {
|
export async function fetchEnrichedRow(ctx: any) {
|
||||||
|
|
|
@ -11,10 +11,12 @@ import {
|
||||||
} from "../../../utilities/fileSystem"
|
} from "../../../utilities/fileSystem"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
import { DocumentType } from "../../../db/utils"
|
import { DocumentType } from "../../../db/utils"
|
||||||
import { context, objectStore, utils } from "@budibase/backend-core"
|
import { context, objectStore, utils, configs } from "@budibase/backend-core"
|
||||||
import AWS from "aws-sdk"
|
import AWS from "aws-sdk"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
|
import * as pro from "@budibase/pro"
|
||||||
|
|
||||||
const send = require("koa-send")
|
const send = require("koa-send")
|
||||||
|
|
||||||
async function prepareUpload({ s3Key, bucket, metadata, file }: any) {
|
async function prepareUpload({ s3Key, bucket, metadata, file }: any) {
|
||||||
|
@ -98,33 +100,74 @@ export const deleteObjects = async function (ctx: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const serveApp = async function (ctx: any) {
|
export const serveApp = async function (ctx: any) {
|
||||||
const db = context.getAppDB({ skip_setup: true })
|
//Public Settings
|
||||||
const appInfo = await db.get(DocumentType.APP_METADATA)
|
const { config } = await configs.getSettingsConfigDoc()
|
||||||
let appId = context.getAppId()
|
const branding = await pro.branding.getBrandingConfig(config)
|
||||||
|
|
||||||
if (!env.isJest()) {
|
let db
|
||||||
const App = require("./templates/BudibaseApp.svelte").default
|
try {
|
||||||
const plugins = objectStore.enrichPluginURLs(appInfo.usedPlugins)
|
db = context.getAppDB({ skip_setup: true })
|
||||||
const { head, html, css } = App.render({
|
const appInfo = await db.get(DocumentType.APP_METADATA)
|
||||||
metaImage:
|
let appId = context.getAppId()
|
||||||
"https://res.cloudinary.com/daog6scxm/image/upload/v1666109324/meta-images/budibase-meta-image_uukc1m.png",
|
|
||||||
title: appInfo.name,
|
|
||||||
production: env.isProd(),
|
|
||||||
appId,
|
|
||||||
clientLibPath: objectStore.clientLibraryUrl(appId!, appInfo.version),
|
|
||||||
usedPlugins: plugins,
|
|
||||||
})
|
|
||||||
|
|
||||||
const appHbs = loadHandlebarsFile(`${__dirname}/templates/app.hbs`)
|
if (!env.isJest()) {
|
||||||
ctx.body = await processString(appHbs, {
|
const App = require("./templates/BudibaseApp.svelte").default
|
||||||
head,
|
const plugins = objectStore.enrichPluginURLs(appInfo.usedPlugins)
|
||||||
body: html,
|
const { head, html, css } = App.render({
|
||||||
style: css.code,
|
metaImage:
|
||||||
appId,
|
branding?.metaImageUrl ||
|
||||||
})
|
"https://res.cloudinary.com/daog6scxm/image/upload/v1666109324/meta-images/budibase-meta-image_uukc1m.png",
|
||||||
} else {
|
metaDescription: branding?.metaDescription || "",
|
||||||
// just return the app info for jest to assert on
|
metaTitle:
|
||||||
ctx.body = appInfo
|
branding?.metaTitle || `${appInfo.name} - built with Budibase`,
|
||||||
|
title: appInfo.name,
|
||||||
|
production: env.isProd(),
|
||||||
|
appId,
|
||||||
|
clientLibPath: objectStore.clientLibraryUrl(appId!, appInfo.version),
|
||||||
|
usedPlugins: plugins,
|
||||||
|
favicon:
|
||||||
|
branding.faviconUrl !== ""
|
||||||
|
? objectStore.getGlobalFileUrl("settings", "faviconUrl")
|
||||||
|
: "",
|
||||||
|
logo:
|
||||||
|
config?.logoUrl !== ""
|
||||||
|
? objectStore.getGlobalFileUrl("settings", "logoUrl")
|
||||||
|
: "",
|
||||||
|
})
|
||||||
|
const appHbs = loadHandlebarsFile(`${__dirname}/templates/app.hbs`)
|
||||||
|
ctx.body = await processString(appHbs, {
|
||||||
|
head,
|
||||||
|
body: html,
|
||||||
|
style: css.code,
|
||||||
|
appId,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// just return the app info for jest to assert on
|
||||||
|
ctx.body = appInfo
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!env.isJest()) {
|
||||||
|
const App = require("./templates/BudibaseApp.svelte").default
|
||||||
|
const { head, html, css } = App.render({
|
||||||
|
title: branding?.metaTitle,
|
||||||
|
metaTitle: branding?.metaTitle,
|
||||||
|
metaImage:
|
||||||
|
branding?.metaImageUrl ||
|
||||||
|
"https://res.cloudinary.com/daog6scxm/image/upload/v1666109324/meta-images/budibase-meta-image_uukc1m.png",
|
||||||
|
metaDescription: branding?.metaDescription || "",
|
||||||
|
favicon:
|
||||||
|
branding.faviconUrl !== ""
|
||||||
|
? objectStore.getGlobalFileUrl("settings", "faviconUrl")
|
||||||
|
: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
const appHbs = loadHandlebarsFile(`${__dirname}/templates/app.hbs`)
|
||||||
|
ctx.body = await processString(appHbs, {
|
||||||
|
head,
|
||||||
|
body: html,
|
||||||
|
style: css.code,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
export let title = ""
|
export let title = ""
|
||||||
export let favicon = ""
|
export let favicon = ""
|
||||||
|
|
||||||
export let metaImage = ""
|
export let metaImage = ""
|
||||||
|
export let metaTitle = ""
|
||||||
|
export let metaDescription = ""
|
||||||
|
|
||||||
export let clientLibPath
|
export let clientLibPath
|
||||||
export let usedPlugins
|
export let usedPlugins
|
||||||
|
@ -13,18 +16,33 @@
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Primary Meta Tags -->
|
||||||
|
<meta name="title" content={metaTitle} />
|
||||||
|
<meta name="description" content={metaDescription} />
|
||||||
|
|
||||||
<!-- Opengraph Meta Tags -->
|
<!-- Opengraph Meta Tags -->
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
|
||||||
<meta name="twitter:site" content="@budibase" />
|
|
||||||
<meta name="twitter:image" content={metaImage} />
|
|
||||||
<meta name="twitter:title" content="{title} - built with Budibase" />
|
|
||||||
<meta property="og:site_name" content="Budibase" />
|
<meta property="og:site_name" content="Budibase" />
|
||||||
<meta property="og:title" content="{title} - built with Budibase" />
|
<meta property="og:title" content={metaTitle} />
|
||||||
|
<meta property="og:description" content={metaDescription} />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:image" content={metaImage} />
|
<meta property="og:image" content={metaImage} />
|
||||||
|
|
||||||
|
<!-- Twitter -->
|
||||||
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
|
<meta property="twitter:site" content="@budibase" />
|
||||||
|
<meta property="twitter:image" content={metaImage} />
|
||||||
|
<meta property="twitter:image:alt" content={metaTitle} />
|
||||||
|
<meta property="twitter:title" content={metaTitle} />
|
||||||
|
<meta property="twitter:description" content={metaDescription} />
|
||||||
|
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<link rel="icon" type="image/png" href={favicon} />
|
{#if favicon !== ""}
|
||||||
|
<link rel="icon" type="image/png" href={favicon} />
|
||||||
|
{:else}
|
||||||
|
<link rel="icon" type="image/png" href="https://i.imgur.com/Xhdt1YP.png" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||||
<link
|
<link
|
||||||
|
@ -83,11 +101,16 @@
|
||||||
|
|
||||||
<body id="app">
|
<body id="app">
|
||||||
<div id="error">
|
<div id="error">
|
||||||
<h1>There was an error loading your app</h1>
|
{#if clientLibPath}
|
||||||
<h2>
|
<h1>There was an error loading your app</h1>
|
||||||
The Budibase client library could not be loaded. Try republishing your
|
<h2>
|
||||||
app.
|
The Budibase client library could not be loaded. Try republishing your
|
||||||
</h2>
|
app.
|
||||||
|
</h2>
|
||||||
|
{:else}
|
||||||
|
<h2>We couldn't find that application</h2>
|
||||||
|
<p />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
window.INIT_TIME = Date.now()
|
window.INIT_TIME = Date.now()
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
import { FieldType } from "@budibase/types"
|
||||||
|
import { AutoFieldSubTypes } from "../../../../constants"
|
||||||
|
import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
|
||||||
|
import { importToRows } from "../utils"
|
||||||
|
|
||||||
|
describe("utils", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await config.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(config.end)
|
||||||
|
|
||||||
|
describe("importToRows", () => {
|
||||||
|
it("consecutive row have consecutive auto ids", async () => {
|
||||||
|
await config.doInContext(config.appId, async () => {
|
||||||
|
const table = await config.createTable({
|
||||||
|
name: "table",
|
||||||
|
type: "table",
|
||||||
|
schema: {
|
||||||
|
autoId: {
|
||||||
|
name: "autoId",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
subtype: AutoFieldSubTypes.AUTO_ID,
|
||||||
|
autocolumn: true,
|
||||||
|
constraints: {
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
presence: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
name: "name",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
constraints: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
presence: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = [{ name: "Alice" }, { name: "Bob" }, { name: "Claire" }]
|
||||||
|
|
||||||
|
const result = importToRows(data, table, config.user)
|
||||||
|
expect(result).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
autoId: 1,
|
||||||
|
name: "Alice",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
autoId: 2,
|
||||||
|
name: "Bob",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
autoId: 3,
|
||||||
|
name: "Claire",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("can import data without a specific user performing the action", async () => {
|
||||||
|
await config.doInContext(config.appId, async () => {
|
||||||
|
const table = await config.createTable({
|
||||||
|
name: "table",
|
||||||
|
type: "table",
|
||||||
|
schema: {
|
||||||
|
autoId: {
|
||||||
|
name: "autoId",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
subtype: AutoFieldSubTypes.AUTO_ID,
|
||||||
|
autocolumn: true,
|
||||||
|
constraints: {
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
presence: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
name: "name",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
constraints: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
presence: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = [{ name: "Alice" }, { name: "Bob" }, { name: "Claire" }]
|
||||||
|
|
||||||
|
const result = importToRows(data, table)
|
||||||
|
expect(result).toHaveLength(3)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue